countdown-video

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Countdown Video

倒计时视频

Make a countdown that ends exactly when it should. The whole craft is one idea: derive the remaining time from the frame (or a monotonic timestamp), then format and animate that number — never count down with a
setInterval
. Covers livestream "starting soon" screens, launch/sale countdowns, and looping backgrounds.
制作一个能精准结束的倒计时。核心思路是:从帧(或单调时间戳)推导剩余时间,然后对该数值进行格式化和动画处理——绝对不要用
setInterval
来倒计时。适用于直播「即将开始」画面、发布/促销倒计时以及循环背景场景。

When to use

适用场景

  • "Starting soon" / "be right back" livestream screens with a 5–10 min timer.
  • Launch and flash-sale countdowns (dd:hh:mm:ss down to a target date; mm:ss for urgency).
  • Any animated counter where the number must land on zero on the exact frame.
  • 带有5-10分钟计时器的直播「即将开始」/「马上回来」画面。
  • 发布和闪购倒计时(精确到目标日期的dd:hh:mm:ss格式;用于制造紧迫感的mm:ss格式)。
  • 任何需要数字在精确帧归零的动画计数器。

The one rule: drive the number from the clock, never a tick

核心规则:基于时钟计算数值,绝不依赖定时触发

A
setInterval(fn, 1000)
countdown drifts — the callback fires at least 1000 ms later, never exactly, and stacks up when the tab is throttled or a render frame is slow. Reports of ~1 second lost per minute are common. The fix is the same for rendered video and live web: compute remaining time as a pure function of an authoritative clock, so a late frame self-corrects on the next frame instead of accumulating error.
ContextAuthoritative clockRemaining time
Remotion render
useCurrentFrame()
total − frame/fps
Web (live page)
performance.now()
or
Date.now()
targetMs − now
To a fixed date
Date.now()
targetTimestamp − Date.now()
Never store remaining time in state and decrement it. Recompute it every frame.
使用
setInterval(fn, 1000)
实现的倒计时会产生漂移——回调函数至少会延迟1000毫秒触发,永远无法精确执行,并且当标签页被节流或渲染帧缓慢时,延迟会不断累积。据反馈,每分钟可能会损失约1秒。无论是渲染视频还是实时网页,解决方法都是一致的:以权威时钟为纯函数计算剩余时间,这样即使某一帧延迟,下一帧也会自动修正,不会累积误差。
场景权威时钟剩余时间计算方式
Remotion渲染
useCurrentFrame()
total − frame/fps
网页(实时页面)
performance.now()
Date.now()
targetMs − now
精确到固定日期
Date.now()
targetTimestamp − Date.now()
绝不要将剩余时间存储在状态中并逐次递减。要在每一帧重新计算。

Frame-accurate countdown (Remotion → MP4)

帧精确倒计时(Remotion → MP4)

useCurrentFrame()
is deterministic: frame N is always rendered at exactly
N/fps
seconds. Subtract from the total and the timer is drift-free by construction.
tsx
import { useCurrentFrame, useVideoConfig } from "remotion";

export const Countdown: React.FC<{ durationInSeconds: number }> = ({
  durationInSeconds,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // remaining seconds, clamped at 0 — ceil so "1" shows for its full second
  const elapsed = frame / fps;
  const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed));

  return <div style={{ fontVariantNumeric: "tabular-nums" }}>{formatMMSS(remaining)}</div>;
};
durationInSeconds
is a prop, not a constant — the same composition renders a 5-minute stream timer or a 30-second sale timer by changing one input. Set the composition's
durationInFrames
to
durationInSeconds * fps
so the video is exactly as long as the countdown.
useCurrentFrame()
具有确定性:第N帧始终精确渲染在
N/fps
秒处。用总时长减去该值,计时器就会天生无漂移。
tsx
import { useCurrentFrame, useVideoConfig } from "remotion";

