// Contact force against time, zoomed to the strokes: the Hertz contact // between the mallet head and the shell, max-pooled by the worker. The // double peaks of a single stroke are the beam's reflection returning to // the head (the paper's Fig. 12); later bumps are the stick bouncing on the // wobbling shell before the wrist clears it. import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils'; interface Props { forceT: Float32Array; forceN: Float32Array; } export default function ContactPlot({ forceT, forceN }: Props) { const ref = useCanvas( (ctx, w, h) => { drawFrame(ctx, w, h); const padL = 40; const padB = 18; const pw = w - padL - 8; const ph = h - padB - 10; let peak = 0; let tFirst = Infinity; let tLast = 0; for (let i = 0; i < forceN.length; i++) { if (forceN[i] > peak) peak = forceN[i]; if (forceN[i] > 0) { tFirst = Math.min(tFirst, forceT[i]); tLast = Math.max(tLast, forceT[i]); } } if (!(peak > 0)) { ctx.fillStyle = PLOT_COLORS.axis; ctx.font = '11px system-ui'; ctx.fillText('no contact', padL + 8, 30); return; } const t0 = Math.max(0, tFirst - 0.01); const t1 = tLast + 0.02; const fx = (t: number) => padL + ((t - t0) / (t1 - t0)) * pw; ctx.font = '10px system-ui'; ctx.textAlign = 'right'; for (let g = 0; g <= 4; g++) { const f = (peak * g) / 4; const yy = 10 + ph - (g / 4) * 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(f.toFixed(0), padL - 4, yy + 3); } ctx.textAlign = 'center'; const span = t1 - t0; const steps = [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1]; const tStep = steps.find((s) => span / s <= 10) ?? 1; for (let t = Math.ceil(t0 / tStep) * tStep; t <= t1; t += tStep) { const x = fx(t); ctx.strokeStyle = PLOT_COLORS.grid; ctx.beginPath(); ctx.moveTo(x, 10); ctx.lineTo(x, 10 + ph); ctx.stroke(); ctx.fillStyle = PLOT_COLORS.axis; ctx.fillText(tStep >= 0.1 ? `${t.toFixed(1)}s` : `${(t * 1000).toFixed(0)}ms`, x, h - 4); } ctx.strokeStyle = PLOT_COLORS.accent; ctx.lineWidth = 1.4; ctx.beginPath(); let started = false; for (let i = 0; i < forceN.length; i++) { if (forceT[i] < t0 || forceT[i] > t1) continue; const x = fx(forceT[i]); const yy = 10 + ph - (forceN[i] / peak) * ph; if (!started) { ctx.moveTo(x, yy); started = true; } else { ctx.lineTo(x, yy); } } ctx.stroke(); ctx.lineWidth = 1; ctx.fillStyle = PLOT_COLORS.axis; ctx.textAlign = 'left'; ctx.fillText(`peak ${peak.toFixed(0)} N`, padL + 6, 20); }, [forceT, forceN], 170, ); return ; }