oneshotlm
Compare all models on this prompt →

Fluid simulation · nex-n2-pro

Create a real-time fluid simulation on a full-screen canvas where dragging the mouse pushes swirling colored dye through the fluid.

⛨ sandboxed Open in new tab ↗
transcript · 6 messages
user

Create a real-time fluid simulation on a full-screen canvas where dragging the mouse pushes swirling colored dye through the fluid.

assistant
thinking
We need respond by creating index.html in current dir. Need build single-page web app full-screen canvas real-time fluid simulation. No external resources. Need implement. We need inspect cwd maybe. Need write file. Need likely use stable fluid sim algorithm. Need create one self-contained HTML. Need maybe implement Navier-Stokes grid, advection, diffusion, projection, splat, color dye. Fullscreen canvas. Drag mouse pushes swirling colored dye. Need all CSS JS inline. Need end with file present. Need maybe include fallback if WebGL unavailable? Could use Canvas2D? Real-time fluid simulation likely can be Canvas2D grid. But full-screen. Need robust and performant. We can implement WebGL fragment shaders for GPU fluid simulation. Single file. Need no external. Need handle mouse/touch. Need drag adds velocity and dye, swirl. Need maybe multiple splats. Need use WebGL1. Need write index.html.

Need consider browser support. We can create full-screen canvas with WebGL, shaders for advection, divergence, curl/vorticity, pressure Jacobi, gradient subtract, splat. Use ping-pong FBOs. Need implement texture format support. Simpler: WebGL1 with RGBA floating point? Some browsers need OES_texture_float, OES_texture_float_linear, OES_texture_half_float? Could use unsigned byte textures and render to textures? WebGL1 can render to unsigned byte textures? Color buffer RGBA unsigned byte is renderable. But fluid velocities need signed and can be in [0,1] encoded? We can use RGBA floating point if supported. Need fallback. Could use half float maybe. Simpler for artifact: use WebGL1 with OES_texture_float and linear, render to float textures. Most desktop browsers support. But maybe not mobile. Could include fallback if not.

Need implement shaders. Use canvas 2D overlay maybe for instructions. The user asked create app; final answer can say created. But need ensure file present.

Let's design.

HTML:
<!DOCTYPE html><html><head><meta charset...><style> html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#05070d;color:white;font-family...} canvas{position:fixed;inset:0;width:100%;height:100%;display:block;} .hud...
</style></head><body><canvas id="fluid"></canvas><div class=hint>...</div><script>
'use strict';
const canvas = document.getElementById('fluid');
const gl = canvas.getContext('webgl', { alpha:false, antialias:false, depth:false, stencil:false, premultipliedAlpha:false, preserveDrawingBuffer:false });
if (!gl) ...
Need full-screen. Set canvas.width = Math.floor(window.innerWidth * dpr?) But simulation resolution maybe lower for perf. Use render resolution maybe min 1024? For full screen. Canvas internal size = CSS size * dpr? If fluid sim at canvas size maybe heavy. We can set sim scale based on DPR but max dimension 1024 or 1280. Use canvas.width = Math.max(2, Math.floor(window.innerWidth * dpr * simScale?)) and height. CSS full screen. Use gl.viewport.

Shaders:
- base vertex shader:
attribute vec2 aPosition; varying vec2 vUv; void main(){vUv=aPosition*0.5+0.5; gl_Position=vec4(aPosition,0,1);}
But full-screen triangle easier:
attribute vec2 aPosition; varying vec2 vUv; void main(){vUv=aPosition*0.5+0.5; gl_Position=vec4(aPosition,0.,1.);}
buffer [-1,-1, 3,-1, -1,3] maybe covers.

- clear shader: precision highp float; uniform sampler2D uTexture; varying vec2 vUv; void main(){gl_FragColor=texture2D(uTexture,vUv);}
Need clear FBO by sampling existing texture.

- splat shader:
precision highp; uniform sampler2D uTarget; uniform float uAspectRatio; uniform vec3 uColor; uniform vec2 uPoint; uniform float uRadius; varying vec2 vUv;
void main(){ vec2 p = vUv - uPoint.xy; p.x *= uAspectRatio; vec3 splat = exp(-dot(p,p)/uRadius)*uColor; vec3 base = texture2D(uTarget,vUv).xyz; gl_FragColor = vec4(base+splat,1.0);}
For velocity and density. For velocity point in uv, color = pointer velocity vector. For density color = dye RGB.

- advection shader:
precision highp; uniform sampler2D uVelocity; uniform sampler2D uSource; uniform vec2 uTexelSize; uniform float uDissipation; varying vec2 vUv;
void main(){ vec2 vel=texture2D(uVelocity,vUv).xy; vec2 coord = vUv - uTexelSize*vel; gl_FragColor=vec4(texture2D(uSource,coord).xyz*uDissipation,1.0);}
Need maybe clamp coord? WebGL repeat? Set CLAMP_TO_EDGE.

- divergence shader:
precision highp; uniform sampler2D uVelocity; uniform vec2 uTexelSize; varying vec2 vUv;
void main(){ float L=texture2D(uVelocity, vUv - vec2(uTexelSize.x,0)).x; float R=...; float B=...; float T=...; float div=0.5*(R-L+T-B); gl_FragColor=vec4(div,0,0,1);}
- curl shader:
float L=...y; R=...; B=...; T=...; float vorticity=R-L-T+B; gl_FragColor=vec4(0.5*vorticity,0,0,1);
- vorticity shader:
uniform sampler2D uVelocity,uCurl; uniform vec2 uTexelSize; uniform float uDt; uniform float uCurlStrength;
vec2 force=0.5*vec2(abs(curlT)-abs(curlB), abs(curlR)-abs(curlL)); force /= length(force)+0.0001; force *= vorticity * uCurlStrength; vec2 vel=texture2D(uVelocity,vUv).xy; vel += force*uDt; gl_FragColor=vec4(vel,0,1);
- pressure Jacobi:
uniform sampler2D uPressure,uDivergence; uniform vec2 uTexelSize;
void main(){ float L=..., p=(L+R+B+T-divergence)*0.25; gl_FragColor=vec4(p,0,0,1);}
- gradient subtract:
uniform sampler2D uPressure,uVelocity; uniform vec2 uTexelSize; uniform float uDt;
void main(){ float L=...; float R=...; float B=...; float T=...; vec2 grad=0.5*vec2(R-L,T-B); vec2 vel=texture2D(uVelocity,vUv).xy - uDt*grad; gl_FragColor=vec4(vel,0,1);}
Need maybe uDt in gradient subtract? Usually subtract grad. If pressure computed from divergence, subtract grad not dt? In stable fluids, subtract gradient pressure. We can use uDt maybe 1.0.
- display shader:
uniform sampler2D uTexture; varying vec2 vUv; gl_FragColor = texture2D(uTexture,vUv);
Could tone map: color = pow(color, vec3(1/1.2)); maybe.

