/2,516 字/阅读约 9 分钟

把产品短片做成可执行规范:封面、节奏、音效、动图四件套

一条 52 秒的产品短片,从「靠感觉做」变成「按规范复现」:四件套的实测参数、为什么每段时长必须是 GIF 的整数遍、音效为什么全部本地合成而不进素材库、指针为什么要放大到界面宽的 4.5%,以及用「承接(carry)」替代「淡入淡出换片」的改造过程。

眠月眠月
joker1point

给一个桌面工具做宣传短片,第一次做完之后,用户(也就是我自己)给的反馈是四句话:封面满意、节奏满意、音效满意、动图满意。于是当场把它定成标准:以后都按这套做。

但"满意"是一句感受,不是规格。真正让标准落地的,是把这条片子里每一个可复制的决定写成参数。这篇文章是那份规范的公开版本 —— 四件套的规格、背后的取舍,以及后来被打回重做的两个地方。

一、反面教材:为什么要有规格

在没有规格之前,每做一条片子都是一次重新发明:版式随手定、段落时长靠感觉、音频要么忘了铺要么糊成一片。

结果就是看起来每一镜都不错,连起来像在翻 PPT —— 这句话不是我说的,是 onetake(一个专治"动效像 PPT"的开源技能)对这类成片的判词:

标题卡、淡出、界面镜头、淡出、功能卡片、淡出、Logo。每一镜单看也许都不错,合起来还是像在翻 PPT。原因是画面和画面之间没有任何联系,后一个场景只是把前一个替换掉了。

它把这件事量化了:给每个段落边界算"承接分"(carry score)—— 边界处有没有东西活下来、并且可见地变成下一段。它自己发布的片子 0.83 通过,而"节奏完美但像 PPT"的 v1 是 0.00,直接判不合格。

这套判据后来成了我改造自己管线的准绳,下面第三节会讲。

二、四件套规格

1. 节奏:六段式,时长由素材决定

js
开场卡 4s → 痛点卡 6s → 六段功能演示(各 7~10s)→ 结尾卡 6.5s ≈ 52s

演示段的时长不是随便定的,有一条硬规矩:

每段时长必须取 GIF 的整数遍。

演示动图是循环播放的。如果一段目标 8 秒、而 GIF 本体 2.92 秒,播 2.7 遍就切走 —— 观众会看到"同一个操作播到一半又从头开始"。取整数遍(这里是 2 遍 = 5.84 秒)之后,循环接缝正好落在段落边界,看不出循环。

代价是每段时长被素材"吸附"到最近的可选值,总时长会有几秒浮动。这个代价必须付 —— 时间轴精确到 0.1 秒,但观众记住的是"那个动作播了一半突然跳回去了"。

2. 音效:全部本地合成,不进素材库

这条是"听着舒服"的关键,也是最反直觉的一条:转场音效不是素材库里挑的,是算式算出来的。

每个段落边界铺一声 whoosh(0.45 秒):

python
# 一阶低通白噪 → 偏粉噪;固定随机种子,保证每次重建音效一致
prev = 0.94 * prev + 0.06 * white
env  = min(1.0, t / 0.09) if t < 0.18 else 1.0 - (t - 0.18) / 0.27
buf[idx] += (white - prev) * 0.07 * env

BGM 走外部音乐,但电平由代码控:volume=0.48、淡入 2 秒 / 淡出 3 秒、整轨峰值归一到 0.95(只做防削波)。这样任何一首 BGM 进来都是同一个响度关系,不会出现"换首歌就盖住画面节奏"。

还有一条容易被忽略的:最后一个段落边界不铺音效 —— 留给结尾卡的淡出。边界音效是"换段"的信号,而成片结尾要的是"结束",多一声 whoosh 反而把收尾切碎。

3. 封面:文字压进安全区

B 站投稿封面要过两刀:首页是 16:9、个人空间是 4:3,只设一个比例约等于没设(会被换成视频首帧,表现为"黑封面")。所以设计稿按 1146×717 做,关键文字一律压在 X=72 起、Y 185~640 的安全区内。

视觉上用的是"深色渐变 + 三处光晕 + 右侧真实界面微倾 2°":左侧四层文案(钩子 → 痛点 → 主张 → 数据行),右侧贴着真实界面截图作为证据。

一个实现细节值得记下来:径向光晕如果直接把模糊后的 alpha 铺满图层,模糊会把 alpha 糊到图层边界被硬切,画面上留下一条可见的直角分界线。正确做法是让椭圆只占图层中间一半、模糊半径也小于到边缘的距离。

4. 动图:真实操作 + 放大指针

演示 GIF 的"真实性"是刻意的:用 gdigrab 录真实窗口渲染,用 SendInput 注入真实键鼠事件 —— 和手操走完全相同的输入路径,不是摆拍截图。每个场景先重启程序,保证起点一致、可重复录制。

但本机有个坑:gdigrab -draw_mouse 不生效,录出来的画面里根本没有指针。于是改成自研方案:记录注入过的光标轨迹,转 GIF 时逐帧叠加一个放大的指针。

