Pular para o conteúdo
Zumkai

Motions premium em Next.js: os 18 padrões do site da ANAI, passo a passo

A arquitetura híbrida do site da ANAI — CSS vars, tweens por seção e canvas — destrinchada em 18 padrões de motion com prompts prontos.

  • motion
  • gsap
Home do site da ANAI: título serifado “Cultivate for Posterity” em letras gigantes sobre fundo branco, com a janela de imagem da floresta abrindo abaixo
Sumário
  1. A filosofia: três camadas, cada uma no seu lugar
  2. A stack e o papel de cada peça
  3. O mapa dos 18 motions
  4. Passo 1 — A fundação: infraestrutura de animação
  5. Passo 2 — O motor duplo: --pg-value no scroll, --x/--y no mouse (Motion 4)
  6. Passo 3 — A tipografia que acende: scramble e sweep (Motion 5)
  7. Passo 4 — Estados globais: preloader, transições e header inteligente
  8. Passo 5 — Vitória rápida: underlines com origem trocada e setas que respiram (Motion 9)
  9. Passo 6 — O cartão de visitas: a janela que se abre e o lens flare
  10. Passo 7 — Produção em série: clips, parede em contra-parallax e rodapé
  11. Passo 8 — Identidade de UI: o menu com pontos vivos e o cursor em duas camadas
  12. Passo 9 — O primeiro slider: vitrine com título central que troca (Motion 12)
  13. Passo 10 — A joia da coroa: slider sincronizado com anéis em canvas (Motion 13)
  14. Passo 11 — Polish rápido: tilt 3D de imagens pelo mouse (Motion 8)
  15. Passo 12 — As camadas "wow": partículas WebGL e física DOM
  16. As 18 armadilhas que vão te morder (leia antes de codar)
  17. Por onde começar hoje

Este é o segundo estudo de engenharia reversa da série. No guia da Plantica, o segredo era radical: o JS quase não animava nada, só escrevia variáveis e classes para o CSS mover. O site desta vez responde à mesma pergunta — como um site premium de verdade organiza suas animações? — com uma arquitetura diferente, e o contraste entre as duas é a melhor aula de motion que você vai ter este ano.

O auditado da vez é a ANAI (anaiwood.com): a Anai Wood Factory, serraria japonesa fundada em 1964, que se apresenta com o lema "We Cultivate for Posterity" — e com um site à altura, construído em Nuxt/Vue com GSAP, SplitText, Three.js e Matter.js. Do preloader com contador aos anéis de crescimento de madeira desenhados em canvas, tudo ali tem intenção. Os 18 padrões abaixo foram catalogados auditando esse site e traduzidos para o ecossistema Next.js; nomes, textos e imagens são placeholders parametrizáveis. O que você leva é o padrão de movimento, a técnica e um prompt de implementação pronto para colar no seu agente de IA ou usar como especificação.

Como no guia anterior, a ordem dos passos não é a ordem do catálogo: é a ordem de construção, do alicerce ao açúcar.

A filosofia: três camadas, cada uma no seu lugar

A Plantica era "CSS-first, JS-orquestrado". A ANAI é uma arquitetura híbrida em três camadas — e entender a fronteira entre elas é entender o site inteiro:

  • Camada 1 — variáveis contínuas. Um motor de scroll escreve o progresso de cada seção numa CSS custom property (--pg-value), e um rastreador global escreve a posição do mouse em duas vars no <html> (--x/--y, com lerp). O CSS consome tudo com calc(): clips, parallax, tilts e flares rodam no compositor, sem um frame de JS.
  • Camada 2 — tweens de entrada. Um observer de seções central dispara animações GSAP pontuais quando cada bloco entra no viewport: fades, varreduras de parágrafo e o scramble de cor dos títulos. São tweens de uma vez só — coreografia de chegada, não de scroll.
  • Camada 3 — canvas isolado. Os momentos "wow" vivem em canvas próprios: anéis concêntricos em 2D sincronizados ao slider, partículas ambientais em WebGL e física de elementos com Matter.js. Todos opcionais, todos degradáveis, nenhum contamina as outras camadas.

Amarrando tudo, os estados globais viajam por classes no <html>: --is-loading, --is-transition, --is-scrolled, --is-menu-active. O CSS inteiro reage a elas.

A stack e o papel de cada peça

BibliotecaPapel no projetoPor quê
GSAP 3.13+ e @gsap/reactTweens de entrada por seção, scramble de texto (SplitText), timelines do preloaderGSAP é 100% gratuito, incluindo todos os plugins, desde a aquisição pela Webflow
ScrollTrigger (plugin GSAP)Escrever --pg-value por seção (substitui o motor de scroll próprio do site auditado)Mesmo resultado com menos código proprietário; scrub curto + Lenis reproduzem o feel
LenisSmooth scroll com lerp no desktop (touch fica nativo, mesma decisão do site auditado)Entrega scroll, velocity e direction prontos, que alimentam o header inteligente
SplitText (plugin GSAP)Split de títulos em chars para o reveal por cor com stagger aleatórioAssinatura tipográfica do site auditado
Three.js (ou OGL)Partículas ambientais (Points + noise no vertex shader)Camada decorativa "viva" sobre imagens de atmosfera
Matter.jsFísica de micro-elementos DOM (corpos sincronizados + corpo-cursor)Micro-interações lúdicas com custo baixo
CSS custom properties e transitionsClips, tilts, parallax e underlines consumindo --pg-value/--x/--yAnimações de scroll e mouse rodam no compositor, sem re-render

Nenhum plugin de carrossel é necessário: os sliders do site auditado são próprios (translate + estado). Se preferir abstração, Embla Carousel cobre os dois sliders deste guia.

O mapa dos 18 motions

