1// Contact force against time, zoomed to the strokes: the Hertz contact
2// between the mallet head and the shell, max-pooled by the worker. The
3// double peaks of a single stroke are the beam's reflection returning to
4// the head (the paper's Fig. 12); later bumps are the stick bouncing on the
5// wobbling shell before the wrist clears it.
7import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils';
9interface Props {
10 forceT: Float32Array;
11 forceN: Float32Array;
12}
14export default function ContactPlot({ forceT, forceN }: Props) {
15 const ref = useCanvas(
16 (ctx, w, h) => {
17 drawFrame(ctx, w, h);
18 const padL = 40;
19 const padB = 18;
20 const pw = w - padL - 8;
21 const ph = h - padB - 10;
23 let peak = 0;
24 let tFirst = Infinity;
25 let tLast = 0;
26 for (let i = 0; i < forceN.length; i++) {
27 if (forceN[i] > peak) peak = forceN[i];
28 if (forceN[i] > 0) {
29 tFirst = Math.min(tFirst, forceT[i]);
30 tLast = Math.max(tLast, forceT[i]);
31 }
32 }
33 if (!(peak > 0)) {
34 ctx.fillStyle = PLOT_COLORS.axis;
35 ctx.font = '11px system-ui';
36 ctx.fillText('no contact', padL + 8, 30);
37 return;
38 }
39 const t0 = Math.max(0, tFirst - 0.01);
40 const t1 = tLast + 0.02;
41 const fx = (t: number) => padL + ((t - t0) / (t1 - t0)) * pw;
43 ctx.font = '10px system-ui';
44 ctx.textAlign = 'right';
45 for (let g = 0; g <= 4; g++) {
46 const f = (peak * g) / 4;
47 const yy = 10 + ph - (g / 4) * ph;
48 ctx.strokeStyle = PLOT_COLORS.grid;
49 ctx.beginPath();
50 ctx.moveTo(padL, yy);
51 ctx.lineTo(padL + pw, yy);
52 ctx.stroke();
53 ctx.fillStyle = PLOT_COLORS.axis;
54 ctx.fillText(f.toFixed(0), padL - 4, yy + 3);
55 }
56 ctx.textAlign = 'center';
57 const span = t1 - t0;
58 const steps = [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1];
59 const tStep = steps.find((s) => span / s <= 10) ?? 1;
60 for (let t = Math.ceil(t0 / tStep) * tStep; t <= t1; t += tStep) {
61 const x = fx(t);
62 ctx.strokeStyle = PLOT_COLORS.grid;
63 ctx.beginPath();
64 ctx.moveTo(x, 10);
65 ctx.lineTo(x, 10 + ph);
66 ctx.stroke();
67 ctx.fillStyle = PLOT_COLORS.axis;
68 ctx.fillText(tStep >= 0.1 ? `${t.toFixed(1)}s` : `${(t * 1000).toFixed(0)}ms`, x, h - 4);
69 }
71 ctx.strokeStyle = PLOT_COLORS.accent;
72 ctx.lineWidth = 1.4;
73 ctx.beginPath();
74 let started = false;
75 for (let i = 0; i < forceN.length; i++) {
76 if (forceT[i] < t0 || forceT[i] > t1) continue;
77 const x = fx(forceT[i]);
78 const yy = 10 + ph - (forceN[i] / peak) * ph;
79 if (!started) {
80 ctx.moveTo(x, yy);
81 started = true;
82 } else {
83 ctx.lineTo(x, yy);
84 }
85 }
86 ctx.stroke();
87 ctx.lineWidth = 1;
88 ctx.fillStyle = PLOT_COLORS.axis;
89 ctx.textAlign = 'left';
90 ctx.fillText(`peak ${peak.toFixed(0)} N`, padL + 6, 20);
91 },
92 [forceT, forceN],
93 170,
94 );
95 return <canvas ref={ref} />;
96}