/ simulated_instruments / cymbal2
Sign in
simulated_instruments / cymbal2
cymbal2 / webapp / src / tutorial / figures / WasherFig.tsx
161 lines · 5.7 KBBlameHistoryRaw
1// The washer as a boundary condition. The shell rests on a felt sleeve at
2// its mounting circle; modeled as a spring per unit length, it gives the
3// whole shell two rigid-body ways to move that a clamp forbids: a bounce
4// (m = 0) and a tilt (m = 1). The figure animates either one, slowed far
5// down, with the closed-form rigid-shell frequencies for the stiffness set
6// by the slider.
8import { useEffect, useRef, useState } from 'react';
9import { drawFrame, PLOT_COLORS } from '../../components/plots/canvasUtils';
10import { Figure, FigControls, FigSlider } from '../figparts';
12const HEIGHT = 230;
13const A = 0.23;
14const C = 0.029;
15const RISE = 0.04;
16const RHOH = 8.86; // kg/m^2
17const SLOW = 8; // animation runs this many times slower than real
19export default function WasherFig() {
20 const [kw, setKw] = useState(1.65e5);
21 const [mode, setMode] = useState<'bounce' | 'tilt'>('bounce');
22 const canvasRef = useRef<HTMLCanvasElement | null>(null);
24 const fBounce = Math.sqrt((kw * 2 * Math.PI * C) / (RHOH * Math.PI * (A * A - C * C))) / (2 * Math.PI);
25 const fTilt = Math.sqrt((4 * kw * C * C * C) / (RHOH * (A ** 4 - C ** 4))) / (2 * Math.PI);
27 useEffect(() => {
28 const canvas = canvasRef.current;
29 if (!canvas) return;
30 let raf = 0;
31 const t0 = performance.now();
32 const draw = () => {
33 const dpr = window.devicePixelRatio || 1;
34 const w = canvas.clientWidth;
35 const bw = Math.max(1, Math.round(w * dpr));
36 const bh = Math.max(1, Math.round(HEIGHT * dpr));
37 if (canvas.width !== bw || canvas.height !== bh) {
38 canvas.width = bw;
39 canvas.height = bh;
40 canvas.style.height = `${HEIGHT}px`;
41 }
42 const ctx = canvas.getContext('2d')!;
43 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
44 drawFrame(ctx, w, HEIGHT);
46 const f = mode === 'bounce' ? fBounce : fTilt;
47 const tSec = (performance.now() - t0) / 1000;
48 const phase = Math.sin(2 * Math.PI * (f / SLOW) * tSec);
50 const cx = w / 2;
51 const baseY = HEIGHT - 56;
52 const scale = (w - 80) / (2 * A);
53 const zScale = (HEIGHT - 130) / RISE;
54 const ampPx = 13;
56 const dz = (r: number, side: number) => {
57 // vertical offset of the shell at signed radius, in px
58 if (mode === 'bounce') return -phase * ampPx;
59 return -phase * ampPx * ((side * r) / A);
60 };
61 const px = (r: number, side: number) => cx + side * r * scale;
62 const py = (z: number) => baseY - z * zScale;
64 // ground
65 ctx.strokeStyle = PLOT_COLORS.grid;
66 ctx.beginPath();
67 ctx.moveTo(20, baseY + 26);
68 ctx.lineTo(w - 20, baseY + 26);
69 ctx.stroke();
71 // springs at +-c
72 const zc = RISE * (1 - (C * C) / (A * A));
73 for (const side of [-1, 1]) {
74 const x = px(C, side);
75 const topY = py(zc) + dz(C, side) + 4;
76 const botY = baseY + 26;
77 ctx.strokeStyle = PLOT_COLORS.accent;
78 ctx.beginPath();
79 ctx.moveTo(x, topY);
80 const nzig = 6;
81 for (let i = 1; i <= nzig; i++) {
82 const yy = topY + ((botY - topY) * i) / (nzig + 1);
83 ctx.lineTo(x + (i % 2 === 0 ? 7 : -7), yy);
84 }
85 ctx.lineTo(x, botY);
86 ctx.stroke();
87 }
89 // shell cross-section, displaced rigidly
90 ctx.strokeStyle = '#c9a227';
91 ctx.lineWidth = 3;
92 ctx.beginPath();
93 const N = 80;
94 for (let i = 0; i <= N; i++) {
95 const rr = -A + (2 * A * i) / N;
96 const side = rr < 0 ? -1 : 1;
97 const r = Math.abs(rr);
98 const z = RISE * (1 - (r * r) / (A * A));
99 const x = px(r, side);
100 const y = py(z) + dz(r, side);
101 if (i === 0) ctx.moveTo(x, y);
102 else ctx.lineTo(x, y);
103 }
104 ctx.stroke();
105 ctx.lineWidth = 1;
107 ctx.fillStyle = PLOT_COLORS.axis;
108 ctx.font = '11px system-ui';
109 ctx.textAlign = 'left';
110 ctx.fillText(
111 `${mode === 'bounce' ? 'bounce (m = 0)' : 'tilt (m = 1)'} at ${f.toFixed(1)} Hz, shown ${SLOW}× slower`,
112 14, 20,
113 );
114 ctx.fillText(
115 `rigid-shell closed forms: bounce ${fBounce.toFixed(1)} Hz, tilt ${fTilt.toFixed(1)} Hz`,
116 14, 38,
117 );
119 raf = requestAnimationFrame(draw);
120 };
121 raf = requestAnimationFrame(draw);
122 return () => cancelAnimationFrame(raf);
123 }, [mode, fBounce, fTilt]);
125 return (
126 <Figure
127 caption={
128 <>
129 The whole shell riding on its felt washer, with the mounting circle drawn as the
130 springs it is. Treating the shell as rigid gives closed forms for the two
131 rigid-body modes; at the paper's measured stiffness (1.65&times;10<sup>5</sup>{' '}
132 N/m per meter of circumference) they land at 22.9 Hz and 4.1 Hz, and letting the
133 shell flex lowers the bounce to about 13 Hz. Both are below hearing, but they set
134 the boundary the audible modes see, and under a hard stroke the shell swings away
135 from the stick by centimeters, which changes what a follow-through stroke does.
136 The <i>Rigid clamp</i> preset in the simulator removes all of this, as the earlier
137 literature did, and is worth an A/B listen.
138 </>
139 }
140 >
141 <canvas ref={canvasRef} />
142 <FigControls>
143 <FigSlider
144 label="washer stiffness"
145 value={kw}
146 min={1e4}
147 max={1e7}
148 log
149 format={(v) => `${v.toExponential(2)} N/m/m`}
150 onChange={setKw}
151 />
152 <button className={mode === 'bounce' ? 'tut-active' : ''} onClick={() => setMode('bounce')}>
153 bounce (m = 0)
154 </button>
155 <button className={mode === 'tilt' ? 'tut-active' : ''} onClick={() => setMode('tilt')}>
156 tilt (m = 1)
157 </button>
158 </FigControls>
159 </Figure>
160 );
↑↓ move↵openescclose