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

153 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 运行时控制
本文档是时间控制、输入映射、预加载和运行时性能的唯一事实源。媒体格式由
[delivery-selection.md](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`,页面中不得维护第二份时间常量。
```yaml
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](../assets/interactive-motion.ts) 的
`createFrameAnimator` 管理目标帧、阻尼和反向。
一维映射:
```text
progress = clamp((value - start) / (end - start), 0, 1)
targetFrame = progress * (frameCount - 1)
```
环形输入使用最短环形距离;二维输入使用二维采样网格,不能压成一维进度。输入事件只更新目标值,实际渲染集中在 `requestAnimationFrame`。
视频 scrub 每次只提交最新整数目标帧,丢弃过时 seek。需要随机访问或快速反向时使用全关键帧视频,并验收 seek 延迟。
## segment-playback
适用于“输入选择下一状态,动作随后自行完成”的交互。使用
[assets/interactive-motion.ts](../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](../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`。
- 不同时渲染两张大图做“平滑”。
- 在冷缓存、弱网、低端移动设备、快速反向和连续输入下检查。