@hyperframes/studio 0.8.139 → 0.8.141
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-5Vx2AiUz.js +473 -0
- package/dist/assets/{index-DsEkE5_G.js → index-BAnh7cSQ.js} +1 -1
- package/dist/assets/{index-Br1Whc0f.js → index-C0thd4eM.js} +204 -204
- package/dist/assets/{index-BmIPrb8I.js → index-P8fgj_Xa.js} +1 -1
- package/dist/{chunk-E422JDF5.js → chunk-H2DUOXTF.js} +2 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.html +1 -1
- package/dist/index.js +6518 -5813
- package/dist/index.js.map +1 -1
- package/dist/player.js +1 -1
- package/package.json +8 -8
- package/src/components/TimelineToolbar.test.tsx +79 -1
- package/src/components/TimelineToolbar.tsx +14 -29
- package/src/components/editor/DomEditOverlay.tsx +2 -2
- package/src/components/editor/PropertyPanel.tsx +1 -0
- package/src/components/editor/PropertyPanelFlat.tsx +1 -0
- package/src/components/editor/manualEdits.ts +1 -1
- package/src/components/editor/manualEditsParsing.ts +14 -2
- package/src/components/editor/previewReadOnly.overlay.test.tsx +3 -2
- package/src/components/editor/propertyPanelFill.tsx +36 -13
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +4 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +13 -7
- package/src/components/editor/propertyPanelImageFill.test.tsx +164 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +10 -7
- package/src/components/nle/NLEPreview.test.ts +60 -1
- package/src/components/nle/NLEPreview.tsx +9 -2
- package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +43 -0
- package/src/components/sidebar/CompositionsTab.tsx +17 -0
- package/src/hooks/externalFileChangeOwnership.test.ts +25 -4
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +3 -22
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +536 -10
- package/src/hooks/useExternalFileChangeCoordinator.ts +72 -45
- package/src/hooks/useMusicBeatAnalysis.ts +2 -1
- package/src/hooks/useSdkSession.lifecycle.test.tsx +20 -0
- package/src/hooks/useSdkSession.ts +4 -1
- package/src/hooks/useThumbnailStripSize.test.tsx +65 -0
- package/src/hooks/useThumbnailStripSize.ts +23 -1
- package/src/index.ts +1 -0
- package/src/player/components/AudioWaveform.test.tsx +153 -1
- package/src/player/components/AudioWaveform.tsx +93 -37
- package/src/player/components/Timeline.scrollRest.test.tsx +22 -0
- package/src/player/components/Timeline.test.ts +3 -1
- package/src/player/components/TimelineCanvas.tsx +4 -0
- package/src/player/components/TimelineClip.tsx +20 -6
- package/src/player/components/TimelineClipFades.test.tsx +11 -0
- package/src/player/components/TimelineClipFades.tsx +4 -4
- package/src/player/components/TimelineClipJoins.tsx +23 -18
- package/src/player/components/TimelineGestureOverlay.tsx +30 -27
- package/src/player/components/TimelineGroupRow.tsx +1 -0
- package/src/player/components/TimelineLaneClip.tsx +127 -0
- package/src/player/components/TimelineLanes.test.tsx +51 -6
- package/src/player/components/TimelineLanes.tsx +166 -178
- package/src/player/components/TimelineRuler.tsx +2 -0
- package/src/player/components/TimelineTimeLayer.tsx +25 -0
- package/src/player/components/VideoThumbnail.test.tsx +204 -12
- package/src/player/components/VideoThumbnail.tsx +37 -9
- package/src/player/components/thumbnailUtils.test.ts +17 -3
- package/src/player/components/thumbnailUtils.ts +17 -1
- package/src/player/components/timelineLayout.test.ts +3 -3
- package/src/player/components/timelineMotion.test.ts +27 -0
- package/src/player/components/timelineMotion.ts +62 -0
- package/src/player/components/timelineZoomInput.test.ts +438 -0
- package/src/player/components/timelineZoomInput.ts +456 -0
- package/src/player/components/useTimelineGeometry.ts +0 -3
- package/src/player/components/useTimelinePlayhead.test.tsx +97 -30
- package/src/player/components/useTimelinePlayhead.ts +53 -59
- package/src/player/components/useTimelineProviderState.tsx +16 -21
- package/src/player/components/useTimelineZoom.ts +7 -12
- package/src/player/lib/runtimeProtocol.ts +6 -0
- package/src/player/lib/thumbnailVideoDecoder.test.ts +230 -30
- package/src/player/lib/thumbnailVideoDecoder.ts +197 -54
- package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
- package/src/player/lib/timelineViewportBudgets.ts +1 -1
- package/src/utils/gsapUndoRestore.browser.test.ts +4 -17
- package/dist/assets/hyperframes-player-CckOoCJ8.js +0 -473
- /package/dist/{chunk-E422JDF5.js.map → chunk-H2DUOXTF.js.map} +0 -0
|
@@ -89,8 +89,11 @@ interface RenderLanesOptions {
|
|
|
89
89
|
renderClipContent?: React.ComponentProps<typeof TimelineLanes>["renderClipContent"];
|
|
90
90
|
snapGuide?: { time: number; type: "beat" | "clip-edge" | "playhead" } | null;
|
|
91
91
|
withoutSelectHandler?: boolean;
|
|
92
|
+
pps?: number;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
const lanePps = (options: RenderLanesOptions) => options.pps ?? 100;
|
|
96
|
+
|
|
94
97
|
const selectHandlerFor = (
|
|
95
98
|
options: RenderLanesOptions,
|
|
96
99
|
handler: (element: TimelineElement | null) => void,
|
|
@@ -102,12 +105,14 @@ function renderLanes(options: RenderLanesOptions = {}): {
|
|
|
102
105
|
rerender: (next: RenderLanesOptions) => void;
|
|
103
106
|
setSelectedElementId: ReturnType<typeof vi.fn>;
|
|
104
107
|
onSelectElement: ReturnType<typeof vi.fn>;
|
|
108
|
+
onRazorSplit: ReturnType<typeof vi.fn>;
|
|
105
109
|
} {
|
|
106
110
|
const host = document.createElement("div");
|
|
107
111
|
document.body.append(host);
|
|
108
112
|
const root = createRoot(host);
|
|
109
113
|
const setSelectedElementId = vi.fn();
|
|
110
114
|
const onSelectElement = vi.fn();
|
|
115
|
+
const onRazorSplit = vi.fn();
|
|
111
116
|
const render = (next: RenderLanesOptions) => {
|
|
112
117
|
const elements = next.elements ?? [element("clip-a", TRACK_A)];
|
|
113
118
|
const gsapAnimations = next.animations ?? new Map<string, GsapAnimation[]>();
|
|
@@ -124,7 +129,7 @@ function renderLanes(options: RenderLanesOptions = {}): {
|
|
|
124
129
|
usePlayerStore.setState({ expandedClipIds: new Set(next.expandedClipIds ?? []) });
|
|
125
130
|
root.render(
|
|
126
131
|
<TimelineLanes
|
|
127
|
-
pps={
|
|
132
|
+
pps={lanePps(next)}
|
|
128
133
|
contentOrigin={232}
|
|
129
134
|
contentGutter={32}
|
|
130
135
|
trackContentWidth={800}
|
|
@@ -186,14 +191,14 @@ function renderLanes(options: RenderLanesOptions = {}): {
|
|
|
186
191
|
onResizeElement={vi.fn()}
|
|
187
192
|
onMoveElement={vi.fn()}
|
|
188
193
|
onSelectElement={selectHandlerFor(next, onSelectElement)}
|
|
189
|
-
onRazorSplit={
|
|
194
|
+
onRazorSplit={onRazorSplit}
|
|
190
195
|
onRazorSplitAll={vi.fn()}
|
|
191
196
|
/>,
|
|
192
197
|
);
|
|
193
198
|
});
|
|
194
199
|
};
|
|
195
200
|
render(options);
|
|
196
|
-
return { host, root, rerender: render, setSelectedElementId, onSelectElement };
|
|
201
|
+
return { host, root, rerender: render, setSelectedElementId, onSelectElement, onRazorSplit };
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
function visibilityLabels(host: HTMLElement): (string | null)[] {
|
|
@@ -586,12 +591,18 @@ describe("TimelineLanes clip joins", () => {
|
|
|
586
591
|
|
|
587
592
|
const joins = view.host.querySelectorAll<HTMLElement>("[data-timeline-clip-join]");
|
|
588
593
|
expect(joins).toHaveLength(1);
|
|
589
|
-
|
|
594
|
+
// Both sit in one-second layers 100px wide: 200% is 200px, at the 2 s join.
|
|
595
|
+
const layerWidth = (el: Element | null | undefined) =>
|
|
596
|
+
(el?.closest("[data-timeline-time-layer]") as HTMLElement | null)?.style.width;
|
|
597
|
+
expect(joins[0]?.style.left).toBe("200%");
|
|
598
|
+
expect(layerWidth(joins[0])).toBe("100px");
|
|
590
599
|
expect(joins[0]?.style.width).toBe("1px");
|
|
591
600
|
expect(joins[0]?.style.background).toBe(defaultTimelineTheme.rowBackground);
|
|
592
601
|
const clipB = view.host.querySelector<HTMLElement>('[data-el-id="clip-b"]');
|
|
593
|
-
expect(clipB?.style.left).toBe("
|
|
594
|
-
expect(clipB?.style.width).toBe("
|
|
602
|
+
expect(clipB?.style.left).toBe("200%");
|
|
603
|
+
expect(clipB?.style.width).toBe("150%");
|
|
604
|
+
expect(clipB?.style.minWidth).toBe("4px");
|
|
605
|
+
expect(layerWidth(clipB)).toBe("100px");
|
|
595
606
|
act(() => view.root.unmount());
|
|
596
607
|
});
|
|
597
608
|
|
|
@@ -624,3 +635,37 @@ describe("TimelineLanes transition seams", () => {
|
|
|
624
635
|
act(() => view.root.unmount());
|
|
625
636
|
});
|
|
626
637
|
});
|
|
638
|
+
|
|
639
|
+
describe("TimelineLanes clips during a zoom", () => {
|
|
640
|
+
afterEach(() => {
|
|
641
|
+
usePlayerStore.setState({ activeTool: "select" });
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("leaves a clip unrendered by a zoom step that does not change what it draws", () => {
|
|
645
|
+
const renderClipContent = vi.fn(() => null);
|
|
646
|
+
const clip = { ...element("clip-a", TRACK_A), start: 0, duration: 2 };
|
|
647
|
+
const view = renderLanes({ elements: [clip], renderClipContent, pps: 100 });
|
|
648
|
+
const drawn = renderClipContent.mock.calls.length;
|
|
649
|
+
// 200px to 220px: still a labelled clip with handles.
|
|
650
|
+
view.rerender({ elements: [clip], renderClipContent, pps: 110 });
|
|
651
|
+
expect(renderClipContent.mock.calls.length).toBe(drawn);
|
|
652
|
+
// 40px: the clip drops its label, so it draws again.
|
|
653
|
+
view.rerender({ elements: [clip], renderClipContent, pps: 20 });
|
|
654
|
+
expect(renderClipContent.mock.calls.length).toBeGreaterThan(drawn);
|
|
655
|
+
act(() => view.root.unmount());
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("splits with the razor at the time under the pointer at the current zoom", () => {
|
|
659
|
+
usePlayerStore.setState({ activeTool: "razor" });
|
|
660
|
+
const clip = { ...element("clip-a", TRACK_A), start: 0, duration: 2 };
|
|
661
|
+
const view = renderLanes({ elements: [clip], pps: 100 });
|
|
662
|
+
view.rerender({ elements: [clip], pps: 105 });
|
|
663
|
+
act(() => {
|
|
664
|
+
view.host
|
|
665
|
+
.querySelector('[data-el-id="clip-a"]')
|
|
666
|
+
?.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX: 105 }));
|
|
667
|
+
});
|
|
668
|
+
expect(view.onRazorSplit).toHaveBeenCalledWith(expect.objectContaining({ id: "clip-a" }), 1);
|
|
669
|
+
act(() => view.root.unmount());
|
|
670
|
+
});
|
|
671
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Fragment, useId, useMemo, type CSSProperties } from "react";
|
|
1
|
+
import { Fragment, useId, useMemo, useRef, type CSSProperties } from "react";
|
|
2
2
|
import { BeatStrip, BeatBackgroundLines } from "./BeatStrip";
|
|
3
|
-
import {
|
|
3
|
+
import { TimelineLaneClip, type TimelineLaneClipActions } from "./TimelineLaneClip";
|
|
4
4
|
import { TimelineCompactDiamonds } from "./TimelineCompactDiamonds";
|
|
5
5
|
import { TimelinePropertyLanes } from "./TimelinePropertyLanes";
|
|
6
6
|
import { TimelineAutomationLaneSlot } from "./TimelineAutomationLaneSlot";
|
|
@@ -10,6 +10,8 @@ import { TimelineTrackHeader } from "./TimelineTrackHeader";
|
|
|
10
10
|
import { TimelineGroupRow } from "./TimelineGroupRow";
|
|
11
11
|
import { useTimelineLaneRowIndexes, useTimelineGroupDisclosure } from "./useTimelineLaneRowIndexes";
|
|
12
12
|
import { useTimelineClipDisclosure } from "./useTimelineClipDisclosure";
|
|
13
|
+
import { TimelineTimeLayer } from "./TimelineTimeLayer";
|
|
14
|
+
import { clipWidthPx } from "./TimelineClip";
|
|
13
15
|
import {
|
|
14
16
|
isTrackRowExpanded,
|
|
15
17
|
resolveTrackKeyframeClip,
|
|
@@ -24,8 +26,7 @@ import { isMultiDragPassenger, multiDragPassengerOffsetPx } from "./timelineMult
|
|
|
24
26
|
import { useTimelineMultiDragActorWindows } from "./useTimelineMultiDragActorWindows";
|
|
25
27
|
import type { TimelineLanesProps } from "./timelineLaneProps";
|
|
26
28
|
import { isAudioTimelineElement, isMusicTrack } from "../../utils/timelineInspector";
|
|
27
|
-
import {
|
|
28
|
-
import { renderClipChildren, resolveClipRenderContext } from "./timelineClipChildren";
|
|
29
|
+
import { resolveClipRenderContext } from "./timelineClipChildren";
|
|
29
30
|
import { TimelineTrackRow } from "./TimelineTrackRow";
|
|
30
31
|
import { isTimelineClipActive } from "./useTimelineActiveClips";
|
|
31
32
|
import { queryTimelineClipIndex } from "../lib/timelineClipIndex";
|
|
@@ -143,6 +144,28 @@ export function TimelineLanes({
|
|
|
143
144
|
onToggleRow: (row) => row.elementId && toggleClipExpandedTracked(row.elementId),
|
|
144
145
|
onDrillDown,
|
|
145
146
|
});
|
|
147
|
+
const clipActions = useRef<TimelineLaneClipActions>(null!);
|
|
148
|
+
clipActions.current = {
|
|
149
|
+
gestures: {
|
|
150
|
+
pps,
|
|
151
|
+
onResizeElement,
|
|
152
|
+
onMoveElement,
|
|
153
|
+
onRazorSplit,
|
|
154
|
+
onRazorSplitAll,
|
|
155
|
+
blockedClipRef,
|
|
156
|
+
suppressClickRef,
|
|
157
|
+
scrollRef,
|
|
158
|
+
setShowPopover,
|
|
159
|
+
setRangeSelection,
|
|
160
|
+
setResizingClip,
|
|
161
|
+
setDraggedClip,
|
|
162
|
+
setSelectedElementId,
|
|
163
|
+
onSelectElement,
|
|
164
|
+
},
|
|
165
|
+
setHoveredClip,
|
|
166
|
+
onContextMenuClip,
|
|
167
|
+
onDrillDown,
|
|
168
|
+
};
|
|
146
169
|
return (
|
|
147
170
|
<div
|
|
148
171
|
role="treegrid"
|
|
@@ -330,6 +353,7 @@ export function TimelineLanes({
|
|
|
330
353
|
<div
|
|
331
354
|
role="gridcell"
|
|
332
355
|
aria-colindex={2}
|
|
356
|
+
data-timeline-zoom-scale=""
|
|
333
357
|
style={{
|
|
334
358
|
width: trackContentWidth,
|
|
335
359
|
marginLeft: contentGutter, // room for a 0% diamond left of t=0
|
|
@@ -368,181 +392,145 @@ export function TimelineLanes({
|
|
|
368
392
|
renderTimeRange={rowsVirtualized ? renderTimeRange : undefined}
|
|
369
393
|
/>
|
|
370
394
|
)}
|
|
371
|
-
{
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
<TimelinePropertyLanes
|
|
498
|
-
key={`${clipKey}-property-lanes`}
|
|
499
|
-
id={lanesId}
|
|
500
|
-
animations={showsLanes ? (gsapAnimations.get(elementKey) ?? []) : []}
|
|
501
|
-
// clipTimingStart, not the raw start: an expanded sub-comp
|
|
502
|
-
// child's start is host-absolute while its tweens are
|
|
503
|
-
// local to its own file.
|
|
504
|
-
clipStart={clipTimingStart(previewElement)}
|
|
505
|
-
clipDuration={previewElement.duration}
|
|
506
|
-
clipLeftPx={previewElement.start * pps}
|
|
507
|
-
clipWidthPx={Math.max(previewElement.duration * pps, 4)}
|
|
508
|
-
passengerStyle={passengerStyle}
|
|
509
|
-
accentColor={clipStyle.accent}
|
|
510
|
-
isSelected={isSelected}
|
|
511
|
-
currentPercentage={
|
|
512
|
-
previewElement.duration > 0
|
|
513
|
-
? ((currentTime - previewElement.start) / previewElement.duration) * 100
|
|
514
|
-
: 0
|
|
515
|
-
}
|
|
516
|
-
elementId={elementKey}
|
|
517
|
-
selectedKeyframes={selectedKeyframes}
|
|
518
|
-
rovingTargetId={keyboard.rovingTargetId}
|
|
519
|
-
onSelectSegment={(target) => onSelectSegment?.(elementKey, target)}
|
|
520
|
-
onClickKeyframe={(target) => onClickKeyframe?.(previewElement, target)}
|
|
521
|
-
onShiftClickKeyframe={(target) =>
|
|
522
|
-
onShiftClickKeyframe?.(elementKey, target)
|
|
523
|
-
}
|
|
524
|
-
onContextMenuKeyframe={(e, target) =>
|
|
525
|
-
onContextMenuKeyframe?.(e, elementKey, target)
|
|
526
|
-
}
|
|
527
|
-
onMoveKeyframe={(target, toClipPercentage) =>
|
|
528
|
-
onMoveKeyframe?.(elementKey, target, toClipPercentage) ??
|
|
529
|
-
Promise.resolve(false)
|
|
530
|
-
}
|
|
531
|
-
suppressClickRef={suppressClickRef}
|
|
532
|
-
/>
|
|
533
|
-
);
|
|
395
|
+
<TimelineTimeLayer pixelsPerSecond={pps}>
|
|
396
|
+
{
|
|
397
|
+
// fallow-ignore-next-line complexity
|
|
398
|
+
renderElements.map((el) => {
|
|
399
|
+
const clipStyle = getTrackStyle(el.tag);
|
|
400
|
+
const elementKey = getTimelineElementIdentity(el);
|
|
401
|
+
// Only the track's active keyframe clip shows expanded lanes;
|
|
402
|
+
// other clips (incl. siblings on a shared track) show compact
|
|
403
|
+
// diamonds on their own bar instead.
|
|
404
|
+
const isTrackKeyframeClip = elementKey === keyframeClipKey;
|
|
405
|
+
const showsLanes = isTrackKeyframeClip && rowExpanded;
|
|
406
|
+
const capabilities = getClipCapabilities(el);
|
|
407
|
+
const isSelected =
|
|
408
|
+
selectedElementId === elementKey || selectedElementIds.has(elementKey);
|
|
409
|
+
// Element identity stays stable across clip splices and reorders.
|
|
410
|
+
const clipKey = elementKey;
|
|
411
|
+
const isDraggingClip =
|
|
412
|
+
draggedClip?.started === true &&
|
|
413
|
+
draggedElement != null &&
|
|
414
|
+
getTimelineElementIdentity(draggedElement) === elementKey;
|
|
415
|
+
if (isDraggingClip) return null;
|
|
416
|
+
const previewElement = getPreviewElement(el);
|
|
417
|
+
const renderContext = resolveClipRenderContext(
|
|
418
|
+
previewElement,
|
|
419
|
+
visibleTimeRange,
|
|
420
|
+
isSelected || hoveredClip === clipKey || pinnedClipIdentities.has(clipKey),
|
|
421
|
+
);
|
|
422
|
+
// Passenger of a live multi-drag: preserve the formation without changing
|
|
423
|
+
// the passenger's timeline data until the owning drag commits.
|
|
424
|
+
const isPassenger =
|
|
425
|
+
multiDragPreview != null && isMultiDragPassenger(clipKey, multiDragPreview);
|
|
426
|
+
const passengerStyle = isPassenger
|
|
427
|
+
? passengerStyleAt(
|
|
428
|
+
multiDragPassengerOffsetPx(clipKey, pps, multiDragPreview),
|
|
429
|
+
)
|
|
430
|
+
: undefined;
|
|
431
|
+
const clip = (
|
|
432
|
+
<TimelineLaneClip
|
|
433
|
+
key={clipKey}
|
|
434
|
+
el={el}
|
|
435
|
+
previewElement={previewElement}
|
|
436
|
+
elementKey={elementKey}
|
|
437
|
+
capabilities={capabilities}
|
|
438
|
+
pps={pps}
|
|
439
|
+
passengerStyle={passengerStyle}
|
|
440
|
+
clipBarHeight={clipBarHeight}
|
|
441
|
+
isSelected={isSelected}
|
|
442
|
+
isHovered={hoveredClip === clipKey}
|
|
443
|
+
isActive={isTimelineClipActive(previewElement, currentTime)}
|
|
444
|
+
tabIndex={
|
|
445
|
+
keyboard.rovingTargetId === timelineClipFocusId(elementKey) ? 0 : -1
|
|
446
|
+
}
|
|
447
|
+
priority={renderContext.priority}
|
|
448
|
+
rich={renderContext.rich}
|
|
449
|
+
theme={theme}
|
|
450
|
+
clipStyle={clipStyle}
|
|
451
|
+
renderClipContent={renderClipContent}
|
|
452
|
+
renderClipOverlay={renderClipOverlay}
|
|
453
|
+
actions={clipActions}
|
|
454
|
+
/>
|
|
455
|
+
);
|
|
456
|
+
const compactKeyframes = keyframeCache?.get(elementKey);
|
|
457
|
+
const compactDiamonds = !showsLanes && compactKeyframes && (
|
|
458
|
+
<TimelineCompactDiamonds
|
|
459
|
+
key={`${clipKey}-diamonds`}
|
|
460
|
+
element={previewElement}
|
|
461
|
+
elementId={elementKey}
|
|
462
|
+
keyframesData={compactKeyframes}
|
|
463
|
+
pixelsPerSecond={pps}
|
|
464
|
+
rowHeight={rowHeight}
|
|
465
|
+
beatsActive={beatStripOnTrack}
|
|
466
|
+
accentColor={clipStyle.accent}
|
|
467
|
+
isSelected={isSelected}
|
|
468
|
+
passengerStyle={passengerStyle}
|
|
469
|
+
currentTime={currentTime}
|
|
470
|
+
selectedKeyframes={selectedKeyframes}
|
|
471
|
+
rovingTargetId={keyboard.rovingTargetId}
|
|
472
|
+
onClickKeyframe={onClickKeyframe}
|
|
473
|
+
onShiftClickKeyframe={onShiftClickKeyframe}
|
|
474
|
+
onContextMenuKeyframe={onContextMenuKeyframe}
|
|
475
|
+
onMoveKeyframe={onMoveKeyframe}
|
|
476
|
+
onSelectSegment={onSelectSegment}
|
|
477
|
+
suppressClickRef={suppressClickRef}
|
|
478
|
+
/>
|
|
479
|
+
);
|
|
480
|
+
// Keep this shell mounted while collapsed so aria-controls stays valid
|
|
481
|
+
// and multi-drag cannot remount the subtree mid-gesture.
|
|
482
|
+
const propertyLanes = isTrackKeyframeClip && (
|
|
483
|
+
<TimelinePropertyLanes
|
|
484
|
+
key={`${clipKey}-property-lanes`}
|
|
485
|
+
id={lanesId}
|
|
486
|
+
animations={showsLanes ? (gsapAnimations.get(elementKey) ?? []) : []}
|
|
487
|
+
// clipTimingStart, not the raw start: an expanded sub-comp
|
|
488
|
+
// child's start is host-absolute while its tweens are
|
|
489
|
+
// local to its own file.
|
|
490
|
+
clipStart={clipTimingStart(previewElement)}
|
|
491
|
+
clipDuration={previewElement.duration}
|
|
492
|
+
clipLeftPx={previewElement.start * pps}
|
|
493
|
+
clipWidthPx={clipWidthPx(previewElement, pps)}
|
|
494
|
+
passengerStyle={passengerStyle}
|
|
495
|
+
accentColor={clipStyle.accent}
|
|
496
|
+
isSelected={isSelected}
|
|
497
|
+
currentPercentage={
|
|
498
|
+
previewElement.duration > 0
|
|
499
|
+
? ((currentTime - previewElement.start) / previewElement.duration) *
|
|
500
|
+
100
|
|
501
|
+
: 0
|
|
502
|
+
}
|
|
503
|
+
elementId={elementKey}
|
|
504
|
+
selectedKeyframes={selectedKeyframes}
|
|
505
|
+
rovingTargetId={keyboard.rovingTargetId}
|
|
506
|
+
onSelectSegment={(target) => onSelectSegment?.(elementKey, target)}
|
|
507
|
+
onClickKeyframe={(target) => onClickKeyframe?.(previewElement, target)}
|
|
508
|
+
onShiftClickKeyframe={(target) =>
|
|
509
|
+
onShiftClickKeyframe?.(elementKey, target)
|
|
510
|
+
}
|
|
511
|
+
onContextMenuKeyframe={(e, target) =>
|
|
512
|
+
onContextMenuKeyframe?.(e, elementKey, target)
|
|
513
|
+
}
|
|
514
|
+
onMoveKeyframe={(target, toClipPercentage) =>
|
|
515
|
+
onMoveKeyframe?.(elementKey, target, toClipPercentage) ??
|
|
516
|
+
Promise.resolve(false)
|
|
517
|
+
}
|
|
518
|
+
suppressClickRef={suppressClickRef}
|
|
519
|
+
/>
|
|
520
|
+
);
|
|
534
521
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
522
|
+
// No wrapper node per clip, and the same Fragment whether or not it rides a
|
|
523
|
+
// drag, so joining or leaving one restyles it, never remounts it.
|
|
524
|
+
return (
|
|
525
|
+
<Fragment key={clipKey}>
|
|
526
|
+
{clip}
|
|
527
|
+
{compactDiamonds}
|
|
528
|
+
{propertyLanes}
|
|
529
|
+
</Fragment>
|
|
530
|
+
);
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
</TimelineTimeLayer>
|
|
546
534
|
<TimelineTransitionOverlays
|
|
547
535
|
seams={transitionSeamsByTrack.get(trackNum) ?? []}
|
|
548
536
|
rowElements={draggedClip?.started ? [] : automationElements}
|
|
@@ -107,6 +107,7 @@ export const TimelineRuler = memo(function TimelineRuler({
|
|
|
107
107
|
{/* Background SVG — beat lines only; major-tick gridlines removed so only
|
|
108
108
|
the ruler's own small ticks mark intervals (no full-height lines). */}
|
|
109
109
|
<svg
|
|
110
|
+
data-timeline-zoom-scale=""
|
|
110
111
|
className="absolute pointer-events-none"
|
|
111
112
|
style={{ left: contentOrigin, width: trackContentWidth, zIndex: 0 }}
|
|
112
113
|
height={totalH}
|
|
@@ -152,6 +153,7 @@ export const TimelineRuler = memo(function TimelineRuler({
|
|
|
152
153
|
{/* Breathing pad before 00:00 is folded into contentOrigin (see
|
|
153
154
|
Timeline.tsx: GUTTER + TRACKS_LEFT_PAD), so no separate pad div. */}
|
|
154
155
|
<div
|
|
156
|
+
data-timeline-zoom-scale=""
|
|
155
157
|
className="relative overflow-hidden"
|
|
156
158
|
style={{
|
|
157
159
|
height: RULER_H,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A box one second wide at the current zoom. What sits in it places itself in percent of it
|
|
5
|
+
* (`left: start * 100%`), so a zoom step rewrites this one width and no clip's style changes.
|
|
6
|
+
*/
|
|
7
|
+
export function TimelineTimeLayer({
|
|
8
|
+
pixelsPerSecond,
|
|
9
|
+
children,
|
|
10
|
+
}: {
|
|
11
|
+
pixelsPerSecond: number;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
data-timeline-time-layer=""
|
|
17
|
+
className="absolute inset-y-0 left-0"
|
|
18
|
+
style={{ width: pixelsPerSecond }}
|
|
19
|
+
>
|
|
20
|
+
{children}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const timeLayerPercent = (seconds: number) => `${seconds * 100}%`;
|