@hyperframes/studio 0.8.102 → 0.8.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-BuRiFtrF.js → hyperframes-player-DAoaajuM.js} +1 -1
- package/dist/assets/{index-BT3sauee.js → index-BDK4rTMb.js} +1 -1
- package/dist/assets/{index-xB5O0hPw.css → index-CQN3M56N.css} +1 -1
- package/dist/assets/{index-C2utDZt-.js → index-De2aO4XM.js} +179 -179
- package/dist/assets/{index-BM35fjFf.js → index-WV9LroAD.js} +1 -1
- package/dist/{chunk-MTAVNAGW.js → chunk-46XP7J3B.js} +2 -2
- package/dist/{chunk-S65AQH6M.js → chunk-DI4LCJHL.js} +3 -2
- package/dist/chunk-DI4LCJHL.js.map +1 -0
- package/dist/{chunk-7X4XRP3L.js → chunk-HIGHVWYD.js} +2 -2
- package/dist/{domEditingLayers-NU6Z5QWA.js → domEditingLayers-AQBDFNUC.js} +3 -3
- package/dist/index.d.ts +183 -170
- package/dist/index.html +2 -2
- package/dist/index.js +550 -389
- package/dist/index.js.map +1 -1
- package/dist/player.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/components/editor/DomEditOverlay.tsx +7 -7
- package/src/components/editor/PropertyPanel.tsx +5 -5
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +46 -6
- package/src/components/editor/domEditOverlayGestures.ts +14 -20
- package/src/components/editor/domEditOverlayStartGesture.test.ts +170 -13
- package/src/components/editor/domEditOverlayStartGesture.ts +17 -27
- package/src/components/editor/manualEdits.test.ts +3 -9
- package/src/components/editor/manualEdits.ts +0 -1
- package/src/components/editor/manualEditsDom.ts +0 -10
- package/src/components/editor/manualEditsDomPatches.test.ts +1 -45
- package/src/components/editor/manualEditsDomPatches.ts +1 -23
- package/src/components/editor/manualEditsSnapshot.ts +2 -0
- package/src/components/editor/manualEditsTypes.ts +2 -0
- package/src/components/editor/manualOffsetDrag.test.ts +57 -0
- package/src/components/editor/manualOffsetDrag.ts +11 -32
- package/src/components/editor/plainTranslate.test.ts +38 -22
- package/src/components/editor/plainTranslate.ts +75 -40
- package/src/components/editor/previewReadOnly.overlay.test.tsx +30 -0
- package/src/components/editor/previewReadOnlyContext.tsx +3 -1
- package/src/components/editor/propertyPanelTransformCommit.test.ts +80 -0
- package/src/components/editor/propertyPanelTransformCommit.ts +11 -6
- package/src/components/editor/propertyPanelTypes.ts +1 -0
- package/src/components/editor/rotateInlineSave.test.ts +111 -0
- package/src/components/editor/rotationDraft.test.ts +111 -0
- package/src/components/editor/rotationDraft.ts +145 -0
- package/src/components/editor/useColorGradingController.test.ts +36 -1
- package/src/components/editor/useColorGradingController.ts +2 -2
- package/src/components/editor/useDomEditNudge.test.tsx +34 -0
- package/src/components/editor/useDomEditNudge.ts +12 -5
- package/src/components/editor/useDomEditOverlayGestures.ts +22 -24
- package/src/hooks/draggedGsapPosition.ts +28 -0
- package/src/hooks/elementOffsetStager.ts +26 -6
- package/src/hooks/gsapRuntimeBridge.ts +5 -3
- package/src/hooks/gsapRuntimeKeyframes.ts +36 -2
- package/src/hooks/plainRotation.test.ts +122 -0
- package/src/hooks/plainRotation.ts +42 -0
- package/src/hooks/useDomEditSession.ts +5 -5
- package/src/hooks/useDomGeometryCommit.test.tsx +75 -0
- package/src/hooks/useDomGeometryCommit.ts +17 -17
- package/src/hooks/useDomGeometryCommits.test.tsx +56 -1
- package/src/hooks/useDomGeometryCommits.ts +28 -36
- package/src/hooks/useGsapAwareEditing.test.tsx +194 -11
- package/src/hooks/useGsapAwareEditing.ts +43 -42
- package/src/hooks/useInlineTextEdit.test.tsx +33 -3
- package/src/hooks/useInlineTextEdit.ts +2 -2
- package/src/hooks/usePreviewInteraction.test.ts +32 -0
- package/src/hooks/usePreviewInteraction.ts +3 -2
- package/src/player/components/TimelineClipFades.test.tsx +103 -3
- package/src/player/components/TimelineClipFades.tsx +53 -50
- package/src/styles/theme.css +1 -1
- package/src/utils/projectRouting.test.ts +9 -0
- package/src/utils/projectRouting.ts +3 -1
- package/dist/chunk-S65AQH6M.js.map +0 -1
- /package/dist/{chunk-MTAVNAGW.js.map → chunk-46XP7J3B.js.map} +0 -0
- /package/dist/{chunk-7X4XRP3L.js.map → chunk-HIGHVWYD.js.map} +0 -0
- /package/dist/{domEditingLayers-NU6Z5QWA.js.map → domEditingLayers-AQBDFNUC.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.104",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,12 +60,12 @@
|
|
|
60
60
|
"dockview-react": "8.3.1",
|
|
61
61
|
"gsap": "^3.13.0",
|
|
62
62
|
"mediabunny": "^1.45.3",
|
|
63
|
-
"@hyperframes/core": "0.8.
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
68
|
-
"@hyperframes/
|
|
63
|
+
"@hyperframes/core": "0.8.104",
|
|
64
|
+
"@hyperframes/parsers": "0.8.104",
|
|
65
|
+
"@hyperframes/player": "0.8.104",
|
|
66
|
+
"@hyperframes/sdk": "0.8.104",
|
|
67
|
+
"@hyperframes/shader-transitions": "0.8.104",
|
|
68
|
+
"@hyperframes/studio-server": "0.8.104"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "^6.4.2",
|
|
82
82
|
"vitest": "^4.1.11",
|
|
83
83
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
84
|
+
"@hyperframes/producer": "0.8.104"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RotationCommit } from "./rotationDraft";
|
|
1
2
|
import { memo, useEffect, useMemo, useRef, type RefObject } from "react";
|
|
2
3
|
import { type DomEditSelection } from "./domEditing";
|
|
3
4
|
import type { PreviewMouseDownOptions } from "../../hooks/usePreviewInteraction";
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
type BlockedMoveState,
|
|
11
12
|
type DomEditGroupPathOffsetCommit,
|
|
12
13
|
type FocusableDomEditOverlay,
|
|
14
|
+
type MoveCommitOptions,
|
|
13
15
|
type GestureState,
|
|
14
16
|
type GroupGestureState,
|
|
15
17
|
focusDomEditOverlayElement,
|
|
@@ -45,7 +47,7 @@ export {
|
|
|
45
47
|
hasDomEditRotationChanged,
|
|
46
48
|
resolveDomEditRotationGesture,
|
|
47
49
|
} from "./domEditOverlayGestures";
|
|
48
|
-
export type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures";
|
|
50
|
+
export type { DomEditGroupPathOffsetCommit, MoveCommitOptions } from "./domEditOverlayGestures";
|
|
49
51
|
|
|
50
52
|
export interface DomEditOverlayProps {
|
|
51
53
|
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
@@ -76,12 +78,12 @@ export interface DomEditOverlayProps {
|
|
|
76
78
|
selection: DomEditSelection,
|
|
77
79
|
options?: { revealPanel?: boolean; additive?: boolean },
|
|
78
80
|
) => void;
|
|
79
|
-
onBlockedMove: (selection: DomEditSelection) => void;
|
|
81
|
+
onBlockedMove: (selection: DomEditSelection, reason?: string) => void;
|
|
80
82
|
onManualDragStart?: () => void;
|
|
81
83
|
onPathOffsetCommit: (
|
|
82
84
|
selection: DomEditSelection,
|
|
83
85
|
next: { x: number; y: number },
|
|
84
|
-
modifiers?:
|
|
86
|
+
modifiers?: MoveCommitOptions,
|
|
85
87
|
) => Promise<unknown> | void;
|
|
86
88
|
onGroupPathOffsetCommit: (updates: DomEditGroupPathOffsetCommit[]) => Promise<unknown> | void;
|
|
87
89
|
onBoxSizeCommit: (
|
|
@@ -90,10 +92,7 @@ export interface DomEditOverlayProps {
|
|
|
90
92
|
offset?: { x: number; y: number },
|
|
91
93
|
restore?: () => void,
|
|
92
94
|
) => Promise<unknown> | void;
|
|
93
|
-
onRotationCommit: (
|
|
94
|
-
selection: DomEditSelection,
|
|
95
|
-
next: { angle: number },
|
|
96
|
-
) => Promise<unknown> | void;
|
|
95
|
+
onRotationCommit: (selection: DomEditSelection, next: RotationCommit) => Promise<unknown> | void;
|
|
97
96
|
onStyleCommit?: (property: string, value: string) => Promise<unknown> | void;
|
|
98
97
|
recordingState?: GestureRecordingState;
|
|
99
98
|
onToggleRecording?: () => void;
|
|
@@ -296,6 +295,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
296
295
|
gestureRef,
|
|
297
296
|
groupGestureRef,
|
|
298
297
|
blockedMoveRef,
|
|
298
|
+
onBlockedMoveRef,
|
|
299
299
|
onManualDragStartRef,
|
|
300
300
|
onPathOffsetCommitRef,
|
|
301
301
|
onGroupPathOffsetCommitRef,
|
|
@@ -3,8 +3,8 @@ import { memo, useMemo, useRef, useState } from "react";
|
|
|
3
3
|
import { Move } from "../../icons/SystemIcons";
|
|
4
4
|
import { InspectorHeaderActions } from "./InspectorHeaderActions";
|
|
5
5
|
import { useStudioShellContext } from "../../contexts/StudioContext";
|
|
6
|
-
import { readStudioBoxSize
|
|
7
|
-
import { readMoveOffset } from "./plainTranslate";
|
|
6
|
+
import { readStudioBoxSize } from "./manualEdits";
|
|
7
|
+
import { readMoveOffset, readShownRotation } from "./plainTranslate";
|
|
8
8
|
import {
|
|
9
9
|
buildElementInfoText,
|
|
10
10
|
EMPTY_STYLES,
|
|
@@ -178,8 +178,9 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
|
|
|
178
178
|
transformPerspective: 0,
|
|
179
179
|
};
|
|
180
180
|
// Unconditional like the hooks above: must not sit behind the `!element` return below.
|
|
181
|
+
const manualOffset = element ? readMoveOffset(element.element) : { x: 0, y: 0 };
|
|
181
182
|
const { manualOffsetEditingDisabled, manualSizeEditingDisabled, manualRotationEditingDisabled } =
|
|
182
|
-
useManualEditDisabledFlags(element?.capabilities);
|
|
183
|
+
useManualEditDisabledFlags(element?.capabilities, manualOffset);
|
|
183
184
|
|
|
184
185
|
if (!element) {
|
|
185
186
|
return (
|
|
@@ -200,7 +201,6 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
|
|
|
200
201
|
// selection) so the Timing section shows for pure-GSAP elements with no data-start.
|
|
201
202
|
const sections = resolveEditingSections(domEditSelectionToFacts(element, gsapAnimations.length));
|
|
202
203
|
const showEditableSections = element.capabilities.canEditStyles && sections.style;
|
|
203
|
-
const manualOffset = readMoveOffset(element.element);
|
|
204
204
|
const manualSize = readStudioBoxSize(element.element);
|
|
205
205
|
const resolvedWidth =
|
|
206
206
|
manualSize.width > 0
|
|
@@ -210,7 +210,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
|
|
|
210
210
|
manualSize.height > 0
|
|
211
211
|
? manualSize.height
|
|
212
212
|
: (parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height);
|
|
213
|
-
const manualRotation =
|
|
213
|
+
const manualRotation = readShownRotation(element.element);
|
|
214
214
|
|
|
215
215
|
const elStart = Number.parseFloat(element?.dataAttributes?.start ?? "0") || 0;
|
|
216
216
|
const elDuration = Number.parseFloat(element?.dataAttributes?.duration ?? "1") || 0;
|
|
@@ -16,23 +16,49 @@ const ORIGIN_CENTER = {
|
|
|
16
16
|
|
|
17
17
|
// Consistent geometry stub: model the physical truth the real DOM would report.
|
|
18
18
|
// A CSS width/height change grows the box from its top-left, so the rendered
|
|
19
|
-
// center drifts by half the size delta; the
|
|
20
|
-
//
|
|
19
|
+
// center drifts by half the size delta; the element's own translate, which the gesture
|
|
20
|
+
// writes in plain px, pulls it back. `elementCornerOverlayPoints`
|
|
21
21
|
// returns the four corners of that drifted box; `overlayCornersCentroid` (kept
|
|
22
22
|
// real) averages them so the anchor loop can measure the true center each frame.
|
|
23
|
+
// An authored `translate: 25% 25%` follows the box size until the gesture writes px over it.
|
|
24
|
+
const authored = vi.hoisted(() => ({ percent: 0 }));
|
|
25
|
+
function inlineTranslate(element: HTMLElement): { x: number; y: number } | null {
|
|
26
|
+
const value = element.style.getPropertyValue("translate");
|
|
27
|
+
if (!value) return null;
|
|
28
|
+
const [x = 0, y = 0] = value.split(" ").map((v) => Number.parseFloat(v) || 0);
|
|
29
|
+
return { x, y };
|
|
30
|
+
}
|
|
31
|
+
function renderedTranslate(element: HTMLElement, width: number, height: number) {
|
|
32
|
+
return inlineTranslate(element) ?? { x: authored.percent * width, y: authored.percent * height };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
vi.mock("./plainTranslate", async () => {
|
|
36
|
+
const actual = await vi.importActual<typeof import("./plainTranslate")>("./plainTranslate");
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
readTranslatePx: (element: HTMLElement) =>
|
|
40
|
+
renderedTranslate(
|
|
41
|
+
element,
|
|
42
|
+
element.offsetWidth || ORIGIN.width,
|
|
43
|
+
element.offsetHeight || ORIGIN.height,
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
23
48
|
vi.mock("./domEditOverlayGeometry", async () => {
|
|
24
49
|
const actual = await vi.importActual<typeof import("./domEditOverlayGeometry")>(
|
|
25
50
|
"./domEditOverlayGeometry",
|
|
26
51
|
);
|
|
27
|
-
const { readStudioBoxSize
|
|
52
|
+
const { readStudioBoxSize } = await import("./manualEditsDom");
|
|
28
53
|
const physicalCenter = (element: HTMLElement) => {
|
|
29
54
|
const size = readStudioBoxSize(element);
|
|
30
55
|
const width = size.width > 0 ? size.width : ORIGIN.width;
|
|
31
56
|
const height = size.height > 0 ? size.height : ORIGIN.height;
|
|
32
|
-
const offset =
|
|
57
|
+
const offset = renderedTranslate(element, width, height);
|
|
58
|
+
const start = { x: authored.percent * ORIGIN.width, y: authored.percent * ORIGIN.height };
|
|
33
59
|
return {
|
|
34
|
-
x: ORIGIN_CENTER.x + (width - ORIGIN.width) / 2 + offset.x,
|
|
35
|
-
y: ORIGIN_CENTER.y + (height - ORIGIN.height) / 2 + offset.y,
|
|
60
|
+
x: ORIGIN_CENTER.x + (width - ORIGIN.width) / 2 + offset.x - start.x,
|
|
61
|
+
y: ORIGIN_CENTER.y + (height - ORIGIN.height) / 2 + offset.y - start.y,
|
|
36
62
|
width,
|
|
37
63
|
height,
|
|
38
64
|
};
|
|
@@ -189,6 +215,7 @@ async function finishResize(handlers: ReturnType<typeof createDomEditOverlayGest
|
|
|
189
215
|
|
|
190
216
|
afterEach(() => {
|
|
191
217
|
document.body.innerHTML = "";
|
|
218
|
+
authored.percent = 0;
|
|
192
219
|
});
|
|
193
220
|
|
|
194
221
|
describe("anchored corner resize — the release commit feeds the center-pin offset", () => {
|
|
@@ -227,6 +254,19 @@ describe("anchored corner resize — the release commit feeds the center-pin off
|
|
|
227
254
|
expect(offset.y).toBeCloseTo(-(size.height - ORIGIN.height) / 2, 0);
|
|
228
255
|
});
|
|
229
256
|
|
|
257
|
+
it("keeps the centre on the first frame when the authored translate is a percent", async () => {
|
|
258
|
+
const { orientedOverlayRect } = await import("./domEditOverlayGeometry");
|
|
259
|
+
authored.percent = 0.25;
|
|
260
|
+
const { handlers, selection } = buildHarness();
|
|
261
|
+
handlers.startGesture("resize", evt(ORIGIN_CENTER.x + 100, ORIGIN_CENTER.y), {
|
|
262
|
+
resizeHandle: "se",
|
|
263
|
+
});
|
|
264
|
+
handlers.onPointerMove(evt(ORIGIN_CENTER.x + 150, ORIGIN_CENTER.y));
|
|
265
|
+
const rect = orientedOverlayRect(null as never, null as never, selection.element)!;
|
|
266
|
+
expect(rect.left + rect.width / 2).toBeCloseTo(ORIGIN_CENTER.x, 1);
|
|
267
|
+
expect(rect.top + rect.height / 2).toBeCloseTo(ORIGIN_CENTER.y, 1);
|
|
268
|
+
});
|
|
269
|
+
|
|
230
270
|
it("does not log a paused save queue as an ordinary resize failure", async () => {
|
|
231
271
|
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
232
272
|
const { handlers } = buildHarness(() => Promise.reject(new DomEditSaveQueueOpenError()));
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
StudioRotationSnapshot,
|
|
7
7
|
} from "./manualEdits";
|
|
8
8
|
import type { ManualOffsetDragMember } from "./manualOffsetDrag";
|
|
9
|
+
import type { CssRotationTarget, RotationCommit } from "./rotationDraft";
|
|
9
10
|
import type { GroupOverlayItem, OverlayRect } from "./domEditOverlayGeometry";
|
|
10
11
|
import type { SnapContext } from "./snapTargetCollection";
|
|
11
12
|
import type { SnapGuidesState } from "./SnapGuideOverlay";
|
|
@@ -63,27 +64,14 @@ export interface GestureState {
|
|
|
63
64
|
actualWidth: number;
|
|
64
65
|
actualHeight: number;
|
|
65
66
|
actualRotation: number;
|
|
67
|
+
/** Null when GSAP owns the rotate; else where its CSS turn is drawn and saved, read at press. */
|
|
68
|
+
plainRotation: CssRotationTarget | null;
|
|
66
69
|
editScaleX: number;
|
|
67
70
|
editScaleY: number;
|
|
68
|
-
// Rendered
|
|
69
|
-
//
|
|
70
|
-
// follows the cursor instead of overshooting by the live scale.
|
|
71
|
+
// Rendered px per CSS px of the element at gesture start (> 1 under a GSAP scale()); the resize
|
|
72
|
+
// draft divides by it so the box follows the cursor instead of overshooting by the live scale.
|
|
71
73
|
contentScaleX: number;
|
|
72
74
|
contentScaleY: number;
|
|
73
|
-
// Resize anchor pinning: with a live scale transform, growing the CSS box
|
|
74
|
-
// shifts the rendered box (scaling happens around the element center), so the
|
|
75
|
-
// un-dragged corner creeps during the draft. The move handler measures the
|
|
76
|
-
// gesture-start top-left drift each frame and counters it through the GSAP
|
|
77
|
-
// position channel; the pin accumulates so the correction converges.
|
|
78
|
-
// Present only on resize gestures.
|
|
79
|
-
resizeAnchor?: {
|
|
80
|
-
anchorX: number;
|
|
81
|
-
anchorY: number;
|
|
82
|
-
baseGsapX: number;
|
|
83
|
-
baseGsapY: number;
|
|
84
|
-
pinX: number;
|
|
85
|
-
pinY: number;
|
|
86
|
-
};
|
|
87
75
|
manualEditDragToken?: string;
|
|
88
76
|
snapContext?: SnapContext;
|
|
89
77
|
lastSnappedDx?: number;
|
|
@@ -232,9 +220,15 @@ export function hasDomEditRotationChanged(initialAngle: number, nextAngle: numbe
|
|
|
232
220
|
// These live here (rather than in DomEditOverlay.tsx or useDomEditOverlayGestures.ts)
|
|
233
221
|
// to break circular imports between those files.
|
|
234
222
|
|
|
223
|
+
export interface MoveCommitOptions {
|
|
224
|
+
altKey?: boolean;
|
|
225
|
+
plainTranslate?: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
235
228
|
export interface DomEditGroupPathOffsetCommit {
|
|
236
229
|
selection: DomEditSelection;
|
|
237
230
|
next: { x: number; y: number };
|
|
231
|
+
plainTranslate?: boolean;
|
|
238
232
|
}
|
|
239
233
|
|
|
240
234
|
// Refs are stable across renders; values are read via .current.
|
|
@@ -253,13 +247,13 @@ export type UseDomEditOverlayGesturesOptions = {
|
|
|
253
247
|
suppressNextBoxClickRef: RefObject<boolean>;
|
|
254
248
|
setOverlayRect: (next: OverlayRect | null) => void;
|
|
255
249
|
setGroupOverlayItems: (next: GroupOverlayItem[]) => void;
|
|
256
|
-
onBlockedMoveRef: RefObject<(selection: DomEditSelection) => void>;
|
|
250
|
+
onBlockedMoveRef: RefObject<(selection: DomEditSelection, reason?: string) => void>;
|
|
257
251
|
onManualDragStartRef: RefObject<(() => void) | undefined>;
|
|
258
252
|
onPathOffsetCommitRef: RefObject<
|
|
259
253
|
(
|
|
260
254
|
s: DomEditSelection,
|
|
261
255
|
n: { x: number; y: number },
|
|
262
|
-
m?:
|
|
256
|
+
m?: MoveCommitOptions,
|
|
263
257
|
) => Promise<unknown> | void
|
|
264
258
|
>;
|
|
265
259
|
onGroupPathOffsetCommitRef: RefObject<
|
|
@@ -274,7 +268,7 @@ export type UseDomEditOverlayGesturesOptions = {
|
|
|
274
268
|
) => Promise<unknown> | void
|
|
275
269
|
>;
|
|
276
270
|
onRotationCommitRef: RefObject<
|
|
277
|
-
(s: DomEditSelection, n:
|
|
271
|
+
(s: DomEditSelection, n: RotationCommit) => Promise<unknown> | void
|
|
278
272
|
>;
|
|
279
273
|
onCanvasPointerMoveRef: RefObject<
|
|
280
274
|
(
|
|
@@ -20,38 +20,48 @@ const PRESS = {
|
|
|
20
20
|
currentTarget: { setPointerCapture() {} },
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
/** Presses a drag on the element and returns the gesture it started. */
|
|
24
|
-
function
|
|
23
|
+
/** Presses a drag or resize on the element and returns the gesture it started. */
|
|
24
|
+
function pressGesture(element: HTMLElement, kind: "drag" | "resize" = "drag"): GestureState | null {
|
|
25
25
|
const opts = pressOptions(element);
|
|
26
26
|
expect(
|
|
27
|
-
createDomEditOverlayGestureHandlers(opts as never).startGesture(
|
|
27
|
+
createDomEditOverlayGestureHandlers(opts as never).startGesture(kind, PRESS as never),
|
|
28
28
|
).toBe(true);
|
|
29
29
|
return opts.gestureRef.current;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function pressOptions(element: HTMLElement) {
|
|
33
33
|
const ref = <T>(current: T) => ({ current });
|
|
34
|
-
const
|
|
34
|
+
const capabilities = { canApplyManualOffset: true, canApplyManualSize: true };
|
|
35
|
+
const selection = { element, capabilities };
|
|
35
36
|
return {
|
|
36
37
|
selectionRef: ref(selection as unknown as DomEditSelection),
|
|
37
38
|
overlayRectRef: ref({ left: 0, top: 0, width: 240, height: 160, editScaleX: 1, editScaleY: 1 }),
|
|
38
|
-
boxRef: ref(
|
|
39
|
+
boxRef: ref(document.createElement("div")),
|
|
39
40
|
overlayRef: ref(null),
|
|
40
41
|
iframeRef: ref(null),
|
|
41
42
|
gestureRef: ref<GestureState | null>(null),
|
|
42
43
|
rafPausedRef: ref(false),
|
|
43
44
|
onManualDragStartRef: ref(vi.fn()),
|
|
44
45
|
onBlockedMoveRef: ref(vi.fn()),
|
|
46
|
+
onPathOffsetCommitRef: ref(vi.fn()),
|
|
47
|
+
snapGuidesRef: ref(null),
|
|
48
|
+
groupGestureRef: ref(null),
|
|
49
|
+
blockedMoveRef: ref(null),
|
|
50
|
+
setOverlayRect: vi.fn(),
|
|
51
|
+
suppressNextBoxClickRef: ref(false),
|
|
52
|
+
hoverSelectionRef: ref(null),
|
|
53
|
+
onCanvasMouseDown: vi.fn(),
|
|
45
54
|
};
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
describe("a drag press on a page that loads GSAP", () => {
|
|
57
|
+
describe("a drag or resize press on a page that loads GSAP", () => {
|
|
49
58
|
it.each([
|
|
50
|
-
["GSAP animates nothing", false],
|
|
51
|
-
["GSAP animates only its parent", true],
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
["drag", "GSAP animates nothing", false],
|
|
60
|
+
["drag", "GSAP animates only its parent", true],
|
|
61
|
+
["resize", "GSAP animates nothing", false],
|
|
62
|
+
] as const)(
|
|
63
|
+
"%s, %s: the press never asks GSAP about the element, so its translate stays CSS",
|
|
64
|
+
(kind, _, parentTween) => {
|
|
55
65
|
const getProperty = vi.fn(() => 0);
|
|
56
66
|
const set = vi.fn();
|
|
57
67
|
const parent = document.createElement("div");
|
|
@@ -62,7 +72,7 @@ describe("a drag press on a page that loads GSAP", () => {
|
|
|
62
72
|
const tween = { targets: () => [parent], vars: { x: 100 }, duration: () => 2 };
|
|
63
73
|
const timelines = { main: { getChildren: () => (parentTween ? [tween] : []) } };
|
|
64
74
|
Object.assign(window, { gsap: { getProperty, set }, __timelines: timelines });
|
|
65
|
-
expect(
|
|
75
|
+
expect(pressGesture(element, kind)?.pathOffsetMember?.plainTranslate).toBe(true);
|
|
66
76
|
expect(getProperty).not.toHaveBeenCalled();
|
|
67
77
|
expect(set).not.toHaveBeenCalled();
|
|
68
78
|
expect(element.style.getPropertyValue("translate")).toBe("40px 30px");
|
|
@@ -77,7 +87,154 @@ describe("a drag press on a centred element without GSAP", () => {
|
|
|
77
87
|
"position: absolute; left: 50%; top: 50%; width: 240px; height: 160px; translate: -50% -50%";
|
|
78
88
|
document.body.append(element);
|
|
79
89
|
const style = element.getAttribute("style");
|
|
80
|
-
expect(
|
|
90
|
+
expect(pressGesture(element)?.pathOffsetMember?.initialOffset).toEqual({ x: -120, y: -80 });
|
|
81
91
|
expect(element.getAttribute("style")).toBe(style);
|
|
82
92
|
});
|
|
83
93
|
});
|
|
94
|
+
|
|
95
|
+
describe("a drag on an element without GSAP", () => {
|
|
96
|
+
it("drops on the route it chose at press, even if GSAP takes the element over mid-drag", () => {
|
|
97
|
+
const element = document.createElement("div");
|
|
98
|
+
element.style.setProperty("translate", "40px 30px");
|
|
99
|
+
document.body.append(element);
|
|
100
|
+
const opts = pressOptions(element);
|
|
101
|
+
const handlers = createDomEditOverlayGestureHandlers(opts as never);
|
|
102
|
+
expect(handlers.startGesture("drag", PRESS as never)).toBe(true);
|
|
103
|
+
Object.assign(element, { _gsap: { renderTransform: () => {} } });
|
|
104
|
+
const release = {
|
|
105
|
+
...PRESS,
|
|
106
|
+
clientX: 110,
|
|
107
|
+
clientY: 70,
|
|
108
|
+
currentTarget: { releasePointerCapture() {} },
|
|
109
|
+
};
|
|
110
|
+
handlers.onPointerUp(release as never);
|
|
111
|
+
expect(opts.onPathOffsetCommitRef.current).toHaveBeenCalledWith(
|
|
112
|
+
expect.anything(),
|
|
113
|
+
expect.anything(),
|
|
114
|
+
expect.objectContaining({ plainTranslate: true }),
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("a group drag of elements without GSAP", () => {
|
|
120
|
+
it("drops every member on the route it chose at press", () => {
|
|
121
|
+
const items = [0, 1].map((i) => {
|
|
122
|
+
const element = document.createElement("div");
|
|
123
|
+
element.style.setProperty("translate", `${i * 10}px 0px`);
|
|
124
|
+
document.body.append(element);
|
|
125
|
+
const selection = { element, capabilities: { canApplyManualOffset: true } };
|
|
126
|
+
const rect = { left: 0, top: 0, width: 240, height: 160, editScaleX: 1, editScaleY: 1 };
|
|
127
|
+
return { key: `m${i}`, selection, element, rect };
|
|
128
|
+
});
|
|
129
|
+
const onGroupPathOffsetCommit = vi.fn();
|
|
130
|
+
const opts = {
|
|
131
|
+
...pressOptions(items[0]!.element),
|
|
132
|
+
groupOverlayItemsRef: { current: items },
|
|
133
|
+
onGroupPathOffsetCommitRef: { current: onGroupPathOffsetCommit },
|
|
134
|
+
setGroupOverlayItems: vi.fn(),
|
|
135
|
+
};
|
|
136
|
+
const handlers = createDomEditOverlayGestureHandlers(opts as never);
|
|
137
|
+
expect(handlers.startGroupDrag(PRESS as never)).toBe(true);
|
|
138
|
+
const release = {
|
|
139
|
+
...PRESS,
|
|
140
|
+
clientX: 110,
|
|
141
|
+
clientY: 70,
|
|
142
|
+
currentTarget: { releasePointerCapture() {} },
|
|
143
|
+
};
|
|
144
|
+
handlers.onPointerUp(release as never);
|
|
145
|
+
const updates = onGroupPathOffsetCommit.mock.calls[0]?.[0] as { plainTranslate?: boolean }[];
|
|
146
|
+
expect(updates.map((update) => update.plainTranslate)).toEqual([true, true]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("a rotate on a page that loads GSAP", () => {
|
|
151
|
+
it("never asks GSAP about an element it does not turn, and draws the turn as its CSS rotate", () => {
|
|
152
|
+
const getProperty = vi.fn(() => 0);
|
|
153
|
+
const set = vi.fn();
|
|
154
|
+
const element = document.createElement("div");
|
|
155
|
+
element.style.setProperty("rotate", "30deg");
|
|
156
|
+
document.body.append(element);
|
|
157
|
+
Object.assign(window, {
|
|
158
|
+
gsap: { getProperty, set },
|
|
159
|
+
__timelines: { main: { getChildren: () => [] } },
|
|
160
|
+
});
|
|
161
|
+
const selection = { element, capabilities: { canApplyManualRotation: true } };
|
|
162
|
+
const ref = <T>(current: T) => ({ current });
|
|
163
|
+
const opts = {
|
|
164
|
+
selectionRef: ref(selection as unknown as DomEditSelection),
|
|
165
|
+
overlayRectRef: ref({ left: 0, top: 0, width: 50, height: 40, editScaleX: 1, editScaleY: 1 }),
|
|
166
|
+
boxRef: ref(document.createElement("div")),
|
|
167
|
+
overlayRef: ref(null),
|
|
168
|
+
iframeRef: ref(null),
|
|
169
|
+
gestureRef: ref<GestureState | null>(null),
|
|
170
|
+
groupGestureRef: ref(null),
|
|
171
|
+
blockedMoveRef: ref(null),
|
|
172
|
+
rafPausedRef: ref(false),
|
|
173
|
+
onCanvasPointerMoveRef: ref(vi.fn()),
|
|
174
|
+
};
|
|
175
|
+
const pointer = (clientX: number, clientY: number) => ({
|
|
176
|
+
clientX,
|
|
177
|
+
clientY,
|
|
178
|
+
pointerId: 1,
|
|
179
|
+
button: 0,
|
|
180
|
+
shiftKey: false,
|
|
181
|
+
preventDefault() {},
|
|
182
|
+
stopPropagation() {},
|
|
183
|
+
currentTarget: { setPointerCapture() {} },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const handlers = createDomEditOverlayGestureHandlers(opts as never);
|
|
187
|
+
expect(handlers.startGesture("rotate", pointer(25, -20) as never)).toBe(true);
|
|
188
|
+
expect(opts.gestureRef.current?.plainRotation).toEqual({
|
|
189
|
+
property: "rotate",
|
|
190
|
+
before: "",
|
|
191
|
+
after: "",
|
|
192
|
+
share: 0,
|
|
193
|
+
sign: 1,
|
|
194
|
+
inline: false,
|
|
195
|
+
});
|
|
196
|
+
expect(opts.gestureRef.current?.actualRotation).toBeCloseTo(30);
|
|
197
|
+
handlers.onPointerMove(pointer(60, 20) as never);
|
|
198
|
+
|
|
199
|
+
expect(element.style.getPropertyValue("rotate")).toMatch(/deg$/);
|
|
200
|
+
expect(element.style.getPropertyValue("rotate")).not.toBe("30deg");
|
|
201
|
+
expect(getProperty).not.toHaveBeenCalled();
|
|
202
|
+
expect(set).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("a rotate press on an element a GSAP tween turns", () => {
|
|
207
|
+
it.each(["rotation", "rotate", "rotateZ"])("reads its base from GSAP (%s)", (channel) => {
|
|
208
|
+
const getProperty = vi.fn(() => 40);
|
|
209
|
+
const element = document.createElement("div");
|
|
210
|
+
document.body.append(element);
|
|
211
|
+
const tween = { targets: () => [element], vars: { [channel]: 40 }, duration: () => 2 };
|
|
212
|
+
const timelines = { main: { getChildren: () => [tween] } };
|
|
213
|
+
Object.assign(window, { gsap: { getProperty, set: vi.fn() }, __timelines: timelines });
|
|
214
|
+
const ref = <T>(current: T) => ({ current });
|
|
215
|
+
const selection = { element, capabilities: { canApplyManualRotation: true } };
|
|
216
|
+
const opts = {
|
|
217
|
+
selectionRef: ref(selection as unknown as DomEditSelection),
|
|
218
|
+
overlayRectRef: ref({ left: 0, top: 0, width: 50, height: 40, editScaleX: 1, editScaleY: 1 }),
|
|
219
|
+
boxRef: ref(document.createElement("div")),
|
|
220
|
+
overlayRef: ref(null),
|
|
221
|
+
iframeRef: ref(null),
|
|
222
|
+
gestureRef: ref<GestureState | null>(null),
|
|
223
|
+
rafPausedRef: ref(false),
|
|
224
|
+
};
|
|
225
|
+
const press = {
|
|
226
|
+
clientX: 25,
|
|
227
|
+
clientY: -20,
|
|
228
|
+
pointerId: 1,
|
|
229
|
+
button: 0,
|
|
230
|
+
preventDefault() {},
|
|
231
|
+
stopPropagation() {},
|
|
232
|
+
currentTarget: { setPointerCapture() {} },
|
|
233
|
+
};
|
|
234
|
+
const handlers = createDomEditOverlayGestureHandlers(opts as never);
|
|
235
|
+
expect(handlers.startGesture("rotate", press as never)).toBe(true);
|
|
236
|
+
expect(opts.gestureRef.current?.plainRotation).toBeNull();
|
|
237
|
+
expect(opts.gestureRef.current?.actualRotation).toBe(40);
|
|
238
|
+
expect(getProperty).toHaveBeenCalledWith(element, "rotation");
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -2,21 +2,20 @@
|
|
|
2
2
|
* Gesture-begin functions: startGroupDrag and startGesture.
|
|
3
3
|
* These are pure "start a new gesture" operations — no draft rect updates.
|
|
4
4
|
*/
|
|
5
|
-
import { readElementGsapNumber } from "../../utils/elementGsap";
|
|
6
5
|
import { type DomEditSelection } from "./domEditing";
|
|
7
6
|
import {
|
|
7
|
+
applyManualOffsetDragDraft,
|
|
8
8
|
createManualOffsetDragMember,
|
|
9
|
-
readGsapRotation,
|
|
10
9
|
restoreManualOffsetDragMembers,
|
|
11
10
|
type ManualOffsetDragMember,
|
|
12
11
|
} from "./manualOffsetDrag";
|
|
12
|
+
import { readCssRotationTarget, readRotationBase } from "./rotationDraft";
|
|
13
13
|
import {
|
|
14
14
|
beginStudioManualEditGesture,
|
|
15
15
|
captureStudioBoxSize,
|
|
16
16
|
captureStudioPathOffset,
|
|
17
17
|
captureStudioRotation,
|
|
18
18
|
readStudioBoxSize,
|
|
19
|
-
readStudioRotation,
|
|
20
19
|
} from "./manualEdits";
|
|
21
20
|
import {
|
|
22
21
|
type OverlayRect,
|
|
@@ -32,6 +31,7 @@ import {
|
|
|
32
31
|
type UseDomEditOverlayGesturesOptions,
|
|
33
32
|
} from "./domEditOverlayGestures";
|
|
34
33
|
import { collectSnapContext, buildExcludeElements } from "./snapTargetCollection";
|
|
34
|
+
import { gsapWritesRotation } from "../../hooks/gsapRuntimeKeyframes";
|
|
35
35
|
import { logResize, resetResizeMoveLog } from "../../utils/resizeDebug";
|
|
36
36
|
import { logDrag, readDragPositions, resetDragMoveLog } from "../../utils/dragDebug";
|
|
37
37
|
|
|
@@ -76,7 +76,7 @@ export function startGroupDrag(
|
|
|
76
76
|
restoreManualOffsetDragMembers(members);
|
|
77
77
|
e.preventDefault();
|
|
78
78
|
e.stopPropagation();
|
|
79
|
-
opts.onBlockedMoveRef.current(result.selection);
|
|
79
|
+
opts.onBlockedMoveRef.current(result.selection, result.reason);
|
|
80
80
|
return false;
|
|
81
81
|
}
|
|
82
82
|
members.push(result.member);
|
|
@@ -171,18 +171,6 @@ export function startGesture(
|
|
|
171
171
|
Number.isFinite(rawContentScaleX) && rawContentScaleX > 0 ? rawContentScaleX : 1;
|
|
172
172
|
const contentScaleY =
|
|
173
173
|
Number.isFinite(rawContentScaleY) && rawContentScaleY > 0 ? rawContentScaleY : 1;
|
|
174
|
-
let resizeAnchor: GestureState["resizeAnchor"];
|
|
175
|
-
if (kind === "resize") {
|
|
176
|
-
const startBcr = sel.element.getBoundingClientRect();
|
|
177
|
-
resizeAnchor = {
|
|
178
|
-
anchorX: startBcr.x,
|
|
179
|
-
anchorY: startBcr.y,
|
|
180
|
-
baseGsapX: readElementGsapNumber(sel.element, "x") ?? 0,
|
|
181
|
-
baseGsapY: readElementGsapNumber(sel.element, "y") ?? 0,
|
|
182
|
-
pinX: 0,
|
|
183
|
-
pinY: 0,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
174
|
let initialPathOffset = captureStudioPathOffset(sel.element);
|
|
187
175
|
let manualEditDragToken: string | undefined;
|
|
188
176
|
let pathOffsetMember: ManualOffsetDragMember | undefined;
|
|
@@ -198,18 +186,16 @@ export function startGesture(
|
|
|
198
186
|
gesture: "drag",
|
|
199
187
|
});
|
|
200
188
|
if (!result.ok) {
|
|
201
|
-
opts.onBlockedMoveRef.current(result.selection);
|
|
189
|
+
opts.onBlockedMoveRef.current(result.selection, result.reason);
|
|
202
190
|
return false;
|
|
203
191
|
}
|
|
204
192
|
pathOffsetMember = result.member;
|
|
205
193
|
initialPathOffset = result.member.initialPathOffset;
|
|
206
194
|
manualEditDragToken = result.member.gestureToken;
|
|
207
195
|
} else {
|
|
208
|
-
// Center-anchored corner resize (CapCut model): the element scales about its
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
// its center per frame (the memberless else-branch is only a defensive fallback
|
|
212
|
-
// if member creation fails, e.g. the element can't take a manual offset).
|
|
196
|
+
// Center-anchored corner resize (CapCut model): the element scales about its planted CENTER,
|
|
197
|
+
// so every corner needs the member that re-pins the center per frame (the memberless
|
|
198
|
+
// branch is only a fallback for an element that can't take a manual offset).
|
|
213
199
|
const needsAnchorOffset = kind === "resize" && sel.capabilities.canApplyManualOffset;
|
|
214
200
|
if (needsAnchorOffset) {
|
|
215
201
|
const result = createManualOffsetDragMember({
|
|
@@ -217,11 +203,14 @@ export function startGesture(
|
|
|
217
203
|
selection: sel,
|
|
218
204
|
element: sel.element,
|
|
219
205
|
rect,
|
|
206
|
+
gesture: "resize",
|
|
220
207
|
});
|
|
221
208
|
if (result.ok) {
|
|
222
209
|
pathOffsetMember = result.member;
|
|
223
210
|
initialPathOffset = result.member.initialPathOffset;
|
|
224
211
|
manualEditDragToken = result.member.gestureToken;
|
|
212
|
+
// Hold a % translate as the same px now, so a growing box can't drag it along mid-frame.
|
|
213
|
+
if (result.member.plainTranslate) applyManualOffsetDragDraft(result.member, 0, 0);
|
|
225
214
|
} else {
|
|
226
215
|
manualEditDragToken = beginStudioManualEditGesture(sel.element);
|
|
227
216
|
}
|
|
@@ -230,10 +219,11 @@ export function startGesture(
|
|
|
230
219
|
}
|
|
231
220
|
}
|
|
232
221
|
|
|
233
|
-
// Rotation base
|
|
234
|
-
// move never asks GSAP: reading a property makes it bake the CSS
|
|
235
|
-
const
|
|
236
|
-
const
|
|
222
|
+
// Rotation base: the angle the element shows. An element GSAP does not turn, or a plain-translate
|
|
223
|
+
// move, never asks GSAP: reading a property makes it bake the CSS into its transform.
|
|
224
|
+
const plain = !!pathOffsetMember?.plainTranslate || !gsapWritesRotation(sel.element);
|
|
225
|
+
const plainRotation = plain && kind === "rotate" ? readCssRotationTarget(sel.element) : null;
|
|
226
|
+
const rotation = { angle: readRotationBase(sel.element, plain) };
|
|
237
227
|
const overlayBounds = overlayEl?.getBoundingClientRect();
|
|
238
228
|
const centerX = (overlayBounds?.left ?? 0) + rect.left + rect.width / 2;
|
|
239
229
|
const centerY = (overlayBounds?.top ?? 0) + rect.top + rect.height / 2;
|
|
@@ -281,11 +271,11 @@ export function startGesture(
|
|
|
281
271
|
actualWidth,
|
|
282
272
|
actualHeight,
|
|
283
273
|
actualRotation: rotation.angle,
|
|
274
|
+
plainRotation,
|
|
284
275
|
editScaleX: rect.editScaleX,
|
|
285
276
|
editScaleY: rect.editScaleY,
|
|
286
277
|
contentScaleX,
|
|
287
278
|
contentScaleY,
|
|
288
|
-
resizeAnchor,
|
|
289
279
|
manualEditDragToken,
|
|
290
280
|
snapContext,
|
|
291
281
|
resizeHandle: kind === "resize" ? (options?.resizeHandle ?? "se") : undefined,
|