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:
2026-06-17 15:23:55 -04:00
parent e7fb2e9a68
commit 63066ee533
47 changed files with 5022 additions and 60 deletions

View File

@@ -8,6 +8,10 @@
============================================================ */
export const DEFAULT_COMPOSITION = {
size: 1600, seed: 'MESON-5113',
// ONE shared wind field (see src/field/wind.js). Consumed by fieldSea
// (time-integrated warp), fieldLake/fieldGrid (instantaneous ripple) and
// drift (seed advection). Groups opt in via their own `wind` sub-config.
wind: { seed: 'ZEPHYR-001', strength: 1, scale: 1.6, gust: 0.35, time: 1 },
background: {
color: 'rgb(229,222,203)',
film: { opacity: 0.6, density: 0.5, seed: 8 },
@@ -16,16 +20,24 @@ export const DEFAULT_COMPOSITION = {
glow: { strength: 0.55, followSun: true }, // lit-page glow; centre tracks the disk
vignette: { strength: 0.4, mode: 'radial', cx: 0.5, cy: 0.48, radius: 0.75, angle: 90, start: 0.5 },
},
fieldLake: {
enabled: false, layerOpacity: 1, transform: { x: 0, y: 0, rotation: 0, scale: 1 }, seed: 'LIMNOS-001',
color: { hueBack: 0.55, hueFront: 0.56, sat: 0.45, light: 0.5 },
layers: 2, chaos: 0.12, blips: 0.25, mound: 0, horizonY: 0.36, lines: 26,
wind: { warp: 0.5 }, // instantaneous — the water shows the wind live
},
fieldSea: {
enabled: true, layerOpacity: 1, transform: { x: 0, y: 0, rotation: 0, scale: 1 }, seed: 'VACUUM-5113',
color: { hueBack: 0.54, hueFront: 0.47, sat: 0.6, light: 0.36 }, // light > ~0.6 → waves lighter than ground
layers: 3, chaos: 0.3, blips: 0.7, mound: 0.3, horizonY: 0.36, lines: 46,
wind: { warp: 0 }, // time-integrated — what the sand remembers
},
fieldGrid: {
enabled: true, layerOpacity: 1, pos: 'over', style: 'floor', transform: { x: 0, y: 0, rotation: 0, scale: 1 },
color: { hue: 0.5, hue2: 0.55, sat: 0.32, lightNear: 0.34, lightFar: 0.62 }, opacity: 0.44,
pitch: 0.45, yaw: 0.42, persp: 1.1, dist: 2.8, nx: 16, nz: 24, originY: 0.34, ripple: { amp: 0, freqI: 0.5, freqK: 0.35, phase: 0 },
resonance: { amp: 0, q: 14, axis: 'ties', hue: 0.06, sat: 0.7, light: 0.45 },
wind: { ripple: 0 }, // instantaneous wind as floor height
},
disk: {
enabled: true, layerOpacity: 1, transform: { x: 0, y: -0.26, rotation: 0, scale: 0.78 },
@@ -37,6 +49,16 @@ export const DEFAULT_COMPOSITION = {
primaries: 18, sweepers: 5, cosmics: 6, eloss: 0.34, bfield: 1.0, deltaRate: 0.6,
showBoundary: true, boundaryR: 0.45, boundaryY: 0.35, boundaryOpacity: 0.4, instrument: 0.35,
},
flora: {
enabled: false, layerOpacity: 1, transform: { x: 0, y: 0, rotation: 0, scale: 1 },
palette: 'dunelake', saturation: 1.05, count: 1, scale: 0.5,
pedicels: 48, droop: 0.35, spread: 1, burst: 0, floretSize: 1,
clusterW: 0.9, clusterH: 0.5, // placement spread when count > 1 (no explicit umbels[])
},
drift: {
enabled: false, layerOpacity: 1,
palette: 'dunelake', count: 24, slip: 0.35, flutter: 0.5, settle: 0.06, tuft: 2,
},
fiduciaries: {
enabled: true, layerOpacity: 1, label: 'auto', pencil: '#39312a', width: 1.0, arrow: true, corners: false, caption: '',
},
@@ -66,6 +88,21 @@ export const GROUPS_SCHEMA = [
R('vignette.angle', 'Vignette angle°', 0, 360, 1),
],
},
{
id: 'fieldLake', label: 'Field · Lake', transform: true, enable: true, controls: [
R('color.hueBack', 'Hue · far', 0, 1, 0.005),
R('color.hueFront', 'Hue · near', 0, 1, 0.005),
R('color.sat', 'Saturation', 0, 1, 0.01),
R('color.light', 'Wave luminosity', 0, 1, 0.01),
R('layers', 'Plate layers', 1, 3, 1),
R('chaos', 'Chaos', 0, 1, 0.01),
R('blips', 'Blips', 0, 2, 0.05),
R('horizonY', 'Horizon Y', 0.2, 0.6, 0.01),
R('lines', '# lines', 8, 80, 1),
R('wind.warp', 'Wind ripple', 0, 2, 0.02),
{ path: 'seed', label: 'Seed', type: 'text' },
],
},
{
id: 'fieldSea', label: 'Field · Sea', transform: true, enable: true, controls: [
R('color.hueBack', 'Hue · far', 0, 1, 0.005),
@@ -78,6 +115,7 @@ export const GROUPS_SCHEMA = [
R('mound', 'Mound', 0, 1, 0.01),
R('horizonY', 'Horizon Y', 0.2, 0.6, 0.01),
R('lines', '# lines', 16, 80, 1),
R('wind.warp', 'Wind warp (dune memory)', 0, 2, 0.02),
{ path: 'seed', label: 'Seed', type: 'text' },
],
},
@@ -96,6 +134,7 @@ export const GROUPS_SCHEMA = [
R('color.hue', 'Hue', 0, 1, 0.005),
R('color.sat', 'Saturation', 0, 1, 0.01),
R('ripple.amp', 'Floor ripple', 0, 2, 0.02),
R('wind.ripple', 'Wind ripple', 0, 2, 0.02),
R('resonance.amp', 'Resonance (high-Q)', 0, 1, 0.01),
R('resonance.q', 'Resonance Q', 2, 40, 1),
R('resonance.hue', 'Resonance hue', 0, 1, 0.005),
@@ -131,6 +170,30 @@ export const GROUPS_SCHEMA = [
R('instrument', 'Structural geometry', 0, 1, 0.01),
],
},
{
id: 'flora', label: 'Flora · milkweed', transform: true, enable: true, controls: [
{ path: 'palette', label: 'Palette', type: 'select', options: ['dunelake', 'magentarise', 'mono', 'kind', 'kindrise', 'kindlife', 'lifecycle'] },
R('saturation', 'Saturation', 0, 1.5, 0.01),
R('count', 'Umbels', 1, 7, 1),
R('scale', 'Head size', 0.15, 1.2, 0.01),
R('pedicels', 'Pedicels', 8, 80, 1),
R('droop', 'Droop', 0, 1, 0.01),
R('spread', 'Fan looseness', 0, 2, 0.01),
R('burst', 'Burst (gone to seed)', 0, 1, 0.01),
R('floretSize', 'Floret size', 0.3, 2.5, 0.01),
{ path: 'seed', label: 'Seed', type: 'text' },
],
},
{
id: 'drift', label: 'Drift · seeds', enable: true, controls: [
R('count', 'Seeds (cap)', 0, 80, 1),
R('slip', 'Launch slip', 0, 1, 0.01),
R('flutter', 'Coma flutter', 0, 1.5, 0.01),
R('settle', 'Settle speed', 0, 0.3, 0.005),
R('tuft', 'Tuft silks', 0, 5, 1),
R('layerOpacity', 'Opacity', 0, 1, 0.01),
],
},
{
id: 'fiduciaries', label: 'Fiduciaries', transform: true, enable: true, controls: [
{ path: 'label', label: 'Label', type: 'text' },