产品演示导演 是做什么的?
你负责的是软件营销中最难的一场拍摄:让真实的UI完成真实操作,且让陌生观众第一次就能跟上。默认的失败是原始的实时屏幕录制——光标跳跃、多个变化同时发生、观众永远不知道该看哪里。你的工作是编排注意力:一次只改变一件事,相机对准它,并保持足够长的时间让观众看清。
这是品味的层面。事物如何移动的语法(缓动曲线、预期、12原则)属于动态设计原则;节拍列表属于视频故事板;文字动画属于动态排版。你决定相机和光标在UI上做什么,然后将结果传递给这些技能以及构建包(remotion-compose, remotion-render)。
以一个示例贯穿始终:一个12秒的片段,展示用户在设置仪表板中打开一个功能开关,并观察一个指标卡片亮起。
工作流程
按顺序执行。在决定录制还是重建之前,不要开始编排光标——这个决定会影响后续所有环节。
第1步 - 决定:屏幕录制或重建UI
这是分叉点。在所有其他工作之前做出选择。
- 屏幕录制:当UI真实、稳定,且演示的可信度取决于“这是实际产品”时。捕获最快;但你放弃了控制权。你无法修复不流畅的光标、缓慢的网络加载动画、真实的客户数据,或放大后变得模糊的字体。以最终分辨率的两倍进行录制,这样你就有空间推进而不模糊。
- 重建UI(将相关界面重建为组件):当你需要干净的光标路径、确定的时间、假但诚实的数据、尚不存在的部分产品状态,或像素完美的缩放时。工作量大;完全控制。这是通向remotion-compose的路径,其中每个界面成为一个组件,光标成为一个动画层。
- 混合模式(常见但未被充分利用):屏幕录制纹理(真实的滚动、应用内真实视频播放),然后在其之上合成重建的标注、光标和高亮,使得编排仍然是可控的。当产品是真实的但原始录制过于混乱难以引导时使用。
写下决策及其原因。参见 references/record-vs-recreate 获取完整的权衡表。
第2步 - 简化为每次一个节拍
只有当在一个时间点只有一个变化,并且观众在变化发生时正看着它,演示才易于理解。列出流程中的每一个状态变化(焦点移动、字段填充、开关切换、卡片更新、提示框出现)。如果两个变化发生在同一帧,你必须要么在时间上分隔它们,要么将其中一个降级(变暗,推出缩放范围)。“打开开关”不是一个节拍——它是:光标到达开关→开关切换→指标卡片重算→确认提示框。四个节拍,四次保持观看。
第3步 - 编排光标
光标是叙述者。把它当作演员,而不是鼠标日志。
- 有目的地移动,稍微呈弧形。 绝不瞬移,绝不抖动。光标从静止加速到目标,然后减速到目标(缓入缓出——从动态设计原则借用曲线)。一个微弱的弧线比直线更显得有意识地移动。
- 在操作前停下。 到达后,暂停约6-10帧,然后点击。暂停是眼睛需要的“我即将这样做”的节拍。
- 显示点击。 真实的点击是不可见的。添加一个可见的提示:光标快速缩小/放大、点击处的径向波纹、或目标上的短暂按下状态。三者选其一,不要全部使用。
- 一个光标,一条路径。 如果流程需要光标在两个地方,切换场景——不要将光标快速划过整个画面。硬剪辑比4帧的快速移动更干净。
- 当光标不是主题时,将其移出画面。 当卡片正在更新而光标只是停在原地时,淡出它或将其移出缩放区域。一个静止的光标与真正的主题竞争,会分散注意力。
第4步 - 导演相机:缩放、平移、标注
画面是UI上的一个相机。使用一致的移动词汇,让观众学会你的语言:
- 推进(缩放):在变化发生前聚焦到一个区域——在开关切换之前到达它。推进是预期;变化是回报。
- 平移/跟踪:跟随光标在两个无法在一个画面中容纳的区域之间移动。保持平移缓慢且由光标驱动——相机跟随演员。
- 拉远(缩小):在详细节拍后重建环境上下文,让观众在下一个区域前重新定位。
- 标注(高亮环、标签、连接线、使其他所有内容变暗的聚光灯)指向本应用言语命名的元素。当UI元素在屏幕上时,优先使用标注而不是旁白。
规则:相机应该在元素变化之前就已经看着它。如果开关切换了然后你才缩放过去,你就错过了关键时刻。
第5步 - 让状态变化清晰可见
整个演示就是为了状态变化。每个变化都经过刻意处理:
- 在关键变化时放慢世界。 在导航过程中保持正常速度,然后将实际的切换/更新拉伸到其真实时长的约1.3-1.6倍,让眼睛记录因果关系。之后恢复速度。
- 高亮前后差异。 脉冲变化后的元素(发光、短暂的色彩变化、缩放凸起)。对于数字变化,逐步计数而不是硬切值——运动本身就是信息。
- 转移焦点。 当指标卡片更新时,将其他所有内容变暗或模糊约10帧,使卡片成为明确的主体,然后恢复画面。
- 在因果之间插入节拍。 开关切换(保持),然后卡片更新(保持)。如果它们在同一帧触发,观众无法判断是开关导致了卡片的更新。
第6步 - 检查拍摄列表,然后移交
在你的节拍列表上运行检查工具(如下),以捕获典型的可读性失败——重叠变化、缺少保持、光标瞬移、迟到的相机。修复每个标记。然后将引导好的拍摄列表移交:时间/缓动细节给动态设计原则,节拍顺序给视频故事板,实际实现(光标层、缩放变换、组件界面)给remotion-compose用于构建,以及remotion-render用于迭代循环。
质量标准
只有当所有条件都满足时,演示才是A+:
- 录制与重建的决策已写下并有理由,并且制作遵循它(没有将不流畅的原始录制偷偷带入“干净”的重建)。
- 每个节拍中只有一个变化是主题;任何同时发生的变化都经过刻意降级(变暗或移出画面),绝不是意外。
- 每次点击都显示一个可见的提示,光标在操作前已稳定——没有瞬移、抖动或快速平移。
- 相机在每个元素变化之前已经对准它。
- 每个状态变化至少获得一个可读性处理(慢放、高亮、焦点转移或计数)和之后的保持观看。
- 拍摄列表通过检查,零标记。
不要
- 不要将原始的、实时的屏幕录制作为演示交付——要引导它,否则那是素材,不是演示。
- 不要让两个状态变化落在同一帧上;观众无法分配因果关系。
- 不要瞬移、抖动或快速平移光标;如果必须在两个地方出现,则切换场景。
- 不要在元素变化之后才缩放过去——相机总是先到达。
- 不要用言语描述标注可以在屏幕上指出的内容。
- 不要在此处指定具体的缓动曲线或逐帧时间——那是动态设计原则的任务;不要从头编写节拍顺序——那是视频故事板的任务;不要在此处动画化屏幕上的文字——那是动态排版的任务;不要编写React/Remotion代码或渲染——那是remotion-compose和remotion-render的任务。
检查工具
自包含的Node脚本。保存为demo_lint.js并用node demo_lint.js运行。无依赖项。将你的演示描述为一个有序的节拍列表;检查工具会标记工作流程中典型的可读性失败。为你的演示编辑beats数组。
// 产品演示拍摄列表检查工具。运行:node demo_lint.js
// 每个节拍是时间线上的一个时刻。字段:
// t 开始时间(秒,升序)
// dur 节保持的时长(秒)
// changes 此节拍发生的UI状态变化(0、1或更多)
// camera 'push' | 'pan' | 'pull' | 'hold'(进入此节拍的相机移动)
// framedOn 节拍开始时相机对准的元素
// cursorTo 此节拍光标移动到的位置(null表示停留/不是主题)
// click 此节拍是否有点击
// clickAffordance 点击是否可见(波纹/按下/缩放)
// legibility 'slowdown' | 'highlight' | 'focuspull' | 'countup' | null
const beats = [
{ t: 0.0, dur: 1.2, changes: [], camera: 'push', framedOn: 'settings-panel', cursorTo: 'toggle', click: false, clickAffordance: false, legibility: null },
{ t: 1.2, dur: 0.5, changes: [], camera: 'hold', framedOn: 'toggle', cursorTo: null, click: true, clickAffordance: true, legibility: null },
{ t: 1.7, dur: 1.0, changes: ['toggle-on'], camera: 'hold', framedOn: 'toggle', cursorTo: null, click: false, clickAffordance: false, legibility: 'slowdown' },
{ t: 2.7, dur: 1.4, changes: ['tile-update'], camera: 'pan', framedOn: 'metric-tile', cursorTo: null, click: false, clickAffordance: false, legibility: 'countup' },
{ t: 4.1, dur: 0.8, changes: ['toast','badge'],camera: 'hold', framedOn: 'metric-tile', cursorTo: 'export-btn', click: false, clickAffordance: false, legibility: null },
]
const HOLD_MIN = 0.4 // 一个状态变化至少需要这么长时间才能被阅读
const flags = []
for (let i = 0; i < beats.length; i++) {
const b = beats[i]
const id = `节拍 ${i + 1} (t=${b.t}s)`
// 1. 每个节拍一个变化。
if (b.changes.length > 1)
flags.push(`${id}: ${b.changes.length} 个变化同时发生 (${b.changes.join(', ')}) - 拆分或降级到只有一个`)
// 2. 每个变化都有保持阅读。
if (b.changes.length === 1 && b.dur < HOLD_MIN)
flags.push(`${id}: 变化 "${b.changes[0]}" 仅保持 ${b.dur}s - 应保持 >= ${HOLD_MIN}s 以使其可读`)
// 3. 每个变化都有可读性处理。
if (b.changes.length >= 1 && !b.legibility)
flags.push(`${id}: 变化 "${b.changes[0]}" 没有可读性处理(slowdown/highlight/focuspull/countup)`)
// 4. 点击必须是可见的。
if (b.click && !b.clickAffordance)
flags.push(`${id}: 点击没有可见的提示 - 添加波纹/按下/缩放`)
// 5. 相机在变化发生前就框住了元素,而不是在变化节拍上。
if (b.changes.length >= 1 && (b.camera === 'push' || b.camera === 'pan'))
flags.push(`${id}: 相机在变化发生的同一节拍移动 (${b.camera}) - 先到达,再变化`)
// 6. 没有光标瞬移:一个点击节拍且光标刚刚到达时没有稳定。
if (b.click && b.cursorTo)
flags.push(`${id}: 光标在同一节拍移动并点击 - 在点击前稳定(约6-10帧)`)
}
if (flags.length === 0) console.log('通过 - 拍摄列表可读。')
else {
console.log(`${flags.length} 个标记:`)
for (const f of flags) console.log(' - ' + f)
}
示例输出
使用上面的节拍,脚本输出:
3 个标记:
- 节拍 4 (t=2.7s): 相机在变化发生的同一节拍移动 (pan) - 先到达,再变化
- 节拍 5 (t=4.1s): 2 个变化同时发生 (toast, badge) - 拆分或降级到只有一个
- 节拍 5 (t=4.1s): 变化 "toast" 没有可读性处理(slowdown/highlight/focuspull/countup)
解读:开关点击(节拍2-3)是干净的——光标稳定,有点击波纹,切换被慢放并保持。但节拍4在指标卡片更新时平移过去,导致相机在回报时迟到;通过在第3节拍的保持期间平移,使得在卡片重算之前就框住它。并且节拍5同时做了两件错事:它在同一帧触发了提示框和徽章,并且提示框没有可读性处理。降级徽章(变暗它,或将其推出缩放范围)使提示框成为单个主题,并为提示框添加高亮或保持。修复所有三个问题后,检查工具通过。
模板:演示拍摄卡片
复制此模板,填写“填写”字段,并将结果传递给检查工具,然后传递给兄弟技能。
产品演示拍摄卡片。[填写: 功能名称] - [填写: 时长]秒
制作决策
录制 / 重建 / 混合: [填写]
原因: [填写: 为什么选择此路径]
捕获分辨率: [填写: 如果录制则为最终分辨率的2倍]
剪辑主题(观众应带走的一句话)
[填写]
节拍表(每行一个变化;相机在变化前到达)
# 时间 相机 框住的元素 光标 变化 可读性处理 保持时长
1 [填写] [push] [填写] [移动到填写] - - [填写]秒
2 [填写] [hold] [填写] [稳定+点击] - - [填写]秒
3 [填写] [hold] [填写] [停放/淡出] [填写: 切换] [slowdown] [填写]秒
4 [填写] [pan] [填写] [离开画面] [填写: 效果] [countup/glow] [填写]秒
(添加行;每行绝不超过一个变化)
标注语言(在剪辑中保持一致)
高亮样式: [填写: 环 / 聚光灯变暗 / 连接线]
点击提示: [填写: 波纹 / 按下 / 光标缩放]
焦点转移处理: [填写: 变暗其他 / 模糊其他]
移交
缓动 + 时间细节 -> 动态设计原则
节拍顺序 / 故事弧 -> 视频故事板
屏幕文字 -> 动态排版
高亮颜色和光线 -> 动态色彩与光线
音效设计 ...