@hyperframes/studio 0.7.14 → 0.7.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-C4cUbqql.js +375 -0
- package/dist/assets/{index-CkrVt5DX.js → index-D2SMcOdY.js} +1 -1
- package/dist/assets/index-DmkOvZns.css +1 -0
- package/dist/assets/{index-DYgkaIlV.js → index-sCOjSz40.js} +1 -1
- package/dist/{chunk-KZXYQYIU.js → chunk-SBGXX7WY.js} +52 -22
- package/dist/chunk-SBGXX7WY.js.map +1 -0
- package/dist/{domEditingLayers-SSXQZHHQ.js → domEditingLayers-VZMLL4AP.js} +2 -4
- package/dist/index.d.ts +23 -6
- package/dist/index.html +2 -2
- package/dist/index.js +3158 -2809
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
- package/src/App.tsx +2 -0
- package/src/components/StudioPreviewArea.tsx +0 -41
- package/src/components/StudioRightPanel.tsx +2 -0
- package/src/components/TimelineToolbar.tsx +4 -4
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/GestureRecordControl.tsx +2 -2
- package/src/components/editor/GsapAnimationSection.tsx +3 -3
- package/src/components/editor/InspectorHeaderActions.tsx +62 -0
- package/src/components/editor/LayersPanel.tsx +35 -2
- package/src/components/editor/MotionPathOverlay.tsx +24 -8
- package/src/components/editor/PropertyPanel.tsx +48 -60
- package/src/components/editor/Transform3DCube.tsx +84 -85
- package/src/components/editor/domEditOverlayGeometry.ts +28 -0
- package/src/components/editor/domEditingDom.ts +36 -2
- package/src/components/editor/domEditingGroups.ts +41 -0
- package/src/components/editor/domEditingLayers.test.ts +97 -1
- package/src/components/editor/domEditingLayers.ts +13 -29
- package/src/components/editor/domEditingTypes.ts +4 -1
- package/src/components/editor/gsapAnimationCallbacks.ts +1 -1
- package/src/components/editor/gsapAnimationHelpers.test.ts +2 -2
- package/src/components/editor/gsapAnimationHelpers.ts +1 -1
- package/src/components/editor/manualEditsTypes.ts +1 -1
- package/src/components/editor/manualOffsetDrag.ts +23 -2
- package/src/components/editor/motionPathCommit.test.ts +1 -1
- package/src/components/editor/motionPathSelection.ts +1 -1
- package/src/components/editor/propertyPanel3dTransform.tsx +70 -30
- package/src/components/editor/propertyPanelHelpers.ts +6 -4
- package/src/components/editor/useDomEditOverlayRects.ts +8 -3
- package/src/components/editor/useMotionPathData.ts +47 -1
- package/src/contexts/DomEditContext.tsx +16 -0
- package/src/contexts/TimelineEditContext.tsx +0 -1
- package/src/hooks/gsapRuntimeBridge.ts +9 -2
- package/src/hooks/gsapTweenSynth.ts +64 -0
- package/src/hooks/useAnimatedPropertyCommit.ts +126 -26
- package/src/hooks/useAppHotkeys.ts +34 -3
- package/src/hooks/useDomEditSession.ts +48 -0
- package/src/hooks/useDomSelection.ts +73 -9
- package/src/hooks/useEnableKeyframes.test.ts +35 -0
- package/src/hooks/useEnableKeyframes.ts +29 -1
- package/src/hooks/useGroupCommits.ts +188 -0
- package/src/hooks/useGsapSelectionHandlers.ts +4 -0
- package/src/hooks/useGsapTweenCache.ts +56 -72
- package/src/hooks/usePreviewInteraction.ts +60 -2
- package/src/player/components/ShortcutsPanel.tsx +2 -0
- package/src/player/components/Timeline.tsx +0 -15
- package/src/player/components/TimelineCanvas.tsx +0 -13
- package/src/player/components/TimelineClipDiamonds.tsx +4 -125
- package/src/player/components/timelineCallbacks.ts +0 -1
- package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +47 -4
- package/src/player/hooks/useTimelineSyncCallbacks.ts +43 -4
- package/src/player/store/playerStore.ts +20 -0
- package/src/utils/sdkCutoverParity.test.ts +1 -1
- package/src/utils/studioPreviewHelpers.ts +37 -0
- package/dist/assets/index-BXG-pbr_.js +0 -375
- package/dist/assets/index-svYFaNuq.css +0 -1
- package/dist/chunk-KZXYQYIU.js.map +0 -1
- package/src/components/editor/keyframeMove.test.ts +0 -101
- package/src/components/editor/keyframeMove.ts +0 -151
- /package/dist/{domEditingLayers-SSXQZHHQ.js.map → domEditingLayers-VZMLL4AP.js.map} +0 -0
|
@@ -209,6 +209,22 @@ export function applyManualOffsetDragMatrix(matrix: ManualOffsetDragMatrix, poin
|
|
|
209
209
|
};
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* The perspective w-divisor (matrix3d m44) of the element's current transform.
|
|
214
|
+
* For a plain `translateZ(z)` under `perspective(p)`, m44 = (p - z) / p, so the
|
|
215
|
+
* element renders 1/m44× larger and a translate of `d` composition px moves
|
|
216
|
+
* `d / m44` px on screen. Returns 1 for 2D transforms (no foreshortening). Used
|
|
217
|
+
* to keep the drag offset → screen-movement mapping correct for depth elements,
|
|
218
|
+
* which the flat-scale fast path below would otherwise get wrong by 1/m44.
|
|
219
|
+
*/
|
|
220
|
+
function readTransformWDivisor(element: HTMLElement): number {
|
|
221
|
+
const t = element.ownerDocument.defaultView?.getComputedStyle(element).transform;
|
|
222
|
+
if (!t || !t.startsWith("matrix3d(")) return 1;
|
|
223
|
+
const parts = t.slice("matrix3d(".length, -1).split(",");
|
|
224
|
+
const w = Number.parseFloat(parts[15] ?? "");
|
|
225
|
+
return Number.isFinite(w) && w > 0 ? w : 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
212
228
|
export function measureManualOffsetDragScreenToOffsetMatrix(
|
|
213
229
|
element: HTMLElement,
|
|
214
230
|
initialOffset: { x: number; y: number },
|
|
@@ -221,7 +237,11 @@ export function measureManualOffsetDragScreenToOffsetMatrix(
|
|
|
221
237
|
) {
|
|
222
238
|
const sx = options.scaleX || 1;
|
|
223
239
|
const sy = options.scaleY || 1;
|
|
224
|
-
|
|
240
|
+
// Fold in the perspective foreshortening: a depth element (z≠0) moves
|
|
241
|
+
// 1/m44× faster on screen than its flat scale implies, so the screen→offset
|
|
242
|
+
// matrix must scale by m44 or the element outruns the pointer/overlay.
|
|
243
|
+
const w = readTransformWDivisor(element);
|
|
244
|
+
return { ok: true, matrix: { a: w / sx, b: 0, c: 0, d: w / sy } };
|
|
225
245
|
}
|
|
226
246
|
|
|
227
247
|
const probeSize = options.probeSize ?? DEFAULT_OFFSET_PROBE_PX;
|
|
@@ -360,6 +380,7 @@ export function createManualOffsetDragMember(input: {
|
|
|
360
380
|
// drag is acceptable — the final committed position is always exact.
|
|
361
381
|
const scaleX = input.rect.editScaleX || 1;
|
|
362
382
|
const scaleY = input.rect.editScaleY || 1;
|
|
383
|
+
const w = readTransformWDivisor(input.element);
|
|
363
384
|
return {
|
|
364
385
|
ok: true,
|
|
365
386
|
member: {
|
|
@@ -370,7 +391,7 @@ export function createManualOffsetDragMember(input: {
|
|
|
370
391
|
baseGsap,
|
|
371
392
|
initialPathOffset,
|
|
372
393
|
gestureToken,
|
|
373
|
-
screenToOffset: { a:
|
|
394
|
+
screenToOffset: { a: w / scaleX, b: 0, c: 0, d: w / scaleY },
|
|
374
395
|
originRect: input.rect,
|
|
375
396
|
},
|
|
376
397
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import type { GsapAnimation } from "@hyperframes/
|
|
2
|
+
import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
|
|
3
3
|
import { editableAnimationId } from "./motionPathSelection";
|
|
4
4
|
import {
|
|
5
5
|
commitNode,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Shared by the overlay and its diagnostics (kept here to avoid a circular
|
|
4
4
|
* import between the two).
|
|
5
5
|
*/
|
|
6
|
-
import type { GsapAnimation } from "@hyperframes/
|
|
6
|
+
import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
|
|
7
7
|
import type { DomEditSelection } from "./domEditing";
|
|
8
8
|
|
|
9
9
|
export function selectorFor(sel: DomEditSelection | null): string | null {
|
|
@@ -6,6 +6,20 @@ import { KeyframeNavigation } from "./KeyframeNavigation";
|
|
|
6
6
|
import { formatPxMetricValue, parsePxMetricValue, RESPONSIVE_GRID } from "./propertyPanelHelpers";
|
|
7
7
|
import { Transform3DCube, type CubePose } from "./Transform3DCube";
|
|
8
8
|
|
|
9
|
+
// translateZ only foreshortens under a perspective lens. Rather than hardcode one
|
|
10
|
+
// (an arbitrary px value reads wrong at different canvas sizes), derive it from the
|
|
11
|
+
// element's composition: perspective = composition height puts the virtual camera
|
|
12
|
+
// one comp-height back, a natural ~53° vertical FOV that looks the same whether the
|
|
13
|
+
// canvas is 720p or 4K. Falls back to the element's own height only if the comp size
|
|
14
|
+
// can't be read (detached/unmeasured), never to a fixed magic number.
|
|
15
|
+
function naturalDepthPerspective(el: HTMLElement | null | undefined): number {
|
|
16
|
+
if (!el) return 0;
|
|
17
|
+
const root = el.closest("[data-hf-inner-root],[data-composition-id]") as HTMLElement | null;
|
|
18
|
+
const compHeight = root?.offsetHeight || el.ownerDocument?.documentElement?.clientHeight || 0;
|
|
19
|
+
if (compHeight > 0) return Math.round(compHeight);
|
|
20
|
+
return Math.round((el.offsetHeight || 0) * 4) || 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
type KeyframeEntry = Array<{
|
|
10
24
|
percentage: number;
|
|
11
25
|
properties: Record<string, number | string>;
|
|
@@ -38,17 +52,10 @@ interface PropertyPanel3dTransformProps {
|
|
|
38
52
|
onLivePreviewProps?: (element: DomEditSelection, props: Record<string, number>) => void;
|
|
39
53
|
}
|
|
40
54
|
|
|
41
|
-
type CommitAnimatedProperty = (
|
|
42
|
-
element: DomEditSelection,
|
|
43
|
-
property: string,
|
|
44
|
-
value: number,
|
|
45
|
-
) => Promise<void>;
|
|
46
|
-
|
|
47
55
|
/** The draggable cube + its commit/recenter/live-preview wiring. */
|
|
48
56
|
function Cube3dControl({
|
|
49
57
|
element,
|
|
50
58
|
gsapRuntimeValues,
|
|
51
|
-
onCommitAnimatedProperty,
|
|
52
59
|
onCommitAnimatedProperties,
|
|
53
60
|
onLivePreviewProps,
|
|
54
61
|
onKeyframe,
|
|
@@ -56,8 +63,7 @@ function Cube3dControl({
|
|
|
56
63
|
}: {
|
|
57
64
|
element: DomEditSelection;
|
|
58
65
|
gsapRuntimeValues: Record<string, number>;
|
|
59
|
-
|
|
60
|
-
onCommitAnimatedProperties?: (
|
|
66
|
+
onCommitAnimatedProperties: (
|
|
61
67
|
element: DomEditSelection,
|
|
62
68
|
props: Record<string, number | string>,
|
|
63
69
|
) => Promise<void>;
|
|
@@ -70,6 +76,15 @@ function Cube3dControl({
|
|
|
70
76
|
rotationY: gsapRuntimeValues.rotationY ?? 0,
|
|
71
77
|
rotationZ: gsapRuntimeValues.rotationZ ?? 0,
|
|
72
78
|
};
|
|
79
|
+
// Comp-derived lens (see naturalDepthPerspective) applied the first time depth is
|
|
80
|
+
// set, so the scene's foreshortening scales with the canvas instead of a magic 800.
|
|
81
|
+
const depthPerspective = naturalDepthPerspective(element.element);
|
|
82
|
+
// A gentle, fixed "depth pose" tilt (degrees) dropped on a flat element the first
|
|
83
|
+
// time it gets depth, so translateZ reads as 3D foreshortening instead of a plain
|
|
84
|
+
// resize — small enough to look like a premium card, not a flip.
|
|
85
|
+
const DEPTH_POSE_X = 10;
|
|
86
|
+
const DEPTH_POSE_Y = -15;
|
|
87
|
+
const isFlat = Math.round(pose.rotationX) === 0 && Math.round(pose.rotationY) === 0;
|
|
73
88
|
// Commit only the rotation axes the drag actually changed (each rounded to a
|
|
74
89
|
// whole degree). Reuses the keyframe-aware animated-property commit, so a drag
|
|
75
90
|
// at the playhead writes/updates a keyframe just like the numeric fields.
|
|
@@ -82,13 +97,8 @@ function Cube3dControl({
|
|
|
82
97
|
const axes = Object.keys(changedProps);
|
|
83
98
|
if (axes.length === 0) return;
|
|
84
99
|
// ONE keyframe for the whole pose change — avoids per-axis commits racing into
|
|
85
|
-
// adjacent duplicate keyframes.
|
|
86
|
-
|
|
87
|
-
void onCommitAnimatedProperties(element, changedProps);
|
|
88
|
-
} else {
|
|
89
|
-
for (const [axis, v] of Object.entries(changedProps))
|
|
90
|
-
onCommitAnimatedProperty(element, axis, v);
|
|
91
|
-
}
|
|
100
|
+
// adjacent duplicate keyframes.
|
|
101
|
+
void onCommitAnimatedProperties(element, changedProps);
|
|
92
102
|
};
|
|
93
103
|
const recenter = () => {
|
|
94
104
|
// ONE commit for the whole reset — six per-axis commits meant six soft-reloads
|
|
@@ -101,15 +111,10 @@ function Cube3dControl({
|
|
|
101
111
|
scale: 1,
|
|
102
112
|
transformPerspective: 0,
|
|
103
113
|
};
|
|
104
|
-
|
|
105
|
-
void onCommitAnimatedProperties(element, identity);
|
|
106
|
-
} else {
|
|
107
|
-
for (const [prop, v] of Object.entries(identity))
|
|
108
|
-
void onCommitAnimatedProperty(element, prop, v);
|
|
109
|
-
}
|
|
114
|
+
void onCommitAnimatedProperties(element, identity);
|
|
110
115
|
};
|
|
111
116
|
// Immediate element feedback while dragging — set the live transform without a
|
|
112
|
-
// source write; the release commits via
|
|
117
|
+
// source write; the release commits via commitPose.
|
|
113
118
|
const livePreview = (next: CubePose) =>
|
|
114
119
|
onLivePreviewProps?.(element, {
|
|
115
120
|
rotationX: next.rotationX,
|
|
@@ -123,18 +128,54 @@ function Cube3dControl({
|
|
|
123
128
|
<Transform3DCube
|
|
124
129
|
pose={pose}
|
|
125
130
|
perspective={gsapRuntimeValues.transformPerspective ?? 0}
|
|
131
|
+
defaultPerspective={depthPerspective}
|
|
132
|
+
z={gsapRuntimeValues.z ?? 0}
|
|
126
133
|
onPoseDraft={livePreview}
|
|
127
134
|
onPoseCommit={commitPose}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
onDepthDraft={(z) => {
|
|
136
|
+
// Preview WITH a lens so depth is visible while scrolling — the same
|
|
137
|
+
// default the commit applies, so the element doesn't snap on release.
|
|
138
|
+
const preview: Record<string, number> = gsapRuntimeValues.transformPerspective
|
|
139
|
+
? { z }
|
|
140
|
+
: { z, transformPerspective: depthPerspective };
|
|
141
|
+
// Depth-pose preview: a flat element only scales under Z, so mirror the
|
|
142
|
+
// commit and preview the gentle tilt that makes the depth read as 3D.
|
|
143
|
+
if (isFlat) {
|
|
144
|
+
preview.rotationX = DEPTH_POSE_X;
|
|
145
|
+
preview.rotationY = DEPTH_POSE_Y;
|
|
146
|
+
}
|
|
147
|
+
onLivePreviewProps?.(element, preview);
|
|
148
|
+
}}
|
|
149
|
+
onDepthCommit={(z) => {
|
|
150
|
+
// Best-UX depth: scroll moves Z, and a 3D transform always has a lens —
|
|
151
|
+
// like an After Effects camera. translateZ is invisible without a
|
|
152
|
+
// perspective, so the FIRST time depth is added (Perspective still 0) we
|
|
153
|
+
// set a sensible comp-derived lens ONCE. Every later scroll touches Z
|
|
154
|
+
// only, and Perspective stays an independent, editable field. The cube's
|
|
155
|
+
// scroll is clamped in front of the lens, so Z can't run away past it.
|
|
156
|
+
const props: Record<string, number> = { z };
|
|
157
|
+
if (!gsapRuntimeValues.transformPerspective && depthPerspective > 0) {
|
|
158
|
+
props.transformPerspective = depthPerspective;
|
|
159
|
+
}
|
|
160
|
+
// Depth-pose: a flat element (no tilt) only scales under Z — it can't read
|
|
161
|
+
// as depth. So the first time depth lands on a flat element, also drop a
|
|
162
|
+
// gentle fixed tilt; the foreshortening makes depth read as 3D IN PLACE
|
|
163
|
+
// (no screen travel, per-element lens unchanged). Once the element has any
|
|
164
|
+
// tilt, depth scrolls touch Z only. Reset tilt to 0 to go flat again.
|
|
165
|
+
if (isFlat) {
|
|
166
|
+
props.rotationX = DEPTH_POSE_X;
|
|
167
|
+
props.rotationY = DEPTH_POSE_Y;
|
|
168
|
+
}
|
|
169
|
+
// One commit for all props so the writes can't race read-modify-write on
|
|
170
|
+
// the same script (which dropped a prop and reverted after a seek).
|
|
171
|
+
void onCommitAnimatedProperties(element, props);
|
|
172
|
+
}}
|
|
132
173
|
onRecenter={recenter}
|
|
133
174
|
onKeyframe={onKeyframe}
|
|
134
175
|
keyframed={keyframed}
|
|
135
176
|
/>
|
|
136
177
|
<p className="mt-1 text-center text-[9px] leading-snug text-neutral-600">
|
|
137
|
-
Drag to tilt · Shift-drag to roll
|
|
178
|
+
Drag to tilt · Shift-drag to roll · Scroll for depth
|
|
138
179
|
</p>
|
|
139
180
|
</div>
|
|
140
181
|
</div>
|
|
@@ -286,11 +327,10 @@ export function PropertyPanel3dTransform({
|
|
|
286
327
|
</button>
|
|
287
328
|
{collapsed ? null : (
|
|
288
329
|
<>
|
|
289
|
-
{
|
|
330
|
+
{onCommitAnimatedProperties && (
|
|
290
331
|
<Cube3dControl
|
|
291
332
|
element={element}
|
|
292
333
|
gsapRuntimeValues={gsapRuntimeValues}
|
|
293
|
-
onCommitAnimatedProperty={onCommitAnimatedProperty}
|
|
294
334
|
onCommitAnimatedProperties={onCommitAnimatedProperties}
|
|
295
335
|
onLivePreviewProps={onLivePreviewProps}
|
|
296
336
|
keyframed={(gsapKeyframes ?? []).some(
|
|
@@ -2,7 +2,7 @@ import { parseCssColor, type ParsedColor } from "./colorValue";
|
|
|
2
2
|
import { COMMON_LOCAL_FONT_FAMILIES } from "./fontCatalog";
|
|
3
3
|
import type { DomEditSelection } from "./domEditing";
|
|
4
4
|
import type { ImportedFontAsset } from "./fontAssets";
|
|
5
|
-
import type { GsapAnimation } from "@hyperframes/
|
|
5
|
+
import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
|
|
6
6
|
import { roundToCenti } from "../../utils/rounding";
|
|
7
7
|
|
|
8
8
|
export interface PropertyPanelProps {
|
|
@@ -13,6 +13,8 @@ export interface PropertyPanelProps {
|
|
|
13
13
|
multiSelectCount?: number;
|
|
14
14
|
copiedAgentPrompt: boolean;
|
|
15
15
|
onClearSelection: () => void;
|
|
16
|
+
/** Dissolve the selected data-hf-group wrapper (shown only for group selections). */
|
|
17
|
+
onUngroup?: () => void;
|
|
16
18
|
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
17
19
|
onSetAttribute: (attr: string, value: string) => void | Promise<void>;
|
|
18
20
|
onSetAttributeLive: (attr: string, value: string | null) => void | Promise<void>;
|
|
@@ -29,7 +31,7 @@ export interface PropertyPanelProps {
|
|
|
29
31
|
fontAssets?: ImportedFontAsset[];
|
|
30
32
|
onImportFonts?: (files: FileList | File[]) => Promise<ImportedFontAsset[]>;
|
|
31
33
|
previewIframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
|
32
|
-
gsapAnimations?: import("@hyperframes/
|
|
34
|
+
gsapAnimations?: import("@hyperframes/parsers/gsap-parser").GsapAnimation[];
|
|
33
35
|
gsapMultipleTimelines?: boolean;
|
|
34
36
|
gsapUnsupportedTimelinePattern?: boolean;
|
|
35
37
|
onUpdateGsapProperty?: (animId: string, prop: string, value: number | string) => void;
|
|
@@ -49,13 +51,13 @@ export interface PropertyPanelProps {
|
|
|
49
51
|
config: {
|
|
50
52
|
enabled: boolean;
|
|
51
53
|
autoRotate?: boolean | number;
|
|
52
|
-
segments?: import("@hyperframes/
|
|
54
|
+
segments?: import("@hyperframes/parsers/gsap-parser").ArcPathSegment[];
|
|
53
55
|
},
|
|
54
56
|
) => void;
|
|
55
57
|
onUpdateArcSegment?: (
|
|
56
58
|
animId: string,
|
|
57
59
|
segmentIndex: number,
|
|
58
|
-
update: Partial<import("@hyperframes/
|
|
60
|
+
update: Partial<import("@hyperframes/parsers/gsap-parser").ArcPathSegment>,
|
|
59
61
|
) => void;
|
|
60
62
|
/** Unroll computed (helper/loop) tweens into literal tweens for direct editing. */
|
|
61
63
|
onUnroll?: (animationId: string) => void;
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ResolvedElementRef,
|
|
12
12
|
groupOverlayItemsEqual,
|
|
13
13
|
isElementVisibleForOverlay,
|
|
14
|
+
groupAwareOverlayRect,
|
|
14
15
|
rectsEqual,
|
|
15
16
|
resolveElementForOverlay,
|
|
16
17
|
selectionCacheKey,
|
|
@@ -155,7 +156,7 @@ export function useDomEditOverlayRects({
|
|
|
155
156
|
// backgroundless full-bleed scene above a subcomposition), which would wrongly
|
|
156
157
|
// hide the selection box. Occlusion stays for hover, where a false hide is cheap.
|
|
157
158
|
if (el && isElementVisibleForOverlay(el)) {
|
|
158
|
-
const nextRect =
|
|
159
|
+
const nextRect = groupAwareOverlayRect(overlayEl, iframe, el);
|
|
159
160
|
setOverlayRect(nextRect);
|
|
160
161
|
const descendants = el.querySelectorAll("*");
|
|
161
162
|
if (descendants.length > 0 && descendants.length <= 60) {
|
|
@@ -196,9 +197,13 @@ export function useDomEditOverlayRects({
|
|
|
196
197
|
const liveGroupKeys = new Set<string>();
|
|
197
198
|
for (const groupSelection of group) {
|
|
198
199
|
const key = selectionCacheKey(groupSelection);
|
|
200
|
+
// Members of the same group collapse to one selection under select-as-unit,
|
|
201
|
+
// so a multi-select can hold the same group twice — dedupe by key to avoid
|
|
202
|
+
// duplicate React keys (and a doubled overlay box).
|
|
203
|
+
if (liveGroupKeys.has(key)) continue;
|
|
199
204
|
liveGroupKeys.add(key);
|
|
200
205
|
const el = resolveGroupElement(doc, groupSelection);
|
|
201
|
-
const rect = el ?
|
|
206
|
+
const rect = el ? groupAwareOverlayRect(overlayEl, iframe, el) : null;
|
|
202
207
|
if (el && rect)
|
|
203
208
|
nextGroupItems.push({ key, selection: groupSelection, element: el, rect });
|
|
204
209
|
}
|
|
@@ -235,7 +240,7 @@ export function useDomEditOverlayRects({
|
|
|
235
240
|
return;
|
|
236
241
|
}
|
|
237
242
|
|
|
238
|
-
setHoverRect(
|
|
243
|
+
setHoverRect(groupAwareOverlayRect(overlayEl, iframe, hoverEl));
|
|
239
244
|
};
|
|
240
245
|
|
|
241
246
|
frame = requestAnimationFrame(update);
|
|
@@ -5,6 +5,38 @@ import { buildMotionPathGeometry, type MotionPathGeometry } from "./motionPathGe
|
|
|
5
5
|
|
|
6
6
|
type Rect = { left: number; top: number; width: number; height: number };
|
|
7
7
|
|
|
8
|
+
// The translate (e/f) components of an element's computed transform, in comp px.
|
|
9
|
+
// A group wrapper dragged via GSAP carries its offset here, not in offsetLeft/Top.
|
|
10
|
+
function transformTranslate(el: HTMLElement): { x: number; y: number } {
|
|
11
|
+
const t = el.ownerDocument?.defaultView?.getComputedStyle(el).transform;
|
|
12
|
+
if (!t || t === "none") return { x: 0, y: 0 };
|
|
13
|
+
const m3 = t.match(/matrix3d\(([^)]+)\)/);
|
|
14
|
+
if (m3) {
|
|
15
|
+
const v = m3[1].split(",").map(Number);
|
|
16
|
+
return { x: v[12] || 0, y: v[13] || 0 };
|
|
17
|
+
}
|
|
18
|
+
const m = t.match(/matrix\(([^)]+)\)/);
|
|
19
|
+
if (m) {
|
|
20
|
+
const v = m[1].split(",").map(Number);
|
|
21
|
+
return { x: v[4] || 0, y: v[5] || 0 };
|
|
22
|
+
}
|
|
23
|
+
return { x: 0, y: 0 };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Perspective foreshortening of the element's OWN transform (matrix3d m44). A
|
|
27
|
+
// depth element (translateZ toward the viewer) renders 1/m44× larger, so its
|
|
28
|
+
// animated x/y offsets travel 1/m44× further on screen than the flat preview
|
|
29
|
+
// scale implies. Returns 1 for 2D transforms. The motion path magnifies its
|
|
30
|
+
// offset points by 1/m44 (and de-magnifies pointer→offset) so the drawn path and
|
|
31
|
+
// its draggable nodes track the projected element instead of drifting off it.
|
|
32
|
+
export function transformWDivisor(el: HTMLElement): number {
|
|
33
|
+
const t = el.ownerDocument?.defaultView?.getComputedStyle(el).transform;
|
|
34
|
+
if (!t || !t.startsWith("matrix3d(")) return 1;
|
|
35
|
+
const v = t.slice("matrix3d(".length, -1).split(",");
|
|
36
|
+
const w = Number.parseFloat(v[15] ?? "");
|
|
37
|
+
return Number.isFinite(w) && w > 0 ? w : 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
8
40
|
export function elementHome(el: HTMLElement): { x: number; y: number } {
|
|
9
41
|
let left = 0;
|
|
10
42
|
let top = 0;
|
|
@@ -12,6 +44,14 @@ export function elementHome(el: HTMLElement): { x: number; y: number } {
|
|
|
12
44
|
while (node) {
|
|
13
45
|
left += node.offsetLeft;
|
|
14
46
|
top += node.offsetTop;
|
|
47
|
+
// Ancestor transforms (e.g. a group wrapper moved via GSAP) shift where the
|
|
48
|
+
// element actually renders, so the path must anchor on top of them. The element's
|
|
49
|
+
// OWN transform is excluded — that's the animated offset the path itself draws.
|
|
50
|
+
if (node !== el) {
|
|
51
|
+
const t = transformTranslate(node);
|
|
52
|
+
left += t.x;
|
|
53
|
+
top += t.y;
|
|
54
|
+
}
|
|
15
55
|
const parent = node.offsetParent as HTMLElement | null;
|
|
16
56
|
if (!parent || parent.hasAttribute("data-composition-id")) break;
|
|
17
57
|
node = parent;
|
|
@@ -62,6 +102,7 @@ export function useMotionPathData(
|
|
|
62
102
|
geometryResolved: boolean;
|
|
63
103
|
visibleInPreview: boolean;
|
|
64
104
|
home: { x: number; y: number } | null;
|
|
105
|
+
pScale: number;
|
|
65
106
|
} {
|
|
66
107
|
const [rect, setRect] = useState<Rect | null>(null);
|
|
67
108
|
const [geometry, setGeometry] = useState<MotionPathGeometry | null>(null);
|
|
@@ -69,6 +110,9 @@ export function useMotionPathData(
|
|
|
69
110
|
const geometryResolved = resolvedForRef.current === selector;
|
|
70
111
|
const [visibleInPreview, setVisibleInPreview] = useState(true);
|
|
71
112
|
const [home, setHome] = useState<{ x: number; y: number } | null>(null);
|
|
113
|
+
// Perspective magnification (1/m44) of the selected element — applied to the
|
|
114
|
+
// path's offset points so depth (translateZ) elements' paths track on screen.
|
|
115
|
+
const [pScale, setPScale] = useState(1);
|
|
72
116
|
|
|
73
117
|
useEffect(() => {
|
|
74
118
|
if (!selector) {
|
|
@@ -105,6 +149,8 @@ export function useMotionPathData(
|
|
|
105
149
|
setHome((prev) =>
|
|
106
150
|
prev && Math.abs(prev.x - h.x) < 0.5 && Math.abs(prev.y - h.y) < 0.5 ? prev : h,
|
|
107
151
|
);
|
|
152
|
+
const ps = 1 / transformWDivisor(live);
|
|
153
|
+
setPScale((p) => (Math.abs(p - ps) < 0.001 ? p : ps));
|
|
108
154
|
}
|
|
109
155
|
}
|
|
110
156
|
raf = requestAnimationFrame(tick);
|
|
@@ -132,5 +178,5 @@ export function useMotionPathData(
|
|
|
132
178
|
return () => window.clearInterval(id);
|
|
133
179
|
}, [selector, iframeRef]);
|
|
134
180
|
|
|
135
|
-
return { rect, geometry, geometryResolved, visibleInPreview, home };
|
|
181
|
+
return { rect, geometry, geometryResolved, visibleInPreview, home, pScale };
|
|
136
182
|
}
|
|
@@ -31,6 +31,9 @@ export interface DomEditActionsValue extends Pick<
|
|
|
31
31
|
| "handleBlockedDomMove"
|
|
32
32
|
| "handleDomManualDragStart"
|
|
33
33
|
| "handleDomEditElementDelete"
|
|
34
|
+
| "handleGroupSelection"
|
|
35
|
+
| "handleUngroupSelection"
|
|
36
|
+
| "setActiveGroupElement"
|
|
34
37
|
| "buildDomSelectionFromTarget"
|
|
35
38
|
| "buildDomSelectionForTimelineElement"
|
|
36
39
|
| "updateDomEditHoverSelection"
|
|
@@ -72,6 +75,7 @@ export interface DomEditSelectionValue extends Pick<
|
|
|
72
75
|
| "domEditSelection"
|
|
73
76
|
| "domEditGroupSelections"
|
|
74
77
|
| "domEditHoverSelection"
|
|
78
|
+
| "activeGroupElement"
|
|
75
79
|
| "domEditSelectionRef"
|
|
76
80
|
| "selectedGsapAnimations"
|
|
77
81
|
| "gsapMultipleTimelines"
|
|
@@ -138,6 +142,10 @@ export function DomEditProvider({
|
|
|
138
142
|
handleBlockedDomMove,
|
|
139
143
|
handleDomManualDragStart,
|
|
140
144
|
handleDomEditElementDelete,
|
|
145
|
+
handleGroupSelection,
|
|
146
|
+
handleUngroupSelection,
|
|
147
|
+
setActiveGroupElement,
|
|
148
|
+
activeGroupElement,
|
|
141
149
|
buildDomSelectionFromTarget,
|
|
142
150
|
buildDomSelectionForTimelineElement,
|
|
143
151
|
updateDomEditHoverSelection,
|
|
@@ -216,6 +224,9 @@ export function DomEditProvider({
|
|
|
216
224
|
handleBlockedDomMove,
|
|
217
225
|
handleDomManualDragStart,
|
|
218
226
|
handleDomEditElementDelete,
|
|
227
|
+
handleGroupSelection,
|
|
228
|
+
handleUngroupSelection,
|
|
229
|
+
setActiveGroupElement,
|
|
219
230
|
buildDomSelectionFromTarget,
|
|
220
231
|
buildDomSelectionForTimelineElement,
|
|
221
232
|
updateDomEditHoverSelection,
|
|
@@ -277,6 +288,9 @@ export function DomEditProvider({
|
|
|
277
288
|
handleBlockedDomMove,
|
|
278
289
|
handleDomManualDragStart,
|
|
279
290
|
handleDomEditElementDelete,
|
|
291
|
+
handleGroupSelection,
|
|
292
|
+
handleUngroupSelection,
|
|
293
|
+
setActiveGroupElement,
|
|
280
294
|
buildDomSelectionFromTarget,
|
|
281
295
|
buildDomSelectionForTimelineElement,
|
|
282
296
|
updateDomEditHoverSelection,
|
|
@@ -319,6 +333,7 @@ export function DomEditProvider({
|
|
|
319
333
|
domEditSelection,
|
|
320
334
|
domEditGroupSelections,
|
|
321
335
|
domEditHoverSelection,
|
|
336
|
+
activeGroupElement,
|
|
322
337
|
domEditSelectionRef,
|
|
323
338
|
selectedGsapAnimations,
|
|
324
339
|
gsapMultipleTimelines,
|
|
@@ -332,6 +347,7 @@ export function DomEditProvider({
|
|
|
332
347
|
domEditSelection,
|
|
333
348
|
domEditGroupSelections,
|
|
334
349
|
domEditHoverSelection,
|
|
350
|
+
activeGroupElement,
|
|
335
351
|
domEditSelectionRef,
|
|
336
352
|
selectedGsapAnimations,
|
|
337
353
|
gsapMultipleTimelines,
|
|
@@ -241,8 +241,15 @@ export async function tryGsapDragIntercept(
|
|
|
241
241
|
// place (idempotent), else add a new one. This also covers the stale-cache
|
|
242
242
|
// phantom — committing a set is correct because the element genuinely has no live motion.
|
|
243
243
|
const hasNonHold = hasNonHoldTweenForElement(iframe, selector);
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
// A KEYFRAMED position tween — even one that's currently a flat constant ("hold",
|
|
245
|
+
// e.g. 0% and 100% identical) — is still an animation the user is building, so a
|
|
246
|
+
// drag must add/update a keyframe, NOT fall back to a static `set`. Without this,
|
|
247
|
+
// dragging an element whose position tween is constant writes a `gsap.set` that
|
|
248
|
+
// fights the tween (the "drag didn't create a keyframe / didn't persist" bug). The
|
|
249
|
+
// static path is only for elements with NO keyframed position tween (truly static,
|
|
250
|
+
// or just a leftover position-hold `set`).
|
|
251
|
+
const hasKeyframedPosTween = !!posAnim?.keyframes;
|
|
252
|
+
if (!hasNonHold && !hasKeyframedPosTween) {
|
|
246
253
|
const existingSet =
|
|
247
254
|
posAnim && posAnim.method === "set" && posAnim.targetSelector === selector
|
|
248
255
|
? posAnim
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GsapAnimation,
|
|
3
|
+
GsapKeyframesData,
|
|
4
|
+
GsapPercentageKeyframe,
|
|
5
|
+
} from "@hyperframes/core/gsap-parser";
|
|
6
|
+
import { PROPERTY_DEFAULTS } from "./gsapShared";
|
|
7
|
+
|
|
8
|
+
export function deduplicateKeyframes(
|
|
9
|
+
keyframes: GsapPercentageKeyframe[],
|
|
10
|
+
): GsapPercentageKeyframe[] {
|
|
11
|
+
const byPct = new Map<number, GsapPercentageKeyframe>();
|
|
12
|
+
for (const kf of keyframes) {
|
|
13
|
+
const existing = byPct.get(kf.percentage);
|
|
14
|
+
if (existing) {
|
|
15
|
+
existing.properties = { ...existing.properties, ...kf.properties };
|
|
16
|
+
if (kf.ease) existing.ease = kf.ease;
|
|
17
|
+
} else {
|
|
18
|
+
byPct.set(kf.percentage, { ...kf, properties: { ...kf.properties } });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return Array.from(byPct.values()).sort((a, b) => a.percentage - b.percentage);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// fallow-ignore-next-line complexity
|
|
25
|
+
export function synthesizeFlatTweenKeyframes(anim: GsapAnimation): GsapKeyframesData | null {
|
|
26
|
+
if (anim.method === "set") {
|
|
27
|
+
// A `set` is a STATIC HOLD — a value applied at one point, not an animated
|
|
28
|
+
// keyframe. It must NOT synthesize a keyframe, or the timeline + panel show a
|
|
29
|
+
// phantom diamond for a value that doesn't animate. This holds for a base
|
|
30
|
+
// `gsap.set` (off-timeline) AND an on-timeline `tl.set`, and aligns the AST
|
|
31
|
+
// path with the runtime scan, which already skips every zero-duration set.
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const toProps = anim.properties;
|
|
35
|
+
const fromProps = anim.fromProperties;
|
|
36
|
+
if (!toProps || Object.keys(toProps).length === 0) return null;
|
|
37
|
+
|
|
38
|
+
const startProps: Record<string, number | string> = {};
|
|
39
|
+
const endProps: Record<string, number | string> = {};
|
|
40
|
+
|
|
41
|
+
if (anim.method === "from") {
|
|
42
|
+
for (const [k, v] of Object.entries(toProps)) {
|
|
43
|
+
startProps[k] = v;
|
|
44
|
+
endProps[k] = PROPERTY_DEFAULTS[k] ?? 0;
|
|
45
|
+
}
|
|
46
|
+
} else if (anim.method === "fromTo" && fromProps) {
|
|
47
|
+
Object.assign(startProps, fromProps);
|
|
48
|
+
Object.assign(endProps, toProps);
|
|
49
|
+
} else {
|
|
50
|
+
for (const [k, v] of Object.entries(toProps)) {
|
|
51
|
+
startProps[k] = PROPERTY_DEFAULTS[k] ?? 0;
|
|
52
|
+
endProps[k] = v;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
format: "percentage",
|
|
58
|
+
keyframes: [
|
|
59
|
+
{ percentage: 0, properties: startProps },
|
|
60
|
+
{ percentage: 100, properties: endProps },
|
|
61
|
+
],
|
|
62
|
+
...(anim.ease ? { ease: anim.ease } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|