GIF 合成参数:宽 1400、12fps、palettegen=stats_mode=diff + paletteuse=dither=bayer:bayer_scale=3。中间录像保留,调参数走 --gif-only 不重录。

三、被打回的两个地方

标准定完的当天,用户回看后又提了两条:转场相对单调、鼠标不太丝滑。

两条都不是"再加点效果"能解决的,都有确切的根因。

转场单调:屏幕级只有一个动作

原版六段演示全是「静态背景卡 + 居中 GIF 循环 + 整屏淡入淡出」—— 把每一段拆开看,屏幕级只有一个动作(fade),剩下的运动全发生在 GIF 内部那一小块。

拆解参考片(vibestart)发现它靠的是三种连续运动:卡片位移进场(从右侧滑入,不是淡出淡入)、卡片堆叠展开(一张横卡变成三张并排的斜向构图)、界面持续滚动(长页面顺滑下滚而不是切屏)。共同点是:没有一帧是死画面。

改造后:段落之间改用 xfade 左滑(位移 + 淡化组合,0.5 秒),静态卡加极缓慢推近(Ken Burns,整段放大 3%)。

这里有个工程细节:9 段的长滤镜链能不能跑通要先用两段验证。ffmpeg 的长滤镜链在长视频里有中段崩溃的前科(94 秒处 exit -1、无报错),所以先用 4s + 6s 两段测:4 + 6 - 0.5 = 9.5s / 285 帧 完全符合预期,再上全链。

还有一处必须联动改:音效的落点。原来音效铺在"段边界",改成 xfade 之后边界变成了 0.5 秒的交叉区间,音效要铺在交叉的中点(视觉上"换段"的那一刻)才对得上。

鼠标不丝滑:三个根因,都在我们自己的代码里

用户的原话是"鼠标也不太丝滑,注意:下次也要给鼠标加上放大特效"。查下来三个原因:

① 移动是瞬移的。 SetCursorPos(x, y) 一步到位,中间没有任何过程。后期叠加的指针因此是"闪现",不是"移动"。

② 指针位置没有插值。 轨迹取值的实现注释写得很直白 —— "取最近的前一个采样"。于是叠加出来的指针是阶跃跳动:在同一位置停几帧,然后突然跳一格。

③ 放大得不够。 指针边长是 72 物理像素,落到成片里只剩 25~31 像素(1280 宽下的实测值)。对比参考片的指针约 20 像素 —— 没有量级差,所以观众看不出"放大特效",只觉得是个普通的鼠标。

修法对应三条(这也是后来写进规范的部分):

  • move() 改成分步缓动移动:按距离算步数(约每步 5 像素),smoothstep 缓动,每步 6~10 毫秒且每一步都记轨迹点;时长随距离增长但封顶 0.45 秒
  • 轨迹取值改成相邻采样之间线性插值(二分查找定位区间)
  • 指针提到 100 物理像素(约占界面卡宽度 4.5%,验收线是不低于 3.5%),并加柔和投影 + 白描边,再加一个点击脉冲圈(点击瞬间在指针脚下扩散一个圆环,0.34 秒)

顺带修掉一个隐患:轨迹和点击数据以前只存在内存里,--gif-only 复用录像时会把指针和脉冲全丢掉。现在它们随录像落成一份 JSON —— 调参数不需要重录,键鼠也不用再被独占。

四、验收:把"感觉"换成判据

规范的最后一段是验收清单。它存在的意义是:别用感觉验收感觉。

  • 帧数 == 时长 × fps(本次 1645 == 54.834 × 30)
  • 整片 -f null 解码无错
  • 音轨峰值留余量(本次 mean −16.0 dB / max −5.9 dB,无削波)
  • 每段时长 == GIF 整数遍
  • 指针相邻帧位移连续(不允许"0 → 大跳变")
  • 封面抓回线上比对,不看后台预览
  • 元数据用接口复核(标题 / 时长 / 标签 / 分区 / 封面)

最后两条尤其重要:发布以"线上抓回"为验收。本地构建通过、后台显示成功,都不算数。

五、留下的那条边界

改造完成后我做过一次对照:旧版的指针在相邻帧之间大跳、又小又平;新版逐帧连续递进、明显更大 —— 这条对比图留下了。

但同时要诚实说清楚规范里没做到的部分:我们做到的"位移 + 淡化"只是承接(carry)的近似 —— 真正的承接是"同一个元素变形进下一段",那需要在 HTML 里重建界面、用逐帧渲染而不是录屏(onetake 走的就是这条路,它的 carry 分能到 0.83)。

录屏路线有它不可替代的价值:说服力来自真实证据。所以现在的分工是:真机功能演示走录屏 + 这套规范;纯动效的概念片,才轮到渲染路线。

规范本身也在版本里:v1 是"满意的那版",v2 是"被打回两条之后修的版",改了什么、为什么改、验证数据是多少,全部写在同一份文档里。下次开工前先读它,而不是重新发明一遍。