// 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)}
)
}