Skip to content

Repository files navigation

Powder Lab ⚗️

A falling-sand sandbox in the browser — paint ~14 materials and watch them flow, burn, dissolve, and react. Built with React + TypeScript + Vite, with the entire 60fps simulation running on a plain canvas outside React's render cycle.

▶ Play it live — multiplayer, start a room and share the link

▶ Single-player mirror (GitHub Pages, no relay)

Powder Lab — basin scene

Features

  • 14 materials across powders, liquids, solids, and energy: sand, water, oil, acid, lava, stone, wood, plant, ice, fire, smoke, steam, gunpowder, walls.
  • Real reactions:
    • 🌋 Lava + water → stone + steam
    • 🔥 Fire spreads through wood / oil / plants and gets doused by water (→ steam)
    • 🧪 Acid dissolves solids and powders
    • 🌱 Plants creep along water
    • 🧊 Ice melts near heat and slowly freezes adjacent water
    • 💥 Gunpowder chain-detonates
  • Density-based layering — oil floats on water, lava sinks through everything, sand rests under water.
  • Real-time bloom/glow on fire and lava.
  • Buttery 120 fps thanks to a dirty-chunk scheduler that only simulates regions where something is actually moving.

Powder Lab — explosion + glow

Controls

Action Input
Draw Drag on the canvas
Erase Right-click drag
Faucet Hold the pointer still
Select material Number / letter keys (1 sand, 3 water, 6 lava, F fire, …)
Pause / play Space
Clear C
Toggle glow G
Brush size [ and ]

Run it

npm install
npm run dev      # http://localhost:5173
npm run build    # typecheck + production build into dist/
npm run preview  # serve the production build

Multiplayer

npm run server   # the relay, ws://localhost:8787
npm run dev      # in another terminal

Open the app, start a room from the pill in the title bar, and share the ?room=CODE link. The relay holds rooms in memory only; there is no database and no account.

Multiplayer

Rooms run deterministic lockstep: every client simulates the same grid and only paint strokes cross the network, stamped with the tick at which everyone applies them. Streaming grid deltas instead would be megabytes per second at 200x150 cells and 75 ticks per second; strokes are tens of bytes.

The price is that the simulation must be bit-identical everywhere, so all randomness comes from one seeded generator whose state travels inside a serialized snapshot, and every mutation — paint, magnet, lightning, clear, scene load, pause — goes over the wire rather than being applied locally first. Room speed is pinned to 1x for the same reason. Clients checksum their grid every 300 ticks; a mismatch pulls a fresh snapshot from the oldest peer and replays the inputs since.

Deployed at powder.ssscribe.app: the static bundle behind Caddy, the relay as a systemd service reachable only through the proxy at /relay. The GitHub Pages build has no relay, so it is single-player.

docs/multiplayer-protocol.md is the full specification. server/ is a plain ws relay that stamps, orders and forwards opaque blobs; it never parses a simulation event.

Known limits in this version: no automatic rejoin after a dropped socket, and a client joining after someone picked a gallery scene gets the room's grid but keeps its own lighting toggles.

How it works

The interesting architectural choice: React owns the chrome, an imperative core owns the frame.

src/
  sim/materials.ts    # material IDs + property tables (density, flammability) + UI palette
  sim/Simulation.ts   # the engine: cellular-automaton rules, reactions, chunk scheduler, renderer
  useSimulation.ts    # the rAF loop + pointer/keyboard input — the bridge between React and the sim
  App.tsx             # toolbar / canvas / controls UI
  • No setState in the hot loop. The render loop reads a mutable useRef config object, so changing the brush or material never triggers a React re-render. React state is just a mirror for displaying the toolbar.
  • Dirty-chunk scheduling. The grid is split into 16×16 chunks; each frame only simulates chunks flagged active, and any cell that changes wakes its neighborhood for the next frame. Settled regions cost nothing — the same trick the game Noita uses.
  • Data-driven materials. Each material is an ID plus a row of properties. Complex behavior (pyramids, oil/water separation, fire fronts) emerges from a handful of local rules.

License

MIT

About

A falling-sand sandbox in the browser — ~14 reacting materials, real-time glow, 60fps. React + TS + Vite.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages