diff --git a/package.json b/package.json
index 13fba23..88e6c40 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"work:validate": "node scripts/validate-works.mjs",
"work:preview": "vite",
"work:capture": "node scripts/capture-screens.mjs",
+ "work:capture-previews": "node scripts/capture-collection-previews.mjs",
"cite:verify": "node scripts/verify-citations.mjs"
},
"dependencies": {
diff --git a/public/collection-preview/brusselator.svg b/public/collection-preview/brusselator.svg
new file mode 100644
index 0000000..f377031
--- /dev/null
+++ b/public/collection-preview/brusselator.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/budyko-sellers.svg b/public/collection-preview/budyko-sellers.svg
new file mode 100644
index 0000000..85f4845
--- /dev/null
+++ b/public/collection-preview/budyko-sellers.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/cahn-hilliard.svg b/public/collection-preview/cahn-hilliard.svg
new file mode 100644
index 0000000..c5d353a
--- /dev/null
+++ b/public/collection-preview/cahn-hilliard.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/carbon-cycle.svg b/public/collection-preview/carbon-cycle.svg
new file mode 100644
index 0000000..a066b03
--- /dev/null
+++ b/public/collection-preview/carbon-cycle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/daisyworld.svg b/public/collection-preview/daisyworld.svg
new file mode 100644
index 0000000..b7002e1
--- /dev/null
+++ b/public/collection-preview/daisyworld.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/double-pendulum.svg b/public/collection-preview/double-pendulum.svg
new file mode 100644
index 0000000..fcfec18
--- /dev/null
+++ b/public/collection-preview/double-pendulum.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/exoplanet-transit.svg b/public/collection-preview/exoplanet-transit.svg
new file mode 100644
index 0000000..eafb197
--- /dev/null
+++ b/public/collection-preview/exoplanet-transit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/fed-reaction-chain.svg b/public/collection-preview/fed-reaction-chain.svg
new file mode 100644
index 0000000..d0869a9
--- /dev/null
+++ b/public/collection-preview/fed-reaction-chain.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/fitzhugh-nagumo.svg b/public/collection-preview/fitzhugh-nagumo.svg
new file mode 100644
index 0000000..5bdc227
--- /dev/null
+++ b/public/collection-preview/fitzhugh-nagumo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/fput-chain.svg b/public/collection-preview/fput-chain.svg
new file mode 100644
index 0000000..7fab16f
--- /dev/null
+++ b/public/collection-preview/fput-chain.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/friedmann-lemaitre.svg b/public/collection-preview/friedmann-lemaitre.svg
new file mode 100644
index 0000000..5c85e98
--- /dev/null
+++ b/public/collection-preview/friedmann-lemaitre.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/gray-scott.svg b/public/collection-preview/gray-scott.svg
new file mode 100644
index 0000000..e32aa7b
--- /dev/null
+++ b/public/collection-preview/gray-scott.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/heat-diffusion.svg b/public/collection-preview/heat-diffusion.svg
new file mode 100644
index 0000000..a038c36
--- /dev/null
+++ b/public/collection-preview/heat-diffusion.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/hodgkin-huxley.svg b/public/collection-preview/hodgkin-huxley.svg
new file mode 100644
index 0000000..1cbae0c
--- /dev/null
+++ b/public/collection-preview/hodgkin-huxley.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/hohmann-transfer.svg b/public/collection-preview/hohmann-transfer.svg
new file mode 100644
index 0000000..8ff3b64
--- /dev/null
+++ b/public/collection-preview/hohmann-transfer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/ising-model.svg b/public/collection-preview/ising-model.svg
new file mode 100644
index 0000000..e66c989
--- /dev/null
+++ b/public/collection-preview/ising-model.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/kepler-orbit.svg b/public/collection-preview/kepler-orbit.svg
new file mode 100644
index 0000000..385b890
--- /dev/null
+++ b/public/collection-preview/kepler-orbit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/kuramoto-oscillators.svg b/public/collection-preview/kuramoto-oscillators.svg
new file mode 100644
index 0000000..260e068
--- /dev/null
+++ b/public/collection-preview/kuramoto-oscillators.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/logistic-map.svg b/public/collection-preview/logistic-map.svg
new file mode 100644
index 0000000..1e71058
--- /dev/null
+++ b/public/collection-preview/logistic-map.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/lorenz-atmosphere.svg b/public/collection-preview/lorenz-atmosphere.svg
new file mode 100644
index 0000000..deceac5
--- /dev/null
+++ b/public/collection-preview/lorenz-atmosphere.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/lotka-volterra.svg b/public/collection-preview/lotka-volterra.svg
new file mode 100644
index 0000000..edea18a
--- /dev/null
+++ b/public/collection-preview/lotka-volterra.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/n-body-system.svg b/public/collection-preview/n-body-system.svg
new file mode 100644
index 0000000..9b72c82
--- /dev/null
+++ b/public/collection-preview/n-body-system.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/oregonator.svg b/public/collection-preview/oregonator.svg
new file mode 100644
index 0000000..9f01a11
--- /dev/null
+++ b/public/collection-preview/oregonator.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/restricted-three-body.svg b/public/collection-preview/restricted-three-body.svg
new file mode 100644
index 0000000..c49dcb7
--- /dev/null
+++ b/public/collection-preview/restricted-three-body.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/schrodinger-wave-packet.svg b/public/collection-preview/schrodinger-wave-packet.svg
new file mode 100644
index 0000000..a677bc0
--- /dev/null
+++ b/public/collection-preview/schrodinger-wave-packet.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/shallow-water.svg b/public/collection-preview/shallow-water.svg
new file mode 100644
index 0000000..c35e171
--- /dev/null
+++ b/public/collection-preview/shallow-water.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/sir-epidemic.svg b/public/collection-preview/sir-epidemic.svg
new file mode 100644
index 0000000..4ebbdff
--- /dev/null
+++ b/public/collection-preview/sir-epidemic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/standard-map.svg b/public/collection-preview/standard-map.svg
new file mode 100644
index 0000000..20a96ef
--- /dev/null
+++ b/public/collection-preview/standard-map.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/stommel-box.svg b/public/collection-preview/stommel-box.svg
new file mode 100644
index 0000000..f41e40d
--- /dev/null
+++ b/public/collection-preview/stommel-box.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/collection-preview/wave-equation.svg b/public/collection-preview/wave-equation.svg
new file mode 100644
index 0000000..92a62fa
--- /dev/null
+++ b/public/collection-preview/wave-equation.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scripts/capture-collection-previews.mjs b/scripts/capture-collection-previews.mjs
new file mode 100644
index 0000000..be3eaa3
--- /dev/null
+++ b/scripts/capture-collection-previews.mjs
@@ -0,0 +1,336 @@
+import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { deflateSync } from 'node:zlib';
+import { createServer } from 'vite';
+
+const OUT = 'public/collection-preview';
+const STAGE = await mkdtemp(join(tmpdir(), 'dynamusium-previews-'));
+const PALETTE = {
+ 'motion-chaos': '#81e5fa',
+ 'matter-pattern': '#d0a5ff',
+ 'life-reaction': '#ffa778',
+ 'earth-climate': '#a9e6a2',
+ 'cosmos-gravity': '#ffd27d',
+};
+const esc = (text) =>
+ String(text)
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+ .replaceAll("'", ''');
+const point = (x, y) => `${x.toFixed(2)},${y.toFixed(2)}`;
+const chartBox = { x: 100, y: 110, width: 1000, height: 405 };
+
+function pngChunk(type, data) {
+ const name = Buffer.from(type);
+ const length = Buffer.alloc(4);
+ length.writeUInt32BE(data.length);
+ const body = Buffer.concat([name, data]);
+ const crc = Buffer.alloc(4);
+ crc.writeUInt32BE(crc32(body));
+ return Buffer.concat([length, body, crc]);
+}
+
+const crcTable = Uint32Array.from({ length: 256 }, (_, index) => {
+ let value = index;
+ for (let bit = 0; bit < 8; bit += 1) value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
+ return value >>> 0;
+});
+
+function crc32(bytes) {
+ let value = 0xffffffff;
+ for (const byte of bytes) value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8);
+ return (value ^ 0xffffffff) >>> 0;
+}
+
+function encodePng(width, height, pixels) {
+ const header = Buffer.alloc(13);
+ header.writeUInt32BE(width, 0);
+ header.writeUInt32BE(height, 4);
+ header[8] = 8;
+ header[9] = 6;
+ const rows = Buffer.alloc(height * (width * 4 + 1));
+ for (let row = 0; row < height; row += 1) {
+ const offset = row * (width * 4 + 1);
+ rows[offset] = 0;
+ pixels.copy(rows, offset + 1, row * width * 4, (row + 1) * width * 4);
+ }
+ return Buffer.concat([
+ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
+ pngChunk('IHDR', header),
+ pngChunk('IDAT', deflateSync(rows, { level: 9 })),
+ pngChunk('IEND', Buffer.alloc(0)),
+ ]);
+}
+
+function fieldColor(normalized) {
+ const value = Math.max(0, Math.min(1, normalized));
+ const fraction = value < 0.5 ? value * 2 : (value - 0.5) * 2;
+ const from = value < 0.5 ? [7, 15, 35] : [56, 55, 122];
+ const to = value < 0.5 ? [56, 55, 122] : [205, 244, 255];
+ return from.map((channel, index) => Math.round(channel + (to[index] - channel) * fraction));
+}
+
+function normalizedBaseline(binding, encodeNumericValue) {
+ return binding.zero === undefined
+ ? 0
+ : encodeNumericValue(binding.zero, { ...binding, zero: undefined }).normalized;
+}
+
+function fieldArtwork(work, result, { requireVisualBinding, encodeNumericValue }) {
+ const field = result.field;
+ const componentId = field.componentId ?? 'scalar';
+ const binding = requireVisualBinding(work, componentId, 'luminance');
+ const frame = result.numerical?.fieldFrames?.at(-1);
+ const values = frame?.components[componentId] ?? field.values;
+ if (values.length !== field.columns * field.rows) {
+ throw new Error(`${work.slug}: field sample count does not match its reviewed grid.`);
+ }
+ if (field.columns === 1 || field.rows === 1) {
+ const count = values.length;
+ const vertical = field.columns === 1;
+ return values
+ .map((value, index) => {
+ const [red, green, blue] = fieldColor(encodeNumericValue(value, binding).normalized);
+ const x = vertical ? 582 : 100 + (index / count) * 1000;
+ const y = vertical ? 110 + (index / count) * 405 : 302;
+ const width = vertical ? 36 : 1000 / count + 0.5;
+ const height = vertical ? 405 / count + 0.5 : 42;
+ return ``;
+ })
+ .join('');
+ }
+ const pixels = Buffer.alloc(values.length * 4);
+ values.forEach((value, index) => {
+ const [red, green, blue] = fieldColor(encodeNumericValue(value, binding).normalized);
+ pixels[index * 4] = red;
+ pixels[index * 4 + 1] = green;
+ pixels[index * 4 + 2] = blue;
+ pixels[index * 4 + 3] = 255;
+ });
+ const image = encodePng(field.columns, field.rows, pixels).toString('base64');
+ return ``;
+}
+
+function pathArtwork(
+ work,
+ result,
+ semantic,
+ {
+ visualLayers,
+ numericDomain,
+ encodeNumericValue,
+ decimateTrajectoryPoints,
+ visibleTrajectoryPath,
+ },
+) {
+ const layers = visualLayers(work).filter(
+ (layer) =>
+ layer.mark === 'path' &&
+ layer.bindings.some((binding) => binding.channel === 'position-x') &&
+ layer.bindings.some((binding) => binding.channel === 'position-y'),
+ );
+ const layer = layers[0];
+ const xBinding = layer?.bindings.find((binding) => binding.channel === 'position-x');
+ const yBinding = layer?.bindings.find((binding) => binding.channel === 'position-y');
+ const xSeries = xBinding && result.series.find((series) => series.id === xBinding.quantityRef);
+ const ySeries = yBinding && result.series.find((series) => series.id === yBinding.quantityRef);
+ if (!layer || !xBinding || !yBinding || !xSeries || !ySeries) {
+ throw new Error(`${work.slug}: no complete reviewed path is available for its preview.`);
+ }
+ const [xMin, xMax] = numericDomain(xBinding);
+ const [yMin, yMax] = numericDomain(yBinding);
+ const xSpan = xMax - xMin;
+ const ySpan = yMax - yMin;
+ const preserveUnits = layer.projection?.aspect !== 'declared-distortion';
+ const scale = preserveUnits ? Math.min(chartBox.width / xSpan, chartBox.height / ySpan) : null;
+ const width = scale === null ? chartBox.width : xSpan * scale;
+ const height = scale === null ? chartBox.height : ySpan * scale;
+ const left = chartBox.x + (chartBox.width - width) / 2;
+ const top = chartBox.y + (chartBox.height - height) / 2;
+ const screenPoints = xSeries.values.flatMap((x, index) => {
+ const y = ySeries.values[index];
+ if (x === undefined || y === undefined) return [];
+ const encodedX = encodeNumericValue(x, xBinding);
+ const encodedY = encodeNumericValue(y, yBinding);
+ return [
+ {
+ x: left + encodedX.normalized * width,
+ y: top + (1 - encodedY.normalized) * height,
+ outsideDomain: encodedX.outsideDomain || encodedY.outsideDomain,
+ },
+ ];
+ });
+ const path = visibleTrajectoryPath(decimateTrajectoryPoints(screenPoints, 1600));
+ return ``;
+}
+
+function snapshotIndex(result) {
+ return Math.round((result.times.length - 1) * 0.72);
+}
+
+function valueAt(result, id, index) {
+ const series = result.series.find((entry) => entry.id === id);
+ return series?.values[index];
+}
+
+function oscillatorArtwork(
+ work,
+ result,
+ semantic,
+ { encodeNumericValue, findVisualBinding, normalizedZero, signedFromBaseline },
+) {
+ const state = result.numerical?.state;
+ if (!state || state.shape[1] !== 12)
+ throw new Error(`${work.slug}: oscillator state is missing.`);
+ const index = snapshotIndex(result);
+ const cx = 600;
+ const cy = 314;
+ const radius = 194;
+ const phases = Array.from(
+ { length: 12 },
+ (_, oscillator) => state.values[index * 12 + oscillator],
+ );
+ const dots = phases
+ .map((phase, oscillator) => {
+ const binding = findVisualBinding(work, `theta-${oscillator + 1}`, 'phase');
+ if (phase === undefined || !binding)
+ throw new Error(`${work.slug}: phase mapping is missing.`);
+ const angle = encodeNumericValue(phase, binding).normalized * 2 * Math.PI;
+ const x = cx + radius * Math.cos(angle);
+ const y = cy - radius * Math.sin(angle);
+ return ``;
+ })
+ .join('');
+ const orderX = valueAt(result, 'order-real', index);
+ const orderY = valueAt(result, 'order-imaginary', index);
+ const coherence = valueAt(result, 'coherence', index);
+ const xBinding = findVisualBinding(work, 'order-real', 'position-x');
+ const yBinding = findVisualBinding(work, 'order-imaginary', 'position-y');
+ const cBinding = findVisualBinding(work, 'coherence', 'area');
+ if (
+ orderX === undefined ||
+ orderY === undefined ||
+ coherence === undefined ||
+ !xBinding ||
+ !yBinding ||
+ !cBinding
+ ) {
+ throw new Error(`${work.slug}: order-vector mapping is incomplete.`);
+ }
+ const normX = signedFromBaseline(
+ encodeNumericValue(orderX, xBinding).normalized,
+ normalizedZero(xBinding) ?? 0.5,
+ );
+ const normY = signedFromBaseline(
+ encodeNumericValue(orderY, yBinding).normalized,
+ normalizedZero(yBinding) ?? 0.5,
+ );
+ const cNorm = encodeNumericValue(coherence, cBinding).normalized;
+ return `${dots}R = ${coherence.toFixed(3)}`;
+}
+
+function energyArtwork(work, result, semantic, { encodeNumericValue, findVisualBinding }) {
+ const index = snapshotIndex(result);
+ return [1, 2, 3, 4]
+ .map((mode, ordinal) => {
+ const id = `mode-${mode}-harmonic-energy`;
+ const value = valueAt(result, id, index);
+ const binding = findVisualBinding(work, id, 'area');
+ if (value === undefined || !binding)
+ throw new Error(`${work.slug}: modal energy is missing.`);
+ const baseline = normalizedBaseline(binding, encodeNumericValue);
+ const height = Math.abs(encodeNumericValue(value, binding).normalized - baseline) * 280;
+ const x = 218 + ordinal * 255;
+ return `mode ${mode}`;
+ })
+ .join('');
+}
+
+function reactionArtwork(work, result, semantic, { encodeNumericValue, findVisualBinding }) {
+ const index = snapshotIndex(result);
+ const quantities = ['a', 'b', 'c', 'collected'];
+ const fluxes = ['a-to-b-flux', 'b-to-c-flux', 'c-to-collected-flux'];
+ const xs = [205, 465, 725, 985];
+ const vessels = quantities
+ .map((id, position) => {
+ const value = valueAt(result, id, index);
+ const binding = findVisualBinding(work, id, 'area');
+ if (value === undefined || !binding)
+ throw new Error(`${work.slug}: quantity mapping is missing.`);
+ const fill =
+ Math.abs(
+ encodeNumericValue(value, binding).normalized -
+ normalizedBaseline(binding, encodeNumericValue),
+ ) * 220;
+ const x = xs[position];
+ return `${id === 'collected' ? 'collected' : id.toUpperCase()}`;
+ })
+ .join('');
+ const channels = fluxes
+ .map((id, indexInChain) => {
+ const value = valueAt(result, id, index);
+ const binding = findVisualBinding(work, id, 'stroke-width');
+ if (value === undefined || !binding)
+ throw new Error(`${work.slug}: flux mapping is missing.`);
+ const width =
+ 2 +
+ Math.abs(
+ encodeNumericValue(value, binding).normalized -
+ normalizedBaseline(binding, encodeNumericValue),
+ ) *
+ 13;
+ const x1 = xs[indexInChain] + 55;
+ const x2 = xs[indexInChain + 1] - 55;
+ const particle = (x1 + x2) / 2;
+ return ``;
+ })
+ .join('');
+ return `${vessels}${channels}`;
+}
+
+function makeSvg(work, result, semantic, helpers) {
+ let artwork;
+ if (result.field) artwork = fieldArtwork(work, result, helpers);
+ else if (work.kernel === 'kuramoto') artwork = oscillatorArtwork(work, result, semantic, helpers);
+ else if (work.kernel === 'fput') artwork = energyArtwork(work, result, semantic, helpers);
+ else if (work.kernel === 'reaction-chain')
+ artwork = reactionArtwork(work, result, semantic, helpers);
+ else artwork = pathArtwork(work, result, semantic, helpers);
+ return ``;
+}
+
+const server = await createServer({
+ server: { middlewareMode: true },
+ appType: 'custom',
+ logLevel: 'error',
+});
+try {
+ const [{ works }, { simulateWork }, semantic, trajectory] = await Promise.all([
+ server.ssrLoadModule('/src/museum/catalog.ts'),
+ server.ssrLoadModule('/src/museum/simulation.ts'),
+ server.ssrLoadModule('/src/museum/semantic-visual.ts'),
+ server.ssrLoadModule('/src/museum/trajectory-path.ts'),
+ ]);
+ for (const work of works) {
+ const result = simulateWork(work, {});
+ const accent = PALETTE[work.gallery];
+ if (!accent) throw new Error(`${work.slug}: no curated gallery color exists.`);
+ const svg = makeSvg(
+ work,
+ result,
+ { accent, gallery: work.gallery.replaceAll('-', ' ') },
+ { ...semantic, ...trajectory },
+ );
+ await writeFile(join(STAGE, `${work.slug}.svg`), svg);
+ console.log(`captured ${OUT}/${work.slug}.svg`);
+ }
+} finally {
+ await server.close();
+}
+await rm(OUT, { recursive: true, force: true });
+await mkdir(OUT, { recursive: true });
+await cp(STAGE, OUT, { recursive: true });
+await rm(STAGE, { recursive: true, force: true });
diff --git a/src/museum/MuseumApp.tsx b/src/museum/MuseumApp.tsx
index 0d9c1ac..127cf70 100644
--- a/src/museum/MuseumApp.tsx
+++ b/src/museum/MuseumApp.tsx
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState, type CSSProperties } from 'react';
import { galleries, workBySlug, works } from './catalog.ts';
+import { learningGuides } from './learning-guides.ts';
import type { WorkRunResult } from './portrait-types.ts';
import type { GalleryId, MuseumMode, Series, WorkManifest, WorkResult } from './types.ts';
import { useWorkSimulation } from './useWorkSimulation.ts';
@@ -11,11 +12,14 @@ import {
numericDomain,
normalizedZero,
requireVisualBinding,
+ signedFromBaseline,
visualLayers,
} from './semantic-visual.ts';
import { overflowExcursionStarts, visibleTrajectoryPath } from './trajectory-path.ts';
const flagshipWorks = works.filter((work) => work.tier === 'flagship');
+const collectionPreviewUrl = (slug: string) =>
+ `${import.meta.env.BASE_URL}collection-preview/${slug}.svg`;
function readRoute() {
const query = new URLSearchParams(window.location.search);
@@ -85,6 +89,13 @@ function Entrance({ onSelect }: { onSelect: (slug: string) => void }) {
+
An archive of systems that move, grow, synchronize, and transform
Enter the living
@@ -92,8 +103,8 @@ function Entrance({ onSelect }: { onSelect: (slug: string) => void }) {
mathematics of nature.
- Thirty landmark models from motion, matter, life, Earth, and the cosmos—presented as
- instruments to observe, not diagrams to merely read.
+ Thirty mathematical models from motion, matter, life, Earth, and the cosmos—each one
+ something to observe, test, and understand.