@hyperframes/studio 0.6.96 → 0.6.98
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/hyperframes-player-DgsMQSvV.js +418 -0
- package/dist/assets/index-B62bDCQv.css +1 -0
- package/dist/assets/index-Ce3pBm_I.js +252 -0
- package/dist/assets/{index-BWFaypdT.js → index-D-ET9M0b.js} +1 -1
- package/dist/assets/index-D-bS9Dxx.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +7 -5
- package/src/App.tsx +182 -177
- package/src/captions/store.ts +11 -11
- package/src/components/StudioHeader.tsx +4 -4
- package/src/components/StudioLeftSidebar.tsx +2 -2
- package/src/components/StudioPreviewArea.tsx +225 -183
- package/src/components/StudioRightPanel.tsx +3 -3
- package/src/components/TimelineToolbar.tsx +25 -0
- package/src/components/editor/DomEditOverlay.tsx +2 -5
- package/src/components/editor/EaseCurveSection.tsx +2 -3
- package/src/components/editor/GestureTrailOverlay.tsx +4 -3
- package/src/components/editor/LayersPanel.tsx +3 -9
- package/src/components/editor/PropertyPanel.tsx +20 -61
- package/src/components/editor/colorValue.ts +3 -1
- package/src/components/editor/domEditOverlayGestures.ts +54 -1
- package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
- package/src/components/editor/gradientValue.ts +3 -3
- package/src/components/editor/keyframeMove.test.ts +101 -0
- package/src/components/editor/keyframeMove.ts +151 -0
- package/src/components/editor/manualEditsDom.ts +0 -12
- package/src/components/editor/propertyPanelHelpers.ts +10 -38
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -5
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -6
- package/src/components/editor/propertyPanelTransformCommit.ts +129 -0
- package/src/components/editor/studioMotionOps.test.ts +1 -1
- package/src/components/editor/studioMotionOps.ts +2 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +1 -46
- package/src/components/nle/NLELayout.tsx +1 -24
- package/src/components/sidebar/BlocksTab.tsx +2 -2
- package/src/contexts/DomEditContext.tsx +134 -31
- package/src/contexts/StudioContext.tsx +90 -40
- package/src/contexts/TimelineEditContext.tsx +47 -0
- package/src/hooks/domEditCommitTypes.ts +14 -0
- package/src/hooks/gsapDragCommit.ts +9 -24
- package/src/hooks/gsapKeyframeCacheHelpers.ts +2 -1
- package/src/hooks/gsapKeyframeCommit.ts +5 -15
- package/src/hooks/gsapRuntimeBridge.ts +18 -52
- package/src/hooks/gsapRuntimeKeyframes.ts +8 -57
- package/src/hooks/gsapRuntimeReaders.ts +19 -26
- package/src/hooks/gsapScriptCommitHelpers.ts +1 -11
- package/src/hooks/gsapScriptCommitTypes.ts +58 -0
- package/src/hooks/gsapShared.ts +157 -0
- package/src/hooks/timelineEditingHelpers.ts +63 -2
- package/src/hooks/useAnimatedPropertyCommit.ts +3 -25
- package/src/hooks/useAppHotkeys.ts +299 -377
- package/src/hooks/useConsoleErrorCapture.ts +33 -5
- package/src/hooks/useDomEditCommits.ts +35 -293
- package/src/hooks/useDomEditPositionPatchCommit.ts +1 -1
- package/src/hooks/useDomEditSession.ts +78 -249
- package/src/hooks/useDomEditTextCommits.ts +1 -1
- package/src/hooks/useDomEditWiring.ts +255 -0
- package/src/hooks/useDomGeometryCommits.ts +181 -0
- package/src/hooks/useDomSelection.ts +10 -27
- package/src/hooks/useEditorSave.ts +82 -0
- package/src/hooks/useElementLifecycleOps.ts +177 -0
- package/src/hooks/useEnableKeyframes.ts +10 -15
- package/src/hooks/useFileManager.ts +32 -114
- package/src/hooks/useFileTree.ts +80 -0
- package/src/hooks/useGestureCommit.ts +7 -5
- package/src/hooks/useGestureRecording.ts +1 -1
- package/src/hooks/useGsapAnimationOps.ts +122 -0
- package/src/hooks/useGsapArcPathOps.ts +61 -0
- package/src/hooks/useGsapAwareEditing.ts +242 -0
- package/src/hooks/useGsapKeyframeOps.ts +167 -0
- package/src/hooks/useGsapPropertyDebounce.ts +135 -0
- package/src/hooks/useGsapScriptCommits.ts +58 -570
- package/src/hooks/useGsapSelectionHandlers.ts +22 -9
- package/src/hooks/useGsapTweenCache.ts +35 -29
- package/src/hooks/useLintModal.ts +7 -0
- package/src/hooks/useMusicBeatAnalysis.ts +152 -0
- package/src/hooks/useRazorSplit.ts +1 -1
- package/src/hooks/useRenderClipContent.ts +46 -21
- package/src/hooks/useTimelineEditing.ts +48 -4
- package/src/player/components/AudioWaveform.tsx +29 -4
- package/src/player/components/BeatStrip.tsx +166 -0
- package/src/player/components/Timeline.tsx +39 -18
- package/src/player/components/TimelineCanvas.tsx +52 -12
- package/src/player/components/TimelineClipDiamonds.tsx +130 -20
- package/src/player/components/TimelinePropertyRows.tsx +8 -2
- package/src/player/components/TimelineRuler.tsx +36 -2
- package/src/player/components/timelineEditing.ts +30 -5
- package/src/player/components/useTimelineClipDrag.ts +155 -4
- package/src/player/components/useTimelinePlayhead.ts +30 -1
- package/src/player/hooks/useTimelinePlayer.ts +47 -45
- package/src/player/lib/mediaProbe.ts +46 -3
- package/src/player/lib/playbackScrub.ts +16 -0
- package/src/player/lib/timelineDOM.ts +10 -2
- package/src/player/lib/timelineIframeHelpers.ts +89 -0
- package/src/player/store/playerStore.ts +92 -33
- package/src/utils/beatEditActions.ts +109 -0
- package/src/utils/beatEditing.ts +136 -0
- package/src/utils/clipboardPayload.ts +3 -2
- package/src/utils/compositionPatterns.ts +2 -0
- package/src/utils/keyframeSelection.test.ts +45 -0
- package/src/utils/keyframeSelection.ts +29 -0
- package/src/utils/rounding.ts +9 -0
- package/src/utils/studioHelpers.ts +5 -2
- package/src/utils/studioUrlState.ts +2 -1
- package/src/utils/timelineAssetDrop.ts +6 -5
- package/src/utils/timelineInspector.ts +15 -100
- package/dist/assets/hyperframes-player-0esDKGRk.js +0 -418
- package/dist/assets/index-B0twsRu0.css +0 -1
- package/dist/assets/index-BA979yF1.js +0 -251
- package/src/components/editor/DopesheetStrip.tsx +0 -141
- package/src/components/editor/StaggerControls.tsx +0 -61
- package/src/components/editor/TimelineLayerPanel.test.ts +0 -42
- package/src/components/editor/TimelineLayerPanel.tsx +0 -15
- package/src/components/nle/TimelineEditorNotice.tsx +0 -133
- package/src/hooks/gsapRuntimePreview.ts +0 -19
- package/src/player/components/timelineUtils.ts +0 -211
- package/src/utils/audioBeatDetection.ts +0 -58
- package/src/utils/keyframeSnapping.test.ts +0 -74
- package/src/utils/keyframeSnapping.ts +0 -63
- package/src/utils/timelineInspector.test.ts +0 -79
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSAP-aware move/resize/rotation wrappers that intercept geometry commits
|
|
3
|
+
* for animated elements and route them through script mutation instead of
|
|
4
|
+
* CSS patching. Also exposes the animated-property commit, arc-path ops,
|
|
5
|
+
* and the thin `commitMutation` facade.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from useDomEditSession to isolate the GSAP intercept routing
|
|
8
|
+
* from the rest of the editing orchestration.
|
|
9
|
+
*/
|
|
10
|
+
import { useCallback } from "react";
|
|
11
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
12
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
13
|
+
import { STUDIO_GSAP_DRAG_INTERCEPT_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
14
|
+
import { GSAP_CSS_FALLBACK_BLOCKED_MESSAGE } from "./useDomGeometryCommits";
|
|
15
|
+
import {
|
|
16
|
+
tryGsapDragIntercept,
|
|
17
|
+
tryGsapResizeIntercept,
|
|
18
|
+
tryGsapRotationIntercept,
|
|
19
|
+
} from "./gsapRuntimeBridge";
|
|
20
|
+
import { useAnimatedPropertyCommit } from "./useAnimatedPropertyCommit";
|
|
21
|
+
import type { CommitMutation } from "./gsapScriptCommitTypes";
|
|
22
|
+
|
|
23
|
+
export interface UseGsapAwareEditingParams {
|
|
24
|
+
domEditSelection: DomEditSelection | null;
|
|
25
|
+
selectedGsapAnimations: GsapAnimation[];
|
|
26
|
+
gsapCommitMutation: CommitMutation | null;
|
|
27
|
+
previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
28
|
+
showToast: (message: string, tone?: "error" | "info") => void;
|
|
29
|
+
bumpGsapCache: () => void;
|
|
30
|
+
makeFetchFallback: (selection: DomEditSelection) => () => Promise<GsapAnimation[]>;
|
|
31
|
+
trackGsapInteractionFailure: (
|
|
32
|
+
error: unknown,
|
|
33
|
+
selection: DomEditSelection,
|
|
34
|
+
mutationType: string,
|
|
35
|
+
label: string,
|
|
36
|
+
) => void;
|
|
37
|
+
// DOM fallbacks (from useDomEditCommits)
|
|
38
|
+
handleDomPathOffsetCommit: (
|
|
39
|
+
selection: DomEditSelection,
|
|
40
|
+
next: { x: number; y: number },
|
|
41
|
+
) => Promise<void>;
|
|
42
|
+
handleDomBoxSizeCommit: (
|
|
43
|
+
selection: DomEditSelection,
|
|
44
|
+
next: { width: number; height: number },
|
|
45
|
+
) => Promise<void>;
|
|
46
|
+
handleDomRotationCommit: (selection: DomEditSelection, next: { angle: number }) => Promise<void>;
|
|
47
|
+
// GSAP script commit ops (from useGsapScriptCommits)
|
|
48
|
+
addGsapAnimation: (
|
|
49
|
+
sel: DomEditSelection,
|
|
50
|
+
method: "to" | "from" | "set" | "fromTo",
|
|
51
|
+
time?: number,
|
|
52
|
+
) => Promise<void>;
|
|
53
|
+
convertToKeyframes: (sel: DomEditSelection, animId: string) => void;
|
|
54
|
+
setArcPath: (
|
|
55
|
+
sel: DomEditSelection,
|
|
56
|
+
animId: string,
|
|
57
|
+
config: {
|
|
58
|
+
enabled: boolean;
|
|
59
|
+
autoRotate?: boolean | number;
|
|
60
|
+
segments?: Array<{
|
|
61
|
+
curviness: number;
|
|
62
|
+
cp1?: { x: number; y: number };
|
|
63
|
+
cp2?: { x: number; y: number };
|
|
64
|
+
}>;
|
|
65
|
+
},
|
|
66
|
+
) => void;
|
|
67
|
+
updateArcSegment: (
|
|
68
|
+
sel: DomEditSelection,
|
|
69
|
+
animId: string,
|
|
70
|
+
segmentIndex: number,
|
|
71
|
+
update: {
|
|
72
|
+
curviness?: number;
|
|
73
|
+
cp1?: { x: number; y: number };
|
|
74
|
+
cp2?: { x: number; y: number };
|
|
75
|
+
},
|
|
76
|
+
) => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function useGsapAwareEditing({
|
|
80
|
+
domEditSelection,
|
|
81
|
+
selectedGsapAnimations,
|
|
82
|
+
gsapCommitMutation,
|
|
83
|
+
previewIframeRef,
|
|
84
|
+
showToast,
|
|
85
|
+
bumpGsapCache,
|
|
86
|
+
makeFetchFallback,
|
|
87
|
+
trackGsapInteractionFailure,
|
|
88
|
+
handleDomPathOffsetCommit,
|
|
89
|
+
handleDomBoxSizeCommit,
|
|
90
|
+
handleDomRotationCommit,
|
|
91
|
+
addGsapAnimation,
|
|
92
|
+
convertToKeyframes,
|
|
93
|
+
setArcPath,
|
|
94
|
+
updateArcSegment,
|
|
95
|
+
}: UseGsapAwareEditingParams) {
|
|
96
|
+
// ── GSAP-aware geometry commits ──
|
|
97
|
+
|
|
98
|
+
const handleGsapAwarePathOffsetCommit = useCallback(
|
|
99
|
+
async (selection: DomEditSelection, next: { x: number; y: number }) => {
|
|
100
|
+
const hasGsapAnims = selectedGsapAnimations.length > 0;
|
|
101
|
+
if (hasGsapAnims && !STUDIO_GSAP_DRAG_INTERCEPT_ENABLED) {
|
|
102
|
+
showToast(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE, "error");
|
|
103
|
+
throw new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
104
|
+
}
|
|
105
|
+
if (STUDIO_GSAP_DRAG_INTERCEPT_ENABLED && gsapCommitMutation) {
|
|
106
|
+
try {
|
|
107
|
+
const handled = await tryGsapDragIntercept(
|
|
108
|
+
selection,
|
|
109
|
+
next,
|
|
110
|
+
selectedGsapAnimations,
|
|
111
|
+
previewIframeRef.current,
|
|
112
|
+
gsapCommitMutation,
|
|
113
|
+
makeFetchFallback(selection),
|
|
114
|
+
);
|
|
115
|
+
if (handled) return;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
trackGsapInteractionFailure(error, selection, "drag", "Move animated layer");
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return handleDomPathOffsetCommit(selection, next);
|
|
122
|
+
},
|
|
123
|
+
[
|
|
124
|
+
handleDomPathOffsetCommit,
|
|
125
|
+
selectedGsapAnimations,
|
|
126
|
+
gsapCommitMutation,
|
|
127
|
+
previewIframeRef,
|
|
128
|
+
makeFetchFallback,
|
|
129
|
+
trackGsapInteractionFailure,
|
|
130
|
+
showToast,
|
|
131
|
+
],
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const handleGsapAwareBoxSizeCommit = useCallback(
|
|
135
|
+
async (selection: DomEditSelection, next: { width: number; height: number }) => {
|
|
136
|
+
if (STUDIO_GSAP_DRAG_INTERCEPT_ENABLED && gsapCommitMutation) {
|
|
137
|
+
try {
|
|
138
|
+
const handled = await tryGsapResizeIntercept(
|
|
139
|
+
selection,
|
|
140
|
+
next,
|
|
141
|
+
selectedGsapAnimations,
|
|
142
|
+
previewIframeRef.current,
|
|
143
|
+
gsapCommitMutation,
|
|
144
|
+
makeFetchFallback(selection),
|
|
145
|
+
);
|
|
146
|
+
if (handled) return;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
trackGsapInteractionFailure(error, selection, "resize", "Resize animated layer");
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return handleDomBoxSizeCommit(selection, next);
|
|
153
|
+
},
|
|
154
|
+
[
|
|
155
|
+
handleDomBoxSizeCommit,
|
|
156
|
+
selectedGsapAnimations,
|
|
157
|
+
gsapCommitMutation,
|
|
158
|
+
previewIframeRef,
|
|
159
|
+
makeFetchFallback,
|
|
160
|
+
trackGsapInteractionFailure,
|
|
161
|
+
],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const handleGsapAwareRotationCommit = useCallback(
|
|
165
|
+
async (selection: DomEditSelection, next: { angle: number }) => {
|
|
166
|
+
if (STUDIO_GSAP_DRAG_INTERCEPT_ENABLED && gsapCommitMutation) {
|
|
167
|
+
try {
|
|
168
|
+
const handled = await tryGsapRotationIntercept(
|
|
169
|
+
selection,
|
|
170
|
+
next.angle,
|
|
171
|
+
selectedGsapAnimations,
|
|
172
|
+
previewIframeRef.current,
|
|
173
|
+
gsapCommitMutation,
|
|
174
|
+
makeFetchFallback(selection),
|
|
175
|
+
);
|
|
176
|
+
if (handled) return;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
trackGsapInteractionFailure(error, selection, "rotation", "Rotate animated layer");
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return handleDomRotationCommit(selection, next);
|
|
183
|
+
},
|
|
184
|
+
[
|
|
185
|
+
handleDomRotationCommit,
|
|
186
|
+
selectedGsapAnimations,
|
|
187
|
+
gsapCommitMutation,
|
|
188
|
+
previewIframeRef,
|
|
189
|
+
makeFetchFallback,
|
|
190
|
+
trackGsapInteractionFailure,
|
|
191
|
+
],
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// ── Animated property commit ──
|
|
195
|
+
|
|
196
|
+
const commitAnimatedProperty = useAnimatedPropertyCommit({
|
|
197
|
+
selectedGsapAnimations,
|
|
198
|
+
gsapCommitMutation,
|
|
199
|
+
addGsapAnimation: (sel, method, time) => addGsapAnimation(sel, method, time),
|
|
200
|
+
convertToKeyframes: (sel, animId) => convertToKeyframes(sel, animId),
|
|
201
|
+
previewIframeRef,
|
|
202
|
+
bumpGsapCache,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ── Arc path wrappers ──
|
|
206
|
+
|
|
207
|
+
const handleSetArcPath = useCallback(
|
|
208
|
+
(animId: string, config: Parameters<typeof setArcPath>[2]) => {
|
|
209
|
+
if (!domEditSelection) return;
|
|
210
|
+
setArcPath(domEditSelection, animId, config);
|
|
211
|
+
},
|
|
212
|
+
[domEditSelection, setArcPath],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const handleUpdateArcSegment = useCallback(
|
|
216
|
+
(animId: string, segmentIndex: number, update: Parameters<typeof updateArcSegment>[3]) => {
|
|
217
|
+
if (!domEditSelection) return;
|
|
218
|
+
updateArcSegment(domEditSelection, animId, segmentIndex, update);
|
|
219
|
+
},
|
|
220
|
+
[domEditSelection, updateArcSegment],
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// ── Thin commitMutation facade ──
|
|
224
|
+
|
|
225
|
+
const commitMutation = useCallback(
|
|
226
|
+
async (mutation: Record<string, unknown>, options: { label: string; softReload?: boolean }) => {
|
|
227
|
+
if (!domEditSelection) return;
|
|
228
|
+
await gsapCommitMutation?.(domEditSelection, mutation, options);
|
|
229
|
+
},
|
|
230
|
+
[domEditSelection, gsapCommitMutation],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
handleGsapAwarePathOffsetCommit,
|
|
235
|
+
handleGsapAwareBoxSizeCommit,
|
|
236
|
+
handleGsapAwareRotationCommit,
|
|
237
|
+
commitAnimatedProperty,
|
|
238
|
+
handleSetArcPath,
|
|
239
|
+
handleUpdateArcSegment,
|
|
240
|
+
commitMutation,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
4
|
+
import { executeOptimistic } from "../utils/optimisticUpdate";
|
|
5
|
+
import type { KeyframeCacheEntry } from "../player/store/playerStore";
|
|
6
|
+
import { commitKeyframeAtTimeImpl } from "./gsapKeyframeCommit";
|
|
7
|
+
import { readKeyframeSnapshot, writeKeyframeCache } from "./gsapKeyframeCacheHelpers";
|
|
8
|
+
import type {
|
|
9
|
+
CommitMutation,
|
|
10
|
+
SafeGsapCommitMutation,
|
|
11
|
+
TrackGsapSaveFailure,
|
|
12
|
+
} from "./gsapScriptCommitTypes";
|
|
13
|
+
|
|
14
|
+
function executeOptimisticKeyframeCacheUpdate(options: {
|
|
15
|
+
sourceFile: string;
|
|
16
|
+
elementId: string | null | undefined;
|
|
17
|
+
apply: (entry: KeyframeCacheEntry) => KeyframeCacheEntry;
|
|
18
|
+
persist: () => Promise<void>;
|
|
19
|
+
}): Promise<void> {
|
|
20
|
+
return executeOptimistic<KeyframeCacheEntry | undefined>({
|
|
21
|
+
apply: () => {
|
|
22
|
+
const prev = readKeyframeSnapshot(options.sourceFile, options.elementId);
|
|
23
|
+
if (prev) writeKeyframeCache(options.sourceFile, options.elementId, options.apply(prev));
|
|
24
|
+
return prev;
|
|
25
|
+
},
|
|
26
|
+
persist: options.persist,
|
|
27
|
+
rollback: (prev) => {
|
|
28
|
+
writeKeyframeCache(options.sourceFile, options.elementId, prev);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface GsapKeyframeOpsParams {
|
|
34
|
+
activeCompPath: string | null;
|
|
35
|
+
commitMutation: CommitMutation;
|
|
36
|
+
commitMutationSafely: SafeGsapCommitMutation;
|
|
37
|
+
trackGsapSaveFailure: TrackGsapSaveFailure;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useGsapKeyframeOps({
|
|
41
|
+
activeCompPath,
|
|
42
|
+
commitMutation,
|
|
43
|
+
commitMutationSafely,
|
|
44
|
+
trackGsapSaveFailure,
|
|
45
|
+
}: GsapKeyframeOpsParams) {
|
|
46
|
+
const addKeyframe = useCallback(
|
|
47
|
+
(
|
|
48
|
+
selection: DomEditSelection,
|
|
49
|
+
animationId: string,
|
|
50
|
+
percentage: number,
|
|
51
|
+
property: string,
|
|
52
|
+
value: number | string,
|
|
53
|
+
) => {
|
|
54
|
+
const sourceFile = selection.sourceFile || activeCompPath || "index.html";
|
|
55
|
+
const mutation = {
|
|
56
|
+
type: "add-keyframe",
|
|
57
|
+
animationId,
|
|
58
|
+
percentage,
|
|
59
|
+
properties: { [property]: value },
|
|
60
|
+
};
|
|
61
|
+
void executeOptimisticKeyframeCacheUpdate({
|
|
62
|
+
sourceFile,
|
|
63
|
+
elementId: selection.id,
|
|
64
|
+
apply: (prev) => ({
|
|
65
|
+
...prev,
|
|
66
|
+
keyframes: [...prev.keyframes, { percentage, properties: { [property]: value } }].sort(
|
|
67
|
+
(a, b) => a.percentage - b.percentage,
|
|
68
|
+
),
|
|
69
|
+
}),
|
|
70
|
+
persist: () =>
|
|
71
|
+
commitMutation(selection, mutation, {
|
|
72
|
+
label: `Add keyframe at ${percentage}%`,
|
|
73
|
+
softReload: true,
|
|
74
|
+
}),
|
|
75
|
+
}).catch((error) => {
|
|
76
|
+
trackGsapSaveFailure(error, selection, mutation, `Add keyframe at ${percentage}%`);
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
[activeCompPath, commitMutation, trackGsapSaveFailure],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const addKeyframeBatch = useCallback(
|
|
83
|
+
(
|
|
84
|
+
selection: DomEditSelection,
|
|
85
|
+
animationId: string,
|
|
86
|
+
percentage: number,
|
|
87
|
+
properties: Record<string, number | string>,
|
|
88
|
+
) => {
|
|
89
|
+
return commitMutation(
|
|
90
|
+
selection,
|
|
91
|
+
{ type: "add-keyframe", animationId, percentage, properties },
|
|
92
|
+
{ label: `Add keyframe at ${percentage}%`, softReload: true },
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
[commitMutation],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const removeKeyframe = useCallback(
|
|
99
|
+
(selection: DomEditSelection, animationId: string, percentage: number) => {
|
|
100
|
+
const sourceFile = selection.sourceFile || activeCompPath || "index.html";
|
|
101
|
+
const mutation = { type: "remove-keyframe", animationId, percentage };
|
|
102
|
+
void executeOptimisticKeyframeCacheUpdate({
|
|
103
|
+
sourceFile,
|
|
104
|
+
elementId: selection.id,
|
|
105
|
+
apply: (prev) => ({
|
|
106
|
+
...prev,
|
|
107
|
+
keyframes: prev.keyframes.filter(
|
|
108
|
+
(kf) => Math.abs((kf.tweenPercentage ?? kf.percentage) - percentage) > 0.2,
|
|
109
|
+
),
|
|
110
|
+
}),
|
|
111
|
+
persist: () =>
|
|
112
|
+
commitMutation(selection, mutation, {
|
|
113
|
+
label: `Remove keyframe at ${percentage}%`,
|
|
114
|
+
softReload: true,
|
|
115
|
+
}),
|
|
116
|
+
}).catch((error) => {
|
|
117
|
+
trackGsapSaveFailure(error, selection, mutation, `Remove keyframe at ${percentage}%`);
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
[activeCompPath, commitMutation, trackGsapSaveFailure],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const convertToKeyframes = useCallback(
|
|
124
|
+
(
|
|
125
|
+
selection: DomEditSelection,
|
|
126
|
+
animationId: string,
|
|
127
|
+
resolvedFromValues?: Record<string, number | string>,
|
|
128
|
+
) => {
|
|
129
|
+
return commitMutation(
|
|
130
|
+
selection,
|
|
131
|
+
{ type: "convert-to-keyframes", animationId, resolvedFromValues },
|
|
132
|
+
{ label: "Convert to keyframes" },
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
[commitMutation],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const removeAllKeyframes = useCallback(
|
|
139
|
+
(selection: DomEditSelection, animationId: string) => {
|
|
140
|
+
commitMutationSafely(
|
|
141
|
+
selection,
|
|
142
|
+
{ type: "remove-all-keyframes", animationId },
|
|
143
|
+
{ label: "Remove all keyframes", softReload: true },
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
[commitMutationSafely],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const commitKeyframeAtTime = useCallback(
|
|
150
|
+
(
|
|
151
|
+
selection: DomEditSelection,
|
|
152
|
+
absoluteTime: number,
|
|
153
|
+
animations: GsapAnimation[],
|
|
154
|
+
properties: Record<string, number | string>,
|
|
155
|
+
) => commitKeyframeAtTimeImpl(selection, absoluteTime, animations, properties, commitMutation),
|
|
156
|
+
[commitMutation],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
addKeyframe,
|
|
161
|
+
addKeyframeBatch,
|
|
162
|
+
removeKeyframe,
|
|
163
|
+
convertToKeyframes,
|
|
164
|
+
removeAllKeyframes,
|
|
165
|
+
commitKeyframeAtTime,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
|
+
import { PROPERTY_DEFAULTS } from "./gsapScriptCommitHelpers";
|
|
4
|
+
import type { SafeGsapCommitMutation } from "./gsapScriptCommitTypes";
|
|
5
|
+
|
|
6
|
+
const DEBOUNCE_MS = 150;
|
|
7
|
+
|
|
8
|
+
export function useGsapPropertyDebounce(commitMutationSafely: SafeGsapCommitMutation) {
|
|
9
|
+
const pendingPropertyEditRef = useRef<{
|
|
10
|
+
selection: DomEditSelection;
|
|
11
|
+
animationId: string;
|
|
12
|
+
property: string;
|
|
13
|
+
value: number | string;
|
|
14
|
+
} | null>(null);
|
|
15
|
+
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
16
|
+
|
|
17
|
+
const flushPendingPropertyEdit = useCallback(() => {
|
|
18
|
+
const pending = pendingPropertyEditRef.current;
|
|
19
|
+
if (!pending) return;
|
|
20
|
+
pendingPropertyEditRef.current = null;
|
|
21
|
+
const { selection, animationId, property, value } = pending;
|
|
22
|
+
commitMutationSafely(
|
|
23
|
+
selection,
|
|
24
|
+
{ type: "update-property", animationId, property, value },
|
|
25
|
+
{
|
|
26
|
+
label: `Edit GSAP ${property}`,
|
|
27
|
+
coalesceKey: `gsap:${animationId}:${property}`,
|
|
28
|
+
softReload: true,
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
}, [commitMutationSafely]);
|
|
32
|
+
|
|
33
|
+
const updateGsapProperty = useCallback(
|
|
34
|
+
(
|
|
35
|
+
selection: DomEditSelection,
|
|
36
|
+
animationId: string,
|
|
37
|
+
property: string,
|
|
38
|
+
value: number | string,
|
|
39
|
+
) => {
|
|
40
|
+
pendingPropertyEditRef.current = { selection, animationId, property, value };
|
|
41
|
+
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
42
|
+
debounceTimerRef.current = setTimeout(flushPendingPropertyEdit, DEBOUNCE_MS);
|
|
43
|
+
},
|
|
44
|
+
[flushPendingPropertyEdit],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
return () => {
|
|
49
|
+
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
50
|
+
flushPendingPropertyEdit();
|
|
51
|
+
};
|
|
52
|
+
}, [flushPendingPropertyEdit]);
|
|
53
|
+
|
|
54
|
+
const addGsapProperty = useCallback(
|
|
55
|
+
(selection: DomEditSelection, animationId: string, property: string) => {
|
|
56
|
+
let defaultValue = PROPERTY_DEFAULTS[property] ?? 0;
|
|
57
|
+
const el = selection.element;
|
|
58
|
+
if (property === "width" || property === "height") {
|
|
59
|
+
const rect = el.getBoundingClientRect();
|
|
60
|
+
defaultValue = Math.round(property === "width" ? rect.width : rect.height);
|
|
61
|
+
} else if (property === "opacity" || property === "autoAlpha") {
|
|
62
|
+
const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
|
|
63
|
+
defaultValue = cs ? Number.parseFloat(cs.opacity) || 1 : 1;
|
|
64
|
+
}
|
|
65
|
+
commitMutationSafely(
|
|
66
|
+
selection,
|
|
67
|
+
{ type: "add-property", animationId, property, defaultValue },
|
|
68
|
+
{ label: `Add GSAP ${property}` },
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
[commitMutationSafely],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const removeGsapProperty = useCallback(
|
|
75
|
+
(selection: DomEditSelection, animationId: string, property: string) => {
|
|
76
|
+
commitMutationSafely(
|
|
77
|
+
selection,
|
|
78
|
+
{ type: "remove-property", animationId, property },
|
|
79
|
+
{ label: `Remove GSAP ${property}` },
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
[commitMutationSafely],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const updateGsapFromProperty = useCallback(
|
|
86
|
+
(
|
|
87
|
+
selection: DomEditSelection,
|
|
88
|
+
animationId: string,
|
|
89
|
+
property: string,
|
|
90
|
+
value: number | string,
|
|
91
|
+
) => {
|
|
92
|
+
commitMutationSafely(
|
|
93
|
+
selection,
|
|
94
|
+
{ type: "update-from-property", animationId, property, value },
|
|
95
|
+
{
|
|
96
|
+
label: `Edit GSAP from-${property}`,
|
|
97
|
+
coalesceKey: `gsap:${animationId}:from:${property}`,
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
[commitMutationSafely],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const addGsapFromProperty = useCallback(
|
|
105
|
+
(selection: DomEditSelection, animationId: string, property: string) => {
|
|
106
|
+
const defaultValue = PROPERTY_DEFAULTS[property] ?? 0;
|
|
107
|
+
commitMutationSafely(
|
|
108
|
+
selection,
|
|
109
|
+
{ type: "add-from-property", animationId, property, defaultValue },
|
|
110
|
+
{ label: `Add GSAP from-${property}` },
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
[commitMutationSafely],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const removeGsapFromProperty = useCallback(
|
|
117
|
+
(selection: DomEditSelection, animationId: string, property: string) => {
|
|
118
|
+
commitMutationSafely(
|
|
119
|
+
selection,
|
|
120
|
+
{ type: "remove-from-property", animationId, property },
|
|
121
|
+
{ label: `Remove GSAP from-${property}` },
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
[commitMutationSafely],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
updateGsapProperty,
|
|
129
|
+
addGsapProperty,
|
|
130
|
+
removeGsapProperty,
|
|
131
|
+
updateGsapFromProperty,
|
|
132
|
+
addGsapFromProperty,
|
|
133
|
+
removeGsapFromProperty,
|
|
134
|
+
};
|
|
135
|
+
}
|