@hyperframes/studio 0.6.88 → 0.6.90
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-BKuDHMYl.js +146 -0
- package/dist/assets/index-D2NkPomd.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +33 -193
- package/src/components/StudioLeftSidebar.tsx +6 -0
- package/src/components/StudioRightPanel.tsx +8 -0
- package/src/components/TimelineToolbar.tsx +54 -31
- package/src/components/editor/AnimationCard.tsx +15 -3
- package/src/components/editor/DomEditOverlay.test.ts +34 -1
- package/src/components/editor/FileTree.tsx +5 -1
- package/src/components/editor/FileTreeNodes.tsx +17 -3
- package/src/components/editor/LayersPanel.tsx +19 -4
- package/src/components/editor/PropertyPanel.tsx +82 -170
- package/src/components/editor/domEditOverlayStartGesture.ts +1 -0
- package/src/components/editor/gsapAnimatesProperty.ts +52 -0
- package/src/components/editor/manualEditsDom.ts +11 -57
- package/src/components/editor/manualOffsetDrag.test.ts +18 -1
- package/src/components/editor/manualOffsetDrag.ts +16 -10
- package/src/components/editor/propertyPanel3dTransform.tsx +133 -0
- package/src/components/editor/propertyPanelHelpers.ts +76 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +1 -9
- package/src/components/editor/useDomEditOverlayGestures.ts +3 -0
- package/src/components/editor/useLayerDrag.ts +6 -3
- package/src/components/renders/RenderQueueItem.tsx +47 -46
- package/src/components/sidebar/CompositionsTab.tsx +15 -2
- package/src/components/sidebar/LeftSidebar.tsx +11 -0
- package/src/hooks/gsapDragCommit.ts +294 -0
- package/src/hooks/gsapKeyframeCacheHelpers.ts +88 -0
- package/src/hooks/gsapRuntimeBridge.ts +49 -402
- package/src/hooks/gsapRuntimeReaders.ts +201 -0
- package/src/hooks/timelineEditingHelpers.ts +148 -0
- package/src/hooks/useAnimatedPropertyCommit.ts +54 -12
- package/src/hooks/useBlockHandlers.ts +150 -0
- package/src/hooks/useClipboard.ts +1 -10
- package/src/hooks/useDomEditPreviewSync.ts +126 -0
- package/src/hooks/useDomEditSession.ts +11 -79
- package/src/hooks/useGestureCommit.ts +166 -0
- package/src/hooks/useGestureRecording.ts +271 -169
- package/src/hooks/useGsapScriptCommits.ts +7 -80
- package/src/hooks/useLintModal.ts +97 -25
- package/src/hooks/useTimelineEditing.ts +10 -132
- package/src/player/components/TimelineCanvas.tsx +24 -7
- package/src/player/components/useTimelinePlayhead.ts +2 -1
- package/src/player/store/playerStore.ts +12 -0
- package/src/utils/gsapSoftReload.ts +18 -1
- package/src/utils/studioUrlState.test.ts +9 -0
- package/dist/assets/index-B9_ctmee.js +0 -143
- package/dist/assets/index-CGlIm_-E.css +0 -1
|
@@ -61,6 +61,7 @@ vi.mock("./useDomEditOverlayRects", async () => {
|
|
|
61
61
|
groupOverlayItems,
|
|
62
62
|
groupOverlayItemsRef,
|
|
63
63
|
setGroupOverlayItems,
|
|
64
|
+
childRects: [],
|
|
64
65
|
};
|
|
65
66
|
},
|
|
66
67
|
};
|
|
@@ -96,7 +97,29 @@ describe("focusDomEditOverlayElement", () => {
|
|
|
96
97
|
});
|
|
97
98
|
|
|
98
99
|
describe("DomEditOverlay", () => {
|
|
99
|
-
it("renders selected bounds right after clicking a movable selection", () => {
|
|
100
|
+
it("renders selected bounds right after clicking a movable selection", async () => {
|
|
101
|
+
// The overlay's compRect updates via a RAF loop reading iframe + overlay
|
|
102
|
+
// getBoundingClientRect. happy-dom returns all zeros for newly-created
|
|
103
|
+
// elements with no layout, so without stubs the RAF early-returns
|
|
104
|
+
// (iRect.width <= 0) and compRect.width stays 0 — gating the selection
|
|
105
|
+
// box (and other bounded UI) behind `compRect.width > 0` (added in the
|
|
106
|
+
// keyframes PR a468550f). Stub element-level getBoundingClientRect for
|
|
107
|
+
// the test so the RAF compRect update produces a real width.
|
|
108
|
+
const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
|
|
109
|
+
Element.prototype.getBoundingClientRect = function (): DOMRect {
|
|
110
|
+
return {
|
|
111
|
+
left: 0,
|
|
112
|
+
top: 0,
|
|
113
|
+
right: 800,
|
|
114
|
+
bottom: 450,
|
|
115
|
+
width: 800,
|
|
116
|
+
height: 450,
|
|
117
|
+
x: 0,
|
|
118
|
+
y: 0,
|
|
119
|
+
toJSON: () => ({}),
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
100
123
|
const host = document.createElement("div");
|
|
101
124
|
document.body.append(host);
|
|
102
125
|
const root = createRoot(host);
|
|
@@ -162,6 +185,15 @@ describe("DomEditOverlay", () => {
|
|
|
162
185
|
root.render(React.createElement(Harness));
|
|
163
186
|
});
|
|
164
187
|
|
|
188
|
+
// Flush the mount's RAF tick so the compRect update lands before the
|
|
189
|
+
// pointer-down. Two animation-frame ticks: the first scheduled by
|
|
190
|
+
// useMountEffect's update(), the second by update()'s tail recursion.
|
|
191
|
+
await act(async () => {
|
|
192
|
+
await new Promise<void>((resolve) => {
|
|
193
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
165
197
|
const overlay = host.querySelector('[aria-label="Composition canvas"]') as HTMLDivElement;
|
|
166
198
|
expect(overlay).toBeTruthy();
|
|
167
199
|
|
|
@@ -183,6 +215,7 @@ describe("DomEditOverlay", () => {
|
|
|
183
215
|
root.unmount();
|
|
184
216
|
});
|
|
185
217
|
HTMLDivElement.prototype.setPointerCapture = originalPointerCapture;
|
|
218
|
+
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
|
186
219
|
host.remove();
|
|
187
220
|
});
|
|
188
221
|
});
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
|
|
16
16
|
// ── Types ──
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
interface FileTreeProps {
|
|
19
19
|
files: string[];
|
|
20
20
|
activeFile: string | null;
|
|
21
21
|
onSelectFile: (path: string) => void;
|
|
@@ -26,6 +26,7 @@ export interface FileTreeProps {
|
|
|
26
26
|
onDuplicateFile?: (path: string) => void;
|
|
27
27
|
onMoveFile?: (oldPath: string, newPath: string) => void;
|
|
28
28
|
onImportFiles?: (files: FileList, dir?: string) => void;
|
|
29
|
+
lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
// ── Main FileTree Component ──
|
|
@@ -41,6 +42,7 @@ export const FileTree = memo(function FileTree({
|
|
|
41
42
|
onDuplicateFile,
|
|
42
43
|
onMoveFile,
|
|
43
44
|
onImportFiles,
|
|
45
|
+
lintFindingsByFile,
|
|
44
46
|
}: FileTreeProps) {
|
|
45
47
|
const tree = useMemo(() => buildTree(files), [files]);
|
|
46
48
|
const children = useMemo(() => sortChildren(tree.children), [tree]);
|
|
@@ -283,6 +285,7 @@ export const FileTree = memo(function FileTree({
|
|
|
283
285
|
onContextMenu={handleContextMenu}
|
|
284
286
|
inlineInput={inlineInput}
|
|
285
287
|
onDragStart={handleDragStart}
|
|
288
|
+
lintInfo={lintFindingsByFile?.get(child.fullPath)}
|
|
286
289
|
/>
|
|
287
290
|
) : (
|
|
288
291
|
<TreeFolder
|
|
@@ -299,6 +302,7 @@ export const FileTree = memo(function FileTree({
|
|
|
299
302
|
onDrop={handleDrop}
|
|
300
303
|
onDragLeave={handleDragLeave}
|
|
301
304
|
dragOverFolder={dragOverFolder}
|
|
305
|
+
lintFindingsByFile={lintFindingsByFile}
|
|
302
306
|
/>
|
|
303
307
|
),
|
|
304
308
|
)}
|
|
@@ -18,8 +18,7 @@ import {
|
|
|
18
18
|
type InlineInputState,
|
|
19
19
|
} from "./FileTreeIcons";
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
export type { TreeNode, ContextMenuState, InlineInputState };
|
|
21
|
+
export type { ContextMenuState, InlineInputState };
|
|
23
22
|
export { buildTree, sortChildren, isActiveInSubtree } from "./FileTreeIcons";
|
|
24
23
|
|
|
25
24
|
const SZ_ICON = 14;
|
|
@@ -300,6 +299,7 @@ export const TreeFile = memo(function TreeFile({
|
|
|
300
299
|
onContextMenu,
|
|
301
300
|
inlineInput,
|
|
302
301
|
onDragStart,
|
|
302
|
+
lintInfo,
|
|
303
303
|
}: {
|
|
304
304
|
node: TreeNode;
|
|
305
305
|
depth: number;
|
|
@@ -308,6 +308,7 @@ export const TreeFile = memo(function TreeFile({
|
|
|
308
308
|
onContextMenu: (e: React.MouseEvent, path: string, isFolder: boolean) => void;
|
|
309
309
|
inlineInput: InlineInputState | null;
|
|
310
310
|
onDragStart: (e: React.DragEvent, path: string) => void;
|
|
311
|
+
lintInfo?: { count: number; messages: string[] };
|
|
311
312
|
}) {
|
|
312
313
|
const isActive = node.fullPath === activeFile;
|
|
313
314
|
const isRenaming = inlineInput?.mode === "rename" && inlineInput.originalPath === node.fullPath;
|
|
@@ -345,7 +346,15 @@ export const TreeFile = memo(function TreeFile({
|
|
|
345
346
|
style={{ paddingLeft: `${8 + depth * 12 + 14}px` }}
|
|
346
347
|
>
|
|
347
348
|
<FileIcon path={node.name} />
|
|
348
|
-
<span className="truncate">{node.name}</span>
|
|
349
|
+
<span className="truncate flex-1">{node.name}</span>
|
|
350
|
+
{lintInfo && lintInfo.count > 0 && (
|
|
351
|
+
<span
|
|
352
|
+
className="flex-shrink-0 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-amber-400 text-center mr-1"
|
|
353
|
+
title={lintInfo.messages.join("\n")}
|
|
354
|
+
>
|
|
355
|
+
{lintInfo.count}
|
|
356
|
+
</span>
|
|
357
|
+
)}
|
|
349
358
|
</button>
|
|
350
359
|
);
|
|
351
360
|
});
|
|
@@ -365,6 +374,7 @@ export const TreeFolder = memo(function TreeFolder({
|
|
|
365
374
|
onDrop,
|
|
366
375
|
onDragLeave,
|
|
367
376
|
dragOverFolder,
|
|
377
|
+
lintFindingsByFile,
|
|
368
378
|
}: {
|
|
369
379
|
node: TreeNode;
|
|
370
380
|
depth: number;
|
|
@@ -378,6 +388,7 @@ export const TreeFolder = memo(function TreeFolder({
|
|
|
378
388
|
onDrop: (e: React.DragEvent, folderPath: string) => void;
|
|
379
389
|
onDragLeave: () => void;
|
|
380
390
|
dragOverFolder: string | null;
|
|
391
|
+
lintFindingsByFile?: Map<string, { count: number; messages: string[] }>;
|
|
381
392
|
}) {
|
|
382
393
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
383
394
|
const toggle = useCallback(() => setIsOpen((v) => !v), []);
|
|
@@ -459,6 +470,7 @@ export const TreeFolder = memo(function TreeFolder({
|
|
|
459
470
|
onContextMenu={onContextMenu}
|
|
460
471
|
inlineInput={inlineInput}
|
|
461
472
|
onDragStart={onDragStart}
|
|
473
|
+
lintInfo={lintFindingsByFile?.get(child.fullPath)}
|
|
462
474
|
/>
|
|
463
475
|
) : child.children.size > 0 ? (
|
|
464
476
|
<TreeFolder
|
|
@@ -475,6 +487,7 @@ export const TreeFolder = memo(function TreeFolder({
|
|
|
475
487
|
onDrop={onDrop}
|
|
476
488
|
onDragLeave={onDragLeave}
|
|
477
489
|
dragOverFolder={dragOverFolder}
|
|
490
|
+
lintFindingsByFile={lintFindingsByFile}
|
|
478
491
|
/>
|
|
479
492
|
) : (
|
|
480
493
|
<TreeFile
|
|
@@ -486,6 +499,7 @@ export const TreeFolder = memo(function TreeFolder({
|
|
|
486
499
|
onContextMenu={onContextMenu}
|
|
487
500
|
inlineInput={inlineInput}
|
|
488
501
|
onDragStart={onDragStart}
|
|
502
|
+
lintInfo={lintFindingsByFile?.get(child.fullPath)}
|
|
489
503
|
/>
|
|
490
504
|
),
|
|
491
505
|
)}
|
|
@@ -122,13 +122,28 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
122
122
|
}, [compositionLoading, collectLayers]);
|
|
123
123
|
|
|
124
124
|
const resolveSelection = useCallback(
|
|
125
|
-
(layer: DomEditLayerItem) =>
|
|
126
|
-
|
|
125
|
+
(layer: DomEditLayerItem) => {
|
|
126
|
+
// Re-find the element from the live DOM — layer.element may be stale
|
|
127
|
+
// after soft reload (which replaces scripts without reloading the iframe).
|
|
128
|
+
let el = layer.element;
|
|
129
|
+
if (!el.isConnected) {
|
|
130
|
+
const iframe = previewIframeRef.current;
|
|
131
|
+
const doc = iframe?.contentDocument;
|
|
132
|
+
if (doc) {
|
|
133
|
+
const found =
|
|
134
|
+
(layer.id ? doc.getElementById(layer.id) : null) ??
|
|
135
|
+
(layer.hfId ? doc.querySelector(`[data-hf-id="${layer.hfId}"]`) : null) ??
|
|
136
|
+
doc.getElementById(layer.key);
|
|
137
|
+
if (found instanceof HTMLElement) el = found;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return resolveDomEditSelection(el, {
|
|
127
141
|
activeCompositionPath: activeCompPath,
|
|
128
142
|
isMasterView,
|
|
129
143
|
preferClipAncestor: false,
|
|
130
|
-
})
|
|
131
|
-
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
[activeCompPath, isMasterView, previewIframeRef],
|
|
132
147
|
);
|
|
133
148
|
|
|
134
149
|
const seekToLayer = useCallback(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { memo, useRef, useState } from "react";
|
|
1
|
+
import { memo, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { Eye, Layers, Move, X } from "../../icons/SystemIcons";
|
|
3
3
|
import { useStudioContext } from "../../contexts/StudioContext";
|
|
4
4
|
import { readStudioBoxSize, readStudioPathOffset, readStudioRotation } from "./manualEdits";
|
|
@@ -7,14 +7,17 @@ import {
|
|
|
7
7
|
formatPxMetricValue,
|
|
8
8
|
parsePxMetricValue,
|
|
9
9
|
RESPONSIVE_GRID,
|
|
10
|
+
readGsapRuntimeValuesForPanel,
|
|
11
|
+
readGsapBorderRadiusForPanel,
|
|
10
12
|
} from "./propertyPanelHelpers";
|
|
11
13
|
import { MetricField, Section } from "./propertyPanelPrimitives";
|
|
12
14
|
import { isMediaElement, MediaSection } from "./propertyPanelMediaSection";
|
|
13
15
|
import { TextSection, StyleSections } from "./propertyPanelSections";
|
|
14
16
|
import { GsapAnimationSection } from "./GsapAnimationSection";
|
|
17
|
+
import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
|
|
15
18
|
import { KeyframeNavigation } from "./KeyframeNavigation";
|
|
16
19
|
import { STUDIO_GSAP_PANEL_ENABLED, STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
|
|
17
|
-
import { usePlayerStore } from "../../player";
|
|
20
|
+
import { usePlayerStore, liveTime } from "../../player";
|
|
18
21
|
import { TimingSection } from "./propertyPanelTimingSection";
|
|
19
22
|
import { type PropertyPanelProps } from "./propertyPanelHelpers";
|
|
20
23
|
|
|
@@ -85,7 +88,29 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
85
88
|
const { showToast } = useStudioContext();
|
|
86
89
|
const [clipboardCopied, setClipboardCopied] = useState(false);
|
|
87
90
|
const clipboardTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
88
|
-
const
|
|
91
|
+
const storeTime = usePlayerStore((s) => s.currentTime);
|
|
92
|
+
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
|
93
|
+
const liveTimeRef = useRef(storeTime);
|
|
94
|
+
const [, forceRender] = useState(0);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!isPlaying) return;
|
|
97
|
+
let timerId: ReturnType<typeof setTimeout> | 0 = 0;
|
|
98
|
+
const unsub = liveTime.subscribe((t) => {
|
|
99
|
+
liveTimeRef.current = t;
|
|
100
|
+
if (!timerId)
|
|
101
|
+
timerId = setTimeout(() => {
|
|
102
|
+
timerId = 0;
|
|
103
|
+
forceRender((v) => v + 1);
|
|
104
|
+
}, 33);
|
|
105
|
+
});
|
|
106
|
+
return () => {
|
|
107
|
+
unsub();
|
|
108
|
+
if (timerId) clearTimeout(timerId);
|
|
109
|
+
};
|
|
110
|
+
}, [isPlaying]);
|
|
111
|
+
const currentTime = isPlaying ? liveTimeRef.current : storeTime;
|
|
112
|
+
const cacheElementKey = element?.id ?? element?.selector ?? "";
|
|
113
|
+
const cacheEntry = usePlayerStore((s) => s.keyframeCache.get(cacheElementKey));
|
|
89
114
|
|
|
90
115
|
if (!element) {
|
|
91
116
|
return (
|
|
@@ -137,7 +162,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
137
162
|
const commitManualOffset = (axis: "x" | "y", nextValue: string) => {
|
|
138
163
|
const parsed = parsePxMetricValue(nextValue);
|
|
139
164
|
if (parsed == null) return;
|
|
140
|
-
if (onCommitAnimatedProperty &&
|
|
165
|
+
if (onCommitAnimatedProperty && hasGsapAnimation) {
|
|
141
166
|
void onCommitAnimatedProperty(element, axis, parsed);
|
|
142
167
|
return;
|
|
143
168
|
}
|
|
@@ -146,6 +171,10 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
146
171
|
onAddKeyframe(gsapAnimId, pct, axis, parsed);
|
|
147
172
|
return;
|
|
148
173
|
}
|
|
174
|
+
if (hasGsapAnimation) {
|
|
175
|
+
showToast?.("Cannot edit position — animation callbacks not available");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
149
178
|
const current = readStudioPathOffset(element.element);
|
|
150
179
|
onSetManualOffset(element, {
|
|
151
180
|
x: axis === "x" ? parsed : current.x,
|
|
@@ -157,6 +186,14 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
157
186
|
const commitManualSize = (axis: "width" | "height", nextValue: string) => {
|
|
158
187
|
const parsed = parsePxMetricValue(nextValue);
|
|
159
188
|
if (parsed == null || parsed <= 0) return;
|
|
189
|
+
if (onCommitAnimatedProperty && hasGsapAnimation) {
|
|
190
|
+
void onCommitAnimatedProperty(element, axis, parsed);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (hasGsapAnimation) {
|
|
194
|
+
showToast?.("Cannot edit size — animation callbacks not available");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
160
197
|
const current = readStudioBoxSize(element.element);
|
|
161
198
|
const width =
|
|
162
199
|
current.width > 0
|
|
@@ -183,74 +220,27 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
183
220
|
const elDuration = Number.parseFloat(element?.dataAttributes?.duration ?? "1") || 0;
|
|
184
221
|
const currentPct = elDuration > 0 ? ((currentTime - elStart) / elDuration) * 100 : 0;
|
|
185
222
|
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
223
|
+
const gsapKfAnim = gsapAnimations?.find((a) => a.keyframes) ?? null;
|
|
224
|
+
const gsapKeyframes = gsapKfAnim?.keyframes?.keyframes ?? null;
|
|
225
|
+
const gsapAnimId = gsapKfAnim?.id ?? gsapAnimations?.[0]?.id ?? null;
|
|
226
|
+
const hasGsapAnimation = !!(gsapAnimId || gsapAnimations.length > 0);
|
|
227
|
+
const navKeyframes = cacheEntry?.keyframes ?? gsapKeyframes;
|
|
228
|
+
const seekFromKfPct = (pct: number) => onSeekToTime?.(elStart + (pct / 100) * elDuration);
|
|
189
229
|
|
|
190
230
|
// Read ALL GSAP-interpolated values at the current seek time.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (!selector) return null;
|
|
198
|
-
try {
|
|
199
|
-
const gsap = (
|
|
200
|
-
iframe.contentWindow as unknown as {
|
|
201
|
-
gsap?: { getProperty: (el: Element, prop: string) => number | string };
|
|
202
|
-
}
|
|
203
|
-
).gsap;
|
|
204
|
-
if (!gsap?.getProperty) return null;
|
|
205
|
-
const el = iframe.contentDocument?.querySelector(selector);
|
|
206
|
-
if (!el) return null;
|
|
207
|
-
const propKeys = new Set<string>();
|
|
208
|
-
for (const anim of gsapAnimations) {
|
|
209
|
-
if (anim.keyframes) {
|
|
210
|
-
for (const kf of anim.keyframes.keyframes) {
|
|
211
|
-
for (const p of Object.keys(kf.properties)) propKeys.add(p);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
for (const p of Object.keys(anim.properties)) propKeys.add(p);
|
|
215
|
-
}
|
|
216
|
-
const result: Record<string, number> = {};
|
|
217
|
-
for (const prop of propKeys) {
|
|
218
|
-
const v = Number(gsap.getProperty(el, prop));
|
|
219
|
-
if (Number.isFinite(v)) result[prop] = Math.round(v * 100) / 100;
|
|
220
|
-
}
|
|
221
|
-
return Object.keys(result).length > 0 ? result : null;
|
|
222
|
-
} catch {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
})();
|
|
231
|
+
const gsapRuntimeValues = readGsapRuntimeValuesForPanel(
|
|
232
|
+
gsapAnimId,
|
|
233
|
+
gsapAnimations,
|
|
234
|
+
element,
|
|
235
|
+
previewIframeRef ?? { current: null },
|
|
236
|
+
);
|
|
226
237
|
|
|
227
|
-
const gsapBorderRadius
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
);
|
|
234
|
-
if (!hasBRProp) return null;
|
|
235
|
-
}
|
|
236
|
-
const iframe = previewIframeRef?.current;
|
|
237
|
-
const selector = element.id ? `#${element.id}` : element.selector;
|
|
238
|
-
if (!iframe?.contentDocument || !selector) return null;
|
|
239
|
-
try {
|
|
240
|
-
const el = iframe.contentDocument.querySelector(selector);
|
|
241
|
-
if (!el) return null;
|
|
242
|
-
const cs = iframe.contentWindow!.getComputedStyle(el);
|
|
243
|
-
const parse = (v: string) => Number.parseFloat(v) || 0;
|
|
244
|
-
return {
|
|
245
|
-
tl: parse(cs.borderTopLeftRadius),
|
|
246
|
-
tr: parse(cs.borderTopRightRadius),
|
|
247
|
-
br: parse(cs.borderBottomRightRadius),
|
|
248
|
-
bl: parse(cs.borderBottomLeftRadius),
|
|
249
|
-
};
|
|
250
|
-
} catch {
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
})();
|
|
238
|
+
const gsapBorderRadius = readGsapBorderRadiusForPanel(
|
|
239
|
+
gsapRuntimeValues,
|
|
240
|
+
gsapAnimations,
|
|
241
|
+
element,
|
|
242
|
+
previewIframeRef ?? { current: null },
|
|
243
|
+
);
|
|
254
244
|
|
|
255
245
|
const displayX = gsapRuntimeValues?.x ?? manualOffset.x;
|
|
256
246
|
const displayY = gsapRuntimeValues?.y ?? manualOffset.y;
|
|
@@ -398,9 +388,9 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
398
388
|
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
|
|
399
389
|
<KeyframeNavigation
|
|
400
390
|
property="x"
|
|
401
|
-
keyframes={
|
|
391
|
+
keyframes={navKeyframes}
|
|
402
392
|
currentPercentage={currentPct}
|
|
403
|
-
onSeek={
|
|
393
|
+
onSeek={seekFromKfPct}
|
|
404
394
|
onAddKeyframe={() =>
|
|
405
395
|
onCommitAnimatedProperty &&
|
|
406
396
|
void onCommitAnimatedProperty(element, "x", displayX)
|
|
@@ -423,9 +413,9 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
423
413
|
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
|
|
424
414
|
<KeyframeNavigation
|
|
425
415
|
property="y"
|
|
426
|
-
keyframes={
|
|
416
|
+
keyframes={navKeyframes}
|
|
427
417
|
currentPercentage={currentPct}
|
|
428
|
-
onSeek={
|
|
418
|
+
onSeek={seekFromKfPct}
|
|
429
419
|
onAddKeyframe={() =>
|
|
430
420
|
onCommitAnimatedProperty &&
|
|
431
421
|
void onCommitAnimatedProperty(element, "y", displayY)
|
|
@@ -448,9 +438,9 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
448
438
|
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
|
|
449
439
|
<KeyframeNavigation
|
|
450
440
|
property="width"
|
|
451
|
-
keyframes={
|
|
441
|
+
keyframes={navKeyframes}
|
|
452
442
|
currentPercentage={currentPct}
|
|
453
|
-
onSeek={
|
|
443
|
+
onSeek={seekFromKfPct}
|
|
454
444
|
onAddKeyframe={() =>
|
|
455
445
|
onCommitAnimatedProperty &&
|
|
456
446
|
void onCommitAnimatedProperty(element, "width", displayW)
|
|
@@ -473,9 +463,9 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
473
463
|
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
|
|
474
464
|
<KeyframeNavigation
|
|
475
465
|
property="height"
|
|
476
|
-
keyframes={
|
|
466
|
+
keyframes={navKeyframes}
|
|
477
467
|
currentPercentage={currentPct}
|
|
478
|
-
onSeek={
|
|
468
|
+
onSeek={seekFromKfPct}
|
|
479
469
|
onAddKeyframe={() =>
|
|
480
470
|
onCommitAnimatedProperty &&
|
|
481
471
|
void onCommitAnimatedProperty(element, "height", displayH)
|
|
@@ -496,9 +486,9 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
496
486
|
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
|
|
497
487
|
<KeyframeNavigation
|
|
498
488
|
property="rotation"
|
|
499
|
-
keyframes={
|
|
489
|
+
keyframes={navKeyframes}
|
|
500
490
|
currentPercentage={currentPct}
|
|
501
|
-
onSeek={
|
|
491
|
+
onSeek={seekFromKfPct}
|
|
502
492
|
onAddKeyframe={() =>
|
|
503
493
|
onCommitAnimatedProperty &&
|
|
504
494
|
void onCommitAnimatedProperty(element, "rotation", displayR)
|
|
@@ -510,97 +500,19 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
510
500
|
</div>
|
|
511
501
|
</div>
|
|
512
502
|
{gsapRuntimeValues && (
|
|
513
|
-
<
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (v != null && onCommitAnimatedProperty) {
|
|
527
|
-
void onCommitAnimatedProperty(element, "z", v);
|
|
528
|
-
}
|
|
529
|
-
}}
|
|
530
|
-
/>
|
|
531
|
-
</div>
|
|
532
|
-
{STUDIO_KEYFRAMES_ENABLED && (gsapAnimId || onCommitAnimatedProperty) && (
|
|
533
|
-
<KeyframeNavigation
|
|
534
|
-
property="z"
|
|
535
|
-
keyframes={gsapKeyframes}
|
|
536
|
-
currentPercentage={currentPct}
|
|
537
|
-
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
|
|
538
|
-
onAddKeyframe={() => {
|
|
539
|
-
if (onCommitAnimatedProperty) {
|
|
540
|
-
void onCommitAnimatedProperty(element, "z", gsapRuntimeValues?.z ?? 0);
|
|
541
|
-
}
|
|
542
|
-
}}
|
|
543
|
-
onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)}
|
|
544
|
-
onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)}
|
|
545
|
-
/>
|
|
546
|
-
)}
|
|
547
|
-
</div>
|
|
548
|
-
<div className="flex items-center gap-1">
|
|
549
|
-
<div className="flex-1">
|
|
550
|
-
<MetricField
|
|
551
|
-
label="Scale"
|
|
552
|
-
value={String(gsapRuntimeValues.scale ?? 1)}
|
|
553
|
-
scrub
|
|
554
|
-
onCommit={(next) => {
|
|
555
|
-
const v = Number.parseFloat(next);
|
|
556
|
-
if (Number.isFinite(v) && onCommitAnimatedProperty) {
|
|
557
|
-
void onCommitAnimatedProperty(element, "scale", v);
|
|
558
|
-
}
|
|
559
|
-
}}
|
|
560
|
-
/>
|
|
561
|
-
</div>
|
|
562
|
-
{STUDIO_KEYFRAMES_ENABLED && (gsapAnimId || onCommitAnimatedProperty) && (
|
|
563
|
-
<KeyframeNavigation
|
|
564
|
-
property="scale"
|
|
565
|
-
keyframes={gsapKeyframes}
|
|
566
|
-
currentPercentage={currentPct}
|
|
567
|
-
onSeek={(pct) => onSeekToTime?.(elStart + (pct / 100) * elDuration)}
|
|
568
|
-
onAddKeyframe={() => {
|
|
569
|
-
if (onCommitAnimatedProperty) {
|
|
570
|
-
void onCommitAnimatedProperty(
|
|
571
|
-
element,
|
|
572
|
-
"scale",
|
|
573
|
-
gsapRuntimeValues?.scale ?? 1,
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
}}
|
|
577
|
-
onRemoveKeyframe={(pct) => gsapAnimId && onRemoveKeyframe?.(gsapAnimId, pct)}
|
|
578
|
-
onConvertToKeyframes={() => gsapAnimId && onConvertToKeyframes?.(gsapAnimId)}
|
|
579
|
-
/>
|
|
580
|
-
)}
|
|
581
|
-
</div>
|
|
582
|
-
<MetricField
|
|
583
|
-
label="RotX"
|
|
584
|
-
value={`${gsapRuntimeValues.rotationX ?? 0}°`}
|
|
585
|
-
onCommit={(next) => {
|
|
586
|
-
const v = Number.parseFloat(next.replace("°", ""));
|
|
587
|
-
if (Number.isFinite(v) && onCommitAnimatedProperty) {
|
|
588
|
-
void onCommitAnimatedProperty(element, "rotationX", v);
|
|
589
|
-
}
|
|
590
|
-
}}
|
|
591
|
-
/>
|
|
592
|
-
<MetricField
|
|
593
|
-
label="RotY"
|
|
594
|
-
value={`${gsapRuntimeValues.rotationY ?? 0}°`}
|
|
595
|
-
onCommit={(next) => {
|
|
596
|
-
const v = Number.parseFloat(next.replace("°", ""));
|
|
597
|
-
if (Number.isFinite(v) && onCommitAnimatedProperty) {
|
|
598
|
-
void onCommitAnimatedProperty(element, "rotationY", v);
|
|
599
|
-
}
|
|
600
|
-
}}
|
|
601
|
-
/>
|
|
602
|
-
</div>
|
|
603
|
-
</div>
|
|
503
|
+
<PropertyPanel3dTransform
|
|
504
|
+
gsapRuntimeValues={gsapRuntimeValues}
|
|
505
|
+
gsapAnimId={gsapAnimId}
|
|
506
|
+
gsapKeyframes={navKeyframes}
|
|
507
|
+
currentPct={currentPct}
|
|
508
|
+
elStart={elStart}
|
|
509
|
+
elDuration={elDuration}
|
|
510
|
+
element={element}
|
|
511
|
+
onCommitAnimatedProperty={onCommitAnimatedProperty}
|
|
512
|
+
onSeekToTime={onSeekToTime}
|
|
513
|
+
onRemoveKeyframe={onRemoveKeyframe}
|
|
514
|
+
onConvertToKeyframes={onConvertToKeyframes}
|
|
515
|
+
/>
|
|
604
516
|
)}
|
|
605
517
|
<div className="mt-3">
|
|
606
518
|
<div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks whether GSAP actively animates one or more CSS/GSAP properties on
|
|
3
|
+
* the given element by inspecting all registered `__timelines`.
|
|
4
|
+
*/
|
|
5
|
+
export function gsapAnimatesProperty(el: HTMLElement, ...props: string[]): boolean {
|
|
6
|
+
const win = el.ownerDocument.defaultView as
|
|
7
|
+
| (Window & {
|
|
8
|
+
__timelines?: Record<
|
|
9
|
+
string,
|
|
10
|
+
{
|
|
11
|
+
getChildren?: (
|
|
12
|
+
deep: boolean,
|
|
13
|
+
) => Array<{ targets?: () => Element[]; vars?: Record<string, unknown> }>;
|
|
14
|
+
}
|
|
15
|
+
>;
|
|
16
|
+
})
|
|
17
|
+
| null;
|
|
18
|
+
if (!win?.__timelines) return false;
|
|
19
|
+
const propSet = new Set(props);
|
|
20
|
+
for (const tl of Object.values(win.__timelines)) {
|
|
21
|
+
if (!tl?.getChildren) continue;
|
|
22
|
+
try {
|
|
23
|
+
for (const child of tl.getChildren(true)) {
|
|
24
|
+
if (!child.targets || !child.vars) continue;
|
|
25
|
+
let targetsEl = false;
|
|
26
|
+
for (const t of child.targets()) {
|
|
27
|
+
if (t === el || (el.id && t.id === el.id)) {
|
|
28
|
+
targetsEl = true;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!targetsEl) continue;
|
|
33
|
+
const vars = child.vars;
|
|
34
|
+
for (const p of propSet) {
|
|
35
|
+
if (p in vars) return true;
|
|
36
|
+
}
|
|
37
|
+
if (vars.keyframes && typeof vars.keyframes === "object") {
|
|
38
|
+
for (const kfVal of Object.values(vars.keyframes as Record<string, unknown>)) {
|
|
39
|
+
if (kfVal && typeof kfVal === "object") {
|
|
40
|
+
for (const p of propSet) {
|
|
41
|
+
if (p in (kfVal as Record<string, unknown>)) return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
/* */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|