Back to Components
Interactive Smoke Simulation Using WebGL (Fluid Dynamics Rendering)
Component

Interactive Smoke Simulation Using WebGL (Fluid Dynamics Rendering)

CodewithLord
October 4, 2025

This project creates a fully animated smoke-like fluid simulation rendered in real-time using WebGL.

🧠 Description

This project creates a fully animated smoke-like fluid simulation rendered in real-time using WebGL. It uses shaders, framebuffers, and GPU calculations to generate dynamic curls, pressure fields, velocity fields, and density textures. The animation reacts to pointer movement, creating smooth splashes and trails. The entire experience runs inside a full-screen rendering surface with a stylized heading placed at the center. Responsive styles adjust typography based on device size for a clean visual experience.


💻 HTML Code


1<!DOCTYPE html> 2<html lang="en"> 3 4 <head> 5 <meta charset="UTF-8"> 6 <title>Smoke Streams</title> 7 <link rel="preconnect" href="https://fonts.googleapis.com"> 8<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 9<link href="https://fonts.googleapis.com/css2?family=Anton&display=swap" rel="stylesheet"><link rel="stylesheet" href="https://public.codepenassets.com/css/normalize-5.0.0.min.css"> 10<link rel="stylesheet" href="./style.css"> 11 12 </head> 13 14 <body> 15 <canvas></canvas> 16<h1>Smoke Streams</h1> 17 <script src="./script.js"></script> 18 19 </body> 20 21</html> 22

CSS Code


