model-optimizer / fluid_bg.js
wangyiyi666's picture
Upload folder using huggingface_hub
7e444c1 verified
Raw
History Blame Contribute Delete
33.8 kB
/*
* Fluid Background — WebGL fluid sim, mouse-follow edition.
* Based on Pavel Dobryakov's WebGL-Fluid-Simulation (MIT).
* Self-bootstrapping: creates its own canvas + overlays on DOMContentLoaded.
* Designed as a background layer for Gradio pages.
*/
'use strict';
function _initFluid () {
/* ---------- bootstrap DOM elements ---------- */
var canvas = document.createElement('canvas');
canvas.id = 'fluid';
canvas.style.cssText = 'position:fixed;inset:0;width:100vw;height:100vh;display:block;z-index:0;pointer-events:none;';
document.body.insertBefore(canvas, document.body.firstChild);
// vignette
var veil = document.createElement('div');
veil.style.cssText = 'position:fixed;inset:0;z-index:1;pointer-events:none;'
+ 'background:radial-gradient(120% 80% at 50% 35%,rgba(5,6,10,0) 0%,rgba(5,6,10,.3) 55%,rgba(5,6,10,.82) 100%),'
+ 'radial-gradient(60% 40% at 50% 100%,rgba(0,0,0,.6),rgba(0,0,0,0) 70%);';
document.body.appendChild(veil);
// grain
var grain = document.createElement('div');
grain.style.cssText = "position:fixed;inset:-50%;z-index:2;pointer-events:none;opacity:.05;mix-blend-mode:overlay;"
+ "background-image:url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'>"
+ "<filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/>"
+ "<feColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0'/></filter>"
+ "<rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.55'/></svg>\");"
+ "animation:_grain 1.6s steps(4) infinite;";
document.body.appendChild(grain);
// grain keyframes
var st = document.createElement('style');
st.textContent = '@keyframes _grain{0%{transform:translate(0,0)}25%{transform:translate(-3%,2%)}50%{transform:translate(2%,-1%)}75%{transform:translate(-1%,3%)}100%{transform:translate(0,0)}}';
document.head.appendChild(st);
/* ---------- config ---------- */
resizeCanvas();
var config = {
SIM_RESOLUTION: 128,
DYE_RESOLUTION: 512,
DENSITY_DISSIPATION: 1.2,
VELOCITY_DISSIPATION: 0.35,
PRESSURE: 0.8,
PRESSURE_ITERATIONS: 20,
CURL: 30,
SPLAT_RADIUS: 0.25,
SPLAT_FORCE: 5000,
SHADING: true,
COLOR_PALETTE: 0,
PAUSED: false,
BLOOM: true,
BLOOM_ITERATIONS: 6,
BLOOM_RESOLUTION: 256,
BLOOM_INTENSITY: 0.55,
BLOOM_THRESHOLD: 0.5,
BLOOM_SOFT_KNEE: 0.7,
BACK_COLOR: { r: 0.012, g: 0.015, b: 0.028 },
TRANSPARENT: false,
};
// palette: blue-cyan tech theme
var PALETTES = [
[[0.15, 0.55, 1.0], [0.0, 0.85, 0.95], [0.45, 0.25, 1.0], [0.05, 0.95, 0.75]],
[[1.0, 0.37, 0.72], [0.21, 0.89, 1.0], [1.0, 0.84, 0.42], [0.60, 0.35, 1.0]],
[[0.20, 1.0, 0.75], [0.50, 0.95, 0.60], [0.10, 0.78, 0.95], [0.95, 1.0, 0.78]],
];
function pickColor () {
var pal = PALETTES[config.COLOR_PALETTE % PALETTES.length];
var c = pal[Math.floor(Math.random() * pal.length)];
return { r: c[0], g: c[1], b: c[2] };
}
function scaleColor (c, k) { return { r: c.r * k, g: c.g * k, b: c.b * k }; }
function pointerPrototype () {
this.id = -1;
this.texcoordX = 0; this.texcoordY = 0;
this.prevTexcoordX = 0; this.prevTexcoordY = 0;
this.deltaX = 0; this.deltaY = 0;
this.down = false;
this.moved = false;
this.color = { r: 30, g: 0, b: 300 };
}
var pointers = [];
var splatStack = [];
pointers.push(new pointerPrototype());
/* ---------- GL setup ---------- */
var glResult = getWebGLContext(canvas);
var gl = glResult.gl;
var ext = glResult.ext;
if (!ext.supportLinearFiltering) {
config.DYE_RESOLUTION = 256;
config.SHADING = false;
config.BLOOM = false;
}
function getWebGLContext (canvas) {
var params = { alpha: true, depth: false, stencil: false, antialias: false, preserveDrawingBuffer: false };
var gl = canvas.getContext('webgl2', params);
var isWebGL2 = !!gl;
if (!isWebGL2) gl = canvas.getContext('webgl', params) || canvas.getContext('experimental-webgl', params);
var halfFloat, supportLinearFiltering;
if (isWebGL2) {
gl.getExtension('EXT_color_buffer_float');
supportLinearFiltering = gl.getExtension('OES_texture_float_linear');
} else {
halfFloat = gl.getExtension('OES_texture_half_float');
supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear');
}
gl.clearColor(0, 0, 0, 1);
var halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat && halfFloat.HALF_FLOAT_OES;
var formatRGBA, formatRG, formatR;
if (isWebGL2) {
formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType);
formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType);
formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType);
} else {
formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType);
}
return { gl: gl, ext: { formatRGBA: formatRGBA, formatRG: formatRG, formatR: formatR, halfFloatTexType: halfFloatTexType, supportLinearFiltering: supportLinearFiltering } };
}
function getSupportedFormat (gl, internalFormat, format, type) {
if (!supportRenderTextureFormat(gl, internalFormat, format, type)) {
switch (internalFormat) {
case gl.R16F: return getSupportedFormat(gl, gl.RG16F, gl.RG, type);
case gl.RG16F: return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type);
default: return null;
}
}
return { internalFormat: internalFormat, format: format };
}
function supportRenderTextureFormat (gl, internalFormat, format, type) {
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null);
var fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
return gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
}
/* ---------- shaders ---------- */
function compileShader (type, source, keywords) {
source = addKeywords(source, keywords);
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) console.warn(gl.getShaderInfoLog(shader));
return shader;
}
function addKeywords (source, keywords) {
if (!keywords) return source;
var prefix = '';
keywords.forEach(function (k) { prefix += '#define ' + k + '\n'; });
return prefix + source;
}
function createProgram (vs, fs) {
var program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
return program;
}
function getUniforms (program) {
var uniforms = {};
var n = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
for (var i = 0; i < n; i++) {
var name = gl.getActiveUniform(program, i).name;
uniforms[name] = gl.getUniformLocation(program, name);
}
return uniforms;
}
function Program (vs, fs) { this.program = createProgram(vs, fs); this.uniforms = getUniforms(this.program); }
Program.prototype.bind = function () { gl.useProgram(this.program); };
function Material (vs, fsSource) { this.vertexShader = vs; this.fragmentShaderSource = fsSource; this.programs = []; this.activeProgram = null; this.uniforms = []; }
Material.prototype.setKeywords = function (keywords) {
var hash = 0;
for (var i = 0; i < keywords.length; i++) hash += hashCode(keywords[i]);
var program = this.programs[hash];
if (program == null) {
var fs = compileShader(gl.FRAGMENT_SHADER, this.fragmentShaderSource, keywords);
program = createProgram(this.vertexShader, fs);
this.programs[hash] = program;
}
if (program === this.activeProgram) return;
this.uniforms = getUniforms(program);
this.activeProgram = program;
};
Material.prototype.bind = function () { gl.useProgram(this.activeProgram); };
function hashCode (s) { var h = 0; for (var i = 0; i < s.length; i++) { h = ((h << 5) - h) + s.charCodeAt(i); h |= 0; } return h; }
var baseVertexShader = compileShader(gl.VERTEX_SHADER, [
'precision highp float;',
'attribute vec2 aPosition;',
'varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB;',
'uniform vec2 texelSize;',
'void main(){',
' vUv=aPosition*0.5+0.5;',
' vL=vUv-vec2(texelSize.x,0.0); vR=vUv+vec2(texelSize.x,0.0);',
' vT=vUv+vec2(0.0,texelSize.y); vB=vUv-vec2(0.0,texelSize.y);',
' gl_Position=vec4(aPosition,0.0,1.0);',
'}'
].join('\n'));
var copyShader = compileShader(gl.FRAGMENT_SHADER, 'precision mediump float;precision mediump sampler2D;varying highp vec2 vUv;uniform sampler2D uTexture;void main(){gl_FragColor=texture2D(uTexture,vUv);}');
var clearShader = compileShader(gl.FRAGMENT_SHADER, 'precision mediump float;precision mediump sampler2D;varying highp vec2 vUv;uniform sampler2D uTexture;uniform float value;void main(){gl_FragColor=value*texture2D(uTexture,vUv);}');
var displayShaderSource = [
'precision highp float;precision highp sampler2D;',
'varying vec2 vUv;varying vec2 vL;varying vec2 vR;varying vec2 vT;varying vec2 vB;',
'uniform sampler2D uTexture;uniform sampler2D uBloom;uniform vec2 texelSize;',
'vec3 linearToGamma(vec3 c){c=max(c,vec3(0));return max(1.055*pow(c,vec3(0.416666667))-0.055,vec3(0));}',
'void main(){',
' vec3 c=texture2D(uTexture,vUv).rgb;',
' #ifdef SHADING',
' vec3 lc=texture2D(uTexture,vL).rgb;vec3 rc=texture2D(uTexture,vR).rgb;',
' vec3 tc=texture2D(uTexture,vT).rgb;vec3 bc=texture2D(uTexture,vB).rgb;',
' float dx=length(rc)-length(lc);float dy=length(tc)-length(bc);',
' vec3 n=normalize(vec3(dx,dy,length(texelSize)));',
' float diffuse=clamp(dot(n,vec3(0,0,1))+0.7,0.7,1.0);c*=diffuse;',
' #endif',
' #ifdef BLOOM',
' vec3 bloom=linearToGamma(texture2D(uBloom,vUv).rgb);c+=bloom;',
' #endif',
' float a=max(c.r,max(c.g,c.b));gl_FragColor=vec4(c,a);',
'}'
].join('\n');
var bloomPrefilterShader = compileShader(gl.FRAGMENT_SHADER, 'precision mediump float;precision mediump sampler2D;varying vec2 vUv;uniform sampler2D uTexture;uniform vec3 curve;uniform float threshold;void main(){vec3 c=texture2D(uTexture,vUv).rgb;float br=max(c.r,max(c.g,c.b));float rq=clamp(br-curve.x,0.0,curve.y);rq=curve.z*rq*rq;c*=max(rq,br-threshold)/max(br,0.0001);gl_FragColor=vec4(c,0.0);}');
var bloomBlurShader = compileShader(gl.FRAGMENT_SHADER, 'precision mediump float;precision mediump sampler2D;varying vec2 vL;varying vec2 vR;varying vec2 vT;varying vec2 vB;uniform sampler2D uTexture;void main(){vec4 s=vec4(0.0);s+=texture2D(uTexture,vL);s+=texture2D(uTexture,vR);s+=texture2D(uTexture,vT);s+=texture2D(uTexture,vB);gl_FragColor=s*0.25;}');
var bloomFinalShader = compileShader(gl.FRAGMENT_SHADER, 'precision mediump float;precision mediump sampler2D;varying vec2 vL;varying vec2 vR;varying vec2 vT;varying vec2 vB;uniform sampler2D uTexture;uniform float intensity;void main(){vec4 s=vec4(0.0);s+=texture2D(uTexture,vL);s+=texture2D(uTexture,vR);s+=texture2D(uTexture,vT);s+=texture2D(uTexture,vB);gl_FragColor=s*0.25*intensity;}');
var splatShader = compileShader(gl.FRAGMENT_SHADER, 'precision highp float;precision highp sampler2D;varying vec2 vUv;uniform sampler2D uTarget;uniform float aspectRatio;uniform vec3 color;uniform vec2 point;uniform float radius;void main(){vec2 p=vUv-point.xy;p.x*=aspectRatio;vec3 splat=exp(-dot(p,p)/radius)*color;vec3 base=texture2D(uTarget,vUv).xyz;gl_FragColor=vec4(base+splat,1.0);}');
var advectionShader = compileShader(gl.FRAGMENT_SHADER, [
'precision highp float;precision highp sampler2D;',
'varying vec2 vUv;uniform sampler2D uVelocity;uniform sampler2D uSource;',
'uniform vec2 texelSize;uniform vec2 dyeTexelSize;uniform float dt;uniform float dissipation;',
'vec4 bilerp(sampler2D sam,vec2 uv,vec2 tsize){vec2 st=uv/tsize-0.5;vec2 iuv=floor(st);vec2 fuv=fract(st);',
'vec4 a=texture2D(sam,(iuv+vec2(0.5,0.5))*tsize);vec4 b=texture2D(sam,(iuv+vec2(1.5,0.5))*tsize);',
'vec4 c=texture2D(sam,(iuv+vec2(0.5,1.5))*tsize);vec4 d=texture2D(sam,(iuv+vec2(1.5,1.5))*tsize);',
'return mix(mix(a,b,fuv.x),mix(c,d,fuv.x),fuv.y);}',
'void main(){',
'#ifdef MANUAL_FILTERING',
'vec2 coord=vUv-dt*bilerp(uVelocity,vUv,texelSize).xy*texelSize;vec4 result=bilerp(uSource,coord,dyeTexelSize);',
'#else',
'vec2 coord=vUv-dt*texture2D(uVelocity,vUv).xy*texelSize;vec4 result=texture2D(uSource,coord);',
'#endif',
'float decay=1.0+dissipation*dt;gl_FragColor=result/decay;}'
].join('\n'), ext.supportLinearFiltering ? null : ['MANUAL_FILTERING']);
var divergenceShader = compileShader(gl.FRAGMENT_SHADER, 'precision mediump float;precision mediump sampler2D;varying highp vec2 vUv;varying highp vec2 vL;varying highp vec2 vR;varying highp vec2 vT;varying highp vec2 vB;uniform sampler2D uVelocity;void main(){float L=texture2D(uVelocity,vL).x;float R=texture2D(uVelocity,vR).x;float T=texture2D(uVelocity,vT).y;float B=texture2D(uVelocity,vB).y;vec2 C=texture2D(uVelocity,vUv).xy;if(vL.x<0.0)L=-C.x;if(vR.x>1.0)R=-C.x;if(vT.y>1.0)T=-C.y;if(vB.y<0.0)B=-C.y;float div=0.5*(R-L+T-B);gl_FragColor=vec4(div,0.0,0.0,1.0);}');
var curlShader = compileShader(gl.FRAGMENT_SHADER, 'precision mediump float;precision mediump sampler2D;varying highp vec2 vUv;varying highp vec2 vL;varying highp vec2 vR;varying highp vec2 vT;varying highp vec2 vB;uniform sampler2D uVelocity;void main(){float L=texture2D(uVelocity,vL).y;float R=texture2D(uVelocity,vR).y;float T=texture2D(uVelocity,vT).x;float B=texture2D(uVelocity,vB).x;float v=R-L-T+B;gl_FragColor=vec4(0.5*v,0.0,0.0,1.0);}');
var vorticityShader = compileShader(gl.FRAGMENT_SHADER, 'precision highp float;precision highp sampler2D;varying vec2 vUv;varying vec2 vL;varying vec2 vR;varying vec2 vT;varying vec2 vB;uniform sampler2D uVelocity;uniform sampler2D uCurl;uniform float curl;uniform float dt;void main(){float L=texture2D(uCurl,vL).x;float R=texture2D(uCurl,vR).x;float T=texture2D(uCurl,vT).x;float B=texture2D(uCurl,vB).x;float C=texture2D(uCurl,vUv).x;vec2 force=0.5*vec2(abs(T)-abs(B),abs(R)-abs(L));force/=length(force)+0.0001;force*=curl*C;force.y*=-1.0;vec2 velocity=texture2D(uVelocity,vUv).xy;velocity+=force*dt;velocity=min(max(velocity,-1000.0),1000.0);gl_FragColor=vec4(velocity,0.0,1.0);}');
var pressureShader = compileShader(gl.FRAGMENT_SHADER, 'precision mediump float;precision mediump sampler2D;varying highp vec2 vUv;varying highp vec2 vL;varying highp vec2 vR;varying highp vec2 vT;varying highp vec2 vB;uniform sampler2D uPressure;uniform sampler2D uDivergence;void main(){float L=texture2D(uPressure,vL).x;float R=texture2D(uPressure,vR).x;float T=texture2D(uPressure,vT).x;float B=texture2D(uPressure,vB).x;float divergence=texture2D(uDivergence,vUv).x;float pressure=(L+R+B+T-divergence)*0.25;gl_FragColor=vec4(pressure,0.0,0.0,1.0);}');
var gradientSubtractShader = compileShader(gl.FRAGMENT_SHADER, 'precision mediump float;precision mediump sampler2D;varying highp vec2 vUv;varying highp vec2 vL;varying highp vec2 vR;varying highp vec2 vT;varying highp vec2 vB;uniform sampler2D uPressure;uniform sampler2D uVelocity;void main(){float L=texture2D(uPressure,vL).x;float R=texture2D(uPressure,vR).x;float T=texture2D(uPressure,vT).x;float B=texture2D(uPressure,vB).x;vec2 velocity=texture2D(uVelocity,vUv).xy;velocity.xy-=vec2(R-L,T-B);gl_FragColor=vec4(velocity,0.0,1.0);}');
/* ---------- fullscreen blit ---------- */
var blit = (function () {
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,-1,1,1,1,1,-1]), gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0,1,2,0,2,3]), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0);
return function (target, clear) {
if (target == null) { gl.viewport(0,0,gl.drawingBufferWidth,gl.drawingBufferHeight); gl.bindFramebuffer(gl.FRAMEBUFFER,null); }
else { gl.viewport(0,0,target.width,target.height); gl.bindFramebuffer(gl.FRAMEBUFFER,target.fbo); }
if (clear) { gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT); }
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
};
})();
/* ---------- programs ---------- */
var copyProgram = new Program(baseVertexShader, copyShader);
var clearProgram = new Program(baseVertexShader, clearShader);
var splatProgram = new Program(baseVertexShader, splatShader);
var advectionProgram = new Program(baseVertexShader, advectionShader);
var divergenceProgram = new Program(baseVertexShader, divergenceShader);
var curlProgram = new Program(baseVertexShader, curlShader);
var vorticityProgram = new Program(baseVertexShader, vorticityShader);
var pressureProgram = new Program(baseVertexShader, pressureShader);
var gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader);
var bloomPrefilterProgram = new Program(baseVertexShader, bloomPrefilterShader);
var bloomBlurProgram = new Program(baseVertexShader, bloomBlurShader);
var bloomFinalProgram = new Program(baseVertexShader, bloomFinalShader);
var displayMaterial = new Material(baseVertexShader, displayShaderSource);
/* ---------- framebuffers ---------- */
var dye, velocity, divergence, curl, pressure, bloom, bloomFramebuffers = [];
function initFramebuffers () {
var simRes = getResolution(config.SIM_RESOLUTION);
var dyeRes = getResolution(config.DYE_RESOLUTION);
var texType = ext.halfFloatTexType;
var rgba = ext.formatRGBA, rg = ext.formatRG, r = ext.formatR;
var filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;
gl.disable(gl.BLEND);
dye = createDoubleFBO(dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering);
velocity = createDoubleFBO(simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering);
divergence = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST);
curl = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST);
pressure = createDoubleFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST);
initBloomFramebuffers();
}
function initBloomFramebuffers () {
var res = getResolution(config.BLOOM_RESOLUTION);
var texType = ext.halfFloatTexType, rgba = ext.formatRGBA;
var filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST;
bloom = createFBO(res.width, res.height, rgba.internalFormat, rgba.format, texType, filtering);
bloomFramebuffers.length = 0;
for (var i = 0; i < config.BLOOM_ITERATIONS; i++) {
var w = res.width >> (i + 1), h = res.height >> (i + 1);
if (w < 2 || h < 2) break;
bloomFramebuffers.push(createFBO(w, h, rgba.internalFormat, rgba.format, texType, filtering));
}
}
function createFBO (w, h, internalFormat, format, type, param) {
gl.activeTexture(gl.TEXTURE0);
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null);
var fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.viewport(0, 0, w, h); gl.clear(gl.COLOR_BUFFER_BIT);
var tsx = 1.0 / w, tsy = 1.0 / h;
return { texture: texture, fbo: fbo, width: w, height: h, texelSizeX: tsx, texelSizeY: tsy,
attach: function (id) { gl.activeTexture(gl.TEXTURE0 + id); gl.bindTexture(gl.TEXTURE_2D, texture); return id; } };
}
function createDoubleFBO (w, h, internalFormat, format, type, param) {
var fbo1 = createFBO(w, h, internalFormat, format, type, param);
var fbo2 = createFBO(w, h, internalFormat, format, type, param);
return {
width: w, height: h, texelSizeX: fbo1.texelSizeX, texelSizeY: fbo1.texelSizeY,
get read () { return fbo1; }, set read (v) { fbo1 = v; },
get write () { return fbo2; }, set write (v) { fbo2 = v; },
swap: function () { var t = fbo1; fbo1 = fbo2; fbo2 = t; },
};
}
function getResolution (resolution) {
var aspect = gl.drawingBufferWidth / gl.drawingBufferHeight;
if (aspect < 1) aspect = 1.0 / aspect;
var min = Math.round(resolution), max = Math.round(resolution * aspect);
if (gl.drawingBufferWidth > gl.drawingBufferHeight) return { width: max, height: min };
return { width: min, height: max };
}
function updateKeywords () {
var keywords = [];
if (config.SHADING) keywords.push('SHADING');
if (config.BLOOM) keywords.push('BLOOM');
displayMaterial.setKeywords(keywords);
}
updateKeywords();
initFramebuffers();
/* ---------- simulation loop ---------- */
var lastUpdate = Date.now();
function update () {
requestAnimationFrame(update);
if (window.__fluidBG && !window.__fluidBG.running) return;
var dt = calcDeltaTime();
if (resizeCanvas()) initFramebuffers();
applyInputs();
if (!config.PAUSED) step(dt);
render(null);
}
function calcDeltaTime () {
var now = Date.now(); var dt = (now - lastUpdate) / 1000;
dt = Math.min(dt, 0.016666); lastUpdate = now; return dt;
}
function resizeCanvas () {
var dpr = Math.min(window.devicePixelRatio || 1, 1.5);
var w = Math.floor(canvas.clientWidth * dpr), h = Math.floor(canvas.clientHeight * dpr);
if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; return true; }
return false;
}
function applyInputs () {
if (splatStack.length > 0) multipleSplats(splatStack.pop());
pointers.forEach(function (p) { if (p.moved) { p.moved = false; splatPointer(p); } });
}
function step (dt) {
gl.disable(gl.BLEND);
curlProgram.bind();
gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0));
blit(curl);
vorticityProgram.bind();
gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0));
gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1));
gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL);
gl.uniform1f(vorticityProgram.uniforms.dt, dt);
blit(velocity.write); velocity.swap();
divergenceProgram.bind();
gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0));
blit(divergence);
clearProgram.bind();
gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0));
gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE);
blit(pressure.write); pressure.swap();
pressureProgram.bind();
gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0));
for (var i = 0; i < config.PRESSURE_ITERATIONS; i++) {
gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1));
blit(pressure.write); pressure.swap();
}
gradienSubtractProgram.bind();
gl.uniform2f(gradienSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0));
gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1));
blit(velocity.write); velocity.swap();
advectionProgram.bind();
gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY);
if (!ext.supportLinearFiltering) gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, velocity.texelSizeX, velocity.texelSizeY);
var vid = velocity.read.attach(0);
gl.uniform1i(advectionProgram.uniforms.uVelocity, vid);
gl.uniform1i(advectionProgram.uniforms.uSource, vid);
gl.uniform1f(advectionProgram.uniforms.dt, dt);
gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION);
blit(velocity.write); velocity.swap();
if (!ext.supportLinearFiltering) gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY);
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0));
gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1));
gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION);
blit(dye.write); dye.swap();
}
function render (target) {
if (config.BLOOM) applyBloom(dye.read, bloom);
gl.disable(gl.BLEND);
displayMaterial.bind();
var w = target == null ? gl.drawingBufferWidth : target.width;
var h = target == null ? gl.drawingBufferHeight : target.height;
if (config.SHADING) gl.uniform2f(displayMaterial.uniforms.texelSize, 1 / w, 1 / h);
gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0));
if (config.BLOOM) gl.uniform1i(displayMaterial.uniforms.uBloom, bloom.attach(1));
blit(target);
}
function applyBloom (source, destination) {
if (bloomFramebuffers.length < 2) return;
var last = destination;
gl.disable(gl.BLEND);
bloomPrefilterProgram.bind();
var knee = config.BLOOM_THRESHOLD * config.BLOOM_SOFT_KNEE + 0.0001;
gl.uniform3f(bloomPrefilterProgram.uniforms.curve, config.BLOOM_THRESHOLD - knee, knee * 2, 0.25 / knee);
gl.uniform1f(bloomPrefilterProgram.uniforms.threshold, config.BLOOM_THRESHOLD);
gl.uniform1i(bloomPrefilterProgram.uniforms.uTexture, source.attach(0));
blit(last);
bloomBlurProgram.bind();
for (var i = 0; i < bloomFramebuffers.length; i++) {
var dest = bloomFramebuffers[i];
gl.uniform2f(bloomBlurProgram.uniforms.texelSize, 1 / last.width, 1 / last.height);
gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0));
blit(dest); last = dest;
}
gl.blendFunc(gl.ONE, gl.ONE); gl.enable(gl.BLEND);
for (var j = bloomFramebuffers.length - 2; j >= 0; j--) {
var base = bloomFramebuffers[j];
gl.uniform2f(bloomBlurProgram.uniforms.texelSize, 1 / last.width, 1 / last.height);
gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0));
gl.viewport(0, 0, base.width, base.height); blit(base); last = base;
}
gl.disable(gl.BLEND);
bloomFinalProgram.bind();
gl.uniform2f(bloomFinalProgram.uniforms.texelSize, 1 / last.width, 1 / last.height);
gl.uniform1i(bloomFinalProgram.uniforms.uTexture, last.attach(0));
gl.uniform1f(bloomFinalProgram.uniforms.intensity, config.BLOOM_INTENSITY);
blit(destination);
}
function splatPointer (p) {
splat(p.texcoordX, p.texcoordY, p.deltaX * config.SPLAT_FORCE, p.deltaY * config.SPLAT_FORCE, p.color);
}
function multipleSplats (amount) {
for (var i = 0; i < amount; i++) {
var color = scaleColor(pickColor(), 8.0);
splat(Math.random(), Math.random(), 1000 * (Math.random() - 0.5), 1000 * (Math.random() - 0.5), color);
}
}
function splat (x, y, dx, dy, color) {
splatProgram.bind();
gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0));
gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height);
gl.uniform2f(splatProgram.uniforms.point, x, y);
gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0);
gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0));
blit(velocity.write); velocity.swap();
gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0));
gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b);
blit(dye.write); dye.swap();
}
function correctRadius (radius) {
var a = canvas.width / canvas.height;
if (a > 1) radius *= a;
return radius;
}
/* ---------- pointer events (mouse-FOLLOW) ---------- */
function getTexX (x) { return x / canvas.clientWidth; }
function getTexY (y) { return 1.0 - y / canvas.clientHeight; }
function correctDeltaX (dx) { var a = canvas.width / canvas.height; if (a < 1) dx *= a; return dx; }
function correctDeltaY (dy) { var a = canvas.width / canvas.height; if (a > 1) dy /= a; return dy; }
function updatePointerMove (pointer, posX, posY) {
pointer.prevTexcoordX = pointer.texcoordX;
pointer.prevTexcoordY = pointer.texcoordY;
pointer.texcoordX = getTexX(posX);
pointer.texcoordY = getTexY(posY);
pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX);
pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY);
pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0;
}
function colorForPointer (pointer) { pointer.color = scaleColor(pickColor(), 0.15); }
var mainPointer = pointers[0];
mainPointer.id = 0;
mainPointer.down = true;
mainPointer.texcoordX = 0.5; mainPointer.texcoordY = 0.5;
mainPointer.prevTexcoordX = 0.5; mainPointer.prevTexcoordY = 0.5;
colorForPointer(mainPointer);
var lastColorTime = 0;
window.addEventListener('mousemove', function (e) {
if (performance.now() - lastColorTime > 260) {
colorForPointer(mainPointer);
lastColorTime = performance.now();
}
updatePointerMove(mainPointer, e.clientX, e.clientY);
});
// touch support
window.addEventListener('touchmove', function (e) {
var touches = e.targetTouches;
for (var i = 0; i < touches.length; i++) {
var p = pointers[0];
updatePointerMove(p, touches[i].clientX, touches[i].clientY);
}
}, { passive: true });
// auto palette cycle
var _palCycle = setInterval(function () {
if (window.__fluidBG && !window.__fluidBG.running) return;
config.COLOR_PALETTE = (config.COLOR_PALETTE + 1) % PALETTES.length;
colorForPointer(mainPointer);
}, 8000);
// auto ambient burst every ~6s
var _ambientBurst = setInterval(function () {
if (window.__fluidBG && !window.__fluidBG.running) return;
splatStack.push(Math.floor(Math.random() * 3) + 2);
}, 6000);
// initial reveal burst
splatStack.push(Math.floor(Math.random() * 6) + 8);
/* ---------- background on/off toggle ---------- */
var _fluidCtrl = { running: true };
window.__fluidBG = _fluidCtrl;
(function () {
var wrap = document.createElement('div');
wrap.style.cssText = 'position:fixed;right:18px;top:18px;z-index:60;display:flex;align-items:center;gap:8px;'
+ 'padding:7px 12px;border-radius:999px;background:rgba(12,15,28,0.6);'
+ 'border:1px solid rgba(100,180,255,0.18);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);'
+ 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:12px;color:#9fc4ff;'
+ 'cursor:pointer;user-select:none;box-shadow:0 4px 18px rgba(0,0,0,0.35);';
wrap.title = '\u5207\u6362\u52a8\u6001\u80cc\u666f';
var txt = document.createElement('span');
txt.textContent = '\u52a8\u6001\u80cc\u666f';
txt.style.cssText = 'letter-spacing:.5px;';
var sw = document.createElement('span');
sw.style.cssText = 'position:relative;width:38px;height:20px;border-radius:999px;'
+ 'transition:background .25s ease;flex:0 0 auto;';
var knob = document.createElement('span');
knob.style.cssText = 'position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;'
+ 'background:#fff;transition:transform .25s ease;box-shadow:0 1px 3px rgba(0,0,0,.4);';
sw.appendChild(knob);
wrap.appendChild(txt);
wrap.appendChild(sw);
document.body.appendChild(wrap);
function _apply (on) {
_fluidCtrl.running = on;
canvas.style.display = on ? 'block' : 'none';
veil.style.display = on ? 'block' : 'none';
grain.style.display = on ? 'block' : 'none';
document.body.classList.toggle('fluid-off', !on);
knob.style.transform = on ? 'translateX(18px)' : 'translateX(0)';
sw.style.background = on ? 'rgba(100,180,255,0.65)' : 'rgba(120,130,150,0.35)';
txt.style.color = on ? '#9fc4ff' : '#8892a6';
try { localStorage.setItem('fluidBG', on ? 'on' : 'off'); } catch (e) {}
}
wrap.addEventListener('click', function () { _apply(!_fluidCtrl.running); });
var _saved = 'on';
try { _saved = localStorage.getItem('fluidBG') || 'on'; } catch (e) {}
_apply(_saved !== 'off');
})();
update();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', _initFluid);
} else {
_initFluid();
}