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>
This commit is contained in:
124
tools/dunelake.mjs
Normal file
124
tools/dunelake.mjs
Normal file
@@ -0,0 +1,124 @@
|
||||
/* ============================================================
|
||||
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`);
|
||||
Reference in New Issue
Block a user