fix(studio): keep a keyframe edit from copying other tweens' channels - #4918
Merged
Merged
Conversation
miguel-heygen
force-pushed
the
fix/studio-commit-keeps-sibling-channels
branch
from
October 2, 2026 21:38
b5c2776 to
a071dd6
Compare
Edit accuracy: accurate 1216 (base branch 1216), smooth 1130 of thoseThe gate passes. Quarantined, measured but not gated (1)
|
miguel-heygen
marked this pull request as ready for review
October 2, 2026 22:05
jrusso1020
approved these changes
Oct 2, 2026
jrusso1020
left a comment
Collaborator
There was a problem hiding this comment.
Reviewed at a071dd64: the full diff, all of readAllAnimatedProperties, gsapWritesChannels / keyframeVarsCarryChannel, and every caller of the reader. Approving.
What I checked:
- "The universal-baseline pass only ever added other tweens' channels" holds. It skipped any prop already in
resultand only consideredgroupedPropKeys ∪ otherTweenProps. A grouped prop that isn't inresultalready failedreadLiveGsapValue, and the pass would have read the same non-finite value again. So the only props it could add were sibling channels, and removing it removes exactly the copying. - Where the copy did harm (
useAnimatedPropertyCommit.ts:340):commitKeyframePropsspreadsruntimePropsinto the playhead keyframe and backfills it into every other keyframe (backfillDefaults, and thewillExtendremap). A sibling channel there gives the element a second tween writing it. The grouped callers (resize, rotation) were already filtered byinGroup, so their behavior is unchanged. - The element-dependent pass's skip:
- The old skip set read top-level
varsonly, minus the edited tween's own grouped keys. gsapWritesChannelsalso sees object, percent and array keyframes, so the same sibling no longer gets different treatment depending on how it was authored.- It also counts the edited tween. That's harmless: its numeric channels are already in
resultand skipped byprop in resultbefore this check, and its top-level non-numeric channels were in the old skip set too.
- The old skip set read top-level
Tests I ran (vitest, real GSAP 3.15):
gsapRuntimeReaders.test.tspasses, 5 of 5.- The new test discriminates: with
gsapRuntimeReaders.tsrestored to the base, it fails withexpected 80 to be 90, which matches the description. - The other GSAP and keyframe hook suites in
src/hookspass: 33 files, 364 tests. Two suites (useGsapSelectionHandlers,useKeyframeKeyboard) didn't load here because they need a built@hyperframes/player, and neither one reaches the reader.
Nits (not blocking):
- The new test asserts the end state only. Adding
expect(read).not.toHaveProperty("rotation")right after the read would name the cause directly, and would keep the test meaningful if the playback check were ever loosened. - The test's fake iframe has
__timelinesoncontentWindow, butgsapWritesChannelsreadsel.ownerDocument.defaultView. So the element-dependent skip isn't exercised here (jsdom's window has no__timelines). In the app both are the iframe's window, so this only affects test coverage.
Verdict: APPROVE
Reasoning: The removed pass could only ever add sibling tweens' channels, and that copying caused the bug. The remaining skip now treats every keyframe form the same, and the regression test fails on the old reader.
— Rames Jusso
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
Editing a keyframed tween in Studio at the playhead also copied the live value of every channel a different tween on the same element animates into the edited tween. Example: a layer with a
rotationXkeyframe tween and a siblingto(el, { rotation: 90, duration: 3 }). Setting RotX to 20 in the 3D panel at 2 s rewrote the first tween askeyframes: { "0%": { rotationX: 0, rotation: 80 }, "50%": { rotationX: 20, rotation: 80 }, "100%": { rotationX: 40, rotation: 80 } }. Two tweens then wrote rotation, and scrubbing back showed the layer stuck at 80° (0.5 s: 80° instead of the rotation tween's 28°).The grouped path already refused to do this (a rotation commit must not carry opacity from an intro tween). The ungrouped path still did, and only for siblings that wrote the channel as a top-level var: a sibling using keyframes was left alone, so the same edit wrote different things depending on how the other tween was authored.
How
readAllAnimatedPropertiesno longer collects other tweens' channels. Its universal-baseline pass only ever added those channels, so it is gone too.gsapWritesChannels, which sees every keyframe form.Tests
gsapRuntimeReaders.test.ts: real GSAP, an opacity keyframe tween plus a sibling rotation tween. It reads at 2 s, writes the edit the way the keyframe commit does (read values plus the edit at the playhead, backfilled into the other keyframes), then plays past the rotation tween's end. Rotation must be 90. On the old reader it is 80.Before
The fixture above at the base of this PR, after the RotX edit at 2 s, scrubbed to 0.5 s. The layer is turned 80°, and the Rotation row shows keyframes copied into the edited tween.
After
The same edit and scrub at this head. The edit writes only rotationX, and the layer follows the rotation tween (28° at 0.5 s).