feat(studio): native GSAP keyframe editing system#1130
Open
miguel-heygen wants to merge 45 commits into
Open
Conversation
7f1c645 to
b499484
Compare
Fallow audit reportFound 165 findings. Duplication (113, showing 50)
Showing 50 of 113 findings. Run fallow locally or inspect the CI output for the full report. Health (52, showing 50)
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.
…rkers, keyframe cache
…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.
…s, set deprioritized, scale-aware fallback
…t handles all cases correctly
… translate/transform stacking
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
b997a7e to
fb2d56a
Compare
Math.round to integer percentages caused visible offset between the playhead and the keyframe diamond at higher zoom levels. Now rounds to 1 decimal place (e.g., 16.3% instead of 16%) for sub-second precision across all keyframe creation paths.
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.
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
GSAP-aware drag pipeline
beforeReloadcallback — CSS offset stays visible during API callstripGsapTranslateFromTransformskipped during active drag gestureTimeline keyframe diamonds
Timeline toolbar
gsap.getProperty()from iframe for accurate multi-property keyframe valuesDesign panel integration
Runtime fixes
totalTime(0)no-op: nudge to t+0.001 then back to force initial keyframe render<=instead of<)Other fixes
data-duration(not hardcoded 0.5s)convertToKeyframesInScriptuses CSS identity values (opacity:1, scale:1) not zeroBehind
STUDIO_KEYFRAMES_ENABLEDflag, defaults tofalse. Enable withVITE_STUDIO_ENABLE_KEYFRAMES=true. Compact 48px track height.Test plan