PTENES
MÓDULO 3.4

🧯 Errores comunes y correcciones

Las seis trampas más comunes que rompen silenciosamente los renders, el lint y el inspect, y cómo corregirlas de una vez por todas.

6
Temas
25
Minutos
Avanzado
Nivel
Debug
Tipo
🎭
Tema 1

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.

Mecánica del framework

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.

✗ PROBLEMA — animar el .clip
// ❌ INCORRECTO: el motor fuerza opacity:1
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)
✓ CORRECCIÓN — animar el .scene-inner
// ✅ CORRECTO: hijo interno
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.set garantiza opacity:0 al final
  • ✓ Cubre el gotcha gsap_exit_missing_hard_kill
💡
Gotcha relacionado: gsap_exit_missing_hard_kill

Incluso 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.

Estructura HTML recomendada
<!-- wrapper gestionado por el framework -->
<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>
Conceptos clave
.clip
wrapper controlado por el engine
.scene-inner
hijo animable con GSAP
opacity:1
forzado en el clip activo
tl.set
hard-kill al final de la escena
🔀
Tema 2

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.

Flujo de tracks en la línea de tiempo
Track 1
Escena 1
0s → 4s
Escena 3
8s → 12s
Escena 5
16s → 20s
Track 2
Caption 1
0s → 4s
Caption 3
8s → 12s
Caption 5
16s → 20s
Track 3
Escena 2
4s → 8s
Escena 4
12s → 16s
Escena 6
20s → 24s
Track 4
Caption 2
4s → 8s
Caption 4
12s → 16s
Caption 6
20s → 24s
Escenas impares (1,3,5…) → track 1 · Escenas pares (2,4,6…) → track 3 · Los captions se reflejan en los tracks 2/4
✗ PROBLEMA — mismo track, bordes en contacto
// ❌ escena 1 y escena 2 en el track 1
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
✓ CORRECCIÓN — tracks alternados
// ✅ plantilla: s.i%2===1 ? 1 : 3
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
⚠️
NO crees gaps entre escenas

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.

Conceptos clave
Track 1/3
escenas impares y pares
Track 2/4
captions alternados
Sin gaps
nunca cambies el data-start
s.i%2
lógica de la plantilla
👻
Tema 3

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 detectará overflow, aunque sea intencional

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.

✗ PROBLEMA — elemento decorativo sin atributo
<!-- texto fantasma desbordado -->
<div
  class="bg-layer"
  style="left:-240px;top:0">
  GHOST TEXT
</div>
// → inspect: desbordamiento detectado
✓ CORRECCIÓN — data-layout-ignore
<!-- ignorar en el cálculo del layout -->
<div
  class="bg-layer"
  style="left:-240px;top:0"
  data-layout-ignore>
  GHOST TEXT
</div>
// → inspect: ok
Al agregar data-layout-ignore
Ghost Text
Texto grande y semitransparente ubicado parcial o totalmente fuera del canvas como efecto decorativo.
Glow / Bloom
Divs de filtro blur() que exceden los bordes del frame para crear un halo suave.
BG-Layer
Capa de fondo que sale del canvas para cubrir los bordes con un degradado, sin crear una barra negra.
Conceptos clave
data-layout-ignore
atributo HTML puro, sin valor
bounding box
calculado por inspect engine
desbordamiento visual
barra negra en el video final
🔤
Tema 4

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.

Por qué falla silenciosamente

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.

✗ PROBLEMA — Google Fonts mediante enlace
<!-- ❌ INCORRECTO: requiere red -->
<link href="https://fonts.googleapis.com/
css2?family=Inter:wght@400;700"
rel="stylesheet">
// → lint: google_fonts_import
✓ CORRECCIÓN — @font-face local
/* ✅ CORRECTO: woff2 local */
@font-face {
  font-family: 'Inter';
  src: url('../fonts/inter-400.woff2')
       format('woff2');
  font-weight: 400;
  font-display: block;
}
Descargando con fetch-fonts.mjs
# descarga el subset latin (cubre PT-BR)
node 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 cubre PT-BR

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.

Conceptos clave
@font-face
declaración local en el CSS
.woff2
formato optimizado local
fetch-fonts.mjs
script de la skill
font-display: block
espera a que se cargue
⌨️
Tema 5

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.

🚨
Exit 0 sin archivo generado: el peor tipo de error

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.

✗ PROBLEMA — sin -nostdin
# ❌ Windows/git-bash sin -nostdin
ffmpeg -y \
  -framerate 30 \
  -i frames/%04d.png \
  -i audio.wav \
  output.mp4
# → exit 0, output.mp4 no existe
✓ CORRECCIÓN — -nostdin siempre
# ✅ -nostdin como primer argumento
ffmpeg -nostdin -y \
  -framerate 30 \
  -i frames/%04d.png \
  -i audio.wav \
  output.mp4
# → output.mp4 generado correctamente
💡
Extrayendo un solo fotograma con -nostdin
ffmpeg -nostdin -y \
  -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 ...

Conceptos clave
-nostdin
siempre el primer argumento
exit 0 falso
sin archivo generado
git-bash
emulador POSIX/Win32
-update 1
para un frame PNG único
🎲
Tema 6

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.

Por qué el render es cuadro a cuadro

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.

✗ PROBLEMA — fuentes no deterministas
  • ✗
    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()
✓ CORRECCIÓN — todo mediante GSAP timeline
  • ✓
    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"]
Pseudoaleatorio determinístico con semilla fija
// ✅ PRNG con semilla — reproducible
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
💡
Otro gotcha: multiple_root_compositions

Solo 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.

Conceptos clave
Determinismo
misma entrada → misma salida
PRNG con semilla
aleatorio reproducible
Sin fetch()
datos preprocesados en el HTML
tl.call()
callbacks en la timeline GSAP
📋
Resumen

Qué aprendiste en este módulo

✓
Animar .scene-inner, no .clip
El framework fuerza opacity:1 en el wrapper; los fades van al hijo + hard-kill con tl.set
✓
Tracks alternados 1/3 y 2/4
Evita overlapping_clips_same_track sin crear gaps o frames negros
✓
data-layout-ignore en elementos decorativos
El texto fantasma y los brillos fuera del canvas necesitan el atributo para evitar que se desborde el bounding box
✓
@font-face con .woff2 local
Google Fonts mediante link falla silenciosamente en Chrome headless sin conexión
✓
ffmpeg -nostdin en Windows
Sin la flag, exit 0 sin archivo: el error más silencioso del pipeline
✓
Render determinista
Prohibido Date.now(), Math.random(), fetch() — usa una timeline de GSAP y un PRNG con semilla
💡
Siguiente ruta
Ruta 4: Aplicaciones

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.

4.1
📺 YouTube y Shorts
4.2
🚀 Incorporación y clases
4.3
🧰 Biblioteca de prompts