5778036cymbalsim: scripted plate and cymbal designer with a modal linear model and a von Karman GPU modelJeremy Magland 1// The mode browser: a table of the modes of the linear model (frequency and
2// decay time), with a filter by frequency; clicking a row selects the mode,
3// whose shape the host then draws on the plate.
5export interface ModeRow {
6 freq: number; // [Hz]
7 t60: number; // [s]
8}
10export class ModeBrowser {
11 private info: HTMLDivElement;
12 private tableWrap: HTMLDivElement;
13 private tbody: HTMLTableSectionElement;
14 private minInput: HTMLInputElement;
15 private maxInput: HTMLInputElement;
16 private clearBtn: HTMLButtonElement;
17 private progress: HTMLDivElement;
18 private progressBar: HTMLDivElement;
19 private rows: ModeRow[] = [];
20 private sel: number | null = null;
22 constructor(container: HTMLElement, private onSelect: (m: number | null) => void) {
23 container.classList.add("modes");
24 const head = document.createElement("div");
25 head.className = "modes-head";
26 const lab = (t: string) => { const l = document.createElement("label"); l.textContent = t; return l; };
27 this.minInput = document.createElement("input");
28 this.minInput.type = "number"; this.minInput.placeholder = "min"; this.minInput.min = "0"; this.minInput.step = "10";
29 this.maxInput = document.createElement("input");
30 this.maxInput.type = "number"; this.maxInput.placeholder = "max"; this.maxInput.min = "0"; this.maxInput.step = "10";
31 this.clearBtn = document.createElement("button");
32 this.clearBtn.textContent = "Deselect";
33 this.clearBtn.disabled = true;
34 head.append(lab("Show"), this.minInput, lab("to"), this.maxInput, lab("Hz"), this.clearBtn);
35 this.info = document.createElement("div");
36 this.info.className = "modes-info";
37 this.progress = document.createElement("div");
38 this.progress.className = "progress";
39 this.progress.hidden = true;
40 this.progressBar = document.createElement("div");
41 this.progress.append(this.progressBar);
42 this.tableWrap = document.createElement("div");
43 this.tableWrap.className = "modes-table";
44 const table = document.createElement("table");
45 const thead = document.createElement("thead");
46 thead.innerHTML = "<tr><th>#</th><th>f (Hz)</th><th>T60 (s)</th></tr>";
47 this.tbody = document.createElement("tbody");
48 table.append(thead, this.tbody);
49 this.tableWrap.append(table);
50 container.append(head, this.info, this.progress, this.tableWrap);
51 this.minInput.addEventListener("change", () => this.renderRows());
52 this.maxInput.addEventListener("change", () => this.renderRows());
53 this.clearBtn.addEventListener("click", () => this.select(null));
54 }
56 /** Shows a progress bar (fraction 0..1) with a message; null hides it. */
57 setProgress(fraction: number | null, message?: string): void {
58 this.progress.hidden = fraction === null;
59 if (fraction !== null) this.progressBar.style.width = `${Math.round(100 * fraction)}%`;
60 if (message !== undefined) this.info.textContent = message;
61 }
63 setModes(rows: ModeRow[], info: string): void {
64 this.rows = rows;
65 this.info.textContent = info;
66 this.sel = null;
67 this.clearBtn.disabled = true;
68 this.renderRows();
69 }
71 clear(message: string): void {
72 this.rows = [];
73 this.sel = null;
74 this.clearBtn.disabled = true;
75 this.info.textContent = message;
76 this.tbody.innerHTML = "";
77 }
79 selected(): number | null {
80 return this.sel;
81 }
83 select(m: number | null): void {
84 this.sel = m;
85 this.clearBtn.disabled = m === null;
86 for (const tr of Array.from(this.tbody.rows)) tr.classList.toggle("selected", Number(tr.dataset.m) === m);
87 this.onSelect(m);
88 }
90 private renderRows(): void {
91 const lo = Number(this.minInput.value) || 0;
92 const hi = Number(this.maxInput.value) || Infinity;
93 this.tbody.innerHTML = "";
94 const frag = document.createDocumentFragment();
95 this.rows.forEach((r, m) => {
96 if (r.freq < lo || r.freq > hi) return;
97 const tr = document.createElement("tr");
98 tr.dataset.m = String(m);
99 if (m === this.sel) tr.classList.add("selected");
100 const f = r.freq < 1000 ? r.freq.toFixed(1) : r.freq.toFixed(0);
101 tr.innerHTML = `<td>${m + 1}</td><td>${f}</td><td>${r.t60.toFixed(2)}</td>`;
102 tr.addEventListener("click", () => this.select(this.sel === m ? null : m));
103 frag.append(tr);
104 });
105 this.tbody.append(frag);
106 }
107}