曲线面板里的动画看起来没问题,切一刀却跳变,变速后节奏漂移,导出又晚了一帧。
它们看起来像插值 bug,真正的原因却常常是同一份数据被解释成了不同含义:关键帧属于哪个时钟,一帧在哪个时刻采样,哪一层负责应用缓动。
关键帧横跨客户端、媒体引擎与渲染引擎。公式只是其中很小一部分,更难的是让所有使用者在同一时刻算出同一个值。先从一条可以亲手调整的曲线开始。
两个关键帧,中间的值从哪里来
关键帧记录某个时刻的属性值。例如位置在开始时为 0,两秒后为 100,中间各帧由系统补全。
线性插值下,时间走过一半,位置也走过一半。缓动改变的是这两种进度之间的关系:可以慢慢起步、加速、轻轻停下,也可以越过终点再回来。端点相同,过程却不同。
试着选择“缓入”,把时间轴停在第 15 帧:时间过了四分之一,位置却还不到四分之一。保持时间不变切到“线性”比较,再选“超调回落”,拖向结尾,观察位置如何先超过 100 再返回。
两帧之间,节奏由你决定。
拖动曲线控制点或下方滑杆,再移动时间轴,观察每一帧的数值如何变化。
x 限制在 0 到 1 之间;y 可以超出端点,让动画先反向蓄势,或越过终点再回落。
曲线上的圆点、数值读数和运动预览使用同一个采样结果。灰色刻线标出两个关键帧的位置,两侧留白用于显示超调。第 60 帧对应结束关键帧。
演示以 30 帧/秒采样两秒区间。第 0 至 59 帧是区间内的帧起点,第 60 帧表示恰好两秒处的结束关键帧。慢设备播放时可能跳过部分显示采样,但逐帧操作始终得到该帧确定的数值;不会自动播放。
x 是时间,y 是进度,u 则是另一回事
三次贝塞尔缓动曲线固定端点 、,两个可编辑控制点为 、。横轴表达时间进度,纵轴表达输出进度。控制点 x 保持在 [0,1],y 可以超出范围以产生超调,这与 CSS 三次贝塞尔缓动 的约定一致。
曲线本身由参数 u 描述:
u 不等于时间进度。 给定 x,要先求解 ,再读取 。直接把 x 代入 y 多项式,一般会得到另一条动画。演示同时显示 x 与 u,就是为了让这个差别可见。
从 a 到 b 的标量属性按以下过程计算:
、 是关键帧时间, 是输出进度, 是最终插值结果。
反解算法也是共享契约的一部分。可以先进行有限次 Newton 迭代,导数太小、迭代越界或不收敛时退回二分法,端点直接返回。各实现固定容差与迭代上限,还要检查输出值误差:x 残差很小,不代表在平坦 x 切线附近的 y 误差也小。
演示的曲线标记、数值与预览共用一个 TypeScript 求值结果。原生编辑栈则可以用共享 C++ 求值器建立相同边界。
先定义“一致”指什么
求值一致性:相同草稿、属性和输出帧时间戳,在 UI 读数、预览、导出和支持的平台上,结果在约定容差内一致。
编辑保真性:对设计为跟随源素材的动画,保留的源采样点在裁剪或切割后维持原值;变速可以改变它出现在时间线上的时刻,但不能意外赋予它不同的源时间属性值。
第二条是产品选择,不是普遍定律。标题可能有意固定在项目时间结束淡出,不随素材变速改变。数据模型必须说明动画锚定在哪个时钟。
| 现象 | 常见分歧 |
|---|---|
| 切割后跳变 | 新片段重新开始了原缓动 |
| 变速后漂移 | 混用源时间与时间线时间 |
| 导出错一帧 | 帧起点、帧中心或取整规则不同 |
| 跨平台运动不同 | 反解、单位或默认值不同 |
| 新版本打开旧草稿后变化 | 插值语义变化没有版本化 |
给动画一个明确的时钟
| 时钟 | 含义 |
|---|---|
| 时间线时间 | 项目中的绝对位置 |
| 片段时间 | 相对片段在时间线起点的偏移 |
| 源时间 | 原始媒体中的位置 |
跟随素材内容的动画可以保存在源时间上,再由片段映射推导 UI 位置。正向恒速播放时:
的单位是“每一秒时间线经过多少秒源素材”。变速片段需要积分速度曲线,倒放则沿所选源区间反向遍历。这些规则应放进共享映射函数。
反向映射也不总有唯一答案:定格可能把多个时间线时刻映射到同一源时间,重复或非单调片段也会产生歧义。此时应返回区间,或要求提供片段标识。
移动片段改变时间线起点;裁剪改变可见源区间,隐藏关键帧可保留以便恢复;变速有意改变时间线上的节奏。替换素材则需要单独定义产品行为,相同源时间戳不代表画面内容相同。
求值一次,再交给渲染
媒体引擎推进时间并管理编解码,渲染引擎应用效果与合成。音量等属性进入对应音频处理链路。
清晰的边界是:渲染前求出最终属性值,随帧交给渲染器;渲染器不再应用第二次 easing。
- 根据输出帧序号获得约定时间戳。
- 映射到动画所属时钟。
- 找到相邻关键帧,范围外按端点策略处理。
- 归一化片段时间,求出缓动进度。
- 按属性语义插值,并执行必要限制。
曲线面板可以直接绘制控制点几何,但播放头与读数应走同一个求值器。预览可以为了实时性跳帧,真正渲染某个时间戳时则应与导出一致。
不同属性也有不同语义:位置可用归一化画布坐标;正比例缩放若追求比例均匀变化,可选择对数空间,但零或负缩放需另外规定;旋转若要从 0° 完整转到 360°,应保留未折返角度;透明度可以限制到 [0,1],位置则通常保留超调;离散属性需要明确阶跃边界。
先对齐帧,再对齐数值
媒体时间使用整数值与 timescale 组成的有理数,固定输出帧采样规则。若采样第 n 帧起点:
整数有理数避免反复浮点累加,但仍要处理溢出、重标度与取整。曲线反解和任意变速映射仍可能是有明确误差界限的数值运算。
可变帧率源素材的显示时间戳不等间隔,输出帧序号不能当成源帧序号。吸附源帧时使用源 PTS,判断 UI 是否精确命中关键帧时比较规范化时间戳或规范采样身份。
切一刀,如何不改变原来的运动
在切点插入一个数值正确的新关键帧还不够。若两个新片段各自复用原 easing,归一化时间被重新开始,两边的运动都会变化。
一种稳妥表示是保留原曲线,只限制各片段使用的源区间。若必须生成独立曲线段,则进行细分:先由切点时间 反解 ,再使用 de Casteljau 算法。
定义 :
左曲线控制点为 ,右曲线为 ,两段共同描出原几何。
保存为归一化缓动段时,还要相对各段端点重新映射 x 与 y。这个变换必须在属性插值空间里做,例如对数缩放空间,而不是已经 clamp 的显示值。如果新控制点无法满足 schema 的范围约束,应保留区间表示,不能直接 clamp 掉新手柄。
尤其要注意:两端值相同,不代表整段恒定。 超调曲线可以离开某个值,再回到同一个值。垂直跨度为零时不能除以零归一化,也不能直接替换成常量。可以保留原曲线与参数区间、使用绝对值样条,或采用能表达这段偏移的其他细分表示。
切点已经是关键帧时直接复用边界。重复时间戳与零时长段应在进入插值之前制定确定策略。
草稿必须保存含义
只存四个控制点数字不够,schema 还要说明时钟、单位、插值空间、默认值与曲线归属。
{
"keyframeSchema": 2,
"clock": "source",
"property": "scale",
"interpolation": "log",
"keys": [
{
"t": { "value": 0, "timescale": 600 },
"value": 1.0,
"outgoingEase": { "type": "cubic", "points": [0.42, 0, 0.58, 1] }
},
{
"t": { "value": 1800, "timescale": 600 },
"value": 1.5
}
]
}
这个例子中,easing 属于从当前关键帧出发的片段,缺失时默认线性,最后一个关键帧没有出段。非有限数值、越界 x 和重复时间戳如何处理,也必须由所有读取者执行相同规则。
语义和结构都需要版本化。给旧草稿补显式 linear,只有在等价于旧版本行为时才安全;不能在迁移时偷偷把缩放从线性改成对数插值。
旧客户端无法计算三次曲线时,可以按声明帧率烘焙最终值,但只保证兼容时钟与属性语义下那些采样点的结果。采样点之间、修改帧率或再次变速后,不保证等价;同时还会增加体积并失去原始可编辑曲线。不支持的组合应只读打开或明确转换,不能静默改变动画。
发布时先部署兼容读取器,再开启新格式写入。Schema 与 evaluator 版本让问题可以追溯到具体契约。
四层验证
求值器共享黄金输入输出,覆盖端点、线性、平坦切线、超调、近重合时间和无效数据,以最终属性值定义容差。相同源码不自动保证跨编译器逐 bit 一致。
编辑操作生成曲线与切割、裁剪、变速、倒放序列,按锚定策略比较保留源采样点。重复切割和零跨度子曲线尤其值得覆盖。
预览与导出先比较带时间戳的属性日志,定位首个分歧,再按合理像素容差比较选定帧。参数相同仍可能因色彩、效果实现与精度出现像素差异。
兼容性验证旧草稿读取、支持格式往返和烘焙回退的声明采样率。诊断记录版本,不收集用户草稿正文。
最后落到几份明确约定:统一时钟模型、统一求值路径、统一采样规则,以及带版本的动画语义。它们让用户编辑的曲线经过切割、预览与导出之后,仍然表达同一件事。