#MotionUso típicoGatilho
1Preloader com progresso + handoffPrimeira cargaLoad
2Transição de rota com máscaraToda navegação internaNavegação
3Scroll suave + header inteligenteGlobalScroll
4Motor progresso → CSS vars + mouse globalBase dos motions 6, 7, 8, 14, 15, 18Scroll/Mouse
5Texto: color scramble + sweepTítulos e parágrafosInview
6Hero: janela clip que abreHero da homeLoad/Scroll
7Lens flare em camadas pelo mouseHero/atmosferasMouse
8Tilt 3D de imagens pelo mouseImagens editoriaisMouse/Hover
9Underlines e setas com micro-animaçãoLinks e navegaçõesHover
10Menu fullscreen com botão "vivo"Header globalClique
11Cursor customizado em 2 camadasDesktopMouse
12Slider de vitrine com título centralVitrine na homeHover/Clique
13Slider sincronizado com anéis em canvasVitrine de projetosClique/Arraste
14Colunas em contra-parallaxSeção temática imersivaScroll
15Clip reveals de mídia por scrollSubpáginas e galeriasScroll
16Partículas ambientais WebGLAtmosferas/heroAutomático
17Física de elementos DOMMicro-interações lúdicasMouse
18Rodapé com assentamento parallaxRodapé globalScroll

Reveals genéricos de seção (fades com autoAlpha, sweeps, scrambles) são disparados pelo observer de seções do Motion 5 e não têm número próprio.

Passo 1 — A fundação: infraestrutura de animação

Tudo depende deste passo — e ele já revela as decisões de arquitetura: o Lenis só entra em dispositivos sem touch (no celular, scroll nativo, como deve ser), os estados globais ganham um store com dono único, e o rastreador de mouse nasce aqui porque meia dúzia de motions vai bebê-lo da mesma fonte.

txt
Em um projeto Next.js 14+ com App Router, TypeScript e Tailwind, configure a
infraestrutura de animação:

1. Instale: gsap @gsap/react lenis three matter-js
   (+ @types/matter-js e @types/three em devDependencies)
2. Crie um componente client "SmoothScrollProvider" que:
   - Instancia Lenis com { lerp: 0.1, smoothWheel: true } apenas em
     dispositivos sem touch (matchMedia '(pointer: fine)'); em touch,
     scroll nativo (mesma decisão do site auditado).
   - Sincroniza com ScrollTrigger usando uma função nomeada:
       const update = (time: number) => lenis.raf(time * 1000);
       lenis.on('scroll', ScrollTrigger.update);
       gsap.ticker.add(update);
       gsap.ticker.lagSmoothing(0);
   - Cleanup COMPLETO no unmount (nesta ordem):
       gsap.ticker.remove(update);   // sem isso o ticker segue chamando
       lenis.off('scroll', ScrollTrigger.update);   // raf() de instância
       lenis.destroy();                             // morta (leak)
   - Exponha a instância via Context (useLenis) para stop()/start()
     (menu, preloader, zoom).
3. Registre plugins uma única vez em lib/gsap.ts:
   gsap.registerPlugin(ScrollTrigger, SplitText, useGSAP); exporte gsap.
   Importado apenas por componentes client.
4. Crie o motor de progresso por seção (Motion 4): hook
   useSectionProgress que escreve --pg-value em cada [data-progress].
5. Crie o rastreador global de mouse (Motion 4): pointermove com lerp
   num rAF escrevendo --x e --y (normalizados de -1 a 1) no <html>.
6. Crie um pequeno store de estados globais (Zustand ou Context) que
   espelha classes no <html>: --is-loading, --is-transition,
   --is-scrolled, --is-menu-active, e o atributo
   data-scroll-direction ('up' | 'down').
7. Hook usePrefersReducedMotion() lendo a media query
   (prefers-reduced-motion: reduce); todo componente de animação deve
   degradar quando true.
8. Tokens no globals.css (o site auditado usa beziers próprios):
   :root {
     --ease-soft-out: cubic-bezier(0.104, 0.204, 0.492, 1);
     --ease-snap-out: cubic-bezier(0.306, 0.968, 0.632, 1);
     --ease-in-out-strong: cubic-bezier(0.642, 0, 0.328, 1);
     --dur-ui: 0.35s; --dur-reveal: 1s;
     --x: 0; --y: 0;      /* mouse global, escrito pelo rastreador */
   }
Entregue os arquivos completos e o layout.tsx atualizado.

Passo 2 — O motor duplo: --pg-value no scroll, --x/--y no mouse (Motion 4)

O coração da Camada 1, invisível por definição. Tudo que se move com o scroll lê o progresso da própria seção de uma variável; tudo que reage ao mouse lê a posição do ponteiro de outras duas, com atraso elástico. É por isso que os efeitos do site parecem todos irmãos: nascem das mesmas duas fontes.

Dois escritores, zero re-render de React. E dois detalhes de profissional: inicializar as vars no CSS (o HTML do servidor já sai no estado certo, sem flash) e escrever com parcimôniatoFixed(4) e um limiar mínimo de variação antes de tocar o DOM. Escrever var a cada frame sem mudança real é custo puro.

txt
Crie o motor duplo de vars em Next.js:

1. Hook client "useSectionProgress(scopeRef)" dentro de useGSAP:
   gsap.utils.toArray('[data-progress]', scope).forEach((el) => {
     ScrollTrigger.create({
       trigger: el,
       start: el.dataset.start ?? 'top bottom',
       end: el.dataset.end ?? 'bottom top',
       scrub: 0.05,
       onUpdate: (self) => el.style.setProperty(
         '--pg-value', self.progress.toFixed(4))
     });
   });
   Inicialize no CSS: :where([data-progress]) { --pg-value: 0 }
   (o SSR renderiza o estado inicial sem flash).
2. Rastreador de mouse (client, no layout raiz):
   - pointermove guarda o alvo normalizado
     tx = (e.clientX / innerWidth) * 2 - 1 (idem ty);
   - num gsap.ticker.add, faça x += (tx - x) * 0.08 e escreva
     document.documentElement.style.setProperty('--x', x.toFixed(4))
     (idem --y). Escreva só se a variação superar 0.0005.
   - Desative em (pointer: coarse) e em prefers-reduced-motion
     (congele --x/--y em 0).
3. Consumo no CSS (exemplos):
   .col   { transform: translateY(calc((1 - var(--pg-value)) *
            var(--dist))); }
   .clip  { clip-path: inset(calc((1 - var(--pg-value)) * 15%)); }
   .tilt  { transform: rotateX(calc(var(--y) * -10deg))
            rotateY(calc(var(--x) * 10deg)); }
