@hyperframes/studio 0.8.11 → 0.8.12
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-C6J6fsAD.js → hyperframes-player-B243szNa.js} +1 -1
- package/dist/assets/{index-BX0piiWc.js → index-BTP86sD-.js} +1 -1
- package/dist/assets/index-BZfUJ2He.js +431 -0
- package/dist/assets/{index-1vMlv05L.js → index-DQ_lkuqY.js} +1 -1
- package/dist/assets/index-yGhfxxoL.css +1 -0
- package/dist/{chunk-ZALQ3CW7.js → chunk-6BT6DTB4.js} +2 -1
- package/dist/{domEditingLayers-XRVXRTNX.js → domEditingLayers-URA7BLWE.js} +2 -2
- package/dist/index.d.ts +84 -43
- package/dist/index.html +2 -2
- package/dist/index.js +6431 -5515
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/App.tsx +0 -2
- package/src/components/StudioRightPanel.tsx +15 -57
- package/src/components/editor/PropertyPanel.test.tsx +101 -3
- package/src/components/editor/PropertyPanel.tsx +15 -1
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +35 -20
- package/src/components/editor/PropertyPanelFlat.tsx +82 -21
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +21 -0
- package/src/components/editor/TimelineFxPopover.tsx +24 -4
- package/src/components/editor/audioFxRevealTarget.test.ts +59 -0
- package/src/components/editor/audioFxRevealTarget.ts +113 -0
- package/src/components/editor/audioFxSignalPath.test.ts +50 -0
- package/src/components/editor/audioFxSignalPath.ts +71 -0
- package/src/components/editor/audioFxSummary.test.ts +8 -0
- package/src/components/editor/audioFxSummary.ts +28 -16
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +101 -0
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +92 -2
- package/src/components/editor/propertyPanelFlatClosedGroup.tsx +46 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +31 -0
- package/src/components/editor/propertyPanelFxCarveModule.tsx +203 -125
- package/src/components/editor/propertyPanelFxControls.test.tsx +50 -0
- package/src/components/editor/propertyPanelFxControls.tsx +10 -2
- package/src/components/editor/propertyPanelFxEqModule.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeRow.tsx +11 -1
- package/src/components/editor/propertyPanelFxPresetMenu.tsx +12 -2
- package/src/components/editor/propertyPanelFxRackChain.tsx +12 -5
- package/src/components/editor/propertyPanelFxSection.test.tsx +42 -0
- package/src/components/editor/propertyPanelFxSection.tsx +63 -4
- package/src/components/editor/propertyPanelFxSectionTypes.ts +19 -0
- package/src/components/editor/useAudioFxRevealSection.ts +64 -0
- package/src/components/editor/useAuditionTransport.test.ts +36 -0
- package/src/components/editor/useAuditionTransport.ts +78 -0
- package/src/components/editor/useFxAudition.ts +12 -1
- package/src/components/editor/useFxCarve.ts +0 -0
- package/src/components/editor/useFxCarveGrouping.test.ts +118 -0
- package/src/components/editor/useFxCarveGrouping.ts +139 -4
- package/src/components/editor/useFxCarveNodes.ts +153 -0
- package/src/components/editor/useFxLevelling.ts +5 -34
- package/src/components/nle/NLEContext.tsx +3 -10
- package/src/components/nle/PreviewPane.tsx +0 -1
- package/src/components/nle/useTimelineEditCallbacks.ts +5 -1
- package/src/contexts/DesignPanelInputContext.tsx +6 -5
- package/src/contexts/DomEditContext.tsx +10 -3
- package/src/contexts/FileManagerContext.tsx +3 -2
- package/src/contexts/PanelLayoutContext.tsx +3 -2
- package/src/contexts/StudioContext.tsx +7 -3
- package/src/contexts/TimelineEditContext.tsx +6 -2
- package/src/contexts/VariablePromoteContext.tsx +6 -2
- package/src/contexts/ViewModeContext.tsx +2 -2
- package/src/hooks/domEditDeleteMembers.ts +34 -0
- package/src/hooks/timelineAudioGroupCreate.ts +345 -0
- package/src/hooks/timelineAudioGroupVolume.test.ts +204 -0
- package/src/hooks/timelineAudioGroupVolume.ts +135 -3
- package/src/hooks/timelineEditingHelpers.test.ts +86 -0
- package/src/hooks/timelineEditingHelpers.ts +19 -7
- package/src/hooks/timelineElementFxAttribute.ts +0 -3
- package/src/hooks/timelineTrackVisibility.test.ts +95 -13
- package/src/hooks/timelineTrackVisibility.ts +6 -182
- package/src/hooks/useAudioGroupCarveAssignment.test.tsx +139 -0
- package/src/hooks/useBlockedTimelineEditToast.ts +27 -0
- package/src/hooks/useDomEditAttributeCommits.ts +13 -0
- package/src/hooks/useDomEditSession.test.tsx +57 -2
- package/src/hooks/useDomEditSession.ts +23 -24
- package/src/hooks/useEffectiveTimelineDuration.ts +9 -7
- package/src/hooks/useLivePlayheadTime.ts +3 -1
- package/src/hooks/useRemoveBackground.ts +70 -0
- package/src/hooks/useTimelineEditing.ts +5 -19
- package/src/hooks/useTimelineEditingTypes.ts +7 -0
- package/src/player/components/AutomationSelectionMenu.tsx +4 -0
- package/src/player/components/LayerDisclosureRow.tsx +59 -25
- package/src/player/components/PlayerControls.tsx +0 -38
- package/src/player/components/Timeline.test.ts +40 -1
- package/src/player/components/Timeline.tsx +5 -2
- package/src/player/components/TimelineAutomationLane.test.tsx +67 -0
- package/src/player/components/TimelineAutomationLane.tsx +89 -12
- package/src/player/components/TimelineAutomationLaneSlot.tsx +22 -3
- package/src/player/components/TimelineFxButton.test.tsx +167 -21
- package/src/player/components/TimelineFxButton.tsx +156 -31
- package/src/player/components/TimelineGroupHeader.test.tsx +0 -5
- package/src/player/components/TimelineGroupHeader.tsx +100 -102
- package/src/player/components/TimelineGroupLaneLabels.tsx +88 -0
- package/src/player/components/TimelineGroupRow.test.tsx +146 -0
- package/src/player/components/TimelineGroupRow.tsx +123 -64
- package/src/player/components/TimelineLanes.test.tsx +3 -3
- package/src/player/components/TimelineLanes.tsx +25 -23
- package/src/player/components/TimelineTrackHeader.test.tsx +298 -13
- package/src/player/components/TimelineTrackHeader.tsx +301 -381
- package/src/player/components/TimelineTrackPlainHeader.test.tsx +47 -69
- package/src/player/components/TimelineTrackPlainHeader.tsx +64 -54
- package/src/player/components/automationLaneData.test.ts +94 -0
- package/src/player/components/automationLaneData.ts +23 -0
- package/src/player/components/groupAutomationElement.test.ts +51 -0
- package/src/player/components/groupAutomationElement.ts +37 -0
- package/src/player/components/timelineCallbacks.ts +6 -2
- package/src/player/components/timelineKeyboardNavigation.test.ts +2 -2
- package/src/player/components/timelineKeyboardNavigation.ts +25 -8
- package/src/player/components/timelineLayout.ts +0 -1
- package/src/player/components/timelineViewModel.ts +30 -1
- package/src/player/components/trackHeaderLabelRows.tsx +328 -0
- package/src/player/components/useTimelineClipDisclosure.ts +41 -0
- package/src/player/components/useTimelineLaneRowIndexes.ts +1 -1
- package/src/player/components/useTimelineLogicalFocus.ts +2 -2
- package/src/player/components/useTimelineLogicalRows.test.tsx +2 -2
- package/src/player/components/useTimelineLogicalRows.ts +3 -3
- package/src/player/components/useTimelineTrackDerivations.ts +47 -8
- package/src/player/components/useTimelineTrackLayout.test.ts +119 -0
- package/src/player/components/useTimelineTrackLayout.ts +12 -4
- package/src/player/hooks/previewMessageRouter.ts +4 -11
- package/src/player/hooks/timelineSyncHydration.ts +395 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +40 -1
- package/src/player/hooks/useTimelinePlayer.ts +2 -7
- package/src/player/hooks/useTimelineSyncCallbacks.ts +43 -219
- package/src/player/lib/automationStoreSync.test.ts +26 -0
- package/src/player/lib/automationStoreSync.ts +28 -4
- package/src/player/lib/runtimeAudioId.test.ts +58 -0
- package/src/player/lib/timelineDOM.test.ts +115 -0
- package/src/player/lib/timelineDOM.ts +3 -32
- package/src/player/lib/timelineElementHelpers.ts +27 -0
- package/src/player/lib/timelineGroupInfo.ts +134 -0
- package/src/player/lib/timelineIframeHelpers.test.ts +21 -0
- package/src/player/lib/timelineIframeHelpers.ts +17 -0
- package/src/player/store/keyframeSlice.ts +79 -5
- package/src/player/store/playerStore.ts +37 -47
- package/src/player/store/playerStoreDevHandle.ts +22 -0
- package/src/player/store/playerStoreSelection.ts +41 -0
- package/src/player/store/timelineElement.ts +32 -0
- package/src/utils/hmrStableContext.ts +64 -0
- package/src/utils/timelineInspector.test.ts +35 -1
- package/src/utils/timelineInspector.ts +38 -0
- package/dist/assets/index-Ba9zbpT9.css +0 -1
- package/dist/assets/index-DoW5v5eS.js +0 -428
- package/src/hooks/useAudioSoloBridge.ts +0 -61
- package/src/hooks/useGroupLevel.ts +0 -36
- package/src/player/components/TimelineGroupBusStrip.test.tsx +0 -146
- package/src/player/components/TimelineGroupBusStrip.tsx +0 -103
- package/src/player/components/TimelineSoloButton.tsx +0 -32
- package/src/player/store/audioSoloSlice.test.ts +0 -113
- package/src/player/store/audioSoloSlice.ts +0 -43
- package/src/player/store/groupLevels.ts +0 -33
- /package/dist/{chunk-ZALQ3CW7.js.map → chunk-6BT6DTB4.js.map} +0 -0
- /package/dist/{domEditingLayers-XRVXRTNX.js.map → domEditingLayers-URA7BLWE.js.map} +0 -0
|
@@ -11,22 +11,34 @@ import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
|
11
11
|
import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
|
|
12
12
|
import { useDomEditActionsContextOptional } from "../../contexts/DomEditContext";
|
|
13
13
|
import { mintGroupId } from "../../components/editor/useFxCarveGrouping";
|
|
14
|
+
import { runtimeAudioId } from "../lib/timelineElementHelpers";
|
|
14
15
|
import { TimelineFxButton } from "./TimelineFxButton";
|
|
15
16
|
import { getTimelinePropertyLanes } from "./TimelinePropertyLanes";
|
|
16
|
-
import { groupAutomationLanes } from "./automationLaneData";
|
|
17
|
+
import { elementFxChain, groupAutomationLanes, isCarveLane } from "./automationLaneData";
|
|
17
18
|
import { AUTOMATION_LANE_H } from "./automationLaneHeight";
|
|
18
19
|
import { clipTimingStart } from "../../hooks/gsapShared";
|
|
19
|
-
import { LayerDisclosureRow } from "./LayerDisclosureRow";
|
|
20
|
-
import { LABEL_COL_W,
|
|
20
|
+
import { LaneToggleButton, LayerDisclosureRow } from "./LayerDisclosureRow";
|
|
21
|
+
import { LABEL_COL_W, TRACK_H, getTimelineLaneTop } from "./timelineLayout";
|
|
21
22
|
import type { TimelineTheme } from "./timelineTheme";
|
|
22
|
-
import {
|
|
23
|
-
resolveLaneHeaderState,
|
|
24
|
-
type KeyframeNavigationState,
|
|
25
|
-
type TimelinePropertyLane,
|
|
26
|
-
} from "./trackHeaderLaneState";
|
|
27
|
-
import { valueReadout } from "./trackHeaderLaneValues";
|
|
28
23
|
import { trackDisplaySuffix } from "./timelineTrackDisplay";
|
|
29
|
-
import {
|
|
24
|
+
import { AutomationLaneHeaderRow, PropertyGroupHeaderRow } from "./trackHeaderLabelRows";
|
|
25
|
+
import { useMemo } from "react";
|
|
26
|
+
|
|
27
|
+
/** Accent rail + inset marking a row as a group MEMBER, matching the level-2
|
|
28
|
+
* nesting its `aria-level` already reports. */
|
|
29
|
+
const GROUP_MEMBER_RAIL = "#3CE6AC59";
|
|
30
|
+
const GROUP_MEMBER_INDENT = 14;
|
|
31
|
+
/** A hair lighter than `gutterBackground`, so a member row reads as sitting
|
|
32
|
+
* INSIDE its group rather than beside it. Overlaid rather than hard-coded so
|
|
33
|
+
* it tracks whatever the theme's gutter is. */
|
|
34
|
+
const GROUP_MEMBER_TINT = "rgba(255,255,255,0.035)";
|
|
35
|
+
|
|
36
|
+
/** The gutter fill for a row, tinted when it belongs to a group. */
|
|
37
|
+
function gutterFill(base: string, isGroupMember: boolean): string {
|
|
38
|
+
return isGroupMember
|
|
39
|
+
? `linear-gradient(${GROUP_MEMBER_TINT}, ${GROUP_MEMBER_TINT}), ${base}`
|
|
40
|
+
: base;
|
|
41
|
+
}
|
|
30
42
|
|
|
31
43
|
interface TimelineTrackHeaderProps {
|
|
32
44
|
/** The track's real key: a FRACTIONAL z-order sort value. Routes callbacks;
|
|
@@ -58,6 +70,8 @@ interface TimelineTrackHeaderProps {
|
|
|
58
70
|
currentTime: number;
|
|
59
71
|
isTrackHidden: boolean;
|
|
60
72
|
isAudioTrack: boolean;
|
|
73
|
+
/** This track is a member of an audio group — indents the row under its header. */
|
|
74
|
+
isGroupMember?: boolean;
|
|
61
75
|
rovingTargetId?: string | null;
|
|
62
76
|
theme: TimelineTheme;
|
|
63
77
|
onToggleClipExpanded: () => void;
|
|
@@ -69,267 +83,6 @@ interface TimelineTrackHeaderProps {
|
|
|
69
83
|
onSeek?: (time: number) => void;
|
|
70
84
|
}
|
|
71
85
|
|
|
72
|
-
// Figma layout: prev-keyframe ‹, the add/remove toggle (children), next ›.
|
|
73
|
-
function PropertyGroupNavigation({
|
|
74
|
-
navigation,
|
|
75
|
-
label,
|
|
76
|
-
expandedElement,
|
|
77
|
-
onSeek,
|
|
78
|
-
children,
|
|
79
|
-
}: {
|
|
80
|
-
navigation: KeyframeNavigationState;
|
|
81
|
-
label: string;
|
|
82
|
-
expandedElement: TimelineElement;
|
|
83
|
-
onSeek?: (time: number) => void;
|
|
84
|
-
children: React.ReactNode;
|
|
85
|
-
}) {
|
|
86
|
-
// The 12x20px glyph is all the lane row has room for, so the WCAG 24x24
|
|
87
|
-
// target is met with a centered transparent ::before overlay instead of a
|
|
88
|
-
// bigger box; focus-visible matches every other control in this header.
|
|
89
|
-
const CHEVRON_BUTTON_CLASS =
|
|
90
|
-
"relative h-5 w-3 border-0 bg-transparent p-0 text-white/55 hover:text-white disabled:text-white/15 " +
|
|
91
|
-
"focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] " +
|
|
92
|
-
"before:absolute before:left-1/2 before:top-1/2 before:h-6 before:w-6 " +
|
|
93
|
-
"before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']";
|
|
94
|
-
const seekTo = (keyframe: { percentage: number } | null) => {
|
|
95
|
-
if (keyframe) {
|
|
96
|
-
onSeek?.(expandedElement.start + (keyframe.percentage / 100) * expandedElement.duration);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
return (
|
|
100
|
-
<span className="flex shrink-0 items-center gap-0.5">
|
|
101
|
-
<button
|
|
102
|
-
type="button"
|
|
103
|
-
aria-label={`Previous ${label} keyframe`}
|
|
104
|
-
disabled={!navigation.prevKeyframe}
|
|
105
|
-
className={CHEVRON_BUTTON_CLASS}
|
|
106
|
-
onClick={(event) => {
|
|
107
|
-
event.stopPropagation();
|
|
108
|
-
seekTo(navigation.prevKeyframe);
|
|
109
|
-
}}
|
|
110
|
-
>
|
|
111
|
-
‹
|
|
112
|
-
</button>
|
|
113
|
-
{children}
|
|
114
|
-
<button
|
|
115
|
-
type="button"
|
|
116
|
-
aria-label={`Next ${label} keyframe`}
|
|
117
|
-
disabled={!navigation.nextKeyframe}
|
|
118
|
-
className={CHEVRON_BUTTON_CLASS}
|
|
119
|
-
onClick={(event) => {
|
|
120
|
-
event.stopPropagation();
|
|
121
|
-
seekTo(navigation.nextKeyframe);
|
|
122
|
-
}}
|
|
123
|
-
>
|
|
124
|
-
›
|
|
125
|
-
</button>
|
|
126
|
-
</span>
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function PropertyGroupHeaderRow({
|
|
131
|
-
lanesId,
|
|
132
|
-
lane,
|
|
133
|
-
laneIndex,
|
|
134
|
-
isLastLane,
|
|
135
|
-
expandedElement,
|
|
136
|
-
currentTime,
|
|
137
|
-
clipPercentage,
|
|
138
|
-
gutterBackground,
|
|
139
|
-
columnWidth,
|
|
140
|
-
onTogglePropertyGroupKeyframe,
|
|
141
|
-
onSeek,
|
|
142
|
-
rovingTargetId = null,
|
|
143
|
-
}: {
|
|
144
|
-
lanesId: string;
|
|
145
|
-
lane: TimelinePropertyLane;
|
|
146
|
-
laneIndex: number;
|
|
147
|
-
isLastLane: boolean;
|
|
148
|
-
expandedElement: TimelineElement;
|
|
149
|
-
currentTime: number;
|
|
150
|
-
clipPercentage: number;
|
|
151
|
-
gutterBackground: string;
|
|
152
|
-
columnWidth: number;
|
|
153
|
-
onTogglePropertyGroupKeyframe?: TimelineEditCallbacks["onTogglePropertyGroupKeyframe"];
|
|
154
|
-
onSeek?: (time: number) => void;
|
|
155
|
-
rovingTargetId: string | null;
|
|
156
|
-
}) {
|
|
157
|
-
const elementId = expandedElement.key ?? expandedElement.id;
|
|
158
|
-
const { navigation, values, label, toggleTarget } = resolveLaneHeaderState(
|
|
159
|
-
lane,
|
|
160
|
-
currentTime,
|
|
161
|
-
clipPercentage,
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
return (
|
|
165
|
-
<div
|
|
166
|
-
id={timelineLogicalRowCellId(lanesId, timelinePropertyRowId(elementId, lane.group), "header")}
|
|
167
|
-
data-timeline-focus-id={timelinePropertyRowId(elementId, lane.group)}
|
|
168
|
-
data-timeline-element-id={elementId}
|
|
169
|
-
tabIndex={rovingTargetId === timelinePropertyRowId(elementId, lane.group) ? 0 : -1}
|
|
170
|
-
data-property-group={lane.group}
|
|
171
|
-
data-timeline-lane-top={getTimelineLaneTop(laneIndex)}
|
|
172
|
-
className="absolute left-0 flex items-center gap-1 overflow-hidden px-1.5 text-[10px] text-white/65"
|
|
173
|
-
style={{
|
|
174
|
-
top: getTimelineLaneTop(laneIndex),
|
|
175
|
-
// The header column narrows to contentOrigin whenever that is under
|
|
176
|
-
// LABEL_COL_W; a lane row pinned to LABEL_COL_W then hangs its value
|
|
177
|
-
// readout over the canvas, on top of the clips it is labelling.
|
|
178
|
-
width: columnWidth,
|
|
179
|
-
height: LANE_H,
|
|
180
|
-
background: gutterBackground,
|
|
181
|
-
}}
|
|
182
|
-
>
|
|
183
|
-
{/* Tree connector: vertical spine (top-half on the last lane) + branch tick. */}
|
|
184
|
-
<span className="relative h-full w-3 shrink-0" aria-hidden="true">
|
|
185
|
-
<span
|
|
186
|
-
className="absolute left-1.5 top-0 w-px bg-white/15"
|
|
187
|
-
style={{ height: isLastLane ? "50%" : "100%" }}
|
|
188
|
-
/>
|
|
189
|
-
<span className="absolute left-1.5 top-1/2 h-px w-1.5 bg-white/15" />
|
|
190
|
-
</span>
|
|
191
|
-
<span className="w-[46px] shrink-0 truncate text-white" title={label}>
|
|
192
|
-
{label}
|
|
193
|
-
</span>
|
|
194
|
-
<PropertyGroupNavigation
|
|
195
|
-
navigation={navigation}
|
|
196
|
-
label={label}
|
|
197
|
-
expandedElement={expandedElement}
|
|
198
|
-
onSeek={onSeek}
|
|
199
|
-
>
|
|
200
|
-
<button
|
|
201
|
-
type="button"
|
|
202
|
-
aria-pressed={!!navigation.currentKeyframe}
|
|
203
|
-
aria-label={`${navigation.currentKeyframe ? "Remove" : "Add"} ${label} keyframe`}
|
|
204
|
-
title={`${navigation.currentKeyframe ? "Remove" : "Add"} ${label} keyframe`}
|
|
205
|
-
// h-6 w-6 = the 24x24 WCAG 2.2 minimum target; the ◆ glyph stays 11px.
|
|
206
|
-
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-[#3CE6AC] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
207
|
-
onClick={(event) => {
|
|
208
|
-
// Same as the disclosure caret and the eye: a control in the label
|
|
209
|
-
// column owns its click, it does not also hit the track row behind it.
|
|
210
|
-
event.stopPropagation();
|
|
211
|
-
if (toggleTarget) {
|
|
212
|
-
void onTogglePropertyGroupKeyframe?.(expandedElement, toggleTarget);
|
|
213
|
-
}
|
|
214
|
-
}}
|
|
215
|
-
>
|
|
216
|
-
{navigation.currentKeyframe ? "◆" : "◇"}
|
|
217
|
-
</button>
|
|
218
|
-
</PropertyGroupNavigation>
|
|
219
|
-
<span
|
|
220
|
-
className="min-w-0 flex-1 truncate text-right tabular-nums text-white/45"
|
|
221
|
-
title={valueReadout(lane.group, values)}
|
|
222
|
-
>
|
|
223
|
-
{valueReadout(lane.group, values)}
|
|
224
|
-
</span>
|
|
225
|
-
</div>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* One envelope's row in the label column.
|
|
231
|
-
*
|
|
232
|
-
* Named here rather than inside the lane, on the same tree connector the
|
|
233
|
-
* keyframe rows use: an automation lane is a child of its clip exactly as a
|
|
234
|
-
* property group is, and drawing its name over the envelope put the label on top
|
|
235
|
-
* of the curve it describes and scrolled it away from its own row.
|
|
236
|
-
*/
|
|
237
|
-
function AutomationLaneHeaderRow({
|
|
238
|
-
target,
|
|
239
|
-
label,
|
|
240
|
-
name,
|
|
241
|
-
param,
|
|
242
|
-
top,
|
|
243
|
-
isLastLane,
|
|
244
|
-
gutterBackground,
|
|
245
|
-
columnWidth,
|
|
246
|
-
onRemove,
|
|
247
|
-
}: {
|
|
248
|
-
/** The lane the ACTIVE clip draws in this row, or null when it draws none —
|
|
249
|
-
* the row belongs to the property, and a clip may be absent from it. */
|
|
250
|
-
target: string | null;
|
|
251
|
-
/** The whole thing on one line: the row's identity, its tooltip, and the
|
|
252
|
-
* remove button's name. */
|
|
253
|
-
label: string;
|
|
254
|
-
/** What the effect is — "Peaking EQ 1.6 kHz". */
|
|
255
|
-
name: string;
|
|
256
|
-
/** Which knob the envelope drives. Empty when there is no second line to draw. */
|
|
257
|
-
param: string;
|
|
258
|
-
top: number;
|
|
259
|
-
isLastLane: boolean;
|
|
260
|
-
gutterBackground: string;
|
|
261
|
-
columnWidth: number;
|
|
262
|
-
onRemove?: (target: string) => void;
|
|
263
|
-
}) {
|
|
264
|
-
return (
|
|
265
|
-
<div
|
|
266
|
-
data-automation-lane-label={label}
|
|
267
|
-
data-timeline-lane-top={top}
|
|
268
|
-
className="absolute left-0 flex items-center gap-1 overflow-hidden px-1.5 text-[10px] text-white/65"
|
|
269
|
-
style={{
|
|
270
|
-
top,
|
|
271
|
-
width: columnWidth,
|
|
272
|
-
height: AUTOMATION_LANE_H,
|
|
273
|
-
background: gutterBackground,
|
|
274
|
-
}}
|
|
275
|
-
>
|
|
276
|
-
{/* Tree connector, as the keyframe rows draw it: spine down the row, branch
|
|
277
|
-
tick at the name's own height. */}
|
|
278
|
-
<span className="relative h-full w-3 shrink-0" aria-hidden="true">
|
|
279
|
-
<span
|
|
280
|
-
className="absolute left-1.5 top-0 w-px bg-white/15"
|
|
281
|
-
style={{ height: isLastLane ? "50%" : "100%" }}
|
|
282
|
-
/>
|
|
283
|
-
<span className="absolute left-1.5 top-1/2 h-px w-1.5 bg-white/15" />
|
|
284
|
-
</span>
|
|
285
|
-
{/* Two lines: what the effect is, then which knob the envelope drives. On
|
|
286
|
-
one line a band's own name was the first thing truncated in a column this
|
|
287
|
-
narrow — "Peaking EQ 1.6 k…" — losing exactly the part that tells two
|
|
288
|
-
bands apart. */}
|
|
289
|
-
<span className="flex min-w-0 flex-1 flex-col justify-center leading-tight" title={label}>
|
|
290
|
-
<span data-automation-lane-name="" className="truncate font-mono text-[9px] text-white/70">
|
|
291
|
-
{name}
|
|
292
|
-
</span>
|
|
293
|
-
{param ? (
|
|
294
|
-
<span
|
|
295
|
-
data-automation-lane-param=""
|
|
296
|
-
className="truncate font-mono text-[9px] text-white/40"
|
|
297
|
-
>
|
|
298
|
-
{param}
|
|
299
|
-
</span>
|
|
300
|
-
) : null}
|
|
301
|
-
</span>
|
|
302
|
-
{/* Beside the name it labels, because that is the only place an envelope is
|
|
303
|
-
named at all: a carve writes its own lanes, and the FX panel's automate
|
|
304
|
-
toggle can only reach a parameter it still lists — so without this an
|
|
305
|
-
envelope could be created and never removed.
|
|
306
|
-
|
|
307
|
-
Only for the clip the header is showing, since that is the only one a
|
|
308
|
-
write can reach: a shared row's other envelopes belong to clips that
|
|
309
|
-
are not selected, and a button that silently removed one of them (or
|
|
310
|
-
none) would be worse than no button. */}
|
|
311
|
-
{onRemove && target !== null && (
|
|
312
|
-
<button
|
|
313
|
-
type="button"
|
|
314
|
-
aria-label={`Remove ${label} automation`}
|
|
315
|
-
title={`Remove ${label} automation`}
|
|
316
|
-
// h-6 w-6 is the 24x24 WCAG 2.2 target; the glyph stays small.
|
|
317
|
-
className="flex h-6 w-6 shrink-0 items-center justify-center border-0 bg-transparent p-0 text-[11px] text-white/35 hover:text-white focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC]"
|
|
318
|
-
onPointerDown={(event) => event.stopPropagation()}
|
|
319
|
-
onClick={(event) => {
|
|
320
|
-
// A control in the label column owns its click; it does not also hit
|
|
321
|
-
// the track row behind it and change the selection.
|
|
322
|
-
event.stopPropagation();
|
|
323
|
-
onRemove(target);
|
|
324
|
-
}}
|
|
325
|
-
>
|
|
326
|
-
×
|
|
327
|
-
</button>
|
|
328
|
-
)}
|
|
329
|
-
</div>
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
86
|
// fallow-ignore-next-line complexity
|
|
334
87
|
export function TimelineTrackHeader({
|
|
335
88
|
trackNumber,
|
|
@@ -345,6 +98,7 @@ export function TimelineTrackHeader({
|
|
|
345
98
|
currentTime,
|
|
346
99
|
isTrackHidden,
|
|
347
100
|
isAudioTrack,
|
|
101
|
+
isGroupMember = false,
|
|
348
102
|
theme,
|
|
349
103
|
onToggleClipExpanded,
|
|
350
104
|
onToggleTrackHidden,
|
|
@@ -369,26 +123,94 @@ export function TimelineTrackHeader({
|
|
|
369
123
|
// order. `target` is the ACTIVE clip's lane in that row, which is the only one
|
|
370
124
|
// the remove button can write to; null when the row belongs to its siblings.
|
|
371
125
|
const activeKey = keyframeClip ? (keyframeClip.key ?? keyframeClip.id) : null;
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
126
|
+
const groupOwner = useMemo(
|
|
127
|
+
() => trackElements.find((element) => element.audioGroup)?.audioGroup,
|
|
128
|
+
[trackElements],
|
|
129
|
+
);
|
|
130
|
+
const groupLabelForNote = useMemo(
|
|
131
|
+
() => trackElements.find((element) => element.audioGroupLabel)?.audioGroupLabel,
|
|
132
|
+
[trackElements],
|
|
133
|
+
);
|
|
134
|
+
const groupAutomationRaw = useMemo(
|
|
135
|
+
() => trackElements.find((element) => element.audioGroupAutomation)?.audioGroupAutomation,
|
|
136
|
+
[trackElements],
|
|
137
|
+
);
|
|
138
|
+
const groupFxChainRaw = useMemo(
|
|
139
|
+
() => trackElements.find((element) => element.audioGroupFxChain)?.audioGroupFxChain,
|
|
140
|
+
[trackElements],
|
|
141
|
+
);
|
|
142
|
+
// Which parameters this track's GROUP also automates. Gain stages multiply,
|
|
143
|
+
// and §5 asks for the explanation rather than leaving the author to wonder
|
|
144
|
+
// why two curves they drew sound quieter than either.
|
|
145
|
+
const groupAutomatedTargets = useMemo(
|
|
146
|
+
() =>
|
|
147
|
+
new Set(
|
|
148
|
+
groupAutomationLanes(
|
|
149
|
+
groupOwner
|
|
150
|
+
? [
|
|
151
|
+
{
|
|
152
|
+
id: groupOwner,
|
|
153
|
+
tag: "audio",
|
|
154
|
+
start: 0,
|
|
155
|
+
duration: 0,
|
|
156
|
+
track: 0,
|
|
157
|
+
...(groupAutomationRaw ? { automation: groupAutomationRaw } : {}),
|
|
158
|
+
...(groupFxChainRaw ? { fxChain: groupFxChainRaw } : {}),
|
|
159
|
+
},
|
|
160
|
+
]
|
|
161
|
+
: [],
|
|
162
|
+
).map((lane) => lane.key),
|
|
163
|
+
),
|
|
164
|
+
[groupAutomationRaw, groupFxChainRaw, groupOwner],
|
|
165
|
+
);
|
|
166
|
+
const revealAudioFx = usePlayerStore((s) => s.setRevealedAudioFxTarget);
|
|
167
|
+
/**
|
|
168
|
+
* Which element's rack a lane's reveal opens, in the PANEL's id space.
|
|
169
|
+
*
|
|
170
|
+
* The bare dom id, not the timeline's `sourceFile#domId` composite: the
|
|
171
|
+
* property panel identifies its element by `element.id`, so a composite would
|
|
172
|
+
* never match — the same boundary `runtimeAudioId` exists for.
|
|
173
|
+
*/
|
|
174
|
+
const revealElementId = keyframeClip ? runtimeAudioId(keyframeClip) : null;
|
|
175
|
+
const automationRows = useMemo(
|
|
176
|
+
() =>
|
|
177
|
+
groupAutomationLanes(trackElements).map((group) => {
|
|
178
|
+
const active = group.entries.find(
|
|
179
|
+
(entry) => (entry.element.key ?? entry.element.id) === activeKey,
|
|
180
|
+
);
|
|
181
|
+
return {
|
|
182
|
+
key: group.key,
|
|
183
|
+
label: group.key,
|
|
184
|
+
name: group.name,
|
|
185
|
+
param: group.param,
|
|
186
|
+
target: active?.lane.target ?? null,
|
|
187
|
+
// Every entry in a row is the same parameter, so the first answers for the
|
|
188
|
+
// row when the active clip is absent from it.
|
|
189
|
+
isCarve: (() => {
|
|
190
|
+
const entry = active ?? group.entries[0];
|
|
191
|
+
return entry ? isCarveLane(entry.lane.target, elementFxChain(entry.element)) : false;
|
|
192
|
+
})(),
|
|
193
|
+
};
|
|
194
|
+
}),
|
|
195
|
+
[activeKey, trackElements],
|
|
196
|
+
);
|
|
381
197
|
// Automation counts as something to disclose: gating the caret on tweens alone
|
|
382
198
|
// left an audio clip's envelopes unreachable, since the track could not expand.
|
|
383
199
|
const disclosable = lanes.length > 0 || automationRows.length > 0;
|
|
384
|
-
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
200
|
+
// Which HEADER LAYOUT the row wears — not the same question as `disclosable`.
|
|
201
|
+
// An audio track that automates something is still an audio track: it keeps
|
|
202
|
+
// the music glyph and the group indent and gains the `∿`. Tying layout to
|
|
203
|
+
// disclosability swapped it for the keyframe-layer row (a `◇`, no indent) the
|
|
204
|
+
// moment an envelope appeared.
|
|
205
|
+
const isKeyframeLayer = !!keyframeClip && disclosable && !isAudioTrack;
|
|
206
|
+
// What the lane disclosure calls this row. A row of several clips is named
|
|
207
|
+
// for the TRACK, not for whichever is selected — the lanes are the track's,
|
|
208
|
+
// shared per property, so "Narration 2 lanes" read as if they were that one
|
|
209
|
+
// slice's. Shared by both layouts so the name cannot change with the layout.
|
|
210
|
+
const laneOwnerName =
|
|
211
|
+
clipCount > 1
|
|
212
|
+
? `Track${trackDisplaySuffix(trackDisplayNumber)}`
|
|
213
|
+
: (keyframeClip?.label ?? keyframeClip?.domId ?? keyframeClip?.id ?? trackLabel);
|
|
392
214
|
|
|
393
215
|
// C1: the FX entry point. A single audio clip has one chain to point at; a
|
|
394
216
|
// track holding several ungrouped ones has no single chain — the design
|
|
@@ -400,6 +222,11 @@ export function TimelineTrackHeader({
|
|
|
400
222
|
const singleAudioClip =
|
|
401
223
|
isAudioTrack && clipCount === 1 && trackElements.length > 0 ? trackElements[0] : null;
|
|
402
224
|
const isTrackGrouped = trackElements.some((el) => el.audioGroup);
|
|
225
|
+
// A video track carries sound the render mixes but preview never routes
|
|
226
|
+
// through Web Audio, which is why §1.4 keeps groups audio-only. It still
|
|
227
|
+
// needs to be TOLD that, so it earns the button and a refusal.
|
|
228
|
+
const isVideoWithAudioTrack =
|
|
229
|
+
!isAudioTrack && trackElements.some((el) => el.tag.toLowerCase() === "video");
|
|
403
230
|
const writeClipFxChain = (clip: TimelineElement, next: HfAudioFxChain, live: boolean) => {
|
|
404
231
|
const value = next.nodes.length ? serializeAudioFxChain(next) : null;
|
|
405
232
|
if (live) onSetElementAttributeLive?.(clip, HF_AUDIO_FX_ATTR, value);
|
|
@@ -408,74 +235,138 @@ export function TimelineTrackHeader({
|
|
|
408
235
|
const openClipFxRack = (clip: TimelineElement) => {
|
|
409
236
|
void domEditActions?.handleTimelineElementSelect(clip);
|
|
410
237
|
};
|
|
411
|
-
|
|
238
|
+
// DOM ids, matching the carve picker's other caller — membership is read back
|
|
239
|
+
// by `resolveAudioGroups`, which only ever sees the document. A clip with no
|
|
240
|
+
// DOM id cannot be a member (resolveAudioGroups skips it), so a track holding
|
|
241
|
+
// one cannot be grouped WHOLE — and grouping the rest would quietly leave
|
|
242
|
+
// those clips outside the bus, past every fader, mute and effect, while the
|
|
243
|
+
// UI showed the track as grouped. The button is withheld instead of acting on
|
|
244
|
+
// a subset, which is also why the carve path's loud guard cannot catch this:
|
|
245
|
+
// the unresolvable ids were filtered out before the call.
|
|
246
|
+
const groupableClipIds = trackElements.map(runtimeAudioId);
|
|
247
|
+
const allGroupableClipIds = groupableClipIds.every((id): id is string => id !== null)
|
|
248
|
+
? groupableClipIds
|
|
249
|
+
: null;
|
|
250
|
+
const canGroupWholeTrack = (allGroupableClipIds?.length ?? 0) >= 2;
|
|
251
|
+
const groupUngroupedClips = (label: string) => {
|
|
412
252
|
const doc = domEditActions?.previewIframeRef.current?.contentDocument;
|
|
413
253
|
if (!doc || !onGroupClips) return;
|
|
414
|
-
|
|
415
|
-
void onGroupClips(
|
|
254
|
+
if (!allGroupableClipIds || allGroupableClipIds.length < 2) return;
|
|
255
|
+
void onGroupClips(allGroupableClipIds, mintGroupId(doc), label);
|
|
416
256
|
};
|
|
417
257
|
|
|
418
258
|
return (
|
|
419
259
|
<div
|
|
420
260
|
role="rowheader"
|
|
421
261
|
aria-colindex={1}
|
|
422
|
-
className=
|
|
423
|
-
!isKeyframeLayer
|
|
424
|
-
? showTrackLabel
|
|
425
|
-
? "flex items-center gap-1 px-1.5 text-white/55"
|
|
426
|
-
: "flex flex-col items-center justify-center gap-0.5"
|
|
427
|
-
: ""
|
|
428
|
-
}`}
|
|
262
|
+
className="sticky left-0 z-[12] shrink-0"
|
|
429
263
|
style={{
|
|
430
264
|
width: showTrackLabel ? LABEL_COL_W : contentOrigin,
|
|
431
|
-
background: theme.gutterBackground,
|
|
265
|
+
background: gutterFill(theme.gutterBackground, isGroupMember),
|
|
432
266
|
borderRight: `1px solid ${theme.gutterBorder}`,
|
|
267
|
+
// A group's member rows are `aria-level="2"`, and until this they read
|
|
268
|
+
// as level 2 to a screen reader while looking identical to every
|
|
269
|
+
// top-level row on screen. The rail is the accent-tinted left border
|
|
270
|
+
// B2's design called for; the inset is what actually makes the nesting
|
|
271
|
+
// legible. Padding rather than margin so the rail stays flush with the
|
|
272
|
+
// gutter's own edge.
|
|
273
|
+
...(isGroupMember
|
|
274
|
+
? {
|
|
275
|
+
borderLeft: `2px solid ${GROUP_MEMBER_RAIL}`,
|
|
276
|
+
paddingLeft: GROUP_MEMBER_INDENT,
|
|
277
|
+
}
|
|
278
|
+
: {}),
|
|
433
279
|
}}
|
|
434
280
|
>
|
|
435
|
-
{!
|
|
281
|
+
{!isKeyframeLayer ? (
|
|
436
282
|
<>
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
283
|
+
{/* The two lines own exactly TRACK_H, not the whole header.
|
|
284
|
+
`justify-center` on the header itself centred them in its FULL
|
|
285
|
+
height — which grows by AUTOMATION_LANE_H per open lane — so
|
|
286
|
+
opening one pushed the name and its controls down THROUGH the lane
|
|
287
|
+
rows below, which are absolutely positioned from the top. */}
|
|
288
|
+
<div
|
|
289
|
+
className={
|
|
290
|
+
showTrackLabel
|
|
291
|
+
? "flex flex-col justify-center gap-0.5 px-1.5 text-white/55"
|
|
292
|
+
: "flex flex-col items-center justify-center gap-0.5"
|
|
293
|
+
}
|
|
294
|
+
style={{ height: TRACK_H }}
|
|
295
|
+
>
|
|
296
|
+
<PlainTrackHeader
|
|
297
|
+
trackNumber={trackNumber}
|
|
298
|
+
trackDisplayNumber={trackDisplayNumber}
|
|
299
|
+
trackLabel={trackLabel}
|
|
300
|
+
clipCount={clipCount}
|
|
301
|
+
showTrackLabel={showTrackLabel}
|
|
302
|
+
isTrackHidden={isTrackHidden}
|
|
303
|
+
isAudioTrack={isAudioTrack}
|
|
304
|
+
onToggleTrackHidden={onToggleTrackHidden}
|
|
305
|
+
// On the control line rather than a third row of its own.
|
|
306
|
+
trailing={
|
|
307
|
+
<>
|
|
308
|
+
{singleAudioClip && (
|
|
309
|
+
<TimelineFxButton
|
|
310
|
+
variant="chain"
|
|
311
|
+
fxChainRaw={singleAudioClip.fxChain}
|
|
312
|
+
trackKind={classifyAudioName(singleAudioClip.id, singleAudioClip.src)}
|
|
313
|
+
onChainChange={(next) => writeClipFxChain(singleAudioClip, next, false)}
|
|
314
|
+
onChainPreview={(next) => writeClipFxChain(singleAudioClip, next, true)}
|
|
315
|
+
// Muted, an audition is silent — so the hover lifts the mute on
|
|
316
|
+
// the running graph and puts it back on the way out, the same
|
|
317
|
+
// borrow-and-return it already does with the playhead.
|
|
318
|
+
auditionSpans={[singleAudioClip]}
|
|
319
|
+
isMuted={isTrackHidden}
|
|
320
|
+
onSetMutedLive={(muted) =>
|
|
321
|
+
onSetElementAttributeLive?.(
|
|
322
|
+
singleAudioClip,
|
|
323
|
+
"data-hidden",
|
|
324
|
+
muted ? "" : null,
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
onOpenRack={() => openClipFxRack(singleAudioClip)}
|
|
328
|
+
/>
|
|
329
|
+
)}
|
|
330
|
+
{clipCount > 1 &&
|
|
331
|
+
!isTrackGrouped &&
|
|
332
|
+
(isAudioTrack ? canGroupWholeTrack : isVideoWithAudioTrack) && (
|
|
333
|
+
<TimelineFxButton
|
|
334
|
+
variant="group-pointer"
|
|
335
|
+
clipCount={trackElements.length}
|
|
336
|
+
defaultLabel={trackLabel}
|
|
337
|
+
// Groups are audio-only in v1 (§1.4). A video track showing no
|
|
338
|
+
// button at all is the silent limit §5 forbids, so it gets the
|
|
339
|
+
// button and a reason instead.
|
|
340
|
+
refusal={
|
|
341
|
+
isAudioTrack
|
|
342
|
+
? undefined
|
|
343
|
+
: "Video audio can't be grouped yet — only audio clips can join a group."
|
|
344
|
+
}
|
|
345
|
+
onGroupClips={groupUngroupedClips}
|
|
346
|
+
/>
|
|
347
|
+
)}
|
|
348
|
+
{/* The lane disclosure, on the row's own layout rather than by
|
|
349
|
+
swapping it for a keyframe-layer row. */}
|
|
350
|
+
{disclosable && (
|
|
351
|
+
<LaneToggleButton
|
|
352
|
+
name={laneOwnerName}
|
|
353
|
+
isExpanded={isExpanded}
|
|
354
|
+
lanesId={lanesId}
|
|
355
|
+
onToggle={onToggleClipExpanded}
|
|
356
|
+
/>
|
|
357
|
+
)}
|
|
358
|
+
</>
|
|
359
|
+
}
|
|
458
360
|
/>
|
|
459
|
-
|
|
460
|
-
{isAudioTrack && clipCount > 1 && !isTrackGrouped && (
|
|
461
|
-
<TimelineFxButton variant="group-pointer" onGroupClips={groupUngroupedClips} />
|
|
462
|
-
)}
|
|
361
|
+
</div>
|
|
463
362
|
</>
|
|
464
363
|
) : (
|
|
465
364
|
<>
|
|
466
365
|
<LayerDisclosureRow
|
|
467
|
-
name={
|
|
468
|
-
// A row holding several clips is named for the track, not for
|
|
469
|
-
// whichever of them is selected — the lanes below it are the
|
|
470
|
-
// track's, shared per property, and naming it "Narration 2" read
|
|
471
|
-
// as if they all belonged to that one slice.
|
|
472
|
-
clipCount > 1
|
|
473
|
-
? `Track${trackDisplaySuffix(trackDisplayNumber)}`
|
|
474
|
-
: (keyframeClip.label ?? keyframeClip.domId ?? keyframeClip.id)
|
|
475
|
-
}
|
|
366
|
+
name={laneOwnerName}
|
|
476
367
|
clipCount={clipCount}
|
|
477
368
|
isExpanded={isExpanded}
|
|
478
|
-
gutterBackground={theme.gutterBackground}
|
|
369
|
+
gutterBackground={gutterFill(theme.gutterBackground, isGroupMember)}
|
|
479
370
|
columnWidth={showTrackLabel ? LABEL_COL_W : contentOrigin}
|
|
480
371
|
lanesId={lanesId}
|
|
481
372
|
onToggleClipExpanded={onToggleClipExpanded}
|
|
@@ -489,57 +380,86 @@ export function TimelineTrackHeader({
|
|
|
489
380
|
hidden={isTrackHidden}
|
|
490
381
|
trackNumber={trackNumber}
|
|
491
382
|
trackDisplayNumber={trackDisplayNumber}
|
|
492
|
-
|
|
493
|
-
|
|
383
|
+
// Audio: only while hidden — see the plain header.
|
|
384
|
+
visible={!isAudioTrack || isTrackHidden}
|
|
494
385
|
onToggle={onToggleTrackHidden}
|
|
495
386
|
/>
|
|
496
387
|
</LayerDisclosureRow>
|
|
497
|
-
{/* The caret expands TWO disjoint subtrees: these label-column rows,
|
|
498
|
-
which carry the per-lane keyframe controls, and the diamond lanes
|
|
499
|
-
on the canvas. `lanesId` names the canvas lanes (rendered by
|
|
500
|
-
TimelineLanes), because that is what a sighted user watches appear
|
|
501
|
-
and what following the reference has to land on. These rows are not
|
|
502
|
-
empty and are not the target; they are absolutely positioned inside
|
|
503
|
-
the sticky column, which is what made a wrapper HERE compute to
|
|
504
|
-
0x0 and hold no diamonds. */}
|
|
505
|
-
{isExpanded &&
|
|
506
|
-
lanes.map((lane, laneIndex) => (
|
|
507
|
-
<PropertyGroupHeaderRow
|
|
508
|
-
key={lane.group}
|
|
509
|
-
lanesId={lanesId}
|
|
510
|
-
lane={lane}
|
|
511
|
-
laneIndex={laneIndex}
|
|
512
|
-
isLastLane={laneIndex === lanes.length - 1 && automationRows.length === 0}
|
|
513
|
-
expandedElement={keyframeClip}
|
|
514
|
-
currentTime={currentTime}
|
|
515
|
-
clipPercentage={clipPercentage}
|
|
516
|
-
gutterBackground={theme.gutterBackground}
|
|
517
|
-
columnWidth={showTrackLabel ? LABEL_COL_W : contentOrigin}
|
|
518
|
-
onTogglePropertyGroupKeyframe={onTogglePropertyGroupKeyframe}
|
|
519
|
-
onSeek={onSeek}
|
|
520
|
-
rovingTargetId={rovingTargetId}
|
|
521
|
-
/>
|
|
522
|
-
))}
|
|
523
|
-
{/* Below the keyframe rows and stepping by its own height, which is how
|
|
524
|
-
TimelineAutomationLaneSlot lays the envelopes out on the canvas. The
|
|
525
|
-
two have to agree or a name labels the wrong curve. */}
|
|
526
|
-
{isExpanded &&
|
|
527
|
-
automationRows.map((row, index) => (
|
|
528
|
-
<AutomationLaneHeaderRow
|
|
529
|
-
key={row.key}
|
|
530
|
-
target={row.target}
|
|
531
|
-
label={row.label}
|
|
532
|
-
name={row.name}
|
|
533
|
-
param={row.param}
|
|
534
|
-
top={getTimelineLaneTop(lanes.length) + index * AUTOMATION_LANE_H}
|
|
535
|
-
isLastLane={index === automationRows.length - 1}
|
|
536
|
-
gutterBackground={theme.gutterBackground}
|
|
537
|
-
columnWidth={showTrackLabel ? LABEL_COL_W : contentOrigin}
|
|
538
|
-
onRemove={onRemoveAutomationLane}
|
|
539
|
-
/>
|
|
540
|
-
))}
|
|
541
388
|
</>
|
|
542
389
|
)}
|
|
390
|
+
{/* The caret expands TWO disjoint subtrees: these label-column rows,
|
|
391
|
+
which carry the per-lane keyframe controls, and the diamond lanes
|
|
392
|
+
on the canvas. `lanesId` names the canvas lanes (rendered by
|
|
393
|
+
TimelineLanes), because that is what a sighted user watches appear
|
|
394
|
+
and what following the reference has to land on. These rows are not
|
|
395
|
+
empty and are not the target; they are absolutely positioned inside
|
|
396
|
+
the sticky column, which is what made a wrapper HERE compute to
|
|
397
|
+
0x0 and hold no diamonds. */}
|
|
398
|
+
{isExpanded &&
|
|
399
|
+
keyframeClip &&
|
|
400
|
+
lanes.map((lane, laneIndex) => (
|
|
401
|
+
<PropertyGroupHeaderRow
|
|
402
|
+
key={lane.group}
|
|
403
|
+
lanesId={lanesId}
|
|
404
|
+
lane={lane}
|
|
405
|
+
laneIndex={laneIndex}
|
|
406
|
+
isLastLane={laneIndex === lanes.length - 1 && automationRows.length === 0}
|
|
407
|
+
expandedElement={keyframeClip}
|
|
408
|
+
currentTime={currentTime}
|
|
409
|
+
clipPercentage={clipPercentage}
|
|
410
|
+
gutterBackground={gutterFill(theme.gutterBackground, isGroupMember)}
|
|
411
|
+
columnWidth={showTrackLabel ? LABEL_COL_W : contentOrigin}
|
|
412
|
+
onTogglePropertyGroupKeyframe={onTogglePropertyGroupKeyframe}
|
|
413
|
+
onSeek={onSeek}
|
|
414
|
+
rovingTargetId={rovingTargetId}
|
|
415
|
+
/>
|
|
416
|
+
))}
|
|
417
|
+
{/* Below the keyframe rows and stepping by its own height, which is how
|
|
418
|
+
TimelineAutomationLaneSlot lays the envelopes out on the canvas. The
|
|
419
|
+
two have to agree or a name labels the wrong curve. */}
|
|
420
|
+
{isExpanded &&
|
|
421
|
+
automationRows.map((row, index) => {
|
|
422
|
+
// Bound outside the closure so it narrows: `row.target` is null on a
|
|
423
|
+
// row the active clip is absent from.
|
|
424
|
+
const revealTarget = row.target;
|
|
425
|
+
return (
|
|
426
|
+
<AutomationLaneHeaderRow
|
|
427
|
+
key={row.key}
|
|
428
|
+
target={row.target}
|
|
429
|
+
label={row.label}
|
|
430
|
+
name={row.name}
|
|
431
|
+
param={row.param}
|
|
432
|
+
alsoAutomatedBy={
|
|
433
|
+
groupAutomatedTargets.has(row.key) ? (groupLabelForNote ?? groupOwner) : undefined
|
|
434
|
+
}
|
|
435
|
+
top={getTimelineLaneTop(lanes.length) + index * AUTOMATION_LANE_H}
|
|
436
|
+
isLastLane={index === automationRows.length - 1}
|
|
437
|
+
gutterBackground={gutterFill(theme.gutterBackground, isGroupMember)}
|
|
438
|
+
columnWidth={showTrackLabel ? LABEL_COL_W : contentOrigin}
|
|
439
|
+
onRemove={onRemoveAutomationLane}
|
|
440
|
+
isCarve={row.isCarve}
|
|
441
|
+
// Only for a lane the ACTIVE clip actually draws: the rack shows one
|
|
442
|
+
// element, so a shared row's other envelopes belong to clips it is
|
|
443
|
+
// not showing and there would be nothing to reveal.
|
|
444
|
+
onReveal={
|
|
445
|
+
revealTarget && revealElementId && keyframeClip
|
|
446
|
+
? () => {
|
|
447
|
+
// Select FIRST: the rack is the property panel's view of
|
|
448
|
+
// the selected element, so a reveal aimed at an unselected
|
|
449
|
+
// clip lands on a panel that says "Nothing selected". The
|
|
450
|
+
// request survives the selection — it is stored, not an
|
|
451
|
+
// event — so the rack consumes it as it mounts.
|
|
452
|
+
openClipFxRack(keyframeClip);
|
|
453
|
+
revealAudioFx({
|
|
454
|
+
elementKey: revealElementId,
|
|
455
|
+
automationTarget: revealTarget,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
: undefined
|
|
459
|
+
}
|
|
460
|
+
/>
|
|
461
|
+
);
|
|
462
|
+
})}
|
|
543
463
|
</div>
|
|
544
464
|
);
|
|
545
465
|
}
|