Files
bubblechambersimart/src/compose/schema.js
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

217 lines
10 KiB
JavaScript

/* ============================================================
compose/schema.js — UI control descriptors for the composer, and
the DEFAULT composition (two-point floor grid). Data only (no DOM).
Each group: { id, label, transform?, enable?, controls:[ {path,label,type,...} ] }
path = dotted path INTO the group's config. types:
range {min,max,step} · color (#hex) · select {options} · toggle · text
Groups with transform:true get an auto x/y/rotation/scale block.
============================================================ */
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 },
aging: { opacity: 0.5, scratches: 5, dust: 0.45, foxing: 0.5, seed: 5 },
grain: { opacity: 0.42, intensity: 0.42, seed: 19 },
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 },
hue: 0.06, sat: 0.82, size: 0.16, pressure: 0.85, intensity: 0.85, striations: 0.6, stain: 0.35, soften: 1.35,
},
bubble: {
enabled: true, layerOpacity: 1, transform: { x: 0.28, y: -0.1, rotation: 10, scale: 0.78 },
palette: 'magentarise', saturation: 1.05, traceHue: 0, transparentBase: true,
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: '',
},
};
const R = (path, label, min, max, step) => ({ path, label, type: 'range', min, max, step });
export const GROUPS_SCHEMA = [
{
id: 'background', label: 'Background', controls: [
{ path: 'color', label: 'Paper colour', type: 'color' },
R('film.opacity', 'Film opacity', 0, 1, 0.01),
R('film.density', 'Film density', 0, 1, 0.01),
R('aging.opacity', 'Aging opacity', 0, 1, 0.01),
R('aging.scratches', 'Scratches', 0, 20, 1),
R('aging.foxing', 'Foxing', 0, 1, 0.01),
R('aging.dust', 'Dust', 0, 1, 0.01),
R('grain.opacity', 'Grain opacity', 0, 1, 0.01),
R('grain.intensity', 'Grain intensity', 0, 1, 0.01),
R('glow.strength', 'Page glow', 0, 1, 0.01),
{ path: 'glow.followSun', label: 'Glow follows sun', type: 'toggle' },
R('vignette.strength', 'Vignette', 0, 1, 0.01),
{ path: 'vignette.mode', label: 'Vignette mode', type: 'select', options: ['radial', 'linear'] },
R('vignette.cx', 'Vignette X', 0, 1, 0.01),
R('vignette.cy', 'Vignette Y', 0, 1, 0.01),
R('vignette.radius', 'Vignette radius', 0.3, 1.4, 0.01),
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),
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('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' },
],
},
{
id: 'fieldGrid', label: 'Field · Grid', transform: true, enable: true, controls: [
{ path: 'style', label: 'Style', type: 'select', options: ['floor', 'radial'] },
{ path: 'pos', label: 'Stack position', type: 'select', options: ['over', 'behind'] },
R('opacity', 'Opacity', 0, 1, 0.01),
R('pitch', 'Pitch', 0, 1.4, 0.01),
R('yaw', 'Yaw · two-point', -1, 1, 0.01),
R('persp', 'Perspective', 0, 2, 0.01),
R('dist', 'Camera distance', 1.2, 6, 0.05),
R('nx', 'Width lines', 4, 36, 1),
R('nz', 'Depth lines', 4, 48, 1),
R('originY', 'Horizon Y', -0.4, 0.6, 0.01),
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),
],
},
{
id: 'disk', label: 'Disk · sun', transform: true, enable: true, controls: [
R('hue', 'Hue', 0, 1, 0.005),
R('sat', 'Saturation', 0, 1, 0.01),
R('size', 'Size', 0.05, 0.4, 0.005),
R('pressure', 'Pressure · dark core', 0, 1, 0.01),
R('intensity', 'Intensity', 0, 1, 0.01),
R('striations', 'Striations', 0, 1, 0.01),
R('stain', 'Staining', 0, 1, 0.01),
],
},
{
id: 'bubble', label: 'Bubble · event', transform: true, enable: true, controls: [
{ path: 'palette', label: 'Palette', type: 'select', options: ['magentarise', 'mono', 'kind', 'kindrise', 'kindlife', 'charge', 'beta', 'lifecycle', 'cyanotype'] },
{ path: 'transparentBase', label: 'Transparent base', type: 'toggle' },
R('saturation', 'Saturation', 0, 1.5, 0.01),
R('traceHue', 'Trace hue', 0, 1, 0.005),
R('primaries', 'Primaries', 3, 40, 1),
R('sweepers', 'Sweepers · arcs', 0, 12, 1),
R('cosmics', 'Cosmics', 0, 16, 1),
R('eloss', 'Energy loss', 0, 1.5, 0.01),
R('bfield', 'B-field', 0.2, 3, 0.01),
R('deltaRate', 'δ-ray rate', 0, 1, 0.01),
{ path: 'showBoundary', label: 'Chamber arc', type: 'toggle' },
R('boundaryR', 'Arc radius', 0.1, 1.0, 0.01),
R('boundaryY', 'Arc position Y', -0.3, 0.7, 0.01),
R('boundaryOpacity', 'Arc opacity', 0, 1, 0.01),
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' },
{ path: 'pencil', label: 'Pencil', type: 'color' },
R('width', 'Line width', 0.3, 3, 0.1),
{ path: 'arrow', label: 'Arrow', type: 'toggle' },
{ path: 'corners', label: 'Registration corners', type: 'toggle' },
{ path: 'caption', label: 'Caption', type: 'text' },
],
},
];
// the common transform block, generated for groups with transform:true
export const COMMON_CONTROLS = [
R('layerOpacity', 'Opacity', 0, 1, 0.01),
R('transform.x', 'Centre X', -1.6, 1.6, 0.01), // beyond ±1 → bleed off-canvas
R('transform.y', 'Centre Y', -1.6, 1.6, 0.01),
R('transform.rotation', 'Rotation°', -180, 180, 1),
R('transform.scale', 'Scale', 0.1, 3, 0.01), // small ↔ overfill / eclipse
];