shots: full-bleed layout (shots.layout bleed) with corner captions, per-size frames, and landscape orientation, on top of the landscape sizes

This commit is contained in:
CRTkafa 2026-10-23 22:45:10 +08:00
parent 9ed1890de8
commit d4cf1ad3a1
11 changed files with 320 additions and 11 deletions

View file

@ -15,7 +15,7 @@ import type { ShipConfig } from '../lib/config';
import type { Ctx } from '../lib/ctx';
import { log, printJson } from '../lib/log';
import { checkFrames, type FrameIssue } from './devices';
import { defaultSizes, frameDims, SIZES } from './lib';
import { defaultSizes, frameDims, renderDims, SIZES } from './lib';
import type { LocalIssue, RendersCheck } from '../lib/validate-rules';
import { allIssues, requiredSets, type RendersScan, resolvedTablets, scanRenders, storeOf } from './report';
import { imageInfo } from './spec';
@ -65,7 +65,29 @@ export function framesNeeded(config: ShipConfig, langs: string[], tablets?: bool
return langs.flatMap(lang => devices.flatMap(device => (device === 'phone' ? phone : scene).map(name => ({ lang, device, name }))));
}
/** shots.layout 'bleed': one frame per scene for every size, at that size's pixels (<lang>/<size>/<name>; the mac and
* chromebook sizes too), and the feature frame in <lang>/feature/. Each must have the size's aspect (1 %) and at least
* its width (no upscaling). */
export function checkBleedFrames(config: ShipConfig, dir: string, langs: string[], tablets?: boolean): FrameIssue[] {
const shots = config.shots!;
const out: FrameIssue[] = [];
const names = [...new Set(shots.scenes.map(s => s.frames[0]!.src))];
for (const lang of langs) for (const size of defaultSizes(config, tablets)) {
const feature = !!SIZES[size]!.feature;
const want = renderDims(size, shots);
for (const name of feature ? (shots.feature?.frame ? [shots.feature.frame] : []) : names) {
const rel = `${lang}/${feature ? 'feature' : size}/${name}`, f = join(dir, rel);
if (!existsSync(f)) { out.push({ file: rel, msg: 'missing' }); continue; }
const i = imageInfo(readFileSync(f));
if (Math.abs((i.w / i.h) / (want.w / want.h) - 1) > 0.01) out.push({ file: rel, msg: `${i.w}x${i.h} has aspect ${(i.w / i.h).toFixed(3)}, ${size} wants ${(want.w / want.h).toFixed(3)} (${want.w}x${want.h}): capture at that size` });
else if (i.w < want.w * 0.99) out.push({ file: rel, msg: `${i.w}x${i.h} is smaller than ${want.w}x${want.h}: capture at native resolution (no upscaling)` });
}
}
return out;
}
export function checkFrameFolder(config: ShipConfig, dir: string, langs: string[], tablets?: boolean): FrameIssue[] {
if (config.shots?.layout === 'bleed') return checkBleedFrames(config, dir, langs, tablets);
const found = new Map<string, { w: number; h: number }>();
for (const lang of langs) for (const device of ['phone', 'tablet']) {
const d = join(dir, lang, device);