feat(studio): on-canvas motion-path overlay#1560
Conversation
d9d0a45 to
ba99ae0
Compare
8a34a71 to
c6c515d
Compare
jrusso1020
left a comment
There was a problem hiding this comment.
Approved at ba99ae07 per Rames D Jusso "stamp-ready with 3 nits." On-canvas motion-path overlay — UI layer, no architectural concerns. Deferring his 3 nits to inline resolution.
Reminder: PR body is the unfilled template (stack-wide process flag); please fill in before merge.
c6c515d to
87a7fd0
Compare
ba99ae0 to
676ba06
Compare
87a7fd0 to
7bb643e
Compare
676ba06 to
f9e5f4f
Compare
vanceingalls
left a comment
There was a problem hiding this comment.
Concurring with the consolidated stack review at HEAD 676ba067. One add: where the cheap test-gap-closing units live.
The 559-line overlay is mostly SVG render + pointer-event delegation into already-tested helpers (buildMotionPathGeometry, nearestPointOnPath, commitNode, commitAddWaypoint, parkPlayheadOnKeyframe). Two testable units could be lifted out with low ceremony and would pin the parts most prone to silent regression:
elementHome(packages/studio/src/components/editor/MotionPathOverlay.tsx:53-78) — the DOM-walk plus--hf-studio-offset-x/yinclusion rule. A jsdom + manual offset-stubbing test pins the "translate var() detection" branch, which is exactly the clever-but-fragile heuristic prone to silent regression on iframe-realm changes.isPreviewHtmlElement(MotionPathOverlay.tsx:84-91) — the cross-realm constructor check. Four lines of test demonstrate the iframe-realm path.
The orchestrating render is genuinely event-delegation glue where unit value is low; an @testing-library/react smoke test that asserts "given a geometry, N nodes render at the expected coordinates" would be a cheap follow-up but is the lower-priority option. Not a blocker — these closures sharpen confidence post-merge rather than gate it.
Review by Via
jrusso1020
left a comment
There was a problem hiding this comment.
Re-approved at f9e5f4fd post-restack. On-canvas motion-path overlay; clean UI layer.
Echoing Via's R2 finding: two testable units worth extracting for cheap coverage closure — elementHome (MotionPathOverlay.tsx:53-78, DOM-walk + offset-rule, jsdom-testable) and isPreviewHtmlElement (:84-91, cross-realm constructor check, 4-line test). Non-blocking, fine as a follow-up.
f9e5f4f to
789f860
Compare
7bb643e to
0230fe5
Compare
789f860 to
80f1a0f
Compare
0230fe5 to
ce7e60a
Compare
80f1a0f to
896e1ae
Compare
SVG overlay with draggable keyframe-diamond nodes, hover-add, right-click remove, and context menu; mounted in the preview area.
ce7e60a to
7f979c5
Compare
896e1ae to
c66ee48
Compare
Consolidates the studio side of the GSAP keyframe/motion-path work into one
PR: runtime read layer + shared helpers, drag/commit/bridge editing infra,
motion-path geometry + commit helpers, on-canvas motion-path overlay, and the
keyframes flag with gesture recording + timeline/selection refinements.
Also fixes "Add keyframe at playhead" for array-form keyframe tweens
(keyframes: [{x,y},…]):
- readElementPosition derived the captured props from anim.properties, which
is empty for array-form keyframes, so the position came back empty and the
add silently no-oped. It now falls back to the union of the keyframe stops'
property keys (then x/y).
- The add site and the toolbar button state computed the playhead percentage
from the element clip range, not the tween range, so keyframes landed at the
wrong percentage. Both now use the tween-relative basis.
- When the playhead is outside the tween range, the button is disabled instead
of silently no-oping (or, post-basis-fix, deleting the boundary keyframe).
Supersedes the separately-reviewed studio PRs #1557, #1558, #1559, #1560, #1561.
Consolidates the studio side of the GSAP keyframe/motion-path work into one
PR: runtime read layer + shared helpers, drag/commit/bridge editing infra,
motion-path geometry + commit helpers, on-canvas motion-path overlay, and the
keyframes flag with gesture recording + timeline/selection refinements.
Also fixes "Add keyframe at playhead" for array-form keyframe tweens
(keyframes: [{x,y},…]):
- readElementPosition derived the captured props from anim.properties, which
is empty for array-form keyframes, so the position came back empty and the
add silently no-oped. It now falls back to the union of the keyframe stops'
property keys (then x/y).
- The add site and the toolbar button state computed the playhead percentage
from the element clip range, not the tween range, so keyframes landed at the
wrong percentage. Both now use the tween-relative basis.
- When the playhead is outside the tween range, "add keyframe at playhead" now
extends the tween to reach it and adds a keyframe there, keeping the existing
keyframes at their absolute times (percentages rescale into the new range),
instead of disabling or no-oping.
Supersedes the separately-reviewed studio PRs #1557, #1558, #1559, #1560, #1561.
Consolidates the studio side of the GSAP keyframe/motion-path work into one
PR: runtime read layer + shared helpers, drag/commit/bridge editing infra,
motion-path geometry + commit helpers, on-canvas motion-path overlay, and the
keyframes flag with gesture recording + timeline/selection refinements.
Also makes "Add keyframe at playhead" do the right thing for every GSAP
animation shape, never disabling or silently no-oping:
- Array-form keyframe tweens (keyframes: [{x,y},…]): readElementPosition now
derives the captured props from the keyframe stops (top-level properties is
empty for array form), and the percentage uses the tween range, not the clip
range — so the add lands at the right spot instead of no-oping.
- Out of the tween range, the action extends the tween to reach the playhead
and adds a hold there, rescaling existing keyframes to keep their absolute
timing (was: disabled / destructive).
- Flat tweens (to/from/fromTo) convert to their natural keyframes, then take
the same add/extend path — so an out-of-range playhead extends them too.
- set() is promoted to a two-stop tween from the set's time to the playhead.
- motionPath/arc tweens add a waypoint at the on-path position (matching
segment, so the curve is preserved) instead of being linearized; outside the
range they extend their duration. A small merge threshold avoids duplicate
waypoints at the path endpoints.
Supersedes the separately-reviewed studio PRs #1557, #1558, #1559, #1560, #1561.
Consolidates the studio side of the GSAP keyframe/motion-path work into one
PR: runtime read layer + shared helpers, drag/commit/bridge editing infra,
motion-path geometry + commit helpers, on-canvas motion-path overlay, and the
keyframes flag with gesture recording + timeline/selection refinements.
Makes "Add keyframe at playhead" do the right thing for every GSAP animation
shape, never disabling or silently no-oping:
- Array-form keyframe tweens (keyframes: [{x,y},…]): readElementPosition now
derives the captured props from the keyframe stops (top-level properties is
empty for array form), and the percentage uses the tween range, not the clip
range — so the add lands at the right spot instead of no-oping.
- Out of the tween range, the action extends the tween to reach the playhead
and adds a hold there, rescaling existing keyframes to keep their absolute
timing (was: disabled / destructive).
- Flat tweens (to/from/fromTo) convert to their natural keyframes, then take
the same add/extend path.
- set() is promoted to a two-stop tween from the set's time to the playhead.
- motionPath/arc tweens add a waypoint at the on-path position (matching
segment, so the curve is preserved) instead of being linearized; outside the
range they extend their duration, with a merge threshold against duplicates.
Also fixes deep-link hydration: on a fresh full-page load the player runtime
isn't ready to honor the first requestSeek, so the one-shot seek latched without
moving the playhead, and the selection hydration (gated on the seek settling)
never ran — a URL with ?t=…&selId=… restored neither. A bounded heartbeat now
retries the seek until the player honors it, then the selection restores.
Supersedes the separately-reviewed studio PRs #1557, #1558, #1559, #1560, #1561.
Consolidates the studio side of the GSAP keyframe/motion-path work into one
PR: runtime read layer + shared helpers, drag/commit/bridge editing infra,
motion-path geometry + commit helpers, on-canvas motion-path overlay, and the
keyframes flag with gesture recording + timeline/selection refinements.
Makes "Add keyframe at playhead" do the right thing for every GSAP animation
shape, never disabling or silently no-oping:
- Array-form keyframe tweens (keyframes: [{x,y},…]): readElementPosition now
derives the captured props from the keyframe stops (top-level properties is
empty for array form), and the percentage uses the tween range, not the clip
range — so the add lands at the right spot instead of no-oping.
- Out of the tween range, the action extends the tween to reach the playhead
and adds a hold there, rescaling existing keyframes to keep their absolute
timing (was: disabled / destructive).
- Flat tweens (to/from/fromTo) convert to their natural keyframes, then take
the same add/extend path.
- set() is promoted to a two-stop tween from the set's time to the playhead.
- motionPath/arc tweens add a waypoint at the on-path position (matching
segment, so the curve is preserved) instead of being linearized; outside the
range they extend their duration, with a merge threshold against duplicates.
Also fixes deep-link hydration. A URL with ?t=…&selId=… restored neither the
playhead nor the selection on a fresh load: useStudioUrlState requests the seek
before the player runtime mounts its requestedSeekTime subscription, so the
request never reached pendingSeekRef, and initializeAdapter (which drained only
pendingSeekRef when the adapter became ready) started at 0 — which also blocked
selection hydration (gated on the seek settling). Fixed at the source:
initializeAdapter now reconciles the store's requestedSeekTime as well, so a seek
requested any time before the adapter is ready lands deterministically.
Supersedes the separately-reviewed studio PRs #1557, #1558, #1559, #1560, #1561.
Consolidates the studio side of the GSAP keyframe/motion-path work into one
PR: runtime read layer + shared helpers, drag/commit/bridge editing infra,
motion-path geometry + commit helpers, on-canvas motion-path overlay, and the
keyframes flag with gesture recording + timeline/selection refinements.
Makes "Add keyframe at playhead" do the right thing for every GSAP animation
shape, never disabling or silently no-oping:
- Array-form keyframe tweens (keyframes: [{x,y},…]): readElementPosition now
derives the captured props from the keyframe stops (top-level properties is
empty for array form), and the percentage uses the tween range, not the clip
range — so the add lands at the right spot instead of no-oping.
- Out of the tween range, the action extends the tween to reach the playhead
and adds a hold there, rescaling existing keyframes to keep their absolute
timing (was: disabled / destructive).
- Flat tweens (to/from/fromTo) convert to their natural keyframes, then take
the same add/extend path.
- set() is promoted to a two-stop tween from the set's time to the playhead.
- motionPath/arc tweens add a waypoint at the on-path position (matching
segment, so the curve is preserved) instead of being linearized; outside the
range they extend their duration, with a merge threshold against duplicates.
Also fixes deep-link hydration. A URL with ?t=…&selId=… restored neither the
playhead nor the selection on a fresh load: useStudioUrlState requests the seek
before the player runtime mounts its requestedSeekTime subscription, so the
request never reached pendingSeekRef, and initializeAdapter (which drained only
pendingSeekRef when the adapter became ready) started at 0 — which also blocked
selection hydration (gated on the seek settling). Fixed at the source:
initializeAdapter now reconciles the store's requestedSeekTime as well, so a seek
requested any time before the adapter is ready lands deterministically.
Supersedes the separately-reviewed studio PRs #1557, #1558, #1559, #1560, #1561.

Stack: GSAP keyframe + motion-path editing — on-canvas overlay (#1553 → #1561).
What
Render the selected element's GSAP motion path over the canvas: a dashed polyline through its x/y keyframes (or motionPath waypoints) with a draggable diamond node at each. Dragging a node rewrites the keyframe and commits to source; clicking a node selects that keyframe and seeks to its time.
Why
Direct, visual editing of motion paths in the Studio, matching the keyframe diamonds in the timeline.
How
MotionPathOverlay.tsx+MotionPathNode.tsx: SVG overlay drawn in declared composition coordinates, positioned over the preview iframe (which lives in the player's shadow DOM), so the path doesn't drift under GSAP transforms or canvas zoom/pan.useDomEditOverlayRects.ts/StudioPreviewArea.tsx/DomEditOverlay.tsx: overlay positioning + wiring.Test plan