@deck.gl-community/timeline-layers 9.2.8 → 9.3.0-beta.1
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/package.json +9 -9
- package/src/index.ts +42 -0
- package/src/layers/timeline-layer/timeline-collision.ts +51 -0
- package/src/layers/timeline-layer/timeline-layer.ts +868 -0
- package/src/layers/timeline-layer/timeline-layout.ts +80 -0
- package/src/layers/timeline-layer/timeline-types.ts +146 -0
- package/src/layers/timeline-layer/timeline-utils.ts +85 -0
- package/dist/index.cjs +0 -538
- package/dist/index.cjs.map +0 -7
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -9
- package/dist/index.js.map +0 -1
- package/dist/layers/horizon-graph-layer/horizon-graph-layer-uniforms.d.ts +0 -23
- package/dist/layers/horizon-graph-layer/horizon-graph-layer-uniforms.d.ts.map +0 -1
- package/dist/layers/horizon-graph-layer/horizon-graph-layer-uniforms.js +0 -33
- package/dist/layers/horizon-graph-layer/horizon-graph-layer-uniforms.js.map +0 -1
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.d.ts +0 -38
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.d.ts.map +0 -1
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.fs.d.ts +0 -3
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.fs.d.ts.map +0 -1
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.fs.js +0 -53
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.fs.js.map +0 -1
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.js +0 -138
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.js.map +0 -1
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.vs.d.ts +0 -3
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.vs.d.ts.map +0 -1
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.vs.js +0 -24
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.vs.js.map +0 -1
- package/dist/layers/horizon-graph-layer/multi-horizon-graph-layer.d.ts +0 -23
- package/dist/layers/horizon-graph-layer/multi-horizon-graph-layer.d.ts.map +0 -1
- package/dist/layers/horizon-graph-layer/multi-horizon-graph-layer.js +0 -100
- package/dist/layers/horizon-graph-layer/multi-horizon-graph-layer.js.map +0 -1
- package/dist/layers/time-axis-layer.d.ts +0 -56
- package/dist/layers/time-axis-layer.d.ts.map +0 -1
- package/dist/layers/time-axis-layer.js +0 -78
- package/dist/layers/time-axis-layer.js.map +0 -1
- package/dist/layers/vertical-grid-layer.d.ts +0 -41
- package/dist/layers/vertical-grid-layer.d.ts.map +0 -1
- package/dist/layers/vertical-grid-layer.js +0 -43
- package/dist/layers/vertical-grid-layer.js.map +0 -1
- package/dist/utils/format-utils.d.ts +0 -7
- package/dist/utils/format-utils.d.ts.map +0 -1
- package/dist/utils/format-utils.js +0 -75
- package/dist/utils/format-utils.js.map +0 -1
- package/dist/utils/tick-utils.d.ts +0 -10
- package/dist/utils/tick-utils.d.ts.map +0 -1
- package/dist/utils/tick-utils.js +0 -32
- package/dist/utils/tick-utils.js.map +0 -1
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {CompositeLayer, COORDINATE_SYSTEM, type PickingInfo, type Layer} from '@deck.gl/core';
|
|
6
|
+
import {SolidPolygonLayer, LineLayer, TextLayer} from '@deck.gl/layers';
|
|
7
|
+
import type {CompositeLayerProps} from '@deck.gl/core';
|
|
8
|
+
import type {LineLayerProps, SolidPolygonLayerProps, TextLayerProps} from '@deck.gl/layers';
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
TimelineClipInfo,
|
|
12
|
+
TimelineTrackInfo,
|
|
13
|
+
TimelineTrack,
|
|
14
|
+
TrackWithSubtracks,
|
|
15
|
+
TrackPosition,
|
|
16
|
+
TrackBackgroundData,
|
|
17
|
+
TrackLabelData,
|
|
18
|
+
ClipPolygonData,
|
|
19
|
+
ClipLabelData,
|
|
20
|
+
ClipWithSubtrack,
|
|
21
|
+
SeparatorLineData,
|
|
22
|
+
AxisLineData,
|
|
23
|
+
AxisLabelData,
|
|
24
|
+
ScrubberLineData,
|
|
25
|
+
ScrubberHandleData,
|
|
26
|
+
ScrubberLabelData,
|
|
27
|
+
TimeAxisLabelFormatter
|
|
28
|
+
} from './timeline-types';
|
|
29
|
+
|
|
30
|
+
import type {SelectionStyle} from './timeline-layout';
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
timeAxisFormatters,
|
|
34
|
+
generateTimelineTicks,
|
|
35
|
+
timeToPosition,
|
|
36
|
+
positionToTime
|
|
37
|
+
} from './timeline-utils';
|
|
38
|
+
import {assignClipsToSubtracks, calculateSubtrackCount} from './timeline-collision';
|
|
39
|
+
|
|
40
|
+
function lightenColor(
|
|
41
|
+
color: [number, number, number, number],
|
|
42
|
+
amount: number = 30
|
|
43
|
+
): [number, number, number, number] {
|
|
44
|
+
return [
|
|
45
|
+
Math.min(255, color[0] + amount),
|
|
46
|
+
Math.min(255, color[1] + amount),
|
|
47
|
+
Math.min(255, color[2] + amount),
|
|
48
|
+
color[3]
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const defaultProps = {
|
|
53
|
+
x: 150,
|
|
54
|
+
y: 100,
|
|
55
|
+
width: 800,
|
|
56
|
+
trackHeight: 40,
|
|
57
|
+
trackSpacing: 10,
|
|
58
|
+
currentTimeMs: 0,
|
|
59
|
+
showScrubber: true,
|
|
60
|
+
showClipLabels: true,
|
|
61
|
+
showTrackLabels: true,
|
|
62
|
+
showAxis: true,
|
|
63
|
+
showSubtrackSeparators: true,
|
|
64
|
+
timeFormatter: timeAxisFormatters.seconds,
|
|
65
|
+
selectionStyle: {
|
|
66
|
+
selectedClipColor: [255, 200, 0, 255] as [number, number, number, number],
|
|
67
|
+
hoveredClipColor: [200, 200, 200, 255] as [number, number, number, number],
|
|
68
|
+
selectedTrackColor: [80, 80, 80, 255] as [number, number, number, number],
|
|
69
|
+
hoveredTrackColor: [70, 70, 70, 255] as [number, number, number, number],
|
|
70
|
+
selectedLineWidth: 3,
|
|
71
|
+
hoveredLineWidth: 2
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type TimelineLayerProps = CompositeLayerProps & {
|
|
76
|
+
/** Array of timeline tracks, each containing clips */
|
|
77
|
+
data: TimelineTrack[];
|
|
78
|
+
/** Start of the full timeline range in milliseconds */
|
|
79
|
+
timelineStart: number;
|
|
80
|
+
/** End of the full timeline range in milliseconds */
|
|
81
|
+
timelineEnd: number;
|
|
82
|
+
|
|
83
|
+
/** X offset of the timeline in canvas coordinates */
|
|
84
|
+
x?: number;
|
|
85
|
+
/** Y offset of the timeline in canvas coordinates */
|
|
86
|
+
y?: number;
|
|
87
|
+
/** Width of the timeline in canvas coordinates */
|
|
88
|
+
width?: number;
|
|
89
|
+
/** Height of each track row in canvas coordinates */
|
|
90
|
+
trackHeight?: number;
|
|
91
|
+
/** Spacing between tracks in canvas coordinates */
|
|
92
|
+
trackSpacing?: number;
|
|
93
|
+
|
|
94
|
+
/** Current playhead time in milliseconds */
|
|
95
|
+
currentTimeMs?: number;
|
|
96
|
+
/** Optional zoomed viewport range */
|
|
97
|
+
viewport?: {startMs?: number; endMs?: number};
|
|
98
|
+
/** Formatter for time axis labels */
|
|
99
|
+
timeFormatter?: TimeAxisLabelFormatter;
|
|
100
|
+
|
|
101
|
+
/** ID of the currently selected clip */
|
|
102
|
+
selectedClipId?: string | number | null;
|
|
103
|
+
/** ID of the currently hovered clip */
|
|
104
|
+
hoveredClipId?: string | number | null;
|
|
105
|
+
/** ID of the currently selected track */
|
|
106
|
+
selectedTrackId?: string | number | null;
|
|
107
|
+
/** ID of the currently hovered track */
|
|
108
|
+
hoveredTrackId?: string | number | null;
|
|
109
|
+
/** Colors and line widths for selected/hovered states */
|
|
110
|
+
selectionStyle?: SelectionStyle;
|
|
111
|
+
|
|
112
|
+
/** Whether to show the playhead scrubber */
|
|
113
|
+
showScrubber?: boolean;
|
|
114
|
+
/** Whether to show labels on clips */
|
|
115
|
+
showClipLabels?: boolean;
|
|
116
|
+
/** Whether to show labels on tracks */
|
|
117
|
+
showTrackLabels?: boolean;
|
|
118
|
+
/** Whether to show the time axis */
|
|
119
|
+
showAxis?: boolean;
|
|
120
|
+
/** Whether to show separators between collision subtracks */
|
|
121
|
+
showSubtrackSeparators?: boolean;
|
|
122
|
+
|
|
123
|
+
/** Override props for the clip polygon sub-layer */
|
|
124
|
+
clipProps?: Partial<SolidPolygonLayerProps<ClipPolygonData>>;
|
|
125
|
+
/** Override props for the track background sub-layer */
|
|
126
|
+
trackProps?: Partial<SolidPolygonLayerProps<TrackBackgroundData>>;
|
|
127
|
+
/** Override props for the track label sub-layer */
|
|
128
|
+
trackLabelProps?: Partial<TextLayerProps<TrackLabelData>>;
|
|
129
|
+
/** Override props for the clip label sub-layer */
|
|
130
|
+
clipLabelProps?: Partial<TextLayerProps<ClipLabelData>>;
|
|
131
|
+
/** Override props for the axis line sub-layer */
|
|
132
|
+
axisLineProps?: Partial<LineLayerProps<AxisLineData>>;
|
|
133
|
+
/** Override props for the axis label sub-layer */
|
|
134
|
+
axisLabelProps?: Partial<TextLayerProps<AxisLabelData>>;
|
|
135
|
+
/** Override props for the scrubber line sub-layer */
|
|
136
|
+
scrubberLineProps?: Partial<LineLayerProps<ScrubberLineData>>;
|
|
137
|
+
|
|
138
|
+
/** Callback when a clip is clicked */
|
|
139
|
+
onClipClick?: (info: TimelineClipInfo, event: PickingInfo) => void;
|
|
140
|
+
/** Callback when a clip is hovered */
|
|
141
|
+
onClipHover?: (info: TimelineClipInfo | null, event: PickingInfo) => void;
|
|
142
|
+
/** Callback when a track is clicked */
|
|
143
|
+
onTrackClick?: (info: TimelineTrackInfo, event: PickingInfo) => void;
|
|
144
|
+
/** Callback when a track is hovered */
|
|
145
|
+
onTrackHover?: (info: TimelineTrackInfo | null, event: PickingInfo) => void;
|
|
146
|
+
/** Callback when the scrubber handle is hovered */
|
|
147
|
+
onScrubberHover?: (isHovering: boolean, event: PickingInfo) => void;
|
|
148
|
+
/** Callback when a scrubber drag begins */
|
|
149
|
+
onScrubberDragStart?: (event: PickingInfo) => void;
|
|
150
|
+
/** Callback when the scrubber is dragged to a new time */
|
|
151
|
+
onScrubberDrag?: (timeMs: number, event: PickingInfo) => void;
|
|
152
|
+
/** Callback when the timeline background is clicked */
|
|
153
|
+
onTimelineClick?: (timeMs: number, event: PickingInfo) => void;
|
|
154
|
+
|
|
155
|
+
/** Callback when the current time changes */
|
|
156
|
+
onCurrentTimeChange?: (timeMs: number) => void;
|
|
157
|
+
/** Callback when the viewport (zoom/pan) changes */
|
|
158
|
+
onViewportChange?: (startMs: number, endMs: number) => void;
|
|
159
|
+
/** Callback when the zoom level changes */
|
|
160
|
+
onZoomChange?: (zoomLevel: number) => void;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export class TimelineLayer extends CompositeLayer<TimelineLayerProps> {
|
|
164
|
+
static layerName = 'TimelineLayer';
|
|
165
|
+
static defaultProps = defaultProps;
|
|
166
|
+
|
|
167
|
+
/** Convert a canvas X coordinate to a time in milliseconds */
|
|
168
|
+
getTimeFromPosition(x: number): number {
|
|
169
|
+
const {timelineStart, timelineEnd, viewport, x: timelineX = 150, width = 800} = this.props;
|
|
170
|
+
const effectiveStartMs = viewport?.startMs ?? timelineStart;
|
|
171
|
+
const effectiveEndMs = viewport?.endMs ?? timelineEnd;
|
|
172
|
+
return positionToTime(x, timelineX, width, effectiveStartMs, effectiveEndMs);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Zoom the timeline viewport around a canvas X coordinate */
|
|
176
|
+
zoomToPoint(zoomFactor: number, mouseX: number, currentZoomLevel: number): void {
|
|
177
|
+
const {
|
|
178
|
+
timelineStart,
|
|
179
|
+
timelineEnd,
|
|
180
|
+
viewport,
|
|
181
|
+
x: timelineX = 150,
|
|
182
|
+
width = 800,
|
|
183
|
+
onViewportChange,
|
|
184
|
+
onZoomChange
|
|
185
|
+
} = this.props;
|
|
186
|
+
|
|
187
|
+
const newZoomLevel = Math.max(1.0, Math.min(100, currentZoomLevel * zoomFactor));
|
|
188
|
+
const mouseRatio = Math.max(0, Math.min(1, (mouseX - timelineX) / width));
|
|
189
|
+
const currentStartMs = viewport?.startMs ?? timelineStart;
|
|
190
|
+
const currentEndMs = viewport?.endMs ?? timelineEnd;
|
|
191
|
+
const mouseTimeMs = currentStartMs + mouseRatio * (currentEndMs - currentStartMs);
|
|
192
|
+
|
|
193
|
+
const fullTimeRange = timelineEnd - timelineStart;
|
|
194
|
+
const newViewportRange = fullTimeRange / newZoomLevel;
|
|
195
|
+
|
|
196
|
+
let newStartMs = mouseTimeMs - mouseRatio * newViewportRange;
|
|
197
|
+
let newEndMs = newStartMs + newViewportRange;
|
|
198
|
+
|
|
199
|
+
if (newStartMs < timelineStart) {
|
|
200
|
+
newStartMs = timelineStart;
|
|
201
|
+
newEndMs = timelineStart + newViewportRange;
|
|
202
|
+
} else if (newEndMs > timelineEnd) {
|
|
203
|
+
newEndMs = timelineEnd;
|
|
204
|
+
newStartMs = timelineEnd - newViewportRange;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (newZoomLevel > 1.0) {
|
|
208
|
+
onViewportChange?.(newStartMs, newEndMs);
|
|
209
|
+
} else {
|
|
210
|
+
onViewportChange?.(timelineStart, timelineEnd);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
onZoomChange?.(newZoomLevel);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ===== LAYOUT CALCULATION =====
|
|
217
|
+
|
|
218
|
+
private _calculateTrackPositions(
|
|
219
|
+
tracksWithSubtracks: TrackWithSubtracks[],
|
|
220
|
+
y: number,
|
|
221
|
+
trackHeight: number,
|
|
222
|
+
trackSpacing: number
|
|
223
|
+
): {trackPositions: TrackPosition[]; totalTimelineHeight: number} {
|
|
224
|
+
let currentY = y;
|
|
225
|
+
const trackPositions: TrackPosition[] = [];
|
|
226
|
+
const subtrackSpacing = 2;
|
|
227
|
+
|
|
228
|
+
for (const {subtrackCount} of tracksWithSubtracks) {
|
|
229
|
+
const trackTotalHeight = subtrackCount * trackHeight + (subtrackCount - 1) * subtrackSpacing;
|
|
230
|
+
trackPositions.push({y: currentY, height: trackTotalHeight, subtrackCount});
|
|
231
|
+
currentY += trackTotalHeight + trackSpacing;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const totalTimelineHeight = currentY - y - trackSpacing;
|
|
235
|
+
return {trackPositions, totalTimelineHeight};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ===== DATA GENERATION =====
|
|
239
|
+
|
|
240
|
+
private _generateTrackBackgrounds(
|
|
241
|
+
tracksWithSubtracks: TrackWithSubtracks[],
|
|
242
|
+
trackPositions: TrackPosition[]
|
|
243
|
+
): TrackBackgroundData[] {
|
|
244
|
+
const {
|
|
245
|
+
x = 150,
|
|
246
|
+
width = 800,
|
|
247
|
+
selectedTrackId,
|
|
248
|
+
hoveredTrackId,
|
|
249
|
+
selectionStyle = defaultProps.selectionStyle
|
|
250
|
+
} = this.props;
|
|
251
|
+
|
|
252
|
+
return tracksWithSubtracks.map(({track, trackIndex}, i) => {
|
|
253
|
+
const {y: trackY, height} = trackPositions[i];
|
|
254
|
+
const isSelected = selectedTrackId === track.id;
|
|
255
|
+
const isHovered = hoveredTrackId === track.id;
|
|
256
|
+
|
|
257
|
+
let color: [number, number, number, number] = [60, 60, 60, 255];
|
|
258
|
+
if (isSelected) {
|
|
259
|
+
color = selectionStyle.selectedTrackColor!;
|
|
260
|
+
} else if (isHovered) {
|
|
261
|
+
color = lightenColor([60, 60, 60, 255], 20);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
id: `track-bg-${track.id}`,
|
|
266
|
+
track,
|
|
267
|
+
trackIndex,
|
|
268
|
+
polygon: [
|
|
269
|
+
[x, trackY],
|
|
270
|
+
[x + width, trackY],
|
|
271
|
+
[x + width, trackY + height],
|
|
272
|
+
[x, trackY + height]
|
|
273
|
+
],
|
|
274
|
+
color
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private _generateTrackLabels(
|
|
280
|
+
tracksWithSubtracks: TrackWithSubtracks[],
|
|
281
|
+
trackPositions: TrackPosition[]
|
|
282
|
+
): TrackLabelData[] {
|
|
283
|
+
const {x = 150, showTrackLabels = true} = this.props;
|
|
284
|
+
if (!showTrackLabels) return [];
|
|
285
|
+
|
|
286
|
+
return tracksWithSubtracks.map(({track}, i) => {
|
|
287
|
+
const label = track.name || `Track ${track.id}`;
|
|
288
|
+
const {y: trackY, height} = trackPositions[i];
|
|
289
|
+
return {text: label, position: [x - 10, trackY + height / 2, 0]};
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private _buildClipPolygon(
|
|
294
|
+
clip: ClipWithSubtrack,
|
|
295
|
+
opts: {
|
|
296
|
+
track: TimelineTrack;
|
|
297
|
+
trackIndex: number;
|
|
298
|
+
clipIndex: number;
|
|
299
|
+
subtrackHeight: number;
|
|
300
|
+
baseTrackY: number;
|
|
301
|
+
x: number;
|
|
302
|
+
width: number;
|
|
303
|
+
effectiveStartMs: number;
|
|
304
|
+
effectiveEndMs: number;
|
|
305
|
+
selectedClipId: string | number | null | undefined;
|
|
306
|
+
hoveredClipId: string | number | null | undefined;
|
|
307
|
+
selectionStyle: SelectionStyle;
|
|
308
|
+
}
|
|
309
|
+
): ClipPolygonData | null {
|
|
310
|
+
const {id: clipId, startMs, endMs, subtrackIndex = 0} = clip;
|
|
311
|
+
const {
|
|
312
|
+
track,
|
|
313
|
+
trackIndex,
|
|
314
|
+
clipIndex,
|
|
315
|
+
subtrackHeight,
|
|
316
|
+
baseTrackY,
|
|
317
|
+
x,
|
|
318
|
+
width,
|
|
319
|
+
effectiveStartMs,
|
|
320
|
+
effectiveEndMs,
|
|
321
|
+
selectedClipId,
|
|
322
|
+
hoveredClipId,
|
|
323
|
+
selectionStyle
|
|
324
|
+
} = opts;
|
|
325
|
+
|
|
326
|
+
if (endMs <= effectiveStartMs || startMs >= effectiveEndMs) return null;
|
|
327
|
+
|
|
328
|
+
const clipPadding = 2;
|
|
329
|
+
const subtrackSpacing = 2;
|
|
330
|
+
const clipTrackY = baseTrackY + subtrackIndex * (subtrackHeight + subtrackSpacing);
|
|
331
|
+
const clipStartRatio = (startMs - effectiveStartMs) / (effectiveEndMs - effectiveStartMs);
|
|
332
|
+
const clipEndRatio = (endMs - effectiveStartMs) / (effectiveEndMs - effectiveStartMs);
|
|
333
|
+
const clipStartX = x + Math.max(0, clipStartRatio) * width;
|
|
334
|
+
const clipEndX = x + Math.min(1, clipEndRatio) * width;
|
|
335
|
+
|
|
336
|
+
const baseColor = clip.color || ([80, 120, 160, 220] as [number, number, number, number]);
|
|
337
|
+
const isSelected = selectedClipId !== null && String(selectedClipId) === String(clipId);
|
|
338
|
+
const isHovered = hoveredClipId !== null && String(hoveredClipId) === String(clipId);
|
|
339
|
+
|
|
340
|
+
let color = baseColor;
|
|
341
|
+
if (isSelected) {
|
|
342
|
+
color = selectionStyle.selectedClipColor!;
|
|
343
|
+
} else if (isHovered) {
|
|
344
|
+
color = lightenColor(baseColor, 40);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
id: clipId,
|
|
349
|
+
clip,
|
|
350
|
+
track,
|
|
351
|
+
clipIndex,
|
|
352
|
+
trackIndex,
|
|
353
|
+
subtrackIndex,
|
|
354
|
+
polygon: [
|
|
355
|
+
[clipStartX, clipTrackY + clipPadding],
|
|
356
|
+
[clipEndX, clipTrackY + clipPadding],
|
|
357
|
+
[clipEndX, clipTrackY + subtrackHeight - clipPadding],
|
|
358
|
+
[clipStartX, clipTrackY + subtrackHeight - clipPadding]
|
|
359
|
+
],
|
|
360
|
+
color,
|
|
361
|
+
label: clip.label || '',
|
|
362
|
+
labelPosition: [clipStartX + (clipEndX - clipStartX) / 2, clipTrackY + subtrackHeight / 2, 0]
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private _generateClipPolygons(
|
|
367
|
+
tracksWithSubtracks: TrackWithSubtracks[],
|
|
368
|
+
trackPositions: TrackPosition[],
|
|
369
|
+
effectiveStartMs: number,
|
|
370
|
+
effectiveEndMs: number
|
|
371
|
+
): ClipPolygonData[] {
|
|
372
|
+
const {
|
|
373
|
+
x = 150,
|
|
374
|
+
width = 800,
|
|
375
|
+
selectedClipId,
|
|
376
|
+
hoveredClipId,
|
|
377
|
+
selectionStyle = defaultProps.selectionStyle
|
|
378
|
+
} = this.props;
|
|
379
|
+
|
|
380
|
+
const subtrackSpacing = 2;
|
|
381
|
+
const clipPolygons: ClipPolygonData[] = [];
|
|
382
|
+
|
|
383
|
+
for (let i = 0; i < tracksWithSubtracks.length; i++) {
|
|
384
|
+
const {track, trackIndex, clips, subtrackCount} = tracksWithSubtracks[i];
|
|
385
|
+
const {y: baseTrackY, height: trackTotalHeight} = trackPositions[i];
|
|
386
|
+
const subtrackHeight =
|
|
387
|
+
(trackTotalHeight - (subtrackCount - 1) * subtrackSpacing) / subtrackCount;
|
|
388
|
+
|
|
389
|
+
for (let clipIndex = 0; clipIndex < clips.length; clipIndex++) {
|
|
390
|
+
const polygon = this._buildClipPolygon(clips[clipIndex], {
|
|
391
|
+
track,
|
|
392
|
+
trackIndex,
|
|
393
|
+
clipIndex,
|
|
394
|
+
subtrackHeight,
|
|
395
|
+
baseTrackY,
|
|
396
|
+
x,
|
|
397
|
+
width,
|
|
398
|
+
effectiveStartMs,
|
|
399
|
+
effectiveEndMs,
|
|
400
|
+
selectedClipId,
|
|
401
|
+
hoveredClipId,
|
|
402
|
+
selectionStyle
|
|
403
|
+
});
|
|
404
|
+
if (polygon) {
|
|
405
|
+
clipPolygons.push(polygon);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return clipPolygons;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private _generateSubtrackSeparators(
|
|
414
|
+
tracksWithSubtracks: TrackWithSubtracks[],
|
|
415
|
+
trackPositions: TrackPosition[]
|
|
416
|
+
): SeparatorLineData[] {
|
|
417
|
+
const {x = 150, width = 800, showSubtrackSeparators = true} = this.props;
|
|
418
|
+
if (!showSubtrackSeparators) return [];
|
|
419
|
+
|
|
420
|
+
const subtrackSpacing = 2;
|
|
421
|
+
const separatorLines: SeparatorLineData[] = [];
|
|
422
|
+
|
|
423
|
+
for (let i = 0; i < tracksWithSubtracks.length; i++) {
|
|
424
|
+
const {subtrackCount} = tracksWithSubtracks[i];
|
|
425
|
+
if (subtrackCount <= 1) {
|
|
426
|
+
// No separators needed for single-subtrack rows
|
|
427
|
+
} else {
|
|
428
|
+
const {y: baseTrackY, height: trackTotalHeight} = trackPositions[i];
|
|
429
|
+
const subtrackHeight =
|
|
430
|
+
(trackTotalHeight - (subtrackCount - 1) * subtrackSpacing) / subtrackCount;
|
|
431
|
+
|
|
432
|
+
for (let j = 1; j < subtrackCount; j++) {
|
|
433
|
+
const separatorY =
|
|
434
|
+
baseTrackY + j * (subtrackHeight + subtrackSpacing) - subtrackSpacing / 2;
|
|
435
|
+
separatorLines.push({
|
|
436
|
+
sourcePosition: [x, separatorY],
|
|
437
|
+
targetPosition: [x + width, separatorY]
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return separatorLines;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private _generateAxis(
|
|
447
|
+
totalTimelineHeight: number,
|
|
448
|
+
effectiveStartMs: number,
|
|
449
|
+
effectiveEndMs: number
|
|
450
|
+
): {axisLines: AxisLineData[]; axisLabels: AxisLabelData[]} {
|
|
451
|
+
const {
|
|
452
|
+
x = 150,
|
|
453
|
+
y = 100,
|
|
454
|
+
width = 800,
|
|
455
|
+
showAxis = true,
|
|
456
|
+
timeFormatter = timeAxisFormatters.seconds
|
|
457
|
+
} = this.props;
|
|
458
|
+
|
|
459
|
+
const axisLines: AxisLineData[] = [];
|
|
460
|
+
const axisLabels: AxisLabelData[] = [];
|
|
461
|
+
|
|
462
|
+
if (!showAxis) return {axisLines, axisLabels};
|
|
463
|
+
|
|
464
|
+
const axisHeight = 30;
|
|
465
|
+
const tickCount = Math.max(4, Math.min(10, Math.floor(width / 80)));
|
|
466
|
+
|
|
467
|
+
const timelineTicks = generateTimelineTicks({
|
|
468
|
+
startMs: effectiveStartMs,
|
|
469
|
+
endMs: effectiveEndMs,
|
|
470
|
+
timelineX: x,
|
|
471
|
+
timelineWidth: width,
|
|
472
|
+
tickCount,
|
|
473
|
+
formatter: timeFormatter
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const axisY = y + totalTimelineHeight + axisHeight;
|
|
477
|
+
|
|
478
|
+
axisLines.push({sourcePosition: [x, axisY], targetPosition: [x + width, axisY]});
|
|
479
|
+
|
|
480
|
+
for (const tick of timelineTicks) {
|
|
481
|
+
axisLines.push({
|
|
482
|
+
sourcePosition: [tick.position, axisY - 5],
|
|
483
|
+
targetPosition: [tick.position, axisY + 5]
|
|
484
|
+
});
|
|
485
|
+
axisLabels.push({text: tick.label, position: [tick.position, axisY + 15, 0]});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return {axisLines, axisLabels};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private _generateScrubber(
|
|
492
|
+
totalTimelineHeight: number,
|
|
493
|
+
effectiveStartMs: number,
|
|
494
|
+
effectiveEndMs: number
|
|
495
|
+
): {
|
|
496
|
+
scrubberLine: ScrubberLineData[];
|
|
497
|
+
scrubberHandle: ScrubberHandleData[];
|
|
498
|
+
scrubberLabel: ScrubberLabelData[];
|
|
499
|
+
} {
|
|
500
|
+
const {
|
|
501
|
+
x = 150,
|
|
502
|
+
y = 100,
|
|
503
|
+
width = 800,
|
|
504
|
+
showScrubber = true,
|
|
505
|
+
currentTimeMs = 0,
|
|
506
|
+
timeFormatter = timeAxisFormatters.seconds
|
|
507
|
+
} = this.props;
|
|
508
|
+
|
|
509
|
+
if (!showScrubber) return {scrubberLine: [], scrubberHandle: [], scrubberLabel: []};
|
|
510
|
+
|
|
511
|
+
const scrubberPosition = timeToPosition(
|
|
512
|
+
currentTimeMs,
|
|
513
|
+
x,
|
|
514
|
+
width,
|
|
515
|
+
effectiveStartMs,
|
|
516
|
+
effectiveEndMs
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const scrubberLine: ScrubberLineData[] = [
|
|
520
|
+
{
|
|
521
|
+
sourcePosition: [scrubberPosition, y - 30],
|
|
522
|
+
targetPosition: [scrubberPosition, y + totalTimelineHeight + 30]
|
|
523
|
+
}
|
|
524
|
+
];
|
|
525
|
+
|
|
526
|
+
const scrubberHandle: ScrubberHandleData[] = [
|
|
527
|
+
{
|
|
528
|
+
id: 'scrubber-handle',
|
|
529
|
+
polygon: [
|
|
530
|
+
[scrubberPosition - 8, y - 35],
|
|
531
|
+
[scrubberPosition + 8, y - 35],
|
|
532
|
+
[scrubberPosition + 8, y - 20],
|
|
533
|
+
[scrubberPosition - 8, y - 20]
|
|
534
|
+
],
|
|
535
|
+
color: [255, 100, 100, 255]
|
|
536
|
+
}
|
|
537
|
+
];
|
|
538
|
+
|
|
539
|
+
const scrubberLabel: ScrubberLabelData[] = [
|
|
540
|
+
{text: timeFormatter(currentTimeMs), position: [scrubberPosition, y - 40, 0]}
|
|
541
|
+
];
|
|
542
|
+
|
|
543
|
+
return {scrubberLine, scrubberHandle, scrubberLabel};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ===== LAYER CREATION =====
|
|
547
|
+
|
|
548
|
+
private _createTrackLayers(
|
|
549
|
+
trackBackgrounds: TrackBackgroundData[],
|
|
550
|
+
trackLabels: TrackLabelData[]
|
|
551
|
+
): Layer[] {
|
|
552
|
+
const {trackProps, showTrackLabels = true, onTrackClick, onTrackHover} = this.props;
|
|
553
|
+
const layers: Layer[] = [];
|
|
554
|
+
|
|
555
|
+
layers.push(
|
|
556
|
+
new SolidPolygonLayer(
|
|
557
|
+
this.getSubLayerProps({
|
|
558
|
+
...trackProps,
|
|
559
|
+
id: 'tracks',
|
|
560
|
+
data: trackBackgrounds,
|
|
561
|
+
getPolygon: (d: TrackBackgroundData) => d.polygon,
|
|
562
|
+
getFillColor: (d: TrackBackgroundData) => d.color,
|
|
563
|
+
stroked: true,
|
|
564
|
+
getLineColor: [100, 100, 100, 255],
|
|
565
|
+
getLineWidth: 1,
|
|
566
|
+
pickable: Boolean(onTrackClick) || Boolean(onTrackHover),
|
|
567
|
+
onClick: (info: PickingInfo) => {
|
|
568
|
+
if (info.object && onTrackClick) {
|
|
569
|
+
const obj = info.object as TrackBackgroundData;
|
|
570
|
+
onTrackClick({track: obj.track, index: obj.trackIndex}, info);
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
onHover: (info: PickingInfo) => {
|
|
574
|
+
if (onTrackHover) {
|
|
575
|
+
const obj = info.object as TrackBackgroundData | undefined;
|
|
576
|
+
onTrackHover(obj ? {track: obj.track, index: obj.trackIndex} : null, info);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
)
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
if (showTrackLabels) {
|
|
584
|
+
layers.push(
|
|
585
|
+
new TextLayer({
|
|
586
|
+
id: `${this.props.id}-track-labels`,
|
|
587
|
+
data: trackLabels,
|
|
588
|
+
getText: (d: TrackLabelData) => d.text,
|
|
589
|
+
getPosition: (d: TrackLabelData) => d.position,
|
|
590
|
+
getSize: 12,
|
|
591
|
+
getColor: [60, 60, 60, 255],
|
|
592
|
+
getTextAnchor: 'end',
|
|
593
|
+
getAlignmentBaseline: 'center',
|
|
594
|
+
fontFamily: 'Arial, sans-serif',
|
|
595
|
+
fontWeight: 'bold',
|
|
596
|
+
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN
|
|
597
|
+
})
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return layers;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private _createClipLayers(
|
|
605
|
+
clipPolygons: ClipPolygonData[],
|
|
606
|
+
clipLabelsData: ClipLabelData[]
|
|
607
|
+
): Layer[] {
|
|
608
|
+
const {
|
|
609
|
+
clipProps,
|
|
610
|
+
showClipLabels = true,
|
|
611
|
+
selectedClipId,
|
|
612
|
+
selectionStyle = defaultProps.selectionStyle,
|
|
613
|
+
onClipClick,
|
|
614
|
+
onClipHover
|
|
615
|
+
} = this.props;
|
|
616
|
+
const layers: Layer[] = [];
|
|
617
|
+
|
|
618
|
+
layers.push(
|
|
619
|
+
new SolidPolygonLayer(
|
|
620
|
+
this.getSubLayerProps({
|
|
621
|
+
...clipProps,
|
|
622
|
+
id: 'clips',
|
|
623
|
+
data: clipPolygons,
|
|
624
|
+
getPolygon: (d: ClipPolygonData) => d.polygon,
|
|
625
|
+
getFillColor: (d: ClipPolygonData) => d.color,
|
|
626
|
+
stroked: true,
|
|
627
|
+
getLineColor: [255, 255, 255, 200],
|
|
628
|
+
getLineWidth: (d: ClipPolygonData) => {
|
|
629
|
+
const isSelected = selectedClipId !== null && String(selectedClipId) === String(d.id);
|
|
630
|
+
return isSelected ? selectionStyle.selectedLineWidth || 3 : 2;
|
|
631
|
+
},
|
|
632
|
+
pickable: Boolean(onClipClick) || Boolean(onClipHover),
|
|
633
|
+
autoHighlight: true,
|
|
634
|
+
onClick: (info: PickingInfo) => {
|
|
635
|
+
if (info.object && onClipClick) {
|
|
636
|
+
const obj = info.object as ClipPolygonData;
|
|
637
|
+
onClipClick(
|
|
638
|
+
{
|
|
639
|
+
clip: obj.clip,
|
|
640
|
+
track: obj.track,
|
|
641
|
+
clipIndex: obj.clipIndex,
|
|
642
|
+
trackIndex: obj.trackIndex,
|
|
643
|
+
subtrackIndex: obj.subtrackIndex
|
|
644
|
+
},
|
|
645
|
+
info
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
onHover: (info: PickingInfo) => {
|
|
650
|
+
if (onClipHover) {
|
|
651
|
+
const obj = info.object as ClipPolygonData | undefined;
|
|
652
|
+
onClipHover(
|
|
653
|
+
obj
|
|
654
|
+
? {
|
|
655
|
+
clip: obj.clip,
|
|
656
|
+
track: obj.track,
|
|
657
|
+
clipIndex: obj.clipIndex,
|
|
658
|
+
trackIndex: obj.trackIndex,
|
|
659
|
+
subtrackIndex: obj.subtrackIndex
|
|
660
|
+
}
|
|
661
|
+
: null,
|
|
662
|
+
info
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
})
|
|
667
|
+
)
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
if (showClipLabels) {
|
|
671
|
+
layers.push(
|
|
672
|
+
new TextLayer({
|
|
673
|
+
id: `${this.props.id}-clip-labels`,
|
|
674
|
+
data: clipLabelsData,
|
|
675
|
+
getText: (d: ClipLabelData) => d.text,
|
|
676
|
+
getPosition: (d: ClipLabelData) => d.position,
|
|
677
|
+
getSize: 10,
|
|
678
|
+
getColor: [255, 255, 255, 255],
|
|
679
|
+
getTextAnchor: 'middle',
|
|
680
|
+
getAlignmentBaseline: 'center',
|
|
681
|
+
fontFamily: 'Arial, sans-serif',
|
|
682
|
+
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN
|
|
683
|
+
})
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return layers;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private _createSubtrackSeparatorLayer(separators: SeparatorLineData[]): Layer | null {
|
|
691
|
+
const {showSubtrackSeparators = true} = this.props;
|
|
692
|
+
if (!showSubtrackSeparators) return null;
|
|
693
|
+
|
|
694
|
+
return new LineLayer({
|
|
695
|
+
id: `${this.props.id}-subtrack-separators`,
|
|
696
|
+
data: separators,
|
|
697
|
+
getSourcePosition: (d: SeparatorLineData) => d.sourcePosition,
|
|
698
|
+
getTargetPosition: (d: SeparatorLineData) => d.targetPosition,
|
|
699
|
+
getColor: [180, 180, 180, 128],
|
|
700
|
+
getWidth: 1,
|
|
701
|
+
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private _createAxisLayers(axisLines: AxisLineData[], axisLabels: AxisLabelData[]): Layer[] {
|
|
706
|
+
const {axisLineProps, axisLabelProps, showAxis = true} = this.props;
|
|
707
|
+
if (!showAxis) return [];
|
|
708
|
+
|
|
709
|
+
return [
|
|
710
|
+
new LineLayer(
|
|
711
|
+
this.getSubLayerProps({
|
|
712
|
+
...axisLineProps,
|
|
713
|
+
id: 'axis-lines',
|
|
714
|
+
data: axisLines,
|
|
715
|
+
getSourcePosition: (d: AxisLineData) => d.sourcePosition,
|
|
716
|
+
getTargetPosition: (d: AxisLineData) => d.targetPosition,
|
|
717
|
+
getColor: [150, 150, 150, 255],
|
|
718
|
+
getWidth: 2
|
|
719
|
+
})
|
|
720
|
+
),
|
|
721
|
+
new TextLayer(
|
|
722
|
+
this.getSubLayerProps({
|
|
723
|
+
...axisLabelProps,
|
|
724
|
+
id: 'axis-labels',
|
|
725
|
+
data: axisLabels,
|
|
726
|
+
getText: (d: AxisLabelData) => d.text,
|
|
727
|
+
getPosition: (d: AxisLabelData) => d.position,
|
|
728
|
+
getSize: 11,
|
|
729
|
+
getColor: [150, 150, 150, 255],
|
|
730
|
+
getTextAnchor: 'middle',
|
|
731
|
+
getAlignmentBaseline: 'top'
|
|
732
|
+
})
|
|
733
|
+
)
|
|
734
|
+
];
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private _createScrubberLayers(
|
|
738
|
+
scrubberLine: ScrubberLineData[],
|
|
739
|
+
scrubberHandle: ScrubberHandleData[],
|
|
740
|
+
scrubberLabel: ScrubberLabelData[]
|
|
741
|
+
): Layer[] {
|
|
742
|
+
const {
|
|
743
|
+
scrubberLineProps,
|
|
744
|
+
showScrubber = true,
|
|
745
|
+
onScrubberDragStart,
|
|
746
|
+
onScrubberHover
|
|
747
|
+
} = this.props;
|
|
748
|
+
if (!showScrubber) return [];
|
|
749
|
+
|
|
750
|
+
return [
|
|
751
|
+
new LineLayer(
|
|
752
|
+
this.getSubLayerProps({
|
|
753
|
+
...scrubberLineProps,
|
|
754
|
+
id: 'scrubber-line',
|
|
755
|
+
data: scrubberLine,
|
|
756
|
+
getSourcePosition: (d: ScrubberLineData) => d.sourcePosition,
|
|
757
|
+
getTargetPosition: (d: ScrubberLineData) => d.targetPosition,
|
|
758
|
+
getColor: [255, 100, 100, 255],
|
|
759
|
+
getWidth: 2
|
|
760
|
+
})
|
|
761
|
+
),
|
|
762
|
+
new SolidPolygonLayer(
|
|
763
|
+
this.getSubLayerProps({
|
|
764
|
+
id: 'scrubber-handle',
|
|
765
|
+
data: scrubberHandle,
|
|
766
|
+
getPolygon: (d: ScrubberHandleData) => d.polygon,
|
|
767
|
+
getFillColor: (d: ScrubberHandleData) => d.color,
|
|
768
|
+
stroked: true,
|
|
769
|
+
getLineColor: [255, 255, 255, 255],
|
|
770
|
+
getLineWidth: 2,
|
|
771
|
+
pickable: true,
|
|
772
|
+
onClick: (info: PickingInfo) => {
|
|
773
|
+
if (onScrubberDragStart && info.object) {
|
|
774
|
+
onScrubberDragStart(info);
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
onHover: (info: PickingInfo) => {
|
|
778
|
+
if (onScrubberHover) {
|
|
779
|
+
onScrubberHover(Boolean(info.object), info);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
})
|
|
783
|
+
),
|
|
784
|
+
new TextLayer(
|
|
785
|
+
this.getSubLayerProps({
|
|
786
|
+
id: 'scrubber-label',
|
|
787
|
+
data: scrubberLabel,
|
|
788
|
+
getText: (d: ScrubberLabelData) => d.text,
|
|
789
|
+
getPosition: (d: ScrubberLabelData) => d.position,
|
|
790
|
+
getSize: 11,
|
|
791
|
+
getColor: [255, 100, 100, 255],
|
|
792
|
+
getTextAnchor: 'middle',
|
|
793
|
+
getAlignmentBaseline: 'bottom'
|
|
794
|
+
})
|
|
795
|
+
)
|
|
796
|
+
];
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ===== MAIN RENDER =====
|
|
800
|
+
|
|
801
|
+
renderLayers(): Layer[] {
|
|
802
|
+
const {
|
|
803
|
+
data: tracks,
|
|
804
|
+
timelineStart,
|
|
805
|
+
timelineEnd,
|
|
806
|
+
viewport,
|
|
807
|
+
y = 100,
|
|
808
|
+
trackHeight = 40,
|
|
809
|
+
trackSpacing = 10
|
|
810
|
+
} = this.props;
|
|
811
|
+
|
|
812
|
+
const effectiveStartMs = viewport?.startMs ?? timelineStart;
|
|
813
|
+
const effectiveEndMs = viewport?.endMs ?? timelineEnd;
|
|
814
|
+
|
|
815
|
+
const visibleTracks = tracks.filter((track) => track.visible !== false);
|
|
816
|
+
|
|
817
|
+
const tracksWithSubtracks: TrackWithSubtracks[] = visibleTracks.map((track, trackIndex) => {
|
|
818
|
+
const clips = track.clips || [];
|
|
819
|
+
const clipsWithSubtracks = assignClipsToSubtracks(clips);
|
|
820
|
+
const subtrackCount = Math.max(1, calculateSubtrackCount(clips));
|
|
821
|
+
return {track, trackIndex, clips: clipsWithSubtracks, subtrackCount};
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const {trackPositions, totalTimelineHeight} = this._calculateTrackPositions(
|
|
825
|
+
tracksWithSubtracks,
|
|
826
|
+
y,
|
|
827
|
+
trackHeight,
|
|
828
|
+
trackSpacing
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
const trackBackgrounds = this._generateTrackBackgrounds(tracksWithSubtracks, trackPositions);
|
|
832
|
+
const trackLabels = this._generateTrackLabels(tracksWithSubtracks, trackPositions);
|
|
833
|
+
const clipPolygons = this._generateClipPolygons(
|
|
834
|
+
tracksWithSubtracks,
|
|
835
|
+
trackPositions,
|
|
836
|
+
effectiveStartMs,
|
|
837
|
+
effectiveEndMs
|
|
838
|
+
);
|
|
839
|
+
const clipLabelsData = clipPolygons.map((clip) => ({
|
|
840
|
+
text: clip.label,
|
|
841
|
+
position: clip.labelPosition
|
|
842
|
+
}));
|
|
843
|
+
const subtrackSeparators = this._generateSubtrackSeparators(
|
|
844
|
+
tracksWithSubtracks,
|
|
845
|
+
trackPositions
|
|
846
|
+
);
|
|
847
|
+
const {axisLines, axisLabels} = this._generateAxis(
|
|
848
|
+
totalTimelineHeight,
|
|
849
|
+
effectiveStartMs,
|
|
850
|
+
effectiveEndMs
|
|
851
|
+
);
|
|
852
|
+
const {scrubberLine, scrubberHandle, scrubberLabel} = this._generateScrubber(
|
|
853
|
+
totalTimelineHeight,
|
|
854
|
+
effectiveStartMs,
|
|
855
|
+
effectiveEndMs
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
const layers = [
|
|
859
|
+
...this._createTrackLayers(trackBackgrounds, trackLabels),
|
|
860
|
+
...this._createClipLayers(clipPolygons, clipLabelsData),
|
|
861
|
+
this._createSubtrackSeparatorLayer(subtrackSeparators),
|
|
862
|
+
...this._createAxisLayers(axisLines, axisLabels),
|
|
863
|
+
...this._createScrubberLayers(scrubberLine, scrubberHandle, scrubberLabel)
|
|
864
|
+
].filter((layer): layer is Layer => layer !== null);
|
|
865
|
+
|
|
866
|
+
return layers;
|
|
867
|
+
}
|
|
868
|
+
}
|