Measure reference logo placeholders and add slot-based logo placement
scripts/logo/measure-placeholders rasterises the reference PDFs and measures each logo placeholder's ink box and density into slots.generated.ts. The new placement contains the logo in the measured box, shrinks denser logos by up to 30 percent to match visual weight, picks knockout or a white chip on dark surfaces, and passes an identity test per template. A contact sheet compares each placeholder with the Test Vendor logo and three test logos.
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MIN_LOGO_HEIGHT, computeLogoPlacement, type SlotLogoInput } from "./logo";
|
||||
import { SLOTS } from "../templates/slots.generated";
|
||||
import { OPTIN_SLOTS, slotFor } from "../templates/slots.optin";
|
||||
import { SLOT_PAGE, type Box, type TemplateId, type TemplateSlot } from "../templates/slots.types";
|
||||
|
||||
const measured = Object.entries(SLOTS).filter((e): e is [TemplateId, TemplateSlot] => e[1] !== null);
|
||||
const optIn = Object.entries(OPTIN_SLOTS) as [TemplateId, TemplateSlot][];
|
||||
|
||||
const logo = (over: Partial<SlotLogoInput>): SlotLogoInput => ({
|
||||
aspect: 1,
|
||||
density: 0.5,
|
||||
kind: "mark",
|
||||
includesName: false,
|
||||
hasKnockout: true,
|
||||
...over,
|
||||
});
|
||||
const Sample_P = logo({ aspect: 4.62, density: 0.437, kind: "wordmark", includesName: true });
|
||||
|
||||
const inside = (inner: Box, outer: Box, eps = 1e-6) =>
|
||||
inner.x >= outer.x - eps && inner.y >= outer.y - eps && inner.x + inner.w <= outer.x + outer.w + eps && inner.y + inner.h <= outer.y + outer.h + eps;
|
||||
const closeBox = (a: Box, b: Box, tol = 0.5) => {
|
||||
for (const k of ["x", "y", "w", "h"] as const) expect(Math.abs(a[k] - b[k]), `${k}: ${a[k]} vs ${b[k]}`).toBeLessThanOrEqual(tol);
|
||||
};
|
||||
|
||||
describe("slots.generated", () => {
|
||||
it("has a measured slot for the nine templates with a reference placeholder, none for the rest", () => {
|
||||
expect(measured.map(([id]) => id).sort()).toEqual(
|
||||
["citrus-split", "cobalt-stripe", "crimson-grid", "monogram", "monolith", "serenity", "slate-band", "tangerine-ledger", "teal-swoosh"],
|
||||
);
|
||||
for (const id of ["classic", "marble", "highlighter", "linea", "purple-pop"] as const) expect(SLOTS[id]).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps every box on the page, with the mark inside the lockup", () => {
|
||||
for (const [id, s] of measured) {
|
||||
const page = { x: 0, y: 0, w: SLOT_PAGE.w, h: SLOT_PAGE.h };
|
||||
expect(s.lockup && inside(s.lockup, page), id).toBe(true);
|
||||
if (s.mark && s.lockup) expect(inside(s.mark, s.lockup, 0.01), id).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("only Slate Band is a dark surface", () => {
|
||||
expect(measured.filter(([, s]) => s.darkSurface).map(([id]) => id)).toEqual(["slate-band"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("slots.optin", () => {
|
||||
it("covers the four templates without a placeholder, all opt-in and off by default", () => {
|
||||
expect(optIn.map(([id]) => id).sort()).toEqual(["highlighter", "linea", "marble", "purple-pop"]);
|
||||
for (const [id, s] of optIn) {
|
||||
expect(s.optIn, id).toBe(true);
|
||||
expect(s.defaultOn, id).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("slotFor prefers a measured slot and gives Classic none", () => {
|
||||
expect(slotFor("monogram")).toBe(SLOTS.monogram);
|
||||
expect(slotFor("marble")).toBe(OPTIN_SLOTS.marble);
|
||||
expect(slotFor("classic")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeLogoPlacement identity: the placeholder itself comes back as its own box", () => {
|
||||
for (const [id, s] of measured) {
|
||||
it(`${id} lockup`, () => {
|
||||
const l = s.lockup!;
|
||||
const p = computeLogoPlacement({
|
||||
logo: logo({ aspect: l.w / l.h, density: s.lockupDensity, kind: "wordmark", includesName: true }),
|
||||
slot: s,
|
||||
});
|
||||
expect(p.slotKind).toBe("lockup");
|
||||
closeBox(p.box, l);
|
||||
});
|
||||
if (s.mark) {
|
||||
it(`${id} mark`, () => {
|
||||
const m = s.mark!;
|
||||
const p = computeLogoPlacement({ logo: logo({ aspect: m.w / m.h, density: s.markDensity }), slot: s });
|
||||
expect(p.slotKind).toBe("mark");
|
||||
closeBox(p.box, m);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("computeLogoPlacement on every slot", () => {
|
||||
const all: [TemplateId, TemplateSlot][] = [...measured, ...optIn];
|
||||
|
||||
it("Test Vendor never exceeds its box and is never heavier than the placeholder ink", () => {
|
||||
for (const [id, s] of all) {
|
||||
const p = computeLogoPlacement({ logo: Sample_P, slot: s });
|
||||
expect(p.slotKind).toBe("lockup");
|
||||
expect(inside(p.box, s.lockup!), id).toBe(true);
|
||||
if (!s.optIn) {
|
||||
const ink = p.w * p.h * Sample_P.density;
|
||||
const placeholderInk = s.lockup!.w * s.lockup!.h * s.lockupDensity;
|
||||
expect(ink, `${id}: ${ink.toFixed(1)} vs ${placeholderInk.toFixed(1)}`).toBeLessThanOrEqual(placeholderInk * 1.05);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("a square mark, a round emblem and a tall logo stay inside the mark box (the lockup box without one)", () => {
|
||||
const shapes = [logo({ aspect: 1, density: 0.6 }), logo({ aspect: 1, density: 0.2, kind: "mark" }), logo({ aspect: 0.5, density: 0.5, kind: "tall" })];
|
||||
for (const [id, s] of all) {
|
||||
for (const l of shapes) {
|
||||
const p = computeLogoPlacement({ logo: l, slot: s });
|
||||
expect(inside(p.box, (s.mark ?? s.lockup)!), `${id} aspect ${l.aspect}`).toBe(true);
|
||||
expect(p.w / p.h).toBeCloseTo(l.aspect, 6);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to the lockup box for a mark-less slot even when the logo has no name", () => {
|
||||
const s = SLOTS.monolith!;
|
||||
expect(s.mark).toBeNull();
|
||||
const p = computeLogoPlacement({ logo: logo({ aspect: 1 }), slot: s });
|
||||
expect(p.slotKind).toBe("lockup");
|
||||
expect(inside(p.box, s.lockup!)).toBe(true);
|
||||
});
|
||||
|
||||
it("a denser logo is drawn smaller, at most down to 70 %, and a lighter one is not enlarged", () => {
|
||||
const s = SLOTS["cobalt-stripe"]!;
|
||||
const m = s.mark!;
|
||||
const dense = computeLogoPlacement({ logo: logo({ aspect: m.w / m.h, density: 1 }), slot: s });
|
||||
expect(dense.h / m.h).toBeCloseTo(Math.max(0.7, Math.sqrt(s.markDensity)), 6);
|
||||
const heavy = computeLogoPlacement({ logo: logo({ aspect: m.w / m.h, density: 50 }), slot: s });
|
||||
expect(heavy.h / m.h).toBeCloseTo(0.7, 6);
|
||||
const light = computeLogoPlacement({ logo: logo({ aspect: m.w / m.h, density: 0.01 }), slot: s });
|
||||
expect(light.h).toBeCloseTo(m.h, 6);
|
||||
});
|
||||
|
||||
it("aligns by the slot's anchor and vAlign", () => {
|
||||
const s = SLOTS["crimson-grid"]!; // right, bottom
|
||||
const m = s.mark!;
|
||||
const p = computeLogoPlacement({ logo: logo({ aspect: 0.5, density: 0.1 }), slot: s });
|
||||
expect(p.x + p.w).toBeCloseTo(m.x + m.w, 6);
|
||||
expect(p.y + p.h).toBeCloseTo(m.y + m.h, 6);
|
||||
const left = computeLogoPlacement({ logo: logo({ aspect: 0.5, density: 0.1 }), slot: { ...s, anchor: "left", vAlign: "top" } });
|
||||
expect(left.x).toBeCloseTo(m.x, 6);
|
||||
expect(left.y).toBeCloseTo(m.y, 6);
|
||||
const mid = computeLogoPlacement({ logo: logo({ aspect: 0.5, density: 0.1 }), slot: { ...s, anchor: "center", vAlign: "middle" } });
|
||||
expect(mid.x + mid.w / 2).toBeCloseTo(m.x + m.w / 2, 6);
|
||||
expect(mid.y + mid.h / 2).toBeCloseTo(m.y + m.h / 2, 6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeLogoPlacement surface", () => {
|
||||
const slate = SLOTS["slate-band"]!;
|
||||
|
||||
it("uses the knockout on a dark surface, a white chip without one, and the print image elsewhere", () => {
|
||||
expect(computeLogoPlacement({ logo: logo({ hasKnockout: true }), slot: slate }).variant).toBe("knockout");
|
||||
const chip = computeLogoPlacement({ logo: logo({ hasKnockout: false }), slot: slate });
|
||||
expect(chip.variant).toBe("chip");
|
||||
expect(chip.chip).toEqual({ x: chip.x - 4, y: chip.y - 4, w: chip.w + 8, h: chip.h + 8 });
|
||||
for (const [id, s] of measured) {
|
||||
if (id === "slate-band") continue;
|
||||
expect(computeLogoPlacement({ logo: logo({ hasKnockout: false }), slot: s }).variant, id).toBe("print");
|
||||
}
|
||||
expect(computeLogoPlacement({ logo: logo({}), slot: slate }).chip).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeLogoPlacement legibility floor", () => {
|
||||
const slot = (h: number): TemplateSlot => ({
|
||||
page: { ...SLOT_PAGE },
|
||||
mark: { x: 50, y: 50, w: 100, h },
|
||||
lockup: { x: 50, y: 50, w: 100, h },
|
||||
markDensity: 0.25,
|
||||
lockupDensity: 0.25,
|
||||
anchor: "left",
|
||||
vAlign: "middle",
|
||||
darkSurface: false,
|
||||
});
|
||||
|
||||
it("raises a logo that the weight factor made too short to 14 pt", () => {
|
||||
// contain gives 16 pt tall; density 1 against 0.25 shrinks it to 11.2 pt.
|
||||
const p = computeLogoPlacement({ logo: logo({ aspect: 1, density: 1 }), slot: slot(16) });
|
||||
expect(p.h).toBe(MIN_LOGO_HEIGHT);
|
||||
expect(p.w).toBe(MIN_LOGO_HEIGHT);
|
||||
});
|
||||
|
||||
it("never grows past the box: a box under 14 pt stays the limit", () => {
|
||||
const p = computeLogoPlacement({ logo: logo({ aspect: 4, density: 0.25 }), slot: slot(9) });
|
||||
expect(p.h).toBeCloseTo(9, 6);
|
||||
expect(p.w).toBeCloseTo(36, 6);
|
||||
});
|
||||
|
||||
it("lets the width follow the floor and clamps it to the box", () => {
|
||||
const p = computeLogoPlacement({ logo: logo({ aspect: 10, density: 0.25 }), slot: slot(40) });
|
||||
// contain: 100 x 10 (width-limited), floor wants 14 x 140, the box width wins.
|
||||
expect(p.w).toBeCloseTo(100, 6);
|
||||
expect(p.h).toBeCloseTo(10, 6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeLogoPlacement page scale and available room", () => {
|
||||
const letter = 612 / 595.28;
|
||||
|
||||
it("scales the box about its anchor: a right-aligned slot keeps its right edge", () => {
|
||||
const s = SLOTS.monogram!;
|
||||
const m = s.mark!;
|
||||
const p = computeLogoPlacement({ logo: logo({ aspect: m.w / m.h, density: s.markDensity }), slot: s, scale: letter });
|
||||
expect(p.w).toBeCloseTo(m.w * letter, 6);
|
||||
expect(p.h).toBeCloseTo(m.h * letter, 6);
|
||||
expect(p.x + p.w).toBeCloseTo(m.x + m.w, 6);
|
||||
});
|
||||
|
||||
it("shrinks to the room the layout has", () => {
|
||||
const s = SLOTS["teal-swoosh"]!;
|
||||
const p = computeLogoPlacement({ logo: Sample_P, slot: s, available: { w: 80, h: 100 } });
|
||||
expect(p.w).toBeLessThanOrEqual(80 + 1e-9);
|
||||
expect(p.x).toBeCloseTo(s.lockup!.x, 6);
|
||||
});
|
||||
});
|
||||
+13
-13
@@ -1,49 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { computeLogoPlacement } from "./logo";
|
||||
import { computeClassicLogoPlacement } from "./logo";
|
||||
|
||||
const BOX = { w: 170, h: 54 };
|
||||
const base = { box: BOX, availableWidth: 299, minTextWidth: 160 };
|
||||
|
||||
describe("computeLogoPlacement", () => {
|
||||
describe("computeClassicLogoPlacement", () => {
|
||||
it("the Test Vendor trimmed 4.6:1 logo is width-limited and stacks above the text", () => {
|
||||
const p = computeLogoPlacement({ ...base, aspect: 4.6 });
|
||||
const p = computeClassicLogoPlacement({ ...base, aspect: 4.6 });
|
||||
expect(p.w).toBeCloseTo(170, 6);
|
||||
expect(p.h).toBeCloseTo(170 / 4.6, 6);
|
||||
expect(p.stacked).toBe(true);
|
||||
});
|
||||
|
||||
it("an untrimmed 3:1 logo is height-limited (54 pt) and still stacks", () => {
|
||||
const p = computeLogoPlacement({ ...base, aspect: 3 });
|
||||
const p = computeClassicLogoPlacement({ ...base, aspect: 3 });
|
||||
expect(p.h).toBe(54);
|
||||
expect(p.w).toBeCloseTo(162, 6);
|
||||
expect(p.stacked).toBe(true);
|
||||
});
|
||||
|
||||
it("a square logo is 54 x 54 and sits beside the text", () => {
|
||||
const p = computeLogoPlacement({ ...base, aspect: 1 });
|
||||
const p = computeClassicLogoPlacement({ ...base, aspect: 1 });
|
||||
expect(p).toEqual({ w: 54, h: 54, stacked: false });
|
||||
});
|
||||
|
||||
it("stacks exactly when the text beside the logo would drop under the minimum", () => {
|
||||
// 299 - 54 - 14 = 231 beside a square; a 2:1 logo (108 wide) leaves 177; a 2.5:1 one (135) leaves 150.
|
||||
expect(computeLogoPlacement({ ...base, aspect: 2 }).stacked).toBe(false);
|
||||
expect(computeLogoPlacement({ ...base, aspect: 2.5 }).stacked).toBe(true);
|
||||
expect(computeClassicLogoPlacement({ ...base, aspect: 2 }).stacked).toBe(false);
|
||||
expect(computeClassicLogoPlacement({ ...base, aspect: 2.5 }).stacked).toBe(true);
|
||||
// The boundary: leaves exactly 160.
|
||||
const w = 299 - 14 - 160;
|
||||
expect(computeLogoPlacement({ ...base, aspect: w / 54 }).stacked).toBe(false);
|
||||
expect(computeLogoPlacement({ ...base, aspect: (w + 0.01) / 54 }).stacked).toBe(true);
|
||||
expect(computeClassicLogoPlacement({ ...base, aspect: w / 54 }).stacked).toBe(false);
|
||||
expect(computeClassicLogoPlacement({ ...base, aspect: (w + 0.01) / 54 }).stacked).toBe(true);
|
||||
});
|
||||
|
||||
it("gives the logo more room on a wider column (Letter)", () => {
|
||||
const a4 = computeLogoPlacement({ ...base, aspect: 2.4, availableWidth: 299.28 });
|
||||
const letter = computeLogoPlacement({ ...base, aspect: 2.4, availableWidth: 316 });
|
||||
const a4 = computeClassicLogoPlacement({ ...base, aspect: 2.4, availableWidth: 299.28 });
|
||||
const letter = computeClassicLogoPlacement({ ...base, aspect: 2.4, availableWidth: 316 });
|
||||
expect(a4.stacked).toBe(true);
|
||||
expect(letter.stacked).toBe(false);
|
||||
});
|
||||
|
||||
it("never exceeds the box, whatever the aspect", () => {
|
||||
for (const aspect of [0.2, 0.5, 1, 1.5, 3, 4.6, 10, 40]) {
|
||||
const p = computeLogoPlacement({ ...base, aspect });
|
||||
const p = computeClassicLogoPlacement({ ...base, aspect });
|
||||
expect(p.w).toBeLessThanOrEqual(170 + 1e-9);
|
||||
expect(p.h).toBeLessThanOrEqual(54 + 1e-9);
|
||||
expect(p.w / p.h).toBeCloseTo(aspect, 6);
|
||||
@@ -51,7 +51,7 @@ describe("computeLogoPlacement", () => {
|
||||
});
|
||||
|
||||
it("falls back to the box aspect for a bad aspect", () => {
|
||||
const p = computeLogoPlacement({ ...base, aspect: Number.NaN });
|
||||
const p = computeClassicLogoPlacement({ ...base, aspect: Number.NaN });
|
||||
expect(p.w).toBeCloseTo(170, 6);
|
||||
expect(p.h).toBeCloseTo(54, 6);
|
||||
});
|
||||
|
||||
+109
-5
@@ -1,3 +1,5 @@
|
||||
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;
|
||||
@@ -24,15 +26,117 @@ 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 computeLogoPlacement({ aspect, box, availableWidth, minTextWidth, gap = LOGO_GAP }: LogoPlacementInput): LogoPlacement {
|
||||
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 };
|
||||
}
|
||||
|
||||
/*
|
||||
* Seam for reference templates: slots.generated.ts (measured placeholder boxes, see
|
||||
* scripts/logo/measure-placeholders.mjs) will supply a template's `logo.box` and may refine the fit.
|
||||
* Until it exists, every template declares its box by hand in its TemplateDefinition.
|
||||
/** 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. 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;
|
||||
const target = 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user