1html, 2body { 3 margin: 0; 4 height: 100%; 5} 6 7canvas { 8 display: block; 9 width: 100%; 10 height: 100vh; 11} 12h1 { 13 white-space: nowrap; 14 font-size: 5rem; 15 font-family: "Anton", sans-serif; 16 letter-spacing: 0.35em; 17 color: #fff; 18 position: absolute; 19 top: 50%; 20 left: 50%; 21 transform: translate(-50%, -50%); 22} 23@media (max-width: 1460px) { 24 h1 { 25 font-size: 4rem; 26 } 27} 28@media (max-width: 850px) { 29 h1 { 30 font-size: 3rem; 31 } 32} 33@media (max-width: 650px) { 34 h1 { 35 font-size: 2rem; 36 } 37}

🎨 CSS EXPLANATION


Your CSS handles:

  1. Reset + Full-screen Layout

Removes default margin.

Forces the entire page to occupy full height.

Makes the rendering surface take 100% width and 100vh height.

  1. Centered Title Styling

Typography uses a bold, wide font (“Anton”).

White text with increased letter-spacing for a cinematic look.

Absolutely positioned at the center using transform: translate(-50%, -50%).

  1. Responsive Font Scaling

Three breakpoints adjust the font size for:

Large screens

Tablets

Small mobile screens

This ensures the title remains readable and visually balanced on any device.


Javascript code


1"use strict"; 2 3let canvas = document.getElementsByTagName("canvas")[0]; 4 5canvas.width = canvas.clientWidth; 6canvas.height = canvas.clientHeight; 7 8let config = { 9 TEXTURE_DOWNSAMPLE: 1, 10 DENSITY_DISSIPATION: 0.98, 11 VELOCITY_DISSIPATION: 0.99, 12 PRESSURE_DISSIPATION: 0.8, 13 PRESSURE_ITERATIONS: 25, 14 CURL: 35, 15 SPLAT_RADIUS: 0.002 16}; 17 18let pointers = []; 19let splatStack = []; 20 21let _getWebGLContext = getWebGLContext(canvas); 22let gl = _getWebGLContext.gl; 23let ext = _getWebGLContext.ext; 24let support_linear_float = _getWebGLContext.support_linear_float; 25 26function getWebGLContext(canvas) { 27 let params = { 28 alpha: false, 29 depth: false, 30 stencil: false, 31 antialias: false 32 }; 33 34 let gl = canvas.getContext("webgl2", params); 35 36 let isWebGL2 = !!gl; 37 38 if (!isWebGL2) 39 gl = 40 canvas.getContext("webgl", params) || 41 canvas.getContext("experimental-webgl", params); 42 43 let halfFloat = gl.getExtension("OES_texture_half_float"); 44 let support_linear_float = gl.getExtension("OES_texture_half_float_linear"); 45 46 if (isWebGL2) { 47 gl.getExtension("EXT_color_buffer_float"); 48 support_linear_float = gl.getExtension("OES_texture_float_linear"); 49 } 50 51 gl.clearColor(0.0, 0.0, 0.0, 1.0); 52 53 let internalFormat = isWebGL2 ? gl.RGBA16F : gl.RGBA; 54 let internalFormatRG = isWebGL2 ? gl.RG16F : gl.RGBA; 55 let formatRG = isWebGL2 ? gl.RG : gl.RGBA; 56 let texType = isWebGL2 ? gl.HALF_FLOAT : halfFloat.HALF_FLOAT_OES; 57 58 return { 59 gl: gl, 60 ext: { 61 internalFormat: internalFormat, 62 internalFormatRG: internalFormatRG, 63 formatRG: formatRG, 64 texType: texType 65 }, 66 support_linear_float: support_linear_float 67 }; 68} 69 70function pointerPrototype() { 71 this.id = -1; 72 this.x = 0; 73 this.y = 0; 74 this.dx = 0; 75 this.dy = 0; 76 this.down = false; 77 this.moved = false; 78 this.color = [30, 0, 300]; 79} 80 81pointers.push(new pointerPrototype()); 82 83let GLProgram = (function () { 84 function GLProgram(vertexShader, fragmentShader) { 85 if (!(this instanceof GLProgram)) 86 throw new TypeError("Cannot call a class as a function"); 87 88 this.uniforms = {}; 89 this.program = gl.createProgram(); 90 91 gl.attachShader(this.program, vertexShader); 92 gl.attachShader(this.program, fragmentShader); 93 gl.linkProgram(this.program); 94 95 if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) 96 throw gl.getProgramInfoLog(this.program); 97 98 let uniformCount = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS); 99 100 for (let i = 0; i < uniformCount; i++) { 101 let uniformName = gl.getActiveUniform(this.program, i).name; 102 103 this.uniforms[uniformName] = gl.getUniformLocation( 104 this.program, 105 uniformName 106 ); 107 } 108 } 109 110 GLProgram.prototype.bind = function bind() { 111 gl.useProgram(this.program); 112 }; 113 114 return GLProgram; 115})(); 116 117function compileShader(type, source) { 118 let shader = gl.createShader(type); 119 120 gl.shaderSource(shader, source); 121 gl.compileShader(shader); 122 123 if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) 124 throw gl.getShaderInfoLog(shader); 125 126 return shader; 127} 128 129let baseVertexShader = compileShader( 130 gl.VERTEX_SHADER, 131 "precision highp float; precision mediump sampler2D; 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); }" 132); 133let clearShader = compileShader( 134 gl.FRAGMENT_SHADER, 135 "precision highp float; precision mediump sampler2D; varying vec2 vUv; uniform sampler2D uTexture; uniform float value; void main () { gl_FragColor = value * texture2D(uTexture, vUv); }" 136); 137let displayShader = compileShader( 138 gl.FRAGMENT_SHADER, 139 "precision highp float; precision mediump sampler2D; varying vec2 vUv; uniform sampler2D uTexture; void main () { gl_FragColor = texture2D(uTexture, vUv); }" 140); 141let splatShader = compileShader( 142 gl.FRAGMENT_SHADER, 143 "precision highp float; precision mediump 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); }" 144); 145let advectionManualFilteringShader = compileShader( 146 gl.FRAGMENT_SHADER, 147 "precision highp float; precision mediump sampler2D; varying vec2 vUv; uniform sampler2D uVelocity; uniform sampler2D uSource; uniform vec2 texelSize; uniform float dt; uniform float dissipation; vec4 bilerp (in sampler2D sam, in vec2 p) { vec4 st; st.xy = floor(p - 0.5) + 0.5; st.zw = st.xy + 1.0; vec4 uv = st * texelSize.xyxy; vec4 a = texture2D(sam, uv.xy); vec4 b = texture2D(sam, uv.zy); vec4 c = texture2D(sam, uv.xw); vec4 d = texture2D(sam, uv.zw); vec2 f = p - st.xy; return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); } void main () { vec2 coord = gl_FragCoord.xy - dt * texture2D(uVelocity, vUv).xy; gl_FragColor = dissipation * bilerp(uSource, coord); gl_FragColor.a = 1.0; }" 148); 149let advectionShader = compileShader( 150 gl.FRAGMENT_SHADER, 151 "precision highp float; precision mediump sampler2D; varying vec2 vUv; uniform sampler2D uVelocity; uniform sampler2D uSource; uniform vec2 texelSize; uniform float dt; uniform float dissipation; void main () { vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize; gl_FragColor = dissipation * texture2D(uSource, coord); }" 152); 153let divergenceShader = compileShader( 154 gl.FRAGMENT_SHADER, 155 "precision highp float; precision mediump sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform sampler2D uVelocity; vec2 sampleVelocity (in vec2 uv) { vec2 multiplier = vec2(1.0, 1.0); if (uv.x < 0.0) { uv.x = 0.0; multiplier.x = -1.0; } if (uv.x > 1.0) { uv.x = 1.0; multiplier.x = -1.0; } if (uv.y < 0.0) { uv.y = 0.0; multiplier.y = -1.0; } if (uv.y > 1.0) { uv.y = 1.0; multiplier.y = -1.0; } return multiplier * texture2D(uVelocity, uv).xy; } void main () { float L = sampleVelocity(vL).x; float R = sampleVelocity(vR).x; float T = sampleVelocity(vT).y; float B = sampleVelocity(vB).y; float div = 0.5 * (R - L + T - B); gl_FragColor = vec4(div, 0.0, 0.0, 1.0); }" 156); 157let curlShader = compileShader( 158 gl.FRAGMENT_SHADER, 159 "precision highp float; precision mediump sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying 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 vorticity = R - L - T + B; gl_FragColor = vec4(vorticity, 0.0, 0.0, 1.0); }" 160); 161let vorticityShader = compileShader( 162 gl.FRAGMENT_SHADER, 163 "precision highp float; precision mediump 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).y; float R = texture2D(uCurl, vR).y; float T = texture2D(uCurl, vT).x; float B = texture2D(uCurl, vB).x; float C = texture2D(uCurl, vUv).x; vec2 force = vec2(abs(T) - abs(B), abs(R) - abs(L)); force *= 1.0 / length(force + 0.00001) * curl * C; vec2 vel = texture2D(uVelocity, vUv).xy; gl_FragColor = vec4(vel + force * dt, 0.0, 1.0); }" 164); 165let pressureShader = compileShader( 166 gl.FRAGMENT_SHADER, 167 "precision highp float; precision mediump sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform sampler2D uPressure; uniform sampler2D uDivergence; vec2 boundary (in vec2 uv) { uv = min(max(uv, 0.0), 1.0); return uv; } void main () { float L = texture2D(uPressure, boundary(vL)).x; float R = texture2D(uPressure, boundary(vR)).x; float T = texture2D(uPressure, boundary(vT)).x; float B = texture2D(uPressure, boundary(vB)).x; float C = texture2D(uPressure, vUv).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); }" 168); 169let gradientSubtractShader = compileShader( 170 gl.FRAGMENT_SHADER, 171 "precision highp float; precision mediump sampler2D; varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB; uniform sampler2D uPressure; uniform sampler2D uVelocity; vec2 boundary (in vec2 uv) { uv = min(max(uv, 0.0), 1.0); return uv; } void main () { float L = texture2D(uPressure, boundary(vL)).x; float R = texture2D(uPressure, boundary(vR)).x; float T = texture2D(uPressure, boundary(vT)).x; float B = texture2D(uPressure, boundary(vB)).x; vec2 velocity = texture2D(uVelocity, vUv).xy; velocity.xy -= vec2(R - L, T - B); gl_FragColor = vec4(velocity, 0.0, 1.0); }" 172); 173 174let textureWidth = void 0; 175let textureHeight = void 0; 176let density = void 0; 177let velocity = void 0; 178let divergence = void 0; 179let curl = void 0; 180let pressure = void 0; 181 182initFramebuffers(); 183 184let clearProgram = new GLProgram(baseVertexShader, clearShader); 185let displayProgram = new GLProgram(baseVertexShader, displayShader); 186let splatProgram = new GLProgram(baseVertexShader, splatShader); 187let advectionProgram = new GLProgram( 188 baseVertexShader, 189 support_linear_float ? advectionShader : advectionManualFilteringShader 190); 191let divergenceProgram = new GLProgram(baseVertexShader, divergenceShader); 192let curlProgram = new GLProgram(baseVertexShader, curlShader); 193let vorticityProgram = new GLProgram(baseVertexShader, vorticityShader); 194let pressureProgram = new GLProgram(baseVertexShader, pressureShader); 195let gradienSubtractProgram = new GLProgram( 196 baseVertexShader, 197 gradientSubtractShader 198); 199 200function initFramebuffers() { 201 textureWidth = gl.drawingBufferWidth >> config.TEXTURE_DOWNSAMPLE; 202 textureHeight = gl.drawingBufferHeight >> config.TEXTURE_DOWNSAMPLE; 203 204 let iFormat = ext.internalFormat; 205 let iFormatRG = ext.internalFormatRG; 206 let formatRG = ext.formatRG; 207 let texType = ext.texType; 208 209 density = createDoubleFBO( 210 0, 211 textureWidth, 212 textureHeight, 213 iFormat, 214 gl.RGBA, 215 texType, 216 support_linear_float ? gl.LINEAR : gl.NEAREST 217 ); 218 velocity = createDoubleFBO( 219 2, 220 textureWidth, 221 textureHeight, 222 iFormatRG, 223 formatRG, 224 texType, 225 support_linear_float ? gl.LINEAR : gl.NEAREST 226 ); 227 divergence = createFBO( 228 4, 229 textureWidth, 230 textureHeight, 231 iFormatRG, 232 formatRG, 233 texType, 234 gl.NEAREST 235 ); 236 curl = createFBO( 237 5, 238 textureWidth, 239 textureHeight, 240 iFormatRG, 241 formatRG, 242 texType, 243 gl.NEAREST 244 ); 245 pressure = createDoubleFBO( 246 6, 247 textureWidth, 248 textureHeight, 249 iFormatRG, 250 formatRG, 251 texType, 252 gl.NEAREST 253 ); 254} 255 256function createFBO(texId, w, h, internalFormat, format, type, param) { 257 gl.activeTexture(gl.TEXTURE0 + texId); 258 259 let texture = gl.createTexture(); 260 261 gl.bindTexture(gl.TEXTURE_2D, texture); 262 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param); 263 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param); 264 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 265 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 266 gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null); 267 268 let fbo = gl.createFramebuffer(); 269 270 gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); 271 gl.framebufferTexture2D( 272 gl.FRAMEBUFFER, 273 gl.COLOR_ATTACHMENT0, 274 gl.TEXTURE_2D, 275 texture, 276 0 277 ); 278 gl.viewport(0, 0, w, h); 279 gl.clear(gl.COLOR_BUFFER_BIT); 280 281 return [texture, fbo, texId]; 282} 283 284function createDoubleFBO(texId, w, h, internalFormat, format, type, param) { 285 let fbo1 = createFBO(texId, w, h, internalFormat, format, type, param); 286 let fbo2 = createFBO(texId + 1, w, h, internalFormat, format, type, param); 287 288 return { 289 get first() { 290 return fbo1; 291 }, 292 get second() { 293 return fbo2; 294 }, 295 swap: function swap() { 296 let temp = fbo1; 297 298 fbo1 = fbo2; 299 fbo2 = temp; 300 } 301 }; 302} 303 304let blit = (function () { 305 gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); 306 gl.bufferData( 307 gl.ARRAY_BUFFER, 308 new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), 309 gl.STATIC_DRAW 310 ); 311 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); 312 gl.bufferData( 313 gl.ELEMENT_ARRAY_BUFFER, 314 new Uint16Array([0, 1, 2, 0, 2, 3]), 315 gl.STATIC_DRAW 316 ); 317 gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); 318 gl.enableVertexAttribArray(0); 319 320 return function (destination) { 321 gl.bindFramebuffer(gl.FRAMEBUFFER, destination); 322 gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); 323 }; 324})(); 325 326let lastTime = Date.now(); 327 328update(); 329 330function update() { 331 resizeCanvas(); 332 333 let dt = Math.min((Date.now() - lastTime) / 1000, 0.016); 334 lastTime = Date.now(); 335 336 gl.viewport(0, 0, textureWidth, textureHeight); 337 338 if (splatStack.length > 0) { 339 for (let m = 0; m < splatStack.pop(); m++) { 340 let color = [Math.random() * 10, Math.random() * 10, Math.random() * 10]; 341 let x = canvas.width * Math.random(); 342 let y = canvas.height * Math.random(); 343 let dx = 1000 * (Math.random() - 0.5); 344 let dy = 1000 * (Math.random() - 0.5); 345 346 splat(x, y, dx, dy, color); 347 } 348 } 349 350 advectionProgram.bind(); 351 gl.uniform2f( 352 advectionProgram.uniforms.texelSize, 353 1.0 / textureWidth, 354 1.0 / textureHeight 355 ); 356 gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.first[2]); 357 gl.uniform1i(advectionProgram.uniforms.uSource, velocity.first[2]); 358 gl.uniform1f(advectionProgram.uniforms.dt, dt); 359 gl.uniform1f( 360 advectionProgram.uniforms.dissipation, 361 config.VELOCITY_DISSIPATION 362 ); 363 blit(velocity.second[1]); 364 velocity.swap(); 365 366 gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.first[2]); 367 gl.uniform1i(advectionProgram.uniforms.uSource, density.first[2]); 368 gl.uniform1f( 369 advectionProgram.uniforms.dissipation, 370 config.DENSITY_DISSIPATION 371 ); 372 blit(density.second[1]); 373 density.swap(); 374 375 for (let i = 0, len = pointers.length; i < len; i++) { 376 let pointer = pointers[i]; 377 378 if (pointer.moved) { 379 splat(pointer.x, pointer.y, pointer.dx, pointer.dy, pointer.color); 380 pointer.moved = false; 381 } 382 } 383 384 curlProgram.bind(); 385 gl.uniform2f( 386 curlProgram.uniforms.texelSize, 387 1.0 / textureWidth, 388 1.0 / textureHeight 389 ); 390 gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.first[2]); 391 blit(curl[1]); 392 393 vorticityProgram.bind(); 394 gl.uniform2f( 395 vorticityProgram.uniforms.texelSize, 396 1.0 / textureWidth, 397 1.0 / textureHeight 398 ); 399 gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.first[2]); 400 gl.uniform1i(vorticityProgram.uniforms.uCurl, curl[2]); 401 gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL); 402 gl.uniform1f(vorticityProgram.uniforms.dt, dt); 403 blit(velocity.second[1]); 404 velocity.swap(); 405 406 divergenceProgram.bind(); 407 gl.uniform2f( 408 divergenceProgram.uniforms.texelSize, 409 1.0 / textureWidth, 410 1.0 / textureHeight 411 ); 412 gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.first[2]); 413 blit(divergence[1]); 414 415 clearProgram.bind(); 416 417 let pressureTexId = pressure.first[2]; 418 419 gl.activeTexture(gl.TEXTURE0 + pressureTexId); 420 gl.bindTexture(gl.TEXTURE_2D, pressure.first[0]); 421 gl.uniform1i(clearProgram.uniforms.uTexture, pressureTexId); 422 gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE_DISSIPATION); 423 blit(pressure.second[1]); 424 pressure.swap(); 425 426 pressureProgram.bind(); 427 gl.uniform2f( 428 pressureProgram.uniforms.texelSize, 429 1.0 / textureWidth, 430 1.0 / textureHeight 431 ); 432 gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence[2]); 433 pressureTexId = pressure.first[2]; 434 gl.activeTexture(gl.TEXTURE0 + pressureTexId); 435 436 for (let _i = 0; _i < config.PRESSURE_ITERATIONS; _i++) { 437 gl.bindTexture(gl.TEXTURE_2D, pressure.first[0]); 438 gl.uniform1i(pressureProgram.uniforms.uPressure, pressureTexId); 439 blit(pressure.second[1]); 440 pressure.swap(); 441 } 442 443 gradienSubtractProgram.bind(); 444 gl.uniform2f( 445 gradienSubtractProgram.uniforms.texelSize, 446 1.0 / textureWidth, 447 1.0 / textureHeight 448 ); 449 gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.first[2]); 450 gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.first[2]); 451 blit(velocity.second[1]); 452 velocity.swap(); 453 454 gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 455 displayProgram.bind(); 456 gl.uniform1i(displayProgram.uniforms.uTexture, density.first[2]); 457 blit(null); 458 459 requestAnimationFrame(update); 460} 461 462function splat(x, y, dx, dy, color) { 463 splatProgram.bind(); 464 gl.uniform1i(splatProgram.uniforms.uTarget, velocity.first[2]); 465 gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height); 466 gl.uniform2f( 467 splatProgram.uniforms.point, 468 x / canvas.width, 469 1.0 - y / canvas.height 470 ); 471 gl.uniform3f(splatProgram.uniforms.color, dx, -dy, 1.0); 472 gl.uniform1f(splatProgram.uniforms.radius, config.SPLAT_RADIUS); 473 blit(velocity.second[1]); 474 velocity.swap(); 475 476 gl.uniform1i(splatProgram.uniforms.uTarget, density.first[2]); 477 gl.uniform3f( 478 splatProgram.uniforms.color, 479 color[0] * 0.3, 480 color[1] * 0.3, 481 color[2] * 0.3 482 ); 483 blit(density.second[1]); 484 density.swap(); 485} 486 487function resizeCanvas() { 488 (canvas.width !== canvas.clientWidth || 489 canvas.height !== canvas.clientHeight) && 490 ((canvas.width = canvas.clientWidth), 491 (canvas.height = canvas.clientHeight), 492 initFramebuffers()); 493} 494 495let count = 0; 496let colorArr = [Math.random() + 0.2, Math.random() + 0.2, Math.random() + 0.2]; 497 498canvas.addEventListener("mousemove", function (e) { 499 count++; 500 501 count > 25 && 502 ((colorArr = [ 503 Math.random() + 0.2, 504 Math.random() + 0.2, 505 Math.random() + 0.2 506 ]), 507 (count = 0)); 508 509 pointers[0].down = true; 510 pointers[0].color = colorArr; 511 pointers[0].moved = pointers[0].down; 512 pointers[0].dx = (e.offsetX - pointers[0].x) * 10.0; 513 pointers[0].dy = (e.offsetY - pointers[0].y) * 10.0; 514 pointers[0].x = e.offsetX; 515 pointers[0].y = e.offsetY; 516}); 517 518canvas.addEventListener( 519 "touchmove", 520 function (e) { 521 e.preventDefault(); 522 523 let touches = e.targetTouches; 524 525 count++; 526 527 count > 25 && 528 ((colorArr = [ 529 Math.random() + 0.2, 530 Math.random() + 0.2, 531 Math.random() + 0.2 532 ]), 533 (count = 0)); 534 535 for (let i = 0, len = touches.length; i < len; i++) { 536 if (i >= pointers.length) pointers.push(new pointerPrototype()); 537 538 pointers[i].id = touches[i].identifier; 539 pointers[i].down = true; 540 pointers[i].x = touches[i].pageX; 541 pointers[i].y = touches[i].pageY; 542 pointers[i].color = colorArr; 543 544 let pointer = pointers[i]; 545 546 pointer.moved = pointer.down; 547 pointer.dx = (touches[i].pageX - pointer.x) * 10.0; 548 pointer.dy = (touches[i].pageY - pointer.y) * 10.0; 549 pointer.x = touches[i].pageX; 550 pointer.y = touches[i].pageY; 551 } 552 }, 553 false 554);

