A falling-sand cellular automaton written in Rust, compiled to WebAssembly, and rendered in the browser.
Paint sand, water, stone, fire, and wood onto a 320×200 grid. The
page loads a small wood-and-fire scene so burning is visible
immediately. The simulation runs in WASM: a flat Vec<Cell>, no
per-frame allocation, and a thin wasm-bindgen boundary so the
core stays unit-testable with plain cargo test.
- Performance — dense grid, no GC pauses in the hot loop
- Safety — particle swaps without undefined behavior
- Portability — one codebase for native tests and the browser
| Input | Action |
|---|---|
| Click / drag | Paint the selected material (gapless stroke) |
| Shift-drag | Straight line (horizontal, vertical, or 45°) |
| Right-click | Erase |
| Brush slider | Radius 1–8; [ ] or scroll wheel |
| 1–9 / 0 / O I G | Select material |
| Arrow keys | Fan facing |
| Scenes | Sink, Float, Burn, Wind, Glass presets |
| Pause / Step | Freeze the sim; advance one tick |
Space / . |
Pause toggle / single-step |
? |
Toggle shortcut overlay |
| Hover | Material + heat under the cursor |
| Sand | Falls and piles; sinks through water |
| Water | Flows and spreads |
| Stone | Static obstacle |
| Fire | Rises, flickers, burns out; heats neighbors |
| Wood | Static; ignites when heat reaches 80 |
| Steam | Rises; condenses when cool; water boils next to fire |
| Fan | Four facing buttons (or arrow keys); blows gases in that 4×4 |
| Gunpowder | Falls like sand; explodes from fire or heat 80 |
| Smoke | Rises slower than steam; leftover from burned-out fire |
| Oil | Floats on water; ignites from fire or heat 60 |
| Ice | Static; melts to water at heat 25; water freezes below 15 |
| Glass | Sand baked at heat 180; locked like stone |
| Erase / Clear | Remove cells |
Requires a Rust toolchain, just,
wasm-pack, and
miniserve.
cargo install just wasm-pack miniserve
just serve # http://localhost:8080
just verify # fmt, clippy, tests, wasm-pack — same as CIPushes to main run that suite and deploy the site to GitHub Pages.
Native World::step() at 960×600, release, 300 ticks
(just bench):
| Scene | Before chunks | After chunks |
|---|---|---|
| Sparse (1 grain) | 1,014 steps/s | 16,303 steps/s (~16×) |
| Dense (top quarter) | 833 steps/s | 604 steps/s |
Sparse worlds skip idle 16×16 chunks. A full grid still visits every active chunk, so dense scenes are not faster. The live demo stays at 320×200; these numbers are simulation throughput, not browser composite FPS.
| File | Role |
|---|---|
src/world.rs |
Grid storage and simulation rules |
src/renderer.rs |
Grid → RGBA frame buffer |
src/lib.rs |
Thin wasm-bindgen boundary |
index.html |
Canvas, palette UI, rAF loop |
justfile |
Local tasks matching CI |
MIT © Joseph Chow