Мастерская переехала на eureka2: страница /workshop — часть сайта (lib/workshop.js, общая шапка/навигация/подвал), оформление в tokens сайта (assets/workshop/ workshop.css), компоненты читают данные пода по SSE /workshop/sse. Компоненты перенесены на сторону сайта, геометрия мозга — assets/workshop/. build.js собирает страницу (active: workshop).
109 lines
3.8 KiB
JavaScript
109 lines
3.8 KiB
JavaScript
/* workshop-seismogram.js — сырое движение земли бегущей линией.
|
||
*
|
||
* Где фингерпринт отвечает «из чего дрожь» (частоты), эта линия — «как двигалась
|
||
* земля» (время). То же окно Земли, две системы координат: фингерпринт считается
|
||
* *из* этого сигнала, так что волна первична, а сетка — её разложение.
|
||
*
|
||
* Линия — окно с вычтенным средним (датчик несёт DC ~1000 отсчётов, это его
|
||
* положение, не движение) и авто-масштабом по пику окна. Высота не абсолютна:
|
||
* важно, как линия меняется, а не насколько она высока.
|
||
*/
|
||
class WorkshopSeismogram extends HTMLElement {
|
||
static get observedAttributes() { return ["height", "line-color"]; }
|
||
|
||
constructor() {
|
||
super();
|
||
this.attachShadow({ mode: "open" });
|
||
this._wave = new Array(256).fill(0);
|
||
this._unsub = null;
|
||
}
|
||
|
||
connectedCallback() { this._render(); this._subscribe(); }
|
||
|
||
disconnectedCallback() {
|
||
if (this._unsub) this._unsub();
|
||
this._unsub = null;
|
||
if (this._ro) this._ro.disconnect();
|
||
this._ro = null;
|
||
}
|
||
|
||
attributeChangedCallback() { if (this.isConnected) this._render(); }
|
||
|
||
get height() {
|
||
const v = parseInt(this.getAttribute("height") || "130", 10);
|
||
return Number.isFinite(v) && v > 0 ? v : 130;
|
||
}
|
||
|
||
_subscribe() {
|
||
if (!this.isConnected || this._unsub) return;
|
||
this._unsub = window.WorkshopStream.on("noise", (ev) => this._update(ev));
|
||
}
|
||
|
||
_render() {
|
||
const h = this.height;
|
||
this.shadowRoot.innerHTML = `
|
||
<style>
|
||
:host { display: block; }
|
||
canvas {
|
||
width: 100%; height: ${h}px; display: block;
|
||
background: var(--workshop-high, #10151a);
|
||
border: 1px solid var(--workshop-line, #232a33);
|
||
border-radius: 6px;
|
||
}
|
||
</style>
|
||
<canvas></canvas>`;
|
||
this._canvas = this.shadowRoot.querySelector("canvas");
|
||
this._ctx = this._canvas.getContext("2d");
|
||
// При connectedCallback элемент ещё не разложен, rect.width = 0 и canvas был
|
||
// бы 1px. Размер — на следующем кадре, синхронизация через ResizeObserver.
|
||
requestAnimationFrame(() => this._resize());
|
||
if (!this._ro && typeof ResizeObserver !== "undefined") {
|
||
this._ro = new ResizeObserver(() => this._resize());
|
||
this._ro.observe(this);
|
||
}
|
||
}
|
||
|
||
_resize() {
|
||
if (!this._canvas) return;
|
||
const w = Math.max(1, Math.round(this.clientWidth || 640));
|
||
const h = this.height;
|
||
if (this._canvas.width !== w || this._canvas.height !== h) {
|
||
this._canvas.width = w;
|
||
this._canvas.height = h;
|
||
}
|
||
this._draw();
|
||
}
|
||
|
||
_update(ev) {
|
||
if (!ev.wave || !ev.wave.length) return;
|
||
this._wave = ev.wave;
|
||
this._draw();
|
||
}
|
||
|
||
_draw() {
|
||
if (!this._ctx) return;
|
||
const ctx = this._ctx;
|
||
const W = this._canvas.width, H = this._canvas.height;
|
||
ctx.clearRect(0, 0, W, H);
|
||
let peak = 0;
|
||
for (const v of this._wave) peak = Math.max(peak, Math.abs(v));
|
||
const scale = peak > 0 ? (H / 2 - 4) / peak : 1;
|
||
ctx.strokeStyle = this.getAttribute("line-color") || "#4a9de0";
|
||
ctx.lineWidth = 1;
|
||
ctx.beginPath();
|
||
const stepX = W / (this._wave.length - 1);
|
||
for (let i = 0; i < this._wave.length; i++) {
|
||
const x = i * stepX;
|
||
const y = H / 2 - this._wave[i] * scale;
|
||
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
||
}
|
||
ctx.stroke();
|
||
ctx.strokeStyle = "#232a33";
|
||
ctx.beginPath();
|
||
ctx.moveTo(0, H / 2);
|
||
ctx.lineTo(W, H / 2);
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
customElements.define("workshop-seismogram", WorkshopSeismogram);
|