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.
This commit is contained in:
tiennm99 committed 2026-10-01 18:45:06 +07:00
1 parent b73347bb4f
commit fc3c7ae0c0
5 files changed
+34 -9

No files matched your search

+3 -2
View File
@@ -67,12 +67,13 @@ only draws it.
### Gacha wish beta
`POST /api/gachabeta` takes the same body and returns the same response as
`/api/gacha`, rendering a 6-second beta style instead: a collectible card pack
`/api/gacha`, rendering a 6-second portrait beta style instead: a collectible card pack
from [pack-cards](https://github.com/paubineau/pack-cards) is torn open, and
its card shows the request's `label`, its rank, and its stars. As on
`/api/gacha`, `rarity` picks the rank (`B` for 3★, `A` for 4★, `S` for 5★),
the star count, the card's material (rare, epic, legendary), and the pack's
colour (blue, purple, gold).
colour (blue, purple, gold). The video is portrait, with `width` as its long
edge: `640` renders 360×640 and `854` renders 480×854.
pack-cards animates on the browser clock, so this route does not use Remotion
compositions. `src/render/render-gacha-beta.js` keeps one headless Chrome per
+1 -1
View File
@@ -30,7 +30,7 @@ body {
/* Sit the pack below centre so the card rising out of it stays in frame. */
#stage .recap-pack-stack {
margin: 24vh 0 0;
margin: 14vh 0 0;
width: min(36vh, 60vw);
}
+10
View File
@@ -6,6 +6,7 @@
import path from 'node:path';
import {getRankLetter, getRarityPalette} from '../remotion/gacha-timeline.js';
import {gachaFrameSizes} from '../schemas/gacha-request.js';
/** @typedef {import('../schemas/gacha-request.js').GachaRenderRequest} GachaRenderRequest */
/** @typedef {import('../remotion/gacha-timeline.js').GachaRarity} GachaRarity */
@@ -25,6 +26,15 @@ export const wishTimeline = Object.freeze({
dragEnd: 1,
});
/**
* The beta wish is portrait: the request's `width` (640 or 854) is the long
* edge, so the frame is the /api/gacha frame turned upright.
*
* @param {GachaRenderRequest['width']} width
* @returns {{width: number, height: number}}
*/
export const getWishFrameSize = (width) => ({width: gachaFrameSizes[width], height: width});
/** pack-cards rarity profile and glow preset for each requested rarity. */
const rarityLooks = Object.freeze({
3: {rarity: 'rare', glow: /** @type {const} */ ('silver')},
+8 -5
View File
@@ -7,13 +7,13 @@ import {ensureBrowser, makeCancelSignal, RenderInternals} from '@remotion/render
import {
gachaBetaTotalSeconds,
getDragPointer,
getWishFrameSize,
getWishPageProps,
resolveWishAsset,
wishOrigin,
} from '../gacha-beta/wish-plan.js';
import {RenderTimeoutError} from '../lib/render-errors.js';
import {cleanupTempDir, createRenderTempDir} from '../lib/tmp-files.js';
import {gachaFrameSizes} from '../schemas/gacha-request.js';
import {createCdpConnection} from './cdp-pipe.js';
/**
@@ -158,7 +158,10 @@ export const closeGachaBetaBrowser = async () => {
* @returns {Promise<void>}
*/
export const warmGachaBetaBrowser = async () => {
await renderGachaBetaVideo({label: 'Warm-up', rarity: 5, fps: 24, width: 640, seed: 1}, {timeoutInMilliseconds: 120000});
await renderGachaBetaVideo(
{label: 'Warm-up', rarity: 5, fps: 24, width: 640, seed: 1},
{timeoutInMilliseconds: 120000},
);
};
/**
@@ -194,13 +197,13 @@ const captureFrames = async (cdp, sessionId, request, dir, guard) => {
*/
const send = (method, params) => guard(cdp.send(method, params, sessionId));
/** @param {string} expression */
const evaluate = async (expression) => (await send('Runtime.evaluate', {expression, returnByValue: true})).result?.value;
const evaluate = async (expression) =>
(await send('Runtime.evaluate', {expression, returnByValue: true})).result?.value;
await send('Page.enable');
await send('Fetch.enable', {patterns: [{urlPattern: '*'}]});
await send('Emulation.setDeviceMetricsOverride', {
width: request.width,
height: gachaFrameSizes[request.width],
...getWishFrameSize(request.width),
deviceScaleFactor: 1,
mobile: false,
});
+12 -1
View File
@@ -3,6 +3,7 @@ import {describe, expect, test} from 'vitest';
import {
gachaBetaTotalSeconds,
getDragPointer,
getWishFrameSize,
getWishPageProps,
resolveWishAsset,
wishOrigin,
@@ -14,12 +15,22 @@ const roots = {page: path.resolve('/srv/page'), packCards: path.resolve('/srv/pa
describe('gacha beta wish plan', () => {
test('the page shows the request label with the same rank and stars as /api/gacha', () => {
const base = {label: '<b>Bún bò</b>', fps: /** @type {const} */ (24), width: /** @type {const} */ (640), seed: 1};
expect(getWishPageProps({...base, rarity: 3})).toMatchObject({label: '<b>Bún bò</b>', rank: 'B', stars: 3, rarity: 'rare'});
expect(getWishPageProps({...base, rarity: 3})).toMatchObject({
label: '<b>Bún bò</b>',
rank: 'B',
stars: 3,
rarity: 'rare',
});
expect(getWishPageProps({...base, rarity: 4})).toMatchObject({rank: 'A', stars: 4, rarity: 'epic'});
expect(getWishPageProps({...base, rarity: 5})).toMatchObject({rank: 'S', stars: 5, rarity: 'legendary'});
expect(getWishPageProps({...base, rarity: 5}).artwork).toEqual({accent: '#ffd36b', tint: '#7a4a10'});
});
test('the frame is portrait, with the requested width as its long edge', () => {
expect(getWishFrameSize(640)).toEqual({width: 360, height: 640});
expect(getWishFrameSize(854)).toEqual({width: 480, height: 854});
});
test('the drag sweeps left to right across the seal, then lets go', () => {
const {dragStart, dragEnd} = wishTimeline;
expect(getDragPointer(dragStart - 0.01)).toBeNull();