fix(core): unpause timelines nested into the registered root - #3427
Open
miguel-heygen wants to merge 2 commits into
Open
fix(core): unpause timelines nested into the registered root#3427miguel-heygen wants to merge 2 commits into
miguel-heygen wants to merge 2 commits into
Conversation
A scene timeline authored as `gsap.timeline({ paused: true })` and nested with
`.add()` keeps its OWN playhead frozen. Seeking the root never advances it, the
root reads `duration()` as 0, and every captured frame renders the t=0 state.
The result is an entirely black video that lint, check and validate all pass,
because none of them looks at pixels.
The trap is that our contract invites it. We ask for the registered root to be
paused so the renderer owns the playhead; applying that to scene timelines too
is the natural misreading, and nothing said otherwise.
`ensureChildCandidatesActive` already did exactly this for sub-composition
children, but it finds them through `[data-composition-id]`, so timelines
combined by hand are invisible to it. This is the same fix for the other path.
Placement is load-bearing and was the subtle part: reading `duration()` while a
child is still paused caches 0 on the parent permanently, and `invalidate()`
does not clear it. So the unpause runs the moment the root timeline is
resolved, before anything can read its duration.
Not a GSAP regression — measured identical on 3.11.5, 3.12.5, 3.13.0, 3.14.0,
3.14.2 and the 3.15.0 we ship. No shipped block is affected; nothing in
registry/ nests a paused timeline.
Verified end to end at the pixel level on the reported pattern (two paused
scene timelines added to a paused master). Frame 150 of a 6s render:
0.8.10 gives 000000, this branch gives 71ae31.
Closes #3419.
Backstop for the same defect the unpause fixes. A timeline that owns children yet reports a duration of 0 cannot be correct, and the symptom is an entirely black render that lint, check and validate all pass, because none of them looks at pixels. There is no legitimate composition where children span time and the total is zero, so the check has no false-positive case. Anything that reaches it has a cause the unpause did not cover, which is exactly the case worth hearing about rather than capturing frozen frames in silence. Emitted to the console because the capture session forwards browser console output into the producer's diagnostics, which is the channel a render actually surfaces. Verified by disabling the unpause and rendering the reported composition: the warning fires through the real pipeline, naming the composition id and the child count.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Timelines nested into the registered root are unpaused before anything reads the root's
duration, so a composition built from paused scene timelines renders instead of coming out
black.
Closes #3419.
Why
A scene timeline authored as
gsap.timeline({ paused: true })and nested with.add()keeps its own playhead frozen. Seeking the root never advances it, the root reads
duration()as 0, and every captured frame renders the t=0 state. The output is an entirelyblack video, and
lint,checkandvalidateall pass, because none of them looks atpixels.
The trap is that our own contract invites it. We ask for the registered root to be paused
so the renderer owns the playhead. Applying that to scene timelines too is the natural
misreading:
Investigation
Not a GSAP regression. Measured identical on every version back to 3.11.5:
parent.duration()Not shipped breakage. Nothing in
registry/nests a paused timeline.The framework already solved this for the neighbouring path.
ensureChildCandidatesActive(init.ts) has been callingpaused(false)on child timelinesall along — but it finds them via
collectRootChildCandidates, which walks[data-composition-id]nodes and looks uptimelines[childId]. That is the sub-compositionpath. Timelines combined by hand are never registered under a composition id, so the
collector cannot see them.
How
One helper, called the moment the root timeline is resolved.
Placement is load-bearing and was the subtle part. Reading
duration()while a child isstill paused caches 0 on the parent permanently —
invalidate()does not clear it:dur=6, rendersdur=0, still blackinvalidate()dur=0, still blackSo the unpause runs before anything can touch the root's duration.
nested:falsekeeps itto one level: a grandchild belongs to its own parent's playhead, which the same pass fixes
when that parent is reached.
Test plan
End to end, at the pixel level, on the reported pattern — two paused scene timelines
added to a paused master, 6s at 30fps. Frame 150 sampled with ffmpeg:
hyperframes@0.8.10000000(black)71ae31(content)Unit test in
init.test.tsasserts a nested paused child ends up unpaused after init.Verified it fails on the unfixed code rather than assuming:
Full
packages/coresuite green: 2491 passed.Not covered
misreading. Saying the registered root would prevent it at the source. Left out of this
PR because it touches skills and docs surfaces that need their own sync.
dynamically, so a static rule would only duplicate it for the subset it can see.