4. will-change apenas nos elementos que animam, e togglado por
   visibilidade: um IntersectionObserver marca data-inview="true" e o
   CSS liga will-change só nesse estado (higiene de GPU idêntica à do
   site auditado).
5. ScrollTrigger.refresh() após load de imagens/fontes e transições
   de rota.

Passo 3 — A tipografia que acende: scramble e sweep (Motion 5)

A assinatura tipográfica da ANAI não sobe nem desliza: ela acende. Cada letra do título começa numa cor apagada e vira a cor final em ordem aleatória, como lâmpadas de um letreiro ligando fora de sequência, até a palavra inteira estar acesa. Nos parágrafos, uma varredura: o bloco é revelado como se um marca-texto invisível passasse da esquerda para a direita.

Este é o coração da Camada 2 — e o passo instala também o observer central de seções, que todos os motions de entrada vão reutilizar (um único IntersectionObserver para a página inteira, não um por componente).

txt
Crie o sistema de reveal tipográfico em Next.js:

1. Observer central: hook "useSectionObserver" com API
   observer.add(sectionEl, callback) usando IntersectionObserver
   (threshold 0.2, once por padrão); todos os motions de entrada se
   registram nele (um único IO para a página inteira).
2. Componente "ScrambleTitle":
   - SplitText.create(el, { type: 'chars', autoSplit: true,
     onSplit: (self) => gsap.set(self.chars, { color: 'var(--col-dim)' }) })
     (autoSplit aguarda as fontes; sem isso as quebras saem erradas);
   - no callback do observer:
     gsap.fromTo(split.chars, { color: cssVar('--col-dim') },
       { duration: 1, color: cssVar('--col-ink'), ease: 'power1.out',
         stagger: { amount: 0.5, from: 'random' } });
   - props from/to sobrescrevem as cores (útil sobre fundos escuros);
   - aria-label com o texto no pai + aria-hidden nos chars.
3. Componente "SweepParagraph":
   - CSS: color: transparent;
     background-image: linear-gradient(90deg, var(--col-ink) 50%,
       var(--col-dim) 50%);
     background-size: 200% 100%; -webkit-background-clip: text;
     background-clip: text; background-position: 100% 100%;
   - no observer: gsap.to(el, { duration: 1,
     backgroundPosition: '0% 0%', ease: 'power1.out' });
4. Complementos padrão de entrada de seção (mesmo callback):
   gsap.fromTo(alvos, { autoAlpha: 0 }, { autoAlpha: 1,
   duration: 0.5–1, delay escalonado 0/0.1/0.2 }).
5. prefers-reduced-motion: cores/fundos já no estado final, sem tween.

Passo 4 — Estados globais: preloader, transições e header inteligente

Três motions que estabelecem --is-loading, --is-transition e --is-scrolled — as classes que o CSS inteiro referencia.

Motion 1 — Preloader com progresso e handoff coreografado

Antes de qualquer conteúdo, a tela mostra o logotipo centralizado e um contador discreto. Enquanto as imagens pesadas carregam, o número avança; ao chegar em 100, o preloader não simplesmente some: ele entrega o palco — o logo se recolhe, a cortina sai e o hero já entra animando. Carregamento e primeira cena viram uma única coreografia. (Visite o anaiwood.com e repare: é a primeira coisa que você vê, e é a capa deste post.)

O detalhe honesto: o progresso é real — Promises dos assets críticos, com o número interpolado via GSAP para nunca pular nem regredir. E o hero nunca anima antes do sinal do onComplete.

txt
Crie um componente client "Preloader" em Next.js:

1. Overlay fixed inset-0 z-[999] com fundo sólido, logo central (SVG) e
   contador (00 → 100). Enquanto ativo: classe '--is-loading' no <html>
   e lenis.stop().
2. Progresso real: Promise.all parcial sobre os assets críticos
   (document.fonts.ready + decode() das imagens above-the-fold);
   cada asset resolvido incrementa a meta. Anime um objeto proxy com
   gsap.to({ v }, { v: meta, duration: 0.6, ease: 'power1.out',
   onUpdate }) para o número subir suave, sem saltos nem regressões.
3. Saída (timeline única):
   - contador e logo: autoAlpha 0 + y sutil, 0.5s;
   - painel do preloader: autoAlpha 0 (ou clip-path de cortina), 0.6s;
   - onComplete: remove '--is-loading', chama lenis.start() e dispara
     o evento/callback que inicia a entrada do hero (Motion 6); o
     hero NUNCA anima antes desse sinal.
4. Rode uma vez por sessão (sessionStorage); nas demais visitas,
   pule direto ao sinal de entrada do hero.
5. Acessibilidade: role="status" aria-live="polite" no contador;
   prefers-reduced-motion encurta tudo para um fade rápido.

Motion 2 — Transição de rota com máscara de escurecimento

Ao clicar num link, a página atual escurece sob um véu translúcido e desliza sutilmente, como se recuasse um passo; a nova entra por cima já disparando seus reveals. E um refinamento raro: indo "para frente" e "voltando", os sentidos do movimento se invertem — a hierarquia espacial da navegação fica legível no próprio motion.

txt
Implemente transições de rota em Next.js App Router:

1. TransitionProvider (client) com navigate(href, direction):
   - Seta '--is-transition' no <html> (CSS: .--is-transition .l-all
     { cursor: wait }) e pausa interações do conteúdo.
   - Anima a saída da view atual: classe '--is-previous' +
     gsap.to(viewAtual, { autoAlpha: 0.999, y: direction === 'forward'
     ? -24 : 24, duration: 0.45, ease: 'power1.inOut' }) em paralelo
     com a máscara: div.transition-mask (absolute inset-0,
     background rgba(0,0,0,0.5), pointer-events none) de opacity 0 → 1.
   - Ao fim: router.push(href).
2. TransitionLink envolvendo next/link (prefetch preservado) que chama
   navigate; links "voltar" passam direction 'backward'.
