Video de Cuenta Regresiva
Haz una cuenta regresiva que termine exactamente cuando debe. Toda la técnica se basa en una idea: derivar el tiempo restante del fotograma (o de una marca de tiempo monótona), luego formatear y animar ese número — nunca contar hacia atrás con un setInterval. Cubre pantallas de "próximamente" para transmisiones en vivo, cuentas regresivas de lanzamientos/ventas y fondos en bucle.
Cuándo usarlo
- Pantallas de transmisión en vivo de "Próximamente" / "Vuelvo enseguida" con un temporizador de 5 a 10 minutos.
- Cuentas regresivas de lanzamientos 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 regla de oro: obtener el número del reloj, nunca de un tick
Una cuenta regresiva con setInterval(fn, 1000) se desvía — la función de retrollamada se ejecuta al menos 1000 ms después, nunca exactamente, y se acumula cuando la pestaña se estrangula o un fotograma de renderizado es lento. Son comunes los 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 autoritativo, de modo que un fotograma tardío se autocorrija en el siguiente fotograma en lugar de acumular errores.
| Contexto | Reloj autoritativo | 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. Vuelve a calcularlo en cada fotograma.
Cuenta regresiva precisa por fotograma (Remotion → MP4)
useCurrentFrame() es determinista: el fotograma N siempre se renderiza exactamente en N/fps segundos. Resta del total y el temporizador no tendrá desviación por construcción.
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" se muestre durante su 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 es una propiedad, no una constante — la misma composición renderiza un temporizador de transmisión de 5 minutos o un temporizador de venta de 30 segundos cambiando una entrada. Establece el durationInFrames de la composición en durationInSeconds * fps para que el video tenga exactamente la duración de la cuenta regresiva.
Temporizador web (página en vivo, requestAnimationFrame)
Para una página en vivo, maneja un bucle rAF desde performance.now() y solo vuelve a pintar cuando el segundo mostrado cambia (sin trabajo 60 veces/segundo 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 hacia 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.
Formateo de tiempo
Descompón un entero de segundos restantes en campos con división + módulo, luego rellena con ceros a la izquierda. Elige el formato que se ajuste a la duración — nunca muestres 00d 00h 05m cuando los minutos son lo único 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 cuando |
|---|---|---|
mm:ss | transmisión "próximamente", temporizadores cortos (<1h) | — |
hh:mm:ss | venta flash, evento del mismo día | ocultar dd mientras sea 0 |
dd:hh:mm:ss | lanzamiento / cuenta regresiva de varios días | ocultar unidades iniciales cero para una lectura más limpia |
Establece siempre 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 cuando cambien los números.
Transiciones de giro / volteo de dígitos
Un simple intercambio de números se ve económico; el aspecto de "odómetro" giratorio vende la cuenta regresiva. 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 -value em */
En Remotion, maneja el desplazamiento de la tira desde 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 se detallan, con componentes ejecutables, en references/digit-flip.md.
Fondo en bucle
Las pantallas de transmisión duran minutos, por lo que el fondo debe repetirse sin problemas. Construye el 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
- El tiempo restante se recalcula a partir de
useCurrentFrame()/performance.now()en cada fotograma — sin decremento desetInterval. - El temporizador llega a
00:00en el fotograma exacto previsto;durationInFrames === durationInSeconds × fps. - La duración es una propiedad de entrada, no una constante codificada.
- Los dígitos usan
tabular-nums; el diseño no se desplaza cuando cambian los números. - El formato coincide con el lapso (mm:ss vs dd:hh:mm:ss); las unidades iniciales cero se eliminan cuando distraen.
- El período del bucle de fondo divide la línea de tiempo uniformemente; sin costura en el borde.
Entregar y verificar (fotogramas renderizados → MP4)
Asistente empaquetado (
scripts/): organiza tus fotogramas 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 regresiva se entrega como una composición de Remotion (<Composition> + schema zod + defaultProps, durationInSeconds como propiedad) — el temporizador es una función pura de useCurrentFrame(), nunca setInterval / Date.now() / Math.random(). Establece durationInFrames = durationInSeconds * fps. Entregable = out/*.mp4 + el proyecto. El valor predeterminado es vertical 9:16 (1080×1920).
Bucle de verificación — renderizar fotogramas → inspeccionar → codificar. El objetivo es la precisión de fotograma, así que verifica el valor del dígito en fotogramas exactos antes de codificar.
# Fotogramas al inicio / medio / final — CON LAS PROPIEDADES ENTREGADAS (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, frame 4500 → 150s → "02:30")
# - el último fotograma muestra 00:00 (o la retención del estado final); sin desviación, sin error de un segundo
# - tabular-nums: el diseño no tiembla entre fotogramas
# - área segura 9:16: el temporizador + el bloque de marca se mantienen alejados del ~12% superior, ~20-35% inferior y el carril de acción derecho
npx remotion render Countdown out/countdown.mp4 --props='{"durationInSeconds":300}' # codificar una vez que los dígitos se 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:
- Los fotogramas se renderizan limpios en el fotograma 0, medio y final — sin errores.
- El valor del dígito en el fotograma N es exactamente
ceil(durationInSeconds - N/fps); el último fotograma llega a 00:00 en el fotograma previsto (sin desviación / error de uno). - El temporizador + la marca permanecen dentro del área segura 9:16 en cada fotograma verificado.
- Solo impulsado por fotogramas — sin
setInterval/Date.now()/ temporizadores;tabular-numspara que el diseño se mantenga. - El
durationInSecondsenviado (no solodefaultProps) se renderiza correctamente; MP4 codificado, GIF opcional.
Archivos de referencia
references/countdown-cookbook.md— componente<Countdown>Remotion completo y ejecutable (duración configurable, mm:ss / dd:hh:mm:ss, retención del estado final), el componente web de cuenta regresiva hasta una fecha fija con manejo de zona horaria, un diseño de pantalla de transmisión "próximamente" (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), y cuándo cada uno se adapta a lanzamiento vs venta vs transmisión.