@hyperframes/studio 0.6.0 → 0.6.2
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-CzwFysqv.js +418 -0
- package/dist/assets/index-hYc4aP7M.js +117 -0
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/App.tsx +2 -13
- package/src/captions/components/CaptionOverlay.tsx +13 -246
- package/src/captions/components/CaptionOverlayUtils.ts +221 -0
- package/src/components/StudioPreviewArea.tsx +6 -2
- package/src/components/editor/DomEditOverlay.tsx +88 -1007
- package/src/components/editor/EaseCurveEditor.tsx +221 -0
- package/src/components/editor/FileTree.tsx +13 -621
- package/src/components/editor/FileTreeIcons.tsx +128 -0
- package/src/components/editor/FileTreeNodes.tsx +496 -0
- package/src/components/editor/MotionPanel.tsx +16 -390
- package/src/components/editor/MotionPanelFields.tsx +185 -0
- package/src/components/editor/domEditOverlayGeometry.ts +211 -0
- package/src/components/editor/domEditOverlayGestures.ts +138 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
- package/src/components/editor/domEditing.ts +44 -1150
- package/src/components/editor/domEditingAgentPrompt.ts +97 -0
- package/src/components/editor/domEditingDom.ts +266 -0
- package/src/components/editor/domEditingElement.ts +329 -0
- package/src/components/editor/domEditingLayers.ts +460 -0
- package/src/components/editor/domEditingTypes.ts +125 -0
- package/src/components/editor/manualEdits.ts +84 -1081
- package/src/components/editor/manualEditsDom.ts +436 -0
- package/src/components/editor/manualEditsParsing.ts +280 -0
- package/src/components/editor/manualEditsSnapshot.ts +333 -0
- package/src/components/editor/manualEditsTypes.ts +141 -0
- package/src/components/editor/studioMotion.ts +47 -434
- package/src/components/editor/studioMotionOps.ts +299 -0
- package/src/components/editor/studioMotionTypes.ts +168 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
- package/src/components/editor/useDomEditOverlayRects.ts +207 -0
- package/src/components/nle/NLELayout.tsx +60 -144
- package/src/components/nle/useCompositionStack.ts +126 -0
- package/src/hooks/useToast.ts +20 -0
- package/src/player/components/Timeline.tsx +189 -1418
- package/src/player/components/TimelineCanvas.tsx +434 -0
- package/src/player/components/TimelineEmptyState.tsx +102 -0
- package/src/player/components/TimelineRuler.tsx +90 -0
- package/src/player/components/timelineIcons.tsx +49 -0
- package/src/player/components/timelineLayout.ts +215 -0
- package/src/player/components/timelineUtils.ts +211 -0
- package/src/player/components/useTimelineClipDrag.ts +388 -0
- package/src/player/components/useTimelinePlayhead.ts +200 -0
- package/src/player/components/useTimelineRangeSelection.ts +135 -0
- package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
- package/src/player/hooks/useTimelinePlayer.ts +69 -1372
- package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
- package/src/player/lib/playbackAdapter.ts +145 -0
- package/src/player/lib/playbackShortcuts.ts +68 -0
- package/src/player/lib/playbackTypes.ts +60 -0
- package/src/player/lib/timelineDOM.ts +373 -0
- package/src/player/lib/timelineElementHelpers.ts +303 -0
- package/src/player/lib/timelineIframeHelpers.ts +269 -0
- package/dist/assets/hyperframes-player-DOFETgjy.js +0 -418
- package/dist/assets/index-DUqUmaoH.js +0 -117
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useRef, useState, useCallback } from "react";
|
|
2
|
+
import { buildClipRangeSelection, type TimelineRangeSelection } from "./timelineEditing";
|
|
3
|
+
import type { TimelineElement } from "../store/playerStore";
|
|
4
|
+
import { liveTime } from "../store/playerStore";
|
|
5
|
+
import { GUTTER } from "./timelineLayout";
|
|
6
|
+
|
|
7
|
+
interface UseTimelineRangeSelectionInput {
|
|
8
|
+
scrollRef: React.RefObject<HTMLDivElement | null>;
|
|
9
|
+
ppsRef: React.RefObject<number>;
|
|
10
|
+
effectiveDuration: number;
|
|
11
|
+
pps: number;
|
|
12
|
+
onSeek?: (time: number) => void;
|
|
13
|
+
seekFromX: (clientX: number) => void;
|
|
14
|
+
autoScrollDuringDrag: (clientX: number) => void;
|
|
15
|
+
dragScrollRaf: React.RefObject<number>;
|
|
16
|
+
isDragging: React.RefObject<boolean>;
|
|
17
|
+
setShowPopover: (v: boolean) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useTimelineRangeSelection({
|
|
21
|
+
scrollRef,
|
|
22
|
+
ppsRef: _ppsRef,
|
|
23
|
+
effectiveDuration: _effectiveDuration,
|
|
24
|
+
pps,
|
|
25
|
+
onSeek: _onSeek,
|
|
26
|
+
seekFromX,
|
|
27
|
+
autoScrollDuringDrag,
|
|
28
|
+
dragScrollRaf,
|
|
29
|
+
isDragging,
|
|
30
|
+
setShowPopover,
|
|
31
|
+
}: UseTimelineRangeSelectionInput) {
|
|
32
|
+
const isRangeSelecting = useRef(false);
|
|
33
|
+
const rangeAnchorTime = useRef(0);
|
|
34
|
+
const [rangeSelection, setRangeSelection] = useState<TimelineRangeSelection | null>(null);
|
|
35
|
+
const shiftClickClipRef = useRef<{
|
|
36
|
+
element: TimelineElement;
|
|
37
|
+
anchorX: number;
|
|
38
|
+
anchorY: number;
|
|
39
|
+
} | null>(null);
|
|
40
|
+
|
|
41
|
+
const handlePointerDown = useCallback(
|
|
42
|
+
(e: React.PointerEvent) => {
|
|
43
|
+
if (e.button !== 0) return;
|
|
44
|
+
if (e.shiftKey) {
|
|
45
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
46
|
+
isRangeSelecting.current = true;
|
|
47
|
+
setShowPopover(false);
|
|
48
|
+
const rect = scrollRef.current?.getBoundingClientRect();
|
|
49
|
+
if (rect) {
|
|
50
|
+
const x = e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - GUTTER;
|
|
51
|
+
const time = Math.max(0, x / pps);
|
|
52
|
+
rangeAnchorTime.current = time;
|
|
53
|
+
setRangeSelection({ start: time, end: time, anchorX: e.clientX, anchorY: e.clientY });
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
shiftClickClipRef.current = null;
|
|
58
|
+
if ((e.target as HTMLElement).closest("[data-clip]")) return;
|
|
59
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
60
|
+
isDragging.current = true;
|
|
61
|
+
setRangeSelection(null);
|
|
62
|
+
setShowPopover(false);
|
|
63
|
+
seekFromX(e.clientX);
|
|
64
|
+
},
|
|
65
|
+
[seekFromX, pps, scrollRef, isDragging, setShowPopover],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const handlePointerMove = useCallback(
|
|
69
|
+
(e: React.PointerEvent) => {
|
|
70
|
+
if (isRangeSelecting.current) {
|
|
71
|
+
const rect = scrollRef.current?.getBoundingClientRect();
|
|
72
|
+
if (rect) {
|
|
73
|
+
const x = e.clientX - rect.left + (scrollRef.current?.scrollLeft ?? 0) - GUTTER;
|
|
74
|
+
setRangeSelection((prev) =>
|
|
75
|
+
prev
|
|
76
|
+
? { ...prev, end: Math.max(0, x / pps), anchorX: e.clientX, anchorY: e.clientY }
|
|
77
|
+
: null,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!isDragging.current) return;
|
|
83
|
+
seekFromX(e.clientX);
|
|
84
|
+
autoScrollDuringDrag(e.clientX);
|
|
85
|
+
},
|
|
86
|
+
[seekFromX, autoScrollDuringDrag, pps, scrollRef, isDragging],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const handlePointerUp = useCallback(() => {
|
|
90
|
+
if (isRangeSelecting.current) {
|
|
91
|
+
isRangeSelecting.current = false;
|
|
92
|
+
const pendingShiftClick = shiftClickClipRef.current;
|
|
93
|
+
shiftClickClipRef.current = null;
|
|
94
|
+
setRangeSelection((prev) => {
|
|
95
|
+
if (prev && pendingShiftClick && Math.abs(prev.end - prev.start) <= 0.2) {
|
|
96
|
+
setShowPopover(true);
|
|
97
|
+
return buildClipRangeSelection(pendingShiftClick.element, pendingShiftClick);
|
|
98
|
+
}
|
|
99
|
+
if (prev && Math.abs(prev.end - prev.start) > 0.2) {
|
|
100
|
+
setShowPopover(true);
|
|
101
|
+
return prev;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
isDragging.current = false;
|
|
108
|
+
cancelAnimationFrame(dragScrollRaf.current);
|
|
109
|
+
}, [isDragging, dragScrollRaf, setShowPopover]);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
rangeSelection,
|
|
113
|
+
setRangeSelection,
|
|
114
|
+
shiftClickClipRef,
|
|
115
|
+
handlePointerDown,
|
|
116
|
+
handlePointerMove,
|
|
117
|
+
handlePointerUp,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ── Seek + scroll utilities (used in Timeline only) ──────────────── */
|
|
122
|
+
export function seekTimeFromScrollX(
|
|
123
|
+
scrollEl: HTMLDivElement,
|
|
124
|
+
clientX: number,
|
|
125
|
+
effectiveDuration: number,
|
|
126
|
+
pps: number,
|
|
127
|
+
onSeek?: (time: number) => void,
|
|
128
|
+
): void {
|
|
129
|
+
const rect = scrollEl.getBoundingClientRect();
|
|
130
|
+
const x = clientX - rect.left + scrollEl.scrollLeft - GUTTER;
|
|
131
|
+
if (x < 0) return;
|
|
132
|
+
const time = Math.max(0, Math.min(effectiveDuration, x / pps));
|
|
133
|
+
liveTime.notify(time);
|
|
134
|
+
onSeek?.(time);
|
|
135
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard shortcut handler for playback (Space/JKL/Arrow keys) and
|
|
3
|
+
* iframe shortcut listener setup.
|
|
4
|
+
*
|
|
5
|
+
* Accepts stable playback callbacks and returns the keyboard event handlers
|
|
6
|
+
* and iframe listener setup function. Has no side effects of its own.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useRef, useCallback } from "react";
|
|
10
|
+
import { useCaptionStore } from "../../captions/store";
|
|
11
|
+
import { shouldIgnorePlaybackShortcutEvent, SHUTTLE_SPEEDS } from "../lib/playbackShortcuts";
|
|
12
|
+
import { usePlayerStore } from "../store/playerStore";
|
|
13
|
+
import { stepFrameTime, STUDIO_PREVIEW_FPS } from "../lib/time";
|
|
14
|
+
import type { PlaybackAdapter } from "../lib/playbackTypes";
|
|
15
|
+
|
|
16
|
+
interface UsePlaybackKeyboardParams {
|
|
17
|
+
iframeRef: React.RefObject<HTMLIFrameElement | null>;
|
|
18
|
+
shuttleDirectionRef: React.MutableRefObject<"forward" | "backward" | null>;
|
|
19
|
+
shuttleSpeedIndexRef: React.MutableRefObject<number>;
|
|
20
|
+
iframeShortcutCleanupRef: React.MutableRefObject<(() => void) | null>;
|
|
21
|
+
getAdapter: () => PlaybackAdapter | null;
|
|
22
|
+
play: () => void;
|
|
23
|
+
playBackward: (rate: number) => void;
|
|
24
|
+
pause: () => void;
|
|
25
|
+
seek: (time: number) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function usePlaybackKeyboard({
|
|
29
|
+
iframeRef,
|
|
30
|
+
shuttleDirectionRef,
|
|
31
|
+
shuttleSpeedIndexRef,
|
|
32
|
+
iframeShortcutCleanupRef,
|
|
33
|
+
getAdapter,
|
|
34
|
+
play,
|
|
35
|
+
playBackward,
|
|
36
|
+
pause,
|
|
37
|
+
seek,
|
|
38
|
+
}: UsePlaybackKeyboardParams) {
|
|
39
|
+
const pressedCodesRef = useRef(new Set<string>());
|
|
40
|
+
const playbackKeyDownRef = useRef<(e: KeyboardEvent) => void>(() => {});
|
|
41
|
+
const playbackKeyUpRef = useRef<(e: KeyboardEvent) => void>(() => {});
|
|
42
|
+
|
|
43
|
+
const stepFrames = useCallback(
|
|
44
|
+
(deltaFrames: number) => {
|
|
45
|
+
const adapter = getAdapter();
|
|
46
|
+
const currentTime = adapter?.getTime() ?? usePlayerStore.getState().currentTime;
|
|
47
|
+
seek(stepFrameTime(currentTime, deltaFrames, STUDIO_PREVIEW_FPS));
|
|
48
|
+
},
|
|
49
|
+
[getAdapter, seek],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const shuttle = useCallback(
|
|
53
|
+
(direction: "forward" | "backward") => {
|
|
54
|
+
if (shuttleDirectionRef.current === direction) {
|
|
55
|
+
shuttleSpeedIndexRef.current = Math.min(
|
|
56
|
+
shuttleSpeedIndexRef.current + 1,
|
|
57
|
+
SHUTTLE_SPEEDS.length - 1,
|
|
58
|
+
);
|
|
59
|
+
} else {
|
|
60
|
+
shuttleSpeedIndexRef.current = 0;
|
|
61
|
+
}
|
|
62
|
+
const speed = SHUTTLE_SPEEDS[shuttleSpeedIndexRef.current];
|
|
63
|
+
usePlayerStore.getState().setPlaybackRate(speed);
|
|
64
|
+
if (direction === "forward") {
|
|
65
|
+
play();
|
|
66
|
+
} else {
|
|
67
|
+
playBackward(speed);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[play, playBackward, shuttleDirectionRef, shuttleSpeedIndexRef],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const togglePlay = useCallback(() => {
|
|
74
|
+
if (usePlayerStore.getState().isPlaying) {
|
|
75
|
+
pause();
|
|
76
|
+
} else {
|
|
77
|
+
play();
|
|
78
|
+
}
|
|
79
|
+
}, [play, pause]);
|
|
80
|
+
|
|
81
|
+
const handlePlaybackKeyDown = useCallback(
|
|
82
|
+
(e: KeyboardEvent) => {
|
|
83
|
+
if (e.defaultPrevented) return;
|
|
84
|
+
const captionState = useCaptionStore.getState();
|
|
85
|
+
if (
|
|
86
|
+
shouldIgnorePlaybackShortcutEvent(e, {
|
|
87
|
+
isCaptionEditMode: captionState.isEditMode,
|
|
88
|
+
selectedCaptionSegmentCount: captionState.selectedSegmentIds.size,
|
|
89
|
+
})
|
|
90
|
+
) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
pressedCodesRef.current.add(e.code);
|
|
94
|
+
if (e.code === "Space") {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
togglePlay();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (e.code === "ArrowLeft") {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
stepFrames(e.shiftKey ? -10 : -1);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (e.code === "ArrowRight") {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
stepFrames(e.shiftKey ? 10 : 1);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (e.repeat) return;
|
|
110
|
+
if (e.code === "KeyK") {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
pause();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (e.code === "KeyJ") {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
if (pressedCodesRef.current.has("KeyK")) {
|
|
118
|
+
stepFrames(-1);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
shuttle("backward");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (e.code === "KeyL") {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
if (pressedCodesRef.current.has("KeyK")) {
|
|
127
|
+
stepFrames(1);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
shuttle("forward");
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
[pause, shuttle, stepFrames, togglePlay],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const handlePlaybackKeyUp = useCallback((e: KeyboardEvent) => {
|
|
137
|
+
pressedCodesRef.current.delete(e.code);
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
playbackKeyDownRef.current = handlePlaybackKeyDown;
|
|
141
|
+
playbackKeyUpRef.current = handlePlaybackKeyUp;
|
|
142
|
+
|
|
143
|
+
const attachIframeShortcutListeners = useCallback(() => {
|
|
144
|
+
iframeShortcutCleanupRef.current?.();
|
|
145
|
+
iframeShortcutCleanupRef.current = null;
|
|
146
|
+
|
|
147
|
+
const iframeWin = iframeRef.current?.contentWindow;
|
|
148
|
+
const iframeDoc = iframeRef.current?.contentDocument;
|
|
149
|
+
if (!iframeWin && !iframeDoc) return;
|
|
150
|
+
|
|
151
|
+
const handleIframeKeyDown = (e: KeyboardEvent) => playbackKeyDownRef.current(e);
|
|
152
|
+
const handleIframeKeyUp = (e: KeyboardEvent) => playbackKeyUpRef.current(e);
|
|
153
|
+
iframeWin?.addEventListener("keydown", handleIframeKeyDown, true);
|
|
154
|
+
iframeWin?.addEventListener("keyup", handleIframeKeyUp, true);
|
|
155
|
+
iframeDoc?.addEventListener("keydown", handleIframeKeyDown, true);
|
|
156
|
+
iframeDoc?.addEventListener("keyup", handleIframeKeyUp, true);
|
|
157
|
+
iframeShortcutCleanupRef.current = () => {
|
|
158
|
+
iframeWin?.removeEventListener("keydown", handleIframeKeyDown, true);
|
|
159
|
+
iframeWin?.removeEventListener("keyup", handleIframeKeyUp, true);
|
|
160
|
+
iframeDoc?.removeEventListener("keydown", handleIframeKeyDown, true);
|
|
161
|
+
iframeDoc?.removeEventListener("keyup", handleIframeKeyUp, true);
|
|
162
|
+
};
|
|
163
|
+
}, [iframeRef, iframeShortcutCleanupRef]);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
playbackKeyDownRef,
|
|
167
|
+
playbackKeyUpRef,
|
|
168
|
+
attachIframeShortcutListeners,
|
|
169
|
+
togglePlay,
|
|
170
|
+
};
|
|
171
|
+
}
|