Commit Graph
67 Commits
Author SHA1 Message Date
tiennm99 2bb5108151 feat(renderer): deploy the renderer next to the bot in compose
The bot now reaches the renderer at http://renderer:3000/api/gif on the
compose network instead of a separately deployed service. Both share
WHEELOFNAMES_API_TOKEN. CI runs the renderer's lint, typecheck, tests,
render smoke, and image build.
2026-10-03 11:16:54 +07:00
tiennm99 c1393f0ec8 style(gacha): slow the mirror glint sweep to 1800ms 2026-10-02 17:28:18 +07:00
tiennm99 994749cee4 style(gacha): drop the glint flare and slow the streaks to a closer pair
Remove the four-ray star from the mirror glint, bring the two streaks
closer and make them wider, and run the sweep for 1200ms on ease-in-out.
2026-10-02 16:13:42 +07:00
tiennm99 4f80538d51 style(gacha): widen and soften the mirror glint, add a trailing streak
The glint band now fades out over a wider span at a lower peak, a thin
streak trails it, and the sweep runs 800ms on easeInOutSine.
2026-10-02 15:52:09 +07:00
tiennm99 c08839d59e style(gacha): cap the mirror glint at 120/255 alpha 2026-10-02 15:29:39 +07:00
tiennm99 c4bc131372 fix(gacha): blend the mirror glint additively and sweep the whole card
The glint band was a card-sized box sliding diagonally, so the top-right
and bottom-left corners never fell inside it. Draw the band on a box three
times the card's size, centred on it, so every corner is lit.

Blend the glint with plus-lighter and tint it with the card's accent so it
brightens the card's own tone instead of painting flat white over it.
2026-10-02 15:17:13 +07:00
tiennm99 21b9e596bc feat(gacha): put the moon emblem on the pack and card back
The pack prints a crescent moon in its diamond in place of the default star, and the card back carries the same moon in its diamond seal over the miti99bot caption.
2026-10-02 14:57:06 +07:00
tiennm99 dc65ce7df2 feat(gacha): show the card text on its last turn face up
The text switches on as the spinning card turns edge-on for the last time, so it rides in on the final turn instead of fading in after the spin stops.
2026-10-02 14:29:18 +07:00
tiennm99 b48f7602f9 feat(gacha): clear printed text and add a mirror glint on reveal
The pack and the card corners no longer carry text. The card's text stays hidden while it spins and fades in once it lies flat, then a mirror glint sweeps from the top-left corner to the bottom-right as the card comes to rest.
2026-10-02 14:17:09 +07:00
tiennm99 59f597016b feat(genshin): serve the Remotion meteor wish on /api/genshin
Brings back the Genshin-style meteor wish that /api/gacha rendered before pack-cards, under genshin names. It takes the same request as /api/gacha and returns the 7-second landscape MP4.
2026-10-01 23:37:27 +07:00
tiennm99 b7a4336a7b chore(gacha): drop the last references to the retired beta route
Removes the test that checked /api/gachabeta answers 404 and the journal for the old beta animation, and points the research reports at /api/gacha.
2026-10-01 23:27:56 +07:00
tiennm99 48a30863c3 fix(gacha): stop the card text moving after the card settles
Three things moved the text once the card looked still:

- The spin's last degrees: Chrome draws text on a slightly tilted card
  differently from a flat one, so glyphs hopped as the spin ran out. The
  spin now lands flat a moment before the card stops gliding.
- The landing: the card flew at its rounded size scaled to a fractional
  layout size, then was laid out again, shifting the text. The card's
  size is now whole pixels.
- The last flight frames: a sub-pixel creep at almost-identity scale drew
  the text differently from the card at rest. The flight now reaches its
  final pose slightly early.
2026-10-01 23:22:51 +07:00
tiennm99 949e5e57ec revert(gacha): restore the original pack tear and drop motion
Only the card rising out of the pack keeps its exponential easing; the scripted tear and the pack falling away move as pack-cards drew them.
2026-10-01 23:22:51 +07:00
tiennm99 76c96464a8 feat(gacha): ease the tear, pack drop, and card rise exponentially
The scripted drag across the seal, the torn pack falling away, and the
card rising out of it now ease exponentially in and out. The pack keeps
pack-cards' 1.35s drop and hold, and the card still clears the pack when
the spin starts.
2026-10-01 22:52:52 +07:00
tiennm99 9e3cf9d78a fix(gacha): park the pointer off the card after tearing the pack
The scripted drag released the mouse over where the card lands. Once the
card arrived under it, pack-cards tilted the hovered card toward the
pointer over its .22s transition, so the face kept moving after the spin
stopped. The pointer now moves to the corner when the drag ends.
2026-10-01 22:43:05 +07:00
tiennm99 9d5a207f54 fix(gacha): keep the polychrome engraving on the card while it flies 2026-10-01 22:34:05 +07:00
tiennm99 6a4d0a1abb feat(gacha): draw the polychrome on the card engravings only
The sliding rainbow sheen over the whole face and the spectrum foil are
gone. 4★ cards are now engraved with pack-cards' epic contour and 5★ cards
with the legendary rings or facets, and those engraved lines carry a still
rainbow; 3★ cards stay plain.

