Files
bubblechambersimart/src/field/wind.js

73 lines
3.1 KiB
JavaScript
Raw Normal View History

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