/* ============================================================ 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 }; }