3. Entrada: template.tsx remonta a cada rota; nele remova
   '--is-transition', faça lenis.scrollTo(0, { immediate: true }),
   anime o container de opacity 0 → 1 (0.35s) e dispare os reveals de
   entrada da página (Motion 5/6). ScrollTrigger.refresh() após as
   imagens carregarem.
4. popstate (voltar do browser): pule a fase de saída e use apenas a
   entrada.
5. prefers-reduced-motion: corte para fades curtos sem deslocamento.

Motion 3 — Scroll suave + header inteligente por direção

Rolagem com inércia amanteigada no desktop (nativa no celular). E um cabeçalho esperto: rolando para baixo, ele se recolhe para devolver a tela ao conteúdo; ao primeiro gesto para cima, volta imediatamente. O site inteiro sabe em que direção você está indo — o Lenis entrega direction de graça, e o header reage só com CSS.

txt
Implemente o header inteligente em Next.js:

1. No SmoothScrollProvider, assine lenis.on('scroll', ({ scroll,
   direction }) => ...):
   - scroll > LIMIAR (ex. 80px) → classe '--is-scrolled' no <html>;
     abaixo do limiar, remova.
   - direction (1 desce, -1 sobe) → atributo
     data-scroll-direction="down"|"up" (atualize apenas quando mudar,
     para não tocar o DOM a cada frame).
   Em touch (sem Lenis), derive dos deltas de window.scrollY num
   listener passive com rAF.
2. CSS do header (fixed top, z-index alto):
   .header-title { transition: opacity var(--dur-ui) var(--ease-soft-out),
     visibility var(--dur-ui), transform var(--dur-ui)
     var(--ease-snap-out); }
   .--is-scrolled[data-scroll-direction="down"] .header-title {
     opacity: 0; visibility: hidden; pointer-events: none;
     transform: translateY(calc(var(--header-h) / -10)); }
3. O header nunca se esconde com o menu aberto ('--is-menu-active'
   tem precedência) nem durante '--is-transition'.
4. Âncoras internas: lenis.scrollTo(target, { offset: -headerH }).

Passo 5 — Vitória rápida: underlines com origem trocada e setas que respiram (Motion 9)

Dois clássicos executados com precisão cirúrgica, 100% CSS. No sublinhado, o refinamento está na saída: ao tirar o mouse, a linha não recua — continua e sai pelo outro lado, como se atravessasse o link. O olho lê continuidade, não retorno. O truque é uma troca de transform-origin entre os estados. Nas setas, o ícone dispara para fora na direção que aponta e reentra por trás no mesmo instante — um respiro que confirma a direção do movimento.

txt
Crie utilitários "u-underline" e "ArrowButton" em Next.js:

1. Underline com origem trocada (classe utilitária):
   .u-underline { position: relative; }
   .u-underline::before { content: ''; position: absolute; bottom: 0;
     left: 0; width: 100%; height: 1px; background: currentColor;
     transform: scaleX(0); transform-origin: 100% 50%;
     transition: transform .4s var(--ease-soft-out); }
   .u-underline:hover::before { transform: scaleX(1);
     transform-origin: 0 50%; }
   (a troca de origin faz a linha entrar pela esquerda e sair pela
   direita; inverta os origins para o sentido oposto)
2. ArrowButton (setas de slider/links):
   @keyframes arrow-out { to { transform: translateX(120%);
     opacity: 0; } }
   @keyframes arrow-in { from { transform: translateX(-120%);
     opacity: 0; } to { transform: translateX(0); opacity: 1; } }
   .arrow-btn:hover .arrow-svg { animation: arrow-out .2s
     var(--ease-in-out-strong) forwards,
     arrow-in .3s .2s var(--ease-snap-out) forwards; }
   Variante backward espelha os sinais; variante externa (links que
   abrem fora) anima na diagonal (translate(120%, -120%)).
   Contêiner com overflow: hidden.
3. Estados desabilitados das setas (fim de slider): opacity .3 +
   pointer-events none.
4. focus-visible sempre com ring; hover só em @media (hover: hover).

Passo 6 — O cartão de visitas: a janela que se abre e o lens flare

O hero valida o motor duplo do Passo 2 — as duas vars trabalhando juntas na mesma dobra.

Motion 6 — Hero: janela de imagem que se abre

O primeiro quadro é uma fotografia de atmosfera emoldurada por margens vazias — uma janela recortada no centro da tela. Sobre ela, o título se acende (Motion 5) e um convite de rolagem pulsa. Ao rolar, a janela se abre: as margens colapsam até a imagem ocupar tudo, enquanto a foto assenta de 105% para 100% de escala. A capa do livro abrindo para a primeira página.

txt
Crie um componente client "HeroWindow" em Next.js:

1. Estrutura:
   <section class="hero" data-progress data-start="top top"
            data-end="bottom top">          <!-- height: ~180vh -->
     <div class="hero-sticky">              <!-- sticky top-0 h-screen -->
       <div class="hero-clip">              <!-- a janela -->
         <img class="hero-img" ... />
       </div>
       <h1 class="hero-ttl">…ScrambleTitle em 2 linhas…</h1>
       <div class="hero-scroll">…indicador de rolagem…</div>
     </div>
   </section>
2. CSS:
   .hero-clip {
     --clip-h: calc((1 - var(--pg-value)) * var(--clip-h-max, 12vh));
     --clip-w: calc((1 - var(--pg-value)) * var(--clip-w-max, 10vw));
     clip-path: inset(var(--clip-h) var(--clip-w));
     height: 100svh;
   }
   .hero-img { transform: scale(calc(1.05 - var(--pg-value) * 0.05));
               object-fit: cover; }
   Espere: --pg-value 0 = janela fechada nas margens máximas;
   1 = fullscreen. Ajuste os máximos por breakpoint.
3. Entrada pós-preloader (sinal do Motion 1): anime --clip-h-max/-w-max
   de valores maiores (janela pequena) até os finais com gsap.to no
   elemento (CSS vars são tweenáveis pelo GSAP), em paralelo com o
   ScrambleTitle do h1 e o fade do indicador de rolagem.
