// The retained rod modes: frequency against ring time T60 = 6.91 / (zeta om), // colored by family. In-plane and out-of-plane modes interleave across the // whole band; the marker area shows how much of each mode's damping is // radiation (a large marker is a mode the air takes energy from, a small one // rings on and is heard mostly by its persistence). import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils'; interface Props { modeF: Float32Array; modeZeta: Float32Array; modeZetaRad: Float32Array; modeFamily: Float32Array; } const F_LO = 100; const F_HI = 24000; export default function ModeMap({ modeF, modeZeta, modeZetaRad, modeFamily }: 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 [100, 200, 500, 1000, 2000, 5000, 10000, 20000]) { 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 radShare = Math.min(1, modeZetaRad[i] / Math.max(1e-12, modeZeta[i])); const r = 2 + 5 * Math.sqrt(radShare); ctx.fillStyle = modeFamily[i] === 0 ? PLOT_COLORS.trace : PLOT_COLORS.accent; ctx.globalAlpha = 0.85; ctx.beginPath(); ctx.arc(x, y, r, 0, 2 * Math.PI); ctx.fill(); ctx.globalAlpha = 1; } ctx.fillStyle = PLOT_COLORS.trace; ctx.textAlign = 'left'; ctx.fillText('in-plane', padL + 6, top + 2); ctx.fillStyle = PLOT_COLORS.accent; ctx.fillText('out-of-plane', padL + 56, top + 2); ctx.fillStyle = PLOT_COLORS.axis; ctx.fillText(`· ${n} modes · marker = radiation share of damping`, padL + 128, top + 2); }, [modeF, modeZeta, modeZetaRad, modeFamily], 210, ); return ; }