产品演示视频
将静态产品截图或设计稿转变成精致、生动的动画演示——无需屏幕录制工具、无需处理真实鼠标的抖动、也无需担心UI版本漂移。对每一帧画面进行构图,引导合成光标精确移动到像素级目标,触发点击波纹,然后放大到关键区域并添加功能说明。由于输入是静态的,动画由代码驱动,每个值都是确定性的,当截图发生变化时,渲染结果完全相同。
适用场景
- App/SaaS 产品演示、功能预告、新手引导讲解(15–90 秒)。
- 展示仅有截图/设计稿的UI中的点击路径。
- 突出密集仪表盘中的某一焦点区域(区域放大 + 标注)。
为什么截图优于屏幕录制
| 屏幕录制 | 截图驱动(本技能) |
|---|---|
| 真实光标会漂移、过冲、抖动 | 光标沿缓动路径精确移动到目标位置 |
| 修复一步需要重新录制整个片段 | 替换一个PNG,重新渲染相同的代码 |
| 录制过程中UI变化,鼠标难以定位 | 每一帧都是清晰、最终状态的资源 |
| 难以放大而不产生像素化 | 以2倍(DPR 2)导出截图 → 放大清晰 |
以显示尺寸的两倍(视网膜屏幕 / deviceScaleFactor: 2)捕获输入,这样2倍放大仍保持清晰。参见 references/screenshot-demo.md 获取 Playwright 捕获脚本。
演示循环
每个演示遵循相同的节拍,每个功能重复一次。足够长的停留以便阅读,有目的地移动,准确地点击,展示结果。
| 节拍 | 持续时间 | 作用 |
|---|---|---|
| 停留在当前画面 | 0.5–1 秒 | 让观看者在任何动作开始前先熟悉画面 |
| 光标移动到目标 | 0.4–0.8 秒 | 缓动移动(ease-in-out),而非线性 |
| 点击波纹 + 状态变化 | 0.2–0.3 秒 | 触觉反馈;画面在同一帧做出反应 |
| 放大到焦点区域 | 进入1–2秒,保持2–4秒 | 2倍是最佳选择——既有足够细节,又保留上下文 |
| 添加功能说明 | 随放大一起保持 | 一句利益短语,例如“一键导出” |
移动摄像机 或 光标,很少同时移动两者——同时移动会分散注意力。
为截图添加框架
原始截图看起来像错误报告;加上框架后看起来像产品。将每个画面包裹在浏览器或设备外壳中,并置于品牌化的背景上。
// 浏览器框架 —— 放入截图,得到可信赖的窗口
const BrowserFrame = ({ url, children }) => (
<div style={{ borderRadius: 12, overflow: "hidden", background: "#1e1e22",
boxShadow: "0 40px 80px rgba(0,0,0,.35)" }}>
<div style={{ height: 40, display: "flex", alignItems: "center", gap: 8, padding: "0 14px" }}>
{["#ff5f57", "#febc2e", "#28c840"].map((c) => (
<span key={c} style={{ width: 12, height: 12, borderRadius: "50%", background: c }} />
))}
<div style={{ marginLeft: 12, flex: 1, height: 24, borderRadius: 6, background: "#2b2b30",
color: "#9aa", fontSize: 13, display: "flex", alignItems: "center", padding: "0 10px" }}>
{url}
</div>
</div>
<div style={{ background: "#fff" }}>{children}</div>
</div>
);
在地址栏中放置真实URL(增加真实感),添加柔和阴影和渐变背景。对于移动端,切换到带刘海和圆角的手机框架。将框架样式统一放在一个主题对象中,使每个画面风格一致。
动画光标 + 点击波纹(核心动作)
使用当前帧驱动位置,绝不使用CSS过渡或系统时钟——视频渲染器需要每一帧都是帧号的纯函数,否则光标会不同步并闪烁。
import { useCurrentFrame, interpolate, spring, useVideoConfig } from "remotion";
// 在 startFrame 和 startFrame+dur 之间,从 `from` 缓动移动到 `to`
const useCursor = (from, to, startFrame, dur) => {
const frame = useCurrentFrame();
const t = interpolate(frame, [startFrame, startFrame + dur], [0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: (x) => x * x * (3 - 2 * x) }); // smoothstep
return { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
};
export const Cursor = ({ from, to, startFrame, dur, clickAt }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const { x, y } = useCursor(from, to, startFrame, dur);
// 波纹在点击帧展开一次
const since = frame - clickAt;
const showRipple = since >= 0 && since < 18;
const r = spring({ frame: since, fps, config: { damping: 18 }, durationInFrames: 18 });
return (
<>
{showRipple && (
<div style={{ position: "absolute", left: x, top: y, transform: "translate(-50%,-50%)",
width: 12 + r * 60, height: 12 + r * 60, borderRadius: "50%",
border: "2px solid rgba(80,140,255,.9)", opacity: 1 - r }} />
)}
<svg width="28" height="28" viewBox="0 0 28 28"
style={{ position: "absolute", left: x, top: y, filter: "drop-shadow(0 2px 3px rgba(0,0,0,.4))" }}>
<path d="M3 2 L3 21 L9 15 L13 24 L16 23 L12 14 L20 14 Z" fill="#fff" stroke="#222" strokeWidth="1.4" />
</svg>
</>
);
};
光标使用 smoothstep 缓动,使其从静止加速,并在目标前减速(线性光标看起来像机器人)。在同一 clickAt 帧触发波纹和画面状态变化,使因果对齐。参见 references/screenshot-demo.md 获取跨多个画面连接多次点击的完整合成方案。
区域放大(UI 版 Ken Burns 效果)
要放大到焦点元素,缩放画面并将 transform-origin 设置在该元素的中心,使得缩放以该元素为中心而非画面中心。
const ZoomToRegion = ({ target, scale = 2, startFrame, dur, children }) => {
const frame = useCurrentFrame();
const s = interpolate(frame, [startFrame, startFrame + dur], [1, scale],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: (x) => 1 - Math.pow(1 - x, 3) }); // easeOutCubic
return (
<div style={{ transform: `scale(${s})`, transformOrigin: `${target.x}px ${target.y}px`,
willChange: "transform" }}>
{children}
</div>
);
};
将放大倍数限制在2倍左右(超过3倍会失去上下文并让人迷失)。保持放大状态2–4秒——足够阅读细节——然后在进入下一个画面之前缓动恢复。将放大与标注配对使用,避免单独使用。
标注注释与功能说明
在关键点进行标注,然后让它消失。标注是一个标签加一个连接器(箭头、圆圈或下划线),指向焦点元素。
- 高亮元素:使用柔和光环或变暗的周围环境(
box-shadow: 0 0 0 9999px rgba(0,0,0,.45)可切出一个聚光灯孔)。 - 每拍一个功能说明:简短的名词短语(“实时分析”),不是句子。在放大时进入,在下一步之前退出。
- 将说明保持在下三分之一区域,并在标题安全区内,以便在裁剪为9:16时仍可见。
参见 references/annotation-and-transitions.md 获取聚光灯遮罩、箭头连接器和打字效果说明轨道。
画面间过渡
在步骤之间,使用一致的语言移动画面。选择一种并保持统一:
| 过渡 | 感觉 | 用途 |
|---|---|---|
| 推入 / 滑动 | "下一步" 的前进运动 | 线性讲解 |
| 交叉缩放 | 有动感,类似应用 | 进入详细视图 |
| 交叉淡入淡出 | 平静、中性 | 切换上下文(设置 → 仪表盘) |
| 共享元素上的匹配剪辑 | 无缝 | 同一卡片在不同画面中持续存在 |
保持当前画面直到点击完成,然后进行过渡——绝不在动画中途切换。参见 references/annotation-and-transitions.md。
输出检查清单
- 输入以2倍(DPR 2)捕获;放大在2倍时保持清晰。
- 每个光标/缩放值都是
useCurrentFrame()的纯函数——无CSS过渡用于动画状态。 - 光标沿缓动路径移动(smoothstep);波纹和状态变化在同一点击帧触发。
- 每个画面都加上了框架(浏览器/设备),包含真实URL和一致的风格。
- 每次只移动一个元素(摄像机或光标);缩放 ≤ 2倍,保持2–4秒。
- 每拍一个功能说明,位于标题安全的下三分之一区域;过渡使用一致的语言。
交付与验证(渲染静帧 → MP4)
打包的帮助脚本(
scripts/):使用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。
这是重量级任务:交付物是MP4文件,而非实时页面。演示使用用户资产——截图/设计稿、可选的Logo——因此验证主要在于正确的图片是否实际加载并正确定位在画面中。
输出约定:
- 一个 Remotion 项目,包含已注册的合成(
<Composition>+ zodschema+defaultProps);每个光标/缩放/过渡值都是useCurrentFrame()的纯函数(无CSS过渡、定时器、Date.now())。 - 截图通过
staticFile()加载,并使用delayRender/continueRender进行门控,确保2倍 PNG(以及任何Web字体)在帧渲染前已存在——否则画面会在演示中空白弹出。 - 交付物 = 渲染的
out/*.mp4文件 + 项目(截图更改时重新渲染)。
验证循环 —— 渲染静帧 → 检查 → 编码。 先使用便宜的PNG,只有确认正确后再生成视频。
# 使用将要交付的PROPS(真实截图路径),而非defaultProps,渲染帧精确静帧
npx remotion still ProductDemo out/f-settle.png --frame=15 --props=demo.json # 画面已加框架,移动前
npx remotion still ProductDemo out/f-click.png --frame=90 --props=demo.json # 光标在目标上 + 波纹
npx remotion still ProductDemo out/f-zoom.png --frame=150 --props=demo.json # 放大到焦点区域 + 说明
# 结束帧 = durationInFrames - 1(npx remotion compositions 会读取它)
检查每个PNG的保真度(正确的截图已加载;光标在精确像素位置;点击波纹和状态变化在同一帧;说明文本正确)和瑕疵(图片空白/未加载、画面偏移画布、放大像素化或超出画布、说明超出标题安全的下三分之一区域、宽高比错误/出现黑边)。
# 仅在静帧确认无误后:
npx remotion render ProductDemo out/demo.mp4 --props=demo.json
npx remotion render ProductDemo out/demo.gif --props=demo.json --codec=gif # README 首画面证明
批量处理(一个模板,多个产品): 当为每个产品重制演示时,先通过静帧验证一个代表性产品的props,然后再批量渲染整个目录——一次性捕获空白图片或偏移画布的错误,而不是重复N次。
完成前:
- 静帧在停留/点击/结束时渲染清晰 —— 无错误,截图实际已加载(非空白)。
- 光标精确落在目标上;波纹+状态变化在同一帧;说明文本正确且在标题安全的下三分之一区域。
- 所有动画由帧驱动 —— 无CSS过渡/定时器/
Date.now()/Math.random()。 - 将要交付的 props(真实截图路径)正确渲染,而不仅仅是
defaultProps。 - 完整MP4已编码并可播放;(可选)为README渲染了GIF。
参考文件
references/screenshot-demo.md—— 一个完整可运行的 Remotion 演示:使用 Playwright 以2倍捕获截图、浏览器/手机框架组件、跨多个画面引导光标并带有点击波纹的多步骤合成,以及渲染命令。references/annotation-and-transitions.md—— 聚光灯遮罩、