4. Indicador de rolagem: rótulo + seta com o keyframe de seta do
   Motion 9 em loop suave; some com o progresso
   (opacity: calc(1 - var(--pg-value) * 2)).
5. Imagem com next/image (priority, sizes, placeholder blur usando a
   versão preload desfocada).
6. prefers-reduced-motion: janela já aberta, sem scale.

Motion 7 — Lens flare em camadas seguindo o mouse

Sobre a foto do hero vive um reflexo de lente: um disco de luz desfocado, um fragmento poligonal e um brilho terciário, como ao fotografar contra o sol. Ao mover o mouse, as camadas derivam em velocidades diferentes — exatamente como um flare real quando a câmera gira. E o custo? Zero JS próprio: são divs com blur e clip-path consumindo as vars globais de mouse com fatores diferentes. Sutil, quase subliminar — percebido antes de ser notado.

txt
Crie um componente "LensFlare" em Next.js (Server Component: só CSS):

1. <div class="flare" aria-hidden="true"> com 3 sub-camadas absolutas
   posicionadas sobre o ponto de luz da foto:
   .flare-1 { width: 10vw; height: 10vw; border-radius: 50%;
     background: rgb(64 64 64 / .1); filter: blur(2px);
     transform: translate(-50%, calc(var(--x) * 15px)); z-index: 1; }
   .flare-2 { width: 4vw; height: 4vw;
     clip-path: polygon(50% 0, 90% 20%, 100% 60%, 75% 100%,
       25% 100%, 0 60%, 10% 20%);      /* fragmento heptagonal */
     background: rgb(255 255 255 / .1);
     transform: translate(-50%, calc(var(--x) * 25px)); z-index: 3; }
   .flare-3 { …variação menor com fator 40px… }
2. Fatores crescentes por camada (15/25/40px) criam a deriva
   diferencial; adicione var(--y) com fatores menores se quiser
   deriva vertical também.
3. will-change: transform nas camadas; pointer-events: none no bloco.
4. Em (pointer: coarse) as vars ficam em 0 (Motion 4) e o flare fica
   estático: comportamento correto sem código extra.
5. Mantenha opacidades baixas (≤ 0.12): o efeito deve ser percebido
   antes de ser notado.

Passo 7 — Produção em série: clips, parede em contra-parallax e rodapé

Três motions sobre a mesma base — quando a fundação está certa, cada seção nova custa minutos.

Motion 15 — Clip reveals de mídia por scroll

O vocabulário do hero reaparece em escala menor pelo site inteiro: imagens que entram abrindo suas molduras conforme a rolagem alcança cada uma. A consistência faz o site parecer feito de um material só — e é o mesmo par de sempre: clip-path + settle de scale, consumindo --pg-value local.

txt
Crie um componente "ClipMedia" reutilizável em Next.js:

1. Props: as: 'img' | 'video', inset (percentual máximo, default 15%),
   axis: 'both' | 'x' | 'y', range ({ start, end } do trigger).
2. Estrutura: <figure class="clip-media" data-progress
   data-start="top bottom" data-end="center center">
   <div class="clip"> <Image/afins class="media" /> </div> </figure>
3. CSS:
   .clip { clip-path: inset(calc((1 - var(--pg-value)) *
     var(--inset-max))); }
   .media { transform: scale(calc(1.05 - var(--pg-value) * 0.05));
     object-fit: cover; }
   axis 'x' → inset(0 calc(...)); axis 'y' → inset(calc(...) 0).
4. Variante "GalleryFront": duas mídias empilhadas; a frontal usa
   inset crescendo com o progresso (revela a de trás):
   clip-path: inset(calc(var(--pg-value) * 100%) 0 0 0).
5. Vídeos: muted loop playsInline com pause fora do viewport.
6. Combine com legendas entrando pelo observer (Motion 5).
7. prefers-reduced-motion: inset 0 e scale 1 fixos.

Motion 14 — Parede de colunas em contra-parallax

O fundo da seção é uma parede de colunas verticais de fotografias. Ao rolar, as colunas se movem em sentidos opostos: a primeira desce enquanto a segunda sobe, como engrenagens de um tear. O conteúdo flutua por cima. Zero JS próprio — só o sinal alternando por coluna no calc().

txt
Crie um componente "ParallaxWall" em Next.js:

1. Estrutura: section[data-progress] (min-h: 130vh) >
   div.wall (absolute inset-0, flex, justify-between, overflow
   hidden) > N div.wall-col (cada uma com M imagens empilhadas) +
   div.content (relative, z-index acima) com título (ScrambleTitle),
   texto (SweepParagraph) e link.
2. CSS:
   .wall-col { --dist: calc(var(--item-h) / 2);
     transform: translateY(calc((1 - var(--pg-value)) * var(--dist)
       * var(--dir))); will-change: transform; }
   .wall-col:nth-child(odd) { --dir: 1;
     margin-top: calc(var(--item-h) * -1); }
   .wall-col:nth-child(even) { --dir: -1; }
   Colunas com 1 item a mais que o necessário para nunca mostrar
   borda vazia durante o percurso.
3. Véu de legibilidade entre a parede e o conteúdo
   (background rgba escura ou gradiente) e data-theme="dark" na
   seção para o cursor/header inverterem (Motions 3 e 11).
4. Imagens com next/image, sizes por coluna, loading lazy (a seção
   fica abaixo da dobra) e placeholder blur.
5. Mobile: reduza para 2-3 colunas e metade da distância; em
   prefers-reduced-motion, congele (--pg-value tratado no motor).

Motion 18 — Rodapé com assentamento parallax

O rodapé chega assentando: enquanto a rolagem o revela, o bloco desliza uns centímetros mais devagar que a página, como uma bandeja encaixando no lugar, e só pousa quando o scroll termina. A distância é modesta de propósito — o efeito é de acomodação, não de cortina.

txt
Crie um componente "SiteFooter" em Next.js:

1. Estrutura: footer > div.footer-inner[data-progress
   data-start="top bottom" data-end="bottom bottom"] com os grupos
   (endereço, colunas de navegação, redes, créditos/idiomas).
