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 };
|
|||
|
|
}
|