@hyperframes/studio 0.7.45 → 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-CZkdm5YL.js → index-DsckwbdW.js} +1 -1
- package/dist/assets/index-VsAbY3rl.js +423 -0
- package/dist/assets/{index-CbQKp0Ek.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-CZUbpYhQ.js +0 -416
- package/dist/assets/index-DoVLXke0.css +0 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { memo, type ReactNode } from "react";
|
|
2
|
-
import { Eye, EyeSlash } from "@phosphor-icons/react";
|
|
1
|
+
import { Fragment, memo, type ReactNode } from "react";
|
|
3
2
|
import { BeatStrip, BeatBackgroundLines } from "./BeatStrip";
|
|
4
3
|
import { TimelineClip } from "./TimelineClip";
|
|
5
4
|
import { TimelineClipDiamonds } from "./TimelineClipDiamonds";
|
|
@@ -12,7 +11,7 @@ import {
|
|
|
12
11
|
type TimelineRangeSelection,
|
|
13
12
|
} from "./timelineEditing";
|
|
14
13
|
import { getRenderedTimelineElement, type TimelineTheme } from "./timelineTheme";
|
|
15
|
-
import { GUTTER, TRACK_H,
|
|
14
|
+
import { GUTTER, TRACK_H, CLIP_Y, CLIP_HANDLE_W } from "./timelineLayout";
|
|
16
15
|
import {
|
|
17
16
|
usePlayerStore,
|
|
18
17
|
type TimelineElement,
|
|
@@ -20,10 +19,21 @@ import {
|
|
|
20
19
|
} from "../store/playerStore";
|
|
21
20
|
import type { DraggedClipState, ResizingClipState, BlockedClipState } from "./useTimelineClipDrag";
|
|
22
21
|
import type { TrackVisualStyle } from "./timelineIcons";
|
|
22
|
+
import type { StackingTimelineLayer, TimelineLayerId } from "./timelineTrackOrder";
|
|
23
23
|
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
|
|
24
24
|
import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
|
|
25
25
|
import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
|
|
26
26
|
import { isMusicTrack } from "../../utils/timelineInspector";
|
|
27
|
+
import { TimelineLayerGutter } from "./TimelineLayerGutter";
|
|
28
|
+
import {
|
|
29
|
+
shouldShowTimelineLayerGroupHeader,
|
|
30
|
+
TimelineLayerGroupHeader,
|
|
31
|
+
} from "./TimelineLayerGroupHeader";
|
|
32
|
+
import { resolveTimelineDropIndicator } from "./timelineDropIndicator";
|
|
33
|
+
import { TimelineDropInsertionLine } from "./TimelineDropInsertionLine";
|
|
34
|
+
import { TimelineDragGhost } from "./TimelineDragGhost";
|
|
35
|
+
import { TimelineSelectionOverlays } from "./TimelineSelectionOverlays";
|
|
36
|
+
import type { TimelineMarqueeOverlayRect } from "./useTimelineMarqueeSelection";
|
|
27
37
|
|
|
28
38
|
function ClipLintDot({ element }: { element: TimelineElement }) {
|
|
29
39
|
const lint = usePlayerStore((s) => s.lintFindingsByElement.get(element.key ?? element.id));
|
|
@@ -46,11 +56,12 @@ interface TimelineCanvasProps {
|
|
|
46
56
|
effectiveDuration: number;
|
|
47
57
|
majorTickInterval: number;
|
|
48
58
|
rangeSelection: TimelineRangeSelection | null;
|
|
59
|
+
marqueeRect: TimelineMarqueeOverlayRect | null;
|
|
49
60
|
theme: TimelineTheme;
|
|
50
|
-
displayTrackOrder:
|
|
51
|
-
trackOrder:
|
|
52
|
-
tracks: [
|
|
53
|
-
trackStyles: Map<
|
|
61
|
+
displayTrackOrder: TimelineLayerId[];
|
|
62
|
+
trackOrder: TimelineLayerId[];
|
|
63
|
+
tracks: StackingTimelineLayer[];
|
|
64
|
+
trackStyles: Map<TimelineLayerId, TrackVisualStyle>;
|
|
54
65
|
selectedElementId: string | null;
|
|
55
66
|
hoveredClip: string | null;
|
|
56
67
|
draggedClip: DraggedClipState | null;
|
|
@@ -104,6 +115,7 @@ export const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
104
115
|
effectiveDuration,
|
|
105
116
|
majorTickInterval,
|
|
106
117
|
rangeSelection,
|
|
118
|
+
marqueeRect,
|
|
107
119
|
theme,
|
|
108
120
|
displayTrackOrder,
|
|
109
121
|
trackOrder,
|
|
@@ -112,7 +124,7 @@ export const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
112
124
|
selectedElementId,
|
|
113
125
|
hoveredClip,
|
|
114
126
|
draggedClip,
|
|
115
|
-
resizingClip
|
|
127
|
+
resizingClip,
|
|
116
128
|
blockedClipRef,
|
|
117
129
|
suppressClickRef,
|
|
118
130
|
scrollRef,
|
|
@@ -141,9 +153,21 @@ export const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
141
153
|
onContextMenuClip,
|
|
142
154
|
beatAnalysis,
|
|
143
155
|
}: TimelineCanvasProps) {
|
|
144
|
-
const {
|
|
145
|
-
|
|
156
|
+
const {
|
|
157
|
+
onResizeElement,
|
|
158
|
+
onMoveElement,
|
|
159
|
+
onToggleTrackHidden,
|
|
160
|
+
onToggleElementHidden,
|
|
161
|
+
onRazorSplit,
|
|
162
|
+
onRazorSplitAll,
|
|
163
|
+
} = useTimelineEditContextOptional();
|
|
146
164
|
const beatDragging = usePlayerStore((s) => s.beatDragging);
|
|
165
|
+
const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
|
|
166
|
+
const activeSnapGuideTime = draggedClip?.started
|
|
167
|
+
? (draggedClip.snapBeatTime ?? draggedClip.snapGuideTime)
|
|
168
|
+
: resizingClip?.started
|
|
169
|
+
? resizingClip.snapGuideTime
|
|
170
|
+
: null;
|
|
147
171
|
const draggedElement = draggedClip?.element ?? null;
|
|
148
172
|
const activeDraggedElement =
|
|
149
173
|
draggedClip?.started === true && draggedElement
|
|
@@ -181,9 +205,16 @@ export const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
181
205
|
)}
|
|
182
206
|
</>
|
|
183
207
|
);
|
|
208
|
+
const activeDropPlacement =
|
|
209
|
+
draggedClip?.started === true ? (draggedClip.previewStackingReorder?.placement ?? null) : null;
|
|
184
210
|
|
|
185
211
|
return (
|
|
186
|
-
|
|
212
|
+
// minWidth:100% makes the lanes and ruler fill the panel when the composition
|
|
213
|
+
// is narrower than the viewport (zoomed out); content stays in time coords.
|
|
214
|
+
<div
|
|
215
|
+
className="relative"
|
|
216
|
+
style={{ height: totalH, width: GUTTER + trackContentWidth, minWidth: "100%" }}
|
|
217
|
+
>
|
|
187
218
|
<TimelineRuler
|
|
188
219
|
major={major}
|
|
189
220
|
minor={minor}
|
|
@@ -198,13 +229,35 @@ export const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
198
229
|
|
|
199
230
|
{
|
|
200
231
|
// fallow-ignore-next-line complexity
|
|
201
|
-
displayTrackOrder.map((
|
|
202
|
-
const
|
|
203
|
-
const
|
|
232
|
+
displayTrackOrder.map((layerId, rowIndex) => {
|
|
233
|
+
const layer = tracks.find((item) => item.id === layerId) ?? null;
|
|
234
|
+
const previousLayerId = displayTrackOrder[rowIndex - 1];
|
|
235
|
+
const previousLayer = previousLayerId
|
|
236
|
+
? (tracks.find((item) => item.id === previousLayerId) ?? null)
|
|
237
|
+
: null;
|
|
238
|
+
const els = layer?.elements ?? [];
|
|
239
|
+
const ts = trackStyles.get(layerId) ?? getTrackStyle("");
|
|
240
|
+
const isAudioLayer = layer?.kind === "audio";
|
|
241
|
+
const isFirstAudioLayer = isAudioLayer && previousLayer?.kind !== "audio";
|
|
204
242
|
const isPendingTrack =
|
|
205
|
-
draggedClip?.started === true && !trackOrder.includes(
|
|
243
|
+
draggedClip?.started === true && !trackOrder.includes(layerId) && els.length === 0;
|
|
244
|
+
const baseRowBackground = rowIndex % 2 === 0 ? theme.rowBackground : "#0D0E12";
|
|
245
|
+
const dropIndicator = resolveTimelineDropIndicator({
|
|
246
|
+
placement: activeDropPlacement,
|
|
247
|
+
layerId,
|
|
248
|
+
layerOrder: displayTrackOrder,
|
|
249
|
+
});
|
|
206
250
|
const rowBackground =
|
|
207
|
-
|
|
251
|
+
dropIndicator?.kind === "onto"
|
|
252
|
+
? theme.clipBackgroundActive
|
|
253
|
+
: isAudioLayer
|
|
254
|
+
? `linear-gradient(90deg, ${theme.gutterBackground} 0, ${baseRowBackground} 74px)`
|
|
255
|
+
: baseRowBackground;
|
|
256
|
+
const showGroupHeader = shouldShowTimelineLayerGroupHeader(
|
|
257
|
+
layer?.contextKey ?? "",
|
|
258
|
+
previousLayer?.contextKey ?? "",
|
|
259
|
+
);
|
|
260
|
+
const rowTrack = layer?.placementTrack ?? els[0]?.track ?? 0;
|
|
208
261
|
// The beat-dot strip occupies the top of this track's lane (active track,
|
|
209
262
|
// or the music track when nothing is selected). When shown, keyframe
|
|
210
263
|
// diamonds shrink + drop to the bottom half so they don't collide with it.
|
|
@@ -215,318 +268,309 @@ export const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
215
268
|
: els.some(isMusicTrack));
|
|
216
269
|
const isTrackHidden = els.length > 0 && els.every((element) => element.hidden === true);
|
|
217
270
|
return (
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
271
|
+
<Fragment key={layerId}>
|
|
272
|
+
{showGroupHeader && layer && (
|
|
273
|
+
<TimelineLayerGroupHeader
|
|
274
|
+
contextKey={layer.contextKey}
|
|
275
|
+
trackContentWidth={trackContentWidth}
|
|
276
|
+
theme={theme}
|
|
277
|
+
accentColor={ts.accent}
|
|
278
|
+
/>
|
|
279
|
+
)}
|
|
227
280
|
<div
|
|
228
|
-
className="
|
|
281
|
+
className="relative flex"
|
|
229
282
|
style={{
|
|
230
|
-
|
|
231
|
-
background:
|
|
232
|
-
|
|
283
|
+
height: TRACK_H,
|
|
284
|
+
background: rowBackground,
|
|
285
|
+
borderTop: isFirstAudioLayer ? `2px solid ${theme.rulerBorder}` : undefined,
|
|
286
|
+
borderBottom: `1px solid ${theme.rowBorder}`,
|
|
287
|
+
boxShadow:
|
|
288
|
+
dropIndicator?.kind === "onto" ? `inset 0 0 0 1px ${ts.accent}` : undefined,
|
|
233
289
|
}}
|
|
234
290
|
>
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
291
|
+
<TimelineLayerGutter
|
|
292
|
+
isAudio={isAudioLayer}
|
|
293
|
+
isTrackHidden={isTrackHidden}
|
|
294
|
+
rowTrack={rowTrack}
|
|
295
|
+
theme={theme}
|
|
296
|
+
onToggleHidden={() => {
|
|
297
|
+
if (onToggleElementHidden && els.length > 0) {
|
|
298
|
+
for (const element of els) {
|
|
299
|
+
void onToggleElementHidden(element.key ?? element.id, !isTrackHidden);
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
void onToggleTrackHidden?.(rowTrack, !isTrackHidden);
|
|
246
304
|
}}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
305
|
+
/>
|
|
306
|
+
<div
|
|
307
|
+
style={{
|
|
308
|
+
width: trackContentWidth,
|
|
309
|
+
opacity: isTrackHidden ? 0.35 : 1,
|
|
310
|
+
transition: "opacity 120ms ease",
|
|
250
311
|
}}
|
|
312
|
+
className="relative"
|
|
251
313
|
>
|
|
252
|
-
{
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
314
|
+
{layer?.contextKey && (
|
|
315
|
+
<span
|
|
316
|
+
className="absolute bottom-0 top-0 pointer-events-none"
|
|
317
|
+
style={{
|
|
318
|
+
left: 0,
|
|
319
|
+
width: 2,
|
|
320
|
+
background: ts.accent,
|
|
321
|
+
opacity: 0.45,
|
|
322
|
+
zIndex: 2,
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
256
325
|
)}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
transition: "opacity 120ms ease",
|
|
264
|
-
}}
|
|
265
|
-
className="relative"
|
|
266
|
-
>
|
|
267
|
-
{/* Faint beat lines in every track's background (behind the clips);
|
|
268
|
-
the active move-snap target is highlighted. */}
|
|
269
|
-
<BeatBackgroundLines
|
|
270
|
-
beatTimes={beatAnalysis?.beatTimes}
|
|
271
|
-
beatStrengths={beatAnalysis?.beatStrengths}
|
|
272
|
-
pps={pps}
|
|
273
|
-
highlightTime={draggedClip?.started ? draggedClip.snapBeatTime : null}
|
|
274
|
-
/>
|
|
275
|
-
{/* Beat dots on the active track (the one holding the selection),
|
|
276
|
-
falling back to the music track when nothing is selected. */}
|
|
277
|
-
{beatStripOnTrack && (
|
|
278
|
-
<BeatStrip
|
|
326
|
+
{dropIndicator?.kind === "line" && (
|
|
327
|
+
<TimelineDropInsertionLine edge={dropIndicator.edge} accentColor={ts.accent} />
|
|
328
|
+
)}
|
|
329
|
+
{/* Faint beat lines in every track's background (behind the clips);
|
|
330
|
+
the active snap target is highlighted. */}
|
|
331
|
+
<BeatBackgroundLines
|
|
279
332
|
beatTimes={beatAnalysis?.beatTimes}
|
|
280
333
|
beatStrengths={beatAnalysis?.beatStrengths}
|
|
281
334
|
pps={pps}
|
|
335
|
+
highlightTime={activeSnapGuideTime}
|
|
282
336
|
/>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
edge
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
previewDuration: el.duration,
|
|
349
|
-
previewPlaybackStart: el.playbackStart,
|
|
350
|
-
started: false,
|
|
351
|
-
});
|
|
352
|
-
}}
|
|
353
|
-
onPointerDown={
|
|
354
|
-
// fallow-ignore-next-line complexity
|
|
355
|
-
(e) => {
|
|
356
|
-
if (e.button !== 0) return;
|
|
357
|
-
if (usePlayerStore.getState().activeTool === "razor") return;
|
|
358
|
-
if (e.shiftKey) {
|
|
359
|
-
shiftClickClipRef.current = {
|
|
360
|
-
element: el,
|
|
361
|
-
anchorX: e.clientX,
|
|
362
|
-
anchorY: e.clientY,
|
|
363
|
-
};
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
const target = e.currentTarget as HTMLElement;
|
|
367
|
-
const rect = target.getBoundingClientRect();
|
|
368
|
-
const blockedIntent = resolveBlockedTimelineEditIntent({
|
|
369
|
-
width: rect.width,
|
|
370
|
-
offsetX: e.clientX - rect.left,
|
|
371
|
-
handleWidth: CLIP_HANDLE_W,
|
|
372
|
-
capabilities,
|
|
373
|
-
});
|
|
374
|
-
if (
|
|
375
|
-
blockedIntent &&
|
|
376
|
-
((blockedIntent === "move" && onMoveElement) ||
|
|
377
|
-
(blockedIntent !== "move" && onResizeElement))
|
|
378
|
-
) {
|
|
379
|
-
blockedClipRef.current = {
|
|
380
|
-
element: el,
|
|
381
|
-
intent: blockedIntent,
|
|
382
|
-
originClientX: e.clientX,
|
|
383
|
-
originClientY: e.clientY,
|
|
384
|
-
started: false,
|
|
385
|
-
};
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
if (!onMoveElement || !capabilities.canMove) return;
|
|
337
|
+
{/* Beat dots on the active track (the one holding the selection),
|
|
338
|
+
falling back to the music track when nothing is selected. */}
|
|
339
|
+
{beatStripOnTrack && (
|
|
340
|
+
<BeatStrip
|
|
341
|
+
beatTimes={beatAnalysis?.beatTimes}
|
|
342
|
+
beatStrengths={beatAnalysis?.beatStrengths}
|
|
343
|
+
pps={pps}
|
|
344
|
+
/>
|
|
345
|
+
)}
|
|
346
|
+
{isPendingTrack && (
|
|
347
|
+
<div
|
|
348
|
+
className="absolute inset-0 flex items-center"
|
|
349
|
+
style={{
|
|
350
|
+
paddingLeft: 16,
|
|
351
|
+
color: ts.label,
|
|
352
|
+
fontSize: 11,
|
|
353
|
+
letterSpacing: 0,
|
|
354
|
+
textTransform: "uppercase",
|
|
355
|
+
opacity: 0.5,
|
|
356
|
+
}}
|
|
357
|
+
>
|
|
358
|
+
New track
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
{
|
|
362
|
+
// fallow-ignore-next-line complexity
|
|
363
|
+
els.map((el) => {
|
|
364
|
+
const clipStyle = getTrackStyle(el.tag);
|
|
365
|
+
const elementKey = el.key ?? el.id;
|
|
366
|
+
const capabilities = getTimelineEditCapabilities(el);
|
|
367
|
+
const isSelected = selectedElementIds.has(elementKey);
|
|
368
|
+
const isComposition = !!el.compositionSrc;
|
|
369
|
+
// elementKey (el.key ?? el.id) is already unique per clip; do NOT
|
|
370
|
+
// fold in the map index, or a splice/reorder remounts every clip
|
|
371
|
+
// at/after the change (DOM flash, drag interruption).
|
|
372
|
+
const clipKey = elementKey;
|
|
373
|
+
const isDraggingClip =
|
|
374
|
+
draggedClip?.started === true &&
|
|
375
|
+
(draggedElement?.key ?? draggedElement?.id) === elementKey;
|
|
376
|
+
if (isDraggingClip) return null;
|
|
377
|
+
const previewElement = getPreviewElement(el);
|
|
378
|
+
return (
|
|
379
|
+
<TimelineClip
|
|
380
|
+
key={clipKey}
|
|
381
|
+
onContextMenu={(e: React.MouseEvent) => {
|
|
382
|
+
e.preventDefault();
|
|
383
|
+
onContextMenuClip?.(e, el);
|
|
384
|
+
}}
|
|
385
|
+
el={previewElement}
|
|
386
|
+
pps={pps}
|
|
387
|
+
clipY={CLIP_Y}
|
|
388
|
+
isSelected={isSelected}
|
|
389
|
+
isHovered={hoveredClip === clipKey}
|
|
390
|
+
isDragging={false}
|
|
391
|
+
hasCustomContent={!!renderClipContent}
|
|
392
|
+
capabilities={capabilities}
|
|
393
|
+
theme={theme}
|
|
394
|
+
isComposition={isComposition}
|
|
395
|
+
onHoverStart={() => setHoveredClip(clipKey)}
|
|
396
|
+
onHoverEnd={() => setHoveredClip(null)}
|
|
397
|
+
onResizeStart={(edge, e) => {
|
|
398
|
+
if (e.button !== 0 || e.shiftKey || !onResizeElement) return;
|
|
399
|
+
if (edge === "start" && !capabilities.canTrimStart) return;
|
|
400
|
+
if (edge === "end" && !capabilities.canTrimEnd) return;
|
|
401
|
+
e.stopPropagation();
|
|
389
402
|
blockedClipRef.current = null;
|
|
390
403
|
setShowPopover(false);
|
|
391
404
|
setRangeSelection(null);
|
|
392
|
-
|
|
405
|
+
setResizingClip({
|
|
393
406
|
element: el,
|
|
407
|
+
edge,
|
|
394
408
|
originClientX: e.clientX,
|
|
395
|
-
originClientY: e.clientY,
|
|
396
|
-
originScrollLeft: scrollRef.current?.scrollLeft ?? 0,
|
|
397
|
-
originScrollTop: scrollRef.current?.scrollTop ?? 0,
|
|
398
|
-
pointerClientX: e.clientX,
|
|
399
|
-
pointerClientY: e.clientY,
|
|
400
|
-
pointerOffsetX: e.clientX - rect.left,
|
|
401
|
-
pointerOffsetY: e.clientY - rect.top,
|
|
402
409
|
previewStart: el.start,
|
|
403
|
-
|
|
404
|
-
|
|
410
|
+
previewDuration: el.duration,
|
|
411
|
+
previewPlaybackStart: el.playbackStart,
|
|
412
|
+
snapGuideTime: null,
|
|
413
|
+
snapGuideKind: null,
|
|
405
414
|
started: false,
|
|
406
415
|
});
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
416
|
+
}}
|
|
417
|
+
onPointerDown={
|
|
418
|
+
// fallow-ignore-next-line complexity
|
|
419
|
+
(e) => {
|
|
420
|
+
if (e.button !== 0) return;
|
|
421
|
+
if (usePlayerStore.getState().activeTool === "razor") return;
|
|
422
|
+
if (e.shiftKey) {
|
|
423
|
+
shiftClickClipRef.current = {
|
|
424
|
+
element: el,
|
|
425
|
+
anchorX: e.clientX,
|
|
426
|
+
anchorY: e.clientY,
|
|
427
|
+
};
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const target = e.currentTarget as HTMLElement;
|
|
431
|
+
const rect = target.getBoundingClientRect();
|
|
432
|
+
const blockedIntent = resolveBlockedTimelineEditIntent({
|
|
433
|
+
width: rect.width,
|
|
434
|
+
offsetX: e.clientX - rect.left,
|
|
435
|
+
handleWidth: CLIP_HANDLE_W,
|
|
436
|
+
capabilities,
|
|
437
|
+
});
|
|
438
|
+
if (
|
|
439
|
+
blockedIntent &&
|
|
440
|
+
((blockedIntent === "move" && onMoveElement) ||
|
|
441
|
+
(blockedIntent !== "move" && onResizeElement))
|
|
442
|
+
) {
|
|
443
|
+
blockedClipRef.current = {
|
|
444
|
+
element: el,
|
|
445
|
+
intent: blockedIntent,
|
|
446
|
+
originClientX: e.clientX,
|
|
447
|
+
originClientY: e.clientY,
|
|
448
|
+
started: false,
|
|
449
|
+
};
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (!onMoveElement || !capabilities.canMove) return;
|
|
453
|
+
blockedClipRef.current = null;
|
|
454
|
+
setShowPopover(false);
|
|
455
|
+
setRangeSelection(null);
|
|
456
|
+
setDraggedClip({
|
|
457
|
+
element: el,
|
|
458
|
+
originClientX: e.clientX,
|
|
459
|
+
originClientY: e.clientY,
|
|
460
|
+
originScrollLeft: scrollRef.current?.scrollLeft ?? 0,
|
|
461
|
+
originScrollTop: scrollRef.current?.scrollTop ?? 0,
|
|
462
|
+
pointerClientX: e.clientX,
|
|
463
|
+
pointerClientY: e.clientY,
|
|
464
|
+
pointerOffsetX: e.clientX - rect.left,
|
|
465
|
+
pointerOffsetY: e.clientY - rect.top,
|
|
466
|
+
previewStart: el.start,
|
|
467
|
+
previewTrack: el.track,
|
|
468
|
+
previewLayerId: layerId,
|
|
469
|
+
previewLayerIndex: rowIndex,
|
|
470
|
+
previewStackingReorder: null,
|
|
471
|
+
snapBeatTime: null,
|
|
472
|
+
snapGuideTime: null,
|
|
473
|
+
snapGuideKind: null,
|
|
474
|
+
started: false,
|
|
475
|
+
});
|
|
476
|
+
syncClipDragAutoScroll(e.clientX, e.clientY);
|
|
433
477
|
}
|
|
434
|
-
return;
|
|
435
478
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
479
|
+
onClick={(e) => {
|
|
480
|
+
e.stopPropagation();
|
|
481
|
+
if (suppressClickRef.current) return;
|
|
482
|
+
const { activeTool } = usePlayerStore.getState();
|
|
483
|
+
if (activeTool === "razor" && onRazorSplit) {
|
|
484
|
+
const clipRect = (
|
|
485
|
+
e.currentTarget as HTMLElement
|
|
486
|
+
).getBoundingClientRect();
|
|
487
|
+
const clickOffsetX = e.clientX - clipRect.left;
|
|
488
|
+
const splitTime = previewElement.start + clickOffsetX / pps;
|
|
489
|
+
const clampedTime = Math.max(
|
|
490
|
+
previewElement.start + SPLIT_BOUNDARY_EPSILON_S,
|
|
491
|
+
Math.min(
|
|
492
|
+
previewElement.start +
|
|
493
|
+
previewElement.duration -
|
|
494
|
+
SPLIT_BOUNDARY_EPSILON_S,
|
|
495
|
+
splitTime,
|
|
496
|
+
),
|
|
497
|
+
);
|
|
498
|
+
if (e.shiftKey && onRazorSplitAll) {
|
|
499
|
+
onRazorSplitAll(clampedTime);
|
|
500
|
+
} else {
|
|
501
|
+
onRazorSplit(el, clampedTime);
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
460
504
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
suppressClickRef
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
505
|
+
const nextElement = isSelected ? null : el;
|
|
506
|
+
setSelectedElementId(nextElement ? elementKey : null);
|
|
507
|
+
onSelectElement?.(nextElement);
|
|
508
|
+
}}
|
|
509
|
+
onDoubleClick={(e) => {
|
|
510
|
+
e.stopPropagation();
|
|
511
|
+
if (suppressClickRef.current) return;
|
|
512
|
+
if (isComposition && onDrillDown) onDrillDown(el);
|
|
513
|
+
}}
|
|
514
|
+
>
|
|
515
|
+
{renderClipChildren(previewElement, clipStyle)}
|
|
516
|
+
{STUDIO_KEYFRAMES_ENABLED && keyframeCache?.get(elementKey) && (
|
|
517
|
+
<TimelineClipDiamonds
|
|
518
|
+
keyframesData={keyframeCache.get(elementKey)!}
|
|
519
|
+
clipWidthPx={Math.max(previewElement.duration * pps, 4)}
|
|
520
|
+
clipHeightPx={TRACK_H - 2 * CLIP_Y}
|
|
521
|
+
beatsActive={beatStripOnTrack}
|
|
522
|
+
beatTimes={beatAnalysis?.beatTimes}
|
|
523
|
+
clipStart={previewElement.start}
|
|
524
|
+
clipDurationSeconds={previewElement.duration}
|
|
525
|
+
pixelsPerSecond={pps}
|
|
526
|
+
accentColor={clipStyle.accent}
|
|
527
|
+
isSelected={isSelected}
|
|
528
|
+
currentPercentage={
|
|
529
|
+
previewElement.duration > 0
|
|
530
|
+
? ((currentTime - previewElement.start) /
|
|
531
|
+
previewElement.duration) *
|
|
532
|
+
100
|
|
533
|
+
: 0
|
|
534
|
+
}
|
|
535
|
+
elementId={elementKey}
|
|
536
|
+
selectedKeyframes={selectedKeyframes}
|
|
537
|
+
onClickKeyframe={(pct) => onClickKeyframe?.(previewElement, pct)}
|
|
538
|
+
onShiftClickKeyframe={onShiftClickKeyframe}
|
|
539
|
+
onContextMenuKeyframe={onContextMenuKeyframe}
|
|
540
|
+
onMoveKeyframe={onMoveKeyframe}
|
|
541
|
+
suppressClickRef={suppressClickRef}
|
|
542
|
+
/>
|
|
543
|
+
)}
|
|
544
|
+
</TimelineClip>
|
|
545
|
+
);
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
</div>
|
|
474
549
|
</div>
|
|
475
|
-
</
|
|
550
|
+
</Fragment>
|
|
476
551
|
);
|
|
477
552
|
})
|
|
478
553
|
}
|
|
479
554
|
|
|
480
|
-
{/* Drag ghost */}
|
|
481
555
|
{activeDraggedElement && activeDraggedPosition && (
|
|
482
|
-
<
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
zIndex: 40,
|
|
490
|
-
}}
|
|
556
|
+
<TimelineDragGhost
|
|
557
|
+
element={activeDraggedElement}
|
|
558
|
+
position={activeDraggedPosition}
|
|
559
|
+
pps={pps}
|
|
560
|
+
selectedElementId={selectedElementId}
|
|
561
|
+
hasCustomContent={!!renderClipContent}
|
|
562
|
+
theme={theme}
|
|
491
563
|
>
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
pps={pps}
|
|
495
|
-
clipY={0}
|
|
496
|
-
isSelected={selectedElementId === (activeDraggedElement.key ?? activeDraggedElement.id)}
|
|
497
|
-
isHovered={false}
|
|
498
|
-
isDragging={true}
|
|
499
|
-
hasCustomContent={!!renderClipContent}
|
|
500
|
-
capabilities={getTimelineEditCapabilities(activeDraggedElement)}
|
|
501
|
-
theme={theme}
|
|
502
|
-
isComposition={!!activeDraggedElement.compositionSrc}
|
|
503
|
-
onHoverStart={() => {}}
|
|
504
|
-
onHoverEnd={() => {}}
|
|
505
|
-
onResizeStart={() => {}}
|
|
506
|
-
onClick={() => {}}
|
|
507
|
-
onDoubleClick={() => {}}
|
|
508
|
-
>
|
|
509
|
-
{renderClipChildren(activeDraggedElement, getTrackStyle(activeDraggedElement.tag))}
|
|
510
|
-
</TimelineClip>
|
|
511
|
-
</div>
|
|
564
|
+
{renderClipChildren(activeDraggedElement, getTrackStyle(activeDraggedElement.tag))}
|
|
565
|
+
</TimelineDragGhost>
|
|
512
566
|
)}
|
|
513
567
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
width: Math.abs(rangeSelection.end - rangeSelection.start) * pps,
|
|
521
|
-
top: RULER_H,
|
|
522
|
-
bottom: 0,
|
|
523
|
-
backgroundColor: "rgba(59, 130, 246, 0.12)",
|
|
524
|
-
borderLeft: "1px solid rgba(59, 130, 246, 0.4)",
|
|
525
|
-
borderRight: "1px solid rgba(59, 130, 246, 0.4)",
|
|
526
|
-
zIndex: 50,
|
|
527
|
-
}}
|
|
528
|
-
/>
|
|
529
|
-
)}
|
|
568
|
+
<TimelineSelectionOverlays
|
|
569
|
+
rangeSelection={rangeSelection}
|
|
570
|
+
marqueeRect={marqueeRect}
|
|
571
|
+
pps={pps}
|
|
572
|
+
accentColor={getTrackStyle("").accent}
|
|
573
|
+
/>
|
|
530
574
|
|
|
531
575
|
{/* Playhead — hidden while dragging a beat so its guideline doesn't
|
|
532
576
|
track the scrub and clutter the beat being moved. */}
|