feat(studio): runtime-first dynamic keyframe system [8/10]#1190
feat(studio): runtime-first dynamic keyframe system [8/10]#1190miguel-heygen wants to merge 1 commit into
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
6e391b2 to
eca150f
Compare
0dbfe93 to
925397e
Compare
eca150f to
dcbe8da
Compare
925397e to
4711c17
Compare
dcbe8da to
3a8e22f
Compare
4711c17 to
32dd0bb
Compare
3a8e22f to
0eb2b93
Compare
32dd0bb to
7f991c4
Compare
0eb2b93 to
626983d
Compare
jrusso1020
left a comment
There was a problem hiding this comment.
The most architecturally consequential PR in the back half of the stack. +773/-38. Reshapes keyframe discovery from "AST-only" to "runtime-first": Studio reads tween.vars.keyframes from the live iframe rather than relying solely on the AST parse. Plus materializeKeyframesInScript / unrollDynamicAnimations to convert dynamic loops into individual tl.to() calls on first edit.
The key risk: AST↔runtime divergence
The "runtime-first" model means there are now two sources of truth for what a tween is:
- AST parse of the source script.
- Live
tween.varsfrom the running iframe.
Some keyframes (the "dynamic" ones — generated by a JS loop like for (let i = 0; i < 10; i++) tl.to(...)) only exist in the runtime view, not the AST. The hasUnresolvedKeyframes / hasUnresolvedSelector flags + materializeKeyframesInScript are the bridge.
Things to verify:
1. Materialization is idempotent
Calling materializeKeyframesInScript on a script that's already been materialized should be a no-op. Otherwise repeated edits compound the unroll and the script grows linearly with edit count.
2. Runtime scan interval
PR body says "interval-based runtime scan". What interval? If it's too tight (< 200ms), it's a perf cost on every Studio session. If too loose (> 2s), the panel feels stale during animation creation. Worth a comment in the code explaining the chosen value.
3. Cold-load race + retry effect
"Retry effect for cold-load animation fetch race" — this hints at a known timing issue. The retry strategy matters:
- Bounded retries: if the runtime never produces tween data, the retry should eventually give up (not loop infinitely).
- Exponential backoff or fixed interval: either is fine, but document.
- User-visible feedback: if retry is exhausted, the user should see "couldn't load keyframes" rather than an empty panel.
4. unrollDynamicAnimations and edit semantics
When a dynamic loop like for (let i = 0; i < 10; i++) tl.to("#item-" + i, ...) is unrolled into 10 static tl.to() calls on first edit:
- The unroll is destructive — you lose the original loop expression. Confirm this is documented as an intentional, irreversible transformation (user can undo at the AST level, but they can't get the original loop back without writing it again).
- Selector resolution: each unrolled call gets a concrete selector. If the loop used
"#item-" + i, the unrolled output should be"#item-0","#item-1", etc. Verify against tests. - Comment / docstring: the unroll comment ideally says "originally generated by loop at line N" so the human reader understands what happened.
5. easeEach placement fix
The fix is the right call — easeEach belongs inside the keyframes: {} object, not at the tween-vars level. This is consistent with GSAP's documented placement. The fix in this PR likely corrects a regression from earlier in the stack (looks like #1167's tween-level easeEach propagation logic had it at the wrong level for emit but right level for parse). Worth a quick parse↔serialize round-trip test that asserts easeEach ends up in the same JSON shape on both sides.
6. Dual-write cache keys
"Dual-write keys" in the keyframe cache — what's the second key for? Migration from old key shape? Coexistence with the legacy code path? Worth a comment explaining the dual-write window (and when one of them can be removed).
This is a meaty PR — I'd push for a screenshot or video showing the live-runtime keyframe panel responding to a scrub before merging.
Review by Jerrai (hyperframes specialist)
…lization Read GSAP keyframe data from the live runtime instead of only the AST parser. Dynamic keyframes (loops, variables, computed selectors) now show diamonds on timeline clips and animation cards in the design panel. On first edit, dynamic code is automatically materialized: - Unresolved keyframes (keyframes: kf) replaced with static object - Unresolved selectors (tl.to(sel, ...)) entire loop unrolled into individual static tl.to() calls per element Key changes: - Parser: hasUnresolvedKeyframes/hasUnresolvedSelector flags - Runtime bridge: scanAllRuntimeKeyframes reads tween.vars from iframe - Tween cache: interval-based runtime scan for dynamic animations - materializeKeyframesInScript + unrollDynamicAnimations parser functions - Keyframe cache dual-writes both sourceFile#id and index.html#id keys - commitMutation updates cache from mutation response - easeEach placement fix (inside keyframes object, not tween vars)
626983d to
7149806
Compare
7f991c4 to
524dc79
Compare

Summary
tween.vars.keyframes) instead of AST onlyhasUnresolvedKeyframes/hasUnresolvedSelectorflags on GsapAnimationscanAllRuntimeKeyframesdiscovers all keyframed tweens in the iframematerializeKeyframesInScript+unrollDynamicAnimations— on first edit, dynamic loops are replaced with individual static tl.to() callseaseEachplacement fix (inside keyframes object, not tween vars level)+773 LOC