export const Countdown: React.FC<{ durationInSeconds: number }> = ({
  durationInSeconds,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // 剩余秒数,限制为0——向上取整,让「1」完整显示一整秒
  const elapsed = frame / fps;
  const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed));

  return <div style={{ fontVariantNumeric: "tabular-nums" }}>{formatMMSS(remaining)}</div>;
};
durationInSeconds
是一个属性,而非常量——只需修改这一个输入,同一个合成就能渲染5分钟的直播计时器或30秒的促销计时器。将合成的
durationInFrames
设置为
durationInSeconds * fps
,这样视频时长就与倒计时完全一致。

Web timer (live page, requestAnimationFrame)

网页计时器(实时页面,requestAnimationFrame)

For a live page, drive an rAF loop from
performance.now()
and only repaint when the displayed second changes (no work 60×/sec for a 1 Hz number):
js
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);
Counting down to a fixed date is the same idea with
Date.now()
:
Math.max(0, target - Date.now())
. See
references/countdown-cookbook.md
for the full date-target version with timezone handling.
对于实时页面,基于
performance.now()
驱动rAF循环,仅当显示的秒数变化时才重绘(无需以60次/秒的频率处理1Hz的数值):
js
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);
精确到固定日期的倒计时思路相同,只是用
Date.now()
计算:
Math.max(0, target - Date.now())
。如需带时区处理的完整日期目标版本,请查看
references/countdown-cookbook.md

Time formatting

时间格式化

Break a remaining-seconds integer into fields with division + modulo, then zero-pad. Pick the format to fit the duration — never show
00d 00h 05m
when minutes is all that matters.
js
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)}`;
};
FormatUseDrop fields when
mm:ss
stream "starting soon", short timers (<1h)
hh:mm:ss
flash sale, same-day eventhide
dd
while 0
dd:hh:mm:ss
launch / multi-day countdownhide leading 0 units for cleaner read
Always set
font-variant-numeric: tabular-nums
(or a monospace digit font) so digits keep equal width and the layout does not jitter as numbers change.
通过除法+取余将剩余秒数整数拆分为不同字段,然后补零。根据时长选择合适的格式——当只需显示分钟时,绝不要显示
00d 00h 05m
js
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)}`;
};
格式适用场景何时隐藏字段
mm:ss
直播「即将开始」、短计时器(<1小时)
hh:mm:ss
闪购、当日活动
dd
为0时隐藏
dd:hh:mm:ss
发布/多日倒计时隐藏前导零字段以提升可读性
务必设置
font-variant-numeric: tabular-nums
(或等宽数字字体),这样数字宽度保持一致,布局不会随数字变化而抖动。

Digit roll / flip transitions

数字滚动/翻转动画

A flat number swap reads as cheap; the rolling "odometer" look sells the countdown. Render each digit as a vertical strip 0–9 and translate it by
-digit × digitHeight
. Animate the position, not the text content, so the change is a glide, not a pop.
css
.reel { height: 1em; overflow: hidden; }
.reel .col { transition: transform .45s cubic-bezier(.22,1,.36,1); }
/* col holds stacked 0..9; shift by -value em */
In Remotion, drive the strip offset off the frame instead of a CSS transition (so it is deterministic). The flip-card style (top half folds down over the bottom) and the per-digit reel are both detailed, with runnable components, in
references/digit-flip.md
.
平淡的数字切换看起来很廉价;类似里程表的滚动效果能让倒计时更有质感。将每个数字渲染为包含0-9的垂直条带,通过
-digit × digitHeight
进行平移。为位置设置动画,而非文本内容,这样变化是平滑滑动而非突然跳转。
css
.reel { height: 1em; overflow: hidden; }
.reel .col { transition: transform .45s cubic-bezier(.22,1,.36,1); }
/* col包含堆叠的0..9;通过-value em进行偏移 */
在Remotion中,通过帧驱动条带偏移而非CSS过渡(确保确定性)。翻牌样式(上半部分向下折叠覆盖下半部分)和单数字滚动效果的详细实现及可运行组件,请查看
references/digit-flip.md

