mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
A home abre o mesmo compositor do /cards ali mesmo (dialog no desktop, drawer no mobile) e perde o CTA: os três modelos viram botões e nada navega. A seção sobe para imediatamente antes da captura de novidades. O nome no card e no tile alinha à esquerda com o SOU (x=213) via leftX + drawCardName extraído; o tile passa a desenhar SEU NOME com o próprio pipeline real (canvas transparente sobre a base). Modal sem o vão interno (p-0 sm:p-0) e ajuda legível no tema campaign-site (--muted-foreground). Remove a variante link e o cardModelHref sem call site. Unit atualizado (+ drawCardName) e e2e com alinhamento por pixel; sem migration, sem PII em rede.
127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
/**
|
|
* S13 — the single canvas renderer shared by the preview and the PNG export:
|
|
* `renderNameCard` draws the master base then the fitted name; `renderPhotoCard`
|
|
* draws the visitor photo full-bleed then the master overlay (the overlay is
|
|
* authoritative — never redraw bands or contra-forms). The context is a
|
|
* structural subset of `CanvasRenderingContext2D` so unit tests drive a fake
|
|
* recorder instead of jsdom canvas.
|
|
*/
|
|
|
|
import { BRAND_FALLBACK_FONT, NAME_CARD_SLOT, type CardModel, type CardRect } from './cardModels'
|
|
import {
|
|
fitCardName,
|
|
type CardMeasureText,
|
|
type CardNameFit,
|
|
type CardNameSlot,
|
|
type CardTextMetrics,
|
|
} from './cardNameFit'
|
|
import {
|
|
cardPhotoDrawRect,
|
|
clampCardPhotoTransform,
|
|
type CardPhotoSize,
|
|
type CardPhotoTransform,
|
|
} from './cardPhotoTransform'
|
|
|
|
export type CardDrawContext = {
|
|
font: string
|
|
fillStyle: string | CanvasGradient | CanvasPattern
|
|
textAlign: CanvasTextAlign
|
|
textBaseline: CanvasTextBaseline
|
|
drawImage(image: CanvasImageSource, dx: number, dy: number, dWidth: number, dHeight: number): void
|
|
fillText(text: string, x: number, y: number): void
|
|
measureText(text: string): CardTextMetrics
|
|
}
|
|
|
|
export const createCardMeasure = (
|
|
ctx: Pick<CardDrawContext, 'font' | 'measureText'>,
|
|
fontFamily: string,
|
|
): CardMeasureText => {
|
|
return (text, fontSize) => {
|
|
ctx.font = `700 ${fontSize}px ${fontFamily}, ${BRAND_FALLBACK_FONT}`
|
|
const metrics = ctx.measureText(text)
|
|
|
|
return {
|
|
width: metrics.width,
|
|
actualBoundingBoxAscent: metrics.actualBoundingBoxAscent ?? 0,
|
|
actualBoundingBoxDescent: metrics.actualBoundingBoxDescent ?? 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
export type NameCardRenderArgs = {
|
|
image: CanvasImageSource
|
|
name: string
|
|
fontFamily: string
|
|
measure: CardMeasureText
|
|
slot?: CardNameSlot
|
|
}
|
|
|
|
export type CardNameDrawArgs = {
|
|
fit: Extract<CardNameFit, { ok: true }>
|
|
fontFamily: string
|
|
slot?: CardNameSlot
|
|
}
|
|
|
|
/**
|
|
* S14 — draws only the fitted name (no base image) so the home tile preview
|
|
* reuses the exact composer geometry. `fit.ok` is the caller's contract: a
|
|
* failed fit never draws (names are never cut silently).
|
|
*/
|
|
export const drawCardName = (ctx: CardDrawContext, args: CardNameDrawArgs): void => {
|
|
const slot = args.slot ?? NAME_CARD_SLOT
|
|
const { fit } = args
|
|
|
|
ctx.font = `700 ${fit.fontSize}px ${args.fontFamily}`
|
|
ctx.fillStyle = slot.fill
|
|
ctx.textAlign = 'left'
|
|
ctx.textBaseline = 'alphabetic'
|
|
|
|
if (fit.lines.length === 1) {
|
|
ctx.fillText(fit.lines[0], slot.leftX, slot.capTop + fit.capHeight)
|
|
return
|
|
}
|
|
|
|
const availableHeight = slot.maxBlockBottom - slot.capTop
|
|
const blockHeight = fit.lineHeight + fit.capHeight
|
|
const blockTop = slot.capTop + Math.max(0, (availableHeight - blockHeight) / 2)
|
|
|
|
fit.lines.forEach((line, index) => {
|
|
ctx.fillText(line, slot.leftX, blockTop + fit.capHeight + index * fit.lineHeight)
|
|
})
|
|
}
|
|
|
|
export const renderNameCard = (
|
|
ctx: CardDrawContext,
|
|
model: CardModel,
|
|
args: NameCardRenderArgs,
|
|
): CardNameFit => {
|
|
const slot = args.slot ?? NAME_CARD_SLOT
|
|
ctx.drawImage(args.image, 0, 0, model.width, model.height)
|
|
|
|
const fit = fitCardName(args.name, args.measure, slot)
|
|
if (!fit.ok) return fit
|
|
|
|
drawCardName(ctx, { fit, fontFamily: args.fontFamily, slot })
|
|
|
|
return fit
|
|
}
|
|
|
|
export type PhotoCardRenderArgs = {
|
|
photo: CanvasImageSource
|
|
frame: CanvasImageSource
|
|
photoSize: CardPhotoSize
|
|
frameSize: CardPhotoSize
|
|
window: CardRect
|
|
transform: CardPhotoTransform
|
|
}
|
|
|
|
export const renderPhotoCard = (
|
|
ctx: CardDrawContext,
|
|
args: PhotoCardRenderArgs,
|
|
): CardPhotoTransform => {
|
|
const rect = cardPhotoDrawRect(args.transform, args.photoSize, args.window)
|
|
ctx.drawImage(args.photo, rect.x, rect.y, rect.width, rect.height)
|
|
ctx.drawImage(args.frame, 0, 0, args.frameSize.width, args.frameSize.height)
|
|
|
|
return clampCardPhotoTransform(args.transform, args.photoSize, args.window)
|
|
}
|