倒计时视频
制作一个精确到帧的倒计时。整个技巧只有一个核心思想:从帧(或单调时间戳)推导出剩余时间,然后格式化并动画显示该数字——绝不使用 setInterval 进行倒计时。涵盖直播“即将开始”屏幕、发布/销售倒计时以及循环背景。
何时使用
- 带有5–10分钟计时器的“即将开始”/“马上回来”直播屏幕。
- 发布和闪购倒计时(精确到目标日期的 dd:hh:mm:ss;用于制造紧迫感的 mm:ss)。
- 任何必须在精确帧上归零的动画计数器。
唯一规则:数字由时钟驱动,绝不使用时钟滴答
setInterval(fn, 1000) 倒计时会漂移——回调至少晚1000毫秒触发,永远不会精确触发,并且在标签页被节流或渲染帧缓慢时会堆积。大约每分钟会丢失1秒。修复方法对于渲染的视频和实时Web都是一样的:将剩余时间计算为一个权威时钟的纯函数,这样延迟的帧会在下一帧自我修正,而不是累积误差。
| 上下文 | 权威时钟 | 剩余时间 |
|---|---|---|
| Remotion 渲染 | useCurrentFrame() | total − frame/fps |
| Web(实时页面) | performance.now() 或 Date.now() | targetMs − now |
| 固定日期 | Date.now() | targetTimestamp − Date.now() |
绝不将剩余时间存储在状态中并递减它。每帧重新计算。
帧精确倒计时(Remotion → MP4)
useCurrentFrame() 是确定性的:第 N 帧总是在 N/fps 秒时渲染。从总时间中减去,计时器自然无漂移。
import { useCurrentFrame, useVideoConfig } from "remotion";
export const Countdown: React.FC<{ durationInSeconds: number }> = ({
durationInSeconds,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// 剩余秒数,钳位在 0 — ceil 以便“1”在整个秒内显示
const elapsed = frame / fps;
const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed));
return <div style={{ fontVariantNumeric: "tabular-nums" }}>{formatMMSS(remaining)}</div>;
};
durationInSeconds 是一个属性(prop),而不是常量——同一个组件通过更改一个输入即可渲染5分钟的流计时器或30秒的销售计时器。将组件的 durationInFrames 设置为 durationInSeconds * fps,以便视频的长度与倒计时完全一致。
Web 计时器(实时页面,requestAnimationFrame)
对于实时页面,使用 performance.now() 驱动一个 rAF 循环,并且仅在显示的秒数发生变化时重新绘制(不需要为了一个1Hz的数字每秒执行60次):
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。
时间格式化
使用除法和取模将剩余秒数整数分解为各个字段,然后补零。选择适合持续时间的格式——当只有分钟重要时,绝不显示 00d 00h 05m。
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 | 流“即将开始”,短计时器(<1h) | — |
hh:mm:ss | 闪购,当日活动 | dd 为0时隐藏 |
dd:hh:mm:ss | 发布 / 多日倒计时 | 为更清晰显示隐藏前导零的单元 |
始终设置 font-variant-numeric: tabular-nums(或使用等宽数字字体),以便数字保持等宽,布局不会在数字变化时抖动。
数字滚动/翻转过渡
平面数字切换显得廉价;滚动的“里程表”外观能营造倒计时的感觉。将每个数字渲染为0-9的垂直条带,并通过 -digit × digitHeight 进行平移。动画化位置而不是文本内容,这样变化是滑行而不是弹出。
.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 中详细说明,并附有可运行组件。
循环背景
流屏幕会持续几分钟,因此背景必须无缝循环。构建一个能平均分割时间线的运动周期(例如,周期为 loopFrames 的渐变或粒子漂移),并将其读取为 frame % loopFrames,这样在回绕点就不会有可见接缝。保持低对比度和慢速——数字是主体,背景是氛围。配方在 references/countdown-cookbook.md 中。
输出检查清单
- 每帧都从
useCurrentFrame()/performance.now()重新计算剩余时间 — 没有setInterval递减。 - 计时器在精确的目标帧上到达
00:00;durationInFrames === durationInSeconds × fps。 - 持续时间是一个输入属性,而不是硬编码常量。
- 数字使用
tabular-nums;布局在数字变化时不变。 - 格式匹配时间范围(mm:ss vs dd:hh:mm:ss);前导零单元在分散注意力时被移除。
- 背景循环周期平均分割时间线;回绕处无接缝。
交付与验证(渲染静帧 → MP4)
打包的辅助脚本(
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() / Math.random()。设置 durationInFrames = durationInSeconds * fps。可交付成果 = out/*.mp4 + 项目。默认是9:16竖屏(1080×1920)。
验证循环 — 渲染静帧 → 检查 → 编码。 重点在于帧精度,因此在编码之前检查精确帧上的数字值。
# 开始 / 中间 / 结束的静帧 — 使用交付的属性(您将渲染的实际持续时间)
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
# 检查每个PNG — 数字必须精确匹配 frame/fps 计算:
# - 在帧 N,显示值 = ceil(durationInSeconds - N/fps) (例如 300s @ 30fps,帧 4500 → 150s → "02:30")
# - 最后一帧显示 00:00(或结束状态保持);无漂移,无1秒偏差
# - tabular-nums:帧之间布局不抖动
# - 9:16 安全区域:计时器 + 品牌块位于顶部约12%和底部约20-35%的UI以及右侧操作栏之外
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,然后计算您想要检查的任何秒数的帧。
在完成之前:
- 静帧渲染在帧0、中间和最后一帧清晰无误。
- 帧 N 的数字值精确等于
ceil(durationInSeconds - N/fps);最后一帧在目标帧到达 00:00(无漂移/1秒偏差)。 - 计时器 + 品牌在每个检查帧上都保持在9:16安全区域内。
- 仅由帧驱动 — 无
setInterval/Date.now()/ 计时器;tabular-nums以保持布局。 - 交付的
durationInSeconds(不仅仅是defaultProps)正确渲染;MP4 已编码,GIF 可选。
参考文件
references/countdown-cookbook.md— 完整的可运行 Remotion<Countdown>(可配置持续时间、mm:ss / dd:hh:mm:ss、结束状态保持)、带时区处理的固定日期倒计时Web组件、“即将开始”流屏幕布局(品牌块 + 社交链接 + 循环渐变背景)以及无缝循环背景模式。references/digit-flip.md— 两个确定性数字动画组件:里程表卷轴(由帧驱动的垂直0–9条带)和翻牌卡(上半部分折叠)效果,以及每个适合用于发布、销售与流场景的说明。