// Where the energy is, and where it goes. The cascade solve tracks the energy // of every mode at the control rate; this collapses it onto a log-frequency by // time grid, so the upward flux is visible directly as the bright region // climbing out of the strike. Turning cascade_gain down to zero flattens it // into horizontal streaks: every mode simply decaying where it was struck. import { useCanvas, drawFrame, PLOT_COLORS, colormap } from './canvasUtils'; interface Props { cascadeDb: Float32Array; // nBands x nFrames, column-major bands: number; frames: number; fLo: number; fHi: number; tEnd: number; } const DB_RANGE = 70; export default function CascadePlot({ cascadeDb, bands, frames, fLo, fHi, tEnd }: Props) { const ref = useCanvas( (ctx, w, h) => { drawFrame(ctx, w, h); const padL = 38; const padB = 16; const pw = w - padL - 8; const ph = h - padB - 8; if (bands < 1 || frames < 1) return; let maxDb = -Infinity; for (let i = 0; i < cascadeDb.length; i++) maxDb = Math.max(maxDb, cascadeDb[i]); if (!Number.isFinite(maxDb)) return; const img = ctx.createImageData(frames, bands); for (let f = 0; f < frames; f++) { for (let b = 0; b < bands; b++) { const v = cascadeDb[f * bands + b] - maxDb; const t = Math.min(1, Math.max(0, (v + DB_RANGE) / DB_RANGE)); const [r, g, bl] = colormap(t); const row = bands - 1 - b; const idx = (row * frames + f) * 4; img.data[idx] = r; img.data[idx + 1] = g; img.data[idx + 2] = bl; img.data[idx + 3] = 255; } } const off = new OffscreenCanvas(frames, bands); off.getContext('2d')!.putImageData(img, 0, 0); ctx.imageSmoothingEnabled = true; ctx.drawImage(off, padL, 8, pw, ph); ctx.fillStyle = PLOT_COLORS.axis; ctx.font = '10px system-ui'; ctx.textAlign = 'right'; const lLo = Math.log10(fLo); const lHi = Math.log10(Math.max(fLo * 1.2, fHi)); for (const f of [50, 100, 200, 500, 1000, 2000, 5000, 10000]) { if (f < fLo || f > fHi) continue; const yy = 8 + ph - ((Math.log10(f) - lLo) / (lHi - lLo)) * ph; ctx.fillText(f >= 1000 ? `${f / 1000}k` : `${f}`, padL - 4, yy + 3); } ctx.textAlign = 'center'; const step = tEnd > 4 ? 1 : tEnd > 1.5 ? 0.5 : 0.1; for (let t = 0; t <= tEnd + 1e-9; t += step) { ctx.fillText(`${t.toFixed(1)}s`, padL + (t / tEnd) * pw, h - 4); } }, [cascadeDb, bands, frames, fLo, fHi, tEnd], 200, ); return ; }