// The mode browser: a table of the modes of the linear model (frequency and // decay time), with a filter by frequency; clicking a row selects the mode, // whose shape the host then draws on the plate. export interface ModeRow { freq: number; // [Hz] t60: number; // [s] } export class ModeBrowser { private info: HTMLDivElement; private tableWrap: HTMLDivElement; private tbody: HTMLTableSectionElement; private minInput: HTMLInputElement; private maxInput: HTMLInputElement; private clearBtn: HTMLButtonElement; private progress: HTMLDivElement; private progressBar: HTMLDivElement; private rows: ModeRow[] = []; private sel: number | null = null; constructor(container: HTMLElement, private onSelect: (m: number | null) => void) { container.classList.add("modes"); const head = document.createElement("div"); head.className = "modes-head"; const lab = (t: string) => { const l = document.createElement("label"); l.textContent = t; return l; }; this.minInput = document.createElement("input"); this.minInput.type = "number"; this.minInput.placeholder = "min"; this.minInput.min = "0"; this.minInput.step = "10"; this.maxInput = document.createElement("input"); this.maxInput.type = "number"; this.maxInput.placeholder = "max"; this.maxInput.min = "0"; this.maxInput.step = "10"; this.clearBtn = document.createElement("button"); this.clearBtn.textContent = "Deselect"; this.clearBtn.disabled = true; head.append(lab("Show"), this.minInput, lab("to"), this.maxInput, lab("Hz"), this.clearBtn); this.info = document.createElement("div"); this.info.className = "modes-info"; this.progress = document.createElement("div"); this.progress.className = "progress"; this.progress.hidden = true; this.progressBar = document.createElement("div"); this.progress.append(this.progressBar); this.tableWrap = document.createElement("div"); this.tableWrap.className = "modes-table"; const table = document.createElement("table"); const thead = document.createElement("thead"); thead.innerHTML = "#f (Hz)T60 (s)"; this.tbody = document.createElement("tbody"); table.append(thead, this.tbody); this.tableWrap.append(table); container.append(head, this.info, this.progress, this.tableWrap); this.minInput.addEventListener("change", () => this.renderRows()); this.maxInput.addEventListener("change", () => this.renderRows()); this.clearBtn.addEventListener("click", () => this.select(null)); } /** Shows a progress bar (fraction 0..1) with a message; null hides it. */ setProgress(fraction: number | null, message?: string): void { this.progress.hidden = fraction === null; if (fraction !== null) this.progressBar.style.width = `${Math.round(100 * fraction)}%`; if (message !== undefined) this.info.textContent = message; } setModes(rows: ModeRow[], info: string): void { this.rows = rows; this.info.textContent = info; this.sel = null; this.clearBtn.disabled = true; this.renderRows(); } clear(message: string): void { this.rows = []; this.sel = null; this.clearBtn.disabled = true; this.info.textContent = message; this.tbody.innerHTML = ""; } selected(): number | null { return this.sel; } select(m: number | null): void { this.sel = m; this.clearBtn.disabled = m === null; for (const tr of Array.from(this.tbody.rows)) tr.classList.toggle("selected", Number(tr.dataset.m) === m); this.onSelect(m); } private renderRows(): void { const lo = Number(this.minInput.value) || 0; const hi = Number(this.maxInput.value) || Infinity; this.tbody.innerHTML = ""; const frag = document.createDocumentFragment(); this.rows.forEach((r, m) => { if (r.freq < lo || r.freq > hi) return; const tr = document.createElement("tr"); tr.dataset.m = String(m); if (m === this.sel) tr.classList.add("selected"); const f = r.freq < 1000 ? r.freq.toFixed(1) : r.freq.toFixed(0); tr.innerHTML = `${m + 1}${f}${r.t60.toFixed(2)}`; tr.addEventListener("click", () => this.select(this.sel === m ? null : m)); frag.append(tr); }); this.tbody.append(frag); } }