图像与设计
做倒计时视频时如何保证计时帧精确不漂移并实现数字滚动动画
用帧或单调时钟推导剩余时间(而非 setInterval 递减),制作帧精确的倒计时视频/网页计时器,并配上数字滚动/翻牌动画与无缝循环背景。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
做倒计时视频时如何保证计时帧精确不漂移并通用
解决思路
- 核心规则:剩余时间必须由权威时钟推导,绝不用 setInterval 递减;每帧重新计算剩余时间,而不是把剩余时间存进 state 再递减。
- 按场景选择权威时钟:Remotion 渲染用 useCurrentFrame(),剩余时间 = total − frame/fps;网页实时页面用 performance.now() 或 Date.now(),剩余时间 = targetMs − now;倒计时到固定日期用 Date.now(),剩余时间 = targetTimestamp − Date.now()。
- Remotion 帧精确实现:用 useCurrentFrame() 和 useVideoConfig() 取 fps,elapsed = frame/fps,remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed)),用 ceil 让数字在其整秒内保持显示。
- 把 durationInSeconds 做成 prop 而非常量,同一 composition 改一个输入即可渲染 5 分钟直播计时或 30 秒促销计时;并把 composition 的 durationInFrames 设为 durationInSeconds * fps,使视频长度与倒计时完全一致。
- 网页实时计时用 requestAnimationFrame 循环驱动:从 performance.now() 计算 remaining = Math.max(0, Math.ceil(duration - (now - start) / 1000)),仅当显示的秒数变化时才重绘(避免 1Hz 数字每秒做 60 次工作),remaining > 0 时继续 requestAnimationFrame。
- 倒计时到固定日期同理用 Date.now():Math.max(0, target - Date.now()),时区处理见 references/countdown-cookbook.md。
- 时间格式化:用除法加取模把剩余秒数拆成各字段再补零,pad = (n) => String(n).padStart(2, "0");formatMMSS = `${pad(Math.floor(s / 60))}:${pad(s % 60)}`;formatDHMS 依次算 d = Math.floor(s / 86400)、h = Math.floor((s % 86400) / 3600)、m = Math.floor((s % 3600) / 60)、sec = s % 60 后拼接。
- 按跨度选择格式:mm:ss 用于直播“即将开始”和 1 小时内的短计时;hh:mm:ss 用于闪购和当天活动(dd 为 0 时隐藏);dd:hh:mm:ss 用于发布/多日倒计时(隐藏前导 0 单位让读数更干净);绝不在只需分钟时显示 00d 00h 05m。
- 始终设置 font-variant-numeric: tabular-nums(或等宽数字字体),保证数字等宽、布局不随数字变化抖动。
- 数字滚动/翻牌过渡:把每个数字渲染成 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 transition,以保证确定性;翻牌卡样式(上半部向下折叠盖住下半部)与逐位滚轮两种做法及可运行组件见 references/digit-flip.md。
- 循环背景:直播画面要持续数分钟,背景必须无缝循环;把运动建立在能整除时间线的周期上(如周期为 loopFrames 的渐变或粒子漂移),用 frame % loopFrames 读取,使回绕处无可见接缝;保持低对比、慢速,数字是主体、背景只是氛围。
- 交付物为 Remotion composition(<Composition> + zod schema + defaultProps,durationInSeconds 作为 prop),计时器是 useCurrentFrame() 的纯函数,绝不用 setInterval / Date.now() / Math.random();设置 durationInFrames = durationInSeconds * fps;交付 out/*.mp4 加项目,默认 9:16 竖屏 1080×1920。
- 验证循环——渲染静帧 → 检查 → 编码:用真实要渲染的 props 渲染起始/中间/结束静帧,例如 npx remotion still Countdown out/f-start.png --frame=0 --props='{"durationInSeconds":300}',中间帧用 --frame=N,结束帧用 --frame=L(L = durationInFrames-1)。
- 逐张检查 PNG:帧 N 显示的数字必须精确等于 ceil(durationInSeconds - N/fps)(如 300s @ 30fps、frame 4500 → 150s → "02:30");最后一帧读到 00:00(或结束态保持);无漂移、无差一秒;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;每个检查帧计时器与品牌都在 9:16 安全区内;纯帧驱动无 setInterval / Date.now() / 计时器且用 tabular-nums;用实际发布的 durationInSeconds(而非仅 defaultProps)渲染正确,MP4 已编码、GIF 可选。
- 用打包脚本辅助: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。
适用边界 · 注意事项
- 不要用 setInterval(fn, 1000) 做倒计时:回调至少晚 1000ms 触发,标签页被节流或渲染帧变慢时会累积误差(常见每分钟丢约 1 秒)。
- 不要把剩余时间存进 state 再递减,也不要让计时器依赖 Date.now() / Math.random() 等非确定性来源(在 Remotion 渲染中会破坏帧精确)。
- 不要在只需分钟级跨度时显示 00d 00h 05m 这类冗余字段,也不要省略 tabular-nums 导致数字变化时布局抖动。