eureka-web/assets/workshop/workshop-seismogram.js
eureka cc3d6c5ca0 workshop: страница в общей оболочке сайта + компоненты
Мастерская переехала на eureka2: страница /workshop — часть сайта (lib/workshop.js,
общая шапка/навигация/подвал), оформление в tokens сайта (assets/workshop/
workshop.css), компоненты читают данные пода по SSE /workshop/sse.
Компоненты перенесены на сторону сайта, геометрия мозга — assets/workshop/.
build.js собирает страницу (active: workshop).
2026-10-04 01:08:06 +03:00

109 lines
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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