@hyperframes/studio 0.7.75 → 0.7.77
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-BrfzYqX3.js → hyperframes-player-CEggaaxR.js} +1 -1
- package/dist/assets/{index-BvGKYVrH.js → index-B1INQNmj.js} +1 -1
- package/dist/assets/{index-CjGZkNxS.js → index-B1Tjjdse.js} +1 -1
- package/dist/assets/index-Bp4jAYZG.js +428 -0
- package/dist/assets/index-CBAfprvx.css +1 -0
- package/dist/index.d.ts +64 -17
- package/dist/index.html +2 -2
- package/dist/index.js +18040 -16293
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +16 -17
- package/src/components/EditorShell.tsx +3 -0
- package/src/components/TimelineToolbar.test.tsx +48 -3
- package/src/components/TimelineToolbar.tsx +89 -36
- package/src/components/editor/AnimationCard.test.tsx +18 -75
- package/src/components/editor/AnimationCard.tsx +26 -2
- package/src/components/editor/GsapAddAnimationControl.tsx +68 -0
- package/src/components/editor/GsapAnimationSection.tsx +18 -157
- package/src/components/editor/KeyframeEaseList.tsx +5 -1
- package/src/components/editor/KeyframeNavigation.tsx +46 -14
- package/src/components/editor/KeyframeNavigationDiamond.test.tsx +59 -0
- package/src/components/editor/MotionPathOverlay.tsx +32 -10
- package/src/components/editor/PropertyPanel.tsx +15 -5
- package/src/components/editor/PropertyPanelFlat.tsx +48 -23
- package/src/components/editor/gsapAnimationCallbacks.test.ts +122 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +108 -4
- package/src/components/editor/keyframeRetime.test.ts +25 -22
- package/src/components/editor/keyframeRetime.ts +1 -2
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +2 -2
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +25 -139
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +32 -16
- package/src/components/editor/propertyPanelFlatTextSection.tsx +21 -4
- package/src/components/nle/PreviewOverlays.tsx +3 -1
- package/src/components/nle/useTimelineEditCallbacks.test.ts +30 -0
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +693 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +197 -57
- package/src/contexts/TimelineEditContext.tsx +1 -1
- package/src/hooks/deleteSelectedKeyframes.ts +24 -8
- package/src/hooks/gsapDragCommit.ts +2 -2
- package/src/hooks/gsapDragPositionCommit.ts +84 -5
- package/src/hooks/gsapKeyframeCacheHelpers.test.ts +176 -1
- package/src/hooks/gsapKeyframeCacheHelpers.ts +93 -52
- package/src/hooks/gsapRuntimeBridge.test.ts +100 -0
- package/src/hooks/gsapScriptCommitHelpers.ts +3 -2
- package/src/hooks/gsapScriptCommitTypes.ts +2 -0
- package/src/hooks/gsapShared.test.ts +109 -1
- package/src/hooks/gsapShared.ts +142 -17
- package/src/hooks/gsapTweenSynth.test.ts +30 -1
- package/src/hooks/gsapTweenSynth.ts +61 -14
- package/src/hooks/keyframeCacheAstLoad.ts +181 -0
- package/src/hooks/timelineEditingHelpers.test.ts +80 -0
- package/src/hooks/timelineMoveAdapter.ts +9 -2
- package/src/hooks/useDomEditWiring.ts +2 -2
- package/src/hooks/useDomSelection.ts +4 -0
- package/src/hooks/useEnableKeyframes.test.ts +122 -0
- package/src/hooks/useEnableKeyframes.ts +116 -56
- package/src/hooks/useGestureCommit.ts +2 -2
- package/src/hooks/useGsapAnimationFetchFallback.ts +1 -1
- package/src/hooks/useGsapKeyframeOps.test.tsx +92 -13
- package/src/hooks/useGsapKeyframeOps.ts +30 -13
- package/src/hooks/useGsapScriptCommits.test.tsx +21 -0
- package/src/hooks/useGsapScriptCommits.ts +2 -0
- package/src/hooks/useGsapSelectionHandlers.test.tsx +69 -3
- package/src/hooks/useGsapSelectionHandlers.ts +90 -65
- package/src/hooks/useGsapTweenCache.test.ts +16 -0
- package/src/hooks/useGsapTweenCache.ts +64 -178
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +68 -0
- package/src/hooks/useStudioContextValue.test.ts +93 -0
- package/src/hooks/useStudioContextValue.ts +17 -3
- package/src/hooks/useStudioTestHooks.ts +63 -0
- package/src/hooks/useTimelineEditing.test.tsx +187 -22
- package/src/hooks/useTimelineEditing.ts +43 -32
- package/src/hooks/useTimelineEditingTypes.ts +2 -0
- package/src/hooks/useTimelineGroupEditing.ts +106 -82
- package/src/player/components/KeyframeDiamondContextMenu.test.tsx +69 -0
- package/src/player/components/KeyframeDiamondContextMenu.tsx +19 -8
- package/src/player/components/LayerDisclosureRow.tsx +76 -0
- package/src/player/components/Timeline.test.ts +420 -90
- package/src/player/components/Timeline.tsx +80 -78
- package/src/player/components/TimelineCanvas.tsx +33 -21
- package/src/player/components/TimelineClip.tsx +3 -1
- package/src/player/components/TimelineClipDiamonds.test.tsx +540 -16
- package/src/player/components/TimelineClipDiamonds.tsx +320 -117
- package/src/player/components/TimelineDiamondConnectors.tsx +145 -0
- package/src/player/components/TimelineLanes.tsx +153 -79
- package/src/player/components/TimelineOverlays.tsx +4 -18
- package/src/player/components/TimelinePropertyLanes.test.tsx +450 -0
- package/src/player/components/TimelinePropertyLanes.tsx +191 -0
- package/src/player/components/TimelineRazorInteraction.tsx +60 -0
- package/src/player/components/TimelineRuler.tsx +11 -21
- package/src/player/components/TimelineTrackHeader.test.tsx +346 -0
- package/src/player/components/TimelineTrackHeader.tsx +363 -0
- package/src/player/components/TrackClipCount.tsx +17 -0
- package/src/player/components/timelineCallbacks.ts +21 -6
- package/src/player/components/timelineClipDragPreview.test.ts +50 -3
- package/src/player/components/timelineClipDragPreview.ts +33 -15
- package/src/player/components/timelineCollision.ts +4 -13
- package/src/player/components/timelineDiamondTypes.ts +120 -0
- package/src/player/components/timelineDragDrop.ts +8 -3
- package/src/player/components/timelineEditing.test.ts +19 -27
- package/src/player/components/timelineEditing.ts +7 -8
- package/src/player/components/timelineKeyframeIdentity.test.ts +49 -0
- package/src/player/components/timelineKeyframeIdentity.ts +66 -0
- package/src/player/components/timelineLayout.test.ts +162 -9
- package/src/player/components/timelineLayout.ts +198 -38
- package/src/player/components/timelineMarquee.test.ts +76 -21
- package/src/player/components/timelineMarquee.ts +17 -14
- package/src/player/components/trackHeaderLaneState.ts +121 -0
- package/src/player/components/trackHeaderLaneValues.ts +139 -0
- package/src/player/components/useAutoExpandKeyframedClips.test.tsx +75 -0
- package/src/player/components/useAutoExpandKeyframedClips.ts +50 -0
- package/src/player/components/useTimelineClipDrag.ts +4 -1
- package/src/player/components/useTimelineGeometry.ts +4 -1
- package/src/player/components/useTimelineKeyframeHandlers.test.tsx +108 -0
- package/src/player/components/useTimelineKeyframeHandlers.ts +50 -15
- package/src/player/components/useTimelinePlayhead.ts +21 -12
- package/src/player/components/useTimelineRangeSelection.ts +35 -14
- package/src/player/components/useTimelineTrackLayout.test.ts +53 -0
- package/src/player/components/useTimelineTrackLayout.ts +162 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +21 -12
- package/src/player/lib/timelineElementHelpers.ts +26 -11
- package/src/player/store/keyframeSlice.ts +122 -0
- package/src/player/store/playerStore.test.ts +25 -0
- package/src/player/store/playerStore.ts +7 -43
- package/src/telemetry/events.test.ts +9 -3
- package/src/telemetry/events.ts +16 -7
- package/src/utils/globalTimeCompiler.test.ts +10 -0
- package/src/utils/globalTimeCompiler.ts +3 -3
- package/dist/assets/index-B37rWXo4.css +0 -1
- package/dist/assets/index-DzDajONI.js +0 -428
- package/src/player/components/TimelineDragGhost.tsx +0 -59
- package/src/utils/keyframeSelection.test.ts +0 -45
- package/src/utils/keyframeSelection.ts +0 -29
|
@@ -9,7 +9,6 @@ import { defaultTimelineTheme } from "./timelineTheme";
|
|
|
9
9
|
import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
|
|
10
10
|
import { useTimelinePlayhead } from "./useTimelinePlayhead";
|
|
11
11
|
import { useTimelineActiveClips } from "./useTimelineActiveClips";
|
|
12
|
-
import { getTrackStyle } from "./timelineIcons";
|
|
13
12
|
import { useTimelineZoom } from "./useTimelineZoom";
|
|
14
13
|
import { useTimelineAssetDrop } from "./timelineDragDrop";
|
|
15
14
|
import { TimelineEmptyState } from "./TimelineEmptyState";
|
|
@@ -17,18 +16,27 @@ import { TimelineCanvas } from "./TimelineCanvas";
|
|
|
17
16
|
import { type KeyframeDiamondContextMenuState } from "./KeyframeDiamondContextMenu";
|
|
18
17
|
import { useTimelineClipDrag } from "./useTimelineClipDrag";
|
|
19
18
|
import { TimelineOverlays } from "./TimelineOverlays";
|
|
19
|
+
import { animationContributesLane } from "./TimelinePropertyLanes";
|
|
20
20
|
import { useTimelineEditPinning } from "./useTimelineEditPinning";
|
|
21
21
|
import { useTimelineStackingSync } from "./useTimelineStackingSync";
|
|
22
22
|
import { useTimelineGeometry } from "./useTimelineGeometry";
|
|
23
|
-
import {
|
|
24
|
-
import { GUTTER, TRACKS_LEFT_PAD, generateTicks
|
|
23
|
+
import { useAutoExpandKeyframedClips } from "./useAutoExpandKeyframedClips";
|
|
24
|
+
import { GUTTER, LABEL_COL_W, TRACKS_LEFT_PAD, generateTicks } from "./timelineLayout";
|
|
25
25
|
import { useTimelineScrollViewport } from "./useTimelineScrollViewport";
|
|
26
26
|
import { STUDIO_PREVIEW_FPS } from "../lib/time";
|
|
27
27
|
import { useResolvedTimelineEditCallbacks } from "./useResolvedTimelineEditCallbacks";
|
|
28
28
|
import type { TimelineProps } from "./TimelineTypes";
|
|
29
|
+
import {
|
|
30
|
+
getTrackStyle,
|
|
31
|
+
useTimelineDisplayLayout,
|
|
32
|
+
useTimelineTrackLayout,
|
|
33
|
+
} from "./useTimelineTrackLayout";
|
|
34
|
+
import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
|
|
35
|
+
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
|
|
29
36
|
import { useTrackGapMenu } from "./useTrackGapMenu";
|
|
30
37
|
import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
|
|
31
38
|
import { useStudioPlaybackContextOptional } from "../../contexts/StudioContext";
|
|
39
|
+
import { TimelineRazorGuide, useTimelineRazorInteraction } from "./TimelineRazorInteraction";
|
|
32
40
|
|
|
33
41
|
// Re-export pure utilities so existing imports from "./Timeline" still resolve.
|
|
34
42
|
export {
|
|
@@ -74,7 +82,6 @@ export const Timeline = memo(function Timeline({
|
|
|
74
82
|
onRazorSplitAll,
|
|
75
83
|
onDeleteKeyframe,
|
|
76
84
|
onDeleteAllKeyframes,
|
|
77
|
-
onChangeKeyframeEase,
|
|
78
85
|
onMoveKeyframeToPlayhead,
|
|
79
86
|
onMoveKeyframe,
|
|
80
87
|
} = useResolvedTimelineEditCallbacks({
|
|
@@ -106,6 +113,25 @@ export const Timeline = memo(function Timeline({
|
|
|
106
113
|
const timelineReady = usePlayerStore((s) => s.timelineReady);
|
|
107
114
|
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
|
|
108
115
|
const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
|
|
116
|
+
const gsapAnimations = usePlayerStore((s) => s.gsapAnimations);
|
|
117
|
+
// Label mode = comp has keyframed clips (not just when expanded): keeps the layer
|
|
118
|
+
// disclosure + property column visible and reserves a GUTTER before 0s (Figma).
|
|
119
|
+
const hasKeyframedClips = useMemo(
|
|
120
|
+
() =>
|
|
121
|
+
Array.from(gsapAnimations.values()).some((list) =>
|
|
122
|
+
// Same lane-contribution predicate the layout uses: real keyframes OR a
|
|
123
|
+
// synthesizable flat tween. Checking animation.keyframes alone left a
|
|
124
|
+
// flat-tween-only comp without its reserved label column.
|
|
125
|
+
list.some((animation) => animationContributesLane(animation)),
|
|
126
|
+
),
|
|
127
|
+
[gsapAnimations],
|
|
128
|
+
);
|
|
129
|
+
const labelMode = STUDIO_KEYFRAMES_ENABLED && hasKeyframedClips;
|
|
130
|
+
// Without the label column the pre-t=0 breathing room is still TRACKS_LEFT_PAD
|
|
131
|
+
// (dropping it would jam clip 0 against the gutter on every non-keyframed
|
|
132
|
+
// composition); in label mode the 232px label column already provides it.
|
|
133
|
+
const contentOrigin = labelMode ? LABEL_COL_W + GUTTER : GUTTER + TRACKS_LEFT_PAD;
|
|
134
|
+
const contentGutter = labelMode ? GUTTER : 0;
|
|
109
135
|
const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
|
|
110
136
|
const currentTime = usePlayerStore((s) => s.currentTime);
|
|
111
137
|
const { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent } = useTimelineZoom();
|
|
@@ -117,7 +143,6 @@ export const Timeline = memo(function Timeline({
|
|
|
117
143
|
const [hoveredClip, setHoveredClip] = useState<string | null>(null);
|
|
118
144
|
const isDragging = useRef(false);
|
|
119
145
|
const [shiftHeld, setShiftHeld] = useState(false);
|
|
120
|
-
const [razorGuideX, setRazorGuideX] = useState<number | null>(null);
|
|
121
146
|
|
|
122
147
|
useMountEffect(() => {
|
|
123
148
|
const key = (e: KeyboardEvent) => e.key === "Shift" && setShiftHeld(e.type === "keydown");
|
|
@@ -154,9 +179,10 @@ export const Timeline = memo(function Timeline({
|
|
|
154
179
|
return Number.isFinite(result) ? result : safeDur;
|
|
155
180
|
}, [rawElements, duration]);
|
|
156
181
|
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
trackOrderRef
|
|
182
|
+
const keyframeCache = usePlayerStore((s) => s.keyframeCache);
|
|
183
|
+
useAutoExpandKeyframedClips(gsapAnimations);
|
|
184
|
+
const { tracks, trackStyles, trackOrder, trackOrderRef, laneCounts, rowHeights, rowHeightsRef } =
|
|
185
|
+
useTimelineTrackLayout(expandedElements, gsapAnimations, selectedElementId, selectedElementIds);
|
|
160
186
|
const expandedElementsRef = useRef(expandedElements);
|
|
161
187
|
expandedElementsRef.current = expandedElements;
|
|
162
188
|
|
|
@@ -223,6 +249,7 @@ export const Timeline = memo(function Timeline({
|
|
|
223
249
|
ppsRef,
|
|
224
250
|
durationRef,
|
|
225
251
|
trackOrderRef,
|
|
252
|
+
rowHeightsRef,
|
|
226
253
|
onMoveElement: pinnedOnMoveElement,
|
|
227
254
|
onMoveElements: pinnedOnMoveElements,
|
|
228
255
|
onResizeElement: pinnedOnResizeElement,
|
|
@@ -241,26 +268,32 @@ export const Timeline = memo(function Timeline({
|
|
|
241
268
|
ppsRef,
|
|
242
269
|
durationRef,
|
|
243
270
|
trackOrderRef,
|
|
271
|
+
rowHeightsRef,
|
|
272
|
+
contentOrigin,
|
|
244
273
|
onFileDrop: pinnedOnFileDrop,
|
|
245
274
|
onAssetDrop: pinnedOnAssetDrop,
|
|
246
275
|
onBlockDrop: pinnedOnBlockDrop,
|
|
247
276
|
onCompositionDrop: pinnedOnCompositionDrop,
|
|
248
277
|
});
|
|
249
278
|
|
|
250
|
-
const
|
|
251
|
-
if (!draggedClip?.started || trackOrder.includes(draggedClip.previewTrack)) return trackOrder;
|
|
252
|
-
return [...trackOrder, draggedClip.previewTrack].sort((a, b) => a - b);
|
|
253
|
-
}, [draggedClip, trackOrder]);
|
|
254
|
-
|
|
255
|
-
const totalH = getTimelineCanvasHeight(displayTrackOrder.length);
|
|
279
|
+
const displayLayout = useTimelineDisplayLayout(draggedClip, trackOrder, rowHeights);
|
|
256
280
|
const { viewportWidth, showShortcutHint, setScrollRef } = useTimelineScrollViewport(scrollRef, [
|
|
257
281
|
timelineReady,
|
|
258
282
|
expandedElements.length,
|
|
259
|
-
totalH,
|
|
283
|
+
displayLayout.totalH,
|
|
260
284
|
]);
|
|
261
|
-
const keyframeCache = usePlayerStore((s) => s.keyframeCache);
|
|
262
285
|
const selectedKeyframes = usePlayerStore((s) => s.selectedKeyframes);
|
|
263
286
|
const toggleSelectedKeyframe = usePlayerStore((s) => s.toggleSelectedKeyframe);
|
|
287
|
+
const { onClickKeyframe, onSelectSegment, onShiftClickKeyframe, onContextMenuKeyframe } =
|
|
288
|
+
useTimelineKeyframeHandlers({
|
|
289
|
+
expandedElements,
|
|
290
|
+
keyframeCache,
|
|
291
|
+
onSelectElement,
|
|
292
|
+
onSeek,
|
|
293
|
+
setSelectedElementId,
|
|
294
|
+
setKfContextMenu,
|
|
295
|
+
toggleSelectedKeyframe,
|
|
296
|
+
});
|
|
264
297
|
|
|
265
298
|
const selectedElement = useMemo(
|
|
266
299
|
() =>
|
|
@@ -291,6 +324,7 @@ export const Timeline = memo(function Timeline({
|
|
|
291
324
|
isDragging,
|
|
292
325
|
scrollRef,
|
|
293
326
|
lastScrollLeftRef,
|
|
327
|
+
contentOrigin,
|
|
294
328
|
});
|
|
295
329
|
|
|
296
330
|
const laneGapStrips = useTimelineGapHighlights({
|
|
@@ -323,12 +357,21 @@ export const Timeline = memo(function Timeline({
|
|
|
323
357
|
setZoomMode,
|
|
324
358
|
setManualZoomPercent,
|
|
325
359
|
onSeek,
|
|
360
|
+
contentOrigin,
|
|
326
361
|
});
|
|
327
362
|
useTimelineActiveClips({
|
|
328
363
|
scrollRef,
|
|
329
364
|
currentTime,
|
|
330
365
|
clipStateVersion,
|
|
331
366
|
});
|
|
367
|
+
const { razorGuideX, updateRazorGuide, clearRazorGuide, splitAllAtPointer } =
|
|
368
|
+
useTimelineRazorInteraction({
|
|
369
|
+
active: activeTool === "razor",
|
|
370
|
+
scrollRef,
|
|
371
|
+
contentOrigin,
|
|
372
|
+
pixelsPerSecond: pps,
|
|
373
|
+
onSplitAll: onRazorSplitAll,
|
|
374
|
+
});
|
|
332
375
|
|
|
333
376
|
const {
|
|
334
377
|
rangeSelection,
|
|
@@ -352,7 +395,9 @@ export const Timeline = memo(function Timeline({
|
|
|
352
395
|
setShowPopover,
|
|
353
396
|
elementsRef: expandedElementsRef,
|
|
354
397
|
trackOrderRef,
|
|
398
|
+
rowHeightsRef,
|
|
355
399
|
onSelectElement,
|
|
400
|
+
contentOrigin,
|
|
356
401
|
});
|
|
357
402
|
setRangeSelectionRef.current = setRangeSelection; // stable ref consumed by useTimelineClipDrag
|
|
358
403
|
|
|
@@ -411,13 +456,8 @@ export const Timeline = memo(function Timeline({
|
|
|
411
456
|
ref={setContainerRef}
|
|
412
457
|
aria-label="Timeline"
|
|
413
458
|
className={`relative border-t select-none h-full overflow-hidden ${isDragOver ? "ring-1 ring-inset ring-studio-accent/60" : ""} ${activeTool === "razor" ? "cursor-crosshair" : shiftHeld ? "cursor-crosshair" : "cursor-default"}`}
|
|
414
|
-
onMouseMove={
|
|
415
|
-
|
|
416
|
-
const rect = scrollRef.current.getBoundingClientRect();
|
|
417
|
-
setRazorGuideX(e.clientX - rect.left + scrollRef.current.scrollLeft);
|
|
418
|
-
}
|
|
419
|
-
}}
|
|
420
|
-
onMouseLeave={() => setRazorGuideX(null)}
|
|
459
|
+
onMouseMove={updateRazorGuide}
|
|
460
|
+
onMouseLeave={clearRazorGuide}
|
|
421
461
|
style={{
|
|
422
462
|
touchAction: "pan-x pan-y",
|
|
423
463
|
background: theme.shellBackground,
|
|
@@ -435,14 +475,10 @@ export const Timeline = memo(function Timeline({
|
|
|
435
475
|
onDragLeave={() => clearDropPreview()}
|
|
436
476
|
onDrop={handleAssetDrop}
|
|
437
477
|
onPointerDown={(e) => {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const splitTime = Math.max(0, x / pps);
|
|
443
|
-
onRazorSplitAll?.(splitTime);
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
478
|
+
// Let interactive controls (keyframe nav/toggle, caret, inputs) handle
|
|
479
|
+
// their own clicks — scrubbing here would preventDefault and eat them.
|
|
480
|
+
if (e.target instanceof Element && e.target.closest("button, input, select, a")) return;
|
|
481
|
+
if (splitAllAtPointer(e)) return;
|
|
446
482
|
handlePointerDown(e);
|
|
447
483
|
}}
|
|
448
484
|
onPointerMove={handlePointerMove}
|
|
@@ -453,18 +489,22 @@ export const Timeline = memo(function Timeline({
|
|
|
453
489
|
major={major}
|
|
454
490
|
minor={minor}
|
|
455
491
|
pps={pps}
|
|
492
|
+
contentOrigin={contentOrigin}
|
|
493
|
+
contentGutter={contentGutter}
|
|
456
494
|
trackContentWidth={displayContentWidth}
|
|
457
|
-
totalH={totalH}
|
|
495
|
+
totalH={displayLayout.totalH}
|
|
458
496
|
effectiveDuration={effectiveDuration}
|
|
459
497
|
majorTickInterval={majorTickInterval}
|
|
460
498
|
rangeSelection={rangeSelection}
|
|
461
499
|
marqueeRect={marqueeRect}
|
|
462
500
|
laneGapStrips={laneGapStrips}
|
|
463
501
|
theme={theme}
|
|
464
|
-
displayTrackOrder={displayTrackOrder}
|
|
502
|
+
displayTrackOrder={displayLayout.displayTrackOrder}
|
|
503
|
+
rowHeights={displayLayout.displayRowHeights}
|
|
465
504
|
trackOrder={trackOrder}
|
|
466
505
|
tracks={tracks}
|
|
467
506
|
trackStyles={trackStyles}
|
|
507
|
+
laneCounts={laneCounts}
|
|
468
508
|
selectedElementId={selectedElementId}
|
|
469
509
|
selectedElementIds={selectedElementIds}
|
|
470
510
|
hoveredClip={hoveredClip}
|
|
@@ -490,43 +530,16 @@ export const Timeline = memo(function Timeline({
|
|
|
490
530
|
getPreviewElement={getPreviewElement}
|
|
491
531
|
getTrackStyle={getTrackStyle}
|
|
492
532
|
keyframeCache={keyframeCache}
|
|
533
|
+
gsapAnimations={gsapAnimations}
|
|
493
534
|
selectedKeyframes={selectedKeyframes}
|
|
494
535
|
currentTime={currentTime}
|
|
536
|
+
onSeek={onSeek}
|
|
495
537
|
beatAnalysis={adjustedBeatAnalysis}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
setSelectedElementId(elKey);
|
|
500
|
-
onSelectElement?.(el);
|
|
501
|
-
// Select the clicked diamond (matches shift-click); cleared above so this single-selects.
|
|
502
|
-
toggleSelectedKeyframe(`${elKey}:${pct}`);
|
|
503
|
-
const absTime = el.start + (pct / 100) * el.duration;
|
|
504
|
-
onSeek?.(absTime);
|
|
505
|
-
const kfData = keyframeCache?.get(elKey);
|
|
506
|
-
const kf = kfData?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.5);
|
|
507
|
-
usePlayerStore.getState().setActiveKeyframePct(kf?.tweenPercentage ?? null);
|
|
508
|
-
}}
|
|
509
|
-
onShiftClickKeyframe={(elId, pct) => {
|
|
510
|
-
toggleSelectedKeyframe(`${elId}:${pct}`);
|
|
511
|
-
}}
|
|
538
|
+
onSelectSegment={onSelectSegment}
|
|
539
|
+
onClickKeyframe={onClickKeyframe}
|
|
540
|
+
onShiftClickKeyframe={onShiftClickKeyframe}
|
|
512
541
|
onMoveKeyframe={onMoveKeyframe}
|
|
513
|
-
onContextMenuKeyframe={
|
|
514
|
-
const el = expandedElements.find((x) => (x.key ?? x.id) === elId);
|
|
515
|
-
if (el) {
|
|
516
|
-
setSelectedElementId(elId);
|
|
517
|
-
onSelectElement?.(el);
|
|
518
|
-
}
|
|
519
|
-
const kfData = keyframeCache.get(elId);
|
|
520
|
-
const kf = kfData?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.2);
|
|
521
|
-
setKfContextMenu({
|
|
522
|
-
x: e.clientX + 4,
|
|
523
|
-
y: e.clientY + 2,
|
|
524
|
-
elementId: elId,
|
|
525
|
-
percentage: pct,
|
|
526
|
-
tweenPercentage: kf?.tweenPercentage,
|
|
527
|
-
currentEase: kf?.ease ?? kfData?.ease,
|
|
528
|
-
});
|
|
529
|
-
}}
|
|
542
|
+
onContextMenuKeyframe={onContextMenuKeyframe}
|
|
530
543
|
onContextMenuClip={(e, el) => {
|
|
531
544
|
e.preventDefault();
|
|
532
545
|
setSelectedElementId(el.key ?? el.id);
|
|
@@ -540,16 +553,7 @@ export const Timeline = memo(function Timeline({
|
|
|
540
553
|
openGapMenu({ x: e.clientX, y: e.clientY, track, time });
|
|
541
554
|
}}
|
|
542
555
|
/>
|
|
543
|
-
{activeTool === "razor" && razorGuideX !== null &&
|
|
544
|
-
<div
|
|
545
|
-
className="absolute top-0 bottom-0 pointer-events-none z-10"
|
|
546
|
-
style={{
|
|
547
|
-
left: razorGuideX,
|
|
548
|
-
width: 1,
|
|
549
|
-
background: "rgba(239,68,68,0.7)",
|
|
550
|
-
}}
|
|
551
|
-
/>
|
|
552
|
-
)}
|
|
556
|
+
{activeTool === "razor" && razorGuideX !== null && <TimelineRazorGuide x={razorGuideX} />}
|
|
553
557
|
</div>
|
|
554
558
|
<TimelineOverlays
|
|
555
559
|
theme={theme}
|
|
@@ -562,9 +566,7 @@ export const Timeline = memo(function Timeline({
|
|
|
562
566
|
setKfContextMenu={setKfContextMenu}
|
|
563
567
|
onDeleteKeyframe={onDeleteKeyframe}
|
|
564
568
|
onDeleteAllKeyframes={onDeleteAllKeyframes}
|
|
565
|
-
onChangeKeyframeEase={onChangeKeyframeEase}
|
|
566
569
|
onMoveKeyframeToPlayhead={onMoveKeyframeToPlayhead}
|
|
567
|
-
keyframeCache={keyframeCache}
|
|
568
570
|
clipContextMenu={clipContextMenu}
|
|
569
571
|
setClipContextMenu={setClipContextMenu}
|
|
570
572
|
currentTime={currentTime}
|
|
@@ -4,16 +4,15 @@ import { PlayheadIndicator } from "./PlayheadIndicator";
|
|
|
4
4
|
import { getTimelineEditCapabilities, type TimelineRangeSelection } from "./timelineEditing";
|
|
5
5
|
import { getRenderedTimelineElement } from "./timelineTheme";
|
|
6
6
|
import {
|
|
7
|
-
GUTTER,
|
|
8
|
-
TRACK_H,
|
|
9
7
|
RULER_H,
|
|
10
8
|
CLIP_Y,
|
|
11
9
|
TRACKS_TOP_PAD,
|
|
12
10
|
TRACKS_BOTTOM_PAD,
|
|
13
|
-
|
|
11
|
+
TRACK_H,
|
|
14
12
|
PLAYHEAD_HEAD_W,
|
|
15
13
|
getTimelinePlayheadLeft,
|
|
16
14
|
getTimelineRowTop,
|
|
15
|
+
getTimelineRowHeight,
|
|
17
16
|
} from "./timelineLayout";
|
|
18
17
|
import { usePlayerStore } from "../store/playerStore";
|
|
19
18
|
import type { ResizingClipState } from "./useTimelineClipDrag";
|
|
@@ -45,8 +44,22 @@ interface TimelineCanvasProps extends TimelineLaneBaseProps {
|
|
|
45
44
|
|
|
46
45
|
export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvasProps) {
|
|
47
46
|
const { draggedClip, scrollRef, selectedElementIds, displayTrackOrder } = props;
|
|
48
|
-
const
|
|
49
|
-
|
|
47
|
+
const draggedRowIndex =
|
|
48
|
+
draggedClip?.started === true ? displayTrackOrder.indexOf(draggedClip.previewTrack) : -1;
|
|
49
|
+
const draggedRowHeight = getTimelineRowHeight(draggedRowIndex, props.rowHeights);
|
|
50
|
+
// A clip bar in an EXPANDED row still renders at TRACK_H (the property lanes
|
|
51
|
+
// occupy the rest of the row — see TimelineLanes' clipHeight), so the drag
|
|
52
|
+
// ghost and drop placeholder must clamp to it or they stretch to the full
|
|
53
|
+
// expanded row height and stop matching the clip being dragged.
|
|
54
|
+
const draggedClipHeight = Math.min(draggedRowHeight, TRACK_H) - CLIP_Y * 2;
|
|
55
|
+
const {
|
|
56
|
+
onResizeElement,
|
|
57
|
+
onMoveElement,
|
|
58
|
+
onToggleTrackHidden,
|
|
59
|
+
onTogglePropertyGroupKeyframe,
|
|
60
|
+
onRazorSplit,
|
|
61
|
+
onRazorSplitAll,
|
|
62
|
+
} = useTimelineEditContextOptional();
|
|
50
63
|
const beatDragging = usePlayerStore((s) => s.beatDragging);
|
|
51
64
|
// Scroll a clip into view when the sidebar (asset card) requests a reveal.
|
|
52
65
|
useTimelineRevealClip(scrollRef);
|
|
@@ -63,8 +76,6 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
63
76
|
// The drag ghost follows the cursor freely (both axes) — CapCut-style. The
|
|
64
77
|
// "magnetic" affordance is a highlight on the destination lane (draggedRowIndex),
|
|
65
78
|
// which flips at the MAGNETIC_TRACK_THRESHOLD point; the clip drops into it.
|
|
66
|
-
const draggedRowIndex =
|
|
67
|
-
draggedClip?.started === true ? displayTrackOrder.indexOf(draggedClip.previewTrack) : -1;
|
|
68
79
|
// Live multi-selection drag: while a selected clip is dragged, ALL selected
|
|
69
80
|
// clips move together as one rigid formation. The GRABBED clip is the free
|
|
70
81
|
// ghost below; its co-selected "passengers" slide by the SAME group-clamped
|
|
@@ -101,7 +112,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
101
112
|
return (
|
|
102
113
|
<div
|
|
103
114
|
className="relative"
|
|
104
|
-
style={{ height: props.totalH, width:
|
|
115
|
+
style={{ height: props.totalH, width: props.contentOrigin + props.trackContentWidth }}
|
|
105
116
|
>
|
|
106
117
|
<TimelineRuler
|
|
107
118
|
major={props.major}
|
|
@@ -113,6 +124,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
113
124
|
majorTickInterval={props.majorTickInterval}
|
|
114
125
|
theme={props.theme}
|
|
115
126
|
beatAnalysis={props.beatAnalysis}
|
|
127
|
+
contentOrigin={props.contentOrigin}
|
|
116
128
|
/>
|
|
117
129
|
|
|
118
130
|
{/* Breathing room between the sticky ruler and the first track lane — the
|
|
@@ -124,6 +136,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
124
136
|
draggedElement={draggedElement}
|
|
125
137
|
multiDragPreview={multiDragPreview}
|
|
126
138
|
onToggleTrackHidden={onToggleTrackHidden}
|
|
139
|
+
onTogglePropertyGroupKeyframe={onTogglePropertyGroupKeyframe}
|
|
127
140
|
onResizeElement={onResizeElement}
|
|
128
141
|
onMoveElement={onMoveElement}
|
|
129
142
|
onRazorSplit={onRazorSplit}
|
|
@@ -148,8 +161,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
148
161
|
key={`gap-${strip.kind}-${strip.track}-${gap.start}`}
|
|
149
162
|
className="pointer-events-none absolute"
|
|
150
163
|
style={{
|
|
151
|
-
top: getTimelineRowTop(rowIndex) + CLIP_Y,
|
|
152
|
-
left:
|
|
164
|
+
top: getTimelineRowTop(rowIndex, props.rowHeights) + CLIP_Y,
|
|
165
|
+
left: props.contentOrigin + gap.start * props.pps,
|
|
153
166
|
width: Math.max((gap.end - gap.start) * props.pps, 2),
|
|
154
167
|
height: TRACK_H - CLIP_Y * 2,
|
|
155
168
|
background: loud ? "rgba(60,230,172,0.18)" : "rgba(60,230,172,0.055)",
|
|
@@ -166,10 +179,10 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
166
179
|
<div
|
|
167
180
|
className="absolute pointer-events-none"
|
|
168
181
|
style={{
|
|
169
|
-
top: getTimelineRowTop(draggedRowIndex) + CLIP_Y,
|
|
170
|
-
left:
|
|
182
|
+
top: getTimelineRowTop(draggedRowIndex, props.rowHeights) + CLIP_Y,
|
|
183
|
+
left: props.contentOrigin + draggedClip.previewStart * props.pps,
|
|
171
184
|
width: Math.max(draggedClip.element.duration * props.pps, 4),
|
|
172
|
-
height:
|
|
185
|
+
height: draggedClipHeight,
|
|
173
186
|
border: "1px solid rgba(60,230,172,0.55)",
|
|
174
187
|
background: "rgba(60,230,172,0.12)",
|
|
175
188
|
borderRadius: 4,
|
|
@@ -184,8 +197,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
184
197
|
<div
|
|
185
198
|
className="absolute pointer-events-none"
|
|
186
199
|
style={{
|
|
187
|
-
top: getTimelineRowTop(draggedClip.insertRow) - 0.5,
|
|
188
|
-
left:
|
|
200
|
+
top: getTimelineRowTop(draggedClip.insertRow, props.rowHeights) - 0.5,
|
|
201
|
+
left: props.contentOrigin,
|
|
189
202
|
width: props.trackContentWidth,
|
|
190
203
|
height: 1,
|
|
191
204
|
background: "#3CE6AC",
|
|
@@ -200,7 +213,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
200
213
|
<div
|
|
201
214
|
className="absolute pointer-events-none"
|
|
202
215
|
style={{
|
|
203
|
-
left:
|
|
216
|
+
left: props.contentOrigin + draggedClip.snapTime * props.pps,
|
|
204
217
|
top: RULER_H,
|
|
205
218
|
bottom: 0,
|
|
206
219
|
width: 1,
|
|
@@ -222,7 +235,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
222
235
|
top: activeDraggedPosition.top,
|
|
223
236
|
left: activeDraggedPosition.left,
|
|
224
237
|
width: Math.max(activeDraggedElement.duration * props.pps, 4),
|
|
225
|
-
height:
|
|
238
|
+
height: draggedClipHeight,
|
|
226
239
|
zIndex: 40,
|
|
227
240
|
}}
|
|
228
241
|
>
|
|
@@ -280,8 +293,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
280
293
|
className="absolute pointer-events-none"
|
|
281
294
|
style={{
|
|
282
295
|
left:
|
|
283
|
-
|
|
284
|
-
TRACKS_LEFT_PAD +
|
|
296
|
+
props.contentOrigin +
|
|
285
297
|
Math.min(props.rangeSelection.start, props.rangeSelection.end) * props.pps,
|
|
286
298
|
width: Math.abs(props.rangeSelection.end - props.rangeSelection.start) * props.pps,
|
|
287
299
|
top: RULER_H,
|
|
@@ -297,13 +309,13 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
297
309
|
{/* Playhead — hidden while dragging a beat so its guideline doesn't
|
|
298
310
|
track the scrub and clutter the beat being moved. Explicit width +
|
|
299
311
|
the half-head offset baked into getTimelinePlayheadLeft keep the
|
|
300
|
-
inner 1px line's CENTER exactly on
|
|
312
|
+
inner 1px line's CENTER exactly on contentOrigin + t * pps (the ruler
|
|
301
313
|
ticks' center), instead of relying on shrink-wrap sizing. */}
|
|
302
314
|
<div
|
|
303
315
|
ref={props.playheadRef}
|
|
304
316
|
className="absolute top-0 bottom-0 pointer-events-none"
|
|
305
317
|
style={{
|
|
306
|
-
left: `${getTimelinePlayheadLeft(0, 0)}px`,
|
|
318
|
+
left: `${getTimelinePlayheadLeft(0, 0, props.contentOrigin)}px`,
|
|
307
319
|
width: PLAYHEAD_HEAD_W,
|
|
308
320
|
zIndex: 100,
|
|
309
321
|
display: beatDragging ? "none" : undefined,
|
|
@@ -8,6 +8,7 @@ interface TimelineClipProps {
|
|
|
8
8
|
el: TimelineElement;
|
|
9
9
|
pps: number;
|
|
10
10
|
clipY: number;
|
|
11
|
+
clipHeight?: number;
|
|
11
12
|
isSelected: boolean;
|
|
12
13
|
isHovered: boolean;
|
|
13
14
|
isDragging?: boolean;
|
|
@@ -30,6 +31,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
30
31
|
el,
|
|
31
32
|
pps,
|
|
32
33
|
clipY,
|
|
34
|
+
clipHeight,
|
|
33
35
|
isSelected,
|
|
34
36
|
isHovered,
|
|
35
37
|
isDragging = false,
|
|
@@ -71,7 +73,7 @@ export const TimelineClip = memo(function TimelineClip({
|
|
|
71
73
|
left: leftPx,
|
|
72
74
|
width: widthPx,
|
|
73
75
|
top: clipY,
|
|
74
|
-
bottom: clipY,
|
|
76
|
+
...(clipHeight === undefined ? { bottom: clipY } : { height: clipHeight }),
|
|
75
77
|
borderRadius: theme.clipRadius,
|
|
76
78
|
zIndex: isDragging ? 20 : isSelected ? 10 : isHovered ? 5 : 1,
|
|
77
79
|
// Regular cursor over clips (CapCut-style, user preference) — no grab hand.
|