Skip to content

fix(studio): Enable keyframes works (and auto-tracks) on elements with a global gsap.set#1728

Merged
miguel-heygen merged 4 commits into
mainfrom
fix/enable-keyframes-button
Jun 26, 2026
Merged

fix(studio): Enable keyframes works (and auto-tracks) on elements with a global gsap.set#1728
miguel-heygen merged 4 commits into
mainfrom
fix/enable-keyframes-button

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

Two related fixes for Enable keyframes on an element whose only animation is a global `gsap.set(...)` (e.g. a statically-positioned/3D-transformed element).

1. Enable keyframes was a silent no-op

resolveTimelinePositions walks animations in document order advancing a cursor. A global gsap.set carries no position arg, so it fell through to the cursor fallback and inherited the comp-end time (sum of every prior tween's duration) as its resolvedStart. But a global set is an off-timeline load hold — its start is 0.

promoteSetToKeyframes bails when playhead <= setStart, so with setStart = comp end, clicking the diamond at any playhead before the end did nothing. Fixed at the root in both parsers: a global set pins to resolvedStart = 0 and doesn't advance the cursor.

2. The generated 0% endpoint didn't auto-track

The _auto endpoint-sync (an unmodified 0%/100% follows its nearest neighbor until edited) was still in the parser, but no flow produced an _auto endpoint anymore — it was silently dropped in #1605. So a 0% the user never touched behaved as a fixed keyframe.

Now promoteSetToKeyframes marks the generated 0% (the held start the user didn't choose) as auto: true_auto: 1; the 100% (the real keyframe placed at the playhead) stays fixed.

How it was found

Added temporary strategic logging to the Enable-keyframes flow and reproduced in the studio — the log showed setStart: 4, blocked: true. Logging removed after the root cause was confirmed.

Tests

  • Parser regression: a global gsap.set resolves to resolvedStart: 0, not the cursor (recast + acorn).
  • promoteSetToKeyframes marks the 0% endpoint auto, leaves 100% fixed.
  • Verified end-to-end in the studio: the Card's gsap.set correctly becomes tl.to("#card", { keyframes: {...}, duration }, 0) with _auto: 1 on the 0%.

…ot the comp end

resolveTimelinePositions walks anims in document order advancing a cursor; a
global `gsap.set(...)` carries no position arg, so it fell through to the
cursor fallback and inherited the comp-end time (every prior tween's duration
summed) as its resolvedStart. A global set is a load-time hold — its start is 0.

This silently broke 'Enable keyframes' on any element whose only animation is a
global gsap.set (e.g. a statically-positioned card): promoteSetToKeyframes bails
when `playhead <= setStart`, and setStart was the comp end, so any playhead
before the end was a no-op. Pin a global set to resolvedStart 0 in both the
recast and acorn parsers; don't let it advance the cursor/prevStart.
…-tracking

When 'Enable keyframes' promotes a static set to a two-stop tween, the 0% (the
held start the user didn't choose) is now marked `auto: true` → serialized as the
`_auto: 1` marker. The parser's endpoint-sync then keeps it tracking the nearest
keyframe until the user edits it directly; the 100% (the real keyframe placed at
the playhead) stays fixed.

This re-wires the auto-endpoint behavior that was silently dropped in #1605 (the
sync logic stayed, but no flow produced an `_auto` endpoint anymore, so an
untouched 0% never tracked). Adds a guard test so it can't be lost again.
The resolveTimelinePositions guard added a branch (pushes it over threshold), and
changed-file scope re-surfaces pre-existing complex functions (readElementPosition,
applyArcWaypointAtPlayhead, the useEnableKeyframes callback). Bare directives only.
Dragging a static element positioned via the legacy --hf-studio-offset CSS var
(e.g. dot-a) flew off-screen — three independent failure modes, all fixed:

1. Live drag integrated: the per-move draft read its base from the live transform
   it set last frame (gsap.getProperty), so base+delta accumulated frame-over-frame.
   Fix: carry a stable baseGsap on the in-memory drag member (immune to mid-drag
   re-renders that wipe the data-hf-drag-* attrs) and use it as the fallback.

2. Commit re-added the delta: the source commit re-read the wiped attrs / live
   transform. Fix: re-stamp the stable base/initial attrs in applyManualOffsetDragCommit
   before the commit reads them.

3. Drop left it offset: the committed source was correct, but the LIVE element kept
   its --hf-studio-offset var + translate:var(...), which composed with the GSAP
   transform (rendered at dropped + offset) until a full reload. Fix: on cleanup,
   when GSAP owns the position, clearStudioPathOffset() migrates the element off the
   legacy CSS channel (leaving transform untouched) — matching the stripped source.

Adds a regression suite covering all three layers.
@miguel-heygen miguel-heygen merged commit 226a741 into main Jun 26, 2026
48 checks passed
@miguel-heygen miguel-heygen deleted the fix/enable-keyframes-button branch June 26, 2026 01:31
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.

1 participant