@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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Fragment, useRef } from "react";
|
|
2
|
+
import { KEYFRAME_DRAG_THRESHOLD_PX } from "../../components/editor/keyframeDrag";
|
|
3
|
+
import { MiniCurveSvg } from "../../components/editor/EaseCurveSection";
|
|
4
|
+
import type { TimelineKeyframeTarget } from "./timelineKeyframeIdentity";
|
|
5
|
+
import type { TimelineDiamondKeyframe } from "./TimelineClipDiamonds";
|
|
6
|
+
|
|
7
|
+
/** One diamond's geometry within its row, as computed by the lane. */
|
|
8
|
+
export interface TimelineDiamondMarker {
|
|
9
|
+
keyframe: TimelineDiamondKeyframe;
|
|
10
|
+
centerX: number;
|
|
11
|
+
hitWidth: number;
|
|
12
|
+
visualSize: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The line between each pair of adjacent diamonds, plus the ease control that
|
|
17
|
+
* sits at the segment's midpoint. Split out of TimelineClipDiamonds only to keep
|
|
18
|
+
* that file inside the repo's file-size cap; it has no state of its own beyond
|
|
19
|
+
* the press-position guard below.
|
|
20
|
+
*/
|
|
21
|
+
export function TimelineDiamondConnectors({
|
|
22
|
+
markers,
|
|
23
|
+
centerY,
|
|
24
|
+
baseColor,
|
|
25
|
+
baseOpacity,
|
|
26
|
+
groupAware,
|
|
27
|
+
globalEase,
|
|
28
|
+
keyframeTarget,
|
|
29
|
+
onSelectSegment,
|
|
30
|
+
}: {
|
|
31
|
+
markers: readonly TimelineDiamondMarker[];
|
|
32
|
+
centerY: number;
|
|
33
|
+
baseColor: string;
|
|
34
|
+
baseOpacity: number;
|
|
35
|
+
groupAware: boolean;
|
|
36
|
+
globalEase: string;
|
|
37
|
+
keyframeTarget: (keyframe: TimelineDiamondKeyframe) => TimelineKeyframeTarget;
|
|
38
|
+
onSelectSegment?: (target: TimelineKeyframeTarget) => void;
|
|
39
|
+
}) {
|
|
40
|
+
// The ease button sits dead centre of its segment, which on a two-keyframe clip
|
|
41
|
+
// is the centre of the clip bar — the natural place to grab a clip and drag it.
|
|
42
|
+
// Swallowing pointerdown there made that grab a no-op. Instead the press falls
|
|
43
|
+
// through to the clip (so the drag starts normally) and the button keeps only
|
|
44
|
+
// the click, which we drop if the pointer actually travelled.
|
|
45
|
+
const pressXRef = useRef<number | null>(null);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
{markers.map((marker, i) => {
|
|
50
|
+
const previous = markers[i - 1];
|
|
51
|
+
if (!previous) return null;
|
|
52
|
+
const kf = marker.keyframe;
|
|
53
|
+
const x1 = previous.centerX;
|
|
54
|
+
const x2 = marker.centerX;
|
|
55
|
+
if (x2 - x1 < 1) return null;
|
|
56
|
+
const connectorLeft = x1 + previous.visualSize / 2;
|
|
57
|
+
const connectorWidth = x2 - x1 - previous.visualSize / 2 - marker.visualSize / 2;
|
|
58
|
+
// The ease button targets one segment, so it needs the keyframe's own
|
|
59
|
+
// animationId/tweenPercentage. On a merged inline row the button is
|
|
60
|
+
// hidden where the segment is ambiguous (two source animations collide
|
|
61
|
+
// at this % with different eases; see easeAmbiguous) or the keyframe has
|
|
62
|
+
// no source animation id (runtime-scanned) so there is no tween to target.
|
|
63
|
+
const target = keyframeTarget(kf);
|
|
64
|
+
const ease = kf.ease ?? globalEase;
|
|
65
|
+
return (
|
|
66
|
+
<Fragment key={`line-${i}-${previous.keyframe.percentage}-${kf.percentage}`}>
|
|
67
|
+
<div
|
|
68
|
+
className="absolute"
|
|
69
|
+
data-keyframe-connector={groupAware ? "" : undefined}
|
|
70
|
+
style={{
|
|
71
|
+
left: connectorLeft,
|
|
72
|
+
top: centerY,
|
|
73
|
+
width: Math.max(0, connectorWidth),
|
|
74
|
+
height: 2,
|
|
75
|
+
transform: "translateY(-1px)",
|
|
76
|
+
background: baseColor,
|
|
77
|
+
opacity: baseOpacity,
|
|
78
|
+
borderRadius: 1,
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
{onSelectSegment && !kf.easeAmbiguous && kf.animationId !== undefined && (
|
|
82
|
+
<div
|
|
83
|
+
className="group absolute"
|
|
84
|
+
data-keyframe-ease-segment=""
|
|
85
|
+
style={{
|
|
86
|
+
left: x1,
|
|
87
|
+
top: centerY,
|
|
88
|
+
width: x2 - x1,
|
|
89
|
+
height: 18,
|
|
90
|
+
transform: "translateY(-50%)",
|
|
91
|
+
// Own a stacking context above the diamond buttons. At fit
|
|
92
|
+
// zoom the 16px ease control can overlap its neighbouring
|
|
93
|
+
// diamond; without a z-index here the later diamond wins the
|
|
94
|
+
// hit test even though the child button has z-index 3.
|
|
95
|
+
zIndex: 3,
|
|
96
|
+
// Only the centered control is interactive. The transparent
|
|
97
|
+
// segment wrapper must not swallow connector/clip gestures.
|
|
98
|
+
pointerEvents: "none",
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
data-keyframe-ease-button=""
|
|
104
|
+
aria-label={`Edit ${ease} easing`}
|
|
105
|
+
title={`Edit ${ease} easing`}
|
|
106
|
+
className="absolute flex items-center justify-center rounded opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100"
|
|
107
|
+
style={{
|
|
108
|
+
left: "50%",
|
|
109
|
+
top: "50%",
|
|
110
|
+
width: 16,
|
|
111
|
+
height: 16,
|
|
112
|
+
transform: "translate(-50%, -50%)",
|
|
113
|
+
zIndex: 3,
|
|
114
|
+
pointerEvents: "auto",
|
|
115
|
+
padding: 0,
|
|
116
|
+
border: "1px solid rgba(255, 255, 255, 0.14)",
|
|
117
|
+
background: "#171717",
|
|
118
|
+
cursor: "pointer",
|
|
119
|
+
}}
|
|
120
|
+
onPointerDown={(e) => {
|
|
121
|
+
pressXRef.current = e.clientX;
|
|
122
|
+
}}
|
|
123
|
+
onClick={(e) => {
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
const pressX = pressXRef.current;
|
|
126
|
+
pressXRef.current = null;
|
|
127
|
+
if (
|
|
128
|
+
pressX !== null &&
|
|
129
|
+
Math.abs(e.clientX - pressX) >= KEYFRAME_DRAG_THRESHOLD_PX
|
|
130
|
+
) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
onSelectSegment(target);
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
<MiniCurveSvg ease={ease} active size={12} />
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</Fragment>
|
|
141
|
+
);
|
|
142
|
+
})}
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
3
|
import { BeatStrip, BeatBackgroundLines } from "./BeatStrip";
|
|
4
4
|
import { TimelineClip } from "./TimelineClip";
|
|
5
5
|
import { TimelineClipDiamonds } from "./TimelineClipDiamonds";
|
|
6
|
+
import { TimelinePropertyLanes } from "./TimelinePropertyLanes";
|
|
7
|
+
import { TimelineTrackHeader } from "./TimelineTrackHeader";
|
|
8
|
+
import { resolveTrackKeyframeClip } from "./useTimelineTrackLayout";
|
|
9
|
+
import type { TimelineKeyframeTarget } from "./timelineKeyframeIdentity";
|
|
6
10
|
import type { MusicBeatAnalysis } from "@hyperframes/core/beats";
|
|
7
11
|
import { getTimelineEditCapabilities, resolveBlockedTimelineEditIntent } from "./timelineEditing";
|
|
8
12
|
import type { TimelineTheme } from "./timelineTheme";
|
|
9
|
-
import {
|
|
13
|
+
import { CLIP_Y, CLIP_HANDLE_W, TRACK_H, getTimelineRowHeight } from "./timelineLayout";
|
|
10
14
|
import {
|
|
11
15
|
usePlayerStore,
|
|
12
16
|
type TimelineElement,
|
|
@@ -21,9 +25,9 @@ import {
|
|
|
21
25
|
import type { TrackVisualStyle } from "./timelineIcons";
|
|
22
26
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
23
27
|
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
|
|
28
|
+
import { trackStudioKeyframeLaneExpand } from "../../telemetry/events";
|
|
24
29
|
import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
|
|
25
30
|
import { isAudioTimelineElement, isMusicTrack } from "../../utils/timelineInspector";
|
|
26
|
-
import { Music } from "../../icons/SystemIcons";
|
|
27
31
|
import { renderClipChildren } from "./timelineClipChildren";
|
|
28
32
|
|
|
29
33
|
/**
|
|
@@ -34,12 +38,16 @@ import { renderClipChildren } from "./timelineClipChildren";
|
|
|
34
38
|
*/
|
|
35
39
|
export interface TimelineLaneBaseProps {
|
|
36
40
|
pps: number;
|
|
41
|
+
contentOrigin: number;
|
|
42
|
+
contentGutter: number;
|
|
37
43
|
trackContentWidth: number;
|
|
38
44
|
theme: TimelineTheme;
|
|
39
45
|
displayTrackOrder: number[];
|
|
46
|
+
rowHeights: readonly number[];
|
|
40
47
|
trackOrder: number[];
|
|
41
48
|
tracks: [number, TimelineElement[]][];
|
|
42
49
|
trackStyles: Map<number, TrackVisualStyle>;
|
|
50
|
+
laneCounts: ReadonlyMap<string, number>;
|
|
43
51
|
selectedElementId: string | null;
|
|
44
52
|
selectedElementIds: Set<string>;
|
|
45
53
|
hoveredClip: string | null;
|
|
@@ -69,16 +77,26 @@ export interface TimelineLaneBaseProps {
|
|
|
69
77
|
getPreviewElement: (element: TimelineElement) => TimelineElement;
|
|
70
78
|
getTrackStyle: (tag: string) => TrackVisualStyle;
|
|
71
79
|
keyframeCache?: Map<string, KeyframeCacheEntry>;
|
|
80
|
+
gsapAnimations: Map<string, GsapAnimation[]>;
|
|
72
81
|
selectedKeyframes: Set<string>;
|
|
73
82
|
currentTime: number;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
onSeek?: (time: number) => void;
|
|
84
|
+
onSelectSegment?: (elementId: string, target: TimelineKeyframeTarget) => void;
|
|
85
|
+
onClickKeyframe?: (element: TimelineElement, target: TimelineKeyframeTarget) => void;
|
|
86
|
+
onShiftClickKeyframe?: (elementId: string, target: TimelineKeyframeTarget) => void;
|
|
87
|
+
onContextMenuKeyframe?: (
|
|
88
|
+
e: React.MouseEvent,
|
|
89
|
+
elementId: string,
|
|
90
|
+
target: TimelineKeyframeTarget,
|
|
91
|
+
) => void;
|
|
77
92
|
onMoveKeyframe?: (
|
|
78
93
|
elementId: string,
|
|
79
|
-
|
|
94
|
+
keyframe: TimelineKeyframeTarget,
|
|
80
95
|
toClipPercentage: number,
|
|
81
|
-
|
|
96
|
+
propertyGroup?: string,
|
|
97
|
+
tweenPercentage?: number,
|
|
98
|
+
animationId?: string,
|
|
99
|
+
) => Promise<boolean>;
|
|
82
100
|
onContextMenuClip?: (e: React.MouseEvent, element: TimelineElement) => void;
|
|
83
101
|
/**
|
|
84
102
|
* Right-click on EMPTY lane space (not on a clip — those preventDefault
|
|
@@ -94,6 +112,7 @@ interface TimelineLanesProps extends TimelineLaneBaseProps {
|
|
|
94
112
|
draggedElement: TimelineElement | null;
|
|
95
113
|
multiDragPreview: MultiDragPreviewInput | null;
|
|
96
114
|
onToggleTrackHidden: TimelineEditCallbacks["onToggleTrackHidden"];
|
|
115
|
+
onTogglePropertyGroupKeyframe: TimelineEditCallbacks["onTogglePropertyGroupKeyframe"];
|
|
97
116
|
onResizeElement: TimelineEditCallbacks["onResizeElement"];
|
|
98
117
|
onMoveElement: TimelineEditCallbacks["onMoveElement"];
|
|
99
118
|
onRazorSplit: TimelineEditCallbacks["onRazorSplit"];
|
|
@@ -102,12 +121,16 @@ interface TimelineLanesProps extends TimelineLaneBaseProps {
|
|
|
102
121
|
|
|
103
122
|
export function TimelineLanes({
|
|
104
123
|
pps,
|
|
124
|
+
contentOrigin,
|
|
125
|
+
contentGutter,
|
|
105
126
|
trackContentWidth,
|
|
106
127
|
theme,
|
|
107
128
|
displayTrackOrder,
|
|
129
|
+
rowHeights,
|
|
108
130
|
trackOrder,
|
|
109
131
|
tracks,
|
|
110
132
|
trackStyles,
|
|
133
|
+
laneCounts,
|
|
111
134
|
selectedElementId,
|
|
112
135
|
selectedElementIds,
|
|
113
136
|
hoveredClip,
|
|
@@ -132,8 +155,11 @@ export function TimelineLanes({
|
|
|
132
155
|
getPreviewElement,
|
|
133
156
|
getTrackStyle,
|
|
134
157
|
keyframeCache,
|
|
158
|
+
gsapAnimations,
|
|
135
159
|
selectedKeyframes,
|
|
136
160
|
currentTime,
|
|
161
|
+
onSeek,
|
|
162
|
+
onSelectSegment,
|
|
137
163
|
onClickKeyframe,
|
|
138
164
|
onShiftClickKeyframe,
|
|
139
165
|
onContextMenuKeyframe,
|
|
@@ -142,11 +168,19 @@ export function TimelineLanes({
|
|
|
142
168
|
onContextMenuLane,
|
|
143
169
|
beatAnalysis,
|
|
144
170
|
onToggleTrackHidden,
|
|
171
|
+
onTogglePropertyGroupKeyframe,
|
|
145
172
|
onResizeElement,
|
|
146
173
|
onMoveElement,
|
|
147
174
|
onRazorSplit,
|
|
148
175
|
onRazorSplitAll,
|
|
149
176
|
}: TimelineLanesProps) {
|
|
177
|
+
const expandedClipIds = usePlayerStore((s) => s.expandedClipIds);
|
|
178
|
+
const toggleClipExpanded = usePlayerStore((s) => s.toggleClipExpanded);
|
|
179
|
+
const toggleClipExpandedTracked = (key: string) => {
|
|
180
|
+
const willExpand = !expandedClipIds.has(key);
|
|
181
|
+
trackStudioKeyframeLaneExpand({ expanded: willExpand });
|
|
182
|
+
toggleClipExpanded(key);
|
|
183
|
+
};
|
|
150
184
|
return (
|
|
151
185
|
<>
|
|
152
186
|
{
|
|
@@ -156,7 +190,8 @@ export function TimelineLanes({
|
|
|
156
190
|
// bounded and virtualization's complexity isn't worth it. TODO: revisit and swap
|
|
157
191
|
// in a virtualizer if editorial workflows ever push very high clip counts.
|
|
158
192
|
// fallow-ignore-next-line complexity
|
|
159
|
-
displayTrackOrder.map((trackNum) => {
|
|
193
|
+
displayTrackOrder.map((trackNum, row) => {
|
|
194
|
+
const rowHeight = getTimelineRowHeight(row, rowHeights);
|
|
160
195
|
const els = tracks.find(([t]) => t === trackNum)?.[1] ?? [];
|
|
161
196
|
const ts = trackStyles.get(trackNum) ?? getTrackStyle("");
|
|
162
197
|
const isPendingTrack =
|
|
@@ -173,58 +208,50 @@ export function TimelineLanes({
|
|
|
173
208
|
: els.some(isMusicTrack));
|
|
174
209
|
const isTrackHidden = els.length > 0 && els.every((element) => element.hidden === true);
|
|
175
210
|
const isAudioTrack = els.length > 0 && els.some(isAudioTimelineElement);
|
|
211
|
+
// The one keyframed element this track shows lanes for (selected, else
|
|
212
|
+
// most lanes). A track can hold several elements; scoping to one keeps
|
|
213
|
+
// their keyframes from cramming into a single row.
|
|
214
|
+
const keyframeClip = STUDIO_KEYFRAMES_ENABLED
|
|
215
|
+
? resolveTrackKeyframeClip(els, laneCounts, selectedElementId, selectedElementIds)
|
|
216
|
+
: null;
|
|
217
|
+
const keyframeClipKey = keyframeClip?.key ?? keyframeClip?.id;
|
|
218
|
+
const keyframeClipExpanded =
|
|
219
|
+
keyframeClipKey != null && expandedClipIds.has(keyframeClipKey);
|
|
176
220
|
return (
|
|
177
|
-
<div
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
221
|
+
<div
|
|
222
|
+
key={trackNum}
|
|
223
|
+
className="relative flex"
|
|
224
|
+
style={{
|
|
225
|
+
height: rowHeight,
|
|
226
|
+
background: rowBackground,
|
|
227
|
+
borderBottom: `1px solid ${theme.rowBorder}`,
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
<TimelineTrackHeader
|
|
231
|
+
trackNumber={trackNum}
|
|
232
|
+
trackLabel={els[0]?.label ?? els[0]?.domId ?? els[0]?.id ?? `Track ${trackNum}`}
|
|
233
|
+
contentOrigin={contentOrigin}
|
|
234
|
+
keyframeClip={keyframeClip}
|
|
235
|
+
clipCount={els.length}
|
|
236
|
+
isExpanded={keyframeClipExpanded}
|
|
237
|
+
animations={keyframeClipKey ? (gsapAnimations.get(keyframeClipKey) ?? []) : []}
|
|
238
|
+
currentTime={currentTime}
|
|
239
|
+
isTrackHidden={isTrackHidden}
|
|
240
|
+
isAudioTrack={isAudioTrack}
|
|
241
|
+
theme={theme}
|
|
242
|
+
onToggleClipExpanded={() => {
|
|
243
|
+
if (keyframeClipKey) {
|
|
244
|
+
toggleClipExpandedTracked(keyframeClipKey);
|
|
245
|
+
}
|
|
185
246
|
}}
|
|
186
|
-
|
|
187
|
-
{
|
|
188
|
-
|
|
189
|
-
)}
|
|
190
|
-
<button
|
|
191
|
-
type="button"
|
|
192
|
-
aria-label={isTrackHidden ? `Show track ${trackNum}` : `Hide track ${trackNum}`}
|
|
193
|
-
title={isTrackHidden ? `Show track ${trackNum}` : `Hide track ${trackNum}`}
|
|
194
|
-
className={`flex h-6 w-6 items-center justify-center rounded border-0 bg-transparent p-0 transition-colors focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-[-1px] focus-visible:outline-[#3CE6AC] ${
|
|
195
|
-
isTrackHidden
|
|
196
|
-
? "text-[#3CE6AC] hover:text-white"
|
|
197
|
-
: "text-white/35 hover:text-white/75"
|
|
198
|
-
}`}
|
|
199
|
-
onPointerDown={(e) => {
|
|
200
|
-
e.stopPropagation();
|
|
201
|
-
}}
|
|
202
|
-
onClick={(e) => {
|
|
203
|
-
e.stopPropagation();
|
|
204
|
-
void onToggleTrackHidden?.(trackNum, !isTrackHidden);
|
|
205
|
-
}}
|
|
206
|
-
>
|
|
207
|
-
{isTrackHidden ? (
|
|
208
|
-
<EyeSlash size={14} weight="bold" aria-hidden="true" />
|
|
209
|
-
) : (
|
|
210
|
-
<Eye size={14} weight="bold" aria-hidden="true" />
|
|
211
|
-
)}
|
|
212
|
-
</button>
|
|
213
|
-
</div>
|
|
214
|
-
{/* Left breathing pad — empty lane surface before t=0, scrolling
|
|
215
|
-
with the content (the horizontal TRACKS_TOP_PAD). Sits OUTSIDE
|
|
216
|
-
the time-mapped content div so clip/beat/menu math stays
|
|
217
|
-
content-relative (clip left = t·pps). */}
|
|
218
|
-
<div
|
|
219
|
-
aria-hidden="true"
|
|
220
|
-
className="flex-shrink-0"
|
|
221
|
-
style={{ width: TRACKS_LEFT_PAD }}
|
|
247
|
+
onToggleTrackHidden={onToggleTrackHidden}
|
|
248
|
+
onTogglePropertyGroupKeyframe={onTogglePropertyGroupKeyframe}
|
|
249
|
+
onSeek={onSeek}
|
|
222
250
|
/>
|
|
223
251
|
<div
|
|
224
252
|
style={{
|
|
225
253
|
width: trackContentWidth,
|
|
226
|
-
|
|
227
|
-
borderBottom: `1px solid ${theme.rowBorder}`,
|
|
254
|
+
marginLeft: contentGutter, // room for a 0% diamond left of t=0
|
|
228
255
|
opacity: isTrackHidden ? 0.35 : 1,
|
|
229
256
|
transition: "opacity 120ms ease",
|
|
230
257
|
}}
|
|
@@ -282,6 +309,13 @@ export function TimelineLanes({
|
|
|
282
309
|
els.map((el) => {
|
|
283
310
|
const clipStyle = getTrackStyle(el.tag);
|
|
284
311
|
const elementKey = el.key ?? el.id;
|
|
312
|
+
// Only the track's active keyframe clip shows expanded lanes;
|
|
313
|
+
// other clips (incl. siblings on a shared track) show compact
|
|
314
|
+
// diamonds on their own bar instead.
|
|
315
|
+
const showsLanes =
|
|
316
|
+
STUDIO_KEYFRAMES_ENABLED &&
|
|
317
|
+
elementKey === keyframeClipKey &&
|
|
318
|
+
keyframeClipExpanded;
|
|
285
319
|
const capabilities = getTimelineEditCapabilities(el);
|
|
286
320
|
const isSelected =
|
|
287
321
|
selectedElementId === elementKey || selectedElementIds.has(elementKey);
|
|
@@ -315,6 +349,7 @@ export function TimelineLanes({
|
|
|
315
349
|
el={previewElement}
|
|
316
350
|
pps={pps}
|
|
317
351
|
clipY={CLIP_Y}
|
|
352
|
+
clipHeight={showsLanes ? TRACK_H - 2 * CLIP_Y : undefined}
|
|
318
353
|
isSelected={isSelected}
|
|
319
354
|
isHovered={hoveredClip === clipKey}
|
|
320
355
|
isDragging={false}
|
|
@@ -457,32 +492,70 @@ export function TimelineLanes({
|
|
|
457
492
|
renderClipContent,
|
|
458
493
|
renderClipOverlay,
|
|
459
494
|
)}
|
|
460
|
-
{STUDIO_KEYFRAMES_ENABLED &&
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
495
|
+
{STUDIO_KEYFRAMES_ENABLED &&
|
|
496
|
+
!showsLanes &&
|
|
497
|
+
keyframeCache?.get(elementKey) && (
|
|
498
|
+
<TimelineClipDiamonds
|
|
499
|
+
keyframesData={keyframeCache.get(elementKey)!}
|
|
500
|
+
clipWidthPx={Math.max(previewElement.duration * pps, 4)}
|
|
501
|
+
clipHeightPx={rowHeight - 2 * CLIP_Y}
|
|
502
|
+
beatsActive={beatStripOnTrack}
|
|
503
|
+
accentColor={clipStyle.accent}
|
|
504
|
+
isSelected={isSelected}
|
|
505
|
+
currentPercentage={
|
|
506
|
+
previewElement.duration > 0
|
|
507
|
+
? ((currentTime - previewElement.start) /
|
|
508
|
+
previewElement.duration) *
|
|
509
|
+
100
|
|
510
|
+
: 0
|
|
511
|
+
}
|
|
512
|
+
elementId={elementKey}
|
|
513
|
+
selectedKeyframes={selectedKeyframes}
|
|
514
|
+
onClickKeyframe={(_elId, target) =>
|
|
515
|
+
onClickKeyframe?.(previewElement, target)
|
|
516
|
+
}
|
|
517
|
+
onShiftClickKeyframe={onShiftClickKeyframe}
|
|
518
|
+
onContextMenuKeyframe={onContextMenuKeyframe}
|
|
519
|
+
onMoveKeyframe={onMoveKeyframe}
|
|
520
|
+
onSelectSegment={onSelectSegment}
|
|
521
|
+
suppressClickRef={suppressClickRef}
|
|
522
|
+
/>
|
|
523
|
+
)}
|
|
483
524
|
</TimelineClip>
|
|
484
525
|
);
|
|
485
|
-
|
|
526
|
+
const propertyLanes = showsLanes && (
|
|
527
|
+
<TimelinePropertyLanes
|
|
528
|
+
key={`${clipKey}-property-lanes`}
|
|
529
|
+
animations={gsapAnimations.get(elementKey) ?? []}
|
|
530
|
+
clipStart={previewElement.start}
|
|
531
|
+
clipDuration={previewElement.duration}
|
|
532
|
+
clipLeftPx={previewElement.start * pps}
|
|
533
|
+
clipWidthPx={Math.max(previewElement.duration * pps, 4)}
|
|
534
|
+
accentColor={clipStyle.accent}
|
|
535
|
+
isSelected={isSelected}
|
|
536
|
+
currentPercentage={
|
|
537
|
+
previewElement.duration > 0
|
|
538
|
+
? ((currentTime - previewElement.start) / previewElement.duration) * 100
|
|
539
|
+
: 0
|
|
540
|
+
}
|
|
541
|
+
elementId={elementKey}
|
|
542
|
+
selectedKeyframes={selectedKeyframes}
|
|
543
|
+
onSelectSegment={(target) => onSelectSegment?.(elementKey, target)}
|
|
544
|
+
onClickKeyframe={(target) => onClickKeyframe?.(previewElement, target)}
|
|
545
|
+
onShiftClickKeyframe={(target) =>
|
|
546
|
+
onShiftClickKeyframe?.(elementKey, target)
|
|
547
|
+
}
|
|
548
|
+
onContextMenuKeyframe={(e, target) =>
|
|
549
|
+
onContextMenuKeyframe?.(e, elementKey, target)
|
|
550
|
+
}
|
|
551
|
+
onMoveKeyframe={(target, toClipPercentage) =>
|
|
552
|
+
onMoveKeyframe?.(elementKey, target, toClipPercentage) ??
|
|
553
|
+
Promise.resolve(false)
|
|
554
|
+
}
|
|
555
|
+
suppressClickRef={suppressClickRef}
|
|
556
|
+
/>
|
|
557
|
+
);
|
|
558
|
+
if (!isPassenger) return [clip, propertyLanes];
|
|
486
559
|
return (
|
|
487
560
|
<div
|
|
488
561
|
key={clipKey}
|
|
@@ -495,6 +568,7 @@ export function TimelineLanes({
|
|
|
495
568
|
}}
|
|
496
569
|
>
|
|
497
570
|
{clip}
|
|
571
|
+
{propertyLanes}
|
|
498
572
|
</div>
|
|
499
573
|
);
|
|
500
574
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
2
2
|
import type { TimelineTheme } from "./timelineTheme";
|
|
3
3
|
import type { TimelineRangeSelection } from "./timelineEditing";
|
|
4
4
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
@@ -38,9 +38,7 @@ interface TimelineOverlaysProps {
|
|
|
38
38
|
setKfContextMenu: (value: KeyframeDiamondContextMenuState | null) => void;
|
|
39
39
|
onDeleteKeyframe: TimelineEditCallbacks["onDeleteKeyframe"];
|
|
40
40
|
onDeleteAllKeyframes: TimelineEditCallbacks["onDeleteAllKeyframes"];
|
|
41
|
-
onChangeKeyframeEase: TimelineEditCallbacks["onChangeKeyframeEase"];
|
|
42
41
|
onMoveKeyframeToPlayhead: TimelineEditCallbacks["onMoveKeyframeToPlayhead"];
|
|
43
|
-
keyframeCache: Map<string, KeyframeCacheEntry>;
|
|
44
42
|
clipContextMenu: ClipContextMenuState | null;
|
|
45
43
|
setClipContextMenu: (value: ClipContextMenuState | null) => void;
|
|
46
44
|
currentTime: number;
|
|
@@ -68,9 +66,7 @@ export function TimelineOverlays({
|
|
|
68
66
|
setKfContextMenu,
|
|
69
67
|
onDeleteKeyframe,
|
|
70
68
|
onDeleteAllKeyframes,
|
|
71
|
-
onChangeKeyframeEase,
|
|
72
69
|
onMoveKeyframeToPlayhead,
|
|
73
|
-
keyframeCache,
|
|
74
70
|
clipContextMenu,
|
|
75
71
|
setClipContextMenu,
|
|
76
72
|
currentTime,
|
|
@@ -106,21 +102,11 @@ export function TimelineOverlays({
|
|
|
106
102
|
<KeyframeDiamondContextMenu
|
|
107
103
|
state={kfContextMenu}
|
|
108
104
|
onClose={() => setKfContextMenu(null)}
|
|
109
|
-
onDelete={(elId,
|
|
110
|
-
onDeleteAll={(
|
|
111
|
-
onChangeEase={(elId, pct, ease) => onChangeKeyframeEase?.(elId, pct, ease)}
|
|
105
|
+
onDelete={(elId, keyframe) => onDeleteKeyframe?.(elId, keyframe)}
|
|
106
|
+
onDeleteAll={(element) => onDeleteAllKeyframes?.(element)}
|
|
112
107
|
onMoveToPlayhead={
|
|
113
|
-
onMoveKeyframeToPlayhead
|
|
114
|
-
? (elId, pct) => onMoveKeyframeToPlayhead(elId, pct)
|
|
115
|
-
: undefined
|
|
108
|
+
onMoveKeyframeToPlayhead ? (...args) => onMoveKeyframeToPlayhead(...args) : undefined
|
|
116
109
|
}
|
|
117
|
-
onCopyProperties={(elId, pct) => {
|
|
118
|
-
const kfData = keyframeCache.get(elId);
|
|
119
|
-
const kf = kfData?.keyframes.find((k) => k.percentage === pct);
|
|
120
|
-
if (kf) {
|
|
121
|
-
void navigator.clipboard.writeText(JSON.stringify(kf.properties, null, 2));
|
|
122
|
-
}
|
|
123
|
-
}}
|
|
124
110
|
/>
|
|
125
111
|
)}
|
|
126
112
|
|