// Contact-force pulse f_c(t). Its width sets brightness: harder mallets give // a shorter pulse and therefore more high-frequency drive. import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils'; interface Props { force: Float32Array; forceFsHz: number; peakForceN: number; contactMs: number; } export default function ContactForcePlot({ force, forceFsHz, peakForceN, contactMs }: Props) { const ref = useCanvas( (ctx, w, h) => { drawFrame(ctx, w, h); const padL = 44; const padB = 16; const pw = w - padL - 8; const ph = h - padB - 10; // window: from 0 to a bit past the end of contact, at least 1 ms let lastIdx = 0; for (let i = 0; i < force.length; i++) if (force[i] > 0) lastIdx = i; const winMs = Math.max(1.0, ((lastIdx / forceFsHz) * 1e3) * 1.4); const nWin = Math.min(force.length, Math.round((winMs / 1e3) * forceFsHz)); let peak = 0; for (let i = 0; i < nWin; i++) peak = Math.max(peak, force[i]); if (peak <= 0) peak = 1; ctx.font = '10px system-ui'; ctx.fillStyle = PLOT_COLORS.axis; ctx.textAlign = 'center'; const step = winMs > 4 ? 1.0 : winMs > 2 ? 0.5 : 0.2; for (let t = 0; t <= winMs + 1e-9; t += step) { const x = padL + (t / winMs) * pw; ctx.strokeStyle = PLOT_COLORS.grid; ctx.beginPath(); ctx.moveTo(x, 10); ctx.lineTo(x, 10 + ph); ctx.stroke(); ctx.fillText(`${t.toFixed(1)}ms`, x, h - 4); } ctx.textAlign = 'right'; ctx.fillText(`${peak.toFixed(0)} N`, padL - 4, 16); ctx.fillText('0', padL - 4, 10 + ph); ctx.strokeStyle = PLOT_COLORS.accent; ctx.lineWidth = 1.5; ctx.beginPath(); for (let px = 0; px <= pw; px++) { const i = Math.min(nWin - 1, Math.round((px / pw) * (nWin - 1))); const yy = 10 + ph - (force[i] / peak) * ph; if (px === 0) ctx.moveTo(padL, yy); else ctx.lineTo(padL + px, yy); } ctx.stroke(); ctx.lineWidth = 1; ctx.fillStyle = PLOT_COLORS.axis; ctx.textAlign = 'left'; ctx.fillText( `contact ${contactMs.toFixed(3)} ms ยท peak ${peakForceN.toFixed(0)} N`, padL + 6, 16, ); }, [force, forceFsHz, peakForceN, contactMs], 170, ); return ; }