feat(C208): artes da Rádio Jorge Solla 1313 — logo, capa e website card (Closes #1243)
Some checks are pending
Deploy / preflight (push) Waiting to run
Deploy / verify (push) Blocked by required conditions
Deploy / deploy staging (teqo-staging) (push) Blocked by required conditions
Deploy / requeue when main advanced (push) Blocked by required conditions
Deploy / deploy production (teqo-1313) (push) Blocked by required conditions
4
.gitignore
vendored
|
|
@ -111,6 +111,10 @@ next-env.d.ts
|
||||||
# workstation, nunca versionado (o deploy npm não o toca).
|
# workstation, nunca versionado (o deploy npm não o toca).
|
||||||
/scripts/reels/.venv/
|
/scripts/reels/.venv/
|
||||||
|
|
||||||
|
# C208 — artes da Rádio Jorge Solla 1313 (zeno.fm): o que é versionado mora em
|
||||||
|
# public/campaign-kit/radio/; aqui ficam só rascunhos/previews locais.
|
||||||
|
/data/radio-artes/
|
||||||
|
|
||||||
# Local-only sensitive material (agents may read; never commit content)
|
# Local-only sensitive material (agents may read; never commit content)
|
||||||
/private/**
|
/private/**
|
||||||
!/private/README.md
|
!/private/README.md
|
||||||
|
|
|
||||||
1
docs/changelog/2026-09-21-c208.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
**C208 — Artes da Rádio Jorge Solla 1313: logo escolhida, capa e website card (2026-09-21):** a estação no zeno.fm (que estava com os três campos de imagem ocupados pela arte da **carreata** eventual) ganha identidade geral, decidida com o humano em conversa e agora registrada no repo. Os ativos versionados vivem em `public/campaign-kit/radio/` e são reproduzidos por `scripts/build-radio-artes.mjs`: **logo** = o "O" do SOLLA da marca positiva em fundo branco (recorte do artboard oficial `public/Prancheta 1@3x.png` — letra com a estrela vazada e o rastro verde/amarelo/azul — em PNG opaco para a estrela seguir branca em qualquer fundo; o zeno exibe o logo em círculo de 152²), **capa** = `RÁDIO` (Brexter Bold) + **marca vertical** oficial (o "1313" embaixo do JORGE SOLLA, com o slogan) + o candidato à direita em conjunto centralizado (ilustração como padrão, `capa-com-foto.png` como alternativa), e **station website card** em 1200×720 no mesmo sistema de fundo branco (nome + assinatura). O contrato de cada campo foi medido na própria página da estação e virou guarda do builder (falha fechado): a capa 2048×1024 tem de manter todo o conteúdo na **faixa segura central 2048×512** (no desktop o zeno corta 4:1) e deixar branca a área do logo circular que ele sobrepõe no canto inferior esquerdo (x 60..380, y ≥700) — a ilustração (500 px) e a marca entram inteiras em y 262..762; o card é exibido em quadrado (`aspect-square`), então tudo vive no quadrado central 720×720. A **ilustração em alta** (`radio/ilustracao-solla-1313.png`, 769×1122 RGBA) sai da ilustração do card de foto de perfil (`public/cards/photo-portrait-frame.png`, que só existia a ~255 px): recorte nativo, 4× por IA (Real-ESRGAN ncnn-vulkan, modelo `realesrgan-x4plus-anime`) e remoção de fundo por flood fill com abertura morfológica (a faixa do frame é o vermelho legado `#b72031`, não a cor do kit) — receita no cabeçalho do builder. As **logos finalistas** ficam em `radio/finalistas/` (arquivo da decisão): "1313" no vermelho, marca completa positiva no branco e o "O" branco no vermelho. O mapa de uso entra na seção nova do `public/campaign-kit/README.md` (a marca continua com dono único no README + manual; nada de recópia de paleta) e o builder depende de Brexter e Arimo no fontconfig local, com instruções no cabeçalho. Sem UI do app, sem migration, sem Consent/access, sem URL pública nova — são assets estáticos + um builder de script.
|
||||||
103
docs/plans/artes-radio-jorge-solla-1313.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# Artes da Rádio Jorge Solla 1313 (zeno.fm) — logo, capa e website card
|
||||||
|
|
||||||
|
Status: entregue (2026-09-21)
|
||||||
|
Atualizado em: 2026-09-21 — as-built: ativos versionados em `public/campaign-kit/radio/` (logo, capa, website card, ilustração em alta e as três logos finalistas), gerados por `scripts/build-radio-artes.mjs`; mapa de uso no `public/campaign-kit/README.md`.
|
||||||
|
Issue: #1241 (C208) · Design UI: N/A — peças estáticas aprovadas pelo humano no fluxo (não é superfície de app)
|
||||||
|
Impeccable: A — assets e builder; nenhuma UI do app tocada
|
||||||
|
Appetite: ~0,5 dia eng; 3 ativos + finalistas + builder; sem migration
|
||||||
|
Responsável: —
|
||||||
|
|
||||||
|
## Intenção
|
||||||
|
|
||||||
|
A estação [`Rádio Jorge Solla 1313`](https://zeno.fm/radio/jorge-solla-1313/) no
|
||||||
|
zeno.fm estava com os três campos de imagem ocupados pela arte da **carreata**
|
||||||
|
(eventual). O humano pediu artes **gerais** da rádio — logo, capa e station
|
||||||
|
website card — para o painel do zeno, com a marca oficial do kit 1313, e depois
|
||||||
|
decidiu o sistema visual em conversa: a **logo escolhida é o "O" do SOLLA**
|
||||||
|
(letra oficial com a estrela vazada e o rastro verde/amarelo/azul) em fundo
|
||||||
|
branco; a capa usa a marca vertical ("1313" embaixo do "JORGE SOLLA", com o
|
||||||
|
slogan) centrada com o candidato à direita; o card segue o mesmo fundo branco.
|
||||||
|
Esta entrega **registra** no repo os ativos aprovados, as logos finalistas, a
|
||||||
|
ilustração do candidato em alta e o builder que os reproduz.
|
||||||
|
|
||||||
|
## Persona e fluxo
|
||||||
|
|
||||||
|
- **Persona / contexto:** quem opera a estação no painel do zeno (comunicação) e
|
||||||
|
quem produz peças da campanha (agentes de design, reels, dossiês).
|
||||||
|
- **Job principal:** achar o ativo oficial certo e saber o contrato de cada
|
||||||
|
campo do zeno (corte circular do logo, faixa segura da capa, corte quadrado do
|
||||||
|
card) sem adivinhar.
|
||||||
|
- **Fluxo desejado:** abre o README do kit → acha a seção da rádio → pega o
|
||||||
|
arquivo do campo → sobe no painel. Se precisar mudar, roda o builder.
|
||||||
|
|
||||||
|
## Contrato do zeno (medido na página da estação)
|
||||||
|
|
||||||
|
| Campo | Arquivo | Como o zeno exibe |
|
||||||
|
| -------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Logo | `radio/radio-jorge-solla-1313-logo.png` (1024², fundo branco opaco) | círculo pequeno (CDN entrega 152², `rounded-full`); o desenho tem de ler dentro do círculo e em ~40 px |
|
||||||
|
| Cover Image | `radio/radio-jorge-solla-1313-capa.png` (2048×1024) | 2:1 no celular, 3:1 e 4:1 no desktop (`object-cover` central) → todo o conteúdo na **faixa segura central 2048×512**; o logo circular sobreposto no desktop (x 60..380, y ≥700) tem de achar a área branca |
|
||||||
|
| Station Website Card | `radio/radio-jorge-solla-1313-website-card.png` (1200×720) | quadrado (`aspect-square`, corte central 720×720) → o conteúdo vive nesse quadrado |
|
||||||
|
|
||||||
|
- Limites de arquivo: logo ≥500² e ≤5 MB; capa ≥2048×1024 e ≤5 MB; card ≥500×300
|
||||||
|
e ≤500 KB. Todos os três ficam muito abaixo (69 KB / 325 KB / 64 KB).
|
||||||
|
- O builder **revalida** os contratos (faixa segura, área do logo do zeno e
|
||||||
|
quadrado central do card) e falha fechado se algo sair do lugar.
|
||||||
|
|
||||||
|
## Decisões (2026-09-21)
|
||||||
|
|
||||||
|
- **Logo:** o "O" do SOLLA da marca positiva em fundo branco — recorte do
|
||||||
|
artboard oficial `public/Prancheta 1@3x.png` (mesma paleta do kit:
|
||||||
|
`#e4102f`/`#184e92`/`#009647`/`#ffeb00` conferidos), em PNG opaco para a
|
||||||
|
estrela continuar branca sobre qualquer fundo. Finalistas arquivadas em
|
||||||
|
`radio/finalistas/`: "1313" no vermelho (`numero-negativo`), marca completa
|
||||||
|
positiva no branco e o "O" branco no vermelho.
|
||||||
|
- **Capa:** `RÁDIO` (Brexter Bold, vermelho) + marca vertical + candidato à
|
||||||
|
direita, em conjunto centralizado; a ilustração (padrão) e a foto
|
||||||
|
(`capa-com-foto.png`) entram inteiras na faixa segura (y 262..762).
|
||||||
|
- **Sem repetir símbolo:** onde o "JORGE SOLLA" estilizado já está, não entra o
|
||||||
|
"O" como badge — a estrela já vive dentro do próprio lockup.
|
||||||
|
- **Ilustração em alta:** a do card de foto de perfil
|
||||||
|
(`public/cards/photo-portrait-frame.png`) só existia a ~255 px; virou
|
||||||
|
769×1122 por recorte + 4× por IA (Real-ESRGAN ncnn-vulkan, modelo
|
||||||
|
`realesrgan-x4plus-anime`) + remoção de fundo por flood fill + abertura
|
||||||
|
morfológica (a faixa do frame é `#b72031`, não a cor do kit). Receita no
|
||||||
|
cabeçalho do builder; a ilustração final é entrada versionada.
|
||||||
|
|
||||||
|
## Objetivo e aceite
|
||||||
|
|
||||||
|
- `public/campaign-kit/radio/` com logo, capa, website card, `capa-com-foto.png`
|
||||||
|
(alternativa) e `ilustracao-solla-1313.png` (alta, RGBA), mais as três
|
||||||
|
finalistas em `radio/finalistas/`.
|
||||||
|
- `public/campaign-kit/README.md` com a seção da rádio (mapa de uso + contrato
|
||||||
|
dos campos + como regenerar) — a marca continua com dono único.
|
||||||
|
- `scripts/build-radio-artes.mjs` reproduz o conjunto a partir dos ativos do kit
|
||||||
|
e falha fechado fora do contrato; depende de Brexter e Arimo instalados no
|
||||||
|
fontconfig local (instruções no cabeçalho).
|
||||||
|
- ONE entrada em `docs/changelog/2026-09-21-c208.md`.
|
||||||
|
|
||||||
|
## Fora de escopo
|
||||||
|
|
||||||
|
- Trocar a arte no painel do zeno (é upload humano; a entrega é o arquivo).
|
||||||
|
- Player próprio da rádio na home (S25, Issue #1239) e qualquer UI do app.
|
||||||
|
- Redesenhar os ativos oficiais do kit ou o card de foto de perfil.
|
||||||
|
- Versionar os intermediários do upscale por IA (o `.4x` fica local; reprodutível
|
||||||
|
pelo comando documentado).
|
||||||
|
|
||||||
|
## Rabbit holes
|
||||||
|
|
||||||
|
- **Segundo mapa de uso da marca.** A seção da rádio descreve o contrato dos
|
||||||
|
campos; as regras de marca continuam só no README do kit + manual. **Corte:**
|
||||||
|
sem recópia de paleta/regra.
|
||||||
|
- **Segunda fonte da ilustração.** Guardar o `.4x` gigante no repo para "não
|
||||||
|
precisar da IA". **Corte:** a entrada é o PNG final; a receita é o comando.
|
||||||
|
- **Arte por campo com nomes próprios.** **Corte:** nomes espelham o campo do
|
||||||
|
zeno (`logo`, `capa`, `website-card`), sem sinônimos.
|
||||||
|
|
||||||
|
## Referências
|
||||||
|
|
||||||
|
- GitHub Issue: #1241 (C208)
|
||||||
|
- `public/campaign-kit/README.md` (mapa de uso; dona da marca) e
|
||||||
|
`docs/campaign-kit/manual-campanha-jorge-solla-1313.pdf`
|
||||||
|
- `scripts/build-radio-artes.mjs` (builder + receita da ilustração)
|
||||||
|
- S21/S22/S24 (`docs/plans/jingles-*.md`, `docs/plans/radio-embed-direto.md`) —
|
||||||
|
a rádio no site; aqui é a identidade da estação fora do site
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
"src/components/ui/marker.tsx",
|
"src/components/ui/marker.tsx",
|
||||||
"src/components/ui/message.tsx"
|
"src/components/ui/message.tsx"
|
||||||
],
|
],
|
||||||
"ignoreBinaries": ["opencode", "ffmpeg", "ffprobe"],
|
"ignoreBinaries": ["opencode", "ffmpeg", "ffprobe", "fc-match"],
|
||||||
"ignoreDependencies": [
|
"ignoreDependencies": [
|
||||||
"tailwindcss-animate",
|
"tailwindcss-animate",
|
||||||
"tw-animate-css",
|
"tw-animate-css",
|
||||||
|
|
|
||||||
|
|
@ -76,3 +76,35 @@ manual; as entradas abaixo descrevem a forma (uso a confirmar).
|
||||||
não é uma quarta mensagem nem headline solta.
|
não é uma quarta mensagem nem headline solta.
|
||||||
- O nome e o número são os elementos de memorização: não recriar lockups
|
- O nome e o número são os elementos de memorização: não recriar lockups
|
||||||
tipográficos quando o ativo oficial existe.
|
tipográficos quando o ativo oficial existe.
|
||||||
|
|
||||||
|
## Rádio Jorge Solla 1313 (zeno.fm)
|
||||||
|
|
||||||
|
Artes da estação [`zeno.fm/radio/jorge-solla-1313`](https://zeno.fm/radio/jorge-solla-1313/),
|
||||||
|
geradas por [`scripts/build-radio-artes.mjs`](../../scripts/build-radio-artes.mjs)
|
||||||
|
a partir dos ativos deste kit e dos artboards de alta em `public/`. Os campos do
|
||||||
|
painel do zeno têm contratos diferentes — medidos na própria página da estação:
|
||||||
|
|
||||||
|
| Arquivo | Campo | Como o zeno exibe |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `radio/radio-jorge-solla-1313-logo.png` (1024², fundo branco opaco) | Logo | círculo pequeno (CDN entrega 152², `rounded-full`) — o desenho tem de ler dentro do círculo e em ~40 px |
|
||||||
|
| `radio/radio-jorge-solla-1313-capa.png` (2048×1024) | Cover Image | 2:1 no celular, 3:1 e 4:1 no desktop (`object-cover` central) → todo o conteúdo na **faixa segura central 2048×512**; a área do logo circular sobreposto no desktop (x 60..380, y ≥700) fica branca |
|
||||||
|
| `radio/radio-jorge-solla-1313-website-card.png` (1200×720) | Station Website Card | quadrado (`aspect-square`, corte central 720×720) → o conteúdo vive nesse quadrado |
|
||||||
|
|
||||||
|
- **Logo escolhida (2026-09-21):** o "O" do SOLLA da marca positiva em fundo
|
||||||
|
branco — recorte do artboard `public/Prancheta 1@3x.png`, com a estrela vazada
|
||||||
|
e o rastro verde/amarelo/azul. As finalistas (arquivo da decisão) ficam em
|
||||||
|
`radio/finalistas/`: "1313" no vermelho, marca completa positiva no branco e o
|
||||||
|
"O" branco no vermelho.
|
||||||
|
- **Capa:** `RÁDIO` (Brexter Bold) + marca vertical oficial (o "1313" embaixo do
|
||||||
|
JORGE SOLLA, com o slogan) + o candidato à direita, em conjunto centralizado —
|
||||||
|
ilustração (padrão) ou foto (`radio/capa-com-foto.png`, alternativa).
|
||||||
|
- **Ilustração em alta:** `radio/ilustracao-solla-1313.png` (769×1122, RGBA)
|
||||||
|
parte da ilustração do card de foto de perfil
|
||||||
|
(`cards/photo-portrait-frame.png`, recorte em x 100..355, y 840..1265),
|
||||||
|
sobe 4× por IA (Real-ESRGAN ncnn-vulkan, modelo `realesrgan-x4plus-anime`) e
|
||||||
|
perde o fundo por flood fill + abertura morfológica (a faixa do frame é
|
||||||
|
`#b72031`, não a cor do kit). A receita completa está no cabeçalho do builder.
|
||||||
|
- **Regenerar:** `node scripts/build-radio-artes.mjs` — exige Brexter e Arimo no
|
||||||
|
fontconfig local (instruções no cabeçalho do script). O builder revalida o
|
||||||
|
contrato da capa e do card (faixa segura, área do logo do zeno, quadrado
|
||||||
|
central) e falha fechado.
|
||||||
|
|
|
||||||
BIN
public/campaign-kit/radio/capa-com-foto.png
Normal file
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 53 KiB |
BIN
public/campaign-kit/radio/ilustracao-solla-1313.png
Normal file
|
After Width: | Height: | Size: 883 KiB |
BIN
public/campaign-kit/radio/radio-jorge-solla-1313-capa.png
Normal file
|
After Width: | Height: | Size: 325 KiB |
BIN
public/campaign-kit/radio/radio-jorge-solla-1313-logo.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 64 KiB |
306
scripts/build-radio-artes.mjs
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
/**
|
||||||
|
* Artes da Rádio Jorge Solla 1313 — `public/campaign-kit/radio/`.
|
||||||
|
*
|
||||||
|
* Gera (e revalida) os ativos da estação no zeno.fm a partir dos ativos
|
||||||
|
* oficiais do kit 1313 e dos artboards de alta em `public/`:
|
||||||
|
*
|
||||||
|
* radio-jorge-solla-1313-logo.png 1024x1024 logo escolhida (o "O"
|
||||||
|
* do SOLLA em fundo branco;
|
||||||
|
* o zeno exibe em círculo)
|
||||||
|
* radio-jorge-solla-1313-capa.png 2048x1024 capa da estação (crop
|
||||||
|
* 4:1 no desktop = a faixa
|
||||||
|
* segura 2048x512 central)
|
||||||
|
* radio-jorge-solla-1313-website-card.png 1200x720 station website card
|
||||||
|
* (exibido em quadrado)
|
||||||
|
* capa-com-foto.png 2048x1024 alternativa da capa com
|
||||||
|
* a foto oficial
|
||||||
|
* finalistas/logo-finalista-*.png 1024x1024 logos finalistas
|
||||||
|
* (arquivo da decisão)
|
||||||
|
* ilustracao-solla-1313.png 769x1122 ilustração em alta
|
||||||
|
* (entrada versionada)
|
||||||
|
*
|
||||||
|
* Regras de marca: `public/campaign-kit/README.md` (paleta #e4102f/#184e92, marca
|
||||||
|
* positiva/oficial para fundo claro, sem recriar lockup). "RÁDIO" usa Brexter
|
||||||
|
* Bold (display face oficial, `src/app/(frontend)/fonts`) e a assinatura usa
|
||||||
|
* Arimo (face dos cards, via next/font). Prerequisito local (fora do repo):
|
||||||
|
*
|
||||||
|
* cp src/app/'(frontend)'/fonts/Brexter-Bold.ttf ~/.local/share/fonts/
|
||||||
|
* cp <repo>/.next/static/media/<arimo-latin>.woff2 ~/.local/share/fonts/Arimo-Regular.woff2
|
||||||
|
* fc-cache -f ~/.local/share/fonts
|
||||||
|
*
|
||||||
|
* Uso: node scripts/build-radio-artes.mjs
|
||||||
|
*/
|
||||||
|
import { execFileSync } from 'node:child_process'
|
||||||
|
import { statSync } from 'node:fs'
|
||||||
|
import { mkdir } from 'node:fs/promises'
|
||||||
|
import { createRequire } from 'node:module'
|
||||||
|
|
||||||
|
const require = createRequire(new URL('../package.json', import.meta.url))
|
||||||
|
const sharp = require('sharp')
|
||||||
|
|
||||||
|
const REPO = new URL('../', import.meta.url).pathname
|
||||||
|
const KIT = `${REPO}public/campaign-kit`
|
||||||
|
const OUT = `${KIT}/radio`
|
||||||
|
const FINALISTS = `${OUT}/finalistas`
|
||||||
|
|
||||||
|
const RED = '#e4102f'
|
||||||
|
const BLUE = '#184e92'
|
||||||
|
const WHITE = '#ffffff'
|
||||||
|
|
||||||
|
const SAFE_TOP = 256
|
||||||
|
const SAFE_BOTTOM = 768
|
||||||
|
const OVERLAY = { left: 60, right: 380, top: 700 } // logo circular do zeno (desktop)
|
||||||
|
|
||||||
|
const assertFont = (family) => {
|
||||||
|
const matched = execFileSync('fc-match', ['--format', '%{family}', `${family}:bold`], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
})
|
||||||
|
if (!matched.toLowerCase().includes(family.toLowerCase())) {
|
||||||
|
console.error(`Fonte "${family}" não está instalada no fontconfig local — veja o cabeçalho.`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function text({ str, font = 'Brexter', size, fill, tracking = 0, weight = 700 }) {
|
||||||
|
const w = Math.ceil(str.length * size * 1.7 + 4 * tracking + 240)
|
||||||
|
const h = Math.ceil(size * 3)
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
|
||||||
|
<text x="${w / 2}" y="${h / 2}" text-anchor="middle" dominant-baseline="central"
|
||||||
|
font-family="${font}" font-size="${size}" font-weight="${weight}" fill="${fill}"
|
||||||
|
letter-spacing="${tracking}">${str}</text></svg>`
|
||||||
|
const t = await sharp(Buffer.from(svg))
|
||||||
|
.png()
|
||||||
|
.trim({ threshold: 1 })
|
||||||
|
.png()
|
||||||
|
.toBuffer({ resolveWithObject: true })
|
||||||
|
return { input: t.data, w: t.info.width, h: t.info.height }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function asset(file, width) {
|
||||||
|
const t = await sharp(file)
|
||||||
|
.trim({ threshold: 5 })
|
||||||
|
.resize({ width })
|
||||||
|
.png()
|
||||||
|
.toBuffer({ resolveWithObject: true })
|
||||||
|
return { input: t.data, w: t.info.width, h: t.info.height }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "O" do SOLLA recortado do artboard de alta (caixa medida no trim de 1654x1245). */
|
||||||
|
async function oMonogram(file, width) {
|
||||||
|
const trim = await sharp(file).trim({ threshold: 5 }).png().toBuffer({ resolveWithObject: true })
|
||||||
|
const glyph = await sharp(trim.data)
|
||||||
|
.extract({ left: 349, top: 258, width: 361, height: 330 })
|
||||||
|
.png()
|
||||||
|
.toBuffer()
|
||||||
|
const t = await sharp(glyph).resize({ width }).png().toBuffer({ resolveWithObject: true })
|
||||||
|
return { input: t.data, w: t.info.width, h: t.info.height }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Marca vertical oficial (nome, 1313 embaixo e slogan). */
|
||||||
|
async function verticalMark(height) {
|
||||||
|
const trim = await sharp(`${REPO}public/Prancheta 1@3x.png`)
|
||||||
|
.trim({ threshold: 5 })
|
||||||
|
.png()
|
||||||
|
.toBuffer({ resolveWithObject: true })
|
||||||
|
const t = await sharp(trim.data).resize({ height }).png().toBuffer({ resolveWithObject: true })
|
||||||
|
return { input: t.data, w: t.info.width, h: t.info.height }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Foto oficial do candidato (cutout com alfa). */
|
||||||
|
async function candidatePhoto(height) {
|
||||||
|
const trim = await sharp(`${REPO}public/JOA00162.avif`)
|
||||||
|
.trim({ threshold: 5 })
|
||||||
|
.png()
|
||||||
|
.toBuffer({ resolveWithObject: true })
|
||||||
|
const t = await sharp(trim.data).resize({ height }).png().toBuffer({ resolveWithObject: true })
|
||||||
|
return { input: t.data, w: t.info.width, h: t.info.height }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ilustração em alta (recorte + upscale por IA + fundo removido — ver README do kit). */
|
||||||
|
async function illustration(height) {
|
||||||
|
const t = await sharp(`${OUT}/ilustracao-solla-1313.png`)
|
||||||
|
.trim({ threshold: 1 })
|
||||||
|
.resize({ height })
|
||||||
|
.png()
|
||||||
|
.toBuffer({ resolveWithObject: true })
|
||||||
|
return { input: t.data, w: t.info.width, h: t.info.height }
|
||||||
|
}
|
||||||
|
|
||||||
|
const place = (layers) =>
|
||||||
|
layers.map(({ asset: a, x, y, cx, cy }) => ({
|
||||||
|
input: a.input,
|
||||||
|
left: Math.round(x ?? cx - a.w / 2),
|
||||||
|
top: Math.round(y ?? cy - a.h / 2),
|
||||||
|
}))
|
||||||
|
|
||||||
|
async function render({ name, width, height, layers, bg = WHITE }) {
|
||||||
|
const buffer = await sharp({ create: { width, height, channels: 4, background: bg } })
|
||||||
|
.composite(place(layers))
|
||||||
|
.png({ compressionLevel: 9 })
|
||||||
|
.toBuffer()
|
||||||
|
await sharp(buffer).toFile(`${OUT}/${name}`)
|
||||||
|
console.log(
|
||||||
|
name,
|
||||||
|
`${width}x${height}`,
|
||||||
|
`${(statSync(`${OUT}/${name}`).size / 1024).toFixed(0)}KB`,
|
||||||
|
)
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Faixa segura da capa inteira branca fora de y 256..768, e livre do logo do zeno. */
|
||||||
|
async function assertCoverSafe(buffer) {
|
||||||
|
const { data, info } = await sharp(buffer).raw().toBuffer({ resolveWithObject: true })
|
||||||
|
const white = (i) => data[i] > 250 && data[i + 1] > 250 && data[i + 2] > 250
|
||||||
|
let outside = 0
|
||||||
|
let overlay = 0
|
||||||
|
for (let y = 0; y < info.height; y++) {
|
||||||
|
for (let x = 0; x < info.width; x++) {
|
||||||
|
const i = (y * info.width + x) * 4
|
||||||
|
if (white(i)) continue
|
||||||
|
if (y < SAFE_TOP || y >= SAFE_BOTTOM) outside++
|
||||||
|
if (x >= OVERLAY.left && x <= OVERLAY.right && y >= OVERLAY.top) overlay++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (outside > 0 || overlay > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`capa fora do contrato: ${outside}px fora da faixa segura, ${overlay}px no logo do zeno`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Card: nada fora do quadrado central 720x720 (é o crop exibido no zeno). */
|
||||||
|
async function assertCardSquare(buffer) {
|
||||||
|
const { data, info } = await sharp(buffer).raw().toBuffer({ resolveWithObject: true })
|
||||||
|
const white = (i) => data[i] > 250 && data[i + 1] > 250 && data[i + 2] > 250
|
||||||
|
let outside = 0
|
||||||
|
for (let y = 0; y < info.height; y++) {
|
||||||
|
for (let x = 0; x < info.width; x++) {
|
||||||
|
if (x >= 240 && x < 960) continue
|
||||||
|
if (!white((y * info.width + x) * 4)) outside++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (outside > 0) throw new Error(`card com ${outside}px fora do quadrado central`)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFont('Brexter')
|
||||||
|
assertFont('Arimo')
|
||||||
|
|
||||||
|
const ILLUSTRATION = `${OUT}/ilustracao-solla-1313.png`
|
||||||
|
try {
|
||||||
|
await sharp(ILLUSTRATION).metadata()
|
||||||
|
} catch {
|
||||||
|
console.error(
|
||||||
|
`Ilustração ausente: ${ILLUSTRATION} — o pipeline está documentado em public/campaign-kit/README.md.`,
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
await mkdir(FINALISTS, { recursive: true })
|
||||||
|
|
||||||
|
/* ------------------------------- Logo -------------------------------
|
||||||
|
* O "O" do SOLLA da marca positiva em fundo branco opaco — legível no círculo
|
||||||
|
* pequeno do zeno e sem repetir o símbolo onde o nome estilizado já está.
|
||||||
|
*/
|
||||||
|
await render({
|
||||||
|
name: 'radio-jorge-solla-1313-logo.png',
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
layers: [{ asset: await oMonogram(`${REPO}public/Prancheta 1@3x.png`, 580), cx: 512, cy: 512 }],
|
||||||
|
})
|
||||||
|
|
||||||
|
/* ------------------------------- Capa -------------------------------
|
||||||
|
* "RÁDIO" + marca vertical (nome, 1313 e slogan) + candidato à direita em
|
||||||
|
* conjunto centralizado; tudo dentro da faixa segura 2048x512.
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
const KICKER_PX = 58
|
||||||
|
const KICKER_GAP = 14
|
||||||
|
const MARK_H = 420
|
||||||
|
const PERSON_H = 500
|
||||||
|
const PERSON_GAP = 60
|
||||||
|
|
||||||
|
const kicker = await text({ str: 'RÁDIO', size: KICKER_PX, fill: RED, tracking: 22 })
|
||||||
|
const mark = await verticalMark(MARK_H)
|
||||||
|
const top = 512 - (kicker.h + KICKER_GAP + mark.h) / 2
|
||||||
|
for (const [name, person] of [
|
||||||
|
['radio-jorge-solla-1313-capa.png', await illustration(PERSON_H)],
|
||||||
|
['capa-com-foto.png', await candidatePhoto(PERSON_H)],
|
||||||
|
]) {
|
||||||
|
const groupW = mark.w + PERSON_GAP + person.w
|
||||||
|
const markX = Math.round((2048 - groupW) / 2)
|
||||||
|
const buffer = await render({
|
||||||
|
name,
|
||||||
|
width: 2048,
|
||||||
|
height: 1024,
|
||||||
|
layers: [
|
||||||
|
{ asset: kicker, cx: markX + mark.w / 2, cy: top + kicker.h / 2 },
|
||||||
|
{ asset: mark, x: markX, y: top + kicker.h + KICKER_GAP },
|
||||||
|
{ asset: person, cx: markX + mark.w + PERSON_GAP + person.w / 2, cy: 512 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
await assertCoverSafe(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------- Website card ----------------------------
|
||||||
|
* 1200x720 (5:3) · o zeno exibe em quadrado (corte central 720x720).
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
const kicker = await text({ str: 'RÁDIO', size: 52, fill: RED, tracking: 16 })
|
||||||
|
const mark = await asset(`${KIT}/jorge-solla-positivo.png`, 560)
|
||||||
|
const tag = await text({
|
||||||
|
str: 'A rádio da nossa caminhada pela Bahia.',
|
||||||
|
font: 'Arimo',
|
||||||
|
weight: 400,
|
||||||
|
size: 30,
|
||||||
|
fill: BLUE,
|
||||||
|
tracking: 1,
|
||||||
|
})
|
||||||
|
let y = 360 - (kicker.h + 14 + mark.h + 22 + tag.h) / 2
|
||||||
|
const layers = []
|
||||||
|
for (const [item, gap] of [
|
||||||
|
[kicker, 14],
|
||||||
|
[mark, 22],
|
||||||
|
[tag, 0],
|
||||||
|
]) {
|
||||||
|
layers.push({ asset: item, cx: 600, y })
|
||||||
|
y += item.h + gap
|
||||||
|
}
|
||||||
|
const buffer = await render({
|
||||||
|
name: 'radio-jorge-solla-1313-website-card.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 720,
|
||||||
|
layers,
|
||||||
|
})
|
||||||
|
await assertCardSquare(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------- Finalistas ----------------------------
|
||||||
|
* O arquivo da decisão de logo (2026-09-21): a escolhida é o "O" (acima).
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
const numero = await asset(`${KIT}/numero-negativo.png`, 860)
|
||||||
|
await render({
|
||||||
|
name: 'finalistas/logo-finalista-1313-no-vermelho.png',
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
bg: RED,
|
||||||
|
layers: [{ asset: numero, cx: 512, cy: 512 }],
|
||||||
|
})
|
||||||
|
|
||||||
|
const completa = await asset(`${REPO}public/Prancheta 1@3x.png`, 840)
|
||||||
|
await render({
|
||||||
|
name: 'finalistas/logo-finalista-marca-completa-no-branco.png',
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
layers: [{ asset: completa, cx: 512, cy: 512 }],
|
||||||
|
})
|
||||||
|
|
||||||
|
const oBranco = await oMonogram(`${REPO}public/Prancheta 3@3x.png`, 580)
|
||||||
|
await render({
|
||||||
|
name: 'finalistas/logo-finalista-o-branco-no-vermelho.png',
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
bg: RED,
|
||||||
|
layers: [{ asset: oBranco, cx: 512, cy: 512 }],
|
||||||
|
})
|
||||||
|
}
|
||||||