Over the last few weeks I built a small rack of physics simulations — 12 of them — that all run client-side in a single HTML file each. No backend, no WebGL frameworks, no build step you can't read. Just <canvas>, requestAnimationFrame, and the actual equations.
Here's the whole rack if you want to click around first: lkforge.com/tools/physics. Below I'll walk through the two I'm happiest with — reaction-diffusion and the Lorenz attractor — because they show two very different flavors of "simple local rule → surprising global behavior."
1. Reaction-diffusion: Turing patterns from two numbers per cell
The reaction-diffusion lab runs the Gray-Scott model: two virtual chemicals U and V sit on a grid, diffuse at different rates, and react via U + 2V → 3V. U is fed in; V is killed off. That's the entire model, and it reproduces spots, stripes, mazes, dividing cells and coral — the same short-range-activation / long-range-inhibition idea Alan Turing proposed in 1952 to explain animal coat patterns.
The core update, on a toroidal grid with a 9-point Laplacian:
var uu = u[c], vv = v[c];
var lapU = (u[W] + u[E] + u[N] + u[S]) * 0.2 +
(u[NW] + u[NE] + u[SW] + u[SE]) * 0.05 - uu;
var lapV = (v[W] + v[E] + v[N] + v[S]) * 0.2 +
(v[NW] + v[NE] + v[SW] + v[SE]) * 0.05 - vv;
var uvv = uu * vv * vv;
un[c] = uu + (Du * lapU - uvv + f * (1 - uu)) * dt; // U: diffuse, react away, feed
vn[c] = vv + (Dv * lapV + uvv - (f + k) * vv) * dt; // V: diffuse, react in, kill
Two things I learned the hard way:
-
The feed/kill pair
(f, k)is everything. Small changes switch the whole regime — coral at(0.0545, 0.062), mitosis near(0.046, 0.063), spots at(0.030, 0.062). I ship these as presets so people don't land on a dead grid. - Seed with a noisy patch, not a solid disk. A solid blob tends to bloom once and then die back to uniform grey. Random speckle lets the pattern nucleate across an area and reach a stable steady state.
Rendering is a createImageData grid mapped through a small color lookup table, drawn to an offscreen canvas and scaled up with drawImage — cheap enough to run a 200×200 grid at 60fps with ~10 solver steps per frame.
2. The Lorenz attractor: determinism without predictability
The Lorenz lab integrates the 1963 Lorenz equations with fourth-order Runge-Kutta and draws the trajectory as a rotating, fading 3-D trail:
function lorenzDeriv(s) {
var x = s[0], y = s[1], z = s[2];
return [SIG * (y - x), x * (rho - z) - y, x * y - BETA * z];
}
The fun part is the butterfly effect made visible: start a second point 1e-3 away from the first, integrate both under the identical rule, and show the separation climbing. The two paths track together, then peel apart onto opposite wings of the attractor — neither ever leaving the shape, neither ever repeating. That's the whole point Edward Lorenz made when a rounded weather-model input (0.506127 → 0.506) sent his forecast somewhere unrecognizable.
A couple of implementation notes:
-
RK4, not Euler. Euler visibly distorts the attractor as the path stretches and folds; RK4 with a small
dtkeeps it honest. - Orthographic projection + one yaw/pitch rotation is enough — no full 3-D pipeline. Drag updates yaw/pitch; an auto-spin adds a constant yaw increment per frame.
- The same engine also renders Rössler and Aizawa attractors by swapping the derivative function.
Why single-file, client-side?
Three reasons that turned out to matter:
- Longevity. No server means nothing to keep alive; these will still run in five years.
-
Embeddable. Because each sim mounts on one canvas id and wires to controls by id, the exact same script drives the tool page and an
/embed/widget — drop it into a blog post with one iframe. - Readable. Anyone can view-source and see the actual physics, which is kind of the point for teaching material.
If you want to poke at the rest — falling sand, a double pendulum, the double-slit experiment, charged particles in a magnetic field, Conway's Game of Life — they're all here: lkforge.com/tools/physics.
Happy to answer questions about any of the models in the comments.
Top comments (0)