shots: app titles get the weights the templates use (variable axis, used range or static weights; font-weight warning), palette.bg as one colour or a CSS gradient string (validated; contrast guard in shots check), photo backgrounds (palette.image, shots.feature.background) with overlay and cover crop

This commit is contained in:
CRTkafa 2026-10-20 13:37:37 +07:00
parent 2ba8278ab7
commit 3a11682a37
21 changed files with 713 additions and 44 deletions

View file

@ -6,6 +6,7 @@ import { dirname, isAbsolute, join, resolve } from 'node:path';
import * as rt from './rt';
import { PROFILE_RE, setConfigAccount } from './credentials';
import { validateCapture, validateRecord } from '../shots/devices';
import { featureBackgroundIssues, paletteIssues } from '../shots/palette';
import { type AppleDataTypeId, type AppleTypeAnswer, validateAppPrivacyTypes } from './app-privacy';
import { type AppEventDef, type CustomProductPageDef, validateCpp, validateEvents } from './cpp-events';
import { type IapOfferCodeDef, type PromotedConfig, validateIapOfferCodes, validatePromoted, validateWinBack, type WinBackDef } from './offers';
@ -125,7 +126,9 @@ export interface DataSafety {
export interface ShotBadgeSlot { y: number; side: 'l' | 'r'; icon?: string; iconBg?: string; hot?: boolean; stars?: boolean; rot?: number }
export interface ShotFrame { src: string; x?: number; dy?: number; scale?: number; rotate?: number }
export interface Palette { bg: [string, string, string]; glow: string; ink: string; shadow: string }
/** bg: gradient stops (2-3 colours), one colour (flat) or a CSS gradient string. image: a photo background (path
* relative to the config, cropped to cover) under a dark overlay (0-0.9, default 0.45) that keeps white ink legible. */
export interface Palette { bg: string | [string, string] | [string, string, string]; glow: string; ink: string; shadow: string; image?: string; overlay?: number }
export interface ShotScene { id: string; palette: string | Palette; hero?: boolean; frames: ShotFrame[]; badges?: ShotBadgeSlot[]; over?: number }
/** A local font file for shots.fonts. license: 'OFL-1.1', 'proprietary' or a short note (shown by `shots fonts`;
@ -158,7 +161,10 @@ export interface ShotsConfig {
* its rotated outline stays inside the canvas with a 24 px margin; 'bleed' keeps the larger phone cropped by the
* 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 };
feature?: { frame: string; palette?: string | Palette; fit?: 'contain' | 'bleed'; scale?: number;
/** Photo background for the feature graphic (path relative to the config, or { image, overlay }); wins over the
* palette's image. Also usable for the App Store Header and Search Results art. */
background?: string | { image: string; overlay?: number } };
/** Phone screen size of the raw frames in CSS px (default 440 x 902 @3x, iPhone 6.9" minus the 54 px status bar).
* `bar` = status bar height the template draws above the frame (and that capture crops off). */
phone?: { w: number; h: number; dpr?: number; bar?: number };
@ -1209,7 +1215,11 @@ export function validateConfig(c: unknown): string[] {
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?.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'));
}
for (const sc of s.scenes ?? []) if (sc.palette !== undefined) e.push(...paletteIssues(sc.palette, `shots.scenes[${sc.id}].palette`));
for (const [k, v] of Object.entries(s.variants ?? {})) for (const [id, p] of Object.entries(v.palettes ?? {})) e.push(...paletteIssues(p, `shots.variants.${k}.palettes.${id}`));
const fontKeys: [string, unknown][] = [['title', s.fonts?.title], ['body', s.fonts?.body], ...(Array.isArray(s.fonts?.fallback) ? s.fonts!.fallback! : s.fonts?.fallback !== undefined ? [s.fonts.fallback] : []).map((f, i) => [`fallback[${i}]`, f] as [string, unknown])];
if (s.fonts?.fallback !== undefined && !Array.isArray(s.fonts.fallback)) e.push('shots.fonts.fallback: a list');
for (const [k, f] of fontKeys) {

File diff suppressed because one or more lines are too long