@hyperframes/studio 0.6.88 → 0.6.89
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-2SbRRd33.js +146 -0
- package/dist/assets/index-D2NkPomd.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +33 -193
- package/src/components/StudioLeftSidebar.tsx +6 -0
- package/src/components/StudioRightPanel.tsx +8 -0
- package/src/components/TimelineToolbar.tsx +54 -31
- package/src/components/editor/AnimationCard.tsx +15 -3
- package/src/components/editor/DomEditOverlay.test.ts +34 -1
- package/src/components/editor/FileTree.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +17 -3
- package/src/components/editor/LayersPanel.tsx +19 -4
- package/src/components/editor/PropertyPanel.tsx +82 -170
- package/src/components/editor/domEditOverlayStartGesture.ts +1 -0
- package/src/components/editor/gsapAnimatesProperty.ts +52 -0
- package/src/components/editor/manualEditsDom.ts +11 -57
- package/src/components/editor/manualOffsetDrag.test.ts +18 -1
- package/src/components/editor/manualOffsetDrag.ts +16 -10
- package/src/components/editor/propertyPanel3dTransform.tsx +133 -0
- package/src/components/editor/propertyPanelHelpers.ts +76 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +1 -9
- package/src/components/editor/useDomEditOverlayGestures.ts +3 -0
- package/src/components/editor/useLayerDrag.ts +6 -3
- package/src/components/renders/RenderQueueItem.tsx +47 -46
- package/src/components/sidebar/CompositionsTab.tsx +15 -2
- package/src/components/sidebar/LeftSidebar.tsx +11 -0
- package/src/hooks/gsapDragCommit.ts +294 -0
- package/src/hooks/gsapKeyframeCacheHelpers.ts +88 -0
- package/src/hooks/gsapRuntimeBridge.ts +49 -402
- package/src/hooks/gsapRuntimeReaders.ts +201 -0
- package/src/hooks/timelineEditingHelpers.ts +148 -0
- package/src/hooks/useAnimatedPropertyCommit.ts +54 -12
- package/src/hooks/useBlockHandlers.ts +150 -0
- package/src/hooks/useClipboard.ts +1 -10
- package/src/hooks/useDomEditPreviewSync.ts +126 -0
- package/src/hooks/useDomEditSession.ts +11 -79
- package/src/hooks/useGestureCommit.ts +166 -0
- package/src/hooks/useGestureRecording.ts +271 -169
- package/src/hooks/useGsapScriptCommits.ts +7 -80
- package/src/hooks/useLintModal.ts +97 -25
- package/src/hooks/useTimelineEditing.ts +10 -132
- package/src/player/components/TimelineCanvas.tsx +24 -7
- package/src/player/components/useTimelinePlayhead.ts +2 -1
- package/src/player/store/playerStore.ts +12 -0
- package/src/utils/gsapSoftReload.ts +18 -1
- package/src/utils/studioUrlState.test.ts +9 -0
- package/dist/assets/index-B9_ctmee.js +0 -143
- package/dist/assets/index-CGlIm_-E.css +0 -1
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
12
12
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
13
|
-
|
|
14
13
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
import { readAllAnimatedProperties, readGsapProperty } from "./gsapRuntimeReaders";
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from "
|
|
17
|
+
commitGsapPositionFromDrag,
|
|
18
|
+
computeCurrentPercentage,
|
|
19
|
+
materializeIfDynamic,
|
|
20
|
+
} from "./gsapDragCommit";
|
|
21
|
+
import type { GsapDragCommitCallbacks } from "./gsapDragCommit";
|
|
21
22
|
|
|
22
23
|
// ── Runtime reads ──────────────────────────────────────────────────────────
|
|
23
24
|
|
|
@@ -59,31 +60,40 @@ function readGsapPositionFromIframe(
|
|
|
59
60
|
// ── Animation matching ─────────────────────────────────────────────────────
|
|
60
61
|
|
|
61
62
|
// fallow-ignore-next-line complexity
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
const props = anim.properties;
|
|
72
|
-
const fromProps = anim.fromProperties;
|
|
73
|
-
if (anim.method === "fromTo") {
|
|
74
|
-
if ("x" in props || "y" in props || (fromProps && ("x" in fromProps || "y" in fromProps))) {
|
|
75
|
-
return anim;
|
|
76
|
-
}
|
|
77
|
-
} else if ("x" in props || "y" in props) {
|
|
78
|
-
return anim;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// Fall back to any keyframed animation — drag will add x/y to it
|
|
82
|
-
for (const anim of animations) {
|
|
83
|
-
if (anim.keyframes) return anim;
|
|
63
|
+
function animHasPosition(anim: GsapAnimation): boolean {
|
|
64
|
+
if (anim.keyframes?.keyframes.some((kf) => "x" in kf.properties || "y" in kf.properties))
|
|
65
|
+
return true;
|
|
66
|
+
if (anim.method === "fromTo") {
|
|
67
|
+
const from = anim.fromProperties;
|
|
68
|
+
return (
|
|
69
|
+
"x" in anim.properties || "y" in anim.properties || !!(from && ("x" in from || "y" in from))
|
|
70
|
+
);
|
|
84
71
|
}
|
|
85
|
-
|
|
86
|
-
|
|
72
|
+
return "x" in anim.properties || "y" in anim.properties;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function findGsapPositionAnimation(
|
|
76
|
+
animations: GsapAnimation[],
|
|
77
|
+
selector?: string,
|
|
78
|
+
): GsapAnimation | null {
|
|
79
|
+
if (animations.length === 0) return null;
|
|
80
|
+
const currentTime = usePlayerStore.getState().currentTime;
|
|
81
|
+
|
|
82
|
+
const scored = animations
|
|
83
|
+
.filter((a) => animHasPosition(a) || a.keyframes || animations.length === 1)
|
|
84
|
+
.map((a) => {
|
|
85
|
+
let score = 0;
|
|
86
|
+
if (animHasPosition(a)) score += 10;
|
|
87
|
+
if (a.keyframes) score += 5;
|
|
88
|
+
if (selector && a.targetSelector === selector) score += 8;
|
|
89
|
+
else if (a.targetSelector.includes(",")) score -= 5;
|
|
90
|
+
const pos = typeof a.position === "number" ? a.position : 0;
|
|
91
|
+
const dur = a.duration ?? 0;
|
|
92
|
+
if (currentTime >= pos - 0.05 && currentTime <= pos + dur + 0.05) score += 4;
|
|
93
|
+
return { anim: a, score };
|
|
94
|
+
});
|
|
95
|
+
scored.sort((a, b) => b.score - a.score);
|
|
96
|
+
return scored[0]?.anim ?? animations[0];
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
// ── Selector resolution ────────────────────────────────────────────────────
|
|
@@ -94,94 +104,16 @@ function selectorForSelection(selection: DomEditSelection): string | null {
|
|
|
94
104
|
return null;
|
|
95
105
|
}
|
|
96
106
|
|
|
97
|
-
// ── Percentage computation ─────────────────────────────────────────────────
|
|
98
|
-
|
|
99
|
-
function computeCurrentPercentage(selection: DomEditSelection, animation?: GsapAnimation): number {
|
|
100
|
-
const currentTime = usePlayerStore.getState().currentTime;
|
|
101
|
-
if (animation) {
|
|
102
|
-
const start = resolveTweenStart(animation);
|
|
103
|
-
const duration = resolveTweenDuration(animation);
|
|
104
|
-
if (start !== null) {
|
|
105
|
-
return absoluteToPercentage(currentTime, start, duration);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
|
|
109
|
-
const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "1") || 1;
|
|
110
|
-
return elDuration > 0
|
|
111
|
-
? Math.max(0, Math.min(100, Math.round(((currentTime - elStart) / elDuration) * 1000) / 10))
|
|
112
|
-
: 0;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// ── Dynamic keyframe materialization ──────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
async function materializeIfDynamic(
|
|
118
|
-
anim: GsapAnimation,
|
|
119
|
-
iframe: HTMLIFrameElement | null,
|
|
120
|
-
commitMutation: GsapDragCommitCallbacks["commitMutation"],
|
|
121
|
-
selection: DomEditSelection,
|
|
122
|
-
): Promise<string | void> {
|
|
123
|
-
if (!anim.hasUnresolvedKeyframes && !anim.hasUnresolvedSelector) return;
|
|
124
|
-
|
|
125
|
-
if (anim.hasUnresolvedSelector) {
|
|
126
|
-
// Unroll: read ALL elements' keyframes from runtime and replace the loop
|
|
127
|
-
const allScanned = scanAllRuntimeKeyframes(iframe);
|
|
128
|
-
if (allScanned.size === 0) return;
|
|
129
|
-
const allElements = Array.from(allScanned.entries()).map(([id, data]) => ({
|
|
130
|
-
selector: `#${id}`,
|
|
131
|
-
keyframes: data.keyframes,
|
|
132
|
-
easeEach: data.easeEach,
|
|
133
|
-
}));
|
|
134
|
-
await commitMutation(
|
|
135
|
-
selection,
|
|
136
|
-
{
|
|
137
|
-
type: "materialize-keyframes",
|
|
138
|
-
animationId: anim.id,
|
|
139
|
-
keyframes: allScanned.get(selection.id ?? "")?.keyframes ?? [],
|
|
140
|
-
allElements,
|
|
141
|
-
},
|
|
142
|
-
{ label: "Unroll dynamic animations", skipReload: true },
|
|
143
|
-
);
|
|
144
|
-
return `${anim.targetSelector}-to-0`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const runtime = readRuntimeKeyframes(iframe, anim.targetSelector);
|
|
148
|
-
if (!runtime || runtime.keyframes.length === 0) return;
|
|
149
|
-
await commitMutation(
|
|
150
|
-
selection,
|
|
151
|
-
{
|
|
152
|
-
type: "materialize-keyframes",
|
|
153
|
-
animationId: anim.id,
|
|
154
|
-
keyframes: runtime.keyframes,
|
|
155
|
-
easeEach: runtime.easeEach,
|
|
156
|
-
},
|
|
157
|
-
{ label: "Materialize dynamic keyframes", skipReload: true },
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
107
|
// ── High-level intercept ───────────────────────────────────────────────────
|
|
162
108
|
|
|
163
|
-
export
|
|
164
|
-
commitMutation: (
|
|
165
|
-
selection: DomEditSelection,
|
|
166
|
-
mutation: Record<string, unknown>,
|
|
167
|
-
options: {
|
|
168
|
-
label: string;
|
|
169
|
-
coalesceKey?: string;
|
|
170
|
-
softReload?: boolean;
|
|
171
|
-
skipReload?: boolean;
|
|
172
|
-
beforeReload?: () => void;
|
|
173
|
-
},
|
|
174
|
-
) => Promise<void>;
|
|
175
|
-
}
|
|
109
|
+
export type { GsapDragCommitCallbacks };
|
|
176
110
|
|
|
177
111
|
/**
|
|
178
112
|
* Attempt to handle a drag commit via the GSAP script mutation path.
|
|
179
113
|
*
|
|
180
114
|
* Returns a Promise that resolves to true if the drag was handled via GSAP
|
|
181
115
|
* (caller should skip the CSS path), or false if no GSAP position animation
|
|
182
|
-
* exists.
|
|
183
|
-
* the preview soft-reloaded — the CSS offset stays visible until then so the
|
|
184
|
-
* element doesn't snap back during the async gap.
|
|
116
|
+
* exists.
|
|
185
117
|
*/
|
|
186
118
|
// fallow-ignore-next-line complexity
|
|
187
119
|
export async function tryGsapDragIntercept(
|
|
@@ -192,16 +124,16 @@ export async function tryGsapDragIntercept(
|
|
|
192
124
|
commitMutation: GsapDragCommitCallbacks["commitMutation"],
|
|
193
125
|
fetchFallbackAnimations?: () => Promise<GsapAnimation[]>,
|
|
194
126
|
): Promise<boolean> {
|
|
195
|
-
|
|
127
|
+
const selector = selectorForSelection(selection);
|
|
128
|
+
if (!selector) return false;
|
|
129
|
+
|
|
130
|
+
let posAnim = findGsapPositionAnimation(animations, selector);
|
|
196
131
|
if (!posAnim && fetchFallbackAnimations) {
|
|
197
132
|
const fresh = await fetchFallbackAnimations();
|
|
198
|
-
posAnim = findGsapPositionAnimation(fresh);
|
|
133
|
+
posAnim = findGsapPositionAnimation(fresh, selector);
|
|
199
134
|
}
|
|
200
135
|
if (!posAnim) return false;
|
|
201
136
|
|
|
202
|
-
const selector = selectorForSelection(selection);
|
|
203
|
-
if (!selector) return false;
|
|
204
|
-
|
|
205
137
|
// Keyframe writes at 0%/100% when outside the tween range. Acceptable
|
|
206
138
|
// trade-off — CSS path must NEVER touch GSAP-targeted elements because
|
|
207
139
|
// changing the CSS offset corrupts all existing keyframes (baked mismatch).
|
|
@@ -215,294 +147,9 @@ export async function tryGsapDragIntercept(
|
|
|
215
147
|
return true;
|
|
216
148
|
}
|
|
217
149
|
|
|
218
|
-
// ──
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Compute the new GSAP position values from runtime-read positions + drag
|
|
222
|
-
* offset, then commit the mutation to the GSAP script.
|
|
223
|
-
*
|
|
224
|
-
* `gsap.getProperty` reads from GSAP's internal cache (element._gsap), not
|
|
225
|
-
* from the DOM transform matrix. The strip in `applyStudioPathOffset` does
|
|
226
|
-
* not affect the cached values, so the formula is simply:
|
|
227
|
-
* newValue = cachedGsapValue + dragOffset
|
|
228
|
-
*
|
|
229
|
-
* For flat tweens (to/set), the mutation would change the tween endpoint,
|
|
230
|
-
* which is invisible at t=0. Instead, we convert to keyframes first so the
|
|
231
|
-
* position is set at the exact seek percentage via a keyframe.
|
|
232
|
-
*/
|
|
233
|
-
// fallow-ignore-next-line complexity
|
|
234
|
-
async function commitGsapPositionFromDrag(
|
|
235
|
-
selection: DomEditSelection,
|
|
236
|
-
anim: GsapAnimation,
|
|
237
|
-
studioOffset: { x: number; y: number },
|
|
238
|
-
gsapPos: { x: number; y: number },
|
|
239
|
-
iframe: HTMLIFrameElement | null,
|
|
240
|
-
selector: string,
|
|
241
|
-
callbacks: GsapDragCommitCallbacks,
|
|
242
|
-
): Promise<void> {
|
|
243
|
-
// CSS composition: translate → rotate → transform. The studioOffset is in
|
|
244
|
-
// pre-rotation space (CSS translate), but GSAP x/y are in post-CSS-rotate
|
|
245
|
-
// space (CSS transform). Counter-rotate the offset to match GSAP's frame.
|
|
246
|
-
const rotStyle = selection.element.style.getPropertyValue("--hf-studio-rotation");
|
|
247
|
-
const rotDeg = Number.parseFloat(rotStyle) || 0;
|
|
248
|
-
const rad = (-rotDeg * Math.PI) / 180;
|
|
249
|
-
const cos = Math.cos(rad);
|
|
250
|
-
const sin = Math.sin(rad);
|
|
251
|
-
const el = selection.element;
|
|
252
|
-
const origX = Number.parseFloat(el.getAttribute("data-hf-drag-initial-offset-x") ?? "") || 0;
|
|
253
|
-
const origY = Number.parseFloat(el.getAttribute("data-hf-drag-initial-offset-y") ?? "") || 0;
|
|
254
|
-
const deltaX = studioOffset.x - origX;
|
|
255
|
-
const deltaY = studioOffset.y - origY;
|
|
256
|
-
const adjX = deltaX * cos - deltaY * sin;
|
|
257
|
-
const adjY = deltaX * sin + deltaY * cos;
|
|
258
|
-
// Use the GSAP base captured at drag start — the live gsapPos is corrupted
|
|
259
|
-
// by the draft's gsap.set() calls during drag.
|
|
260
|
-
const baseGsapX =
|
|
261
|
-
Number.parseFloat(el.getAttribute("data-hf-drag-gsap-base-x") ?? "") || gsapPos.x;
|
|
262
|
-
const baseGsapY =
|
|
263
|
-
Number.parseFloat(el.getAttribute("data-hf-drag-gsap-base-y") ?? "") || gsapPos.y;
|
|
264
|
-
const newX = Math.round(baseGsapX + adjX);
|
|
265
|
-
const newY = Math.round(baseGsapY + adjY);
|
|
266
|
-
// Restore the CSS offset to pre-drag value so the baked translate stays
|
|
267
|
-
// consistent with existing keyframes. The drag is captured in the new keyframe.
|
|
268
|
-
const restoreOffset = () => {
|
|
269
|
-
el.style.setProperty("--hf-studio-offset-x", `${origX}px`);
|
|
270
|
-
el.style.setProperty("--hf-studio-offset-y", `${origY}px`);
|
|
271
|
-
el.removeAttribute("data-hf-drag-initial-offset-x");
|
|
272
|
-
el.removeAttribute("data-hf-drag-initial-offset-y");
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
if (anim.keyframes) {
|
|
276
|
-
const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
|
|
277
|
-
const effectiveAnim = newId ? { ...anim, id: newId } : anim;
|
|
278
|
-
const runtimeProps = readAllAnimatedProperties(iframe, selector, anim);
|
|
279
|
-
|
|
280
|
-
// Check if current time is outside the tween's range — extend the tween
|
|
281
|
-
// to cover the playhead, remap existing keyframes, then add the new one.
|
|
282
|
-
const ct = usePlayerStore.getState().currentTime;
|
|
283
|
-
const ts = resolveTweenStart(effectiveAnim);
|
|
284
|
-
const td = resolveTweenDuration(effectiveAnim);
|
|
285
|
-
if (ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01)) {
|
|
286
|
-
await extendTweenAndAddKeyframe(
|
|
287
|
-
selection,
|
|
288
|
-
effectiveAnim,
|
|
289
|
-
{ ...runtimeProps, x: newX, y: newY },
|
|
290
|
-
ct,
|
|
291
|
-
ts,
|
|
292
|
-
td,
|
|
293
|
-
callbacks,
|
|
294
|
-
restoreOffset,
|
|
295
|
-
);
|
|
296
|
-
} else {
|
|
297
|
-
await commitKeyframedPosition(
|
|
298
|
-
selection,
|
|
299
|
-
effectiveAnim,
|
|
300
|
-
{ ...runtimeProps, x: newX, y: newY },
|
|
301
|
-
callbacks,
|
|
302
|
-
restoreOffset,
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
} else if (anim.method === "from" || anim.method === "fromTo") {
|
|
306
|
-
// from()/fromTo() — convert to keyframes in a single mutation, placing
|
|
307
|
-
// the dragged position at the 100% (rest) keyframe. A single mutation
|
|
308
|
-
// avoids the stable-id flip (from→to) that breaks chained mutations.
|
|
309
|
-
await callbacks.commitMutation(
|
|
310
|
-
selection,
|
|
311
|
-
{
|
|
312
|
-
type: "convert-to-keyframes",
|
|
313
|
-
animationId: anim.id,
|
|
314
|
-
resolvedFromValues: { x: newX, y: newY },
|
|
315
|
-
},
|
|
316
|
-
{ label: "Move layer (keyframe rest)", softReload: true, beforeReload: restoreOffset },
|
|
317
|
-
);
|
|
318
|
-
} else {
|
|
319
|
-
// Flat to()/set() — convert to keyframes then add at current percentage.
|
|
320
|
-
const runtimeProps = readAllAnimatedProperties(iframe, selector, anim);
|
|
321
|
-
await commitFlatViaKeyframes(
|
|
322
|
-
selection,
|
|
323
|
-
anim,
|
|
324
|
-
{ ...runtimeProps, x: newX, y: newY },
|
|
325
|
-
callbacks,
|
|
326
|
-
restoreOffset,
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Extend a tween's time range to cover `targetTime`, remap all existing
|
|
333
|
-
* keyframe percentages to preserve their absolute positions, then add
|
|
334
|
-
* a new keyframe at the target time.
|
|
335
|
-
*/
|
|
336
|
-
async function extendTweenAndAddKeyframe(
|
|
337
|
-
selection: DomEditSelection,
|
|
338
|
-
anim: GsapAnimation,
|
|
339
|
-
properties: Record<string, number>,
|
|
340
|
-
targetTime: number,
|
|
341
|
-
tweenStart: number,
|
|
342
|
-
tweenDuration: number,
|
|
343
|
-
callbacks: GsapDragCommitCallbacks,
|
|
344
|
-
beforeReload?: () => void,
|
|
345
|
-
): Promise<void> {
|
|
346
|
-
const tweenEnd = tweenStart + tweenDuration;
|
|
347
|
-
const newStart = Math.min(targetTime, tweenStart);
|
|
348
|
-
const newEnd = Math.max(targetTime, tweenEnd);
|
|
349
|
-
const newDuration = Math.max(0.01, newEnd - newStart);
|
|
350
|
-
|
|
351
|
-
// Step 1: Remap all existing keyframes to preserve their absolute times
|
|
352
|
-
// in the new range, then add the new keyframe.
|
|
353
|
-
const existingKfs = anim.keyframes?.keyframes ?? [];
|
|
354
|
-
const remappedKfs: Array<{ percentage: number; properties: Record<string, number | string> }> =
|
|
355
|
-
[];
|
|
356
|
-
for (const kf of existingKfs) {
|
|
357
|
-
const absTime = tweenStart + (kf.percentage / 100) * tweenDuration;
|
|
358
|
-
const newPct = Math.round(((absTime - newStart) / newDuration) * 1000) / 10;
|
|
359
|
-
remappedKfs.push({ percentage: newPct, properties: { ...kf.properties } });
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Add the new keyframe at the target time
|
|
363
|
-
const targetPct = Math.round(((targetTime - newStart) / newDuration) * 1000) / 10;
|
|
364
|
-
remappedKfs.push({ percentage: targetPct, properties });
|
|
365
|
-
|
|
366
|
-
// Sort and dedupe
|
|
367
|
-
remappedKfs.sort((a, b) => a.percentage - b.percentage);
|
|
368
|
-
|
|
369
|
-
// Step 2: Delete the old tween and create a new one with the extended range
|
|
370
|
-
// and all remapped keyframes. Using delete + add-with-keyframes as an atomic pair.
|
|
371
|
-
await callbacks.commitMutation(
|
|
372
|
-
selection,
|
|
373
|
-
{ type: "delete", animationId: anim.id },
|
|
374
|
-
{ label: "Extend tween range", skipReload: true },
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
const selector = anim.targetSelector;
|
|
378
|
-
await callbacks.commitMutation(
|
|
379
|
-
selection,
|
|
380
|
-
{
|
|
381
|
-
type: "add-with-keyframes",
|
|
382
|
-
targetSelector: selector,
|
|
383
|
-
position: Math.round(newStart * 1000) / 1000,
|
|
384
|
-
duration: Math.round(newDuration * 1000) / 1000,
|
|
385
|
-
keyframes: remappedKfs,
|
|
386
|
-
},
|
|
387
|
-
{ label: `Move layer (extended keyframe)`, softReload: true, beforeReload },
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// fallow-ignore-next-line complexity
|
|
392
|
-
async function commitKeyframedPosition(
|
|
393
|
-
selection: DomEditSelection,
|
|
394
|
-
anim: GsapAnimation,
|
|
395
|
-
properties: Record<string, number>,
|
|
396
|
-
callbacks: GsapDragCommitCallbacks,
|
|
397
|
-
beforeReload?: () => void,
|
|
398
|
-
): Promise<void> {
|
|
399
|
-
const pct = computeCurrentPercentage(selection, anim);
|
|
400
|
-
|
|
401
|
-
await callbacks.commitMutation(
|
|
402
|
-
selection,
|
|
403
|
-
{
|
|
404
|
-
type: "add-keyframe",
|
|
405
|
-
animationId: anim.id,
|
|
406
|
-
percentage: pct,
|
|
407
|
-
properties,
|
|
408
|
-
},
|
|
409
|
-
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload },
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* For flat to()/set() tweens, convert to keyframes first so we can place the
|
|
415
|
-
* drag position at the current percentage. Without conversion, the mutation
|
|
416
|
-
* only changes the tween endpoint, which is invisible at t=0.
|
|
417
|
-
*/
|
|
418
|
-
// fallow-ignore-next-line complexity
|
|
419
|
-
async function commitFlatViaKeyframes(
|
|
420
|
-
selection: DomEditSelection,
|
|
421
|
-
anim: GsapAnimation,
|
|
422
|
-
properties: Record<string, number>,
|
|
423
|
-
callbacks: GsapDragCommitCallbacks,
|
|
424
|
-
beforeReload?: () => void,
|
|
425
|
-
): Promise<void> {
|
|
426
|
-
await callbacks.commitMutation(
|
|
427
|
-
selection,
|
|
428
|
-
{ type: "convert-to-keyframes", animationId: anim.id },
|
|
429
|
-
{ label: "Convert to keyframes for drag", skipReload: true },
|
|
430
|
-
);
|
|
150
|
+
// ── Runtime property readers (re-exported for external callers) ───────────
|
|
431
151
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
await callbacks.commitMutation(
|
|
435
|
-
selection,
|
|
436
|
-
{
|
|
437
|
-
type: "add-keyframe",
|
|
438
|
-
animationId: anim.id,
|
|
439
|
-
percentage: pct,
|
|
440
|
-
properties,
|
|
441
|
-
},
|
|
442
|
-
{ label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload },
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// ── Runtime property reader ───────────────────────────────────────────────
|
|
447
|
-
|
|
448
|
-
export function readGsapProperty(
|
|
449
|
-
iframe: HTMLIFrameElement | null,
|
|
450
|
-
selector: string | null,
|
|
451
|
-
prop: string,
|
|
452
|
-
): number | null {
|
|
453
|
-
if (!iframe?.contentWindow || !selector) return null;
|
|
454
|
-
try {
|
|
455
|
-
const gsap = (iframe.contentWindow as unknown as { gsap?: IframeGsap }).gsap;
|
|
456
|
-
if (!gsap?.getProperty) return null;
|
|
457
|
-
const el = iframe.contentDocument?.querySelector(selector);
|
|
458
|
-
if (!el) return null;
|
|
459
|
-
const val = Number(gsap.getProperty(el, prop));
|
|
460
|
-
return Number.isFinite(val) ? Math.round(val) : null;
|
|
461
|
-
} catch {
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
export function readAllAnimatedProperties(
|
|
467
|
-
iframe: HTMLIFrameElement | null,
|
|
468
|
-
selector: string,
|
|
469
|
-
anim: GsapAnimation,
|
|
470
|
-
): Record<string, number> {
|
|
471
|
-
const result: Record<string, number> = {};
|
|
472
|
-
if (!iframe?.contentWindow) return result;
|
|
473
|
-
let gsap: IframeGsap | undefined;
|
|
474
|
-
try {
|
|
475
|
-
gsap = (iframe.contentWindow as unknown as { gsap?: IframeGsap }).gsap;
|
|
476
|
-
} catch {
|
|
477
|
-
return result;
|
|
478
|
-
}
|
|
479
|
-
if (!gsap?.getProperty) return result;
|
|
480
|
-
let doc: Document | null = null;
|
|
481
|
-
try {
|
|
482
|
-
doc = iframe.contentDocument;
|
|
483
|
-
} catch {
|
|
484
|
-
return result;
|
|
485
|
-
}
|
|
486
|
-
const el = doc?.querySelector(selector);
|
|
487
|
-
if (!el) return result;
|
|
488
|
-
|
|
489
|
-
const propKeys = new Set<string>();
|
|
490
|
-
if (anim.keyframes) {
|
|
491
|
-
for (const kf of anim.keyframes.keyframes) {
|
|
492
|
-
for (const p of Object.keys(kf.properties)) {
|
|
493
|
-
if (typeof kf.properties[p] === "number") propKeys.add(p);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
} else {
|
|
497
|
-
for (const p of Object.keys(anim.properties)) propKeys.add(p);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
for (const prop of propKeys) {
|
|
501
|
-
const val = Number(gsap.getProperty(el, prop));
|
|
502
|
-
if (Number.isFinite(val)) result[prop] = Math.round(val);
|
|
503
|
-
}
|
|
504
|
-
return result;
|
|
505
|
-
}
|
|
152
|
+
export { readGsapProperty, readAllAnimatedProperties };
|
|
506
153
|
|
|
507
154
|
// ── Resize intercept ──────────────────────────────────────────────────────
|
|
508
155
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level GSAP runtime property readers shared by gsapRuntimeBridge and gsapDragCommit.
|
|
3
|
+
*/
|
|
4
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
5
|
+
|
|
6
|
+
interface IframeGsap {
|
|
7
|
+
getProperty: (el: Element, prop: string) => number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function readGsapProperty(
|
|
11
|
+
iframe: HTMLIFrameElement | null,
|
|
12
|
+
selector: string | null,
|
|
13
|
+
prop: string,
|
|
14
|
+
): number | null {
|
|
15
|
+
if (!iframe?.contentWindow || !selector) return null;
|
|
16
|
+
try {
|
|
17
|
+
const gsap = (iframe.contentWindow as unknown as { gsap?: IframeGsap }).gsap;
|
|
18
|
+
if (!gsap?.getProperty) return null;
|
|
19
|
+
const el = iframe.contentDocument?.querySelector(selector);
|
|
20
|
+
if (!el) return null;
|
|
21
|
+
const val = Number(gsap.getProperty(el, prop));
|
|
22
|
+
return Number.isFinite(val) ? Math.round(val) : null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const POSITION_PROPS = new Set(["x", "y", "xPercent", "yPercent"]);
|
|
29
|
+
const GSAP_CONFIG_KEYS = new Set([
|
|
30
|
+
"duration",
|
|
31
|
+
"ease",
|
|
32
|
+
"delay",
|
|
33
|
+
"stagger",
|
|
34
|
+
"id",
|
|
35
|
+
"onComplete",
|
|
36
|
+
"onUpdate",
|
|
37
|
+
"onStart",
|
|
38
|
+
"onRepeat",
|
|
39
|
+
"repeat",
|
|
40
|
+
"yoyo",
|
|
41
|
+
"repeatDelay",
|
|
42
|
+
"paused",
|
|
43
|
+
"immediateRender",
|
|
44
|
+
"lazy",
|
|
45
|
+
"overwrite",
|
|
46
|
+
"keyframes",
|
|
47
|
+
"parent",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export function readAllAnimatedProperties(
|
|
51
|
+
iframe: HTMLIFrameElement | null,
|
|
52
|
+
selector: string,
|
|
53
|
+
anim: GsapAnimation,
|
|
54
|
+
): Record<string, number> {
|
|
55
|
+
const result: Record<string, number> = {};
|
|
56
|
+
if (!iframe?.contentWindow) return result;
|
|
57
|
+
let gsap: IframeGsap | undefined;
|
|
58
|
+
try {
|
|
59
|
+
gsap = (iframe.contentWindow as unknown as { gsap?: IframeGsap }).gsap;
|
|
60
|
+
} catch {
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
if (!gsap?.getProperty) return result;
|
|
64
|
+
let doc: Document | null = null;
|
|
65
|
+
try {
|
|
66
|
+
doc = iframe.contentDocument;
|
|
67
|
+
} catch {
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
const el = doc?.querySelector(selector);
|
|
71
|
+
if (!el) return result;
|
|
72
|
+
|
|
73
|
+
const propKeys = new Set<string>();
|
|
74
|
+
if (anim.keyframes) {
|
|
75
|
+
for (const kf of anim.keyframes.keyframes) {
|
|
76
|
+
for (const p of Object.keys(kf.properties)) {
|
|
77
|
+
if (typeof kf.properties[p] === "number") propKeys.add(p);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
for (const p of Object.keys(anim.properties)) propKeys.add(p);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const prop of propKeys) {
|
|
85
|
+
const val = Number(gsap.getProperty(el, prop));
|
|
86
|
+
if (Number.isFinite(val)) {
|
|
87
|
+
result[prop] = POSITION_PROPS.has(prop) ? Math.round(val) : Math.round(val * 1000) / 1000;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const otherTweenProps = new Set<string>();
|
|
92
|
+
try {
|
|
93
|
+
const win = iframe.contentWindow as unknown as { __timelines?: Record<string, unknown> };
|
|
94
|
+
const timelines = win.__timelines;
|
|
95
|
+
if (timelines) {
|
|
96
|
+
for (const tl of Object.values(timelines)) {
|
|
97
|
+
const tlObj = tl as {
|
|
98
|
+
getChildren?: (
|
|
99
|
+
deep: boolean,
|
|
100
|
+
) => Array<{ targets?: () => Element[]; vars?: Record<string, unknown> }>;
|
|
101
|
+
};
|
|
102
|
+
if (!tlObj?.getChildren) continue;
|
|
103
|
+
for (const child of tlObj.getChildren(true)) {
|
|
104
|
+
if (typeof child.targets !== "function") continue;
|
|
105
|
+
const targets = child.targets();
|
|
106
|
+
if (!targets.includes(el)) continue;
|
|
107
|
+
const vars = child.vars;
|
|
108
|
+
if (!vars) continue;
|
|
109
|
+
for (const k of Object.keys(vars)) {
|
|
110
|
+
if (!GSAP_CONFIG_KEYS.has(k)) otherTweenProps.add(k);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.warn(
|
|
117
|
+
"Cross-tween guard failed — baseline capture may include values from other tweens",
|
|
118
|
+
e,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
for (const p of propKeys) otherTweenProps.delete(p);
|
|
122
|
+
|
|
123
|
+
// Tier 1: Transform + visual properties with universal CSS defaults.
|
|
124
|
+
// Safe to compare against hardcoded values — these are always 0 or 1
|
|
125
|
+
// regardless of the element's stylesheet.
|
|
126
|
+
const UNIVERSAL_BASELINE: Record<string, number> = {
|
|
127
|
+
opacity: 1,
|
|
128
|
+
scale: 1,
|
|
129
|
+
scaleX: 1,
|
|
130
|
+
scaleY: 1,
|
|
131
|
+
scaleZ: 1,
|
|
132
|
+
rotation: 0,
|
|
133
|
+
rotationX: 0,
|
|
134
|
+
rotationY: 0,
|
|
135
|
+
skewX: 0,
|
|
136
|
+
skewY: 0,
|
|
137
|
+
z: 0,
|
|
138
|
+
xPercent: 0,
|
|
139
|
+
yPercent: 0,
|
|
140
|
+
transformPerspective: 0,
|
|
141
|
+
blur: 0,
|
|
142
|
+
brightness: 1,
|
|
143
|
+
contrast: 1,
|
|
144
|
+
saturate: 1,
|
|
145
|
+
hueRotate: 0,
|
|
146
|
+
grayscale: 0,
|
|
147
|
+
sepia: 0,
|
|
148
|
+
invert: 0,
|
|
149
|
+
};
|
|
150
|
+
for (const [prop, defaultVal] of Object.entries(UNIVERSAL_BASELINE)) {
|
|
151
|
+
if (prop in result) continue;
|
|
152
|
+
if (otherTweenProps.has(prop)) continue;
|
|
153
|
+
const val = Number(gsap.getProperty(el, prop));
|
|
154
|
+
if (Number.isFinite(val) && Math.round(val * 1000) !== Math.round(defaultVal * 1000)) {
|
|
155
|
+
result[prop] = Math.round(val * 1000) / 1000;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Tier 2: Element-dependent properties — their "default" depends on the
|
|
160
|
+
// stylesheet, so we compare GSAP's runtime value against the element's
|
|
161
|
+
// computed CSS value. Only capture if GSAP has actively changed it.
|
|
162
|
+
const COMPUTED_BASELINE = [
|
|
163
|
+
"borderRadius",
|
|
164
|
+
"borderTopLeftRadius",
|
|
165
|
+
"borderTopRightRadius",
|
|
166
|
+
"borderBottomLeftRadius",
|
|
167
|
+
"borderBottomRightRadius",
|
|
168
|
+
"letterSpacing",
|
|
169
|
+
"wordSpacing",
|
|
170
|
+
"lineHeight",
|
|
171
|
+
"fontSize",
|
|
172
|
+
"outlineOffset",
|
|
173
|
+
"outlineWidth",
|
|
174
|
+
"strokeDashoffset",
|
|
175
|
+
"strokeWidth",
|
|
176
|
+
"backgroundPositionX",
|
|
177
|
+
"backgroundPositionY",
|
|
178
|
+
];
|
|
179
|
+
let computedStyle: CSSStyleDeclaration | null = null;
|
|
180
|
+
try {
|
|
181
|
+
computedStyle = doc?.defaultView?.getComputedStyle(el) ?? null;
|
|
182
|
+
} catch {}
|
|
183
|
+
for (const prop of COMPUTED_BASELINE) {
|
|
184
|
+
if (prop in result) continue;
|
|
185
|
+
if (otherTweenProps.has(prop)) continue;
|
|
186
|
+
const gsapVal = Number(gsap.getProperty(el, prop));
|
|
187
|
+
if (!Number.isFinite(gsapVal)) continue;
|
|
188
|
+
let cssVal = NaN;
|
|
189
|
+
if (computedStyle) {
|
|
190
|
+
const raw = computedStyle.getPropertyValue(
|
|
191
|
+
prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`),
|
|
192
|
+
);
|
|
193
|
+
cssVal = parseFloat(raw);
|
|
194
|
+
}
|
|
195
|
+
if (Number.isFinite(cssVal) && Math.round(gsapVal * 1000) === Math.round(cssVal * 1000))
|
|
196
|
+
continue;
|
|
197
|
+
result[prop] = Math.round(gsapVal * 1000) / 1000;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return result;
|
|
201
|
+
}
|