2. CSS:
   .footer-inner { transform: translateY(calc((1 - var(--pg-value))
     * var(--settle, 8rem))); will-change: transform; }
   (em --pg-value: 1 o footer está pousado; o percurso curto do
   trigger faz o assentamento coincidir com o fim da rolagem)
3. Links com .u-underline (Motion 9); e-mail/telefone como âncoras
   reais; link externo com ArrowButton na variante diagonal.
4. Entradas internas discretas via observer (Motion 5): grupos com
   autoAlpha + y: 16, stagger 0.06 (uma vez).
5. prefers-reduced-motion: --settle: 0.

Passo 8 — Identidade de UI: o menu com pontos vivos e o cursor em duas camadas

Motion 10 — Menu fullscreen com botão de pontos "vivos"

O botão de menu é um círculo com três pontinhos que pulsam em onda, como se o botão respirasse. Clicou: um overlay escuro toma a tela e o conteúdo entra em camadas — links grandes, endereço, redes, cada grupo um instante depois do anterior. E o easter egg de arquitetura: quando a camada de física está ativa (Motion 17), os pontinhos abandonam o keyframe e ganham corpos físicos que reagem ao cursor.

txt
Crie um componente client "MenuOverlay" em Next.js:

1. Botão fixo (canto superior): <button aria-expanded aria-controls>
   com: div.dots > 3 span.dot; div.circle (fundo circular no hover);
   div.labels (máscara com "Menu"/"Close" empilhados).
   CSS dos dots:
   @keyframes dot-pulse { 0%,100% { transform: translateY(0) }
     50% { transform: translateY(-3px) } }
   .dot { animation: dot-pulse .75s var(--ease-soft-out) infinite; }
   .dot.--2 { animation-delay: .1s } .dot.--3 { animation-delay: .2s }
   Quando a camada de física estiver ativa (Motion 17):
   .--is-physics .dot { animation: none } (a simulação assume).
2. Rótulo flip: máscara overflow-hidden com 2 divs; aberto →
   translateY(-100%) revela "Close".
3. Overlay: div fixed inset-0 z-50 pointer-events-none; com
   '--is-menu-active' no <html>: fundo escuro fade .5s, conteúdo
   opacity/visibility visíveis e pointer-events auto.
4. Conteúdo em 3 grupos (nav principal, contatos, idiomas/sociais)
   entrando com gsap.fromTo autoAlpha + y: 24, stagger 0.08 entre
   itens e delay 0.1 entre grupos; saída inversa rápida (0.3s).
5. Comportamento: Escape fecha; lenis.stop()/start(); focus trap com
   retorno ao botão; troca de rota pelo menu fecha em paralelo à
   transição (Motion 2); '--is-menu-active' impede o hide do header
   (Motion 3).
6. prefers-reduced-motion: dots estáticos e overlay com fade simples.

Motion 11 — Cursor customizado em duas camadas

Um marcador acompanha o ponteiro em duas camadas: a externa chega quase junto com o mouse; a interna assenta um instante depois, criando um rastro elástico de milímetros. Sobre áreas interativas, muda de estado (anel que cresce, ponto, cor invertida em fundos escuros). E a regra inegociável: ele nunca substitui o cursor do sistema — é um satélite.

txt
Crie um componente client "MouseFollower" em Next.js:

1. Estrutura no layout raiz: div.mouse (fixed top-0 left-0, z-max,
   pointer-events-none) > div.mouse-parent > div.mouse-child.
2. Movimento em camadas (useGSAP):
   const px = gsap.quickTo('.mouse-parent', 'x',
     { duration: 0.25, ease: 'power2.out' });   // idem 'y'
   const cx = gsap.quickTo('.mouse-child', 'x',
     { duration: 0.55, ease: 'power3.out' });   // idem 'y'
   window pointermove → px/py e cx/cy com as mesmas coordenadas
   (as durations diferentes criam o rastro).
3. Estados: .mouse-child::before (anel, scale 0→1) e ::after (ponto)
   com transitions; delegação de mouseover no document lendo
   closest('[data-cursor]') para classes '--link', '--drag',
   '--view'; seções escuras marcam data-theme="dark" e o cursor
   inverte a cor (color: currentColor).
4. Visibilidade: oculto até o primeiro pointermove; some ao sair da
   janela (mouseleave no document) e durante '--is-transition'.
5. Renderize apenas em (hover: hover) and (pointer: fine); nunca use
   cursor: none no site (o marcador é complemento, não substituto).
6. prefers-reduced-motion: uma camada única sem rastro.

Passo 9 — O primeiro slider: vitrine com título central que troca (Motion 12)

Uma fileira de imagens e, presidindo a seção, um nome em letras grandes. Ao pairar sobre cada cartão ou avançar o slider, o nome central se troca — o atual apaga, o novo acende — junto com a descrição. A seção funciona como um mostruário cujo rótulo acompanha o que sua atenção toca. Na variante premium, a troca re-dispara o scramble do Motion 5 em vez de um fade simples.

txt
Crie um componente client "ShowcaseSlider" em Next.js:

1. Dados: N itens { slug, title, description, image }. Layout:
   fileira de cartões (overflow + translate por índice, ou Embla),
   título central absoluto (h2.showcase-ttl) sobreposto à fileira e
   pilha de descrições.
2. Swap do título (por hover no desktop, por slide ativo no touch):
   - onEnter(item): gsap.to(ttl, { autoAlpha: 0, duration: 0.2,
     onComplete: () => { ttl.textContent = item.title;
       gsap.to(ttl, { autoAlpha: 1, duration: 0.3 }); } });
     Variante premium: re-dispare o ScrambleTitle (Motion 5) no novo
     texto em vez do fade.
   - onLeave sem novo alvo: volte ao título default da seção.
3. Descrições: todas montadas empilhadas (absolute); a ativa com
   opacity 1, demais 0 (transition .35s). aria-live="polite" na área.
4. Navegação: setas ArrowButton (Motion 9) com estados desabilitados
   nas extremidades; arraste opcional (pointer events → deltaX no
   translate, com snap ao índice mais próximo no release).
