Port fluid modes Fuoco/Anello/Vortici/Onda from the Mac app
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e0e5377fa1
commit
be3387871e
2 files changed
+141
-35
No files matched your search
Vendored
+11
-4
@@ -34,7 +34,13 @@ const FAMILIES = [
|
||||
{ name: 'Reattivo Bassi', scale: 1.0, bgDark: true },
|
||||
{ name: 'Reattivo Medi', scale: 1.0, bgDark: true },
|
||||
{ name: 'Reattivo Alti', scale: 1.0, bgDark: true },
|
||||
{ name: 'Fluido', scale: 1.0, fluid: true } // GPU fluid sim (fluid.js), not the uber-shader
|
||||
// GPU fluid sim families (fluid.js), not the uber-shader; fluidMode picks
|
||||
// the emitter choreography over the same solver.
|
||||
{ name: 'Fluido', scale: 1.0, fluid: true },
|
||||
{ name: 'Fluido Fuoco', scale: 1.0, fluid: true, fluidMode: 'fire' },
|
||||
{ name: 'Fluido Anello', scale: 1.0, fluid: true, fluidMode: 'ring' },
|
||||
{ name: 'Fluido Vortici', scale: 1.0, fluid: true, fluidMode: 'vortex' },
|
||||
{ name: 'Fluido Onda', scale: 1.0, fluid: true, fluidMode: 'wave' }
|
||||
];
|
||||
|
||||
// Palettes: low colour (a) -> high colour (b), plus optional hue-cycle/sat.
|
||||
@@ -90,9 +96,10 @@ function makeEffect(fi, pi, vi) {
|
||||
colorA: pal.a,
|
||||
colorB: pal.b,
|
||||
bgDark: fam.bgDark ? 1 : 0,
|
||||
// Always present (0/1) so Object.assign merges in setEffect can't leave a
|
||||
// stale flag behind when switching between fluid and shader presets.
|
||||
isFluid: fam.fluid ? 1 : 0
|
||||
// Always present (0/1 and a mode string) so Object.assign merges in
|
||||
// setEffect can't leave a stale flag behind when switching presets.
|
||||
isFluid: fam.fluid ? 1 : 0,
|
||||
fluidMode: fam.fluidMode || 'ink'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+130
-31
@@ -68,6 +68,7 @@ class FluidSim {
|
||||
this.pDiv = this._program(FluidSim.DIV);
|
||||
this.pPress = this._program(FluidSim.PRESS);
|
||||
this.pGrad = this._program(FluidSim.GRAD);
|
||||
this.pBuoy = this._program(FluidSim.BUOY);
|
||||
this.pShow = this._program(FluidSim.SHOW);
|
||||
}
|
||||
|
||||
@@ -167,8 +168,98 @@ class FluidSim {
|
||||
gl.disable(gl.BLEND);
|
||||
|
||||
// -- audio-driven splats --------------------------------------------------
|
||||
// Each fluidMode is a different emitter choreography over the same solver:
|
||||
// 'ink' wandering brushes, 'fire' bottom jets + buoyancy, 'ring' rotating
|
||||
// circle with radial beat bursts, 'vortex' two counter-rotating stirrers,
|
||||
// 'wave' a curtain sweeping across the screen.
|
||||
const hueShift = (e.hueBase || 0) + t * (e.hueCycle || 0) * 0.5;
|
||||
const sat = e.sat !== undefined ? e.sat : 1;
|
||||
const mode = e.fluidMode || 'ink';
|
||||
// Palette colour helper: A<->B oscillation, hue cycling, then normalized
|
||||
// to full brightness (palette 'a' colours are near-black backgrounds in
|
||||
// the shader families; the palette identity lives in the hue).
|
||||
const dyeCol = (ph) => {
|
||||
const k = 0.5 + 0.5 * Math.sin(t * 0.21 + ph * Math.PI * 2);
|
||||
let col = [
|
||||
(e.colorA[0] * (1 - k) + e.colorB[0] * k),
|
||||
(e.colorA[1] * (1 - k) + e.colorB[1] * k),
|
||||
(e.colorA[2] * (1 - k) + e.colorB[2] * k)];
|
||||
col = FluidSim._hue(col, hueShift + ph * 0.16, sat);
|
||||
const mx = Math.max(col[0], col[1], col[2], 1e-4);
|
||||
return [col[0] / mx, col[1] / mx, col[2] / mx];
|
||||
};
|
||||
// Injection is balanced against the dye dissipation below: too much and
|
||||
// the whole screen saturates to a solid colour, too little and it fades.
|
||||
const force = (14 + 320 * bass) * dt * 60;
|
||||
const inj = (0.5 + 2.8 * bass) * dtReal;
|
||||
const rad = 0.0009 + 0.0013 * bass;
|
||||
this.beatCool -= dt;
|
||||
const onBeat = beat > 0.85 && this.beatCool <= 0;
|
||||
if (onBeat) this.beatCool = 0.18;
|
||||
|
||||
if (mode === 'fire') {
|
||||
// flickering upward jets along the bottom edge (buoyancy pass below)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const bx = 0.5 + (i - 1) * 0.24 + 0.05 * Math.sin(t * (1.3 + i * 0.37) + i * 2.1);
|
||||
const flick = 0.6 + 0.4 * Math.sin(t * (5.1 + i) + i * 13.7);
|
||||
const c = dyeCol(i / 3);
|
||||
this._splat(bx, 0.04, 0, force * 0.22 * (0.7 + 0.8 * flick),
|
||||
c[0] * inj * 2.2, c[1] * inj * 2.2, c[2] * inj * 2.2, rad * 1.2);
|
||||
}
|
||||
if (onBeat) {
|
||||
const c = dyeCol(0.5);
|
||||
this._splat(0.5, 0.06, 0, 1400 * (0.4 + bass), c[0] * 0.8, c[1] * 0.8, c[2] * 0.8, 0.006);
|
||||
}
|
||||
} else if (mode === 'ring') {
|
||||
// emitters on a slowly spinning circle, stirring tangentially
|
||||
const N = 6, R = 0.27, spin = t * 0.5;
|
||||
for (let i = 0; i < N; i++) {
|
||||
const a = spin + i * Math.PI * 2 / N;
|
||||
const c = dyeCol(i / N);
|
||||
this._splat(0.5 + R * Math.cos(a), 0.5 + R * Math.sin(a),
|
||||
-Math.sin(a) * force, Math.cos(a) * force, c[0] * inj, c[1] * inj, c[2] * inj, rad);
|
||||
}
|
||||
if (onBeat) { // radial shockwave from the centre
|
||||
const c = dyeCol(Math.sin(t * 3.1) * 0.5 + 0.5);
|
||||
for (let i = 0; i < N; i++) {
|
||||
const a = spin + (i + 0.5) * Math.PI * 2 / N, F = 1100 * (0.4 + bass);
|
||||
this._splat(0.5 + 0.06 * Math.cos(a), 0.5 + 0.06 * Math.sin(a),
|
||||
Math.cos(a) * F, Math.sin(a) * F, c[0] * 0.35, c[1] * 0.35, c[2] * 0.35, 0.0025);
|
||||
}
|
||||
}
|
||||
} else if (mode === 'vortex') {
|
||||
// two counter-rotating stirrers -> double spiral galaxies
|
||||
for (let s = 0; s < 2; s++) {
|
||||
const sgn = s ? -1 : 1;
|
||||
const cx = 0.5 + sgn * (0.20 + 0.05 * Math.sin(t * 0.4));
|
||||
const cy = 0.5 + 0.08 * Math.sin(t * 0.31 + s * 2.6);
|
||||
const a = t * 1.6 * sgn + s * Math.PI, R = 0.11;
|
||||
const c = dyeCol(s * 0.5 + 0.1);
|
||||
this._splat(cx + R * Math.cos(a), cy + R * Math.sin(a),
|
||||
-Math.sin(a) * sgn * force * 1.3, Math.cos(a) * sgn * force * 1.3,
|
||||
c[0] * inj * 1.4, c[1] * inj * 1.4, c[2] * inj * 1.4, rad);
|
||||
}
|
||||
if (onBeat) { // bright puff where the spirals meet
|
||||
const c = dyeCol(0.8);
|
||||
this._splat(0.5, 0.5, 0, 0, c[0] * 0.5, c[1] * 0.5, c[2] * 0.5, 0.004);
|
||||
}
|
||||
} else if (mode === 'wave') {
|
||||
// travelling sine curtain sweeping rightwards
|
||||
const N = 4;
|
||||
for (let i = 0; i < N; i++) {
|
||||
const x = (t * 0.07 + i / N) % 1;
|
||||
const ph = t * 0.9 + i * 1.7 + x * 6.28;
|
||||
const c = dyeCol(i / N);
|
||||
this._splat(x, 0.5 + 0.28 * Math.sin(ph),
|
||||
force * 0.6, Math.cos(ph) * force * 0.35, c[0] * inj * 1.6, c[1] * inj * 1.6, c[2] * inj * 1.6, rad * 1.3);
|
||||
}
|
||||
if (onBeat) { // side blast riding the wave
|
||||
const c = dyeCol(0.3);
|
||||
this._splat(0.06, 0.5 + 0.3 * Math.sin(t * 5.3), 1000 * (0.4 + bass), 0,
|
||||
c[0] * 0.5, c[1] * 0.5, c[2] * 0.5, 0.0018);
|
||||
}
|
||||
} else {
|
||||
// 'ink': wandering brushes (the original mode)
|
||||
for (let i = 0; i < this.emitters.length; i++) {
|
||||
const em = this.emitters[i];
|
||||
const x = 0.5 + 0.36 * Math.sin(t * em.fx + em.phase) * Math.cos(t * 0.09 + em.phase);
|
||||
@@ -178,41 +269,33 @@ class FluidSim {
|
||||
const dx = (0.5 + 0.36 * Math.sin((t + h) * em.fx + em.phase) * Math.cos((t + h) * 0.09 + em.phase)) - x;
|
||||
const dy = (0.5 + 0.36 * Math.sin((t + h) * em.fy + em.phase * 1.7)) - y;
|
||||
const inv = 1 / Math.max(1e-5, Math.hypot(dx, dy));
|
||||
// colour: palette A<->B per emitter, slow oscillation + hue cycling
|
||||
const k = 0.5 + 0.5 * Math.sin(t * 0.21 + em.hue * Math.PI * 2);
|
||||
let col = [
|
||||
(e.colorA[0] * (1 - k) + e.colorB[0] * k),
|
||||
(e.colorA[1] * (1 - k) + e.colorB[1] * k),
|
||||
(e.colorA[2] * (1 - k) + e.colorB[2] * k)];
|
||||
col = FluidSim._hue(col, hueShift + em.hue * 0.16, sat);
|
||||
// Normalize to full brightness: palette 'a' colours are near-black
|
||||
// backgrounds in the shader families, but dye must always be vivid —
|
||||
// the palette identity lives in the hue, not the luminance.
|
||||
const mx = Math.max(col[0], col[1], col[2], 1e-4);
|
||||
col = [col[0] / mx, col[1] / mx, col[2] / mx];
|
||||
// continuous stir: quiet baseline so it never freezes, boosted by bass.
|
||||
// Injection is balanced against the dye dissipation below: too much and
|
||||
// the whole screen saturates to a solid colour, too little and it fades.
|
||||
const force = (14 + 320 * bass) * dt * 60;
|
||||
const inj = (0.5 + 2.8 * bass) * dtReal;
|
||||
const c = dyeCol(em.hue);
|
||||
this._splat(x, y, dx * inv * force, dy * inv * force,
|
||||
col[0] * inj, col[1] * inj, col[2] * inj, 0.0009 + 0.0013 * bass);
|
||||
c[0] * inj, c[1] * inj, c[2] * inj, rad);
|
||||
}
|
||||
// beat burst: one bright directional shot from a random edge point
|
||||
this.beatCool -= dt;
|
||||
if (beat > 0.85 && this.beatCool <= 0) {
|
||||
this.beatCool = 0.18;
|
||||
if (onBeat) { // bright directional shot from a pseudo-random edge point
|
||||
const a = Math.sin(t * 37.7) * Math.PI * 2;
|
||||
const x = 0.5 + 0.30 * Math.cos(a), y = 0.5 + 0.30 * Math.sin(a);
|
||||
let col = FluidSim._hue([e.colorB[0], e.colorB[1], e.colorB[2]], hueShift, sat);
|
||||
const c = dyeCol(0.0);
|
||||
const F = 900 * (0.4 + bass);
|
||||
this._splat(x, y, -Math.cos(a) * F, -Math.sin(a) * F,
|
||||
col[0] * 0.7, col[1] * 0.7, col[2] * 0.7, 0.003 + 0.003 * bass);
|
||||
this._splat(0.5 + 0.30 * Math.cos(a), 0.5 + 0.30 * Math.sin(a),
|
||||
-Math.cos(a) * F, -Math.sin(a) * F, c[0] * 0.7, c[1] * 0.7, c[2] * 0.7, 0.003 + 0.003 * bass);
|
||||
}
|
||||
}
|
||||
|
||||
// -- solver ---------------------------------------------------------------
|
||||
const v = this.vel;
|
||||
let p = this.pCurl;
|
||||
let p;
|
||||
if (mode === 'fire') {
|
||||
// buoyancy: hot (bright) dye rises, like smoke over a flame
|
||||
p = this.pBuoy;
|
||||
gl.useProgram(p);
|
||||
gl.uniform1i(p._u.uVel, this._bind(p, 0, v.a.tex));
|
||||
gl.uniform1i(p._u.uDye, this._bind(p, 1, this.dye.a.tex));
|
||||
gl.uniform1f(p._u.uK, 170);
|
||||
gl.uniform1f(p._u.uDt, dt);
|
||||
this._blit(v.b, p); v.swap();
|
||||
}
|
||||
p = this.pCurl;
|
||||
gl.useProgram(p);
|
||||
gl.uniform2f(p._u.uTexel, v.a.texelX, v.a.texelY);
|
||||
gl.uniform1i(p._u.uVel, this._bind(p, 0, v.a.tex));
|
||||
@@ -255,12 +338,14 @@ class FluidSim {
|
||||
gl.uniform1f(p._u.uDt, dt);
|
||||
gl.uniform1i(p._u.uVel, this._bind(p, 0, v.a.tex));
|
||||
gl.uniform1i(p._u.uSrc, this._bind(p, 1, v.a.tex));
|
||||
gl.uniform1f(p._u.uDiss, 0.25);
|
||||
gl.uniform1f(p._u.uDiss, mode === 'fire' ? 1.1 : 0.25);
|
||||
gl.uniform1f(p._u.uHeightK, 0.0);
|
||||
this._blit(v.b, p); v.swap();
|
||||
|
||||
gl.uniform1i(p._u.uVel, this._bind(p, 0, v.a.tex));
|
||||
gl.uniform1i(p._u.uSrc, this._bind(p, 1, this.dye.a.tex));
|
||||
gl.uniform1f(p._u.uDiss, 0.6 / (e.speed || 1)); // dye must fade or constant injection saturates; speed-normalized
|
||||
gl.uniform1f(p._u.uDiss, (mode === 'fire' ? 1.0 : 0.6) / (e.speed || 1)); // fire: short-lived flames; others: slow fade
|
||||
gl.uniform1f(p._u.uHeightK, mode === 'fire' ? 8.0 : 0.0);
|
||||
this._blit(this.dye.b, p); this.dye.swap();
|
||||
|
||||
// -- display --------------------------------------------------------------
|
||||
@@ -289,10 +374,11 @@ void main(){ vUv = aPos*0.5+0.5; gl_Position = vec4(aPos,0.,1.); }`;
|
||||
|
||||
FluidSim.ADVECT = `#version 300 es
|
||||
precision highp float; in vec2 vUv; out vec4 o;
|
||||
uniform sampler2D uVel, uSrc; uniform vec2 uTexel; uniform float uDt, uDiss;
|
||||
uniform sampler2D uVel, uSrc; uniform vec2 uTexel; uniform float uDt, uDiss, uHeightK;
|
||||
void main(){
|
||||
vec2 pos = vUv - uDt * texture(uVel, vUv).xy * uTexel;
|
||||
o = clamp(texture(uSrc, pos) / (1.0 + uDiss * uDt), -1200., 1200.); // NaN/Inf guard
|
||||
float diss = uDiss * (1.0 + uHeightK * vUv.y); // fire mode: fade with altitude
|
||||
o = clamp(texture(uSrc, pos) / (1.0 + diss * uDt), -1200., 1200.); // NaN/Inf guard
|
||||
}`;
|
||||
|
||||
FluidSim.SPLAT = `#version 300 es
|
||||
@@ -365,6 +451,19 @@ void main(){
|
||||
o = vec4(texture(uVel, vUv).xy - 0.5*vec2(R-L, T-B), 0., 1.);
|
||||
}`;
|
||||
|
||||
FluidSim.BUOY = `#version 300 es
|
||||
precision highp float; in vec2 vUv; out vec4 o;
|
||||
uniform sampler2D uVel, uDye; uniform float uK, uDt;
|
||||
void main(){
|
||||
float l = dot(texture(uDye, vUv).rgb, vec3(0.333));
|
||||
vec2 v = texture(uVel, vUv).xy;
|
||||
// Relax the vertical velocity toward uK*luma: a bounded updraft, unlike an
|
||||
// additive force which integrates to the clamp and turns flames into a wall.
|
||||
float a = min(1., uDt * 4.) * step(0.01, l);
|
||||
v.y += (uK * min(l, 1.5) - v.y) * a;
|
||||
o = vec4(v, 0., 1.);
|
||||
}`;
|
||||
|
||||
FluidSim.SHOW = `#version 300 es
|
||||
precision highp float; in vec2 vUv; out vec4 o;
|
||||
uniform sampler2D uDye; uniform float uSym, uContrast, uInvert, uGlow, uRot;
|
||||
|
||||
Reference in new issue
Block a user