1实现原理 · 为什么它能做到
全篇只有一条铁律:剩余时间必须由权威时钟推导,禁止把剩余值放进 state 递减。
Never store remaining time in state and decrement it. Recompute it every frame.
用机制层理由解释 setInterval 为什么会漂(回调至少晚 1000ms 触发,且在标签页被节流或渲染帧变慢时叠加)。
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.
Remotion 侧的可依赖时钟是 useCurrentFrame():第 N 帧恒等于 N/fps 秒,于是「帧精确」是构造性成立的。
`useCurrentFrame()` is deterministic: frame N is always rendered at exactly `N/fps` seconds.
取整用 ceil 并 clamp 到 0,使「1」能显示满整秒、末帧正好落在 00:00。
const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed));
时长是 prop 而非常量,并与 durationInFrames 绑定,避免倒计时走到 00:00 后仍有余帧。
Set the composition's `durationInFrames` to `durationInSeconds * fps` so the video is exactly as long as the countdown.
Web(非渲染)路径改用 rAF 驱动 performance.now(),且只在显示秒变化时重绘。
if (remaining !== lastShown) { render(remaining); lastShown = remaining; }
字体层用 tabular-nums 固定字宽,数字跳动不引起版式位移。
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.
数字动画的原则是「动位置、不动文本内容」,且必须帧驱动而非 CSS transition。
Animate the *position*, not the text content, so the change is a glide, not a pop.
直播/常驻画面要求背景无缝循环:运动周期必须整除时间线,按 frame % loopFrames 读取。
read it as `frame % loopFrames` so there is no visible seam at the wrap
验收被写成数字等式:第 N 帧显示值必须等于 ceil(duration − N/fps),末帧 00:00。
at frame N, displayed = ceil(durationInSeconds - N/fps) (e.g. 300s @ 30fps, frame 4500 → 150s → "02:30")
reference 把纪律落成可运行组件,并给出到固定日期的 web 变体(每帧 target − Date.now() 重算 + 时区处理)。
Register it so `durationInFrames` matches the countdown exactly — the video ends when the timer does:
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| cli | npx remotion still / render / compositions |
| cli | scripts/contact-sheet.sh + scripts/probe-mp4.sh(仓库根共享验证工具箱,SKILL.md 明列) |
| package | ffmpeg / ffprobe(系统依赖) |
| cli | npx -y playwright screenshot(scripts/seek-shot.sh,仓库级 Light tier 通用件) |
4风险提醒 风险提醒:蓝色 · 知晓即可
- 时间语义错误不会被静帧发现 — 若「到固定日期」的时区/目标时刻弄错,数字仍逐帧精确——静帧验收只验证「帧↔读数」,验证不了「读数↔现实」。交付前必须由用户确认目标时刻(reference 给了 timezone handling,但选错时区仍无拦截)。
- 验收依赖真实 duration prop,偷懒用 defaultProps 就验不到实际路径 — SKILL.md 反复强调 stills 要带 shipped props、收尾清单第 5 条点名 durationInSeconds;但这是纪律而非机制强制,agent 跳过时无任何自动化拦截。
- 渲染成本随 duration 线性增长 — 5–10 分钟的帧数意味着长时间本地编码;SKILL.md 未给性能预算或分段建议(同仓的 caption-animation 反而给了分段与 Remotion Lambda 建议),长倒计时应预留渲染时间与磁盘。
- 循环背景的「整除」要求容易漏 — reference §4 以 loopFrames = fps*6 示范;若改成非整除周期,接缝只有肉眼能发现,skill 无自动检查(probe-mp4 也查不到接缝)。
5第二遍独立确认
- [ok] 「禁止 setInterval 递减」是否在 reference 的示例实现里也真的遵守(防第一遍只信 SKILL.md 的说法) — reference 全量 grep:countdown-cookbook.md 组件 `const frame = useCurrentFrame();`,注册处 `durationInFrames={DURATION_SECONDS * FPS}`;到日期的 web 组件 `Math.max(0, target - Date.now())` 每帧重算并自述 "never a decrement";digit-flip.md 两个组件同样 `const frame = useCurrentFrame();`。setInterval 全 skill 仅作反例出现 1 次。
- [ok] 「倒计时结束时视频正好结束」是否真被约束住 — SKILL.md 明写 `durationInFrames` = `durationInSeconds * fps`;cookbook 给出 "Register it so `durationInFrames` matches the countdown exactly — the video ends when the timer does:" 的注册片段;收尾清单第 2 条把它列为验收项。文档、示例、验收三者一致,不是单向声明。
- [unlocatable] 「每分钟约丢 1 秒」的漂移量级是否有据 — SKILL.md 写 "Reports of ~1 second lost per minute are common.",但仓库内无基准脚本、无引用、无数据文件;本次为只读侦查且不运行计时实验,故不验证该数值,仅记为文档声明,不在结论中当已核事实使用。
- [ok] seek-shot.sh(Light tier)对本 skill 是否可用(防「Packaged helper」被读者误解为三条脚本都属本 skill) — scripts/README.md 把 seek-shot 归为「standalone HTML + `?t=N` harness」的 Light tier;本 skill 交付是 Remotion 组件(Heavy tier),SKILL.md 的 Packaged helper 行只点名 contact-sheet 与 probe-mp4。不存在悬空引用,也不存在被夸大的能力。
- [ok] 三仓库共享脚本是否同内容(跨包一致性) — md5 逐字节比对:contact-sheet.sh b7dcc28034c9bf8fda5d48683e54d8b5、probe-mp4.sh 167b7977c50f492014e1a6fef8c9ed12、seek-shot.sh 24a4130cd1e64874154429b0b4af12da,iart-tiktok / iart-text-message / iart-youtube 三包完全相同。
- [ok] SKILL.md 是否声称了 reference 未兑现的能力(防文档夸大) — SKILL.md 承诺的四件事——可配置 duration、mm:ss 与 dd:hh:mm:ss、drift-free 计时、无缝循环背景——在 countdown-cookbook.md 与 digit-flip.md 里各有对应实现(§1 组件、§2 到日期、§4 循环、digit-flip 两组件),逐项可定位,无空头承诺。
6结论
2730e6053383cccb…2a775336b5