@hyperframes/studio 0.7.46 → 0.7.47
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-Dq7FEg0K.css +1 -0
- package/dist/assets/{index-BGVoOXdg.js → index-DsckwbdW.js} +1 -1
- package/dist/assets/index-VsAbY3rl.js +423 -0
- package/dist/assets/{index-DO2MAFSG.js → index-uvB30_yI.js} +1 -1
- package/dist/index.d.ts +96 -29
- package/dist/index.html +2 -2
- package/dist/index.js +8443 -4305
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +26 -32
- package/src/components/DesignPanelPromoteProvider.tsx +45 -0
- package/src/components/PanelTabButton.tsx +31 -0
- package/src/components/StudioPreviewArea.tsx +75 -3
- package/src/components/StudioRightPanel.tsx +111 -113
- package/src/components/editor/LayersPanel.tsx +2 -22
- package/src/components/editor/PromotableControl.tsx +105 -0
- package/src/components/editor/propertyPanelSections.tsx +42 -21
- package/src/components/panels/VariablesBindElement.tsx +266 -0
- package/src/components/panels/VariablesDeclarationForm.test.ts +135 -0
- package/src/components/panels/VariablesDeclarationForm.tsx +321 -0
- package/src/components/panels/VariablesOtherCompositions.tsx +159 -0
- package/src/components/panels/VariablesPanel.tsx +554 -0
- package/src/components/panels/VariablesRowAction.tsx +25 -0
- package/src/components/panels/VariablesValueControls.tsx +215 -0
- package/src/components/renders/useRenderQueue.ts +12 -0
- package/src/contexts/TimelineEditContext.tsx +7 -2
- package/src/contexts/VariablePromoteContext.tsx +138 -0
- package/src/contexts/variablePromoteHelpers.test.ts +124 -0
- package/src/contexts/variablePromoteHelpers.ts +53 -0
- package/src/contexts/variablePromoteIntegration.test.ts +80 -0
- package/src/hooks/previewVariablesStore.ts +34 -0
- package/src/hooks/timelineEditingHelpers.test.ts +104 -0
- package/src/hooks/timelineEditingHelpers.ts +386 -10
- package/src/hooks/useDomEditWiring.ts +9 -7
- package/src/hooks/useDomSelection.test.ts +36 -2
- package/src/hooks/useDomSelection.ts +56 -75
- package/src/hooks/useElementLifecycleOps.ts +57 -17
- package/src/hooks/usePreviewDocumentVersion.ts +27 -0
- package/src/hooks/useProjectCompositionVariables.ts +131 -0
- package/src/hooks/useStudioContextValue.ts +6 -1
- package/src/hooks/useStudioSdkSessions.ts +37 -0
- package/src/hooks/useTimelineEditing.test.tsx +963 -0
- package/src/hooks/useTimelineEditing.ts +122 -100
- package/src/hooks/useTimelineEditingTypes.ts +15 -0
- package/src/hooks/useTimelineGroupEditing.ts +370 -0
- package/src/hooks/useTimelineSelectionPreviewSync.test.tsx +129 -0
- package/src/hooks/useTimelineSelectionPreviewSync.ts +130 -0
- package/src/hooks/useVariablesPersist.ts +61 -0
- package/src/player/components/BeatStrip.tsx +19 -3
- package/src/player/components/Player.tsx +7 -1
- package/src/player/components/Timeline.test.ts +38 -0
- package/src/player/components/Timeline.tsx +102 -80
- package/src/player/components/TimelineCanvas.tsx +340 -296
- package/src/player/components/TimelineClip.test.tsx +11 -0
- package/src/player/components/TimelineClipDiamonds.tsx +40 -9
- package/src/player/components/TimelineDragGhost.tsx +59 -0
- package/src/player/components/TimelineDropInsertionLine.tsx +32 -0
- package/src/player/components/TimelineLayerGroupHeader.tsx +97 -0
- package/src/player/components/TimelineLayerGutter.tsx +61 -0
- package/src/player/components/TimelineRuler.tsx +4 -2
- package/src/player/components/TimelineSelectionOverlays.tsx +55 -0
- package/src/player/components/timelineCallbacks.ts +20 -2
- package/src/player/components/timelineClipDragPreview.ts +52 -0
- package/src/player/components/timelineDragDrop.ts +19 -3
- package/src/player/components/timelineDropIndicator.test.ts +48 -0
- package/src/player/components/timelineDropIndicator.ts +33 -0
- package/src/player/components/timelineEditing.test.ts +251 -1
- package/src/player/components/timelineEditing.ts +178 -19
- package/src/player/components/timelineGroupEditing.ts +159 -0
- package/src/player/components/timelineLayerDrag.test.ts +200 -0
- package/src/player/components/timelineLayerDrag.ts +383 -0
- package/src/player/components/timelineLayout.test.ts +34 -0
- package/src/player/components/timelineLayout.ts +47 -0
- package/src/player/components/timelineMarqueeSelection.test.ts +87 -0
- package/src/player/components/timelineSnapTargets.test.ts +144 -0
- package/src/player/components/timelineSnapTargets.ts +164 -0
- package/src/player/components/timelineStacking.ts +62 -0
- package/src/player/components/timelineTrackOrder.test.ts +144 -0
- package/src/player/components/timelineTrackOrder.ts +173 -0
- package/src/player/components/useTimelineActiveClips.test.ts +20 -0
- package/src/player/components/useTimelineActiveClips.ts +15 -4
- package/src/player/components/useTimelineClipDrag.test.tsx +506 -0
- package/src/player/components/useTimelineClipDrag.ts +172 -126
- package/src/player/components/useTimelineClipGroupDrag.ts +417 -0
- package/src/player/components/useTimelineKeyframeHandlers.ts +73 -0
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +231 -0
- package/src/player/components/useTimelineMarqueeSelection.ts +276 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +4 -1
- package/src/player/hooks/useTimelinePlayer.ts +7 -0
- package/src/player/lib/layerOrdering.test.ts +77 -0
- package/src/player/lib/layerOrdering.ts +98 -0
- package/src/player/lib/playbackTypes.ts +3 -0
- package/src/player/lib/timelineDOM.test.ts +103 -0
- package/src/player/lib/timelineDOM.ts +80 -0
- package/src/player/lib/timelineElementHelpers.ts +1 -1
- package/src/player/store/playerStore.test.ts +92 -0
- package/src/player/store/playerStore.ts +69 -9
- package/src/utils/blockInstaller.ts +2 -14
- package/src/utils/editHistory.test.ts +61 -0
- package/src/utils/editHistory.ts +7 -1
- package/src/utils/rootDuration.test.ts +34 -0
- package/src/utils/rootDuration.ts +17 -0
- package/src/utils/sdkCutover.ts +45 -0
- package/src/utils/studioHelpers.test.ts +31 -0
- package/src/utils/studioHelpers.ts +29 -1
- package/src/utils/studioUrlState.test.ts +29 -0
- package/src/utils/studioUrlState.ts +16 -1
- package/dist/assets/index-BsLQ2qGn.js +0 -420
- package/dist/assets/index-DoVLXke0.css +0 -1
|
@@ -28,13 +28,17 @@ export const BeatBackgroundLines = memo(function BeatBackgroundLines({
|
|
|
28
28
|
beatTimes: number[] | undefined;
|
|
29
29
|
beatStrengths: number[] | undefined;
|
|
30
30
|
pps: number;
|
|
31
|
-
/**
|
|
31
|
+
/** Snap guide time — drawn as a bright line even when it is not a beat. */
|
|
32
32
|
highlightTime?: number | null;
|
|
33
33
|
}) {
|
|
34
|
-
|
|
34
|
+
const visibleBeatTimes = beatTimes && !beatsTooDense(beatTimes, pps) ? beatTimes : null;
|
|
35
|
+
const highlightIsBeat =
|
|
36
|
+
highlightTime != null &&
|
|
37
|
+
visibleBeatTimes?.some((t) => Math.abs(t - highlightTime) < 1e-3) === true;
|
|
38
|
+
if (!visibleBeatTimes && highlightTime == null) return null;
|
|
35
39
|
return (
|
|
36
40
|
<div className="absolute inset-0 pointer-events-none" style={{ zIndex: 0 }}>
|
|
37
|
-
{
|
|
41
|
+
{visibleBeatTimes?.map((t, i) => {
|
|
38
42
|
const isHighlight = highlightTime != null && Math.abs(t - highlightTime) < 1e-3;
|
|
39
43
|
const strength = Math.pow(Math.min(1, beatStrengths?.[i] ?? 0.5), 2.2);
|
|
40
44
|
const opacity = isHighlight ? 1 : 0.06 + strength * 0.16;
|
|
@@ -52,6 +56,18 @@ export const BeatBackgroundLines = memo(function BeatBackgroundLines({
|
|
|
52
56
|
/>
|
|
53
57
|
);
|
|
54
58
|
})}
|
|
59
|
+
{highlightTime != null && !highlightIsBeat && (
|
|
60
|
+
<div
|
|
61
|
+
className="absolute top-0 bottom-0"
|
|
62
|
+
style={{
|
|
63
|
+
left: highlightTime * pps,
|
|
64
|
+
width: 2,
|
|
65
|
+
background: "rgba(34,197,94,1)",
|
|
66
|
+
boxShadow: "0 0 6px rgba(34,197,94,0.9)",
|
|
67
|
+
zIndex: 1,
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
55
71
|
</div>
|
|
56
72
|
);
|
|
57
73
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { forwardRef, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { isLottieAnimationLoaded } from "@hyperframes/core/runtime/lottie-readiness";
|
|
3
3
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
4
|
+
import { applyPreviewVariablesToUrl } from "../../hooks/previewVariablesStore";
|
|
4
5
|
import { HyperframesLoader } from "../../components/ui";
|
|
5
6
|
// NOTE: importing "@hyperframes/player" registers a class extending HTMLElement
|
|
6
7
|
// at module load, which throws under SSR. Defer the import to the mount effect
|
|
@@ -154,7 +155,12 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
|
|
|
154
155
|
|
|
155
156
|
// Create the web component imperatively to avoid JSX custom-element typing.
|
|
156
157
|
const player = document.createElement("hyperframes-player") as HyperframesPlayerElement;
|
|
157
|
-
const
|
|
158
|
+
const srcUrl = new URL(
|
|
159
|
+
directUrl || `/api/projects/${projectId}/preview`,
|
|
160
|
+
window.location.origin,
|
|
161
|
+
);
|
|
162
|
+
applyPreviewVariablesToUrl(srcUrl);
|
|
163
|
+
const src = srcUrl.pathname + srcUrl.search;
|
|
158
164
|
player.setAttribute("shader-capture-scale", "1");
|
|
159
165
|
player.setAttribute("shader-loading", "player");
|
|
160
166
|
player.setAttribute("src", src);
|
|
@@ -117,6 +117,10 @@ describe("Timeline provider boundary", () => {
|
|
|
117
117
|
);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
// Flush passive effects (ResizeObserver-driven layout) so the gutter row is
|
|
121
|
+
// mounted before we query it.
|
|
122
|
+
act(() => {});
|
|
123
|
+
|
|
120
124
|
const button = host.querySelector<HTMLButtonElement>('button[aria-label="Show track 0"]');
|
|
121
125
|
expect(button).not.toBeNull();
|
|
122
126
|
if (!button) throw new Error("Expected a track visibility toggle");
|
|
@@ -199,6 +203,40 @@ describe("Timeline provider boundary", () => {
|
|
|
199
203
|
expect(onSeek).not.toHaveBeenCalled();
|
|
200
204
|
act(() => root.unmount());
|
|
201
205
|
});
|
|
206
|
+
|
|
207
|
+
it("marks every clip in selectedElementIds as selected", () => {
|
|
208
|
+
const host = document.createElement("div");
|
|
209
|
+
document.body.append(host);
|
|
210
|
+
Object.defineProperty(host, "clientWidth", {
|
|
211
|
+
configurable: true,
|
|
212
|
+
value: 720,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
usePlayerStore.setState({
|
|
216
|
+
duration: 6,
|
|
217
|
+
timelineReady: true,
|
|
218
|
+
selectedElementId: "clip-2",
|
|
219
|
+
selectedElementIds: new Set(["clip-1", "clip-2"]),
|
|
220
|
+
elements: [
|
|
221
|
+
{ id: "clip-1", tag: "div", start: 0, duration: 1, track: 0 },
|
|
222
|
+
{ id: "clip-2", tag: "div", start: 1.5, duration: 1, track: 1 },
|
|
223
|
+
{ id: "clip-3", tag: "div", start: 3, duration: 1, track: 2 },
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const root = createRoot(host);
|
|
228
|
+
act(() => {
|
|
229
|
+
root.render(React.createElement(Timeline));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const selectedClips = host.querySelectorAll(".timeline-clip.is-selected");
|
|
233
|
+
expect(selectedClips).toHaveLength(2);
|
|
234
|
+
expect(host.querySelector('[data-el-id="clip-3"]')?.classList.contains("is-selected")).toBe(
|
|
235
|
+
false,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
act(() => root.unmount());
|
|
239
|
+
});
|
|
202
240
|
});
|
|
203
241
|
|
|
204
242
|
describe("generateTicks", () => {
|
|
@@ -8,6 +8,7 @@ import { useMountEffect } from "../../hooks/useMountEffect";
|
|
|
8
8
|
import { EditPopover } from "./EditModal";
|
|
9
9
|
import { defaultTimelineTheme } from "./timelineTheme";
|
|
10
10
|
import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
|
|
11
|
+
import { useTimelineMarqueeSelection } from "./useTimelineMarqueeSelection";
|
|
11
12
|
import { useTimelinePlayhead } from "./useTimelinePlayhead";
|
|
12
13
|
import { useTimelineActiveClips } from "./useTimelineActiveClips";
|
|
13
14
|
import { type TrackVisualStyle, getTrackStyle } from "./timelineIcons";
|
|
@@ -21,18 +22,22 @@ import {
|
|
|
21
22
|
type KeyframeDiamondContextMenuState,
|
|
22
23
|
} from "./KeyframeDiamondContextMenu";
|
|
23
24
|
import { useTimelineClipDrag } from "./useTimelineClipDrag";
|
|
25
|
+
import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
|
|
24
26
|
import { ClipContextMenu } from "./ClipContextMenu";
|
|
25
27
|
import { TimelineShortcutHint } from "./TimelineShortcutHint";
|
|
28
|
+
import { buildStackingTimelineLayers, insertPreviewTrackOrder } from "./timelineTrackOrder";
|
|
29
|
+
import { getTimelineLayerGroupHeaderTotalHeight } from "./TimelineLayerGroupHeader";
|
|
26
30
|
import {
|
|
27
31
|
GUTTER,
|
|
28
|
-
|
|
32
|
+
generateVisibleTicks,
|
|
29
33
|
getTimelineCanvasHeight,
|
|
30
34
|
shouldShowTimelineShortcutHint,
|
|
35
|
+
computeTimelineBasisDuration,
|
|
36
|
+
computeTimelineEffectiveDuration,
|
|
31
37
|
} from "./timelineLayout";
|
|
32
38
|
import { useResolvedTimelineEditCallbacks } from "./useResolvedTimelineEditCallbacks";
|
|
33
39
|
import type { TimelineProps } from "./TimelineTypes";
|
|
34
40
|
|
|
35
|
-
// Re-export pure utilities so existing imports from "./Timeline" still resolve.
|
|
36
41
|
export {
|
|
37
42
|
generateTicks,
|
|
38
43
|
formatTimelineTickLabel,
|
|
@@ -66,6 +71,10 @@ export const Timeline = memo(function Timeline({
|
|
|
66
71
|
const {
|
|
67
72
|
onMoveElement,
|
|
68
73
|
onResizeElement,
|
|
74
|
+
onMoveElements,
|
|
75
|
+
onResizeElements,
|
|
76
|
+
onPreviewMoveElements,
|
|
77
|
+
onPreviewResizeElements,
|
|
69
78
|
onBlockedEditAttempt,
|
|
70
79
|
onSplitElement,
|
|
71
80
|
onRazorSplitAll,
|
|
@@ -99,14 +108,12 @@ export const Timeline = memo(function Timeline({
|
|
|
99
108
|
const { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent } = useTimelineZoom();
|
|
100
109
|
|
|
101
110
|
const playheadRef = useRef<HTMLDivElement>(null);
|
|
102
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
103
111
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
104
112
|
const activeTool = usePlayerStore((s) => s.activeTool);
|
|
105
113
|
const [hoveredClip, setHoveredClip] = useState<string | null>(null);
|
|
106
114
|
const isDragging = useRef(false);
|
|
107
115
|
const [shiftHeld, setShiftHeld] = useState(false);
|
|
108
116
|
const [razorGuideX, setRazorGuideX] = useState<number | null>(null);
|
|
109
|
-
|
|
110
117
|
useMountEffect(() => {
|
|
111
118
|
const down = (e: KeyboardEvent) => e.key === "Shift" && setShiftHeld(true);
|
|
112
119
|
const up = (e: KeyboardEvent) => e.key === "Shift" && setShiftHeld(false);
|
|
@@ -132,7 +139,6 @@ export const Timeline = memo(function Timeline({
|
|
|
132
139
|
const [viewportWidth, setViewportWidth] = useState(0);
|
|
133
140
|
const roRef = useRef<ResizeObserver | null>(null);
|
|
134
141
|
const shortcutHintRafRef = useRef(0);
|
|
135
|
-
|
|
136
142
|
const syncShortcutHintVisibility = useCallback(() => {
|
|
137
143
|
const scroll = scrollRef.current;
|
|
138
144
|
setShowShortcutHint(
|
|
@@ -148,10 +154,6 @@ export const Timeline = memo(function Timeline({
|
|
|
148
154
|
});
|
|
149
155
|
}, [syncShortcutHintVisibility]);
|
|
150
156
|
|
|
151
|
-
const setContainerRef = useCallback((el: HTMLDivElement | null) => {
|
|
152
|
-
containerRef.current = el;
|
|
153
|
-
}, []);
|
|
154
|
-
|
|
155
157
|
const setScrollRef = useCallback(
|
|
156
158
|
(el: HTMLDivElement | null) => {
|
|
157
159
|
if (roRef.current) {
|
|
@@ -178,41 +180,30 @@ export const Timeline = memo(function Timeline({
|
|
|
178
180
|
if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
|
|
179
181
|
});
|
|
180
182
|
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const result = Math.max(safeDur, maxEnd);
|
|
186
|
-
return Number.isFinite(result) ? result : safeDur;
|
|
187
|
-
}, [rawElements, duration]);
|
|
188
|
-
|
|
189
|
-
const tracks = useMemo(() => {
|
|
190
|
-
const map = new Map<number, typeof expandedElements>();
|
|
191
|
-
for (const el of expandedElements) {
|
|
192
|
-
const list = map.get(el.track) ?? [];
|
|
193
|
-
list.push(el);
|
|
194
|
-
map.set(el.track, list);
|
|
195
|
-
}
|
|
196
|
-
return Array.from(map.entries()).sort(([a], [b]) => a - b);
|
|
197
|
-
}, [expandedElements]);
|
|
183
|
+
const tracks = useMemo(
|
|
184
|
+
() => buildStackingTimelineLayers(expandedElements).rows,
|
|
185
|
+
[expandedElements],
|
|
186
|
+
);
|
|
198
187
|
|
|
199
188
|
const trackStyles = useMemo(() => {
|
|
200
|
-
const map = new Map<
|
|
201
|
-
for (const
|
|
202
|
-
map.set(
|
|
189
|
+
const map = new Map<string, TrackVisualStyle>();
|
|
190
|
+
for (const layer of tracks) {
|
|
191
|
+
map.set(layer.id, getTrackStyle(layer.elements[0]?.tag ?? ""));
|
|
203
192
|
}
|
|
204
193
|
return map;
|
|
205
194
|
}, [tracks]);
|
|
206
195
|
|
|
207
|
-
const trackOrder = useMemo(() => tracks.map((
|
|
196
|
+
const trackOrder = useMemo(() => tracks.map((layer) => layer.id), [tracks]);
|
|
208
197
|
const trackOrderRef = useRef(trackOrder);
|
|
209
198
|
trackOrderRef.current = trackOrder;
|
|
199
|
+
const timelineLayersRef = useRef(tracks);
|
|
200
|
+
timelineLayersRef.current = tracks;
|
|
201
|
+
const expandedElementsRef = useRef(expandedElements);
|
|
202
|
+
expandedElementsRef.current = expandedElements;
|
|
210
203
|
|
|
211
204
|
const ppsRef = useRef(100);
|
|
212
|
-
const durationRef = useRef(
|
|
213
|
-
durationRef.current = effectiveDuration;
|
|
205
|
+
const durationRef = useRef(Number.isFinite(duration) ? duration : 0);
|
|
214
206
|
|
|
215
|
-
// Stable ref so useTimelineClipDrag can clear rangeSelection without circular dep
|
|
216
207
|
const setRangeSelectionRef = useRef<((sel: null) => void) | null>(null);
|
|
217
208
|
|
|
218
209
|
const {
|
|
@@ -226,29 +217,67 @@ export const Timeline = memo(function Timeline({
|
|
|
226
217
|
} = useTimelineClipDrag({
|
|
227
218
|
scrollRef,
|
|
228
219
|
ppsRef,
|
|
229
|
-
durationRef,
|
|
230
220
|
trackOrderRef,
|
|
221
|
+
timelineLayersRef,
|
|
222
|
+
timelineElementsRef: expandedElementsRef,
|
|
231
223
|
onMoveElement,
|
|
232
224
|
onResizeElement,
|
|
225
|
+
onMoveElements,
|
|
226
|
+
onResizeElements,
|
|
227
|
+
onPreviewMoveElements,
|
|
228
|
+
onPreviewResizeElements,
|
|
233
229
|
onBlockedEditAttempt,
|
|
234
230
|
setShowPopover,
|
|
235
231
|
setRangeSelectionRef,
|
|
236
232
|
});
|
|
237
233
|
|
|
234
|
+
const basisDuration = useMemo(
|
|
235
|
+
() =>
|
|
236
|
+
computeTimelineBasisDuration(
|
|
237
|
+
duration,
|
|
238
|
+
rawElements.map((el) => el.start + el.duration),
|
|
239
|
+
),
|
|
240
|
+
[rawElements, duration],
|
|
241
|
+
);
|
|
242
|
+
const effectiveDuration = useMemo(
|
|
243
|
+
() =>
|
|
244
|
+
computeTimelineEffectiveDuration(basisDuration, [
|
|
245
|
+
draggedClip?.started ? draggedClip.previewStart + draggedClip.element.duration : null,
|
|
246
|
+
resizingClip?.started ? resizingClip.previewStart + resizingClip.previewDuration : null,
|
|
247
|
+
]),
|
|
248
|
+
[basisDuration, draggedClip, resizingClip],
|
|
249
|
+
);
|
|
250
|
+
durationRef.current = effectiveDuration;
|
|
251
|
+
|
|
238
252
|
const displayTrackOrder = useMemo(() => {
|
|
239
253
|
if (
|
|
240
254
|
!draggedClip?.started ||
|
|
241
255
|
trackOrder.length === 0 ||
|
|
242
|
-
trackOrder.includes(draggedClip.
|
|
256
|
+
trackOrder.includes(draggedClip.previewLayerId)
|
|
243
257
|
)
|
|
244
258
|
return trackOrder;
|
|
245
|
-
return
|
|
259
|
+
return insertPreviewTrackOrder(
|
|
260
|
+
trackOrder,
|
|
261
|
+
draggedClip.previewLayerId,
|
|
262
|
+
draggedClip.previewLayerIndex,
|
|
263
|
+
);
|
|
246
264
|
}, [draggedClip, trackOrder]);
|
|
247
265
|
|
|
248
|
-
const totalH =
|
|
266
|
+
const totalH =
|
|
267
|
+
getTimelineCanvasHeight(displayTrackOrder.length) +
|
|
268
|
+
getTimelineLayerGroupHeaderTotalHeight(displayTrackOrder, tracks);
|
|
249
269
|
const keyframeCache = usePlayerStore((s) => s.keyframeCache);
|
|
250
270
|
const selectedKeyframes = usePlayerStore((s) => s.selectedKeyframes);
|
|
251
271
|
const toggleSelectedKeyframe = usePlayerStore((s) => s.toggleSelectedKeyframe);
|
|
272
|
+
const keyframeHandlers = useTimelineKeyframeHandlers({
|
|
273
|
+
expandedElements,
|
|
274
|
+
keyframeCache,
|
|
275
|
+
onSelectElement,
|
|
276
|
+
onSeek,
|
|
277
|
+
setSelectedElementId,
|
|
278
|
+
setKfContextMenu,
|
|
279
|
+
toggleSelectedKeyframe,
|
|
280
|
+
});
|
|
252
281
|
|
|
253
282
|
const selectedElement = useMemo(
|
|
254
283
|
() =>
|
|
@@ -259,8 +288,8 @@ export const Timeline = memo(function Timeline({
|
|
|
259
288
|
selectedElementRef.current = selectedElement;
|
|
260
289
|
|
|
261
290
|
const fitPps =
|
|
262
|
-
viewportWidth > GUTTER &&
|
|
263
|
-
? (viewportWidth - GUTTER - 2) /
|
|
291
|
+
viewportWidth > GUTTER && basisDuration > 0
|
|
292
|
+
? (viewportWidth - GUTTER - 2) / basisDuration
|
|
264
293
|
: 100;
|
|
265
294
|
const pps = getTimelinePixelsPerSecond(fitPps, zoomMode, manualZoomPercent);
|
|
266
295
|
ppsRef.current = pps;
|
|
@@ -325,8 +354,27 @@ export const Timeline = memo(function Timeline({
|
|
|
325
354
|
isDragging,
|
|
326
355
|
setShowPopover,
|
|
327
356
|
});
|
|
328
|
-
|
|
357
|
+
const {
|
|
358
|
+
marqueeRect,
|
|
359
|
+
handlePointerDown: handleMarqueePointerDown,
|
|
360
|
+
handlePointerMove: handleMarqueePointerMove,
|
|
361
|
+
handlePointerUp: handleMarqueePointerUp,
|
|
362
|
+
} = useTimelineMarqueeSelection({
|
|
363
|
+
scrollRef,
|
|
364
|
+
ppsRef,
|
|
365
|
+
trackOrderRef,
|
|
366
|
+
timelineLayersRef,
|
|
367
|
+
disabled: activeTool === "razor",
|
|
368
|
+
setShowPopover,
|
|
369
|
+
setRangeSelectionRef,
|
|
370
|
+
seekFromX,
|
|
371
|
+
});
|
|
329
372
|
setRangeSelectionRef.current = setRangeSelection;
|
|
373
|
+
// Pointer-up and lost-capture end a gesture identically (marquee-claims-first).
|
|
374
|
+
const releasePointer = (event: Parameters<typeof handleMarqueePointerUp>[0]) => {
|
|
375
|
+
if (handleMarqueePointerUp(event)) return;
|
|
376
|
+
handlePointerUp();
|
|
377
|
+
};
|
|
330
378
|
|
|
331
379
|
const prevSelectedRef = useRef(selectedElementRef.current);
|
|
332
380
|
// eslint-disable-next-line no-restricted-syntax, react-hooks/exhaustive-deps
|
|
@@ -341,8 +389,8 @@ export const Timeline = memo(function Timeline({
|
|
|
341
389
|
});
|
|
342
390
|
|
|
343
391
|
const { major, minor } = useMemo(
|
|
344
|
-
() =>
|
|
345
|
-
[effectiveDuration, pps],
|
|
392
|
+
() => generateVisibleTicks(effectiveDuration, pps, viewportWidth, GUTTER),
|
|
393
|
+
[effectiveDuration, pps, viewportWidth],
|
|
346
394
|
);
|
|
347
395
|
const majorTickInterval = major.length >= 2 ? major[1] - major[0] : effectiveDuration;
|
|
348
396
|
|
|
@@ -373,6 +421,7 @@ export const Timeline = memo(function Timeline({
|
|
|
373
421
|
ppsRef,
|
|
374
422
|
durationRef,
|
|
375
423
|
trackOrderRef,
|
|
424
|
+
timelineLayersRef,
|
|
376
425
|
onFileDrop,
|
|
377
426
|
onAssetDrop,
|
|
378
427
|
onBlockDrop,
|
|
@@ -392,7 +441,6 @@ export const Timeline = memo(function Timeline({
|
|
|
392
441
|
|
|
393
442
|
return (
|
|
394
443
|
<div
|
|
395
|
-
ref={setContainerRef}
|
|
396
444
|
aria-label="Timeline"
|
|
397
445
|
className={`relative border-t select-none h-full overflow-hidden ${activeTool === "razor" ? "cursor-crosshair" : shiftHeld ? "cursor-crosshair" : "cursor-default"}`}
|
|
398
446
|
onMouseMove={(e) => {
|
|
@@ -423,11 +471,15 @@ export const Timeline = memo(function Timeline({
|
|
|
423
471
|
onRazorSplitAll?.(splitTime);
|
|
424
472
|
return;
|
|
425
473
|
}
|
|
474
|
+
if (handleMarqueePointerDown(e)) return;
|
|
426
475
|
handlePointerDown(e);
|
|
427
476
|
}}
|
|
428
|
-
onPointerMove={
|
|
429
|
-
|
|
430
|
-
|
|
477
|
+
onPointerMove={(e) => {
|
|
478
|
+
if (handleMarqueePointerMove(e)) return;
|
|
479
|
+
handlePointerMove(e);
|
|
480
|
+
}}
|
|
481
|
+
onPointerUp={releasePointer}
|
|
482
|
+
onLostPointerCapture={releasePointer}
|
|
431
483
|
>
|
|
432
484
|
<TimelineCanvas
|
|
433
485
|
major={major}
|
|
@@ -438,6 +490,7 @@ export const Timeline = memo(function Timeline({
|
|
|
438
490
|
effectiveDuration={effectiveDuration}
|
|
439
491
|
majorTickInterval={majorTickInterval}
|
|
440
492
|
rangeSelection={rangeSelection}
|
|
493
|
+
marqueeRect={marqueeRect}
|
|
441
494
|
theme={theme}
|
|
442
495
|
displayTrackOrder={displayTrackOrder}
|
|
443
496
|
trackOrder={trackOrder}
|
|
@@ -469,41 +522,10 @@ export const Timeline = memo(function Timeline({
|
|
|
469
522
|
selectedKeyframes={selectedKeyframes}
|
|
470
523
|
currentTime={currentTime}
|
|
471
524
|
beatAnalysis={adjustedBeatAnalysis}
|
|
472
|
-
onClickKeyframe={
|
|
473
|
-
|
|
474
|
-
const elKey = el.key ?? el.id;
|
|
475
|
-
setSelectedElementId(elKey);
|
|
476
|
-
onSelectElement?.(el);
|
|
477
|
-
// Visually select the clicked diamond (matches shift-click / motion-path
|
|
478
|
-
// selection); cleared above so this single-selects it.
|
|
479
|
-
toggleSelectedKeyframe(`${elKey}:${pct}`);
|
|
480
|
-
const absTime = el.start + (pct / 100) * el.duration;
|
|
481
|
-
onSeek?.(absTime);
|
|
482
|
-
const kfData = keyframeCache?.get(elKey);
|
|
483
|
-
const kf = kfData?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.5);
|
|
484
|
-
usePlayerStore.getState().setActiveKeyframePct(kf?.tweenPercentage ?? null);
|
|
485
|
-
}}
|
|
486
|
-
onShiftClickKeyframe={(elId, pct) => {
|
|
487
|
-
toggleSelectedKeyframe(`${elId}:${pct}`);
|
|
488
|
-
}}
|
|
525
|
+
onClickKeyframe={keyframeHandlers.onClickKeyframe}
|
|
526
|
+
onShiftClickKeyframe={keyframeHandlers.onShiftClickKeyframe}
|
|
489
527
|
onMoveKeyframe={onMoveKeyframe}
|
|
490
|
-
onContextMenuKeyframe={
|
|
491
|
-
const el = expandedElements.find((x) => (x.key ?? x.id) === elId);
|
|
492
|
-
if (el) {
|
|
493
|
-
setSelectedElementId(elId);
|
|
494
|
-
onSelectElement?.(el);
|
|
495
|
-
}
|
|
496
|
-
const kfData = keyframeCache.get(elId);
|
|
497
|
-
const kf = kfData?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.2);
|
|
498
|
-
setKfContextMenu({
|
|
499
|
-
x: e.clientX + 4,
|
|
500
|
-
y: e.clientY + 2,
|
|
501
|
-
elementId: elId,
|
|
502
|
-
percentage: pct,
|
|
503
|
-
tweenPercentage: kf?.tweenPercentage,
|
|
504
|
-
currentEase: kf?.ease ?? kfData?.ease,
|
|
505
|
-
});
|
|
506
|
-
}}
|
|
528
|
+
onContextMenuKeyframe={keyframeHandlers.onContextMenuKeyframe}
|
|
507
529
|
onContextMenuClip={(e, el) => {
|
|
508
530
|
e.preventDefault();
|
|
509
531
|
setSelectedElementId(el.key ?? el.id);
|