TL;DR
You don’t need WebGL for convincing dice. With a single requestAnimationFrame loop, a tiny 2D physics model, and pooled WebAudio, rolls feel real, stay reproducible, and run smoothly on mobile.
Why 2D, not 3D?
Three reasons: performance, determinism, and size. We target 60fps on desktop / 30fps+ on phones, deterministic replays via seeded randomness, and a small bundle. A carefully tuned 2D model gives us 90% of the “casino feel” with a fraction of the cost.
From gesture to throw
A roll begins with a flick gesture:
- Initial linear velocity: direction and magnitude from the drag vector.
- Initial angular velocity: a randomized spin tied to flick speed (not just a constant).
- Staggered starts: when rolling 5–10 dice, we offset launch times by a few milliseconds so they don’t overlap perfectly.
That’s enough to make each roll personal while preserving determinism under a fixed seed.
Collisions, friction, and that last wobble
We use a 2D rigid body approximation with:
- Wall collisions: low elasticity so dice don’t ping-pong forever.
- Die–die collisions: slightly lower restitution than walls; impulse-based response.
- Friction: dynamic → static; when linear and angular speeds fall below thresholds for ~0.5s, we snap to a stable face.
The “casino wobble” comes from asymmetric damping: linear speed decays faster than angular, then flips as the die starts to skid. It looks right and converges quickly.
Sound sells the illusion
All sounds come from a pooled WebAudio bus:
- Click: small rim contacts; short, higher-pitched.
- Thunk: heavy impacts; pitched down and slightly louder.
- Settle: a soft, low click as the last micro-bounce disappears.
We cap concurrent voices (e.g., 6) and randomize pitch ±7% to avoid repetition. On mobile, audio starts only after the first user interaction—then it works consistently.
Reproducibility with seeds
Every roll is driven by a seeded PRNG. The same seed + same gesture produce the same collision sequence and the same final face(s). That’s why our Share links can replay both process and outcome across devices.
Performance guardrails
- One RAF per page; clamp Δt ≤ 50 ms so missed frames don’t explode the simulation.
- Canvas-first rendering (no DOM animation for dice); hit-tests and HUD stay minimal.
- Auto-degrade: if fps < 45 for a second, we downshift shadows/particles.
Test-yourself checklist
- Ten dice, hard flick → multiple wall hits, varied spins, no “phase lock.”
- Light flick → short slide, believable micro-bounces.
- Same seed, three repeats → identical end faces and order.
- Phone test → 30fps+ and zero console errors.
Try next:
Roll some Dice with 6–10 pieces and compare stats.
See how seeding works.
Contrast with Wheel vs. Coin vs. Dice.