The animation's weakest moment was its final frame, which is also the frame most chat clients show as the GIF's poster image. Celebration: - Confetti launches from two cannons framing the wheel instead of bursting inside the hub, using closed-form drag ballistics so chips decelerate and flutter rather than flying a clean parabola. The field previously drained from 70 particles to 3 before the GIF ended; it now stays populated, and a 2500ms hold ends with 68 on screen instead of 0. - The opening frame rendered at opacity 0, throwing away the one frame where particles are still clustered. Removed the fade-in and made the tail frame-relative and quantized to bound its palette cost. - Chips streak along their direction of travel in proportion to speed, faking the motion blur a GIF cannot carry. A second volley, sized to the celebration window, keeps long holds from thinning out. - Each theme carries its own confetti palette. Slice colors are tuned to hold dark text over a large area, which left five of six mono colors under 2.4:1 against the background and invisible at particle size. - The winner's name appears over the hub during the hold. At a 200px chat display the pointer is ~17px and a slice label ~9px, so the poster frame did not say who won. - Winner emphasis is an opaque stroke, not a drop-shadow. A filter on a Pie paints outside the shape, so later siblings overpainted it and only one neighbour ever showed the halo. Motion: - Turn count scales with the frame budget and slice width. A fixed 5 turns put peak speed at ~40deg per frame regardless of option count, so 65 of 98 frames advanced more than a full slice at 32 options and the wheel appeared to run backwards. Turn counts floor rather than round, since rounding up reintroduces the aliasing, and stay integral so the winner still lands on the detent. - The spin launch now blends into the deceleration at a matched slope. The previous curve started at maximum speed, and a naive ease-in jumped from 18 to 41deg per frame in a single frame. - A damped recoil rocks the wheel into its detent, resolving to exactly zero so the landing and the flapper's rest position are unaffected. - The pointer is drawn as SVG with a real outline and a tick as slices pass. clip-path had been discarding its border and shadow entirely. Legibility: - Labels flip by their rest-frame angle so every name reads upright in the final frame. This reverses an earlier decision to leave them slice-aligned; three of eight names rendered mirrored on a default wheel. - Label text renders inside a promoted compositing layer, which switches Chromium from LCD sub-pixel to grayscale antialiasing. Sub-pixel fringes put 2178 colored pixels into an all-grayscale theme and consumed 255 of 256 palette entries. Promoting the one rotating container rather than each label keeps render time flat on dense wheels. - Line boxes leave room for stacked Vietnamese diacritics and shrink to fit rather than clipping. - Slice colors are ordered for colorblind separation, and the last slice avoids reusing the first slice's color when the option count leaves a remainder of one.
wheelofnames
Self-hosted API that renders wheel-of-names GIF animations with Remotion.
API
POST /api/gif
Content-Type: application/json
Accept: image/gif
Authorization: Bearer change-me
{
"options": ["alice", "bob", "carol"],
"winnerIndex": 1,
"durationMs": 6500,
"holdMs": 1200,
"fps": 15,
"size": 512,
"theme": "classic"
}
Response is image/gif with winner metadata headers:
X-Wheel-Winner-IndexX-Wheel-WinnerX-Render-Duration-Ms
X-Wheel-Winner is URL-encoded so non-ASCII labels are safe in HTTP headers.
The GIF includes a winner celebration with a deterministic confetti burst during the hold phase, using colors from the chosen theme palette.
Local
Install dependencies and Chromium once:
pnpm install
pnpm browser:ensure
Start the local API:
pnpm dev
Generate GIF files locally
Generate the quick smoke fixture at the git-ignored path
fixtures/smoke.gif:
pnpm render:smoke
Generate the complete fixture set at fixtures/smoke.gif,
fixtures/vietnamese.gif, and fixtures/sixteen-options.gif:
pnpm render:fixtures
Render a custom GIF directly without starting the API server:
pnpm render:local -- `
--output wheel.gif `
--option "Chiều nay uống CraneTea" `
--option "Chiều nay uống CraneTea" `
--option "Cà phê" `
--winner 1
macOS, Linux, or Git Bash:
pnpm render:local -- \
--output wheel.gif \
--option "Chiều nay uống CraneTea" \
--option "Chiều nay uống CraneTea" \
--option "Cà phê" \
--winner 1
--winner is a zero-based index and is random when omitted. Run
pnpm render:local -- --help for duration, hold, FPS, size, theme, and timeout
options. The documented root wheel.gif and fixtures/*.gif outputs are
git-ignored and safe to delete; custom output paths may need their own ignore
rule.
Verify
Run the API smoke test and quality gates:
pnpm api:smoke
pnpm lint
pnpm typecheck
pnpm test
Deploy
Use a container runtime first. Static-only platforms cannot satisfy
POST /api/gif because Remotion server rendering needs Node, Chromium/runtime
dependencies, and FFmpeg/compositor support.
docker build -t wheelofnames .
docker run --rm -p 3000:3000 -e API_TOKEN=change-me wheelofnames
Recommended starting resources: 1-2 vCPU and 1-2 GB RAM, with
MAX_CONCURRENT_RENDERS=1. RENDER_TIMEOUT_MS defaults to 15000 and is
raised to Remotion's 7000ms browser timeout floor when configured lower.
API_TOKEN is required when NODE_ENV=production.