Site Reels: site → MP4 → deck
Requisitos
| Necessário para | O quê | Verificar |
|---|---|---|
| tudo | Windows, macOS ou Linux com GPU e área de trabalho visível (Chrome roda com interface) | n/a |
| tudo | Node.js 18+ | node -v |
| tudo | ffmpeg + ffprobe no PATH (ou FFMPEG=<caminho completo>) | ffmpeg -version |
| tudo | Chrome, Chromium ou Edge (detectado automaticamente, veja abaixo) | node -e "import('./scripts/find-chrome.mjs').then(m=>console.log(m.findChrome()))" |
record.mjs | puppeteer-core + puppeteer-screen-recorder (npm, no projeto) | npm ls puppeteer-core puppeteer-screen-recorder |
snap.mjs | puppeteer-core + timesnap (traz timeweb) | npm ls timesnap |
| showreel | Projeto Remotion | npx remotion --version |
| auditorias (opcional) | ImageMagick | magick -version |
| Upload Figma | Figma MCP (use_figma, upload_assets) + skills figma-use, figma-use-slides | Lista de ferramentas MCP |
Detecção do Chrome (scripts/find-chrome.mjs, usado por record.mjs e snap.mjs): CHROME
variável de ambiente, se definida; senão Chrome/Edge instalado (Windows: Program Files e LocalAppData; macOS:
/Applications e ~/Applications; Linux: google-chrome, chromium, microsoft-edge no PATH);
senão o mais novo ms-playwright/chromium-<N> (ou PLAYWRIGHT_BROWSERS_PATH); senão para e
informa como definir CHROME ou rodar npx playwright install chromium. No Windows adiciona
--use-angle=d3d11; em outros SOs o Chrome mantém seu backend GPU padrão.
O que faz
Grava cenas programadas de um site rodando localmente e entrega MP4s prontos para o Figma (1920×1080, 30fps, x264, < 100MB). Motor validado: puppeteer-core + puppeteer-screen-recorder (screencast CDP + ffmpeg). Nunca grava tela.
Executar
mkdir <proj>-reels && cd <proj>-reels && npm init -y && npm i puppeteer-core puppeteer-screen-recorder
mkdir -p scripts && cp <skill-dir>/scripts/{record,find-chrome}.mjs scripts/
cp <skill-dir>/scripts/scenes.example.mjs scenes.mjs # edite as cenas
# no repositório do site: npx next build && npx next start -p 3100 (build de produção: WebGL mais suave)
OUT="<pasta de saída>/Videos" node scripts/record.mjs scenes.mjs [scene ...]
Execute os scripts copiados para o projeto: ESM resolve puppeteer-core a partir da pasta do
script, não do diretório de trabalho.
Env: BASE (padrão http://localhost:3100), OUT, CHROME, FFMPEG, W, H, DPR,
MOBILE=1 (emulação de toque), KEEP_UI=1 (mantém a navegação), MIN_FPS (24). Celular 4:5:
W=360 H=450 DPR=2 MOBILE=1 resulta em 720×900.
Cada cena = { name, path, theme?, trim?, crf?, css?, prepare(page, ctx), act(page, ctx) }.
prepare executa antes da gravação (rolar até a seção, aguardar carregamento do 3D); act é o que vai
para o vídeo. ctx tem wait, scrollTo(page, y|selector, ms) e
scrollThrough(page, selector|null, ms, from, to) com easing. Introdução que roda ao carregar:
page.reload() dentro de act e use trim para cortar o carregamento.
Roteiro de marca que funciona: logo/hero intro (claro e escuro), troca de mídia hero, seção 3D com a câmera entrando no scroll, rota imersiva (walk), wordmark no rodapé e uma passada de página inteira. 5–7 cenas de 12–55s.
Auditar antes de entregar (não confie no código de saída)
- Folha de contato por vídeo:
ffmpeg -i x.mp4 -vf "fps=6/<dur>,scale=480:-1,tile=3x2" -frames:v 1 sheet.pnge olhe. - 1MB para 19s = cena em branco. > 100MB = não entra no Figma (o
crfda cena corrige; 24 para uma cena com granulação + trilhas). - Faixa superior (
crop=1920:120:0:0) em vários momentos para capturar uma navegação vazando. LOW-FPScom uma pausa longa no início/fim é um falso alarme (o medidor conta quadros parados); verifique a folha de contato.
Armadilhas conhecidas
- Janela oculta congela rAF → quadros congelados. O lançamento já passa
--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion.headless: falsepropositalmente: headless cai no SwiftShader e o WebGL engasga. - CSS de ocultar UI desaparece ao recarregar e quando a hidratação do React limpa
<head>→ o script reanexa um<style id="reels-hide">ao body a cada 100ms. - Um seletor inválido quebra toda a regra:
\[em um template literal JS vira[e.z-[60]invalidaheader, nav, .z-[60]. Uma regra por seletor arriscado. - Não oculte
[data-cursor]: sites marcam o canvas 3D e carrosséis com ele. Mire no elemento de cursor personalizado (fixed + mix-blend + pointer-events-none). - 3D carregado sob demanda (IntersectionObserver + import dinâmico): aguarde 5–7s após alcançar a
seção. Ler o canvas WebGL retorna vazio sem
preserveDrawingBuffer: use uma espera fixa ou uma captura de tela. - Screencast entrega ~32 quadros únicos/s → grave a 30fps (60 dá quadros repetidos irregulares).
O
trimpadrão de 0.3s descarta o primeiro quadro em cache. - Screencast engasga em uma máquina carregada (Photoshop aberto, render paralelo): o MP4 diz
"30fps" com 9–18 quadros únicos/s.
record.mjsmede (mpdecimate=hi=64:lo=24:frac=0.05) e regrava até 2x abaixo deMIN_FPS(24). Para placas WebGL/3D prefirasnap.mjs. - Banner de cookies: aceite em
prepare(clique genérico em "Aceitar/Accept/OK") antes de gravar. - Uma página de ferramenta sem scroll (gerador, editor de efeitos) grava parada: a cena precisa
interagir (fazer upload de um arquivo para
input[type=file], clicar em predefinições pelo texto do botão). Um slider personalizado não responde a uminputsintético.
Placas travadas a 60fps: snap.mjs (determinístico)
scripts/snap.mjs injeta timeweb (relógio virtual do timesnap: rAF, performance.now,
setTimeout e vídeo), avança 1/60s por quadro e captura telas. Independente da carga da máquina.
Cena: { name, path, theme, css, dur, wait (ms reais para carregar 3D), pre (ms virtuais antes de rolar, 2000; **0 para uma introdução que roda ao carregar**), start (s virtuais pulados), scroll: { sel, from, to, at, len } | { bottom: [from, to] em alturas de tela }, frame(page, t) (ex.: mouse) }. A rolagem é uma função do tempo virtual.
npm i puppeteer-core timesnap && mkdir -p scripts
cp <skill-dir>/scripts/{snap,find-chrome,patch-timeweb}.mjs scripts/
npm pkg set scripts.postinstall="node scripts/patch-timeweb.mjs" && npm run postinstall
FPS=60 OUT=... node scripts/snap.mjs scenes-snap.mjs [scene]
- Não use
timesnap()em si: com Next + R3F + Lenis ele gravou uma página em branco e deixou trechos congelados no hero WebGL (rAF não executa). O próprio loop (goTo + scrollTo + screenshot) funcionou em todos os lugares: 60/60 quadros únicos. - Patch do timeweb é obrigatório: ele substitui
video.play()por uma versão sem Promise e sem eventoplaying→video.play().catch()lança erro e a página fica em branco. preserveDrawingBuffer: trueforçado no getContext (desligue compreserve: false).- Quadros vão para o diretório temporário do SO, nunca para uma unidade sincronizada (milhares de jpgs sincronizando).
- Nenhum trabalho GPU em paralelo (render Remotion, Photoshop): o WebGL perde seu contexto e grava
segundos apenas de fundo. O script avisa
FLASH(quadro piscando por luminância); um longo trecho em branco só aparece na folha de contato. Olhe. - Auditoria de suavidade real:
magick compare -metric MAEentre quadros vizinhos em resolução total. 0 = quadro repetido. - Entrega:
scripts/deliver.example.sh(copie e renomeie as placas;V=obrigatório) constrói revelações, loops (loop.sh) e 3D limpo, aumentando crf em 3 até < 95MB.
Upload para o Figma Slides (oculto, sem perturbar a equipe)
O MP4 não entra via MCP: use_figma não tem rede e a Plugin API não cria
vídeo a partir de bytes; upload_assets só aceita imagens (≤ 10MB). Então:
- Capa de cada vídeo:
ffmpeg -ss <t> -i x.mp4 -frames:v 1 -q:v 2 _capas/x.jpg. use_figma(skillsfigma-use+figma-use-slides):createSlideRow(idx)antes do slide final,row.name = 'WIP · Motion (hidden)', umcreateSlide(idx, i)por vídeo comisSkippedSlide = true, fundo preto, um retângulo 1920×1080 chamadoVIDEO · x.mp4e a instrução emspeakerNotes.upload_assetscom osnodeIdsdos retângulos → multipart POST das capas.- O usuário arrasta o MP4 para o retângulo (substitui a capa pelo vídeo). Diga isso claramente; nunca prometa um vídeo inserido.
getSlideGrid() omite slides ocultos: verifique a estrutura via SLIDE_GRID.children[i].children.
Showreel de marca (Remotion): placas + cenas projetadas
Para um showreel de branding (logo, cores, tipografia, layouts, manifesto, mockups), grave apenas
placas limpas do site (sem texto: CSS por cena oculta título/navegação) e componha o resto no
Remotion (render quadro a quadro, sem tremulação). Blocos de construção reutilizáveis que valem a pena ter em
src/fx.tsx: Mist, MistWords, Grain, KenBurns, Plate, uma transição nevoa() (névoa),
com dados da marca em brand.ts.
- A identidade visual vem do deck, não do site: verifique a tipografia oficial e a paleta nos slides (os sites se desviam do manual da marca).
- Cenas que funcionam: frase de abertura na névoa, revelação gravada, construção do logo (
evolvePath+ guias medidas no raster do logo), manifesto frase por frase, pilares com eixowghtvariável, paleta em faixas, tipografia, gráficos em camadas, 3D com título emmix-blend-difference, colunas de layout com scroll, montagem de mockups 1/2/3-up, produto sincronizado com o nome, encerramento no wordmark. - Revise por stills (
npx remotion still ... --frame=N+ montagem) antes de renderizar. - Renderize com
--concurrency=3: em 8, com Photoshop aberto, uma máquina de 32GB ficou sem memória. - Loops:
scripts/loop.sh in out [start] [dur] [crossfade]. Verifique a emenda commagick compare -metric RMSEentre o primeiro e o último quadro (< 0,5% = invisível). - Placa ruim é descarte: um sólido de revolução girando não mostra movimento; uma câmera enquadrada para um canvas retrato parece pequena em 16:9.
- Reel frenético (ritmo aprovado): 25–35s, 60fps,
Seriescom cortes secos em uma grade de 120 BPM (30 quadros), rajadas de imagem a cada 3–6 quadros, recorte enorme de logo sobre uma grade, pesos de fonte piscando, blocos de paleta se reorganizando, retrato em uma máscara circular, rajada gráfica, frase cinética, mockups de um tempo, logo final mantido ~2,5s. Zero rótulos: texto apenas como gráficos. Um showreel lento de 1:40 foi rejeitado. - Reel suave/esfumaçado: grave a fumaça hero sem logo ou foto, preto sobre branco puro
(cena com
storage= params em localStorage eblock: ['assets/logo.png']; o PNG em branco deve ser transparente; não bloqueie o vídeo: um vídeo quebrado congela o timeweb). Use a placa como uma sobreposiçãomix-blend-mode: multiply: abre no início da cena, uma versão invertida (ffmpeg -vf reverse) a fecha; o corte cai no preto. Transição em ~1,1s (taxa 2), ou metade da cena vai para preto. - Montagem de layouts do Figma Slides: clone o slide (oculto), duplique cada cartão 3x com apenas
fundo / logo / texto visível, baixe cada nó com
download_assets(escala 2) e exclua os clones. Slides não exporta transparência: exporte logo/texto com o slide preto e depois branco e extraia o alfa por diferença (ImageMagick). Nunca retorne base64 viause_figma(estoura o contexto).
Showreel de produto (aplicativo real filmado)
Telas são filmadas de verdade (Playwright + CDP screencast a 2x, etapas
goto/move/click/type/scroll/wait, trilha do cursor salva como <id>.cursor.json) e o cursor
é redesenhado no Remotion por cima.
Rejeitado, não repita:
- Colar um reel projetado na tela real (abertura ilustrada + meio filmado + CTA desenhado): duas linguagens, o personagem desaparece e retorna, o cartão preto entre os atos quebra o ritmo. Uma linguagem por vídeo.
- Tela de produto pequena e acelerada em uma janela 16:9: feed e ranking viram uma parede ilegível.
- Zoom que corta a tela e texto carimbado sobre o vídeo (verbo gigante, contador 01/07, gradiente): intrusivo.
- Espera vazia na tela ("aguardando…"): corte direto para a ação.
- Cópia vaga: tem que ser óbvia e na linguagem simples do público.
A forma que funciona: por ação, apenas texto primeiro, centralizado e curto (2 batidas), depois a tela inteira, sem cortes, em uma janela centralizada na proporção de captura, sobre um fundo limpo. Texto discreto (peso 700, entrada palavra por palavra com leve desfoque), sem contador, sem gradiente. Cortes na grade rítmica (14 quadros = 128,57 BPM). Máximo 40s.
Técnica que valeu a pena:
- Acelerar uma captura:
<OffthreadVideo trimBefore={from} playbackRate={rate}>e recalcular os quadros de clique/ rate(som e círculo do cursor). - Iniciar outra composição no meio:
<Sequence from={X}><Sequence from={-start}><Comp/></Sequence></Sequence>. Verifique o primeiro quadro do trecho: um elemento compartilhado "voando" aparece solto. Pule alguns quadros e reposicione o som de impacto que caiu antes. - Idioma por composição com uma timeline em nível de módulo: uma flag
globalThisdefinida por seu próprio arquivo de entrada importado antes do Root. - Faixa contínua entre cortes:
trimBeforena base para que continue de onde o trecho anterior parou. - Loudness: renderize, meça e normalize com 2-pass
loudnorm(-14 LUFS / -1 dBTP), copiando o vídeo. Meça apenas no final: a mixagem bruta do Remotion ultrapassa 0 dBFS. - Auditoria: folha de contato com timestamp (
fps=2,drawtext=%{pts\:hms},tile=8x6) de cada render, e uma folha densa (fps=6) em cada junção de trecho. - Um projeto sem git, editado por outra sessão ao mesmo tempo: novo corte em um novo arquivo,
backup
*.bak-*antes de editar um arquivo compartilhado, e avise o usuário.
Cartões de portfólio e reel de estúdio
record.mjscomKEEP_UI=1(o cartão mostra o site como ele é) e o preset de celular acima.- Cenas geradas a partir de um arquivo de inventário (apenas itens públicos com URL ativa); sem URL ativa = Ken Burns da captura de tela.
- Pacote do cartão: loop crossfade do final ao início, bitrate 2-pass para um limite KB, VP9 WebM junto, poster WebP do quadro 1.
- Logo com alfa para a web: `-
…
[truncado para tradução]