/ simulated_instruments / cymbal
Sign in
simulated_instruments / cymbal
cymbal / webapp / src / components / plots / CascadePlot.tsx
73 lines · 2.6 KBBlameHistoryRaw
1// Where the energy is, and where it goes. The cascade solve tracks the energy
2// of every mode at the control rate; this collapses it onto a log-frequency by
3// time grid, so the upward flux is visible directly as the bright region
4// climbing out of the strike. Turning cascade_gain down to zero flattens it
5// into horizontal streaks: every mode simply decaying where it was struck.
7import { useCanvas, drawFrame, PLOT_COLORS, colormap } from './canvasUtils';
9interface Props {
10 cascadeDb: Float32Array; // nBands x nFrames, column-major
11 bands: number;
12 frames: number;
13 fLo: number;
14 fHi: number;
15 tEnd: number;
18const DB_RANGE = 70;
20export default function CascadePlot({ cascadeDb, bands, frames, fLo, fHi, tEnd }: Props) {
21 const ref = useCanvas(
22 (ctx, w, h) => {
23 drawFrame(ctx, w, h);
24 const padL = 38;
25 const padB = 16;
26 const pw = w - padL - 8;
27 const ph = h - padB - 8;
28 if (bands < 1 || frames < 1) return;
30 let maxDb = -Infinity;
31 for (let i = 0; i < cascadeDb.length; i++) maxDb = Math.max(maxDb, cascadeDb[i]);
32 if (!Number.isFinite(maxDb)) return;
34 const img = ctx.createImageData(frames, bands);
35 for (let f = 0; f < frames; f++) {
36 for (let b = 0; b < bands; b++) {
37 const v = cascadeDb[f * bands + b] - maxDb;
38 const t = Math.min(1, Math.max(0, (v + DB_RANGE) / DB_RANGE));
39 const [r, g, bl] = colormap(t);
40 const row = bands - 1 - b;
41 const idx = (row * frames + f) * 4;
42 img.data[idx] = r;
43 img.data[idx + 1] = g;
44 img.data[idx + 2] = bl;
45 img.data[idx + 3] = 255;
46 }
47 }
48 const off = new OffscreenCanvas(frames, bands);
49 off.getContext('2d')!.putImageData(img, 0, 0);
50 ctx.imageSmoothingEnabled = true;
51 ctx.drawImage(off, padL, 8, pw, ph);
53 ctx.fillStyle = PLOT_COLORS.axis;
54 ctx.font = '10px system-ui';
55 ctx.textAlign = 'right';
56 const lLo = Math.log10(fLo);
57 const lHi = Math.log10(Math.max(fLo * 1.2, fHi));
58 for (const f of [50, 100, 200, 500, 1000, 2000, 5000, 10000]) {
59 if (f < fLo || f > fHi) continue;
60 const yy = 8 + ph - ((Math.log10(f) - lLo) / (lHi - lLo)) * ph;
61 ctx.fillText(f >= 1000 ? `${f / 1000}k` : `${f}`, padL - 4, yy + 3);
62 }
63 ctx.textAlign = 'center';
64 const step = tEnd > 4 ? 1 : tEnd > 1.5 ? 0.5 : 0.1;
65 for (let t = 0; t <= tEnd + 1e-9; t += step) {
66 ctx.fillText(`${t.toFixed(1)}s`, padL + (t / tEnd) * pw, h - 4);
67 }
68 },
69 [cascadeDb, bands, frames, fLo, fHi, tEnd],
70 200,
71 );
72 return <canvas ref={ref} />;
↑↓ move↵openescclose