teqo-backup/src/lib/cardModels.ts
Francisco Solla 681f3bb566 feat(S13): cards personalizados da campanha no site público
Home ganha a seção Faça parte com os três modelos e a rota /cards
concentra catálogo + compositor client-side (dialog desktop, bottom
drawer mobile): nome no slot medido do mestre com Brexter 700 e foto
full-bleed sob o overlay oficial (pan/zoom clampados), download PNG
sem conta e sem enviar nome/foto. Núcleo puro em src/lib/card* com
unit tests; e2e cobre home→deep-link, nome→PNG real, erro fail-closed,
foto e drawer mobile. Sem migration/PII.
2026-09-12 03:25:43 -03:00

90 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* S13 — the three personalized-card models of the campaign site. Pure data so
* the home section, the `/cards` gallery and the canvas renderer share one
* catalog (no second editor, no CMS): each entry carries the master asset, the
* real output size in pixels and, for photo models, the transparent window the
* visitor's photo must cover.
*
* Geometry comes from the master files (pixel-measured, keep in sync with
* `docs/plans/cards-personalizados-campanha-impl.md`): the photo frames are
* 1000×1000 and 1000×1440 despite the `1080x` in the original file names.
*/
const CARD_MODEL_IDS = ['eu-sou-solla', 'perfil-quadrado', 'perfil-retangular'] as const
export type CardModelId = (typeof CARD_MODEL_IDS)[number]
type CardModelKind = 'name' | 'photo'
export type CardRect = {
x: number
y: number
width: number
height: number
}
export type CardModel = {
id: CardModelId
kind: CardModelKind
label: string
assetSrc: string
width: number
height: number
/** Photo models only: the transparent window the photo must cover. */
photoWindow?: CardRect
}
export const CARD_MODELS: readonly CardModel[] = [
{
id: 'eu-sou-solla',
kind: 'name',
label: 'Card com seu nome',
assetSrc: '/cards/name-card-base.jpg',
width: 1080,
height: 1440,
},
{
id: 'perfil-quadrado',
kind: 'photo',
label: 'Moldura quadrada',
assetSrc: '/cards/photo-square-frame.png',
width: 1000,
height: 1000,
photoWindow: { x: 0, y: 0, width: 1000, height: 740 },
},
{
id: 'perfil-retangular',
kind: 'photo',
label: 'Moldura vertical',
assetSrc: '/cards/photo-portrait-frame.png',
width: 1000,
height: 1440,
photoWindow: { x: 0, y: 0, width: 1000, height: 1044 },
},
]
/**
* Name model slot, pixel-measured from the filled master (`FULANO` example):
* glyph cap band tops at y=430 with a 106px cap height, ink spans x[213..926]
* (714px) centered at x≈570 — not at the canvas center. The visitor's name is
* the only thing drawn over the base; the fixed lines already live in the art.
*/
export const NAME_CARD_SLOT = {
centerX: 570,
capTop: 430,
capHeight: 106,
maxInkWidth: 714,
maxBlockBottom: 630,
minCapHeight: 48,
fill: '#ffec01',
maxLines: 2,
} as const
export const BRAND_FALLBACK_FONT = 'sans-serif'
export const isCardModelId = (value: unknown): value is CardModelId =>
typeof value === 'string' && (CARD_MODEL_IDS as readonly string[]).includes(value)
export const getCardModel = (id: CardModelId): CardModel | undefined =>
CARD_MODELS.find((model) => model.id === id)
export const cardModelHref = (id: CardModelId): string => `/cards?model=${id}`