@hyperframes/studio 0.8.90 → 0.8.92
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-DDjER_ii.js → hyperframes-player-DKJPDHUp.js} +1 -1
- package/dist/assets/{index-BKE3_N4v.js → index-C5_j5-3u.js} +178 -178
- package/dist/assets/{index-5SFeUXP8.js → index-CHZ4kFWm.js} +1 -1
- package/dist/assets/{index-CVsCxnlz.js → index-DpzpqJ_j.js} +1 -1
- package/dist/{chunk-KU4F7QZ3.js → chunk-QPSOTK6M.js} +8 -1
- package/dist/chunk-QPSOTK6M.js.map +1 -0
- package/dist/index.d.ts +18 -14
- package/dist/index.html +1 -1
- package/dist/index.js +9190 -8916
- package/dist/index.js.map +1 -1
- package/dist/player.js +1 -1
- package/package.json +8 -8
- package/src/components/TimelineToolbar.test.tsx +12 -0
- package/src/components/editor/MotionPathOverlay.tsx +5 -1
- package/src/components/editor/commitPreflightHandles.test.tsx +7 -20
- package/src/components/editor/elementPositionOffset.test.ts +62 -0
- package/src/components/editor/elementPositionOffset.ts +64 -0
- package/src/components/editor/motionPathCommit.test.ts +25 -5
- package/src/components/editor/motionPathSelection.ts +10 -3
- package/src/components/editor/useLayerRevealOverride.test.ts +8 -0
- package/src/components/editor/useLayerRevealOverride.ts +2 -1
- package/src/components/nle/NLEContext.tsx +1 -1
- package/src/components/nle/useTimelineEditCallbacks.test.tsx +53 -0
- package/src/hooks/domEditPersistFailure.ts +8 -6
- package/src/hooks/elementOffsetStager.ts +65 -0
- package/src/hooks/gestureTransaction.ts +4 -2
- package/src/hooks/gsapDragCommit.ts +1 -0
- package/src/hooks/gsapEditOutcome.ts +2 -1
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +157 -0
- package/src/hooks/gsapRuntimeBridge.ts +50 -14
- package/src/hooks/gsapShared.test.ts +1 -1
- package/src/hooks/gsapShared.ts +16 -12
- package/src/hooks/gsapTweenReach.ts +30 -0
- package/src/hooks/timelineEditingHelpers.ts +2 -1
- package/src/hooks/useClipboard.source.test.ts +58 -0
- package/src/hooks/useClipboard.ts +12 -3
- package/src/hooks/useCommitPreflightCapabilities.ts +2 -2
- package/src/hooks/useDomEditCommits.ts +2 -0
- package/src/hooks/useDomEditPositionPatchCommit.ts +4 -1
- package/src/hooks/useDomEditSession.ts +2 -0
- package/src/hooks/useDomGeometryCommit.test.tsx +102 -0
- package/src/hooks/useDomGeometryCommit.ts +42 -1
- package/src/hooks/useDomGeometryCommits.test.tsx +27 -0
- package/src/hooks/useDomGeometryCommits.ts +14 -1
- package/src/hooks/useGsapAwareEditing.test.tsx +113 -5
- package/src/hooks/useGsapAwareEditing.ts +44 -7
- package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +2 -1
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +8 -4
- package/src/hooks/useRazorSplit.test.ts +5 -5
- package/src/hooks/useTimelineAssetDropOps.test.tsx +138 -0
- package/src/hooks/useTimelineAssetDropOps.ts +40 -18
- package/src/hooks/useTimelineEditing.test.tsx +384 -0
- package/src/hooks/useTimelineEditing.ts +12 -12
- package/src/hooks/useTimelineGroupEditing.ts +16 -7
- package/src/player/components/Timeline.rulerSeek.test.tsx +111 -0
- package/src/player/components/Timeline.showKeyframes.test.tsx +28 -2
- package/src/player/components/TimelinePropertyLanes.test.tsx +1 -1
- package/src/player/components/TimelineProvider.tsx +1 -0
- package/src/player/components/TimelineTrackHeader.test.tsx +1 -1
- package/src/player/components/TimelineTypes.ts +1 -1
- package/src/player/components/timelineClipDragCommit.test.ts +32 -66
- package/src/player/components/timelineClipDragCommit.ts +19 -37
- package/src/player/components/timelineClipDragPreview.test.ts +283 -24
- package/src/player/components/timelineClipDragPreview.ts +51 -83
- package/src/player/components/timelineClipDragTypes.ts +3 -8
- package/src/player/components/timelineCollision.test.ts +100 -302
- package/src/player/components/timelineCollision.ts +73 -160
- package/src/player/components/timelineDragDrop.test.tsx +19 -5
- package/src/player/components/timelineDragDrop.ts +7 -9
- package/src/player/components/timelineDragLanding.ts +0 -49
- package/src/player/components/timelineEditing.ts +2 -1
- package/src/player/components/timelineGroupEditing.ts +7 -2
- package/src/player/components/timelineLayout.test.ts +0 -25
- package/src/player/components/timelineLayout.ts +0 -16
- package/src/player/components/timelineMultiDragPreview.test.ts +34 -23
- package/src/player/components/timelineMultiDragPreview.ts +26 -25
- package/src/player/components/timelineProviderStateBuilders.test.ts +1 -0
- package/src/player/components/timelineProviderStateBuilders.ts +14 -6
- package/src/player/components/timelineShellMeta.tsx +99 -0
- package/src/player/components/timelineZMirror.test.ts +2 -2
- package/src/player/components/timelineZMirror.ts +1 -1
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +8 -5
- package/src/player/components/useTimelineClipDrag.ts +3 -2
- package/src/player/components/useTimelineDragPreviewState.ts +34 -0
- package/src/player/components/useTimelinePlayhead.test.tsx +215 -26
- package/src/player/components/useTimelinePlayhead.ts +56 -27
- package/src/player/components/useTimelineProviderState.tsx +29 -66
- package/src/player/components/useTimelineZoom.ts +9 -1
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +44 -0
- package/src/player/hooks/useTimelinePlayer.ts +7 -4
- package/src/player/lib/timelineDOM.test.ts +79 -0
- package/src/player/lib/timelineDOM.ts +16 -7
- package/src/player/lib/timelineIframeHelpers.test.ts +36 -0
- package/src/player/lib/timelineIframeHelpers.ts +12 -3
- package/src/player/store/liveTime.ts +6 -0
- package/src/player/store/playerStore.ts +2 -0
- package/src/player/store/timelineElement.ts +28 -3
- package/src/utils/razorSplitTransaction.test.ts +1 -1
- package/src/utils/razorSplitTransaction.ts +3 -3
- package/src/utils/timelineDropTrackInsert.test.ts +60 -1
- package/src/utils/timelineDropTrackInsert.ts +36 -5
- package/dist/chunk-KU4F7QZ3.js.map +0 -1
|
@@ -12,6 +12,22 @@ import {
|
|
|
12
12
|
import { getTimelinePlayheadTransform } from "./timelinePlayheadTransform";
|
|
13
13
|
import { applyTimelineHorizontalAutoScrollStep } from "./timelineEditing";
|
|
14
14
|
|
|
15
|
+
function revealPlayheadScrollLeft(
|
|
16
|
+
scroll: HTMLDivElement,
|
|
17
|
+
playheadX: number,
|
|
18
|
+
contentOrigin: number,
|
|
19
|
+
from = scroll.scrollLeft,
|
|
20
|
+
): number {
|
|
21
|
+
if (playheadX >= from + contentOrigin && playheadX <= from + scroll.clientWidth) return from;
|
|
22
|
+
return getTimelinePlaybackFollowScrollLeft({
|
|
23
|
+
playheadX,
|
|
24
|
+
currentScrollLeft: from,
|
|
25
|
+
viewportWidth: scroll.clientWidth,
|
|
26
|
+
contentOrigin,
|
|
27
|
+
maxScrollLeft: scroll.scrollWidth - scroll.clientWidth,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
interface UseTimelinePlayheadInput {
|
|
16
32
|
playheadRef: React.RefObject<HTMLDivElement | null>;
|
|
17
33
|
scrollRef: React.RefObject<HTMLDivElement | null>;
|
|
@@ -58,10 +74,13 @@ export function useTimelinePlayhead({
|
|
|
58
74
|
}: UseTimelinePlayheadInput) {
|
|
59
75
|
const dragScrollRaf = useRef(0);
|
|
60
76
|
const previousZoomModeRef = useRef<ZoomMode | null>(zoomMode);
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// anchors at the cursor instead, so it opts out via `skipCenterAnchorRef`.
|
|
77
|
+
// A toolbar zoom keeps the playhead in place; a resize keeps the centre, or 00:00 at the start.
|
|
78
|
+
// The pinch handler anchors at the cursor instead, so it opts out via `skipCenterAnchorRef`.
|
|
64
79
|
const previousAnchorPpsRef = useRef(pps);
|
|
80
|
+
const userZoomCount = usePlayerStore((s) => s.userZoomCount);
|
|
81
|
+
const previousZoomCountRef = useRef(userZoomCount);
|
|
82
|
+
const lastLiveTimeRef = useRef(usePlayerStore.getState().currentTime);
|
|
83
|
+
const lastSeekCountRef = useRef(liveTime.seekCount());
|
|
65
84
|
const skipCenterAnchorRef = useRef(false);
|
|
66
85
|
const contentOriginRef = useRef(contentOrigin);
|
|
67
86
|
contentOriginRef.current = contentOrigin;
|
|
@@ -70,15 +89,22 @@ export function useTimelinePlayhead({
|
|
|
70
89
|
const scroll = scrollRef.current;
|
|
71
90
|
const prevPps = previousAnchorPpsRef.current;
|
|
72
91
|
previousAnchorPpsRef.current = pps;
|
|
92
|
+
const prevZoomCount = previousZoomCountRef.current;
|
|
93
|
+
previousZoomCountRef.current = userZoomCount;
|
|
73
94
|
// Always consume the skip flag, even when pps didn't change — otherwise a
|
|
74
95
|
// pinch that produced no pps change (already at the zoom clamp) would strand
|
|
75
96
|
// it true and the next toolbar zoom would wrongly skip center-anchoring.
|
|
76
97
|
const skip = skipCenterAnchorRef.current;
|
|
77
98
|
skipCenterAnchorRef.current = false;
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
if (!scroll || pps === prevPps || skip) return;
|
|
100
|
+
const zoomed = userZoomCount !== prevZoomCount;
|
|
101
|
+
if (!zoomed && scroll.scrollLeft < 1) return;
|
|
102
|
+
const time = Math.max(0, lastLiveTimeRef.current);
|
|
103
|
+
const playheadX = contentOrigin + time * prevPps;
|
|
104
|
+
const onScreen =
|
|
105
|
+
revealPlayheadScrollLeft(scroll, playheadX, contentOrigin) === scroll.scrollLeft;
|
|
80
106
|
const nextScrollLeft = getTimelineScrollLeftForZoomAnchor({
|
|
81
|
-
pointerX: scroll.clientWidth / 2,
|
|
107
|
+
pointerX: zoomed && onScreen ? playheadX - scroll.scrollLeft : scroll.clientWidth / 2,
|
|
82
108
|
currentScrollLeft: scroll.scrollLeft,
|
|
83
109
|
contentOrigin,
|
|
84
110
|
currentPixelsPerSecond: prevPps,
|
|
@@ -86,8 +112,11 @@ export function useTimelinePlayhead({
|
|
|
86
112
|
duration: durationRef.current,
|
|
87
113
|
});
|
|
88
114
|
const maxScrollLeft = Math.max(0, scroll.scrollWidth - scroll.clientWidth);
|
|
89
|
-
|
|
90
|
-
|
|
115
|
+
const anchored = Math.max(0, Math.min(maxScrollLeft, nextScrollLeft));
|
|
116
|
+
scroll.scrollLeft = zoomed
|
|
117
|
+
? revealPlayheadScrollLeft(scroll, contentOrigin + time * pps, contentOrigin, anchored)
|
|
118
|
+
: anchored;
|
|
119
|
+
}, [pps, userZoomCount, scrollRef, durationRef, contentOrigin]);
|
|
91
120
|
|
|
92
121
|
const syncPlayheadPosition = useCallback(
|
|
93
122
|
(time: number) => {
|
|
@@ -127,7 +156,6 @@ export function useTimelinePlayhead({
|
|
|
127
156
|
}, [zoomMode, scrollRef]);
|
|
128
157
|
|
|
129
158
|
useMountEffect(() => {
|
|
130
|
-
let lastLiveTime = usePlayerStore.getState().currentTime;
|
|
131
159
|
const place = (t: number, atRest: boolean) => {
|
|
132
160
|
if (!playheadRef.current || durationRef.current <= 0) return false;
|
|
133
161
|
playheadRef.current.style.transform = getTimelinePlayheadTransform(
|
|
@@ -138,29 +166,30 @@ export function useTimelinePlayhead({
|
|
|
138
166
|
);
|
|
139
167
|
return true;
|
|
140
168
|
};
|
|
169
|
+
const dragging = () => isDragging.current || usePlayerStore.getState().beatDragging;
|
|
141
170
|
const unsubPlaying = usePlayerStore.subscribe((state, prev) => {
|
|
142
|
-
if (prev.isPlaying && !state.isPlaying) place(
|
|
171
|
+
if (prev.isPlaying && !state.isPlaying) place(lastLiveTimeRef.current, true);
|
|
143
172
|
});
|
|
173
|
+
lastSeekCountRef.current = liveTime.seekCount();
|
|
144
174
|
const unsub = liveTime.subscribe((t) => {
|
|
145
|
-
|
|
146
|
-
|
|
175
|
+
const sought = liveTime.seekCount() !== lastSeekCountRef.current;
|
|
176
|
+
lastSeekCountRef.current = liveTime.seekCount();
|
|
177
|
+
lastLiveTimeRef.current = t;
|
|
178
|
+
const playing = usePlayerStore.getState().isPlaying;
|
|
179
|
+
if (!place(t, !playing)) return;
|
|
147
180
|
const playheadX = contentOriginRef.current + Math.max(0, t) * ppsRef.current;
|
|
148
181
|
const scroll = scrollRef.current;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
viewportWidth: scroll.clientWidth,
|
|
161
|
-
contentOrigin: contentOriginRef.current,
|
|
162
|
-
maxScrollLeft: scroll.scrollWidth - scroll.clientWidth,
|
|
163
|
-
});
|
|
182
|
+
// Paused, only a seek scrolls: a reload's republish, frame-rounded, must not undo a person's scroll.
|
|
183
|
+
if (!scroll || dragging() || zoomModeRef.current === "fit" || (!playing && !sought)) return;
|
|
184
|
+
const nextScrollLeft = playing
|
|
185
|
+
? getTimelinePlaybackFollowScrollLeft({
|
|
186
|
+
playheadX,
|
|
187
|
+
currentScrollLeft: scroll.scrollLeft,
|
|
188
|
+
viewportWidth: scroll.clientWidth,
|
|
189
|
+
contentOrigin: contentOriginRef.current,
|
|
190
|
+
maxScrollLeft: scroll.scrollWidth - scroll.clientWidth,
|
|
191
|
+
})
|
|
192
|
+
: revealPlayheadScrollLeft(scroll, playheadX, contentOriginRef.current);
|
|
164
193
|
if (Math.abs(nextScrollLeft - scroll.scrollLeft) >= 0.5) {
|
|
165
194
|
scroll.scrollLeft = nextScrollLeft;
|
|
166
195
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useRef, useMemo, useCallback, useState } from "react";
|
|
2
2
|
import { useAdjustedBeatAnalysis, useMusicBeatAnalysis } from "../../hooks/useMusicBeatAnalysis";
|
|
3
|
-
import { usePlayerStore
|
|
3
|
+
import { usePlayerStore } from "../store/playerStore";
|
|
4
4
|
import { defaultTimelineTheme } from "./timelineTheme";
|
|
5
5
|
import { useTimelinePlayhead } from "./useTimelinePlayhead";
|
|
6
6
|
import { useTimelineZoom } from "./useTimelineZoom";
|
|
@@ -8,18 +8,14 @@ import { useTimelineAssetDrop } from "./timelineDragDrop";
|
|
|
8
8
|
import { type KeyframeDiamondContextMenuState } from "./KeyframeDiamondContextMenu";
|
|
9
9
|
import { useTimelineClipDrag } from "./useTimelineClipDrag";
|
|
10
10
|
import type { ClipContextMenuState, TimelineContextValue } from "./TimelineProvider";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
resolveMultiDragPreview,
|
|
14
|
-
resolveResizingElementIds,
|
|
15
|
-
shouldIgnoreTimelinePointerDown,
|
|
16
|
-
} from "./timelineProviderStateBuilders";
|
|
11
|
+
import { buildTimelineShellMeta } from "./timelineShellMeta";
|
|
12
|
+
import { useTimelineDragPreviewState } from "./useTimelineDragPreviewState";
|
|
17
13
|
import { useTimelineOverlaysState } from "./useTimelineOverlaysState";
|
|
18
14
|
import { useTimelineEditPinning } from "./useTimelineEditPinning";
|
|
19
15
|
import { useTimelineStackingSync } from "./useTimelineStackingSync";
|
|
20
16
|
import { useTimelineGeometry } from "./useTimelineGeometry";
|
|
21
17
|
import { useAutoExpandKeyframedClips } from "./useAutoExpandKeyframedClips";
|
|
22
|
-
import { GUTTER
|
|
18
|
+
import { GUTTER } from "./timelineLayout";
|
|
23
19
|
import { useTimelineLabelColumn } from "./useTimelineLabelColumn";
|
|
24
20
|
import { useTimelineKeyframeData } from "./useTimelineKeyframeData";
|
|
25
21
|
import { useTimelineScrollViewport } from "./useTimelineScrollViewport";
|
|
@@ -33,9 +29,9 @@ import {
|
|
|
33
29
|
} from "./useTimelineTrackLayout";
|
|
34
30
|
import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
|
|
35
31
|
import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
|
|
36
|
-
import {
|
|
32
|
+
import { useTimelineRazorInteraction } from "./TimelineRazorInteraction";
|
|
37
33
|
import { useTimelinePerformanceTelemetry } from "./useTimelinePerformanceTelemetry";
|
|
38
|
-
import { getEffectiveTimelineDuration
|
|
34
|
+
import { getEffectiveTimelineDuration } from "./timelineViewModel";
|
|
39
35
|
import { useTimelineShiftModifier } from "./useTimelineShiftModifier";
|
|
40
36
|
import { useTimelineTicks } from "./useTimelineTicks";
|
|
41
37
|
import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
|
|
@@ -44,7 +40,6 @@ import { useTimelineActiveClips } from "./useTimelineActiveClips";
|
|
|
44
40
|
import { useTimelineLaneMoveRefresh } from "./useTimelineLaneMoveRefresh";
|
|
45
41
|
import { useTimelineLogicalFocus } from "./useTimelineLogicalFocus";
|
|
46
42
|
import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
|
|
47
|
-
import { resolveSnapGuide } from "./timelineSnapping";
|
|
48
43
|
export function useTimelineProviderState({
|
|
49
44
|
onSeek,
|
|
50
45
|
onDrillDown,
|
|
@@ -107,8 +102,7 @@ export function useTimelineProviderState({
|
|
|
107
102
|
const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
|
|
108
103
|
const focusedEaseSegment = usePlayerStore((s) => s.focusedEaseSegment);
|
|
109
104
|
const { gsapAnimations, keyframeCache } = useTimelineKeyframeData(showKeyframes);
|
|
110
|
-
const
|
|
111
|
-
const { labelMode, contentOrigin } = useTimelineLabelColumn(namedAnimations, timelineElements);
|
|
105
|
+
const { labelMode, contentOrigin } = useTimelineLabelColumn(gsapAnimations, timelineElements);
|
|
112
106
|
const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
|
|
113
107
|
const currentTime = usePlayerStore((s) => s.currentTime);
|
|
114
108
|
const beatDragging = usePlayerStore((s) => s.beatDragging);
|
|
@@ -223,7 +217,8 @@ export function useTimelineProviderState({
|
|
|
223
217
|
sessionEpoch,
|
|
224
218
|
});
|
|
225
219
|
const displayLayout = useTimelineDisplayLayout(draggedClip, trackOrder, rowGeometry);
|
|
226
|
-
const resizingElementIds =
|
|
220
|
+
const { resizingElementIds, getPreviewElement, draggedElement, multiDragPreview, snapGuide } =
|
|
221
|
+
useTimelineDragPreviewState(draggedClip, resizingClip, selectedElementIds, timelineElements);
|
|
227
222
|
const { recordTimelineScroll } = useTimelinePerformanceTelemetry({
|
|
228
223
|
totalClipCount: timelineElements.length,
|
|
229
224
|
totalRowCount: displayLayout.displayTrackOrder.length,
|
|
@@ -422,12 +417,6 @@ export function useTimelineProviderState({
|
|
|
422
417
|
timeDisplayMode,
|
|
423
418
|
timelineFocus.rowVirtualizationActive ? renderTimeRange : undefined,
|
|
424
419
|
);
|
|
425
|
-
const getPreviewElement = useCallback(
|
|
426
|
-
(element: TimelineElement): TimelineElement => getTimelinePreviewElement(element, resizingClip),
|
|
427
|
-
[resizingClip],
|
|
428
|
-
);
|
|
429
|
-
const draggedElement = draggedClip?.element ?? null;
|
|
430
|
-
const multiDragPreview = resolveMultiDragPreview(draggedClip, selectedElementIds);
|
|
431
420
|
const canvasProps = {
|
|
432
421
|
major,
|
|
433
422
|
minor,
|
|
@@ -502,7 +491,7 @@ export function useTimelineProviderState({
|
|
|
502
491
|
onMoveElement,
|
|
503
492
|
beatDragging,
|
|
504
493
|
draggedElement,
|
|
505
|
-
snapGuide
|
|
494
|
+
snapGuide,
|
|
506
495
|
multiDragPreview,
|
|
507
496
|
onToggleTrackHidden: editContext.onToggleTrackHidden,
|
|
508
497
|
onTogglePropertyGroupKeyframe: editContext.onTogglePropertyGroupKeyframe,
|
|
@@ -520,60 +509,34 @@ export function useTimelineProviderState({
|
|
|
520
509
|
)),
|
|
521
510
|
[holdNewClipContent, renderClipContent],
|
|
522
511
|
);
|
|
523
|
-
const timelineMeta =
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
512
|
+
const timelineMeta = buildTimelineShellMeta({
|
|
513
|
+
isDragOver: assetDrop.isDragOver,
|
|
514
|
+
hasFileDrop: !!onFileDrop,
|
|
515
|
+
drop: {
|
|
527
516
|
onDragOver: assetDrop.handleAssetDragOver,
|
|
528
517
|
onDragLeave: assetDrop.handleAssetDragLeave,
|
|
529
518
|
onDrop: assetDrop.handleAssetDrop,
|
|
530
519
|
},
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
"aria-label": "Timeline track view",
|
|
534
|
-
"data-timeline-element-count": timelineElements.length,
|
|
535
|
-
isDragOver: assetDrop.isDragOver,
|
|
536
|
-
activeTool,
|
|
537
|
-
shiftHeld,
|
|
538
|
-
accentClass: "ring-1 ring-inset ring-studio-accent/60",
|
|
539
|
-
onMouseMove: updateRazorGuide,
|
|
540
|
-
onMouseLeave: clearRazorGuide,
|
|
541
|
-
style: {
|
|
542
|
-
touchAction: "pan-x pan-y",
|
|
543
|
-
background: theme.shellBackground,
|
|
544
|
-
borderColor: theme.shellBorder,
|
|
545
|
-
},
|
|
546
|
-
},
|
|
547
|
-
viewport: {
|
|
548
|
-
ref: setScrollRef,
|
|
549
|
-
tabIndex: -1,
|
|
550
|
-
labelMode,
|
|
551
|
-
zoomMode,
|
|
552
|
-
onScroll: (e) => {
|
|
553
|
-
lastScrollLeftRef.current = e.currentTarget.scrollLeft;
|
|
554
|
-
recordTimelineScroll(e.currentTarget);
|
|
555
|
-
syncScrollViewport(e.currentTarget, true);
|
|
556
|
-
},
|
|
557
|
-
...timelineFocus.timelineFocusProps,
|
|
558
|
-
onDragOver: assetDrop.handleAssetDragOver,
|
|
559
|
-
onDragLeave: assetDrop.handleAssetDragLeave,
|
|
560
|
-
onDrop: assetDrop.handleAssetDrop,
|
|
561
|
-
onPointerDown: (e) => {
|
|
562
|
-
if (shouldIgnoreTimelinePointerDown(e.target)) return;
|
|
563
|
-
if (splitAllAtPointer(e)) return;
|
|
564
|
-
handlePointerDown(e);
|
|
565
|
-
},
|
|
520
|
+
pointer: {
|
|
521
|
+
onPointerDown: handlePointerDown,
|
|
566
522
|
onPointerMove: handlePointerMove,
|
|
567
523
|
onPointerUp: handlePointerUp,
|
|
568
524
|
onPointerCancel: handlePointerCancel,
|
|
569
|
-
onLostPointerCapture: handlePointerCancel,
|
|
570
525
|
},
|
|
526
|
+
setContainerRef,
|
|
527
|
+
setScrollRef,
|
|
528
|
+
focusProps: timelineFocus.timelineFocusProps,
|
|
571
529
|
elementCount: timelineElements.length,
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
530
|
+
activeTool,
|
|
531
|
+
shiftHeld,
|
|
532
|
+
labelMode,
|
|
533
|
+
contentOrigin,
|
|
534
|
+
zoomMode,
|
|
535
|
+
theme,
|
|
536
|
+
razor: { razorGuideX, updateRazorGuide, clearRazorGuide, splitAllAtPointer },
|
|
537
|
+
lastScrollLeftRef,
|
|
538
|
+
recordTimelineScroll,
|
|
539
|
+
syncScrollViewport,
|
|
577
540
|
});
|
|
578
541
|
const contextValue: TimelineContextValue = {
|
|
579
542
|
state: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// fallow-ignore-file dead-code
|
|
2
|
+
import { useCallback } from "react";
|
|
2
3
|
import { usePlayerStore, type ZoomMode } from "../store/playerStore";
|
|
3
4
|
|
|
4
5
|
export interface TimelineZoomState {
|
|
@@ -13,6 +14,13 @@ export function useTimelineZoom(): TimelineZoomState {
|
|
|
13
14
|
const zoomMode = usePlayerStore((s) => s.zoomMode);
|
|
14
15
|
const manualZoomPercent = usePlayerStore((s) => s.manualZoomPercent);
|
|
15
16
|
const setZoomMode = usePlayerStore((s) => s.setZoomMode);
|
|
16
|
-
const
|
|
17
|
+
const setStorePercent = usePlayerStore((s) => s.setManualZoomPercent);
|
|
18
|
+
const setManualZoomPercent = useCallback(
|
|
19
|
+
(percent: number) => {
|
|
20
|
+
usePlayerStore.setState((s) => ({ userZoomCount: s.userZoomCount + 1 }));
|
|
21
|
+
setStorePercent(percent);
|
|
22
|
+
},
|
|
23
|
+
[setStorePercent],
|
|
24
|
+
);
|
|
17
25
|
return { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent };
|
|
18
26
|
}
|
|
@@ -6,6 +6,8 @@ import type { useTimelinePlayer } from "./useTimelinePlayer";
|
|
|
6
6
|
import {
|
|
7
7
|
attachIframeAdapter,
|
|
8
8
|
attachIframeWindow,
|
|
9
|
+
makeAdapterWindow,
|
|
10
|
+
makeFakeIframe,
|
|
9
11
|
renderTimelinePlayerHarness,
|
|
10
12
|
resetPlayerStore,
|
|
11
13
|
} from "./timelinePlayerTestHarness";
|
|
@@ -263,6 +265,48 @@ describe("useTimelinePlayer audio controls (#835)", () => {
|
|
|
263
265
|
});
|
|
264
266
|
|
|
265
267
|
describe("useTimelinePlayer seek keepPlaying option (#834)", () => {
|
|
268
|
+
it("publishes a seek as a person's seek, so a paused timeline follows it", () => {
|
|
269
|
+
const { root, api } = renderAttachedTimelinePlayer();
|
|
270
|
+
const before = liveTime.seekCount();
|
|
271
|
+
seekWithAct(api, 5);
|
|
272
|
+
expect(liveTime.seekCount()).toBe(before + 1);
|
|
273
|
+
expect(liveTime.latest()).toBe(5);
|
|
274
|
+
unmountWithAct(root);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("does not count a reload's hand-over as a person's seek", () => {
|
|
278
|
+
const { getApi, root } = renderTimelinePlayerHarness();
|
|
279
|
+
act(() => {
|
|
280
|
+
getApi().iframeRef.current = makeFakeIframe(makeAdapterWindow().win);
|
|
281
|
+
getApi().onIframeLoad();
|
|
282
|
+
});
|
|
283
|
+
seekWithAct(getApi(), 7);
|
|
284
|
+
act(() => getApi().refreshPlayer());
|
|
285
|
+
const seeks = liveTime.seekCount();
|
|
286
|
+
const gen = getApi().previewSlots.find((s) => s.role === "shadow")!.gen;
|
|
287
|
+
const shadow = makeFakeIframe(makeAdapterWindow().win);
|
|
288
|
+
shadow.src = "http://localhost/api/projects/demo/preview?_t=1";
|
|
289
|
+
act(() => {
|
|
290
|
+
getApi().setShadowIframeNode(shadow);
|
|
291
|
+
getApi().onShadowIframeLoad(gen);
|
|
292
|
+
getApi().onShadowReadyChange(gen, true);
|
|
293
|
+
});
|
|
294
|
+
expect(getApi().iframeRef.current).toBe(shadow);
|
|
295
|
+
expect(liveTime.latest()).toBe(7);
|
|
296
|
+
expect(liveTime.seekCount()).toBe(seeks);
|
|
297
|
+
unmountWithAct(root);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("does not count the audition's return to the paused time as a person's seek", () => {
|
|
301
|
+
const { api, root } = renderAttachedTimelinePlayer();
|
|
302
|
+
seekWithAct(api, 4);
|
|
303
|
+
const seeks = liveTime.seekCount();
|
|
304
|
+
act(() => usePlayerStore.getState().requestPlayback(false, 2));
|
|
305
|
+
expect(liveTime.latest()).toBe(2);
|
|
306
|
+
expect(liveTime.seekCount()).toBe(seeks);
|
|
307
|
+
unmountWithAct(root);
|
|
308
|
+
});
|
|
309
|
+
|
|
266
310
|
it("default seek() clears isPlaying when the store reports playing", () => {
|
|
267
311
|
const { api, root } = renderAttachedTimelinePlayer();
|
|
268
312
|
setStorePlaying();
|
|
@@ -47,6 +47,9 @@ export interface UseTimelinePlayerOptions {
|
|
|
47
47
|
onPreviewReloadFailed?: (message: string) => void;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const publishSeek = (time: number, options?: { follow?: boolean }) =>
|
|
51
|
+
options?.follow === false ? liveTime.notify(time) : liveTime.notifySeek(time);
|
|
52
|
+
|
|
50
53
|
export function useTimelinePlayer({
|
|
51
54
|
onShadowPromoted,
|
|
52
55
|
onPreviewReloadFailed,
|
|
@@ -276,7 +279,7 @@ export function useTimelinePlayer({
|
|
|
276
279
|
stopRAFLoop();
|
|
277
280
|
}, [getAdapter, setCurrentTime, setIsPlaying, stopRAFLoop, stopReverseLoop]);
|
|
278
281
|
const seek = useCallback(
|
|
279
|
-
(time: number, options?: { keepPlaying?: boolean }) => {
|
|
282
|
+
(time: number, options?: { keepPlaying?: boolean; follow?: boolean }) => {
|
|
280
283
|
const wasReverseShuttle = shuttleDirectionRef.current === "backward";
|
|
281
284
|
stopReverseLoop();
|
|
282
285
|
const adapter = getAdapter();
|
|
@@ -295,7 +298,7 @@ export function useTimelinePlayer({
|
|
|
295
298
|
nextTime,
|
|
296
299
|
});
|
|
297
300
|
adapter.seek(nextTime, options);
|
|
298
|
-
|
|
301
|
+
publishSeek(nextTime, options); // Direct DOM updates (playhead, timecode, progress) — no re-render
|
|
299
302
|
setCurrentTime(nextTime); // sync store so Split/Delete have accurate time
|
|
300
303
|
if (!shouldResumeAfterSeek && !keepPlaying) scrubMusicAtSeek(iframeRef.current, nextTime);
|
|
301
304
|
if (shouldResumeAfterSeek) {
|
|
@@ -344,7 +347,7 @@ export function useTimelinePlayer({
|
|
|
344
347
|
if (request.playing) play();
|
|
345
348
|
else {
|
|
346
349
|
pause();
|
|
347
|
-
if (request.returnTo !== null) seek(request.returnTo);
|
|
350
|
+
if (request.returnTo !== null) seek(request.returnTo, { follow: false });
|
|
348
351
|
}
|
|
349
352
|
usePlayerStore.getState().clearPlaybackRequest();
|
|
350
353
|
}
|
|
@@ -409,7 +412,7 @@ export function useTimelinePlayer({
|
|
|
409
412
|
onReloadFailed: onPreviewReloadFailed,
|
|
410
413
|
handOverPlayback: (time, playing) => {
|
|
411
414
|
// keepPlaying: move the playhead without the paused-seek audio scrub.
|
|
412
|
-
seek(time, { keepPlaying: true });
|
|
415
|
+
seek(time, { keepPlaying: true, follow: false });
|
|
413
416
|
const adapter = getAdapter();
|
|
414
417
|
// An edit that cut the film short of the live time stops it at the new end, as playback does.
|
|
415
418
|
if (playing && adapter && adapter.getTime() < adapter.getDuration()) play();
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
mergeTimelineElementsPreservingDowngrades,
|
|
8
8
|
} from "./timelineDOM";
|
|
9
9
|
import { isTimelineIgnoredElement } from "./timelineElementHelpers";
|
|
10
|
+
import { clipTimingStart, resolveClipTimingBasis } from "../../hooks/gsapShared";
|
|
11
|
+
import { toAuthoredStart } from "../store/timelineElement";
|
|
10
12
|
import { computeResizePreview } from "../components/timelineClipDragPreview";
|
|
11
13
|
import type { TimelineElement } from "../store/playerStore";
|
|
12
14
|
import {
|
|
@@ -25,6 +27,83 @@ function makeDoc(html: string): Document {
|
|
|
25
27
|
return d;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
describe("parseTimelineFromDOM — nested master time", () => {
|
|
31
|
+
it("adds every enclosing host's start to a clip inside a sub-composition", () => {
|
|
32
|
+
const doc = makeDoc(`
|
|
33
|
+
<div data-composition-id="main" data-start="0" data-duration="20">
|
|
34
|
+
<div id="intro" data-composition-id="intro" data-start="2" data-duration="10">
|
|
35
|
+
<div data-composition-id="intro">
|
|
36
|
+
<div id="logo" data-composition-id="logo" data-start="3" data-duration="5">
|
|
37
|
+
<div data-composition-id="logo">
|
|
38
|
+
<div id="badge" class="clip" data-start="1" data-duration="2"></div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
`);
|
|
45
|
+
const starts = parseTimelineFromDOM(doc, 20).map((e) => [
|
|
46
|
+
e.domId,
|
|
47
|
+
e.start,
|
|
48
|
+
e.parentCompositionStart,
|
|
49
|
+
]);
|
|
50
|
+
expect(starts).toEqual([
|
|
51
|
+
["intro", 2, 0],
|
|
52
|
+
["logo", 5, 2],
|
|
53
|
+
["badge", 6, 5],
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("places a clip inside a referenced scene after the scene's authored length", () => {
|
|
58
|
+
const doc = makeDoc(`
|
|
59
|
+
<div data-composition-id="main" data-start="0" data-duration="20">
|
|
60
|
+
<div id="s1" data-composition-id="s1" data-start="0" data-hf-authored-duration="8"></div>
|
|
61
|
+
<div id="s2" data-composition-id="s2" data-start="s1 + 1" data-duration="6">
|
|
62
|
+
<div data-composition-id="s2">
|
|
63
|
+
<div id="c" class="clip" data-start="1" data-duration="2"></div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
`);
|
|
68
|
+
const timelines = { s1: { duration: () => 6 } } as never;
|
|
69
|
+
const c = parseTimelineFromDOM(doc, 20, timelines).find((e) => e.domId === "c");
|
|
70
|
+
expect(c?.start).toBe(10);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("parseTimelineFromDOM — nested rows' keyframe basis", () => {
|
|
75
|
+
const doc = () =>
|
|
76
|
+
makeDoc(`
|
|
77
|
+
<div data-composition-id="main" data-start="0" data-duration="20">
|
|
78
|
+
<div id="intro" data-composition-id="intro" data-start="2" data-duration="10">
|
|
79
|
+
<div data-composition-id="intro">
|
|
80
|
+
<video id="vo" data-start="7" data-duration="2" data-hf-media-start-basis="global"></video>
|
|
81
|
+
<div id="logo" data-composition-id="logo" data-start="3" data-duration="5">
|
|
82
|
+
<div data-composition-id="logo">
|
|
83
|
+
<div id="badge" class="clip" data-start="1" data-duration="2"></div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
it("measures diamonds and keyframe percentages against the local tween clock", () => {
|
|
92
|
+
const rows = parseTimelineFromDOM(doc(), 20);
|
|
93
|
+
const basis = (id: string) => resolveClipTimingBasis(id, "index.html", rows, []).elStart;
|
|
94
|
+
const at = (id: string) => rows.find((e) => e.domId === id)!;
|
|
95
|
+
expect([clipTimingStart(at("logo")), basis("logo")]).toEqual([3, 3]);
|
|
96
|
+
expect([clipTimingStart(at("badge")), basis("badge")]).toEqual([1, 1]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("keys a legacy root-time video on its host's clock but writes its start as master time", () => {
|
|
100
|
+
const vo = parseTimelineFromDOM(doc(), 20).find((e) => e.domId === "vo")!;
|
|
101
|
+
expect(vo.start).toBe(7);
|
|
102
|
+
expect(clipTimingStart(vo)).toBe(5);
|
|
103
|
+
expect(toAuthoredStart(vo, 8)).toBe(8);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
28
107
|
describe("parseTimelineFromDOM — media in-point", () => {
|
|
29
108
|
it("reads a negative in-point as 0, as the runtime does, so a head trim keeps the clip", () => {
|
|
30
109
|
const doc = makeDoc(`
|
|
@@ -12,6 +12,7 @@ import type { TimelineElement } from "../store/playerStore";
|
|
|
12
12
|
import type { ClipManifestClip, IframeWindow, TimelineLike } from "./playbackTypes";
|
|
13
13
|
import { resolveCssStackingContextId } from "@hyperframes/core/runtime/stacking-context";
|
|
14
14
|
import { readClipTiming } from "@hyperframes/core/composition-contract";
|
|
15
|
+
import { createRuntimeStartTimeResolver } from "@hyperframes/core/runtime/start-resolver";
|
|
15
16
|
import { groupInfoFor } from "./timelineGroupInfo";
|
|
16
17
|
import { transitionLabelsForDocument } from "./timelineTransitionMetadata";
|
|
17
18
|
import {
|
|
@@ -228,10 +229,13 @@ export function parseTimelineFromDOM(
|
|
|
228
229
|
const nodes = doc.querySelectorAll("[data-start]");
|
|
229
230
|
const els: TimelineElement[] = [];
|
|
230
231
|
let trackCounter = 0;
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
const timelineRegistry = timelines ?? (doc.defaultView as IframeWindow | null)?.__timelines;
|
|
233
|
+
const transitionLabels = transitionLabelsForDocument(doc, timelineRegistry);
|
|
234
|
+
const masterStart = createRuntimeStartTimeResolver({
|
|
235
|
+
timelineRegistry,
|
|
236
|
+
includeAuthoredTimingAttrs: true,
|
|
237
|
+
documentRef: doc,
|
|
238
|
+
});
|
|
235
239
|
|
|
236
240
|
// fallow-ignore-next-line complexity
|
|
237
241
|
nodes.forEach((node) => {
|
|
@@ -239,11 +243,14 @@ export function parseTimelineFromDOM(
|
|
|
239
243
|
if (isTimelineIgnoredElement(node)) return;
|
|
240
244
|
const el = node as HTMLElement;
|
|
241
245
|
const timing = readClipTiming(el);
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
if (timing.start == null) return;
|
|
247
|
+
const tagLower = el.tagName.toLowerCase();
|
|
248
|
+
const start =
|
|
249
|
+
tagLower === "video" || tagLower === "audio"
|
|
250
|
+
? masterStart.resolveMediaStartForElement(el)
|
|
251
|
+
: masterStart.resolveStartForElement(el);
|
|
244
252
|
if (Number.isFinite(rootDuration) && rootDuration > 0 && start >= rootDuration) return;
|
|
245
253
|
|
|
246
|
-
const tagLower = el.tagName.toLowerCase();
|
|
247
254
|
let dur = timing.duration ?? 0;
|
|
248
255
|
if (dur <= 0) dur = Math.max(0, rootDuration - start);
|
|
249
256
|
if (Number.isFinite(rootDuration) && rootDuration > 0) {
|
|
@@ -287,6 +294,8 @@ export function parseTimelineFromDOM(
|
|
|
287
294
|
: "element",
|
|
288
295
|
tag: tagLower,
|
|
289
296
|
start,
|
|
297
|
+
parentCompositionStart: masterStart.resolveHostStartForElement(el),
|
|
298
|
+
...(masterStart.isRootGlobalMediaStartForElement(el) && { authoredStartIsMasterTime: true }),
|
|
290
299
|
duration: dur,
|
|
291
300
|
track,
|
|
292
301
|
domId: el.id || undefined,
|
|
@@ -103,6 +103,42 @@ describe("buildMissingCompositionElements — hfId (R7)", () => {
|
|
|
103
103
|
});
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
describe("buildMissingCompositionElements — nested master time", () => {
|
|
107
|
+
it("places hosts two and three levels deep at their master time", () => {
|
|
108
|
+
const doc = makeDoc(`
|
|
109
|
+
<div data-composition-id="main" data-start="0" data-duration="20">
|
|
110
|
+
<div id="intro" data-composition-id="intro" data-start="2" data-duration="10">
|
|
111
|
+
<div data-composition-id="intro">
|
|
112
|
+
<div id="logo" data-composition-id="logo" data-start="3" data-duration="5">
|
|
113
|
+
<div data-composition-id="logo">
|
|
114
|
+
<div id="badge" data-composition-id="badge" data-start="1" data-duration="2"></div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
`);
|
|
121
|
+
const { missing } = buildMissingCompositionElements(doc, window as IframeWindow, [], 20);
|
|
122
|
+
expect(missing.map((e) => [e.domId, e.start, e.duration, e.parentCompositionStart])).toEqual([
|
|
123
|
+
["intro", 2, 10, 0],
|
|
124
|
+
["logo", 5, 5, 2],
|
|
125
|
+
["badge", 6, 2, 5],
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("starts a host after a referenced scene's authored length", () => {
|
|
130
|
+
const doc = makeDoc(`
|
|
131
|
+
<div data-composition-id="main" data-start="0" data-duration="20">
|
|
132
|
+
<div id="s1" data-composition-id="s1" data-start="0" data-hf-authored-duration="8"></div>
|
|
133
|
+
<div id="s2" data-composition-id="s2" data-start="s1 + 1" data-duration="4"></div>
|
|
134
|
+
</div>
|
|
135
|
+
`);
|
|
136
|
+
const win = { __timelines: { s1: { duration: () => 6 } } } as unknown as IframeWindow;
|
|
137
|
+
const { missing } = buildMissingCompositionElements(doc, win, [], 20);
|
|
138
|
+
expect(missing.find((e) => e.domId === "s2")?.start).toBe(9);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
106
142
|
describe("setPreviewMediaVolume", () => {
|
|
107
143
|
it("sends a clamped runtime volume to a direct preview iframe", () => {
|
|
108
144
|
const iframe = document.createElement("iframe");
|