动态排版 是做什么的?
文字在运动中的首要任务只有一个:它必须可读。动画排版最常见的失败不是难看,而是文字在观众读完第一个词之前一闪而过,或是文字动得太多,让眼睛追着运动而非阅读内容。这是工艺层面。它决定了每行文字的停留时长、入场和离场方式,以及哪些属性可以动——并将具体的帧数预算交给 remotion-compose,后者编写实际渲染的 React/TypeScript。
核心规则:动画服务于阅读,而非装饰。 只有当运动能引导视线、建立层级或标记节拍时,它才有存在的意义。以下所有内容都遵循这一原则。
全程以一个运行的示例为例:一个16:9的产品标题卡,显示 "Ship video in one prompt"(5个词),30fps,以及一个动画化的下三分之一字幕轨道位于其下方。
工作流程
按顺序执行。在确定停留预算之前,不要选择入场动画——预算是决定你是否还有空间做动画的依据。
第1步 - 首先计算每词阅读时间预算
文字必须在屏幕上停留足够长的时间,以便以轻松的速度阅读,再加上其入场和出场所需的时间。在触碰任何动画之前,先预算停留时间。
- 屏幕视频的基准阅读速度比印刷品慢:假设舒适阅读速度约为 ~3.5 词/秒(≈ 200 wpm)。字幕惯例类似——单次可读停留时间绝不低于 ~0.3s 每词。
- 最小屏幕停留时间 = 词数 ÷ 3.5 秒,每行最低为 0.8s(即使是一个词也需要一个节拍来识别),并且对于眼睛无法预测的陌生术语、数字或名称,额外增加 +0.3s 术语税。
- 入场和出场时间额外计算——不计入阅读时间,因为在过渡过程中文字并非完全可读。总时间 = 入场 + 停留 + 出场。
运行 type_budget.js(见下文),将一行文字转化为帧数预算。对于标题卡,它返回大约 43 帧的停留时间;这个数字就是你交给 remotion-compose 作为 Sequence 可读窗口的值,入场和出场时间在此基础上叠加。
第2步 - 选择与剪辑匹配的入场/出场时间,而非默认值
入场和出场是标点符号,而非句子。保持简短,让停留时间发挥作用。
- 入场:8-15帧(0.25-0.5s)@ 30fps。 足够快,感觉干脆;足够慢,显得有意识。标题卡可以用较长的时长;快速字幕轨道则用较短的。
- 出场:等于或短于入场。 出场不应拖沓——一旦读完,文字应立即清除。6-12帧。
- 匹配出场与后续内容。 硬切到下一场景 → 在剪辑点同时退出文字(无淡出,让剪辑本身完成)。交叉溶解 → 在溶解过程中淡出文字。绝不要为硬切设计复杂的出场;这会与剪辑冲突。这是
motion-design-principles(剪辑和缓动)与本技能的交汇点——从那里获取缓动曲线,在此处应用于文字。 - 交错多元素揭示,不要同时动画化。标题 + 副标题的最佳阅读效果是副标题在标题稳定后 4-8 帧再入场,从而使视线自上而下引导。
第3步 - 每行选择一种入场技术,并只动画化一个属性
同时动画化的属性越多,文字在过渡过程中就越难阅读。默认只动画化一个属性;只有在有明确意图时才组合两个属性(例如,透明度 + 一个小位移)。
- 淡入(透明度 0→1) - 最安全,始终可读,是字幕轨道和主体文字的正确默认选择。
- 升起/滑动(小位移,20-60px,与淡入配对) - 增加方向和活力;是标题卡的常用手法。保持距离小——大滑动会让眼睛追踪移动而非阅读。
- 逐词揭示 - 每个词依次通过自己的短交错淡入/升起。用于强调行和歌词式片段,不用于密集字幕(阅读会变得断断续续)。整行揭示应在停留时间内完成。
- 缩放/弹出(spring 从 0.85→1) - 有活力,适合单个主打词或数字标注。绝不要缩放整个句子;重排会难以阅读。
- 遮罩/擦除揭示 - 高级、有方向性,适合单个标题。制作成本较高;保留给最重要的标题。
无论选择何种方式,动画化的是属性;文字本身在动画过程中不会重排。 动画化 opacity 和 transform(translate/scale/clip)——绝不使用 font-size、width 或会重新布局字形的方式修改 letter-spacing,因为布局驱动的动画会产生抖动且难以阅读。
第4步 - 将粗细和大小作为过渡来动画化,而非噪声
粗细和大小是文字在运动中表达强调的方式——但它们是停留状态之间的过渡,而非连续的抖动。
- 粗细过渡: 一个词可以从较轻的粗细过渡到较重的粗细以体现强调(例如,在 8 帧内从 400→700 并稳定下来)。使用真正的可变字体粗细轴或字体的两个重量级别;绝不要通过缩放来伪造粗体。
- 大小过渡: 数字或关键词可以一次放大以占据层级,然后保持。放大并保持,而非放大并脉冲。文字上的循环脉冲看起来像是渲染错误,而非强调。
- 这些都是强调。每行最多一个粗细/大小强调,放在最重要的那个词上。如果所有内容都强调,那么就毫无强调。
第5步 - 设置运动中字体层级(顺序、大小、粗细、时间)
运动中的层级由四个信号组成;将它们一起使用,使眼睛知道看哪里以及按什么顺序看。
- 入场顺序 - 第一个动画化的内容最先被阅读。以标题开头,接着是副标题,最后是行动号召(CTA)。
- 大小对比 - 保持清晰的比例(标题 ≥ 1.6× 副标题)。运动无法挽救弱的静态层级。
- 粗细对比 - 越粗 = 越重要。屏幕上最多同时出现两个重量级别。
- 时间 - 最重要的行获得最长的停留时间,并且如果有的话,入场也最引人注目。最不重要的(如持续存在的 lower-third、水印)在单次入场后不再动画化。
将每行的预算、技术和层级角色填入 motion-type-spec 模板(见下文)。填写完成的规格就是交给 remotion-compose 的交接内容。
第6步 - 决定哪里不进行动画化
高级的做法是节制。在定稿前,从以下“不”列表中删除所有动画。一个具有良好预算的上升效果的标题卡,比每个词都做点什么的效果更好。
质量标准
完成的动态排版规格只有在满足以下所有条件时才达到 A+ 等级:
- 每行都通过阅读时间预算 - 停留时间 ≥
max(0.8s, 词数 ÷ 3.5 + 术语税),由type_budget.js计算,入场/出场时间在此基础上叠加,绝不从中扣除。 - 每行动画化一个属性(只有明确意图时可两个),并且没有行在动画过程中重排字形——只使用
opacity/transform。 - 入场为 8-15 帧,出场 ≤ 入场,并且出场与后续编辑(硬切 vs. 溶解)匹配,而非默认。
- 层级按顺序可读:入场顺序、大小比例(≥1.6×)和粗细都指向同一最重要的行。
- 每行最多一个粗细/大小强调,且没有循环或脉冲。
- 规格中命名了帧编号(入场开始、停留开始、停留结束、出场结束),以便
remotion-compose无需猜测即可编写 Sequence。
禁忌
- 不要交付停留时间短于阅读预算的文字——这是动态排版最常见的失败。如果没有足够空间停留,请删减词汇,而非缩短停留时间。
- 不要动画化
font-size、width、letter-spacing或任何会重新布局字形的属性;布局抖动会让文字难以阅读。改用transform进行缩放。 - 不要每行动画化超过一两个属性,并且绝不要逐词揭示密集字幕——阅读会变得断断续续。
- 不要循环、脉冲、反复弹跳或让文字持续运动;不停止的运动无法被阅读。
- 不要动画化每个元素。lower-third、水印、持续标签和法律文本只做一次入场然后保持静止。不要用复杂的入场动画化主体/字幕文字——淡入是正确的选择。
- 不要在此处决定底层缓动曲线或场景剪辑——那是
motion-design-principles的职责。不要在此处编写 Remotion 组件——将填写完成的规格交给remotion-compose。 - 不要编造文案。动画化用户给你的文字;如果某行太长而无法停留,请让他们缩短。
兄弟技能 - 交接,不要吸收
本技能负责文字时间与阅读。相邻的决策属于兄弟技能;从它们那里获取信息,不要在此重复它们的工作。
motion-design-principles- 剪辑、非文字运动的节奏,以及缓动曲线(加速形状)。从那里获取曲线并在此处应用于文字。video-storyboard- 存在哪些场景和镜头,以及它们的顺序。首先获取场景列表;本技能只处理场景内文字的时间。motion-color-and-light- 文字相对于背景的颜色、对比度和可读性。如果因为颜色导致文字难以阅读,这是该技能的职责,而非在此处调整时间。product-demo-director- 产品演示的整体叙事和节拍列表。它决定标题卡说什么以及何时出现;本技能决定每个标题卡如何动画化。sound-and-music-sync- 在节拍或重拍上落定文字揭示。将holdStart@/ 入场帧编号交给它,以便揭示可以对齐音轨。social-video-formatter- 根据平台调整和裁剪字幕轨道(9:16 TikTok、1:1 feed、安全区域)。在此处一次制定时间;让该技能按平台重新调整。
计算器
自包含 Node 脚本。保存为 type_budget.js 并使用 node type_budget.js 运行。无依赖项。编辑 lines 数组为你实际的内容;它会根据你的 fps 返回每行的帧数预算。
// 动态排版阅读预算。编辑 lines + fps,然后:node type_budget.js
const fps = 30
const WORDS_PER_SEC = 3.5 // 舒适屏幕阅读速度(~200 wpm)
const FLOOR_SEC = 0.8 // 任何行的最短停留时间,即使只有一个词
const TERM_TAX_SEC = 0.3 // 每行包含数字/名称/术语的额外时间
const ENTRANCE_FRAMES = 12 // 0.4s 入场
const EXIT_FRAMES = 9 // 出场 ≤ 入场
const lines = [
{ text: 'Ship video in one prompt', hasUnfamiliarTerm: false }, // 标题卡
{ text: 'Rendered headless in 4K', hasUnfamiliarTerm: true }, // 字幕:'4K'
{ text: 'Go', hasUnfamiliarTerm: false }, // 主打词
]
const sec2frames = (s) => Math.ceil(s * fps)
function budget(line) {
const words = line.text.trim().split(/\s+/).length
const raw = words / WORDS_PER_SEC + (line.hasUnfamiliarTerm ? TERM_TAX_SEC : 0)
const holdSec = Math.max(FLOOR_SEC, raw)
const holdFrames = sec2frames(holdSec)
const totalFrames = ENTRANCE_FRAMES + holdFrames + EXIT_FRAMES
return { words, holdSec, holdFrames, totalFrames }
}
let cursor = 0
for (const line of lines) {
const b = budget(line)
const enterIn = cursor
const holdStart = enterIn + ENTRANCE_FRAMES
const holdEnd = holdStart + b.holdFrames
const exitOut = holdEnd + EXIT_FRAMES
console.log(`"${line.text}" (${b.words}w)`)
console.log(` hold: ${b.holdSec.toFixed(2)}s = ${b.holdFrames} frames`)
console.log(` total slot: ${b.totalFrames} frames (enter ${ENTRANCE_FRAMES} + hold ${b.holdFrames} + exit ${EXIT_FRAMES})`)
console.log(` Sequence: enter@${enterIn} holdStart@${holdStart} holdEnd@${holdEnd} exitOut@${exitOut}`)
cursor = exitOut
}
console.log(`Total track length: ${cursor} frames (${(cursor / fps).toFixed(2)}s)`)
示例输出
"Ship video in one prompt" (5w)
hold: 1.43s = 43 frames
total slot: 64 frames (enter 12 + hold 43 + exit 9)
Sequence: enter@0 holdStart@12 holdEnd@55 exitOut@64
"Rendered headless in 4K" (4w)
hold: 1.44s = 44 frames
total slot: 65 frames (enter 12 + hold 44 + exit 9)
Sequence: enter@64 holdStart@76 holdEnd@120 exitOut@129
"Go" (1w)
hold: 0.80s = 24 frames
total slot: 45 frames (enter 12 + hold 24 + exit 9)
Sequence: enter@129 holdStart@141 holdEnd@165 exitOut@174
Total track length: 174 frames (5.80s)
解读:5 个词的标题需要 43 帧的停留、完全可读时间——而且这还不包括 12 帧的入场和 9 帧的出场,所以实际总时间为 64 帧。4 个词的字幕看起来更短,但包含一个数字(4K),所以术语税将其停留时间推高到 44 帧——尽管词数更少,但比标题略长。单个词 Go 触及了 0.8s 的最低值:即使是一个词也需要一个节拍。这些 enter@ / holdStart@ / holdEnd@ / exitOut@ 帧编号可以直接放入 remotion-compose 作为 <Sequence from={…} durationInFrames={…}> 边界——无需猜测文字应停留多久。
模板:motion-type-spec
填写此模板并交给 remotion-compose。每行文字一个块。
MOTION TYPE SPEC. [填写: 合成名称]. fps: [填写] canvas: [填写: 1920x1080 / 1080x1920]
LINE 1
Copy (verbatim): [填写]
Hierarchy role: [填写: headline / subtitle / caption / lower-third / CTA / hero-word]
Size: [填写]px Weight: [填写: 400 / 700 / variable axis]
Reading budget: [填写]w -> hold [填写]s = [填写] frames (from type_budget.js)
Entrance: [填写: fade / rise+fade / word-by-word / pop / wipe] over [填写: 8-15] frames
Exit: [填写: fade / cut-on-edit] over [填写: <=entrance] frames
Property animated: [填写: opacity / opacity+translateY / scale] (NO font-size/width)
Accent (max one): [填写: none / weight 400->700 on word "X" / scale-up on "X"]
Frame numbers: enter@[填写]
...
[为翻译而截断]