动态图形视频制作
通过编写一个逐帧渲染的网页来生成视频。 以下两条路径实现相同的效果;区别在于你编写的内容。
- HyperFrames — 一个 HTML 文件,CSS 负责外观,一个暂停的 GSAP 时间线负责动效。
node scripts/hf.mjs运行锁定版本,并从提交的锁文件自动安装一次。Apache 2.0 许可。 - Remotion — React 组件。
useCurrentFrame()返回当前帧索引,你自行计算。成熟、强类型,但仅限 3 人以内免费使用。
在编写组合代码之前,请加载对应的参考文档。框架规则不可猜测,且大多数失败是无声的:一个从未注册时间线的 HyperFrames 文件会渲染一个静帧,且没有任何错误提示。
轻量路径
用户用任意语言描述他们想要的内容。他们从不撰写简报、规格说明或节拍表——由 Claude 来编译。
/motion 一个为 Inkwell 制作的6秒 Logo 动画,要简洁有力
这就是全部输入。底层执行流程:
- 编译 请求,生成
brief.json— 时长、尺寸、帧率、带时间点的节奏点、逐字文案、缓动效果、调色板,以及一个assumptions(假设)列表。每个空缺都从references/brief.md中的默认值表获取;所有字段都被定义,不留下任何未定义项,也不提出疑问。 - 展示 紧凑摘要(一个表格,约 12 行),并在同一轮交互中构建。无需批准环节。
- 基于模板构建、校验、渲染、查看帧画面。
后续迭代针对字段进行编辑,而非重新构建。“让它更有力”会修改 feel.ease 和两个持续时长;除非 kind 或 motion 发生变化,否则代码不会变动。
简报就是让这一切变得经济高效的关键产物。它存放在组合代码旁边,因此修订就是一个差异对比(diff),重新渲染也只需一条命令。
路径选择
| 信号 | 路径 |
|---|---|
| 全新构建,未指定框架 | HyperFrames |
| 有现成的 HTML/CSS/GSAP/Lottie 可复用 | HyperFrames |
不需要 node_modules,不需要构建步骤 | HyperFrames |
| 团队规模超过 3 人 | HyperFrames(许可要求) |
| 用户指名 Remotion,或仓库是 React/TS | Remotion |
| 需要设计系统、图表库或复杂组件 | Remotion |
| 移植现有的 Remotion 项目 | Remotion(移植成本往往高于收益) |
当用户同时指定两者时,请使用 HyperFrames 构建,并用一句话说明 Remotion 移植版可按需提供。除非用户明确要求对比,否则对同一作品进行两次完整构建是浪费工作。
工作循环
1. 编译简报
在编写代码之前,将请求转换为 brief.json。模式定义、kind/motion 语法、默认值表以及迭代杠杆点都位于 references/brief.md 中。
node scripts/brief.mjs brief.json # 校验并输出摘要表格
node scripts/brief.mjs brief.json --json # 机器可读结果
向用户打印摘要,修复任何 ERROR,然后在同一轮交互中继续。警告是判断性问题——你接受的一个警告会被记录到 assumptions 中。
简报还包含节拍信息。选择 audio.bpm,使拍点网格能整分切点——在 bpm 下,一个拍时 = 60 / bpm 秒,每个 beats[].start 都应落在拍点上。120 BPM 每拍 0.5 秒,适合大多数短视频。如果 brief.mjs 报告 off_beat_grid,移动切点或更改速度;不要忽略它,因为这决定了作品是精准卡点还是感觉微妙地不对劲。
完成条件: brief.mjs 退出码为 0,每个拍点都有时间窗口、类型、动效和逐字文案,且没有切点偏离拍点网格。
2. 项目搭建
从本技能的模板开始,而不是从空白文件开始——它们针对两种契约都正确无误,并省去了两个容易出错的步骤(时间线注册、序列定时)。
# HyperFrames — 纯 HTML。固定版本的引擎会自动安装一次,需要网络
cp -R assets/hyperframes/. ./my-video/
node scripts/hf.mjs lint ./my-video
# Remotion — React 项目,需要一次性安装其固定版本依赖
cp -R assets/remotion/. ./my-video/
cd my-video && npm ci
# PowerShell
Copy-Item -Recurse -Force assets\hyperframes\* .\my-video\
Copy-Item -Recurse -Force assets\remotion\* .\my-video\
完成条件: node scripts/hf.mjs lint 通过,或 npx remotion compositions src/index.tsx 能列出组合 ID。
3. 构建各拍点
每个拍点一个文件。保持时间线扁平化——HyperFrames 使用绝对时间,Remotion 使用 <Sequence> 帧。当构建过程改变定时时更新拍点表,然后重新检查总时长。
以其拍点 ID 命名每个场景,因为校验程序通过它来查找:HyperFrames 宿主元素上的 data-brief-beat="<id>",Remotion 中的 <Sequence name="<id>">。如果 Remotion Sequence 的定时是计算得出的,还需在其内部的一个元素上放置 data-brief-beat="<id>",以便测量时机。
完成条件: 拍点表中的每个拍点在代码中都存在,组合总时长与拍点表匹配,且没有 TODO、存根(stub)或占位符文案。
4. 验证动效
静帧会欺骗你对时机的判断。先查看帧画面,再查看渲染输出。
node scripts/check-contract.mjs --dir ./my-video --preflight # 静态检查,无需安装
node scripts/check-contract.mjs --dir ./my-video --runtime # 同时在浏览器中检查
node scripts/hf.mjs check ./my-video # 布局、动效、对比度
node scripts/hf.mjs snapshot ./my-video --review # 输出拍点帧和衔接帧的 PNG 图片
node scripts/hf.mjs keyframes ./my-video --selector "#card" --shot motion.png
npx remotion still src/index.tsx LaunchCard out/f.png --frame=45
npx remotion studio src/index.tsx # 以 1 倍速度观看
check-contract.mjs 无需安装任何东西即可捕获静默失败:未注册的时间线、不匹配的 window.__timelines 键、未暂停的时间线、缺少的时长、组合中的 Math.random()、未钳位的 interpolate。它是一次预检,不能替代真正的 linter。
当 brief.json 位于项目中(或通过 --brief 指向一个文件)时,它还会将构建结果与简报进行比较。每个需求都会标记为 verified(已验证)、failed(失败)或 unverified(未验证)。 unverified 不是通过状态,并且会阻止渲染。
| 代码 | 状态 | 含义 |
|---|---|---|
brief_format_mismatch | failed | 宽度、高度、帧率或时长与 format 不符 |
brief_scene_count, brief_scene_unmapped | failed | 某个拍点没有声明其 ID 的场景,或者某个场景没有对应的拍点。位置本身不代表身份 |
brief_timing_mismatch | failed | 某个场景的开始或结束时间与简报不符,误差超过一帧 |
brief_copy_missing | failed | 该文案行不在场景中。注释和未使用的变量不计入 |
brief_copy_wrong_scene | failed | 该文案行仅出现在另一个场景中 |
copy_not_visible | failed | 运行时:在 DOM 中但被隐藏、透明、移出屏幕、裁剪、模糊或遮挡 |
copy_hold_short | failed | 运行时:可读时长少于 beats[].hold |
first_frame_empty | failed | 运行时:visibleAtStart 已设置,但该拍点的内容没有任何渲染 |
first_frame_unverified | unverified | 运行时:第一帧上只有图形,或者该拍点没有文案。请查看帧画面 |
brief_timing_unverified, brief_format_unverified | unverified | 源代码计算得出的值。运行时可以最终确定 |
brief_copy_dynamic, brief_copy_unverified | unverified | 文案是否显示是一个运行时问题。除非经过运行时检查,否则 Remotion 的文案始终显示为此状态 |
匹配忽略大小写和空白,并统一弯曲引号。其他标点有重要意义,"12,480" 匹配一个最终停在 12480 的计数动画。当需求仍为 unverified 时,明确说明哪些需求,并尝试使其可测量再进行接受。接受一个需求意味着要指名道姓,使用 --allow-unverified "beat.lines.copy[0].visible",并向用户报告。永远不要接受通配符模式,并且 failed 状态根本无法被接受。请阅读 references/validation.md 了解确切的规则和限制。
在代码中修复偏差,或者有意修改简报并重新运行 brief.mjs——绝不允许两者不一致。
检查帧画面——所有检查都可能在一个有缺陷的影片上通过
一个通过的契约检查只表明时机和文案匹配,以及文本可读。它没有说明调色板、缓动、转场或影片本身是否有效。本技能的模板自身曾通过了 lint、布局、动效、对比度和工艺检查,但同时每个拍点都切到黑屏,且计数动画在数字显示的一瞬间就退出了。只有查看帧画面才发现了这个问题。捕获它们:
# HyperFrames:拍点帧,每个切点前后 5 帧的序列,以及手机尺寸预览图
node scripts/motion-render.mjs --brief ./my-video/brief.json --snapshot --no-render
# Remotion:对于每个切点 c(以帧为单位),抓取 c-6、c、c+6,以及每个拍点的中间帧
# (模板的切点在 75、210 和 270 帧)
node scripts/motion-render.mjs --dir ./my-video --no-render \
--stills 37,69,75,81,142,204,210,216,240,264,270,276,315
然后查看图片并回答每个问题。修复代码,重新捕获,再次查看:
- 切点是否出现黑屏? 一个切点周围出现多于一帧的空帧,意味着前一个拍点过早结束,下一个拍点是从无到有淡入。移动前一个拍点的退出动画,使其正好在切点结束(
start = beat duration − exit duration)。 - 高潮部分是否有足够停留时间? 计数动画的最终数字,或一行文案的最后一个词,在离开前应至少在屏幕上停留一个拍点的时间(
60 / bpm秒)。在数字落定的瞬间就退出意味着用户根本来不及阅读。 - 在手机上是否可读? 检查
review-phone.jpg,或者设想一下帧在 360 像素宽下的效果。在 1080p 下,大约 40 像素以下的标签和标语通常无法看清。 - 是否使用了简报指定的字体? 如果回退到 Segoe UI 或 Arial,说明字体从未加载成功,另一台机器将渲染出不同的影片。
--runtime会通过font_fallback发出警告,并且报告会记录实际绘制的字体。Remotion 需要@fontsource/*(模板的选择)或@remotion/fonts来加载本地字体文件。 - 是否使用了简报指定的转场?
feel.transition体现在每个切点周围的帧中,而不是一个到黑色的渐变。
完成条件: 你已经查看了所有拍点帧和每个衔接帧序列,回答了上述每一个问题,并且“发布检查清单”下的每一项都满足。
5. 渲染
node scripts/motion-render.mjs --brief ./my-video/brief.json
node scripts/motion-render.mjs --brief ./my-video/brief.json --batch rows.json --no-audio
该封装脚本从简报中读取路径、格式、帧率和 slug(标识符),并在简报、静态检查、工具、资源或运行时检查要求停止时,在花费任何渲染时间之前停止。失败时退出码为 1,存在未验证需求时退出码为 3。--no-checks 会跳过验证并在报告中记录。然后它会执行 lint、渲染、使用 ffprobe 测量文件,并写入 <out>.report.json。
当 audio.policy 为 generated(无论哪种路径),它会使用 music.mjs 合成背景音乐(bed),仅渲染无声音的视频,然后通过一次 ffmpeg 调用将两者混合——这样组合代码就永远不依赖于引擎的音频混合器。然后它会测量文件是否包含音频流,以及是否为静音。
对于透明视频:使用 --format webm 或 --format mov——H.264 无法携带 Alpha 通道。--no-audio 会跳过背景音乐,生成一个没有音频流的文件。GIF 无法携带音频,因此如果简报中包含背景音乐,除非你传递 --no-audio,否则会拒绝渲染为 GIF。
使用 --batch 时,一份简报加一个行数据文件会生成 N 个视频:HyperFrames 原生渲染每一行,Remotion 则为每一行生成一个 props 文件。批处理模式下只有视频,如果简报中包含背景音乐,除非你传递 --no-audio,否则会被拒绝。
完成条件: 输出文件存在,报告显示 rendered,其 notVerified 列表为空或者你已告知用户列表中的内容,并且文件的第一帧和最后一帧都是有意设计的。
无声失败的契约检查
HyperFrames — 四项,全部必需:
- 根元素必须包含
data-composition-id、data-width、data-height、data-duration、data-start="0"。 - 每个时间元素必须包含
id+data-start+data-duration。 - GSAP 时间线必须使用
{ paused: true }构建。 window.__timelines[<data-composition-id>] = tl— 键名必须相同。在添加完所有补间动画之后执行。如果遗漏此项,你将得到一个静帧,无任何错误。
Remotion — 三项:
- 在入口文件中调用
registerRoot(),在根组件中使用<Composition>。 interpolate(..., { extrapolateLeft: "clamp", extrapolateRight: "clamp" })— 每次都要钳位两端。- 组合中不能使用
Math.random()或Date.now()。请使用random(seed)。
决定作品是否显得廉价的关键四原则
这些是最常出问题的地方。完整集合及代码示例请参阅 references/motion-craft.md。
- 只移动活动的部分。 已就位的元素保持静止。空闲时“呼吸”式的缩放看起来像机器生成的。
- 重叠,不要排队。 交错约 15–30% 的元素自身时长。下一个元素在上一个元素还在收尾时就开始。
- 文字不要有弹跳效果。 过冲(overshoot)适用于有质量的物体——卡片、翻板、角色。文本、数字和 UI 标签使用
power3.out/expo.out,绝不产生过冲。 - 单个动效传达单一意图。
y + opacity表示“正在出现”。增加旋转和缩放后,文字在飞行过程中将无法阅读。
另外:一个转场应当是一个退出,而不是一个淡出后再接一个淡入。后者渲染出来会是一个中间变暗的跳切。
发布检查清单
-
brief.json已验证:定时节拍,第一帧有意设计,结尾停留 ≥ 2 秒。 -
check-contract.mjs --runtime输出verified,或未验证的 ID 已按名称接受并向用户报告。 - 文案、数字、链接和 URL 与要求完全一致——没有假文(lorem),没有占位符。
- 时间线已使用组合 ID 注册(HyperFrames),或
npx remotion compositions能列出组合(Remotion)。 - Lint/检查无报错。
-
<out>.report.json显示rendered,并包含预期的尺寸、帧率、时长和音频信息。 - 帧画面已检查:每个拍点、每个衔接帧、手机宽度——切点处无黑屏,
…
[翻译截断]