Animação de construção por agrupamento de agentes
Um canvas overlay fixo desenha uma pequena equipe de cursores que "constroem" cada seção de uma página quando ela entra na viewport. O conteúdo é HTML real, renderizado no servidor o tempo todo. O motor apenas oculta itens por alguns segundos e executa uma coreografia curta para cada um.
Exemplo ao vivo: https://www.lamanhtruong.com. Seu código-fonte está fixado em portfolio-anh@a5d1651. assets/swarm/ contém o mesmo motor, limpo para reutilização.
Quando usar e quando não usar
Use onde a própria página é o argumento de venda: um portfólio, uma página de lançamento ou de história do produto, um estudo de caso, uma página "sobre nossa IA". As pessoas visitam uma vez, rolam do topo ao fim, e a construção conta algo a elas ("esta pessoa entrega com agentes").
Ignore para qualquer coisa que as pessoas usam todos os dias: painéis, documentação, configurações, checkout, leitura longa. A construção custa alguns segundos por seção, e um usuário que retorna não tem motivo para assistir duas vezes. É por isso que o motor lembra de uma construção concluída por aba (sessionStorage) e nunca a repete ao recarregar.
O que custa:
- Cerca de 5.000 linhas de TypeScript. A camada de construção é carregada com
next/dynamicapós a hidratação, então a primeira pintura nunca espera por ela; as peças de rolagem suave e parallax (Lenis, GSAP, ScrollTrigger) carregam com a página. Um Next.js 15 novo com todas as quatro skills living-ui chegou a cerca de 67 kB de JS de rota (170 kB na primeira carga). - Um pequeno escore de layout-shift enquanto o log ao vivo preenche (cerca de 0,03 em uma página de teste, bem abaixo da linha "boa" de 0,1): novas linhas de log se movem dentro de sua caixa de altura fixa. O resto da página nunca se desloca.
- Um canvas fixo extra e um loop
gsap.ticker. O loop para quando nada está se movendo. - GSAP 3.13+ (gratuito, incluindo ScrollTrigger),
@gsap/react, Lenis.
Como funciona em um parágrafo
Um pequeno script inline em <head> define html[data-build="on"] antes da primeira pintura, e o CSS então oculta cada [data-build-item] não construído com opacity: 0. Opacidade em vez de display ou visibilidade não causa mudança de layout e nada é removido da árvore de acessibilidade. Após a hidratação, o motor escaneia a página em um grafo de tarefas por [data-build-section], arma um ScrollTrigger para cada seção e, quando uma entra na viewport, executa plano → construção → revisão:
- Plano: um planejador desenha guias e distribui tarefas.
- Construção: trabalhadores reivindicam tarefas por distância, função e caminho crítico, e cada tarefa executa uma timeline do GSAP.
- Revisão: um revisor delineia a seção, marca uma lista de verificação e a sela.
Itens e seções concluídos recebem [data-built]. Se o motor nunca iniciar, o script de cabeçalho remove a flag após 3 s para que nada permaneça oculto.
Arquitetura, fluxo de dados e como estender: references/architecture.md.
Instalação (Next.js App Router)
-
Dependências. Testado com Next.js 15.1 + React 18.3 (o exemplo trabalhado) e Next.js 15.5 + React 19.1 + Tailwind 4 (um
create-next-appnovo), TypeScript 5.8–5.9,[email protected]–3.15,@gsap/[email protected],[email protected]:npm i gsap@^3.13 @gsap/react@^2.1 lenis@^1.3 -
Copie o motor. Copie
assets/swarm/parasrc/components/swarm/. As importações dentro dele são relativas, então qualquer pasta funciona. -
Adicione o script de flag ao
<head>do layout raiz. Ele define um atributo em<html>antes do React hidratar, então adicionesuppressHydrationWarninga<html>:// app/layout.tsx import { BuildFlagScript } from "@/components/swarm/build-flag-script"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" suppressHydrationWarning> <head> <BuildFlagScript /> </head> <body>{children}</body> </html> ); } -
Monte o motor uma vez, nas páginas que o usam, após o conteúdo:
// app/page.tsx (um componente servidor; permanece estático) import "@/components/swarm/swarm.css"; import { SwarmRoot } from "@/components/swarm/swarm-root"; export default function Page() { return ( <> <main>{/* seções marcadas com data-build-* */}</main> <SwarmRoot /> </> ); }SwarmRoottambém monta a rolagem suave Lenis orientada porgsap.ticker. Se o aplicativo já executa Lenis, exclua<SmoothScroll />deswarm-root.tsx.build-layer.tsxlê a instância através deuseLenis(), que precisa de uma instânciarootdo ReactLenis em algum lugar da página (<ReactLenis root />, comosmooth-scroll.tsxmonta). Não precisa ser um pai. -
Mantenha a rota estática. O motor apenas decora HTML que já existe. Não chame
cookies(),headers()ouauth()na árvore de layout da página apenas para ler uma sessão. Busque isso no cliente, e a página pode ser pré-renderizada e servida pelo CDN. Em páginas que não montamSwarmRoot, o script de flag é inofensivo: nenhum elemento carregadata-build-item, e o fail-safe limpa a flag. -
Personalize o tema. Defina variáveis CSS (
--swarm-font-mono,--swarm-accent,--swarm-text,--swarm-muted,--swarm-dim,--swarm-line,--swarm-neutral-text|fill|stroke), então editeCOLORSeROLE_COLORempalette.ts. O canvas não consegue ler variáveis CSS de forma barata a cada quadro, então sua paleta vive em TS.
Marque a página
Tudo é orientado por atributos de dados na marcação renderizada no servidor. A referência completa, com padrões e comportamento para celular, está em references/dom-contract.md. Receitas de copiar e colar para hero, grade de cartões, lista, imagem ou vídeo, faixa de métricas e final estão em references/recipes.md.
| Atributo | Em | Significado |
|---|---|---|
data-build-section="nome" | wrapper da seção | Uma unidade de construção com seu próprio plano, equipe e revisão |
data-build-start="load" | seção | Construir logo após a inicialização em vez de na rolagem, com o orçamento maior do hero |
data-build-item + data-build="frame|type|reveal|image|dup" | elemento | Uma tarefa desse tipo |
data-build-list | container de lista | Seu primeiro item é o mestre (frame); irmãos dup são copiados dele |
data-build-grid | container ou a seção | Um agente de layout desenha a grade e enquadra cada célula primeiro |
data-build-font / -weight / -scale | itens type | Rascunho mono e troca de fonte; varredura de peso; começar pequeno, depois redimensionar e mover |
data-build-color="text|fill|stroke" | qualquer item | Roda de cores escolhe a cor real após o item aparecer |
data-build-tune | frame | Abre o inspetor de raio e preenchimento |
data-build-from="left|right|top|bottom" | image | Borda de onde a imagem é arrastada |
data-build-keep / ="mobile" | qualquer item | Nunca oculto (apenas celular com mobile), ainda anotado |
data-build-label, -order, -role | qualquer item | Texto da etiqueta, dica de ordenação, qual função o assume |
data-swarm-spawn="role" | <g> SVG no hero | Nós de malha de onde a equipe sai na inicialização (≥1024 px) |
data-swarm-log | container | Linhas de fallback renderizadas no servidor são substituídas por um log ao vivo |
data-build-finale + data-deploy-target + data-deploy-text | última seção, seu CTA | Implantador "entrega" a página após a última revisão |
?build=off / ?build=force | URL | Desabilitar para esta visualização / repetir mesmo se esta aba já assistiu |
O motor define: html[data-build="on"] enquanto ativo, [data-built] em itens e seções concluídos, [data-lit] em nós de malha durante a inicialização e [data-deployed] no alvo do final.
Regras que o mantêm funcionando:
- Tamanho da seção. Cerca de uma viewport de itens por seção. Divida páginas longas: uma seção por trabalho em uma linha do tempo, uma por cartão em uma grade grande, uma seção separada para um cabeçalho de seção. Seções pequenas terminam dentro de seu orçamento e parecem deliberadas; enormes caem na velocidade de recuperação.
- Aninhamento. Um item aninhado dentro de outro item espera pelo pai. Marque o cartão como
framee seu título, texto e chips dentro dele. - Digitação. Use
typeapenas para texto curto (abaixo de cerca de 120 caracteres). Texto mais longo automaticamente cai parareveal. A digitação lê caixas de glifo uma vez, então texto gerenciado pelo React que muda após a hidratação (contadores, contagens de visualização) deve ser umframe, nãotype. - LCP. Mantenha o título principal visível em celulares com
data-build-keep="mobile". Em celulares, o título do hero é o Largest Contentful Paint, e ocultá-lo custa Core Web Vitals. Desktop ainda recebe a construção completa com troca de fonte. - Log ao vivo. O container
data-swarm-logdeve ser renderizado no servidor com linhas de fallback reais e uma altura fixa (.swarm-log). O motor as substitui por linhas ao vivo e as coloca de volta ao pular ou desmontar.
Hero mínimo:
<section data-build-section="hero" data-build-start="load">
<p data-build-item data-build="type">sua linha de apoio</p>
<h1
data-build-item data-build="type" data-build-font data-build-weight
data-build-keep="mobile" data-build-order="-1" data-build-role="coder"
>
Seu Nome <span className="serif">(Acento)</span>
</h1>
<div data-build-item data-build="image" data-build-from="right" className="avatar">
<img src="/me.jpg" alt="Retrato" />
</div>
{/* next/image também funciona (renderiza um <img> interno que o motor lê). */}
<div data-build-list className="buttons">
<a data-build-item data-build="frame" href="#">GitHub</a>
<a data-build-item data-build="dup" href="#">LinkedIn</a>
<a data-build-item data-build="dup" data-build-color="fill" href="#">Currículo</a>
</div>
</section>
Regras de suavidade
Elas vieram de observar construções reais. Cada uma corrigiu um problema visível.
- Nunca force o término em um prazo. Uma versão inicial pulava cada timeline em execução para 5× de velocidade e colocava os itens não reivindicados no prazo de cada seção. As pessoas viam como um solavanco no final de cada seção. Orçamentos agora são alvos de planejamento.
estimate()escala as durações das tarefas para que o plano se encaixe.- Quando uma seção atrasa,
pace()aumenta suavemente um fator de recuperação, no máximo 1,7× ao longo de 0,7 s. - Cada tween permanece pelo menos 120 ms de duração mesmo na recuperação total, e passos de digitação permanecem pelo menos 16 ms. Nada surge.
- Se precisar mais rápido, torne os cursores mais rápidos (
speedempalette.ts). Não corte animações.
- Anime apenas opacidade, clip-path, filtro e transform. Sem escritas de layout. Rascunhos de troca de fonte são clones em uma camada fora do fluxo, e a tarefa de escala remove sua transformação inline no final.
- Um relógio. A simulação e o desenho do canvas rodam em um único callback
gsap.ticker. Mudanças de estado do React ocorrem apenas quando a construção inicia ou para, nunca por quadro. O ticker para quando nenhum agente está na tela. - Meça adiante. Seções até cerca de duas viewports abaixo da dobra são medidas em fatias
requestIdleCallback. Uma seção iniciando nunca causa uma grande rajada de leitura de layout no quadro de rolagem. - Respeite o leitor.
- Focar qualquer coisa dentro de uma seção não construída termina essa seção imediatamente, para que usuários de teclado nunca esperem.
- Um redimensionamento no meio da construção finaliza as seções em execução, porque seus retângulos medidos estão desatualizados.
- Uma aba oculta pausa tudo.
- O HUD sempre oferece Pular, Repetir e Inspecionar.
- Celulares recebem menos. Cada perfil tem seu próprio orçamento, equipe e detalhe (
detectProfile()empalette.ts).- Celulares: 8 agentes, cerca de 2,2 s por seção, uma troca de fonte por página, uma escolha de cor por seção, uma varredura de duplicação por lista, sem painéis de inspetor, sem tarefa de escala, sem implantador.
- Tablets: 11 agentes.
- Desktop: 15 agentes, cerca de 3,9 s por seção, 5,25 s para o hero.
Botões para "mais rápido", "mais calmo" e "mais agentes": references/tuning.md.
Movimento reduzido
O motor executa para cada visitante, incluindo pessoas cujo sistema operacional pede movimento reduzido. O proprietário do site decidiu que a construção é o ponto da página, e o HUD sempre oferece Pular, então esta é uma escolha deliberada, não um padrão para copiar cegamente. Para respeitar a configuração, faça todas essas três alterações:
// build-flag-script.tsx: dentro de CODE, antes de d.setAttribute('data-build','on')
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
// motion.ts
export function offByUrl(): boolean {
return OFF_RE.test(window.location.search) ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
// smooth-scroll.tsx
respectReducedMotion: true,
Teste de ambas as maneiras: alguns navegadores (e algumas configurações de automação) relatam reduce mesmo quando ninguém escolheu, o que é uma razão pela qual o padrão aqui é "ligado".
Trabalhando com outros efeitos
-
Porte em
[data-built]. O motor lê retângulos de elemento e alterna opacidade e clip-path durante a construção. Tilt, glitch, scrambled, parallax em conteúdo, ou qualquer outra coisa que transforme um item corromperá essas medições se executar cedo. Inicie tais efeitos apenas quando o item e sua seção carregarem[data-built], ou quandohtml[data-build]não for"on". Um auxiliar pronto:export function isBuilt(el: Element): boolean { if (document.documentElement.getAttribute("data-build") !== "on") return true; const item = el.closest("[data-build-item]"); if (item && !item.hasAttribute("data-built")) return false; const section = el.closest("[data-build-section]"); return !section || section.hasAttribute("data-built"); } -
Vídeos. Um item
imageque envolve um<video poster>é arrastado usando seu quadro de pôster como miniatura. Dê ao wrapper umdata-build-labelcomoclip.mp4, porque não há<img>para ler um nome de arquivo. -
Parallax decorativo é bom em qualquer lugar onde move apenas decoração. Nunca coloque em algo que o motor mede.
Faça seu próprio
O motor aqui é uma implementação original, e o mesmo padrão se aplica a qualquer coisa que você construir a partir dele. Se um site te inspirou, escreva o que torna aquele site reconhecível e fique longe exatamente dessas coisas. Pegar emprestado ideias é diferente de copiar a aparência. Se você construir algo que parece com o exemplo, mude o suficiente para que ninguém confunda os dois.
Referências
references/architecture.md- Arquitetura e fluxo de dadosreferences/dom-contract.md- Contrato DOM completo com valores padrãoreferences/recipes.md- Receitas de copiar e colar para elementos comunsreferences/tuning.md- Botões de ajuste para velocidade, calma e número de agentesreferences/faq.md- Perguntas frequentes e solução de problemas
