☰
Oil Motion运行时控制器源码剖析:frame-scrub与分段播放的阻尼、环形距离和反向撤回
2026/10/4 5:51:01 网站建设 项目流程

Oil Motion运行时控制器源码剖析:frame-scrub与分段播放的阻尼、环形距离和反向撤回

【免费下载链接】oil-motion设计并实现随滚动、拖动、指针或状态变化响应的网页动画,覆盖素材、时间轴和运行时。项目地址: https://gitcode.com/gh_mirrors/oi/oil-motion

Oil Motion 是一个把 AI 视频、序列帧变成网页交互动画的开源 Skill,它负责生成关键帧、编译时间轴,并通过运行时控制器让画面跟随滚动、指针、拖动或状态变化。本文剖析它运行时层的核心源码 assets/interactive-motion.ts:frame-scrub 控制器如何用阻尼让画面平滑随动、如何用最短环形距离处理 360° 旋转输入,以及 segment-playback 控制器如何实现分段播放与反向撤回。

先搞清楚:控制器是怎么选出来的?

Oil Motion 规定“格式选择”和“播放方式”是两件独立的事。预算脚本 scripts/motion_budget.py 根据 Motion Brief 中的time_control直接决定runtime.controller,映射关系只有三种(见 references/runtime.md):

time_controlruntime.controller行为
scrubframe-scrub输入值持续映射到帧位置,停手画面停在原地
segment-playsegment-playback输入只选择下一状态,片段随后按时间自行播放
autonomousautonomous-playback时间自行推进,交互只负责开始/暂停

所有控制器都只读取编译生成的build/timeline.json,页面里不允许维护第二份时间常量。本文重点拆解前两个控制器。

frame-scrub:输入停在哪,画面就停在哪

