@hyperframes/studio 0.6.73 → 0.6.75
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-DcyZuBcU.css +1 -0
- package/dist/assets/index-uB_W2GDl.js +140 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +30 -24
- package/src/components/StudioPreviewArea.tsx +101 -26
- package/src/components/StudioRightPanel.tsx +3 -0
- package/src/components/StudioToast.tsx +18 -0
- package/src/components/TimelineToolbar.tsx +230 -4
- package/src/components/editor/AnimationCard.tsx +68 -4
- package/src/components/editor/DomEditOverlay.tsx +70 -1
- package/src/components/editor/GridOverlay.tsx +50 -0
- package/src/components/editor/KeyframeDiamond.tsx +49 -0
- package/src/components/editor/KeyframeNavigation.tsx +139 -0
- package/src/components/editor/LayersPanel.test.ts +135 -0
- package/src/components/editor/LayersPanel.tsx +151 -15
- package/src/components/editor/PropertyPanel.tsx +293 -140
- package/src/components/editor/SnapGuideOverlay.tsx +166 -0
- package/src/components/editor/SnapToolbar.tsx +163 -0
- package/src/components/editor/SpringEaseEditor.tsx +256 -0
- package/src/components/editor/domEditOverlayGestures.ts +7 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +28 -0
- package/src/components/editor/gsapAnimationConstants.ts +42 -0
- package/src/components/editor/gsapAnimationHelpers.ts +2 -1
- package/src/components/editor/manualEditingAvailability.ts +6 -0
- package/src/components/editor/manualEditsDom.ts +56 -2
- package/src/components/editor/manualOffsetDrag.ts +19 -3
- package/src/components/editor/propertyPanelHelpers.ts +90 -0
- package/src/components/editor/propertyPanelTimingSection.tsx +64 -0
- package/src/components/editor/snapEngine.test.ts +657 -0
- package/src/components/editor/snapEngine.ts +575 -0
- package/src/components/editor/snapTargetCollection.ts +147 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +137 -10
- package/src/components/editor/useLayerDrag.ts +213 -0
- package/src/components/nle/NLELayout.tsx +18 -0
- package/src/contexts/DomEditContext.tsx +27 -0
- package/src/hooks/gsapRuntimeBridge.ts +585 -0
- package/src/hooks/gsapRuntimeKeyframes.ts +170 -0
- package/src/hooks/useAnimatedPropertyCommit.ts +131 -0
- package/src/hooks/useAppHotkeys.ts +63 -1
- package/src/hooks/useDomEditCommits.ts +88 -4
- package/src/hooks/useDomEditSession.ts +179 -65
- package/src/hooks/useGsapScriptCommits.ts +144 -7
- package/src/hooks/useGsapSelectionHandlers.ts +202 -0
- package/src/hooks/useGsapTweenCache.ts +174 -3
- package/src/hooks/useTimelineEditing.ts +93 -0
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/ClipContextMenu.tsx +99 -0
- package/src/player/components/KeyframeDiamondContextMenu.tsx +164 -0
- package/src/player/components/Timeline.test.ts +2 -1
- package/src/player/components/Timeline.tsx +108 -68
- package/src/player/components/TimelineCanvas.tsx +47 -1
- package/src/player/components/TimelineClip.tsx +8 -3
- package/src/player/components/TimelineClipDiamonds.tsx +174 -0
- package/src/player/components/timelineDragDrop.ts +103 -0
- package/src/player/components/timelineLayout.ts +1 -1
- package/src/player/store/playerStore.ts +42 -0
- package/src/utils/editHistory.ts +1 -1
- package/src/utils/optimisticUpdate.test.ts +53 -0
- package/src/utils/optimisticUpdate.ts +18 -0
- package/src/utils/studioUiPreferences.ts +17 -0
- package/dist/assets/index-CrxThtSJ.css +0 -1
- package/dist/assets/index-Dc2HfqON.js +0 -140
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// fallow-ignore-file code-duplication
|
|
1
2
|
/**
|
|
2
3
|
* Gesture handling for DomEditOverlay.
|
|
3
4
|
* Owns: onPointerMove, onPointerUp, clearPointerState.
|
|
@@ -23,7 +24,12 @@ import {
|
|
|
23
24
|
restoreStudioPathOffset,
|
|
24
25
|
restoreStudioRotation,
|
|
25
26
|
} from "./manualEdits";
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
type GroupOverlayItem,
|
|
29
|
+
type OverlayRect,
|
|
30
|
+
resolveDomEditGroupOverlayRect,
|
|
31
|
+
toOverlayRect,
|
|
32
|
+
} from "./domEditOverlayGeometry";
|
|
27
33
|
import {
|
|
28
34
|
BLOCKED_MOVE_THRESHOLD_PX,
|
|
29
35
|
type BlockedMoveState,
|
|
@@ -39,6 +45,13 @@ import {
|
|
|
39
45
|
startGesture as _startGesture,
|
|
40
46
|
startGroupDrag as _startGroupDrag,
|
|
41
47
|
} from "./domEditOverlayStartGesture";
|
|
48
|
+
import {
|
|
49
|
+
resolveSnapAdjustment,
|
|
50
|
+
resolveResizeSnapAdjustment,
|
|
51
|
+
resolveEquidistanceGuides,
|
|
52
|
+
SNAP_THRESHOLD_PX,
|
|
53
|
+
} from "./snapEngine";
|
|
54
|
+
import type { SnapGuidesState } from "./SnapGuideOverlay";
|
|
42
55
|
|
|
43
56
|
// Refs are stable across renders; values are read via .current.
|
|
44
57
|
export type UseDomEditOverlayGesturesOptions = {
|
|
@@ -79,6 +92,7 @@ export type UseDomEditOverlayGesturesOptions = {
|
|
|
79
92
|
e: React.MouseEvent<HTMLDivElement>,
|
|
80
93
|
o?: { preferClipAncestor?: boolean },
|
|
81
94
|
) => void;
|
|
95
|
+
snapGuidesRef: RefObject<SnapGuidesState | null>;
|
|
82
96
|
};
|
|
83
97
|
|
|
84
98
|
export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGesturesOptions) {
|
|
@@ -111,6 +125,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
111
125
|
options?: { selection?: DomEditSelection; rect?: OverlayRect | null },
|
|
112
126
|
) => _startGesture(kind, e, opts, options);
|
|
113
127
|
|
|
128
|
+
// fallow-ignore-next-line complexity
|
|
114
129
|
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
115
130
|
const g = opts.gestureRef.current;
|
|
116
131
|
const groupG = opts.groupGestureRef.current;
|
|
@@ -133,8 +148,48 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
133
148
|
}
|
|
134
149
|
|
|
135
150
|
if (groupG) {
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
let dx = e.clientX - groupG.startX;
|
|
152
|
+
let dy = e.clientY - groupG.startY;
|
|
153
|
+
|
|
154
|
+
const sc = groupG.snapContext;
|
|
155
|
+
if (sc?.snapEnabled && sc.targets.length > 0) {
|
|
156
|
+
const groupBounds = resolveDomEditGroupOverlayRect(
|
|
157
|
+
groupG.originItems.map((item) => item.rect),
|
|
158
|
+
);
|
|
159
|
+
if (groupBounds) {
|
|
160
|
+
const allTargets = sc.compositionTarget
|
|
161
|
+
? [...sc.targets, sc.compositionTarget]
|
|
162
|
+
: sc.targets;
|
|
163
|
+
const snap = resolveSnapAdjustment({
|
|
164
|
+
movingRect: groupBounds,
|
|
165
|
+
proposedDx: dx,
|
|
166
|
+
proposedDy: dy,
|
|
167
|
+
targets: allTargets,
|
|
168
|
+
gridEdges: sc.gridEdges ?? undefined,
|
|
169
|
+
threshold: SNAP_THRESHOLD_PX,
|
|
170
|
+
disabled: e.altKey,
|
|
171
|
+
});
|
|
172
|
+
dx = snap.dx;
|
|
173
|
+
dy = snap.dy;
|
|
174
|
+
const movedRect = {
|
|
175
|
+
left: groupBounds.left + dx,
|
|
176
|
+
top: groupBounds.top + dy,
|
|
177
|
+
width: groupBounds.width,
|
|
178
|
+
height: groupBounds.height,
|
|
179
|
+
};
|
|
180
|
+
const spacingGuides = e.altKey
|
|
181
|
+
? []
|
|
182
|
+
: resolveEquidistanceGuides({
|
|
183
|
+
movingRect: movedRect,
|
|
184
|
+
targets: allTargets,
|
|
185
|
+
threshold: SNAP_THRESHOLD_PX,
|
|
186
|
+
});
|
|
187
|
+
opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
groupG.lastSnappedDx = dx;
|
|
191
|
+
groupG.lastSnappedDy = dy;
|
|
192
|
+
|
|
138
193
|
setDraftGroupOverlayItems(
|
|
139
194
|
groupG.originItems.map((item) => ({
|
|
140
195
|
...item,
|
|
@@ -146,8 +201,8 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
146
201
|
}
|
|
147
202
|
|
|
148
203
|
if (!g || !sel) return;
|
|
149
|
-
|
|
150
|
-
|
|
204
|
+
let dx = e.clientX - g.startX;
|
|
205
|
+
let dy = e.clientY - g.startY;
|
|
151
206
|
|
|
152
207
|
if (g.kind === "rotate") {
|
|
153
208
|
applyStudioRotationDraft(
|
|
@@ -167,6 +222,46 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
167
222
|
}
|
|
168
223
|
|
|
169
224
|
if (g.kind === "drag") {
|
|
225
|
+
const sc = g.snapContext;
|
|
226
|
+
if (sc?.snapEnabled && sc.targets.length > 0) {
|
|
227
|
+
const movingRect = {
|
|
228
|
+
left: g.originLeft,
|
|
229
|
+
top: g.originTop,
|
|
230
|
+
width: g.originWidth,
|
|
231
|
+
height: g.originHeight,
|
|
232
|
+
};
|
|
233
|
+
const allTargets = sc.compositionTarget
|
|
234
|
+
? [...sc.targets, sc.compositionTarget]
|
|
235
|
+
: sc.targets;
|
|
236
|
+
const snap = resolveSnapAdjustment({
|
|
237
|
+
movingRect,
|
|
238
|
+
proposedDx: dx,
|
|
239
|
+
proposedDy: dy,
|
|
240
|
+
targets: allTargets,
|
|
241
|
+
gridEdges: sc.gridEdges ?? undefined,
|
|
242
|
+
threshold: SNAP_THRESHOLD_PX,
|
|
243
|
+
disabled: e.altKey,
|
|
244
|
+
});
|
|
245
|
+
dx = snap.dx;
|
|
246
|
+
dy = snap.dy;
|
|
247
|
+
const movedRect = {
|
|
248
|
+
left: movingRect.left + dx,
|
|
249
|
+
top: movingRect.top + dy,
|
|
250
|
+
width: movingRect.width,
|
|
251
|
+
height: movingRect.height,
|
|
252
|
+
};
|
|
253
|
+
const spacingGuides = e.altKey
|
|
254
|
+
? []
|
|
255
|
+
: resolveEquidistanceGuides({
|
|
256
|
+
movingRect: movedRect,
|
|
257
|
+
targets: allTargets,
|
|
258
|
+
threshold: SNAP_THRESHOLD_PX,
|
|
259
|
+
});
|
|
260
|
+
opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides };
|
|
261
|
+
}
|
|
262
|
+
g.lastSnappedDx = dx;
|
|
263
|
+
g.lastSnappedDy = dy;
|
|
264
|
+
|
|
170
265
|
const nextBoxLeft = g.originLeft + dx;
|
|
171
266
|
const nextBoxTop = g.originTop + dy;
|
|
172
267
|
setDraftOverlayRect({
|
|
@@ -184,6 +279,32 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
184
279
|
if (g.pathOffsetMember) applyManualOffsetDragDraft(g.pathOffsetMember, dx, dy);
|
|
185
280
|
} else {
|
|
186
281
|
if (!box) return;
|
|
282
|
+
|
|
283
|
+
const sc = g.snapContext;
|
|
284
|
+
if (sc?.snapEnabled && sc.targets.length > 0) {
|
|
285
|
+
const movingRect = {
|
|
286
|
+
left: g.originLeft,
|
|
287
|
+
top: g.originTop,
|
|
288
|
+
width: g.originWidth,
|
|
289
|
+
height: g.originHeight,
|
|
290
|
+
};
|
|
291
|
+
const allTargets = sc.compositionTarget
|
|
292
|
+
? [...sc.targets, sc.compositionTarget]
|
|
293
|
+
: sc.targets;
|
|
294
|
+
const snap = resolveResizeSnapAdjustment({
|
|
295
|
+
movingRect,
|
|
296
|
+
proposedDx: dx,
|
|
297
|
+
proposedDy: dy,
|
|
298
|
+
targets: allTargets,
|
|
299
|
+
gridEdges: sc.gridEdges ?? undefined,
|
|
300
|
+
threshold: SNAP_THRESHOLD_PX,
|
|
301
|
+
disabled: e.altKey,
|
|
302
|
+
});
|
|
303
|
+
dx = snap.dx;
|
|
304
|
+
dy = snap.dy;
|
|
305
|
+
opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides: [] };
|
|
306
|
+
}
|
|
307
|
+
|
|
187
308
|
const nextSize = resolveDomEditResizeGesture({
|
|
188
309
|
originWidth: g.originWidth,
|
|
189
310
|
originHeight: g.originHeight,
|
|
@@ -223,7 +344,9 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
223
344
|
}
|
|
224
345
|
};
|
|
225
346
|
|
|
347
|
+
// fallow-ignore-next-line complexity
|
|
226
348
|
const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
349
|
+
opts.snapGuidesRef.current = null;
|
|
227
350
|
const g = opts.gestureRef.current;
|
|
228
351
|
const groupG = opts.groupGestureRef.current;
|
|
229
352
|
const sel = g?.selection ?? opts.selectionRef.current;
|
|
@@ -233,13 +356,15 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
233
356
|
if (groupG) {
|
|
234
357
|
opts.groupGestureRef.current = null;
|
|
235
358
|
opts.rafPausedRef.current = false;
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
if (Math.hypot(
|
|
359
|
+
const rawDx = e.clientX - groupG.startX;
|
|
360
|
+
const rawDy = e.clientY - groupG.startY;
|
|
361
|
+
if (Math.hypot(rawDx, rawDy) < BLOCKED_MOVE_THRESHOLD_PX) {
|
|
239
362
|
restoreGroupPathOffsets(groupG);
|
|
240
363
|
opts.suppressNextBoxClickRef.current = true;
|
|
241
364
|
return;
|
|
242
365
|
}
|
|
366
|
+
const dx = groupG.lastSnappedDx ?? rawDx;
|
|
367
|
+
const dy = groupG.lastSnappedDy ?? rawDy;
|
|
243
368
|
setDraftGroupOverlayItems(
|
|
244
369
|
groupG.originItems.map((item) => ({
|
|
245
370
|
...item,
|
|
@@ -327,8 +452,8 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
327
452
|
})
|
|
328
453
|
.finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
|
|
329
454
|
} else if (g.kind === "drag") {
|
|
330
|
-
const dx = e.clientX - g.startX;
|
|
331
|
-
const dy = e.clientY - g.startY;
|
|
455
|
+
const dx = g.lastSnappedDx ?? e.clientX - g.startX;
|
|
456
|
+
const dy = g.lastSnappedDy ?? e.clientY - g.startY;
|
|
332
457
|
if (!g.pathOffsetMember) return;
|
|
333
458
|
const finalOffset = applyManualOffsetDragCommit(g.pathOffsetMember, dx, dy);
|
|
334
459
|
const nextBoxLeft = g.originLeft + dx;
|
|
@@ -372,7 +497,9 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
|
|
|
372
497
|
}
|
|
373
498
|
};
|
|
374
499
|
|
|
500
|
+
// fallow-ignore-next-line complexity
|
|
375
501
|
const clearPointerState = (selectionRef: RefObject<DomEditSelection | null>) => {
|
|
502
|
+
opts.snapGuidesRef.current = null;
|
|
376
503
|
const groupG = opts.groupGestureRef.current;
|
|
377
504
|
if (groupG) restoreGroupPathOffsets(groupG);
|
|
378
505
|
const g = opts.gestureRef.current;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { useRef, useState, useCallback } from "react";
|
|
2
|
+
import type { DomEditLayerItem } from "./domEditingTypes";
|
|
3
|
+
|
|
4
|
+
const DRAG_THRESHOLD_PX = 4;
|
|
5
|
+
|
|
6
|
+
interface DragState {
|
|
7
|
+
pointerId: number;
|
|
8
|
+
startY: number;
|
|
9
|
+
dragLayerIndex: number;
|
|
10
|
+
siblingIndices: number[];
|
|
11
|
+
fromSiblingPos: number;
|
|
12
|
+
insertSiblingPos: number;
|
|
13
|
+
siblingRects: DOMRect[];
|
|
14
|
+
activated: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LayerReorderEvent {
|
|
18
|
+
siblingLayers: DomEditLayerItem[];
|
|
19
|
+
fromIndex: number;
|
|
20
|
+
toIndex: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseLayerDragOptions {
|
|
24
|
+
visibleLayers: DomEditLayerItem[];
|
|
25
|
+
scrollContainerRef: React.RefObject<HTMLDivElement | null>;
|
|
26
|
+
onReorder: (event: LayerReorderEvent) => void;
|
|
27
|
+
onSingleSibling?: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UseLayerDragReturn {
|
|
31
|
+
dragKey: string | null;
|
|
32
|
+
insertionLineY: number | null;
|
|
33
|
+
handleRowPointerDown: (layerIndex: number, e: React.PointerEvent) => void;
|
|
34
|
+
handleContainerPointerMove: (e: React.PointerEvent) => void;
|
|
35
|
+
handleContainerPointerUp: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isLayerDraggable(layer: DomEditLayerItem): boolean {
|
|
39
|
+
if (!(layer.selector || layer.id)) return false;
|
|
40
|
+
let el: HTMLElement | null = layer.element;
|
|
41
|
+
while (el) {
|
|
42
|
+
if (el.hasAttribute("data-timeline-locked")) return false;
|
|
43
|
+
el = el.parentElement;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function findSiblingIndices(visibleLayers: DomEditLayerItem[], layerIndex: number): number[] {
|
|
49
|
+
const depth = visibleLayers[layerIndex].depth;
|
|
50
|
+
const indices: number[] = [];
|
|
51
|
+
|
|
52
|
+
let start = layerIndex;
|
|
53
|
+
while (start > 0) {
|
|
54
|
+
start--;
|
|
55
|
+
if (visibleLayers[start].depth < depth) {
|
|
56
|
+
start++;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (let i = start; i < visibleLayers.length; i++) {
|
|
62
|
+
const d = visibleLayers[i].depth;
|
|
63
|
+
if (d < depth) break;
|
|
64
|
+
if (d === depth) indices.push(i);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return indices;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function measureSiblingRects(container: HTMLDivElement, siblingIndices: number[]): DOMRect[] {
|
|
71
|
+
const rows = container.querySelectorAll<HTMLElement>("[data-layer-index]");
|
|
72
|
+
const rects: DOMRect[] = [];
|
|
73
|
+
for (const idx of siblingIndices) {
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
if (row.dataset.layerIndex === String(idx)) {
|
|
76
|
+
rects.push(row.getBoundingClientRect());
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return rects;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function computeInsertionPos(clientY: number, siblingRects: DOMRect[]): number {
|
|
85
|
+
if (siblingRects.length === 0) return 0;
|
|
86
|
+
|
|
87
|
+
if (clientY <= siblingRects[0].top + siblingRects[0].height / 2) return 0;
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < siblingRects.length - 1; i++) {
|
|
90
|
+
const midpoint = (siblingRects[i].bottom + siblingRects[i + 1].top) / 2;
|
|
91
|
+
if (clientY <= midpoint) return i + 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const last = siblingRects[siblingRects.length - 1];
|
|
95
|
+
if (clientY <= last.top + last.height / 2) return siblingRects.length - 1;
|
|
96
|
+
|
|
97
|
+
return siblingRects.length;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function computeInsertionLineY(
|
|
101
|
+
insertPos: number,
|
|
102
|
+
siblingRects: DOMRect[],
|
|
103
|
+
containerRect: DOMRect,
|
|
104
|
+
): number | null {
|
|
105
|
+
if (siblingRects.length === 0) return null;
|
|
106
|
+
if (insertPos <= 0) return siblingRects[0].top - containerRect.top;
|
|
107
|
+
if (insertPos >= siblingRects.length) {
|
|
108
|
+
return siblingRects[siblingRects.length - 1].bottom - containerRect.top;
|
|
109
|
+
}
|
|
110
|
+
return siblingRects[insertPos].top - containerRect.top;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function useLayerDrag({
|
|
114
|
+
visibleLayers,
|
|
115
|
+
scrollContainerRef,
|
|
116
|
+
onReorder,
|
|
117
|
+
onSingleSibling,
|
|
118
|
+
}: UseLayerDragOptions): UseLayerDragReturn {
|
|
119
|
+
const dragRef = useRef<DragState | null>(null);
|
|
120
|
+
const [dragKey, setDragKey] = useState<string | null>(null);
|
|
121
|
+
const [insertionLineY, setInsertionLineY] = useState<number | null>(null);
|
|
122
|
+
|
|
123
|
+
const handleRowPointerDown = useCallback(
|
|
124
|
+
(layerIndex: number, e: React.PointerEvent) => {
|
|
125
|
+
if (e.button !== 0) return;
|
|
126
|
+
const layer = visibleLayers[layerIndex];
|
|
127
|
+
if (!layer || !isLayerDraggable(layer)) return;
|
|
128
|
+
|
|
129
|
+
const siblingIndices = findSiblingIndices(visibleLayers, layerIndex);
|
|
130
|
+
if (siblingIndices.length <= 1) {
|
|
131
|
+
onSingleSibling?.();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const fromSiblingPos = siblingIndices.indexOf(layerIndex);
|
|
136
|
+
if (fromSiblingPos === -1) return;
|
|
137
|
+
|
|
138
|
+
const container = scrollContainerRef.current;
|
|
139
|
+
if (!container) return;
|
|
140
|
+
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
container.setPointerCapture(e.pointerId);
|
|
143
|
+
|
|
144
|
+
dragRef.current = {
|
|
145
|
+
pointerId: e.pointerId,
|
|
146
|
+
startY: e.clientY,
|
|
147
|
+
dragLayerIndex: layerIndex,
|
|
148
|
+
siblingIndices,
|
|
149
|
+
fromSiblingPos,
|
|
150
|
+
insertSiblingPos: fromSiblingPos,
|
|
151
|
+
siblingRects: measureSiblingRects(container, siblingIndices),
|
|
152
|
+
activated: false,
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
[visibleLayers, scrollContainerRef, onSingleSibling],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const handleContainerPointerMove = useCallback(
|
|
159
|
+
(e: React.PointerEvent) => {
|
|
160
|
+
const drag = dragRef.current;
|
|
161
|
+
if (!drag) return;
|
|
162
|
+
|
|
163
|
+
if (!drag.activated) {
|
|
164
|
+
if (Math.abs(e.clientY - drag.startY) < DRAG_THRESHOLD_PX) return;
|
|
165
|
+
drag.activated = true;
|
|
166
|
+
setDragKey(visibleLayers[drag.dragLayerIndex]?.key ?? null);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const insertPos = computeInsertionPos(e.clientY, drag.siblingRects);
|
|
170
|
+
drag.insertSiblingPos = insertPos;
|
|
171
|
+
|
|
172
|
+
const container = scrollContainerRef.current;
|
|
173
|
+
if (container) {
|
|
174
|
+
const containerRect = container.getBoundingClientRect();
|
|
175
|
+
setInsertionLineY(computeInsertionLineY(insertPos, drag.siblingRects, containerRect));
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
[visibleLayers, scrollContainerRef],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const handleContainerPointerUp = useCallback(() => {
|
|
182
|
+
const drag = dragRef.current;
|
|
183
|
+
dragRef.current = null;
|
|
184
|
+
setDragKey(null);
|
|
185
|
+
setInsertionLineY(null);
|
|
186
|
+
|
|
187
|
+
if (!drag || !drag.activated) return;
|
|
188
|
+
|
|
189
|
+
const container = scrollContainerRef.current;
|
|
190
|
+
if (container) {
|
|
191
|
+
try {
|
|
192
|
+
container.releasePointerCapture(drag.pointerId);
|
|
193
|
+
} catch {
|
|
194
|
+
// already released
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let toPos = drag.insertSiblingPos;
|
|
199
|
+
if (toPos > drag.fromSiblingPos) toPos--;
|
|
200
|
+
if (toPos === drag.fromSiblingPos) return;
|
|
201
|
+
|
|
202
|
+
const siblingLayers = drag.siblingIndices.map((i) => visibleLayers[i]);
|
|
203
|
+
onReorder({ siblingLayers, fromIndex: drag.fromSiblingPos, toIndex: toPos });
|
|
204
|
+
}, [visibleLayers, scrollContainerRef, onReorder]);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
dragKey,
|
|
208
|
+
insertionLineY,
|
|
209
|
+
handleRowPointerDown,
|
|
210
|
+
handleContainerPointerMove,
|
|
211
|
+
handleContainerPointerUp,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -69,7 +69,13 @@ interface NLELayoutProps {
|
|
|
69
69
|
updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
|
|
70
70
|
) => Promise<void> | void;
|
|
71
71
|
onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
|
|
72
|
+
onSplitElement?: (element: TimelineElement, splitTime: number) => Promise<void> | void;
|
|
72
73
|
onSelectTimelineElement?: (element: TimelineElement | null) => void;
|
|
74
|
+
onDeleteKeyframe?: (elementId: string, percentage: number) => void;
|
|
75
|
+
onDeleteAllKeyframes?: (elementId: string) => void;
|
|
76
|
+
onChangeKeyframeEase?: (elementId: string, percentage: number, ease: string) => void;
|
|
77
|
+
onMoveKeyframe?: (element: TimelineElement, oldPct: number, newPct: number) => void;
|
|
78
|
+
onToggleKeyframeAtPlayhead?: (element: TimelineElement) => void;
|
|
73
79
|
/** Exposes the compIdToSrc map for parent components (e.g., useRenderClipContent) */
|
|
74
80
|
onCompIdToSrcChange?: (map: Map<string, string>) => void;
|
|
75
81
|
/** Whether the timeline panel is visible (default: true) */
|
|
@@ -117,7 +123,13 @@ export const NLELayout = memo(function NLELayout({
|
|
|
117
123
|
onMoveElement,
|
|
118
124
|
onResizeElement,
|
|
119
125
|
onBlockedEditAttempt,
|
|
126
|
+
onSplitElement,
|
|
120
127
|
onSelectTimelineElement,
|
|
128
|
+
onDeleteKeyframe,
|
|
129
|
+
onDeleteAllKeyframes,
|
|
130
|
+
onChangeKeyframeEase,
|
|
131
|
+
onMoveKeyframe,
|
|
132
|
+
onToggleKeyframeAtPlayhead,
|
|
121
133
|
onCompIdToSrcChange,
|
|
122
134
|
timelineVisible,
|
|
123
135
|
onToggleTimeline,
|
|
@@ -447,7 +459,13 @@ export const NLELayout = memo(function NLELayout({
|
|
|
447
459
|
onMoveElement={onMoveElement}
|
|
448
460
|
onResizeElement={onResizeElement}
|
|
449
461
|
onBlockedEditAttempt={onBlockedEditAttempt}
|
|
462
|
+
onSplitElement={onSplitElement}
|
|
450
463
|
onSelectElement={onSelectTimelineElement}
|
|
464
|
+
onDeleteKeyframe={onDeleteKeyframe}
|
|
465
|
+
onDeleteAllKeyframes={onDeleteAllKeyframes}
|
|
466
|
+
onChangeKeyframeEase={onChangeKeyframeEase}
|
|
467
|
+
onMoveKeyframe={onMoveKeyframe}
|
|
468
|
+
onToggleKeyframeAtPlayhead={onToggleKeyframeAtPlayhead}
|
|
451
469
|
/>
|
|
452
470
|
</div>
|
|
453
471
|
{timelineFooter && <div className="flex-shrink-0">{timelineFooter}</div>}
|
|
@@ -32,6 +32,7 @@ export function DomEditProvider({
|
|
|
32
32
|
handleDomHtmlAttributeCommit,
|
|
33
33
|
handleDomPathOffsetCommit,
|
|
34
34
|
handleDomGroupPathOffsetCommit,
|
|
35
|
+
handleDomZIndexReorderCommit,
|
|
35
36
|
handleDomBoxSizeCommit,
|
|
36
37
|
handleDomRotationCommit,
|
|
37
38
|
handleDomManualEditsReset,
|
|
@@ -65,6 +66,14 @@ export function DomEditProvider({
|
|
|
65
66
|
handleGsapUpdateFromProperty,
|
|
66
67
|
handleGsapAddFromProperty,
|
|
67
68
|
handleGsapRemoveFromProperty,
|
|
69
|
+
handleGsapAddKeyframe,
|
|
70
|
+
handleGsapRemoveKeyframe,
|
|
71
|
+
handleGsapConvertToKeyframes,
|
|
72
|
+
handleGsapRemoveAllKeyframes,
|
|
73
|
+
handleResetSelectedElementKeyframes,
|
|
74
|
+
commitAnimatedProperty,
|
|
75
|
+
invalidateGsapCache,
|
|
76
|
+
previewIframeRef,
|
|
68
77
|
},
|
|
69
78
|
children,
|
|
70
79
|
}: {
|
|
@@ -92,6 +101,7 @@ export function DomEditProvider({
|
|
|
92
101
|
handleDomHtmlAttributeCommit,
|
|
93
102
|
handleDomPathOffsetCommit,
|
|
94
103
|
handleDomGroupPathOffsetCommit,
|
|
104
|
+
handleDomZIndexReorderCommit,
|
|
95
105
|
handleDomBoxSizeCommit,
|
|
96
106
|
handleDomRotationCommit,
|
|
97
107
|
handleDomManualEditsReset,
|
|
@@ -125,6 +135,14 @@ export function DomEditProvider({
|
|
|
125
135
|
handleGsapUpdateFromProperty,
|
|
126
136
|
handleGsapAddFromProperty,
|
|
127
137
|
handleGsapRemoveFromProperty,
|
|
138
|
+
handleGsapAddKeyframe,
|
|
139
|
+
handleGsapRemoveKeyframe,
|
|
140
|
+
handleGsapConvertToKeyframes,
|
|
141
|
+
handleGsapRemoveAllKeyframes,
|
|
142
|
+
handleResetSelectedElementKeyframes,
|
|
143
|
+
commitAnimatedProperty,
|
|
144
|
+
invalidateGsapCache,
|
|
145
|
+
previewIframeRef,
|
|
128
146
|
}),
|
|
129
147
|
[
|
|
130
148
|
domEditSelection,
|
|
@@ -146,6 +164,7 @@ export function DomEditProvider({
|
|
|
146
164
|
handleDomHtmlAttributeCommit,
|
|
147
165
|
handleDomPathOffsetCommit,
|
|
148
166
|
handleDomGroupPathOffsetCommit,
|
|
167
|
+
handleDomZIndexReorderCommit,
|
|
149
168
|
handleDomBoxSizeCommit,
|
|
150
169
|
handleDomRotationCommit,
|
|
151
170
|
handleDomManualEditsReset,
|
|
@@ -179,6 +198,14 @@ export function DomEditProvider({
|
|
|
179
198
|
handleGsapUpdateFromProperty,
|
|
180
199
|
handleGsapAddFromProperty,
|
|
181
200
|
handleGsapRemoveFromProperty,
|
|
201
|
+
handleGsapAddKeyframe,
|
|
202
|
+
handleGsapRemoveKeyframe,
|
|
203
|
+
handleGsapConvertToKeyframes,
|
|
204
|
+
handleGsapRemoveAllKeyframes,
|
|
205
|
+
handleResetSelectedElementKeyframes,
|
|
206
|
+
commitAnimatedProperty,
|
|
207
|
+
invalidateGsapCache,
|
|
208
|
+
previewIframeRef,
|
|
182
209
|
],
|
|
183
210
|
);
|
|
184
211
|
return <DomEditContext value={stable}>{children}</DomEditContext>;
|