import type { Box, TemplateSlot } from "../templates/slots.types"; export interface LogoPlacementInput { /** Width / height of the image (use the trimmed ink aspect once the logo pipeline provides it). */ aspect: number; /** The largest box the logo may fill, pt. */ box: { w: number; h: number }; /** Width of the column the logo and the text beside it share. */ availableWidth: number; /** Narrowest the text may get beside the logo before the logo moves above it. */ minTextWidth: number; /** Space between logo and text when side by side. Default 14. */ gap?: number; } export interface LogoPlacement { w: number; h: number; /** True when the logo goes above the text instead of beside it. */ stacked: boolean; } export const LOGO_GAP = 14; /** * Fits the logo inside `box` by its aspect (contain), then decides whether it still leaves the text * `minTextWidth` beside it. Same maths as the Phase 0 planLogo. */ export function computeClassicLogoPlacement({ aspect, box, availableWidth, minTextWidth, gap = LOGO_GAP }: LogoPlacementInput): LogoPlacement { const a = Number.isFinite(aspect) && aspect > 0 ? aspect : box.w / box.h; const h = Math.min(box.h, box.w / a); const w = h * a; return { w, h, stacked: availableWidth - w - gap < minTextWidth }; } /** Smallest height a logo is drawn at when its box allows it, pt. */ export const MIN_LOGO_HEIGHT = 14; /** White padding of the chip behind a logo that has no knockout variant, pt. */ export const CHIP_PADDING = 4; /** The weight factor never shrinks a logo below this share of its contain-fit size. */ const MIN_WEIGHT_FACTOR = 0.7; /** Ratio of the page's content width to the reference page's (1 for the page the slot was measured on). */ export type PageScale = number; export interface SlotLogoInput { /** Width / height of the logo's ink box. */ aspect: number; /** Share of the ink box that is ink, 0 to 1. */ density: number; kind: "wordmark" | "mark" | "tall"; /** The logo already carries the business name, so it replaces the template's whole lockup. */ includesName: boolean; /** A white-ink variant exists, for dark surfaces. */ hasKnockout: boolean; } export interface SlotLogoPlacementInput { logo: SlotLogoInput; slot: TemplateSlot; scale?: PageScale; /** Room the layout can actually give the logo; the target box shrinks to it, keeping its anchor. */ available?: { w: number; h: number }; } export interface SlotLogoPlacement { /** Where the logo image goes, page points from the top-left. */ box: Box; x: number; y: number; w: number; h: number; variant: "print" | "knockout" | "chip"; /** Which placeholder box the logo was fitted to. */ slotKind: "lockup" | "mark"; /** For the chip variant: the white rounded rectangle behind the logo (the logo box plus CHIP_PADDING). */ chip?: Box; /** Only Classic's own stack-or-beside decision sets this; slot placement never stacks. */ stacked?: boolean; } const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v)); /** A w x h box inside `target`, aligned the way the placeholder was. */ function align(target: Box, w: number, h: number, slot: TemplateSlot): Box { const x = slot.anchor === "left" ? target.x : slot.anchor === "right" ? target.x + target.w - w : target.x + (target.w - w) / 2; const y = slot.vAlign === "top" ? target.y : slot.vAlign === "bottom" ? target.y + target.h - h : target.y + (target.h - h) / 2; return { x, y, w, h }; } /** * Places the user's logo where a template's logo placeholder is. Contain-fit in the measured box, then * a shrink-only weight factor so a denser logo draws smaller and matches the placeholder's visual * weight. A wordmark (includesName) replaces the whole lockup; a mark replaces the icon only. A * text-only placeholder (Monolith's "CAVEIN", Tangerine's "Rimberio") fits the template's allowance * instead, with the weight factor still computed against the text's density. The legibility floor never pushes past the box: a box under MIN_LOGO_HEIGHT stays the limit. */ export function computeLogoPlacement({ logo, slot, scale = 1, available }: SlotLogoPlacementInput): SlotLogoPlacement { const wantLockup = logo.includesName || !slot.mark; // A text-only placeholder is too small for a logo; its allowance (from the template docs) is the target. const target = slot.textOnly ? slot.allowance : wantLockup ? (slot.lockup ?? slot.mark) : slot.mark; if (!target) throw new Error("slot has neither a mark nor a lockup box"); const slotKind = wantLockup ? "lockup" : "mark"; const placeholderDensity = wantLockup ? slot.lockupDensity : slot.markDensity; // Scale the box about its anchor so a right-aligned slot keeps its right edge. let bw = target.w * scale; let bh = target.h * scale; if (available) { bw = Math.min(bw, available.w); bh = Math.min(bh, available.h); } const box = align(target, bw, bh, slot); const aspect = Number.isFinite(logo.aspect) && logo.aspect > 0 ? logo.aspect : bw / bh; let h = Math.min(bh, bw / aspect); let w = h * aspect; const valid = logo.density > 0 && placeholderDensity > 0; const f = valid ? clamp(Math.sqrt(placeholderDensity / logo.density), MIN_WEIGHT_FACTOR, 1) : 1; w *= f; h *= f; if (h < MIN_LOGO_HEIGHT) { h = MIN_LOGO_HEIGHT; w = h * aspect; if (w > bw) { w = bw; h = w / aspect; } if (h > bh) { h = bh; w = h * aspect; } } const placed = align(box, w, h, slot); const variant = slot.darkSurface ? (logo.hasKnockout ? "knockout" : "chip") : "print"; const out: SlotLogoPlacement = { box: placed, ...placed, variant, slotKind }; if (variant === "chip") { out.chip = { x: placed.x - CHIP_PADDING, y: placed.y - CHIP_PADDING, w: placed.w + 2 * CHIP_PADDING, h: placed.h + 2 * CHIP_PADDING }; } return out; }