Files
workbuddy_skills/oil-motion/references/runtime.md
T
admin 237a09a5b0 修:draw-ui / oil-motion 原被当子模块指针收录 ⇒ 改为正常文件入库(两份内容原先对别人是空的)
一、问题(本轮实测)
`draw-ui` 与 `oil-motion` 目录里**各自带一个内嵌 `.git`** ⇒ 上一次提交把它们记成了 **gitlink(子模块指针)**
⇒ 仓库里只存了一个不属于任何远端的 commit id,**别人克隆下来这两份是空的** ✗(`git status` 显示 ` m draw-ui` / ` m oil-motion` = 子模块内容有改动)。

二、处置(可回退)
· 把两处的 `.git` **挪走**(⛔ 不是删除)⇒ `归档/内嵌git-20261008/{draw-ui,oil-motion}.git`;
· `git rm --cached` 掉那两个 gitlink,再 `git add` 两个目录 ⇒ **按正常文件入库**(内容才真的进仓库)。

三、副作用(如实记)
挪走 `.git` 后,这两个技能**不能再原地 `git pull` 取上游更新**(要更新得重新拉一份覆盖);
如需恢复其本地仓库,把 `归档/内嵌git-20261008/` 里的 `.git` 挪回原处即可。
2026-10-08 22:29:52 +08:00

7.5 KiB
Raw Blame History

运行时控制

本文档是时间控制、输入映射、预加载和运行时性能的唯一事实源。媒体格式由 delivery-selection.md 决定;两者独立。

先读预算结果

build/motion-budget.json 必须分别给出:

  • delivery.selected:使用哪种媒体和渲染器。
  • runtime.controller:输入如何控制媒体时间。
time_control runtime.controller 行为
scrub frame-scrub 输入值持续映射到帧或时间
segment-play segment-playback 输入选择相邻状态,片段按时间播放
autonomous autonomous-playback 媒体时间自行推进

不要根据 scroll、分页布局或视频格式猜控制器。滚动既可以 scrub,也可以触发片段播放。

时间轴清单

所有控制器只读取编译生成的 build/timeline.json,页面中不得维护第二份时间常量。

schemaVersion: 1
fps: <实际编码帧率>
frameDuration: <1 / fps>
initialState: <稳定状态 ID>
states:
  - id: <页面与运行时共同使用的状态 ID>
    hold: <该状态的停帧时间>
segments:
  - id: <稳定标识>
    from: <起点状态 ID>
    to: <终点状态 ID>
    start: <本段第一张可见帧的时间,包含>
    hold: <本段结束后应停留的最后可见帧时间>
    endExclusive: <本段编码边界,不包含>
    curve:
      type: constant | edge-mid-edge
      rate: <constant 使用>
      edgeRate: <edge-mid-edge 使用>
      midRate: <edge-mid-edge 使用>

硬性语义:

  • start <= hold < endExclusive。
  • 停止播放时只能落在 hold,不能落在 endExclusive。
  • states 顺序必须与片段的 from → to 一致;页面状态直接使用这些 ID,不另建索引映射表。
  • 所有时间都来自最终编码后的实际帧,不从生成时长或原始素材手工推算。
  • 裁剪、拼接或重新编码后必须重新生成清单。

frame-scrub

适用于“输入停在哪里,画面就停在哪里”的交互。使用 assets/interactive-motion.ts 的 createFrameAnimator 管理目标帧、阻尼和反向。

一维映射:

progress = clamp((value - start) / (end - start), 0, 1)
targetFrame = progress * (frameCount - 1)

环形输入使用最短环形距离;二维输入使用二维采样网格,不能压成一维进度。输入事件只更新目标值,实际渲染集中在 requestAnimationFrame。

视频 scrub 每次只提交最新整数目标帧,丢弃过时 seek。需要随机访问或快速反向时使用全关键帧视频,并验收 seek 延迟。

segment-playback

适用于“输入选择下一状态,动作随后自行完成”的交互。使用 assets/interactive-motion.ts 的 createSegmentPlayer,不要在页面重新实现播放状态机。

必须满足:

  1. 生产时可以分段生成,但连续链交付前必须合并;运行时使用一个持续存在的媒体实例,切换状态不替换 src、视频节点或图片层。
  2. 输入一发生就启动媒体;页面导航或其他几何动画可以并行,不等待动作结束。
  3. 反向输入先取消当前播放,再从当前 currentTime 向上一状态撤回。
  4. 前进可以使用用户输入触发的 video.play()。反向不能依赖浏览器支持负 playbackRate,由共享控制器按时间轴回放。
  5. 使用 requestVideoFrameCallback,无支持时回退 requestAnimationFrame;不得用低频 timeupdate 判断停帧。
  6. 接近目标时先暂停,再精确设到 hold,避免越过目标后回跳。
  7. 播放速率曲线属于时间轴清单。需要两端快、中间慢时使用 edge-mid-edge;正放与倒放读取同一曲线。
  8. 快速连续输入只保留最新目标,旧任务必须可取消。

桌面与移动媒体版本在初始化时选择。普通 resize 不换源;确需重载另一版本时,恢复到当前状态的 hold 后再继续。

分页导航只负责选择目标状态 ID。它不能把分段时间轴改成多个互不相关的视频,也不能用页面切换遮盖媒体接缝。

分步手势策略

segment-playback 与分页导航组合时,必须从 Motion Brief 读取 gesture_policy:

  • one-gesture-one-step:把同一次滚轮或触控板惯性序列合并成一个方向意图,不能按每个原始事件连续跳状态。
  • while_active:明确新输入是重定向、排队还是忽略;默认需要可反向的交互使用 retarget。
  • boundary:首尾状态执行 clamp 或合同明确的 loop,不能越界创建空状态。
  • programmatic_navigation=ignore:页面自身的平滑滚动和位置校正不得再次触发媒体状态变化。
  • 页面元素的状态 ID 必须直接匹配 timeline.json.states[].id;找不到、重复或顺序不一致时在初始化阶段失败。

从 assets/step-gesture.ts 的 createStepGestureAdapter 开始实现。滚轮、触控板或触摸层只把方向增量送入该适配器;手势阈值、惯性结束判定和程序化导航锁不得在页面监听、媒体控制器和分页组件中各写一份。

autonomous-playback

适用于待机、循环或进入可见区域后自行播放的动画。浏览器允许时使用 muted playsinline;需要声音或浏览器阻止自动播放时,等待明确用户手势。循环只在素材本身通过首尾接缝验收时启用。

渲染器

Alpha 图集

  • 单元格尺寸统一,并至少覆盖最大 CSS 尺寸乘目标 DPR。
  • 清单保存帧数、行列、单元格尺寸和参数映射。
  • 切帧只更新 background-position,不创建多张透明图片交叉淡化。
  • 默认单张纹理不超过 4096;超预算回到自动路线选择。

Chroma 视频

  • 视频保留均匀色键,由 chroma-video-renderer.ts 绘制透明 Canvas。
  • 页面背景、文字和其他视觉层位于 Canvas 外部。
  • 运行时从 compile.json.runtime.keying 读取全部参数,不在页面另写阈值。
  • WebGL 或视频失败时显示静态 Alpha 降级图,不能露出色键母版。

Baked 视频

  • 视频本身包含完整画面,不做抠色、色键或背景合成。
  • 可由视频元素直接显示,也可绘制到 Canvas。
  • 失败时显示普通 poster.png。

输入、布局与生命周期

  • pointermove、scroll 和触摸事件只记录输入,不在事件回调中反复写 DOM。
  • 布局变化后重新读取主体位置;不要永久缓存 getBoundingClientRect()。
  • 使用 IntersectionObserver 暂停离屏计算,使用 ResizeObserver 更新布局。
  • 手机方向权限必须由用户手势请求;拒绝或不可用时回退触摸或静态状态。
  • 页面切后台时暂停;恢复后以最新目标和当前媒体时间继续。

预加载与降级

  • 图集预加载清单、静态帧和图集,并等待 Image.decode()。
  • 视频预加载静态降级、元数据和首个需要的媒体;loadedmetadata 前不得 seek 或播放。
  • 加载完成前只显示一张静态降级图或简洁加载层。
  • 资源失败时解除页面锁定并回退静态画面,不让次要动画阻塞页面。
  • prefers-reduced-motion 使用合同指定的静态状态,不自动播放或连续 scrub。

性能验收

  • 每个动画帧最多一次 DOM 写入;目标未变化时不重复渲染。
  • 离屏或状态稳定时停止 requestAnimationFrame。
  • 不同时渲染两张大图做“平滑”。
  • 在冷缓存、弱网、低端移动设备、快速反向和连续输入下检查。