eureka-web/assets/workshop/workshop-seismogram.js

110 lines
3.8 KiB
JavaScript
Raw Normal View History

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