Files
tiennm99bot/renderer
tiennm99 f9accfd70a feat(remotion): rework winner celebration and spin motion
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.
2026-07-25 15:18:04 +07:00
..
2026-07-06 22:20:48 +07:00
2026-07-06 22:20:48 +07:00
2026-07-06 22:20:48 +07:00
2026-07-06 22:20:48 +07:00

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-Index
  • X-Wheel-Winner
  • X-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.