Animar .scene-inner, nunca el .clip
El framework HyperFrames fuerza opacity:1 en todo .clip que está activo. Si animas el wrapper directamente, el fade no ocurre: el engine sobrescribe tu animación cuadro a cuadro. La solución es siempre animar un hijo interno.
HyperFrames recorre los clips activos en cada frame y aplica element.style.opacity = "1" directamente en .clip. Cualquier animación GSAP que intente hacer gsap.to(".clip", {opacity:0}) se sobrescribirá en el próximo tick de render — lo que resulta en un fade fantasma que nunca ocurre.
tl.to("#clip-cena-2", {
opacity: 0,
duration: 0.45
}, 4.5);
- ✗ La opacidad se sobrescribe en el siguiente frame
- ✗ El fade nunca es visible en el video renderizado
- ✗ Difícil de depurar (parece funcionar en la vista previa)
tl.to("#scene-inner-2", {
opacity: 0,
duration: 0.45
}, 4.5);
// hard-kill obligatorio:
tl.set("#scene-inner-2", {
opacity: 0
}, 4.95);
- ✓ El fade se ejecuta en el DOM hijo — el framework no interfiere
- ✓ Hard-kill con
tl.setgarantiza opacity:0 al final - ✓ Cubre el gotcha
gsap_exit_missing_hard_kill
gsap_exit_missing_hard_killIncluso usando scene-inner, si el tween de GSAP termina antes del clip desaparezca, la opacidad puede volver. Siempre agrega un tl.set(target, {opacity:0}, tempoFinal) como centinela. El composition-template.mjs ya incluye esa línea automáticamente.
<div id="clip-cena-2"
class="clip"
data-start="4.0"
data-duration="3.0"
data-track-index="1">
<!-- hijo animable -->
<div id="scene-inner-2" class="scene-inner">
<!-- contenido de la escena -->
</div>
</div>
Tracks alternados para evitar overlapping_clips_same_track
Las escenas adyacentes que se tocan en el límite temporal —incluso por fracciones de float— activan el error overlapping_clips_same_track durante el lint. La corrección canónica consiste en alternar los data-track-index: escenas en 1/3, captions en 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" // escena 1 (impar)
data-start="4.0" data-duration="4.0"
data-track-index="3" // escena 2 (par)
// → lint: ok
La tentación es agregar un pequeño gap (data-start="4.001") para evitar la colisión. Esto crea un fotograma negro visible en el video y además puede provocar otros errores del lint. La solución correcta siempre es cambiar el track, nunca el tiempo.
data-layout-ignore en elementos decorativos fuera del lienzo
El texto fantasma, los brillos y las bg-layers ubicadas fuera del canvas visible (translateX negativo, left:-200px, etc.) hacen que inspect detecte desbordamiento, aunque sean invisibles visualmente. El atributo data-layout-ignore indica al engine que estos elementos están intencionalmente fuera del canvas.
El inspect de HyperFrames calcula el bounding box de todos los elementos visibles. Los elementos decorativos fuera del canvas, incluso con overflow:hidden en el padre — pueden filtrarse y ampliar el bounding box medido. Resultado: video renderizado con barras negras en los bordes.
<div
class="bg-layer"
style="left:-240px;top:0">
GHOST TEXT
</div>
// → inspect: desbordamiento detectado
<div
class="bg-layer"
style="left:-240px;top:0"
data-layout-ignore>
GHOST TEXT
</div>
// → inspect: ok
blur() que exceden los bordes del frame para crear un halo suave.Fuentes locales: nunca Google Fonts mediante <link> o @import
HyperFrames renderiza en un entorno offline (Chrome headless sin red). Un <link> de Google Fonts falla silenciosamente — el video se renderiza con la fuente de respaldo del sistema, sin errores visibles en la terminal. El lint detecta google_fonts_import e font_family_without_font_face.
Chrome headless no reporta los fallos de red como errores de GSAP o JS. La página simplemente recurre a la fuente de respaldo (sans-serif → Arial/Helvetica). El video se renderiza normalmente, pero con la tipografía incorrecta. Puede que no lo notes hasta ver el resultado final en alta resolución.
<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 (importación lista)
El subconjunto latin incluye todos los caracteres usados en portugués brasileño (ã, ç, õ, á, é, etc.). No es necesario descargar el subset completo. Usa font-display: block para garantizar que Chrome headless espere a que se cargue la fuente antes de capturar el frame.
ffmpeg -nostdin en Windows / git-bash
Al ejecutar ffmpeg en Windows a través de git-bash (o cualquier emulador POSIX sobre Win32), ffmpeg puede leer stdin inadvertidamente, interpretar EOF como una entrada válida y devolver exit 0 sin generar ningún archivo de salida. El problema es completamente silencioso.
El pipeline continúa sin fallar. El siguiente paso intenta leer un archivo que no existe. El error real aparece varios pasos después, por lo que es difícil de rastrear. La flag -nostdin se resuelve por completo — y no tiene costo en entornos normales.
ffmpeg -y \
-framerate 30 \
-i frames/%04d.png \
-i audio.wav \
output.mp4
# → exit 0, output.mp4 no existe
ffmpeg -nostdin -y \
-framerate 30 \
-i frames/%04d.png \
-i audio.wav \
output.mp4
# → output.mp4 generado correctamente
-ss 2.5 \
-i renders/video-16x9.mp4 \
-vframes 1 \
-update 1 \
thumbnail.png
Si necesitas una ruta absoluta en Windows: /c/ffmpeg/bin/ffmpeg.exe -nostdin ...
Determinismo: prohibido Date.now(), Math.random() e fetch()
El render de HyperFrames es determinista por diseño: el mismo HTML debe producir exactamente los mismos frames en cualquier máquina y en cualquier momento. Cualquier fuente de falta de determinismo —tiempo real, aleatoriedad, datos externos— rompe esa garantía y produce frames inconsistentes o un error de render.
El engine captura frames individuales recorriendo de forma sintética el tiempo de la timeline GSAP: no ejecuta el video en tiempo real. Esto significa que Date.now() devuelve valores diferentes para cada frame capturado, Math.random() nunca es seeded de forma reproducible y fetch() puede fallar o devolver datos diferentes en cada ejecución.
-
✗
Date.now()— devuelve un timestamp diferente en cada frame -
✗
Math.random()— secuencia no reproducible -
✗
fetch()— los datos externos pueden cambiar o fallar -
✗
setInterval()— basado en el tiempo real del SO -
✗
new Date()— mismo problema que Date.now()
-
✓
Posiciones calculadas:
gsap.utils.mapRange() -
✓
Datos externos: preintegrados en el HTML en tiempo de build
-
✓
Pseudoaleatorio:
seededRand(n)con semilla fija -
✓
Contadores: variables JS actualizadas por
tl.call() -
✓
Timeline registrada:
window.__timelines["main"]
función seededRand(semilla) {
let s = seed;
return () => {
s = (s * 1664525 + 1013904223) & 0xffffffff;
return (s >>> 0) / 0xffffffff;
};
}
const rand = seededRand(42);
// rand() siempre devuelve la misma secuencia
multiple_root_compositionsSolo puede existir uno solo archivo con data-composition-id en la raíz del proyecto. Si dejas index-vertical.html, index-backup.html o cualquier variante, el generador build-index.mjs entra en conflicto y el lint falla. Usa siempre solo index.html como raíz de la composición.
Qué aprendiste en este módulo
Completaste la Ruta 3 — Por dentro. Ahora que entiendes la estructura interna, los gotchas y cómo funciona el generador, es hora de ponerlo en práctica: YouTube y Shorts, incorporación, clases y la biblioteca de prompts para acelerar tu producción.