Vídeo de Contagem Regressiva
Crie uma contagem regressiva que termina exatamente no momento certo. Toda a técnica se baseia em uma ideia: derivar o tempo restante a partir do quadro (ou de um timestamp monotônico), formatar e animar esse número — nunca faça contagem com setInterval. Abrange telas de "começando em breve" para lives, contagens regressivas de lançamentos/vendas e fundos em loop.
Quando usar
- Telas de "começando em breve" / "volto já" para lives com cronômetro de 5 a 10 minutos.
- Contagens regressivas de lançamentos e vendas relâmpago (dd:hh:mm:ss até uma data alvo; mm:ss para urgência).
- Qualquer contador animado onde o número deve chegar a zero no quadro exato.
A única regra: derive o número do relógio, nunca de um tick
Uma contagem com setInterval(fn, 1000) sofre desvio — o callback é disparado no mínimo 1000 ms depois, nunca exato, e acumula atrasos quando a aba é limitada ou um quadro de renderização está lento. Relatos de ~1 segundo perdido por minuto são comuns. A solução é a mesma para vídeo renderizado e web ao vivo: compute o tempo restante como uma função pura de um relógio autoritário, de modo que um quadro atrasado se autocorrija no próximo quadro, em vez de acumular erro.
| Contexto | Relógio autoritário | Tempo restante |
|---|---|---|
| Renderização Remotion | useCurrentFrame() | total − frame/fps |
| Web (página ao vivo) | performance.now() ou Date.now() | targetMs − now |
| Até uma data fixa | Date.now() | targetTimestamp − Date.now() |
Nunca armazene o tempo restante no estado e o decremente. Recalcule-o a cada quadro.
Contagem regressiva precisa por quadro (Remotion → MP4)
useCurrentFrame() é determinística: o quadro N é sempre renderizado exatamente em N/fps segundos. Subtraia do total e o cronômetro fica livre de desvios por construção.
import { useCurrentFrame, useVideoConfig } from "remotion";
export const Countdown: React.FC<{ durationInSeconds: number }> = ({
durationInSeconds,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// segundos restantes, limitados a 0 — ceil para que "1" apareça por seu segundo inteiro
const elapsed = frame / fps;
const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed));
return <div style={{ fontVariantNumeric: "tabular-nums" }}>{formatMMSS(remaining)}</div>;
};
durationInSeconds é uma prop, não uma constante — a mesma composição renderiza um cronômetro de 5 minutos para live ou de 30 segundos para venda apenas alterando uma entrada. Defina durationInFrames da composição como durationInSeconds * fps para que o vídeo tenha exatamente a duração da contagem.
Temporizador web (página ao vivo, requestAnimationFrame)
Para uma página ao vivo, controle um loop rAF com performance.now() e só repinte quando o segundo exibido mudar (sem trabalho 60×/s para um número de 1 Hz):
const start = performance.now();
let lastShown = -1;
function tick(now) {
const remaining = Math.max(0, Math.ceil(duration - (now - start) / 1000));
if (remaining !== lastShown) { render(remaining); lastShown = remaining; }
if (remaining > 0) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
Contar até uma data fixa segue a mesma ideia com Date.now(): Math.max(0, target - Date.now()). Veja references/countdown-cookbook.md para a versão completa com data alvo e tratamento de fuso horário.
Formatação de tempo
Converta um inteiro de segundos restantes em campos usando divisão + módulo, depois preencha com zeros. Escolha o formato adequado à duração — nunca mostre 00d 00h 05m quando minutos é tudo que importa.
const pad = (n) => String(n).padStart(2, "0");
const formatMMSS = (s) => `${pad(Math.floor(s / 60))}:${pad(s % 60)}`;
const formatDHMS = (s) => {
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60), sec = s % 60;
return `${pad(d)}:${pad(h)}:${pad(m)}:${pad(sec)}`;
};
| Formato | Uso | Ocultar campos quando |
|---|---|---|
mm:ss | "começando em breve", temporizadores curtos (<1h) | — |
hh:mm:ss | venda relâmpago, evento no mesmo dia | ocultar dd enquanto for 0 |
dd:hh:mm:ss | lançamento / contagem de vários dias | ocultar unidades líderes zero para leitura mais limpa |
Sempre defina font-variant-numeric: tabular-nums (ou uma fonte de dígitos monoespaçada) para que os dígitos mantenham largura igual e o layout não trema quando os números mudarem.
Transições de rolagem / virada de dígitos
Uma simples troca de número parece barata; o visual de "odômetro" rolante valoriza a contagem. Renderize cada dígito como uma faixa vertical de 0 a 9 e desloque-a por -dígito × alturaDoDígito. Anime a posição, não o conteúdo do texto, para que a mudança seja um deslize, não um pulo.
.reel { height: 1em; overflow: hidden; }
.reel .col { transition: transform .45s cubic-bezier(.22,1,.36,1); }
/* col mantém 0..9 empilhados; desloca por -valor em */
No Remotion, controle o deslocamento da faixa pelo quadro em vez de uma transição CSS (para que seja determinístico). O estilo de carta virada (metade superior dobra sobre a inferior) e o rolo por dígito são detalhados, com componentes executáveis, em references/digit-flip.md.
Fundo em loop
Telas de live duram minutos, então o fundo deve fazer loop sem emendas. Construa o movimento em um ciclo que se divida uniformemente na linha do tempo (por exemplo, um gradiente ou deriva de partículas cujo período seja loopFrames) e leia como frame % loopFrames para que não haja emenda visível no final. Mantenha baixo contraste e lento — o número é o assunto; o fundo é ambiente. Receitas em references/countdown-cookbook.md.
Lista de verificação de saída
- Tempo restante recalculado a partir de
useCurrentFrame()/performance.now()a cada quadro — sem decremento comsetInterval. - O cronômetro chega a
00:00no quadro pretendido;durationInFrames === durationInSeconds × fps. - A duração é uma prop de entrada, não uma constante codificada.
- Dígitos usam
tabular-nums; o layout não se desloca conforme os números mudam. - O formato corresponde ao intervalo (mm:ss vs dd:hh:mm:ss); unidades líderes zero são omitidas quando atrapalham.
- O período do loop do fundo divide a linha do tempo uniformemente; sem emenda no final.
Entregar e verificar (imagens estáticas renderizadas → MP4)
Ajudante empacotado (
scripts/): monte suas imagens comscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png, depois verifique a codificação comscripts/probe-mp4.sh out.mp4 [LxA] [fps]. Vejascripts/README.md.
A contagem regressiva é fornecida como uma composição Remotion (<Composition> + zod schema + defaultProps, durationInSeconds como prop) — o cronômetro é uma função pura de useCurrentFrame(), nunca setInterval / Date.now() / Math.random(). Defina durationInFrames = durationInSeconds * fps. Entregável = out/*.mp4 + o projeto. O padrão é vertical 9:16 (1080×1920).
Verifique em loop — renderize imagens → inspecione → codifique. Todo o objetivo é a precisão de quadro, então verifique o valor do dígito em quadros exatos antes de codificar.
# Imagens no início / meio / fim — COM AS PROPS FORNECIDAS (a duração real que você renderizará)
npx remotion still Countdown out/f-start.png --frame=0 --props='{"durationInSeconds":300}'
npx remotion still Countdown out/f-mid.png --frame=N --props='{"durationInSeconds":300}'
npx remotion still Countdown out/f-end.png --frame=L --props='{"durationInSeconds":300}' # L = durationInFrames-1
# Inspecione cada PNG — os dígitos devem corresponder EXATAMENTE à matemática quadro/fps:
# - no quadro N, exibido = ceil(durationInSeconds - N/fps) (ex.: 300s @ 30fps, quadro 4500 → 150s → "02:30")
# - o último quadro mostra 00:00 (ou o estado final); sem desvio, sem erro de um segundo
# - tabular-nums: layout não treme entre quadros
# - área segura 9:16: o cronômetro + bloco de marca ficam livres dos ~12% superiores e dos ~20-35% inferiores da UI e da barra de ação direita
npx remotion render Countdown out/countdown.mp4 --props='{"durationInSeconds":300}' # codifique assim que os dígitos forem verificados
npx remotion render Countdown out/demo.gif --codec=gif # clipe de prova para README
Use npx remotion compositions para confirmar durationInFrames/fps, depois calcule o quadro de qualquer segundo que queira verificar.
Antes de finalizar:
- As imagens estáticas renderizam corretamente no quadro 0, meio e fim — sem erros.
- O valor do dígito no quadro N é exatamente
ceil(durationInSeconds - N/fps); o último quadro chega a 00:00 no quadro pretendido (sem desvio / erro de um). - Cronômetro + marca permanecem dentro da área segura 9:16 em todos os quadros verificados.
- Apenas orientado por quadro — sem
setInterval/Date.now()/ temporizadores;tabular-numspara manter o layout. - O
durationInSecondsfornecido (não apenasdefaultProps) renderiza corretamente; MP4 codificado, GIF opcional.
Arquivos de referência
references/countdown-cookbook.md— componente<Countdown>Remotion completo e executável (duração configurável, mm:ss / dd:hh:mm:ss, estado final), componente web de contagem até data fixa com tratamento de fuso horário, layout de tela de live "começando em breve" (bloco de marca + redes sociais + fundo gradiente em loop) e padrão de fundo contínuo em loop.references/digit-flip.md— dois componentes determinísticos de animação de dígitos: o rolo de odômetro (faixa vertical de 0 a 9 controlada por quadro) e o efeito de carta virada (dobra da metade superior), além de quando cada um é adequado para lançamento, venda ou live.