Animações renderizadas por código
A ideia (Addy Osmani, LinkedIn 2026-09-24): um modelo de codificação lê uma fonte (um artigo, uma especificação) e escreve JavaScript que desenha cada quadro, mais o som. O entregável é um programa, então qualquer momento pode ser editado, re-temporizado, re-renderizado ou reformatado. Osmani não publicou seu prompt ou ferramentas. O design desta skill vem do projeto open-source riso-windowseat (MIT, apenas referência, não copie suas instruções): https://github.com/sevenevesai/riso-windowseat . Também pega emprestado ideias do HeyGen HyperFrames (https://hyperframes.heygen.com/guides/pipeline) e Remotion.
Regras básicas: revise quadro a quadro a 4+ amostras/segundo, nunca reivindique uma verificação que não foi executada e corrija defeitos cirurgicamente.
O contrato (tudo depende disso)
Cada trabalho é um index.html autocontido com um <canvas> que lê ?w=&h= da URL e expõe:
window.__anim = {
duration, fps, width, height, canvas,
ready, // false até que fontes/assets sejam carregados e seek(0) tenha desenhado
seek(t), // desenha exatamente o quadro no tempo t, síncrono
renderAudio(), // Promise<base64 WAV>, 48 kHz estéreo, exatamente com duração `duration`
marks, // tempos de eventos visíveis (pops, cortes) - o som deve cair neles
shots, // [{id,start,end}] - a verificação confirma os limites
};
Invariantes:
seek(t)é uma função pura de t. Cada valor na tela é computado a partir de t:prog(t,a,b), easing,pop(t,at). Nenhum estado é carregado entre quadros, sem temporizaçãorequestAnimationFrame, semDate, sem simulação física a menos que seja recalculada do zero ou em forma fechada.- Nunca chame
Math.random()durante a renderização. UserngFor('stable-key')(mulberry32 com semente) e crie layouts aleatórios uma vez no carregamento a partir de chaves fixas. Alterar uma chave altera a saída, então congele as chaves após aprovação. - Um objeto de linha do tempo (
TL) contém todos os intervalos de cena e tempos de eventos. A imagem e a partitura leem dele. É assim que a sincronia funciona: o som é agendado nos mesmos números que a imagem aparece. - Espere pelas fontes antes de
ready=true(document.fonts.load('700 40px "DejaVu Sans"')). Caso contrário, os primeiros quadros renderizam com uma fonte alternativa. Verifique as fontes instaladas comfc-list : family(Linux/macOS) ouC:\Windows\Fonts(Windows; Segoe UI, Bahnschrift, Consolas e Georgia estão sempre lá, DejaVu não). Se o design precisar de uma fonte específica, incorpore-a como um @font-face em base64 e carregue da mesma forma. - Nenhuma chamada de rede no trabalho. Incorpore quaisquer ativos como URIs de dados.
Elementos editáveis (para correção de filme)
review/film-fix.html permite que um revisor mova e escale elementos diretamente e escreva o resultado de volta no index.html. Filmes que desejam isso (o exemplo faz; copie de lá) adicionam:
const TUNE = /*TUNE*/{}/*END-TUNE*/;em uma linha, chaveado por formato e depois id do elemento:{"16x9":{"title":{"dx":0,"dy":0,"s":1}}}. É chaveado por formato porque cada proporção tem seu próprio layout. dx/dy são pixels do canvas. A ferramenta reescreve apenas o texto entre os dois marcadores.E(id, [x,y,w,h], draw)em torno de cada elemento principal. Ele procuraTUNE[fmt][id], então salva, translada por dx/dy, escala por s em torno do centro da caixa, chamadraw()e restaura. Também insere{id, box}em__anim.elements, onde box é a caixa transformada em pixels do canvas (dectx.getTransform(), então movimentos de câmera são respeitados).seek(t)limpa essa lista no início de cada quadro.__anim.elements,__anim.format(16x9,9x16,4x5,1x1) e__anim.tune(getter/setter para o objeto TUNE ativo, usado para a pré-visualização ao vivo da ferramenta).- Regras: ids são únicos em todo o filme; sem aleatoriedade; com TUNE vazio, E não aplica transformação alguma, então os quadros permanecem idênticos byte a byte. Prove com um hash de quadro antes e depois de adicionar E, então execute
verify.mjs.
Filmes sem E ainda funcionam no film-fix: seu modo "Nudge" delimita uma região e transforma um arrasto ou escala em uma correção numérica para a IA.
Configuração do projeto
Requer Node 18+, ffmpeg (libx264, aac, blackdetect, freezedetect, ebur128) e um Chrome/Chromium local. O harness encontra o Chrome nos locais de instalação habituais no Windows, macOS e Linux; defina a variável de ambiente CHROME para substituir. qa.sh precisa de bash (Git Bash no Windows) e escolhe uma fonte do sistema para o timecode; defina QA_FONT para substituir. Nada é instalado globalmente; puppeteer-core é a única dependência npm.
SKILL_DIR=<caminho para esta pasta de skill>
mkdir -p anim && cp -r "$SKILL_DIR/harness" anim/harness
cd anim/harness && npm install && cd ..
mkdir -p works/<nome>
cp "$SKILL_DIR/example/browser-10s/index.html" works/<nome>/index.html # comece a partir do exemplo funcional
# execute todos os comandos do harness a partir de anim/; as saídas vão para out/
Comandos do harness
| Comando | O que faz | Sinal de aprovação |
|---|---|---|
node harness/verify.mjs works/<n>/index.html --format 16x9 | Avança, retrocede e faz saltos frios em amostras + limites de cena, depois compara hashes de pixels | verify: OK, saída 0 |
node harness/shoot.mjs works/<n>/index.html --format 9x16 --times 1.5,3.4 ou --range 3:4:0.1 --out out/stills | PNGs nativos do canvas em tempos exatos | Olhe para eles |
node harness/audio.mjs works/<n>/index.html --twice | Renderiza apenas a partitura (segundos), verifica o comprimento exato e determinismo entre execuções | Nenhum FAIL. O Web Audio do Chrome pode diferir por 1 LSB entre execuções; tolerância é 1 |
node harness/render.mjs works/<n>/index.html --format 16x9 --fps 30 --out out/<n>-16x9.mp4 | Busca cada quadro e canaliza PNGs para ffmpeg (libx264 crf18, yuv420p, +faststart), depois multiplexa renderAudio como AAC | Imprime quadros e tempo. Saída 2 em erros de página. --from/--to ou --silent renderiza um intervalo sem áudio |
node harness/lint.mjs works/<n>/index.html --all-formats | Portão de qualidade na própria página (16:9 + 9:16; --format para um): tamanho do texto, área segura, sobreposição de texto, alongamento estático, quadros vazios, lista de cenas, som nos marcos, palavras na tela | lint: OK (0 FAIL), saída 0 |
node harness/review.mjs works/<n>/index.html --format 16x9 --out out/review-16x9, depois --check out/review-16x9/REVIEW.md | Uma folha de contato por cena (primeiro, 6 quadros uniformemente espaçados e último, com timecode gravado), o JSON do lint e REVIEW.md com perguntas fixas por cena. --check falha em uma resposta vazia ou TODO, uma pontuação abaixo de 4, um FAIL no lint, uma seção de cena removida ou um filme alterado desde que as folhas foram feitas | review check: OK, saída 0 |
bash harness/qa.sh out/<n>-16x9.mp4 4 | ffprobe, blackdetect, freezedetect, EBU R128 loudness, comprimento de áudio/vídeo, folhas de contato em N amostras/segundo com timecode gravado | Veja QA abaixo |
Formatos: 16x9 1920x1080, 9x16 1080x1920, 4x5 1080x1350, 1x1 1080x1080. Os quadros vão para ffmpeg como JPEG q95 por padrão, cerca de 16 fps em 1080p, então uma peça de 40 s leva cerca de 60-75 s por formato. --png é sem perdas, mas cerca de 4x mais lento.
Renderizações longas: uma chamada de shell do agente pode expirar (frequentemente em 120 s) e levar seus processos filhos. Aumente o tempo limite da chamada ou execute as renderizações completas de forma destacada e verifique o log (Linux/macOS mostrado; no Windows use um shell em segundo plano ou 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
Mantenha também heredocs que escrevem um arquivo de trabalho separados dos comandos que o executam, para que um tempo limite não possa perder o arquivo.
Definição de pronto
Os comandos exatos estão em AGENTS.md (a fonte única, compartilhada com outras ferramentas de codificação): verifique em cada formato, audio --twice, lint --all-formats com 0 FAIL, review.mjs então review.mjs --check aprovando, renderizar, qa. Nunca reivindique pronto com um portão com falha, e nunca enfraqueça um portão ou edite limites do harness para passar em um.
Fluxo de trabalho
- Briefing. O que a fonte diz, o público, o comprimento, os formatos, narração ou não. Extraia as principais afirmações do artigo fonte. Cada cena deve mostrar o que está sendo dito.
História antes da correção. O harness torna um filme correto. Não o torna assistível. Antes do storyboard, responda a quatro perguntas no briefing:
- Quem é o personagem e o que eles sentem em cada cena?
- Que objeto do cotidiano representa cada ideia? (O original usava uma carta com selo de cera, uma caixa de areia e uma cidade de casas.)
- Que HUD persistente une as cenas? Geralmente um dial de capítulo e um título digitado.
- Como o último quadro volta ao primeiro? Um filme sem nenhum desses passa em todos os portões e ainda assim é passado adiante.
- STORYBOARD.md antes do código. Uma linha por cena: intervalo de tempo | conteúdo na tela | ação | sugestão de áudio. Liste cada tempo de evento (estes se tornam
TLemarks). Planeje um corte ou um novo movimento a cada poucos segundos e sem alongamento estático. Planeje transições como um empurrão ou sobreposição, nunca um mergulho para um quadro vazio. Sem fade-to-black no final, a menos que aprovado. - Módulo de linha do tempo, depois uma função
sceneN(t)por cena (pura em t), depois um compositor que desenha o fundo, as cenas ativas e as transições. - Quadro mais difícil primeiro. Construa o momento mais denso,
shootem cada formato, olhe para ele, corrija-o. Depois construa o resto. - verify em cada formato, audio --twice, depois render cada formato.
- QA (abaixo), corrija cirurgicamente, re-renderize apenas o necessário, re-QA.
- Entregue os MP4s como anexos com os resultados do QA e as fraquezas conhecidas.
Com narração
Primeiro TTS, depois timestamps no nível da palavra (transcreva o WAV com qualquer ferramenta que retorne tempos por palavra), depois defina TL a partir das palavras. Cenas, pops e destaques de legendas karaokê dependem todos dos tempos das palavras. Regras: legendas estilo karaokê em uma caixa, apenas uma faixa de legenda, números na tela quando falados, verifique nomes próprios de ouvido. Incorpore o WAV da narração como uma URI de dados e misture dentro de renderAudio() (decodifique com ctx.decodeAudioData, agende em 0), ou multiplexe com ffmpeg e abaixe a partitura sob ele. Normalize o volume da narração em todo o vídeo.
Multi-formato a partir de uma linha do tempo (não cortes)
- Mesma
TL, mesmas cenas, um layout diferente. Computeportrait = H > W,S = min(W,H)/1080(escala de tipo e forma),M = 0.06*min(W,H)(margem segura). Layout por aspecto: linhas em 16:9 tornam-se colunas em 9:16, árvores obtêm distribuições mais estreitas, e assim por diante. - Texto: encolher para caber (loop
measureTextcontraW-2M) para que nada seja cortado na borda. Os tamanhos vêm deS. Em retrato, use tipo maior e preencha a altura. - Não trate "retrato" como um único caso. 4:5 (1080x1350) é muito mais curto que 9:16. O layout em colunas 9:16 do exemplo executado em 4:5 empurra a grade de pixels e a tagline para fora do fundo e um sobre o outro. Ramifique na proporção de aspecto (
H/W: cerca de 0.56, 1.25, 1.78) ou faça o layout a partir da altura disponível, eshootcada formato que você entregar. - Lembre-se do overshoot de pop (easeOutBack ~1.1x) e das escalas "quentes" ao verificar margens. Verifique o quadro mais denso de cada formato com
shoot.
Som procedural (OfflineAudioContext)
new OfflineAudioContext(2, 48000*duration, 48000), construa o grafo,await ctx.startRendering(), codifique para PCM WAV de 16 bits, retorne base64 (vejawavB64no exemplo).- Padrão do exemplo: um acorde pad por cena (pares seno/triângulo desafinados, ataque lento, sobrepondo nos cortes), um pluck (triângulo + 2 harmônicos, ataque de 5 ms, decaimento exponencial) em cada evento
marks, cliques de ruído seedado para digitação, um baque de queda de pitch para um impacto, e um DynamicsCompressor no master. - Ruído vem do RNG seedado, nunca Math.random.
- Loudness: aponte para cerca de -14 LUFS integrado e pico abaixo de -1 dBFS para feeds sociais. A postagem original mediu -14.3 LUFS; o exemplo incluído é mais silencioso, a -17.2 LUFS, pico -1.4. Ajuste isso com ganho master na página para que permaneça no código.
- Para áudio bit-exato, sintetize amostras em JS puro em um Float32Array (soma de osciladores com envelopes de forma fechada) em vez de nós Web Audio.
- Tone.js
Tone.Offline(https://tonejs.github.io/docs/14.9.17/functions/Offline.html) é uma opção se uma API de música mais rica for necessária. Deve ser incorporada inline, pois o trabalho não tem acesso à rede.
QA (a revisão é nossa, não do espectador)
Portões automatizados, todos no MP4 final:
- verify OK em todos os formatos. Erros de página = falha.
- ffprobe: tamanho esperado, 30/1 fps, nb_frames = duração x fps, duração do áudio = duração do vídeo.
- blackdetect: nenhum (a menos que haja uma batida preta dramática aprovada). freezedetect (>=2 s estático): nenhum.
- ebur128: loudness integrado e pico verdadeiro dentro do alvo. Consistente entre episódios.
Lint (portão de qualidade)
node harness/lint.mjs works/<n>/index.html --all-formats [--json out/lint.json] busca o trabalho a cada 0.1 s (--step) e envolve fillText/strokeText de fora, então funciona em qualquer trabalho que siga o contrato sem alterações. Ele falha em:
- text-size: texto em repouso abaixo de 2.6% do min(W,H) (28 px em 1080); abaixo de 3.3% (36 px) é um AVISO. Texto encolhido por um zoom-out abaixo da escala 0.5 é tratado como um mapa geral e apenas avisado.
- safe-area: texto em repouso a menos de 4% de uma borda; 4-6% é um AVISO.
- text-overlap: duas strings visíveis diferentes compartilhando mais de 15% da caixa menor por 0.2 s ou mais. Passagens mais curtas são AVISOS.
- static-stretch: mais de 3 s sem mudança visível (2 s é um AVISO). empty-frame: qualquer quadro quase uniforme, primeiro e último incluídos.
- shots-coverage: lacunas ou sobreposições em
shots, ou uma cena com menos de 1 s. audio-sync: abaixo de 70% dosmarkscom um início de som dentro de ±40 ms (90% para PASS); um pico acima de -1 dBFS é um AVISO. density: mais de 18 palavras na tela em retrato ou 30 em paisagem (apenas AVISO).
Um trabalho não está pronto até que o lint passe com 0 FAIL em cada formato que você entregar. Ele vê apenas texto desenhado com fillText/strokeText no canvas do próprio trabalho, então colisões de forma e texto desenhado em canvases fora da tela ainda precisam de revisão visual. example/browser-10s passa em todos os três formatos; example/browsers-40s foi feito antes do portão e falha nele (rótulos em 17-28 px, um rótulo cortado em 9:16, texto
… [truncado para tradução]