// The passive resonator of the first note's fingering: |Y(f)| of the series // loop (window mass + bore input impedance) on a log-log axis, with the // fitted modal centre frequencies overlaid and the frequency the jet // actually locked to marked. The playable notes are the peaks; where the // played line sits relative to the first peak is the intonation. import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils'; interface Props { gridF: Float32Array; gridY: Float32Array; modeF: Float32Array; fPlay: number; } const DB_RANGE = 60; export default function AdmittancePlot({ gridF, gridY, modeF, fPlay }: Props) { const ref = useCanvas( (ctx, w, h) => { drawFrame(ctx, w, h); if (gridF.length < 2) return; const padL = 34; const padB = 18; const pw = w - padL - 6; const ph = h - padB - 8; const fLo = Math.max(80, gridF[0]); const fHi = gridF[gridF.length - 1]; const fx = (f: number) => padL + ((Math.log10(f) - Math.log10(fLo)) / (Math.log10(fHi) - Math.log10(fLo))) * pw; ctx.font = '10px system-ui'; ctx.textAlign = 'center'; for (const f of [100, 200, 500, 1000, 2000, 5000, 10000]) { if (f < fLo || f > fHi) continue; const x = fx(f); ctx.strokeStyle = PLOT_COLORS.grid; ctx.beginPath(); ctx.moveTo(x, 8); ctx.lineTo(x, 8 + ph); ctx.stroke(); ctx.fillStyle = PLOT_COLORS.axis; ctx.fillText(f >= 1000 ? `${f / 1000}k` : `${f}`, x, h - 5); } let yPk = 0; for (let i = 0; i < gridY.length; i++) yPk = Math.max(yPk, gridY[i]); if (yPk <= 0) return; ctx.textAlign = 'right'; for (let db = 0; db >= -DB_RANGE; db -= 20) { const yy = 8 + (-db / DB_RANGE) * ph; ctx.strokeStyle = PLOT_COLORS.grid; ctx.beginPath(); ctx.moveTo(padL, yy); ctx.lineTo(padL + pw, yy); ctx.stroke(); ctx.fillStyle = PLOT_COLORS.axis; ctx.fillText(`${db}`, padL - 4, yy + 3); } // fitted modes, then the played frequency, then the trace on top ctx.setLineDash([4, 3]); for (let i = 0; i < modeF.length; i++) { const f = modeF[i]; if (f < fLo || f > fHi) continue; ctx.strokeStyle = 'rgba(232, 163, 61, 0.45)'; ctx.beginPath(); ctx.moveTo(fx(f), 8); ctx.lineTo(fx(f), 8 + ph); ctx.stroke(); } ctx.setLineDash([]); if (fPlay > fLo && fPlay < fHi) { ctx.strokeStyle = 'rgba(111, 191, 115, 0.9)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(fx(fPlay), 8); ctx.lineTo(fx(fPlay), 8 + ph); ctx.stroke(); ctx.lineWidth = 1; } ctx.strokeStyle = PLOT_COLORS.trace; ctx.beginPath(); let started = false; for (let i = 0; i < gridF.length; i++) { if (gridF[i] < fLo) continue; const x = fx(gridF[i]); const db = 20 * Math.log10(gridY[i] / yPk + 1e-12); const yy = 8 + Math.min(ph, (-db / DB_RANGE) * ph); if (!started) { ctx.moveTo(x, yy); started = true; } else { ctx.lineTo(x, yy); } } ctx.stroke(); ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(232, 163, 61, 0.9)'; ctx.fillText('fitted modes', padL + 4, 18); ctx.fillStyle = 'rgba(111, 191, 115, 0.9)'; ctx.fillText('played', padL + 74, 18); }, [gridF, gridY, modeF, fPlay], 200, ); return ; }