Salon Noir: a private 3D casino in the browser (blackjack & roulette, no build step)

**Play it:** https://onealper.github.io/salon-noir/

**Source:** GitHub - onealper/salon-noir: Salon noir blackjack and roulette simulation · GitHub

Salon Noir is a first-person casino I made for a few friends to play at home: three blackjack tables (Classic, Free Bet, Lightning) and three roulette tables (European, Lightning, Mega Lightning). It’s play chips only. Croupiers with their own voices deal at real pace, AI players sit at the table, and it runs on desktop and on an iPhone held upright, where it installs as a home-screen app and plays offline.

It’s plain HTML and ES modules, with three.js r169 vendored, no build step and no framework. It’s served from any static folder.

## A few things that might interest this forum

**Render on demand, down to zero.** Each room draws only when something changes. Animations register as tickers flagged `still` (moves no shadow caster, so the shadow map is kept) or `lazy` (asks for its own frames). Frames are paced to 60/s on any refresh rate. When nothing moves, nothing is drawn: no rAF loop and no CSS animation left running. On a MacBook Air M4 the page sits at 0 renders at rest, and a frame-time watcher steps the pixel ratio down if a GPU can’t keep up.

**A roulette ball that’s simulated, not animated.** The ball and wheel follow a physics model (after Small & Tse, 2012). The number is drawn when the croupier calls “No more bets”, and the ending is chosen from tries simulated a frame at a time while the ball runs, so it lands in the drawn pocket without ever being pushed. The wheel’s depth is painted occlusion, so neither it nor the ball needs a shadow map, and the fine chrome is given real widths and a little roughness so it doesn’t sparkle while it turns.

**A clear acrylic card shoe.** A custom shader handles both surfaces with Fresnel, absorption follows Beer–Lambert, and the cut edges refract a reflection probe of the real table with a little dispersion. The card reader in its tongue glows red as each card slides over it. And the cards bend like paper as they’re drawn and turned.

**Lightning drawn the way lightning behaves.** A dim stepped leader feels its way down, then the return stroke lights the channel many times brighter, restrikes along the same path one to three times and leaves a violet afterimage. Where it grounds there’s a hit-stop of a few frames, a shockwave, a branching Lichtenberg burn that cools from blue-white to ember, sparks and smoke. The multiplier is forged onto the cloth as a plaque.

**A portal between the two rooms.** Each game has its own renderer and scene. Switching games in the lobby opens a ring in the floor that swallows the table, while the incoming room is laid out unseen and warmed with `renderer.compileAsync`. Under a flash the rooms change places, and the new table drops out of a ring in the air, cut off at the ring by a clipping plane on its materials. It lands with a squash and a ring of dust, and the camera cranes up into the lobby orbit.

**One lesson for anyone layering CSS over a canvas:** in Firefox (and Zen, built on it), `backdrop-filter` over an animating WebGL canvas made the whole window recomposite every frame, and a fanless laptop ran hot where Chrome stayed cool. I dropped the blur in Gecko browsers only, behind `@supports (-moz-appearance: none)`.

## Also in there

- Web Audio with five buses: cards and chips panned to where they land, music that ducks under the dealer, and a ball roll that is synthesised live rather than recorded.

- Hands survive a reload: each blackjack hand is saved move by move and played out on your next visit.

- Graphics presets, Reduce motion, and a Flashing effects setting (Full / Soft / Off) for the lightning.

Feedback is very welcome, especially on rendering cost: the busiest frames are the spins (about 2.8 MP with 4× MSAA, ~94 draw calls, a few clearcoat and sheen materials), and I’m weighing baked lightmaps from Blender next.

## Screenshots