The warm-up also renders a 4★ wish, since the first card with the contour
engraving otherwise rendered about seven seconds slower.
2026-10-01 22:26:37 +07:00
tiennm99 9665a3b47b docs: describe the shipped miti99bot wheel integration 2026-10-01 22:15:17 +07:00
tiennm99 7ca1468a43 feat(gacha)!: render /api/gacha as the pack-cards wish
/api/gacha now renders the 6-second portrait pack-cards opening that was
served on /api/gachabeta. The Remotion meteor wish, its timeline, and the
beta route are removed, and the pack-cards renderer and page take the plain
gacha names.

BREAKING CHANGE: /api/gachabeta is gone, and /api/gacha returns a 6-second
portrait video (360x640 for width 640, 480x854 for width 854) instead of the
7-second landscape one.
2026-10-01 22:12:25 +07:00
tiennm99 d657c49c83 feat(gacha): give the beta card a polychrome rainbow foil 2026-10-01 22:07:12 +07:00
tiennm99 eb4208f1df feat(gacha): spin the beta card after it clears the pack, with sparkles
The card now rises out of the pack at pack-cards' own speed and only starts spinning once it is clear. The rest of the flight is stretched so three and a half turns ease out smoothly before the card lands face up, and glowing sparkles in the rarity colour burst around it while it spins. pack-cards is unchanged; the page reshapes its flight animations.
2026-10-01 19:03:31 +07:00
tiennm99 fc3c7ae0c0 feat(gacha): render the beta wish in portrait
/api/gachabeta now returns a portrait video with the requested width as its long edge: 640 renders 360x640 and 854 renders 480x854. The request body is unchanged, and /api/gacha stays landscape. The pack sits higher so it stays in frame while the card rises.
2026-10-01 18:45:06 +07:00
tiennm99 b73347bb4f feat(gacha): spin the beta card one and a half turns as it is revealed
pack-cards reveals the card with a half turn from rotateY(180deg) to 0deg. The page starts that one animation at 540deg, so the card spins further before landing face up, without changing the library.
2026-10-01 18:45:06 +07:00
tiennm99 f44b8564eb docs(plans): record research on open-source gacha animation projects 2026-10-01 18:22:30 +07:00
tiennm99 8c06fdf498 feat(gacha): render the beta wish as a pack-cards pack opening
/api/gachabeta now tears open a pack-cards collectible pack whose card
shows the request's label, the same B/A/S rank as /api/gacha, and one
star per rarity level; rarity also picks the card material and the pack
colour. pack-cards animates on the browser clock, so the route drives a
shared headless Chrome in deterministic mode over a debugging pipe,
steps virtual time frame by frame, tears the pack with a scripted drag,
and encodes the captured frames with Remotion's bundled ffmpeg. The
page and package are served from disk with all other requests blocked.

The browser stays alive per process and is warmed at start-up, because
the first render compiles the pack's WebGL shaders in software. The
Remotion beta composition and its scene code are removed. pack-cards is
installed from a GitHub tarball pinned to a commit, since it has no npm
release and a moving URL would break npm ci.
2026-10-01 18:22:30 +07:00
tiennm99 f75c0e560b fix(gacha): show the requested rank and stars on the beta reveal
The beta splash card used a fixed SSS rank and six stars. It now uses the
same rarity info as /api/gacha: the rank letter (B, A, or S) on the emblem
and name plate, and one star per rarity level beside the caller's label.
2026-10-01 16:36:22 +07:00
tiennm99 081ab00f2e feat(gacha): restyle the beta wish as a six-star wish sequence
The beta wish now cuts like an anime gacha game's six-star wish over 10.5
seconds: a dive through a painterly cloud vortex, a beam pass, a meteor
that stops inside a rainbow halo and bursts, a red flash, falling crystal
comets joined by a red meteor, a black emblem silhouette shedding shards
on a red disc, and a splash card with a name plate and six stars.

