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.
This commit is contained in:
tiennm99 committed 2026-10-01 22:26:37 +07:00
1 parent 9665a3b47b
commit 6a4d0a1abb
4 files changed
+37 -69

No files matched your search

+8 -5
View File
@@ -53,9 +53,11 @@ Authorization: Bearer change-me
Renders a 6-second portrait wish: a collectible card pack from
[pack-cards](https://github.com/paubineau/pack-cards) is torn open, its card
spins out with a burst of sparkles, and lands showing the request's `label`,
its rank, and its stars under a polychrome rainbow foil. `rarity` (required)
picks the rank (`B` for 3★, `A` for 4★, `S` for 5★), the star count, the
card's material (rare, epic, legendary), and the colour (blue, purple, gold).
its rank, and its stars. `rarity` (required) picks the rank (`B` for 3★, `A`
for 4★, `S` for 5★), the star count, the card's material (rare, epic,
legendary), and the colour (blue, purple, gold). 4★ and 5★ cards are engraved
with pack-cards' rarity textures (contour on 4★; rings or facets on 5★, chosen
by the label) drawn in a still polychrome rainbow; 3★ cards are plain.
`fps` is `24` or `30`. `width` is the long edge of the portrait frame:
`640` renders 360×640 and `854` renders 480×854. Optional `seed` (integer,
`0` to `2147483647`) seeds the page's randomness; when omitted the service
@@ -72,8 +74,9 @@ server process in `--deterministic-mode`, steps virtual time one frame at a
time, tears the pack with a scripted drag, captures each frame, and encodes
them with Remotion's bundled ffmpeg. The page (`src/gacha/page/`) and the
package are served from disk; the page has no network access. The first render
compiles the pack's WebGL shaders in software, which takes several seconds, so
server start-up runs one throwaway wish first.
compiles the pack's WebGL shaders in software and the first card of each
engraving rasterises its texture, which takes several seconds, so server
start-up renders a throwaway 5★ and 4★ wish first.
pack-cards has no npm release, so it is installed from a GitHub tarball pinned
to a commit. A moving branch URL would change the tarball's checksum and break
+14 -47
View File
@@ -49,57 +49,24 @@ body {
}
/*
* Polychrome: a rainbow band pattern glazed over the card under the print,
* sliding diagonally so the colours shift. Only a transform animates, which
* the compositor moves without repainting the foil each frame.
* Polychrome engraving: the tooled lines (pack-cards' rarity-epic-contour,
* rarity-legendary, and rarity-legendary-facets textures) are filled with a
* fixed rainbow instead of the metallic glint, so the colours hold still.
*/
.wish-polychrome {
border-radius: inherit;
inset: 0;
mix-blend-mode: screen;
opacity: var(--wish-polychrome);
overflow: hidden;
pointer-events: none;
position: absolute;
}
.wish-polychrome::before {
animation: wish-polychrome 2.4s ease-in-out infinite alternate;
background: repeating-linear-gradient(
#stage
.recap-card.recap-collectible:is([data-effect='engraved'], [data-effect='engraved-lettering'])
.recap-effect-texture::before {
background: linear-gradient(
125deg,
#ff5f9e 0%,
#ffb347 6%,
#fff275 12%,
#6dffb0 18%,
#5fd4ff 24%,
#9a7bff 30%,
#ff5f9e 36%
#ffb347 18%,
#fff275 34%,
#6dffb0 50%,
#5fd4ff 66%,
#9a7bff 82%,
#ff5f9e 100%
);
content: '';
height: 300%;
left: -100%;
position: absolute;
top: -100%;
width: 300%;
}
/* The band overhangs the card by a third of its size on each side, so it may slide at most that far. */
@keyframes wish-polychrome {
to {
transform: translate(30%, 30%);
}
}
.wish-card > :not(.wish-polychrome) {
position: relative;
}
/*
* The spectrum foil sets --accent inline on the card to its own colour; the
* print inside keeps the rarity colour.
*/
.wish-card > * {
--accent: var(--wish-accent);
opacity: 1;
}
.wish-rank {
+8 -14
View File
@@ -33,9 +33,6 @@ const el = (tag, className, text) => {
return node;
};
/** @type {Record<number, number>} */
const polychromeStrength = {3: 0.3, 4: 0.42, 5: 0.55};
const createFace = () => {
const face = el('article', 'wish-card');
const header = el('header', 'recap-card-header');
@@ -46,10 +43,7 @@ const createFace = () => {
content.append(el('span', 'wish-rank', wish.rank), title, el('p', 'wish-stars', '★'.repeat(wish.stars)));
const footer = el('footer', 'recap-card-footer');
footer.append(el('span', '', `${wish.stars}★`), el('span', '', '01 / 01'));
// A rainbow sheen drifting across the whole face, stronger on rarer cards.
const sheen = el('span', 'wish-polychrome');
sheen.style.setProperty('--wish-polychrome', String(polychromeStrength[wish.stars] ?? 0.3));
face.append(sheen, header, content, footer);
face.append(header, content, footer);
return face;
};
@@ -121,15 +115,15 @@ const burstSparkles = (deck, delay, duration) => {
};
/**
* Polychrome foil: every rarity gets pack-cards' rainbow holographic print,
* from a border on 3★ to the whole card on 4★ and 5★.
* Engravings carry the polychrome (page.css): 4★ cards are tooled with the
* epic contour, 5★ cards with the legendary rings or facets, picked by
* pack-cards from the card's label. 3★ cards stay plain.
*
* @type {import('pack-cards').AppearanceSettingsOptions['rarities']}
*/
const polychrome = Object.freeze({
rare: {foil: 'holographic', coverage: 'border', palette: 'spectrum', pattern: 'stardust'},
epic: {foil: 'holographic', coverage: 'full', palette: 'spectrum', pattern: 'stardust'},
legendary: {foil: 'holographic', coverage: 'full', palette: 'spectrum', pattern: 'facets'},
const engravings = Object.freeze({
epic: {engraving: 'contour'},
legendary: {engraving: ['radial', 'facets']},
});
const animate = Element.prototype.animate;
@@ -177,7 +171,7 @@ const view = createPackView(stage, {
label: 'Gacha',
artwork: wish.artwork,
labels: {title: 'Gacha', swipe: '', tap: ''},
appearance: {opening: 'animated', motion: 'interactive', glow: wish.glow, pack_zoom: true, rarities: polychrome},
appearance: {opening: 'animated', motion: 'interactive', glow: wish.glow, pack_zoom: true, rarities: engravings},
});
view.showPack({
+7 -3
View File
@@ -152,13 +152,17 @@ export const closeGachaBrowser = async () => {
};
/**
* Pays the shared browser's one-time shader compilation with a throwaway
* wish, so the first real request renders as fast as later ones.
* Pays the shared browser's one-time costs with throwaway wishes, so the first
* real request renders as fast as later ones: the WebGL shader compilation,
* and the first raster of each card's engraving texture. 4★ and 5★ cards use
* different engravings, so both are warmed.
*
* @returns {Promise<void>}
*/
export const warmGachaBrowser = async () => {
await renderGachaVideo({label: 'Warm-up', rarity: 5, fps: 24, width: 640, seed: 1}, {timeoutInMilliseconds: 120000});
for (const rarity of /** @type {const} */ ([5, 4])) {
await renderGachaVideo({label: 'Warm-up', rarity, fps: 24, width: 640, seed: 1}, {timeoutInMilliseconds: 120000});
}
};
/**