Scroll motion graphics
Uma linha do tempo em canvas controlada pela posição da rolagem: um estágio fixo, reels que são funções puras do progresso e legendas que o desenho nunca pode tocar. Este método roda o tambor de história na página inicial da ImuTechz, o tambor de P&D e o filme do caminho do Fundador. Ele também alimenta a demonstração de cinco reels em assets/demo.html.
A técnica inteira cabe em uma frase. A posição da rolagem é o único relógio: mapeie-a para p ∈ [0, 1] e faça de cada pixel, legenda e rotação uma função de p.
Como é o bom resultado
- Scruba como um vídeo. Role para frente e o filme toca. Role para trás e ele volta, quadro a quadro. Parar no meio deixa um quadro que faz sentido.
- A página nunca é sequestrada. Roda, trackpad, toque e teclas movem a página exatamente como em qualquer outro lugar. A seção é apenas alta.
- As palavras são sempre legíveis. As legendas são texto DOM real em um cartão. O desenho se organiza no espaço que as legendas deixam, e o canvas é recortado ao redor delas como uma rede de segurança.
- É um controle, não apenas um show. As faces de um tambor de história são links. Sua barra, botões de seta, teclas de seta e arrasto do mouse buscam rolando a página, para que a rolagem e o tambor nunca possam discordar.
- Uma página ociosa não custa nada. Nenhum requestAnimationFrame é executado depois que p se estabiliza ou após a seção sair da janela de visualização.
- Movimento reduzido é respeitado. Sem suavização, sem rotação intermediária do tambor e sem animação de entrada de legenda. Todo o conteúdo ainda está lá.
- Funciona de 360 a 1920 px de largura, em claro e escuro, em DPR 1 e 2.
Regras rígidas
- p vem da posição da rolagem, nunca de um relógio. Não use
performance.now(),Date, animações CSS ousetIntervaldentro de um reel. Suavização (um lerp em direção ao p alvo) é permitida porque converge para o mesmo quadro. - Um reel é uma função pura
draw(ctx, t, env). Ele não mantém estado entre quadros e nunca chamaMath.random(); use umhash(i)com semente. O mesmo (t, env) deve pintar os mesmos pixels. - Prenda com CSS, não com JS. Use uma trilha alta (
position: relative; height: N×vh) segurando um estágioposition: sticky; top: 0; height: 100svh. Nunca definaposition: fixedno estágio, nunca bloqueie o body e nunca coloqueoverflow: hidden|auto|clipem um ancestral da trilha. - Nunca capture entrada. Não use
preventDefaultna roda ou toque, nenhumoverflow: hiddenem html ou body e nenhum scroll snapping na trilha. Apenas um tambor focado pode lidar com teclas de seta, e ele o faz rolando a página. - Busque rolando. Cada botão, item da barra, tecla e âncora altera
window.scrollY. Nunca defina p ou uma rotação diretamente. Usebehavior: "instant"para saltos programáticos que não devem animar, porque"auto"obedece ao CSSscroll-behavior: smoothda página. - O texto vive no DOM. Legendas, títulos e links são HTML. O texto do canvas é limitado a rótulos decorativos, como anos, marcas de eixo e nomes de etapas.
- Meça as legendas a cada quadro e mantenha o desenho longe delas. Cada legenda e elemento HUD é uma zona de exclusão (
data-keep). Os reels se distribuem dentro deenv.free, e o motor recorta as zonas de exclusão com um recorte par-ímpar. - Nunca mostre duas legendas ao mesmo tempo. As janelas de legenda ficam dentro de seu reel com uma lacuna em cada limite, e os reels fazem crossfade dentro dessa lacuna.
- O loop para quando ocioso. Inicie o rAF na rolagem ou redimensionamento apenas enquanto a seção estiver perto da janela de visualização (IntersectionObserver) e pare-o quando p for igual ao alvo.
- O canvas é dimensionado para o estágio em
min(devicePixelRatio, 2). ChamesetTransform(dpr, …)uma vez por quadro e limpe-o a cada quadro. - As cores vêm de propriedades personalizadas CSS. Não codifique uma paleta de modo claro em um reel. Releia a paleta na mudança de
prefers-color-scheme. - Honre
prefers-reduced-motion: reduce. Não use suavização, faça o tambor encaixar face a face e desligue as animações de entrada de legenda. Os quadros ainda seguem a rolagem, porque o usuário os controla.
Ordem de construção
Faça nesta ordem. Cada etapa é testável antes da próxima.
- Conteúdo primeiro. Escreva os capítulos: um chamariz, um título, uma ou duas frases e um link opcional para cada um. Escolha o número de reels (3 a 7) e dê a cada um cerca de 80 a 100 svh de rolagem.
- Trilha e estágio. Adicione a seção alta com um estágio adesivo de 100svh dentro dela e verifique se ela prende sem nenhum JS (veja layout-and-captions).
- O motor de scrub. Conecte
ScrollMotion.scruboufilm(veja engine) e registre p. Verifique se ele lê 0 no início da fixação e 1 no final, e se move nos dois sentidos. - Legendas e HUD no DOM. Coloque-as por ponto de interrupção, marque-as como
data-keepe deixe o motor controlar sua opacidade a partir de p. - Reels, um de cada vez. Escreva
draw(ctx, t, env)dentro deenv.free(veja drawing) e faça scrub para frente e para trás antes de escrever o próximo. - Gramática de movimento. Adicione envelopes de entrada, manutenção e saída, escalonamentos, pausas e crossfades (veja motion-grammar).
- Interatividade, se for um tambor. Adicione links de face, a barra, botões anterior e próximo, teclas e arrasto, todos buscando por rolagem.
- Passagem de desempenho e acessibilidade. Cubra o loop ocioso, DPR, movimento reduzido, ordem de foco, texto do leitor de tela e modo escuro (veja performance-and-a11y).
- Verifique em 360, 390, 414, 768, 1024, 1280, 1440 e 1920 com o script headless (veja verification). Não considere concluído até que todas as verificações passem.
Listas de verificação de decisão
Filme em canvas ou transformações DOM/CSS?
- Muitas peças pequenas em movimento, linhas que desenham, partículas ou gráficos → filme em canvas (
ScrollMotion.film). - Um punhado de cartões, imagens ou painéis de texto → transformações DOM controladas por
scrub(o tambor é um). - Malhas 3D reais ou shaders → esta skill não é suficiente. Use WebGL, mas mantenha as regras 1 a 5 e 9 a 12.
Modo pin ou view?
- A seção deve segurar a tela enquanto sua história é reproduzida →
mode: "pin"(trilha alta e estágio adesivo). - O efeito acompanha conforme o conteúdo passa, como em marquises ou parallax →
mode: "view", sem fixação.
Qual é a altura da trilha?
- Use
n × 80–100svh + 100svhpara um filme en × 70vh + 100vhpara um tambor. Se um leitor de teste disser que está "muito rápido", aumente a altura. Nunca diminua a velocidade capturando a roda.
Onde colocar as legendas?
- Desktop (≥ 760 px): ao lado do desenho (cartão esquerdo ou direito) ou como um cartão inferior largo. Telefones: sempre uma folha inferior, com o desenho acima.
- Com um cabeçalho de site fixo, defina
--sm-nav-hpara que o HUD e o tambor fiquem desobstruídos.
Precisa do tambor?
- Use o tambor quando os capítulos são coisas para as quais o usuário pode querer ir (páginas, produtos, seções). Caso contrário, use apenas o filme.
Modos de falha e correções
| Sintoma | Causa | Correção |
|---|---|---|
| O estágio rola para longe em vez de prender | Um ancestral tem overflow: hidden/auto/clip, ou o estágio não tem top | Remova o overflow (use overflow-x: clip no body, se necessário) e defina top: 0 |
| A animação só toca para frente ou pula ao rolar para cima | Estado acumulado entre quadros, ou um relógio ou Math.random em um reel | Torne o reel uma função pura de t; use hash(i) para aleatoriedade |
| O desenho passa por baixo da legenda | Reel distribuído em todo o estágio | Distribua em env.free; mantenha data-keep na legenda; deixe o motor recortar |
| Caixa de legenda medida no lugar errado durante o fade | Usou getBoundingClientRect (inclui a transformação) | Meça caixas de layout (offsetLeft/Top/Width/Height, veja boxIn) |
| Duas legendas se sobrepõem em um limite | Janelas de legenda se tocam | Apague as legendas em 1 − cross e acenda em cross; os reels fazem crossfade na lacuna |
| Ventiladores giram em uma página ociosa | O loop rAF nunca para | Pare quando cur === target; limite com IntersectionObserver |
| Canvas borrado em telefones / lento em 4K | Canvas no tamanho CSS, ou DPR sem limite | Use width = W × min(dpr, 2) e setTransform(dpr, 0, 0, dpr, 0, 0) |
| Tambor e página discordam depois de clicar na barra | O controle definiu a rotação diretamente | Todo controle chama scrollToProgress; a rotação só vem de p |
| O arrasto treme e depois volta | A página tem scroll-behavior: smooth, então scrollTo(x, y) anima | Use scrollTo({ top, behavior: "instant" }) durante o arrasto |
| Clicar em uma face após um arrasto abre seu link | O clique que termina um arrasto é disparado | Engula um clique na fase de captura após um arrasto que moveu > 4 px |
| Salto de 100vh no celular quando a barra de URL se esconde | vh no estágio | Use 100svh para o estágio; calcule p com a altura do próprio estágio |
| Modo escuro mostra um desenho do modo claro | Cores codificadas | Leia os tokens --sm-* via readPalette; releia na mudança de esquema |
| As faces traseiras do tambor aparecem | Falta backface-visibility: hidden | Defina-o, com o prefixo -webkit-, em cada face |
| Miniaturas das faces do tambor desenhadas no tamanho errado | Usou o retângulo projetado de uma face com transformação 3D | Dimensione por clientWidth/clientHeight |
Arquivos
- references/engine.md: p, o loop de scrub,
film(), busca, o hook React. - references/drawing.md: o contrato do reel, configuração do canvas, rotas, pop-ins, partículas e paletas.
- references/layout-and-captions.md: CSS da trilha e estágio, zonas de exclusão, o retângulo livre, o recorte, o layout do tambor.
- references/motion-grammar.md: envelopes, escalonamentos, pausa, crossfades, ritmo e easing.
- references/performance-and-a11y.md: o loop ocioso, custo de layout, movimento reduzido, teclado, foco e leitores de tela.
- references/verification.md: as oito larguras, o script headless Brave/Chrome e a verificação manual.
- assets/demo.html: autônomo, zero dependências, clique duplo para executar. Cinco reels e o tambor de história.
- assets/scroll-motion.js: o motor (
scrub,film,drum,scrollToProgresse helpers), UMD. - assets/reels.js: cinco exemplos de reels (orbit, path, stack, wave, close) e
still()para miniaturas. - assets/scroll-motion.css: estilos de filme e tambor, tokens claro e escuro.
- assets/ScrollFilm.jsx: o wrapper React para o filme, mais o hook
useScrub. - assets/StoryDrum.jsx: o tambor de história React, mais um
DrumPreviewautorrolante. assets/preview-orbit.jpg,assets/preview-path.jpg,assets/preview-close.jpg: stills da demonstração em 1440 × 900.
Comece pela demonstração: abra-a, veja o código-fonte e substitua o conteúdo. A fiação na parte inferior do arquivo tem cerca de 60 linhas.