O que Vídeo de Contagem Regressiva faz?
Faça uma contagem regressiva que termina exatamente quando deveria. Toda a lógica se baseia em uma ideia: derivar o tempo restante do frame (ou de um timestamp monotônico), formatar e animar esse número — nunca contar regressivamente com um setInterval. Abrange telas de "começando em breve" para lives, contagens regressivas para lançamentos e promoções, e fundos com loop contínuo.
Quando usar
- Telas de "Começando em breve" / "Já volto" para lives com timer de 5 a 10 min.
- Contagens regressivas de lançamentos e promoções 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 exatamente no frame correto.
A regra de ouro: derive o número do relógio, nunca de um tick
Um setInterval(fn, 1000) para contagem regressiva desvia — o callback sempre dispara pelo menos 1000 ms depois, nunca exatamente, e se acumula quando a aba é suprimida ou um frame de renderização é lento. Relatos de ~1 segundo perdido por minuto são comuns. A solução é a mesma para vídeo renderizado e web ao vivo: calcular o tempo restante como função pura de um relógio autoritativo, para que um frame atrasado se autocorrija no próximo frame, em vez de acumular erro.
| Contexto | Relógio autoritativo | Tempo restante |
|---|---|---|
| Renderização Remotion | useCurrentFrame() | total − frame/fps |
| Web (página ao vivo) | performance.now() ou Date.now() | targetMs − now |
| Para uma data fixa | Date.now() | targetTimestamp − Date.now() |
Nunca armazene o tempo restante em estado e o decremente. Recalcule a cada frame.
Contagem regressiva precisa por frame (Remotion → MP4)
useCurrentFrame() é determinística: o frame N é sempre renderizado em exatamente N/fps segundos. Subtraia do total e o timer estará 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 — usa ceil para que "1" mostre durante seu segundo completo
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 timer de 5 minutos para live ou um timer de 30 segundos para promoção, alterando apenas uma entrada. Defina durationInFrames da composição como durationInSeconds * fps para que o vídeo tenha exatamente a duração da contagem regressiva.
Timer web (página ao vivo, requestAnimationFrame)
Para uma página ao vivo, use um loop rAF baseado em 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 regressivamente até uma data fixa é 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 | live "começando em breve", timers curtos (<1h) | — |
hh:mm:ss | promoção relâmpago, evento no mesmo dia | ocultar dd enquanto 0 |
dd:hh:mm:ss | lançamento / contagem regressiva de vários dias | ocultar unidades com zeros à esquerda para leitura mais limpa |
Sempre defina font-variant-numeric: tabular-nums (ou uma fonte de dígitos monoespaçada) para que os dígitos tenham largura igual e o layout não oscile conforme os números mudam.
Transições de dígito (rolagem / flip)
Uma simples troca de número parece barata; o visual de odômetro girando valoriza a contagem regressiva. Renderize cada dígito como uma faixa vertical de 0 a 9 e a desloque por -dígito × alturaDí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 contém 0..9 empilhados; desloca por -valor em */
No Remotion, direcione o deslocamento da faixa a partir do frame, em vez de uma transição CSS (para que seja determinístico). O estilo flip-card (metade superior dobra sobre a metade inferior) e o carretel por dígito são detalhados, com componentes executáveis, em references/digit-flip.md.
Fundo com loop
Telas de live duram minutos, então o fundo precisa se repetir perfeitamente. Construa o movimento em um ciclo que divida uniformemente a timeline (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 corte. Mantenha baixo contraste e lento — o número é o assunto; o fundo é ambiente. Receitas em references/countdown-cookbook.md.
Checklist de saída
- Tempo restante recalculado de
useCurrentFrame()/performance.now()a cada frame — sem decremento porsetInterval. - Timer chega a
00:00exatamente no frame pretendido;durationInFrames === durationInSeconds × fps. - Duração é uma prop de entrada, não uma constante fixa.
- Dígitos usam
tabular-nums; layout não se desloca conforme os números mudam. - Formato corresponde ao intervalo (mm:ss vs dd:hh:mm:ss); unidades com zeros à esquerda são removidas quando distraem.
- Período do loop do fundo divide a timeline uniformemente; sem emenda no corte.
Entregar e verificar (stills renderizados → MP4)
Script auxiliar (
scripts/): monte seus stills 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 [WxH] [fps]. Vejascripts/README.md.
A contagem regressiva é entregue como uma composição Remotion (<Composition> + esquema zod + defaultProps, durationInSeconds como prop) — o timer é uma função pura de useCurrentFrame(), nunca setInterval / Date.now() / Math.random(). Defina durationInFrames = durationInSeconds * fps. Entregável = out/*.mp4 + o projeto. 9:16 vertical (1080×1920) é o padrão.
Verificação em loop — renderizar stills → inspecionar → codificar. O objetivo é precisão de frame, então verifique o valor do dígito em frames exatos antes de codificar.
# Stills no início / meio / fim — COM AS PROPS ENVIADAS (a duração real que será renderizada)
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 ler EXATAMENTE a matemática frame/fps:
# - no frame N, exibido = ceil(durationInSeconds - N/fps) (ex.: 300s @ 30fps, frame 4500 → 150s → "02:30")
# - último frame mostra 00:00 (ou o estado final); sem desvio, sem erro de um segundo
# - tabular-nums: layout não oscila entre frames
# - área segura 9:16: o timer + bloco de marca ficam limpos de ~12% superior e ~20-35% inferior da UI e da barra de ações direita
npx remotion render Countdown out/countdown.mp4 --props='{"durationInSeconds":300}' # codifica assim que os dígitos verificarem
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 frame para qualquer segundo que queira verificar.
Antes de finalizar:
- Stills renderizam limpos no frame 0, meio e último — sem erros.
- O valor do dígito no frame N é exatamente
ceil(durationInSeconds - N/fps); último frame chega a 00:00 no frame pretendido (sem desvio / erro de um segundo). - Timer + marca permanecem dentro da área segura 9:16 em todos os frames verificados.
- Apenas orientado por frame — sem
setInterval/Date.now()/ timers;tabular-numspara manter o layout. durationInSecondsenviado (não apenasdefaultProps) renderiza corretamente; MP4 codificado, GIF opcional.
Arquivos de referência
references/countdown-cookbook.md— componente Remotion<Countdown>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 com loop) e padrão de fundo com loop contínuo.references/digit-flip.md— dois componentes determinísticos de animação de dígitos: o carretel de odômetro (faixa vertical 0–9 orientada por frame) e o efeito flip-card (dobra da metade superior), além de quando cada um se adequa a lançamento vs promoção vs live.