5. Imagens dos cartões: moldura overflow-hidden com hover zoom
   (scale 1.05, .6s) e clip sutil de entrada via observer (Motion 5).
6. Sincronize hover ↔ slide: hover marca o item "em foco" sem mover o
   slider; setas movem o slider e atualizam o título pelo item central.

Passo 10 — A joia da coroa: slider sincronizado com anéis em canvas (Motion 13)

O momento mais autoral do site. Ao lado da lista de projetos, um desenho de anéis concêntricos — alusão direta aos anéis de crescimento da madeira que é a matéria-prima da ANAI — vive num canvas 2D. A cada avanço do slider, os anéis giram meia volta com easing, como se o tempo da matéria acompanhasse a navegação; o número da página troca com um flip de dígito. Conteúdo, contador e desenho pulsam juntos: navegação vira ritual.

É a Camada 3 em seu melhor: o canvas não sabe nada do slider — ele só desenha em função de um número contínuo (frames), e o slider tweena esse número. Acoplamento mínimo, expressividade máxima.

txt
Crie um componente client "RingsSlider" em Next.js:

1. Classe "Rings" (canvas 2D):
   - new Rings({ elem, color, steps, base, outer, padding }) cria um
     <canvas> responsivo (resize observer, dpr) dentro de elem;
   - draw(frames): limpa e desenha os anéis concêntricos; use frames
     para modular o desenho (raio de fase, rotação de um marcador ou
     espessura alternada; parametrize a interpretação);
   - update() força um redraw (chamado quando a seção entra em view).
   Responsivo: menos anéis/raio menor em mobile (props por breakpoint).
2. Estado compartilhado: const frames = { current: 0, next: 0 }.
   Ao trocar de slide (setas, arraste ou clique num item):
   frames.next = frames.current + 0.5;
   gsap.killTweensOf(frames);
   gsap.to(frames, { duration: 0.25, current: frames.next,
     ease: 'power1.out', onUpdate: () => rings.draw(frames.current) });
3. Slider de conteúdo: painéis de texto/imagem trocando por índice
   (translate ou crossfade); título do item com ScrambleTitle
   (Motion 5) a cada troca.
4. Paginação com flip de dígito:
   <span class="nums"> (1em x 1em, overflow hidden) com duas camadas
   .num-child empilhadas; na troca: a atual ganha '--is-hide'
   (translateY(-100%) + fade) e a nova '--is-show' entrando de baixo;
   ao fim, troque os papéis (sempre só 2 nós).
5. Acessibilidade: canvas aria-hidden (decorativo); slider com
   role="group", aria-roledescription="carousel", botões rotulados;
   contador com aria-live="polite".
6. Pause: só desenhe com a seção no viewport (IntersectionObserver);
   cleanup de canvas/observers no unmount.
7. prefers-reduced-motion: anéis estáticos (draw único) e troca de
   slide por fade.

Passo 11 — Polish rápido: tilt 3D de imagens pelo mouse (Motion 8)

Certas fotografias inclinam-se sutilmente na direção do mouse, como cartões suspensos reagindo à sua presença. Nada persegue o cursor: tudo apenas se curva a ele. E aqui a arquitetura paga o investimento do Passo 2: como o lerp mora no rastreador global, N instâncias de tilt custam o mesmo que uma — nenhum listener local, só CSS lendo as vars.

txt
Crie um componente "TiltImage" em Next.js:

1. Estrutura: <figure class="tilt-wrap"> (perspective: 1000px)
   > <div class="tilt" style="--rx: 10deg; --ry: -10deg">
   > <img class="tilt-img">.
2. CSS:
   .tilt { transform: rotateX(calc(var(--y) * var(--ry)))
                      rotateY(calc(var(--x) * var(--rx)));
           will-change: transform; }
   .tilt-img { transition: transform .6s var(--ease-soft-out); }
   .tilt-wrap:hover .tilt-img { transform: scale(1.05); }
   Opcional: overlay ::before com opacity 0 → 1 no hover (véu).
3. Amplitudes como props (--rx/--ry por instância); inverta os sinais
   para "fugir" ou "seguir" o mouse conforme a direção desejada.
4. A suavidade vem do lerp do rastreador global (Motion 4): sem
   listeners locais, N instâncias custam o mesmo que uma.
5. Em touch/reduced-motion as vars ficam em 0: imagem estática com
   hover-scale apenas onde houver hover real (@media (hover: hover)).

Passo 12 — As camadas "wow": partículas WebGL e física DOM

As duas últimas camadas são açúcar puro — e é exatamente assim que devem ser tratadas: o site precisa funcionar idêntico sem elas. Em reduced motion ou hardware fraco, elas simplesmente não montam.

Motion 16 — Partículas ambientais WebGL

Sobre a fotografia, partículas minúsculas flutuam no ar: poeira ao sol, cada uma com tamanho, brilho e ritmo próprios, subindo e derivando, esmaecendo ao longe. Quase invisível quando se olha direto — e é exatamente o que faz a cena parecer ar em vez de papel de parede.

txt
Crie um componente client "AmbientParticles" em Next.js com Three.js:

1. Wrapper "GLCanvas": WebGLRenderer({ alpha: true, antialias: true }),
   setPixelRatio(min(devicePixelRatio, 2)), canvas absoluto sobre a
   mídia (pointer-events none), resize via ResizeObserver.
2. Geometria: BufferGeometry com COUNT (300-800) partículas:
   position (x/y aleatórios no volume, z em faixas), e atributos
   aProgress, aSize, aAlpha (Math.random() por partícula).
3. Vertex shader: posição deslocada por simplex noise 3D
   (freq/multiplier em uniforms) + avanço vertical por
   (aProgress + iTime * uSpeed) mod 1; gl_PointSize proporcional a
   aSize e à profundidade; varying vAlpha.
4. Fragment shader: disco suave (smoothstep radial) * vAlpha * fog.
5. Uniforms: iTime (frames), iMouse (lido do rastreador global do
   Motion 4 para deriva sutil), iResolution, uSize, uNoiseFrequency,
   uNoiseMultiplier, uProgressSpeed (todos como props).
