/ simulated_instruments / cymbal
Sign in
simulated_instruments / cymbal
cymbal / webapp / src / components / plots / ModeMap.tsx
102 lines · 3.6 KBCodeBlameHistory
4ddcc1bCymbal simulator: shell-of-revolution modal model with a nonlinear energy cascade, numbl MATLAB reference, and interactive web appJeremy Magland 1// The retained plate modes: frequency against ring time T60 = 6.91 / (zeta om),
2// colored by nodal-diameter order and sized by how much of each mode's damping
3// is radiation. Two things a cymbal shows here that the sibling triangle does
4// not: the modes are dense enough that the cloud is continuous above about a
5// kilohertz, and the radiation share is negligible almost everywhere, because
6// a thin bronze plate below its coincidence frequency is a very poor radiator
7// and loses its energy internally instead.
9import { useCanvas, drawFrame, PLOT_COLORS, modeColor } from './canvasUtils';
11interface Props {
12 modeF: Float32Array;
13 modeZeta: Float32Array;
14 modeZetaRad: Float32Array;
15 modeN: Float32Array;
18const F_LO = 40;
19const F_HI = 20000;
21export default function ModeMap({ modeF, modeZeta, modeZetaRad, modeN }: Props) {
22 let maxN = 1;
23 for (let i = 0; i < modeN.length; i++) maxN = Math.max(maxN, modeN[i]);
24 const ref = useCanvas(
25 (ctx, w, h) => {
26 drawFrame(ctx, w, h);
27 const padL = 40;
28 const padB = 20;
29 const pw = w - padL - 10;
30 const ph = h - padB - 14;
31 const top = 12;
32 const n = modeF.length;
33 if (n === 0) return;
35 const t60 = new Float64Array(n);
36 let tMin = Infinity;
37 let tMax = 0;
38 for (let i = 0; i < n; i++) {
39 t60[i] = 6.91 / (Math.max(1e-9, modeZeta[i]) * 2 * Math.PI * modeF[i]);
40 tMin = Math.min(tMin, t60[i]);
41 tMax = Math.max(tMax, t60[i]);
42 }
43 tMin = Math.max(1e-3, tMin / 1.5);
44 tMax = tMax * 1.5;
46 const fx = (f: number) =>
47 padL + ((Math.log10(f) - Math.log10(F_LO)) / (Math.log10(F_HI) - Math.log10(F_LO))) * pw;
48 const ty = (t: number) =>
49 top + ph - ((Math.log10(t) - Math.log10(tMin)) / (Math.log10(tMax) - Math.log10(tMin))) * ph;
51 ctx.font = '10px system-ui';
52 ctx.textAlign = 'center';
53 for (const f of [50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000]) {
54 const x = fx(f);
55 ctx.strokeStyle = PLOT_COLORS.grid;
56 ctx.beginPath();
57 ctx.moveTo(x, top);
58 ctx.lineTo(x, top + ph);
59 ctx.stroke();
60 ctx.fillStyle = PLOT_COLORS.axis;
61 ctx.fillText(f >= 1000 ? `${f / 1000}k` : `${f}`, x, h - 7);
62 }
63 ctx.textAlign = 'right';
64 for (let e = Math.ceil(Math.log10(tMin)); e <= Math.floor(Math.log10(tMax)); e++) {
65 const t = 10 ** e;
66 const y = ty(t);
67 ctx.strokeStyle = PLOT_COLORS.grid;
68 ctx.beginPath();
69 ctx.moveTo(padL, y);
70 ctx.lineTo(padL + pw, y);
71 ctx.stroke();
72 ctx.fillStyle = PLOT_COLORS.axis;
73 ctx.fillText(t >= 1 ? `${t}s` : `${t * 1000}ms`, padL - 4, y + 3);
74 }
76 for (let i = 0; i < n; i++) {
77 const x = fx(Math.min(F_HI, Math.max(F_LO, modeF[i])));
78 const y = ty(t60[i]);
79 const radShare = Math.min(1, modeZetaRad[i] / Math.max(1e-12, modeZeta[i]));
80 const r = 1.6 + 5 * Math.sqrt(radShare);
81 ctx.fillStyle = modeColor(modeN[i], maxN, 0.8);
82 ctx.beginPath();
83 ctx.arc(x, y, r, 0, 2 * Math.PI);
84 ctx.fill();
85 }
87 ctx.textAlign = 'left';
88 ctx.fillStyle = PLOT_COLORS.axis;
89 ctx.fillText(`${n} modes · marker = radiation share of damping · nodal diameters 0`, padL + 6, top + 2);
90 const x0 = padL + 300;
91 for (let k = 0; k <= maxN; k++) {
92 ctx.fillStyle = modeColor(k, maxN, 0.95);
93 ctx.fillRect(x0 + (k / maxN) * 60, top - 5, 60 / (maxN + 1) + 1, 8);
94 }
95 ctx.fillStyle = PLOT_COLORS.axis;
96 ctx.fillText(`${maxN}`, x0 + 64, top + 2);
97 },
98 [modeF, modeZeta, modeZetaRad, modeN, maxN],
99 210,
100 );
101 return <canvas ref={ref} />;
↑↓ move↵openescclose