Draw the winner outline as an unfilled pie after every slice: slices paint
in index order, so the next one covered the shared radial edge and the
outline stopped short of wrapping the wedge.
Derive the announcement colors from the winning slice - its own color as
the chip, a deepened version as the frame and the wedge outline - instead
of a fixed dark chip that read as unrelated to the wheel.
Raise the confetti above the announcement so the burst is no longer hidden
behind it, and steepen the launch fan so the field arcs around the name
rather than settling on it: horizontal reach is vx / drag, which parked the
flat fan at mid-canvas. Measured over the hold, particle samples inside the
pill drop from 22.4% to 6.2% while disc coverage holds at 85.5%.
Three advisory reviews with measured evidence: the confetti celebration, the
wheel and theme design, and the API/operator experience. The API findings are
untouched work - broken render:local CLI examples, 400/413/415 conditions
surfacing as 500s, an unenforced timeout guarantee, and a production token
default in compose.yml.
The refinement plan covers the second pass and records what was deliberately
excluded, including the bezel, the font-size cap, a night theme, and narrowing
the request schema.
README described confetti as reusing theme slice colors and made no mention of
the winner callout or the reveal timing.
Fill in the benchmark rows that were TBD, and record where the GIF growth
actually came from. Isolating each change shows the celebration rework, pill,
spin retune, palette reorder and pointer together account for +3.3%, while
grayscale label antialiasing accounts for +22.4% on its own because it affects
every spin frame rather than only the celebration. That trade is deliberate:
it removes visible color fringing and stops the palette being exhausted.
Also correct the size levers. Particle count is not one - cutting 43% of the
particles changes the file by 1.1%. Note that render times on this machine vary
more than 2x across repeat runs of identical input, so single timings are
indicative only.
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.
Update three transitive dependencies to their patched releases (fast-uri 3.1.4, find-my-way 9.7.0, postcss 8.5.22), clearing 3 high-severity Dependabot alerts. All were within existing parent semver ranges. Verified with pnpm audit (clean), lint, typecheck, 58 tests, and the API render smoke test.
Route the CLI through the shared winner picker instead of crypto.randomInt so the positive-integer guard and selection intent stay consistent with the API.
Drop prettier (no config, script, or eslint integration), the unused render-semaphore state() method and CreateRenderSemaphore typedef, and ignore .claude/settings.local.json.
The guard compared import.meta.url against 'file://'+argv[1], which never matched on Windows (mismatched slashes and drive prefix), so the process started without ever calling app.listen(). Compare import.meta.filename to argv[1] instead.
Deterministic frame-based confetti fired from the wheel center during the winner hold, seeded by winner index and colored from the theme palette. Uses Remotion's random() so each GIF frame renders identically.