1// The stochastic impact process itself: every grain-shell collision as a dot
2// at its time and speed, over the expected-rate envelope the strokes impose.
3// The gap between bursts is where the maraca's rhythm lives, and a burst
4// width comparable to the stroke period smears it into a continuous wash.
6import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils';
8interface Props {
9 impactT: Float32Array;
10 impactV: Float32Array;
11 rateT: Float32Array;
12 rateHz: Float32Array;
13 durationS: number;
14 impactCount: number;
15 plotted: number;
16}
18export default function ImpactRaster({
19 impactT,
20 impactV,
21 rateT,
22 rateHz,
23 durationS,
24 impactCount,
25 plotted,
26}: Props) {
27 const ref = useCanvas(
28 (ctx, w, h) => {
29 drawFrame(ctx, w, h);
30 const padL = 40;
31 const padB = 16;
32 const pw = w - padL - 8;
33 const ph = h - padB - 12;
34 const top = 10;
36 let vMax = 0;
37 for (let i = 0; i < impactV.length; i++) vMax = Math.max(vMax, impactV[i]);
38 if (vMax <= 0) vMax = 1;
39 let rMax = 0;
40 for (let i = 0; i < rateHz.length; i++) rMax = Math.max(rMax, rateHz[i]);
42 const tx = (t: number) => padL + (t / durationS) * pw;
44 // time grid
45 ctx.font = '10px system-ui';
46 ctx.textAlign = 'center';
47 const tStep = durationS > 4 ? 1.0 : durationS > 1.5 ? 0.5 : 0.1;
48 for (let t = 0; t <= durationS + 1e-9; t += tStep) {
49 const x = tx(t);
50 ctx.strokeStyle = PLOT_COLORS.grid;
51 ctx.beginPath();
52 ctx.moveTo(x, top);
53 ctx.lineTo(x, top + ph);
54 ctx.stroke();
55 ctx.fillStyle = PLOT_COLORS.axis;
56 ctx.fillText(`${t.toFixed(tStep < 0.5 ? 1 : 1)}s`, x, h - 4);
57 }
58 ctx.textAlign = 'right';
59 ctx.fillStyle = PLOT_COLORS.axis;
60 ctx.fillText(`${vMax.toFixed(1)} m/s`, padL - 4, top + 4);
61 ctx.fillText('0', padL - 4, top + ph);
63 // expected-rate envelope, filled under the curve
64 if (rMax > 0) {
65 ctx.fillStyle = 'rgba(111, 191, 115, 0.16)';
66 ctx.beginPath();
67 ctx.moveTo(padL, top + ph);
68 for (let i = 0; i < rateT.length; i++) {
69 ctx.lineTo(tx(rateT[i]), top + ph - (rateHz[i] / rMax) * ph * 0.9);
70 }
71 ctx.lineTo(padL + pw, top + ph);
72 ctx.closePath();
73 ctx.fill();
74 }
76 // impacts
77 ctx.fillStyle = 'rgba(77, 208, 225, 0.55)';
78 for (let i = 0; i < impactT.length; i++) {
79 const x = tx(impactT[i]);
80 const y = top + ph - (impactV[i] / vMax) * ph;
81 ctx.fillRect(x, y, 1.4, 1.4);
82 }
84 ctx.fillStyle = PLOT_COLORS.axis;
85 ctx.textAlign = 'left';
86 const note =
87 plotted < impactCount
88 ? `${impactCount} impacts (${plotted} plotted) · peak rate ${rMax.toFixed(0)}/s`
89 : `${impactCount} impacts · peak rate ${rMax.toFixed(0)}/s`;
90 ctx.fillText(note, padL + 6, top + 4);
91 },
92 [impactT, impactV, rateT, rateHz, durationS, impactCount, plotted],
93 190,
94 );
95 return <canvas ref={ref} />;
96}