Looping background

循环背景

Stream screens hold for minutes, so the background must loop seamlessly. Build motion on a cycle that divides evenly into the timeline (e.g. a gradient or particle drift whose period is
loopFrames
), and read it as
frame % loopFrames
so there is no visible seam at the wrap. Keep it low-contrast and slow — the number is the subject; the background is ambience. Recipes in
references/countdown-cookbook.md
.
直播画面会持续数分钟,因此背景必须无缝循环。构建与时间线能整除的循环动画(例如周期为
loopFrames
的渐变或粒子漂移),并通过
frame % loopFrames
读取,这样在循环处就不会出现可见接缝。背景要保持低对比度和慢速度——数字才是主体,背景只是烘托氛围。相关方案请查看
references/countdown-cookbook.md

Output checklist

输出检查清单

  • Remaining time recomputed from
    useCurrentFrame()
    /
    performance.now()
    every frame — no
    setInterval
    decrement.
  • Timer hits
    00:00
    on the exact intended frame;
    durationInFrames === durationInSeconds × fps
    .
  • Duration is an input prop, not a hardcoded constant.
  • Digits use
    tabular-nums
    ; layout does not shift as numbers change.
  • Format matches the span (mm:ss vs dd:hh:mm:ss); leading zero units dropped when distracting.
  • Background loop period divides the timeline evenly; no seam at the wrap.
  • 剩余时间每帧都从
    useCurrentFrame()
    /
    performance.now()
    重新计算——绝不使用
    setInterval
    递减。
  • 计时器在精确的目标帧到达
    00:00
    durationInFrames === durationInSeconds × fps
  • 时长是输入属性,而非硬编码常量。
  • 数字使用
    tabular-nums
    ;布局不会随数字变化而偏移。
  • 格式与时长匹配(mm:ss vs dd:hh:mm:ss);前导零字段在干扰可读性时隐藏。
  • 背景循环周期与时间线能整除;循环处无接缝。

Deliver & verify (rendered stills → MP4)

交付与验证(渲染静帧 → MP4)

