Compte à Rebours Vidéo
Créez un compte à rebours qui se termine exactement quand il le doit. L'ensemble repose sur une seule idée : déduire le temps restant de la trame (ou d'une horodatage monotone), puis formater et animer ce nombre — ne jamais compter à rebours avec un setInterval. Cela couvre les écrans « Démarrage imminent » de livestream, les comptes à rebours de lancement/vente, et les fonds en boucle.
Quand l'utiliser
- Écrans de livestream « Démarrage imminent » / « De retour dans un instant » avec un minuteur de 5 à 10 minutes.
- Comptes à rebours de lancement et de vente flash (dd:hh:mm:ss jusqu'à une date cible ; mm:ss pour l'urgence).
- Tout compteur animé où le nombre doit atteindre zéro exactement à la trame prévue.
La règle d'or : piloter le nombre par l'horloge, jamais par une tique
Un compte à rebours avec setInterval(fn, 1000) dérive — le rappel se déclenche au moins 1000 ms plus tard, jamais exactement, et s'accumule lorsque l'onglet est ralenti ou qu'une trame de rendu est lente. Des rapports de ~1 seconde perdue par minute sont courants. La solution est la même pour une vidéo rendue et une page web en direct : calculer le temps restant comme une fonction pure d'une horloge faisant autorité, de sorte qu'une trame tardive se corrige d'elle-même à la trame suivante au lieu d'accumuler l'erreur.
| Contexte | Horloge faisant autorité | Temps restant |
|---|---|---|
| Rendu Remotion | useCurrentFrame() | total − frame/fps |
| Web (page en direct) | performance.now() ou Date.now() | targetMs − now |
| Vers une date fixe | Date.now() | targetTimestamp − Date.now() |
Ne stockez jamais le temps restant dans un état et ne le décrémentez pas. Recalculez-le à chaque trame.
Compte à rebours précis à la trame (Remotion → MP4)
useCurrentFrame() est déterministe : la trame N est toujours rendue exactement à N/fps secondes. Soustrayez du total et le minuteur est sans dérive par construction.
import { useCurrentFrame, useVideoConfig } from "remotion";
export const Countdown: React.FC<{ durationInSeconds: number }> = ({
durationInSeconds,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// secondes restantes, plafonnées à 0 — ceil pour que « 1 » s'affiche pendant sa seconde entière
const elapsed = frame / fps;
const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed));
return <div style={{ fontVariantNumeric: "tabular-nums" }}>{formatMMSS(remaining)}</div>;
};
durationInSeconds est une propriété, pas une constante — la même composition rend un minuteur de stream de 5 minutes ou un minuteur de vente de 30 secondes en changeant une entrée. Définissez durationInFrames de la composition à durationInSeconds * fps pour que la vidéo soit exactement aussi longue que le compte à rebours.
Minuteur web (page en direct, requestAnimationFrame)
Pour une page en direct, pilotez une boucle rAF à partir de performance.now() et ne repeignez que lorsque la seconde affichée change (pas de travail 60×/s pour un nombre à 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);
Compter à rebours jusqu'à une date fixe utilise la même idée avec Date.now() : Math.max(0, target - Date.now()). Voir references/countdown-cookbook.md pour la version complète avec date cible et gestion du fuseau horaire.
Formatage du temps
Divisez un entier de secondes restantes en champs avec division + modulo, puis ajoutez des zéros non significatifs. Choisissez le format adapté à la durée — n'affichez jamais 00d 00h 05m quand seules les minutes comptent.
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)}`;
};
| Format | Utilisation | Masquer les champs quand |
|---|---|---|
mm:ss | « Démarrage imminent » du stream, minuteurs courts (<1h) | — |
hh:mm:ss | Vente flash, événement le jour même | masquer dd quand 0 |
dd:hh:mm:ss | Lancement / compte à rebours multi-jours | masquer les unités nulles en tête pour plus de lisibilité |
Définissez toujours font-variant-numeric: tabular-nums (ou une police à chiffres monospacés) pour que les chiffres conservent une largeur égale et que la disposition ne vibre pas lorsque les nombres changent.
Transitions de roulement / retournement de chiffres
Un simple changement de nombre semble bon marché ; l'aspect « compteur kilométrique » qui roule donne de la crédibilité au compte à rebours. Rendez chaque chiffre comme une bande verticale de 0 à 9 et translatez-la de -chiffre × hauteurChiffre. Animez la position, pas le contenu textuel, pour que le changement soit une glisse, pas un saut.
.reel { height: 1em; overflow: hidden; }
.reel .col { transition: transform .45s cubic-bezier(.22,1,.36,1); }
/* col contient 0..9 empilés ; décalage de -valeur em */
Dans Remotion, pilotez le décalage de la bande en fonction de la trame au lieu d'une transition CSS (pour qu'il soit déterministe). L'effet de carte à retourner (la moitié supérieure se replie sur la moitié inférieure) et le rouleau par chiffre sont tous deux détaillés, avec des composants exécutables, dans references/digit-flip.md.
Fond en boucle
Les écrans de stream durent des minutes, donc le fond doit boucler de manière transparente. Construisez le mouvement sur un cycle qui se divise de manière égale dans la timeline (par exemple un dégradé ou une dérive de particules dont la période est loopFrames), et lisez-le avec frame % loopFrames pour qu'il n'y ait pas de couture visible à la jonction. Gardez-le à faible contraste et lent — le nombre est le sujet ; le fond est l'ambiance. Recettes dans references/countdown-cookbook.md.
Liste de vérification de la sortie
- Temps restant recalculé à partir de
useCurrentFrame()/performance.now()à chaque trame — pas desetIntervaldécrémental. - Le minuteur atteint
00:00exactement à la trame prévue ;durationInFrames === durationInSeconds × fps. - La durée est une propriété d'entrée, pas une constante codée en dur.
- Les chiffres utilisent
tabular-nums; la disposition ne change pas lorsque les nombres varient. - Le format correspond à la durée (mm:ss vs dd:hh:mm:ss) ; les unités nulles en tête sont supprimées lorsqu'elles distraient.
- La période de boucle du fond divise la timeline de manière égale ; pas de couture à la jonction.
Livrer et vérifier (images fixes rendues → MP4)
Assistant packagé (
scripts/) : disposez vos images fixes avecscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png, puis vérifiez l'encodage avecscripts/probe-mp4.sh out.mp4 [LxH] [fps]. Voirscripts/README.md.
Le compte à rebours est livré comme une composition Remotion (<Composition> + schéma zod + defaultProps, durationInSeconds comme propriété) — le minuteur est une fonction pure de useCurrentFrame(), jamais setInterval / Date.now() / Math.random(). Définissez durationInFrames = durationInSeconds * fps. Livrable = out/*.mp4 + le projet. La sortie verticale 9:16 (1080×1920) est la valeur par défaut.
Boucle de vérification — rendre les images fixes → inspecter → encoder. Tout l'intérêt est la précision à la trame, alors vérifiez la valeur du chiffre à des trames exactes avant d'encoder.
# Images au début / milieu / fin — AVEC LES PROPRIÉTÉS FOURNIES (la durée réelle que vous allez rendre)
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
# Inspectez chaque PNG — les chiffres doivent correspondre EXACTEMENT au calcul frame/fps :
# - à la trame N, affiché = ceil(durationInSeconds - N/fps) (ex. 300s @ 30fps, trame 4500 → 150s → "02:30")
# - la dernière trame affiche 00:00 (ou le maintien de l'état final) ; pas de dérive, pas de décalage d'une seconde
# - tabular-nums : la disposition ne vibre pas entre les trames
# - zone de sécurité 9:16 : le minuteur + le bloc de marque se situent à l'écart du haut ~12% et du bas ~20-35% de l'interface utilisateur et du rail d'action droit
npx remotion render Countdown out/countdown.mp4 --props='{"durationInSeconds":300}' # encoder une fois les chiffres vérifiés
npx remotion render Countdown out/demo.gif --codec=gif # clip de preuve README
Utilisez npx remotion compositions pour confirmer durationInFrames/fps, puis calculez la trame pour n'importe quelle seconde que vous souhaitez vérifier.
Avant de terminer :
- Les images fixes se rendent proprement à la trame 0, au milieu et à la dernière — pas d'erreur.
- La valeur du chiffre à la trame N est exactement
ceil(durationInSeconds - N/fps); la dernière trame atteint 00:00 à la trame prévue (pas de dérive / de décalage d'une seconde). - Le minuteur + la marque restent à l'intérieur de la zone de sécurité 9:16 à chaque trame vérifiée.
- Piloté uniquement par trame — pas de
setInterval/Date.now()/ minuteurs ;tabular-numspour que la disposition tienne. - La propriété
durationInSecondsfournie (pas seulementdefaultProps) s'affiche correctement ; MP4 encodé, GIF optionnel.
Fichiers de référence
references/countdown-cookbook.md— composant<Countdown>Remotion complet et exécutable (durée configurable, mm:ss / dd:hh:mm:ss, maintien de l'état final), composant web de compte à rebours jusqu'à une date fixe avec gestion du fuseau horaire, une mise en page d'écran de stream « Démarrage imminent » (bloc de marque + poignées sociales + fond dégradé en boucle), et le modèle de boucle transparente pour fond.references/digit-flip.md— deux composants déterministes d'animation de chiffres : le rouleau d'odomètre (bande verticale 0–9 pilotée par trame) et l'effet de carte à retourner (pli de la moitié supérieure), ainsi que quand chacun convient pour un lancement, une vente ou un stream.