Animaciones code-rendered
La idea (Addy Osmani, LinkedIn 2026-09-24): un modelo de codificación lee una fuente (un artículo, una especificación) y escribe JavaScript que dibuja cada fotograma, además del sonido. El entregable es un programa, por lo que cualquier momento puede editarse, re-timearse, re-renderizarse o reformatearse. Osmani no publicó su prompt ni su herramienta. El diseño de este skill proviene del proyecto de código abierto riso-windowseat (MIT, solo referencia, no copies sus instrucciones): https://github.com/sevenevesai/riso-windowseat . También toma ideas de HeyGen HyperFrames (https://hyperframes.heygen.com/guides/pipeline) y Remotion.
Reglas básicas: revisar fotograma por fotograma a 4+ muestras por segundo, nunca afirmar una comprobación que no se haya ejecutado, y corregir defectos de forma quirúrgica.
El contrato (todo depende de ello)
Cada trabajo es un index.html autónomo con un <canvas> que lee ?w=&h= de la URL y expone:
window.__anim = {
duration, fps, width, height, canvas,
ready, // false hasta que las fuentes/activos estén cargados y seek(0) haya dibujado
seek(t), // dibuja exactamente el fotograma en el tiempo t, de forma síncrona
renderAudio(), // Promise<base64 WAV>, 48 kHz estéreo, exactamente `duration` de duración
marks, // tiempos de eventos visibles (pops, cortes) - el sonido debe coincidir con estos
shots, // [{id,start,end}] - la verificación comprueba los límites
};
Invariantes:
seek(t)es una función pura de t. Cada valor en pantalla se calcula a partir de t:prog(t,a,b), easing,pop(t,at). No hay estado entre fotogramas, no hay temporización conrequestAnimationFrame, no hayDate, no hay simulación física a menos que se recalcule desde 0 o en forma cerrada.- Nunca llamar a
Math.random()durante el renderizado. UsarrngFor('stable-key')(mulberry32 con semilla), y crear diseños aleatorios una vez al cargar a partir de claves fijas. Cambiar una clave cambia la salida, así que congelar las claves después de la aprobación. - Un objeto de línea de tiempo (
TL) contiene todos los rangos de escenas y tiempos de eventos. La imagen y la partitura leen de él. Así funciona la sincronización: el sonido se programa en los mismos números que la imagen aparece. - Esperar a las fuentes antes de
ready=true(document.fonts.load('700 40px "DejaVu Sans"')). De lo contrario, los primeros fotogramas se renderizan con una fuente de respaldo. Comprobar las fuentes instaladas confc-list : family(Linux/macOS) oC:\Windows\Fonts(Windows; Segoe UI, Bahnschrift, Consolas y Georgia están siempre presentes, DejaVu no). Si el diseño necesita una fuente específica, incrustarla como un @font-face en base64 y cargarla de la misma manera. - No hay llamadas de red en el trabajo. Incrustar cualquier activo como URI de datos.
Elementos editables (para film-fix)
review/film-fix.html permite a un revisor mover y escalar elementos directamente y escribe el resultado de vuelta en index.html. Las películas que quieran esto (el ejemplo lo hace; copiarlo de allí) añaden:
const TUNE = /*TUNE*/{}/*END-TUNE*/;en una línea, indexado por formato y luego por id de elemento:{"16x9":{"title":{"dx":0,"dy":0,"s":1}}}. Está indexado por formato porque cada aspecto tiene su propio diseño. dx/dy son píxeles del canvas. La herramienta reescribe solo el texto entre los dos marcadores.E(id, [x,y,w,h], draw)alrededor de cada elemento principal. BuscaTUNE[fmt][id], luego guarda, traslada por dx/dy, escala por s alrededor del centro de la caja, llama adraw()y restaura. También añade{id, box}a__anim.elements, donde box es la caja transformada en píxeles del canvas (dectx.getTransform(), por lo que los movimientos de cámara se respetan).seek(t)limpia esa lista al inicio de cada fotograma.__anim.elements,__anim.format(16x9,9x16,4x5,1x1) y__anim.tune(getter/setter para el objeto TUNE en vivo, usado para la vista previa en vivo de la herramienta).- Reglas: los ids son únicos en toda la película; sin aleatoriedad; con TUNE vacío, E no aplica ninguna transformación, por lo que los fotogramas permanecen idénticos byte a byte. Demostrarlo con un hash de fotograma antes y después de añadir E, luego ejecutar
verify.mjs.
Las películas sin E todavía funcionan en film-fix: su modo "Nudge" encuadra una región y convierte un arrastre o escalado en una corrección numérica para la IA.
Configuración del proyecto
Requiere Node 18+, ffmpeg (libx264, aac, blackdetect, freezedetect, ebur128) y un Chrome/Chromium local. El harness encuentra Chrome en las ubicaciones de instalación habituales en Windows, macOS y Linux; configurar la variable de entorno CHROME para sobrescribir. qa.sh necesita bash (Git Bash en Windows) y elige una fuente del sistema para el timecode; configurar QA_FONT para sobrescribir. No se instala nada a nivel de sistema; puppeteer-core es la única dependencia npm.
SKILL_DIR=<ruta a esta carpeta de skill>
mkdir -p anim && cp -r "$SKILL_DIR/harness" anim/harness
cd anim/harness && npm install && cd ..
mkdir -p works/<nombre>
cp "$SKILL_DIR/example/browser-10s/index.html" works/<nombre>/index.html # comenzar desde el ejemplo funcional
# ejecutar todos los comandos del harness desde anim/; las salidas van a out/
Comandos del harness
| Comando | Qué hace | Señal de éxito |
|---|---|---|
node harness/verify.mjs works/<n>/index.html --format 16x9 | Busca hacia adelante, hacia atrás y saltos en frío en las muestras + límites de tomas, luego compara hashes de píxeles | verify: OK, exit 0 |
node harness/shoot.mjs works/<n>/index.html --format 9x16 --times 1.5,3.4 o --range 3:4:0.1 --out out/stills | PNGs nativos del canvas en tiempos exactos | Míralos |
node harness/audio.mjs works/<n>/index.html --twice | Renderiza solo la partitura (segundos), comprueba la duración exacta y la determinismo entre ejecuciones | Sin FAIL. Chrome Web Audio puede diferir en 1 LSB entre ejecuciones; la tolerancia es 1 |
node harness/render.mjs works/<n>/index.html --format 16x9 --fps 30 --out out/<n>-16x9.mp4 | Busca cada fotograma y envía PNGs a ffmpeg (libx264 crf18, yuv420p, +faststart), luego multiplexa renderAudio como AAC | Imprime fotogramas y tiempo. Exit 2 en errores de página. --from/--to o --silent renderiza un rango sin audio |
node harness/lint.mjs works/<n>/index.html --all-formats | Puerta de calidad en la propia página (16:9 + 9:16; --format para uno): tamaño de texto, área segura, superposición de texto, tramo estático, fotogramas vacíos, lista de tomas, sonido en las marcas, palabras en pantalla | lint: OK (0 FAIL), exit 0 |
node harness/review.mjs works/<n>/index.html --format 16x9 --out out/review-16x9, luego --check out/review-16x9/REVIEW.md | Una hoja de contacto por toma (primero, 6 espaciadas uniformemente y último fotograma, timecode quemado), el JSON de lint, y REVIEW.md con preguntas fijas por toma. --check falla en una respuesta vacía o TODO, una puntuación inferior a 4, un FAIL de lint, una sección de toma eliminada o una película cambiada desde que se hicieron las hojas | review check: OK, exit 0 |
bash harness/qa.sh out/<n>-16x9.mp4 4 | ffprobe, blackdetect, freezedetect, EBU R128 loudness, duración de audio/video, hojas de contacto a N muestras/segundo con timecode quemado | Ver QA abajo |
Formatos: 16x9 1920x1080, 9x16 1080x1920, 4x5 1080x1350, 1x1 1080x1080. Los fotogramas van a ffmpeg como JPEG q95 por defecto, aproximadamente 16 fps a 1080p, por lo que una pieza de 40 s tarda unos 60-75 s por formato. --png es sin pérdidas pero aproximadamente 4 veces más lento.
Renderizados largos: la llamada al shell de un agente puede agotar el tiempo de espera (a menudo a 120 s) y llevarse consigo sus procesos hijos. O aumentar el tiempo de espera de la llamada o ejecutar renderizados completos de forma independiente y consultar el registro (se muestra para Linux/macOS; en Windows usar un shell en segundo plano o Start-Process):
setsid nohup bash -c 'node harness/render.mjs works/<n>/index.html --format 16x9 --out out/<n>-16x9.mp4; echo RENDER_DONE' > out/render.log 2>&1 < /dev/null & disown
tr '\r' '\n' < out/render.log | tail -2
También mantener los heredocs que escriben un archivo de trabajo separados de los comandos que lo ejecutan, para que un tiempo de espera no pueda perder el archivo.
Definición de completado
Los comandos exactos están en AGENTS.md (la fuente única, compartida con otras herramientas de codificación): verificar en cada formato, audio --twice, lint --all-formats con 0 FAIL, review.mjs luego review.mjs --check aprobado, renderizar, qa. Nunca afirmar completado con una puerta fallida, y nunca debilitar una puerta o editar umbrales del harness para aprobar una.
Flujo de trabajo
- Brief. Lo que dice la fuente, la audiencia, la duración, los formatos, narración o no. Extraer las afirmaciones clave del artículo fuente. Cada escena debe mostrar lo que se dice.
Historia antes que corrección. El harness hace que una película sea correcta. No la hace visible. Antes del storyboard, responder cuatro preguntas en el brief:
- ¿Quién es el personaje, y qué sienten en cada escena?
- ¿Qué objeto cotidiano representa cada idea? (El original usaba una carta con un sello de cera, un arenero y un pueblo de casas.)
- ¿Qué HUD persistente mantiene unidas las escenas? Normalmente un dial de capítulo y un titular escrito.
- ¿Cómo vuelve el último fotograma al primero? Una película sin nada de esto pasa todas las puertas y aún así se la pasa de largo.
- STORYBOARD.md antes que el código. Una fila por escena: rango de tiempo | contenido en pantalla | acción | señal de audio. Listar cada tiempo de evento (estos se convierten en
TLymarks). Planificar un corte o un nuevo movimiento cada pocos segundos y sin tramo estático. Planificar las transiciones como un empuje o superposición, nunca una caída a un fotograma vacío. Sin fundido a negro al final a menos que esté aprobado. - Módulo de línea de tiempo, luego una función
sceneN(t)por escena (pura en t), luego un compositor que dibuja el fondo, las escenas activas y las transiciones. - El fotograma más difícil primero. Construir el momento más denso,
shooten cada formato, mirarlo, arreglarlo. Luego construir el resto. - verify en cada formato, audio --twice, luego render cada formato.
- QA (abajo), arreglar quirúrgicamente, re-renderizar solo lo necesario, re-QA.
- Entregar los MP4s como adjuntos con los resultados de QA y las debilidades conocidas.
Con narración
Primero TTS, luego marcas de tiempo a nivel de palabra (transcribir el WAV con cualquier herramienta que devuelva tiempos por palabra), luego configurar TL a partir de las palabras. Las escenas, los pops y los resaltados de subtítulos karaoke dependen de los tiempos de las palabras. Reglas: subtítulos estilo karaoke en un recuadro, solo una pista de subtítulos, números en pantalla cuando se hablan, comprobar nombres propios de oído. Incrustar el WAV de narración como URI de datos y mezclarlo dentro de renderAudio() (decodificar con ctx.decodeAudioData, programar en 0), o multiplexarlo con ffmpeg y bajar la partitura debajo de él. Normalizar la sonoridad de la narración en todo el video.
Multi-formato desde una línea de tiempo (no recortes)
- Mismo
TL, mismas escenas, un diseño diferente. Calcularportrait = H > W,S = min(W,H)/1080(escala de tipo y forma),M = 0.06*min(W,H)(margen seguro). Distribuir por aspecto: las filas en 16:9 se convierten en columnas en 9:16, los árboles se vuelven más estrechos, etc. - Texto: shrink-to-fit (bucle
measureTextcontraW-2M) para que nada se corte nunca en el borde. Los tamaños provienen deS. En retrato, usar tipos más grandes y llenar la altura. - No tratar "retrato" como un solo caso. 4:5 (1080x1350) es mucho más corto que 9:16. El diseño de columnas 9:16 del ejemplo ejecutado en 4:5 empuja la cuadrícula de píxeles y la etiqueta fuera de la parte inferior y una encima de la otra. Ramificar en la relación de aspecto (
H/W: aproximadamente 0.56, 1.25, 1.78) o diseñar desde la altura disponible, yshootcada formato que entregues. - Recordar el overshoot de pop (easeOutBack ~1.1x) y las escalas "calientes" al comprobar márgenes. Verificar el fotograma más denso de cada formato con
shoot.
Sonido procedural (OfflineAudioContext)
new OfflineAudioContext(2, 48000*duration, 48000), construir el grafo,await ctx.startRendering(), codificar a WAV PCM de 16 bits, devolver base64 (verwavB64en el ejemplo).- Patrón del ejemplo: un acorde pad por escena (pares de seno/triángulo desafinados, ataque lento, superpuestos en cortes), un pluck (triángulo + 2 armónicos, ataque de 5 ms, decaimiento exponencial) en cada evento de
marks, clics de ráfaga de ruido con semilla para escritura, un golpe de caída de tono para un impacto, y un DynamicsCompressor en el maestro. - El ruido proviene del RNG con semilla, nunca Math.random.
- Sonoridad: apuntar a aproximadamente -14 LUFS integrado y pico por debajo de -1 dBFS para feeds sociales. La publicación original midió -14.3 LUFS; el ejemplo incluido es más silencioso a -17.2 LUFS, pico -1.4. Configurar esto con ganancia maestra en la página para que permanezca en el código.
- Para audio bit-exacto, sintetizar muestras en JS plano en un Float32Array (suma de osciladores con envolventes de forma cerrada) en lugar de nodos Web Audio.
- Tone.js
Tone.Offline(https://tonejs.github.io/docs/14.9.17/functions/Offline.html) es una opción si se necesita una API musical más rica. Debe estar incrustado en línea ya que el trabajo no tiene acceso a la red.
QA (la revisión es nuestra, no del espectador)
Puertas automatizadas, todas en el MP4 final:
- verify OK en cada formato. Errores de página = fallo.
- ffprobe: tamaño esperado, 30/1 fps, nb_frames = duración x fps, duración de audio = duración de video.
- blackdetect: ninguno (a menos que haya un ritmo negro dramático aprobado). freezedetect (>=2 s estático): ninguno.
- ebur128: sonoridad integrada y pico verdadero dentro del objetivo. Consistente entre episodios.
Lint (puerta de calidad)
node harness/lint.mjs works/<n>/index.html --all-formats [--json out/lint.json] busca el trabajo cada 0.1 s (--step) y envuelve fillText/strokeText desde fuera, por lo que se ejecuta en cualquier trabajo que siga el contrato sin cambios. Falla en:
- text-size: texto en reposo por debajo del 2.6% de min(W,H) (28 px a 1080); por debajo del 3.3% (36 px) es un WARN. El texto reducido por un zoom out por debajo de la escala 0.5 se trata como un mapa de visión general y solo se advierte.
- safe-area: texto en reposo dentro del 4% de un borde; 4-6% es un WARN.
- text-overlap: dos cadenas visibles diferentes que comparten más del 15% de la caja más pequeña durante 0.2 segundos o más. Los pasos más cortos son WARNs.
- static-stretch: más de 3 s sin cambio visible (2 s es un WARN). empty-frame: cualquier fotograma casi uniforme, incluidos el primero y el último.
- shots-coverage: huecos o superposiciones en
shots, o una toma de menos de 1 s. audio-sync: menos del 70% demarkscon un inicio de sonido dentro de ±40 ms (90% para PASS); un pico por encima de -1 dBFS es un WARN. density: más de 18 palabras en pantalla en retrato o 30 en paisaje (solo WARN).
Un trabajo no está terminado hasta que lint pasa con 0 FAIL en cada formato que entregues. Solo ve texto dibujado con fillText/strokeText en el canvas del propio trabajo, por lo que las colisiones de formas y el texto dibujado en canvases fuera de pantalla aún necesitan la revisión visual. example/browser-10s pasa en los tres formatos; example/browsers-40s se hizo antes de la puerta y falla (etiquetas a 17-28 px, una etiqueta cortada en 9:16, texto...
(nota: el contenido se truncó en el original)