Skip to content

fix(studio): keep a keyframe edit from copying other tweens' channels - #4918

Merged
miguel-heygen merged 1 commit into
mainfrom
fix/studio-commit-keeps-sibling-channels
Oct 2, 2026
Merged

miguel-heygen merged 1 commit into
mainfrom
fix/studio-commit-keeps-sibling-channels

Conversation

@miguel-heygen

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

Copy link
Copy Markdown
Collaborator

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 rotationX keyframe tween and a sibling to(el, { rotation: 90, duration: 3 }). Setting RotX to 20 in the 3D panel at 2 s rewrote the first tween as keyframes: { "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

  • readAllAnimatedProperties no longer collects other tweens' channels. Its universal-baseline pass only ever added those channels, so it is gone too.
  • The element-dependent baseline pass still skips a channel another tween writes, now asked through the shared gsapWritesChannels, which sees every keyframe form.
  • Net: 2 files, source about -90 lines.

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.
  • Studio unit suite and typecheck pass.

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.

Before: the layer is held at 80 degrees after a RotX edit

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

After: the layer follows its rotation tween

Base automatically changed from fix/studio-gsap-owns-keyframe-arrays to main October 2, 2026 21:17
@miguel-heygen
miguel-heygen force-pushed the fix/studio-commit-keeps-sibling-channels branch from b5c2776 to a071dd6 Compare October 2, 2026 21:38
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Edit accuracy: accurate 1216 (base branch 1216), smooth 1130 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 (1)

@miguel-heygen
miguel-heygen marked this pull request as ready for review October 2, 2026 22:05

@jrusso1020 jrusso1020 left a comment

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.

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 result and only considered groupedPropKeys ∪ otherTweenProps. A grouped prop that isn't in result already failed readLiveGsapValue, 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): commitKeyframeProps spreads runtimeProps into the playhead keyframe and backfills it into every other keyframe (backfillDefaults, and the willExtend remap). A sibling channel there gives the element a second tween writing it. The grouped callers (resize, rotation) were already filtered by inGroup, so their behavior is unchanged.
  • The element-dependent pass's skip:
    • The old skip set read top-level vars only, minus the edited tween's own grouped keys.
    • gsapWritesChannels also 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 result and skipped by prop in result before this check, and its top-level non-numeric channels were in the old skip set too.

Tests I ran (vitest, real GSAP 3.15):

  • gsapRuntimeReaders.test.ts passes, 5 of 5.
  • The new test discriminates: with gsapRuntimeReaders.ts restored to the base, it fails with expected 80 to be 90, which matches the description.
  • The other GSAP and keyframe hook suites in src/hooks pass: 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):

  1. 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.
  2. The test's fake iframe has __timelines on contentWindow, but gsapWritesChannels reads el.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

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit dc9c38e Oct 2, 2026
86 of 87 checks passed
@miguel-heygen
miguel-heygen deleted the fix/studio-commit-keeps-sibling-channels branch October 2, 2026 22:20
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.

2 participants