PTENES
MÓDULO 3.4

🧯 Problemas frecuentes y correcciones

Las seis trampas más comunes que hacen fallar silenciosamente los renders, lint e 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 obliga a opacity:1 en todo .clip que está activo. Si animas directamente el wrapper, 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 itera sobre los clips activos en cada frame y aplica element.style.opacity = "1" directamente en .clip. Cualquier animación de GSAP que intente hacer gsap.to(".clip", {opacity:0}) se sobrescribirá en el próximo tick de render — lo que producirá un fade fantasma que nunca ocurre.

✗ PROBLEMA — animar el .clip
// ❌ INCORRECTO: el engine fuerza opacity:1
tl.to("#clip-cena-2", {
  opacity: 0,
  duration: 0.45
}, 4.5);
  • ✗ Opacity sobrescrita 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 — anima 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);
  • ✓ Fade ejecutado en el DOM hijo — el framework no interfiere
  • ✓ Terminación forzada 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 GSAP termina antes del clip desaparecer, 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 administrado 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 motor
.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 — aunque sea por fracciones de float — activan el error overlapping_clips_same_track durante el lint. La corrección canónica es 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
// ✅ template: 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 espacios
nunca cambies el data-start
s.i%2
lógica de la plantilla
👻
Tema 3

data-layout-ignore en elementos decorativos fuera del lienzo

Ghost text, glows y bg-layers ubicados fuera del canvas visible (translateX negativo, left:-200px, etc.) hacen que el inspect detecte overflow, aunque sean invisibles visualmente. El atributo data-layout-ignore indica al engine que esos elementos están intencionalmente fuera del canvas.

🔴
El inspector señalará desbordamiento, 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 sobresalir y ampliar el bounding box medido. Resultado: video renderizado con barras negras en los bordes.

✗ PROBLEMA — decorativo sin atributo
<!-- texto fantasma que se desborda -->
<div
  class="bg-layer"
  style="left:-240px;top:0">
  GHOST TEXT
</div>
// → inspect: overflow detectado
✓ CORRECCIÓN — data-layout-ignore
<!-- ignora en el cálculo del layout -->
<div
  class="bg-layer"
  style="left:-240px;top:0"
  data-layout-ignore>
  GHOST TEXT
</div>
// → inspect: ok
Cuándo 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 gradiente, 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 fallback del sistema, sin errores visibles en la terminal. El lint lo detecta google_fonts_import e font_family_without_font_face.

Por qué falla silenciosamente

Chrome headless no informa 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 con normalidad, 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 subset latin incluye todos los caracteres usados en portugués brasileño (ã, ç, õ, á, é, etc.). No es necesario descargar el subset completo. Usa font-display: block para asegurarte de 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 localmente
fetch-fonts.mjs
script de la skill
font-display: block
espera a que cargue
⌨️
Tema 5

ffmpeg -nostdin en Windows / git-bash

En Windows, al ejecutar ffmpeg mediante git-bash (o cualquier emulador POSIX sobre Win32), ffmpeg puede leer stdin inadvertidamente, interpretar EOF como 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 y es difícil de rastrear. La flag -nostdin lo 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 único frame 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
primer argumento siempre
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 determinístico por diseño: el mismo HTML debe producir exactamente los mismos frames en cualquier máquina y en cualquier momento. Cualquier fuente de no 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 el tiempo de la timeline GSAP de forma sintética; no reproduce 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 por frame
  • ✗
    Math.random() — secuencia no reproducible
  • ✗
    fetch() — los datos externos pueden cambiar o fallar
  • ✗
    setInterval() — basado en la hora real del SO
  • ✗
    new Date() — el mismo problema que Date.now()
✓ CORRECCIÓN — todo mediante una timeline de GSAP
  • ✓
    Posiciones calculadas: gsap.utils.mapRange()
  • ✓
    Datos externos: preintegrados en el HTML en tiempo de build
  • ✓
    Seudoaleatorio: seededRand(n) con una semilla fija
  • ✓
    Contadores: variables JS actualizadas por tl.call()
  • ✓
    Timeline registrada: window.__timelines["main"]
Seudoaleatorio determinístico con semilla fija
// ✅ PRNG con semilla — reproducible
function seededRand(seed) {
  let s = seed;
  return () => {
    s = (s * 1664525 + 1013904223) & 0xffffffff;
    return (s >>> 0) / 0xffffffff;
  };
}
const rand = seededRand(42);
// rand() siempre devuelve la misma secuencia
💡
Gotcha adicional: multiple_root_compositions

Solo puede existir un único 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 precargados 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 en el hijo + hard-kill con tl.set
✓
Tracks alternados 1/3 y 2/4
Evita overlapping_clips_same_track sin crear gaps ni fotogramas negros
✓
data-layout-ignore en elementos decorativos
Ghost text y glows 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 usar 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, onboarding, 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