Was macht Countdown-Video?
Erstelle einen Countdown, der exakt dann endet, wenn er soll. Das gesamte Konzept basiert auf einer einzigen Idee: Leite die verbleibende Zeit aus dem Frame (oder einem monotonen Zeitstempel) ab, formatiere sie und animiere diese Zahl – zähle niemals mit einem setInterval herunter. Deckt „Startet bald“-Bildschirme für Livestreams, Launch-/Sale-Countdowns und sich wiederholende Hintergründe ab.
Wann verwenden
- „Startet bald“ / „Bin gleich zurück“-Livestream-Bildschirme mit einem 5–10 Min. Timer.
- Launch- und Flash-Sale-Countdowns (dd:hh:mm:ss bis zu einem Zieltermin; mm:ss für Dringlichkeit).
- Jeder animierte Zähler, bei dem die Zahl exakt auf dem richtigen Frame Null erreichen muss.
Die eine Regel: Die Zahl von der Uhr ableiten, niemals von einem Tick
Ein setInterval(fn, 1000)-Countdown driftet – der Callback feuert mindestens 1000 ms später, nie exakt, und staut sich auf, wenn der Tab gedrosselt wird oder ein Render-Frame langsam ist. Berichte über etwa 1 Sekunde Verlust pro Minute sind üblich. Die Lösung ist für gerendertes Video und Live-Web gleich: Berechne die verbleibende Zeit als reine Funktion einer autoritativen Uhr, sodass ein später Frame sich im nächsten Frame selbst korrigiert, anstatt Fehler zu akkumulieren.
| Kontext | Authoritative Uhr | Verbleibende Zeit |
|---|---|---|
| Remotion-Render | useCurrentFrame() | gesamt − frame/fps |
| Web (Live-Seite) | performance.now() oder Date.now() | zielMs − jetzt |
| Bis zu einem festen Datum | Date.now() | zielTimestamp − Date.now() |
Speichere die verbleibende Zeit niemals im State und dekrementiere sie. Berechne sie in jedem Frame neu.
Frame-genauer Countdown (Remotion → MP4)
useCurrentFrame() ist deterministisch: Frame N wird immer exakt bei N/fps Sekunden gerendert. Ziehe das von der Gesamtzeit ab und der Timer ist konstruktionsbedingt driftfrei.
import { useCurrentFrame, useVideoConfig } from "remotion";
export const Countdown: React.FC<{ durationInSeconds: number }> = ({
durationInSeconds,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// verbleibende Sekunden, bei 0 geklammert – ceil, damit "1" für die gesamte 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, indem ein Eingabewert geändert wird. Setze durationInFrames der Komposition auf durationInSeconds * fps, damit das Video genau so lang ist wie der Countdown.
Web-Timer (Live-Seite, requestAnimationFrame)
Treibe für eine Live-Seite eine rAF-Schleife mit performance.now() an und male nur neu, wenn sich die angezeigte Sekunde ändert (keine 60×/s Arbeit 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 auf ein festes 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 einen Integer der verbleibenden Sekunden mit Division + Modulo in Felder und fülle mit Nullen auf. Wähle das Format passend zur Dauer – zeige niemals 00d 00h 05m an, wenn nur Minuten zählen.
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 „Startet bald“, kurze Timer (<1h) | — |
hh:mm:ss | Flash-Sale, gleichtägiges Event | dd ausblenden, solange 0 |
dd:hh:mm:ss | Launch / mehrtägiger Countdown | Führende Null-Einheiten für sauberere Darstellung ausblenden |
Setze immer font-variant-numeric: tabular-nums (oder eine monospaced Ziffernschrift), damit die Ziffern gleich breit bleiben und das Layout beim Zahlenwechsel nicht springt.
Ziffern-Roll-/Flip-Übergänge
Ein einfacher Zahlenwechsel wirkt billig; der rollende „Kilometerzähler“-Look verkauft den Countdown. Rendere jede Ziffer als vertikalen Streifen 0–9 und verschiebe ihn um -Ziffer × ZifferHöhe. Animiere die Position, nicht den Textinhalt, sodass die Änderung ein Gleiten ist, kein Sprung.
.reel { height: 1em; overflow: hidden; }
.reel .col { transition: transform .45s cubic-bezier(.22,1,.36,1); }
/* col enthält gestapelte 0..9; um -Wert em verschieben */
In Remotion treibe den Strip-Offset über den Frame anstelle eines CSS-Übergangs (damit er deterministisch ist). Der Flip-Card-Stil (obere Hälfte klappt über die untere) und die Ziffern-Rolle werden detailliert mit lauffähigen Komponenten in references/digit-flip.md beschrieben.
Hintergrund-Schleife
Stream-Bildschirme bleiben Minuten lang sichtbar, daher muss der Hintergrund nahtlos schleifen. Baue eine Bewegung auf einem Zyklus auf, der gleichmäßig in die Timeline passt (z. B. ein Verlaufs- oder Partikeldrift, dessen Periode loopFrames ist), und greife darauf mit frame % loopFrames zu, sodass 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
- Die verbleibende Zeit wird aus
useCurrentFrame()/performance.now()in jedem Frame neu berechnet – keinesetInterval-Dekrementierung. - Der Timer erreicht
00:00auf dem exakt vorgesehenen Frame;durationInFrames === durationInSeconds × fps. - Die Dauer ist eine Eingabe-Prop, keine fest codierte Konstante.
- Ziffern verwenden
tabular-nums; das Layout verschiebt sich beim Zahlenwechsel nicht. - Das Format passt zur Spanne (mm:ss vs. dd:hh:mm:ss); führende Null-Einheiten werden weggelassen, wenn sie ablenken.
- Die Hintergrund-Schleifenperiode teilt die Timeline gleichmäßig; keine Naht an der Übergangsstelle.
Ausliefern & verifizieren (gerenderte Einzelbilder → MP4)
Verpackter Helfer (
scripts/): Lege deine Einzelbilder mitscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.pngals Kontaktbogen an und überprüfe 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 als Prop) – der Timer ist eine reine Funktion von useCurrentFrame(), niemals setInterval / Date.now() / Math.random(). Setze durationInFrames = durationInSeconds * fps. Auslieferung = out/*.mp4 + das Projekt. 9:16 hochkant (1080×1920) ist der Standard.
Verifikationsschleife – Einzelbilder rendern → prüfen → kodieren. Der ganze Sinn ist Frame-Genauigkeit, also prüfe den Ziffernwert an exakten Frames vor der Kodierung.
# Einzelbilder am Anfang / Mitte / Ende – MIT DEN AUSGELIEFERTEN PROPS (der tatsächlichen 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
# Prüfe jedes PNG – die Ziffern müssen EXAKT der Frame/fps-Mathematik entsprechen:
# - bei Frame N wird angezeigt = ceil(durationInSeconds - N/fps) (z.B. 300s bei 30fps, Frame 4500 → 150s → "02:30")
# - letzter Frame zeigt 00:00 (oder den Endzustand); kein Drift, kein Off-by-one-Sekunde
# - tabular-nums: Das Layout springt nicht zwischen den Frames
# - 9:16 sicherer Bereich: Timer + Markenblock liegen klar außerhalb der oberen ~12% und unteren ~20-35% UI sowie der rechten Aktionsschiene
npx remotion render Countdown out/countdown.mp4 --props='{"durationInSeconds":300}' # kodieren, sobald die Ziffern geprüft sind
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 gewünschte Sekunde, die du prüfen möchtest.
Bevor du fertig bist:
- Einzelbilder werden sauber bei Frame 0, Mitte und letztem Frame gerendert – keine Fehler.
- Der Ziffernwert bei Frame N ist exakt gleich
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 des 9:16-Sicherheitsbereichs.
- Nur Frame-gesteuert – kein
setInterval/Date.now()/ Timer;tabular-nums, damit das Layout hält. - Die ausgelieferte
durationInSeconds(nicht nurdefaultProps) wird korrekt gerendert; MP4 kodiert, GIF optional.
Referenzdateien
references/countdown-cookbook.md– vollständige lauffähige Remotion<Countdown>(konfigurierbare Dauer, mm:ss / dd:hh:mm:ss, Endzustand-Halte), die Countdown-bis-zu-einem-festen-Datum-Webkomponente mit Zeitzonenbehandlung, ein „Startet bald“-Stream-Bildschirmlayout (Markenblock + Social-Media-Links + sich wiederholender Verlaufshintergrund) und das nahtlose Schleifen-Hintergrundmuster.references/digit-flip.md– zwei deterministische Ziffernanimationskomponenten: die Kilometerzähler-Rolle (vertikaler 0–9-Streifen, vom Frame gesteuert) und der Flip-Card-Effekt (obere Hälfte faltet sich), plus wann welche für Launch vs. Sale vs. Stream geeignet ist.