Files
bubblechambersimart/tools/dunelake.mjs
noisedestroyers 63066ee533 Hidden Realities — Milkweed & shared wind field
First plate of the hidden-realities series: natural subjects as
instrument photographs. One invisible wind field, three detectors —
dunes (time-integrated, what the sand remembers), water (instantaneous),
milkweed seeds (advected test particles).

- src/field/wind.js: divergence-free curl-noise field with closed-form
  time integral; displacement / scalar / stepped-vec samplers.
- src/scene/umbel.js: milkweed head as a particle interaction vertex;
  burst releases pedicels to seed. src/scene/drift.js: seeds advected,
  same track contract as track.js.
- composition.js: switch -> GROUP_BUILDERS registry; fieldLake/flora/
  drift layers + wind plumbing. carpet.js/perspgrid.js: warpFn/phaseFn/
  modesFn/heightFn injection hooks (default off, no regression).
- schema + composer panels, dunelake ink palette, dune-lake template,
  tools/dunelake.mjs (17-plate matrix) and its curated SVG set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:23:55 -04:00

125 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================
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`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Dunes on a Great Lake — one wind, three detectors</title>
<style>html,body{margin:0;background:#15140f;color:#cabfa6;font:12px ui-monospace,monospace}h1{font-weight:400;letter-spacing:.2em;text-transform:uppercase;color:#9fb7af;padding:18px 14px 0}p.sub{color:#8a8068;padding:0 14px;margin:6px 0 0}.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:14px}figure{margin:0;position:relative;background:#fff;overflow:hidden}img{width:100%;display:block}figcaption{position:absolute;left:0;bottom:0;right:0;padding:6px 9px;font:12px ui-monospace,monospace;color:#fff;background:linear-gradient(transparent,#000d)}</style></head><body>
<h1>Dunes on a Great Lake with Milkweed (${V.length})</h1>
<p class="sub">one invisible wind field · three detectors — sand (integrated) · water (instantaneous) · seeds (advected)</p>
<div class="grid">${V.map(v => `<figure><img src="${v.name}.svg"><figcaption>${v.note}</figcaption></figure>`).join('')}</div></body></html>`);
console.log(`-> ${OUT}/matrix.html`);