@hyperframes/studio 0.7.14 → 0.7.16
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-C4cUbqql.js +375 -0
- package/dist/assets/{index-CkrVt5DX.js → index-D2SMcOdY.js} +1 -1
- package/dist/assets/index-DmkOvZns.css +1 -0
- package/dist/assets/{index-DYgkaIlV.js → index-sCOjSz40.js} +1 -1
- package/dist/{chunk-KZXYQYIU.js → chunk-SBGXX7WY.js} +52 -22
- package/dist/chunk-SBGXX7WY.js.map +1 -0
- package/dist/{domEditingLayers-SSXQZHHQ.js → domEditingLayers-VZMLL4AP.js} +2 -4
- package/dist/index.d.ts +23 -6
- package/dist/index.html +2 -2
- package/dist/index.js +3158 -2809
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
- package/src/App.tsx +2 -0
- package/src/components/StudioPreviewArea.tsx +0 -41
- package/src/components/StudioRightPanel.tsx +2 -0
- package/src/components/TimelineToolbar.tsx +4 -4
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- package/src/components/editor/GestureRecordControl.tsx +2 -2
- package/src/components/editor/GsapAnimationSection.tsx +3 -3
- package/src/components/editor/InspectorHeaderActions.tsx +62 -0
- package/src/components/editor/LayersPanel.tsx +35 -2
- package/src/components/editor/MotionPathOverlay.tsx +24 -8
- package/src/components/editor/PropertyPanel.tsx +48 -60
- package/src/components/editor/Transform3DCube.tsx +84 -85
- package/src/components/editor/domEditOverlayGeometry.ts +28 -0
- package/src/components/editor/domEditingDom.ts +36 -2
- package/src/components/editor/domEditingGroups.ts +41 -0
- package/src/components/editor/domEditingLayers.test.ts +97 -1
- package/src/components/editor/domEditingLayers.ts +13 -29
- package/src/components/editor/domEditingTypes.ts +4 -1
- package/src/components/editor/gsapAnimationCallbacks.ts +1 -1
- package/src/components/editor/gsapAnimationHelpers.test.ts +2 -2
- package/src/components/editor/gsapAnimationHelpers.ts +1 -1
- package/src/components/editor/manualEditsTypes.ts +1 -1
- package/src/components/editor/manualOffsetDrag.ts +23 -2
- package/src/components/editor/motionPathCommit.test.ts +1 -1
- package/src/components/editor/motionPathSelection.ts +1 -1
- package/src/components/editor/propertyPanel3dTransform.tsx +70 -30
- package/src/components/editor/propertyPanelHelpers.ts +6 -4
- package/src/components/editor/useDomEditOverlayRects.ts +8 -3
- package/src/components/editor/useMotionPathData.ts +47 -1
- package/src/contexts/DomEditContext.tsx +16 -0
- package/src/contexts/TimelineEditContext.tsx +0 -1
- package/src/hooks/gsapRuntimeBridge.ts +9 -2
- package/src/hooks/gsapTweenSynth.ts +64 -0
- package/src/hooks/useAnimatedPropertyCommit.ts +126 -26
- package/src/hooks/useAppHotkeys.ts +34 -3
- package/src/hooks/useDomEditSession.ts +48 -0
- package/src/hooks/useDomSelection.ts +73 -9
- package/src/hooks/useEnableKeyframes.test.ts +35 -0
- package/src/hooks/useEnableKeyframes.ts +29 -1
- package/src/hooks/useGroupCommits.ts +188 -0
- package/src/hooks/useGsapSelectionHandlers.ts +4 -0
- package/src/hooks/useGsapTweenCache.ts +56 -72
- package/src/hooks/usePreviewInteraction.ts +60 -2
- package/src/player/components/ShortcutsPanel.tsx +2 -0
- package/src/player/components/Timeline.tsx +0 -15
- package/src/player/components/TimelineCanvas.tsx +0 -13
- package/src/player/components/TimelineClipDiamonds.tsx +4 -125
- package/src/player/components/timelineCallbacks.ts +0 -1
- package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +47 -4
- package/src/player/hooks/useTimelineSyncCallbacks.ts +43 -4
- package/src/player/store/playerStore.ts +20 -0
- package/src/utils/sdkCutoverParity.test.ts +1 -1
- package/src/utils/studioPreviewHelpers.ts +37 -0
- package/dist/assets/index-BXG-pbr_.js +0 -375
- package/dist/assets/index-svYFaNuq.css +0 -1
- package/dist/chunk-KZXYQYIU.js.map +0 -1
- package/src/components/editor/keyframeMove.test.ts +0 -101
- package/src/components/editor/keyframeMove.ts +0 -151
- /package/dist/{domEditingLayers-SSXQZHHQ.js.map → domEditingLayers-VZMLL4AP.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.16",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -45,9 +45,11 @@
|
|
|
45
45
|
"dompurify": "^3.2.4",
|
|
46
46
|
"marked": "^14.1.4",
|
|
47
47
|
"mediabunny": "^1.45.3",
|
|
48
|
-
"@hyperframes/core": "0.7.
|
|
49
|
-
"@hyperframes/
|
|
50
|
-
"@hyperframes/
|
|
48
|
+
"@hyperframes/core": "0.7.16",
|
|
49
|
+
"@hyperframes/player": "0.7.16",
|
|
50
|
+
"@hyperframes/parsers": "0.7.16",
|
|
51
|
+
"@hyperframes/studio-server": "0.7.16",
|
|
52
|
+
"@hyperframes/sdk": "0.7.16"
|
|
51
53
|
},
|
|
52
54
|
"devDependencies": {
|
|
53
55
|
"@types/react": "19",
|
|
@@ -62,7 +64,7 @@
|
|
|
62
64
|
"vite": "^6.4.2",
|
|
63
65
|
"vitest": "^3.2.4",
|
|
64
66
|
"zustand": "^5.0.0",
|
|
65
|
-
"@hyperframes/producer": "0.7.
|
|
67
|
+
"@hyperframes/producer": "0.7.16"
|
|
66
68
|
},
|
|
67
69
|
"peerDependencies": {
|
|
68
70
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -258,6 +258,8 @@ export function StudioApp() {
|
|
|
258
258
|
onResetKeyframes: () => resetKeyframesRef.current(),
|
|
259
259
|
onDeleteSelectedKeyframes: () => deleteSelectedKeyframesRef.current(),
|
|
260
260
|
onAfterUndoRedo: () => invalidateGsapCacheRef.current(),
|
|
261
|
+
onGroupSelection: () => domEditSessionRef.current.handleGroupSelection(),
|
|
262
|
+
onUngroupSelection: () => domEditSessionRef.current.handleUngroupSelection(),
|
|
261
263
|
activeCompPath,
|
|
262
264
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
263
265
|
onToggleRecording: STUDIO_KEYFRAMES_ENABLED
|
|
@@ -21,8 +21,6 @@ import { useDomEditActionsContext, useDomEditSelectionContext } from "../context
|
|
|
21
21
|
import { TimelineEditProvider } from "../contexts/TimelineEditContext";
|
|
22
22
|
import type { BlockPreviewInfo } from "./sidebar/BlocksTab";
|
|
23
23
|
import { readStudioUiPreferences } from "../utils/studioUiPreferences";
|
|
24
|
-
import { fetchParsedAnimations } from "../hooks/useGsapTweenCache";
|
|
25
|
-
import { pickKeyframeTween, computeKeyframeMovePlan } from "./editor/keyframeMove";
|
|
26
24
|
import type { GestureRecordingState } from "./editor/GestureRecordControl";
|
|
27
25
|
|
|
28
26
|
export interface StudioPreviewAreaProps {
|
|
@@ -182,45 +180,6 @@ export function StudioPreviewArea({
|
|
|
182
180
|
}
|
|
183
181
|
},
|
|
184
182
|
// fallow-ignore-next-line complexity
|
|
185
|
-
onMoveKeyframe: async (_el: TimelineElement, oldPct: number, newPct: number) => {
|
|
186
|
-
// Resolve the dragged element's selection + parsed animations on demand
|
|
187
|
-
// (both awaited and cached) rather than relying on the async DOM-edit
|
|
188
|
-
// session being loaded for this element — that coupling made the commit
|
|
189
|
-
// intermittently no-op (revert) when dragging before the session caught up.
|
|
190
|
-
if (!projectId) return;
|
|
191
|
-
const sourceFile = _el.sourceFile || activeCompPath || "index.html";
|
|
192
|
-
const [selection, parsed] = await Promise.all([
|
|
193
|
-
buildDomSelectionForTimelineElement(_el),
|
|
194
|
-
fetchParsedAnimations(projectId, sourceFile),
|
|
195
|
-
]);
|
|
196
|
-
if (!selection || !parsed) return;
|
|
197
|
-
|
|
198
|
-
const cached = usePlayerStore.getState().keyframeCache.get(_el.key ?? _el.id);
|
|
199
|
-
const cachedKf = cached?.keyframes.find((k) => Math.abs(k.percentage - oldPct) < 0.2);
|
|
200
|
-
const origAbsTime = _el.start + (oldPct / 100) * _el.duration;
|
|
201
|
-
const anim = pickKeyframeTween(
|
|
202
|
-
parsed.animations,
|
|
203
|
-
_el,
|
|
204
|
-
origAbsTime,
|
|
205
|
-
cachedKf?.propertyGroup,
|
|
206
|
-
);
|
|
207
|
-
if (!anim) return;
|
|
208
|
-
|
|
209
|
-
const plan = computeKeyframeMovePlan(
|
|
210
|
-
anim,
|
|
211
|
-
cachedKf?.tweenPercentage ?? oldPct,
|
|
212
|
-
_el,
|
|
213
|
-
newPct,
|
|
214
|
-
);
|
|
215
|
-
if (plan.meta) handleGsapUpdateMeta(anim.id, plan.meta, selection);
|
|
216
|
-
for (const pct of plan.removes) handleGsapRemoveKeyframe(anim.id, pct, selection);
|
|
217
|
-
for (const add of plan.adds) {
|
|
218
|
-
for (const [prop, val] of Object.entries(add.properties)) {
|
|
219
|
-
handleGsapAddKeyframe(anim.id, add.pct, prop, val, selection);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
// fallow-ignore-next-line complexity
|
|
224
183
|
onToggleKeyframeAtPlayhead: (el: TimelineElement) => {
|
|
225
184
|
const currentTime = usePlayerStore.getState().currentTime;
|
|
226
185
|
const pct =
|
|
@@ -93,6 +93,7 @@ export function StudioRightPanel({
|
|
|
93
93
|
domEditGroupSelections,
|
|
94
94
|
copiedAgentPrompt,
|
|
95
95
|
clearDomSelection,
|
|
96
|
+
handleUngroupSelection,
|
|
96
97
|
handleDomStyleCommit,
|
|
97
98
|
handleDomAttributeCommit,
|
|
98
99
|
handleDomAttributeLiveCommit,
|
|
@@ -241,6 +242,7 @@ export function StudioRightPanel({
|
|
|
241
242
|
multiSelectCount={domEditGroupSelections.length}
|
|
242
243
|
copiedAgentPrompt={copiedAgentPrompt}
|
|
243
244
|
onClearSelection={clearDomSelection}
|
|
245
|
+
onUngroup={handleUngroupSelection}
|
|
244
246
|
onSetStyle={handleDomStyleCommit}
|
|
245
247
|
onSetAttribute={handleDomAttributeCommit}
|
|
246
248
|
onSetAttributeLive={handleDomAttributeLiveCommit}
|
|
@@ -133,12 +133,12 @@ export function TimelineToolbar({
|
|
|
133
133
|
<Tooltip
|
|
134
134
|
label={
|
|
135
135
|
keyframeState === "active"
|
|
136
|
-
? "Remove keyframe at playhead"
|
|
136
|
+
? "Remove keyframe at playhead (K)"
|
|
137
137
|
: keyframeState === "inactive"
|
|
138
138
|
? keyframeWillExtend
|
|
139
|
-
? "Add keyframe at playhead
|
|
140
|
-
: "Add keyframe at playhead"
|
|
141
|
-
: "
|
|
139
|
+
? "Add keyframe at playhead — extends animation (K)"
|
|
140
|
+
: "Add keyframe at playhead (K)"
|
|
141
|
+
: "Add keyframe (K)"
|
|
142
142
|
}
|
|
143
143
|
>
|
|
144
144
|
<button
|
|
@@ -3,7 +3,7 @@ import { useMountEffect } from "../../hooks/useMountEffect";
|
|
|
3
3
|
import { type DomEditSelection } from "./domEditing";
|
|
4
4
|
import { useMarqueeGestures } from "./marqueeCommit";
|
|
5
5
|
import { MarqueeOverlay } from "./MarqueeOverlay";
|
|
6
|
-
import {
|
|
6
|
+
import { groupAwareOverlayRect, resolveDomEditGroupOverlayRect } from "./domEditOverlayGeometry";
|
|
7
7
|
import { collectDomEditLayerItems } from "./domEditingLayers";
|
|
8
8
|
import { isElementComputedVisible } from "./domEditingElement";
|
|
9
9
|
import {
|
|
@@ -248,7 +248,10 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
248
248
|
const elMap = new Map<string, HTMLElement>();
|
|
249
249
|
for (const item of items) {
|
|
250
250
|
if (!isElementComputedVisible(item.element)) continue;
|
|
251
|
-
|
|
251
|
+
// Groups use their members' union (where they actually render), so a group
|
|
252
|
+
// whose members sit inside the canvas isn't flagged off-canvas by a stale
|
|
253
|
+
// wrapper box.
|
|
254
|
+
const r = groupAwareOverlayRect(overlay, iframe, item.element);
|
|
252
255
|
if (!r) continue;
|
|
253
256
|
// Any edge crossing the composition border → gray-zone indicator (the
|
|
254
257
|
// in-canvas portion is clipped away below, so only the sliver shows).
|
|
@@ -43,8 +43,8 @@ export function GestureRecordPanelButton({
|
|
|
43
43
|
>
|
|
44
44
|
<GestureRecordIcon recording={recording} />
|
|
45
45
|
{recording
|
|
46
|
-
? `Stop recording ${(recordingDuration ?? 0).toFixed(1)}s
|
|
47
|
-
: "Record gesture (R)
|
|
46
|
+
? `Stop recording ${(recordingDuration ?? 0).toFixed(1)}s -- press R`
|
|
47
|
+
: "Record gesture (R) -- move pointer to capture motion"}
|
|
48
48
|
</button>
|
|
49
49
|
</div>
|
|
50
50
|
);
|
|
@@ -46,9 +46,9 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
|
|
|
46
46
|
)}
|
|
47
47
|
{unsupportedTimelinePattern && (
|
|
48
48
|
<p className="mb-2 rounded-lg bg-amber-500/10 px-3 py-2 text-[11px] leading-relaxed text-amber-400">
|
|
49
|
-
This
|
|
50
|
-
|
|
51
|
-
enable editing.
|
|
49
|
+
This timeline uses a computed key (window.__timelines[variable]) the editor can't
|
|
50
|
+
resolve statically. Use a string-literal key (window.__timelines["id"]) or a
|
|
51
|
+
variable declaration (const tl = gsap.timeline()) to enable editing.
|
|
52
52
|
</p>
|
|
53
53
|
)}
|
|
54
54
|
{multipleTimelines || unsupportedTimelinePattern ? null : (
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { X } from "../../icons/SystemIcons";
|
|
2
|
+
import type { DomEditSelection } from "./domEditingTypes";
|
|
3
|
+
|
|
4
|
+
/** The action buttons in the inspector header: Ungroup (groups only), copy, clear. */
|
|
5
|
+
export function InspectorHeaderActions({
|
|
6
|
+
element,
|
|
7
|
+
copied,
|
|
8
|
+
onCopy,
|
|
9
|
+
onClear,
|
|
10
|
+
onUngroup,
|
|
11
|
+
}: {
|
|
12
|
+
element: DomEditSelection;
|
|
13
|
+
copied: boolean;
|
|
14
|
+
onCopy: () => void;
|
|
15
|
+
onClear: () => void;
|
|
16
|
+
onUngroup?: () => void;
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex items-center gap-1">
|
|
20
|
+
{onUngroup && element.dataAttributes["hf-group"] != null && (
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={onUngroup}
|
|
24
|
+
title="Ungroup (⌘⇧G)"
|
|
25
|
+
className="flex h-6 items-center rounded px-2 text-[11px] font-medium text-neutral-400 transition-colors hover:bg-neutral-800 hover:text-neutral-200"
|
|
26
|
+
>
|
|
27
|
+
Ungroup
|
|
28
|
+
</button>
|
|
29
|
+
)}
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={onCopy}
|
|
33
|
+
className={`flex h-6 w-6 items-center justify-center rounded transition-colors ${
|
|
34
|
+
copied
|
|
35
|
+
? "text-studio-accent"
|
|
36
|
+
: "text-neutral-500 hover:bg-neutral-800 hover:text-neutral-300"
|
|
37
|
+
}`}
|
|
38
|
+
title={copied ? "Copied!" : "Copy element info to clipboard"}
|
|
39
|
+
>
|
|
40
|
+
<svg
|
|
41
|
+
width="13"
|
|
42
|
+
height="13"
|
|
43
|
+
viewBox="0 0 16 16"
|
|
44
|
+
fill="none"
|
|
45
|
+
stroke="currentColor"
|
|
46
|
+
strokeWidth="1.5"
|
|
47
|
+
>
|
|
48
|
+
<rect x="5" y="5" width="9" height="9" rx="1.5" />
|
|
49
|
+
<path d="M11 5V3.5A1.5 1.5 0 009.5 2h-6A1.5 1.5 0 002 3.5v6A1.5 1.5 0 003.5 11H5" />
|
|
50
|
+
</svg>
|
|
51
|
+
</button>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
aria-label="Clear selection"
|
|
55
|
+
onClick={onClear}
|
|
56
|
+
className="flex h-6 w-6 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
|
|
57
|
+
>
|
|
58
|
+
<X size={13} />
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -59,9 +59,11 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
59
59
|
const currentTime = usePlayerStore((s) => s.currentTime);
|
|
60
60
|
const {
|
|
61
61
|
domEditSelection,
|
|
62
|
+
activeGroupElement,
|
|
62
63
|
applyDomSelection,
|
|
63
64
|
updateDomEditHoverSelection,
|
|
64
65
|
handleDomZIndexReorderCommit,
|
|
66
|
+
setActiveGroupElement,
|
|
65
67
|
} = useDomEditContext();
|
|
66
68
|
|
|
67
69
|
const [layers, setLayers] = useState<DomEditLayerItem[]>([]);
|
|
@@ -86,12 +88,16 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
86
88
|
doc.querySelector<HTMLElement>("[data-composition-id]") ?? doc.documentElement ?? null;
|
|
87
89
|
if (!root) return;
|
|
88
90
|
|
|
91
|
+
// A preview reload detaches the drilled-into wrapper; exit drill-in if so.
|
|
92
|
+
if (activeGroupElement && !activeGroupElement.isConnected) setActiveGroupElement(null);
|
|
93
|
+
|
|
89
94
|
const items = collectDomEditLayerItems(root, {
|
|
90
95
|
activeCompositionPath: activeCompPath,
|
|
91
96
|
isMasterView,
|
|
97
|
+
activeGroupElement,
|
|
92
98
|
});
|
|
93
99
|
setLayers(sortLayersByZIndex(items));
|
|
94
|
-
}, [previewIframeRef, activeCompPath, isMasterView]);
|
|
100
|
+
}, [previewIframeRef, activeCompPath, isMasterView, activeGroupElement, setActiveGroupElement]);
|
|
95
101
|
|
|
96
102
|
useEffect(() => {
|
|
97
103
|
collectLayers();
|
|
@@ -135,9 +141,10 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
135
141
|
activeCompositionPath: activeCompPath,
|
|
136
142
|
isMasterView,
|
|
137
143
|
preferClipAncestor: false,
|
|
144
|
+
activeGroupElement,
|
|
138
145
|
});
|
|
139
146
|
},
|
|
140
|
-
[activeCompPath, isMasterView, previewIframeRef],
|
|
147
|
+
[activeCompPath, isMasterView, previewIframeRef, activeGroupElement],
|
|
141
148
|
);
|
|
142
149
|
|
|
143
150
|
const seekToLayer = useCallback(
|
|
@@ -183,6 +190,19 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
183
190
|
[resolveSelection, applyDomSelection, seekToLayer],
|
|
184
191
|
);
|
|
185
192
|
|
|
193
|
+
// Double-click a group row → drill into it; any other row → select it.
|
|
194
|
+
const handleLayerDoubleClick = useCallback(
|
|
195
|
+
async (layer: DomEditLayerItem) => {
|
|
196
|
+
const selection = await resolveSelection(layer);
|
|
197
|
+
if (selection?.element.hasAttribute("data-hf-group")) {
|
|
198
|
+
setActiveGroupElement(selection.element);
|
|
199
|
+
} else {
|
|
200
|
+
await handleSelectLayer(layer);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
[resolveSelection, setActiveGroupElement, handleSelectLayer],
|
|
204
|
+
);
|
|
205
|
+
|
|
186
206
|
const handleLayerHover = useCallback(
|
|
187
207
|
async (layer: DomEditLayerItem | null) => {
|
|
188
208
|
if (!layer) {
|
|
@@ -271,6 +291,18 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
271
291
|
onPointerUp={handleContainerPointerUp}
|
|
272
292
|
onPointerCancel={handleContainerPointerUp}
|
|
273
293
|
>
|
|
294
|
+
{activeGroupElement && (
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
onClick={() => setActiveGroupElement(null)}
|
|
298
|
+
className="flex w-full items-center gap-1.5 px-2 py-1 text-left text-[11px] text-panel-text-3 hover:bg-panel-hover/40 hover:text-panel-text-1"
|
|
299
|
+
>
|
|
300
|
+
<span aria-hidden="true">←</span>
|
|
301
|
+
<span className="truncate">
|
|
302
|
+
{activeGroupElement.getAttribute("data-hf-group") || "Group"}
|
|
303
|
+
</span>
|
|
304
|
+
</button>
|
|
305
|
+
)}
|
|
274
306
|
{visibleLayers.map((layer, index) => {
|
|
275
307
|
const selected = layer.key === selectedKey;
|
|
276
308
|
const isDragged = layer.key === dragKey;
|
|
@@ -286,6 +318,7 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
286
318
|
role="button"
|
|
287
319
|
tabIndex={0}
|
|
288
320
|
onClick={() => !dragKey && handleSelectLayer(layer)}
|
|
321
|
+
onDoubleClick={() => !dragKey && handleLayerDoubleClick(layer)}
|
|
289
322
|
onPointerDown={(e) => handleRowPointerDown(index, e)}
|
|
290
323
|
onPointerEnter={() => !dragKey && handleLayerHover(layer)}
|
|
291
324
|
onKeyDown={(e) => {
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
elementHome,
|
|
22
22
|
hasMotionPathPlugin,
|
|
23
23
|
isPreviewHtmlElement,
|
|
24
|
+
transformWDivisor,
|
|
24
25
|
useMotionPathData,
|
|
25
26
|
} from "./useMotionPathData";
|
|
26
27
|
|
|
@@ -39,6 +40,7 @@ type DragState = {
|
|
|
39
40
|
initX: number;
|
|
40
41
|
initY: number;
|
|
41
42
|
scale: number;
|
|
43
|
+
pScale: number;
|
|
42
44
|
ref: MotionNodeRef;
|
|
43
45
|
};
|
|
44
46
|
|
|
@@ -71,7 +73,7 @@ export const MotionPathOverlay = memo(function MotionPathOverlay({
|
|
|
71
73
|
handleGsapRemoveKeyframe,
|
|
72
74
|
handleGsapDeleteAllForElement,
|
|
73
75
|
} = useDomEditContext();
|
|
74
|
-
const { rect, geometry, geometryResolved, visibleInPreview, home } = useMotionPathData(
|
|
76
|
+
const { rect, geometry, geometryResolved, visibleInPreview, home, pScale } = useMotionPathData(
|
|
75
77
|
iframeRef,
|
|
76
78
|
selectorFor(selection),
|
|
77
79
|
);
|
|
@@ -156,8 +158,12 @@ export const MotionPathOverlay = memo(function MotionPathOverlay({
|
|
|
156
158
|
e.preventDefault();
|
|
157
159
|
const sc = r.width / compW;
|
|
158
160
|
const elHome = elementHome(live);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
// De-magnify: the click lands on the projected (1/m44-magnified) path, so
|
|
162
|
+
// divide the home-relative offset by the perspective factor to recover the
|
|
163
|
+
// stored composition offset (inverse of the `* pScale` applied at draw).
|
|
164
|
+
const ps = 1 / transformWDivisor(live);
|
|
165
|
+
const px = Math.round(((e.clientX - r.left) / sc - elHome.x) / ps);
|
|
166
|
+
const py = Math.round(((e.clientY - r.top) / sc - elHome.y) / ps);
|
|
161
167
|
const t = Math.round(usePlayerStore.getState().currentTime * 100) / 100;
|
|
162
168
|
void commitCreatePath(createSelector, t, px, py, commitMutation);
|
|
163
169
|
setMotionPathArmed(false);
|
|
@@ -232,7 +238,16 @@ export const MotionPathOverlay = memo(function MotionPathOverlay({
|
|
|
232
238
|
: geometry.nodes;
|
|
233
239
|
// ax/ay = absolute composition position (home + offset) for drawing; n.x/n.y
|
|
234
240
|
// stay offsets so the drag commit writes the right tween values.
|
|
235
|
-
|
|
241
|
+
// Magnify the animated offsets by the element's perspective factor (1/m44, via
|
|
242
|
+
// pScale) so the path tracks the *projected* element. `home` is the projection
|
|
243
|
+
// pivot (transform-origin), so it stays put; only the offsets foreshorten. 2D
|
|
244
|
+
// elements have pScale = 1 (no change). Inverse (de-magnify) applied wherever a
|
|
245
|
+
// pointer position is mapped back to a stored offset (create + node drag).
|
|
246
|
+
const abs = nodes.map((n) => ({
|
|
247
|
+
...n,
|
|
248
|
+
ax: home.x + n.x * pScale,
|
|
249
|
+
ay: home.y + n.y * pScale,
|
|
250
|
+
}));
|
|
236
251
|
const points = abs.map((p) => `${p.ax},${p.ay}`).join(" ");
|
|
237
252
|
// Map a VIEWPORT pointer to composition space. Use the iframe's LIVE viewport
|
|
238
253
|
// rect, not `rect` — `rect.left/top` are stored pan-surface-relative (for the
|
|
@@ -264,6 +279,7 @@ export const MotionPathOverlay = memo(function MotionPathOverlay({
|
|
|
264
279
|
initX: x,
|
|
265
280
|
initY: y,
|
|
266
281
|
scale,
|
|
282
|
+
pScale,
|
|
267
283
|
ref,
|
|
268
284
|
};
|
|
269
285
|
setDraft({ index, x, y });
|
|
@@ -273,8 +289,8 @@ export const MotionPathOverlay = memo(function MotionPathOverlay({
|
|
|
273
289
|
if (!d) return;
|
|
274
290
|
setDraft({
|
|
275
291
|
index: d.index,
|
|
276
|
-
x: d.initX + (e.clientX - d.startX) / d.scale,
|
|
277
|
-
y: d.initY + (e.clientY - d.startY) / d.scale,
|
|
292
|
+
x: d.initX + (e.clientX - d.startX) / d.scale / d.pScale,
|
|
293
|
+
y: d.initY + (e.clientY - d.startY) / d.scale / d.pScale,
|
|
278
294
|
});
|
|
279
295
|
};
|
|
280
296
|
// fallow-ignore-next-line complexity
|
|
@@ -286,8 +302,8 @@ export const MotionPathOverlay = memo(function MotionPathOverlay({
|
|
|
286
302
|
if (!animId) return;
|
|
287
303
|
const screenDx = e.clientX - d.startX;
|
|
288
304
|
const screenDy = e.clientY - d.startY;
|
|
289
|
-
const x = Math.round(d.initX + screenDx / d.scale);
|
|
290
|
-
const y = Math.round(d.initY + screenDy / d.scale);
|
|
305
|
+
const x = Math.round(d.initX + screenDx / d.scale / d.pScale);
|
|
306
|
+
const y = Math.round(d.initY + screenDy / d.scale / d.pScale);
|
|
291
307
|
// Click-vs-drag is decided in SCREEN space, not composition px: the old guard
|
|
292
308
|
// compared rounded comp-px, which at high zoom (scale ≫ 1) swallowed real
|
|
293
309
|
// multi-px screen drags whose sub-comp-px delta rounds to 0 → the node would
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Eye, Layers, Move
|
|
2
|
+
import { Eye, Layers, Move } from "../../icons/SystemIcons";
|
|
3
|
+
import { InspectorHeaderActions } from "./InspectorHeaderActions";
|
|
3
4
|
import { useStudioShellContext } from "../../contexts/StudioContext";
|
|
4
5
|
import { readStudioBoxSize, readStudioPathOffset, readStudioRotation } from "./manualEdits";
|
|
5
6
|
import {
|
|
@@ -53,6 +54,7 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
53
54
|
multiSelectCount = 0,
|
|
54
55
|
copiedAgentPrompt: _copiedAgentPrompt,
|
|
55
56
|
onClearSelection,
|
|
57
|
+
onUngroup,
|
|
56
58
|
onSetStyle,
|
|
57
59
|
onSetAttribute,
|
|
58
60
|
onSetAttributeLive,
|
|
@@ -146,6 +148,19 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
146
148
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
149
|
[gsapRuntimeValues, gsapAnimations, element, currentTime],
|
|
148
150
|
);
|
|
151
|
+
// The 3D Transform panel should be reachable on ANY element, not only ones GSAP is
|
|
152
|
+
// already animating — otherwise you can't add depth/rotation to a fresh static
|
|
153
|
+
// element (the panel never appears, the classic chicken-and-egg). Default to
|
|
154
|
+
// identity when there are no runtime values yet; the first edit creates the
|
|
155
|
+
// gsap.set via commitStaticSet, after which real runtime values flow in.
|
|
156
|
+
const gsap3dValues: Record<string, number> = gsapRuntimeValues ?? {
|
|
157
|
+
rotationX: 0,
|
|
158
|
+
rotationY: 0,
|
|
159
|
+
rotationZ: 0,
|
|
160
|
+
z: 0,
|
|
161
|
+
scale: 1,
|
|
162
|
+
transformPerspective: 0,
|
|
163
|
+
};
|
|
149
164
|
|
|
150
165
|
if (!element) {
|
|
151
166
|
return (
|
|
@@ -295,38 +310,13 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
295
310
|
</div>
|
|
296
311
|
<div className="mt-0.5 truncate text-[11px] text-neutral-500">{sourceLabel}</div>
|
|
297
312
|
</div>
|
|
298
|
-
<
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
: "text-neutral-500 hover:bg-neutral-800 hover:text-neutral-300"
|
|
306
|
-
}`}
|
|
307
|
-
title={clipboardCopied ? "Copied!" : "Copy element info to clipboard"}
|
|
308
|
-
>
|
|
309
|
-
<svg
|
|
310
|
-
width="13"
|
|
311
|
-
height="13"
|
|
312
|
-
viewBox="0 0 16 16"
|
|
313
|
-
fill="none"
|
|
314
|
-
stroke="currentColor"
|
|
315
|
-
strokeWidth="1.5"
|
|
316
|
-
>
|
|
317
|
-
<rect x="5" y="5" width="9" height="9" rx="1.5" />
|
|
318
|
-
<path d="M11 5V3.5A1.5 1.5 0 009.5 2h-6A1.5 1.5 0 002 3.5v6A1.5 1.5 0 003.5 11H5" />
|
|
319
|
-
</svg>
|
|
320
|
-
</button>
|
|
321
|
-
<button
|
|
322
|
-
type="button"
|
|
323
|
-
aria-label="Clear selection"
|
|
324
|
-
onClick={onClearSelection}
|
|
325
|
-
className="flex h-6 w-6 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
|
|
326
|
-
>
|
|
327
|
-
<X size={13} />
|
|
328
|
-
</button>
|
|
329
|
-
</div>
|
|
313
|
+
<InspectorHeaderActions
|
|
314
|
+
element={element}
|
|
315
|
+
copied={clipboardCopied}
|
|
316
|
+
onCopy={handleCopyElementInfo}
|
|
317
|
+
onClear={onClearSelection}
|
|
318
|
+
onUngroup={onUngroup}
|
|
319
|
+
/>
|
|
330
320
|
</div>
|
|
331
321
|
</div>
|
|
332
322
|
<div className="flex-1 overflow-y-auto">
|
|
@@ -513,33 +503,31 @@ export const PropertyPanel = memo(function PropertyPanel({
|
|
|
513
503
|
)}
|
|
514
504
|
</div>
|
|
515
505
|
</div>
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
/>
|
|
542
|
-
)}
|
|
506
|
+
<PropertyPanel3dTransform
|
|
507
|
+
gsapRuntimeValues={gsap3dValues}
|
|
508
|
+
gsapAnimId={gsapAnimId}
|
|
509
|
+
resolveAnimIdForProp={animIdForProp}
|
|
510
|
+
gsapKeyframes={navKeyframes}
|
|
511
|
+
currentPct={currentPct}
|
|
512
|
+
elStart={elStart}
|
|
513
|
+
elDuration={elDuration}
|
|
514
|
+
element={element}
|
|
515
|
+
onCommitAnimatedProperty={onCommitAnimatedProperty}
|
|
516
|
+
onCommitAnimatedProperties={onCommitAnimatedProperties}
|
|
517
|
+
onSeekToTime={onSeekToTime}
|
|
518
|
+
onRemoveKeyframe={onRemoveKeyframe}
|
|
519
|
+
onConvertToKeyframes={onConvertToKeyframes}
|
|
520
|
+
onLivePreviewProps={(el, props) => {
|
|
521
|
+
const iframe = iframeRef.current;
|
|
522
|
+
const win = iframe?.contentWindow as
|
|
523
|
+
| { gsap?: { set: (t: Element, v: Record<string, number>) => void } }
|
|
524
|
+
| null
|
|
525
|
+
| undefined;
|
|
526
|
+
const sel = el.id ? `#${el.id}` : el.selector;
|
|
527
|
+
const node = sel ? iframe?.contentDocument?.querySelector(sel) : null;
|
|
528
|
+
if (win?.gsap && node) win.gsap.set(node, props);
|
|
529
|
+
}}
|
|
530
|
+
/>
|
|
543
531
|
<div className="mt-3">
|
|
544
532
|
<div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
|
|
545
533
|
Stacking
|