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>
73 lines
3.1 KiB
JavaScript
73 lines
3.1 KiB
JavaScript
/* ============================================================
|
||
field/wind.js — the SHARED WIND FIELD: one invisible field,
|
||
three detectors. The same seeded flow is sampled three ways:
|
||
|
||
displacement(x,y,T) time-integrated — what the SAND remembers
|
||
(warps the dune ridgelines)
|
||
vec(x,y,t) / scalar instantaneous — what the WATER shows
|
||
(drives lake ripple phase)
|
||
vec stepped advected — what the SEEDS trace
|
||
(milkweed drift = test particles)
|
||
|
||
Divergence-free curl noise from a streamfunction ψ(x,y,t) built
|
||
as a sum of K seeded sinusoidal modes plus a mean drift, so the
|
||
velocity has a closed-form time integral (no numerics, fully
|
||
deterministic). Coordinates are frame-normalized −1..1; typical
|
||
|vec| ≈ strength.
|
||
============================================================ */
|
||
import { makeRng, range } from '../rng.js';
|
||
|
||
export function makeWindField(seed, opts = {}) {
|
||
const o = Object.assign({
|
||
modes: 6, scale: 1.6, strength: 1,
|
||
drift: [0.6, 0.05], // prevailing wind (mean velocity, frame units)
|
||
gust: 0.35, // how much the modes stir vs. the mean drift
|
||
}, opts);
|
||
const rng = makeRng(seed, 'wind');
|
||
const modes = [];
|
||
for (let m = 0; m < o.modes; m++) {
|
||
const ang = range(rng, 0, Math.PI * 2);
|
||
const k = range(rng, 0.6, 2.2) * o.scale; // low wavenumber: long swells
|
||
modes.push({
|
||
kx: Math.cos(ang) * k, ky: Math.sin(ang) * k,
|
||
a: range(rng, 0.5, 1) / k, // energy at the large scales
|
||
w: range(rng, 0.4, 1.4), // temporal frequency
|
||
phi: range(rng, 0, Math.PI * 2),
|
||
});
|
||
}
|
||
const norm = modes.reduce((s, m) => s + m.a * Math.hypot(m.kx, m.ky), 0) || 1;
|
||
const g = o.gust * o.strength / norm; // mode velocity normaliser
|
||
const dx0 = o.drift[0] * o.strength, dy0 = o.drift[1] * o.strength;
|
||
|
||
// instantaneous velocity: u = ∂ψ/∂y, v = −∂ψ/∂x (divergence-free) + drift
|
||
const vec = (x, y, t = 0) => {
|
||
let u = 0, v = 0;
|
||
for (const m of modes) {
|
||
const c = m.a * Math.cos(m.kx * x + m.ky * y + m.w * t + m.phi);
|
||
u += m.ky * c; v -= m.kx * c;
|
||
}
|
||
return { u: u * g * modes.length + dx0, v: v * g * modes.length + dy0 };
|
||
};
|
||
|
||
// Eulerian time integral 0..T of the velocity at a fixed point — the
|
||
// closed-form "memory" of the wind (dune displacement field)
|
||
const displacement = (x, y, T = 1) => {
|
||
let dx = 0, dy = 0;
|
||
for (const m of modes) {
|
||
const p = m.kx * x + m.ky * y + m.phi;
|
||
const s = m.a * (Math.sin(p + m.w * T) - Math.sin(p)) / m.w;
|
||
dx += m.ky * s; dy -= m.kx * s;
|
||
}
|
||
return { dx: dx * g * modes.length + dx0 * T, dy: dy * g * modes.length + dy0 * T };
|
||
};
|
||
|
||
// the streamfunction itself — a scalar to phase ripples with
|
||
const scalar = (x, y, t = 0) => {
|
||
let s = 0;
|
||
for (const m of modes) s += m.a * Math.sin(m.kx * x + m.ky * y + m.w * t + m.phi);
|
||
return s * o.strength / (modes.reduce((q, m) => q + m.a, 0) || 1);
|
||
};
|
||
|
||
return { vec, displacement, scalar };
|
||
}
|