// One azimuthal wavenumber at a time. Left: the shape cos(m theta) on the // disk, the pattern every quantity is decomposed into. Right: the eigenvalue // of the discrete azimuthal second difference against the continuum value // m^2, showing where the grid stops being faithful. import { useState } from 'react'; import { useCanvas, drawFrame, PLOT_COLORS } from '../../components/plots/canvasUtils'; import { Figure, FigControls, FigSlider } from '../figparts'; const HEIGHT = 270; const NTH = 60; export default function WavenumberFig() { const [m, setM] = useState(4); const ref = useCanvas( (ctx, w, h) => { drawFrame(ctx, w, h); const half = Math.min(w * 0.42, h); const cx = half / 2 + 14; const cy = h / 2; const R = Math.min(half, h) / 2 - 18; const Rin = R * 0.14; // disk shaded by cos(m theta) with a radial envelope, via offscreen bitmap const px = 160; const off = document.createElement('canvas'); off.width = px; off.height = px; const octx = off.getContext('2d')!; const img = octx.createImageData(px, px); for (let yy = 0; yy < px; yy++) { for (let xx = 0; xx < px; xx++) { const dx = (xx - px / 2) / (px / 2); const dy = (yy - px / 2) / (px / 2); const r = Math.hypot(dx, dy); const o = 4 * (yy * px + xx); if (r > 1 || r < Rin / R) { img.data[o + 3] = 0; continue; } const th = Math.atan2(dy, dx); const env = (r - Rin / R) / (1 - Rin / R); const v = Math.cos(m * th) * env; // -1..1 // cyan for +, amber for - const t = Math.abs(v); const rr = v >= 0 ? 77 : 232; const gg = v >= 0 ? 208 : 163; const bb = v >= 0 ? 225 : 61; img.data[o] = Math.round(24 + (rr - 24) * t); img.data[o + 1] = Math.round(27 + (gg - 27) * t); img.data[o + 2] = Math.round(32 + (bb - 32) * t); img.data[o + 3] = 255; } } octx.putImageData(img, 0, 0); ctx.drawImage(off, cx - R, cy - R, 2 * R, 2 * R); ctx.strokeStyle = PLOT_COLORS.axis; ctx.beginPath(); ctx.arc(cx, cy, R, 0, 2 * Math.PI); ctx.stroke(); ctx.fillStyle = PLOT_COLORS.axis; ctx.font = '11px system-ui'; ctx.textAlign = 'center'; ctx.fillText(`cos(${m}θ): ${m} nodal diameter${m === 1 ? '' : 's'}`, cx, h - 8); // right panel: -lam2 vs m^2 const padL = half + 60; const padR = 16; const padT = 16; const padB = 34; const pw = w - padL - padR; const ph = h - padT - padB; const mMax = NTH / 2; const hth = (2 * Math.PI) / NTH; const yMax = mMax * mMax; const X = (mm: number) => padL + (mm / mMax) * pw; const Y = (v: number) => padT + ph - (v / yMax) * ph; ctx.strokeStyle = PLOT_COLORS.grid; ctx.strokeRect(padL, padT, pw, ph); // continuum m^2, dashed ctx.strokeStyle = 'rgba(139, 147, 161, 0.8)'; ctx.setLineDash([4, 4]); ctx.beginPath(); for (let mm = 0; mm <= mMax; mm += 0.5) { const x = X(mm); const y = Y(mm * mm); if (mm === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); ctx.setLineDash([]); // discrete eigenvalue ctx.strokeStyle = PLOT_COLORS.trace; ctx.lineWidth = 2; ctx.beginPath(); for (let mm = 0; mm <= mMax; mm += 0.5) { const lam = (2 - 2 * Math.cos(mm * hth)) / (hth * hth); const x = X(mm); const y = Y(lam); if (mm === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); ctx.lineWidth = 1; // current m marker const lamM = (2 - 2 * Math.cos(m * hth)) / (hth * hth); ctx.fillStyle = PLOT_COLORS.accent; ctx.beginPath(); ctx.arc(X(m), Y(lamM), 4, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle = PLOT_COLORS.axis; ctx.textAlign = 'center'; ctx.fillText('wavenumber m', padL + pw / 2, h - 8); for (const mm of [0, 10, 20, 30]) { ctx.fillText(`${mm}`, X(mm), padT + ph + 13); } ctx.textAlign = 'left'; ctx.fillStyle = PLOT_COLORS.trace; ctx.fillText('discrete −λ₂(m)', padL + 8, padT + 14); ctx.fillStyle = PLOT_COLORS.axis; ctx.fillText('continuum m²', padL + 8, padT + 28); ctx.fillText(`grid cutoff m = ${mMax}`, X(mMax) - 100, padT + ph - 8); }, [m], HEIGHT, ); return (
Left: everything on the grid is decomposed azimuthally into patterns cos(mθ) and sin(mθ); m counts nodal diameters. Right: on the discrete ring the second derivative in θ acts on such a pattern as multiplication by a single number, −λ2(m) (cyan), which follows the continuum m² (dashed) at low order and falls away toward the grid cutoff at m = Nθ/2 = 30. The two consequences: every linear solve splits into independent small radial problems, one per wavenumber; and the highest orders the grid carries are progressively detuned, which is one reason the model's top octaves are duller than a real crash. } > `${Math.round(v)}`} onChange={(v) => setM(Math.round(v))} />
); }