@hyperframes/studio 0.6.97 → 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-HveJ0MuV.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-Daj5djxa.js +0 -418
- package/dist/assets/index-B0twsRu0.css +0 -1
- package/dist/assets/index-Cfye9xzo.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
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { useState, useCallback, useRef
|
|
1
|
+
import { useState, useCallback, useRef } from "react";
|
|
2
2
|
import type { EditingFile } from "../utils/studioHelpers";
|
|
3
3
|
import { FONT_EXT, isMediaFile } from "../utils/mediaTypes";
|
|
4
4
|
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
|
5
|
-
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
|
6
5
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
7
6
|
import { findTagByTarget, type PatchTarget } from "../utils/sourcePatcher";
|
|
8
|
-
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
9
7
|
import {
|
|
10
8
|
createStudioSaveHttpError,
|
|
11
9
|
retryStudioSave,
|
|
12
10
|
StudioSaveNetworkError,
|
|
13
11
|
} from "../utils/studioSaveDiagnostics";
|
|
12
|
+
import { useFileTree } from "./useFileTree";
|
|
13
|
+
import { useEditorSave } from "./useEditorSave";
|
|
14
14
|
|
|
15
15
|
// ── Types ──
|
|
16
16
|
|
|
@@ -38,54 +38,31 @@ export function useFileManager({
|
|
|
38
38
|
domEditSaveTimestampRef,
|
|
39
39
|
setRefreshKey,
|
|
40
40
|
}: UseFileManagerOptions) {
|
|
41
|
-
// ──
|
|
41
|
+
// ── Shared refs ──
|
|
42
42
|
|
|
43
43
|
const [editingFile, setEditingFile] = useState<EditingFile | null>(null);
|
|
44
|
-
const [projectDir, setProjectDir] = useState<string | null>(null);
|
|
45
|
-
const [fileTree, setFileTree] = useState<string[]>([]);
|
|
46
|
-
const [compositionPaths, setCompositionPaths] = useState<string[]>([]);
|
|
47
|
-
const [fileTreeLoaded, setFileTreeLoaded] = useState(false);
|
|
48
44
|
const [revealSourceOffset, setRevealSourceOffset] = useState<number | null>(null);
|
|
49
45
|
|
|
50
|
-
// ── Refs ──
|
|
51
|
-
|
|
52
46
|
const editingPathRef = useRef(editingFile?.path);
|
|
53
47
|
editingPathRef.current = editingFile?.path;
|
|
54
48
|
|
|
55
49
|
const projectIdRef = useRef(projectId);
|
|
56
50
|
projectIdRef.current = projectId;
|
|
57
51
|
|
|
58
|
-
const saveRafRef = useRef<number | null>(null);
|
|
59
|
-
const refreshRafRef = useRef<number | null>(null);
|
|
60
52
|
const importedFontAssetsRef = useRef<ImportedFontAsset[]>([]);
|
|
61
53
|
|
|
62
|
-
// ──
|
|
54
|
+
// ── File tree ──
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.then((data: { files?: string[]; dir?: string; compositions?: string[] }) => {
|
|
75
|
-
if (!cancelled && data.files) setFileTree(data.files);
|
|
76
|
-
if (!cancelled && data.compositions) setCompositionPaths(data.compositions);
|
|
77
|
-
if (!cancelled) setProjectDir(typeof data.dir === "string" ? data.dir : null);
|
|
78
|
-
})
|
|
79
|
-
.catch(() => {
|
|
80
|
-
if (!cancelled) setProjectDir(null);
|
|
81
|
-
})
|
|
82
|
-
.finally(() => {
|
|
83
|
-
if (!cancelled) setFileTreeLoaded(true);
|
|
84
|
-
});
|
|
85
|
-
return () => {
|
|
86
|
-
cancelled = true;
|
|
87
|
-
};
|
|
88
|
-
}, [projectId]);
|
|
56
|
+
const {
|
|
57
|
+
projectDir,
|
|
58
|
+
fileTree,
|
|
59
|
+
setFileTree,
|
|
60
|
+
fileTreeLoaded,
|
|
61
|
+
refreshFileTree,
|
|
62
|
+
compositions,
|
|
63
|
+
assets,
|
|
64
|
+
fontAssets,
|
|
65
|
+
} = useFileTree({ projectId, projectIdRef });
|
|
89
66
|
|
|
90
67
|
// ── Core file I/O ──
|
|
91
68
|
|
|
@@ -139,8 +116,23 @@ export function useFileManager({
|
|
|
139
116
|
return typeof data.content === "string" ? data.content : "";
|
|
140
117
|
}, []);
|
|
141
118
|
|
|
119
|
+
// ── Editor save (debounced content change) ──
|
|
120
|
+
|
|
121
|
+
const { saveRafRef, handleContentChange } = useEditorSave({
|
|
122
|
+
editingPathRef,
|
|
123
|
+
projectIdRef,
|
|
124
|
+
readProjectFile,
|
|
125
|
+
writeProjectFile,
|
|
126
|
+
recordEdit,
|
|
127
|
+
domEditSaveTimestampRef,
|
|
128
|
+
setRefreshKey,
|
|
129
|
+
});
|
|
130
|
+
|
|
142
131
|
// ── File select ──
|
|
143
132
|
|
|
133
|
+
const revealRequestIdRef = useRef(0);
|
|
134
|
+
const revealAbortRef = useRef<AbortController | null>(null);
|
|
135
|
+
|
|
144
136
|
const handleFileSelect = useCallback((path: string) => {
|
|
145
137
|
const pid = projectIdRef.current;
|
|
146
138
|
if (!pid) return;
|
|
@@ -162,47 +154,7 @@ export function useFileManager({
|
|
|
162
154
|
.catch(() => {});
|
|
163
155
|
}, []);
|
|
164
156
|
|
|
165
|
-
// ──
|
|
166
|
-
|
|
167
|
-
const handleContentChange = useCallback(
|
|
168
|
-
(content: string) => {
|
|
169
|
-
const pid = projectIdRef.current;
|
|
170
|
-
if (!pid) return;
|
|
171
|
-
const path = editingPathRef.current;
|
|
172
|
-
if (!path) return;
|
|
173
|
-
|
|
174
|
-
if (saveRafRef.current != null) cancelAnimationFrame(saveRafRef.current);
|
|
175
|
-
saveRafRef.current = requestAnimationFrame(() => {
|
|
176
|
-
domEditSaveTimestampRef.current = Date.now();
|
|
177
|
-
saveProjectFilesWithHistory({
|
|
178
|
-
projectId: pid,
|
|
179
|
-
label: "Edit source",
|
|
180
|
-
kind: "source",
|
|
181
|
-
coalesceKey: `source:${path}`,
|
|
182
|
-
files: { [path]: content },
|
|
183
|
-
readFile: readProjectFile,
|
|
184
|
-
writeFile: writeProjectFile,
|
|
185
|
-
recordEdit,
|
|
186
|
-
})
|
|
187
|
-
.then(() => {
|
|
188
|
-
if (refreshRafRef.current != null) cancelAnimationFrame(refreshRafRef.current);
|
|
189
|
-
refreshRafRef.current = requestAnimationFrame(() => setRefreshKey((k) => k + 1));
|
|
190
|
-
})
|
|
191
|
-
.catch((error) => {
|
|
192
|
-
trackStudioEvent("save_failure", {
|
|
193
|
-
source: "code_editor",
|
|
194
|
-
error_message: error instanceof Error ? error.message : "unknown",
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
},
|
|
199
|
-
[domEditSaveTimestampRef, readProjectFile, recordEdit, setRefreshKey, writeProjectFile],
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
// ── Open source for selection (click-to-source) ──
|
|
203
|
-
|
|
204
|
-
const revealRequestIdRef = useRef(0);
|
|
205
|
-
const revealAbortRef = useRef<AbortController | null>(null);
|
|
157
|
+
// ── Click-to-source ──
|
|
206
158
|
|
|
207
159
|
const openSourceForSelection = useCallback(
|
|
208
160
|
(sourceFile: string, target: PatchTarget) => {
|
|
@@ -235,16 +187,6 @@ export function useFileManager({
|
|
|
235
187
|
[editingFile?.content],
|
|
236
188
|
);
|
|
237
189
|
|
|
238
|
-
// ── File tree refresh ──
|
|
239
|
-
|
|
240
|
-
const refreshFileTree = useCallback(async () => {
|
|
241
|
-
const pid = projectIdRef.current;
|
|
242
|
-
if (!pid) return;
|
|
243
|
-
const res = await fetch(`/api/projects/${pid}`);
|
|
244
|
-
const data = await res.json();
|
|
245
|
-
if (data.files) setFileTree(data.files);
|
|
246
|
-
}, []);
|
|
247
|
-
|
|
248
190
|
// ── Upload ──
|
|
249
191
|
|
|
250
192
|
const uploadProjectFiles = useCallback(
|
|
@@ -289,7 +231,7 @@ export function useFileManager({
|
|
|
289
231
|
[refreshFileTree, setRefreshKey, showToast],
|
|
290
232
|
);
|
|
291
233
|
|
|
292
|
-
// ── File
|
|
234
|
+
// ── File CRUD ──
|
|
293
235
|
|
|
294
236
|
const handleCreateFile = useCallback(
|
|
295
237
|
async (path: string) => {
|
|
@@ -320,7 +262,6 @@ export function useFileManager({
|
|
|
320
262
|
async (path: string) => {
|
|
321
263
|
const pid = projectIdRef.current;
|
|
322
264
|
if (!pid) return;
|
|
323
|
-
// Create a .gitkeep inside the folder so it appears in the tree
|
|
324
265
|
const res = await fetch(
|
|
325
266
|
`/api/projects/${pid}/files/${encodeURIComponent(path + "/.gitkeep")}`,
|
|
326
267
|
{
|
|
@@ -371,7 +312,6 @@ export function useFileManager({
|
|
|
371
312
|
handleFileSelect(newPath);
|
|
372
313
|
}
|
|
373
314
|
await refreshFileTree();
|
|
374
|
-
// Refresh preview — references in compositions may have been updated
|
|
375
315
|
setRefreshKey((k) => k + 1);
|
|
376
316
|
} else {
|
|
377
317
|
const err = await res.json().catch(() => ({ error: "unknown" }));
|
|
@@ -437,28 +377,6 @@ export function useFileManager({
|
|
|
437
377
|
[uploadProjectFiles],
|
|
438
378
|
);
|
|
439
379
|
|
|
440
|
-
// ── Derived state ──
|
|
441
|
-
|
|
442
|
-
const compositions = compositionPaths;
|
|
443
|
-
|
|
444
|
-
const assets = useMemo(
|
|
445
|
-
() =>
|
|
446
|
-
fileTree.filter((f) => !f.endsWith(".html") && !f.endsWith(".md") && !f.endsWith(".json")),
|
|
447
|
-
[fileTree],
|
|
448
|
-
);
|
|
449
|
-
|
|
450
|
-
const fontAssets = useMemo<ImportedFontAsset[]>(
|
|
451
|
-
() =>
|
|
452
|
-
assets
|
|
453
|
-
.filter((asset) => FONT_EXT.test(asset))
|
|
454
|
-
.map((asset) => ({
|
|
455
|
-
family: fontFamilyFromAssetPath(asset),
|
|
456
|
-
path: asset,
|
|
457
|
-
url: `/api/projects/${projectId}/preview/${asset}`,
|
|
458
|
-
})),
|
|
459
|
-
[assets, projectId],
|
|
460
|
-
);
|
|
461
|
-
|
|
462
380
|
// ── Return ──
|
|
463
381
|
|
|
464
382
|
return {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
import { FONT_EXT } from "../utils/mediaTypes";
|
|
3
|
+
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
|
4
|
+
|
|
5
|
+
interface UseFileTreeOptions {
|
|
6
|
+
projectId: string | null;
|
|
7
|
+
projectIdRef: React.RefObject<string | null>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useFileTree({ projectId, projectIdRef }: UseFileTreeOptions) {
|
|
11
|
+
const [projectDir, setProjectDir] = useState<string | null>(null);
|
|
12
|
+
const [fileTree, setFileTree] = useState<string[]>([]);
|
|
13
|
+
const [compositionPaths, setCompositionPaths] = useState<string[]>([]);
|
|
14
|
+
const [fileTreeLoaded, setFileTreeLoaded] = useState(false);
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!projectId) {
|
|
19
|
+
setFileTreeLoaded(false);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
setFileTreeLoaded(false);
|
|
24
|
+
fetch(`/api/projects/${projectId}`)
|
|
25
|
+
.then((r) => r.json())
|
|
26
|
+
.then((data: { files?: string[]; dir?: string; compositions?: string[] }) => {
|
|
27
|
+
if (!cancelled && data.files) setFileTree(data.files);
|
|
28
|
+
if (!cancelled && data.compositions) setCompositionPaths(data.compositions);
|
|
29
|
+
if (!cancelled) setProjectDir(typeof data.dir === "string" ? data.dir : null);
|
|
30
|
+
})
|
|
31
|
+
.catch(() => {
|
|
32
|
+
if (!cancelled) setProjectDir(null);
|
|
33
|
+
})
|
|
34
|
+
.finally(() => {
|
|
35
|
+
if (!cancelled) setFileTreeLoaded(true);
|
|
36
|
+
});
|
|
37
|
+
return () => {
|
|
38
|
+
cancelled = true;
|
|
39
|
+
};
|
|
40
|
+
}, [projectId]);
|
|
41
|
+
|
|
42
|
+
const refreshFileTree = useCallback(async () => {
|
|
43
|
+
const pid = projectIdRef.current;
|
|
44
|
+
if (!pid) return;
|
|
45
|
+
const res = await fetch(`/api/projects/${pid}`);
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
if (data.files) setFileTree(data.files);
|
|
48
|
+
}, [projectIdRef]);
|
|
49
|
+
|
|
50
|
+
const compositions = compositionPaths;
|
|
51
|
+
|
|
52
|
+
const assets = useMemo(
|
|
53
|
+
() =>
|
|
54
|
+
fileTree.filter((f) => !f.endsWith(".html") && !f.endsWith(".md") && !f.endsWith(".json")),
|
|
55
|
+
[fileTree],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const fontAssets = useMemo<ImportedFontAsset[]>(
|
|
59
|
+
() =>
|
|
60
|
+
assets
|
|
61
|
+
.filter((asset) => FONT_EXT.test(asset))
|
|
62
|
+
.map((asset) => ({
|
|
63
|
+
family: fontFamilyFromAssetPath(asset),
|
|
64
|
+
path: asset,
|
|
65
|
+
url: `/api/projects/${projectId}/preview/${asset}`,
|
|
66
|
+
})),
|
|
67
|
+
[assets, projectId],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
projectDir,
|
|
72
|
+
fileTree,
|
|
73
|
+
setFileTree,
|
|
74
|
+
fileTreeLoaded,
|
|
75
|
+
refreshFileTree,
|
|
76
|
+
compositions,
|
|
77
|
+
assets,
|
|
78
|
+
fontAssets,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -8,6 +8,7 @@ import { simplifyGestureSamples } from "../utils/rdpSimplify";
|
|
|
8
8
|
import { usePlayerStore } from "../player";
|
|
9
9
|
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
10
10
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
11
|
+
import { roundTo3 } from "../utils/rounding";
|
|
11
12
|
import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
|
|
12
13
|
|
|
13
14
|
// Minimal subset of the session used by gesture commit
|
|
@@ -74,7 +75,8 @@ export function useGestureCommit({
|
|
|
74
75
|
}
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
|
-
const duration =
|
|
78
|
+
const duration =
|
|
79
|
+
frozenSamples.length > 0 ? (frozenSamples[frozenSamples.length - 1]?.time ?? 0) : 0;
|
|
78
80
|
|
|
79
81
|
if (frozenSamples.length <= 2) {
|
|
80
82
|
showToast("No gesture detected — move the pointer while recording", "error");
|
|
@@ -171,8 +173,8 @@ export function useGestureCommit({
|
|
|
171
173
|
{
|
|
172
174
|
type: "add-with-keyframes",
|
|
173
175
|
targetSelector: selector,
|
|
174
|
-
position:
|
|
175
|
-
duration:
|
|
176
|
+
position: roundTo3(recStart),
|
|
177
|
+
duration: roundTo3(duration),
|
|
176
178
|
keyframes,
|
|
177
179
|
},
|
|
178
180
|
{ label: "Gesture recording (new range)", softReload: true },
|
|
@@ -183,8 +185,8 @@ export function useGestureCommit({
|
|
|
183
185
|
{
|
|
184
186
|
type: "add-with-keyframes",
|
|
185
187
|
targetSelector: selector,
|
|
186
|
-
position:
|
|
187
|
-
duration:
|
|
188
|
+
position: roundTo3(recStart),
|
|
189
|
+
duration: roundTo3(duration),
|
|
188
190
|
keyframes,
|
|
189
191
|
},
|
|
190
192
|
{ label: "Gesture recording", softReload: true },
|
|
@@ -430,7 +430,7 @@ export function useGestureRecording() {
|
|
|
430
430
|
r.cleanup?.();
|
|
431
431
|
r.cleanup = null;
|
|
432
432
|
const frozen = r.samples.slice();
|
|
433
|
-
setRecordingDuration(frozen.length > 0 ? frozen[frozen.length - 1]
|
|
433
|
+
setRecordingDuration(frozen.length > 0 ? (frozen[frozen.length - 1]?.time ?? 0) : 0);
|
|
434
434
|
setIsRecording(false);
|
|
435
435
|
return frozen;
|
|
436
436
|
}, []); // No deps — uses refs only
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
|
+
import { roundTo3 } from "../utils/rounding";
|
|
4
|
+
import {
|
|
5
|
+
assignGsapTargetAutoIdIfNeeded,
|
|
6
|
+
ensureElementAddressable,
|
|
7
|
+
} from "./gsapScriptCommitHelpers";
|
|
8
|
+
import type { CommitMutation, SafeGsapCommitMutation } from "./gsapScriptCommitTypes";
|
|
9
|
+
|
|
10
|
+
interface GsapAnimationOpsParams {
|
|
11
|
+
projectIdRef: React.MutableRefObject<string | null>;
|
|
12
|
+
activeCompPath: string | null;
|
|
13
|
+
commitMutation: CommitMutation;
|
|
14
|
+
commitMutationSafely: SafeGsapCommitMutation;
|
|
15
|
+
showToast: (message: string, tone?: "error" | "info") => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useGsapAnimationOps({
|
|
19
|
+
projectIdRef,
|
|
20
|
+
activeCompPath,
|
|
21
|
+
commitMutation,
|
|
22
|
+
commitMutationSafely,
|
|
23
|
+
showToast,
|
|
24
|
+
}: GsapAnimationOpsParams) {
|
|
25
|
+
const updateGsapMeta = useCallback(
|
|
26
|
+
(
|
|
27
|
+
selection: DomEditSelection,
|
|
28
|
+
animationId: string,
|
|
29
|
+
updates: { duration?: number; ease?: string; position?: number },
|
|
30
|
+
) => {
|
|
31
|
+
commitMutationSafely(
|
|
32
|
+
selection,
|
|
33
|
+
{ type: "update-meta", animationId, updates },
|
|
34
|
+
{
|
|
35
|
+
label: "Edit GSAP animation",
|
|
36
|
+
coalesceKey: `gsap:${animationId}:meta`,
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
[commitMutationSafely],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const deleteGsapAnimation = useCallback(
|
|
44
|
+
(selection: DomEditSelection, animationId: string) => {
|
|
45
|
+
commitMutationSafely(
|
|
46
|
+
selection,
|
|
47
|
+
{ type: "delete", animationId, stripStudioEdits: true },
|
|
48
|
+
{ label: "Delete GSAP animation" },
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
[commitMutationSafely],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const deleteAllForSelector = useCallback(
|
|
55
|
+
(selection: DomEditSelection, targetSelector: string) => {
|
|
56
|
+
void commitMutation(
|
|
57
|
+
selection,
|
|
58
|
+
{ type: "delete-all-for-selector", targetSelector },
|
|
59
|
+
{ label: "Delete all animations for element" },
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
[commitMutation],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const addGsapAnimation = useCallback(
|
|
66
|
+
async (
|
|
67
|
+
selection: DomEditSelection,
|
|
68
|
+
method: "to" | "from" | "set" | "fromTo",
|
|
69
|
+
_currentTime?: number,
|
|
70
|
+
) => {
|
|
71
|
+
const { selector, autoId } = ensureElementAddressable(selection);
|
|
72
|
+
|
|
73
|
+
if (autoId) {
|
|
74
|
+
const pid = projectIdRef.current;
|
|
75
|
+
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
76
|
+
if (!pid) return;
|
|
77
|
+
const assigned = await assignGsapTargetAutoIdIfNeeded({
|
|
78
|
+
projectId: pid,
|
|
79
|
+
targetPath,
|
|
80
|
+
selection,
|
|
81
|
+
autoId,
|
|
82
|
+
showToast,
|
|
83
|
+
});
|
|
84
|
+
if (!assigned) return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
|
|
88
|
+
const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "1") || 1;
|
|
89
|
+
const position = roundTo3(elStart);
|
|
90
|
+
const duration = roundTo3(elDuration);
|
|
91
|
+
const toDefaults: Record<string, Record<string, number>> = {
|
|
92
|
+
from: { opacity: 0 },
|
|
93
|
+
to: { x: 0, y: 0, opacity: 1 },
|
|
94
|
+
set: { opacity: 1 },
|
|
95
|
+
fromTo: { x: 0, y: 0, opacity: 1 },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await commitMutation(
|
|
99
|
+
selection,
|
|
100
|
+
{
|
|
101
|
+
type: "add",
|
|
102
|
+
targetSelector: selector,
|
|
103
|
+
method,
|
|
104
|
+
position,
|
|
105
|
+
duration: method === "set" ? undefined : duration,
|
|
106
|
+
ease: method === "set" ? undefined : "power2.out",
|
|
107
|
+
properties: toDefaults[method] ?? { opacity: 1 },
|
|
108
|
+
fromProperties: method === "fromTo" ? { opacity: 0 } : undefined,
|
|
109
|
+
},
|
|
110
|
+
{ label: `Add GSAP ${method} animation` },
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
[activeCompPath, commitMutation, projectIdRef, showToast],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
updateGsapMeta,
|
|
118
|
+
deleteGsapAnimation,
|
|
119
|
+
deleteAllForSelector,
|
|
120
|
+
addGsapAnimation,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
|
+
import type { SafeGsapCommitMutation } from "./gsapScriptCommitTypes";
|
|
4
|
+
|
|
5
|
+
export function useGsapArcPathOps(commitMutationSafely: SafeGsapCommitMutation) {
|
|
6
|
+
const setArcPath = useCallback(
|
|
7
|
+
(
|
|
8
|
+
selection: DomEditSelection,
|
|
9
|
+
animationId: string,
|
|
10
|
+
config: {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
autoRotate?: boolean | number;
|
|
13
|
+
segments?: Array<{
|
|
14
|
+
curviness: number;
|
|
15
|
+
cp1?: { x: number; y: number };
|
|
16
|
+
cp2?: { x: number; y: number };
|
|
17
|
+
}>;
|
|
18
|
+
},
|
|
19
|
+
) => {
|
|
20
|
+
commitMutationSafely(
|
|
21
|
+
selection,
|
|
22
|
+
{ type: "set-arc-path" as const, animationId, ...config },
|
|
23
|
+
{ label: config.enabled ? "Enable arc path" : "Disable arc path", softReload: true },
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
[commitMutationSafely],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const updateArcSegment = useCallback(
|
|
30
|
+
(
|
|
31
|
+
selection: DomEditSelection,
|
|
32
|
+
animationId: string,
|
|
33
|
+
segmentIndex: number,
|
|
34
|
+
update: {
|
|
35
|
+
curviness?: number;
|
|
36
|
+
cp1?: { x: number; y: number };
|
|
37
|
+
cp2?: { x: number; y: number };
|
|
38
|
+
},
|
|
39
|
+
) => {
|
|
40
|
+
commitMutationSafely(
|
|
41
|
+
selection,
|
|
42
|
+
{ type: "update-arc-segment" as const, animationId, segmentIndex, ...update },
|
|
43
|
+
{ label: "Update arc segment", softReload: true },
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
[commitMutationSafely],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const removeArcPath = useCallback(
|
|
50
|
+
(selection: DomEditSelection, animationId: string) => {
|
|
51
|
+
commitMutationSafely(
|
|
52
|
+
selection,
|
|
53
|
+
{ type: "remove-arc-path" as const, animationId },
|
|
54
|
+
{ label: "Remove arc path", softReload: true },
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
[commitMutationSafely],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return { setArcPath, updateArcSegment, removeArcPath };
|
|
61
|
+
}
|