@hyperframes/studio 0.7.6 → 0.7.8
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-B_NnmU6q.js → index--lOAjFJl.js} +1 -1
- package/dist/assets/{index-ysftPins.js → index-BSyZKYmH.js} +204 -203
- package/dist/assets/{index-DsBhGFPe.js → index-Dgeszckd.js} +1 -1
- package/dist/assets/index-svYFaNuq.css +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.html +2 -2
- package/dist/index.js +3650 -2431
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/components/StudioRightPanel.tsx +7 -1
- package/src/components/editor/AnimationCard.tsx +6 -0
- package/src/components/editor/DomEditOverlay.tsx +4 -12
- package/src/components/editor/EaseCurveSection.tsx +175 -99
- package/src/components/editor/GsapAnimationSection.tsx +2 -0
- package/src/components/editor/KeyframeEaseList.tsx +67 -3
- package/src/components/editor/MarqueeOverlay.tsx +40 -0
- package/src/components/editor/OffCanvasIndicators.tsx +2 -7
- package/src/components/editor/PropertyPanel.tsx +23 -2
- package/src/components/editor/Transform3DCube.tsx +313 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +2 -0
- package/src/components/editor/gsapAnimationConstants.ts +11 -35
- package/src/components/editor/gsapAnimationHelpers.test.ts +1 -1
- package/src/components/editor/marqueeCommit.ts +63 -20
- package/src/components/editor/propertyPanel3dTransform.tsx +311 -92
- package/src/components/editor/propertyPanelHelpers.ts +46 -18
- package/src/components/editor/propertyPanelTimingSection.tsx +35 -2
- package/src/components/editor/transform3dProjection.test.ts +99 -0
- package/src/components/editor/transform3dProjection.ts +172 -0
- package/src/components/editor/useMotionPathData.ts +2 -1
- package/src/components/sidebar/AssetContextMenu.tsx +97 -0
- package/src/components/sidebar/AssetsTab.tsx +364 -266
- package/src/components/sidebar/AudioRow.tsx +202 -0
- package/src/components/sidebar/assetHelpers.ts +40 -0
- package/src/contexts/DomEditContext.tsx +8 -0
- package/src/hooks/gsapDragCommit.test.ts +9 -5
- package/src/hooks/gsapDragCommit.ts +129 -9
- package/src/hooks/gsapRuntimeBridge.ts +22 -0
- package/src/hooks/gsapRuntimeKeyframes.test.ts +47 -0
- package/src/hooks/gsapRuntimeKeyframes.ts +63 -5
- package/src/hooks/gsapRuntimePatch.ts +162 -35
- package/src/hooks/useAnimatedPropertyCommit.ts +256 -78
- package/src/hooks/useDomEditCommits.ts +0 -2
- package/src/hooks/useDomEditSession.ts +24 -2
- package/src/hooks/useDomEditWiring.ts +3 -0
- package/src/hooks/useDomGeometryCommits.ts +3 -31
- package/src/hooks/useGestureCommit.ts +0 -4
- package/src/hooks/useGsapAwareEditing.ts +31 -5
- package/src/hooks/useGsapKeyframeOps.ts +4 -1
- package/src/hooks/useGsapScriptCommits.ts +0 -12
- package/src/hooks/useGsapSelectionHandlers.ts +12 -9
- package/src/hooks/useGsapTweenCache.test.ts +45 -1
- package/src/hooks/useGsapTweenCache.ts +125 -42
- package/src/hooks/useMusicBeatAnalysis.ts +36 -21
- package/src/hooks/useRazorSplit.ts +0 -3
- package/src/utils/marqueeGeometry.test.ts +15 -98
- package/src/utils/marqueeGeometry.ts +1 -158
- package/dist/assets/index-wJZf6FK3.css +0 -1
- package/src/utils/editDebugLog.ts +0 -9
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unified helper for committing any GSAP property value from the design panel.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Routing depends on whether the element is animated (has keyframes on any tween):
|
|
5
|
+
* - Animated → write the value into a keyframe at the current playhead (convert a
|
|
6
|
+
* flat tween first if needed). An existing static `set` auto-converts to keyframes.
|
|
7
|
+
* - Static (no keyframes anywhere) → persist as a `tl.set`, NEVER keyframes — same
|
|
8
|
+
* as manual drag / resize / rotate. Updates an existing set or creates one.
|
|
8
9
|
*/
|
|
9
10
|
import { useCallback } from "react";
|
|
10
11
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
@@ -12,6 +13,7 @@ import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
|
|
|
12
13
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
13
14
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
14
15
|
import { readAllAnimatedProperties, readGsapProperty } from "./gsapRuntimeBridge";
|
|
16
|
+
import type { SetPatchProps } from "./gsapRuntimePatch";
|
|
15
17
|
import { selectorFromSelection, computeElementPercentage } from "./gsapShared";
|
|
16
18
|
|
|
17
19
|
interface CommitAnimatedPropertyDeps {
|
|
@@ -47,6 +49,7 @@ function pickBestAnimation(
|
|
|
47
49
|
const currentTime = usePlayerStore.getState().currentTime;
|
|
48
50
|
const targetGroup = property ? classifyPropertyGroup(property) : undefined;
|
|
49
51
|
|
|
52
|
+
// fallow-ignore-next-line complexity
|
|
50
53
|
const scored = animations.map((a) => {
|
|
51
54
|
let score = 0;
|
|
52
55
|
if (targetGroup && a.propertyGroup === targetGroup) score += 20;
|
|
@@ -62,96 +65,271 @@ function pickBestAnimation(
|
|
|
62
65
|
return scored[0]?.anim;
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Auto-keyframe a just-updated static `set`: if the element is already animated
|
|
70
|
+
* (its clip carries keyframes on another tween), convert the set to keyframes so
|
|
71
|
+
* subsequent edits at other playheads interpolate — matching the drag / resize /
|
|
72
|
+
* rotate UX. Purely static elements (no other keyframes) are left as a set.
|
|
73
|
+
*/
|
|
74
|
+
async function maybeAutoKeyframeSet(
|
|
75
|
+
selection: DomEditSelection,
|
|
76
|
+
setAnim: GsapAnimation,
|
|
77
|
+
animations: GsapAnimation[],
|
|
78
|
+
commit: NonNullable<CommitAnimatedPropertyDeps["gsapCommitMutation"]>,
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
const animatedTween = animations.find((a) => a.keyframes && a.id !== setAnim.id);
|
|
81
|
+
if (!animatedTween) return;
|
|
82
|
+
await commit(
|
|
83
|
+
selection,
|
|
84
|
+
{
|
|
85
|
+
type: "convert-to-keyframes",
|
|
86
|
+
animationId: setAnim.id,
|
|
87
|
+
duration: animatedTween.duration ?? 1,
|
|
88
|
+
},
|
|
89
|
+
{ label: "Keyframe 3D transform", softReload: true },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type Commit = NonNullable<CommitAnimatedPropertyDeps["gsapCommitMutation"]>;
|
|
94
|
+
|
|
95
|
+
/** Merge ALL props into the static `set` in ONE commit (value-only, instant), then
|
|
96
|
+
* auto-keyframe. One mutation — a per-property loop would shift the set's
|
|
97
|
+
* group-derived id mid-way (e.g. reset adding `scale` to a rotation set), 404-ing
|
|
98
|
+
* the next update. */
|
|
99
|
+
async function commitSetProps(
|
|
100
|
+
selection: DomEditSelection,
|
|
101
|
+
setAnim: GsapAnimation,
|
|
102
|
+
propEntries: [string, number | string][],
|
|
103
|
+
selector: string | null,
|
|
104
|
+
animations: GsapAnimation[],
|
|
105
|
+
commit: Commit,
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
const properties = Object.fromEntries(propEntries);
|
|
108
|
+
const numericProps: SetPatchProps = {};
|
|
109
|
+
for (const [k, v] of propEntries) {
|
|
110
|
+
if (typeof v === "number") numericProps[k as keyof SetPatchProps] = v;
|
|
111
|
+
}
|
|
112
|
+
const instantPatch =
|
|
113
|
+
selector && Object.keys(numericProps).length > 0
|
|
114
|
+
? {
|
|
115
|
+
selector,
|
|
116
|
+
change: {
|
|
117
|
+
kind: (setAnim.global ? "global-set" : "set") as "set" | "global-set",
|
|
118
|
+
props: numericProps,
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
: undefined;
|
|
122
|
+
await commit(
|
|
123
|
+
selection,
|
|
124
|
+
{ type: "update-properties", animationId: setAnim.id, properties },
|
|
125
|
+
{ label: "Set 3D transform", softReload: true, ...(instantPatch ? { instantPatch } : {}) },
|
|
126
|
+
);
|
|
127
|
+
await maybeAutoKeyframeSet(selection, setAnim, animations, commit);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Static element (no keyframes on ANY of its tweens): persist the 3D props as a
|
|
132
|
+
* `tl.set` — NEVER keyframes. Mirrors manual drag / resize / rotate, which `tl.set`
|
|
133
|
+
* a static element instead of animating it. Updates an existing `set` in place, or
|
|
134
|
+
* creates a dedicated `set` at position 0 when the element has none.
|
|
135
|
+
*/
|
|
136
|
+
async function commitStaticSet(
|
|
137
|
+
selection: DomEditSelection,
|
|
138
|
+
propEntries: [string, number | string][],
|
|
139
|
+
selector: string | null,
|
|
140
|
+
animations: GsapAnimation[],
|
|
141
|
+
commit: Commit,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
if (!selector) return;
|
|
144
|
+
// Update an existing `set` in ONE batched commit — NEVER a flat `to`/`from`. A
|
|
145
|
+
// set's id is GROUP-derived, so a per-prop loop shifts it the instant a new-group
|
|
146
|
+
// prop lands (e.g. `scale` onto a rotation set), 404-ing the next prop; commitSetProps
|
|
147
|
+
// sends them together. A static element with no set gets a dedicated `set` carrying
|
|
148
|
+
// ALL props in ONE `add`.
|
|
149
|
+
const existingSet = animations.find((a) => a.method === "set" && a.targetSelector === selector);
|
|
150
|
+
if (existingSet) {
|
|
151
|
+
await commitSetProps(selection, existingSet, propEntries, selector, animations, commit);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Base `gsap.set` (off-timeline) — a static hold with no 0% keyframe marker, so
|
|
155
|
+
// adjusting a 3D transform on a non-keyframed element doesn't drop a keyframe on
|
|
156
|
+
// the timeline (matches the manual-drag UX). The global-set instant patch applies
|
|
157
|
+
// it straight to the element so the first edit shows with no soft-reload flash.
|
|
158
|
+
const numericProps: SetPatchProps = {};
|
|
159
|
+
for (const [k, v] of propEntries) {
|
|
160
|
+
if (typeof v === "number") numericProps[k as keyof SetPatchProps] = v;
|
|
161
|
+
}
|
|
162
|
+
await commit(
|
|
163
|
+
selection,
|
|
164
|
+
{
|
|
165
|
+
type: "add",
|
|
166
|
+
targetSelector: selector,
|
|
167
|
+
method: "set",
|
|
168
|
+
position: 0,
|
|
169
|
+
properties: Object.fromEntries(propEntries),
|
|
170
|
+
global: true,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
label: "Set 3D transform",
|
|
174
|
+
softReload: true,
|
|
175
|
+
...(Object.keys(numericProps).length > 0
|
|
176
|
+
? {
|
|
177
|
+
instantPatch: {
|
|
178
|
+
selector,
|
|
179
|
+
change: { kind: "global-set" as const, props: numericProps },
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
: {}),
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Convert-if-flat, then write ALL props into ONE keyframe at the playhead. */
|
|
188
|
+
// fallow-ignore-next-line complexity
|
|
189
|
+
async function commitKeyframeProps(
|
|
190
|
+
selection: DomEditSelection,
|
|
191
|
+
anim: GsapAnimation,
|
|
192
|
+
props: Record<string, number | string>,
|
|
193
|
+
propEntries: [string, number | string][],
|
|
194
|
+
primaryProp: string,
|
|
195
|
+
selector: string | null,
|
|
196
|
+
iframe: HTMLIFrameElement | null,
|
|
197
|
+
commit: Commit,
|
|
198
|
+
): Promise<void> {
|
|
199
|
+
if (!anim.keyframes) {
|
|
200
|
+
await commit(
|
|
201
|
+
selection,
|
|
202
|
+
{ type: "convert-to-keyframes", animationId: anim.id },
|
|
203
|
+
{ label: "Convert to keyframes", skipReload: true },
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const pct = computeElementPercentage(usePlayerStore.getState().currentTime, selection, anim);
|
|
207
|
+
const runtimeProps = selector ? readAllAnimatedProperties(iframe, selector, anim) : {};
|
|
208
|
+
const properties: Record<string, number | string> = { ...runtimeProps, ...props };
|
|
209
|
+
|
|
210
|
+
const backfillDefaults: Record<string, number | string> = { ...runtimeProps };
|
|
211
|
+
for (const [property, value] of propEntries) {
|
|
212
|
+
if (!(property in runtimeProps) && selector) {
|
|
213
|
+
const cssVal = readGsapProperty(iframe, selector, property);
|
|
214
|
+
if (cssVal != null) backfillDefaults[property] = cssVal;
|
|
215
|
+
}
|
|
216
|
+
backfillDefaults[property] = value;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const existingKf = anim.keyframes?.keyframes.some((kf) => Math.abs(kf.percentage - pct) < 0.05);
|
|
220
|
+
// Rebuild the live keyframe tween in place so the edit shows instantly (no flash);
|
|
221
|
+
// rebuildKeyframeTween declines → soft reload if the tween can't be safely rebuilt.
|
|
222
|
+
const numericProps: Record<string, number> = {};
|
|
223
|
+
for (const [k, v] of Object.entries(properties)) {
|
|
224
|
+
if (typeof v === "number") numericProps[k] = v;
|
|
225
|
+
}
|
|
226
|
+
const instantPatch =
|
|
227
|
+
selector && Object.keys(numericProps).length > 0
|
|
228
|
+
? { selector, change: { kind: "keyframe-rebuild" as const, pct, props: numericProps } }
|
|
229
|
+
: undefined;
|
|
230
|
+
await commit(
|
|
231
|
+
selection,
|
|
232
|
+
existingKf
|
|
233
|
+
? { type: "update-keyframe", animationId: anim.id, percentage: pct, properties }
|
|
234
|
+
: {
|
|
235
|
+
type: "add-keyframe",
|
|
236
|
+
animationId: anim.id,
|
|
237
|
+
percentage: pct,
|
|
238
|
+
properties,
|
|
239
|
+
backfillDefaults,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
label: `Edit ${primaryProp} (keyframe ${pct}%)`,
|
|
243
|
+
softReload: true,
|
|
244
|
+
...(instantPatch ? { instantPatch } : {}),
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
65
249
|
export function useAnimatedPropertyCommit(deps: CommitAnimatedPropertyDeps) {
|
|
66
|
-
const {
|
|
67
|
-
selectedGsapAnimations,
|
|
68
|
-
gsapCommitMutation,
|
|
69
|
-
addGsapAnimation,
|
|
70
|
-
previewIframeRef,
|
|
71
|
-
bumpGsapCache,
|
|
72
|
-
} = deps;
|
|
250
|
+
const { selectedGsapAnimations, gsapCommitMutation, previewIframeRef, bumpGsapCache } = deps;
|
|
73
251
|
|
|
74
|
-
const
|
|
75
|
-
async (
|
|
76
|
-
selection: DomEditSelection,
|
|
77
|
-
property: string,
|
|
78
|
-
value: number | string,
|
|
79
|
-
): Promise<void> => {
|
|
252
|
+
const commitAnimatedProperties = useCallback(
|
|
253
|
+
async (selection: DomEditSelection, props: Record<string, number | string>): Promise<void> => {
|
|
80
254
|
if (!gsapCommitMutation) return;
|
|
255
|
+
const propEntries = Object.entries(props);
|
|
256
|
+
if (propEntries.length === 0) return;
|
|
257
|
+
const primaryProp = propEntries[0]![0];
|
|
81
258
|
|
|
82
259
|
const iframe = previewIframeRef.current;
|
|
83
260
|
const selector = selectorFromSelection(selection);
|
|
84
261
|
|
|
85
|
-
|
|
262
|
+
const anim: GsapAnimation | undefined = pickBestAnimation(
|
|
86
263
|
selectedGsapAnimations,
|
|
87
264
|
selector,
|
|
88
|
-
|
|
265
|
+
primaryProp,
|
|
89
266
|
);
|
|
267
|
+
// Whether the element is animated at all. A 3D edit only creates/edits
|
|
268
|
+
// keyframes when it IS — a static element (no keyframes on any of its tweens)
|
|
269
|
+
// gets a `tl.set`, never new keyframes (matches manual drag / resize / rotate).
|
|
270
|
+
const elementHasKeyframes = selectedGsapAnimations.some((a) => !!a.keyframes);
|
|
90
271
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
272
|
+
// The picked anim comes from the (possibly stale) panel cache: if keyframes
|
|
273
|
+
// were just removed or the script changed underneath us, its id is gone
|
|
274
|
+
// server-side and the commit 404s. The raw commit already toasts; we catch
|
|
275
|
+
// so the rejection doesn't escape as an uncaught promise, and bump the cache
|
|
276
|
+
// so selectedGsapAnimations re-syncs and the user's next edit self-heals.
|
|
277
|
+
try {
|
|
278
|
+
// Existing static hold — merge the props into the `set`, then auto-keyframe
|
|
279
|
+
// ONLY if the element is already animated (maybeAutoKeyframeSet no-ops if not).
|
|
280
|
+
if (anim?.method === "set") {
|
|
281
|
+
await commitSetProps(
|
|
282
|
+
selection,
|
|
283
|
+
anim,
|
|
284
|
+
propEntries,
|
|
285
|
+
selector,
|
|
286
|
+
selectedGsapAnimations,
|
|
287
|
+
gsapCommitMutation,
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
103
291
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
292
|
+
// Static element — persist as a `tl.set`, never keyframes (incl. the
|
|
293
|
+
// no-animation case, which now creates a set instead of a keyframed tween).
|
|
294
|
+
if (!elementHasKeyframes) {
|
|
295
|
+
await commitStaticSet(
|
|
296
|
+
selection,
|
|
297
|
+
propEntries,
|
|
298
|
+
selector,
|
|
299
|
+
selectedGsapAnimations,
|
|
300
|
+
gsapCommitMutation,
|
|
301
|
+
);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Animated element — write ALL props into ONE keyframe so a multi-axis cube
|
|
306
|
+
// edit doesn't race into adjacent duplicates.
|
|
307
|
+
if (!anim) {
|
|
308
|
+
bumpGsapCache();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
await commitKeyframeProps(
|
|
107
312
|
selection,
|
|
108
|
-
|
|
109
|
-
|
|
313
|
+
anim,
|
|
314
|
+
props,
|
|
315
|
+
propEntries,
|
|
316
|
+
primaryProp,
|
|
317
|
+
selector,
|
|
318
|
+
iframe,
|
|
319
|
+
gsapCommitMutation,
|
|
110
320
|
);
|
|
321
|
+
} catch {
|
|
322
|
+
bumpGsapCache();
|
|
111
323
|
}
|
|
112
|
-
|
|
113
|
-
const pct = computeElementPercentage(usePlayerStore.getState().currentTime, selection, anim);
|
|
114
|
-
|
|
115
|
-
// Read all currently animated properties from runtime for backfill
|
|
116
|
-
const runtimeProps = selector ? readAllAnimatedProperties(iframe, selector, anim) : {};
|
|
117
|
-
|
|
118
|
-
// Build the properties object: all runtime props + the new value
|
|
119
|
-
const properties: Record<string, number | string> = { ...runtimeProps };
|
|
120
|
-
properties[property] = value;
|
|
121
|
-
|
|
122
|
-
// Compute backfill defaults for properties not in existing keyframes
|
|
123
|
-
const backfillDefaults: Record<string, number | string> = { ...runtimeProps };
|
|
124
|
-
if (!(property in runtimeProps) && selector) {
|
|
125
|
-
const cssVal = readGsapProperty(iframe, selector, property);
|
|
126
|
-
if (cssVal != null) backfillDefaults[property] = cssVal;
|
|
127
|
-
}
|
|
128
|
-
backfillDefaults[property] = typeof value === "number" ? value : value;
|
|
129
|
-
|
|
130
|
-
const existingKf = anim.keyframes?.keyframes.some(
|
|
131
|
-
(kf) => Math.abs(kf.percentage - pct) < 0.05,
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
await gsapCommitMutation(
|
|
135
|
-
selection,
|
|
136
|
-
existingKf
|
|
137
|
-
? {
|
|
138
|
-
type: "update-keyframe",
|
|
139
|
-
animationId: anim.id,
|
|
140
|
-
percentage: pct,
|
|
141
|
-
properties,
|
|
142
|
-
}
|
|
143
|
-
: {
|
|
144
|
-
type: "add-keyframe",
|
|
145
|
-
animationId: anim.id,
|
|
146
|
-
percentage: pct,
|
|
147
|
-
properties,
|
|
148
|
-
backfillDefaults,
|
|
149
|
-
},
|
|
150
|
-
{ label: `Edit ${property} (keyframe ${pct}%)`, softReload: true },
|
|
151
|
-
);
|
|
152
324
|
},
|
|
153
|
-
[selectedGsapAnimations, gsapCommitMutation,
|
|
325
|
+
[selectedGsapAnimations, gsapCommitMutation, previewIframeRef, bumpGsapCache],
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const commitAnimatedProperty = useCallback(
|
|
329
|
+
(selection: DomEditSelection, property: string, value: number | string) =>
|
|
330
|
+
commitAnimatedProperties(selection, { [property]: value }),
|
|
331
|
+
[commitAnimatedProperties],
|
|
154
332
|
);
|
|
155
333
|
|
|
156
|
-
return commitAnimatedProperty;
|
|
334
|
+
return { commitAnimatedProperty, commitAnimatedProperties };
|
|
157
335
|
}
|
|
@@ -301,7 +301,6 @@ export function useDomEditCommits({
|
|
|
301
301
|
|
|
302
302
|
const {
|
|
303
303
|
handleDomPathOffsetCommit,
|
|
304
|
-
handleDomGroupPathOffsetCommit,
|
|
305
304
|
handleDomBoxSizeCommit,
|
|
306
305
|
handleDomRotationCommit,
|
|
307
306
|
handleDomManualEditsReset,
|
|
@@ -340,7 +339,6 @@ export function useDomEditCommits({
|
|
|
340
339
|
handleDomAddTextField,
|
|
341
340
|
handleDomRemoveTextField,
|
|
342
341
|
handleDomPathOffsetCommit,
|
|
343
|
-
handleDomGroupPathOffsetCommit,
|
|
344
342
|
handleDomBoxSizeCommit,
|
|
345
343
|
handleDomRotationCommit,
|
|
346
344
|
handleDomManualEditsReset,
|
|
@@ -215,7 +215,6 @@ export function useDomEditSession({
|
|
|
215
215
|
handleDomTextFieldStyleCommit,
|
|
216
216
|
handleDomAddTextField,
|
|
217
217
|
handleDomRemoveTextField,
|
|
218
|
-
handleDomGroupPathOffsetCommit,
|
|
219
218
|
handleDomBoxSizeCommit,
|
|
220
219
|
handleDomManualEditsReset,
|
|
221
220
|
handleDomEditElementDelete,
|
|
@@ -368,9 +367,11 @@ export function useDomEditSession({
|
|
|
368
367
|
|
|
369
368
|
const {
|
|
370
369
|
handleGsapAwarePathOffsetCommit,
|
|
370
|
+
handleGsapAwareGroupPathOffsetCommit,
|
|
371
371
|
handleGsapAwareBoxSizeCommit,
|
|
372
372
|
handleGsapAwareRotationCommit,
|
|
373
373
|
commitAnimatedProperty,
|
|
374
|
+
commitAnimatedProperties,
|
|
374
375
|
handleSetArcPath,
|
|
375
376
|
handleUpdateArcSegment,
|
|
376
377
|
handleUnroll,
|
|
@@ -410,6 +411,25 @@ export function useDomEditSession({
|
|
|
410
411
|
[gsapCommitMutation, domEditSelectionRef],
|
|
411
412
|
);
|
|
412
413
|
|
|
414
|
+
// Apply one ease to every segment at once (AE select-all + F9): set easeEach
|
|
415
|
+
// and strip per-keyframe overrides in a single mutation.
|
|
416
|
+
const handleSetAllKeyframeEases = useCallback(
|
|
417
|
+
(animationId: string, ease: string) => {
|
|
418
|
+
const sel = domEditSelectionRef.current;
|
|
419
|
+
if (!sel) return;
|
|
420
|
+
gsapCommitMutation(
|
|
421
|
+
sel,
|
|
422
|
+
{
|
|
423
|
+
type: "update-meta",
|
|
424
|
+
animationId,
|
|
425
|
+
updates: { easeEach: ease, resetKeyframeEases: true },
|
|
426
|
+
},
|
|
427
|
+
{ label: "Apply ease to all segments", softReload: true },
|
|
428
|
+
);
|
|
429
|
+
},
|
|
430
|
+
[gsapCommitMutation, domEditSelectionRef],
|
|
431
|
+
);
|
|
432
|
+
|
|
413
433
|
return {
|
|
414
434
|
// State
|
|
415
435
|
domEditSelection,
|
|
@@ -433,7 +453,7 @@ export function useDomEditSession({
|
|
|
433
453
|
handleDomAttributeLiveCommit,
|
|
434
454
|
handleDomHtmlAttributeCommit,
|
|
435
455
|
handleDomPathOffsetCommit: handleGsapAwarePathOffsetCommit,
|
|
436
|
-
handleDomGroupPathOffsetCommit,
|
|
456
|
+
handleDomGroupPathOffsetCommit: handleGsapAwareGroupPathOffsetCommit,
|
|
437
457
|
handleDomZIndexReorderCommit,
|
|
438
458
|
handleDomBoxSizeCommit: handleGsapAwareBoxSizeCommit,
|
|
439
459
|
handleDomRotationCommit: handleGsapAwareRotationCommit,
|
|
@@ -477,7 +497,9 @@ export function useDomEditSession({
|
|
|
477
497
|
handleGsapRemoveAllKeyframes,
|
|
478
498
|
handleResetSelectedElementKeyframes,
|
|
479
499
|
handleUpdateKeyframeEase,
|
|
500
|
+
handleSetAllKeyframeEases,
|
|
480
501
|
commitAnimatedProperty,
|
|
502
|
+
commitAnimatedProperties,
|
|
481
503
|
handleSetArcPath,
|
|
482
504
|
handleUpdateArcSegment,
|
|
483
505
|
handleUnroll,
|
|
@@ -197,6 +197,9 @@ export function useDomEditWiring({
|
|
|
197
197
|
? { id: domEditSelection.id ?? null, selector: domEditSelection.selector ?? null }
|
|
198
198
|
: null,
|
|
199
199
|
gsapCacheVersion,
|
|
200
|
+
// Pass the preview iframe so class/selector tweens (e.g. `.dot`) resolve to
|
|
201
|
+
// the live element and surface in the inspector — not just by #id match.
|
|
202
|
+
previewIframeRef,
|
|
200
203
|
);
|
|
201
204
|
|
|
202
205
|
// ── Telemetry & fallback ──
|
|
@@ -16,11 +16,10 @@ import {
|
|
|
16
16
|
buildClearBoxSizePatches,
|
|
17
17
|
buildClearRotationPatches,
|
|
18
18
|
} from "../components/editor/manualEditsDomPatches";
|
|
19
|
-
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
20
19
|
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
21
20
|
import { isElementGsapTargeted } from "./gsapTargetCache";
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
const GSAP_CSS_FALLBACK_BLOCKED_MESSAGE =
|
|
24
23
|
"This element is GSAP-animated — dragging via CSS would corrupt keyframes";
|
|
25
24
|
|
|
26
25
|
// ── Hook ──
|
|
@@ -46,7 +45,8 @@ export function useDomGeometryCommits({
|
|
|
46
45
|
// elements fall through to commitPositionPatchToHtml → persistDomEditOperations →
|
|
47
46
|
// onTrySdkPersist and are already SDK-cut-over as setStyle/setAttribute (§3.3 done).
|
|
48
47
|
// Upgrade path for GSAP: add a moveElementGsap SDK op in a separate SDK PR.
|
|
49
|
-
|
|
48
|
+
const gsapTargeted = isElementGsapTargeted(previewIframeRef.current, selection.element);
|
|
49
|
+
if (gsapTargeted) {
|
|
50
50
|
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
51
51
|
showToast(error.message, "error");
|
|
52
52
|
return Promise.reject(error);
|
|
@@ -60,33 +60,6 @@ export function useDomGeometryCommits({
|
|
|
60
60
|
[commitPositionPatchToHtml, previewIframeRef, showToast],
|
|
61
61
|
);
|
|
62
62
|
|
|
63
|
-
const handleDomGroupPathOffsetCommit = useCallback(
|
|
64
|
-
(updates: DomEditGroupPathOffsetCommit[]) => {
|
|
65
|
-
if (updates.length === 0) return Promise.resolve();
|
|
66
|
-
const blockedUpdate = updates.find(({ selection }) =>
|
|
67
|
-
isElementGsapTargeted(previewIframeRef.current, selection.element),
|
|
68
|
-
);
|
|
69
|
-
if (blockedUpdate) {
|
|
70
|
-
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
71
|
-
showToast(error.message, "error");
|
|
72
|
-
return Promise.reject(error);
|
|
73
|
-
}
|
|
74
|
-
const coalesceKey = updates
|
|
75
|
-
.map((u) => getDomEditTargetKey(u.selection))
|
|
76
|
-
.sort()
|
|
77
|
-
.join(":");
|
|
78
|
-
const saves = updates.map(({ selection, next }) => {
|
|
79
|
-
applyStudioPathOffset(selection.element, next);
|
|
80
|
-
return commitPositionPatchToHtml(selection, buildPathOffsetPatches(selection.element), {
|
|
81
|
-
label: `Move ${updates.length} layers`,
|
|
82
|
-
coalesceKey: `group-path-offset:${coalesceKey}`,
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
return Promise.all(saves).then(() => undefined);
|
|
86
|
-
},
|
|
87
|
-
[commitPositionPatchToHtml, previewIframeRef, showToast],
|
|
88
|
-
);
|
|
89
|
-
|
|
90
63
|
const handleDomBoxSizeCommit = useCallback(
|
|
91
64
|
(selection: DomEditSelection, next: { width: number; height: number }) => {
|
|
92
65
|
if (isElementGsapTargeted(previewIframeRef.current, selection.element)) {
|
|
@@ -142,7 +115,6 @@ export function useDomGeometryCommits({
|
|
|
142
115
|
|
|
143
116
|
return {
|
|
144
117
|
handleDomPathOffsetCommit,
|
|
145
|
-
handleDomGroupPathOffsetCommit,
|
|
146
118
|
handleDomBoxSizeCommit,
|
|
147
119
|
handleDomRotationCommit,
|
|
148
120
|
handleDomManualEditsReset,
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Extracted from App.tsx to keep file sizes under the 600-line limit.
|
|
4
4
|
*/
|
|
5
5
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
6
|
-
import { editLog } from "../utils/editDebugLog";
|
|
7
6
|
import { useGestureRecording } from "./useGestureRecording";
|
|
8
7
|
import { simplifyGestureSamples } from "../utils/rdpSimplify";
|
|
9
8
|
import { fitEasesFromVelocity } from "../utils/velocityEaseFitter";
|
|
@@ -294,9 +293,6 @@ export function useGestureCommit({
|
|
|
294
293
|
|
|
295
294
|
// fallow-ignore-next-line complexity
|
|
296
295
|
const handleToggleRecording = useCallback(() => {
|
|
297
|
-
editLog("gesture", gestureStateRef.current === "recording" ? "stop" : "start", {
|
|
298
|
-
id: domEditSessionRef.current.domEditSelection?.id,
|
|
299
|
-
});
|
|
300
296
|
if (gestureStateRef.current === "recording") {
|
|
301
297
|
void stopAndCommitRecording();
|
|
302
298
|
return;
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* from the rest of the editing orchestration.
|
|
9
9
|
*/
|
|
10
10
|
import { useCallback } from "react";
|
|
11
|
-
import { editLog } from "../utils/editDebugLog";
|
|
12
11
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
13
12
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
14
13
|
import {
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
useSafeGsapCommitMutation,
|
|
23
22
|
} from "./useSafeGsapCommitMutation";
|
|
24
23
|
import type { CommitMutation } from "./gsapScriptCommitTypes";
|
|
24
|
+
import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
|
|
25
25
|
|
|
26
26
|
export interface UseGsapAwareEditingParams {
|
|
27
27
|
domEditSelection: DomEditSelection | null;
|
|
@@ -97,7 +97,6 @@ export function useGsapAwareEditing({
|
|
|
97
97
|
next: { x: number; y: number },
|
|
98
98
|
modifiers?: { altKey?: boolean },
|
|
99
99
|
) => {
|
|
100
|
-
editLog("manual-drag:move", { id: selection.id, next, altKey: modifiers?.altKey });
|
|
101
100
|
if (gsapCommitMutation) {
|
|
102
101
|
try {
|
|
103
102
|
await tryGsapDragIntercept(
|
|
@@ -124,9 +123,35 @@ export function useGsapAwareEditing({
|
|
|
124
123
|
],
|
|
125
124
|
);
|
|
126
125
|
|
|
126
|
+
// Multi-select (group) drag: route EACH element through the SAME GSAP intercept as
|
|
127
|
+
// a single drag, so every position is written as GSAP code (tl.set / keyframes /
|
|
128
|
+
// gsap.set) — NEVER the deprecated `--hf-studio-offset` CSS var, and GSAP-animated
|
|
129
|
+
// elements are no longer blocked in a group. No CSS fallback: with no GSAP
|
|
130
|
+
// composition there's nothing to write (a no-op, exactly like the single-drag path).
|
|
131
|
+
const handleGsapAwareGroupPathOffsetCommit = useCallback(
|
|
132
|
+
async (updates: DomEditGroupPathOffsetCommit[]) => {
|
|
133
|
+
if (!gsapCommitMutation) return;
|
|
134
|
+
for (const { selection, next } of updates) {
|
|
135
|
+
try {
|
|
136
|
+
await tryGsapDragIntercept(
|
|
137
|
+
selection,
|
|
138
|
+
next,
|
|
139
|
+
[],
|
|
140
|
+
previewIframeRef.current,
|
|
141
|
+
gsapCommitMutation,
|
|
142
|
+
makeFetchFallback(selection),
|
|
143
|
+
);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
trackGsapInteractionFailure(error, selection, "drag", "Move animated layer (group)");
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
[gsapCommitMutation, previewIframeRef, makeFetchFallback, trackGsapInteractionFailure],
|
|
151
|
+
);
|
|
152
|
+
|
|
127
153
|
const handleGsapAwareBoxSizeCommit = useCallback(
|
|
128
154
|
async (selection: DomEditSelection, next: { width: number; height: number }) => {
|
|
129
|
-
editLog("manual-drag:resize", { id: selection.id, next });
|
|
130
155
|
if (gsapCommitMutation) {
|
|
131
156
|
try {
|
|
132
157
|
const handled = await tryGsapResizeIntercept(
|
|
@@ -157,7 +182,6 @@ export function useGsapAwareEditing({
|
|
|
157
182
|
|
|
158
183
|
const handleGsapAwareRotationCommit = useCallback(
|
|
159
184
|
async (selection: DomEditSelection, next: { angle: number }) => {
|
|
160
|
-
editLog("manual-drag:rotate", { id: selection.id, next });
|
|
161
185
|
if (gsapCommitMutation) {
|
|
162
186
|
try {
|
|
163
187
|
// Single source of truth for rotation too: tryGsapRotationIntercept handles
|
|
@@ -188,7 +212,7 @@ export function useGsapAwareEditing({
|
|
|
188
212
|
|
|
189
213
|
// ── Animated property commit ──
|
|
190
214
|
|
|
191
|
-
const commitAnimatedProperty = useAnimatedPropertyCommit({
|
|
215
|
+
const { commitAnimatedProperty, commitAnimatedProperties } = useAnimatedPropertyCommit({
|
|
192
216
|
selectedGsapAnimations,
|
|
193
217
|
gsapCommitMutation,
|
|
194
218
|
addGsapAnimation: (sel, method, time) => addGsapAnimation(sel, method, time),
|
|
@@ -250,9 +274,11 @@ export function useGsapAwareEditing({
|
|
|
250
274
|
|
|
251
275
|
return {
|
|
252
276
|
handleGsapAwarePathOffsetCommit,
|
|
277
|
+
handleGsapAwareGroupPathOffsetCommit,
|
|
253
278
|
handleGsapAwareBoxSizeCommit,
|
|
254
279
|
handleGsapAwareRotationCommit,
|
|
255
280
|
commitAnimatedProperty,
|
|
281
|
+
commitAnimatedProperties,
|
|
256
282
|
handleSetArcPath,
|
|
257
283
|
handleUpdateArcSegment,
|
|
258
284
|
handleUnroll,
|
|
@@ -206,6 +206,7 @@ export function useGsapKeyframeOps({
|
|
|
206
206
|
selection: DomEditSelection,
|
|
207
207
|
animationId: string,
|
|
208
208
|
resolvedFromValues?: Record<string, number | string>,
|
|
209
|
+
duration?: number,
|
|
209
210
|
) => {
|
|
210
211
|
if (sdkSession && sdkDeps) {
|
|
211
212
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
@@ -221,7 +222,9 @@ export function useGsapKeyframeOps({
|
|
|
221
222
|
}
|
|
222
223
|
return commitMutation(
|
|
223
224
|
selection,
|
|
224
|
-
|
|
225
|
+
// `duration` only applies when the target is a static `set` (which has
|
|
226
|
+
// none) — it spans the converted keyframes across the element's clip.
|
|
227
|
+
{ type: "convert-to-keyframes", animationId, resolvedFromValues, duration },
|
|
225
228
|
{ label: "Convert to keyframes" },
|
|
226
229
|
);
|
|
227
230
|
},
|