6. Material: transparent, blending: THREE.AdditiveBlending,
   depthWrite: false.
7. Loop: gsap.ticker (função nomeada); renderize apenas com o canvas
   no viewport (IntersectionObserver). Cleanup: ticker.remove,
   geometry/material.dispose(), renderer.dispose().
8. Fallback: sem WebGL ou em prefers-reduced-motion, não monte
   (a cena vive sem as partículas).

Motion 17 — Física de elementos DOM (Matter.js)

Alguns elementos da interface têm peso de verdade: soltos num recipiente invisível, caem, quicam nas bordas e se acomodam; aproximar o cursor os empurra fisicamente, como moedas numa bandeja. Na ANAI, os pontinhos do botão de menu ganham essa vida quando a camada avançada está ativa — deixam o pulso coreografado e passam a reagir ao seu mouse.

txt
Crie um sistema de física DOM em Next.js com Matter.js:

1. Classe "PhysicsWorld({ container, gravity })":
   - Engine.create(); walls: 4 Bodies.rectangle estáticos nas bordas
     do container (espessura generosa, invisíveis);
   - cursor: Bodies.circle estático (raio ~24px) atualizado no
     pointermove (Body.setPosition), colide e empurra os corpos;
   - runner próprio via gsap.ticker (função nomeada) chamando
     Engine.update(engine, 1000/60).
2. Classe "PhysicsItem({ el, shape: 'rect' | 'circle' })":
   - mede o el (getBoundingClientRect relativo ao container) e cria o
     corpo com restitution .6, friction .1;
   - update(): lê position/angle do corpo e aplica no el
     transform: translate(xpx, ypx) rotate(deg) (arredonde a 2 casas;
     o el fica position: absolute no estado inicial medido).
3. Aplicação exemplo (pontos do botão de menu): ao ativar a camada
   ('--is-physics' no <html>), os dots trocam o keyframe de pulso
   (animation: none) pelos corpos físicos dentro do círculo do botão;
   ao desativar, destrua o mundo e restaure o keyframe.
4. Resize: recrie paredes e reposicione corpos (debounce).
5. Cleanup completo: ticker.remove, Composite.clear, Engine.clear,
   listeners removidos.
6. Física é açúcar: em touch de baixo desempenho e em
   prefers-reduced-motion, mantenha a versão coreografada em CSS.

As 18 armadilhas que vão te morder (leia antes de codar)

A lista cresceu em relação ao guia anterior — canvas e física trazem armadilhas próprias. Passe por ela antes da primeira linha, e volte quando algo quebrar "misteriosamente".

  • "use client" obrigatório em todo componente que toca GSAP, Lenis, Three, Matter ou observers.
  • Sempre useGSAP({ scope }) em vez de useEffect cru: o context.revert() automático mata tweens e triggers órfãos ao navegar entre rotas.
  • Cleanup do Lenis é em 3 passos: gsap.ticker.remove(update) + lenis.off('scroll', ...) + lenis.destroy().
  • Trava de scroll com Lenis: lenis.stop()/start() (preloader, menu), nunca apenas overflow: hidden.
  • CSS vars + SSR: inicialize --pg-value: 0 e --x/--y: 0 no CSS (não só via JS) para o primeiro paint do servidor já sair correto.
  • Vars de mouse no <html> são globais de propósito: N consumidores (flare, tilt, cursor, partículas) leem as mesmas duas vars; nunca crie um listener de pointermove por componente.
  • Escreva vars com parcimônia: toFixed(4) e um limiar mínimo de variação antes de tocar o DOM; escrever a cada frame sem mudança real é custo puro.
  • transform, opacity e clip-path nas animações por CSS var; nunca width, height, top ou left (layout thrashing).
  • will-change togglado por visibilidade (atributo data-inview via IntersectionObserver), exatamente como o site auditado: will-change permanente degrada a memória de GPU.
  • ScrollTrigger.refresh() após load de imagens e fontes e após cada transição de rota.
  • SplitText só depois das fontes (autoSplit: true ou document.fonts.ready); aria-label no pai + aria-hidden nos chars, ou leitores de tela soletram o título.
  • gsap.matchMedia() para variações responsivas; ScrollTrigger.matchMedia está deprecado desde o GSAP 3.11.
  • Estados globais têm um dono só: --is-loading, --is-transition, --is-scrolled, --is-menu-active e data-scroll-direction vivem num store central que espelha classes no <html>; toggles espalhados criam estados fantasma.
  • Canvas sempre com dpr limitado (min(devicePixelRatio, 2)), pausa fora do viewport e dispose completo (Three: geometry/material/renderer; Matter: Composite/Engine; 2D: observers).
  • Cursor, tilt e flare: desktop apenas ((hover: hover) and (pointer: fine)); em touch as vars ficam em 0 e os componentes degradam sozinhos. Nunca cursor: none.
  • Física e partículas são açúcar: o site precisa funcionar idêntico sem elas (fallback coreografado em CSS, camada não montada em reduced motion ou hardware fraco).
  • O preloader precisa de progresso real: contador fake que trava em 99 esperando um asset é o anti-padrão; some Promises de verdade e interpole com GSAP.
  • prefers-reduced-motion em tudo: preloader vira fade, janela do hero nasce aberta, scramble vira cor final, sliders trocam por fade.

Por onde começar hoje

O trio de entrada é o mesmo espírito do guia anterior: Passo 1 (a fundação), Passo 2 (o motor duplo — que aqui rende dobrado, porque alimenta scroll E mouse) e Passo 5 (underlines e setas — CSS puro, resultado imediato). Com o Passo 3 logo depois, você tem a assinatura tipográfica que muda a cara de qualquer página.

E fica o convite ao estudo comparado: abra o anaiwood.com e o site da Plantica lado a lado, com os dois guias abertos. Uma marca de madeira e um estúdio floral, uma arquitetura híbrida de três camadas e uma CSS-first radical — e por baixo, os mesmos princípios: variáveis para o contínuo, tweens para os eventos, canvas para o espetáculo, e acessibilidade como fundação. É esse repertório de decisões — não os efeitos em si — que você leva para o próximo projeto.

Leia também