Feature graphic fits the phone: shots.feature.fit 'contain' (default) sizes the device so its rotated box stays inside 1024x500 with a 24 px margin in both templates, 'bleed' keeps the crop, scale 0.5-1.5, config validated; shots render records measured device boxes (report.json devices) and shots check fails a feature graphic whose device leaves the canvas (warns for a screenshot's front device off a side or the top); tests/feature-fit.test.ts (rotated-box geometry, both templates rendered at 1024x500, bleeding fixture) (pitfall 55)
This commit is contained in:
parent
6e2dbf03cc
commit
19d34286d5
15 changed files with 239 additions and 12 deletions
|
|
@ -195,3 +195,54 @@ export function variantRoot(renders: string, variant: string | undefined, varian
|
|||
if (!variants?.[variant]) throw new Error(`--variant ${variant}: not in shots.variants (${Object.keys(variants ?? {}).join(', ') || 'none configured'})`);
|
||||
return `${renders.replace(/\/$/, '')}/${variant}`;
|
||||
}
|
||||
|
||||
/** Axis-aligned bounding box of a w x h rectangle rotated by `deg` around its centre. */
|
||||
export function rotatedBox(w: number, h: number, deg: number): { w: number; h: number } {
|
||||
const r = (deg * Math.PI) / 180, c = Math.abs(Math.cos(r)), s = Math.abs(Math.sin(r));
|
||||
return { w: w * c + h * s, h: w * s + h * c };
|
||||
}
|
||||
|
||||
/** Feature graphic canvas (Play: 1024 x 500) and the device's place per template: centre x, design width, rotation,
|
||||
* and the top the old fixed layout used (kept by fit 'bleed'). */
|
||||
export const FEATURE_CANVAS = { w: 1024, h: 500, margin: 24 } as const;
|
||||
export const FEATURE_DEVICE: Record<'game' | 'app', { cx: number; width: number; angle: number; bleedTop: number }> = {
|
||||
game: { cx: 864, width: 232, angle: 7, bleedTop: 36 },
|
||||
app: { cx: 805, width: 230, angle: 0, bleedTop: 40 },
|
||||
};
|
||||
|
||||
export interface FeatureLayout { left: number; top: number; width: number; height: number; angle: number;
|
||||
/** Rotated bounding box in canvas px. */
|
||||
box: { x: number; y: number; w: number; h: number }; fit: 'contain' | 'bleed' }
|
||||
|
||||
/** Where the feature graphic's device goes. fit 'contain' (default): the device (frame aspect dev.w : dev.h + dev.bar,
|
||||
* rotated by the template's angle) is sized so its rotated bounding box stays inside the canvas with `margin` on every
|
||||
* side, centred vertically; `scale` (default 1) multiplies the design width before that clamp. fit 'bleed': the design
|
||||
* width x scale at the old fixed top, cropped by the canvas edge on purpose. */
|
||||
export function featureLayout(style: 'game' | 'app', dev: { w: number; h: number; bar: number },
|
||||
o: { fit?: 'contain' | 'bleed'; scale?: number } = {}): FeatureLayout {
|
||||
const d = FEATURE_DEVICE[style], C = FEATURE_CANVAS, fit = o.fit ?? 'contain';
|
||||
const k = (dev.h + dev.bar) / dev.w;
|
||||
let width = d.width * (o.scale ?? 1);
|
||||
if (fit === 'contain') {
|
||||
const unit = rotatedBox(1, k, d.angle);
|
||||
width = Math.min(width, (C.h - 2 * C.margin) / unit.h, (2 * Math.min(d.cx - C.margin, C.w - C.margin - d.cx)) / unit.w);
|
||||
}
|
||||
const height = width * k;
|
||||
const top = fit === 'contain' ? (C.h - height) / 2 : d.bleedTop;
|
||||
const bb = rotatedBox(width, height, d.angle);
|
||||
const cy = top + height / 2;
|
||||
return { left: d.cx - width / 2, top, width, height, angle: d.angle, fit,
|
||||
box: { x: d.cx - bb.w / 2, y: cy - bb.h / 2, w: bb.w, h: bb.h } };
|
||||
}
|
||||
|
||||
export interface DeviceBox { x: number; y: number; w: number; h: number }
|
||||
|
||||
/** Sides of a device box that leave a W x H canvas (more than `tol` px). */
|
||||
export function boxOutside(b: DeviceBox, W: number, H: number, tol = 1): string[] {
|
||||
const out: string[] = [];
|
||||
if (b.y < -tol) out.push(`top ${Math.round(-b.y)} px`);
|
||||
if (b.y + b.h > H + tol) out.push(`bottom ${Math.round(b.y + b.h - H)} px`);
|
||||
if (b.x < -tol) out.push(`left ${Math.round(-b.x)} px`);
|
||||
if (b.x + b.w > W + tol) out.push(`right ${Math.round(b.x + b.w - W)} px`);
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue