vid-motion-graphics
Gera motion graphics multi-cena como MP4. Renderiza animações HTML/CSS em Chromium headless via Playwright (Web Animations API frame-seeking), monta quadros PNG com FFmpeg. Sem React, sem APIs de IA, sem Python — zero novas dependências além da família graphic-gif.
Fontes CDN apenas. Sem bibliotecas externas no HTML.
Regras Críticas (leia antes de cada geração)
- Use
window.renderFrame(t)— sem CSS@keyframespara transições de cena. A buscacurrentTimede animação CSS é silenciosamente ignorada para retrocessos no Chromium. A abordagem renderFrame: uma função JS pura calculaopacity/transformdiretamente a partir de milissegundos. Playwright a chama uma vez por quadro. Determinística, livre de condições de corrida. - Sem
animation-delayem QUALQUER elemento. Não é necessário com renderFrame. Se você se pegar escrevendoanimation-delay, pare — você está usando a arquitetura errada. window.__videoReady = trueapenas dentro dedocument.fonts.ready.then(...). Nunca defina de forma síncrona — as fontes devem carregar antes do Playwright capturar o quadro 1 ou o texto será renderizado com fontes substitutas.- Exponha
window.__stopPreview(). O loop de pré-visualização rAF do navegador compete com as chamadas evaluate/screenshot do Playwright.capture-frames.mjschama__stopPreview()antes do loop de quadros. Sempre inclua. - Use
t < startMs(não<=) nas verificações de limite de cena.t <= 0no quadro 0 torna a cena 1 preta. A proteção correta éif (t < startMs || t >= endMs) return hidden. - Body = dimensões exatas em pixels. Largura e altura são inteiros (
1080px,1920px). Sem%,vw/vhou unidades responsivas. - Nenhuma duas cenas visíveis simultaneamente (exceto sobreposição de entrada de 10%). Todas as cenas com
opacity: 0fora da janela renderFrame. - Transições usam
opacityapenas. Sem alternânciadisplay, semvisibility— opacidade compostada pela GPU é perfeita para quadros. - Nunca despeje HTML no chat. Salve em arquivo, mostre apenas o resumo.
- O título comunica a mensagem, não o tópico. "3 Razões Pelas Quais Q4 Bateu Metas" não "Vídeo de Desempenho Q4 2024".
- Leia
references/scene-library.mdANTES de gerar QUALQUER HTML. Use a estrutura HTML exata e nomes de classe CSS desse arquivo.
Etapa 1: Coleta de Informações
Obrigatório: content_brief
Parâmetros opcionais e padrões:
| Parâmetro | Padrão | Descrição |
|---|---|---|
| content_brief | — | Texto descrevendo o que o vídeo comunica (obrigatório) |
| scenes | auto | Número de cenas (1–6). Auto = derivado do resumo. |
| duration_per_scene | 3s | Duração por cena em segundos (1–8s) |
| style | kinetic-dark | kinetic-dark / editorial-light / data-pulse / bold-type / minimal-clean |
| aspect_ratio | 1:1 | 1:1 (1080×1080) / 16:9 (1920×1080) / 9:16 (1080×1920) |
| fps | 30 | Quadros por segundo (24, 30 ou 60) |
| music | none | Caminho para arquivo de áudio para trilha de fundo (mp3/m4a/wav) |
| source | — | Atribuição de fonte mostrada no rodapé do quadro final |
Se content_brief estiver faltando, pergunte exatamente:
"Para criar o vídeo, preciso de um resumo de conteúdo — o que o vídeo deve comunicar?
Exemplo: 'Mostre 3 razões pelas quais a receita do Q4 cresceu 85%: novos contratos empresariais, redução de churn, aumento de preço. Use números em negrito. Estilo: data-pulse.'
Opcional: estilo (padrão: kinetic-dark), proporção de aspecto (padrão: 1:1), segundos por cena (padrão: 3s)"
Se content_brief estiver presente → prossiga para a Etapa 2 imediatamente.
Etapa 2: Arquitetura Interna (nunca mostrada ao usuário)
1. Analise o resumo em cenas (máx. 6):
- Cena 1 = gancho ou título (sempre)
- Cenas 2–N-1 = pontos de suporte, métricas ou batidas da história
- Cena N = CTA ou resumo final (sempre, se mais de 1 cena)
- Uma ideia chave por cena — se o resumo tiver 7+ ideias, consolide as mais fracas
2. Leia references/scene-library.md — escolha o tipo de cena para cada cena:
- Gancho/abertura →
title-card - Métrica única →
stat-reveal - Lista de 2–4 pontos →
bullet-list - Antes vs depois / dois valores →
split-screen - Depoimento / citação →
quote-card - Final / CTA →
cta-card
3. Leia references/style-presets.md — carregue tokens CSS + personalidade de animação para o estilo escolhido.
4. Calcule o tempo:
totalDuration = sceneCount × duration_per_scene (segundos)
totalFrames = totalDuration × fps
Cada cena ocupa (100 / sceneCount)% do intervalo total de @keyframes.
| Cena | Início % | Fim % |
|---|---|---|
| 1 | 0% | (100/N)% |
| 2 | (100/N)% | (200/N)% |
| … | … | … |
| N | ((N-1)×100/N)% | 100% |
Dentro do intervalo de cada cena:
- Entrada: primeiros 10% do intervalo da cena
- Manutenção: 10% a 85% do intervalo da cena
- Saída: 85% a 100% do intervalo da cena
5. Determine as dimensões em pixels:
1:1→ L=1080, A=108016:9→ L=1920, A=10809:16→ L=1080, A=1920
Etapa 3: Geração de HTML
Leia references/scene-library.md E references/style-presets.md antes de escrever qualquer código.
Estrutura HTML obrigatória:
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
[link CDN de fonte do preset de estilo]
<style>
:root {
[todos os tokens CSS do preset de estilo]
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: [L]px; height: [A]px;
overflow: hidden;
background: var(--bg);
font-family: var(--font-body);
position: relative;
}
.scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px;
opacity: 0;
will-change: opacity, transform;
}
.scene-inner {
width: 100%;
max-width: 960px;
}
/* CSS de tipo de cena de scene-library.md */
[cole o CSS do tipo de cena aqui]
</style>
</head>
<body>
<div class="scene scene-1">
<div class="scene-inner">
[HTML da cena 1 do template scene-library.md]
</div>
</div>
[repita para cada cena]
<script>
window.__videoReady = false;
window.TOTAL_DURATION_MS = [totalDuration * 1000];
// ── Auxiliares de animação ─────────────────────────────────────────────────────────
function lerp(a, b, p) { return a + (b - a) * p; }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function easeOutCubic(t) { return 1 - Math.pow(1 - clamp(t, 0, 1), 3); }
function sceneState(t, startMs, endMs) {
if (t < startMs || t >= endMs) return { opacity: 0, ty: 0 };
const prog = (t - startMs) / (endMs - startMs);
if (prog < 0.10) {
const p = easeOutCubic(prog / 0.10);
return { opacity: p, ty: lerp(24, 0, p) };
}
if (prog < 0.85) return { opacity: 1, ty: 0 };
const p = (prog - 0.85) / 0.15;
return { opacity: 1 - p, ty: lerp(0, -12, p) };
}
function applySceneState(el, state) {
el.style.opacity = state.opacity;
el.style.transform = state.ty !== 0 ? `translateY(${state.ty.toFixed(2)}px)` : '';
}
// ── Função de renderização principal — chamada pelo Playwright uma vez por quadro ────────────────
window.renderFrame = function(t) {
// Cena 1: 0ms – [D]ms
applySceneState(document.querySelector('.scene-1'), sceneState(t, 0, [D]));
// Cena 2: [D]ms – [2D]ms
applySceneState(document.querySelector('.scene-2'), sceneState(t, [D], [2D]));
// ... repita por cena ...
};
// ── Loop de pré-visualização — parado pelo Playwright antes da captura de quadros ─────────────────
let __previewActive = false;
let __previewRafId = null;
window.__stopPreview = function() {
__previewActive = false;
if (__previewRafId !== null) { cancelAnimationFrame(__previewRafId); __previewRafId = null; }
};
document.fonts.ready.then(() => {
window.renderFrame(0);
window.__videoReady = true;
__previewActive = true;
const startTime = performance.now();
function previewTick() {
if (!__previewActive) return;
const elapsed = performance.now() - startTime;
if (elapsed < window.TOTAL_DURATION_MS) {
window.renderFrame(elapsed);
__previewRafId = requestAnimationFrame(previewTick);
} else {
window.renderFrame(window.TOTAL_DURATION_MS - 1);
__previewActive = false;
}
}
__previewRafId = requestAnimationFrame(previewTick);
});
</script>
</body>
</html>
Regras de qualidade de design:
- Tamanho da fonte do título: nunca menor que 60px — o texto deve ser legível no tamanho de miniatura móvel
- Números e estatísticas: sempre o maior elemento na tela (120–200px)
- Espaçamento entre letras apertado em tipo de exibição:
-0.02ema-0.04em - Uma cor de destaque por cena — não espalhe destaque em vários elementos
- Presets escuros: divisores
rgba(255,255,255,0.10), nunca sólidos - Presets claros: divisores
rgba(0,0,0,0.10), nunca sólidos transform-origin: center centerem todo elemento que usatransform- Preenchimento dentro de
.scene: mínimo 80px — nunca deixe o texto tocar as bordas da viewport
Etapa 4: Auto-QA (corrija cada falha antes da Etapa 5)
Correção do renderFrame:
-
window.renderFrame(t)definido — função pura, sem efeitos colaterais fora de escritas de estilo - Limite de cena usa
t < startMs(nãot <= startMs) — evita quadro 0 preto - Zero instâncias de
animation-delayou@keyframespara transições de cena - Nenhuma duas cenas têm janelas
opacity: 1sobrepostas (exceto sobreposição de entrada de 10%) -
window.__stopPreview()exposto e loop rAF de pré-visualização verifica__previewActive
Sinal de prontidão:
-
window.__videoReady = falsedeclarado antes dedocument.fonts.ready -
window.__videoReady = truedefinido SOMENTE dentro dedocument.fonts.ready.then(...) -
window.renderFrame(0)chamado dentro dedocument.fonts.ready.then(...)antes de definir__videoReady = true
Layout:
-
html, bodyusam dimensões exatas em pixels ([L]px,[A]px) - Sem unidades
%,vw,vh,remna largura/altura dobody -
overflow: hiddenemhtml, body - Todas as cenas
position: absolute; inset: 0
Design:
- Todas as cores de tokens de preset de estilo — sem valores hexadecimais livres
- Todas as fontes do preset de estilo — sem strings font-family livres
- Tamanho da fonte do título ≥ 60px
- Fonte no quadro final se o parâmetro
sourcefor fornecido
Etapa 5: Exportação
Determine o slug a partir do conteúdo do resumo (kebab-case, ≤30 caracteres):
mkdir -p chart/[slug]
Salve o HTML: chart/[slug]/video.html
Pré-visualização no navegador:
open chart/[slug]/video.html
Execute a exportação (substitua [skill-root] pelo caminho para o diretório deste skill):
bash [skill-root]/scripts/export-video.sh \
chart/[slug]/video.html \
chart/[slug]/video.mp4 \
--duration [totalDuration] \
--fps [fps] \
--width [L] \
--height [A] \
[--music path/to/audio.mp3]
O script instala o Playwright na primeira execução (~200 MB Chromium), captura todos os quadros e então monta o MP4 com FFmpeg.
Etapa 6: Resumo da Saída
## Vídeo: [título do resumo]
Data: [AAAA-MM-DD] | Cenas: [N] | Estilo: [estilo] | Proporção: [proporção]
Duração: [N]s | FPS: [fps] | Quadros: [N]
Arquivos
Fonte: chart/[slug]/video.html
Saída: chart/[slug]/video.mp4
Tamanho: [X] MB
Lista de Verificação
- [ ] Todas as cenas aparecem em sequência sem quadros em branco
- [ ] Texto legível no tamanho de miniatura móvel
- [ ] Transições de cena suaves (sem cortes abruptos)
- [ ] Cena final inclui CTA ou mensagem de encerramento
- [ ] Atribuição de fonte presente no quadro final (se fornecida)
Dicas de Prompt (mostre quando o usuário pedir orientação)
"Forneça um resumo de conteúdo — pontos-chave do que cada cena deve dizer."
"Nomeie o insight diretamente. '3 razões pelas quais Q4 cresceu 85%' dá uma espinha dorsal ao vídeo."
"Mencione o estilo se tiver preferência: kinetic-dark (padrão), editorial-light, data-pulse, bold-type, minimal-clean."
"Especifique a proporção de aspecto para a plataforma: 1:1 para feed do LinkedIn/Instagram, 9:16 para Stories/Reels, 16:9 para YouTube/apresentações."
✅ Bom: "Crie um vídeo de 9 segundos. Receita do Q4 atingiu $4,2M (crescimento de 85%). Impulsionadores: contratos empresariais, churn 1,2%, aumento de preço. CTA: acme.com/q4. Estilo: data-pulse. Proporção: 1:1."
❌ Ruim: "faça um vídeo sobre nossa empresa"