countdown-video
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCountdown 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 . Covers livestream "starting soon" screens, launch/sale countdowns, and looping backgrounds.
setInterval制作一个能精准结束的倒计时。核心思路是:从帧(或单调时间戳)推导剩余时间,然后对该数值进行格式化和动画处理——绝对不要用来倒计时。适用于直播「即将开始」画面、发布/促销倒计时以及循环背景场景。
setIntervalWhen 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 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.
setInterval(fn, 1000)| Context | Authoritative clock | Remaining time |
|---|---|---|
| Remotion render | | |
| Web (live page) | | |
| To a fixed date | | |
Never store remaining time in state and decrement it. Recompute it every frame.
使用实现的倒计时会产生漂移——回调函数至少会延迟1000毫秒触发,永远无法精确执行,并且当标签页被节流或渲染帧缓慢时,延迟会不断累积。据反馈,每分钟可能会损失约1秒。无论是渲染视频还是实时网页,解决方法都是一致的:以权威时钟为纯函数计算剩余时间,这样即使某一帧延迟,下一帧也会自动修正,不会累积误差。
setInterval(fn, 1000)| 场景 | 权威时钟 | 剩余时间计算方式 |
|---|---|---|
| Remotion渲染 | | |
| 网页(实时页面) | | |
| 精确到固定日期 | | |
绝不要将剩余时间存储在状态中并逐次递减。要在每一帧重新计算。
Frame-accurate countdown (Remotion → MP4)
帧精确倒计时(Remotion → MP4)
useCurrentFrame()N/fpstsx
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>;
};durationInSecondsdurationInFramesdurationInSeconds * fpsuseCurrentFrame()N/fpstsx
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>;
};durationInSecondsdurationInFramesdurationInSeconds * fpsWeb timer (live page, requestAnimationFrame)
网页计时器(实时页面,requestAnimationFrame)
For a live page, drive an rAF loop from and only repaint when the displayed second changes (no work 60×/sec for a 1 Hz number):
performance.now()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 : . See for the full date-target version with timezone handling.
Date.now()Math.max(0, target - Date.now())references/countdown-cookbook.md对于实时页面,基于驱动rAF循环,仅当显示的秒数变化时才重绘(无需以60次/秒的频率处理1Hz的数值):
performance.now()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.mdTime formatting
时间格式化
Break a remaining-seconds integer into fields with division + modulo, then zero-pad. Pick the format to fit the duration — never show when minutes is all that matters.
00d 00h 05mjs
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 | Use | Drop fields when |
|---|---|---|
| stream "starting soon", short timers (<1h) | — |
| flash sale, same-day event | hide |
| launch / multi-day countdown | hide leading 0 units for cleaner read |
Always set (or a monospace digit font) so digits keep equal width and the layout does not jitter as numbers change.
font-variant-numeric: tabular-nums通过除法+取余将剩余秒数整数拆分为不同字段,然后补零。根据时长选择合适的格式——当只需显示分钟时,绝不要显示。
00d 00h 05mjs
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)}`;
};| 格式 | 适用场景 | 何时隐藏字段 |
|---|---|---|
| 直播「即将开始」、短计时器(<1小时) | — |
| 闪购、当日活动 | 当 |
| 发布/多日倒计时 | 隐藏前导零字段以提升可读性 |
务必设置(或等宽数字字体),这样数字宽度保持一致,布局不会随数字变化而抖动。
font-variant-numeric: tabular-numsDigit 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 . Animate the position, not the text content, so the change is a glide, not a pop.
-digit × digitHeightcss
.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 × digitHeightcss
.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.mdLooping 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 ), and read it as 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 .
loopFramesframe % loopFramesreferences/countdown-cookbook.md直播画面会持续数分钟,因此背景必须无缝循环。构建与时间线能整除的循环动画(例如周期为的渐变或粒子漂移),并通过读取,这样在循环处就不会出现可见接缝。背景要保持低对比度和慢速度——数字才是主体,背景只是烘托氛围。相关方案请查看。
loopFramesframe % loopFramesreferences/countdown-cookbook.mdOutput checklist
输出检查清单
- Remaining time recomputed from /
useCurrentFrame()every frame — noperformance.now()decrement.setInterval - Timer hits on the exact intended frame;
00:00.durationInFrames === durationInSeconds × fps - Duration is an input prop, not a hardcoded constant.
- Digits use ; layout does not shift as numbers change.
tabular-nums - 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 (): tile your stills withscripts/, then assert the encode withscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png. Seescripts/probe-mp4.sh out.mp4 [WxH] [fps].scripts/README.md
The countdown ships as a Remotion composition ( + zod + , a prop) — the timer is a pure function of , never / / . Set . Deliverable = + the project. 9:16 vertical (1080×1920) is the default.
<Composition>schemadefaultPropsdurationInSecondsuseCurrentFrame()setIntervalDate.now()Math.random()durationInFrames = durationInSeconds * fpsout/*.mp4Verify 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合成组件形式交付( + zod + ,为属性)——计时器是的纯函数,绝不依赖 / / 随机数。设置。交付物 = + 项目文件。默认采用9:16竖屏(1080×1920)。
<Composition>schemadefaultPropsdurationInSecondsuseCurrentFrame()setIntervalDate.now()durationInFrames = durationInSeconds * fpsout/*.mp4验证循环——渲染静帧 → 检查 → 编码。核心目标是帧精确性,因此在编码前要检查精确帧处的数字值。
bash
undefinedStills 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
参考文件
- — complete runnable Remotion
references/countdown-cookbook.md(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.<Countdown> - — 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/digit-flip.md
- ——完整可运行的Remotion
references/countdown-cookbook.md组件(可配置时长、mm:ss/dd:hh:mm:ss格式、结束状态保持),带时区处理的精确到固定日期的网页组件,「即将开始」直播画面布局(品牌区块+社交账号+循环渐变背景),以及无缝循环背景方案。<Countdown> - ——两个确定性数字动画组件:由帧驱动的里程表滚动(垂直0-9条带)和翻牌(上半部分折叠)效果,以及各自适用于发布/促销/直播场景的说明。
references/digit-flip.md