/* ============================================================ dunelake.mjs — DUNES ON A GREAT LAKE WITH MILKWEED. First plate of the hidden-realities series: one invisible wind field made visible by three detectors — dunes (its time integral, what the sand remembers), water (its instantaneous trace), milkweed seeds (its advection). The umbel is a particle vertex grown botanically. A curated variation matrix in the liberation.mjs manner: worlds (seed × wind character) × horizons × milkweed states × palettes. Emits SVGs + exportable .mjs compositions + gallery. Usage: node tools/dunelake.mjs [size] ============================================================ */ import { writeFileSync, mkdirSync } from 'node:fs'; import { DEFAULT_COMPOSITION } from '../src/compose/schema.js'; import { renderComposition } from '../src/compose/composition.js'; const SIZE = +(process.argv[2] || 1300); const OUT = 'output/dunelake'; mkdirSync(OUT, { recursive: true }); const merge = (a, b) => { const o = { ...a }; for (const k in b) o[k] = (b[k] && typeof b[k] === 'object' && !Array.isArray(b[k])) ? merge(a[k] || {}, b[k]) : b[k]; return o; }; /* The landscape base: no collision event, no sun — the subjects are the field's three detectors. horizon = waterline (frame fraction). */ const base = (seed, windSeed, windStrength, horizon, over = {}) => { const hClip = horizon; // lake band ends where dunes begin const c = merge(JSON.parse(JSON.stringify(DEFAULT_COMPOSITION)), merge({ seed, wind: { seed: windSeed, strength: windStrength, scale: 1.6, gust: 0.4, time: 1.4 }, background: { color: 'rgb(226,216,192)', glow: { strength: 0.4, followSun: false }, vignette: { strength: 0.32, mode: 'radial', cy: 0.42, radius: 0.9 }, film: { opacity: 0.5 }, aging: { opacity: 0.35, scratches: 3 }, grain: { opacity: 0.38 }, }, // WATER: a flat far band, instantaneous wind, cool — clipped above the dune line fieldLake: { enabled: true, seed: 'LIMNOS-' + seed, color: { hueBack: 0.555, hueFront: 0.52, sat: 0.42, light: 0.52 }, layers: 2, chaos: 0.1, blips: 0.2, mound: 0, lines: 24, horizonY: Math.max(0.2, horizon - 0.16), wind: { warp: 0.55 }, clip: [0, Math.max(0, horizon - 0.2), 1, horizon + 0.02], }, // DUNES: the ridgeline carpet warped by the wind's time integral, warm sand fieldSea: { seed: 'AEOLIAN-' + seed, color: { hueBack: 0.09, hueFront: 0.07, sat: 0.42, light: 0.42 }, layers: 3, chaos: 0.22, blips: 0.35, mound: 0.55, horizonY: horizon, lines: 52, wind: { warp: 0.9 }, }, fieldGrid: { enabled: false }, disk: { enabled: false }, bubble: { enabled: false }, // MILKWEED: vertex-flowers on the foredune flora: { enabled: true, seed: 'ASCLEPIAS-' + seed, palette: 'dunelake', count: 3, scale: 0.42, pedicels: 46, droop: 0.4, burst: 0.25, clusterW: 0.7, clusterH: 0.3, transform: { x: -0.34, y: 0.42, rotation: 0, scale: 1 }, }, drift: { enabled: true, count: 28, slip: 0.4, flutter: 0.55, tuft: 2 }, fiduciaries: { enabled: true, label: 'ASCLEPIAS SYRIACA', arrow: true, target: [-0.34, 0.42], from: [0.35, -0.3], caption: 'wind · integrated | instantaneous | advected' }, }, over)); return c; }; // dusk: warm-on-cool — sand holds the late light, the water goes slate const dusk = { background: { color: 'rgb(210,190,170)', glow: { strength: 0.5 } }, fieldSea: { color: { hueBack: 0.05, hueFront: 0.95, sat: 0.5, light: 0.4 } }, fieldLake: { color: { hueBack: 0.62, hueFront: 0.58, sat: 0.5, light: 0.42 } }, }; // nocturne: the night-plate move — dark ground, pale silks (liberation #11) const nocturne = { background: { color: 'rgb(22,23,30)', glow: { strength: 0.3, followSun: false }, vignette: { strength: 0.2 }, film: { opacity: 0.28 }, grain: { opacity: 0.3 } }, fieldSea: { color: { hueBack: 0.6, hueFront: 0.08, sat: 0.3, light: 0.62 } }, fieldLake: { color: { hueBack: 0.58, hueFront: 0.56, sat: 0.4, light: 0.66 } }, fiduciaries: { pencil: '#cfc4a8' }, }; const W = [ { id: 'breeze', seed: 'ZEPHYR-011', strength: 0.55 }, { id: 'gale', seed: 'BOREAS-104', strength: 1.25 }, ]; const SEEDS = ['LAKEMICH-0610', 'SLEEPING-BEAR-22', 'CRITICALDUNE-7']; const V = []; for (const s of SEEDS) { for (const w of W) { const tag = `${s.split('-')[0].toLowerCase()}-${w.id}`; // big-sky vs big-water horizons V.push({ name: `${tag}_low-horizon`, note: `${w.id} · big sky, dunes low (${s})`, comp: base(s, w.seed, w.strength, 0.62) }); V.push({ name: `${tag}_high-water`, note: `${w.id} · big water, high dune line (${s})`, comp: base(s, w.seed, w.strength, 0.44, { fieldSea: { lines: 44 }, flora: { transform: { x: -0.3, y: 0.5 } }, fiduciaries: { target: [-0.3, 0.5] } }) }); } } // milkweed states — closed / bursting / full release streaming across the lake const sBurst = SEEDS[0], sFull = SEEDS[1]; V.push({ name: 'states_closed-left', note: 'umbels closed — the vertex before the decay', comp: base(sBurst, W[0].seed, W[0].strength, 0.56, { flora: { burst: 0, count: 2, transform: { x: -0.5, y: 0.38 } }, drift: { enabled: false }, fiduciaries: { target: [-0.5, 0.38], caption: 'pre-release · all florets held' } }) }); V.push({ name: 'states_bursting-right', note: 'right-third bursting — half the florets gone to silk', comp: base(sBurst, W[1].seed, W[1].strength, 0.56, { flora: { burst: 0.55, transform: { x: 0.42, y: 0.44 } }, drift: { count: 40 }, fiduciaries: { target: [0.42, 0.44], caption: 'release fraction 0.55' } }) }); V.push({ name: 'states_full-release', note: 'off-corner full burst — seeds streaming across the water', comp: base(sFull, W[1].seed, 1.4, 0.5, { flora: { burst: 1, count: 2, transform: { x: -0.78, y: 0.62 }, scale: 0.5 }, drift: { count: 64, slip: 0.5 }, fiduciaries: { target: [-0.78, 0.62], from: [0.4, -0.35], caption: 'all silk · the field made visible' } }) }); // palette rows over one world: day-sand (the base), dusk, nocturne V.push({ name: 'palette_dusk', note: 'dusk — sand holds the late light, slate water', comp: base(SEEDS[2], W[0].seed, 0.8, 0.55, dusk) }); V.push({ name: 'palette_nocturne', note: 'nocturne — dark plate, pale silks (night-plate move)', comp: base(SEEDS[2], W[1].seed, 1.1, 0.55, merge(nocturne, { flora: { burst: 0.6 }, drift: { count: 48 } })) }); console.log(`dunelake — ${V.length} compositions → ${OUT}/`); for (const v of V) { writeFileSync(`${OUT}/${v.name}.svg`, renderComposition(v.comp, SIZE)); writeFileSync(`${OUT}/${v.name}.mjs`, `/* dune-lake-milkweed · ${v.note} */\nexport const composition = ${JSON.stringify(v.comp, null, 2)};\n`); console.log(` ${v.name} — ${v.note}`); } writeFileSync(`${OUT}/matrix.html`, `
one invisible wind field · three detectors — sand (integrated) · water (instantaneous) · seeds (advected)