// The stochastic impact process itself: every grain-shell collision as a dot // at its time and speed, over the expected-rate envelope the strokes impose. // The gap between bursts is where the maraca's rhythm lives, and a burst // width comparable to the stroke period smears it into a continuous wash. import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils'; interface Props { impactT: Float32Array; impactV: Float32Array; rateT: Float32Array; rateHz: Float32Array; durationS: number; impactCount: number; plotted: number; } export default function ImpactRaster({ impactT, impactV, rateT, rateHz, durationS, impactCount, plotted, }: Props) { const ref = useCanvas( (ctx, w, h) => { drawFrame(ctx, w, h); const padL = 40; const padB = 16; const pw = w - padL - 8; const ph = h - padB - 12; const top = 10; let vMax = 0; for (let i = 0; i < impactV.length; i++) vMax = Math.max(vMax, impactV[i]); if (vMax <= 0) vMax = 1; let rMax = 0; for (let i = 0; i < rateHz.length; i++) rMax = Math.max(rMax, rateHz[i]); const tx = (t: number) => padL + (t / durationS) * pw; // time grid ctx.font = '10px system-ui'; ctx.textAlign = 'center'; const tStep = durationS > 4 ? 1.0 : durationS > 1.5 ? 0.5 : 0.1; for (let t = 0; t <= durationS + 1e-9; t += tStep) { const x = tx(t); 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(`${t.toFixed(tStep < 0.5 ? 1 : 1)}s`, x, h - 4); } ctx.textAlign = 'right'; ctx.fillStyle = PLOT_COLORS.axis; ctx.fillText(`${vMax.toFixed(1)} m/s`, padL - 4, top + 4); ctx.fillText('0', padL - 4, top + ph); // expected-rate envelope, filled under the curve if (rMax > 0) { ctx.fillStyle = 'rgba(111, 191, 115, 0.16)'; ctx.beginPath(); ctx.moveTo(padL, top + ph); for (let i = 0; i < rateT.length; i++) { ctx.lineTo(tx(rateT[i]), top + ph - (rateHz[i] / rMax) * ph * 0.9); } ctx.lineTo(padL + pw, top + ph); ctx.closePath(); ctx.fill(); } // impacts ctx.fillStyle = 'rgba(77, 208, 225, 0.55)'; for (let i = 0; i < impactT.length; i++) { const x = tx(impactT[i]); const y = top + ph - (impactV[i] / vMax) * ph; ctx.fillRect(x, y, 1.4, 1.4); } ctx.fillStyle = PLOT_COLORS.axis; ctx.textAlign = 'left'; const note = plotted < impactCount ? `${impactCount} impacts (${plotted} plotted) · peak rate ${rMax.toFixed(0)}/s` : `${impactCount} impacts · peak rate ${rMax.toFixed(0)}/s`; ctx.fillText(note, padL + 6, top + 4); }, [impactT, impactV, rateT, rateHz, durationS, impactCount, plotted], 190, ); return ; }