🧠 JAVASCRIPT EXPLANATION (BROKEN DOWN PERFECTLY)

Your JavaScript builds a fully GPU-powered fluid simulation using WebGL.

Here’s the simplest structured explanation ever:

  1. Canvas Initialization

Retrieves the rendering surface.

Sets its width and height to match the visible layout.


  1. Configuration Settings

Defines physical behavior:

Dissipation of density & velocity

Strength of curl

Number of pressure iterations

Radius of splat (when user moves pointer)

These control how smoke spreads, fades, and reacts to interactions.

  1. Pointer Handling

Creates a base pointer object.

Tracks position, movement, and color used to generate splashes.

When the user moves the cursor or touches the screen, smoke bursts appear.

  1. WebGL Context Setup

The system requests:

WebGL2 (fallback to WebGL1 if needed)

Extensions for floating-point textures

Linear filtering support

This ensures high-quality smooth simulations.

  1. Shader Creation

Shaders define how GPU should calculate colors and motion.

You compile:

Vertex Shader

Sends UV positions + neighbor pixel locations

Used by all fragment shaders

Fragment Shaders

Each handles a piece of the fluid physics:

Clear Shader: slowly fade textures

Display Shader: draw the final density

Splat Shader: add splashes when user moves pointer

Advection Shader: moves smoke along velocity field

