Skip to content

Add AnimationTimeline startOffset and compositionStartOffset runtime support with signed offset playback#3606

Open
ooLittlePanda wants to merge 1 commit into
mainfrom
feature/jaxonbai_timeline_offset
Open

Add AnimationTimeline startOffset and compositionStartOffset runtime support with signed offset playback#3606
ooLittlePanda wants to merge 1 commit into
mainfrom
feature/jaxonbai_timeline_offset

Conversation

@ooLittlePanda

Copy link
Copy Markdown
Collaborator

补齐 PAGX 运行时缺失:

  1. 图层延迟起播:timeline运行 N 帧后才开始播放,延迟期间内容冻结在首帧
  2. Composition动画起点偏移:控制动画开始播放后,首帧落在哪里

@codecov-commenter

codecov-commenter commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.23810% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.83%. Comparing base (4a8fb4f) to head (4512f69).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
src/pagx/PAGAnimation.cpp 64.28% 6 Missing and 4 partials ⚠️
src/pagx/runtime/PAGComposition.cpp 87.50% 0 Missing and 1 partial ⚠️
src/pagx/xml/XMLBuilder.h 75.00% 0 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

* 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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

公开注释未提示该字段仅在 LoopMode::Once 下正确。PAGAnimation.h 的私有成员注释已说明:Loop/PingPong 下偏移作用于 WrapTime 之后,每个循环前 [0,offset) 被夹到首帧、尾部 [duration-offset,duration) 被丢弃,行为不正确。API 使用者只读此公开头,无法感知该限制。建议将此限制补进本注释;或在 spawnTimelines 检测到 offset!=0 且 loop!=Once 时上报 warning,避免静默产出错误画面。

Comment thread include/pagx/nodes/AnimationTimeline.h Outdated
* 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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startOffset 与 compositionStartOffset 都是「帧偏移」,但语义差异大:前者延迟整体起播且内容完整播放,后者偏移求值点并裁剪内容;正值下二者在起始阶段观感相似(都保持首帧),易被误用。二者分别对应 PAG 的 Layer.startTime 与 compositionStartTime,必要性成立、无需删减,但建议在注释中补一句二者区别与选型指引。

Comment thread src/pagx/runtime/PAGComposition.cpp Outdated
}
auto timeline = std::shared_ptr<PAGAnimation>(
new PAGAnimation(animation, binding.get(), document, scene));
if (animationDriver->startOffset > 0 && animation->frameRate > 0.0f) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

新增运行时测试全部使用 LoopMode::Once。已知有缺陷的 Loop/PingPong 路径(见 compositionStartOffset 的限制说明),以及 startOffset 与 compositionStartOffset 同时设置的组合,均无测试覆盖。建议补一个记录当前 Loop 行为的用例(或标注 TODO)与一个双字段组合用例,为已知限制提供回归守护。

Comment thread include/pagx/nodes/AnimationTimeline.h Outdated
* 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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

【startOffset 宿主层级建议商榷】startOffset 对应 PAG 的 Layer.startTime,语义是「子 Layer 在 parentComposition 时间轴上何时开始出现」,本质是 parentComposition 对其子 Layer 的时间调度信息。当前把它挂在 AnimationTimeline(Animation driver)上存在几个不一致:

  1. 与 PAGX 时间模型不符:PAGX 里 Layer 无独立时间概念,时间都归属于 Composition 的 timeline。「某个子 Layer 何时出现」应属于 parentComposition 的 timeline 调度,而非驱动该 Layer 内容的某个 Animation driver。

  2. 宿主不唯一:一个 Layer 可挂多个 AnimationTimeline,而「Layer 何时开始」是图层级的单一锚点,挂在 driver 上会导致语义分散(多个 driver 各有 startOffset 时如何取舍?)。

  3. 覆盖不全:纯内容 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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

【整体设计建议: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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

【补充设计边界: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 运行时再套偏移。烘焙后:

  1. 属性动画的时间基准即 PAGX 时间轴本身,无需运行时再偏移;
  2. startOffset(可见窗口,纯渲染 gate,时间照走)与属性求值互不冲突——窗口外照常求值得到的就是烘焙后的正确值,恢复显示无跳变;
  3. compositionStartOffset 只服务「引用可复用的独立 Animation / 子合成、需运行时错位播放」的场景,不承担属性动画的时间基准。

职责划分

  • 转换器:负责解算 PAG 的时间映射链,产出 PAGX 原生坐标的关键帧数据(烘焙)。
  • PAGX 运行时:只处理两件事——可见窗口渲染 gate(startOffset + duration,归父 Composition)、可复用内容求值偏移(compositionStartOffset,归 Layer timeline)。

这样三条时间线(属性动画基准 / 可见窗口 / 内容求值)不会在运行时纠缠:属性动画在转换时被烘焙消解,运行时只剩正交的两层偏移。建议在 startOffset 重新设计时把这条边界一并纳入,确保模型对 PAG 可无损转换。

@ooLittlePanda
ooLittlePanda force-pushed the feature/jaxonbai_timeline_offset branch from df7500a to 4512f69 Compare July 21, 2026 09:29
* 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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个不是composition的,应该是放在channel那边。控制layer visible

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这么看,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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

【汇总建议:可见性应下沉为节点的 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 跟踪。

Comment thread src/pagx/PAGAnimation.cpp
resolveTargets();
}
ApplyResolved(resolvedTargets, animation, effectiveBinding, currentTimeUs, clamped);
auto evaluationTimeUs = std::max<int64_t>(0, currentTimeUs - compositionStartOffsetMicros);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个直接叫startOffset就行

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants