@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.
Files changed (58) hide show
  1. package/dist/assets/hyperframes-player-CzwFysqv.js +418 -0
  2. package/dist/assets/index-hYc4aP7M.js +117 -0
  3. package/dist/index.html +1 -1
  4. package/package.json +4 -4
  5. package/src/App.tsx +2 -13
  6. package/src/captions/components/CaptionOverlay.tsx +13 -246
  7. package/src/captions/components/CaptionOverlayUtils.ts +221 -0
  8. package/src/components/StudioPreviewArea.tsx +6 -2
  9. package/src/components/editor/DomEditOverlay.tsx +88 -1007
  10. package/src/components/editor/EaseCurveEditor.tsx +221 -0
  11. package/src/components/editor/FileTree.tsx +13 -621
  12. package/src/components/editor/FileTreeIcons.tsx +128 -0
  13. package/src/components/editor/FileTreeNodes.tsx +496 -0
  14. package/src/components/editor/MotionPanel.tsx +16 -390
  15. package/src/components/editor/MotionPanelFields.tsx +185 -0
  16. package/src/components/editor/domEditOverlayGeometry.ts +211 -0
  17. package/src/components/editor/domEditOverlayGestures.ts +138 -0
  18. package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
  19. package/src/components/editor/domEditing.ts +44 -1150
  20. package/src/components/editor/domEditingAgentPrompt.ts +97 -0
  21. package/src/components/editor/domEditingDom.ts +266 -0
  22. package/src/components/editor/domEditingElement.ts +329 -0
  23. package/src/components/editor/domEditingLayers.ts +460 -0
  24. package/src/components/editor/domEditingTypes.ts +125 -0
  25. package/src/components/editor/manualEdits.ts +84 -1081
  26. package/src/components/editor/manualEditsDom.ts +436 -0
  27. package/src/components/editor/manualEditsParsing.ts +280 -0
  28. package/src/components/editor/manualEditsSnapshot.ts +333 -0
  29. package/src/components/editor/manualEditsTypes.ts +141 -0
  30. package/src/components/editor/studioMotion.ts +47 -434
  31. package/src/components/editor/studioMotionOps.ts +299 -0
  32. package/src/components/editor/studioMotionTypes.ts +168 -0
  33. package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
  34. package/src/components/editor/useDomEditOverlayRects.ts +207 -0
  35. package/src/components/nle/NLELayout.tsx +60 -144
  36. package/src/components/nle/useCompositionStack.ts +126 -0
  37. package/src/hooks/useToast.ts +20 -0
  38. package/src/player/components/Timeline.tsx +189 -1418
  39. package/src/player/components/TimelineCanvas.tsx +434 -0
  40. package/src/player/components/TimelineEmptyState.tsx +102 -0
  41. package/src/player/components/TimelineRuler.tsx +90 -0
  42. package/src/player/components/timelineIcons.tsx +49 -0
  43. package/src/player/components/timelineLayout.ts +215 -0
  44. package/src/player/components/timelineUtils.ts +211 -0
  45. package/src/player/components/useTimelineClipDrag.ts +388 -0
  46. package/src/player/components/useTimelinePlayhead.ts +200 -0
  47. package/src/player/components/useTimelineRangeSelection.ts +135 -0
  48. package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
  49. package/src/player/hooks/useTimelinePlayer.ts +69 -1372
  50. package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
  51. package/src/player/lib/playbackAdapter.ts +145 -0
  52. package/src/player/lib/playbackShortcuts.ts +68 -0
  53. package/src/player/lib/playbackTypes.ts +60 -0
  54. package/src/player/lib/timelineDOM.ts +373 -0
  55. package/src/player/lib/timelineElementHelpers.ts +303 -0
  56. package/src/player/lib/timelineIframeHelpers.ts +269 -0
  57. package/dist/assets/hyperframes-player-DOFETgjy.js +0 -418
  58. 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
+ }