@hyperframes/studio 0.7.56 → 0.7.58
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-CtTDO63S.js +459 -0
- package/dist/assets/{index-CmVCjZjp.js → index-B_UvTX3E.js} +205 -205
- package/dist/assets/{index-BWnOxAiH.js → index-C47jAC3Q.js} +1 -1
- package/dist/assets/{index-C4csZims.js → index-DeQPzqwH.js} +1 -1
- package/dist/assets/index-uahwWkgw.css +1 -0
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +41 -4
- package/dist/index.html +2 -2
- package/dist/index.js +7436 -5423
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +3 -3
- package/src/components/StudioFeedbackBar.tsx +2 -2
- package/src/components/editor/CanvasContextMenu.test.tsx +166 -2
- package/src/components/editor/CanvasContextMenu.tsx +100 -20
- package/src/components/editor/DomEditOverlay.tsx +34 -62
- package/src/components/editor/LayersPanel.tsx +111 -91
- package/src/components/editor/canvasContextMenuZOrder.test.ts +243 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +184 -23
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/layersPanelSort.ts +80 -0
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +61 -37
- package/src/components/nle/PreviewOverlays.tsx +61 -16
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +19 -2
- package/src/components/sidebar/AudioRow.tsx +19 -2
- package/src/hooks/domEditCommitTypes.ts +29 -2
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/timelineEditingHelpers.test.ts +183 -1
- package/src/hooks/timelineEditingHelpers.ts +63 -259
- package/src/hooks/timelineMoveAdapter.test.ts +44 -40
- package/src/hooks/timelineMoveAdapter.ts +7 -4
- package/src/hooks/timelineTimingSync.test.ts +820 -0
- package/src/hooks/timelineTimingSync.ts +597 -0
- package/src/hooks/timelineTrackVisibility.test.ts +3 -1
- package/src/hooks/timelineTrackVisibility.ts +4 -3
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useDomEditCommits.test.tsx +392 -23
- package/src/hooks/useDomEditCommits.ts +180 -53
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +1 -1
- package/src/hooks/useElementLifecycleOps.test.tsx +559 -28
- package/src/hooks/useElementLifecycleOps.ts +175 -69
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/usePreviewPersistence.ts +2 -2
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +591 -277
- package/src/hooks/useTimelineEditing.ts +202 -282
- package/src/hooks/useTimelineEditingTypes.ts +1 -1
- package/src/hooks/useTimelineGroupEditing.ts +187 -134
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +7 -2
- package/src/player/components/Timeline.test.ts +105 -18
- package/src/player/components/Timeline.tsx +69 -98
- package/src/player/components/TimelineCanvas.tsx +47 -7
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +70 -47
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +370 -11
- package/src/player/components/timelineClipDragCommit.ts +267 -68
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +119 -30
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +102 -1
- package/src/player/components/timelineStackingSync.ts +45 -5
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +11 -2
- package/src/player/components/useTimelineStackingSync.ts +17 -4
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +19 -1
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +4 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +11 -1
- package/src/player/index.ts +3 -1
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +12 -0
- package/src/player/lib/timelineElementHelpers.ts +54 -11
- package/src/player/lib/timelineIframeHelpers.ts +2 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +49 -1
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -0
- package/src/utils/sourceScopedSelectorIndex.ts +29 -0
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +62 -86
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-D-GyYi2d.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -19,10 +19,13 @@ import {
|
|
|
19
19
|
shouldAutoScrollTimeline,
|
|
20
20
|
} from "./Timeline";
|
|
21
21
|
import {
|
|
22
|
+
FIT_ZOOM_HEADROOM,
|
|
22
23
|
GUTTER,
|
|
23
24
|
MIN_TIMELINE_EXTENT_S,
|
|
25
|
+
PLAYHEAD_HEAD_W,
|
|
24
26
|
RULER_H,
|
|
25
27
|
TRACK_H,
|
|
28
|
+
TRACKS_LEFT_PAD,
|
|
26
29
|
getTimelineDisplayContentWidth,
|
|
27
30
|
getTimelineFitPps,
|
|
28
31
|
} from "./timelineLayout";
|
|
@@ -150,7 +153,8 @@ describe("Timeline provider boundary", () => {
|
|
|
150
153
|
});
|
|
151
154
|
|
|
152
155
|
const row = button.parentElement?.parentElement;
|
|
153
|
-
|
|
156
|
+
// Row children: [sticky gutter, TRACKS_LEFT_PAD spacer, time-mapped content].
|
|
157
|
+
const trackContent = row?.children.item(2);
|
|
154
158
|
expect(onToggleTrackHidden).toHaveBeenCalledWith(0, false);
|
|
155
159
|
expect(trackContent).toBeInstanceOf(HTMLElement);
|
|
156
160
|
if (!(trackContent instanceof HTMLElement)) {
|
|
@@ -327,6 +331,60 @@ describe("generateTicks", () => {
|
|
|
327
331
|
const { major } = generateTicks(180, 80);
|
|
328
332
|
expect(major[1] - major[0]).toBe(2);
|
|
329
333
|
});
|
|
334
|
+
|
|
335
|
+
it("picks 'nice' NLE steps across zoom levels (no 7s-style intervals)", () => {
|
|
336
|
+
// step = first nice interval whose px spacing >= 88 at that pps.
|
|
337
|
+
const cases: Array<[number, number]> = [
|
|
338
|
+
[2, 60], // 60s * 2pps = 120px
|
|
339
|
+
[10, 10], // 10s * 10pps = 100px
|
|
340
|
+
[20, 5], // 5s * 20pps = 100px
|
|
341
|
+
[50, 2], // 2s * 50pps = 100px
|
|
342
|
+
[100, 1], // 1s * 100pps = 100px
|
|
343
|
+
];
|
|
344
|
+
for (const [pps, expected] of cases) {
|
|
345
|
+
const { major } = generateTicks(600, pps);
|
|
346
|
+
expect(major[1] - major[0]).toBe(expected);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("uses minute/hour steps when zoomed far out instead of colliding 10m labels", () => {
|
|
351
|
+
// 0.05 pps → 600s step would be 30px apart (labels collide); 1800s = 90px.
|
|
352
|
+
const { major } = generateTicks(7200, 0.05);
|
|
353
|
+
expect(major[1] - major[0]).toBe(1800);
|
|
354
|
+
expect(major).toContain(3600);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("does not drift on long rulers (ticks are exact multiples of the step)", () => {
|
|
358
|
+
const { major } = generateTicks(600, 100); // 1s step, 601 ticks
|
|
359
|
+
expect(major[599]).toBe(599);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe("frame display mode (frameRate provided)", () => {
|
|
363
|
+
it("snaps sub-frame steps up to one whole frame (no duplicate frame labels)", () => {
|
|
364
|
+
// 4400 pps would pick a 0.02s step = 0.6 frames at 30fps → snapped to 1 frame.
|
|
365
|
+
const { major } = generateTicks(2, 4400, 30);
|
|
366
|
+
const frames = major.map((t) => Math.round(t * 30));
|
|
367
|
+
// Frame labels are consecutive integers — no duplicates, no gaps.
|
|
368
|
+
frames.forEach((f, i) => expect(f).toBe(i));
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("keeps major AND minor ticks on whole frames", () => {
|
|
372
|
+
// 200 pps → 0.5s step (15 frames); quarters (3.75f) are rejected in
|
|
373
|
+
// frame mode in favour of fifths (3f).
|
|
374
|
+
const { major, minor } = generateTicks(20, 200, 30);
|
|
375
|
+
expect(major[1]).toBeCloseTo(0.5);
|
|
376
|
+
expect(minor).toContain(0.1); // 3 frames
|
|
377
|
+
for (const t of [...major, ...minor]) {
|
|
378
|
+
const frames = t * 30;
|
|
379
|
+
expect(Math.abs(frames - Math.round(frames))).toBeLessThan(1e-3);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("leaves whole-second steps unchanged", () => {
|
|
384
|
+
const { major } = generateTicks(60, 100, 30);
|
|
385
|
+
expect(major[1] - major[0]).toBe(1);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
330
388
|
});
|
|
331
389
|
|
|
332
390
|
describe("formatTime", () => {
|
|
@@ -397,33 +455,47 @@ describe("shouldAutoScrollTimeline", () => {
|
|
|
397
455
|
});
|
|
398
456
|
});
|
|
399
457
|
|
|
400
|
-
describe("getTimelineFitPps (min 60s extent)", () => {
|
|
401
|
-
const viewport = 632; // usable width = 632 - GUTTER -
|
|
458
|
+
describe("getTimelineFitPps (min 60s extent + fit headroom)", () => {
|
|
459
|
+
const viewport = 632; // usable width = 632 - GUTTER - TRACKS_LEFT_PAD - 2
|
|
402
460
|
|
|
403
461
|
it("computes fit pps against the 60s floor for short compositions", () => {
|
|
404
462
|
// A 10s comp maps 60s onto the viewport → the comp takes ~1/6 of the width.
|
|
463
|
+
// (10 * 1.2 = 12s of headroom-padded content is still under the 60s floor.)
|
|
405
464
|
const pps = getTimelineFitPps(viewport, 10);
|
|
406
|
-
expect(pps).toBeCloseTo((viewport - GUTTER - 2) / MIN_TIMELINE_EXTENT_S);
|
|
407
|
-
expect(10 * pps).toBeCloseTo((viewport - GUTTER - 2) / 6);
|
|
465
|
+
expect(pps).toBeCloseTo((viewport - GUTTER - TRACKS_LEFT_PAD - 2) / MIN_TIMELINE_EXTENT_S);
|
|
466
|
+
expect(10 * pps).toBeCloseTo((viewport - GUTTER - TRACKS_LEFT_PAD - 2) / 6);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("fits duration * FIT_ZOOM_HEADROOM (not the bare duration) for long compositions", () => {
|
|
470
|
+
expect(getTimelineFitPps(viewport, 60)).toBeCloseTo(
|
|
471
|
+
(viewport - GUTTER - TRACKS_LEFT_PAD - 2) / (60 * FIT_ZOOM_HEADROOM),
|
|
472
|
+
);
|
|
473
|
+
expect(getTimelineFitPps(viewport, 120)).toBeCloseTo(
|
|
474
|
+
(viewport - GUTTER - TRACKS_LEFT_PAD - 2) / (120 * FIT_ZOOM_HEADROOM),
|
|
475
|
+
);
|
|
408
476
|
});
|
|
409
477
|
|
|
410
|
-
it("
|
|
411
|
-
|
|
412
|
-
|
|
478
|
+
it("leaves CapCut-style trailing headroom: the comp ends at 1/1.2 of the usable width", () => {
|
|
479
|
+
const usable = viewport - GUTTER - TRACKS_LEFT_PAD - 2;
|
|
480
|
+
const pps = getTimelineFitPps(viewport, 120);
|
|
481
|
+
// Composition content occupies usable/1.2 px; the remaining ~17% is empty
|
|
482
|
+
// droppable ruler/lane surface past the end.
|
|
483
|
+
expect(120 * pps).toBeCloseTo(usable / FIT_ZOOM_HEADROOM);
|
|
484
|
+
expect(120 * pps).toBeLessThan(usable);
|
|
413
485
|
});
|
|
414
486
|
|
|
415
487
|
it("falls back to 100 pps before the viewport is measured", () => {
|
|
416
488
|
expect(getTimelineFitPps(0, 10)).toBe(100);
|
|
417
|
-
expect(getTimelineFitPps(GUTTER, 10)).toBe(100);
|
|
489
|
+
expect(getTimelineFitPps(GUTTER + TRACKS_LEFT_PAD, 10)).toBe(100);
|
|
418
490
|
expect(getTimelineFitPps(Number.NaN, 10)).toBe(100);
|
|
419
491
|
});
|
|
420
492
|
|
|
421
493
|
it("uses the floor for zero/invalid durations", () => {
|
|
422
494
|
expect(getTimelineFitPps(viewport, 0)).toBeCloseTo(
|
|
423
|
-
(viewport - GUTTER - 2) / MIN_TIMELINE_EXTENT_S,
|
|
495
|
+
(viewport - GUTTER - TRACKS_LEFT_PAD - 2) / MIN_TIMELINE_EXTENT_S,
|
|
424
496
|
);
|
|
425
497
|
expect(getTimelineFitPps(viewport, Number.NaN)).toBeCloseTo(
|
|
426
|
-
(viewport - GUTTER - 2) / MIN_TIMELINE_EXTENT_S,
|
|
498
|
+
(viewport - GUTTER - TRACKS_LEFT_PAD - 2) / MIN_TIMELINE_EXTENT_S,
|
|
427
499
|
);
|
|
428
500
|
});
|
|
429
501
|
});
|
|
@@ -439,7 +511,7 @@ describe("getTimelineDisplayContentWidth", () => {
|
|
|
439
511
|
it("still fills the viewport when that is larger than the 60s floor", () => {
|
|
440
512
|
expect(
|
|
441
513
|
getTimelineDisplayContentWidth({ trackContentWidth: 200, viewportWidth: 2000, pps: 5 }),
|
|
442
|
-
).toBe(2000 - GUTTER - 2);
|
|
514
|
+
).toBe(2000 - GUTTER - TRACKS_LEFT_PAD - 2);
|
|
443
515
|
});
|
|
444
516
|
|
|
445
517
|
it("tracks a drag ghost past every other bound (drag-to-extend)", () => {
|
|
@@ -529,13 +601,28 @@ describe("getTimelineScrollLeftForZoomAnchor", () => {
|
|
|
529
601
|
});
|
|
530
602
|
|
|
531
603
|
describe("getTimelinePlayheadLeft", () => {
|
|
532
|
-
it("
|
|
533
|
-
|
|
604
|
+
it("offsets the wrapper by half the head width so the line CENTER = GUTTER + TRACKS_LEFT_PAD + t*pps", () => {
|
|
605
|
+
// Wrapper left + PLAYHEAD_HEAD_W/2 (where the 1px line is centered) must
|
|
606
|
+
// equal GUTTER + TRACKS_LEFT_PAD + t*pps at any zoom.
|
|
607
|
+
expect(getTimelinePlayheadLeft(4, 20) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
608
|
+
GUTTER + TRACKS_LEFT_PAD + 4 * 20,
|
|
609
|
+
);
|
|
610
|
+
expect(getTimelinePlayheadLeft(10, 7.5) + PLAYHEAD_HEAD_W / 2).toBe(
|
|
611
|
+
GUTTER + TRACKS_LEFT_PAD + 75,
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("centers the line exactly on the left pad's end (the 00:00 tick) at t = 0", () => {
|
|
616
|
+
expect(getTimelinePlayheadLeft(0, 20) + PLAYHEAD_HEAD_W / 2).toBe(GUTTER + TRACKS_LEFT_PAD);
|
|
534
617
|
});
|
|
535
618
|
|
|
536
619
|
it("guards invalid input", () => {
|
|
537
|
-
expect(getTimelinePlayheadLeft(Number.NaN, 20)).toBe(
|
|
538
|
-
|
|
620
|
+
expect(getTimelinePlayheadLeft(Number.NaN, 20)).toBe(
|
|
621
|
+
GUTTER + TRACKS_LEFT_PAD - PLAYHEAD_HEAD_W / 2,
|
|
622
|
+
);
|
|
623
|
+
expect(getTimelinePlayheadLeft(4, Number.NaN)).toBe(
|
|
624
|
+
GUTTER + TRACKS_LEFT_PAD - PLAYHEAD_HEAD_W / 2,
|
|
625
|
+
);
|
|
539
626
|
});
|
|
540
627
|
});
|
|
541
628
|
|
|
@@ -604,7 +691,7 @@ describe("resolveTimelineAssetDrop", () => {
|
|
|
604
691
|
trackHeight: 72,
|
|
605
692
|
trackOrder: [0, 3, 7],
|
|
606
693
|
},
|
|
607
|
-
|
|
694
|
+
480, // rectLeft(100) + GUTTER + TRACKS_LEFT_PAD + 3s*100pps
|
|
608
695
|
// clientY updated for TRACKS_TOP_PAD=72: rectTop(200) + RULER_H(24) +
|
|
609
696
|
// TRACKS_TOP_PAD(72) + TRACK_H(48) + TRACK_H/2(24) = 368 → row 1 → track 3.
|
|
610
697
|
368,
|
|
@@ -625,7 +712,7 @@ describe("resolveTimelineAssetDrop", () => {
|
|
|
625
712
|
trackHeight: 72,
|
|
626
713
|
trackOrder: [0, 3, 7],
|
|
627
714
|
},
|
|
628
|
-
250,
|
|
715
|
+
250 + TRACKS_LEFT_PAD,
|
|
629
716
|
600,
|
|
630
717
|
),
|
|
631
718
|
).toEqual({ start: 1.18, track: 8 });
|
|
@@ -9,7 +9,7 @@ import { defaultTimelineTheme } from "./timelineTheme";
|
|
|
9
9
|
import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
|
|
10
10
|
import { useTimelinePlayhead } from "./useTimelinePlayhead";
|
|
11
11
|
import { useTimelineActiveClips } from "./useTimelineActiveClips";
|
|
12
|
-
import {
|
|
12
|
+
import { getTrackStyle } from "./timelineIcons";
|
|
13
13
|
import { useTimelineZoom } from "./useTimelineZoom";
|
|
14
14
|
import { useTimelineAssetDrop } from "./timelineDragDrop";
|
|
15
15
|
import { TimelineEmptyState } from "./TimelineEmptyState";
|
|
@@ -20,14 +20,14 @@ import { TimelineOverlays } from "./TimelineOverlays";
|
|
|
20
20
|
import { useTimelineEditPinning } from "./useTimelineEditPinning";
|
|
21
21
|
import { useTimelineStackingSync } from "./useTimelineStackingSync";
|
|
22
22
|
import { useTimelineGeometry } from "./useTimelineGeometry";
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
shouldShowTimelineShortcutHint,
|
|
28
|
-
} from "./timelineLayout";
|
|
23
|
+
import { useTimelineTrackDerivations } from "./useTimelineTrackDerivations";
|
|
24
|
+
import { GUTTER, TRACKS_LEFT_PAD, generateTicks, getTimelineCanvasHeight } from "./timelineLayout";
|
|
25
|
+
import { useTimelineScrollViewport } from "./useTimelineScrollViewport";
|
|
26
|
+
import { STUDIO_PREVIEW_FPS } from "../lib/time";
|
|
29
27
|
import { useResolvedTimelineEditCallbacks } from "./useResolvedTimelineEditCallbacks";
|
|
30
28
|
import type { TimelineProps } from "./TimelineTypes";
|
|
29
|
+
import { useTrackGapMenu } from "./useTrackGapMenu";
|
|
30
|
+
import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
|
|
31
31
|
|
|
32
32
|
// Re-export pure utilities so existing imports from "./Timeline" still resolve.
|
|
33
33
|
export {
|
|
@@ -95,6 +95,7 @@ export const Timeline = memo(function Timeline({
|
|
|
95
95
|
[beatAnalysis, musicElement, beatEdits],
|
|
96
96
|
);
|
|
97
97
|
const duration = usePlayerStore((s) => s.duration);
|
|
98
|
+
const timeDisplayMode = usePlayerStore((s) => s.timeDisplayMode);
|
|
98
99
|
const timelineReady = usePlayerStore((s) => s.timelineReady);
|
|
99
100
|
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
|
|
100
101
|
const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
|
|
@@ -112,109 +113,41 @@ export const Timeline = memo(function Timeline({
|
|
|
112
113
|
const [razorGuideX, setRazorGuideX] = useState<number | null>(null);
|
|
113
114
|
|
|
114
115
|
useMountEffect(() => {
|
|
115
|
-
const
|
|
116
|
-
const up = (e: KeyboardEvent) => e.key === "Shift" && setShiftHeld(false);
|
|
116
|
+
const key = (e: KeyboardEvent) => e.key === "Shift" && setShiftHeld(e.type === "keydown");
|
|
117
117
|
const blur = () => setShiftHeld(false);
|
|
118
|
-
window.addEventListener("keydown",
|
|
119
|
-
window.addEventListener("keyup",
|
|
118
|
+
window.addEventListener("keydown", key);
|
|
119
|
+
window.addEventListener("keyup", key);
|
|
120
120
|
window.addEventListener("blur", blur);
|
|
121
121
|
return () => {
|
|
122
|
-
window.removeEventListener("keydown",
|
|
123
|
-
window.removeEventListener("keyup",
|
|
122
|
+
window.removeEventListener("keydown", key);
|
|
123
|
+
window.removeEventListener("keyup", key);
|
|
124
124
|
window.removeEventListener("blur", blur);
|
|
125
125
|
};
|
|
126
126
|
});
|
|
127
127
|
|
|
128
128
|
const [showPopover, setShowPopover] = useState(false);
|
|
129
|
-
const [showShortcutHint, setShowShortcutHint] = useState(true);
|
|
130
129
|
const [kfContextMenu, setKfContextMenu] = useState<KeyframeDiamondContextMenuState | null>(null);
|
|
131
130
|
const [clipContextMenu, setClipContextMenu] = useState<{
|
|
132
131
|
x: number;
|
|
133
132
|
y: number;
|
|
134
133
|
element: TimelineElement;
|
|
135
134
|
} | null>(null);
|
|
136
|
-
const [viewportWidth, setViewportWidth] = useState(0);
|
|
137
|
-
const roRef = useRef<ResizeObserver | null>(null);
|
|
138
|
-
const shortcutHintRafRef = useRef(0);
|
|
139
|
-
|
|
140
|
-
const syncShortcutHintVisibility = useCallback(() => {
|
|
141
|
-
const scroll = scrollRef.current;
|
|
142
|
-
setShowShortcutHint(
|
|
143
|
-
scroll ? shouldShowTimelineShortcutHint(scroll.scrollHeight, scroll.clientHeight) : true,
|
|
144
|
-
);
|
|
145
|
-
}, []);
|
|
146
|
-
|
|
147
|
-
const scheduleShortcutHintVisibilitySync = useCallback(() => {
|
|
148
|
-
if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
|
|
149
|
-
shortcutHintRafRef.current = requestAnimationFrame(() => {
|
|
150
|
-
shortcutHintRafRef.current = 0;
|
|
151
|
-
syncShortcutHintVisibility();
|
|
152
|
-
});
|
|
153
|
-
}, [syncShortcutHintVisibility]);
|
|
154
135
|
|
|
155
136
|
const setContainerRef = useCallback((el: HTMLDivElement | null) => {
|
|
156
137
|
containerRef.current = el;
|
|
157
138
|
}, []);
|
|
158
139
|
|
|
159
|
-
// Last horizontal scroll offset,
|
|
160
|
-
// post-edit iframe reload: an edit re-derives elements (and may shrink the
|
|
161
|
-
// content width), which the browser clamps into a scroll jump. Paired with the
|
|
162
|
-
// pinned zoom (which keeps pps constant so the pixel offset stays meaningful),
|
|
163
|
-
// restoring this keeps the user parked at the same spot after any edit.
|
|
140
|
+
// Last horizontal scroll offset, restored across the post-edit iframe reload (pinned zoom).
|
|
164
141
|
const lastScrollLeftRef = useRef(0);
|
|
165
|
-
const setScrollRef = useCallback(
|
|
166
|
-
(el: HTMLDivElement | null) => {
|
|
167
|
-
if (roRef.current) {
|
|
168
|
-
roRef.current.disconnect();
|
|
169
|
-
roRef.current = null;
|
|
170
|
-
}
|
|
171
|
-
scrollRef.current = el;
|
|
172
|
-
if (!el) return;
|
|
173
|
-
|
|
174
|
-
const syncScrollViewport = () => {
|
|
175
|
-
setViewportWidth(el.clientWidth);
|
|
176
|
-
scheduleShortcutHintVisibilitySync();
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
syncScrollViewport();
|
|
180
|
-
roRef.current = new ResizeObserver(syncScrollViewport);
|
|
181
|
-
roRef.current.observe(el);
|
|
182
|
-
},
|
|
183
|
-
[scheduleShortcutHintVisibilitySync],
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
useMountEffect(() => () => {
|
|
187
|
-
roRef.current?.disconnect();
|
|
188
|
-
if (shortcutHintRafRef.current) cancelAnimationFrame(shortcutHintRafRef.current);
|
|
189
|
-
});
|
|
190
142
|
|
|
191
143
|
const effectiveDuration = useMemo(() => {
|
|
192
144
|
const safeDur = Number.isFinite(duration) ? duration : 0;
|
|
193
145
|
if (rawElements.length === 0) return safeDur;
|
|
194
|
-
const
|
|
195
|
-
const result = Math.max(safeDur, maxEnd);
|
|
146
|
+
const result = Math.max(safeDur, ...rawElements.map((el) => el.start + el.duration));
|
|
196
147
|
return Number.isFinite(result) ? result : safeDur;
|
|
197
148
|
}, [rawElements, duration]);
|
|
198
149
|
|
|
199
|
-
const tracks =
|
|
200
|
-
const map = new Map<number, typeof expandedElements>();
|
|
201
|
-
for (const el of expandedElements) {
|
|
202
|
-
const list = map.get(el.track) ?? [];
|
|
203
|
-
list.push(el);
|
|
204
|
-
map.set(el.track, list);
|
|
205
|
-
}
|
|
206
|
-
return Array.from(map.entries()).sort(([a], [b]) => a - b);
|
|
207
|
-
}, [expandedElements]);
|
|
208
|
-
|
|
209
|
-
const trackStyles = useMemo(() => {
|
|
210
|
-
const map = new Map<number, TrackVisualStyle>();
|
|
211
|
-
for (const [trackNum, els] of tracks) {
|
|
212
|
-
map.set(trackNum, getTrackStyle(els[0]?.tag ?? ""));
|
|
213
|
-
}
|
|
214
|
-
return map;
|
|
215
|
-
}, [tracks]);
|
|
216
|
-
|
|
217
|
-
const trackOrder = useMemo(() => tracks.map(([trackNum]) => trackNum), [tracks]);
|
|
150
|
+
const { tracks, trackStyles, trackOrder } = useTimelineTrackDerivations(expandedElements);
|
|
218
151
|
const trackOrderRef = useRef(trackOrder);
|
|
219
152
|
trackOrderRef.current = trackOrder;
|
|
220
153
|
const expandedElementsRef = useRef(expandedElements);
|
|
@@ -223,8 +156,7 @@ export const Timeline = memo(function Timeline({
|
|
|
223
156
|
const ppsRef = useRef(100);
|
|
224
157
|
const durationRef = useRef(effectiveDuration);
|
|
225
158
|
durationRef.current = effectiveDuration;
|
|
226
|
-
// Declared
|
|
227
|
-
// wrappers can close over it; `fitPpsRef.current` is refreshed each render.
|
|
159
|
+
// Declared before the fitPps derivation so the edit-pin wrappers can close over it.
|
|
228
160
|
const fitPpsRef = useRef(100);
|
|
229
161
|
|
|
230
162
|
const {
|
|
@@ -253,6 +185,22 @@ export const Timeline = memo(function Timeline({
|
|
|
253
185
|
expandedElementsRef,
|
|
254
186
|
});
|
|
255
187
|
|
|
188
|
+
const {
|
|
189
|
+
gapMenuModel,
|
|
190
|
+
gapHighlight,
|
|
191
|
+
setHoveredGapAction,
|
|
192
|
+
openGapMenu,
|
|
193
|
+
dismissGapMenu,
|
|
194
|
+
closeTrackGap,
|
|
195
|
+
closeAllTrackGaps,
|
|
196
|
+
} = useTrackGapMenu({
|
|
197
|
+
tracks,
|
|
198
|
+
expandedElementsRef,
|
|
199
|
+
trackOrderRef,
|
|
200
|
+
onMoveElement: pinnedOnMoveElement,
|
|
201
|
+
onMoveElements: pinnedOnMoveElements,
|
|
202
|
+
});
|
|
203
|
+
|
|
256
204
|
const {
|
|
257
205
|
draggedClip,
|
|
258
206
|
setDraggedClip,
|
|
@@ -294,6 +242,11 @@ export const Timeline = memo(function Timeline({
|
|
|
294
242
|
}, [draggedClip, trackOrder]);
|
|
295
243
|
|
|
296
244
|
const totalH = getTimelineCanvasHeight(displayTrackOrder.length);
|
|
245
|
+
const { viewportWidth, showShortcutHint, setScrollRef } = useTimelineScrollViewport(scrollRef, [
|
|
246
|
+
timelineReady,
|
|
247
|
+
expandedElements.length,
|
|
248
|
+
totalH,
|
|
249
|
+
]);
|
|
297
250
|
const keyframeCache = usePlayerStore((s) => s.keyframeCache);
|
|
298
251
|
const selectedKeyframes = usePlayerStore((s) => s.selectedKeyframes);
|
|
299
252
|
const toggleSelectedKeyframe = usePlayerStore((s) => s.toggleSelectedKeyframe);
|
|
@@ -329,6 +282,16 @@ export const Timeline = memo(function Timeline({
|
|
|
329
282
|
lastScrollLeftRef,
|
|
330
283
|
});
|
|
331
284
|
|
|
285
|
+
const laneGapStrips = useTimelineGapHighlights({
|
|
286
|
+
gapHighlight,
|
|
287
|
+
tracks,
|
|
288
|
+
selectedElementId,
|
|
289
|
+
selectedElementIds,
|
|
290
|
+
expandedElements,
|
|
291
|
+
dragActive: draggedClip?.started === true || resizingClip != null,
|
|
292
|
+
displayDuration,
|
|
293
|
+
});
|
|
294
|
+
|
|
332
295
|
const { seekFromX, autoScrollDuringDrag, dragScrollRaf } = useTimelinePlayhead({
|
|
333
296
|
playheadRef,
|
|
334
297
|
scrollRef,
|
|
@@ -380,8 +343,7 @@ export const Timeline = memo(function Timeline({
|
|
|
380
343
|
trackOrderRef,
|
|
381
344
|
onSelectElement,
|
|
382
345
|
});
|
|
383
|
-
|
|
384
|
-
setRangeSelectionRef.current = setRangeSelection;
|
|
346
|
+
setRangeSelectionRef.current = setRangeSelection; // stable ref consumed by useTimelineClipDrag
|
|
385
347
|
|
|
386
348
|
const prevSelectedRef = useRef(selectedElementRef.current);
|
|
387
349
|
// eslint-disable-next-line no-restricted-syntax, react-hooks/exhaustive-deps
|
|
@@ -395,16 +357,14 @@ export const Timeline = memo(function Timeline({
|
|
|
395
357
|
}
|
|
396
358
|
});
|
|
397
359
|
|
|
360
|
+
// Frame display mode labels ruler ticks as frame numbers — pass the fps so ticks snap to frames.
|
|
361
|
+
const tickFps = timeDisplayMode === "frame" ? STUDIO_PREVIEW_FPS : undefined;
|
|
398
362
|
const { major, minor } = useMemo(
|
|
399
|
-
() => generateTicks(displayDuration, pps),
|
|
400
|
-
[displayDuration, pps],
|
|
363
|
+
() => generateTicks(displayDuration, pps, tickFps),
|
|
364
|
+
[displayDuration, pps, tickFps],
|
|
401
365
|
);
|
|
402
366
|
const majorTickInterval = major.length >= 2 ? major[1] - major[0] : effectiveDuration;
|
|
403
367
|
|
|
404
|
-
useEffect(() => {
|
|
405
|
-
syncShortcutHintVisibility();
|
|
406
|
-
}, [syncShortcutHintVisibility, timelineReady, expandedElements.length, totalH]);
|
|
407
|
-
|
|
408
368
|
const getPreviewElement = useCallback(
|
|
409
369
|
(element: TimelineElement): TimelineElement => {
|
|
410
370
|
if (
|
|
@@ -458,8 +418,7 @@ export const Timeline = memo(function Timeline({
|
|
|
458
418
|
tabIndex={-1}
|
|
459
419
|
className={`${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-none`}
|
|
460
420
|
onScroll={(e) => {
|
|
461
|
-
|
|
462
|
-
lastScrollLeftRef.current = e.currentTarget.scrollLeft;
|
|
421
|
+
lastScrollLeftRef.current = e.currentTarget.scrollLeft; // restored across post-edit reload
|
|
463
422
|
}}
|
|
464
423
|
onDragOver={handleAssetDragOver}
|
|
465
424
|
onDragLeave={() => clearDropPreview()}
|
|
@@ -467,7 +426,8 @@ export const Timeline = memo(function Timeline({
|
|
|
467
426
|
onPointerDown={(e) => {
|
|
468
427
|
if (activeTool === "razor" && e.shiftKey && e.button === 0 && scrollRef.current) {
|
|
469
428
|
const rect = scrollRef.current.getBoundingClientRect();
|
|
470
|
-
const x =
|
|
429
|
+
const x =
|
|
430
|
+
e.clientX - rect.left + scrollRef.current.scrollLeft - GUTTER - TRACKS_LEFT_PAD;
|
|
471
431
|
const splitTime = Math.max(0, x / pps);
|
|
472
432
|
onRazorSplitAll?.(splitTime);
|
|
473
433
|
return;
|
|
@@ -488,6 +448,7 @@ export const Timeline = memo(function Timeline({
|
|
|
488
448
|
majorTickInterval={majorTickInterval}
|
|
489
449
|
rangeSelection={rangeSelection}
|
|
490
450
|
marqueeRect={marqueeRect}
|
|
451
|
+
laneGapStrips={laneGapStrips}
|
|
491
452
|
theme={theme}
|
|
492
453
|
displayTrackOrder={displayTrackOrder}
|
|
493
454
|
trackOrder={trackOrder}
|
|
@@ -526,8 +487,7 @@ export const Timeline = memo(function Timeline({
|
|
|
526
487
|
const elKey = el.key ?? el.id;
|
|
527
488
|
setSelectedElementId(elKey);
|
|
528
489
|
onSelectElement?.(el);
|
|
529
|
-
//
|
|
530
|
-
// selection); cleared above so this single-selects it.
|
|
490
|
+
// Select the clicked diamond (matches shift-click); cleared above so this single-selects.
|
|
531
491
|
toggleSelectedKeyframe(`${elKey}:${pct}`);
|
|
532
492
|
const absTime = el.start + (pct / 100) * el.duration;
|
|
533
493
|
onSeek?.(absTime);
|
|
@@ -560,8 +520,14 @@ export const Timeline = memo(function Timeline({
|
|
|
560
520
|
e.preventDefault();
|
|
561
521
|
setSelectedElementId(el.key ?? el.id);
|
|
562
522
|
onSelectElement?.(el);
|
|
523
|
+
dismissGapMenu();
|
|
563
524
|
setClipContextMenu({ x: e.clientX, y: e.clientY, element: el });
|
|
564
525
|
}}
|
|
526
|
+
onContextMenuLane={(e, track, time) => {
|
|
527
|
+
if (draggedClip?.started || resizingClip) return;
|
|
528
|
+
setClipContextMenu(null);
|
|
529
|
+
openGapMenu({ x: e.clientX, y: e.clientY, track, time });
|
|
530
|
+
}}
|
|
565
531
|
/>
|
|
566
532
|
{activeTool === "razor" && razorGuideX !== null && (
|
|
567
533
|
<div
|
|
@@ -594,6 +560,11 @@ export const Timeline = memo(function Timeline({
|
|
|
594
560
|
onSplitElement={onSplitElement}
|
|
595
561
|
pinZoomBeforeEdit={pinZoomBeforeEdit}
|
|
596
562
|
onDeleteElement={_onDeleteElement}
|
|
563
|
+
gapContextMenu={gapMenuModel}
|
|
564
|
+
onDismissGapContextMenu={dismissGapMenu}
|
|
565
|
+
onCloseTrackGap={closeTrackGap}
|
|
566
|
+
onCloseAllTrackGaps={closeAllTrackGaps}
|
|
567
|
+
onHoverGapAction={setHoveredGapAction}
|
|
597
568
|
/>
|
|
598
569
|
</div>
|
|
599
570
|
);
|
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
CLIP_Y,
|
|
11
11
|
TRACKS_TOP_PAD,
|
|
12
12
|
TRACKS_BOTTOM_PAD,
|
|
13
|
+
TRACKS_LEFT_PAD,
|
|
14
|
+
PLAYHEAD_HEAD_W,
|
|
15
|
+
getTimelinePlayheadLeft,
|
|
13
16
|
getTimelineRowTop,
|
|
14
17
|
} from "./timelineLayout";
|
|
15
18
|
import { usePlayerStore } from "../store/playerStore";
|
|
@@ -20,6 +23,8 @@ import type { Rect } from "../../utils/marqueeGeometry";
|
|
|
20
23
|
import { TimelineClip } from "./TimelineClip";
|
|
21
24
|
import { TimelineLanes, type TimelineLaneBaseProps } from "./TimelineLanes";
|
|
22
25
|
import { renderClipChildren } from "./timelineClipChildren";
|
|
26
|
+
import { useTimelineRevealClip } from "./useTimelineRevealClip";
|
|
27
|
+
import type { TimelineLaneGapStrips } from "./useTimelineGapHighlights";
|
|
23
28
|
|
|
24
29
|
interface TimelineCanvasProps extends TimelineLaneBaseProps {
|
|
25
30
|
major: number[];
|
|
@@ -34,6 +39,8 @@ interface TimelineCanvasProps extends TimelineLaneBaseProps {
|
|
|
34
39
|
/** Playhead is being actively scrubbed — fills the grab-handle head. */
|
|
35
40
|
isScrubbing: boolean;
|
|
36
41
|
playheadRef: React.RefObject<HTMLDivElement | null>;
|
|
42
|
+
/** Gap strips: loud on gap-menu-row hover, quiet on the selected clip's lane. */
|
|
43
|
+
laneGapStrips: TimelineLaneGapStrips[];
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvasProps) {
|
|
@@ -41,6 +48,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
41
48
|
const { onResizeElement, onMoveElement, onToggleTrackHidden, onRazorSplit, onRazorSplitAll } =
|
|
42
49
|
useTimelineEditContextOptional();
|
|
43
50
|
const beatDragging = usePlayerStore((s) => s.beatDragging);
|
|
51
|
+
// Scroll a clip into view when the sidebar (asset card) requests a reveal.
|
|
52
|
+
useTimelineRevealClip(scrollRef);
|
|
44
53
|
const draggedElement = draggedClip?.element ?? null;
|
|
45
54
|
const activeDraggedElement =
|
|
46
55
|
draggedClip?.started === true && draggedElement
|
|
@@ -92,7 +101,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
92
101
|
return (
|
|
93
102
|
<div
|
|
94
103
|
className="relative"
|
|
95
|
-
style={{ height: props.totalH, width: GUTTER + props.trackContentWidth }}
|
|
104
|
+
style={{ height: props.totalH, width: GUTTER + TRACKS_LEFT_PAD + props.trackContentWidth }}
|
|
96
105
|
>
|
|
97
106
|
<TimelineRuler
|
|
98
107
|
major={props.major}
|
|
@@ -126,6 +135,31 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
126
135
|
new bottom track comfortably (see TRACKS_BOTTOM_PAD / getTimelineCanvasHeight). */}
|
|
127
136
|
<div aria-hidden="true" style={{ height: TRACKS_BOTTOM_PAD }} />
|
|
128
137
|
|
|
138
|
+
{/* Gap strips — loud dashed fill for the gap(s) a hovered "Close gap(s)"
|
|
139
|
+
menu row would collapse; a quiet tint for every gap on the selected
|
|
140
|
+
clip's lane. Geometry mirrors the drop placeholder (row top + clip
|
|
141
|
+
inset) so strips sit exactly where a clip body would. */}
|
|
142
|
+
{props.laneGapStrips.map((strip) => {
|
|
143
|
+
const rowIndex = displayTrackOrder.indexOf(strip.track);
|
|
144
|
+
if (rowIndex < 0) return null;
|
|
145
|
+
const loud = strip.kind === "hover";
|
|
146
|
+
return strip.intervals.map((gap) => (
|
|
147
|
+
<div
|
|
148
|
+
key={`gap-${strip.kind}-${strip.track}-${gap.start}`}
|
|
149
|
+
className="pointer-events-none absolute"
|
|
150
|
+
style={{
|
|
151
|
+
top: getTimelineRowTop(rowIndex) + CLIP_Y,
|
|
152
|
+
left: GUTTER + TRACKS_LEFT_PAD + gap.start * props.pps,
|
|
153
|
+
width: Math.max((gap.end - gap.start) * props.pps, 2),
|
|
154
|
+
height: TRACK_H - CLIP_Y * 2,
|
|
155
|
+
background: loud ? "rgba(60,230,172,0.18)" : "rgba(60,230,172,0.055)",
|
|
156
|
+
borderRadius: 4,
|
|
157
|
+
zIndex: 25,
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
));
|
|
161
|
+
})}
|
|
162
|
+
|
|
129
163
|
{/* Drop placeholder — a clip-sized slot at the exact landing spot (target
|
|
130
164
|
lane + snapped start), parallel to the ghost. Hidden in insert mode. */}
|
|
131
165
|
{draggedClip?.started && draggedClip.insertRow == null && draggedRowIndex >= 0 && (
|
|
@@ -133,7 +167,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
133
167
|
className="absolute pointer-events-none"
|
|
134
168
|
style={{
|
|
135
169
|
top: getTimelineRowTop(draggedRowIndex) + CLIP_Y,
|
|
136
|
-
left: GUTTER + draggedClip.previewStart * props.pps,
|
|
170
|
+
left: GUTTER + TRACKS_LEFT_PAD + draggedClip.previewStart * props.pps,
|
|
137
171
|
width: Math.max(draggedClip.element.duration * props.pps, 4),
|
|
138
172
|
height: TRACK_H - CLIP_Y * 2,
|
|
139
173
|
border: "1px solid rgba(60,230,172,0.55)",
|
|
@@ -151,7 +185,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
151
185
|
className="absolute pointer-events-none"
|
|
152
186
|
style={{
|
|
153
187
|
top: getTimelineRowTop(draggedClip.insertRow) - 0.5,
|
|
154
|
-
left: GUTTER,
|
|
188
|
+
left: GUTTER + TRACKS_LEFT_PAD,
|
|
155
189
|
width: props.trackContentWidth,
|
|
156
190
|
height: 1,
|
|
157
191
|
background: "#3CE6AC",
|
|
@@ -166,7 +200,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
166
200
|
<div
|
|
167
201
|
className="absolute pointer-events-none"
|
|
168
202
|
style={{
|
|
169
|
-
left: GUTTER + draggedClip.snapTime * props.pps,
|
|
203
|
+
left: GUTTER + TRACKS_LEFT_PAD + draggedClip.snapTime * props.pps,
|
|
170
204
|
top: RULER_H,
|
|
171
205
|
bottom: 0,
|
|
172
206
|
width: 1,
|
|
@@ -246,7 +280,9 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
246
280
|
className="absolute pointer-events-none"
|
|
247
281
|
style={{
|
|
248
282
|
left:
|
|
249
|
-
GUTTER +
|
|
283
|
+
GUTTER +
|
|
284
|
+
TRACKS_LEFT_PAD +
|
|
285
|
+
Math.min(props.rangeSelection.start, props.rangeSelection.end) * props.pps,
|
|
250
286
|
width: Math.abs(props.rangeSelection.end - props.rangeSelection.start) * props.pps,
|
|
251
287
|
top: RULER_H,
|
|
252
288
|
bottom: 0,
|
|
@@ -259,12 +295,16 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
|
|
|
259
295
|
)}
|
|
260
296
|
|
|
261
297
|
{/* Playhead — hidden while dragging a beat so its guideline doesn't
|
|
262
|
-
track the scrub and clutter the beat being moved.
|
|
298
|
+
track the scrub and clutter the beat being moved. Explicit width +
|
|
299
|
+
the half-head offset baked into getTimelinePlayheadLeft keep the
|
|
300
|
+
inner 1px line's CENTER exactly on GUTTER + t * pps (the ruler
|
|
301
|
+
ticks' center), instead of relying on shrink-wrap sizing. */}
|
|
263
302
|
<div
|
|
264
303
|
ref={props.playheadRef}
|
|
265
304
|
className="absolute top-0 bottom-0 pointer-events-none"
|
|
266
305
|
style={{
|
|
267
|
-
left: `${
|
|
306
|
+
left: `${getTimelinePlayheadLeft(0, 0)}px`,
|
|
307
|
+
width: PLAYHEAD_HEAD_W,
|
|
268
308
|
zIndex: 100,
|
|
269
309
|
display: beatDragging ? "none" : undefined,
|
|
270
310
|
}}
|