Add AnimationTimeline startOffset and compositionStartOffset runtime support with signed offset playback#3606
Add AnimationTimeline startOffset and compositionStartOffset runtime support with signed offset playback#3606ooLittlePanda wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3606 +/- ##
==========================================
+ Coverage 82.57% 82.83% +0.26%
==========================================
Files 707 707
Lines 91382 93023 +1641
Branches 25871 26204 +333
==========================================
+ Hits 75461 77059 +1598
+ Misses 10437 10423 -14
- Partials 5484 5541 +57 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| * frame until the offset elapses), a negative offset skips ahead (content starts from that | ||
| * frame). Carries the semantics of PAG's compositionStartTime. Default is 0. | ||
| */ | ||
| int64_t compositionStartOffset = 0; |
There was a problem hiding this comment.
公开注释未提示该字段仅在 LoopMode::Once 下正确。PAGAnimation.h 的私有成员注释已说明:Loop/PingPong 下偏移作用于 WrapTime 之后,每个循环前 [0,offset) 被夹到首帧、尾部 [duration-offset,duration) 被丢弃,行为不正确。API 使用者只读此公开头,无法感知该限制。建议将此限制补进本注释;或在 spawnTimelines 检测到 offset!=0 且 loop!=Once 时上报 warning,避免静默产出错误画面。
| * timeline reaches frame N; before that the Animation stays at its first frame. Carries the | ||
| * semantics of PAG's Layer.startTime. Default is 0 (starts immediately). | ||
| */ | ||
| int64_t startOffset = 0; |
There was a problem hiding this comment.
startOffset 与 compositionStartOffset 都是「帧偏移」,但语义差异大:前者延迟整体起播且内容完整播放,后者偏移求值点并裁剪内容;正值下二者在起始阶段观感相似(都保持首帧),易被误用。二者分别对应 PAG 的 Layer.startTime 与 compositionStartTime,必要性成立、无需删减,但建议在注释中补一句二者区别与选型指引。
| } | ||
| auto timeline = std::shared_ptr<PAGAnimation>( | ||
| new PAGAnimation(animation, binding.get(), document, scene)); | ||
| if (animationDriver->startOffset > 0 && animation->frameRate > 0.0f) { |
There was a problem hiding this comment.
startOffset 声明为 int64_t,但此处仅 >0 分支生效:负值不建缓冲、不推进延迟,也未经 Importer 上报(其它非法属性会 ReportError)。负延迟虽无意义,但静默丢弃与项目「异常应上报」的一致性不符,建议对 startOffset<0 上报一次告警。
| * 0 and red on its last frame 30, so the three sampled instants distinguish "still delayed", | ||
| * "playing" and "reached the end". | ||
| */ | ||
| PAGX_TEST(PAGXRuntimeTest, AnimationTimelineStartOffset) { |
There was a problem hiding this comment.
新增运行时测试全部使用 LoopMode::Once。已知有缺陷的 Loop/PingPong 路径(见 compositionStartOffset 的限制说明),以及 startOffset 与 compositionStartOffset 同时设置的组合,均无测试覆盖。建议补一个记录当前 Loop 行为的用例(或标注 TODO)与一个双字段组合用例,为已知限制提供回归守护。
| * timeline reaches frame N; before that the Animation stays at its first frame. Carries the | ||
| * semantics of PAG's Layer.startTime. Default is 0 (starts immediately). | ||
| */ | ||
| int64_t startOffset = 0; |
There was a problem hiding this comment.
【startOffset 宿主层级建议商榷】startOffset 对应 PAG 的 Layer.startTime,语义是「子 Layer 在 parentComposition 时间轴上何时开始出现」,本质是 parentComposition 对其子 Layer 的时间调度信息。当前把它挂在 AnimationTimeline(Animation driver)上存在几个不一致:
-
与 PAGX 时间模型不符:PAGX 里 Layer 无独立时间概念,时间都归属于 Composition 的 timeline。「某个子 Layer 何时出现」应属于 parentComposition 的 timeline 调度,而非驱动该 Layer 内容的某个 Animation driver。
-
宿主不唯一:一个 Layer 可挂多个 AnimationTimeline,而「Layer 何时开始」是图层级的单一锚点,挂在 driver 上会导致语义分散(多个 driver 各有 startOffset 时如何取舍?)。
-
覆盖不全:纯内容 Layer(引用 Composition 但无 Animation driver)无法表达 startOffset。
建议:将 startOffset 上移到 parentComposition 的 timeline 层(针对子 Layer 的调度),对齐 PAG 的 Layer.startTime 语义。compositionStartOffset 保持在 AnimationTimeline 上是合理的——它描述 driver 引用的动画从哪一帧求值,属于 driver 层行为,无需调整。
| @@ -44,6 +45,23 @@ class AnimationTimeline : public Timeline { | |||
| */ | |||
| bool playing = true; | |||
|
|
|||
There was a problem hiding this comment.
【整体设计建议:startOffset 与 compositionStartOffset 应分属两层】
讨论后认为这两个参数描述的是方向相反的两种关系,不应同挂在 AnimationTimeline 上:
1. compositionStartOffset —— 挂 timeline 合理,保留
它描述「这个 Layer 实例如何播放它引用的内容(Animation 的求值起点)」,是向内的关系,属于 Layer timeline 的行为。正向、无需反查,放在 AnimationTimeline 上正确。(唯一遗留问题:Loop/PingPong 下 post-wrap 求值不正确,PR 已标注 deferred,建议补 TODO 跟踪。)
2. startOffset —— 宿主与语义都需重新定位
(a) 语义:它对应 PAG 的 Layer.startTime,本质是「父 Composition 里这个子 Layer 何时显示」,是Layer 的可见性调度,而非「动画延迟起播」。当前 PR 的实现是「延迟 animation、期间冻结首帧、Layer 始终可见」,与 PAG 的「窗口外 Layer 不显示」语义不一致。
(b) 宿主:可见性调度是父 Composition 对其子 Layer 的职责(父拥有 layers 列表)。timeline 挂 Layer 是对的(用哪个 timeline 驱动是 Layer 实例的属性),但「子 Layer 何时显示」是往外一层的父调度,不该塞进 Layer 的 driver。当前 PR 已暴露矛盾——字段定义在 AnimationTimeline,消费却在 PAGComposition::advance(),定义与消费不在同一层。
(c) 语义边界(建议):可见窗口应为纯渲染 gate——窗口外 skip draw,但 Layer 属性 / timeline / DataBind 照常求值推进(时间不停),恢复显示时状态连续无跳变。不做「时间冻结」。
3. 需要补 duration
PAG 的 Layer.startTime 总是配 duration,可见窗口是区间 [start, start+duration)。当前 PR 只有 startOffset(起点)无 duration(长度),无法表达 PAG 的「图层在某帧消失」。为保证 PAG→PAGX 可无损转换,需要补 duration。
4. 转换目标而非完全对齐
PAG 用一个公式(compositionOffset = compositionStartTime - startTime)把「可见窗口调度」和「内容求值映射」两个偏移揉在一起。PAGX 不必照抄实现,但模型需能表达这两种能力以支持 PAG 无损转换。把它们拆到正确的两个宿主(可见窗口→父 Composition 渲染 gate;求值偏移→Layer timeline),反而比 PAG 更清晰。
建议:startOffset 从 AnimationTimeline 移出,与 duration 一起作为父 Composition 对子 Layer 的可见性调度(渲染 gate);compositionStartOffset 保留在 AnimationTimeline。若本次 PR 范围有限,至少 startOffset 不宜加深 Layer/driver 耦合,可考虑拆分为后续独立设计。
| @@ -44,6 +45,23 @@ class AnimationTimeline : public Timeline { | |||
| */ | |||
| bool playing = true; | |||
|
|
|||
There was a problem hiding this comment.
【补充设计边界:Layer 属性动画的时间偏移由转换阶段烘焙,不进运行时】
承接上一条关于 startOffset / compositionStartOffset 分层的讨论,再钉一条边界,避免把 PAG 的时间映射链误带进 PAGX 运行时:
PAG 侧:Layer 的可动画属性(transform / alpha / mask / effect 等)关键帧是相对 Layer 局部时间轴的,实际生效时刻要经过 startTime 平移、compositionStartTime 映射、stretch/timeScale 拉伸这一整条链解算。属性动画和 startTime 是联动的。
PAGX 侧:属性动画是 Composition 内的 Animation → channel 关键帧,运行时按 Animation 自身时间轴直接求值(channel->evaluateAt(microseconds, frameRate),见 PAGAnimation.cpp ApplyResolved),关键帧时间就是 PAGX 原生坐标,不存在 PAG 那套逐层时间映射。
由此确定的转换边界:PAG → PAGX 转换时,Layer 属性动画应在转换阶段就把 PAG 的完整时间映射解算完、烘焙成 PAGX 坐标系下的新关键帧,而不是原样搬运、指望 PAGX 运行时再套偏移。烘焙后:
- 属性动画的时间基准即 PAGX 时间轴本身,无需运行时再偏移;
- startOffset(可见窗口,纯渲染 gate,时间照走)与属性求值互不冲突——窗口外照常求值得到的就是烘焙后的正确值,恢复显示无跳变;
- compositionStartOffset 只服务「引用可复用的独立 Animation / 子合成、需运行时错位播放」的场景,不承担属性动画的时间基准。
职责划分:
- 转换器:负责解算 PAG 的时间映射链,产出 PAGX 原生坐标的关键帧数据(烘焙)。
- PAGX 运行时:只处理两件事——可见窗口渲染 gate(startOffset + duration,归父 Composition)、可复用内容求值偏移(compositionStartOffset,归 Layer timeline)。
这样三条时间线(属性动画基准 / 可见窗口 / 内容求值)不会在运行时纠缠:属性动画在转换时被烘焙消解,运行时只剩正交的两层偏移。建议在 startOffset 重新设计时把这条边界一并纳入,确保模型对 PAG 可无损转换。
…nd signed content evaluation shift
df7500a to
4512f69
Compare
| * frame until the offset elapses), a negative offset skips ahead (content starts from that | ||
| * frame). Carries the semantics of PAG's compositionStartTime. Default is 0. | ||
| */ | ||
| int64_t compositionStartOffset = 0; |
There was a problem hiding this comment.
compositionStartOffset直接叫startOffset吧
| * nodes this animation drives are hidden. Carries the semantics of PAG's Layer.startTime combined | ||
| * with the layer visibility window. Default is 0 (visible from the first frame). | ||
| */ | ||
| Frame startOffset = 0; |
There was a problem hiding this comment.
这个不是composition的,应该是放在channel那边。控制layer visible
There was a problem hiding this comment.
这么看,animation的好像不用加startTime和duration
| * nodes this animation drives are hidden. Carries the semantics of PAG's Layer.startTime combined | ||
| * with the layer visibility window. Default is 0 (visible from the first frame). | ||
| */ | ||
| Frame startOffset = 0; |
There was a problem hiding this comment.
【汇总建议:可见性应下沉为节点的 visible channel,移除 Animation 的 startOffset / duration 复用与 composition 的 elapsedUs 特殊时钟】
本版把「可见窗口 gate」的方向做对了(比上版把 startOffset 放 AnimationTimeline 更进一步),但落点仍偏——从 AnimationTimeline 挪到了 Animation,可见性还是没落到正确的宿主。三个具体问题:
1. 可见性是节点/Layer 的属性,不是 Animation 的
applyWithVisibilityWindow 里对 resolved targets 写 binding->apply(node, "visible", ...),等于让 Animation 反向强制它所驱动的目标节点的 visible。方向拧了:可见性应是节点自己的一条 channel,由时间轴驱动,而不是在 Animation 定义里挂 startOffset 去 gate 别人的 visible。
2. duration 被双重语义复用
窗口 endUs = FramesToUs(startOffset + animation->duration),把 Animation 本来的 duration(动画时长)兼作可见窗口长度,两者绑死。动画播 30 帧但希望可见 60 帧(后段停末帧仍可见)这类场景无法表达。可见窗口与动画时长是独立概念,不该共用一个 duration。
3. elapsedUs 是为此新造的特殊时钟
为了有个「可见窗口参考钟」,PAGComposition 新增了 elapsedUs 单调时钟。若可见性走 channel 驱动,这个特殊时钟不需要——channel 的关键帧本就按时间轴求值。
建议方向(方案 A:可见性 = 一条 visible channel):
- 可见性下沉为节点的 visible bool channel,关键帧驱动可见区间(到某帧 visible=true/false)。
- 底层机制已具备:KeyValue 支持 bool;binding->apply(node, "visible", ...) 本版已在用;channel 求值已支持 bool discrete channel。可见性完全能走现有 Animation channel 驱动链路,无需新造。
- 收益:移除 Animation.startOffset 与 duration 的窗口复用;移除 PAGComposition.elapsedUs 特殊时钟;移除 applyWithVisibilityWindow 特殊路径。可见窗口与动画时长解耦(visible channel 想在哪段 true 就哪段);复用现有 channel 链路,零新机制。
compositionStartOffset 命名:可精简为 startOffset(求值偏移语义);注意与上面拟移除的 Animation.startOffset 不再冲突。其 Loop/PingPong 下 post-wrap 求值不正确的已知限制,建议补 TODO 跟踪。
| resolveTargets(); | ||
| } | ||
| ApplyResolved(resolvedTargets, animation, effectiveBinding, currentTimeUs, clamped); | ||
| auto evaluationTimeUs = std::max<int64_t>(0, currentTimeUs - compositionStartOffsetMicros); |
There was a problem hiding this comment.
evaluationTimeUs不用为去max,直接用currentTimeUs - compositionStartOffsetMicros就行了
| private: | ||
| PAGAnimation(Animation* animation, RuntimeBinding* binding, PAGXDocument* contextDoc, | ||
| std::weak_ptr<PAGScene> owner); | ||
| std::weak_ptr<PAGScene> owner, int64_t compositionStartOffsetMicros = 0); |
补齐 PAGX 运行时缺失: