Skip to content

feat(studio): native GSAP keyframe editing system#1130

Open
miguel-heygen wants to merge 45 commits into
mainfrom
feat/enable-gsap-panel
Open

feat(studio): native GSAP keyframe editing system#1130
miguel-heygen wants to merge 45 commits into
mainfrom
feat/enable-gsap-panel

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented May 30, 2026

Summary

Complete GSAP keyframe editing system for HyperFrames Studio — drag to auto-keyframe, timeline diamond markers, toolbar toggle, clip-path/filter support, and design panel integration.

Core keyframe infrastructure

  • AST parser for 3 GSAP keyframe formats (percentage, object-array, simple-array) with 5 mutation functions
  • Spring physics solver with 5 presets (gentle, bouncy, stiff, wobbly, heavy)
  • 5 new Studio API mutation routes (add/remove/update keyframe, convert-to-keyframes, remove-all)

GSAP-aware drag pipeline

  • Async mutation flow with beforeReload callback — CSS offset stays visible during API call
  • Flat tweens auto-convert to keyframes on first drag
  • Fallback fetch when animation cache is stale (eliminates toolbar→drag timing race)
  • stripGsapTranslateFromTransform skipped during active drag gesture

Timeline keyframe diamonds

  • Diamond markers on clips with connecting lines, 80% clip height
  • Click to select + seek, teal highlight at playhead (0.05% tolerance)
  • Right-click context menu with ease picker, delete, copy properties
  • 0.1% percentage precision for sub-second keyframe placement

Timeline toolbar

  • Diamond toggle button: add/remove keyframe, convert flat tween, create new animation
  • Reads gsap.getProperty() from iframe for accurate multi-property keyframe values
  • Shows for any selected element (with or without existing animations)

Design panel integration

  • Property fields (X, Y, W, H, R) are keyframe-aware — edits update the nearest keyframe
  • clip-path and filter as string properties with preset buttons
  • Value constraints with per-property min/max/step clamping
  • KeyframeNavigation (◀ ◆ ▶) and KeyframeDiamond components

Runtime fixes

  • GSAP 3.x totalTime(0) no-op: nudge to t+0.001 then back to force initial keyframe render
  • Element visibility inclusive at duration boundary (<= instead of <)
  • Stale CSS offset artifacts stripped at runtime for GSAP-targeted elements
  • CSS offset persistence guard: never writes studio attributes to HTML for GSAP elements

Other fixes

  • Undo/redo: cache invalidation fires before preview reload
  • Toolbar animations use element's data-duration (not hardcoded 0.5s)
  • convertToKeyframesInScript uses CSS identity values (opacity:1, scale:1) not zero
  • Undo coalesce window reduced from 1500ms to 300ms
  • Animation position rounded to 3 decimal places

Behind STUDIO_KEYFRAMES_ENABLED flag, defaults to false. Enable with VITE_STUDIO_ENABLE_KEYFRAMES=true. Compact 48px track height.

Test plan

  • Enable flag, drag elements at various seek times — keyframes persist after release and page refresh
  • Flat tween auto-converts to keyframes on first drag
  • Timeline diamonds appear, click seeks to keyframe time
  • Toolbar diamond toggle adds/removes keyframes with correct interpolated values
  • Right-click diamond context menu: ease change, delete, copy properties
  • Design panel X/Y edits update keyframes (not CSS position) when keyframes exist
  • clip-path and filter editable via text input + presets in animation card
  • Undo/redo syncs keyframe cache and preview
  • Elements visible at exact end of composition (t=duration)
  • Flag off: no keyframe UI visible, existing behavior unchanged

@miguel-heygen miguel-heygen force-pushed the feat/enable-gsap-panel branch from 7f1c645 to b499484 Compare June 2, 2026 17:09
@miguel-heygen miguel-heygen changed the title feat(studio): enable GSAP design panel by default feat(studio): gsap keyframe editing — drag persistence, timeline diamonds, toolbar toggle Jun 2, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

Fallow audit report

Found 165 findings.

Duplication (113, showing 50)
Severity Rule Location Description
minor fallow/code-duplication packages/core/src/parsers/gsapParser.stress.test.ts:296 Code clone group 1 (10 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.stress.test.ts:313 Code clone group 2 (13 lines, 5 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.stress.test.ts:499 Code clone group 2 (13 lines, 5 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:281 Code clone group 1 (10 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:296 Code clone group 2 (13 lines, 5 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:312 Code clone group 2 (13 lines, 5 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:346 Code clone group 2 (13 lines, 5 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1096 Code clone group 3 (14 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1139 Code clone group 3 (14 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1197 Code clone group 4 (16 lines, 3 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1209 Code clone group 5 (10 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1210 Code clone group 6 (6 lines, 3 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1216 Code clone group 7 (6 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1224 Code clone group 4 (16 lines, 3 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1239 Code clone group 5 (10 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1240 Code clone group 6 (6 lines, 3 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1248 Code clone group 7 (6 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1257 Code clone group 4 (16 lines, 3 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1271 Code clone group 6 (6 lines, 3 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1451 Code clone group 8 (8 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.test.ts:1470 Code clone group 8 (8 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.ts:564 Code clone group 9 (7 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.ts:662 Code clone group 9 (7 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.ts:921 Code clone group 10 (8 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.ts:1172 Code clone group 11 (15 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.ts:1253 Code clone group 11 (15 lines, 2 instances)
minor fallow/code-duplication packages/core/src/parsers/gsapParser.ts:1316 Code clone group 10 (8 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:53 Code clone group 12 (15 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:100 Code clone group 15 (10 lines, 4 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:100 Code clone group 13 (13 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:100 Code clone group 14 (11 lines, 3 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:104 Code clone group 16 (7 lines, 5 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:112 Code clone group 17 (14 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:130 Code clone group 15 (10 lines, 4 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:130 Code clone group 13 (13 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:130 Code clone group 14 (11 lines, 3 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:134 Code clone group 16 (7 lines, 5 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:142 Code clone group 17 (14 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:160 Code clone group 18 (12 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:183 Code clone group 19 (12 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:202 Code clone group 18 (12 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:217 Code clone group 19 (12 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:235 Code clone group 15 (10 lines, 4 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:241 Code clone group 20 (23 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:246 Code clone group 21 (8 lines, 3 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:253 Code clone group 22 (8 lines, 3 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:283 Code clone group 23 (37 lines, 2 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:286 Code clone group 16 (7 lines, 5 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:325 Code clone group 14 (11 lines, 3 instances)
minor fallow/code-duplication packages/core/src/runtime/init.test.ts:325 Code clone group 15 (10 lines, 4 instances)

Showing 50 of 113 findings. Run fallow locally or inspect the CI output for the full report.

Health (52, showing 50)
Severity Rule Location Description
critical fallow/high-crap-score packages/core/src/parsers/gsapParser.ts:73 'resolveNode' has CRAP score 315.9 (threshold: 30.0, cyclomatic 36)
minor fallow/high-crap-score packages/core/src/parsers/gsapParser.ts:147 'selectorFromQueryCall' has CRAP score 49.5 (threshold: 30.0, cyclomatic 13)
minor fallow/high-crap-score packages/core/src/parsers/gsapParser.ts:225 'visitCallExpression' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
minor fallow/high-crap-score packages/core/src/parsers/gsapParser.ts:284 'resolveTargetSelector' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
minor fallow/high-crap-score packages/core/src/parsers/gsapParser.ts:313 'objectExpressionToRecord' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
minor fallow/high-crap-score packages/core/src/parsers/gsapParser.ts:404 'visitCallExpression' has CRAP score 31.6 (threshold: 30.0, cyclomatic 10)
minor fallow/high-crap-score packages/core/src/parsers/gsapParser.ts:961 'buildTweenStatementCode' has CRAP score 31.6 (threshold: 30.0, cyclomatic 10)
major fallow/high-crap-score packages/core/src/parsers/gsapParser.ts:1287 'resolveConversionProps' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
minor fallow/high-crap-score packages/core/src/parsers/gsapSerialize.ts:66 'lines' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
minor fallow/high-crap-score packages/core/src/parsers/gsapSerialize.ts:222 '<arrow>' has CRAP score 37.1 (threshold: 30.0, cyclomatic 11)
minor fallow/high-crap-score packages/core/src/parsers/gsapSerialize.ts:274 '<arrow>' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
minor fallow/high-cognitive-complexity packages/core/src/runtime/init.ts:30 'initSandboxRuntimeModular' has cognitive complexity 22 (threshold: 15)
critical fallow/high-crap-score packages/core/src/runtime/init.ts:264 'applyClipLayout' has CRAP score 567.6 (threshold: 30.0, cyclomatic 49)
minor fallow/high-crap-score packages/core/src/runtime/init.ts:477 'resolveAuthoredCompositionDurationFloorSeconds' has CRAP score 31.6 (threshold: 30.0, cyclomatic 10)
critical fallow/high-crap-score packages/core/src/runtime/init.ts:545 'resolveRootTimelineFromDocument' has CRAP score 385.6 (threshold: 30.0, cyclomatic 40)
minor fallow/high-crap-score packages/core/src/runtime/init.ts:651 'collectRootChildCandidates' has CRAP score 37.1 (threshold: 30.0, cyclomatic 11)
minor fallow/high-crap-score packages/core/src/runtime/init.ts:1022 'emitRootStageLayoutDiagnostics' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
minor fallow/high-crap-score packages/core/src/runtime/init.ts:1126 'onError' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
minor fallow/high-crap-score packages/core/src/runtime/init.ts:1190 'rebindTimelineFromResolution' has CRAP score 37.1 (threshold: 30.0, cyclomatic 11)
major fallow/high-crap-score packages/core/src/runtime/init.ts:1240 '<arrow>' has CRAP score 79.4 (threshold: 30.0, cyclomatic 17)
minor fallow/high-crap-score packages/core/src/runtime/init.ts:1358 'resolveDurationSeconds' has CRAP score 49.5 (threshold: 30.0, cyclomatic 13)
minor fallow/high-crap-score packages/core/src/runtime/init.ts:1790 'seekStandaloneRegisteredTimelines' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
critical fallow/high-crap-score packages/core/src/runtime/init.ts:1878 'transportTick' has CRAP score 482.4 (threshold: 30.0, cyclomatic 45)
major fallow/high-crap-score packages/core/src/runtime/init.ts:1998 'hardSyncAllMedia' has CRAP score 71.3 (threshold: 30.0, cyclomatic 16)
major fallow/high-crap-score packages/core/src/runtime/init.ts:2022 '<arrow>' has CRAP score 97.0 (threshold: 30.0, cyclomatic 19)
critical fallow/high-crap-score packages/core/src/runtime/init.ts:2179 'teardown' has CRAP score 106.4 (threshold: 30.0, cyclomatic 20)
critical fallow/high-crap-score packages/core/src/studio-api/routes/files.ts:434 '<arrow>' has CRAP score 160.0 (threshold: 30.0, cyclomatic 25)
critical fallow/high-crap-score packages/core/src/studio-api/routes/files.ts:605 '<arrow>' has CRAP score 238.6 (threshold: 30.0, cyclomatic 31)
minor fallow/high-crap-score packages/studio/src/components/StudioPreviewArea.tsx:149 '<arrow>' has CRAP score 42.0 (threshold: 30.0, cyclomatic 6)
critical fallow/high-crap-score packages/studio/src/components/TimelineToolbar.tsx:12 'interpolateKeyframeProperties' has CRAP score 240.0 (threshold: 30.0, cyclomatic 15)
critical fallow/high-crap-score packages/studio/src/components/TimelineToolbar.tsx:45 'readRuntimeKeyframeValues' has CRAP score 240.0 (threshold: 30.0, cyclomatic 15)
critical fallow/high-crap-score packages/studio/src/components/TimelineToolbar.tsx:122 'onToggle' has CRAP score 272.0 (threshold: 30.0, cyclomatic 16)
major fallow/high-crap-score packages/studio/src/components/TimelineToolbar.tsx:161 'TimelineToolbar' has CRAP score 90.0 (threshold: 30.0, cyclomatic 9)
minor fallow/high-crap-score packages/studio/src/components/editor/AnimationCard.tsx:76 'PropertyRow' has CRAP score 49.5 (threshold: 30.0, cyclomatic 13)
minor fallow/high-crap-score packages/studio/src/components/editor/manualEditsDom.ts:225 'stripGsapTranslateFromTransform' has CRAP score 43.1 (threshold: 30.0, cyclomatic 12)
major fallow/high-crap-score packages/studio/src/hooks/gsapRuntimeBridge.ts:93 'computeCurrentPercentage' has CRAP score 72.0 (threshold: 30.0, cyclomatic 8)
minor fallow/high-crap-score packages/studio/src/hooks/useAppHotkeys.ts:142 'handleUndo' has CRAP score 42.0 (threshold: 30.0, cyclomatic 6)
minor fallow/high-crap-score packages/studio/src/hooks/useAppHotkeys.ts:167 'handleRedo' has CRAP score 42.0 (threshold: 30.0, cyclomatic 6)
critical fallow/high-crap-score packages/studio/src/hooks/useAppHotkeys.ts:217 '<arrow>' has CRAP score 1892.0 (threshold: 30.0, cyclomatic 43)
minor fallow/high-crap-score packages/studio/src/hooks/useAppHotkeys.ts:372 'syncPreviewTimelineHotkey' has CRAP score 30.0 (threshold: 30.0, cyclomatic 5)
major fallow/high-crap-score packages/studio/src/hooks/useAppHotkeys.ts:419 'syncPreviewHistoryHotkey' has CRAP score 72.0 (threshold: 30.0, cyclomatic 8)
critical fallow/high-crap-score packages/studio/src/hooks/useDomEditCommits.ts:42 'isElementGsapTargeted' has CRAP score 240.0 (threshold: 30.0, cyclomatic 15)
critical fallow/high-crap-score packages/studio/src/hooks/useDomEditCommits.ts:166 'persistDomEditOperations' has CRAP score 992.0 (threshold: 30.0, cyclomatic 31)
minor fallow/high-crap-score packages/studio/src/hooks/useDomEditCommits.ts:399 'handleDomMotionCommit' has CRAP score 30.0 (threshold: 30.0, cyclomatic 5)
major fallow/high-crap-score packages/studio/src/hooks/useDomEditCommits.ts:426 'handleDomMotionClear' has CRAP score 72.0 (threshold: 30.0, cyclomatic 8)
critical fallow/high-crap-score packages/studio/src/hooks/useDomEditCommits.ts:455 'handleDomEditElementDelete' has CRAP score 306.0 (threshold: 30.0, cyclomatic 17)
minor fallow/high-crap-score packages/studio/src/hooks/useDomEditSession.ts:306 'handled' has CRAP score 30.0 (threshold: 30.0, cyclomatic 5)
critical fallow/high-crap-score packages/studio/src/player/components/Timeline.tsx:370 'handleAssetDrop' has CRAP score 160.0 (threshold: 30.0, cyclomatic 25)
minor fallow/high-crap-score packages/studio/src/player/components/TimelineCanvas.tsx:237 '<arrow>' has CRAP score 37.1 (threshold: 30.0, cyclomatic 11)
major fallow/high-crap-score packages/studio/src/player/components/TimelineCanvas.tsx:283 '<arrow>' has CRAP score 56.3 (threshold: 30.0, cyclomatic 14)

Showing 50 of 52 findings. Run fallow locally or inspect the CI output for the full report.

Generated by fallow.

Flip the STUDIO_GSAP_PANEL_ENABLED fallback from false to true. The
panel has been behind a feature flag since initial development; after
the bug bash (hf#1126) and soft-reload optimization (hf#1129) it is
stable enough for general use. Users can still disable it via
VITE_STUDIO_ENABLE_GSAP_PANEL=false if needed.
Add skewX, skewY, borderRadius, color, backgroundColor, borderColor,
filter, fontSize, letterSpacing to SUPPORTED_PROPS (11 → 20) and
introduce GsapPercentageKeyframe, GsapKeyframeFormat, GsapKeyframesData
types for native GSAP keyframes support.
The GSAP AST parser now recognizes the keyframes property on tweens
instead of dropping it. Supports all three GSAP v3 keyframe formats:

- Percentage objects: { "0%": { x: 0 }, "50%": { x: 100 }, "100%": { x: 200 } }
- Object arrays: [{ x: 0, duration: 0.5 }, { x: 100, duration: 1 }]
- Simple arrays: { x: [0, 100, 200], easeEach: "power2.inOut" }

Each format is normalized into GsapPercentageKeyframe[] with percentage
positions, per-keyframe properties, and optional ease. Three-level easing
(tween ease, easeEach, per-keyframe ease) is preserved. Keyframes tweens
correctly produce empty top-level properties since all animatable values
live inside the keyframes structure.
…emoveAll

Five new exported functions for in-place AST manipulation of native GSAP
percentage keyframes:

- addKeyframeToScript: insert at sorted position, replace if exists
- removeKeyframeFromScript: remove by percentage, collapse to flat if <2 remain
- updateKeyframeInScript: replace properties at an existing percentage
- convertToKeyframesInScript: convert flat to/from/fromTo to keyframes format
- removeAllKeyframesFromScript: collapse all keyframes to flat tween (last kf)

All follow the existing recast-based mutation pattern: parse AST, find target
animation by stable ID, modify nodes in place, reprint preserving formatting.
Wire five new GSAP mutation types through the existing
POST /projects/:id/gsap-mutations/* endpoint: add-keyframe,
remove-keyframe, update-keyframe, convert-to-keyframes, and
remove-all-keyframes. Each delegates to the corresponding
parser function added in the previous commit.
Damped harmonic oscillator solver that outputs SVG path data strings
compatible with GSAP CustomEase.create(). Supports underdamped (bouncy),
critically damped, and overdamped spring configurations.

Presets: gentle, bouncy, stiff, wobbly, heavy — registered in
SUPPORTED_EASES and EASE_LABELS so they appear in the Studio UI.
…n panel

Add KeyframeNavigation component (prev/diamond/next inline controls) that
enables per-property keyframe navigation in the Layout section of the
PropertyPanel. The diamond state reflects whether the current playhead is
on an existing keyframe (active), between keyframes (inactive), or has no
keyframes at all (ghost). Clicking the diamond converts, adds, or removes
a keyframe accordingly.

Wire the new add-keyframe, remove-keyframe, and convert-to-keyframes
mutations through useGsapScriptCommits -> useDomEditSession ->
DomEditContext -> StudioRightPanel -> PropertyPanel, following the
existing GSAP mutation pattern (commitMutation with undo history and
soft reload).
Add SpringEaseEditor component alongside the existing cubic-bezier editor
in both MotionPanel and EaseCurveSection. Users toggle between Bezier and
Spring modes via tab buttons in the Ease Curve section header.

The spring editor provides:
- Three sliders (mass 0.1-5, stiffness 10-500, damping 1-50)
- Five presets from the core spring solver (gentle, bouncy, stiff, wobbly, heavy)
- Live SVG curve preview matching the existing editor layout
- Debounced commit (120ms) to avoid flooding GSAP CustomEase.create calls

Adds @hyperframes/core/spring-ease subpath export so the studio can import
generateSpringEaseData and SPRING_PRESETS without pulling in the recast-
dependent gsap-parser subpath.
Split the Delete/Backspace hotkey handler:
- Delete key: deletes the selected timeline clip (unchanged)
- Backspace key: if the selected element has GSAP keyframes, removes
  all keyframes (collapsing to a flat tween) via the existing
  remove-all-keyframes mutation; falls through to delete if no
  keyframes are present

Adds removeAllKeyframes to useGsapScriptCommits and wires it through
useDomEditSession via a ref-bridge pattern in App.tsx (same pattern
used for domEditElementDelete).
Keyframe mutations (add, remove, convert-to-keyframes) now update the
player store cache immediately and persist to the server async. On
failure the cache rolls back to its pre-mutation state.

Adds a generic executeOptimistic utility that takes apply/persist/rollback
callbacks, used by the three keyframe mutation paths in useGsapScriptCommits.
…t menu, delete

Add multi-select, drag-to-reposition, right-click context menu, and
Delete key support for timeline keyframe diamonds:

- selectedKeyframes Set in player store with toggle/clear actions
- Shift+click diamonds to add/remove from selection
- Pointer-capture drag to reposition keyframes by percentage
- Right-click context menu with ease picker, delete, and copy properties
- Delete/Backspace prioritizes selected keyframes over clip deletion
- Wire mutation callbacks through NLELayout and StudioPreviewArea
When dragging GSAP-animated elements in the preview, position changes now
write to the GSAP script instead of CSS inline styles that GSAP overwrites
on the next seek:

- Flat tweens: updates x/y properties directly via update-property mutation
- Keyframed tweens: adds/updates a keyframe at the current playhead
  percentage with the new x/y values (auto-keyframing)
- Studio offset is cleared after the GSAP commit since GSAP handles
  position after the soft-reload

The intercept point is handleDomPathOffsetCommit, which checks for GSAP
animations with position properties before falling through to the standard
CSS path. GSAP position helpers live in gsapDragCommit.ts as pure functions
to keep useDomEditCommits.ts under the 600-line limit.
When dragging GSAP-animated elements, the studio now reads the actual
interpolated x/y from window.gsap.getProperty() in the preview iframe
at the current seek time. This eliminates the conflict between CSS
translate offsets and GSAP transforms that caused previous approaches
to fail.

Drag commit path:
- Keyframed tweens: auto-inserts a keyframe at the current percentage
- from()/fromTo(): shifts from/to properties by the drag delta
- to()/set(): writes absolute position (runtime value + drag offset)
- No GSAP x/y: falls through to standard CSS offset path

The probe measurement fallback in manualOffsetDrag.ts uses preview scale
as an approximation when GSAP transforms interfere with probing, since
the commit path always reads exact runtime values.
…onds, toolbar toggle

GSAP-aware drag pipeline:
- Async mutation flow: CSS offset stays visible during API call, cleared
  via beforeReload callback right before soft-reload replaces the script
- skipReload for intermediate x/y mutations prevents reload cascade
- Flat to()/set() tweens auto-convert to keyframes on first drag
- Promise propagated to gesture handler for proper error recovery

Runtime seek fix:
- GSAP 3.x skips rendering when totalTime(0) on a freshly created paused
  timeline. Nudge to t+0.001 then back to force initial keyframe render.

Timeline keyframe diamonds:
- Diamond markers on clips with connecting lines, 80% clip height
- Click to select clip + seek, teal highlight at playhead
- Keyframe cache populated on load for all elements

Timeline toolbar diamond toggle button for add/remove keyframe.
Compact 48px track height. STUDIO_KEYFRAMES_ENABLED defaults false.
…ame at playhead on convert

- Widen flat-anim filter from x/y-only to any non-keyframed animation
- After convert-to-keyframes, also add keyframe at current playhead %
The toggle was hardcoding x:0 at the playhead percentage, breaking
interpolation. Now: convert-only for flat tweens, remove-only for
existing keyframes. Adding keyframes with correct interpolated values
is done via drag (runtime bridge reads gsap.getProperty).
After undo/redo reverts the file, the keyframe cache had stale
optimistic data. Now bumps the GSAP cache version after each
undo/redo, triggering a re-fetch that syncs timeline diamonds
with the actual file state.
currentTime was passed unrounded when adding animations, producing
values like 0.20127724590558768. Now rounds to millisecond precision
at write time and on display in the animation card and summary text.
…s exist

When all tweens are removed, the toggle now creates a new to()
animation so the user can start keyframing from scratch.
…nterpolated keyframes

- findGsapPositionAnimation falls back to any animation (not just x/y)
  so drag intercept fires even for opacity-only tweens
- Toolbar diamond toggle adds keyframes with linearly interpolated
  values from surrounding keyframes instead of doing nothing
…, empty keyframes

1. Toolbar creates to() with x:0, y:0 (not opacity-only) so drag
   intercept can match position animations immediately
2. Drag intercept does a synchronous fallback fetch when
   selectedGsapAnimations is stale — eliminates the timing race
   between toolbar-create and immediate drag
3. Remove optimistic empty-keyframe cache write on convert — let
   the server response populate the cache with real values
U1: Guard CSS offset persistence for GSAP-animated elements — checks
iframe __timelines for tween targets before persisting offset attributes
to HTML. GSAP elements get visual offset during drag but no HTML leak.

U2: Strip stale CSS offset artifacts at runtime — after timeline bind
and totalTime nudge, removes data-hf-studio-path-offset and CSS custom
properties from elements that are targeted by GSAP tweens.

U3: Fix undo/redo cache ordering — bumps GSAP cache version BEFORE
preview reload (was after), so the iframe loads with fresh keyframe data.

U4: Align toolbar animation duration with element lifetime — reads
data-duration and data-start from the element instead of hardcoding
0.5s and currentTime, so keyframe percentages are consistent.
…actions

Right-clicking a keyframe diamond now selects the clip and seeks to
that keyframe's time before opening the context menu. This ensures
selectedGsapAnimations is populated when delete or ease-change fires.
convertToKeyframesInScript created empty 0% or 100% keyframes when
resolvedFromValues wasn't passed — GSAP interpolated from undefined,
causing elements to vanish or accumulate offset on drag. Now zeros
all numeric properties so the keyframe has a valid baseline.
…vert

opacity defaults to 1, scale/scaleX/scaleY default to 1 — not 0.
Previous zero-fill made elements invisible at 0% (opacity: 0) and
collapsed to zero scale. All other transform properties (x, y,
rotation, skew) correctly default to 0.
…toolbar

When the toolbar diamond creates a new GSAP animation for an element
that already has CSS offset attributes from a prior non-GSAP drag,
the old offset is now cleared from both the DOM and the HTML file.
Prevents CSS translate stacking with the new GSAP transform.
stripGsapTranslateFromTransform was cancelling the CSS drag offset
by subtracting it from the GSAP transform matrix m41/m42. During an
active drag this made the element stay at its GSAP position while the
overlay followed the cursor. Now skips the strip when the element has
the manual-edit-gesture attribute (set during drag, cleared on release).
…untime bridge toolbar add

- Add clipPath to SUPPORTED_PROPS; string input + presets for filter
  and clip-path in AnimationCard (blur, brightness, grayscale, circle,
  inset presets)
- Property panel X/Y edits update the nearest keyframe when the element
  has keyframes, instead of the CSS position
- Toolbar diamond reads gsap.getProperty() from iframe for all animated
  properties when adding a keyframe, falling back to linear interpolation
- Wire previewIframeRef through DomEditContext for toolbar access
- Add labels for all SUPPORTED_PROPS in gsapAnimationConstants
@miguel-heygen miguel-heygen force-pushed the feat/enable-gsap-panel branch from b997a7e to fb2d56a Compare June 2, 2026 23:18
@miguel-heygen miguel-heygen changed the title feat(studio): gsap keyframe editing — drag persistence, timeline diamonds, toolbar toggle feat(studio): native GSAP keyframe editing system Jun 2, 2026
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