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

108 lines
4.1 KiB
JavaScript
Raw Permalink Normal View History

/* workshop-fingerprint.js — 8x8 спектральный фингерпринт живой земли.
*
* 64 ячейки = 64 лог-полосы частот окна сейсмограммы, row-major: сверху-слева
* низшая частота, снизу-справа высшая. Яркость — энергия полосы по лог-шкале.
* Верхние строки светятся (микросейсмы, низкий гул Земли), нижние тёмные
* (тишина на высоких частотах): это красный шум планеты, не настройка экрана.
*
* Темы читаются из CSS-переменных сайта (наследуются в shadow DOM), с фолбэком
* на токены workshop.
*/
class WorkshopFingerprint extends HTMLElement {
// Частотный охват строки — из band_edges окна 1024 сэмпла при 40 Гц.
static ROWS = [
"0.04–0.35 Гц", "0.35–0.66 Гц", "0.66–0.98 Гц", "0.98–1.29 Гц",
"1.29–1.91 Гц", "1.91–4.22 Гц", "4.22–9.18 Гц", "9.18–20 Гц",
];
static get observedAttributes() { return ["cell-size", "caption", "labels"]; }
constructor() {
super();
this.attachShadow({ mode: "open" });
this._cells = [];
this._unsub = null;
}
connectedCallback() { this._render(); this._subscribe(); }
disconnectedCallback() {
if (this._unsub) this._unsub();
this._unsub = null;
}
attributeChangedCallback() { if (this.isConnected) this._render(); }
get cellSize() {
const v = parseInt(this.getAttribute("cell-size") || "26", 10);
return Number.isFinite(v) && v > 0 ? v : 26;
}
_subscribe() {
if (!this.isConnected || this._unsub) return;
this._unsub = window.WorkshopStream.on("noise", (ev) => this._update(ev));
}
_render() {
const px = this.cellSize;
const caption = this.getAttribute("caption") || "";
const labels = this.hasAttribute("labels");
const body = WorkshopFingerprint.ROWS.map((label, r) => `
<div class="row">
<div class="grid" data-row="${r}"></div>
${labels ? `<div class="rowlabel">${label}</div>` : ""}
</div>`).join("");
this.shadowRoot.innerHTML = `
<style>
:host { display: inline-block; font: inherit; }
.wrap { display: inline-flex; flex-direction: column; gap: 8px; }
.rows { display: flex; flex-direction: column; gap: 3px; }
.row { display: flex; align-items: center; gap: 8px; }
.grid { display: grid; grid-template-columns: repeat(8, ${px}px); gap: 3px; }
.cell {
width: ${px}px; height: ${px}px; border-radius: 3px;
background: var(--workshop-high, #10151a);
transition: background .12s linear;
}
.rowlabel {
color: var(--workshop-dim, #9aa3ad); font-size: 11px;
font-variant-numeric: tabular-nums; white-space: nowrap;
}
.caption {
color: var(--workshop-dim, #9aa3ad); font-size: 12px;
letter-spacing: .04em;
}
</style>
<div class="wrap">
<div class="rows">${body}</div>
${caption ? `<div class="caption">${caption}</div>` : ""}
</div>`;
this._cells = [];
for (let r = 0; r < 8; r++) {
const grid = this.shadowRoot.querySelector(`.grid[data-row="${r}"]`);
for (let i = 0; i < 8; i++) {
const c = document.createElement("div");
c.className = "cell";
grid.appendChild(c);
this._cells.push(c);
}
}
}
_update(ev) {
if (!ev.bands || ev.bands.length < 64) return;
const LO = Math.log10(0.001), HI = Math.log10(0.3);
for (let i = 0; i < 64; i++) {
const x = ev.bands[i];
let v = x > 0 ? (Math.log10(x) - LO) / (HI - LO) : 0;
v = Math.max(0, Math.min(1, v));
const r = Math.round(18 + 235 * v * v);
const g = Math.round(30 + 170 * v);
const b = Math.round(45 + 90 * (1 - v));
this._cells[i].style.background = `rgb(${r},${g},${b})`;
}
}
}
customElements.define("workshop-fingerprint", WorkshopFingerprint);