Skip to content

feat(studio): GSAP drag/commit/bridge editing infra#1558

Open
miguel-heygen wants to merge 1 commit into
feat/studio-gsap-read-layerfrom
feat/studio-gsap-drag-infra
Open

feat(studio): GSAP drag/commit/bridge editing infra#1558
miguel-heygen wants to merge 1 commit into
feat/studio-gsap-read-layerfrom
feat/studio-gsap-drag-infra

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Stack: GSAP keyframe + motion-path editing — drag/commit bridge (#1553#1561).

What

Infrastructure to edit GSAP-animated elements by direct manipulation: a runtime bridge that routes canvas drags into keyframe / tween source mutations instead of CSS patches.

Why

Dragging a GSAP-owned element via CSS corrupts its transform (GSAP owns style.transform and rewrites it every tick). Geometry edits must resolve the live tween and commit through script mutation.

How

  • gsapRuntimeBridge.ts: resolves the live position tween for a selection and intercepts drag/resize/rotation.
  • gsapDragCommit.ts: converts a drag delta into a keyframe / flat-tween edit (auto-converting a flat tween to keyframes when needed).
  • useGsapAwareEditing.ts: routes geometry commits to the bridge, falling back to CSS only for non-GSAP elements.
  • manualEditsDom.ts: GSAP-animated elements push the offset through gsap.set and keep translate:none rather than composing a CSS translate on top of GSAP's matrix.

Test plan

  • Unit tests added (drag commit, runtime bridge, GSAP-aware apply)
  • Manual studio drag of GSAP elements

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Async-semantics axis — second-pass correction crediting @rames-jusso

Updating my take. Rames flagged the commitMutation facade's broken await semantics in a parallel review, and he's right. My initial pass declared it "internally consistent" because I checked what comes AFTER each await commitMutation(...) — and in useGestureCommit / useEnableKeyframes the next step is showToast(...) or return. I concluded the await was "vestigial but harmless." That was wrong — I missed that the showToast("Recorded N keyframes", "info") at useGestureCommit.ts:198 is itself a UI claim that depends on save success. With the facade's promise now resolving on next microtask (before the save lands), on a save failure the user sees:

  1. The optimistic "Recorded N keyframes" toast (because we already "succeeded"), then
  2. The wrapper's "Couldn't save animation: ..." toast from safeGsapCommit's .catch().

Two contradictory toasts. The outer try/catch in useGestureCommit is also dead for save errors now — they're swallowed by the wrapper, never re-thrown.

Verified findings (still valid)

  • The facade type signature didn't change — outer fn still returns Promise<void> — so callers compile without warning. The semantic break is invisible to TypeScript.
  • No pre-existing try/catch on the 2-arg facade path; the dead try/catch is in NEW code (useGestureCommit) added in this same PR.
  • The 3-arg commitMutation(selection, mutation, options) chain is untouched — useGsapKeyframeOps.ts:121,192 continue to chain .catch() correctly on the underlying CommitMutation.

Endorsing Rames's fix

Easiest mitigation matches his suggestion: have useSafeGsapCommitMutation return the inner promise instead of returning void, so await semantics hold:

return useCallback(
  (selection, mutation, options) =>
    commitMutation(selection, mutation, options).catch((error) => {
      trackGsapSaveFailure(error, selection, mutation, options.label);
      showToast?.(`Couldn't save animation: ${getStudioSaveErrorMessage(error)}`, "error");
    }),
  [...],
);

That keeps the caller's await honest AND keeps the toast-on-error behavior. Drop the void prefix, return the chained promise.

Lesson for me

Tracing "what comes after the await" was the wrong frame. The right question was "does the resolution timestamp of this promise carry meaning to downstream UI claims?" — and showToast("Recorded...") IS such a claim. Calibration for the next async-semantics pass.

— Review by tai (pr-review)

Drag-to-keyframe commit + parkPlayheadOnKeyframe, a stale-parse guard, from()/
fromTo() out-of-range replace-not-add, and the gsap.set path-offset helper.
@miguel-heygen miguel-heygen force-pushed the feat/studio-gsap-drag-infra branch from 4c38428 to a3a26a9 Compare June 18, 2026 15:47
@miguel-heygen miguel-heygen force-pushed the feat/studio-gsap-read-layer branch from 74c37f3 to 52dfaa9 Compare June 18, 2026 15:48
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