Case study · CS-001 · Computational fluid dynamics

Vortex — a Navier–Stokes solver for the browser.

The wake behind this text is not a video. An incompressible flow solver is running on your GPU right now, shedding a Kármán street off the cylinder at the golden section. Drag through it; the pressure solve will put it back.

Solver live · this page is the demo
RoleDesign · Engineering Year2026 StackWebGL 1/2 · GLSL · Vanilla JS WeightOne file · zero dependencies StatusRunning behind this page
§ 01The problem

Fluid simulation is a desktop luxury. It shouldn't be.

CFD lives in overnight batch jobs and licensed workstations. But the qualitative behavior that makes flow legible — recirculation, shedding, the way a wake forgets a disturbance — fits comfortably in a browser tab if you spend the milliseconds where they matter.

The brief I set myself: a real Navier–Stokes solver, one HTML file, no dependencies, 60 fps on a mid-range phone — honest enough that an engineer wouldn't wince, and beautiful enough to be the front door of a portfolio.

§ 02The physics

Incompressible flow, split into solvable pieces.

The solver integrates the incompressible Navier–Stokes equations:

∂u/∂t + (u · ∇)u = −∇p + ν∇²u,   ∇ · u = 0 momentum · continuity

Following Stam's stable fluids scheme, each frame splits the operator into steps that are individually unconditionally stable — the simulation cannot blow up, only blur:

  1. Advection — semi-Lagrangian: each cell traces its velocity backward through time and samples where it came from. Stability for free, at the cost of numerical diffusion.
  2. Vorticity confinement — the diffusion tax is paid back by measuring curl and re-injecting force toward vortex centers, keeping eddies crisp at scales the grid would otherwise smear away.
  3. Pressure projection — a Jacobi relaxation (24 sweeps per frame) solves the Poisson equation for pressure, whose gradient is subtracted to make the field divergence-free. This is the step that makes it incompressible — and the reason a disturbance heals.
  4. Boundary conditions — a soft inlet on the left holds the free stream, walls close top and bottom, and a no-slip cylinder sits at x = 0.382·L: the golden section of the working section, because the composition system governs the physics too.

Nothing above is a trick: the Kármán street you see is not animated. It is an instability of the solved velocity field — put a bluff body in a steady stream and symmetry breaks on its own, at a shedding frequency the solver chooses, not me.

§ 03The engineering

Where the milliseconds actually go.

The state lives in ping-ponged floating-point textures — velocity, pressure, divergence, curl, and dye — and every solver step is one fragment-shader pass. Velocity resolves on a coarse grid; the dye (the visible gold) advects through it at five times the resolution, which is why the streaklines stay silky while the physics stays cheap.

Velocity grid200 × H (128 on phones)
Dye resolution1024 (448 on phones)
Pressure sweeps24 Jacobi / frame, governor-shed to 12
Frame budget16.6 ms, self-measured
Texture formatRGBA16F, verified renderable at boot
Warm start160 pre-steps behind the reveal

What broke, and what fixed it: a family of Android GPUs renders half-float textures but refuses to filter them — the advection shader carries a manual-bilinear fallback selected at boot. The pressure field is warm-started at 0.8× the previous frame instead of cleared, halving the sweeps needed for the same divergence. And the wake is fully developed before the page reveals: 160 solver steps run behind the loading state so the first thing you see is already interesting.

On touch devices, a gesture lock resolves the classic conflict: a horizontal drag stirs the flow and is captured; a vertical drag scrolls and is not. The page never fights your thumb.

§ 04What it taught me

Constraints are a style.

Everything distinctive about this piece came from refusing an easier version of it. No library meant understanding every pass. One file meant every byte arguing for its place. A phone budget meant building the quality governor — which then made the desktop version unshakeable. The golden-section cylinder placement started as a composition rule and ended as the thing people ask about.

The same architecture — split operators, ping-pong state, honest fallbacks — now underpins the beam simulation on the front page and the geodesic renderer in the observatory. One discipline, three instruments.

← All work Next: CS-002 Millwright — in writing Discuss this →