/* * 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," + "" + "" + "\");" + "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(); }