A single-file three.js coin explosion. Coins burst out of the centre of the screen straight into the camera, collide with each other in real time, and catch the light like polished gold. No build step, no npm install, no bundler — one index.html you can open by double-clicking it.
Drop in any .glb and it becomes the coin.
- Real-time collision physics. A hand-rolled solver — gravity, impulse resolution with configurable restitution, spatial-hash broad phase, fixed sub-steps at 1/120 s — so hundreds of coins bounce off each other instead of passing through.
- Instanced rendering. Every coin is one
InstancedMeshinstance: a single draw call for the whole swarm, 700-coin pool, 60 fps on a mid-range GPU. - Procedural HDR environment. No external
.hdrdownload — the environment map is painted to a canvas and run throughPMREMGeneratorat startup, so gold has something real to reflect. - Bring your own model. Drag a
.glb/.gltfanywhere on the page, or use the Load your own button. Multi-mesh files are flattened and merged automatically. - Built for video work. Green-screen and true-alpha background modes, plus one-click WebM capture straight from the canvas — drop the clip into After Effects / Premiere and key it out.
- Bounded lifetime. Every coin is hard-capped at 6 seconds and recycled through a pool, so the scene never accumulates cost no matter how long it runs.
- Works on phones. Below 820px the control panel becomes a bottom sheet behind a hamburger button — swipe it down or tap the backdrop to dismiss. Touch builds also drop the pixel ratio, halve the bloom resolution and start at a lighter coin count, so a mid-range phone still holds a steady frame rate.
- Zero dependencies to install. three.js is pulled from a CDN via an import map; the demo model is inlined as base64, so the page also works from
file://.
git clone https://github.com/USERNAME/coin-jackpot.git
cd coin-jackpotThen open index.html in a browser. That's it — there is no build step.
To serve it locally instead (needed only if you swap the inline model for a fetched file):
npx serve . # or: python3 -m http.server 8080| Control | What it does |
|---|---|
| Coins per second | Emission rate, 2–160 |
| Animation speed | Time scale for the whole simulation, 0.15×–2.5× |
| Launch power | Initial velocity out of the emitter |
| Gravity | Downward acceleration; 0 gives a weightless space-drift look |
| Coin size | World radius — also drives the collision radius and the emitter spread |
| Spin | Angular velocity on spawn and the tumble kick on impact |
| Bloom | UnrealBloomPass strength |
| Restitution | Collision bounciness, 0 = fully inelastic, 0.9 = superball |
| Roughness | Gold material roughness — low is mirror-like, high is brushed |
| Background | Dark / green screen / transparent alpha |
| Key | Action |
|---|---|
H |
Hide or show the control panel (hamburger button on touch devices) |
Space |
Pause / resume |
B |
Fire an extra burst |
Drag any .glb or .gltf onto the page. The loader:
- bakes world matrices into every mesh it finds,
- strips everything down to position / normal / uv and de-indexes it, so merging is safe for arbitrary files,
- merges the parts into one geometry, recenters it, and measures its bounding sphere.
That bounding sphere is what the Coin size slider scales against, so a model authored in millimetres and one authored in metres both land at the same on-screen size. Textures and materials from the file are discarded on purpose — every instance shares one gold MeshStandardMaterial, which is what keeps the whole swarm at a single draw call.
To bundle a different model permanently, base64-encode it and replace the DEMO_GLB constant in index.html:
base64 -w0 my-coin.glbThe simulation is deliberately not a physics-engine integration. Coins are treated as spheres with equal mass:
- Broad phase — a spatial hash keyed on
floor(pos / cellSize), cell size equal to one coin diameter, so each coin only tests the 27 cells around it. - Narrow phase — sphere overlap, split depenetration (clamped so a dense spawn cluster can't launch coins out of frame), then a standard impulse
j = -(1 + e)·v_rel / (1/m₁ + 1/m₂)along the contact normal. - Rotation — quaternion integration from an angular-velocity vector, with a random tumble impulse added on each impact and a cap to keep things readable.
- Sub-stepping — the frame delta is split into at most six fixed steps so the solver stays stable when you push Animation speed to 2.5× or the tab drops frames.
Emission is a cone with a strong +Z bias from a small disc at the world origin; coins retire when they pass the camera plane, leave the frustum, or hit the 6-second cap, and their pool slot is immediately reusable.
Any browser with WebGL 2 and ES module import maps — Chrome / Edge 89+, Firefox 108+, Safari 16.4+. WebM capture uses MediaRecorder and is Chromium-only in practice; the button disables itself elsewhere.
MIT — see LICENSE. The bundled demo coin in assets/ is included as sample content; swap it for your own artwork in anything you ship.