mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:53:05 +00:00
Home abre no agradecimento pelos 152.049 votos com board 1+3, trajetória, bandeiras do mandato, galeria de fotos e rodapé institucional; chrome novo (header home/interno, lockup de nome, menu mobile acessível) e /artigos//fotos com título compacto. As superfícies de campanha saem do ar (308 de /conteudos, /jingles e /cards), junto com newsletter, estúdio público de cards e UI da Central/jingles; download de foto e .ics perdem o 1313. Sem migration, sem Consent novo e sem apagar dados do CMS.
216 lines
6.4 KiB
TypeScript
216 lines
6.4 KiB
TypeScript
/**
|
|
* S13 — pure cover/pan/zoom math for the photo card models. The visitor photo
|
|
* is drawn full-bleed under the master overlay and, by default, must always
|
|
* cover the transparent window: `clampCardPhotoTransform` keeps the zoom inside
|
|
* [1, 4] and pins the offsets so no gap can open at the window edges.
|
|
*
|
|
* S33 — the two team models carry `photoPosition: 'free'` (human gate of
|
|
* 2026-09-24): their adjustment is not clamped in any axis — the photo may
|
|
* leave the frame — while the zoom keeps its range. The policy arrives through
|
|
* `CardPhotoClampOptions`; the omitted default is the S13 bounded behavior.
|
|
*
|
|
* `centerCardPhotoTransform` is the initial framing (cover on the window, not
|
|
* on the whole card) so the face lands on the visible area of the frame.
|
|
*/
|
|
|
|
import type { CardPhotoPosition, CardRect } from './cardModels'
|
|
|
|
export const CARD_PHOTO_MIN_ZOOM = 1
|
|
export const CARD_PHOTO_MAX_ZOOM = 4
|
|
|
|
/**
|
|
* S15 — a cutout smaller than this (in source pixels) is treated as an empty
|
|
* segmentation: the card would show a speck, so the composer fails closed and
|
|
* offers a retry instead.
|
|
*/
|
|
const CARD_CUTOUT_MIN_BBOX = 24
|
|
|
|
/**
|
|
* Alpha noise floor of a cutout (matches the mask's soft edge): the bbox read
|
|
* ignores it and the S17 tone sample ignores it too, so both see the same photo.
|
|
*/
|
|
export const CARD_CUTOUT_ALPHA_THRESHOLD = 32
|
|
|
|
export type CardPhotoSize = {
|
|
width: number
|
|
height: number
|
|
}
|
|
|
|
export type CardPhotoTransform = {
|
|
zoom: number
|
|
offsetX: number
|
|
offsetY: number
|
|
}
|
|
|
|
/**
|
|
* S33 — the adjustment policy of the call site: `bounded` (omitted default)
|
|
* pins the offsets so the photo covers the window; `free` keeps the offsets
|
|
* untouched in both axes, normalizing only the zoom.
|
|
*/
|
|
export type CardPhotoClampOptions = {
|
|
position?: CardPhotoPosition
|
|
}
|
|
|
|
/** Alpha bounding box of a cutout photo, in source pixels. */
|
|
export type CardAlphaBbox = {
|
|
x: number
|
|
y: number
|
|
width: number
|
|
height: number
|
|
}
|
|
|
|
const clamp = (value: number, min: number, max: number): number =>
|
|
Math.min(Math.max(value, min), max)
|
|
|
|
export const coverScale = (source: CardPhotoSize, window: CardRect): number => {
|
|
if (source.width <= 0 || source.height <= 0 || window.width <= 0 || window.height <= 0) return 1
|
|
|
|
return Math.max(window.width / source.width, window.height / source.height)
|
|
}
|
|
|
|
export const cardPhotoDrawRect = (
|
|
transform: CardPhotoTransform,
|
|
source: CardPhotoSize,
|
|
window: CardRect,
|
|
options: CardPhotoClampOptions = {},
|
|
): CardRect => {
|
|
const clamped = clampCardPhotoTransform(transform, source, window, options)
|
|
const scale = coverScale(source, window) * clamped.zoom
|
|
|
|
return {
|
|
x: clamped.offsetX,
|
|
y: clamped.offsetY,
|
|
width: source.width * scale,
|
|
height: source.height * scale,
|
|
}
|
|
}
|
|
|
|
export const clampCardPhotoTransform = (
|
|
transform: CardPhotoTransform,
|
|
source: CardPhotoSize,
|
|
window: CardRect,
|
|
options: CardPhotoClampOptions = {},
|
|
): CardPhotoTransform => {
|
|
const zoom = clamp(transform.zoom, CARD_PHOTO_MIN_ZOOM, CARD_PHOTO_MAX_ZOOM)
|
|
if (options.position === 'free') {
|
|
return { zoom, offsetX: transform.offsetX, offsetY: transform.offsetY }
|
|
}
|
|
|
|
const scale = coverScale(source, window) * zoom
|
|
const width = source.width * scale
|
|
const height = source.height * scale
|
|
|
|
return {
|
|
zoom,
|
|
offsetX: clamp(transform.offsetX, window.x + window.width - width, window.x),
|
|
offsetY: clamp(transform.offsetY, window.y + window.height - height, window.y),
|
|
}
|
|
}
|
|
|
|
export const centerCardPhotoTransform = (
|
|
source: CardPhotoSize,
|
|
window: CardRect,
|
|
): CardPhotoTransform => {
|
|
const scale = coverScale(source, window)
|
|
const width = source.width * scale
|
|
const height = source.height * scale
|
|
|
|
return {
|
|
zoom: CARD_PHOTO_MIN_ZOOM,
|
|
offsetX: window.x + (window.width - width) / 2,
|
|
offsetY: window.y + (window.height - height) / 2,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* S15 — initial framing for a cutout photo: the alpha bbox is scaled to cover
|
|
* the window, anchored at the bbox top (head against the top of the slot) and
|
|
* centered horizontally on it. Returns `null` when the bbox is degenerate
|
|
* (empty segmentation) or would demand more than the max zoom — the composer
|
|
* fails closed instead of drawing an invisible or speck-sized photo.
|
|
*/
|
|
export const frameCardPhotoOnBbox = (
|
|
source: CardPhotoSize,
|
|
window: CardRect,
|
|
bbox: CardAlphaBbox,
|
|
): CardPhotoTransform | null => {
|
|
if (
|
|
source.width <= 0 ||
|
|
source.height <= 0 ||
|
|
window.width <= 0 ||
|
|
window.height <= 0 ||
|
|
bbox.width < CARD_CUTOUT_MIN_BBOX ||
|
|
bbox.height < CARD_CUTOUT_MIN_BBOX
|
|
) {
|
|
return null
|
|
}
|
|
|
|
const scale = coverScale(source, window)
|
|
const requiredZoom = Math.max(
|
|
window.width / bbox.width / scale,
|
|
window.height / bbox.height / scale,
|
|
)
|
|
if (!Number.isFinite(requiredZoom) || requiredZoom > CARD_PHOTO_MAX_ZOOM) return null
|
|
|
|
const zoom = Math.max(CARD_PHOTO_MIN_ZOOM, requiredZoom)
|
|
const applied = scale * zoom
|
|
|
|
return clampCardPhotoTransform(
|
|
{
|
|
zoom,
|
|
offsetX: window.x + window.width / 2 - (bbox.x + bbox.width / 2) * applied,
|
|
offsetY: window.y - bbox.y * applied,
|
|
},
|
|
source,
|
|
window,
|
|
)
|
|
}
|
|
|
|
/** Nudges the offset by a step in card pixels; clamped to the window by default. */
|
|
export const panCardPhotoTransform = (
|
|
transform: CardPhotoTransform,
|
|
source: CardPhotoSize,
|
|
window: CardRect,
|
|
dx: number,
|
|
dy: number,
|
|
options: CardPhotoClampOptions = {},
|
|
): CardPhotoTransform =>
|
|
clampCardPhotoTransform(
|
|
{ ...transform, offsetX: transform.offsetX + dx, offsetY: transform.offsetY + dy },
|
|
source,
|
|
window,
|
|
options,
|
|
)
|
|
|
|
/**
|
|
* Rescales around a window anchor (used by the zoom control): keeps the point
|
|
* under `anchor` stable from old to new zoom, then clamps — under the free
|
|
* policy only the zoom is normalized.
|
|
*/
|
|
export const zoomCardPhotoTransform = (
|
|
transform: CardPhotoTransform,
|
|
source: CardPhotoSize,
|
|
window: CardRect,
|
|
nextZoom: number,
|
|
anchor: { x: number; y: number } = {
|
|
x: window.x + window.width / 2,
|
|
y: window.y + window.height / 2,
|
|
},
|
|
options: CardPhotoClampOptions = {},
|
|
): CardPhotoTransform => {
|
|
const zoom = clamp(nextZoom, CARD_PHOTO_MIN_ZOOM, CARD_PHOTO_MAX_ZOOM)
|
|
if (zoom === transform.zoom) return clampCardPhotoTransform(transform, source, window, options)
|
|
|
|
const ratio = zoom / transform.zoom
|
|
|
|
return clampCardPhotoTransform(
|
|
{
|
|
zoom,
|
|
offsetX: anchor.x - (anchor.x - transform.offsetX) * ratio,
|
|
offsetY: anchor.y - (anchor.y - transform.offsetY) * ratio,
|
|
},
|
|
source,
|
|
window,
|
|
options,
|
|
)
|
|
}
|