// Colours, assigned by the job each one does. // // The set is the reference data-viz palette for a dark surface (#1a1a19), // taken unchanged from the sibling laplace-dirichlet tutorial, where it was // validated as a set: // // node scripts/validate_palette.js \ // "#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9,#e66767" --mode dark // lightness band PASS chroma floor PASS CVD separation PASS (worst 8.4) // normal-vision floor PASS (worst 19.3) contrast vs surface PASS // // Only the assignment of hues to meanings is new here; the particle-filters // tutorial reuses the same eight hexes with its own assignment. The worst adjacent // pair sits in the 6-8 CVD band's neighbourhood, so every plot that shows more // than two series also carries a legend, a distinct marker shape per series, // and direct end-labels: identity is never colour alone. If you add a series, // re-run the validator rather than picking a hex that looks nice. export const surface = '#1a1a19' /** * Identity: which method produced a curve or a sound. Fixed slots, never * cycled and never reassigned when a plot shows a subset. */ export const method = { /** second-order leapfrog finite differences, the workhorse */ fd2: '#3987e5', /** fourth-order finite differences, in space or in space and time */ fd4: '#d95926', /** modal synthesis and spectral methods: a sum of exact standing waves */ modal: '#199e70', /** delay lines: the digital waveguide and Karplus-Strong */ waveguide: '#c98500', /** boundary treatments: cut-cell stencils, absorbing layers, image sources */ boundary: '#d55181', /** the exact answer when it is drawn as a series rather than as reference */ exact: '#9085e9', /** a spare slot, for the extra series a single chapter needs */ alt: '#e66767', } as const export type MethodKey = keyof typeof method /** Marker shapes, so that identity survives greyscale and colour blindness. */ export const marker: Record = { fd2: 'circle', fd4: 'square', modal: 'triangle', waveguide: 'diamond', boundary: 'cross', exact: 'plus', alt: 'star', } /** State. Reserved: never reused as a series colour. */ export const status = { good: '#008300', warning: '#c98500', critical: '#e66767', } as const /** Sequential ramp for magnitude (an energy, a spectrum level): one hue. */ export const sequential = [ '#cde2fb', '#b7d3f6', '#9ec5f4', '#86b6ef', '#6da7ec', '#5598e7', '#3987e5', '#2a78d6', '#256abf', '#1c5cab', '#184f95', '#104281', '#0d366b', ] as const /** Diverging pair for a signed field: two poles, neutral grey midpoint. */ export const diverging = { low: '#3987e5', mid: '#383835', high: '#e66767', } as const /** Chart chrome and ink. Text always wears these, never a series colour. */ export const ink = { primary: '#e8e6df', secondary: '#c3c2b7', muted: '#8a8880', grid: '#272724', axis: '#3c3c38', /** the exact solution: the reference everything is measured against */ reference: '#8a8880', /** grid nodes, receivers, individual sample points */ node: '#e8e6df', /** a family of curves drawn as a mass: many modes, many images */ family: '#4a4a45', } as const function hexToRgb(hex: string): [number, number, number] { const v = parseInt(hex.slice(1), 16) return [(v >> 16) & 255, (v >> 8) & 255, v & 255] } function rgbToHex(r: number, g: number, b: number): string { const c = (x: number) => Math.max(0, Math.min(255, Math.round(x))).toString(16).padStart(2, '0') return `#${c(r)}${c(g)}${c(b)}` } function mix(a: string, b: string, t: number): string { const [r1, g1, b1] = hexToRgb(a) const [r2, g2, b2] = hexToRgb(b) return rgbToHex(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t) } /** Sequential colour for t in [0, 1]: light at 0 (near zero), dark at 1. */ export function seqColor(t: number): string { if (!isFinite(t)) return ink.grid const u = Math.max(0, Math.min(1, t)) * (sequential.length - 1) const i = Math.floor(u) return i >= sequential.length - 1 ? sequential[sequential.length - 1] : mix(sequential[i], sequential[i + 1], u - i) } /** Diverging colour for t in [-1, 1]: blue below, grey at zero, red above. */ export function divColor(t: number): string { if (!isFinite(t)) return ink.grid const u = Math.max(-1, Math.min(1, t)) return u < 0 ? mix(diverging.mid, diverging.low, -u) : mix(diverging.mid, diverging.high, u) } /** Mix a colour towards the surface, for a recessive version of a series. */ export function faded(c: string, t = 0.55): string { return mix(c, surface, t) } /** * One method at several settings of a parameter (a Courant number, a grid * size): a family rather than several identities, so it is drawn in shades of * one hue, light to dark in the order of the parameter, and every member also * carries its own marker shape. Only the lighter two-thirds of the * sequential ramp is used, because its dark end sinks into this page's * surface. */ export function familyColor(k: number, n: number): string { const t = n <= 1 ? 0.3 : 0.05 + (0.62 * k) / (n - 1) return seqColor(t) } export const familyShapes = ['circle', 'square', 'triangle', 'diamond', 'star', 'plus', 'cross'] as const