/ proof-of-concept / cymbalsim
Sign in
proof-of-concept / cymbalsim
cymbalsim / src / ui / modes.ts
115 lines · 4.8 KBCodeBlameHistory
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.
2a1a6daDerive the losses from physical constants instead of a prescribed decay curveJeremy Magland 5import { t60Text } from "../loss.ts";
8 freq: number; // [Hz]
2a1a6daDerive the losses from physical constants instead of a prescribed decay curveJeremy Magland 9 /** Decay time of the mode [s], and the decay time each mechanism alone would give (Infinity when it contributes nothing). */
10 t60: number;
11 radiation: number;
12 material: number;
13 thermoelastic: number;
19 private info: HTMLDivElement;
20 private tableWrap: HTMLDivElement;
21 private tbody: HTMLTableSectionElement;
22 private minInput: HTMLInputElement;
23 private maxInput: HTMLInputElement;
24 private clearBtn: HTMLButtonElement;
25 private progress: HTMLDivElement;
26 private progressBar: HTMLDivElement;
27 private rows: ModeRow[] = [];
28 private sel: number | null = null;
30 constructor(container: HTMLElement, private onSelect: (m: number | null) => void) {
31 container.classList.add("modes");
32 const head = document.createElement("div");
33 head.className = "modes-head";
34 const lab = (t: string) => { const l = document.createElement("label"); l.textContent = t; return l; };
35 this.minInput = document.createElement("input");
36 this.minInput.type = "number"; this.minInput.placeholder = "min"; this.minInput.min = "0"; this.minInput.step = "10";
37 this.maxInput = document.createElement("input");
38 this.maxInput.type = "number"; this.maxInput.placeholder = "max"; this.maxInput.min = "0"; this.maxInput.step = "10";
39 this.clearBtn = document.createElement("button");
40 this.clearBtn.textContent = "Deselect";
41 this.clearBtn.disabled = true;
42 head.append(lab("Show"), this.minInput, lab("to"), this.maxInput, lab("Hz"), this.clearBtn);
43 this.info = document.createElement("div");
44 this.info.className = "modes-info";
45 this.progress = document.createElement("div");
46 this.progress.className = "progress";
47 this.progress.hidden = true;
48 this.progressBar = document.createElement("div");
49 this.progress.append(this.progressBar);
50 this.tableWrap = document.createElement("div");
51 this.tableWrap.className = "modes-table";
52 const table = document.createElement("table");
53 const thead = document.createElement("thead");
2481a63Remove the standJeremy Magland 54 thead.innerHTML = "<tr><th>#</th><th>f (Hz)</th><th title=\"Decay time of the mode\">T60 (s)</th><th title=\"Decay time from radiation into the air alone\">air</th><th title=\"Decay time from the metal's internal friction alone\">metal</th><th title=\"Decay time from thermoelastic damping alone\">thermal</th></tr>";
5778036cymbalsim: scripted plate and cymbal designer with a modal linear model and a von Karman GPU modelJeremy Magland 55 this.tbody = document.createElement("tbody");
56 table.append(thead, this.tbody);
57 this.tableWrap.append(table);
58 container.append(head, this.info, this.progress, this.tableWrap);
59 this.minInput.addEventListener("change", () => this.renderRows());
60 this.maxInput.addEventListener("change", () => this.renderRows());
61 this.clearBtn.addEventListener("click", () => this.select(null));
62 }
64 /** Shows a progress bar (fraction 0..1) with a message; null hides it. */
65 setProgress(fraction: number | null, message?: string): void {
66 this.progress.hidden = fraction === null;
67 if (fraction !== null) this.progressBar.style.width = `${Math.round(100 * fraction)}%`;
68 if (message !== undefined) this.info.textContent = message;
69 }
71 setModes(rows: ModeRow[], info: string): void {
72 this.rows = rows;
73 this.info.textContent = info;
74 this.sel = null;
75 this.clearBtn.disabled = true;
76 this.renderRows();
77 }
79 clear(message: string): void {
80 this.rows = [];
81 this.sel = null;
82 this.clearBtn.disabled = true;
83 this.info.textContent = message;
84 this.tbody.innerHTML = "";
85 }
87 selected(): number | null {
88 return this.sel;
89 }
91 select(m: number | null): void {
92 this.sel = m;
93 this.clearBtn.disabled = m === null;
94 for (const tr of Array.from(this.tbody.rows)) tr.classList.toggle("selected", Number(tr.dataset.m) === m);
95 this.onSelect(m);
96 }
98 private renderRows(): void {
99 const lo = Number(this.minInput.value) || 0;
100 const hi = Number(this.maxInput.value) || Infinity;
101 this.tbody.innerHTML = "";
102 const frag = document.createDocumentFragment();
103 this.rows.forEach((r, m) => {
104 if (r.freq < lo || r.freq > hi) return;
105 const tr = document.createElement("tr");
106 tr.dataset.m = String(m);
107 if (m === this.sel) tr.classList.add("selected");
108 const f = r.freq < 1000 ? r.freq.toFixed(1) : r.freq.toFixed(0);
2481a63Remove the standJeremy Magland 109 tr.innerHTML = `<td>${m + 1}</td><td>${f}</td><td>${fmtT60(r.t60)}</td><td>${fmtT60(r.radiation)}</td><td>${fmtT60(r.material)}</td><td>${fmtT60(r.thermoelastic)}</td>`;
5778036cymbalsim: scripted plate and cymbal designer with a modal linear model and a von Karman GPU modelJeremy Magland 110 tr.addEventListener("click", () => this.select(this.sel === m ? null : m));
111 frag.append(tr);
112 });
113 this.tbody.append(frag);
114 }
↑↓ move↵openescclose