Skip to content

fix(core): renders keep a fromTo's from-only values from the frame it starts on - #5125

Merged
miguel-heygen merged 4 commits into
mainfrom
fix/render-fromto-from-only-props
Oct 6, 2026
Merged

miguel-heygen merged 4 commits into
mainfrom
fix/render-fromto-from-only-props

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

What

A rendered fromTo() with immediateRender: false keeps the properties that only its from-vars set, such as transformOrigin, from the frame the tween starts on. That matches Studio playback and 0.8.112 render output again.

Fixes #5122

Why

Since #4911, every seek is re-rendered silently from just below the target time, so same-time set() steps apply in authored order. When a tween starts exactly on the target frame, that step crosses back over its start. GSAP then reverts the values the tween applied at its start (its startAt). The forward pass re-applies only the properties in the to-vars, so a property set only in the from-vars, like transformOrigin, stays at its CSS default. From that frame on, the tween animates about the wrong origin.

The re-render already redoes a keyframed tween that starts on the target time, between the two passes. This change also re-applies any tween's start values there, keyframed or not, including each target of a stagger (those live in the stagger's own inner timeline). A set() authored later at the same time still wins, because the final forward pass applies the timeline in authored order after it.

Test plan

  • New: gsap.steps.test.ts seeks the issue's tween frame by frame, as a render does, and expects transformOrigin: 0% 0% from its first frame. It fails on main (the origin reads undefined from frame 3) and passes with the fix.
  • New, from review: a keyframed fromTo after another fromTo on the same element keeps its own from-only origin (fails on the first version of this PR and on main).
  • New, from review: every target of a staggered fromTo matches GSAP's own forward playback on each of 13 frames (fails on main at frame 6, the second target).
  • New: a set() authored later on the tween's start still wins over the restored from-values. This passes on both main and the branch, and pins the ordering fix(core): render GSAP set steps on the frame they land on #4911 protects.
  • The runtime adapter tests pass 3 runs in a row (262 tests), including every fix(core): render GSAP set steps on the frame they land on #4911 step and call test. The whole core suite passes (4219 tests), and so does test:hyperframe-runtime-seek.
  • Renders of the issue's composition (GSAP 3.14.2, the issue's exact command). White box bounds at frames 3, 4 and 5:
Frame 3 Frame 4 Frame 5
main x50-349 / y270-449 x54-345 / y272-447 x58-341 / y275-444
this branch x100-399 / y300-479 x100-391 / y300-474 x100-384 / y300-470

The branch matches the reporter's 0.8.112 bounds exactly. The yellow box, which sets no origin, is unchanged.

Not in this PR

  • A tween or set() authored before the fromTo that writes, at the fromTo's start time, a property only the fromTo's from-vars set, still wins over that from-value. With a set() it lasts the start frame only; with a tween ending exactly there it can last the whole fromTo. Main behaves the same, so this is not a regression; it needs the re-render's ordering reworked and is left as a follow-up.
  • A nested timeline that starts exactly on the seek time is left as GSAP's own seek leaves it: in a render, nothing of it shows on that frame and the correct values show from the next frame, the same on main. Scrubbing back onto that exact frame can still lose a from-only value there, as it can for the first target of a stagger that starts on the seek time.
  • One rare shape where GSAP disagrees with itself: a staggered fromTo with keyframes, immediateRender: false and a from-only property. Played frame by frame, GSAP never applies the later target's from-only value; jumping straight to a later frame, it does. Renders now show the value (what the author wrote); main matched frame-by-frame playback.
  • The independent review also found two cases this fixes beyond the issue: to() with startAt: {...}, and a fromTo starting on the seek time inside a nested timeline that started earlier.

Why a separate small PR

This fixes a render regression a user hit in production (#5122). The only other open change in this area is an unrelated Studio fix in another package, so this ships on its own rather than waiting on it.

Review

An independent adversarial review compared main, each head of this PR and GSAP's own forward seek over 32 scenarios, in render order and scrub order, on GSAP 3.14.2 and 3.15.0. The first version missed keyframed fromTo tweens (found in code review) and staggers (found by that review); both are fixed above with tests. A last pass over 40 scenarios (adding repeating, yoyo, from(), zero-gap, eased and nested staggers) found the rest matching GSAP; its remaining notes are the bullets under "Not in this PR".

Before / After

Frame 3 of the issue's composition: main on the left, this branch on the right.

Frame 3: main left, this branch right

@miguel-heygen
miguel-heygen marked this pull request as ready for review October 6, 2026 18:21

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2, should fix] Keyframed fromTo loses its from-only transform origin after an overlapping tween — packages/core/src/runtime/adapters/gsap.ts:119-129

