/ proof-of-concept / cymmodes
Sign in
proof-of-concept / cymmodes
cymmodes / src / ui / modeList.ts
95 lines · 3.7 KBBlameHistoryRaw
1// The mode table: one row per mode with its frequency, decay time, heuristic (m, n) label and,
2// during or after a run, its energy in dB below the loudest mode. Clicking a row selects the
3// mode. The labels are filled in lazily in idle chunks; the energy column is refreshed at a few
4// hertz, not per frame.
5import { classifyMode } from "../modal/classify.ts";
6import type { ModesData } from "../modes.ts";
8export class ModeList {
9 private readonly body: HTMLTableSectionElement;
10 private rows: HTMLTableRowElement[] = [];
11 private energyCells: HTMLTableCellElement[] = [];
12 private labelCells: HTMLTableCellElement[] = [];
13 private modes: ModesData | null = null;
14 private labelTimer = 0;
15 selected = -1;
16 driven = -1;
17 onSelect: ((mode: number) => void) | null = null;
19 constructor(container: HTMLElement) {
20 const table = document.createElement("table");
21 table.innerHTML = "<thead><tr><th>#</th><th>f [Hz]</th><th>T60 [s]</th><th>(m, n)</th><th>E [dB]</th></tr></thead>";
22 this.body = document.createElement("tbody");
23 table.appendChild(this.body);
24 container.appendChild(table);
25 }
27 setModes(modes: ModesData | null, freq: Float64Array | null, sigma: Float64Array | null) {
28 clearTimeout(this.labelTimer);
29 this.modes = modes;
30 this.body.replaceChildren();
31 this.rows = []; this.energyCells = []; this.labelCells = [];
32 if (!modes || !freq || !sigma) return;
33 const frag = document.createDocumentFragment();
34 for (let m = 0; m < modes.nModes; m++) {
35 const tr = document.createElement("tr");
36 const cells = [String(m), freq[m] < 1000 ? freq[m].toFixed(1) : freq[m].toFixed(0), (6.907755 / sigma[m]).toFixed(2), "", ""];
37 for (const c of cells) { const td = document.createElement("td"); td.textContent = c; tr.appendChild(td); }
38 tr.onclick = () => this.onSelect?.(m);
39 this.rows.push(tr);
40 this.labelCells.push(tr.children[3] as HTMLTableCellElement);
41 this.energyCells.push(tr.children[4] as HTMLTableCellElement);
42 frag.appendChild(tr);
43 }
44 this.body.appendChild(frag);
45 this.fillLabels(0);
46 this.applyMarks();
47 }
49 /** Refresh the frequency and T60 columns after a live material or damping change. */
50 setDerived(freq: Float64Array, sigma: Float64Array) {
51 for (let m = 0; m < this.rows.length; m++) {
52 const c = this.rows[m].children;
53 (c[1] as HTMLElement).textContent = freq[m] < 1000 ? freq[m].toFixed(1) : freq[m].toFixed(0);
54 (c[2] as HTMLElement).textContent = (6.907755 / sigma[m]).toFixed(2);
55 }
56 }
58 private fillLabels(from: number) {
59 const modes = this.modes;
60 if (!modes) return;
61 const t0 = performance.now();
62 let m = from;
63 while (m < modes.nModes && performance.now() - t0 < 12) {
64 const { diameters, circles } = classifyMode(modes, m);
65 this.labelCells[m].textContent = `(${diameters}, ${circles})`;
66 m++;
67 }
68 if (m < modes.nModes) this.labelTimer = window.setTimeout(() => this.fillLabels(m), 30);
69 }
71 setEnergies(E: Float32Array | null, maxE: number) {
72 for (let m = 0; m < this.energyCells.length; m++) {
73 const cell = this.energyCells[m];
74 if (!E || !(maxE > 0)) { if (cell.textContent) cell.textContent = ""; continue; }
75 const dB = 10 * Math.log10(Math.max(E[m], 1e-300) / maxE);
76 cell.textContent = dB > -120 ? dB.toFixed(0) : "";
77 }
78 }
80 select(mode: number) {
81 this.selected = mode;
82 this.applyMarks();
83 const row = this.rows[mode];
84 if (row) row.scrollIntoView({ block: "nearest" });
85 }
87 setDriven(mode: number) { this.driven = mode; this.applyMarks(); }
89 private applyMarks() {
90 for (let m = 0; m < this.rows.length; m++) {
91 this.rows[m].classList.toggle("selected", m === this.selected);
92 this.rows[m].classList.toggle("driven", m === this.driven);
93 }
94 }
↑↓ move↵openescclose