Animar .scene-inner, nunca o .clip
O framework HyperFrames força opacity:1 em todo .clip que está ativo. Se você animar o wrapper diretamente, o fade não ocorre — o engine sobrescreve sua animação quadro a quadro. A solução é sempre animar um filho interno.
O HyperFrames itera sobre os clips ativos a cada frame e aplica element.style.opacity = "1" diretamente no .clip. Qualquer animação GSAP que tente fazer gsap.to(".clip", {opacity:0}) será sobrescrita no próximo tick de render — resultando em fade fantasma que nunca acontece.
tl.to("#clip-cena-2", {
opacity: 0,
duration: 0.45
}, 4.5);
- ✗ Opacity sobrescrita no próximo frame
- ✗ Fade nunca visível no vídeo renderizado
- ✗ Difícil de debugar (parece funcionar no preview)
tl.to("#scene-inner-2", {
opacity: 0,
duration: 0.45
}, 4.5);
// hard-kill obrigatório:
tl.set("#scene-inner-2", {
opacity: 0
}, 4.95);
- ✓ Fade executado no DOM filho — framework não interfere
- ✓ Hard-kill com
tl.setgarante opacity:0 no fim - ✓ Cobre o gotcha
gsap_exit_missing_hard_kill
gsap_exit_missing_hard_killMesmo usando scene-inner, se o GSAP tween terminar antes do clip desaparecer, a opacidade pode voltar. Sempre adicione um tl.set(target, {opacity:0}, tempoFinal) como sentinela. O composition-template.mjs já inclui essa linha automaticamente.
<div id="clip-cena-2"
class="clip"
data-start="4.0"
data-duration="3.0"
data-track-index="1">
<!-- filho animável -->
<div id="scene-inner-2" class="scene-inner">
<!-- conteúdo da cena -->
</div>
</div>
Tracks alternados para evitar overlapping_clips_same_track
Cenas adjacentes que tocam na borda temporal — mesmo por frações de float — disparam o erro overlapping_clips_same_track durante o lint. A correção canônica é alternar os data-track-index: cenas em 1/3, captions em 2/4.
0s → 4s
8s → 12s
16s → 20s
0s → 4s
8s → 12s
16s → 20s
4s → 8s
12s → 16s
20s → 24s
4s → 8s
12s → 16s
20s → 24s
data-start="0" data-duration="4.0"
data-track-index="1"
data-start="4.0" data-duration="4.0"
data-track-index="1"
// → lint: overlapping_clips_same_track
data-start="0" data-duration="4.0"
data-track-index="1" // cena 1 (ímpar)
data-start="4.0" data-duration="4.0"
data-track-index="3" // cena 2 (par)
// → lint: ok
A tentação é adicionar um pequeno gap (data-start="4.001") para evitar a colisão. Isso cria um frame preto visível no vídeo e ainda pode disparar outros erros do lint. A solução correta é sempre alterar o track, nunca o tempo.
data-layout-ignore em decorativos off-canvas
Ghost text, glows e bg-layers posicionados fora do canvas visível (translateX negativo, left:-200px etc.) fazem o inspect acusar overflow — mesmo que visualmente estejam invisíveis. O atributo data-layout-ignore sinaliza ao engine que esses elementos são intencionalmente off-canvas.
O inspect do HyperFrames calcula o bounding box de todos os elementos visíveis. Elementos decorativos off-canvas — mesmo com overflow:hidden no pai — podem vazar e ampliar o bounding box medido. Resultado: vídeo renderizado com barras pretas nas bordas.
<div
class="bg-layer"
style="left:-240px;top:0">
GHOST TEXT
</div>
// → inspect: overflow detectado
<div
class="bg-layer"
style="left:-240px;top:0"
data-layout-ignore>
GHOST TEXT
</div>
// → inspect: ok
blur() que excedem as bordas do frame para criar halo suave.Fontes locais: nunca Google Fonts via <link> ou @import
O HyperFrames renderiza em ambiente offline (Chrome headless sem rede). Um <link> do Google Fonts falha silenciosamente — o vídeo é renderizado com a fonte fallback do sistema, sem erro visível no terminal. O lint acusa google_fonts_import e font_family_without_font_face.
O Chrome headless não reporta falhas de rede como erros GSAP ou JS. A página simplesmente cai para a fonte de fallback (sans-serif → Arial/Helvetica). O vídeo renderiza normalmente — mas com tipografia errada. Você pode não notar até assistir o resultado final em alta resolução.
<link href="https://fonts.googleapis.com/
css2?family=Inter:wght@400;700"
rel="stylesheet">
// → lint: google_fonts_import
@font-face {
font-family: 'Inter';
src: url('../fonts/inter-400.woff2')
format('woff2');
font-weight: 400;
font-display: block;
}
fetch-fonts.mjsnode scripts/fetch-fonts.mjs \
--family Inter \
--weights 400,500,600,700,800 \
--subset latin \
--out assets/fonts/
# resultado: assets/fonts/inter-400.woff2
# assets/fonts/inter-700.woff2
# assets/fonts/fonts.css (import pronto)
O subset latin inclui todos os caracteres usados em português brasileiro (ã, ç, õ, á, é, etc.). Não é necessário baixar o subset completo. Use font-display: block para garantir que o Chrome headless aguarde a fonte antes de capturar o frame.
ffmpeg -nostdin no Windows / git-bash
No Windows executando ffmpeg via git-bash (ou qualquer emulador POSIX sobre Win32), o ffmpeg pode ler stdin inadvertidamente, interpretar EOF como entrada válida e retornar exit 0 sem gerar nenhum arquivo de saída. O problema é completamente silencioso.
O pipeline continua sem falhar. O próximo passo tenta ler um arquivo que não existe. O erro real aparece vários passos depois, difícil de rastrear. A flag -nostdin resolve completamente — e não tem custo em ambientes normais.
ffmpeg -y \
-framerate 30 \
-i frames/%04d.png \
-i audio.wav \
output.mp4
# → exit 0, output.mp4 não existe
ffmpeg -nostdin -y \
-framerate 30 \
-i frames/%04d.png \
-i audio.wav \
output.mp4
# → output.mp4 gerado corretamente
-ss 2.5 \
-i renders/video-16x9.mp4 \
-vframes 1 \
-update 1 \
thumbnail.png
Se precisar de caminho absoluto no Windows: /c/ffmpeg/bin/ffmpeg.exe -nostdin ...
Determinismo: proibido Date.now(), Math.random() e fetch()
O render do HyperFrames é determinístico por design: o mesmo HTML deve produzir exatamente os mesmos frames em qualquer máquina, em qualquer momento. Qualquer fonte de não-determinismo — tempo real, aleatoriedade, dados externos — quebra essa garantia e produz frames inconsistentes ou erro de render.
O engine captura frames individuais navegando pelo tempo da timeline GSAP de forma sintética — não executa o vídeo em tempo real. Isso significa que Date.now() retorna valores diferentes para cada frame capturado, Math.random() nunca é seeded de forma reproduzível e fetch() pode falhar ou retornar dados diferentes a cada execução.
-
✗
Date.now()— retorna timestamp diferente por frame -
✗
Math.random()— sequência não-reproduzível -
✗
fetch()— dados externos podem mudar ou falhar -
✗
setInterval()— baseado em tempo real do SO -
✗
new Date()— mesmo problema do Date.now()
-
✓
Posições calculadas:
gsap.utils.mapRange() -
✓
Dados externos: pré-baked no HTML em tempo de build
-
✓
Pseudo-random:
seededRand(n)com semente fixa -
✓
Contadores: variáveis JS atualizadas por
tl.call() -
✓
Timeline registrada:
window.__timelines["main"]
function seededRand(seed) {
let s = seed;
return () => {
s = (s * 1664525 + 1013904223) & 0xffffffff;
return (s >>> 0) / 0xffffffff;
};
}
const rand = seededRand(42);
// rand() sempre retorna a mesma sequência
multiple_root_compositionsSó pode existir um único arquivo com data-composition-id na raiz do projeto. Se você deixar index-vertical.html, index-backup.html ou qualquer variante, o gerador build-index.mjs conflita e o lint falha. Sempre use apenas index.html como raiz da composição.
O que você aprendeu neste módulo
Você concluiu a Trilha 3 — Por dentro. Agora que entende a estrutura interna, os gotchas e como o gerador funciona, é hora de aplicar: YouTube & Shorts, onboarding, aulas e a biblioteca de prompts para acelerar sua produção.