Cloud layers are painted once per roll onto a canvas with a seeded noise
displacement, so frames only move the baked image instead of running a
live SVG filter. The /api/gachabeta contract is unchanged.
2026-10-01 16:32:30 +07:00
tiennm99 b8d5544fd6 feat(gacha): rebuild the beta wish as a meteor shower reveal
The beta wish now renders an 8-second night sky over a snow-rimmed
mountain ridge, where coloured meteors glide down the diagonal. A hero
meteor in the rarity colour slows and burns out at the centre, where the
label appears under rank SSS. The toon cloud, comet, and perspective
camera are removed. The /api/gachabeta contract is unchanged.
2026-10-01 16:07:46 +07:00
tiennm99 adea684a2f feat(gacha): refine beta wish animation and SSS reveal 2026-10-01 15:42:54 +07:00
tiennm99 d2f09ecb33 feat(gacha): add /api/gachabeta toon-shaded wish style
The beta wish renders an 8-second astrology-themed night sky in toon
shading through a perspective action camera: it dollies toward a hero
cloud, a comet lights the cloud's rim from behind and bursts through it,
the camera chases the comet as its flare builds to a starburst, and the
label appears with an S, SS, or SSS rank. It shares the /api/gacha request
contract and render slots.
2026-10-01 15:07:56 +07:00
tiennm99 dccd524f9d feat(gacha): fly into the rank emblem with a rainbow sunburst and twinkling sky
The meteor now flies in from the upper left and lands on the rank emblem,
so the impact burst becomes the badge reveal. The 5-star rainbow ring is
replaced by a rainbow sunburst of counter-rotating rays. Sky stars twinkle
with short bright flares and cross glints, and each roll draws a fresh
layout from a per-request seed that the route picks when the caller sends
none.
2026-10-01 14:43:46 +07:00
tiennm99 9b93c53647 revert: restore the gacha animation to bd314b4
Reverts be27792 (measured straight glide) and 028ad08 (gravity arc), going
back to the B/A/S tier-escalation version with the original curved fall.
2026-10-01 14:26:33 +07:00
tiennm99 dc8f7f6ae7 feat(gacha): match the meteor glide to the source game's motion
Measured from reference footage, the meteor sweeps in along a straight line
from the upper left at about 23 degrees and eases out exponentially to a
hover above the horizon, replacing the gravity arc. The trail is now a soft
beam that narrows into the head with ribbons fanning along it, every tier
gets the horizontal lens flare, and sparkles drift above the beam.
2026-10-01 14:12:01 +07:00
tiennm99 004325d947 feat(gacha): make the meteor fall under real gravity
The meteor now follows a ballistic parabola: constant horizontal speed and
vertical speed growing under gravity solved from the launch and impact
points, so it climbs slightly, bends over, and dives while speeding up. The
trail samples past positions in time so it lengthens with speed, the head
stretches along its velocity, and sparks inherit the meteor's velocity and
fall under their own gravity.
2026-10-01 13:58:38 +07:00
tiennm99 adc2b94ae9 feat(gacha): escalate the wish animation by rarity with B/A/S rank emblems
Every beat now scales from a per-tier effects table: 4-star adds a bigger
meteor, lens flare, impact shake, and double shockwave; 5-star adds a
rainbow halo before landing, a gold sky flood, a starburst, counter-rotating
rays, falling sparkles, and an emblem sheen. The emblem shows the rank
letter instead of the label's first character.
2026-10-01 13:25:06 +07:00
tiennm99 97d8aeaadf feat(gacha): add /api/gacha wish animation rendered as MP4
A meteor coloured by rarity falls across a night sky, lands in a white
flash, and the label is revealed with its stars popping in. The caller
picks the result and rarity; the route only draws it. Renders as silent
H.264 so Telegram plays it as an animation without GIF banding, and
shares the render slots with /api/gif.
2026-10-01 11:43:42 +07:00
tiennm99 61d0e74709 refactor(render): share the render core and bearer check across routes 2026-10-01 11:43:42 +07:00
tiennm99 26c9431331 build: stop publishing the host port under Coolify
Coolify attaches the container to its proxy network and routes to the exposed
port, so the host publish was redundant and additionally reachable on
0.0.0.0:3000 outside TLS. Left commented for standalone `docker compose up`.
2026-09-19 16:32:45 +07:00
tiennm99 f475a5cb46 build: move from pnpm to npm
Replace pnpm-lock.yaml with package-lock.json. allowBuilds for esbuild becomes
package.json#allowScripts. The Dockerfile installs with npm ci --omit=dev and
no longer prepares pnpm through corepack; .npmrc keeps engine-strict, which npm
honours natively.

minimumReleaseAgeExclude is dropped rather than translated: pnpm's
minimumReleaseAge was never set, so the 30-entry exclusion list had nothing to
exclude from. Direct dependency versions resolve identically to the pnpm
lockfile.
2026-08-17 12:30:41 +07:00
tiennm99 529da3621d feat(schema): allow single-entry wheels 2026-08-06 11:43:10 +07:00
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