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>
217 lines
10 KiB
JavaScript
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
|
|
];
|