@hyperframes/studio 0.6.86 → 0.6.87
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-BA19FAPN.js +143 -0
- package/dist/assets/index-CGlIm_-E.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +159 -6
- package/src/components/StudioHeader.tsx +20 -7
- package/src/components/StudioPreviewArea.tsx +6 -1
- package/src/components/StudioRightPanel.tsx +13 -0
- package/src/components/StudioToast.tsx +47 -7
- package/src/components/TimelineToolbar.tsx +12 -122
- package/src/components/editor/AnimationCard.tsx +64 -10
- package/src/components/editor/ArcPathControls.tsx +131 -0
- package/src/components/editor/BorderRadiusEditor.tsx +209 -0
- package/src/components/editor/DomEditOverlay.tsx +70 -11
- package/src/components/editor/DopesheetStrip.tsx +141 -0
- package/src/components/editor/EaseCurveSection.tsx +82 -7
- package/src/components/editor/GestureTrailOverlay.tsx +132 -0
- package/src/components/editor/GsapAnimationSection.tsx +14 -1
- package/src/components/editor/KeyframeDiamond.tsx +27 -12
- package/src/components/editor/LayersPanel.tsx +14 -12
- package/src/components/editor/MotionPathOverlay.tsx +146 -0
- package/src/components/editor/PropertyPanel.tsx +196 -66
- package/src/components/editor/SourceEditor.tsx +0 -1
- package/src/components/editor/StaggerControls.tsx +61 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +13 -0
- package/src/components/editor/domEditOverlayGeometry.ts +2 -1
- package/src/components/editor/domEditing.test.ts +43 -0
- package/src/components/editor/domEditing.ts +2 -0
- package/src/components/editor/domEditingElement.ts +25 -2
- package/src/components/editor/domEditingLayers.test.ts +78 -0
- package/src/components/editor/domEditingLayers.ts +33 -13
- package/src/components/editor/domEditingTypes.ts +1 -0
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/editor/manualEdits.ts +3 -0
- package/src/components/editor/manualEditsDom.ts +23 -5
- package/src/components/editor/manualOffsetDrag.ts +59 -0
- package/src/components/editor/panelTokens.ts +10 -0
- package/src/components/editor/propertyPanelColor.tsx +2 -2
- package/src/components/editor/propertyPanelFill.tsx +1 -1
- package/src/components/editor/propertyPanelHelpers.ts +18 -2
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +38 -25
- package/src/components/editor/propertyPanelSections.tsx +4 -6
- package/src/components/editor/propertyPanelStyleSections.tsx +30 -8
- package/src/components/editor/useDomEditOverlayRects.ts +46 -2
- package/src/components/renders/RenderQueue.tsx +121 -100
- package/src/components/renders/RenderQueueItem.tsx +13 -13
- package/src/contexts/DomEditContext.tsx +12 -0
- package/src/contexts/FileManagerContext.tsx +3 -0
- package/src/contexts/StudioContext.tsx +0 -4
- package/src/hooks/gsapKeyframeCommit.ts +92 -0
- package/src/hooks/gsapRuntimeBridge.ts +147 -85
- package/src/hooks/gsapRuntimeKeyframes.ts +75 -24
- package/src/hooks/gsapRuntimePreview.ts +19 -0
- package/src/hooks/useAppHotkeys.ts +18 -0
- package/src/hooks/useAskAgentModal.ts +2 -4
- package/src/hooks/useDomEditCommits.ts +11 -17
- package/src/hooks/useDomEditSession.ts +47 -4
- package/src/hooks/useEnableKeyframes.ts +171 -0
- package/src/hooks/useFileManager.ts +7 -0
- package/src/hooks/useGestureRecording.ts +340 -0
- package/src/hooks/useGsapScriptCommits.ts +171 -35
- package/src/hooks/useGsapSelectionHandlers.ts +27 -8
- package/src/hooks/useGsapTweenCache.ts +169 -11
- package/src/hooks/useKeyframeKeyboard.ts +103 -0
- package/src/hooks/useStudioContextValue.ts +5 -4
- package/src/hooks/useStudioUrlState.ts +1 -2
- package/src/hooks/useTimelineEditing.ts +50 -3
- package/src/hooks/useToast.ts +6 -1
- package/src/player/components/ShortcutsPanel.tsx +40 -0
- package/src/player/components/TimelineClipDiamonds.tsx +3 -3
- package/src/player/components/TimelinePropertyRows.tsx +120 -0
- package/src/player/lib/timelineDOM.test.ts +55 -0
- package/src/player/lib/timelineDOM.ts +13 -0
- package/src/player/lib/timelineIframeHelpers.test.ts +51 -0
- package/src/player/lib/timelineIframeHelpers.ts +1 -0
- package/src/player/store/playerStore.ts +43 -0
- package/src/utils/audioBeatDetection.ts +58 -0
- package/src/utils/globalTimeCompiler.test.ts +169 -0
- package/src/utils/globalTimeCompiler.ts +77 -0
- package/src/utils/gsapSoftReload.ts +30 -10
- package/src/utils/keyframeSnapping.test.ts +74 -0
- package/src/utils/keyframeSnapping.ts +63 -0
- package/src/utils/rdpSimplify.ts +183 -0
- package/src/utils/sourcePatcher.ts +2 -0
- package/dist/assets/index-BT9VHgSy.js +0 -140
- package/dist/assets/index-DHcptK1_.css +0 -1
|
@@ -57,15 +57,15 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
57
57
|
onPointerLeave={() => setHovered(false)}
|
|
58
58
|
onClick={isComplete ? handleOpen : undefined}
|
|
59
59
|
className={[
|
|
60
|
-
"px-3 py-2.5 border-b border-
|
|
61
|
-
isComplete ? "cursor-pointer hover:bg-
|
|
60
|
+
"px-3 py-2.5 border-b border-panel-border last:border-0 transition-colors duration-150",
|
|
61
|
+
isComplete ? "cursor-pointer hover:bg-panel-hover/30" : "",
|
|
62
62
|
]
|
|
63
63
|
.filter(Boolean)
|
|
64
64
|
.join(" ")}
|
|
65
65
|
>
|
|
66
66
|
<div className="flex items-center gap-2.5">
|
|
67
67
|
{/* Thumbnail — static frame; swaps to live video on hover */}
|
|
68
|
-
<div className="w-20 h-[45px] rounded overflow-hidden bg-
|
|
68
|
+
<div className="w-20 h-[45px] rounded-md overflow-hidden bg-panel-input flex-shrink-0 relative">
|
|
69
69
|
{isComplete && (
|
|
70
70
|
<>
|
|
71
71
|
{/* Live video — visible on hover */}
|
|
@@ -90,7 +90,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
90
90
|
)}
|
|
91
91
|
{job.status === "rendering" && (
|
|
92
92
|
<div className="w-full h-full flex items-center justify-center">
|
|
93
|
-
<div className="w-2 h-2 rounded-full bg-
|
|
93
|
+
<div className="w-2 h-2 rounded-full bg-panel-accent animate-pulse" />
|
|
94
94
|
</div>
|
|
95
95
|
)}
|
|
96
96
|
{job.status === "failed" && (
|
|
@@ -108,11 +108,11 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
108
108
|
{/* Info */}
|
|
109
109
|
<div className="flex-1 min-w-0">
|
|
110
110
|
<div className="flex items-center gap-1.5">
|
|
111
|
-
<span className="text-[11px] font-medium text-
|
|
111
|
+
<span className="text-[11px] font-medium text-panel-text-2 truncate">
|
|
112
112
|
{job.filename}
|
|
113
113
|
</span>
|
|
114
114
|
{job.durationMs && (
|
|
115
|
-
<span className="text-[9px] text-
|
|
115
|
+
<span className="text-[9px] text-panel-text-5 flex-shrink-0">
|
|
116
116
|
{formatDuration(job.durationMs)}
|
|
117
117
|
</span>
|
|
118
118
|
)}
|
|
@@ -121,12 +121,12 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
121
121
|
{job.status === "rendering" && (
|
|
122
122
|
<div className="mt-1">
|
|
123
123
|
<div className="flex items-center justify-between mb-0.5">
|
|
124
|
-
<span className="text-[9px] text-
|
|
125
|
-
<span className="text-[9px] font-mono text-
|
|
124
|
+
<span className="text-[9px] text-panel-text-4">{job.stage || "Rendering"}</span>
|
|
125
|
+
<span className="text-[9px] font-mono text-panel-accent">{job.progress}%</span>
|
|
126
126
|
</div>
|
|
127
|
-
<div className="w-full h-1 bg-
|
|
127
|
+
<div className="w-full h-1 bg-panel-border rounded-full overflow-hidden">
|
|
128
128
|
<div
|
|
129
|
-
className="h-full bg-
|
|
129
|
+
className="h-full bg-panel-accent rounded-full transition-all duration-300"
|
|
130
130
|
style={{ width: `${job.progress}%` }}
|
|
131
131
|
/>
|
|
132
132
|
</div>
|
|
@@ -138,7 +138,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
138
138
|
)}
|
|
139
139
|
|
|
140
140
|
{job.status !== "rendering" && (
|
|
141
|
-
<span className="text-[9px] text-
|
|
141
|
+
<span className="text-[9px] text-panel-text-5">{formatTimeAgo(job.createdAt)}</span>
|
|
142
142
|
)}
|
|
143
143
|
</div>
|
|
144
144
|
|
|
@@ -148,7 +148,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
148
148
|
{isComplete && (
|
|
149
149
|
<button
|
|
150
150
|
onClick={handleDownload}
|
|
151
|
-
className="p-1 rounded text-
|
|
151
|
+
className="p-1 rounded text-panel-text-4 hover:text-panel-accent transition-colors"
|
|
152
152
|
title="Download"
|
|
153
153
|
>
|
|
154
154
|
<svg
|
|
@@ -172,7 +172,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
|
|
|
172
172
|
e.stopPropagation();
|
|
173
173
|
onDelete();
|
|
174
174
|
}}
|
|
175
|
-
className="p-1 rounded text-
|
|
175
|
+
className="p-1 rounded text-panel-text-4 hover:text-red-400 transition-colors"
|
|
176
176
|
title="Remove"
|
|
177
177
|
>
|
|
178
178
|
<svg
|
|
@@ -67,13 +67,17 @@ export function DomEditProvider({
|
|
|
67
67
|
handleGsapAddFromProperty,
|
|
68
68
|
handleGsapRemoveFromProperty,
|
|
69
69
|
handleGsapAddKeyframe,
|
|
70
|
+
handleGsapAddKeyframeBatch,
|
|
70
71
|
handleGsapRemoveKeyframe,
|
|
71
72
|
handleGsapConvertToKeyframes,
|
|
72
73
|
handleGsapRemoveAllKeyframes,
|
|
73
74
|
handleResetSelectedElementKeyframes,
|
|
74
75
|
commitAnimatedProperty,
|
|
76
|
+
handleSetArcPath,
|
|
77
|
+
handleUpdateArcSegment,
|
|
75
78
|
invalidateGsapCache,
|
|
76
79
|
previewIframeRef,
|
|
80
|
+
commitMutation,
|
|
77
81
|
},
|
|
78
82
|
children,
|
|
79
83
|
}: {
|
|
@@ -136,13 +140,17 @@ export function DomEditProvider({
|
|
|
136
140
|
handleGsapAddFromProperty,
|
|
137
141
|
handleGsapRemoveFromProperty,
|
|
138
142
|
handleGsapAddKeyframe,
|
|
143
|
+
handleGsapAddKeyframeBatch,
|
|
139
144
|
handleGsapRemoveKeyframe,
|
|
140
145
|
handleGsapConvertToKeyframes,
|
|
141
146
|
handleGsapRemoveAllKeyframes,
|
|
142
147
|
handleResetSelectedElementKeyframes,
|
|
143
148
|
commitAnimatedProperty,
|
|
149
|
+
handleSetArcPath,
|
|
150
|
+
handleUpdateArcSegment,
|
|
144
151
|
invalidateGsapCache,
|
|
145
152
|
previewIframeRef,
|
|
153
|
+
commitMutation,
|
|
146
154
|
}),
|
|
147
155
|
[
|
|
148
156
|
domEditSelection,
|
|
@@ -199,13 +207,17 @@ export function DomEditProvider({
|
|
|
199
207
|
handleGsapAddFromProperty,
|
|
200
208
|
handleGsapRemoveFromProperty,
|
|
201
209
|
handleGsapAddKeyframe,
|
|
210
|
+
handleGsapAddKeyframeBatch,
|
|
202
211
|
handleGsapRemoveKeyframe,
|
|
203
212
|
handleGsapConvertToKeyframes,
|
|
204
213
|
handleGsapRemoveAllKeyframes,
|
|
205
214
|
handleResetSelectedElementKeyframes,
|
|
206
215
|
commitAnimatedProperty,
|
|
216
|
+
handleSetArcPath,
|
|
217
|
+
handleUpdateArcSegment,
|
|
207
218
|
invalidateGsapCache,
|
|
208
219
|
previewIframeRef,
|
|
220
|
+
commitMutation,
|
|
209
221
|
],
|
|
210
222
|
);
|
|
211
223
|
return <DomEditContext value={stable}>{children}</DomEditContext>;
|
|
@@ -26,6 +26,7 @@ export function FileManagerProvider({
|
|
|
26
26
|
readProjectFile,
|
|
27
27
|
writeProjectFile,
|
|
28
28
|
readOptionalProjectFile,
|
|
29
|
+
updateEditingFileContent,
|
|
29
30
|
revealSourceOffset,
|
|
30
31
|
openSourceForSelection,
|
|
31
32
|
handleFileSelect,
|
|
@@ -64,6 +65,7 @@ export function FileManagerProvider({
|
|
|
64
65
|
readProjectFile,
|
|
65
66
|
writeProjectFile,
|
|
66
67
|
readOptionalProjectFile,
|
|
68
|
+
updateEditingFileContent,
|
|
67
69
|
revealSourceOffset,
|
|
68
70
|
openSourceForSelection,
|
|
69
71
|
handleFileSelect,
|
|
@@ -96,6 +98,7 @@ export function FileManagerProvider({
|
|
|
96
98
|
readProjectFile,
|
|
97
99
|
writeProjectFile,
|
|
98
100
|
readOptionalProjectFile,
|
|
101
|
+
updateEditingFileContent,
|
|
99
102
|
revealSourceOffset,
|
|
100
103
|
openSourceForSelection,
|
|
101
104
|
handleFileSelect,
|
|
@@ -12,7 +12,6 @@ export interface StudioContextValue {
|
|
|
12
12
|
compositionLoading: boolean;
|
|
13
13
|
refreshKey: number;
|
|
14
14
|
setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
|
|
15
|
-
currentTime: number;
|
|
16
15
|
timelineElements: TimelineElement[];
|
|
17
16
|
isPlaying: boolean;
|
|
18
17
|
editHistory: {
|
|
@@ -63,7 +62,6 @@ export function StudioProvider({
|
|
|
63
62
|
compositionLoading,
|
|
64
63
|
refreshKey,
|
|
65
64
|
setRefreshKey,
|
|
66
|
-
currentTime,
|
|
67
65
|
timelineElements,
|
|
68
66
|
isPlaying,
|
|
69
67
|
editHistory,
|
|
@@ -89,7 +87,6 @@ export function StudioProvider({
|
|
|
89
87
|
compositionLoading,
|
|
90
88
|
refreshKey,
|
|
91
89
|
setRefreshKey,
|
|
92
|
-
currentTime,
|
|
93
90
|
timelineElements,
|
|
94
91
|
isPlaying,
|
|
95
92
|
editHistory,
|
|
@@ -112,7 +109,6 @@ export function StudioProvider({
|
|
|
112
109
|
captionEditMode,
|
|
113
110
|
compositionLoading,
|
|
114
111
|
refreshKey,
|
|
115
|
-
currentTime,
|
|
116
112
|
isPlaying,
|
|
117
113
|
compositionDimensions,
|
|
118
114
|
timelineVisible,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
2
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
|
+
import { absoluteToPercentageForAnimation, findTweenAtTime } from "../utils/globalTimeCompiler";
|
|
4
|
+
|
|
5
|
+
const PROPERTY_DEFAULTS: Record<string, number> = {
|
|
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
|
+
};
|
|
16
|
+
|
|
17
|
+
type CommitFn = (
|
|
18
|
+
selection: DomEditSelection,
|
|
19
|
+
mutation: Record<string, unknown>,
|
|
20
|
+
options: {
|
|
21
|
+
label: string;
|
|
22
|
+
coalesceKey?: string;
|
|
23
|
+
softReload?: boolean;
|
|
24
|
+
skipReload?: boolean;
|
|
25
|
+
},
|
|
26
|
+
) => Promise<void>;
|
|
27
|
+
|
|
28
|
+
export async function commitKeyframeAtTimeImpl(
|
|
29
|
+
selection: DomEditSelection,
|
|
30
|
+
absoluteTime: number,
|
|
31
|
+
animations: GsapAnimation[],
|
|
32
|
+
properties: Record<string, number | string>,
|
|
33
|
+
commitMutation: CommitFn,
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
const selector = selection.id ? `#${selection.id}` : selection.selector;
|
|
36
|
+
if (!selector) return;
|
|
37
|
+
|
|
38
|
+
const tween = findTweenAtTime(absoluteTime, animations, selector);
|
|
39
|
+
if (tween) {
|
|
40
|
+
const pct = absoluteToPercentageForAnimation(absoluteTime, tween);
|
|
41
|
+
if (pct === null) return;
|
|
42
|
+
|
|
43
|
+
const hasExplicitKeyframes = !!tween.keyframes && tween.keyframes.keyframes.length > 0;
|
|
44
|
+
if (!hasExplicitKeyframes) {
|
|
45
|
+
await commitMutation(
|
|
46
|
+
selection,
|
|
47
|
+
{ type: "convert-to-keyframes", animationId: tween.id },
|
|
48
|
+
{ label: "Convert to keyframes", skipReload: true },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const backfillDefaults: Record<string, number | string> = {};
|
|
53
|
+
for (const key of Object.keys(properties)) {
|
|
54
|
+
backfillDefaults[key] = PROPERTY_DEFAULTS[key] ?? 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await commitMutation(
|
|
58
|
+
selection,
|
|
59
|
+
{
|
|
60
|
+
type: "add-keyframe",
|
|
61
|
+
animationId: tween.id,
|
|
62
|
+
percentage: pct,
|
|
63
|
+
properties,
|
|
64
|
+
backfillDefaults,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: `Add keyframe at ${Math.round(absoluteTime * 100) / 100}s`,
|
|
68
|
+
coalesceKey: `keyframe:${tween.id}:${pct}`,
|
|
69
|
+
softReload: true,
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
const defaultDuration = 0.5;
|
|
74
|
+
await commitMutation(
|
|
75
|
+
selection,
|
|
76
|
+
{
|
|
77
|
+
type: "add-with-keyframes" as const,
|
|
78
|
+
targetSelector: selector,
|
|
79
|
+
position: absoluteTime,
|
|
80
|
+
duration: defaultDuration,
|
|
81
|
+
keyframes: [
|
|
82
|
+
{ percentage: 0, properties },
|
|
83
|
+
{ percentage: 100, properties },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: `New animation at ${Math.round(absoluteTime * 100) / 100}s`,
|
|
88
|
+
softReload: true,
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
12
12
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
15
15
|
import { readRuntimeKeyframes, scanAllRuntimeKeyframes } from "./gsapRuntimeKeyframes";
|
|
16
|
+
import {
|
|
17
|
+
absoluteToPercentage,
|
|
18
|
+
resolveTweenStart,
|
|
19
|
+
resolveTweenDuration,
|
|
20
|
+
} from "../utils/globalTimeCompiler";
|
|
16
21
|
|
|
17
22
|
// ── Runtime reads ──────────────────────────────────────────────────────────
|
|
18
23
|
|
|
@@ -91,10 +96,17 @@ function selectorForSelection(selection: DomEditSelection): string | null {
|
|
|
91
96
|
|
|
92
97
|
// ── Percentage computation ─────────────────────────────────────────────────
|
|
93
98
|
|
|
94
|
-
function computeCurrentPercentage(selection: DomEditSelection): number {
|
|
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
|
+
}
|
|
95
108
|
const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
|
|
96
109
|
const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "1") || 1;
|
|
97
|
-
const currentTime = usePlayerStore.getState().currentTime;
|
|
98
110
|
return elDuration > 0
|
|
99
111
|
? Math.max(0, Math.min(100, Math.round(((currentTime - elStart) / elDuration) * 1000) / 10))
|
|
100
112
|
: 0;
|
|
@@ -190,6 +202,10 @@ export async function tryGsapDragIntercept(
|
|
|
190
202
|
const selector = selectorForSelection(selection);
|
|
191
203
|
if (!selector) return false;
|
|
192
204
|
|
|
205
|
+
// Keyframe writes at 0%/100% when outside the tween range. Acceptable
|
|
206
|
+
// trade-off — CSS path must NEVER touch GSAP-targeted elements because
|
|
207
|
+
// changing the CSS offset corrupts all existing keyframes (baked mismatch).
|
|
208
|
+
|
|
193
209
|
const gsapPos = readGsapPositionFromIframe(iframe, selector);
|
|
194
210
|
if (!gsapPos) return false;
|
|
195
211
|
|
|
@@ -232,50 +248,155 @@ async function commitGsapPositionFromDrag(
|
|
|
232
248
|
const rad = (-rotDeg * Math.PI) / 180;
|
|
233
249
|
const cos = Math.cos(rad);
|
|
234
250
|
const sin = Math.sin(rad);
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const
|
|
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
|
+
};
|
|
240
274
|
|
|
241
275
|
if (anim.keyframes) {
|
|
242
276
|
const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
|
|
243
277
|
const effectiveAnim = newId ? { ...anim, id: newId } : anim;
|
|
244
278
|
const runtimeProps = readAllAnimatedProperties(iframe, selector, anim);
|
|
245
|
-
|
|
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(
|
|
246
310
|
selection,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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 },
|
|
251
317
|
);
|
|
252
|
-
} else if (anim.method === "from") {
|
|
253
|
-
await commitFromPosition(selection, anim, studioOffset, callbacks, clearOffset);
|
|
254
|
-
} else if (anim.method === "fromTo") {
|
|
255
|
-
await commitFromToPosition(selection, anim, studioOffset, callbacks, clearOffset);
|
|
256
318
|
} else {
|
|
257
|
-
// Flat to()/set() — convert to keyframes
|
|
258
|
-
// is captured at the current seek time, not just the tween endpoint.
|
|
319
|
+
// Flat to()/set() — convert to keyframes then add at current percentage.
|
|
259
320
|
const runtimeProps = readAllAnimatedProperties(iframe, selector, anim);
|
|
260
321
|
await commitFlatViaKeyframes(
|
|
261
322
|
selection,
|
|
262
323
|
anim,
|
|
263
324
|
{ ...runtimeProps, x: newX, y: newY },
|
|
264
325
|
callbacks,
|
|
265
|
-
|
|
326
|
+
restoreOffset,
|
|
266
327
|
);
|
|
267
328
|
}
|
|
268
329
|
}
|
|
269
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
|
+
|
|
270
391
|
// fallow-ignore-next-line complexity
|
|
271
392
|
async function commitKeyframedPosition(
|
|
272
393
|
selection: DomEditSelection,
|
|
273
394
|
anim: GsapAnimation,
|
|
274
395
|
properties: Record<string, number>,
|
|
275
396
|
callbacks: GsapDragCommitCallbacks,
|
|
276
|
-
beforeReload
|
|
397
|
+
beforeReload?: () => void,
|
|
277
398
|
): Promise<void> {
|
|
278
|
-
const pct = computeCurrentPercentage(selection);
|
|
399
|
+
const pct = computeCurrentPercentage(selection, anim);
|
|
279
400
|
|
|
280
401
|
await callbacks.commitMutation(
|
|
281
402
|
selection,
|
|
@@ -300,7 +421,7 @@ async function commitFlatViaKeyframes(
|
|
|
300
421
|
anim: GsapAnimation,
|
|
301
422
|
properties: Record<string, number>,
|
|
302
423
|
callbacks: GsapDragCommitCallbacks,
|
|
303
|
-
beforeReload
|
|
424
|
+
beforeReload?: () => void,
|
|
304
425
|
): Promise<void> {
|
|
305
426
|
await callbacks.commitMutation(
|
|
306
427
|
selection,
|
|
@@ -308,7 +429,7 @@ async function commitFlatViaKeyframes(
|
|
|
308
429
|
{ label: "Convert to keyframes for drag", skipReload: true },
|
|
309
430
|
);
|
|
310
431
|
|
|
311
|
-
const pct = computeCurrentPercentage(selection);
|
|
432
|
+
const pct = computeCurrentPercentage(selection, anim);
|
|
312
433
|
|
|
313
434
|
await callbacks.commitMutation(
|
|
314
435
|
selection,
|
|
@@ -322,65 +443,6 @@ async function commitFlatViaKeyframes(
|
|
|
322
443
|
);
|
|
323
444
|
}
|
|
324
445
|
|
|
325
|
-
async function commitFromPosition(
|
|
326
|
-
selection: DomEditSelection,
|
|
327
|
-
anim: GsapAnimation,
|
|
328
|
-
delta: { x: number; y: number },
|
|
329
|
-
callbacks: GsapDragCommitCallbacks,
|
|
330
|
-
beforeReload: () => void,
|
|
331
|
-
): Promise<void> {
|
|
332
|
-
const fromX = Math.round(Number(anim.properties.x ?? 0) + delta.x);
|
|
333
|
-
const fromY = Math.round(Number(anim.properties.y ?? 0) + delta.y);
|
|
334
|
-
|
|
335
|
-
await callbacks.commitMutation(
|
|
336
|
-
selection,
|
|
337
|
-
{ type: "update-property", animationId: anim.id, property: "x", value: fromX },
|
|
338
|
-
{ label: "Move layer (GSAP from x)", skipReload: true },
|
|
339
|
-
);
|
|
340
|
-
await callbacks.commitMutation(
|
|
341
|
-
selection,
|
|
342
|
-
{ type: "update-property", animationId: anim.id, property: "y", value: fromY },
|
|
343
|
-
{ label: "Move layer (GSAP from y)", softReload: true, beforeReload },
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// fallow-ignore-next-line complexity
|
|
348
|
-
async function commitFromToPosition(
|
|
349
|
-
selection: DomEditSelection,
|
|
350
|
-
anim: GsapAnimation,
|
|
351
|
-
delta: { x: number; y: number },
|
|
352
|
-
callbacks: GsapDragCommitCallbacks,
|
|
353
|
-
beforeReload: () => void,
|
|
354
|
-
): Promise<void> {
|
|
355
|
-
if (anim.fromProperties) {
|
|
356
|
-
const fromX = Math.round(Number(anim.fromProperties.x ?? 0) + delta.x);
|
|
357
|
-
const fromY = Math.round(Number(anim.fromProperties.y ?? 0) + delta.y);
|
|
358
|
-
await callbacks.commitMutation(
|
|
359
|
-
selection,
|
|
360
|
-
{ type: "update-from-property", animationId: anim.id, property: "x", value: fromX },
|
|
361
|
-
{ label: "Move (GSAP from x)", skipReload: true },
|
|
362
|
-
);
|
|
363
|
-
await callbacks.commitMutation(
|
|
364
|
-
selection,
|
|
365
|
-
{ type: "update-from-property", animationId: anim.id, property: "y", value: fromY },
|
|
366
|
-
{ label: "Move (GSAP from y)", skipReload: true },
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const toX = Math.round(Number(anim.properties.x ?? 0) + delta.x);
|
|
371
|
-
const toY = Math.round(Number(anim.properties.y ?? 0) + delta.y);
|
|
372
|
-
await callbacks.commitMutation(
|
|
373
|
-
selection,
|
|
374
|
-
{ type: "update-property", animationId: anim.id, property: "x", value: toX },
|
|
375
|
-
{ label: "Move (GSAP to x)", skipReload: true },
|
|
376
|
-
);
|
|
377
|
-
await callbacks.commitMutation(
|
|
378
|
-
selection,
|
|
379
|
-
{ type: "update-property", animationId: anim.id, property: "y", value: toY },
|
|
380
|
-
{ label: "Move (GSAP to y)", softReload: true, beforeReload },
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
446
|
// ── Runtime property reader ───────────────────────────────────────────────
|
|
385
447
|
|
|
386
448
|
export function readGsapProperty(
|
|
@@ -461,7 +523,7 @@ export async function tryGsapResizeIntercept(
|
|
|
461
523
|
}
|
|
462
524
|
if (!anim) return false;
|
|
463
525
|
|
|
464
|
-
const pct = computeCurrentPercentage(selection);
|
|
526
|
+
const pct = computeCurrentPercentage(selection, anim);
|
|
465
527
|
|
|
466
528
|
if (anim.hasUnresolvedKeyframes || anim.hasUnresolvedSelector) {
|
|
467
529
|
const newId = await materializeIfDynamic(anim, iframe, commitMutation, selection);
|
|
@@ -545,7 +607,7 @@ export async function tryGsapRotationIntercept(
|
|
|
545
607
|
}
|
|
546
608
|
}
|
|
547
609
|
|
|
548
|
-
const pct = computeCurrentPercentage(selection);
|
|
610
|
+
const pct = computeCurrentPercentage(selection, anim);
|
|
549
611
|
const newRotation = Math.round(gsapRotation + angle);
|
|
550
612
|
|
|
551
613
|
if (anim.hasUnresolvedKeyframes || anim.hasUnresolvedSelector) {
|