/* 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 = ` `; 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);