Packaged helper (
scripts/
): tile your stills with
scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png
, then assert the encode with
scripts/probe-mp4.sh out.mp4 [WxH] [fps]
. See
scripts/README.md
.
The countdown ships as a Remotion composition (
<Composition>
+ zod
schema
+
defaultProps
,
durationInSeconds
a prop) — the timer is a pure function of
useCurrentFrame()
, never
setInterval
/
Date.now()
/
Math.random()
. Set
durationInFrames = durationInSeconds * fps
. Deliverable =
out/*.mp4
+ the project. 9:16 vertical (1080×1920) is the default.
Verify loop — render stills → inspect → encode. The whole point is frame-accuracy, so check the digit value at exact frames before encoding.
bash
undefined
打包工具
scripts/
):使用
scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png
拼接静帧,然后用
scripts/probe-mp4.sh out.mp4 [WxH] [fps]
验证编码。详情请查看
scripts/README.md
倒计时以Remotion合成组件形式交付(
<Composition>
+ zod
schema
+
defaultProps
durationInSeconds
为属性)——计时器是
useCurrentFrame()
的纯函数,绝不依赖
setInterval
/
Date.now()
/ 随机数。设置
durationInFrames = durationInSeconds * fps
。交付物 =
out/*.mp4
+ 项目文件。默认采用9:16竖屏(1080×1920)。
验证循环——渲染静帧 → 检查 → 编码。核心目标是帧精确性,因此在编码前要检查精确帧处的数字值
bash
undefined

Stills at start / mid / end — WITH SHIPPED PROPS (the real duration you'll render)

渲染开始/中间/结束处的静帧——使用交付属性(实际要渲染的时长)

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
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

Inspect each PNG — the digits must read EXACTLY the frame/fps math:

检查每张PNG——数字必须完全符合frame/fps计算结果:

- at frame N, displayed = ceil(durationInSeconds - N/fps) (e.g. 300s @ 30fps, frame 4500 → 150s → "02:30")

- 在第N帧,显示值 = ceil(durationInSeconds - N/fps) (例如300秒@30fps,第4500帧 → 150秒 → "02:30")

- last frame reads 00:00 (or the end-state hold); no drift, no off-by-one second

- 最后一帧显示00:00(或结束状态保持);无漂移,无差一秒的错误

- tabular-nums: layout does not jitter between frames

- 使用tabular-nums:帧之间布局无抖动

- 9:16 safe area: the timer + brand block sit clear of top ~12% and bottom ~20-35% UI and the right action rail

- 9:16安全区域:计时器+品牌区块完全避开顶部约12%、底部约20-35%的UI区域以及右侧操作栏

npx remotion render Countdown out/countdown.mp4 --props='{"durationInSeconds":300}' # encode once digits verify npx remotion render Countdown out/demo.gif --codec=gif # README proof clip

Use `npx remotion compositions` to confirm `durationInFrames`/`fps`, then compute the frame for any second you want to check.

**Before you finish:**
1. Stills render cleanly at frame 0, mid, and last — no errors.
2. The digit value at frame N equals `ceil(durationInSeconds - N/fps)` exactly; last frame hits 00:00 on the intended frame (no drift / off-by-one).
3. Timer + brand stay inside the 9:16 safe area at every checked frame.
4. Frame-driven only — no `setInterval` / `Date.now()` / timers; `tabular-nums` so layout holds.
5. Shipped `durationInSeconds` (not just `defaultProps`) renders correctly; MP4 encoded, GIF optional.
npx remotion render Countdown out/countdown.mp4 --props='{"durationInSeconds":300}' # 数字验证通过后再编码 npx remotion render Countdown out/demo.gif --codec=gif # README演示动图

使用`npx remotion compositions`确认`durationInFrames`/`fps`,然后计算要检查的任意秒数对应的帧。

**完成前检查:**
1. 第0帧、中间帧和最后一帧的静帧渲染正常——无错误。
2. 第N帧的数字值完全等于`ceil(durationInSeconds - N/fps)`;最后一帧在目标帧准确到达00:00(无漂移/差一秒错误)。
3. 计时器+品牌在所有检查帧中都处于9:16安全区域内。
4. 仅由帧驱动——无`setInterval` / `Date.now()` / 计时器;使用`tabular-nums`确保布局稳定。
5. 交付的`durationInSeconds`(不仅是`defaultProps`)渲染正确;MP4已编码,GIF可选。

Reference files

参考文件

  • references/countdown-cookbook.md
    — complete runnable Remotion
    <Countdown>
    (configurable duration, mm:ss / dd:hh:mm:ss, end-state hold), the count-down-to-a-fixed-date web component with timezone handling, a "starting soon" stream-screen layout (brand block + social handles + looping gradient background), and the seamless-loop background pattern.
  • references/digit-flip.md
    — two deterministic digit-animation components: the odometer reel (vertical 0–9 strip driven by frame) and the flip-card (top-half fold) effect, plus when each suits launch vs sale vs stream.
  • references/countdown-cookbook.md
    ——完整可运行的Remotion
    <Countdown>
    组件(可配置时长、mm:ss/dd:hh:mm:ss格式、结束状态保持),带时区处理的精确到固定日期的网页组件,「即将开始」直播画面布局(品牌区块+社交账号+循环渐变背景),以及无缝循环背景方案。
  • references/digit-flip.md
    ——两个确定性数字动画组件:由帧驱动的里程表滚动(垂直0-9条带)和翻牌(上半部分折叠)效果,以及各自适用于发布/促销/直播场景的说明。