d8098edAn interactive tutorial on the wave equation in acoustics: strings, drums and rooms, measured by earJeremy Magland 1// Static figures: hand-drawn SVG, no data and no session behind them.
2//
3// A lab shows what a method does once the reader presses run; these show what
4// it *is*, on first sight and in print. They are deliberately plain: the same
5// palette as the plots, unicode rather than KaTeX for the few symbols that
6// appear, and one idea per picture.
7import type { ReactNode } from 'react'
8import { ink } from '../plot/palette.ts'
10export const SANS = 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif'
11export const MONO = 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
13/** Wrapper: a captioned, width-filling SVG with a fixed aspect ratio. */
14export function Figure({
15 width,
16 height,
17 caption,
18 maxWidth,
19 children,
20}: {
21 width: number
22 height: number
23 caption?: string
24 /** in px; the figure never grows past this even in a wide column */
25 maxWidth?: number
26 children: ReactNode
27}) {
28 return (
29 <figure className="static-figure" style={maxWidth ? { maxWidth } : undefined}>
30 <svg
31 viewBox={`0 0 ${width} ${height}`}
32 width="100%"
33 preserveAspectRatio="xMidYMid meet"
34 role="img"
35 aria-label={caption ?? 'diagram'}
36 style={{ display: 'block', overflow: 'visible' }}
37 >
38 {children}
39 </svg>
40 {caption ? <figcaption>{caption}</figcaption> : null}
41 </figure>
42 )
43}
45/** Arrowhead markers, one set per figure so the ids never collide. */
46export function Arrowheads({ id, colors }: { id: string; colors: string[] }) {
47 return (
48 <defs>
49 {colors.map((c, i) => (
50 <marker
51 key={i}
52 id={`${id}-ah-${i}`}
53 viewBox="0 0 8 8"
54 refX="7"
55 refY="4"
56 markerWidth="6"
57 markerHeight="6"
58 orient="auto-start-reverse"
59 >
60 <path d="M 0 0.7 L 8 4 L 0 7.3 z" fill={c} />
61 </marker>
62 ))}
63 </defs>
64 )
65}
67export const arrow = (id: string, i: number) => `url(#${id}-ah-${i})`
69/** A text label. `anchor` and `baseline` are the usual SVG values. */
70export function Label({
71 x,
72 y,
73 children,
74 fill = ink.secondary,
75 size = 12,
76 anchor = 'middle',
77 baseline = 'middle',
78 italic = false,
79 mono = false,
80 weight,
81}: {
82 x: number
83 y: number
84 children: ReactNode
85 fill?: string
86 size?: number
87 anchor?: 'start' | 'middle' | 'end'
88 baseline?: 'middle' | 'hanging' | 'auto' | 'central'
89 italic?: boolean
90 mono?: boolean
91 weight?: number
92}) {
93 return (
94 <text
95 x={x}
96 y={y}
97 fill={fill}
98 fontSize={size}
99 fontFamily={mono ? MONO : SANS}
100 fontStyle={italic ? 'italic' : undefined}
101 fontWeight={weight}
102 textAnchor={anchor}
103 dominantBaseline={baseline}
104 >
105 {children}
106 </text>
107 )
108}
110/** A grid node. */
111export function Node({ x, y, r = 3.2, fill = ink.node, stroke }: { x: number; y: number; r?: number; fill?: string; stroke?: string }) {
112 return <circle cx={x} cy={y} r={r} fill={fill} stroke={stroke} strokeWidth={stroke ? 1.2 : undefined} />
113}