982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 1// Animated free-free bar mode shapes. One mode is shown large and oscillating;
2// the strike position is marked so nodes/antinodes can be read against it.
4import { useEffect, useRef, useState } from 'react';
5import { PLOT_COLORS } from './canvasUtils';
7interface Props {
8 modeShapes: Float32Array; // n x shapePts
9 shapePts: number;
10 partialsHz: number[];
11 strikePosition: number;
12}
14export default function ModeShapes({ modeShapes, shapePts, partialsHz, strikePosition }: Props) {
15 const n = partialsHz.length;
16 const [selected, setSelected] = useState(0);
17 const canvasRef = useRef<HTMLCanvasElement | null>(null);
18 const sel = Math.min(selected, Math.max(0, n - 1));
20 useEffect(() => {
21 const canvas = canvasRef.current;
22 if (!canvas || n === 0) return;
23 let raf = 0;
24 const t0 = performance.now();
26 const draw = () => {
27 raf = requestAnimationFrame(draw);
28 const dpr = window.devicePixelRatio || 1;
29 const w = canvas.clientWidth;
30 const h = 180;
31 canvas.width = Math.round(w * dpr);
32 canvas.height = Math.round(h * dpr);
33 canvas.style.height = `${h}px`;
34 const ctx = canvas.getContext('2d')!;
35 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
36 ctx.fillStyle = PLOT_COLORS.bg;
37 ctx.fillRect(0, 0, w, h);
39 const padL = 14;
40 const pw = w - 2 * padL;
41 const midY = h / 2;
42 const amp = h * 0.32;
44 // oscillation rate: slow, slightly faster for higher modes
45 const t = (performance.now() - t0) / 1000;
46 const rate = 0.8 + sel * 0.25;
47 const osc = Math.sin(2 * Math.PI * rate * t);
49 // static envelope
50 ctx.strokeStyle = PLOT_COLORS.grid;
51 ctx.setLineDash([3, 3]);
52 for (const sgn of [1, -1]) {
53 ctx.beginPath();
54 for (let i = 0; i < shapePts; i++) {
55 const x = padL + (i / (shapePts - 1)) * pw;
56 const y = midY - sgn * modeShapes[sel * shapePts + i] * amp;
57 if (i === 0) ctx.moveTo(x, y);
58 else ctx.lineTo(x, y);
59 }
60 ctx.stroke();
61 }
62 ctx.setLineDash([]);
64 // rest axis
65 ctx.strokeStyle = PLOT_COLORS.grid;
66 ctx.beginPath();
67 ctx.moveTo(padL, midY);
68 ctx.lineTo(padL + pw, midY);
69 ctx.stroke();
71 // strike position
72 const sx = padL + strikePosition * pw;
73 ctx.strokeStyle = PLOT_COLORS.warn;
74 ctx.setLineDash([4, 3]);
75 ctx.beginPath();
76 ctx.moveTo(sx, 14);
77 ctx.lineTo(sx, h - 14);
78 ctx.stroke();
79 ctx.setLineDash([]);
80 ctx.fillStyle = PLOT_COLORS.warn;
81 ctx.font = '10px system-ui';
82 ctx.textAlign = 'center';
83 ctx.fillText('strike', sx, 11);
85 // animated shape as a thick bar
86 ctx.strokeStyle = PLOT_COLORS.trace;
87 ctx.lineWidth = 3;
88 ctx.beginPath();
89 for (let i = 0; i < shapePts; i++) {
90 const x = padL + (i / (shapePts - 1)) * pw;
91 const y = midY - modeShapes[sel * shapePts + i] * amp * osc;
92 if (i === 0) ctx.moveTo(x, y);
93 else ctx.lineTo(x, y);
94 }
95 ctx.stroke();
96 ctx.lineWidth = 1;
98 // value of the mode at the strike point (drive efficiency)
99 const idx = Math.round(strikePosition * (shapePts - 1));
100 const phiS = Math.abs(modeShapes[sel * shapePts + idx]);
101 ctx.fillStyle = PLOT_COLORS.axis;
102 ctx.textAlign = 'left';
103 ctx.fillText(
104 `mode ${sel + 1} · ${partialsHz[sel].toFixed(0)} Hz · |φ| at strike: ${(phiS * 100).toFixed(0)}%`,
105 padL,
106 h - 4,
107 );
108 };
109 draw();
110 return () => cancelAnimationFrame(raf);
111 }, [modeShapes, shapePts, partialsHz, strikePosition, sel, n]);
113 if (n === 0) return null;
114 return (
115 <div>
116 <div className="mode-list">
117 {partialsHz.map((f, i) => (
118 <button
119 key={i}
120 className={i === sel ? 'active' : ''}
121 onClick={() => setSelected(i)}
122 >
123 {f.toFixed(0)} Hz
124 </button>
125 ))}
126 </div>
127 <canvas ref={canvasRef} />
128 </div>
129 );
130}