// Figures for the introduction and Part I: the three settings, d'Alembert's // travelling halves, folding, the leapfrog stencil, the dispersion curves and // the delay loop. Every curve that looks computed is computed, here, from // the same closed forms the chapters use. import type { ReactElement } from 'react' import { Arrowheads, Figure, Label, Node, arrow } from './svg.tsx' import { faded, familyColor, ink, method } from '../plot/palette.ts' const pts = (xy: [number, number][]) => xy.map(([x, y], i) => `${i ? 'L' : 'M'}${x.toFixed(1)} ${y.toFixed(1)}`).join('') /** The three settings of the tutorial, with the boundary each one has. */ export function Settings() { // a plucked string const s0 = 40 const s1 = 210 const sy = 110 const apex = s0 + 0.3 * (s1 - s0) // a square membrane, drawn with the sign pattern of mode (2, 1) const q0 = 282 const qs = 132 const qy = 44 const cells: ReactElement[] = [] const n = 12 for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { const x = (j + 0.5) / n const y = (i + 0.5) / n const v = Math.sin(2 * Math.PI * x) * Math.sin(Math.PI * y) const c = v > 0 ? method.boundary : method.fd2 cells.push( , ) } } // a room in oblique projection const r0 = 500 const ry = 72 const rw = 150 const rh = 96 const dx = 44 const dy = -30 const S: [number, number] = [r0 + 36, ry + 70] const R: [number, number] = [r0 + 128, ry + 52] const hit: [number, number] = [r0 + 84, ry + rh] return (
{/* string */} {/* drum */} {cells} {/* room */}
) } /** d'Alembert: the plucked shape as two halves travelling apart. */ export function Dalembert() { const beta = 0.3 const x0 = 70 const x1 = 646 const W = x1 - x0 const phi = (x: number) => { let r = ((x % 2) + 2) % 2 let sg = 1 if (r > 1) { r = 2 - r sg = -1 } return sg * Math.min(r / beta, (1 - r) / (1 - beta)) } const rows = [0, 0.12, 0.3, 0.55, 1.0] const rh = 64 const top = 18 const amp = 26 const curve = (f: (x: number) => number, y0: number) => { const out: [number, number][] = [] for (let i = 0; i <= 200; i++) { const x = i / 200 out.push([x0 + x * W, y0 - amp * f(x)]) } return pts(out) } return (
{rows.map((ct, k) => { const y0 = top + k * rh + rh / 2 return ( 0.5 * phi(x - ct), y0)} fill="none" stroke={method.fd2} strokeWidth={1.2} strokeDasharray="4 3" /> 0.5 * phi(x + ct), y0)} fill="none" stroke={method.boundary} strokeWidth={1.2} strokeDasharray="4 3" /> 0.5 * (phi(x - ct) + phi(x + ct)), y0)} fill="none" stroke={method.modal} strokeWidth={2.2} /> ) })}
) } /** Partials above the Nyquist frequency fold back below it. */ export function Folding() { const x0 = 50 const x1 = 670 const fs = 1 const f0 = fs / 7.35 const fmax = 1.02 * fs const sx = (f: number) => x0 + ((x1 - x0) * f) / fmax const base = 140 const h = 118 const parts = Array.from({ length: 7 }, (_, i) => i + 1) const amp = (n: number) => h / n const fold = (f: number) => { const r = f % fs return r > fs / 2 ? fs - r : r } const ay = base + 26 return (
{parts.map((n) => { const f = n * f0 const above = f > fs / 2 const g = fold(f) const rx = (sx(f) - sx(g)) / 2 return ( {above && ( <> )} ) })}
) } /** The leapfrog stencil in space and time, and the two domains of dependence. */ export function Stencil() { const gx = 60 const gy = 30 const ox = 90 const oy = 190 const node = (j: number, n: number): [number, number] => [ox + j * gx, oy - n * gy * 1.5] const lam = 0.6 const P = node(4, 3) const nodes: ReactElement[] = [] for (let n = 0; n <= 3; n++) { for (let j = 0; j <= 8; j++) { const [x, y] = node(j, n) nodes.push() } } const used: [number, number, string][] = [ [3, 2, method.fd2], [4, 2, method.fd2], [5, 2, method.fd2], [4, 1, method.fd2], ] const cone = (w: number) => pts([node(4 - 3 * w, 0), P, node(4 + 3 * w, 0)]) return (
{nodes} {used.map(([j, n, c]) => { const [x, y] = node(j, n) return ( ) })} {[0, 1, 2, 3].map((n) => ( ))}
) } /** Phase speed against wavenumber for several Courant numbers. */ export function Dispersion() { const x0 = 70 const x1 = 560 const y0 = 196 const y1 = 24 const vmin = 0.55 const sx = (t: number) => x0 + (x1 - x0) * t const sy = (v: number) => y0 - ((y0 - y1) * (v - vmin)) / (1 - vmin) const lams = [1, 0.75, 0.5, 0.25] const curve = (lam: number) => { const out: [number, number][] = [] for (let i = 1; i <= 200; i++) { const kh = (Math.PI * i) / 200 const r = (2 * Math.asin(lam * Math.sin(kh / 2))) / (lam * kh) out.push([sx(i / 200), sy(r)]) } return pts(out) } const ticks = [0.55, 0.7, 0.85, 1] const ppw = [20, 10, 5, 4, 3, 2] return (
{ticks.map((v) => ( ))} {ppw.map((P) => ( ))} {lams.map((l, k) => ( ))}
) } /** The Karplus-Strong loop as a block diagram. */ export function Loop() { const y = 90 const box = (x: number, w: number, label: string, sub: string, c: string) => ( ) return (
{box(124, 150, 'delay z⁻ᴾ', 'P whole samples', method.waveguide)} {box(314, 160, '½ (1 + z⁻¹)', 'loss, and ½ sample', method.fd2)} {box(514, 140, 'fractional delay', 'η of a sample', method.boundary)}
) }