vortex.js
69 lines · in natura-src-1.0.0.zip as natura/vortex.js · a module · its entry in the module reference · its test: vortex-test.js
// vortex.js -- Natura VORTEX-BLOB FLUID (physical / micro-landscape): a Lagrangian fluid substrate. Instead of a
// grid, the flow is carried by a handful of VORTEX PARTICLES, each a little spin (circulation) in the fluid; each
// one whirls the fluid around it (smoothed 2D Biot-Savart), and so they advect EACH OTHER. Out of that fall the
// classic emergent motions: a counter-rotating PAIR self-propels in a straight line (a smoke ring's cross-section),
// like-signed vortices ORBIT one another, and circulation is conserved throughout. It gives a cheap VELOCITY FIELD
// other systems can drift smoke / fog / seeds / scent through. SYSTEMS.md #12 named "a smoke-ring (vortex-blob)"
// as a planned MICRO member; this builds it. From "It Carries Itself". Deterministic, node-testable. UMD.
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory();
else root.Vortex = factory();
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
// config: { vortices:[{x,y,gamma}], core, speed, dtInner, maxSub, secPerDay, name }
function makeVortex(config) {
config = config || {};
const core = config.core != null ? config.core : 0.08; // blob core radius (desingularizes Biot-Savart)
const core2 = core * core;
const speed = config.speed != null ? config.speed : 1.0; // overall time rate
const dtInner = config.dtInner != null ? config.dtInner : 0.01;
const maxSub = config.maxSub != null ? config.maxSub : 400;
const secPerDay = config.secPerDay != null ? config.secPerDay : 86400;
// velocity induced at (px,py) by all vortices (optionally skipping index `skip`)
function induce(vort, px, py, skip) {
let vx = 0, vy = 0;
for (let j = 0; j < vort.length; j++) { if (j === skip) continue; const o = vort[j];
const dx = px - o.x, dy = py - o.y, r2 = dx * dx + dy * dy + core2, f = o.gamma / (6.283185307 * r2);
vx += -f * dy; vy += f * dx; }
return [vx, vy];
}
return {
name: config.name || 'vortex',
domain: 'physical',
scale: 'micro',
params: config,
init() {
const src = config.vortices || [{ x: -0.5, y: 0, gamma: 1 }, { x: 0.5, y: 0, gamma: 1 }];
return { vort: src.map(v => ({ x: v.x, y: v.y, gamma: v.gamma })), t: 0 };
},
step(st, ctx, dt) {
const nsub = clamp(Math.round((dt / secPerDay) * speed / dtInner), 1, maxSub);
const vort = st.vort, vx = st._vx || (st._vx = new Float64Array(vort.length)), vy = st._vy || (st._vy = new Float64Array(vort.length));
for (let s = 0; s < nsub; s++) {
for (let i = 0; i < vort.length; i++) { const uv = induce(vort, vort[i].x, vort[i].y, i); vx[i] = uv[0]; vy[i] = uv[1]; }
for (let i = 0; i < vort.length; i++) { vort[i].x += vx[i] * dtInner; vort[i].y += vy[i] * dtInner; }
}
st.t += (dt / secPerDay) * speed;
return st;
},
// OUT: the vortices (x,y,gamma), the total circulation (an invariant), the pair centroid, and a `sampleAt`
// so other systems drift through the induced velocity field.
fields(st) {
const vort = st.vort; let g = 0, cx = 0, cy = 0, agsum = 0;
for (const v of vort) { g += v.gamma; const ag = Math.abs(v.gamma); cx += v.x * ag; cy += v.y * ag; agsum += ag; }
return { vortices: vort, circulation: +g.toFixed(6), absCirc: +agsum.toFixed(6),
centroid: [+(cx / (agsum || 1)).toFixed(4), +(cy / (agsum || 1)).toFixed(4)],
sampleAt: (px, py) => induce(vort, px, py, -1) };
},
events() { return []; }
};
}
return { makeVortex };
});