Scrollytelling cinematográfico
Arquitetura base (não negociável)
- Um único pin, uma única linha do tempo. O hero (ou o capítulo principal) fixa um contêiner que dura N viewport-heights e um progresso 0→1 dirige tudo. Se houver mais de uma animação de scroll competindo, é uma web normal com efeitos: não é isso.
- Nunca scroll-jacking. O usuário controla a velocidade com seu scroll nativo; a animação é "scrubeada" (
scrub: true). Proibido sequestrar a roda ou mover o scroll pelo usuário. - Um único loop de animação. Lenis integrado no ticker do GSAP (
autoRaf: false) e ScrollTrigger sincronizado; ou scroll nativo com scrub. Dois loops competindo é a causa nº1 de jank e não se resolve otimizando assets. - O texto vive no DOM, sobreposto ao canvas, com camadas por progresso. Nunca renderizar cópia dentro do canvas.
Sequências de frames (técnica tipo Apple)
- Fonte preferida: vídeo real → frames com
ffmpeg(fps=12..24, escala para resolução de tela). Exportar WebP q≈68-75 e ajustar olhando o resultado, não por intuição. - Máximo ~90-120 frames; em dispositivos móveis servir metade ou versão 720p.
- Carregar com janela deslizante: manter ±6-10 frames ao redor do atual; jamais decodificar a sequência completa (uma sequência 1080p completa mata iOS Safari).
- Pré-carregar com
IntersectionObserver(rootMargin ~800px),requestIdleCallbackpara decode efetchPriority="low"exceto para o hero. - Canvas com
devicePixelRatiolimitado a 1.5 ecover/containcalculado; desenhar apenas frames decodificados; se faltar o frame exato, usar o mais próximo já carregado (nunca pintar em preto). - Cache imutável (
Cache-Control: public, max-age=31536000, immutable) com nome de arquivo versionado. - Fallback com
prefers-reduced-motione conexão lenta: pôster estático + capítulos normais sem pin. - Alternativa mais simples quando não é necessário pós-processo: trocar
srcde uma<img>por frame (confiável em todos os navegadores, sem canvas).
Vídeo scrubbing (se usar <video>)
currentTimedirigido por progresso em um únicorequestAnimationFrame, com suavização; coalescer seeks (Safari acumula fila).- Carregar como blob para ser 100% seekable; codificar com GOP curto (keyframe a cada poucos frames) ou o scrub trava.
- Nunca autoplay: quem move é o scroll.
Capítulos e overlays
- Beats definidos por intervalos de progresso (10%, 35%, 60%, 85%...), com fade in/out e entrada alternando lados para ritmo.
- Cada capítulo mantém seu texto no DOM em ordem semântica correta para SEO.
- Galerias horizontais: pin da seção +
xligado ao progresso; os cards escalonam em direção ao centro.
Desempenho e verificação
- Animar apenas
transform,opacityefilter;will-changepontual e removido ao terminar. - Medir frames com
requestAnimationFramelocalmente e em dispositivo móvel real, se possível; objetivo 60fps em notebook médio. - A captura headless pode ler o canvas preto (buffer não preservado): verificar pelo DOM (estilos computados, elementos, estado exposto em
window.debug) e com o modo reduzido ativado. - Testar em 375/768/1280/1440; o redimensionamento de um navegador desktop não reproduz o crash de memória do iOS: informar no relatório de QA.
Estrutura de dados recomendada
Um array ordenado de "planos": cada plano é { tipo: 'frames' | 'overlay' | 'galeria', ... }. Reordenar o filme = reordenar a lista. Os frames são referenciados por padrão (/seq/plano-01/0001.webp).