# Blocos: hero 3D feito no Blender, movimento e ícone do app **Autor:** Evandro · **Data:** 2026-11-09 · **Porte:** grande · **Status:** em execução ## 1. Objetivo Trocar o fundo de partículas do hero por uma peça 3D modelada no Blender (os blocos da marca, que se remontam em tela, integrações e dados), dar ao site uma linguagem de movimento única (escrita, entrada e saída de componentes, troca de tela) e corrigir o ícone do aplicativo instalado. Hoje o 3D é um fundo discreto, só no computador, e o celular não recebe nada. Leitura (frontend-orchestrator): refino com mundo visual novo no hero · Persuade (suposição S1) · web · `3d`, `premium` → impeccable, awwwards, threejs-fundamentals/geometry/materials/shaders/animation. ## 2. Escopo - Dentro: ícones do manifesto; pipeline Blender → web em `3d/`; hero em três níveis (tempo real, tempo real leve, pôster); escrita, entrada/saída e troca de tela em CSS; peças 3D pré-renderizadas nos cartões de Soluções e no 404. - Fora (não fazer): biblioteca de animação (GSAP, Motion) ou React Three Fiber, removidos em 2026-10-14 por desempenho; mudar texto, SEO ou estrutura das seções; service worker; modelos gerados por IA ou de bibliotecas de terceiros; vídeo de rolagem no celular; merge dos branches `wip/*`. ## 3. Critérios de aceite - CA1 (ícone): o manifesto serve 4 ícones em URLs novas (`any` e `maskable`, 192 e 512) e `id: "/"`; no maskable a marca cabe na zona segura (caixa ≤ 57% do lado, centrada ±2%) → `pnpm test -- app/manifest.test.ts` e `node scripts/build-icons.mjs --check`. - CA2 (ativos): `blender -b --python 3d/blocos.py` recria do zero `public/3d/` (`bloco.glb` ≤ 40 KB, `blocos.json` ≤ 40 KB em gzip, 2 matcaps ≤ 30 KB cada, `hero-poster.webp` ≤ 90 KB), com N ≤ 320 blocos e ≤ 60 mil triângulos → o comando sai com `OK` e código 0. - CA3 (pôster): sem JavaScript ou com movimento reduzido o hero mostra o pôster da marca em blocos, todo o conteúdo visível, sem pedido a `/3d/bloco.glb` nem ao pacote do Three.js → `pnpm test:e2e -- hero-3d`. - CA4 (computador): em aparelho `full` os blocos montam a marca em até 2,6 s e a rolagem leva marca → tela → integrações → dados dentro dos trechos de `hero-captions.ts` → `pnpm test -- lib/blocos.test.ts` e captura local com GPU nos progressos 0, 0,35, 0,62 e 0,85. - CA5 (celular): em aparelho `lite` a cena roda com DPR ≤ 1,5 e 30 quadros por segundo sob demanda, trocando de estado sozinha enquanto está à vista; se a média dos 60 primeiros quadros passar de 28 ms, volta ao pôster e lembra na sessão → `pnpm test -- lib/device.test.ts BlocosScene` e inspeção num celular real. - CA6 (desempenho): em 390×844 com rede e CPU limitadas, LCP < 2,5 s e continua sendo o H1, CLS < 0,05, os dois botões inteiros na primeira dobra → `shoot.mjs` do awwwards (`mobile.vitals`) sobre `pnpm build && pnpm start`. - CA7 (escrita): rótulos em fonte mono entram digitados (cursor de bloco) e o trecho verde do H1 entra como marca-texto; o texto está no HTML desde o primeiro quadro; só `transform`, `opacity` e `clip-path` animam → `pnpm test -- Hero` e inspeção com movimento reduzido. - CA8 (entrada e saída): menu do celular, vídeo ampliado, aviso de cookies, botão do WhatsApp e perguntas frequentes animam ao abrir e ao fechar, sem JavaScript novo; navegador sem suporte troca na hora → `pnpm test:e2e -- mobile-menu keyboard` verdes e inspeção. - CA9 (troca de tela): a troca de página usa a cortina de blocos (≤ 700 ms) e a capa do projeto vira o topo do case; com movimento reduzido não há animação → `pnpm test:e2e -- navigation routes` verdes e captura dos quadros da troca. - CA10 (peças): os 3 cartões de Soluções e o 404 mostram peças do mesmo kit renderizadas no Blender (loop ≤ 3 s, ≤ 250 KB por arquivo, pôster ≤ 40 KB), tocando só à vista, sem contexto WebGL extra → `ls -l public/3d` e `pnpm test:e2e`. - CA11 (fechamento): `pnpm lint && pnpm typecheck && pnpm test && pnpm build && pnpm test:e2e` verdes; impeccable critique sem heurística abaixo de 3/4; audit sem falha; jury com 8 ou mais em cada categoria. ## 4. Contexto verificado Código lido no `main` do Gitea (`4671837`, 2026-10-24), que é o que está no ar. O checkout local está em `b2f3899` (2026-10-05), 54 commits atrás, com `origin` num repositório do GitHub que não existe mais. - `package.json`: só `three` 0.182 e `lenis`; animação é CSS (`animation-timeline`, View Transitions). - `app/components/cinematic/HeroFieldScene.tsx:26,422,692,969,999`: nuvem de pontos procedural em três camadas, fundo ambiente. Padrão de referência do motor (renderizador, descarte, desenho por software). - `app/components/cinematic/HeroField.tsx:195,201` e `lib/device.ts:42-53`: toque, ≤ 4 núcleos, ≤ 4 GB, economia de dados ou movimento reduzido não recebem cena nem baixam o Three.js. - `app/components/Hero.tsx:64-66,92,143`: palco, H1 (LCP) e `HeroWork` (navegador e celular com trabalho real, 40% da largura). - `app/components/hero-captions.ts`: legendas "A tela", "As integrações", "Os dados" com trechos 0,18–0,49, 0,51–0,75, 0,79–1,07. - `app/manifest.ts:17-28`: dois ícones (`/brand/icon-192.png`, `/icon.png`), sem `purpose` nem `id`. Arquivos iguais aos do ar (md5) e com o símbolo de blocos atual. A marca ocupa 395×392 de 512 (77%), fora da zona segura. - `app/template.tsx:29` e `app/globals.css:1038-1054`: troca de página por ``, fade com escala. - `app/globals.css:755-767,963-977,1075`: entrada do título, das seções (`ds-scene-in`, `view()`) e dos títulos por palavra. Não há saída animada. - `app/components/WorkModalities.tsx:265,348`, `app/not-found.tsx:34-46` (404 fora da marca), `Faq.tsx:43`, `Header.tsx:247`, `showcase/ProjectLightbox.tsx:253`, `showcase/ProjectList.tsx:84`, `app/cases/_components/CaseCover.tsx:57`. - Máquina: Blender 5.2.2 LTS (`io_scene_gltf2`, Cycles com OptiX na RTX 5060 Ti 16 GB, addon `blender_mcp`), `ffmpeg`, `magick`. Não há outro programa de 3D instalado. ## 5. Desenho **Conceito.** Ideia: os blocos da marca são as peças com que se monta o site ou o sistema de cada cliente. Assinatura: os blocos se soltam e se remontam com a rolagem. Verbo: encaixar (chega rápido, assenta curto, sem quique). Quadro para compartilhar: a marca em porcelana com os quatro cubos verdes ao lado do título. Decisões: - Um kit, quatro estados (`marca`, `tela`, `integracoes`, `dados`) com o mesmo número de blocos. As três legendas atuais ficam como estão. - Fonte é o script `3d/blocos.py` (mapas de blocos, chanfro, materiais, luz, exportação). O `.blend` é gerado em `3d/out/` (ignorado no git). Descartado: `.blend` modelado à mão e versionado, binário que ninguém revisa nem reconstrói. - Tempo real com uma `InstancedMesh` e matcaps renderizados dos materiais do Blender; a troca de estado é interpolada no vertex shader. Descartado: luz e sombra em tempo real (peso no celular) e vídeo de rolagem (o projeto já o tirou do celular). - `GLTFLoader` do próprio `three`, sem Draco nem meshopt (a geometria é um bloco só). - Três níveis: `full` (regra atual), `lite` (toque, ≥ 6 núcleos, ≥ 4 GB ou não informado), `static` (o resto, e qualquer aparelho que estoure o orçamento de quadro). - Movimento 2D só com CSS: `steps()` nos detalhes (cursor, cortina), ease-out no resto, `@starting-style` com `allow-discrete` para saída. - Peças secundárias pré-renderizadas no Cycles (vídeo curto com pôster), nunca WebGL por cartão. Interfaces: ```ts // public/3d/blocos.json { n: number; estados: ["marca","tela","integracoes","dados"]; dados: number[] } // [estado][bloco] = x, y, z, escala, tom (0 porcelana, 1 lima) // lib/blocos.ts export function stateAt(p: number): { from: number; to: number; t: number } // lib/device.ts export type SceneTier = "full" | "lite" | "static"; export function sceneTier(): SceneTier // app/components/cinematic/BlocosScene.tsx: mesmas props de HeroFieldSceneProps export function prepareBlocos(): Promise export default function BlocosScene(props: HeroFieldSceneProps) ``` Mapa de arquivos. Criar: `3d/blocos.py`, `3d/pecas.py`, `public/3d/*`, `public/brand/pwa-*.png`, `scripts/build-icons.mjs`, `lib/blocos.ts`, `app/components/cinematic/BlocosScene.tsx`, `e2e/hero-3d.spec.ts`, `DESIGN.md`. Modificar: `app/manifest.ts`, `lib/device.ts`, `HeroField.tsx`, `Hero.tsx`, `app/globals.css`, `WorkModalities.tsx`, `not-found.tsx`, `ProjectList.tsx`, `CaseCover.tsx`. Remover no fim: `HeroFieldScene.tsx` e seus testes, `public/brand/icon-192.png`. ## 6. Restrições, riscos, suposições - Restrições: Next 16 com APIs diferentes (ler `node_modules/next/dist/docs/` antes de codar, `AGENTS.md`); trabalho em branch, nunca na `main`; nenhum servidor de desenvolvimento em segundo plano (`pnpm build && pnpm start`, encerrado ao fim). - Riscos: o CI roda sem GPU, então a cena em tempo real só se confere em captura local; o celular pode perder LCP (o orçamento de quadro e o CA6 seguram); sincronizar o checkout exige trocar o `origin`. Reverter é desfazer o branch; nada toca dados. - O worktree `~/.cache/security-fixes/portifolio-evandrodevbr` (branch `chore/security-updates-2026-11-09`) nasceu do mesmo commit velho e vai conflitar com o `main` real. - S1: o site é de venda (Persuade), com vitrine de portfólio. - S2: há um celular Android e um iPhone para a inspeção dos CA5 e CA1. - P1 (bloqueia T2): qual é a "logo nova"? O repositório e o site no ar já usam o símbolo de blocos nos ícones; nada mais novo foi encontrado. Resposta recomendada: é esse símbolo, e o que se vê no celular é ícone sem versão maskable mais o ícone guardado da instalação antiga. - P2 (bloqueia T7): no computador, a peça 3D ocupa a coluna da direita e as molduras de `HeroWork` saem do hero (os projetos reais começam uma rolagem abaixo)? Resposta recomendada: sim. ## 7. Microtasks ### Fase 0 · base e ícone ### T0 · Sincronizar o checkout com o Gitea · N0 - Serve: todos · Depende de: "ok" do usuário - Fazer: apagar `AGENTS.md` e `CLAUDE.md` não rastreados (idênticos aos do Gitea); `git remote set-url origin https://git.evandro.dev.br/evandro/portifolio-evandrodevbr.git`; `git pull --ff-only`; `git switch -c feat/blocos-3d-motion`; `pnpm install --frozen-lockfile`. - Verificar: `git log -1 --format=%h` → `4671837` ou mais novo; `pnpm test` verde. - [x] feito ### T1 · Escrever o teste do manifesto · N2 - Serve: CA1 · Depende de: T0 · Arquivos: criar `app/manifest.test.ts` · Padrão: `app/robots.test.ts` - Fazer: afirmar `id === "/"` e exatamente estes ícones: `/brand/pwa-192.png` e `/brand/pwa-512.png` (`purpose: "any"`), `/brand/pwa-maskable-192.png` e `/brand/pwa-maskable-512.png` (`purpose: "maskable"`), todos `image/png`, e que cada arquivo existe em `public/`. - Verificar: `pnpm test -- app/manifest.test.ts` → falha (ainda não implementado). - [x] feito ### T2 · Gerar os ícones a partir do símbolo · N2 - Serve: CA1 · Depende de: T1, bloqueada por P1 · Arquivos: criar `scripts/build-icons.mjs` e os 4 PNG em `public/brand/` - Fazer: com `sharp`, compor `public/brand/mark-dark.svg` sobre fundo `rgb(8,10,9)`. `any`: marca com 77% do lado. `maskable`: marca com 56% do lado, centrada. `--check` relê os PNG e falha se o tamanho ou a caixa da marca saírem do CA1. - Verificar: `node scripts/build-icons.mjs && node scripts/build-icons.mjs --check` → `OK`. - [x] feito ### T3 · Apontar o manifesto para os ícones novos · N2 - Serve: CA1 · Depende de: T2 · Arquivos: modificar `app/manifest.ts:17-28`; remover `public/brand/icon-192.png` - Fazer: trocar a lista de ícones pela do T1 e acrescentar `id: "/"`. - Verificar: `pnpm test -- app/manifest.test.ts` → passa; `grep -rn "icon-192" app lib scripts e2e` → vazio. - [x] feito ### CP1 · após T0–T3 · N0 - Verificar: `pnpm lint && pnpm typecheck && pnpm test && pnpm build`. Depois do deploy, reinstalar o app no Android e no iPhone (o iPhone só troca o ícone reinstalando) e conferir o ícone. ### Fase 1 · Blender e hero ### T4 · Registrar conceito e movimento em DESIGN.md · N4 - Serve: CA4, CA7, CA9 · Depende de: T0 · Arquivos: criar `DESIGN.md` - Fazer: cartão do conceito da seção 5, tokens que já existem em `globals.css`, durações e curvas do verbo "encaixar", os três níveis de aparelho. - Pronto quando: `impeccable context` lê o arquivo sem aviso. - [x] feito ### T5 · Montar o kit e os estados no Blender · N3 - Serve: CA2 · Depende de: T0 · Arquivos: criar `3d/blocos.py`; `.gitignore` com `3d/out/` - Fazer: bloco chanfrado (2 segmentos); os 4 estados como mapas de blocos no próprio script, com `marca` derivado dos retângulos de `public/brand/mark-dark.svg`; exportar `bloco.glb` e `blocos.json` no formato da seção 5; afirmar no script o mesmo N em todos os estados e os limites do CA2. - Verificar: `blender -b --python 3d/blocos.py` → `OK blocos= tris=` e código 0. - [x] feito ### T6 · Acertar material, luz e pôster · N4 - Serve: CA2, CA3 · Depende de: T5 · Arquivos: modificar `3d/blocos.py` - Fazer: porcelana fosca `#f4f3ef` e esmalte `#d5fa00` com emissão leve, luz de estúdio, Cycles com OptiX; renderizar os 2 matcaps (256 px) e `hero-poster.webp` (1200 px, fundo transparente). Ajuste fino pelo Blender MCP (`execute_blender_code`, `look`), com os valores copiados para o script. - Pronto quando: pôster sem ruído, com as proporções de `mark-dark.svg` e 4 cubos verdes; arquivos dentro do CA2. - [x] feito ### CP2 · após T4–T6 · N0 - Verificar: mostrar o pôster ao usuário e seguir só com o "ok" dele. ### T7 · Compor o hero com o pôster · N4 - Serve: CA3, CA6 · Depende de: T6, bloqueada por P2 · Arquivos: `Hero.tsx:64-66,143`, `app/globals.css` (bloco `.hero-*`), criar `e2e/hero-3d.spec.ts` - Fazer: pôster via `next/image` (lazy, prioridade baixa) no lugar de `.hero-glow::after`; no computador, coluna da direita; em 390 px, sem cobrir texto e com ≥ 96 px visíveis na primeira dobra. Teste e2e do CA3 primeiro. - Verificar: `pnpm test:e2e -- hero-3d` → passa; capturas em 1440×900 e 390×844 aprovadas pelo usuário. - [x] feito ### T8 · Classificar o aparelho em três níveis · N2 - Serve: CA5 · Depende de: T0 · Arquivos: `lib/device.ts:42-53`, `lib/device.test.ts` - Fazer: `sceneTier()`: `static` com movimento reduzido, economia de dados, ≤ 4 núcleos ou memória informada < 4; senão `lite` com ponteiro `coarse` (exige ≥ 6 núcleos quando informado); senão `full` pela regra atual. Remover `isLowPowerDevice` e trocar a chamada em `HeroField.tsx:195`. Testes primeiro. - Verificar: `pnpm test -- lib/device.test.ts` → passa; `grep -rn isLowPowerDevice app lib` → vazio. - [x] feito ### T9 · Escrever o motor da cena de blocos · N3 - Serve: CA4, CA5 · Depende de: T5, T6, T8 · Arquivos: criar `lib/blocos.ts`, `lib/blocos.test.ts`, `app/components/cinematic/BlocosScene.tsx` · Padrão: `HeroFieldScene.tsx:692-999`, `FramePulse.ts` - Fazer: `stateAt(p)` com os trechos de `hero-captions.ts` (teste primeiro); `InstancedMesh` com os estados em atributos por instância e mistura no vertex shader; entrada em 2,6 s; `full` segue `progressRef`, `lite` troca de estado a cada 4 s à vista; orçamento de 28 ms nos 60 primeiros quadros chama `onError` e grava na sessão. - Verificar: `pnpm test -- lib/blocos.test.ts BlocosScene` → passa. - [x] feito ### T10 · Ligar a cena ao hero · N2 - Serve: CA3, CA4, CA5 · Depende de: T7, T9 · Arquivos: `HeroField.tsx`, `app/globals.css` (`.hero-field`) - Fazer: importar `BlocosScene` e `prepareBlocos` no lugar de `HeroFieldScene` e `prepareField`; `static` não carrega nada; `lite` sem `trackDive`; o canvas cobre o pôster com fade quando `data-ready`. - Verificar: `pnpm test -- HeroField` e `pnpm test:e2e -- hero-3d scroll` → passam. - [x] feito ### CP3 · após T7–T10 · N0 - Verificar: suíte e build; captura local com GPU (CA4); celular real (CA5); `shoot.mjs` (CA6). ### T11 · Remover a cena de partículas · N2 - Serve: CA11 · Depende de: CP3 · Arquivos: remover `HeroFieldScene.tsx` e `HeroFieldScene*.test.tsx`; ajustar o comentário de `hero-captions.ts` - Verificar: `grep -rn HeroFieldScene app lib` → vazio; `pnpm typecheck && pnpm test` → passam. - [x] feito ### Fase 2 · movimento 2D ### T12 · Animar a escrita · N2 - Serve: CA7 · Depende de: T4 · Arquivos: `app/globals.css` (junto de `:755-767`), `Hero.tsx:92`, `Section.tsx` - Fazer: classe `.type-line` (largura em `ch` com `steps(var(--n))`, cursor de bloco verde que pisca 3 vezes) nos rótulos mono; marca-texto do trecho verde do H1 por `scaleX` num pseudo-elemento; tudo dentro de `prefers-reduced-motion: no-preference`. - Verificar: `pnpm test -- Hero Section` → passa; inspeção com movimento reduzido: texto inteiro, sem cursor. - [x] feito ### T13 · Animar entrada e saída dos componentes · N2 - Serve: CA8 · Depende de: T4 · Arquivos: `app/globals.css`, `app/components/showcase/lightbox.css`, `app/components/conversion.css` - Fazer: `@starting-style` com `transition-behavior: allow-discrete` no menu do celular, no `` e no `::backdrop`, no aviso de cookies e no botão do WhatsApp; `interpolate-size: allow-keywords` com `::details-content` nas perguntas frequentes; 180 ms para sair, 280 ms para entrar. - Verificar: `pnpm test:e2e -- mobile-menu keyboard contact` → passam. - [x] feito ### T14 · Trocar de página com a cortina de blocos · N3 - Serve: CA9 · Depende de: T4 · Arquivos: `app/globals.css:1038-1054` - Fazer: substituir `page-out` e `page-in` por uma máscara em grade que fecha e abre na diagonal em `steps()`, 240 ms para sair e 460 ms para entrar; manter o cabeçalho parado e o bloco de movimento reduzido. - Verificar: `pnpm test:e2e -- navigation routes` → passam; captura de 3 quadros da troca. - [x] feito ### T15 · Levar a capa do projeto ao topo do case · N3 - Serve: CA9 · Depende de: T14 · Arquivos: `showcase/ProjectList.tsx:84`, `app/cases/_components/CaseCover.tsx:57` · Padrão: `app/template.tsx:29` - Fazer: envolver as duas imagens em ``, conforme a documentação do Next 16 instalada. - Verificar: `pnpm test -- ProjectList CaseCover` e `pnpm test:e2e -- navigation` → passam. - [x] feito ### CP4 · após T12–T15 · N0 - Verificar: suíte, build e revisão do diff de CSS (só `transform`, `opacity`, `clip-path`, `mask`). ### Fase 3 · peças 3D ### T16 · Renderizar as quatro peças · N3 - Serve: CA10 · Depende de: T6 · Arquivos: criar `3d/pecas.py` - Fazer: com o kit do T5, montar `site` (vitrine com balão de conversa), `sistema` (grade que vira barras), `revisao` (escudo) e `404` (marca com um bloco caindo); loop de 3 s a 24 quadros por segundo no Cycles, 640 px, fundo `--ink`; `ffmpeg` gera `.webm` e `.mp4` mais pôster `.webp`; o script afirma os limites do CA10. - Verificar: `blender -b --python 3d/pecas.py` → `OK`. - [x] feito ### T17 · Usar as peças nos cartões de Soluções · N2 - Serve: CA10 · Depende de: T16 · Arquivos: `WorkModalities.tsx:265,348` · Padrão: `showcase/useShowcaseMedia.ts` - Fazer: trocar a ilustração de cada cartão por `