/ simulated_instruments / maraca
Sign in
simulated_instruments / maraca
maraca / webapp / src / components / plots / ModeMap.tsx
107 lines · 3.6 KBBlameHistoryRaw
1// The retained shell modes, plotted as frequency against spherical-harmonic
2// degree l. The two membrane branches separate clearly: the lower one climbs
3// slowly from the l = 2 mode and is what the grains mostly excite, the upper
4// one starts at the breathing mode. Marker area is the radiation efficiency
5// sigma_l, and the bar under each marker is its damping ratio, so a mode that
6// is loud but short-lived is visible as a large marker with a long bar.
8import { useCanvas, drawFrame, PLOT_COLORS } from './canvasUtils';
10interface Props {
11 modeF: Float32Array;
12 modeL: Float32Array;
13 modeZeta: Float32Array;
14 modeSigma: Float32Array;
15 breathingHz: number;
18export default function ModeMap({ modeF, modeL, modeZeta, modeSigma, breathingHz }: Props) {
19 const ref = useCanvas(
20 (ctx, w, h) => {
21 drawFrame(ctx, w, h);
22 const padL = 40;
23 const padB = 20;
24 const pw = w - padL - 10;
25 const ph = h - padB - 14;
26 const top = 12;
27 const n = modeF.length;
28 if (n === 0) return;
30 let lMax = 1;
31 let fMax = 1;
32 let zMax = 1e-9;
33 let sMax = 1e-9;
34 for (let i = 0; i < n; i++) {
35 lMax = Math.max(lMax, modeL[i]);
36 fMax = Math.max(fMax, modeF[i]);
37 zMax = Math.max(zMax, modeZeta[i]);
38 sMax = Math.max(sMax, modeSigma[i]);
39 }
40 fMax *= 1.08;
42 const lx = (l: number) => padL + (l / lMax) * pw;
43 const fy = (f: number) => top + ph - (f / fMax) * ph;
45 ctx.font = '10px system-ui';
46 ctx.textAlign = 'center';
47 const lStep = lMax > 20 ? 5 : lMax > 8 ? 2 : 1;
48 for (let l = 0; l <= lMax; l += lStep) {
49 const x = lx(l);
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(`${l}`, x, h - 7);
57 }
58 ctx.fillText('degree l', padL + pw / 2, h - 0);
59 ctx.textAlign = 'right';
60 const fStep = fMax > 16000 ? 5000 : fMax > 6000 ? 2000 : 1000;
61 for (let f = 0; f <= fMax; f += fStep) {
62 const y = fy(f);
63 ctx.strokeStyle = PLOT_COLORS.grid;
64 ctx.beginPath();
65 ctx.moveTo(padL, y);
66 ctx.lineTo(padL + pw, y);
67 ctx.stroke();
68 ctx.fillStyle = PLOT_COLORS.axis;
69 ctx.fillText(f >= 1000 ? `${f / 1000}k` : `${f}`, padL - 4, y + 3);
70 }
72 // damping bars, then markers on top
73 for (let i = 0; i < n; i++) {
74 const x = lx(modeL[i]);
75 const y = fy(modeF[i]);
76 const barH = (modeZeta[i] / zMax) * 22;
77 ctx.strokeStyle = 'rgba(232, 163, 61, 0.5)';
78 ctx.beginPath();
79 ctx.moveTo(x, y);
80 ctx.lineTo(x, y + barH);
81 ctx.stroke();
82 }
83 for (let i = 0; i < n; i++) {
84 const x = lx(modeL[i]);
85 const y = fy(modeF[i]);
86 const r = 2.5 + 5 * Math.sqrt(modeSigma[i] / sMax);
87 const isBreathing = Math.abs(modeF[i] - breathingHz) < 1e-6;
88 ctx.fillStyle = isBreathing ? PLOT_COLORS.good : PLOT_COLORS.trace;
89 ctx.beginPath();
90 ctx.arc(x, y, r, 0, 2 * Math.PI);
91 ctx.fill();
92 if (isBreathing) {
93 ctx.fillStyle = PLOT_COLORS.good;
94 ctx.textAlign = 'left';
95 ctx.fillText(`breathing ${breathingHz.toFixed(0)} Hz`, x + r + 4, y + 3);
96 }
97 }
99 ctx.fillStyle = PLOT_COLORS.axis;
100 ctx.textAlign = 'left';
101 ctx.fillText(`${n} modes · marker = radiation efficiency · bar = damping`, padL + 6, top + 2);
102 },
103 [modeF, modeL, modeZeta, modeSigma, breathingHz],
104 210,
105 );
106 return <canvas ref={ref} />;
↑↓ move↵openescclose