shots: feature rotate, per-language frame fallback, layout-aware capture selectors

This commit is contained in:
CRTkafa 2026-10-24 13:00:48 +08:00
parent 2e1c7cdc3c
commit 61b5fa0623
15 changed files with 355 additions and 17 deletions

View file

@ -167,6 +167,10 @@ export interface ShotsConfig {
copy: string;
/** Raw frames: <frames>/<languageId>/<phone|tablet>/<name>.png */
frames: string;
/** true: every language needs its own frames (<frames>/<lang>/...). Default false: a language with copy but no frames
* folder uses the default language's frames (play.defaultLanguage, else the first language); `shots check` / `render`
* say so as an info line. */
perLanguageFrames?: boolean;
/** Output: <out>/<languageId>/<size>/NN.png and feature/1024x500.png */
out: string;
/** Optional logo image for the hero scene and the feature graphic. */
@ -183,6 +187,9 @@ export interface ShotsConfig {
* bottom edge on purpose (shots check then allows it). `scale` (0.5-1.5, default 1) multiplies the phone's width
* (still clamped to the canvas with 'contain'). */
feature?: { frame: string; palette?: string | Palette; fit?: 'contain' | 'bleed'; scale?: number;
/** Degrees added to the phone's tilt (layout "bleed": the frame image is turned), like a scene frame's `rotate`: -90 stands a
* landscape game frame upright in a phone lying on its side. Default 0. */
rotate?: number;
/** layout "bleed": the name on the feature graphic when there is no logo (default app.name). */
title?: string;
/** Photo background for the feature graphic (path relative to the config, or { image, overlay }); wins over the
@ -207,7 +214,16 @@ export interface ShotsConfig {
}
/** One raw frame. Web: `path` (+ query). Simulator / emulator: `url` deep link and / or `args` / `extras`. */
/** Web capture: a selector, candidates tried in order, or per layout { phone, tablet, any } (own list first). Prefer data
* attributes (`[data-shot="shop-row"]`); `text=Shop` and `role=button[name="Shop"]` work as fallbacks. */
export type ShotSelector = string | string[] | { phone?: string | string[]; tablet?: string | string[]; any?: string | string[] };
export interface ShotSpec {
/** Web: wait (up to the waitFor timeout) until one of these matches, before waitFor; a miss names the selector and the size. */
waitSelector?: ShotSelector;
/** Web: element(s) to click after waitSelector, in order (a list of selectors is ONE click with fallbacks; use
* [{...}, {...}] objects for several clicks). */
click?: ShotSelector | ShotSelector[];
/** Web: URL path after baseUrl, "{lang}" replaced. */
path?: string;
/** ios-sim / android-emu: deep link opened after launch, e.g. "mygame://shot/{shot}?lang={lang}". */
@ -1259,6 +1275,7 @@ export function validateConfig(c: unknown): string[] {
if (s.feature !== undefined) {
if (!s.feature || typeof s.feature.frame !== 'string' || !s.feature.frame) e.push('shots.feature.frame: a frame name');
if (s.feature?.fit !== undefined && s.feature.fit !== 'contain' && s.feature.fit !== 'bleed') e.push("shots.feature.fit: 'contain' | 'bleed'");
if (s.feature?.rotate !== undefined && !(typeof s.feature.rotate === 'number' && Math.abs(s.feature.rotate) <= 360)) e.push('shots.feature.rotate: a number of degrees from -360 to 360');
if (s.feature?.scale !== undefined && !(typeof s.feature.scale === 'number' && s.feature.scale >= 0.5 && s.feature.scale <= 1.5)) e.push('shots.feature.scale: a number from 0.5 to 1.5');
e.push(...featureBackgroundIssues(s.feature?.background));
if (s.feature?.palette !== undefined) e.push(...paletteIssues(s.feature.palette, 'shots.feature.palette'));

File diff suppressed because one or more lines are too long