// The coupled modes of the first note: frequency against ring time // T60 = 6.91 / (zeta om), colored by body-energy fraction. String-dominated // modes form the long-ringing partial ladder; body-dominated modes sit low // and damped; a mixed pair straddling one frequency is the wolf signature. import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils'; interface Props { modeF: Float32Array; modeZeta: Float32Array; modeBodyFrac: Float32Array; } const F_LO = 30; const F_HI = 8000; function mixColor(t: number): string { const r = Math.round(77 + t * (232 - 77)); const g = Math.round(208 + t * (163 - 208)); const b = Math.round(225 + t * (61 - 225)); return `rgb(${r}, ${g}, ${b})`; } export default function ModeMap({ modeF, modeZeta, modeBodyFrac }: Props) { const ref = useCanvas( (ctx, w, h) => { drawFrame(ctx, w, h); const padL = 40; const padB = 20; const pw = w - padL - 10; const ph = h - padB - 14; const top = 12; const n = modeF.length; if (n === 0) return; const t60 = new Float64Array(n); let tMin = Infinity; let tMax = 0; for (let i = 0; i < n; i++) { t60[i] = 6.91 / (Math.max(1e-9, modeZeta[i]) * 2 * Math.PI * modeF[i]); tMin = Math.min(tMin, t60[i]); tMax = Math.max(tMax, t60[i]); } tMin = Math.max(1e-3, tMin / 1.5); tMax = tMax * 1.5; const fx = (f: number) => padL + ((Math.log10(f) - Math.log10(F_LO)) / (Math.log10(F_HI) - Math.log10(F_LO))) * pw; const ty = (t: number) => top + ph - ((Math.log10(t) - Math.log10(tMin)) / (Math.log10(tMax) - Math.log10(tMin))) * ph; ctx.font = '10px system-ui'; ctx.textAlign = 'center'; for (const f of [30, 50, 100, 200, 500, 1000, 2000, 5000]) { const x = fx(f); ctx.strokeStyle = PLOT_COLORS.grid; ctx.beginPath(); ctx.moveTo(x, top); ctx.lineTo(x, top + ph); ctx.stroke(); ctx.fillStyle = PLOT_COLORS.axis; ctx.fillText(f >= 1000 ? `${f / 1000}k` : `${f}`, x, h - 7); } ctx.textAlign = 'right'; for (let e = Math.ceil(Math.log10(tMin)); e <= Math.floor(Math.log10(tMax)); e++) { const t = 10 ** e; const y = ty(t); ctx.strokeStyle = PLOT_COLORS.grid; ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(padL + pw, y); ctx.stroke(); ctx.fillStyle = PLOT_COLORS.axis; ctx.fillText(t >= 1 ? `${t}s` : `${t * 1000}ms`, padL - 4, y + 3); } for (let i = 0; i < n; i++) { const x = fx(Math.min(F_HI, Math.max(F_LO, modeF[i]))); const y = ty(t60[i]); const bf = Math.min(1, Math.max(0, modeBodyFrac[i])); ctx.fillStyle = mixColor(bf); ctx.globalAlpha = 0.85; ctx.beginPath(); ctx.arc(x, y, 2.5 + 2.5 * bf, 0, 2 * Math.PI); ctx.fill(); ctx.globalAlpha = 1; } ctx.textAlign = 'left'; ctx.fillStyle = mixColor(0); ctx.fillText('string-dominated', padL + 6, top + 2); ctx.fillStyle = mixColor(1); ctx.fillText('body-dominated', padL + 96, top + 2); ctx.fillStyle = PLOT_COLORS.axis; ctx.fillText(`· ${n} coupled modes of the first note · ring time T60`, padL + 182, top + 2); }, [modeF, modeZeta, modeBodyFrac], 210, ); return ; }