createFrameAnimator(interactive-motion.ts#L66-L158)是 frame-scrub 的完整实现,对外只暴露四个方法:

  • setTarget(frame):直接指定目标帧;
  • setProgress(0~1):把一维进度映射到progress × (frameCount - 1);
  • setDirection(x, y):把指针相对主体的方向角映射到帧环上;
  • getCurrentFrame()/destroy():读取当前位置、释放动画帧。

阻尼:为什么快速甩动指针不会闪烁

核心在smoothDamp函数(interactive-motion.ts#L36-L64),它移植了 Unity 经典的 SmoothDamp 算法:

  1. 用omega = 2 / smoothTime把平滑时间换算成角频率,smoothTime默认0.11秒——值越小跟随越快,值越大“拖尾”越明显;
  2. 用maxSpeed限制每帧最大位移,默认frameCount × 2,即最快约半秒扫完整个时间轴,防止指针急甩时帧数瞬间跳变;
  3. 每帧按指数衰减更新速度和位置,并在越过目标点时强制落位归零(L56-L62),保证不会过冲后回弹。

渲染全部集中在requestAnimationFrame循环里(L95-L124):输入事件只更新target,不直接碰 DOM;当位置与目标差、速度都小于0.002时主动停表,符合项目“每个动画帧最多一次 DOM 写入、状态稳定时停止 rAF”的性能验收标准。若系统开启了prefers-reduced-motion,则直接跳到目标帧,不做连续 scrub。

最短环形距离:360° 展台自转的关键

当素材是闭环动画(circular: true,如角色圆周注视、产品展台自转),不能把帧号当直线处理。wrap与shortestCircularDelta(interactive-motion.ts#L25-L34)负责这件事:

delta = wrap(to) - wrap(from) 若 delta > 帧数/2 → delta -= 帧数 若 delta < -帧数/2 → delta += 帧数

也就是说,从第 239 帧转到第 1 帧(240 帧图集),走的是+2 帧的短路径,而不是倒退 238 帧。setTarget在环形模式下会把目标改写成position + 最短距离(L133-L139),阻尼系统看到的就是一条连续前进的轨迹。

setDirection(x, y, startAngle)(L140-L145)则把指针极角归一化到0~1再乘帧数,默认起始角-0.75π,正好对应“指针在下方时角色正视镜头”。一个 240 帧、16 列 15 行的环形图集配置示例可参考 assets/motion-manifest.example.json。

segment-playback:分段播放与反向撤回

“点击下一章节播放转场”这类交互用的是createSegmentPlayer(interactive-motion.ts#L257-L512)。它初始化时就严格校验timeline.json:start <= hold < endExclusive、各段不重叠、states数量必须等于段数加一、状态 ID 唯一且与from → to顺序一致——任何一项不满足直接抛错,不进入“运行时才崩”的状态。

正向播放:速度曲线属于时间轴

每段可声明播放速率曲线SegmentRateCurve(L233-L243):

  • constant:恒定速率;
  • edge-mid-edge:两端快、中间慢,按cos²权重在 edgeRate 与 midRate 之间插值,适合“起势快、结尾快”的镜头感。

正向播放直接调用video.play(),并用requestVideoFrameCallback逐帧监听进度(L322-L331),不支持时才回退 rAF;文档明确禁止用低频的timeupdate判断停帧。

反向撤回:不依赖负 playbackRate 的手动倒放

这是本文件最有巧思的部分。浏览器普遍不支持负playbackRate,所以反向时控制器暂停视频、手动逐帧回退currentTime(L428-L438):

video.pause() video.currentTime = max(targetTime, currentTime - rate × delta)

围绕这条倒放链路,源码做了三层防护:

  1. 边界间隙清除:removeBoundaryGap(L349-L376)在起播和每帧 tick 时检查当前时间是否落在“上一段 hold 与下一段 start 之间”的剪辑缝隙里,若是则直接跳过缝隙,避免倒放时画面卡在半透明接缝帧上;
  2. 精确落位:接近目标frameDuration/2内就调用settle(L378-L386)——先pause()再把currentTime精确设到目标状态的hold,杜绝“越过目标再回跳”;
  3. 快速连续输入可取消:每次startRun递增runToken(L388-L390),旧的 tick 回调发现 token 不匹配立即退出;cancel()则直接作废当前任务并暂停视频。因此用户连点五次“上一步”,只有最后一个目标生效,这正是 runtime 规范中“快速连续输入只保留最新目标”的落地。

step(-1)/step(1)(L485-L487)把方向增量转成goTo(targetState ± 1),边界状态自动 clamp,不会越界出空状态。

分步手势:把惯性滚轮合并成一步

分段播放与分页导航组合时,输入侧由 assets/step-gesture.ts 的createStepGestureAdapter统一处理(L44-L67):

  • 累计增量达到threshold(默认 40px)才触发一次onStep,把一次滚轮/触控板惯性序列合并成一个方向意图,而不是每个 wheel 事件跳一个状态;
  • 反向输入立刻清零累计值并解锁方向,天然支持“上滚两格再下滚一格”的撤回;
  • idleMilliseconds(默认 140ms)无新输入后自动复位;
  • setProgrammaticNavigation(true)可屏蔽页面自身平滑滚动引起的误触发。

手势阈值、惯性结束判定只写在这一处,媒体控制器和分页组件不得各写一份。

接入与验收要点

结合 SKILL.md 第 8 步与 references/runtime.md,接入两个控制器时记住这几条硬性要求:

  • 一个持续存在的媒体实例:切换状态不替换src、不重建视频节点,分段播放的反向永远是“从当前画面撤回”,不得换源硬切;
  • 时间值只来自编译结果:hold、start、endExclusive全部取自最终编码帧,禁止手工推算;
  • 反向必须倒放同一段:推进变拉远、溶解变复原;爆炸、泼洒这类倒放违反直觉的动作要单独生成反向片段;
  • 降级路径:prefers-reduced-motion使用合同指定的静态状态,资源失败时回退静态画面并解锁页面。

从smoothDamp的过冲防护、到shortestCircularDelta的环形取短、再到反向撤回的 token 取消机制,两个控制器共同体现了 Oil Motion 的一条设计原则:输入事件只表达意图,平滑、落位和取消都交给共享控制器——这也是它的动画在快速反向、连续输入下不粘滞、不抽动、不回跳的原因。

更多运行时细节可查阅 references/runtime.md,交付格式的选择逻辑见 references/delivery-selection.md。

【免费下载链接】oil-motion设计并实现随滚动、拖动、指针或状态变化响应的网页动画,覆盖素材、时间轴和运行时。项目地址: https://gitcode.com/gh_mirrors/oi/oil-motion

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询