/ concept-collection / turing-surface
Sign in
concept-collection / turing-surface
90 lines · 3.7 KBCodeBlameHistory
0af3386Reaction-diffusion on spherical-harmonic surfacesJeremy Magland 1import type { ColormapFunc } from './colormaps.ts';
3/** Compact numeric label: 3 significant digits, trailing zeros trimmed. */
4export const fmtValue = (v: number): string =>
5 Number.isFinite(v) ? v.toPrecision(3).replace(/\.?0+$/, '') : '—';
8 * Smallest span the colormap may be stretched across, relative to the field's
9 * own magnitude.
10 *
11 * Set from measurement, not taste. A constant field analysed and re-synthesized
12 * in fp32 comes back constant only to
13 *
14 * lmax 63: 2.9e-5 relative lmax 127: 9.6e-5 lmax 255: 2.4e-4
15 *
16 * and the residue is not white noise — it is concentrated in a few rings at the
17 * poles (74x the equatorial level at lmax 63, 1710x at lmax 255), because what
18 * survives the analysis is high-degree m = 0 content whose Legendre functions
19 * all peak at the poles *and add in phase there*. The same round trip in f64 is
20 * 2.4e-8 and flat, so this is fp32, not the algorithm.
21 *
22 * A floor of 1e-2 puts the worst of that (about 5e-4 of span at lmax 255) into
23 * roughly 5% of the colormap rather than all of it, while the variation these
24 * models actually carry — a few percent of the field's magnitude and up — is
25 * left alone entirely.
26 */
27const RANGE_FLOOR_REL = 1e-2;
28/** And an absolute floor, for a field whose magnitude is itself near zero. */
29const RANGE_FLOOR_ABS = 1e-9;
31/**
32 * Widen a value range so that a field which is uniform to numerical precision
33 * is drawn as uniform.
34 *
35 * Scaling the colormap to a field's own extremes gives full contrast to
36 * whatever variation it has — including none. Schnakenberg's `v` at t = 0 is
37 * literally constant (`vs * ones(...)`), so its extremes are set purely by the
38 * roundoff described above; painting that across the whole colormap produces a
39 * vivid pole-capped picture that reads as structure, and since the residue
40 * belongs to the grid, two runs at different lmax produce two entirely
41 * different pictures of the same constant — which looks exactly like a broken
42 * initial condition, and is not one.
43 *
44 * A floor rather than an "is this field constant?" test, so nothing ever jumps:
45 * a real pattern growing up through the floor hands the range over from the
46 * floor to its own data gradually, and once it is any larger than roundoff the
47 * floor has no effect at all.
48 */
49export function floorRange(lo: number, hi: number): { lo: number; hi: number } {
50 const minSpan = Math.max(
51 RANGE_FLOOR_ABS,
52 RANGE_FLOOR_REL * Math.max(Math.abs(lo), Math.abs(hi)),
53 );
54 if (hi - lo >= minSpan) return { lo, hi };
55 const mid = (lo + hi) / 2;
56 return { lo: mid - minSpan / 2, hi: mid + minSpan / 2 };
0af3386Reaction-diffusion on spherical-harmonic surfacesJeremy Magland 59/** Vertical colorbar drawn on a small canvas, with min/max labels. */
60export class Colorbar {
61 #canvas: HTMLCanvasElement;
62 #minLabel: HTMLElement;
63 #maxLabel: HTMLElement;
65 constructor(container: HTMLElement) {
66 container.classList.add('colorbar');
67 this.#maxLabel = document.createElement('div');
68 this.#maxLabel.className = 'colorbar-label';
69 this.#canvas = document.createElement('canvas');
70 this.#canvas.width = 12;
71 this.#canvas.height = 160;
72 this.#minLabel = document.createElement('div');
73 this.#minLabel.className = 'colorbar-label';
74 container.append(this.#maxLabel, this.#canvas, this.#minLabel);
75 }
77 update(cmap: ColormapFunc, vmin: number, vmax: number): void {
78 const ctx = this.#canvas.getContext('2d');
79 if (!ctx) return;
80 const h = this.#canvas.height;
81 for (let y = 0; y < h; y++) {
82 const t = 1 - y / (h - 1);
83 const [r, g, b] = cmap(t);
84 ctx.fillStyle = `rgb(${r},${g},${b})`;
85 ctx.fillRect(0, y, this.#canvas.width, 1);
86 }
87 this.#maxLabel.textContent = fmtValue(vmax);
88 this.#minLabel.textContent = fmtValue(vmin);
89 }
moveopenescclose