Divergence Shader: calculates how much fluid is spreading

Curl Shader: detects swirling motion

Vorticity Shader: amplifies curls for natural fluid motion

Pressure Shader: solves pressure equations

Gradient Subtract Shader: stabilizes simulation

These shaders together create realistic fluid flow.

  1. Framebuffers

Creates GPU textures for:

Density (smoke)

Velocity

Divergence

Curl

Pressure

Some textures are "double FBOs" meaning they swap each frame to avoid overwriting their own data.

This is essential for fluid simulations.


  1. Simulation Steps

Every animation frame performs:

a. Apply user splashes

Pointer movement adds bursts of color into density + velocity.

b. Compute curl

Simulates swirling.

c. Add vorticity force

Enhances natural turbulence.

d. Compute divergence

Measures fluid flux.

e. Pressure solve

Runs multiple iterations to stabilize fluid.

f. Subtract pressure gradient

Ensures no unnatural expansion happens.

g. Advect textures

Moves smoke and velocity according to flow direction.

h. Render final density

Draws the smoke to screen.

All steps run on GPU using the shaders defined.

  1. Animation Loop

A continuous loop:

Updates framebuffers

Processes physics

Draws smoke

Repeats

Creating smooth, endless motion.

Love this component?

Explore more components and build amazing UIs.

View All Components