import { useCallback } from 'react' import { useCanvas } from '../hooks/useCanvas' import { renderPeriod } from '../synth' type Props = { amplitudes: number[] /** radians */ phases: number[] color: string /** vertical half-range; pass the same value for both tones so they share a scale */ yMax: number } const M = { l: 40, r: 10, t: 10, b: 22 } /** One period of the waveform x(t), unscaled. */ export function WaveformPlot({ amplitudes, phases, color, yMax }: Props) { const draw = useCallback( (ctx: CanvasRenderingContext2D, w: number, h: number) => { const pw = w - M.l - M.r const ph = h - M.t - M.b const y0 = M.t + ph / 2 const ymax = yMax > 0 ? yMax : 1 const toY = (v: number) => y0 - (v / ymax) * (ph / 2) ctx.font = '11px system-ui, sans-serif' ctx.fillStyle = '#666' ctx.strokeStyle = '#ddd' ctx.lineWidth = 1 // frame and zero line ctx.strokeRect(M.l + 0.5, M.t + 0.5, pw, ph) ctx.beginPath() ctx.moveTo(M.l, y0 + 0.5) ctx.lineTo(M.l + pw, y0 + 0.5) ctx.stroke() // quarter-period grid for (let q = 1; q < 4; q++) { const x = Math.round(M.l + (pw * q) / 4) + 0.5 ctx.beginPath() ctx.moveTo(x, M.t) ctx.lineTo(x, M.t + ph) ctx.stroke() } // ticks ctx.textAlign = 'right' ctx.textBaseline = 'middle' ctx.fillText(ymax.toFixed(1), M.l - 4, toY(ymax)) ctx.fillText('0', M.l - 4, y0) ctx.fillText((-ymax).toFixed(1), M.l - 4, toY(-ymax)) ctx.textAlign = 'center' ctx.textBaseline = 'top' ctx.fillText('0', M.l, M.t + ph + 4) ctx.fillText('T/2', M.l + pw / 2, M.t + ph + 4) ctx.fillText('T', M.l + pw, M.t + ph + 4) // curve const n = Math.max(64, Math.round(pw * 2)) const y = renderPeriod(amplitudes, phases, n) ctx.strokeStyle = color ctx.lineWidth = 1.8 ctx.lineJoin = 'round' ctx.beginPath() for (let i = 0; i <= n; i++) { const px = M.l + (pw * i) / n const py = toY(y[i % n]) if (i === 0) ctx.moveTo(px, py) else ctx.lineTo(px, py) } ctx.stroke() }, [amplitudes, phases, color, yMax], ) const ref = useCanvas(draw) return }