At the start of a keyframed fromTo, primedAtItsStart() takes the keyframe branch and returns without rendering that tween's _startAt. A controlled seek reproduces a wrong rendered pivot: put three fromTo tweens on one element at 0, 0.1, and 0.2 seconds; set their from-only transformOrigin to 0% 0%, 100% 100%, and 0% 0% respectively, and give the third tween two y keyframes. Seek sequentially through [0, 2/30, .1, 4/30, .2, 7/30]. At .2 and 7/30, direct GSAP and the exact PR base adapter render 0% 0%, while this head renders the previous 100% 100%. The incorrect pivot persists after the landing frame, affecting the visual motion.

I reproduced this in an isolated test at head 22a52eb87548640cdc3be16d68ad4882290b3191 and verified the base adapter fixture byte-for-byte against base 5fad52f21d0cb4742245d0b13c012d53c952d7ef. The intended #5122 frame-3 fix and focused tests pass, but this overlapping keyframed/from-only case needs coverage and correction before approval. No repository changes were made in this review.

— Review by tai (pr-review)

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Edit accuracy: accurate 2055 (base branch 2055), smooth 1590 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • crop-none-px-r0-root-z100: tracking 0.04, pressJump 0, drop 40.03, reload 40.03, render 39.98, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.06, reload 0.06, render 0.03, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.06, reload 0.06, render 0.03, renderKey -, undo true, teleport true

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Thanks, reproduced and fixed (cfbbd59, plus a follow-on commit for staggers).

Your case at 22a52eb, seeking [0, 2/30, .1, 4/30, .2, 7/30]:

0.1 4/30 0.2 7/30
GSAP 100% 100% 100% 100% 0% 0% 0% 0%
base 0% 0% 0% 0% 0% 0% 0% 0%
22a52eb 100% 100% 100% 100% 100% 100% 100% 100%
cfbbd59 100% 100% 100% 100% 0% 0% 0% 0%

The base is right at 0.2 only because the 0.1 origin had already been lost there (the #5122 bug); it is wrong at 0.1 and 4/30.

The keyframe branch now re-applies the tween's start values after its keyframe prime, the same as any other tween starting on the seek time. Your case is a test: keeps a keyframed fromTo's from-only values over an earlier tween's on the same element. It fails on 22a52eb and on the base, and passes now. With both fixes, adapter tests pass 3 runs in a row (262), the core suite passes (4219), and the issue's render still matches 0.8.112 exactly.

Checking the same path turned up one more case: each target of a stagger keeps its start values in the stagger's inner timeline, which the priming step never visited, so a later target lost its from-only values in renders (on the base too). The priming now descends into that timeline, and a test compares every target with GSAP's own playback on each of 13 frames. The PR body lists what is still left, both also on the base.

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved at exact head 1e9f4d174c5a7d7701436037cd2f9570abc6e981 against base 5fad52f21d0cb4742245d0b13c012d53c952d7ef. This follow-up fixes my prior keyframed-overlap finding: the three-tween fromTo probe now matches direct GSAP at and after the keyframed start. Both added keyframe/stagger tests fail against the old head and pass here; focused adapter tests passed 262/262 and the runtime-seek checks passed. Independent forward-render stagger probes found no new regression. All 11 required CI contexts passed at this head.

A repeated-stagger reverse-seek origin mismatch was reproduced on the exact PR base and both heads, so it is an inherited scrub-path limitation, not a newly introduced blocker for this forward-render fix. This approval supersedes my earlier changes-requested review. No merge or deployment performed by tai.

— Review by tai (pr-review)

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 755ab7d Oct 6, 2026
169 of 170 checks passed
@miguel-heygen
miguel-heygen deleted the fix/render-fromto-from-only-props branch October 6, 2026 20:41
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.

Render drops from-only fromTo properties when immediateRender is false (since 0.8.113)

2 participants