Framebuffers:
Need create texture:
function createTexture(width,height, internalFormat=gl.RGBA, format=gl.RGBA, type=gl.FLOAT) { gl.createTexture(); bind; texParameteri MIN/MAG LINEAR, WRAP CLAMP; texImage2D(null,width,height,0,format,type,null);}
Need check extensions:
const ext = gl.getExtension('OES_texture_float'); const linear = gl.getExtension('OES_texture_float_linear'); if not ext fail. Could use half float fallback:
const halfFloatExt = gl.getExtension('OES_texture_half_float'); const halfLinear = gl.getExtension('OES_texture_half_float_linear');
const isWebGL2 = canvas.getContext('webgl2') maybe. But code using WebGL1. Could attempt WebGL2 first for float renderability? WebGL2 supports RGBA32F? renderable? Actually RGBA32F color-renderable in WebGL2? Yes if EXT_color_buffer_float. But simpler WebGL1 with OES_texture_float. Need maybe extension support. Could use half float if no float. But OES_texture_half_float renderable maybe? OES_texture_half_float allows render? It defines HALF_FLOAT_OES texture format, renderability depends? I think yes? Maybe not linear. We can implement floatType = gl.FLOAT if OES_texture_float else if halfFloatExt gl.HALF_FLOAT_OES. Need precision highp maybe.

Need create FBO:
function createFBO(width,height,type) { texture=createTexture; framebuffer=gl.createFramebuffer(); bind; framebufferTexture2D; check status; return {texture, fbo,width,height,attach(id,target){bind...}}}
Need pingpong:
function createDoubleFBO(width,height,type){let fbo1=createFBO, fbo2=createFBO; return {width,height, texelSizeX:1/width,texelSizeY:1/height, get read(){return this.fbo1}, get write(){return this.fbo2}, swap(){let temp=this.fbo1; this.fbo1=this.fbo2; this.fbo2=temp;}}}
Need maybe for velocity, density, divergence, curl, pressure.

Need program compile:
function compileShader(type,source){... if fail console.error throw}
function createProgram(vertex, fragment){...}
function getUniforms(program){? Could manually use gl.getUniformLocation.}
Need set program.

