@hyperframes/studio 0.6.7 → 0.6.8
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-BSe0Kibk.js +115 -0
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/App.tsx +5 -10
- package/src/components/StudioLeftSidebar.tsx +16 -2
- package/src/components/StudioRightPanel.tsx +15 -2
- package/src/components/editor/MotionPanel.tsx +8 -8
- package/src/components/editor/SourceEditor.tsx +14 -0
- package/src/components/editor/manualEdits.ts +2 -0
- package/src/components/editor/manualEditsDom.ts +56 -0
- package/src/components/editor/studioMotion.ts +96 -0
- package/src/components/editor/studioMotionOps.test.ts +445 -0
- package/src/components/editor/studioMotionOps.ts +78 -4
- package/src/components/renders/RenderQueue.tsx +20 -6
- package/src/components/renders/renderSettings.ts +38 -0
- package/src/components/renders/useRenderQueue.ts +11 -1
- package/src/components/sidebar/CompositionsTab.tsx +43 -1
- package/src/components/sidebar/LeftSidebar.tsx +6 -0
- package/src/contexts/FileManagerContext.tsx +6 -0
- package/src/hooks/useDomEditCommits.ts +45 -33
- package/src/hooks/useDomEditSession.ts +26 -25
- package/src/hooks/useFileManager.ts +42 -0
- package/src/hooks/useManifestPersistence.ts +40 -218
- package/src/hooks/usePreviewInteraction.ts +7 -0
- package/src/player/components/Player.tsx +12 -3
- package/src/player/components/PlayerControls.tsx +29 -2
- package/src/player/components/useTimelineRangeSelection.ts +30 -3
- package/src/utils/sourcePatcher.test.ts +285 -0
- package/src/utils/sourcePatcher.ts +26 -6
- package/dist/assets/index-Yvtxngdi.js +0 -116
package/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>HyperFrames Studio</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BSe0Kibk.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-Ckqo37Co.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"@phosphor-icons/react": "^2.1.10",
|
|
33
33
|
"codemirror": "^6.0.1",
|
|
34
34
|
"motion": "^12.38.0",
|
|
35
|
-
"@hyperframes/core": "0.6.
|
|
36
|
-
"@hyperframes/player": "0.6.
|
|
35
|
+
"@hyperframes/core": "0.6.8",
|
|
36
|
+
"@hyperframes/player": "0.6.8"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/react": "19",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"vite": "^6.4.2",
|
|
48
48
|
"vitest": "^3.2.4",
|
|
49
49
|
"zustand": "^5.0.0",
|
|
50
|
-
"@hyperframes/producer": "0.6.
|
|
50
|
+
"@hyperframes/producer": "0.6.8"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useMemo, useEffect } from "react";
|
|
2
|
-
import type { LeftSidebarHandle } from "./components/sidebar/LeftSidebar";
|
|
2
|
+
import type { LeftSidebarHandle, SidebarTab } from "./components/sidebar/LeftSidebar";
|
|
3
3
|
import { useRenderQueue } from "./components/renders/useRenderQueue";
|
|
4
4
|
import { usePlayerStore } from "./player";
|
|
5
5
|
import { LintModal } from "./components/LintModal";
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
26
26
|
STUDIO_MOTION_PANEL_ENABLED,
|
|
27
27
|
} from "./components/editor/manualEditingAvailability";
|
|
28
|
-
import {
|
|
28
|
+
import { readStudioMotionFromElement } from "./components/editor/studioMotion";
|
|
29
29
|
import type { DomEditSelection } from "./components/editor/domEditing";
|
|
30
30
|
import { AskAgentModal } from "./components/AskAgentModal";
|
|
31
31
|
import { StudioGlobalDragOverlay } from "./components/StudioGlobalDragOverlay";
|
|
@@ -200,9 +200,6 @@ export function StudioApp() {
|
|
|
200
200
|
showToast,
|
|
201
201
|
refreshPreviewDocumentVersion,
|
|
202
202
|
queueDomEditSave: manifestPersistence.queueDomEditSave,
|
|
203
|
-
commitStudioMotionManifestOptimistically:
|
|
204
|
-
manifestPersistence.commitStudioMotionManifestOptimistically,
|
|
205
|
-
applyCurrentStudioMotionToPreview: manifestPersistence.applyCurrentStudioMotionToPreview,
|
|
206
203
|
readProjectFile: fileManager.readProjectFile,
|
|
207
204
|
writeProjectFile: fileManager.writeProjectFile,
|
|
208
205
|
domEditSaveTimestampRef,
|
|
@@ -215,10 +212,11 @@ export function StudioApp() {
|
|
|
215
212
|
refreshKey,
|
|
216
213
|
rightPanelTab: panelLayout.rightPanelTab,
|
|
217
214
|
applyStudioManualEditsToPreviewRef: manifestPersistence.applyStudioManualEditsToPreviewRef,
|
|
218
|
-
applyStudioMotionToPreviewRef: manifestPersistence.applyStudioMotionToPreviewRef,
|
|
219
215
|
syncPreviewHistoryHotkey: appHotkeys.syncPreviewHistoryHotkey,
|
|
220
216
|
reloadPreview,
|
|
221
217
|
setRefreshKey,
|
|
218
|
+
openSourceForSelection: fileManager.openSourceForSelection,
|
|
219
|
+
selectSidebarTab: (tab: SidebarTab) => leftSidebarRef.current?.selectTab(tab),
|
|
222
220
|
});
|
|
223
221
|
|
|
224
222
|
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
|
|
@@ -292,10 +290,7 @@ export function StudioApp() {
|
|
|
292
290
|
|
|
293
291
|
const selectedStudioMotion =
|
|
294
292
|
STUDIO_INSPECTOR_PANELS_ENABLED && domEditSession.domEditSelection
|
|
295
|
-
?
|
|
296
|
-
manifestPersistence.studioMotionManifestRef.current,
|
|
297
|
-
domEditSession.domEditSelection,
|
|
298
|
-
)
|
|
293
|
+
? readStudioMotionFromElement(domEditSession.domEditSelection.element)
|
|
299
294
|
: null;
|
|
300
295
|
const layersPanelActive =
|
|
301
296
|
STUDIO_INSPECTOR_PANELS_ENABLED && panelLayout.rightPanelTab === "layers";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useCallback, type RefObject } from "react";
|
|
2
2
|
import { SourceEditor } from "./editor/SourceEditor";
|
|
3
3
|
import { LeftSidebar, type LeftSidebarHandle } from "./sidebar/LeftSidebar";
|
|
4
4
|
import { MediaPreview } from "./MediaPreview";
|
|
@@ -6,6 +6,7 @@ import { isMediaFile } from "../utils/mediaTypes";
|
|
|
6
6
|
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
|
|
7
7
|
import { useStudioContext } from "../contexts/StudioContext";
|
|
8
8
|
import { useFileManagerContext } from "../contexts/FileManagerContext";
|
|
9
|
+
import { getPersistedRenderSettings } from "./renders/renderSettings";
|
|
9
10
|
|
|
10
11
|
export interface StudioLeftSidebarProps {
|
|
11
12
|
leftSidebarRef: RefObject<LeftSidebarHandle | null>;
|
|
@@ -28,12 +29,13 @@ export function StudioLeftSidebar({
|
|
|
28
29
|
handlePanelResizeMove,
|
|
29
30
|
handlePanelResizeEnd,
|
|
30
31
|
} = usePanelLayoutContext();
|
|
31
|
-
const { projectId } = useStudioContext();
|
|
32
|
+
const { projectId, renderQueue, waitForPendingDomEditSaves } = useStudioContext();
|
|
32
33
|
const {
|
|
33
34
|
compositions,
|
|
34
35
|
assets,
|
|
35
36
|
editingFile,
|
|
36
37
|
fileTree,
|
|
38
|
+
revealSourceOffset,
|
|
37
39
|
handleFileSelect,
|
|
38
40
|
handleCreateFile,
|
|
39
41
|
handleCreateFolder,
|
|
@@ -45,6 +47,15 @@ export function StudioLeftSidebar({
|
|
|
45
47
|
handleContentChange,
|
|
46
48
|
} = useFileManagerContext();
|
|
47
49
|
|
|
50
|
+
const handleRenderComposition = useCallback(
|
|
51
|
+
async (comp: string) => {
|
|
52
|
+
await waitForPendingDomEditSaves();
|
|
53
|
+
const { format, quality, fps } = getPersistedRenderSettings();
|
|
54
|
+
await renderQueue.startRender({ composition: comp, format, quality, fps });
|
|
55
|
+
},
|
|
56
|
+
[renderQueue, waitForPendingDomEditSaves],
|
|
57
|
+
);
|
|
58
|
+
|
|
48
59
|
if (leftCollapsed) {
|
|
49
60
|
return (
|
|
50
61
|
<div className="flex w-10 flex-shrink-0 flex-col items-center border-r border-neutral-800/50 bg-neutral-950 pt-1">
|
|
@@ -103,10 +114,13 @@ export function StudioLeftSidebar({
|
|
|
103
114
|
content={editingFile.content ?? ""}
|
|
104
115
|
filePath={editingFile.path}
|
|
105
116
|
onChange={handleContentChange}
|
|
117
|
+
revealOffset={revealSourceOffset}
|
|
106
118
|
/>
|
|
107
119
|
)
|
|
108
120
|
) : undefined
|
|
109
121
|
}
|
|
122
|
+
onRenderComposition={handleRenderComposition}
|
|
123
|
+
isRendering={renderQueue.isRendering}
|
|
110
124
|
onLint={onLint}
|
|
111
125
|
linting={linting}
|
|
112
126
|
onToggleCollapse={toggleLeftSidebar}
|
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
STUDIO_INSPECTOR_PANELS_ENABLED,
|
|
10
10
|
STUDIO_MOTION_PANEL_ENABLED,
|
|
11
11
|
} from "./editor/manualEditingAvailability";
|
|
12
|
+
|
|
13
|
+
/** Motion data without targeting metadata. */
|
|
14
|
+
type StudioMotionData = Omit<StudioGsapMotion, "kind" | "target" | "updatedAt">;
|
|
12
15
|
import { useCallback } from "react";
|
|
13
16
|
import { resolveDomEditSelection, type DomEditLayerItem } from "./editor/domEditing";
|
|
14
17
|
import { useStudioContext } from "../contexts/StudioContext";
|
|
@@ -17,7 +20,7 @@ import { useFileManagerContext } from "../contexts/FileManagerContext";
|
|
|
17
20
|
import { useDomEditContext } from "../contexts/DomEditContext";
|
|
18
21
|
|
|
19
22
|
export interface StudioRightPanelProps {
|
|
20
|
-
selectedStudioMotion:
|
|
23
|
+
selectedStudioMotion: StudioMotionData | null;
|
|
21
24
|
designPanelActive: boolean;
|
|
22
25
|
motionPanelActive: boolean;
|
|
23
26
|
}
|
|
@@ -195,7 +198,17 @@ export function StudioRightPanel({
|
|
|
195
198
|
onClearCompleted={renderQueue.clearCompleted}
|
|
196
199
|
onStartRender={async (format, quality, resolution, fps) => {
|
|
197
200
|
await waitForPendingDomEditSaves();
|
|
198
|
-
|
|
201
|
+
const composition =
|
|
202
|
+
activeCompPath && activeCompPath !== "index.html"
|
|
203
|
+
? activeCompPath
|
|
204
|
+
: undefined;
|
|
205
|
+
await renderQueue.startRender({
|
|
206
|
+
fps,
|
|
207
|
+
quality,
|
|
208
|
+
format,
|
|
209
|
+
resolution,
|
|
210
|
+
composition,
|
|
211
|
+
});
|
|
199
212
|
}}
|
|
200
213
|
compositionDimensions={compositionDimensions}
|
|
201
214
|
isRendering={renderQueue.isRendering}
|
|
@@ -24,14 +24,14 @@ import {
|
|
|
24
24
|
} from "./MotionPanelFields";
|
|
25
25
|
import { EaseCurveEditor } from "./EaseCurveEditor";
|
|
26
26
|
|
|
27
|
+
/** Motion data without targeting metadata (kind/target/updatedAt are derived from context). */
|
|
28
|
+
type StudioMotionData = Omit<StudioGsapMotion, "kind" | "target" | "updatedAt">;
|
|
29
|
+
|
|
27
30
|
interface MotionPanelProps {
|
|
28
31
|
element: DomEditSelection | null;
|
|
29
|
-
motion:
|
|
32
|
+
motion: StudioMotionData | null;
|
|
30
33
|
onClearSelection: () => void;
|
|
31
|
-
onSetMotion: (
|
|
32
|
-
element: DomEditSelection,
|
|
33
|
-
motion: Omit<StudioGsapMotion, "kind" | "target" | "updatedAt">,
|
|
34
|
-
) => void;
|
|
34
|
+
onSetMotion: (element: DomEditSelection, motion: StudioMotionData) => void;
|
|
35
35
|
onClearMotion: (element: DomEditSelection) => void;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -43,19 +43,19 @@ const MOTION_PRESET_OPTIONS: Array<{ label: string; value: StudioGsapMotionPrese
|
|
|
43
43
|
|
|
44
44
|
const MOTION_DIRECTION_OPTIONS: StudioGsapMotionDirection[] = ["up", "down", "left", "right"];
|
|
45
45
|
|
|
46
|
-
function motionValueDistance(motion:
|
|
46
|
+
function motionValueDistance(motion: StudioMotionData | null): number {
|
|
47
47
|
if (!motion) return 32;
|
|
48
48
|
return Math.max(Math.abs(motion.from.x ?? 0), Math.abs(motion.from.y ?? 0), 1);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function inferMotionPreset(motion:
|
|
51
|
+
function inferMotionPreset(motion: StudioMotionData | null): StudioGsapMotionPreset {
|
|
52
52
|
if (!motion) return "fade-up";
|
|
53
53
|
if (motion.from.scale != null || motion.to.scale != null) return "pop";
|
|
54
54
|
if (motion.from.x != null || motion.to.x != null) return "slide";
|
|
55
55
|
return "fade-up";
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function inferMotionDirection(motion:
|
|
58
|
+
function inferMotionDirection(motion: StudioMotionData | null): StudioGsapMotionDirection {
|
|
59
59
|
if (!motion) return "up";
|
|
60
60
|
const x = motion.from.x ?? 0;
|
|
61
61
|
const y = motion.from.y ?? 0;
|
|
@@ -55,6 +55,7 @@ interface SourceEditorProps {
|
|
|
55
55
|
language?: string;
|
|
56
56
|
onChange?: (content: string) => void;
|
|
57
57
|
readOnly?: boolean;
|
|
58
|
+
revealOffset?: number | null;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
export const SourceEditor = memo(function SourceEditor({
|
|
@@ -63,6 +64,7 @@ export const SourceEditor = memo(function SourceEditor({
|
|
|
63
64
|
language,
|
|
64
65
|
onChange,
|
|
65
66
|
readOnly = false,
|
|
67
|
+
revealOffset,
|
|
66
68
|
}: SourceEditorProps) {
|
|
67
69
|
const editorRef = useRef<EditorView | null>(null);
|
|
68
70
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -132,5 +134,17 @@ export const SourceEditor = memo(function SourceEditor({
|
|
|
132
134
|
}
|
|
133
135
|
}, [content]);
|
|
134
136
|
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
const view = editorRef.current;
|
|
139
|
+
if (!view || revealOffset == null || revealOffset < 0) return;
|
|
140
|
+
const docLen = view.state.doc.length;
|
|
141
|
+
const pos = Math.min(revealOffset, docLen);
|
|
142
|
+
view.dispatch({
|
|
143
|
+
selection: { anchor: pos },
|
|
144
|
+
effects: EditorView.scrollIntoView(pos, { y: "center" }),
|
|
145
|
+
});
|
|
146
|
+
view.focus();
|
|
147
|
+
}, [revealOffset]);
|
|
148
|
+
|
|
135
149
|
return <div ref={mountEditor} className="h-full w-full overflow-hidden" />;
|
|
136
150
|
});
|
|
@@ -31,6 +31,13 @@ import {
|
|
|
31
31
|
STUDIO_ROTATION_TRANSFORM_ORIGIN,
|
|
32
32
|
} from "./manualEditsTypes";
|
|
33
33
|
import { roundRotationAngle } from "./manualEditsParsing";
|
|
34
|
+
import {
|
|
35
|
+
STUDIO_MOTION_ATTR,
|
|
36
|
+
STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR,
|
|
37
|
+
STUDIO_MOTION_ORIGINAL_OPACITY_ATTR,
|
|
38
|
+
STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR,
|
|
39
|
+
} from "./studioMotionTypes";
|
|
40
|
+
import { applyStudioMotionFromDom } from "./studioMotion";
|
|
34
41
|
|
|
35
42
|
/* ── Gesture tracking ─────────────────────────────────────────────── */
|
|
36
43
|
let studioManualEditGestureId = 0;
|
|
@@ -755,6 +762,52 @@ export function buildClearRotationPatches(element: HTMLElement): PatchOperation[
|
|
|
755
762
|
return ops;
|
|
756
763
|
}
|
|
757
764
|
|
|
765
|
+
/* ── Motion HTML patch builders ──────────────────────────────────── */
|
|
766
|
+
|
|
767
|
+
export function buildMotionPatches(element: HTMLElement): PatchOperation[] {
|
|
768
|
+
const motionJson = element.getAttribute(STUDIO_MOTION_ATTR);
|
|
769
|
+
if (!motionJson) return [];
|
|
770
|
+
const ops: PatchOperation[] = [
|
|
771
|
+
{ type: "attribute", property: STUDIO_MOTION_ATTR, value: motionJson },
|
|
772
|
+
];
|
|
773
|
+
const origTransform = element.getAttribute(STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR);
|
|
774
|
+
if (origTransform !== null) {
|
|
775
|
+
ops.push({
|
|
776
|
+
type: "attribute",
|
|
777
|
+
property: STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR,
|
|
778
|
+
value: origTransform,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
const origOpacity = element.getAttribute(STUDIO_MOTION_ORIGINAL_OPACITY_ATTR);
|
|
782
|
+
if (origOpacity !== null) {
|
|
783
|
+
ops.push({
|
|
784
|
+
type: "attribute",
|
|
785
|
+
property: STUDIO_MOTION_ORIGINAL_OPACITY_ATTR,
|
|
786
|
+
value: origOpacity,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
const origVisibility = element.getAttribute(STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR);
|
|
790
|
+
if (origVisibility !== null) {
|
|
791
|
+
ops.push({
|
|
792
|
+
type: "attribute",
|
|
793
|
+
property: STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR,
|
|
794
|
+
value: origVisibility,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
return ops;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export function buildClearMotionPatches(_element: HTMLElement): PatchOperation[] {
|
|
801
|
+
return [
|
|
802
|
+
{ type: "attribute", property: STUDIO_MOTION_ATTR, value: null },
|
|
803
|
+
{ type: "attribute", property: STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR, value: null },
|
|
804
|
+
{ type: "attribute", property: STUDIO_MOTION_ORIGINAL_OPACITY_ATTR, value: null },
|
|
805
|
+
{ type: "attribute", property: STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR, value: null },
|
|
806
|
+
];
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/* ── Seek reapply (position + motion) ────────────────────────────── */
|
|
810
|
+
|
|
758
811
|
export function reapplyPositionEditsAfterSeek(doc: Document): void {
|
|
759
812
|
const htmlElement = doc.defaultView?.HTMLElement;
|
|
760
813
|
if (!htmlElement) return;
|
|
@@ -793,4 +846,7 @@ export function reapplyPositionEditsAfterSeek(doc: Document): void {
|
|
|
793
846
|
applyStudioRotation(el, { angle });
|
|
794
847
|
}
|
|
795
848
|
}
|
|
849
|
+
|
|
850
|
+
// Reapply DOM-backed motion timeline after seek
|
|
851
|
+
applyStudioMotionFromDom(doc);
|
|
796
852
|
}
|
|
@@ -30,8 +30,12 @@ export {
|
|
|
30
30
|
upsertStudioGsapMotion,
|
|
31
31
|
removeStudioMotionForSelection,
|
|
32
32
|
getStudioMotionForSelection,
|
|
33
|
+
readStudioMotionFromElement,
|
|
34
|
+
writeStudioMotionToElement,
|
|
35
|
+
clearStudioMotionFromElement,
|
|
33
36
|
} from "./studioMotionOps";
|
|
34
37
|
|
|
38
|
+
import { readStudioMotionFromElement as readMotionAttr } from "./studioMotionOps";
|
|
35
39
|
import {
|
|
36
40
|
STUDIO_MOTION_ATTR,
|
|
37
41
|
STUDIO_MOTION_ORIGINAL_TRANSFORM_ATTR,
|
|
@@ -39,6 +43,7 @@ import {
|
|
|
39
43
|
STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR,
|
|
40
44
|
STUDIO_MOTION_TIMELINE_ID,
|
|
41
45
|
type StudioGsapMotion,
|
|
46
|
+
type StudioGsapMotionValues,
|
|
42
47
|
type StudioMotionManifest,
|
|
43
48
|
type StudioMotionTarget,
|
|
44
49
|
type StudioMotionWindow,
|
|
@@ -220,6 +225,97 @@ export function applyStudioMotionManifest(
|
|
|
220
225
|
return applied;
|
|
221
226
|
}
|
|
222
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Reads motion data from `data-hf-studio-motion` JSON attributes in the DOM,
|
|
230
|
+
* builds a GSAP timeline, and seeks to the current time.
|
|
231
|
+
* This replaces the manifest-based `applyStudioMotionManifest` for the studio preview.
|
|
232
|
+
*/
|
|
233
|
+
export function applyStudioMotionFromDom(document: Document, currentTime?: number): number {
|
|
234
|
+
const win = document.defaultView as StudioMotionWindow | null;
|
|
235
|
+
if (!win) return 0;
|
|
236
|
+
const gsap = win.gsap;
|
|
237
|
+
win.__timelines = win.__timelines ?? {};
|
|
238
|
+
win.__timelines[STUDIO_MOTION_TIMELINE_ID]?.kill?.();
|
|
239
|
+
delete win.__timelines[STUDIO_MOTION_TIMELINE_ID];
|
|
240
|
+
|
|
241
|
+
// Restore elements that had GSAP motion applied previously but whose attribute
|
|
242
|
+
// is now just the legacy marker "true" (i.e. they were restored/cleared).
|
|
243
|
+
const HTMLElementCtor = document.defaultView?.HTMLElement;
|
|
244
|
+
if (!HTMLElementCtor) return 0;
|
|
245
|
+
|
|
246
|
+
// Collect elements that have JSON motion data in their attribute
|
|
247
|
+
const motionElements: Array<{
|
|
248
|
+
element: HTMLElement;
|
|
249
|
+
motion: {
|
|
250
|
+
start: number;
|
|
251
|
+
duration: number;
|
|
252
|
+
ease: string;
|
|
253
|
+
customEase?: { id: string; data: string };
|
|
254
|
+
from: StudioGsapMotionValues;
|
|
255
|
+
to: StudioGsapMotionValues;
|
|
256
|
+
};
|
|
257
|
+
}> = [];
|
|
258
|
+
|
|
259
|
+
for (const el of Array.from(document.querySelectorAll(`[${STUDIO_MOTION_ATTR}]`))) {
|
|
260
|
+
if (!(el instanceof HTMLElementCtor)) continue;
|
|
261
|
+
const motionData = readMotionAttr(el);
|
|
262
|
+
if (motionData) {
|
|
263
|
+
motionElements.push({ element: el, motion: motionData });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!gsap?.timeline || motionElements.length === 0) return 0;
|
|
268
|
+
|
|
269
|
+
const timeline = gsap.timeline({
|
|
270
|
+
paused: true,
|
|
271
|
+
defaults: { overwrite: "auto" },
|
|
272
|
+
});
|
|
273
|
+
let applied = 0;
|
|
274
|
+
for (const { element, motion } of motionElements) {
|
|
275
|
+
if (!timeline.fromTo) continue;
|
|
276
|
+
// Original styles are already captured when writeStudioMotionToElement was called
|
|
277
|
+
const fromVars: Record<string, unknown> = { ...motion.from };
|
|
278
|
+
const ease = resolveGsapEaseFromPayload(win, motion);
|
|
279
|
+
const toVars: Record<string, unknown> = {
|
|
280
|
+
...motion.to,
|
|
281
|
+
duration: motion.duration,
|
|
282
|
+
ease,
|
|
283
|
+
overwrite: "auto",
|
|
284
|
+
immediateRender: false,
|
|
285
|
+
};
|
|
286
|
+
timeline.fromTo(element, fromVars, toVars, motion.start);
|
|
287
|
+
applied += 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (applied === 0) {
|
|
291
|
+
timeline.kill?.();
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
win.__timelines[STUDIO_MOTION_TIMELINE_ID] = timeline;
|
|
295
|
+
timeline.pause?.();
|
|
296
|
+
const safeTime = readCurrentTime(win, currentTime);
|
|
297
|
+
if (timeline.totalTime) timeline.totalTime(safeTime, false);
|
|
298
|
+
else timeline.time?.(safeTime);
|
|
299
|
+
return applied;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function resolveGsapEaseFromPayload(
|
|
303
|
+
win: StudioMotionWindow,
|
|
304
|
+
motion: { ease: string; customEase?: { id: string; data: string } },
|
|
305
|
+
): string {
|
|
306
|
+
const customEase = motion.customEase;
|
|
307
|
+
if (!customEase) return motion.ease;
|
|
308
|
+
const customEasePlugin = win.CustomEase;
|
|
309
|
+
if (typeof customEasePlugin?.create !== "function") return motion.ease;
|
|
310
|
+
try {
|
|
311
|
+
win.gsap?.registerPlugin?.(customEasePlugin);
|
|
312
|
+
customEasePlugin.create(customEase.id, customEase.data);
|
|
313
|
+
return customEase.id;
|
|
314
|
+
} catch {
|
|
315
|
+
return motion.ease;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
223
319
|
export function installStudioMotionSeekReapply(win: Window, apply: () => void): boolean {
|
|
224
320
|
const studioWin = win as StudioMotionWindow;
|
|
225
321
|
studioWin.__hfStudioMotionApply = () => {
|