Linea (the vendor's original format.pdf design), Monogram and Serenity are rebuilt from the reference PDFs as one parametric family on the engine, with the user's logo in the measured slot, a home for every GST field and A4/Letter support. Adds static Montserrat, Open Sans and Poppins fonts and a generated Ms Madi outline for Serenity's vertical word. Text-only logo placeholders (Monolith, Tangerine) use a doc-derived allowance box. A Carbon TemplatePicker with committed thumbnails opens from the page setup controls.
145 lines
5.7 KiB
TypeScript
145 lines
5.7 KiB
TypeScript
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;
|
|
}
|