@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
|
@@ -2,18 +2,12 @@ import { createContext, useContext, useMemo, type ReactNode } from "react";
|
|
|
2
2
|
import type { TimelineElement } from "../player";
|
|
3
3
|
import type { CompositionDimensions } from "../components/renders/RenderQueue";
|
|
4
4
|
|
|
5
|
-
export interface
|
|
5
|
+
export interface StudioShellValue {
|
|
6
6
|
projectId: string;
|
|
7
7
|
activeCompPath: string | null;
|
|
8
8
|
setActiveCompPath: (path: string | null) => void;
|
|
9
9
|
showToast: (message: string, tone?: "error" | "info") => void;
|
|
10
10
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
11
|
-
captionEditMode: boolean;
|
|
12
|
-
compositionLoading: boolean;
|
|
13
|
-
refreshKey: number;
|
|
14
|
-
setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
|
|
15
|
-
timelineElements: TimelineElement[];
|
|
16
|
-
isPlaying: boolean;
|
|
17
11
|
editHistory: {
|
|
18
12
|
canUndo: boolean;
|
|
19
13
|
canRedo: boolean;
|
|
@@ -32,24 +26,49 @@ export interface StudioContextValue {
|
|
|
32
26
|
compositionDimensions: CompositionDimensions | null;
|
|
33
27
|
waitForPendingDomEditSaves: () => Promise<void>;
|
|
34
28
|
handlePreviewIframeRef: (iframe: HTMLIFrameElement | null) => void;
|
|
35
|
-
refreshPreviewDocumentVersion: () => void;
|
|
36
29
|
timelineVisible: boolean;
|
|
37
30
|
toggleTimelineVisibility: () => void;
|
|
38
31
|
}
|
|
39
32
|
|
|
40
|
-
|
|
33
|
+
export interface StudioPlaybackValue {
|
|
34
|
+
captionEditMode: boolean;
|
|
35
|
+
compositionLoading: boolean;
|
|
36
|
+
refreshKey: number;
|
|
37
|
+
setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
|
|
38
|
+
timelineElements: TimelineElement[];
|
|
39
|
+
isPlaying: boolean;
|
|
40
|
+
refreshPreviewDocumentVersion: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type StudioContextValue = StudioShellValue & StudioPlaybackValue;
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
const StudioShellContext = createContext<StudioShellValue | null>(null);
|
|
46
|
+
const StudioPlaybackContext = createContext<StudioPlaybackValue | null>(null);
|
|
47
|
+
|
|
48
|
+
export function useStudioShellContext(): StudioShellValue {
|
|
49
|
+
const ctx = useContext(StudioShellContext);
|
|
50
|
+
if (!ctx) throw new Error("useStudioShellContext must be used within StudioShellProvider");
|
|
45
51
|
return ctx;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
export function
|
|
54
|
+
export function useStudioPlaybackContext(): StudioPlaybackValue {
|
|
55
|
+
const ctx = useContext(StudioPlaybackContext);
|
|
56
|
+
if (!ctx) throw new Error("useStudioPlaybackContext must be used within StudioPlaybackProvider");
|
|
57
|
+
return ctx;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @deprecated Use useStudioShellContext and/or useStudioPlaybackContext instead. */
|
|
61
|
+
export function useStudioContext(): StudioContextValue {
|
|
62
|
+
const shell = useStudioShellContext();
|
|
63
|
+
const playback = useStudioPlaybackContext();
|
|
64
|
+
return useMemo(() => ({ ...shell, ...playback }), [shell, playback]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function StudioShellProvider({
|
|
49
68
|
value,
|
|
50
69
|
children,
|
|
51
70
|
}: {
|
|
52
|
-
value:
|
|
71
|
+
value: StudioShellValue;
|
|
53
72
|
children: ReactNode;
|
|
54
73
|
}) {
|
|
55
74
|
const {
|
|
@@ -58,12 +77,6 @@ export function StudioProvider({
|
|
|
58
77
|
setActiveCompPath,
|
|
59
78
|
showToast,
|
|
60
79
|
previewIframeRef,
|
|
61
|
-
captionEditMode,
|
|
62
|
-
compositionLoading,
|
|
63
|
-
refreshKey,
|
|
64
|
-
setRefreshKey,
|
|
65
|
-
timelineElements,
|
|
66
|
-
isPlaying,
|
|
67
80
|
editHistory,
|
|
68
81
|
handleUndo,
|
|
69
82
|
handleRedo,
|
|
@@ -71,24 +84,17 @@ export function StudioProvider({
|
|
|
71
84
|
compositionDimensions,
|
|
72
85
|
waitForPendingDomEditSaves,
|
|
73
86
|
handlePreviewIframeRef,
|
|
74
|
-
refreshPreviewDocumentVersion,
|
|
75
87
|
timelineVisible,
|
|
76
88
|
toggleTimelineVisibility,
|
|
77
89
|
} = value;
|
|
78
90
|
|
|
79
|
-
const stable = useMemo<
|
|
91
|
+
const stable = useMemo<StudioShellValue>(
|
|
80
92
|
() => ({
|
|
81
93
|
projectId,
|
|
82
94
|
activeCompPath,
|
|
83
95
|
setActiveCompPath,
|
|
84
96
|
showToast,
|
|
85
97
|
previewIframeRef,
|
|
86
|
-
captionEditMode,
|
|
87
|
-
compositionLoading,
|
|
88
|
-
refreshKey,
|
|
89
|
-
setRefreshKey,
|
|
90
|
-
timelineElements,
|
|
91
|
-
isPlaying,
|
|
92
98
|
editHistory,
|
|
93
99
|
handleUndo,
|
|
94
100
|
handleRedo,
|
|
@@ -96,36 +102,80 @@ export function StudioProvider({
|
|
|
96
102
|
compositionDimensions,
|
|
97
103
|
waitForPendingDomEditSaves,
|
|
98
104
|
handlePreviewIframeRef,
|
|
99
|
-
refreshPreviewDocumentVersion,
|
|
100
105
|
timelineVisible,
|
|
101
106
|
toggleTimelineVisibility,
|
|
102
107
|
}),
|
|
103
|
-
// Representative subset of deps that actually change — stable callbacks
|
|
104
|
-
// (showToast, setActiveCompPath, etc.) are included for correctness but
|
|
105
|
-
// won't trigger re-renders on their own.
|
|
106
108
|
[
|
|
107
109
|
projectId,
|
|
108
110
|
activeCompPath,
|
|
109
|
-
captionEditMode,
|
|
110
|
-
compositionLoading,
|
|
111
|
-
refreshKey,
|
|
112
|
-
isPlaying,
|
|
113
111
|
compositionDimensions,
|
|
114
112
|
timelineVisible,
|
|
115
113
|
editHistory,
|
|
116
|
-
timelineElements,
|
|
117
114
|
renderQueue,
|
|
118
115
|
setActiveCompPath,
|
|
119
116
|
showToast,
|
|
120
117
|
previewIframeRef,
|
|
121
|
-
setRefreshKey,
|
|
122
118
|
handleUndo,
|
|
123
119
|
handleRedo,
|
|
124
120
|
waitForPendingDomEditSaves,
|
|
125
121
|
handlePreviewIframeRef,
|
|
126
|
-
refreshPreviewDocumentVersion,
|
|
127
122
|
toggleTimelineVisibility,
|
|
128
123
|
],
|
|
129
124
|
);
|
|
130
|
-
return <
|
|
125
|
+
return <StudioShellContext value={stable}>{children}</StudioShellContext>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function StudioPlaybackProvider({
|
|
129
|
+
value,
|
|
130
|
+
children,
|
|
131
|
+
}: {
|
|
132
|
+
value: StudioPlaybackValue;
|
|
133
|
+
children: ReactNode;
|
|
134
|
+
}) {
|
|
135
|
+
const {
|
|
136
|
+
captionEditMode,
|
|
137
|
+
compositionLoading,
|
|
138
|
+
refreshKey,
|
|
139
|
+
setRefreshKey,
|
|
140
|
+
timelineElements,
|
|
141
|
+
isPlaying,
|
|
142
|
+
refreshPreviewDocumentVersion,
|
|
143
|
+
} = value;
|
|
144
|
+
|
|
145
|
+
const stable = useMemo<StudioPlaybackValue>(
|
|
146
|
+
() => ({
|
|
147
|
+
captionEditMode,
|
|
148
|
+
compositionLoading,
|
|
149
|
+
refreshKey,
|
|
150
|
+
setRefreshKey,
|
|
151
|
+
timelineElements,
|
|
152
|
+
isPlaying,
|
|
153
|
+
refreshPreviewDocumentVersion,
|
|
154
|
+
}),
|
|
155
|
+
[
|
|
156
|
+
captionEditMode,
|
|
157
|
+
compositionLoading,
|
|
158
|
+
refreshKey,
|
|
159
|
+
timelineElements,
|
|
160
|
+
isPlaying,
|
|
161
|
+
setRefreshKey,
|
|
162
|
+
refreshPreviewDocumentVersion,
|
|
163
|
+
],
|
|
164
|
+
);
|
|
165
|
+
return <StudioPlaybackContext value={stable}>{children}</StudioPlaybackContext>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @deprecated Use StudioShellProvider and StudioPlaybackProvider instead. */
|
|
169
|
+
export function StudioProvider({
|
|
170
|
+
value,
|
|
171
|
+
children,
|
|
172
|
+
}: {
|
|
173
|
+
value: StudioContextValue;
|
|
174
|
+
children: ReactNode;
|
|
175
|
+
}) {
|
|
176
|
+
return (
|
|
177
|
+
<StudioShellProvider value={value}>
|
|
178
|
+
<StudioPlaybackProvider value={value}>{children}</StudioPlaybackProvider>
|
|
179
|
+
</StudioShellProvider>
|
|
180
|
+
);
|
|
131
181
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
|
2
|
+
import type { TimelineEditCallbacks } from "../player/components/timelineCallbacks";
|
|
3
|
+
|
|
4
|
+
const TimelineEditContext = createContext<TimelineEditCallbacks | null>(null);
|
|
5
|
+
|
|
6
|
+
export function useTimelineEditContext(): TimelineEditCallbacks {
|
|
7
|
+
const ctx = useContext(TimelineEditContext);
|
|
8
|
+
if (!ctx) throw new Error("useTimelineEditContext must be used within TimelineEditProvider");
|
|
9
|
+
return ctx;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Optional access — returns an empty object when outside a provider.
|
|
14
|
+
* Useful in components that can render both inside and outside the NLE.
|
|
15
|
+
*/
|
|
16
|
+
export function useTimelineEditContextOptional(): TimelineEditCallbacks {
|
|
17
|
+
return useContext(TimelineEditContext) ?? {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function TimelineEditProvider({
|
|
21
|
+
value,
|
|
22
|
+
children,
|
|
23
|
+
}: {
|
|
24
|
+
value: TimelineEditCallbacks;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}) {
|
|
27
|
+
const memoized = useMemo(
|
|
28
|
+
() => value,
|
|
29
|
+
// Each callback is a stable reference from the parent — memoize the bag
|
|
30
|
+
// so consumers don't re-render when unrelated parent state changes.
|
|
31
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
32
|
+
[
|
|
33
|
+
value.onMoveElement,
|
|
34
|
+
value.onResizeElement,
|
|
35
|
+
value.onBlockedEditAttempt,
|
|
36
|
+
value.onSplitElement,
|
|
37
|
+
value.onRazorSplit,
|
|
38
|
+
value.onRazorSplitAll,
|
|
39
|
+
value.onDeleteKeyframe,
|
|
40
|
+
value.onDeleteAllKeyframes,
|
|
41
|
+
value.onChangeKeyframeEase,
|
|
42
|
+
value.onMoveKeyframe,
|
|
43
|
+
value.onToggleKeyframeAtPlayhead,
|
|
44
|
+
],
|
|
45
|
+
);
|
|
46
|
+
return <TimelineEditContext.Provider value={memoized}>{children}</TimelineEditContext.Provider>;
|
|
47
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
2
|
+
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
3
|
+
|
|
4
|
+
export type PersistDomEditOperations = (
|
|
5
|
+
selection: DomEditSelection,
|
|
6
|
+
operations: PatchOperation[],
|
|
7
|
+
options?: {
|
|
8
|
+
label?: string;
|
|
9
|
+
coalesceKey?: string;
|
|
10
|
+
skipRefresh?: boolean;
|
|
11
|
+
prepareContent?: (html: string, sourceFile: string) => string;
|
|
12
|
+
shouldSave?: () => boolean;
|
|
13
|
+
},
|
|
14
|
+
) => Promise<void>;
|
|
@@ -6,11 +6,9 @@ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
|
6
6
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
7
7
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
8
8
|
import { readRuntimeKeyframes, scanAllRuntimeKeyframes } from "./gsapRuntimeKeyframes";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
resolveTweenDuration,
|
|
13
|
-
} from "../utils/globalTimeCompiler";
|
|
9
|
+
import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler";
|
|
10
|
+
import { roundTo3 } from "../utils/rounding";
|
|
11
|
+
import { computeElementPercentage } from "./gsapShared";
|
|
14
12
|
export interface GsapDragCommitCallbacks {
|
|
15
13
|
commitMutation: (
|
|
16
14
|
selection: DomEditSelection,
|
|
@@ -26,25 +24,12 @@ export interface GsapDragCommitCallbacks {
|
|
|
26
24
|
fetchAnimations?: () => Promise<GsapAnimation[]>;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
//
|
|
30
|
-
|
|
27
|
+
// Re-export for backward compatibility with existing imports.
|
|
31
28
|
export function computeCurrentPercentage(
|
|
32
29
|
selection: DomEditSelection,
|
|
33
30
|
animation?: GsapAnimation,
|
|
34
31
|
): number {
|
|
35
|
-
|
|
36
|
-
if (animation) {
|
|
37
|
-
const start = resolveTweenStart(animation);
|
|
38
|
-
const duration = resolveTweenDuration(animation);
|
|
39
|
-
if (start !== null) {
|
|
40
|
-
return absoluteToPercentage(currentTime, start, duration);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
|
|
44
|
-
const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "1") || 1;
|
|
45
|
-
return elDuration > 0
|
|
46
|
-
? Math.max(0, Math.min(100, Math.round(((currentTime - elStart) / elDuration) * 1000) / 10))
|
|
47
|
-
: 0;
|
|
32
|
+
return computeElementPercentage(usePlayerStore.getState().currentTime, selection, animation);
|
|
48
33
|
}
|
|
49
34
|
|
|
50
35
|
// ── Dynamic keyframe materialization ──────────────────────────────────────
|
|
@@ -133,8 +118,8 @@ async function extendTweenAndAddKeyframe(
|
|
|
133
118
|
type: "replace-with-keyframes",
|
|
134
119
|
animationId: anim.id,
|
|
135
120
|
targetSelector: anim.targetSelector,
|
|
136
|
-
position:
|
|
137
|
-
duration:
|
|
121
|
+
position: roundTo3(newStart),
|
|
122
|
+
duration: roundTo3(newDuration),
|
|
138
123
|
keyframes: remappedKfs,
|
|
139
124
|
},
|
|
140
125
|
{ label: `Move layer (extended keyframe)`, softReload: true, beforeReload },
|
|
@@ -330,8 +315,8 @@ export async function commitGsapPositionFromDrag(
|
|
|
330
315
|
{
|
|
331
316
|
type: "add-with-keyframes",
|
|
332
317
|
targetSelector: anim.targetSelector,
|
|
333
|
-
position:
|
|
334
|
-
duration:
|
|
318
|
+
position: roundTo3(newStart),
|
|
319
|
+
duration: roundTo3(newDuration),
|
|
335
320
|
keyframes,
|
|
336
321
|
},
|
|
337
322
|
{ label: "Move layer (from extended)", softReload: true, beforeReload: restoreOffset },
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
6
6
|
import { usePlayerStore, type KeyframeCacheEntry } from "../player/store/playerStore";
|
|
7
|
+
import { toAbsoluteTime } from "./gsapShared";
|
|
7
8
|
|
|
8
9
|
export function updateKeyframeCacheFromParsed(
|
|
9
10
|
animations: GsapAnimation[],
|
|
@@ -29,7 +30,7 @@ export function updateKeyframeCacheFromParsed(
|
|
|
29
30
|
const elStart = timelineEl?.start ?? 0;
|
|
30
31
|
const elDuration = timelineEl?.duration ?? 1;
|
|
31
32
|
const clipKeyframes = anim.keyframes.keyframes.map((kf) => {
|
|
32
|
-
const absTime = tweenPos
|
|
33
|
+
const absTime = toAbsoluteTime(tweenPos, tweenDur, kf.percentage);
|
|
33
34
|
const clipPct =
|
|
34
35
|
elDuration > 0 ? Math.round(((absTime - elStart) / elDuration) * 1000) / 10 : kf.percentage;
|
|
35
36
|
return {
|
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
2
2
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
3
|
import { absoluteToPercentageForAnimation, findTweenAtTime } from "../utils/globalTimeCompiler";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
opacity: 1,
|
|
7
|
-
x: 0,
|
|
8
|
-
y: 0,
|
|
9
|
-
scale: 1,
|
|
10
|
-
scaleX: 1,
|
|
11
|
-
scaleY: 1,
|
|
12
|
-
rotation: 0,
|
|
13
|
-
width: 100,
|
|
14
|
-
height: 100,
|
|
15
|
-
};
|
|
4
|
+
import { PROPERTY_DEFAULTS, selectorFromSelection } from "./gsapShared";
|
|
5
|
+
import { roundToCenti } from "../utils/rounding";
|
|
16
6
|
|
|
17
7
|
type CommitFn = (
|
|
18
8
|
selection: DomEditSelection,
|
|
@@ -32,7 +22,7 @@ export async function commitKeyframeAtTimeImpl(
|
|
|
32
22
|
properties: Record<string, number | string>,
|
|
33
23
|
commitMutation: CommitFn,
|
|
34
24
|
): Promise<void> {
|
|
35
|
-
const selector = selection
|
|
25
|
+
const selector = selectorFromSelection(selection);
|
|
36
26
|
if (!selector) return;
|
|
37
27
|
|
|
38
28
|
const tween = findTweenAtTime(absoluteTime, animations, selector);
|
|
@@ -64,7 +54,7 @@ export async function commitKeyframeAtTimeImpl(
|
|
|
64
54
|
backfillDefaults,
|
|
65
55
|
},
|
|
66
56
|
{
|
|
67
|
-
label: `Add keyframe at ${
|
|
57
|
+
label: `Add keyframe at ${roundToCenti(absoluteTime)}s`,
|
|
68
58
|
coalesceKey: `keyframe:${tween.id}:${pct}`,
|
|
69
59
|
softReload: true,
|
|
70
60
|
},
|
|
@@ -84,7 +74,7 @@ export async function commitKeyframeAtTimeImpl(
|
|
|
84
74
|
],
|
|
85
75
|
},
|
|
86
76
|
{
|
|
87
|
-
label: `New animation at ${
|
|
77
|
+
label: `New animation at ${roundToCenti(absoluteTime)}s`,
|
|
88
78
|
softReload: true,
|
|
89
79
|
},
|
|
90
80
|
);
|
|
@@ -20,37 +20,20 @@ import {
|
|
|
20
20
|
} from "./gsapDragCommit";
|
|
21
21
|
import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler";
|
|
22
22
|
import type { GsapDragCommitCallbacks } from "./gsapDragCommit";
|
|
23
|
+
import { getIframeGsap, queryIframeElement, selectorFromSelection } from "./gsapShared";
|
|
24
|
+
import { roundTo3 } from "../utils/rounding";
|
|
23
25
|
|
|
24
26
|
// ── Runtime reads ──────────────────────────────────────────────────────────
|
|
25
27
|
|
|
26
|
-
interface IframeGsap {
|
|
27
|
-
getProperty: (el: Element, prop: string) => number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
28
|
// fallow-ignore-next-line complexity
|
|
31
29
|
function readGsapPositionFromIframe(
|
|
32
30
|
iframe: HTMLIFrameElement | null,
|
|
33
31
|
elementSelector: string,
|
|
34
32
|
): { x: number; y: number } | null {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
let gsap: IframeGsap | undefined;
|
|
38
|
-
try {
|
|
39
|
-
gsap = (iframe.contentWindow as unknown as { gsap?: IframeGsap }).gsap;
|
|
40
|
-
} catch {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
if (!gsap?.getProperty) return null;
|
|
44
|
-
|
|
45
|
-
let doc: Document | null = null;
|
|
46
|
-
try {
|
|
47
|
-
doc = iframe.contentDocument;
|
|
48
|
-
} catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
if (!doc) return null;
|
|
33
|
+
const gsap = getIframeGsap(iframe);
|
|
34
|
+
if (!gsap) return null;
|
|
52
35
|
|
|
53
|
-
const element =
|
|
36
|
+
const element = queryIframeElement(iframe, elementSelector);
|
|
54
37
|
if (!element) return null;
|
|
55
38
|
|
|
56
39
|
const x = Number(gsap.getProperty(element, "x")) || 0;
|
|
@@ -99,12 +82,6 @@ function findGsapPositionAnimation(
|
|
|
99
82
|
|
|
100
83
|
// ── Selector resolution ────────────────────────────────────────────────────
|
|
101
84
|
|
|
102
|
-
function selectorForSelection(selection: DomEditSelection): string | null {
|
|
103
|
-
if (selection.id) return `#${selection.id}`;
|
|
104
|
-
if (selection.selector) return selection.selector;
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
85
|
// ── Property-group tween resolution ───────────────────────────────────────
|
|
109
86
|
|
|
110
87
|
/**
|
|
@@ -193,7 +170,7 @@ export async function tryGsapDragIntercept(
|
|
|
193
170
|
commitMutation: GsapDragCommitCallbacks["commitMutation"],
|
|
194
171
|
fetchFallbackAnimations?: () => Promise<GsapAnimation[]>,
|
|
195
172
|
): Promise<boolean> {
|
|
196
|
-
const selector =
|
|
173
|
+
const selector = selectorFromSelection(selection);
|
|
197
174
|
if (!selector) return false;
|
|
198
175
|
|
|
199
176
|
// Resolve the position-group tween, splitting legacy mixed tweens if needed.
|
|
@@ -284,15 +261,15 @@ export async function tryGsapResizeIntercept(
|
|
|
284
261
|
const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "5") || 5;
|
|
285
262
|
const ct = usePlayerStore.getState().currentTime;
|
|
286
263
|
const pct = elDuration > 0 ? Math.round(((ct - elStart) / elDuration) * 1000) / 10 : 0;
|
|
287
|
-
const sel =
|
|
264
|
+
const sel = selectorFromSelection(selection);
|
|
288
265
|
if (!sel) return false;
|
|
289
266
|
await commitMutation(
|
|
290
267
|
selection,
|
|
291
268
|
{
|
|
292
269
|
type: "add-with-keyframes",
|
|
293
270
|
targetSelector: sel,
|
|
294
|
-
position:
|
|
295
|
-
duration:
|
|
271
|
+
position: roundTo3(elStart),
|
|
272
|
+
duration: roundTo3(elDuration),
|
|
296
273
|
keyframes: [
|
|
297
274
|
{
|
|
298
275
|
percentage: Math.max(0, Math.min(100, pct)),
|
|
@@ -310,7 +287,7 @@ export async function tryGsapResizeIntercept(
|
|
|
310
287
|
if (activeKeyframePct != null) setActiveKeyframePct(null);
|
|
311
288
|
const coalesceKey = `gsap:resize:${anim.id}`;
|
|
312
289
|
|
|
313
|
-
const selector =
|
|
290
|
+
const selector = selectorFromSelection(selection);
|
|
314
291
|
const runtimeProps = selector ? readAllAnimatedProperties(iframe, selector, anim) : {};
|
|
315
292
|
|
|
316
293
|
let resizeProps: Record<string, number>;
|
|
@@ -320,7 +297,7 @@ export async function tryGsapResizeIntercept(
|
|
|
320
297
|
// saved by the draft system before it ran.
|
|
321
298
|
const origW = Number.parseFloat(el?.getAttribute("data-hf-studio-original-width") ?? "");
|
|
322
299
|
const cssW = Number.isFinite(origW) && origW > 0 ? origW : 200;
|
|
323
|
-
const newScale =
|
|
300
|
+
const newScale = roundTo3(size.width / cssW);
|
|
324
301
|
resizeProps = { scale: newScale };
|
|
325
302
|
} else {
|
|
326
303
|
resizeProps = {
|
|
@@ -395,8 +372,8 @@ export async function tryGsapResizeIntercept(
|
|
|
395
372
|
type: "replace-with-keyframes",
|
|
396
373
|
animationId: anim.id,
|
|
397
374
|
targetSelector: anim.targetSelector,
|
|
398
|
-
position:
|
|
399
|
-
duration:
|
|
375
|
+
position: roundTo3(newStart),
|
|
376
|
+
duration: roundTo3(newDuration),
|
|
400
377
|
keyframes: remapped,
|
|
401
378
|
},
|
|
402
379
|
{ label: `Resize (extended to ${ct.toFixed(2)}s)`, softReload: true, coalesceKey },
|
|
@@ -455,25 +432,14 @@ export async function tryGsapRotationIntercept(
|
|
|
455
432
|
}
|
|
456
433
|
if (!anim) return false;
|
|
457
434
|
|
|
458
|
-
const selector =
|
|
435
|
+
const selector = selectorFromSelection(selection);
|
|
459
436
|
if (!selector) return false;
|
|
460
437
|
|
|
461
438
|
let gsapRotation = 0;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
gsap?: { getProperty: (el: Element, prop: string) => number };
|
|
467
|
-
}
|
|
468
|
-
).gsap;
|
|
469
|
-
const doc = iframe.contentDocument;
|
|
470
|
-
const el = doc?.querySelector(selector);
|
|
471
|
-
if (gsap?.getProperty && el) {
|
|
472
|
-
gsapRotation = Number(gsap.getProperty(el, "rotation")) || 0;
|
|
473
|
-
}
|
|
474
|
-
} catch {
|
|
475
|
-
/* cross-origin guard */
|
|
476
|
-
}
|
|
439
|
+
const gsap = getIframeGsap(iframe);
|
|
440
|
+
const rotEl = gsap ? queryIframeElement(iframe, selector) : null;
|
|
441
|
+
if (gsap && rotEl) {
|
|
442
|
+
gsapRotation = Number(gsap.getProperty(rotEl, "rotation")) || 0;
|
|
477
443
|
}
|
|
478
444
|
|
|
479
445
|
const pct = computeCurrentPercentage(selection, anim);
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Used to discover dynamic keyframes that the AST parser can't resolve
|
|
4
4
|
* (loops, variables, computed selectors).
|
|
5
5
|
*/
|
|
6
|
+
import { parsePercentageKeyframes } from "./gsapShared";
|
|
7
|
+
import { roundTo3 } from "../utils/rounding";
|
|
6
8
|
|
|
7
9
|
interface RuntimeTween {
|
|
8
10
|
targets?: () => Element[];
|
|
@@ -66,33 +68,8 @@ export function readRuntimeKeyframes(
|
|
|
66
68
|
const vars = tween.vars;
|
|
67
69
|
if (!vars.keyframes || typeof vars.keyframes !== "object") continue;
|
|
68
70
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
let easeEach: string | undefined;
|
|
72
|
-
|
|
73
|
-
for (const [key, val] of Object.entries(kfObj)) {
|
|
74
|
-
if (key === "easeEach") {
|
|
75
|
-
if (typeof val === "string") easeEach = val;
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
const pctMatch = key.match(/^(\d+(?:\.\d+)?)%$/);
|
|
79
|
-
if (!pctMatch || !val || typeof val !== "object") continue;
|
|
80
|
-
const percentage = parseFloat(pctMatch[1]);
|
|
81
|
-
const properties: Record<string, number | string> = {};
|
|
82
|
-
for (const [pk, pv] of Object.entries(val as Record<string, unknown>)) {
|
|
83
|
-
if (pk === "ease") continue;
|
|
84
|
-
if (typeof pv === "number") properties[pk] = Math.round(pv * 1000) / 1000;
|
|
85
|
-
else if (typeof pv === "string") properties[pk] = pv;
|
|
86
|
-
}
|
|
87
|
-
if (Object.keys(properties).length > 0) {
|
|
88
|
-
result.push({ percentage, properties });
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (result.length > 0) {
|
|
93
|
-
result.sort((a, b) => a.percentage - b.percentage);
|
|
94
|
-
return { keyframes: result, easeEach };
|
|
95
|
-
}
|
|
71
|
+
const parsed = parsePercentageKeyframes(vars.keyframes as Record<string, unknown>);
|
|
72
|
+
if (parsed) return parsed;
|
|
96
73
|
}
|
|
97
74
|
return null;
|
|
98
75
|
}
|
|
@@ -133,38 +110,12 @@ export function scanAllRuntimeKeyframes(iframe: HTMLIFrameElement | null): Map<
|
|
|
133
110
|
const vars = tween.vars;
|
|
134
111
|
|
|
135
112
|
if (vars.keyframes && typeof vars.keyframes === "object") {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
percentage: number;
|
|
139
|
-
properties: Record<string, number | string>;
|
|
140
|
-
}> = [];
|
|
141
|
-
let easeEach: string | undefined;
|
|
142
|
-
|
|
143
|
-
for (const [key, val] of Object.entries(kfObj)) {
|
|
144
|
-
if (key === "easeEach") {
|
|
145
|
-
if (typeof val === "string") easeEach = val;
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
const pctMatch = key.match(/^(\d+(?:\.\d+)?)%$/);
|
|
149
|
-
if (!pctMatch || !val || typeof val !== "object") continue;
|
|
150
|
-
const percentage = parseFloat(pctMatch[1]);
|
|
151
|
-
const properties: Record<string, number | string> = {};
|
|
152
|
-
for (const [pk, pv] of Object.entries(val as Record<string, unknown>)) {
|
|
153
|
-
if (pk === "ease") continue;
|
|
154
|
-
if (typeof pv === "number") properties[pk] = Math.round(pv * 1000) / 1000;
|
|
155
|
-
else if (typeof pv === "string") properties[pk] = pv;
|
|
156
|
-
}
|
|
157
|
-
if (Object.keys(properties).length > 0) {
|
|
158
|
-
keyframes.push({ percentage, properties });
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (keyframes.length > 0) {
|
|
163
|
-
keyframes.sort((a, b) => a.percentage - b.percentage);
|
|
113
|
+
const parsed = parsePercentageKeyframes(vars.keyframes as Record<string, unknown>);
|
|
114
|
+
if (parsed) {
|
|
164
115
|
for (const target of tween.targets()) {
|
|
165
116
|
const id = (target as HTMLElement).id;
|
|
166
117
|
if (id && !result.has(id)) {
|
|
167
|
-
result.set(id,
|
|
118
|
+
result.set(id, parsed);
|
|
168
119
|
}
|
|
169
120
|
}
|
|
170
121
|
continue;
|
|
@@ -195,7 +146,7 @@ export function scanAllRuntimeKeyframes(iframe: HTMLIFrameElement | null): Map<
|
|
|
195
146
|
]);
|
|
196
147
|
for (const [k, v] of Object.entries(vars)) {
|
|
197
148
|
if (skip.has(k)) continue;
|
|
198
|
-
if (typeof v === "number") properties[k] =
|
|
149
|
+
if (typeof v === "number") properties[k] = roundTo3(v);
|
|
199
150
|
else if (typeof v === "string") properties[k] = v;
|
|
200
151
|
}
|
|
201
152
|
if (Object.keys(properties).length === 0) continue;
|