Spaces:
Sleeping
Sleeping
| /* | |
| * 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. | |
| */ | |
| ; | |
| 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(); | |
| } | |