Countdown-Video
Erstelle einen Countdown, der genau dann endet, wenn er soll. Das ganze Handwerk basiert auf einer Idee: Leite die verbleibende Zeit vom Frame (oder einem monotonen Zeitstempel) ab, formatiere und animiere diese Zahl – zähle niemals mit einem setInterval herunter. Deckt Livestream-„Starting Soon“-Bildschirme, Launch-/Sale-Countdowns und loopende Hintergründe ab.
Wann verwenden
- „Starting soon“ / „Be right back“ Livestream-Bildschirme mit einem 5–10 Minuten-Timer.
- Launch- und Flash-Sale-Countdowns (
dd:hh:mm:ssbis zu einem Zieldatum;mm:ssfür Dringlichkeit). - Jeder animierte Zähler, bei dem die Zahl auf dem exakten Frame auf Null landen muss.
Die eine Regel: Treibe die Zahl von der Uhr, niemals von einem Tick
Ein setInterval(fn, 1000) Countdown driftet – der Callback feuert mindestens 1000 ms später, niemals exakt, und staut sich auf, wenn der Tab gedrosselt wird oder ein Render-Frame langsam ist. Berichte über ~1 Sekunde Verlust pro Minute sind üblich. Die Lösung ist für gerendertes Video und Live-Web dieselbe: Berechne die verbleibende Zeit als reine Funktion einer autoritativen Uhr, sodass ein später Frame im nächsten Frame selbst korrigiert wird, anstatt Fehler zu akkumulieren.
| Kontext | Autoritative Uhr | Verbleibende Zeit |
|---|---|---|
| Remotion-Render | useCurrentFrame() | total − frame/fps |
| Web (Live-Seite) | performance.now() oder Date.now() | targetMs − now |
| Bis zu einem festen Datum | Date.now() | targetTimestamp − Date.now() |
Speichere die verbleibende Zeit niemals im State und dekrementiere sie nicht. Berechne sie in jedem Frame neu.
Frame-genauer Countdown (Remotion → MP4)
useCurrentFrame() ist deterministisch: Frame N wird immer zu exakt N/fps Sekunden gerendert. Subtrahiere von der Gesamtzeit, und der Timer ist per Konstruktion driftfrei.
import { useCurrentFrame, useVideoConfig } from "remotion";
export const Countdown: React.FC<{ durationInSeconds: number }> = ({
durationInSeconds,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// verbleibende Sekunden, bei 0 gedeckelt – ceil, damit "1" für die volle Sekunde angezeigt wird
const elapsed = frame / fps;
const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed));
return <div style={{ fontVariantNumeric: "tabular-nums" }}>{formatMMSS(remaining)}</div>;
};
durationInSeconds ist eine Prop, keine Konstante – dieselbe Komposition rendert einen 5-Minuten-Stream-Timer oder einen 30-Sekunden-Sale-Timer durch Änderung einer Eingabe. Setze durationInFrames der Komposition auf durationInSeconds * fps, damit das Video exakt so lang ist wie der Countdown.
Web-Timer (Live-Seite, requestAnimationFrame)
Für eine Live-Seite treibe eine rAF-Schleife mit performance.now() an und male nur neu, wenn sich die angezeigte Sekunde ändert (keine Arbeit 60×/Sek. für eine 1-Hz-Zahl):
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);
Das Herunterzählen bis zu einem festen Datum funktioniert mit Date.now() nach dem gleichen Prinzip: Math.max(0, target - Date.now()). Siehe references/countdown-cookbook.md für die vollständige Datumsziel-Version mit Zeitzonenbehandlung.
Zeitformatierung
Zerlege eine Ganzzahl der verbleibenden Sekunden mit Division + Modulo in Felder und fülle sie mit Nullen auf. Wähle das Format passend zur Dauer – zeige niemals 00d 00h 05m an, wenn nur Minuten wichtig sind.
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 | Verwendung | Felder ausblenden, wenn |
|---|---|---|
mm:ss | Stream „starting soon“, kurze Timer (<1h) | — |
hh:mm:ss | Flash-Sale, Event am selben Tag | dd ausblenden, solange 0 |
dd:hh:mm:ss | Launch / mehrtägiger Countdown | Führende Null-Einheiten für sauberere Lesbarkeit ausblenden |
Setze immer font-variant-numeric: tabular-nums (oder eine monospace Ziffernschriftart), sodass Ziffern gleich breit bleiben und das Layout nicht wackelt, wenn sich die Zahlen ändern.
Ziffern-Roll- / Flip-Übergänge
Ein einfacher Austausch der Zahl wirkt billig; der rollende „Kilometerzähler“-Look verkauft den Countdown. Rendere jede Ziffer als vertikalen Streifen 0–9 und verschiebe ihn um -digit × digitHeight. Animiere die Position, nicht den Textinhalt, sodass die Änderung ein Gleiten und kein Aufpoppen ist.
.reel { height: 1em; overflow: hidden; }
.reel .col { transition: transform .45s cubic-bezier(.22,1,.36,1); }
/* col enthält gestapelte 0..9; Verschiebung um -Wert em */
In Remotion treibe den Streifen-Offset auf Basis des Frames anstelle eines CSS-Übergangs (damit es deterministisch ist). Der Flip-Card-Stil (obere Hälfte klappt über die untere) und die pro-Ziffer-Rolle sind beide detailliert mit ausführbaren Komponenten in references/digit-flip.md beschrieben.
Looping-Hintergrund
Stream-Bildschirme laufen Minuten lang, daher muss der Hintergrund nahtlos loopen. Baue Bewegung auf einem Zyklus, der gleichmäßig in die Zeitachse passt (z. B. ein Farbverlauf oder Partikeldrift, dessen Periode loopFrames ist), und lies ihn als frame % loopFrames, damit an der Nahtstelle kein sichtbarer Übergang entsteht. Halte ihn kontrastarm und langsam – die Zahl ist das Subjekt; der Hintergrund ist Atmosphäre. Rezepte in references/countdown-cookbook.md.
Ausgabe-Checkliste
- Verbleibende Zeit wird in jedem Frame aus
useCurrentFrame()/performance.now()neu berechnet – keinsetInterval-Dekrement. - Timer erreicht
00:00auf dem exakt vorgesehenen Frame;durationInFrames === durationInSeconds × fps. - Dauer ist eine Eingabe-Prop, kein hartcodierter Wert.
- Ziffern verwenden
tabular-nums; Layout verschiebt sich nicht, wenn sich Zahlen ändern. - Format passt zur Spanne (
mm:ssvs.dd:hh:mm:ss); führende Null-Einheiten werden weggelassen, wenn sie ablenken. - Die Hintergrund-Loop-Periode teilt die Zeitachse gleichmäßig; kein sichtbarer Übergang an der Nahtstelle.
Liefern & verifizieren (gerenderte Standbilder → MP4)
Verpacktes Hilfsprogramm (
scripts/): Lege deine Standbilder mitscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.pngals Tafel an und überprüfe dann die Kodierung mitscripts/probe-mp4.sh out.mp4 [WxH] [fps]. Siehescripts/README.md.
Der Countdown wird als Remotion-Komposition ausgeliefert (<Composition> + Zod schema + defaultProps, durationInSeconds eine Prop) – der Timer ist eine reine Funktion von useCurrentFrame(), niemals setInterval / Date.now() / Math.random(). Setze durationInFrames = durationInSeconds * fps. Liefergegenstand = out/*.mp4 + das Projekt. 9:16 Hochformat (1080×1920) ist die Voreinstellung.
Verifikations-Loop – Standbilder rendern → prüfen → kodieren. Der ganze Sinn ist Framegenauigkeit, also überprüfe den Ziffernwert bei exakten Frames vor dem Kodieren.
# Standbilder am Start / Mitte / Ende – MIT AUSGELIEFERTEN PROPS (die echte Dauer, die du rendern wirst)
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
# Überprüfe jedes PNG – die Ziffern müssen EXAKT der frame/fps-Mathematik entsprechen:
# - bei Frame N, angezeigt = ceil(durationInSeconds - N/fps) (z. B. 300s @ 30fps, Frame 4500 → 150s → "02:30")
# - Letzter Frame zeigt 00:00 (oder den Endzustand); kein Drift, kein Off-by-One-Sekunden-Fehler
# - tabular-nums: Layout wackelt nicht zwischen Frames
# - 9:16-Safe-Area: Timer + Markenblock liegen klar außerhalb der oberen ~12% und unteren ~20-35% UI und der rechten Aktionsleiste
npx remotion render Countdown out/countdown.mp4 --props='{"durationInSeconds":300}' # kodieren, sobald Ziffern verifiziert
npx remotion render Countdown out/demo.gif --codec=gif # README-Beweisclip
Verwende npx remotion compositions, um durationInFrames/fps zu bestätigen, und berechne dann den Frame für jede Sekunde, die du überprüfen möchtest.
Bevor du abschließt:
- Standbilder rendern sauber bei Frame 0, Mitte und am Ende – keine Fehler.
- Der Ziffernwert bei Frame N entspricht exakt
ceil(durationInSeconds - N/fps); letzter Frame erreicht 00:00 auf dem vorgesehenen Frame (kein Drift / Off-by-One). - Timer + Marke bleiben bei jedem geprüften Frame innerhalb der 9:16-Safe-Area.
- Nur Frame-getrieben – kein
setInterval/Date.now()/ Timer;tabular-nums, damit Layout stabil bleibt. - Ausgelieferte
durationInSeconds(nicht nurdefaultProps) rendert korrekt; MP4 kodiert, GIF optional.
Referenzdateien
references/countdown-cookbook.md– Vollständige ausführbare Remotion<Countdown>(konfigurierbare Dauer, mm:ss / dd:hh:mm:ss, Endzustand-Halten), die Countdown-bis-zu-einem-festen-Datum Web-Komponente mit Zeitzonenbehandlung, ein „Starting Soon“-Stream-Bildschirm-Layout (Markenblock + Social-Media-Handles + loopender Farbverlaufshintergrund) und das nahtlos loopende Hintergrundmuster.references/digit-flip.md– Zwei deterministische Ziffern-Animation-Komponenten: die Kilometerzähler-Rolle (vertikaler 0–9 Streifen, vom Frame getrieben) und der Flip-Card-Effekt (obere Hälfte klappt um), sowie wann welche für Launch vs. Sale vs. Stream geeignet ist.