@hyperframes/studio 0.6.122-alpha.0 → 0.7.0
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-CbNg8Ghm.js +374 -0
- package/dist/assets/{index-DSLrl2tB.js → index-CccCJ2B6.js} +24 -24
- package/dist/assets/{index-i-MLqhmm.js → index-DsxCdV8b.js} +1 -1
- package/dist/assets/index-DzWIinxk.css +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.html +3 -3
- package/dist/index.js +3757 -2232
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/App.tsx +1 -0
- package/src/components/StudioHeader.tsx +2 -3
- package/src/components/StudioPreviewArea.tsx +12 -0
- package/src/components/TimelineToolbar.tsx +29 -16
- package/src/components/editor/DomEditOverlay.test.ts +0 -13
- package/src/components/editor/DomEditOverlay.tsx +3 -10
- package/src/components/editor/KeyframeNavigation.test.ts +36 -0
- package/src/components/editor/KeyframeNavigation.tsx +34 -4
- package/src/components/editor/MotionPathNode.tsx +98 -0
- package/src/components/editor/MotionPathOverlay.tsx +481 -0
- package/src/components/editor/SnapToolbar.tsx +28 -3
- package/src/components/editor/SourceEditor.tsx +21 -9
- package/src/components/editor/domEditOverlayGeometry.ts +12 -0
- package/src/components/editor/domEditOverlayGestures.ts +5 -1
- package/src/components/editor/domEditOverlayStartGesture.ts +5 -1
- package/src/components/editor/domEditingDom.ts +24 -9
- package/src/components/editor/manualEditingAvailability.test.ts +0 -12
- package/src/components/editor/manualEditingAvailability.ts +1 -11
- package/src/components/editor/manualEdits.ts +1 -0
- package/src/components/editor/manualEditsDom.ts +68 -39
- package/src/components/editor/manualEditsDomGsap.test.ts +82 -0
- package/src/components/editor/manualOffsetDrag.test.ts +12 -0
- package/src/components/editor/manualOffsetDrag.ts +79 -4
- package/src/components/editor/motionPathCommit.test.ts +130 -0
- package/src/components/editor/motionPathCommit.ts +83 -0
- package/src/components/editor/motionPathGeometry.test.ts +127 -0
- package/src/components/editor/motionPathGeometry.ts +116 -0
- package/src/components/editor/motionPathSelection.ts +33 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +36 -18
- package/src/components/editor/useDomEditOverlayRects.ts +6 -0
- package/src/components/editor/useMotionPathData.ts +135 -0
- package/src/components/nle/NLELayout.tsx +17 -6
- package/src/components/panels/SlideshowPanel.tsx +2 -1
- package/src/components/panels/SlideshowSubPanels.tsx +2 -1
- package/src/components/renders/RenderQueue.tsx +1 -1
- package/src/components/renders/useRenderQueue.ts +3 -2
- package/src/hooks/draggedGsapPosition.ts +47 -0
- package/src/hooks/gsapDragCommit.test.ts +435 -0
- package/src/hooks/gsapDragCommit.ts +278 -290
- package/src/hooks/gsapDragPositionCommit.ts +410 -0
- package/src/hooks/gsapRuntimeBridge.test.ts +147 -0
- package/src/hooks/gsapRuntimeBridge.ts +86 -69
- package/src/hooks/gsapRuntimeKeyframes.test.ts +129 -1
- package/src/hooks/gsapRuntimeKeyframes.ts +183 -11
- package/src/hooks/gsapRuntimePatch.test.ts +462 -0
- package/src/hooks/gsapRuntimePatch.ts +159 -0
- package/src/hooks/gsapScriptCommitTypes.ts +11 -1
- package/src/hooks/gsapShared.test.ts +58 -0
- package/src/hooks/gsapShared.ts +28 -10
- package/src/hooks/gsapTargetCache.ts +0 -3
- package/src/hooks/useDomEditSession.ts +0 -4
- package/src/hooks/useDomSelection.ts +12 -5
- package/src/hooks/useEnableKeyframes.test.ts +130 -0
- package/src/hooks/useEnableKeyframes.ts +298 -45
- package/src/hooks/useGestureCommit.ts +121 -53
- package/src/hooks/useGestureRecording.ts +61 -42
- package/src/hooks/useGsapAnimationFetchFallback.test.ts +36 -0
- package/src/hooks/useGsapAnimationFetchFallback.ts +65 -9
- package/src/hooks/useGsapAnimationOps.ts +1 -1
- package/src/hooks/useGsapAwareEditing.ts +38 -40
- package/src/hooks/useGsapScriptCommits.test.tsx +366 -0
- package/src/hooks/useGsapScriptCommits.ts +102 -6
- package/src/hooks/useGsapTweenCache.ts +7 -1
- package/src/hooks/useRazorSplit.ts +21 -2
- package/src/hooks/useSafeGsapCommitMutation.test.tsx +86 -0
- package/src/hooks/useSafeGsapCommitMutation.ts +8 -4
- package/src/hooks/useServerConnection.ts +4 -1
- package/src/hooks/useStudioContextValue.ts +3 -2
- package/src/hooks/useStudioUrlState.ts +97 -37
- package/src/player/components/ShortcutsPanel.tsx +8 -0
- package/src/player/components/TimelineClipDiamonds.tsx +10 -12
- package/src/player/hooks/useExpandedTimelineElements.test.ts +34 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +20 -2
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/lib/mediaProbe.ts +7 -1
- package/src/player/store/playerStore.ts +38 -1
- package/src/telemetry/config.ts +3 -2
- package/src/utils/editDebugLog.ts +16 -0
- package/src/utils/generateId.ts +7 -0
- package/src/utils/gsapSoftReload.test.ts +217 -13
- package/src/utils/gsapSoftReload.ts +228 -50
- package/src/utils/studioHelpers.test.ts +31 -1
- package/src/utils/studioHelpers.ts +24 -0
- package/src/utils/studioPreviewHelpers.test.ts +26 -1
- package/src/utils/studioPreviewHelpers.ts +30 -6
- package/src/utils/studioTelemetry.ts +3 -1
- package/dist/assets/index-Baz2T-9U.js +0 -269
- package/dist/assets/index-BvEhf7SS.css +0 -1
- package/src/player/components/TimelinePropertyRows.tsx +0 -126
|
@@ -9,6 +9,8 @@ import { readRuntimeKeyframes, scanAllRuntimeKeyframes } from "./gsapRuntimeKeyf
|
|
|
9
9
|
import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler";
|
|
10
10
|
import { roundTo3 } from "../utils/rounding";
|
|
11
11
|
import { computeElementPercentage } from "./gsapShared";
|
|
12
|
+
import { computeDraggedGsapPosition } from "./draggedGsapPosition";
|
|
13
|
+
import type { RuntimeTweenChange, SetPatchProps } from "./gsapRuntimePatch";
|
|
12
14
|
export interface GsapDragCommitCallbacks {
|
|
13
15
|
commitMutation: (
|
|
14
16
|
selection: DomEditSelection,
|
|
@@ -19,6 +21,13 @@ export interface GsapDragCommitCallbacks {
|
|
|
19
21
|
softReload?: boolean;
|
|
20
22
|
skipReload?: boolean;
|
|
21
23
|
beforeReload?: () => void;
|
|
24
|
+
/**
|
|
25
|
+
* Value-only fast path: when set, `runCommit` patches the changed tween in
|
|
26
|
+
* the preview runtime in place (instant, no re-run) and only falls back to
|
|
27
|
+
* the soft reload if the patch can't be safely applied. Attached only to
|
|
28
|
+
* value-only `set` commits; structural/keyframe commits omit it.
|
|
29
|
+
*/
|
|
30
|
+
instantPatch?: { selector: string; change: RuntimeTweenChange };
|
|
22
31
|
},
|
|
23
32
|
) => Promise<void>;
|
|
24
33
|
fetchAnimations?: () => Promise<GsapAnimation[]>;
|
|
@@ -32,6 +41,17 @@ export function computeCurrentPercentage(
|
|
|
32
41
|
return computeElementPercentage(usePlayerStore.getState().currentTime, selection, animation);
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
// When a drag edits a SELECTED keyframe, park the playhead on that keyframe's exact
|
|
45
|
+
// time. Otherwise the playhead can sit a frame outside the tween (e.g. 1.1666 vs a
|
|
46
|
+
// 1.2 start), so the post-commit reseek renders the element's base pose and the edit
|
|
47
|
+
// looks like it snapped away. Keeping the playhead on the edited keyframe avoids that.
|
|
48
|
+
export function parkPlayheadOnKeyframe(anim: GsapAnimation, pct: number): void {
|
|
49
|
+
const ts = resolveTweenStart(anim);
|
|
50
|
+
const td = resolveTweenDuration(anim);
|
|
51
|
+
if (ts == null || !td || td <= 0) return;
|
|
52
|
+
usePlayerStore.getState().requestSeek(roundTo3(ts + (pct / 100) * td));
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
// ── Dynamic keyframe materialization ──────────────────────────────────────
|
|
36
56
|
|
|
37
57
|
export async function materializeIfDynamic(
|
|
@@ -77,198 +97,262 @@ export async function materializeIfDynamic(
|
|
|
77
97
|
);
|
|
78
98
|
}
|
|
79
99
|
|
|
80
|
-
// ──
|
|
100
|
+
// ── Drag → GSAP position math ──────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/** The shape of an `update-property` mutation a static-set nudge POSTs. */
|
|
103
|
+
interface UpdatePropertyMutation {
|
|
104
|
+
type: "update-property";
|
|
105
|
+
animationId: string;
|
|
106
|
+
property: string;
|
|
107
|
+
value: number;
|
|
108
|
+
}
|
|
81
109
|
|
|
82
110
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
111
|
+
* Build the `instantPatch` for a value-only `tl.set` from the SAME
|
|
112
|
+
* `update-property` mutation(s) that are POSTed — so the patch can never carry a
|
|
113
|
+
* value the source write didn't (one source of truth). Each mutation contributes
|
|
114
|
+
* its `{property: value}` channel to the patch's props.
|
|
86
115
|
*/
|
|
87
|
-
|
|
116
|
+
function setPatchFromUpdateProperties(
|
|
117
|
+
selector: string,
|
|
118
|
+
mutations: UpdatePropertyMutation[],
|
|
119
|
+
): { selector: string; change: RuntimeTweenChange } {
|
|
120
|
+
const props: SetPatchProps = {};
|
|
121
|
+
for (const m of mutations) props[m.property as keyof SetPatchProps] = m.value;
|
|
122
|
+
return { selector, change: { kind: "set", props } };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Single-mutation convenience over {@link setPatchFromUpdateProperties}. */
|
|
126
|
+
function setPatchFromUpdateProperty(
|
|
127
|
+
selector: string,
|
|
128
|
+
mutation: UpdatePropertyMutation,
|
|
129
|
+
): { selector: string; change: RuntimeTweenChange } {
|
|
130
|
+
return setPatchFromUpdateProperties(selector, [mutation]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Find the studio position-hold `set` for a selector — a `tl.set("#el",{x,y})`
|
|
135
|
+
* with no duration. This is what a static-element nudge writes/updates.
|
|
136
|
+
*/
|
|
137
|
+
function findPositionSetAnimation(
|
|
138
|
+
animations: GsapAnimation[],
|
|
139
|
+
selector: string,
|
|
140
|
+
): GsapAnimation | null {
|
|
141
|
+
return (
|
|
142
|
+
animations.find(
|
|
143
|
+
(a) =>
|
|
144
|
+
a.method === "set" &&
|
|
145
|
+
a.targetSelector === selector &&
|
|
146
|
+
("x" in a.properties || "y" in a.properties),
|
|
147
|
+
) ?? null
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Commit a STATIC element drag as a `tl.set("#el",{x,y})` — the single-source
|
|
153
|
+
* position channel for elements with no position animation. Idempotent: a
|
|
154
|
+
* re-nudge of an element that already has a `set` UPDATES that set's x/y
|
|
155
|
+
* (two `update-property` mutations) rather than stacking a second set or
|
|
156
|
+
* converting it to keyframes (plan R2 / KTD3). New elements get one `add`
|
|
157
|
+
* mutation with `method:"set"` at position 0.
|
|
158
|
+
*/
|
|
159
|
+
export async function commitStaticGsapPosition(
|
|
88
160
|
selection: DomEditSelection,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
tweenDuration: number,
|
|
161
|
+
studioOffset: { x: number; y: number },
|
|
162
|
+
gsapPos: { x: number; y: number },
|
|
163
|
+
selector: string,
|
|
164
|
+
existingSet: GsapAnimation | null,
|
|
94
165
|
callbacks: GsapDragCommitCallbacks,
|
|
95
|
-
beforeReload?: () => void,
|
|
96
166
|
): Promise<void> {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
167
|
+
const { newX, newY } = computeDraggedGsapPosition(selection.element, studioOffset, gsapPos);
|
|
168
|
+
if (existingSet) {
|
|
169
|
+
// Update in place — two single-property mutations (the API updates one prop
|
|
170
|
+
// per call). Coalesce them and reload only after the second lands.
|
|
171
|
+
const coalesceKey = `gsap:set-nudge:${existingSet.id}`;
|
|
172
|
+
// Build each mutation FIRST, then derive its instantPatch from the SAME
|
|
173
|
+
// object that's POSTed — so a future caller can't ship a clean mutation with
|
|
174
|
+
// a stale/malformed patch (the validated `value` flows straight into the
|
|
175
|
+
// patch). `findUnsafeMutationValues` validates the mutation upstream.
|
|
176
|
+
const xMutation = {
|
|
177
|
+
type: "update-property",
|
|
178
|
+
animationId: existingSet.id,
|
|
179
|
+
property: "x",
|
|
180
|
+
value: newX,
|
|
181
|
+
} as const;
|
|
182
|
+
const yMutation = {
|
|
183
|
+
type: "update-property",
|
|
184
|
+
animationId: existingSet.id,
|
|
185
|
+
property: "y",
|
|
186
|
+
value: newY,
|
|
187
|
+
} as const;
|
|
188
|
+
// Patch BOTH coalesced commits. If the SECOND POST fails server-side, the
|
|
189
|
+
// first (x) already persisted — patching its commit too means the live
|
|
190
|
+
// preview still reflects what DID persist. The x commit carries skipReload
|
|
191
|
+
// (no reload), so its instantPatch gives instant feedback without a reload;
|
|
192
|
+
// the y commit triggers the soft reload (skipped when the patch applies).
|
|
193
|
+
await callbacks.commitMutation(selection, xMutation, {
|
|
194
|
+
label: "Move layer",
|
|
195
|
+
skipReload: true,
|
|
196
|
+
coalesceKey,
|
|
197
|
+
instantPatch: setPatchFromUpdateProperty(selector, xMutation),
|
|
198
|
+
});
|
|
199
|
+
await callbacks.commitMutation(selection, yMutation, {
|
|
200
|
+
label: "Move layer",
|
|
201
|
+
softReload: true,
|
|
202
|
+
coalesceKey,
|
|
203
|
+
// Final commit of the coalesced x/y pair: carry both channels so the
|
|
204
|
+
// runtime `tl.set` lands the complete {x,y} pose in place.
|
|
205
|
+
instantPatch: setPatchFromUpdateProperties(selector, [xMutation, yMutation]),
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
108
208
|
}
|
|
109
|
-
|
|
110
|
-
const targetPct = Math.round(((targetTime - newStart) / newDuration) * 1000) / 10;
|
|
111
|
-
remappedKfs.push({ percentage: targetPct, properties });
|
|
112
|
-
|
|
113
|
-
remappedKfs.sort((a, b) => a.percentage - b.percentage);
|
|
114
|
-
|
|
115
209
|
await callbacks.commitMutation(
|
|
116
210
|
selection,
|
|
117
211
|
{
|
|
118
|
-
type: "
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
position:
|
|
122
|
-
|
|
123
|
-
keyframes: remappedKfs,
|
|
212
|
+
type: "add",
|
|
213
|
+
targetSelector: selector,
|
|
214
|
+
method: "set",
|
|
215
|
+
position: 0,
|
|
216
|
+
properties: { x: newX, y: newY },
|
|
124
217
|
},
|
|
125
|
-
{ label:
|
|
218
|
+
{ label: "Move layer", softReload: true },
|
|
126
219
|
);
|
|
127
220
|
}
|
|
128
221
|
|
|
129
|
-
|
|
130
|
-
|
|
222
|
+
export { findPositionSetAnimation };
|
|
223
|
+
|
|
224
|
+
function findRotationSetAnimation(
|
|
225
|
+
animations: GsapAnimation[],
|
|
226
|
+
selector: string,
|
|
227
|
+
): GsapAnimation | null {
|
|
228
|
+
return (
|
|
229
|
+
animations.find(
|
|
230
|
+
(a) => a.method === "set" && a.targetSelector === selector && "rotation" in a.properties,
|
|
231
|
+
) ?? null
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Commit a STATIC element rotation as a `tl.set("#el",{rotation})` — the single-
|
|
237
|
+
* source rotation channel for elements with no rotation animation (mirrors
|
|
238
|
+
* `commitStaticGsapPosition`). `newRotation` is the already-resolved absolute angle
|
|
239
|
+
* (current runtime rotation + drag delta). Idempotent: re-rotating an element that
|
|
240
|
+
* already has a rotation `set` UPDATES it in place (one `update-property`, rotation
|
|
241
|
+
* is a single value unlike x/y); a new element gets one `add` with `method:"set"`.
|
|
242
|
+
*/
|
|
243
|
+
export async function commitStaticGsapRotation(
|
|
131
244
|
selection: DomEditSelection,
|
|
132
|
-
|
|
133
|
-
|
|
245
|
+
newRotation: number,
|
|
246
|
+
selector: string,
|
|
247
|
+
existingSet: GsapAnimation | null,
|
|
134
248
|
callbacks: GsapDragCommitCallbacks,
|
|
135
|
-
beforeReload?: () => void,
|
|
136
249
|
): Promise<void> {
|
|
137
|
-
|
|
138
|
-
|
|
250
|
+
if (existingSet) {
|
|
251
|
+
// Derive the instantPatch from the SAME mutation object that's POSTed (single
|
|
252
|
+
// source of truth — see commitStaticGsapPosition), so the validated `value`
|
|
253
|
+
// flows into the patch and the two can't drift.
|
|
254
|
+
const rotationMutation = {
|
|
255
|
+
type: "update-property",
|
|
256
|
+
animationId: existingSet.id,
|
|
257
|
+
property: "rotation",
|
|
258
|
+
value: newRotation,
|
|
259
|
+
} as const;
|
|
260
|
+
await callbacks.commitMutation(selection, rotationMutation, {
|
|
261
|
+
label: "Rotate layer",
|
|
262
|
+
softReload: true,
|
|
263
|
+
// Value-only rotation set: patch the runtime `tl.set` rotation in place.
|
|
264
|
+
instantPatch: setPatchFromUpdateProperty(selector, rotationMutation),
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
139
268
|
await callbacks.commitMutation(
|
|
140
269
|
selection,
|
|
141
270
|
{
|
|
142
|
-
type: "add
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
271
|
+
type: "add",
|
|
272
|
+
targetSelector: selector,
|
|
273
|
+
method: "set",
|
|
274
|
+
position: 0,
|
|
275
|
+
properties: { rotation: newRotation },
|
|
146
276
|
},
|
|
147
|
-
{ label:
|
|
277
|
+
{ label: "Rotate layer", softReload: true },
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export { findRotationSetAnimation };
|
|
282
|
+
|
|
283
|
+
function findSizeSetAnimation(animations: GsapAnimation[], selector: string): GsapAnimation | null {
|
|
284
|
+
return (
|
|
285
|
+
animations.find(
|
|
286
|
+
(a) =>
|
|
287
|
+
a.method === "set" &&
|
|
288
|
+
a.targetSelector === selector &&
|
|
289
|
+
("width" in a.properties || "height" in a.properties),
|
|
290
|
+
) ?? null
|
|
148
291
|
);
|
|
149
|
-
if (activeKeyframePct != null) setActiveKeyframePct(null);
|
|
150
292
|
}
|
|
151
293
|
|
|
152
294
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
295
|
+
* Commit a STATIC element resize as a `tl.set("#el",{width,height})` — the
|
|
296
|
+
* single-source size channel for elements with no size animation (mirrors
|
|
297
|
+
* `commitStaticGsapPosition`). Use this instead of a single-stop `keyframes`
|
|
298
|
+
* tween: one keyframe at the playhead % renders NaN/0 at every other frame, so
|
|
299
|
+
* the element collapses/disappears (worst when resized off the 0% mark). A `set`
|
|
300
|
+
* holds the size at all times. Re-resizing an element that already has a size
|
|
301
|
+
* `set` UPDATES it in place (two `update-property`, like x/y); a new element
|
|
302
|
+
* gets one `add` with `method:"set"`.
|
|
155
303
|
*/
|
|
156
|
-
|
|
157
|
-
async function commitFlatViaKeyframes(
|
|
304
|
+
export async function commitStaticGsapSize(
|
|
158
305
|
selection: DomEditSelection,
|
|
159
|
-
|
|
160
|
-
|
|
306
|
+
size: { width: number; height: number },
|
|
307
|
+
selector: string,
|
|
308
|
+
existingSet: GsapAnimation | null,
|
|
161
309
|
callbacks: GsapDragCommitCallbacks,
|
|
162
|
-
beforeReload?: () => void,
|
|
163
|
-
iframe?: HTMLIFrameElement | null,
|
|
164
|
-
selector?: string,
|
|
165
310
|
): Promise<void> {
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// not the identity value (x=0) that a blind convert would produce.
|
|
174
|
-
const resolvedFromValues: Record<string, number | string> = {};
|
|
175
|
-
if (iframe && selector && ts !== null) {
|
|
176
|
-
try {
|
|
177
|
-
const iframeWin = iframe.contentWindow as any;
|
|
178
|
-
const gsapLib = iframeWin?.gsap;
|
|
179
|
-
const el = iframe.contentDocument?.querySelector(selector);
|
|
180
|
-
const timelines = iframeWin?.__timelines;
|
|
181
|
-
const mainTl = timelines ? (Object.values(timelines)[0] as any) : null;
|
|
182
|
-
if (gsapLib && el && mainTl?.seek) {
|
|
183
|
-
mainTl.seek(ts);
|
|
184
|
-
for (const key of Object.keys(properties)) {
|
|
185
|
-
const v = Number(gsapLib.getProperty(el, key));
|
|
186
|
-
if (Number.isFinite(v)) resolvedFromValues[key] = roundTo3(v);
|
|
187
|
-
}
|
|
188
|
-
mainTl.seek(ct);
|
|
189
|
-
}
|
|
190
|
-
} catch {
|
|
191
|
-
/* iframe access failed — fall back to identity values */
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (outsideRange && ts !== null) {
|
|
196
|
-
// Outside the tween's range: add a brand new keyframed tween at the drag
|
|
197
|
-
// time instead of extending/replacing the existing one. This keeps all
|
|
198
|
-
// existing tweens untouched and creates a clean hold at the dragged position.
|
|
199
|
-
const tweenEnd = ts + td;
|
|
200
|
-
const holdStart = ct > tweenEnd ? tweenEnd : ct;
|
|
201
|
-
const holdEnd = ct > tweenEnd ? ct : ts;
|
|
202
|
-
const holdDur = Math.max(0.01, holdEnd - holdStart);
|
|
203
|
-
const kfs =
|
|
204
|
-
ct > tweenEnd
|
|
205
|
-
? [
|
|
206
|
-
{ percentage: 0, properties: resolvedFromValues },
|
|
207
|
-
{ percentage: 100, properties },
|
|
208
|
-
]
|
|
209
|
-
: [
|
|
210
|
-
{ percentage: 0, properties },
|
|
211
|
-
{ percentage: 100, properties: resolvedFromValues },
|
|
212
|
-
];
|
|
213
|
-
console.log(
|
|
214
|
-
"[drag:5] outside range — adding new tween",
|
|
215
|
-
JSON.stringify({
|
|
216
|
-
ct,
|
|
217
|
-
ts,
|
|
218
|
-
td,
|
|
219
|
-
holdStart: roundTo3(holdStart),
|
|
220
|
-
holdDur: roundTo3(holdDur),
|
|
221
|
-
from: resolvedFromValues,
|
|
222
|
-
to: properties,
|
|
223
|
-
}),
|
|
311
|
+
const width = Math.round(size.width);
|
|
312
|
+
const height = Math.round(size.height);
|
|
313
|
+
if (existingSet) {
|
|
314
|
+
await callbacks.commitMutation(
|
|
315
|
+
selection,
|
|
316
|
+
{ type: "delete", animationId: existingSet.id },
|
|
317
|
+
{ label: "Resize layer", skipReload: true },
|
|
224
318
|
);
|
|
225
319
|
await callbacks.commitMutation(
|
|
226
320
|
selection,
|
|
227
321
|
{
|
|
228
|
-
type: "add
|
|
229
|
-
targetSelector:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
322
|
+
type: "add",
|
|
323
|
+
targetSelector: selector,
|
|
324
|
+
method: "set",
|
|
325
|
+
position: 0,
|
|
326
|
+
properties: { width, height },
|
|
233
327
|
},
|
|
234
|
-
{ label: "
|
|
328
|
+
{ label: "Resize layer", softReload: true },
|
|
235
329
|
);
|
|
236
330
|
return;
|
|
237
331
|
}
|
|
238
|
-
|
|
239
|
-
// Inside range: convert the flat tween to keyframes, then add at current %.
|
|
240
|
-
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
|
241
332
|
await callbacks.commitMutation(
|
|
242
333
|
selection,
|
|
243
334
|
{
|
|
244
|
-
type: "
|
|
245
|
-
|
|
246
|
-
|
|
335
|
+
type: "add",
|
|
336
|
+
targetSelector: selector,
|
|
337
|
+
method: "set",
|
|
338
|
+
position: 0,
|
|
339
|
+
properties: { width, height },
|
|
247
340
|
},
|
|
248
|
-
{ label: "
|
|
249
|
-
);
|
|
250
|
-
const pct = computeCurrentPercentage(selection, anim);
|
|
251
|
-
|
|
252
|
-
await callbacks.commitMutation(
|
|
253
|
-
selection,
|
|
254
|
-
{
|
|
255
|
-
type: "add-keyframe",
|
|
256
|
-
animationId: anim.id,
|
|
257
|
-
percentage: pct,
|
|
258
|
-
properties,
|
|
259
|
-
},
|
|
260
|
-
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload, coalesceKey },
|
|
341
|
+
{ label: "Resize layer", softReload: true },
|
|
261
342
|
);
|
|
262
343
|
}
|
|
263
344
|
|
|
264
|
-
|
|
345
|
+
export { findSizeSetAnimation };
|
|
346
|
+
|
|
347
|
+
// ── Whole-path offset (plain drag on animated element) ──────────────────
|
|
265
348
|
|
|
266
349
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
350
|
+
* Offset the entire animation path by the drag delta — every keyframe's x/y
|
|
351
|
+
* shifts together so the animation shape is preserved and the element can't
|
|
352
|
+
* dart off-screen. For flat tweens (no keyframes), convert first then shift.
|
|
269
353
|
*/
|
|
270
354
|
// fallow-ignore-next-line complexity
|
|
271
|
-
export async function
|
|
355
|
+
export async function commitWholePathOffset(
|
|
272
356
|
selection: DomEditSelection,
|
|
273
357
|
anim: GsapAnimation,
|
|
274
358
|
studioOffset: { x: number; y: number },
|
|
@@ -277,24 +361,16 @@ export async function commitGsapPositionFromDrag(
|
|
|
277
361
|
selector: string,
|
|
278
362
|
callbacks: GsapDragCommitCallbacks,
|
|
279
363
|
): Promise<void> {
|
|
280
|
-
const rotStyle = selection.element.style.getPropertyValue("--hf-studio-rotation");
|
|
281
|
-
const rotDeg = Number.parseFloat(rotStyle) || 0;
|
|
282
|
-
const rad = (-rotDeg * Math.PI) / 180;
|
|
283
|
-
const cos = Math.cos(rad);
|
|
284
|
-
const sin = Math.sin(rad);
|
|
285
364
|
const el = selection.element;
|
|
365
|
+
const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
|
|
366
|
+
el,
|
|
367
|
+
studioOffset,
|
|
368
|
+
gsapPos,
|
|
369
|
+
);
|
|
370
|
+
const deltaX = newX - baseGsapX;
|
|
371
|
+
const deltaY = newY - baseGsapY;
|
|
286
372
|
const origX = Number.parseFloat(el.getAttribute("data-hf-drag-initial-offset-x") ?? "") || 0;
|
|
287
373
|
const origY = Number.parseFloat(el.getAttribute("data-hf-drag-initial-offset-y") ?? "") || 0;
|
|
288
|
-
const deltaX = studioOffset.x - origX;
|
|
289
|
-
const deltaY = studioOffset.y - origY;
|
|
290
|
-
const adjX = deltaX * cos - deltaY * sin;
|
|
291
|
-
const adjY = deltaX * sin + deltaY * cos;
|
|
292
|
-
const parsedBaseX = Number.parseFloat(el.getAttribute("data-hf-drag-gsap-base-x") ?? "");
|
|
293
|
-
const parsedBaseY = Number.parseFloat(el.getAttribute("data-hf-drag-gsap-base-y") ?? "");
|
|
294
|
-
const baseGsapX = Number.isFinite(parsedBaseX) ? parsedBaseX : gsapPos.x;
|
|
295
|
-
const baseGsapY = Number.isFinite(parsedBaseY) ? parsedBaseY : gsapPos.y;
|
|
296
|
-
const newX = Math.round(baseGsapX + adjX);
|
|
297
|
-
const newY = Math.round(baseGsapY + adjY);
|
|
298
374
|
const restoreOffset = () => {
|
|
299
375
|
el.style.setProperty("--hf-studio-offset-x", `${origX}px`);
|
|
300
376
|
el.style.setProperty("--hf-studio-offset-y", `${origY}px`);
|
|
@@ -302,141 +378,53 @@ export async function commitGsapPositionFromDrag(
|
|
|
302
378
|
el.removeAttribute("data-hf-drag-initial-offset-y");
|
|
303
379
|
};
|
|
304
380
|
|
|
305
|
-
|
|
381
|
+
let effectiveAnim = anim;
|
|
306
382
|
if (anim.keyframes) {
|
|
307
383
|
const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const ts = resolveTweenStart(effectiveAnim);
|
|
312
|
-
const td = resolveTweenDuration(effectiveAnim);
|
|
313
|
-
const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
314
|
-
if (outsideRange) {
|
|
315
|
-
await extendTweenAndAddKeyframe(
|
|
316
|
-
selection,
|
|
317
|
-
effectiveAnim,
|
|
318
|
-
dragProps,
|
|
319
|
-
ct,
|
|
320
|
-
ts,
|
|
321
|
-
td,
|
|
322
|
-
callbacks,
|
|
323
|
-
restoreOffset,
|
|
324
|
-
);
|
|
325
|
-
} else {
|
|
326
|
-
await commitKeyframedPosition(selection, effectiveAnim, dragProps, callbacks, restoreOffset);
|
|
327
|
-
}
|
|
328
|
-
} else if (anim.method === "from" || anim.method === "fromTo") {
|
|
329
|
-
const ct = usePlayerStore.getState().currentTime;
|
|
330
|
-
const ts = resolveTweenStart(anim);
|
|
331
|
-
const td = resolveTweenDuration(anim);
|
|
332
|
-
const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
|
|
333
|
-
const dragProps: Record<string, number> = { x: newX, y: newY };
|
|
334
|
-
|
|
335
|
-
if (outsideRange && ts !== null) {
|
|
336
|
-
// Split the original from() tween into property groups first.
|
|
337
|
-
await callbacks.commitMutation(
|
|
338
|
-
selection,
|
|
339
|
-
{ type: "split-into-property-groups", animationId: anim.id },
|
|
340
|
-
{ label: "Split from() for drag", skipReload: true },
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
const allAnims = callbacks.fetchAnimations ? await callbacks.fetchAnimations() : [];
|
|
344
|
-
const existingPosAnim = allAnims.find(
|
|
345
|
-
(a) => a.propertyGroup === "position" && a.targetSelector === anim.targetSelector,
|
|
346
|
-
);
|
|
384
|
+
if (newId) effectiveAnim = { ...anim, id: newId };
|
|
385
|
+
}
|
|
347
386
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const posTd = resolveTweenDuration(existingPosAnim);
|
|
352
|
-
if (posTs !== null) {
|
|
353
|
-
await extendTweenAndAddKeyframe(
|
|
354
|
-
selection,
|
|
355
|
-
existingPosAnim,
|
|
356
|
-
{ x: newX, y: newY },
|
|
357
|
-
ct,
|
|
358
|
-
posTs,
|
|
359
|
-
posTd,
|
|
360
|
-
callbacks,
|
|
361
|
-
restoreOffset,
|
|
362
|
-
);
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
387
|
+
const ts = resolveTweenStart(effectiveAnim);
|
|
388
|
+
const td = resolveTweenDuration(effectiveAnim);
|
|
389
|
+
const ease = effectiveAnim.keyframes?.easeEach ?? effectiveAnim.ease;
|
|
366
390
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
391
|
+
let kfs = effectiveAnim.keyframes?.keyframes ?? [];
|
|
392
|
+
if (kfs.length === 0) {
|
|
393
|
+
const fromProps = effectiveAnim.fromProperties ?? {};
|
|
394
|
+
const toProps = effectiveAnim.properties ?? {};
|
|
395
|
+
const startX =
|
|
396
|
+
typeof fromProps.x === "number" ? fromProps.x : typeof toProps.x === "number" ? 0 : 0;
|
|
397
|
+
const startY =
|
|
398
|
+
typeof fromProps.y === "number" ? fromProps.y : typeof toProps.y === "number" ? 0 : 0;
|
|
399
|
+
const endX = typeof toProps.x === "number" ? toProps.x : startX;
|
|
400
|
+
const endY = typeof toProps.y === "number" ? toProps.y : startY;
|
|
401
|
+
kfs = [
|
|
402
|
+
{ percentage: 0, properties: { x: startX, y: startY } },
|
|
403
|
+
{ percentage: 100, properties: { x: endX, y: endY } },
|
|
404
|
+
];
|
|
405
|
+
}
|
|
374
406
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
keyframes.push({ percentage: 0, properties: { x: 0, y: 0 } });
|
|
385
|
-
if (origEndPct > 0.5 && origEndPct < 99.5) {
|
|
386
|
-
keyframes.push({ percentage: origEndPct, properties: { x: 0, y: 0 } });
|
|
387
|
-
}
|
|
388
|
-
keyframes.push({ percentage: 100, properties: { x: newX, y: newY } });
|
|
389
|
-
}
|
|
390
|
-
keyframes.sort((a, b) => a.percentage - b.percentage);
|
|
407
|
+
const shifted = kfs.map((kf) => ({
|
|
408
|
+
percentage: kf.percentage,
|
|
409
|
+
properties: {
|
|
410
|
+
...kf.properties,
|
|
411
|
+
x: roundTo3((typeof kf.properties.x === "number" ? kf.properties.x : 0) + deltaX),
|
|
412
|
+
y: roundTo3((typeof kf.properties.y === "number" ? kf.properties.y : 0) + deltaY),
|
|
413
|
+
},
|
|
414
|
+
...(kf.ease ? { ease: kf.ease } : {}),
|
|
415
|
+
}));
|
|
391
416
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const coalesceKey = `gsap:convert-drag:${anim.id}`;
|
|
406
|
-
await callbacks.commitMutation(
|
|
407
|
-
selection,
|
|
408
|
-
{
|
|
409
|
-
type: "convert-to-keyframes",
|
|
410
|
-
animationId: anim.id,
|
|
411
|
-
},
|
|
412
|
-
{ label: "Convert from() for drag", skipReload: true, coalesceKey },
|
|
413
|
-
);
|
|
414
|
-
const pct = computeCurrentPercentage(selection, anim);
|
|
415
|
-
await callbacks.commitMutation(
|
|
416
|
-
selection,
|
|
417
|
-
{
|
|
418
|
-
type: "add-keyframe",
|
|
419
|
-
animationId: anim.id,
|
|
420
|
-
percentage: pct,
|
|
421
|
-
properties: dragProps,
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
label: `Move layer (keyframe ${pct}%)`,
|
|
425
|
-
softReload: true,
|
|
426
|
-
beforeReload: restoreOffset,
|
|
427
|
-
coalesceKey,
|
|
428
|
-
},
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
} else {
|
|
432
|
-
await commitFlatViaKeyframes(
|
|
433
|
-
selection,
|
|
434
|
-
anim,
|
|
435
|
-
{ x: newX, y: newY },
|
|
436
|
-
callbacks,
|
|
437
|
-
restoreOffset,
|
|
438
|
-
iframe,
|
|
439
|
-
selector,
|
|
440
|
-
);
|
|
441
|
-
}
|
|
417
|
+
await callbacks.commitMutation(
|
|
418
|
+
selection,
|
|
419
|
+
{
|
|
420
|
+
type: "replace-with-keyframes",
|
|
421
|
+
animationId: effectiveAnim.id,
|
|
422
|
+
targetSelector: effectiveAnim.targetSelector,
|
|
423
|
+
position: roundTo3(ts ?? 0),
|
|
424
|
+
duration: roundTo3(td || 1),
|
|
425
|
+
keyframes: shifted,
|
|
426
|
+
ease,
|
|
427
|
+
},
|
|
428
|
+
{ label: "Move animation path", softReload: true, beforeReload: restoreOffset },
|
|
429
|
+
);
|
|
442
430
|
}
|