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 @@ +DYNAMUSIUM · LIFE REACTIONBrusselatorMODEL-DERIVED STUDY · 1968A chemical clock in two variables \ 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 @@ +DYNAMUSIUM · EARTH CLIMATEBudyko–Sellers ClimateMODEL-DERIVED STUDY · 1969An ice line with a memory \ 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 @@ +DYNAMUSIUM · MATTER PATTERNCahn–Hilliard SeparationMODEL-DERIVED STUDY · 1958Domains coarsening through conservation \ 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 @@ +DYNAMUSIUM · EARTH CLIMATEThree-Box Carbon CycleMODEL-DERIVED STUDY · 1957Carbon moving between timescales \ 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 @@ +DYNAMUSIUM · EARTH CLIMATEDaisyworldMODEL-DERIVED STUDY · 1983Life bending a planet’s thermostat \ 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 @@ +DYNAMUSIUM · MOTION CHAOSDouble PendulumMODEL-DERIVED STUDY · 2006A hinge that forgets its future \ 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 @@ +DYNAMUSIUM · COSMOS GRAVITYExoplanet TransitMODEL-DERIVED STUDY · 1999A distant world measured as missing light \ 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 @@ +ABCcollectedDYNAMUSIUM · MATTER PATTERNFed Reaction ChainMODEL-DERIVED STUDY · 1864 / curated 2026Matter moving through delay \ 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 @@ +DYNAMUSIUM · LIFE REACTIONFitzHugh–NagumoMODEL-DERIVED STUDY · 1961–1962Excitation reduced to its silhouette \ 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 @@ +mode 1mode 2mode 3mode 4DYNAMUSIUM · MOTION CHAOSFPUT ChainMODEL-DERIVED STUDY · 1955The recurrence that surprised computation \ 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 @@ +DYNAMUSIUM · COSMOS GRAVITYFriedmann–Lemaître UniverseMODEL-DERIVED STUDY · 1922–1927The scale of space becoming time \ 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 @@ +DYNAMUSIUM · MATTER PATTERNGray–Scott PatternMODEL-DERIVED STUDY · 1983Spots born from reaction and diffusion \ 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 @@ +DYNAMUSIUM · MATTER PATTERNHeat / DiffusionMODEL-DERIVED STUDY · 1822Difference dissolving into distance \ 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 @@ +DYNAMUSIUM · LIFE REACTIONHodgkin–Huxley NeuronMODEL-DERIVED STUDY · 1952Voltage becoming an event \ 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 @@ +DYNAMUSIUM · COSMOS GRAVITYHohmann TransferMODEL-DERIVED STUDY · 1925The quiet economy of two burns \ 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 @@ +DYNAMUSIUM · MATTER PATTERNIsing ModelMODEL-DERIVED STUDY · 1925A phase transition made of choices \ 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 @@ +DYNAMUSIUM · COSMOS GRAVITYKepler OrbitMODEL-DERIVED STUDY · 1609Equal areas, unequal speeds \ 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 @@ +R = 0.992DYNAMUSIUM · MOTION CHAOSKuramoto OscillatorsMODEL-DERIVED STUDY · 1975Many clocks finding one pulse \ 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 @@ +DYNAMUSIUM · MOTION CHAOSLogistic MapMODEL-DERIVED STUDY · 1976Infinity folded into one line \ 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 @@ +DYNAMUSIUM · EARTH CLIMATELorenz AtmosphereMODEL-DERIVED STUDY · 1963Weather on a butterfly-shaped set \ 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 @@ +DYNAMUSIUM · LIFE REACTIONLotka–VolterraMODEL-DERIVED STUDY · 1925–1926A living orbit with no resting point \ 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 @@ +DYNAMUSIUM · COSMOS GRAVITYN-Body SystemMODEL-DERIVED STUDY · 1687Gravity writing with several hands \ 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 @@ +DYNAMUSIUM · LIFE REACTIONOregonatorMODEL-DERIVED STUDY · 1972The pulse inside an oscillating reaction \ 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 @@ +DYNAMUSIUM · COSMOS GRAVITYRestricted Three-BodyMODEL-DERIVED STUDY · 1772A small body inside two gravities \ 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 @@ +DYNAMUSIUM · MATTER PATTERNSchrödinger Wave PacketMODEL-DERIVED STUDY · 1926Probability moving as phase \ 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 @@ +DYNAMUSIUM · EARTH CLIMATEShallow-Water WavesMODEL-DERIVED STUDY · 1776An ocean compressed into a moving surface \ 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 @@ +DYNAMUSIUM · LIFE REACTIONSIR EpidemicMODEL-DERIVED STUDY · 1927An outbreak crossing its own threshold \ 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 @@ +DYNAMUSIUM · MOTION CHAOSStandard MapMODEL-DERIVED STUDY · 1969Islands of order in a chaotic sea \ 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 @@ +DYNAMUSIUM · EARTH CLIMATEStommel Ocean BoxMODEL-DERIVED STUDY · 1961Two stable oceans under one climate \ 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 @@ +DYNAMUSIUM · MOTION CHAOSWave EquationMODEL-DERIVED STUDY · 1747Standing waves between fixed edges \ 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 `${artwork}DYNAMUSIUM · ${esc(semantic.gallery.toUpperCase())}${esc(work.title)}MODEL-DERIVED STUDY · ${esc(work.year)}${esc(work.subtitle)}`; +} + +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 }) {