77 Commits
Author SHA1 Message Date
tiennm99 ffa9729683 revert: restore tree to 757e41d869f20acbecfee64642514878b18dd528 2026-07-28 10:42:34 +07:00
tiennm99 3b28e46069 fix(remotion): tie the winner celebration to the winning slice
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%.
2026-07-27 17:42:38 +07:00
tiennm99 8a9e3696c4 docs(plans): add UI/UX review reports and the refinement plan
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.
2026-07-25 15:18:21 +07:00
tiennm99 11750ac552 docs: describe the new celebration and record measured render costs
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.
2026-07-25 15:18:21 +07:00
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
tiennm99 ad3e50b7f4 docs: add confetti and staleness review report 2026-07-25 02:27:50 +07:00
tiennm99 07e43960b8 chore(deps): patch fast-uri, find-my-way, and postcss advisories
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.
2026-07-25 01:55:09 +07:00
tiennm99 a6c65768f5 test: cover render concurrency limit and winner picker 2026-07-25 01:17:19 +07:00
tiennm99 8fece5fd7f refactor(cli): reuse pickWinnerIndex for local winner selection
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.
2026-07-25 01:17:18 +07:00
tiennm99 ccb91adef4 fix(security): compare API token in constant time
Length-guarded timingSafeEqual avoids leaking token information through response timing on the auth path.
2026-07-25 01:17:18 +07:00
tiennm99 5048fff952 chore(schema): drop unused exported constants
themes, allowedSizes, and allowedFps had no importers; the theme list already lives in the request schema.
2026-07-25 01:17:18 +07:00
tiennm99 e20169bd62 docs: document winner confetti celebration 2026-07-25 01:17:18 +07:00
tiennm99 e8a3f87e3b chore: remove unused prettier dep and dead code
Drop prettier (no config, script, or eslint integration), the unused render-semaphore state() method and CreateRenderSemaphore typedef, and ignore .claude/settings.local.json.
2026-07-24 23:34:33 +07:00
tiennm99 f053682f84 fix(server): bind entrypoint on Windows
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.
2026-07-24 23:34:25 +07:00
tiennm99 376a7ae688 feat(renderer): add winner-reveal confetti burst
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.
2026-07-24 23:34:13 +07:00
tiennm99 8de459b131 fix(renderer): use consistent wheel label colors 2026-07-22 14:34:01 +07:00
tiennm99 566b06a461 feat(renderer): add local GIF render CLI 2026-07-15 15:50:34 +07:00
tiennm99 963e8087a8 docs: clean legacy plans and document local GIFs 2026-07-15 15:01:00 +07:00
tiennm99 dfd20b699a docs(renderer): record adaptive label rollout 2026-07-15 14:42:57 +07:00
tiennm99 440ec899d2 fix(renderer): improve adaptive label readability 2026-07-15 14:42:33 +07:00
tiennm99 2e1c3be0a4 docs(renderer): record wrapped label rollout 2026-07-15 12:21:25 +07:00
tiennm99 d57009e63b feat(renderer): add bold wrapped wheel labels 2026-07-15 12:21:14 +07:00
tiennm99 4632e87150 fix(renderer): apply single eased wheel spin 2026-07-07 00:11:32 +07:00
tiennm99 8ab45eb5ea fix(renderer): correct pie slice angle conversion 2026-07-06 23:44:41 +07:00
tiennm99 407d4f3a2c fix(renderer): align wheel pointer and labels 2026-07-06 23:12:51 +07:00
tiennm99 da92e9b443 fix: align wheel labels and compose env 2026-07-06 22:50:11 +07:00
tiennm99 3f5ed700bb feat: initialize wheelofnames API 2026-07-06 22:20:48 +07:00