← Blog
SimulationPhysicsCanvas APIMath

Fluid Simulation in the Browser — Real-Time Computation on a 100x60 Grid

Updated May 27, 202610 min read

Go deeper on this topic

Creative Coding

Mathematical simulations, nature visualization, and generative art in the browser

Article 2 of 12 in this series.

View topic page

Use the topic hub as a map, then continue to the next article in the series. New here? Start at the home hub →

Three Steps of Fluid Simulation

Directly solving the Navier-Stokes equations is too computationally expensive. Following Jos Stam's method, the problem is decomposed into three steps:

  1. Diffusion: The velocity and density fields spread to their surroundings
  2. Advection: The fluid is carried along its own velocity field
  3. Pressure Correction (Projection): The velocity field is corrected to maintain incompressibility

Grid Structure

Scalar values (density) and vector values (velocity) are stored on a 100x60 grid:

const N = 100
const M = 60
const size = (N + 2) * (M + 2) // Including boundary cells

const density = new Float32Array(size)
const velocityX = new Float32Array(size)
const velocityY = new Float32Array(size)

Float32Array is used because numerical operations are faster than with regular arrays, and memory usage is halved (64-bit to 32-bit).

Diffusion Step

Diffusion is approximated using Gauss-Seidel iteration:

function diffuse(
  b: number,
  x: Float32Array,
  x0: Float32Array,
  diff: number,
  dt: number,
) {
  const a = dt * diff * N * M
  for (let k = 0; k < 20; k++) {
    for (let j = 1; j <= M; j++) {
      for (let i = 1; i <= N; i++) {
        const idx = i + (N + 2) * j
        x[idx] =
          (x0[idx] + a * (x[idx - 1] + x[idx + 1] +
            x[idx - (N + 2)] + x[idx + (N + 2)])) /
          (1 + 4 * a)
      }
    }
    setBoundary(b, x)
  }
}

20 iterations is a trade-off between accuracy and speed. In FluidSimulation, reducing to 4 iterations produces virtually no visual difference.

Advection Step

Using the semi-Lagrangian method, values are interpolated by backtracing through the velocity field from each cell:

function advect(
  b: number,
  d: Float32Array,
  d0: Float32Array,
  u: Float32Array,
  v: Float32Array,
  dt: number,
) {
  for (let j = 1; j <= M; j++) {
    for (let i = 1; i <= N; i++) {
      const idx = i + (N + 2) * j

      // Backtrace
      let x = i - dt * N * u[idx]
      let y = j - dt * M * v[idx]

      // Clamp
      x = Math.max(0.5, Math.min(N + 0.5, x))
      y = Math.max(0.5, Math.min(M + 0.5, y))

      // Bilinear interpolation
      const i0 = Math.floor(x)
      const j0 = Math.floor(y)
      const s1 = x - i0
      const t1 = y - j0
      const s0 = 1 - s1
      const t0 = 1 - t1

      d[idx] =
        s0 * (t0 * d0[i0 + (N + 2) * j0] + t1 * d0[i0 + (N + 2) * (j0 + 1)]) +
        s1 * (t0 * d0[i0 + 1 + (N + 2) * j0] + t1 * d0[i0 + 1 + (N + 2) * (j0 + 1)])
    }
  }
  setBoundary(b, d)
}

Backtracing is counterintuitive, but it ensures numerical stability. Forward tracing would require handling overlap when determining "which cell it reaches."

Pressure Correction (Projection)

Helmholtz decomposition creates a divergence-free velocity field:

function project(
  u: Float32Array,
  v: Float32Array,
  p: Float32Array,
  div: Float32Array,
) {
  for (let j = 1; j <= M; j++) {
    for (let i = 1; i <= N; i++) {
      const idx = i + (N + 2) * j
      div[idx] = -0.5 * (
        (u[idx + 1] - u[idx - 1]) / N +
        (v[idx + (N + 2)] - v[idx - (N + 2)]) / M
      )
      p[idx] = 0
    }
  }
  setBoundary(0, div)
  setBoundary(0, p)

  // Solve Poisson equation with Gauss-Seidel
  for (let k = 0; k < 20; k++) {
    for (let j = 1; j <= M; j++) {
      for (let i = 1; i <= N; i++) {
        const idx = i + (N + 2) * j
        p[idx] = (div[idx] + p[idx - 1] + p[idx + 1] +
          p[idx - (N + 2)] + p[idx + (N + 2)]) / 4
      }
    }
    setBoundary(0, p)
  }

  // Subtract gradient from velocity field
  for (let j = 1; j <= M; j++) {
    for (let i = 1; i <= N; i++) {
      const idx = i + (N + 2) * j
      u[idx] -= 0.5 * N * (p[idx + 1] - p[idx - 1])
      v[idx] -= 0.5 * M * (p[idx + (N + 2)] - p[idx - (N + 2)])
    }
  }
  setBoundary(1, u)
  setBoundary(2, v)
}

Canvas API Rendering

Density values are mapped to colors and rendered. ImageData is manipulated directly for pixel-level painting:

function renderFluid(
  ctx: CanvasRenderingContext2D,
  density: Float32Array,
  width: number,
  height: number,
) {
  const imageData = ctx.createImageData(width, height)
  const cellW = width / N
  const cellH = height / M

  for (let j = 1; j <= M; j++) {
    for (let i = 1; i <= N; i++) {
      const d = Math.min(255, density[i + (N + 2) * j] * 255)
      const px = Math.floor((i - 1) * cellW)
      const py = Math.floor((j - 1) * cellH)
      // Paint pixels within the cell (simplified)
      const idx = (py * width + px) * 4
      imageData.data[idx] = d * 0.3     // R
      imageData.data[idx + 1] = d * 0.6 // G
      imageData.data[idx + 2] = d        // B
      imageData.data[idx + 3] = 255      // A
    }
  }
  ctx.putImageData(imageData, 0, 0)
}

Difference from FluidPipes

FluidPipes specializes in fluid "transport," with pipe network flow rate calculation as its main concern. It doesn't use Navier-Stokes; instead, it computes flow rates from pressure differences across each pipe — a much simpler model. They look similar, but the computational models are entirely different.

Summary: Tools for Browser Fluid Simulation

For understanding the mathematical foundations of fluid dynamics, the books on the tool shelf are excellent references. Reading Jos Stam's original paper "Stable Fluids" (1999) alongside them deepens understanding further.

FAQ

Q. Is it possible to run a real-time fluid simulation in the browser?
Yes. By applying a simplified 3-step Navier-Stokes process (diffusion, advection, pressure correction) to a 100x60 grid and using Float32Array with ImageData for fast rendering, it runs at 60fps.
Q. How many iterations are needed for the diffusion step in fluid simulation?
20 iterations with Gauss-Seidel is standard, but for real-time rendering, reducing to 4 iterations produces virtually no visual difference.
Q. What is the difference between FluidSimulation and FluidPipes?
FluidSimulation is a continuous fluid simulation based on the Navier-Stokes equations. FluidPipes is a discrete model that calculates flow from pressure differences across a pipe network — the computational approaches are entirely different.