Need full screen quad:
const blit = (()=> { gl.bindBuffer gl.ARRAY_BUFFER; gl.bufferData(new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW); gl.bindBuffer gl.ELEMENT_ARRAY_BUFFER; gl.bufferData(new Uint16Array([0,1,2,1,2,3]), gl.STATIC_DRAW); gl.vertexAttribPointer(posLoc,2,gl.FLOAT,false,0,0); gl.enableVertexAttribArray(posLoc); gl.drawElements(gl.TRIANGLES,6,gl.UNSUPPORTED?)}
Use element array with Uint16. Need drawElements UNSIGNED_SHORT. Good.
Or drawArrays TRIANGLE_STRIP with 4 vertices. Simpler:
buffer [-1,-1,1,-1,-1,1,1,1]; drawArrays(TRIANGLE_STRIP,0,4). No element buffer.
function blit(targetFBO=null){ if targetFBO bind; else gl.bindFramebuffer(null); gl.viewport(0,0,width,height); gl.drawArrays(gl.TRIANGLE_STRIP,0,4);}

Need create programs:
const baseVertex = `...`;
const splatProgram = createProgram(baseVertex, splatFragment);
...
Need set uniforms.

Need simulation step:
function step(dt){
// advection velocity
gl.disable(gl.BLEND);
gl.useProgram(advectionProgram);
set uniforms uVelocity=velocity.read.texture, uSource=velocity.read.texture, uTexelSize [velocity.texelSizeX,Y], uDissipation 0.99 maybe.
blit(velocity.write); velocity.swap();
// advection density
uVelocity=velocity.read.texture, uSource=density.read.texture, uDissipation 0.98 maybe.
blit(density.write); density.swap();
// vorticity confinement
gl.useProgram(curlProgram); uniform uVelocity=velocity.read.texture, uTexelSize; blit(curlFBO);
gl.useProgram(vorticityProgram); uniform uVelocity=velocity.read.texture,uCurl=curlFBO.texture,uTexelSize,uDt=dt,uCurlStrength maybe 25; blit(velocity.write); velocity.swap();
// divergence
gl.useProgram(divergenceProgram); uniform uVelocity=velocity.read.texture,uTexelSize; blit(divergence);
// clear pressure?
gl.useProgram(clearProgram); uniform uTexture=pressure.read.texture? Actually pressure needs initial zero. Could use a clear target FBO each frame by rendering pressure.read into pressure.write? Or set pressure.read texture to zero? We can create pressure double and "clear pressure" by splat? Easier: use pressure double, but before Jacobi iterations, render pressure.read into pressure.write and swap, so both zero? Hmm if pressure accumulates, okay Jacobi starts from previous pressure? Usually okay. But need divergence. We can do gl.clear? For FBO, gl.clearColor(0,0,0,1); bind pressure.write.fbo; gl.clear; then pressure.swap? But pressure.read should be zero. Since doubleFBO has fbo1/fbo2. We can bind fbo2 and clear, then set pressure.fbo1? Need access. Simpler: pressure = createDoubleFBO; at each step:
gl.bindFramebuffer(gl.FRAMEBUFFER, pressure.write.fbo); gl.clear(...); pressure.swap(); // now read is zero, write is previous? Then Jacobi writes to write, reads zero.
But after swap, read zero. Good.
// Jacobi iterations 20-30
for i=0;i<pressureIterations;i++ { use pressureProgram; uPressure=pressure.read.texture;uDivergence=divergence.texture; blit(pressure.write); pressure.swap();}
// gradient subtract
use gradientSubtract; uPressure=pressure.read.texture,uVelocity=velocity.read.texture; blit(velocity.write); velocity.swap();
}
Need note after Jacobi, pressure.read contains final pressure because swap each iteration. Good.

Need display density:
gl.useProgram(displayProgram); uniform uTexture=density.read.texture; blit(null);

Need handle resize:
function resize(){
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const width = Math.max(2, Math.floor(window.innerWidth * dpr));
const height = ...
if canvas.width !== width etc set.
Need maybe simulation resolution cap for performance. Since canvas internal size full dpr could be huge. Use simDpr = Math.min(devicePixelRatio, 2); cap max dimension maybe 1200. If CSS 4K maybe 3840, simulation too heavy. Use scale = Math.min(simDpr, 1.5?) and max 1024? But full-screen canvas should be full-screen. We can set canvas.width/height to CSS*dpr and fluid sim at same. If 4K, heavy. Could cap at 1280 while CSS still full-screen; gl viewport same resolution, canvas stretches. The canvas internal size can be capped: const maxSimDim=1200; const scale = Math.min(window.devicePixelRatio||1, maxSimDim/Math.max(innerWidth,innerHeight)); const width = floor(innerWidth*scale). Need if CSS 1920x1080 dpr 2 => max dimension 2160 > 1200, scale 0.555 -> 1066x600. Good. Display stretched to canvas? Actually canvas internal width 1066, CSS full screen, browser stretches. Fine. For crisp maybe max 1400. Need performance. Use maxSimDim=1280.
Need if resize, recreate FBOs.
function initFramebuffers(){
velocity=createDoubleFBO(simWidth,simHeight,type);
density=createDoubleFBO(simWidth,simHeight,type);
divergence=createFBO(...); curl=createFBO(...); pressure=createDoubleFBO(...);
}
Need preserve previous? no.

Need pointer events:
Need track pointer down/move. Use canvas event listeners pointerdown, pointermove, pointerup, pointercancel. Use pointer capture.
For dragging, push swirling colored dye. Need convert client coords to UV with canvas width/height: x=(clientX / rect.width), y=1 - clientY / rect.height? Our texture coordinates vUv has origin bottom-left? Canvas WebGL texture coordinates in shader vUv from bottom? Our full-screen quad vUv = position*0.5+0.5; position y -1 -> 0, +1 ->1. Display texture vUv y=0 bottom, y=1 top. Pointer clientY top, so uvY = 1 - y/height. Velocity vector: dx/dt in CSS pixels? Need convert to UV units per second: vx = dx / rect.width / dt; vy = -dy / rect.height / dt (because y down to up). But splat velocity expects UV units per second. dt in seconds. Use velocity magnitude clamp maybe 100. Actually if dx=100 px in 0.016s over width=1000 => 6.25 uv/s. Good. For pressure maybe okay. Use clamp length 100? Maybe 50.
Splat:
function splat(x,y,dx,dy,color) {
const velocityColor = new Float32Array([dx, dy, 0, 0])? Need splat shader expects uColor vec3, uPoint vec2, uRadius. For velocity target, uColor vec3 with z ignored. For density, uColor vec3 dye.
For velocity:
gl.useProgram(splatProgram); bind velocity.read.texture to texture unit 0; set uTarget=0,uPoint=[x,y],uColor=[vx,vy,0],uRadius=0.0006? radius in UV squared. Need radius adapt to sim resolution maybe 0.002? Splat radius = 0.0006 means diameter? exp(-dist^2/radius). At dist=sqrt(radius)=0.0245 UV. For 1000 width 24 px. Good. For high res maybe. Could set radius = Math.pow(0.0035 * Math.min(width,height)? no). Splat shader p.x *= aspect. uRadius dimensionless. Use 0.0005 to 0.002. If radius too small. Let's use radius = 0.0008 maybe. For 1280 wide, sqrt=.028 -> 36 px. Good.
blit(velocity.write); velocity.swap();
Density:
color = hsvToRgb(hue,1,1), maybe dynamic hue. uRadius = 0.0012. blit(density.write); density.swap();
Need hue changes over time and pointer movement maybe.
Need initial dye? Could seed central splats.
Need random pointer? Maybe on load create some swirling colored dye. But user asked dragging; can have subtle background. Could initialize with a few splats and velocities.

Need touch. Use pointer events. Need preventDefault to avoid scroll. CSS touch-action:none.
Need if no pointer events? Add mouse fallback? Pointer events supported modern. Could include mouse/touch fallback maybe not necessary. But robust.

Need mouse dragging pushes swirling colored dye. On pointermove if pointerDown:
const dx=clientX-lastX; dy=clientY-lastY; if distance small maybe still add. dt = (timestamp-lastT)/1000; if dt <=0.001 dt=0.016. vx = dx / rect.width / dt; vy = -dy/rect.height/dt; clamp length maybe 80. But if dt small, huge. Use maxVel = 60. Also maybe add swirl by rotating velocity? The algorithm's vorticity creates swirl. We can also add tangential swirl around pointer? "pushes swirling colored dye" maybe velocity from drag plus tangential component. Could implement splat velocity vector = [vx,vy] + tangential swirl. Need decide. Vorticity confinement already makes swirl. But maybe add swirl to velocity on splat:
const speed = Math.min(Math.hypot(vx,vy), 100);
const angle = Math.atan2(vy,vx);
const swirl = 0.25*speed;
const vxS = vx + Math.cos(angle+Math.PI/2)*swirl;
const vyS = vy + Math.sin(angle+Math.PI/2)*swirl;
This creates rotating motion. But might make drag weird. Use 0.2.
Need maybe on pointerdown with no movement, add radial/tangential swirl.
Need add dye at pointer location. Color based on time and pointer position:
let hue = (performance.now()*0.04 + x*80 + y*40) % 360; color = hsvToRgb(hue/360,1,1).
Maybe include color interpolation.

Need "real-time fluid simulation" maybe use WebGL. Need maybe fallback if no WebGL: show message. But user asked create. Could include fallback canvas 2D? Maybe not needed. But no external.

Need consider WebGL context lost. Could handle lost/restored? Not necessary.

Need code details.

Potential issue with OES_texture_float and gl.FLOAT framebuffer completeness. Need gl.getExtension('OES_texture_float') and OES_texture_float_linear. If no ext, fallback to gl.UNSIGNED_BYTE with shaders storing 0-1 only; velocities negative cannot be stored. Could implement encoding for velocity? Too much. Could use WebGL2 with RGBA16F? But no.

Maybe use WebGL2 first to avoid extensions:
const gl = canvas.getContext('webgl2', {...}) || canvas.getContext('webgl', {...});
But shaders differ? WebGL2 uses #version 300 es, in/out, texture. Need separate shaders or transpile. Could stick WebGL1. Use WebGL2 if available but still WebGL1 shaders not accepted. Could use WebGL1 context. Fine.

Could use WebGL2 with separate shaders? More code. But WebGL1 widely. Need maybe "OES_texture_float" not available on iOS Safari? Safari supports WebGL2 and float? Actually WebGL2 may. Could use WebGL2 and half float? Hmm.

Maybe implement WebGL2 path with modern shader and float/half? But code longer. Maybe acceptable.

Alternative Canvas2D fluid simulation grid is simpler and no extension issues. But real-time full-screen maybe lower resolution. Could implement stable fluids on a 2D array CPU with JS. But full-screen high resolution expensive; could use 128x96 grid and scale with smoothing. Drag pushes dye. This would work in all browsers, no WebGL extension. But "real-time fluid simulation" maybe okay. However canvas full-screen with grid upscaled may be pixelated. Could implement bilinear interpolation rendering. CPU grid of 160x100, 20 fps maybe. But WebGL GPU is better.

Need maybe include graceful fallback to 2D if WebGL float not supported. But artifact should run. Could implement fallback 2D? Maybe not needed but beneficial. However no external. We can code fallback maybe heavy. Need decide.

Given user asked "Create a real-time fluid simulation on a full-screen canvas where dragging the mouse pushes swirling colored dye through the fluid." We can deliver a nice WebGL fluid sim. Need all in one file. We can create index.html.

Need maybe use "full-screen canvas" means canvas covers viewport. Good.

Need maybe no network. Good.

Need maybe final answer: "Done — index.html created." But before final ensure file exists. Need write file. Need maybe include code. Let's craft carefully.

Need ensure no syntax errors. Let's write file with complete code.

Let's design code in detail.

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title>Real-Time Fluid Simulation</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
html, body { margin:0; width:100%; height:100%; overflow:hidden; background:#02030a; }
body { font-family: system-ui, ...; }
canvas { position:fixed; inset:0; width:100vw; height:100vh; display:block; touch-action:none; cursor:crosshair; background: radial-gradient(circle at 30% 20%, #0b1024, #02030a 70%); }
.hud { position: fixed; left: clamp(12px,2vw,24px); bottom: clamp(12px,2vw,24px); padding: 10px 14px; border: 1px solid rgba(255,255,255,.12); border-radius: 14px; background: rgba(3,6,16,.45); backdrop-filter: blur(10px); box-shadow: 0 12px 40px rgba(0,0,0,.25); color: rgba(255,255,255,.82); font-size: 13px; line-height:1.4; user-select:none; pointer-events:none; }
.hud b { color:#fff; font-weight:650; }
</style>
</head>
<body>
<canvas id="fluid"></canvas>
<div class="hud"><b>Drag</b> to push dye &nbsp;·&nbsp; Move fast for stronger vortices</div>
<script>
(() => { ... })();
</script>
</body>
</html>

Need maybe if WebGL unavailable, hide hint and show overlay. Could add <div id="error" class=...> hidden. Use CSS .error.

JS:

(() => {
'use strict';
const canvas = document.getElementById('fluid');
const hint = document.querySelector('.hud');
const error = document.getElementById('error')? maybe no error div. Could create.
const gl = canvas.getContext('webgl', { alpha: false, antialias:false, depth:false, stencil:false, powerPreference:'high-performance' });
if (!gl) { show error; return; }

const ext = gl.getExtension('OES_texture_float');
const linear = gl.getExtension('OES_texture_float_linear');
if (!ext) { show error maybe "This browser needs WebGL floating point textures." return; }
gl.getExtension('OES_texture_float_linear'); // optional
gl.getExtension('OES_standard_derivatives')? not needed.
gl.disable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE);

Need maybe OES_texture_float_linear not necessary but improves. If not, NEAREST? Could still work but blurry? We'll set linear if available? If not, use NEAREST? But advection needs sampling. If no linear, NEAREST is okay but lower quality. We can check.
const hasLinear = !!gl.getExtension('OES_texture_float_linear');
const texFilter = hasLinear ? gl.LINEAR : gl.NEAREST;

Need maybe in WebGL1, gl.HALF_FLOAT_OES for OES_texture_half_float; but we use float. Good.

Constants:
let width=0,height=0,dpr=1;
let velocity,density,divergence,curl,pressure;
let pressureIterations = 25;
let simScale = 1;
const maxSimDimension = 1280;
const minSimDimension = 512? Maybe for small screens.

function resize() {
const cssW = window.innerWidth; cssH=window.innerHeight;
const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
const maxDim = Math.max(cssW, cssH);
let scale = pixelRatio;
if (maxDim * scale > maxSimDimension) scale = maxSimDimension / maxDim;
scale = Math.max(scale, 0.5);? If large screen with DPR 1, scale=1. If 4K 3840, scale=.333. Good. If small mobile 390, scale=1. Good.
const newW = Math.max(2, Math.floor(cssW * scale));
const newH = Math.max(2, Math.floor(cssH * scale));
canvas.width = newW; canvas.height = newH;
width = newW; height = newH;
gl.viewport(0,0,width,height);
if (width !== oldW || height !== oldH) { initFramebuffers(); seed(); }
}
Need if called before programs? yes.

Need initFramebuffers after programs? createFBO doesn't need programs. But maybe okay. We'll define.

Need maybe set canvas CSS size via CSS, no JS.

Framebuffers:
function createTexture(width,height) {
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D,tex);
gl.texParameteri(...MIN...texFilter);
gl.texParameteri(...MAG...texFilter);
gl.texParameteri(...WRAP_S,gl.CLAMP_TO_EDGE);
gl.texParameteri(...WRAP_T,gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,width,height,0,gl.RGBA,gl.FLOAT,null);
return tex;
}
function createFBO(w,h) { const texture=createTexture(w,h); const fbo=gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER,fbo); gl.framebufferTexture2D(...); const status=gl.checkFramebufferStatus(gl.FRAMEBUFFER); if(status!==gl.FRAMEBUFFER_COMPLETE) throw new Error(...); gl.bindFramebuffer(gl.FRAMEBUFFER,null); return {texture,fbo,width:w,height:h}; }
Need OES_texture_float maybe if texImage2D with gl.FLOAT and RGBA works. Good.

Need "createDoubleFBO" returns fbo1/fbo2. Could initialize both zero? gl clear? We'll clear in step. But texture content undefined. Display may show garbage before first step. We'll clear density maybe. In initFramebuffers, clear density and velocity to zero:
velocity = createDoubleFBO; density = createDoubleFBO; etc.
clearFBO(velocity.fbo1); clearFBO(velocity.fbo2); clearFBO(density...); clear pressure.
function clearFBO(fbo){ gl.bindFramebuffer(gl.FRAMEBUFFER,fbo); gl.viewport(0,0,width,height); gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT); gl.bindFramebuffer(gl.FRAMEBUFFER,null); }
Need for density maybe clear.

Programs:
const baseVertex = `
attribute vec2 aPosition;
varying vec2 vUv;
void main() {
vUv = aPosition * 0.5 + 0.5;
gl_Position = vec4(aPosition, 0.0, 1.0);
}
`;
Need if attribute location? We can get loc = gl.getAttribLocation(program,'aPosition').
Need maybe set attribute pointer before each draw.

Fragments:
splat:
precision highp float;
uniform sampler2D uTarget;
uniform vec2 uPoint;
uniform vec3 uColor;
uniform float uRadius;
uniform float uAspectRatio;
varying vec2 vUv;
void main() {
vec2 p = vUv - uPoint.xy;
p.x *= uAspectRatio;
vec3 result = texture2D(uTarget, vUv).xyz;
result += exp(-dot(p,p) / uRadius) * uColor;
gl_FragColor = vec4(result, 1.0);
}
Need if uAspectRatio = width/height. If width>height, p.x scaled, circle in screen. Good.

advection:
precision highp float;
uniform sampler2D uVelocity;
uniform sampler2D uSource;
uniform vec2 uTexelSize;
uniform float uDissipation;
varying vec2 vUv;
void main() {
vec2 velocity = texture2D(uVelocity, vUv).xy;
vec2 coord = vUv - uTexelSize * velocity;
vec4 result = texture2D(uSource, coord);
float decay = 1.0 + uDissipation * 0.006; // maybe? Actually uDissipation passed maybe 0.99?
gl_FragColor = vec4(result.rgb * uDissipation, 1.0);
}
Maybe uDissipation should be per frame close to 1. For velocity 0.992, density 0.985. Good.

divergence:
precision highp float;
uniform sampler2D uVelocity;
uniform vec2 uTexelSize;
varying vec2 vUv;
void main() {
float L = texture2D(uVelocity, vUv - vec2(uTexelSize.x, 0.0)).x;
...
float div = 0.5 * (R - L + T - B);
gl_FragColor = vec4(div, 0.0, 0.0, 1.0);
}
curl:
float vorticity = R - L - T + B; gl_FragColor=vec4(0.5*vorticity,0,0,1);
vorticity:
precision highp;
uniform sampler2D uVelocity;
uniform sampler2D uCurl;
uniform vec2 uTexelSize;
uniform float uDt;
uniform float uStrength;
void main(){
float C = texture2D(uCurl,vUv).x;
float L = texture2D(uCurl, vUv - vec2(uTexelSize.x,0.0)).x;
...
vec2 force = 0.5 * vec2(abs(T)-abs(B), abs(R)-abs(L));
force /= length(force) + 0.000001;
force *= uStrength * C * uDt;
vec2 vel = texture2D(uVelocity,vUv).xy;
vel += force;
gl_FragColor = vec4(vel,0,1);
}
Need vorticity C sign? Some algorithms use force = 0.5 * vec2(abs(T)-abs(B), abs(R)-abs(L)); force = normalize(force) * curl * strength; okay.
pressure:
precision highp;
uniform sampler2D uPressure;
uniform sampler2D uDivergence;
uniform vec2 uTexelSize;
void main(){
float L=...;
float divergence = texture2D(uDivergence,vUv).x;
float pressure = (L+R+B+T - divergence) * 0.25;
gl_FragColor=vec4(pressure,0,0,1);
}
gradient:
uniform sampler2D uPressure,uVelocity; uniform vec2 uTexelSize; uniform float uDt;
void main(){
float L=...;
vec2 gradient = 0.5 * vec2(R-L,T-B);
vec2 velocity = texture2D(uVelocity,vUv).xy - gradient * uDt;
gl_FragColor=vec4(velocity,0,1);
}
Maybe use uDt=1.0.
display:
precision highp;
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
vec3 color = texture2D(uTexture,vUv).rgb;
color = max(color - 0.004, 0.0);
color = pow(color, vec3(0.85)); // brighten? If color >1? float texture can >1. gl_FragColor color maybe clamp? default framebuffer unsigned byte clamps. pow of >1 >1 okay clamp.
gl_FragColor = vec4(color,1.0);
}
Need maybe add background:
display could mix with dark background:
vec3 color = texture2D(...).rgb;
color = max(color,0.0);
color = color / (color + vec3(0.15)); // tonemap
color = pow(color, vec3(0.9));
gl_FragColor=vec4(color,1);
Need if color can negative? no. Good.
Maybe use "uTime" for subtle? Not needed.

clearProgram:
uniform sampler2D uTexture; display same. But to clear pressure by sampling zero? Could use gl.clear. Need clearProgram not needed. But maybe for density initial? We can use gl.clear.
Maybe "splat" uses uTarget.

Need createProgram:
function createProgram(vertexSource, fragmentSource) {
const vs=compile(gl.VERTEX_SHADER,...), fs=...
const program=gl.createProgram(); gl.attachShader...; gl.linkProgram; if !link throw.
return program;
}
Need compile errors.

Need get uniforms:
function bindTexture(unit,texture){ gl.activeTexture(gl.TEXTURE0+unit); gl.bindTexture(gl.TEXTURE_2D,texture);}
function setUniform? We'll use gl.uniform*.

Need blit:
const quadBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer); gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW);
const aPosition = gl.getAttribLocation(quadProgram? Need each program has same attribute name. Could get per program. In blit, need program's aPosition. Could use a global currentProgram? Simpler blit(target, program) { gl.bindFramebuffer; gl.viewport; gl.useProgram? no; const loc=gl.getAttribLocation(program,'aPosition')? Too expensive. Could store program.aPosition = gl.getAttribLocation(program,'aPosition'); Then blit(program) sets pointer.
function blit(target, program) {
if (target) gl.bindFramebuffer(gl.FRAMEBUFFER,target.fbo); else gl.bindFramebuffer(gl.FRAMEBUFFER,null);
gl.viewport(0,0,width,height);
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
gl.enableVertexAttribArray(program.aPosition);
gl.vertexAttribPointer(program.aPosition,2,gl.FLOAT,false,0,0);
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
}
Need if target null viewport width/height.

Need maybe use gl.pixelStorei? no.

Need simulation step:
function step(dt) {
dt = Math.min(dt, 0.033); // avoid large jumps
gl.disable(gl.BLEND);
gl.useProgram(advectionProgram);
gl.uniform1i(advectionProgram.uVelocity,0);
gl.uniform1i(advectionProgram.uSource,1);
gl.uniform2f(advectionProgram.uTexelSize, velocity.texelSizeX, velocity.texelSizeY);
gl.uniform1f(advectionProgram.uDissipation, 0.992);
bindTexture(0, velocity.read.texture); bindTexture(1, velocity.read.texture); blit(velocity.write, advectionProgram); velocity.swap();

gl.useProgram(advectionProgram);
gl.uniform1i(...); gl.uniform1f(advectionProgram.uDissipation, 0.985);
bindTexture(0, velocity.read.texture); bindTexture(1, density.read.texture); blit(density.write, advectionProgram); density.swap();

gl.useProgram(curlProgram);
gl.uniform1i(curlProgram.uVelocity,0); gl.uniform2f(...);
bindTexture(0, velocity.read.texture); blit(curl, curlProgram);

gl.useProgram(vorticityProgram);
gl.uniform1i(...uVelocity,0); gl.uniform1i(...uCurl,1); gl.uniform2f(...); gl.uniform1f(uDt,dt); gl.uniform1f(uStrength, 28.0);
bindTexture(0,velocity.read.texture); bindTexture(1,curl.texture); blit(velocity.write,vorticityProgram); velocity.swap();

gl.useProgram(divergenceProgram);
...
gl.bindFramebuffer pressure.write.fbo; gl.clear...
pressure.swap();
for i=0; i<pressureIterations; i++ {
gl.useProgram(pressureProgram);
...
blit(pressure.write, pressureProgram); pressure.swap();
}
gl.useProgram(gradientSubtractProgram);
...
blit(velocity.write, gradientSubtractProgram); velocity.swap();

gl.useProgram(displayProgram);
gl.uniform1i(displayProgram.uTexture,0);
bindTexture(0,density.read.texture);
blit(null, displayProgram);
}
Need pressure clear:
gl.bindFramebuffer(gl.FRAMEBUFFER, pressure.write.fbo);
gl.viewport(0,0,width,height);
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
pressure.swap();
Now pressure.read is zero, pressure.write is old. Jacobi first writes to pressure.write (old) reading pressure.read zero. Good.
After loop, pressure.read is final. Good.

Need maybe divergence/curl FBOs are single. Need no swap.

Need seed:
function seed() {
const now = performance.now() * 0.001;
const points = [
{ x: 0.25+Math.random()*0.5, y:0.25+Math.random()*0.5, vx: 8, vy: 4, hue: 180},
...
];
for each point:
const color = hsvToRgb((point.hue/360 + now*0.03)%1,1,1);
splat(point.x, point.y, point.vx, point.vy, color, 0.0025, 'density');
splat(point.x, point.y, point.vx*0.5, point.vy*0.5, [0,0,0], 0.0009, 'velocity');
Maybe add multiple. But splat uses current FBOs. Need if called after init. Good.
Need maybe if resize later, seed again? It resets fluid. Fine.

Need pointer state:
let pointer = {
down:false,
x:0, y:0,
prevX:0, prevY:0,
prevT: performance.now(),
id:null
};
function pointerDown(e) {
pointer.down = true; pointer.id=e.pointerId; canvas.setPointerCapture?.(e.pointerId);
const pos = getPointerPos(e);
pointer.x=pointer.prevX=pos.x; pointer.y=pointer.prevY=pos.y; pointer.prevT=performance.now();
addSplatAt(pos.x,pos.y,0,0, performance.now(), true);
e.preventDefault();
}
function pointerMove(e) {
if (!pointer.down || e.pointerId !== pointer.id) return;
const pos=getPointerPos(e);
const now=performance.now();
let dt=(now-pointer.prevT)/1000; if(dt <= 0.001) dt=0.001; if(dt > 0.05) dt=0.05;
let dx=pos.x-pointer.prevX, dy=pos.y-pointer.prevY;
let vx=dx / Math.max(width,1) / dt; // Wait canvas internal width vs CSS? pos.x from CSS. Need convert using rect.width/height. getPointerPos returns uv based on rect. prevX also uv. dx in uv. dt. Good.
let vy=dy / dt; (because uv y bottom). If getPointerPos y = 1 - clientY/rect.height. Then dy = pos.y-prevY. Good.
const speed = Math.hypot(vx,vy); const maxSpeed=80; if speed>maxSpeed scale=maxSpeed/speed; vx*=scale; vy*=scale;
const swirl = Math.min(speed, 80) * 0.18;
const angle = Math.atan2(vy,vx);
const svx = vx + Math.cos(angle+Math.PI/2)*swirl;
const svy = vy + Math.sin(angle+Math.PI/2)*swirl;
const hue = (now*0.00006 + pos.x*0.7 + pos.y*0.35) % 1; // too slow? now*0.00006 = per sec 0.06 cycles, okay.
const color = hsvToRgb(hue,1,1);
splat(pos.x,pos.y,svx,svy,color,0.0015,'density');
splat(pos.x,pos.y,svx,svy,[0,0,0],0.0007,'velocity');
// maybe add extra dye behind? no.
pointer.prevX=pos.x; pointer.prevY=pos.y; pointer.prevT=now;
e.preventDefault();
}
function pointerUp(e) { if id matches; pointer.down=false; release; }
Need if pointer leaves but captured. Good.
Need "dragging pushes swirling colored dye" maybe also on pointermove if not down? Could add small dye if hover? But dragging only. Maybe pointerdown creates initial.

Need getPointerPos:
const rect = canvas.getBoundingClientRect();
return {x: clamp((e.clientX - rect.left)/rect.width,0,1), y: clamp(1 - (e.clientY - rect.top)/rect.height,0,1)};
Need clamp function.

Need "hue" maybe:
function hsvToRgb(h,s,v) { // h 0-1
const i = Math.floor(h*6);
const f = h*6 - i;
const p = v*(1-s);
const q = v*(1-f*s);
const t = v*(1-(1-f)*s);
switch(i%6)...
}
Need return [r,g,b].

Need "splat":
function splat(x,y,vx,vy,color,radius,type) {
gl.useProgram(splatProgram);
gl.uniform1i(splatProgram.uTarget,0);
gl.uniform1f(splatProgram.uAspectRatio,width/height);
gl.uniform2f(splatProgram.uPoint,x,y);
gl.uniform3f(splatProgram.uColor, vx, vy, 0.0); // for density, vx/vy ignored? Actually shader adds vec3 uColor, so if color array [r,g,b], but function parameter color. Need call with [r,g,b] for density. For velocity call with [vx,vy,0].
gl.uniform1f(splatProgram.uRadius,radius);
bindTexture(0, target.read.texture);
blit(target.write, splatProgram);
target.swap();
}
Need for density, if call splat(density, [r,g,b], color), shader adds to density. Good.

Need maybe "splat" for velocity with uColor [vx,vy,0]. For density, uColor [r,g,b]. Need radius.

Need maybe "addVelocity" and "addDensity".

Need frame loop:
let last = performance.now();
function frame(now) {
const dt = Math.min((now-last)/1000, 0.033);
last = now;
step(dt);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

Need maybe hide hint after few seconds? Could leave.

Need handle resize:
window.addEventListener('resize', resize, {passive:true});
resize();
Maybe orientationchange.

Need if error:
function showFatal(message) { hint.innerHTML = `<b>WebGL unavailable</b><br>${message}`; hint.style...; }
No external.

Need maybe "gl.getExtension('OES_texture_float')" may return null in WebGL2 context? We use webgl. Good.
Need maybe "gl.FLOAT" texture with OES_texture_float but not color renderable unless OES_texture_float_linear? Actually renderability is OES_texture_float. Linear optional. Good.
Need maybe "highp float" in fragment may not be supported? Check:
const highp = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT);
const precision = highp && highp.precision > 0 ? 'highp' : 'mediump';
But velocity values can be >1, mediump low precision. Use highp if available. Most. We can define shader precision highp. If highp unsupported, compile fail. Could use conditional:
const floatPrecision = highp && highp.precision > 0 ? 'highp' : 'mediump';
Then shader source needs `precision ${floatPrecision} float;` but our sources include precision highp. We can generate by replace? Could use replaceAll('precision highp float;', `precision ${floatPrecision} float;`). Simpler:
function fragment(source) { return source.replace('precision highp float;', `precision ${floatPrecision} float;`); }
Need createProgram can accept fragment source with no precision? But fine.

Need maybe if highp precision range low? Use highp. Good.

Need maybe if `gl.getShaderPrecisionFormat` returns null? fallback mediump.

Need maybe no semicolons issue.

Potential issue: `gl.uniform2f(program.uTexelSize, target.texelSizeX, target.texelSizeY);` Need set program uniform locations after link:
function programInfo(vertex, frag) { const p=createProgram; p.aPosition=gl.getAttribLocation(p,'aPosition'); p.uTarget=...; return p; }
Could manually after creation:
const splatProgram = createProgram(...);
splatProgram.uTarget = gl.getUniformLocation(splatProgram,'uTarget'); etc.
Need for unused uniforms returns null. Fine.

Need maybe all shader sources include `varying vec2 vUv;`. Good.

Need maybe if `OES_texture_float_linear` not available, `texFilter=gl.NEAREST`. But advection with NEAREST maybe still works. Could use half float? no.
Need maybe if `gl.checkFramebufferStatus` after creating texture before context? okay.

Need maybe use `gl.activeTexture(gl.TEXTURE0 + unit)` for unit 0/1. Good.

Need maybe after `gl.clear` pressure, no need use program.

Need maybe "pressureIterations" adapt to resolution:
pressureIterations = Math.round(20 + Math.log2(Math.max(width,height)/256)*4); maybe 20-30. But if heavy. Use 24.
Maybe `pressureIterations = width * height > 900000 ? 18 : 24;`
Need code:
pressureIterations = Math.max(16, Math.min(32, Math.round(20 + Math.log2(Math.max(width,height) / 256) * 2)));
At 1280 log2(5)=2.32*2=4.6 ->25. At 512 ->20. Good.

Need maybe "clear density" after resize. `clearFBO(density.fbo1); clearFBO(density.fbo2); clearFBO(velocity.fbo1); clearFBO(velocity.fbo2); clearFBO(pressure.fbo1); clearFBO(pressure.fbo2); clearFBO(divergence.fbo); clearFBO(curl.fbo);`
Need `clearFBO` uses current viewport maybe width/height. Good.
Need maybe `seed()` after clearing. But `seed()` uses splat and swaps FBOs. Good. It may leave density/velocity in read. Fine.
Need maybe if resize called before pointer events? okay.

Need maybe "canvas.width = newW" resets context state? It doesn't reset extensions/programs. It clears drawing buffer and resets viewport? We call viewport. It doesn't reset shader programs. FBOs recreated. Good.

Potential issue: `canvas.width` assignment resets WebGL state? It may reset viewport but not program state. We set viewport. Fine.

Need maybe if context lost, draw errors. Not necessary.

Need maybe "fullscreen canvas" but not actual Fullscreen API. Good.

Need maybe "no external resources, CDNs or network requests": no external. Good.

Need maybe final answer not too long. Need after writing, verify file. Could run `ls -l index.html` maybe. Need maybe maybe use `head`? We'll use bash `test -f index.html && wc -c index.html`.

Before final, write file. Need ensure code has no syntax errors. Let's compose full code carefully.

Potential shader compile issue: In WebGL1, precision statement must be first after optional preprocessor? It can be first. If we replace "precision highp float;" with "precision highp float;" okay. Need all sources have that exact line. Good.
Need not include comments with `//`? okay.

Let's craft code.

Potential issue with `gl.texImage2D` with null and gl.FLOAT in WebGL1: extension `OES_texture_float` may support textures but not renderable? Actually OES_texture_float says texture images can be used as render targets if complete? I think yes. Need maybe `OES_texture_float_linear` not required for renderability. Good.

Potential issue with framebuffer completeness for float textures: WebGL1 + OES_texture_float allows renderable? Yes.

Potential issue with "highp" fragment in WebGL1 on some mobile. We check precision. If no highp, use mediump. But mediump may not support values >65504? It does but low precision. Fine.
Need shader source replacement:
const fragmentPrecision = highp && highp.precision > 0 ? 'highp' : 'mediump';
function makeFragment(src) { return src.replace('precision highp float;', `precision ${fragmentPrecision} float;`); }
Need if no highp, mediump may not support texture2D with float textures? Hmm.
Need maybe use `precision mediump float;` in vertex too? Vertex always highp. Fine.

Potential issue with `gl.getExtension('OES_texture_float_linear')` returns null; we still use NEAREST. Good.

Potential issue with `gl.clearColor(0,0,0,1)` and float FBO clear: ok.

Potential issue with `uAspectRatio` if height 0. no.

Potential issue with `splatProgram.uColor` for density: If color values >1, float texture stores. Good.

Potential issue with display shader `color = color / (color + vec3(0.18));` If color negative? density never negative. Good.
Maybe add "glow": `color += vec3(0.015);` But background black. Could mix.

Potential issue with `texture2D(uSource, coord)` coord outside [0,1], clamp to edge. Good.

Potential issue with "velocity" after splat is in UV units per second. Advection shader uses coord = vUv - texelSize*velocity. If velocity 80 uv/s and dt 0.016, advection moves 1.28 uv per frame if no? Wait velocity field stores velocity in UV/sec. Advection shader should multiply by dt: coord = vUv - uTexelSize * velocity * uDt. Our shader lacks uDt. Oops! Need include uDt in advection. Important. Otherwise velocities huge (80) move 80 texels per frame, not per second. Need include dt. Add uniform float uDt; coord = vUv - uTexelSize * velocity * uDt. Then step passes dt. Good. For vorticity force: force *= uStrength * C * uDt. Good. Gradient subtract: subtract gradient * uDt? Pressure solved for current divergence, velocity update should subtract gradient * uDt? In semi-Lagrangian stable fluids, pressure projection subtracts grad(p) (no dt) after pressure solved with divergence. But if using dt in advection, divergence units 1/sec, pressure units? We can subtract gradient * dt? Many GPU fluid implementations use:
advect velocity with dt
advect density with dt
vorticity force * dt
subtract pressure gradient (no dt)
In "WebGL Fluid Sim" by PavelDoGreat, advection shader uses `coord = vUv - uTexelSize * velocity * uDeltaTime`; gradient subtract uses `velocity - gradient`. Pressure Jacobi solves divergence. So no dt. We'll use no dt in gradient. Need remove uDt from gradient or keep but set 1.0. Simpler no uDt.
Advection shader:
uniform float uDt;
coord = vUv - uTexelSize * velocity * uDt;
gl_FragColor = vec4(result.rgb * uDissipation, 1.0);
Step sets uDt=dt.
Need for velocity dissipation maybe `0.992` per frame. Good.
Need vorticity uses uDt.

Potential issue with pressureIterations and dt; okay.

Need maybe "clear pressure" each frame. Good.

Potential issue with "splat velocity" values maybe in UV/sec. If dt 0.016 and velocity 80, advection moves 1.28 uv per frame, too high. But max speed should maybe 15? Let's calculate pointer dx 100 px / width 1000 = 0.1 uv in 0.016 => 6.25 uv/s. Advection moves 0.1 uv per frame, okay. Max 30 maybe. If huge pointer, clamp 35. Good. Let's set maxSpeed=35 or 45. Vorticity strength maybe 35.
If speed=35, dt=0.016 => 0.56 uv/frame, can move across screen in 2 sec. Good.
Maybe clamp maxSpeed=25. Use 30.
Need maybe add swirl 0.22*speed, so total 36. Good.
Need velocity splat radius 0.0007.

Potential issue with "density dissipation 0.985" means dye fades quickly. Use 0.992 maybe. At 60fps half-life 1.1 sec for 0.992? Actually 0.992^60=0.62 per sec. Too fade. Use 0.995? 0.995^60=0.74/sec. Good. Use 0.996. Velocity 0.999 maybe. But fluid should persist. Use velocityDissipation=0.999, density=0.992? Let's choose:
velocity 0.998, density 0.990. At 60 fps density 0.55/sec. Good. Maybe 0.996. Use 0.997.
Need maybe "colored dye" persists. Use 0.995. At 60fps 0.74/sec. Good.
Set velocity dissipation 0.999, density 0.992? Let's set velocity 0.998, density 0.995.
Need maybe background dark.

Potential issue with "full-screen canvas" and CSS `backdrop-filter` maybe unsupported but okay. No external.

Need maybe "dragging mouse" but on touch too. Good.

Need maybe "swirling colored dye through fluid" Could add "vorticity confinement" as above. Good.

Need maybe "real-time" with requestAnimationFrame. Good.

Let's craft final file.

Need maybe if WebGL unavailable, show overlay:
<div id="fallback" class="fallback" hidden>...</div>
CSS .fallback fixed center...
JS:
function fail(message) {
hint.innerHTML = `<b>WebGL fluid simulation unavailable</b><br>${message}`;
hint.style.borderColor = ...
}
No error div needed. But if no gl, hint visible. Good.
Need if no ext:
fail('This browser needs WebGL with floating-point textures.');
return.

Need maybe "powerPreference" unsupported in Safari? It ignores. Good.

Need maybe "passive:false" for pointer events to preventDefault. Use addEventListener('pointerdown', pointerDown, {passive:false}) etc.

Need maybe `canvas.setPointerCapture(e.pointerId);` can throw if not. Use try.
Need maybe pointerup/pointercancel release.

Need maybe if pointer moves with pointerId but pointer.down false, ignore. Good.
Need maybe if pointerdown then pointermove before pointer id? okay.

Need maybe "mousemove" fallback if pointer events not supported:
if (window.PointerEvent) { ... } else { canvas.addEventListener('mousedown'...), mousemove, mouseup; }
Could include fallback. Since modern all. But maybe better.

Implement fallback:
if (window.PointerEvent) { add pointer events } else {
canvas.addEventListener('mousedown', e => { pointer.down=true; pointer.id=null; handlePointerDownLike(e);});
canvas.addEventListener('mousemove', e => { if(pointer.down) handlePointerMoveLike(e);});
window.addEventListener('mouseup', ...);
}
Need simpler use pointer events only.

Need maybe "preventDefault" on pointermove; CSS touch-action none.

Need maybe "resize" event:
window.addEventListener('resize', resize);
window.addEventListener('orientationchange', () => setTimeout(resize, 100));
Need maybe call `resize()` after programs created and before requestAnimationFrame. Good.

Potential issue with "canvas.width = newW" when same? If same, no reinit. But maybe context lost? no.

Need maybe "resize" called after `seed` maybe. Good.

Need maybe "random seed" on resize resets fluid. okay.

Potential issue with `Math.random()` in seed each resize. Good.

Need maybe "splat" calls `blit` which sets viewport and draws. If `splatProgram.aPosition` maybe -1? Should not.
Need maybe "program uniform locations" if not found? no.

Need maybe "programInfo" function:
function createProgramInfo(vertexSource, fragmentSource) {
const program = createProgram(vertexSource, makeFragment(fragmentSource));
program.aPosition = gl.getAttribLocation(program, 'aPosition');
Object.assign(program, {
uTarget: gl.getUniformLocation(program, 'uTarget'), ...
});
return program;
}
But different programs have different uniforms. Could manually set after. Simpler:
function attachUniforms(program, names) { names.forEach(n => program[n] = gl.getUniformLocation(program,n)); }
Then:
const splatProgram = createProgramInfo(baseVertex, splatFragment, ['uTarget','uPoint','uColor','uRadius','uAspectRatio']);
...
Need define `createProgramInfo(vertex, fragment, uniforms)`.
Need not include `uDt` in gradient. Good.

Need maybe "clearProgram" not needed.

Need maybe "displayProgram" with `uTexture`.

Need maybe "baseVertex" same. Good.

Let's write.
write /tmp/ws/index.html (23365 bytes)
toolResult

Successfully wrote 23365 bytes to /tmp/ws/index.html

assistant
thinking

bash {"command":"test -f /tmp/ws/index.html && wc -c /tmp/ws/index.html","timeout":5}
toolResult

23366 /tmp/ws/index.html

assistant
thinking

Done — created `/tmp/ws/index.html` as a self-contained full-screen WebGL fluid simulation with inline CSS and JS, no external resources.

TAKE IT TO CODE

A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.

RUN

modelnex-n2-pro
vendorNex AGI
harnesspi-agent
generated2026-08-01

EVALUATION

1/5 broken

Canvas stays completely black with only a static instruction label; WebGL error prevents fluid rendering entirely.

off-prompt

Vision grade · anthropic/claude-sonnet-5

MEASUREMENTS

Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.

idle motion0.0%
on click0.0%
on drag0.0%
on wheel0.0%
on enter + space0.0%
on arrow keys0.0%
on w/a/s/d0.0%
frame spread3.2 / 255
console errors0
js errors
  • Failed to execute 'bindFramebuffer' on 'WebGLRenderingContext': parameter 2 is not of type 'WebGLFramebuffer'.

METRICS

runtime161.6s
tokens in44k
tokens out21k
cached3.2k
cost / run$0.0321