Countdown Video
Crea una cuenta atrás que termine exactamente cuando debe. Todo el oficio es una idea: derivar el tiempo restante del fotograma (o una marca de tiempo monótona), luego formatear y animar ese número — nunca cuentes atrás con un setInterval. Cubre pantallas de "empezando pronto" en directo, cuentas atrás de lanzamientos/ventas y fondos en bucle.
Cuándo usarlo
- Pantallas de "empezando pronto" / "vuelvo enseguida" para directos con un temporizador de 5–10 min.
- Cuentas atrás de lanzamiento y ventas flash (dd:hh:mm:ss hasta una fecha objetivo; mm:ss para urgencia).
- Cualquier contador animado donde el número deba llegar a cero en el fotograma exacto.
La única regla: obtener el número del reloj, nunca de un tick
Una cuenta atrás con setInterval(fn, 1000) deriva — la llamada de retorno se dispara al menos 1000 ms después, nunca exactamente, y se acumula cuando la pestaña está limitada o un fotograma de renderizado es lento. Son comunes informes de ~1 segundo perdido por minuto. La solución es la misma para video renderizado y web en vivo: calcular el tiempo restante como una función pura de un reloj autoritario, de modo que un fotograma tardío se autocorrija en el siguiente fotograma en lugar de acumular error.
| Contexto | Reloj autoritario | Tiempo restante |
|---|---|---|
| Renderizado Remotion | useCurrentFrame() | total − frame/fps |
| Web (página en vivo) | performance.now() o Date.now() | targetMs − now |
| Hasta una fecha fija | Date.now() | targetTimestamp − Date.now() |
Nunca almacenes el tiempo restante en un estado y lo disminuyas. Recalcúlalo en cada fotograma.
Cuenta atrás precisa por fotograma (Remotion → MP4)
useCurrentFrame() es determinista: el fotograma N siempre se renderiza exactamente en N/fps segundos. Réstalo del total y el temporizador estará libre de deriva por construcción.
import { useCurrentFrame, useVideoConfig } from "remotion";
export const Countdown: React.FC<{ durationInSeconds: number }> = ({
durationInSeconds,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// segundos restantes, fijados en 0 — ceil para que "1" se muestre durante todo su segundo
const elapsed = frame / fps;
const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed));
return <div style={{ fontVariantNumeric: "tabular-nums" }}>{formatMMSS(remaining)}</div>;
};
durationInSeconds es una prop, no una constante — la misma composición renderiza un temporizador de 5 minutos para directo o uno de 30 segundos para una venta cambiando una entrada. Establece durationInFrames de la composición como durationInSeconds * fps para que el video dure exactamente lo mismo que la cuenta atrás.
Temporizador web (página en vivo, requestAnimationFrame)
Para una página en vivo, ejecuta un bucle rAF desde performance.now() y solo vuelve a pintar cuando cambie el segundo mostrado (sin trabajo 60×/seg para un 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 atrás hasta una fecha fija es la misma idea con Date.now(): Math.max(0, target - Date.now()). Consulta references/countdown-cookbook.md para la versión completa con fecha objetivo y manejo de zona horaria.
Formato de tiempo
Descompón un número entero de segundos restantes en campos mediante división + módulo, luego rellena con ceros. Elige el formato que se ajuste a la duración — nunca muestres 00d 00h 05m cuando solo importan los minutos.
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 | Omitir campos cuando |
|---|---|---|
mm:ss | directo "empezando pronto", temporizadores cortos (<1h) | — |
hh:mm:ss | venta flash, evento del mismo día | ocultar dd mientras sea 0 |
dd:hh:mm:ss | lanzamiento / cuenta atrás de varios días | ocultar unidades con cero a la izquierda para una lectura más limpia |
Siempre establece font-variant-numeric: tabular-nums (o una fuente de dígitos monoespaciada) para que los dígitos mantengan el mismo ancho y el diseño no tiemble al cambiar los números.
Transiciones de giro / volteo de dígitos
Un simple cambio de número plano se ve barato; el aspecto de "odómetro" giratorio vende la cuenta atrás. Renderiza cada dígito como una tira vertical del 0 al 9 y desplázala por -digit × digitHeight. Anima la posición, no el contenido del texto, para que el cambio sea un deslizamiento, no un salto.
.reel { height: 1em; overflow: hidden; }
.reel .col { transition: transform .45s cubic-bezier(.22,1,.36,1); }
/* col contiene 0..9 apilados; desplazar por -valor em */
En Remotion, maneja el desplazamiento de la tira basándote en el fotograma en lugar de una transición CSS (para que sea determinista). El estilo de tarjeta giratoria (la mitad superior se pliega sobre la inferior) y el carrete por dígito están detallados, con componentes ejecutables, en references/digit-flip.md.
Fondo en bucle
Las pantallas de directo duran minutos, por lo que el fondo debe repetirse sin problemas. Construye un movimiento en un ciclo que se divida uniformemente en la línea de tiempo (por ejemplo, un gradiente o una deriva de partículas cuyo período sea loopFrames), y léelo como frame % loopFrames para que no haya una costura visible en el borde. Mantenlo de bajo contraste y lento — el número es el sujeto; el fondo es ambiente. Recetas en references/countdown-cookbook.md.
Lista de verificación de salida
- Tiempo restante recalculado desde
useCurrentFrame()/performance.now()en cada fotograma — sin decremento desetInterval. - El temporizador llega a
00:00en el fotograma previsto exacto;durationInFrames === durationInSeconds × fps. - La duración es una prop de entrada, no una constante codificada.
- Los dígitos usan
tabular-nums; el diseño no se desplaza al cambiar los números. - El formato coincide con el intervalo (mm:ss vs dd:hh:mm:ss); las unidades con cero a la izquierda se eliminan cuando distraen.
- El período del bucle de fondo divide la línea de tiempo uniformemente; sin costura en el borde.
Entrega y verificación (imágenes estáticas renderizadas → MP4)
Script empaquetado (
scripts/): coloca tus imágenes en mosaico conscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png, luego verifica la codificación conscripts/probe-mp4.sh out.mp4 [WxH] [fps]. Consultascripts/README.md.
La cuenta atrás se entrega como una composición de Remotion (<Composition> + esquema zod + defaultProps, durationInSeconds como prop) — el temporizador es una función pura de useCurrentFrame(), nunca setInterval / Date.now() / Math.random(). Establece durationInFrames = durationInSeconds * fps. Entregable = out/*.mp4 + el proyecto. 9:16 vertical (1080×1920) es el valor predeterminado.
Bucle de verificación — renderizar imágenes estáticas → inspeccionar → codificar. El punto principal es la precisión de fotograma, así que verifica el valor del dígito en fotogramas exactos antes de codificar.
# Imágenes estáticas al inicio / medio / final — CON LAS PROPS ENVIADAS (la duración real que renderizarás)
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
# Inspecciona cada PNG — los dígitos deben leer EXACTAMENTE la matemática frame/fps:
# - en el fotograma N, mostrado = ceil(durationInSeconds - N/fps) (ej. 300s @ 30fps, fotograma 4500 → 150s → "02:30")
# - el último fotograma lee 00:00 (o el estado final mantenido); sin deriva, sin error de un segundo
# - tabular-nums: el diseño no tiembla entre fotogramas
# - área segura 9:16: el temporizador + bloque de marca se sitúan libres del ~12% superior y ~20-35% inferior de la interfaz y del carril de acción derecho
npx remotion render Countdown out/countdown.mp4 --props='{"durationInSeconds":300}' # codificar una vez que los dígitos verifiquen
npx remotion render Countdown out/demo.gif --codec=gif # clip de prueba README
Usa npx remotion compositions para confirmar durationInFrames/fps, luego calcula el fotograma para cualquier segundo que quieras verificar.
Antes de terminar:
- Las imágenes estáticas se renderizan limpiamente en el fotograma 0, medio y último — sin errores.
- El valor del dígito en el fotograma N es exactamente igual a
ceil(durationInSeconds - N/fps); el último fotograma llega a 00:00 en el fotograma previsto (sin deriva / error de uno). - Temporizador + marca se mantienen dentro del área segura 9:16 en cada fotograma verificado.
- Solo basado en fotogramas — sin
setInterval/Date.now()/ temporizadores;tabular-numspara que el diseño se mantenga. durationInSecondsenviado (no solodefaultProps) se renderiza correctamente; MP4 codificado, GIF opcional.
Archivos de referencia
references/countdown-cookbook.md— componente<Countdown>de Remotion completo y ejecutable (duración configurable, mm:ss / dd:hh:mm:ss, estado final mantenido), el componente web de cuenta atrás hasta una fecha fija con manejo de zona horaria, un diseño de pantalla de directo "empezando pronto" (bloque de marca + enlaces sociales + fondo de gradiente en bucle) y el patrón de fondo de bucle sin costuras.references/digit-flip.md— dos componentes de animación de dígitos deterministas: el carrete de odómetro (tira vertical 0–9 impulsada por fotograma) y el efecto de tarjeta giratoria (pliegue de la mitad superior), además de cuándo cada uno es adecuado para lanzamiento vs venta vs directo.