Muhammad-Mehdi's picture
more template configured for stock footage
dcb21e8
Raw
History Blame Contribute Delete
33.9 kB
/**
* Maps legacy / alias layout IDs to the canonical key used in LAYOUT_IMAGE_BOX_DIMS.
* Add any new aliases here when templates rename layouts.
*/
import { buildMagazineImageBoxDims } from "./magazine/magazineImageBoxDims";
const LAYOUT_ID_ALIASES: Record<string, string> = {
// nightfall legacy names
cinematic_title: "cinematic_title",
glass_narrative: "glass_narrative",
glow_metric: "glow_metric",
glass_code: "glass_code",
kinetic_insight: "kinetic_insight",
kinetix_insight: "kinetic_insight",
glass_stack: "glass_stack",
split_glass: "split_glass",
chapter_break: "chapter_break",
glass_image: "glass_image",
// newscast legacy β†’ normalized
newscast_cinematic_title: "opening",
newscast_glass_narrative: "anchor_narrative",
newscast_glass_image: "field_image_focus",
newscast_glass_code: "briefing_code_panel",
newscast_split_glass: "side_by_side_brief",
newscast_chapter_break: "segment_break",
newscast_glow_metric: "live_metrics_board",
newscast_glass_stack: "story_stack",
newscast_kinetic_insight: "headline_insight",
newscast_glass_stack2: "story_stack",
// older multi-word aliases
opening: "opening",
anchor_narrative: "anchor_narrative",
field_image_focus: "field_image_focus",
briefing_code_panel: "briefing_code_panel",
side_by_side_brief: "side_by_side_brief",
segment_break: "segment_break",
live_metrics_board: "live_metrics_board",
story_stack: "story_stack",
headline_insight: "headline_insight",
ending_socials: "ending_socials",
// gridcraft backward-compat alias
intro: "bento_hero",
};
/**
* Normalize a raw layout ID (which may be a legacy alias) to the canonical key
* used in LAYOUT_IMAGE_BOX_DIMS. Returns the input unchanged if no alias matches.
*/
export function normalizeLayoutId(layoutId: string): string {
return LAYOUT_ID_ALIASES[layoutId] ?? layoutId;
}
export interface ImageBoxDims {
landscape: { w: number; h: number };
portrait: { w: number; h: number };
/** When true, the layout crops the image into a circle β€” the adjust-modal
* preview box should render round (border-radius 50%) to match. */
circular?: boolean;
}
/**
* Image box dimensions for every layout that renders an image, expressed as
* fractions of the template canvas (imageBoxWidth / canvasWidth, imageBoxHeight / canvasHeight).
*
* Canvas base sizes (landscape):
* default, nightfall, gridcraft, spotlight, matrix, mosaic, blackswan β†’ 1920 Γ— 1080
* whiteboard, newspaper, newscast β†’ 1920 Γ— 1080
* magazine β†’ 1920 Γ— 1080
*
* In portrait mode the canvas is rotated (e.g. 1080 Γ— 1920), and the fractions
* are applied to those swapped dimensions by getImageBoxAspectRatio().
*
* Layouts with no image support are not listed β€” they fall back to full-canvas (w:1, h:1).
*/
export const LAYOUT_IMAGE_BOX_DIMS: Record<string, ImageBoxDims> = {
// ─────────────────────────────────────────────────────────────────────────
// DEFAULT template (canvas 1920 Γ— 1080)
// ─────────────────────────────────────────────────────────────────────────
// flexRow: image is the right flex:1 cell β†’ 50% width, full height
// portrait flexColumn: bottom flex:1 cell β†’ full width, 50% height
hero_image: {
landscape: { w: 0.5, h: 1.0 }, // 960 Γ— 1080
portrait: { w: 1.0, h: 0.5 }, // 1080 Γ— 960
},
// Use a landscape-oriented preview box for landscape projects to avoid portrait-looking framing.
// Portrait keeps the tall 45% height treatment from the layout.
image_caption: {
landscape: { w: 0.50, h: 0.6 }, // ~960 Γ— 304 (landscape ratio)
portrait: { w: 0.907, h: 0.45 }, // ~980 Γ— 864 on 1080Γ—1920
},
// flexRow: right flex:1 cell β€” image is half the canvas width
// portrait flexColumn: image takes flex:1 portion (~50% height)
bullet_list: {
landscape: { w: 0.5, h: 1.0 }, // 960 Γ— 1080
portrait: { w: 1.0, h: 0.5 }, // 1080 Γ— 960
},
// flexRow: image flex:1, portrait flex:0.6 (roughly 37% of height)
timeline: {
landscape: { w: 0.5, h: 1.0 }, // 960 Γ— 1080
portrait: { w: 1.0, h: 0.40 }, // 1080 Γ— 768
},
// legacy default image layout id still used in some scene payloads
animated_image: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// ─────────────────────────────────────────────────────────────────────────
// NIGHTFALL template (canvas 1920 Γ— 1080)
// ─────────────────────────────────────────────────────────────────────────
// Full-bleed background β€” same as canvas
cinematic_title: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Full-bleed Ken-Burns image β€” same as canvas
glass_image: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Landscape: image lives inside a centered card (max-width 1400) at flex 42%, fixed 380 px height.
// Portrait: image is full-width inside padded card content, fixed 400 px height.
glass_narrative: {
landscape: { w: 0.274, h: 0.352 }, // ~527 Γ— 380 on 1920Γ—1080
portrait: { w: 0.751, h: 0.208 }, // ~811 Γ— 400 on 1080Γ—1920
},
// Landscape: image sits in the left panel and should stay landscape-oriented in adjust modal.
// Portrait: split-screen card, image section is the top flex:1 half.
glow_metric: {
landscape: { w: 0.40, h: 0.50 }, // landscape image box (avoid portrait-looking preview)
portrait: { w: 1.0, h: 0.5 }, // 1080 Γ— 960
},
// flex "0 0 40%", height 400 px landscape / full width, height 50% portrait
glass_stack: {
landscape: { w: 0.40, h: 0.370 }, // 768 Γ— 400
portrait: { w: 1.0, h: 0.5 }, // 1080 Γ— 960
},
// top-right media card in split layout
split_glass: {
landscape: { w: 0.40, h: 0.370 },
portrait: { w: 1.0, h: 0.5 },
},
// image-forward title stack (full canvas treatment)
kinetic_insight: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// ─────────────────────────────────────────────────────────────────────────
// GRIDCRAFT template (canvas 1920 Γ— 1080)
// ─────────────────────────────────────────────────────────────────────────
// Landscape: right-top grid cell in a 2fr/1fr x 1fr/1fr grid inside a 90%Γ—80% container.
// Portrait: bottom-left grid cell in a 1fr/1fr x 2fr/1fr grid inside same container.
bento_hero: {
landscape: { w: 0.30, h: 0.40 }, // ~576 Γ— 432
portrait: { w: 0.45, h: 0.267 }, // ~486 Γ— 512
},
// flex "0 0 38%", height 320 px landscape / width 80%, height 220 px portrait
bento_compare: {
landscape: { w: 0.38, h: 0.296 }, // 730 Γ— 320
portrait: { w: 0.8, h: 0.115 }, // 864 Γ— 220
},
// identical card style to bento_compare
bento_features: {
landscape: { w: 0.38, h: 0.296 },
portrait: { w: 0.8, h: 0.115 },
},
// identical card style to bento_compare
bento_steps: {
landscape: { w: 0.38, h: 0.296 },
portrait: { w: 0.8, h: 0.115 },
},
// grid 1.8fr/1fr rows; main box spans full container width (90% canvas).
// In landscape flexRow: image=left flex:1 = 45% of canvas Γ— 50% of canvas height
// In portrait flexCol: image=top flex:1 = 90% Γ— 24% (main box β‰ˆ60% of 80%-height container)
bento_highlight: {
landscape: { w: 0.45, h: 0.50 }, // 864 Γ— 540
portrait: { w: 0.90, h: 0.24 }, // 972 Γ— 461
},
// Editorial split card: image is one flex half of a 90%Γ—80% container.
editorial_body: {
landscape: { w: 0.45, h: 0.80 }, // ~864 Γ— 864
portrait: { w: 0.90, h: 0.40 }, // ~972 Γ— 768
},
// flex "0 0 38%", height 320 px landscape / width 100%, height 180 px portrait
kpi_grid: {
landscape: { w: 0.38, h: 0.296 }, // 730 Γ— 320
portrait: { w: 1.0, h: 0.094 }, // 1080 Γ— 180
},
// side media pane in a split code layout
bento_code: {
landscape: { w: 0.42, h: 1.0 },
portrait: { w: 1.0, h: 0.34 },
},
// PullQuote image card inside padded quote panel.
pull_quote: {
landscape: { w: 0.308, h: 0.259 }, // ~592 Γ— 280
portrait: { w: 0.63, h: 0.104 }, // ~680 Γ— 200
},
// ─────────────────────────────────────────────────────────────────────────
// SPOTLIGHT template (canvas 1920 Γ— 1080)
// ─────────────────────────────────────────────────────────────────────────
// Full-bleed with vignette overlay
spotlight_image: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Full-bleed background under text
cascade_list: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// flex "0 0 38%", height 360 px landscape / width 70%, height 220 px portrait
impact_title: {
landscape: { w: 0.38, h: 0.333 }, // 730 Γ— 360
portrait: { w: 0.70, h: 0.115 }, // 756 Γ— 220
},
// flex "0 0 38%", height 400 px landscape / width 80%, height 240 px portrait
statement: {
landscape: { w: 0.38, h: 0.370 }, // 730 Γ— 400
portrait: { w: 0.80, h: 0.125 }, // 864 Γ— 240
},
// flex "0 0 35%", height 350 px landscape / width 70%, height 200 px portrait
stat_stage: {
landscape: { w: 0.35, h: 0.324 }, // 672 Γ— 350
portrait: { w: 0.70, h: 0.104 }, // 756 Γ— 200
},
// Optional image card below headline: width 42% / height 32% landscape, 72% / 26% portrait
word_punch: {
landscape: { w: 0.42, h: 0.32 }, // ~806 Γ— 346
portrait: { w: 0.72, h: 0.26 }, // ~778 Γ— 499
},
// flex "0 0 38%", full height landscape / full width, height 280 px portrait
versus: {
landscape: { w: 0.38, h: 1.0 }, // 730 Γ— 1080
portrait: { w: 1.0, h: 0.146 }, // 1080 Γ— 280
},
// flex "0 0 38%" of padded container (outer pad 8% right), inner pad 8% top/bottom/left
// β†’ image area ~672 Γ— 972 landscape (tall portrait box); 76% Γ— 44% portrait
rapid_points: {
landscape: { w: 0.35, h: 0.90 }, // ~672 Γ— 972
portrait: { w: 0.64, h: 0.44 }, // ~691 Γ— 845
},
// Optional image card: width 38% / height 320 px landscape, 70% / 220 px portrait
closer: {
landscape: { w: 0.38, h: 0.396 }, // ~730 Γ— 320
portrait: { w: 0.70, h: 0.115 }, // 756 Γ— 220
},
// ─────────────────────────────────────────────────────────────────────────
// MATRIX template (canvas 1920 Γ— 1080)
// ─────────────────────────────────────────────────────────────────────────
// Full-bleed with clip-path horizontal reveal
matrix_image: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Left 50% panel, full height landscape / full width, 280 px portrait
fork_choice: {
landscape: { w: 0.5, h: 1.0 }, // 960 Γ— 1080
portrait: { w: 1.0, h: 0.146 }, // 1080 Γ— 280
},
// flex "0 0 38%", height 360 px landscape / width 70%, height 220 px portrait
matrix_title: {
landscape: { w: 0.38, h: 0.333 }, // 730 Γ— 360
portrait: { w: 0.70, h: 0.115 }, // 756 Γ— 220
},
// width 35%, height auto (approx 300 px) landscape / width 60%, height auto portrait
data_stream: {
landscape: { w: 0.35, h: 0.278 }, // 672 Γ— 300 (estimated β€” height is "auto")
portrait: { w: 0.60, h: 0.182 }, // 648 Γ— 350 (estimated)
},
// flex "0 0 38%", fixed 400 px height landscape / width 80%, fixed 240 px portrait
terminal_text: {
landscape: { w: 0.38, h: 0.370 }, // ~730 Γ— 400
portrait: { w: 0.80, h: 0.125 }, // 864 Γ— 240
},
glitch_punch: {
landscape: { w: 0.38, h: 0.333 }, // ~730 Γ— 360
portrait: { w: 0.70, h: 0.115 }, // 756 Γ— 220
},
cipher_metric: {
landscape: { w: 0.35, h: 0.324 }, // ~672 Γ— 350
portrait: { w: 0.70, h: 0.104 }, // 756 Γ— 200
},
transmission: {
// same image rail geometry as rapid_points in landscape
landscape: { w: 0.35, h: 0.90 }, // ~672 Γ— 972
portrait: { w: 1.0, h: 1.0 }, // portrait branch currently stacks content; keep full-canvas fallback
},
awakening: {
landscape: { w: 0.38, h: 0.296 }, // ~730 Γ— 320
portrait: { w: 0.70, h: 0.115 }, // 756 Γ— 220
},
// ─────────────────────────────────────────────────────────────────────────
// BLACKSWAN template (canvas 1920 Γ— 1080)
// ─────────────────────────────────────────────────────────────────────────
// Full-bleed with fade-in at end of scene
droplet_intro: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Full-bleed background at 0.18 opacity
neon_narrative: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// flex:1 with CSS aspectRatio "16/9" landscape / width 88%, height 36% portrait
arc_features: {
landscape: { w: 0.5, h: 0.5 }, // ~960 Γ— 540 (16/9 box in flex:1 column)
portrait: { w: 0.88, h: 0.36 }, // 950 Γ— 691
},
// absolute positioned: left/right 18%, height 45% landscape / left/right 10%, height 35% portrait
dive_insight: {
landscape: { w: 0.64, h: 0.45 }, // 1229 Γ— 486
portrait: { w: 0.80, h: 0.35 }, // 864 Γ— 672
},
pulse_metric: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
signal_split: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
reactor_code: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
flight_path: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// ─────────────────────────────────────────────────────────────────────────
// MOSAIC template (canvas 1920 Γ— 1080)
// ─────────────────────────────────────────────────────────────────────────
// Full-bleed tile-by-tile reveal
mosaic_title: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Full-bleed tile reveal (same component as mosaic_title)
mosaic_punch: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Absolute-pinned left panel: width 46%, full height in both landscape and portrait
mosaic_text: {
landscape: { w: 0.46, h: 1.0 }, // 883 Γ— 1080
portrait: { w: 0.46, h: 1.0 }, // 331 Γ— 720 on 720Γ—1280 canvas
},
mosaic_stream: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
mosaic_metric: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
mosaic_phrases: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
mosaic_close: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// ─────────────────────────────────────────────────────────────────────────
// NEWSCAST template (canvas 1280 Γ— 720)
// ─────────────────────────────────────────────────────────────────────────
// Full-bleed under hero chrome
opening: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Full-bleed with zoom+shift animation
field_image_focus: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Full-bleed background under split panels
side_by_side_brief: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Full-bleed background (NewsCastLayoutImageBackground)
live_metrics_board: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Full-bleed background
story_stack: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
headline_insight: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// flex "0 0 40%", full height landscape / no image panel in portrait β†’ full canvas fallback
anchor_narrative: {
landscape: { w: 0.40, h: 1.0 }, // 512 Γ— 720
portrait: { w: 1.0, h: 1.0 }, // no image panel in portrait β€” full canvas
},
briefing_code_panel: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
segment_break: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// ─────────────────────────────────────────────────────────────────────────
// NEWSPAPER template (canvas 1280 Γ— 720)
// ─────────────────────────────────────────────────────────────────────────
// Polaroid-style card: absolute positioned 40% Γ— 50% landscape / 80% Γ— 35% portrait
news_headline: {
landscape: { w: 0.40, h: 0.5 }, // 512 Γ— 360
portrait: { w: 0.80, h: 0.35 }, // 576 Γ— 448
},
// Tilted photo card: 38% Γ— 60% landscape / 88% Γ— 38% portrait
article_lead: {
landscape: { w: 0.38, h: 0.6 }, // 486 Γ— 432
portrait: { w: 0.88, h: 0.38 }, // 634 Γ— 486
},
// Portrait-oriented card (aspect-ratio 3/4): flex 0.8 landscape / width 100%, height 300 px portrait
fact_check: {
landscape: { w: 0.44, h: 1.0 }, // ~563 Γ— 720 (3/4 portrait card, clipped at canvas height)
portrait: { w: 1.0, h: 0.234 }, // 720 Γ— 300
},
// Landscape photo card: 45% Γ— 300 px landscape / 100% Γ— 400 px portrait
news_timeline: {
landscape: { w: 0.45, h: 0.417 }, // 576 Γ— 300
portrait: { w: 1.0, h: 0.313 }, // 720 Γ— 400
},
data_snapshot: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Polaroid expert photo: left 45% column landscape / top 38% band portrait
// (see ExpertProfile.tsx). Landscape: 45%-wide column, side padding 6%/4%
// (% is width-relative) β†’ card ~448px wide; flex:1 fills the column minus the
// credit line β†’ ~76% canvas height. Portrait: top 38% band with 6% margins.
expert_profile: {
landscape: { w: 0.42, h: 0.76 }, // widened preview box for the left-column polaroid card
portrait: { w: 0.88, h: 0.38 }, // top 38% with 6% side margin β†’ 634 Γ— 486
},
// ─────────────────────────────────────────────────────────────────────────
// WHITEBOARD template (canvas 1280 Γ— 720)
// ─────────────────────────────────────────────────────────────────────────
// flex "0 0 36%" of inner row (with 7% side padding), stretched to row height (~88% canvas)
// portrait uses width 100% with fixed 500 px height
marker_story: {
landscape: { w: 0.309, h: 0.88 }, // ~396 Γ— 634 on 1280Γ—720
portrait: { w: 1.0, h: 0.391 }, // 720 Γ— 500
},
// ─────────────────────────────────────────────────────────────────────────
// CHRONICLE template (canvas 1920 Γ— 1080)
// ─────────────────────────────────────────────────────────────────────────
// Two-page spread: image is the right page (~flex 1.1 of ~50% canvas width).
// Portrait stacks: image is ~88% Γ— 35%.
parchment_scroll: {
landscape: { w: 0.52, h: 0.70 }, // ~998 Γ— 756
portrait: { w: 0.88, h: 0.35 }, // ~950 Γ— 672
},
// Quote with optional inline image plate (flex 0 0 30%, ~60% width within column)
illuminated_quote: {
landscape: { w: 0.30, h: 0.40 }, // ~576 Γ— 432
portrait: { w: 0.60, h: 0.20 }, // ~648 Γ— 384
},
// Two-page versus folio: each side holds an embossed image (~70% wide on each half).
// We use the larger half for the preview box.
versus_folio: {
landscape: { w: 0.35, h: 0.55 }, // ~672 Γ— 594
portrait: { w: 1.00, h: 0.28 }, // ~1080 Γ— 537 (top page)
},
// Unfurled cartographer's map β€” occupies most of the canvas.
map_reveal: {
landscape: { w: 0.80, h: 0.85 }, // ~1536 Γ— 918
portrait: { w: 0.95, h: 0.70 }, // ~1026 Γ— 1344
},
// Ledger-of-facts header image (optional): ~55% wide landscape, 85% wide portrait
ledger_stats: {
landscape: { w: 0.55, h: 0.40 }, // ~1056 Γ— 432
portrait: { w: 0.85, h: 0.30 }, // ~918 Γ— 576
},
// book_open, chapter_plate, ledger_stats, chronicle_timeline, decree_seal and
// ending_socials hide the image (layouts_without_image). Let them fall back to
// the default full-canvas placeholder since no image adjustment is exposed.
// ─────────────────────────────────────────────────────────────────────────
// LADUC template (canvas 1920 Γ— 1080)
// Mirrors LADUC_LAYOUTS in templateConfig.tsx + LADUC_LAYOUT_REGISTRY in
// laduc/layouts/index.ts. `market_annotation` and `ticker` deliberately
// omitted β€” they're declared in meta.json `layouts_without_image`, so the
// editor hides the image picker entirely. `tier_showcase` is NOT a real
// LaDuc layout (no registry entry, no validLayouts membership).
// ─────────────────────────────────────────────────────────────────────────
// Full-bleed editorial canvas β€” image fills the whole frame via
// LaDucLayoutImageBackground in both orientations.
masthead: { landscape: { w: 1.0, h: 1.0 }, portrait: { w: 1.0, h: 1.0 } },
data_impact: { landscape: { w: 1.0, h: 1.0 }, portrait: { w: 1.0, h: 1.0 } },
two_column: { landscape: { w: 1.0, h: 1.0 }, portrait: { w: 1.0, h: 1.0 } },
kinetic_quote: { landscape: { w: 1.0, h: 1.0 }, portrait: { w: 1.0, h: 1.0 } },
sign_off: { landscape: { w: 1.0, h: 1.0 }, portrait: { w: 1.0, h: 1.0 } },
ending_socials: { landscape: { w: 1.0, h: 1.0 }, portrait: { w: 1.0, h: 1.0 } },
// Right-side editorial card (landscape) / full-bleed (portrait).
// Each of these three render `<ZoomCropImg>` in an explicit absolute card
// when landscape, and switch to a full-bleed LaDucLayoutImageBackground
// in portrait β€” so the focus picker mirrors that shape.
// DeepDive: right:0, top:0, bottom:0, width:38% β†’ 38% Γ— 100% (β‰ˆ 730 Γ— 1080)
// See LaDucDeepDive.tsx:132-145
deep_dive: {
landscape: { w: 0.38, h: 1.00 },
portrait: { w: 1.00, h: 1.00 },
},
// ThesisStatement: right:padH(8%), top:14%, bottom:14%, width:34%
// β†’ 34% Γ— 72% (β‰ˆ 653 Γ— 778)
// See LaDucThesisStatement.tsx:147-160
thesis_statement: {
landscape: { w: 0.34, h: 0.72 },
portrait: { w: 1.00, h: 1.00 },
},
// FrameworkFlow: image card inside the steps rail.
// rail β‰ˆ canvas Γ— (1 βˆ’ 2Γ—7.5% padH) β‰ˆ 85% wide
// rail β‰ˆ 55% canvas height (top header + bottom footnote + padV 5.5% Γ— 2)
// image takes flex: 0 0 28% of rail width β†’ 0.28 Γ— 0.85 β‰ˆ 0.24 wide Γ— 0.55 high
// See LaDucFrameworkFlow.tsx:330-344
// ─────────────────────────────────────────────────────────────────────────
// STICKMAN 2 template (canvas 1920 Γ— 1080)
// Full-bleed atmospheric background at 0.35 opacity β€” same as chalk_title hero.
// ─────────────────────────────────────────────────────────────────────────
chalk_title: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
shooting_star: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
night_walk: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
lantern_dialogue: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
neon_countdown: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// ─────────────────────────────────────────────────────────────────────────
// STICKMAN_FOOTBALL template (canvas 1920 Γ— 1080)
// Only kickoff_title, freekick_setup and goal_moment render an image; every
// other layout is in meta.json `layouts_without_image`. All three draw the
// image as a centered circular "mood" vignette of a fixed size β€” 560Γ—560 in
// landscape, 420Γ—420 in portrait (see KickoffTitle/FreekickSetup/GoalMoment).
// ─────────────────────────────────────────────────────────────────────────
kickoff_title: {
landscape: { w: 0.292, h: 0.519 }, // 560 Γ— 560 on 1920Γ—1080 (1:1 box)
portrait: { w: 0.389, h: 0.219 }, // 420 Γ— 420 on 1080Γ—1920 (1:1 box)
circular: true,
},
freekick_setup: {
landscape: { w: 0.292, h: 0.519 },
portrait: { w: 0.389, h: 0.219 },
circular: true,
},
goal_moment: {
landscape: { w: 0.292, h: 0.519 },
portrait: { w: 0.389, h: 0.219 },
circular: true,
},
// ─────────────────────────────────────────────────────────────────────────
// ECONOMIST template (canvas 1920 Γ— 1080)
// Image-capable: cover_reveal, image_feature, section_divider (full-bleed
// hero/backdrop) and the matted leader_article (left plate) / leader_quote
// (portrait mat card). See CoverReveal/ImageFeature/SectionDivider/
// LeaderArticle/LeaderQuote.
// ─────────────────────────────────────────────────────────────────────────
cover_reveal: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
image_feature: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
section_divider: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Left duotone plate ~42% width / full height (landscape); top band in portrait.
leader_article: {
landscape: { w: 0.42, h: 0.85 }, // ~806 Γ— 918
portrait: { w: 0.86, h: 0.28 }, // ~929 Γ— 528
},
// Matted editorial portrait card up the right margin (landscape) / lower band (portrait).
leader_quote: {
landscape: { w: 0.26, h: 0.58 }, // ~499 Γ— 626
portrait: { w: 0.64, h: 0.22 }, // ~691 Γ— 422
},
// ─────────────────────────────────────────────────────────────────────────
// SAKURA template (canvas 1920 Γ— 1080)
// Mirrors SAKURA_LAYOUTS in templateConfig.tsx + SAKURA_LAYOUT_REGISTRY.
// Only sakura_intro, sakura_section, sakura_text_narration,
// sakura_list_scene and sakura_stat_highlight render an image; every other
// sakura layout (incl. sakura_two_column_detail) is in meta.json
// `layouts_without_image`, so the editor hides the image picker.
// ─────────────────────────────────────────────────────────────────────────
// Hero: image is a full-bleed blended BACKGROUND behind the title stack.
// See SakuraIntro.tsx (heroBg).
sakura_intro: {
landscape: { w: 1.0, h: 1.0 },
portrait: { w: 1.0, h: 1.0 },
},
// Right-column photo panel: 620Γ—700 landscape; (widthβˆ’160)Γ—620 portrait.
// See SakuraSection.tsx (panelW/panelH).
sakura_section: {
landscape: { w: 0.323, h: 0.648 }, // 620 Γ— 700
portrait: { w: 0.852, h: 0.323 }, // (1080βˆ’160) Γ— 620
},
// Right-side supporting panel: 620Γ—700 landscape; (widthβˆ’160)Γ—620 portrait.
// See SakuraTextNarration.tsx (panelW/panelH).
sakura_text_narration: {
landscape: { w: 0.323, h: 0.648 },
portrait: { w: 0.852, h: 0.323 },
},
// Right-side supporting panel down the list's empty right edge: 36% Γ— full
// height landscape; bottom band (widthβˆ’160 Γ— 30%) portrait.
// See SakuraListScene.tsx (imagePanel).
sakura_list_scene: {
landscape: { w: 0.360, h: 1.0 },
portrait: { w: 0.852, h: 0.30 },
},
// Circular vignette behind the number: 560 landscape / 420 portrait.
// See SakuraStatHighlight.tsx (vignetteSize).
sakura_stat_highlight: {
landscape: { w: 0.292, h: 0.519 }, // 560 Γ— 560 (1:1 box)
portrait: { w: 0.389, h: 0.219 }, // 420 Γ— 420 (1:1 box)
circular: true,
},
// ─────────────────────────────────────────────────────────────────────────
// MAGAZINE template β€” computed from layout geometry (magazineImageBoxDims.ts)
// ─────────────────────────────────────────────────────────────────────────
...buildMagazineImageBoxDims(),
};
/**
* True when a layout crops its image into a circle, so the adjust-modal preview
* box should be rendered round. Falls back to false for unknown layouts.
*/
export function isImageBoxCircular(layoutId: string | null): boolean {
if (!layoutId) return false;
return LAYOUT_IMAGE_BOX_DIMS[normalizeLayoutId(layoutId)]?.circular ?? false;
}
/**
* Compute the CSS `aspect-ratio` string for the image adjustment modal preview box.
* Returns e.g. "960 / 1080", "1920 / 1080", "1080 / 1920".
*
* @param layoutId The scene's layout registry key (e.g. "hero_image"). Pass null for unknown.
* @param aspectRatioStr Project aspect ratio β€” "portrait" or "landscape".
* @param baseWidth Template base canvas width in landscape (from templateConfig.baseWidth).
* @param baseHeight Template base canvas height in landscape (from templateConfig.baseHeight).
*/
export function getImageBoxAspectRatio(
layoutId: string | null,
aspectRatioStr: string,
baseWidth: number,
baseHeight: number,
): string {
const isPortrait = aspectRatioStr === "portrait";
const canvasW = isPortrait ? baseHeight : baseWidth;
const canvasH = isPortrait ? baseWidth : baseHeight;
const dims = layoutId ? LAYOUT_IMAGE_BOX_DIMS[normalizeLayoutId(layoutId)] : undefined;
const { w, h } = dims
? (isPortrait ? dims.portrait : dims.landscape)
: { w: 1, h: 1 };
return `${Math.round(canvasW * w)} / ${Math.round(canvasH * h)}`;
}