@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,215 @@
1
+ import { formatTime } from "../lib/time";
2
+ import type { ZoomMode } from "../store/playerStore";
3
+
4
+ /* ── Layout constants ──────────────────────────────────────────────── */
5
+ export const GUTTER = 32;
6
+ export const TRACK_H = 72;
7
+ export const RULER_H = 24;
8
+ export const CLIP_Y = 3;
9
+ export const CLIP_HANDLE_W = 18;
10
+ export const TIMELINE_SCROLL_BUFFER = 20;
11
+
12
+ /* ── Tick generation ──────────────────────────────────────────────── */
13
+ function getMajorTickInterval(duration: number, pixelsPerSecond?: number): number {
14
+ const zoomIntervals = [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600];
15
+ if (Number.isFinite(pixelsPerSecond) && (pixelsPerSecond ?? 0) > 0) {
16
+ const targetMajorPx = 128;
17
+ return (
18
+ zoomIntervals.find((interval) => interval * (pixelsPerSecond ?? 0) >= targetMajorPx) ?? 600
19
+ );
20
+ }
21
+ const durationIntervals = [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60];
22
+ const target = duration / 6;
23
+ return durationIntervals.find((interval) => interval >= target) ?? 60;
24
+ }
25
+
26
+ function getMinorTickInterval(majorInterval: number, pixelsPerSecond?: number): number {
27
+ let interval = majorInterval / 2;
28
+ if (majorInterval >= 30) interval = majorInterval / 6;
29
+ else if (majorInterval >= 15) interval = majorInterval / 3;
30
+ else if (majorInterval >= 5) interval = majorInterval / 5;
31
+ else if (majorInterval >= 1) interval = majorInterval / 4;
32
+
33
+ if (
34
+ Number.isFinite(pixelsPerSecond) &&
35
+ (pixelsPerSecond ?? 0) > 0 &&
36
+ interval * (pixelsPerSecond ?? 0) < 20
37
+ ) {
38
+ return Math.max(0.25, majorInterval / 2);
39
+ }
40
+ return Math.max(0.25, interval);
41
+ }
42
+
43
+ export function generateTicks(
44
+ duration: number,
45
+ pixelsPerSecond?: number,
46
+ ): { major: number[]; minor: number[] } {
47
+ if (duration <= 0 || !Number.isFinite(duration) || duration > 7200)
48
+ return { major: [], minor: [] };
49
+ const majorInterval = getMajorTickInterval(duration, pixelsPerSecond);
50
+ const minorInterval = getMinorTickInterval(majorInterval, pixelsPerSecond);
51
+ const major: number[] = [];
52
+ const minor: number[] = [];
53
+ const maxTicks = 2000; // Safety cap to prevent runaway tick generation
54
+ for (
55
+ let t = 0;
56
+ t <= duration + 0.001 && major.length + minor.length < maxTicks;
57
+ t += minorInterval
58
+ ) {
59
+ const rounded = Math.round(t * 100) / 100;
60
+ const isMajor =
61
+ Math.abs(rounded % majorInterval) < 0.01 ||
62
+ Math.abs((rounded % majorInterval) - majorInterval) < 0.01;
63
+ if (isMajor) major.push(rounded);
64
+ else minor.push(rounded);
65
+ }
66
+ return { major, minor };
67
+ }
68
+
69
+ export function formatTimelineTickLabel(time: number, duration: number, majorInterval: number) {
70
+ if (!Number.isFinite(time)) return "0:00";
71
+ const safeTime = Math.max(0, time);
72
+ if (majorInterval < 1) {
73
+ const totalTenths = Math.round(safeTime * 10);
74
+ const wholeSeconds = Math.floor(totalTenths / 10);
75
+ const tenth = totalTenths % 10;
76
+ return `${formatTime(wholeSeconds)}.${tenth}`;
77
+ }
78
+ if (duration >= 3600 || safeTime >= 3600) {
79
+ const totalSeconds = Math.floor(safeTime);
80
+ const hours = Math.floor(totalSeconds / 3600);
81
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
82
+ const seconds = totalSeconds % 60;
83
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
84
+ }
85
+ return formatTime(safeTime);
86
+ }
87
+
88
+ /* ── Scroll / zoom helpers ────────────────────────────────────────── */
89
+ export function shouldAutoScrollTimeline(
90
+ zoomMode: ZoomMode,
91
+ scrollWidth: number,
92
+ clientWidth: number,
93
+ ): boolean {
94
+ if (zoomMode === "fit") return false;
95
+ if (!Number.isFinite(scrollWidth) || !Number.isFinite(clientWidth)) return false;
96
+ return scrollWidth - clientWidth > 1;
97
+ }
98
+
99
+ export function getTimelineScrollLeftForZoomTransition(
100
+ previousZoomMode: ZoomMode | null,
101
+ nextZoomMode: ZoomMode,
102
+ currentScrollLeft: number,
103
+ ): number {
104
+ if (previousZoomMode === "manual" && nextZoomMode === "fit") return 0;
105
+ return currentScrollLeft;
106
+ }
107
+
108
+ export function getTimelineScrollLeftForZoomAnchor(input: {
109
+ pointerX: number;
110
+ currentScrollLeft: number;
111
+ gutter: number;
112
+ currentPixelsPerSecond: number;
113
+ nextPixelsPerSecond: number;
114
+ duration: number;
115
+ }): number {
116
+ const currentPps = Math.max(0, input.currentPixelsPerSecond);
117
+ const nextPps = Math.max(0, input.nextPixelsPerSecond);
118
+ if (
119
+ !Number.isFinite(input.pointerX) ||
120
+ !Number.isFinite(input.currentScrollLeft) ||
121
+ !Number.isFinite(input.duration) ||
122
+ input.duration <= 0 ||
123
+ currentPps <= 0 ||
124
+ nextPps <= 0
125
+ ) {
126
+ return Math.max(0, input.currentScrollLeft);
127
+ }
128
+ const timelineX = Math.max(0, input.currentScrollLeft + input.pointerX - input.gutter);
129
+ const timeAtPointer = Math.max(0, Math.min(input.duration, timelineX / currentPps));
130
+ return Math.max(0, input.gutter + timeAtPointer * nextPps - input.pointerX);
131
+ }
132
+
133
+ /* ── Playhead / canvas ────────────────────────────────────────────── */
134
+ export function getTimelinePlayheadLeft(time: number, pixelsPerSecond: number): number {
135
+ if (!Number.isFinite(time) || !Number.isFinite(pixelsPerSecond)) return GUTTER;
136
+ return GUTTER + Math.max(0, time) * Math.max(0, pixelsPerSecond);
137
+ }
138
+
139
+ export function getTimelineCanvasHeight(trackCount: number): number {
140
+ return RULER_H + Math.max(0, trackCount) * TRACK_H + TIMELINE_SCROLL_BUFFER;
141
+ }
142
+
143
+ /* ── UI helpers ───────────────────────────────────────────────────── */
144
+ export function shouldShowTimelineShortcutHint(
145
+ scrollHeight: number,
146
+ clientHeight: number,
147
+ ): boolean {
148
+ if (!Number.isFinite(scrollHeight) || !Number.isFinite(clientHeight)) return true;
149
+ return scrollHeight - clientHeight <= 1;
150
+ }
151
+
152
+ export function shouldHandleTimelineDeleteKey(input: {
153
+ key: string;
154
+ metaKey?: boolean;
155
+ ctrlKey?: boolean;
156
+ altKey?: boolean;
157
+ target?: EventTarget | null;
158
+ }): boolean {
159
+ if (input.key !== "Delete" && input.key !== "Backspace") return false;
160
+ if (input.metaKey || input.ctrlKey || input.altKey) return false;
161
+ const target =
162
+ input.target && typeof input.target === "object"
163
+ ? (input.target as {
164
+ tagName?: string;
165
+ isContentEditable?: boolean;
166
+ closest?: (selector: string) => Element | null;
167
+ })
168
+ : null;
169
+ if (target) {
170
+ const tag = target.tagName?.toLowerCase() ?? "";
171
+ if (target.isContentEditable) return false;
172
+ if (["input", "textarea", "select"].includes(tag)) return false;
173
+ if (typeof target.closest === "function" && target.closest("[contenteditable='true']")) {
174
+ return false;
175
+ }
176
+ }
177
+ return true;
178
+ }
179
+
180
+ /* ── Asset drop ───────────────────────────────────────────────────── */
181
+ export function getDefaultDroppedTrack(trackOrder: number[], rowIndex?: number): number {
182
+ if (trackOrder.length === 0) return 0;
183
+ if (rowIndex == null || rowIndex < 0) return trackOrder[0];
184
+ if (rowIndex >= trackOrder.length) {
185
+ return Math.max(...trackOrder) + 1;
186
+ }
187
+ return trackOrder[rowIndex] ?? trackOrder[trackOrder.length - 1] ?? 0;
188
+ }
189
+
190
+ export function resolveTimelineAssetDrop(
191
+ input: {
192
+ rectLeft: number;
193
+ rectTop: number;
194
+ scrollLeft: number;
195
+ scrollTop: number;
196
+ pixelsPerSecond: number;
197
+ duration: number;
198
+ trackHeight: number;
199
+ trackOrder: number[];
200
+ },
201
+ clientX: number,
202
+ clientY: number,
203
+ ): { start: number; track: number } {
204
+ const x = clientX - input.rectLeft + input.scrollLeft - GUTTER;
205
+ const y = clientY - input.rectTop + input.scrollTop - RULER_H;
206
+ const start = Math.max(
207
+ 0,
208
+ Math.min(input.duration, Math.round((x / Math.max(input.pixelsPerSecond, 1)) * 100) / 100),
209
+ );
210
+ const rowIndex = Math.floor(y / Math.max(input.trackHeight, 1));
211
+ return {
212
+ start,
213
+ track: getDefaultDroppedTrack(input.trackOrder, rowIndex),
214
+ };
215
+ }
@@ -0,0 +1,211 @@
1
+ import { formatTime } from "../lib/time";
2
+ import type { ZoomMode } from "../store/playerStore";
3
+
4
+ /* ── Layout constants ─────────────────────────────────────────────── */
5
+ export const GUTTER = 32;
6
+ export const TRACK_H = 72;
7
+ export const RULER_H = 24;
8
+ export const CLIP_Y = 3;
9
+ export const CLIP_HANDLE_W = 18;
10
+ export const TIMELINE_SCROLL_BUFFER = 20;
11
+
12
+ /* ── Tick Generation ────────────────────────────────────────────────── */
13
+ function getMajorTickInterval(duration: number, pixelsPerSecond?: number): number {
14
+ const zoomIntervals = [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600];
15
+ if (Number.isFinite(pixelsPerSecond) && (pixelsPerSecond ?? 0) > 0) {
16
+ const targetMajorPx = 128;
17
+ return (
18
+ zoomIntervals.find((interval) => interval * (pixelsPerSecond ?? 0) >= targetMajorPx) ?? 600
19
+ );
20
+ }
21
+ const durationIntervals = [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60];
22
+ const target = duration / 6;
23
+ return durationIntervals.find((interval) => interval >= target) ?? 60;
24
+ }
25
+
26
+ function getMinorTickInterval(majorInterval: number, pixelsPerSecond?: number): number {
27
+ let interval = majorInterval / 2;
28
+ if (majorInterval >= 30) interval = majorInterval / 6;
29
+ else if (majorInterval >= 15) interval = majorInterval / 3;
30
+ else if (majorInterval >= 5) interval = majorInterval / 5;
31
+ else if (majorInterval >= 1) interval = majorInterval / 4;
32
+
33
+ if (
34
+ Number.isFinite(pixelsPerSecond) &&
35
+ (pixelsPerSecond ?? 0) > 0 &&
36
+ interval * (pixelsPerSecond ?? 0) < 20
37
+ ) {
38
+ return Math.max(0.25, majorInterval / 2);
39
+ }
40
+ return Math.max(0.25, interval);
41
+ }
42
+
43
+ export function generateTicks(
44
+ duration: number,
45
+ pixelsPerSecond?: number,
46
+ ): { major: number[]; minor: number[] } {
47
+ if (duration <= 0 || !Number.isFinite(duration) || duration > 7200)
48
+ return { major: [], minor: [] };
49
+ const majorInterval = getMajorTickInterval(duration, pixelsPerSecond);
50
+ const minorInterval = getMinorTickInterval(majorInterval, pixelsPerSecond);
51
+ const major: number[] = [];
52
+ const minor: number[] = [];
53
+ const maxTicks = 2000;
54
+ for (
55
+ let t = 0;
56
+ t <= duration + 0.001 && major.length + minor.length < maxTicks;
57
+ t += minorInterval
58
+ ) {
59
+ const rounded = Math.round(t * 100) / 100;
60
+ const isMajor =
61
+ Math.abs(rounded % majorInterval) < 0.01 ||
62
+ Math.abs((rounded % majorInterval) - majorInterval) < 0.01;
63
+ if (isMajor) major.push(rounded);
64
+ else minor.push(rounded);
65
+ }
66
+ return { major, minor };
67
+ }
68
+
69
+ export function formatTimelineTickLabel(time: number, duration: number, majorInterval: number) {
70
+ if (!Number.isFinite(time)) return "0:00";
71
+ const safeTime = Math.max(0, time);
72
+ if (majorInterval < 1) {
73
+ const totalTenths = Math.round(safeTime * 10);
74
+ const wholeSeconds = Math.floor(totalTenths / 10);
75
+ const tenth = totalTenths % 10;
76
+ return `${formatTime(wholeSeconds)}.${tenth}`;
77
+ }
78
+ if (duration >= 3600 || safeTime >= 3600) {
79
+ const totalSeconds = Math.floor(safeTime);
80
+ const hours = Math.floor(totalSeconds / 3600);
81
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
82
+ const seconds = totalSeconds % 60;
83
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
84
+ }
85
+ return formatTime(safeTime);
86
+ }
87
+
88
+ export function shouldAutoScrollTimeline(
89
+ zoomMode: ZoomMode,
90
+ scrollWidth: number,
91
+ clientWidth: number,
92
+ ): boolean {
93
+ if (zoomMode === "fit") return false;
94
+ if (!Number.isFinite(scrollWidth) || !Number.isFinite(clientWidth)) return false;
95
+ return scrollWidth - clientWidth > 1;
96
+ }
97
+
98
+ export function getTimelineScrollLeftForZoomTransition(
99
+ previousZoomMode: ZoomMode | null,
100
+ nextZoomMode: ZoomMode,
101
+ currentScrollLeft: number,
102
+ ): number {
103
+ if (previousZoomMode === "manual" && nextZoomMode === "fit") return 0;
104
+ return currentScrollLeft;
105
+ }
106
+
107
+ export function getTimelineScrollLeftForZoomAnchor(input: {
108
+ pointerX: number;
109
+ currentScrollLeft: number;
110
+ gutter: number;
111
+ currentPixelsPerSecond: number;
112
+ nextPixelsPerSecond: number;
113
+ duration: number;
114
+ }): number {
115
+ const currentPps = Math.max(0, input.currentPixelsPerSecond);
116
+ const nextPps = Math.max(0, input.nextPixelsPerSecond);
117
+ if (
118
+ !Number.isFinite(input.pointerX) ||
119
+ !Number.isFinite(input.currentScrollLeft) ||
120
+ !Number.isFinite(input.duration) ||
121
+ input.duration <= 0 ||
122
+ currentPps <= 0 ||
123
+ nextPps <= 0
124
+ ) {
125
+ return Math.max(0, input.currentScrollLeft);
126
+ }
127
+ const timelineX = Math.max(0, input.currentScrollLeft + input.pointerX - input.gutter);
128
+ const timeAtPointer = Math.max(0, Math.min(input.duration, timelineX / currentPps));
129
+ return Math.max(0, input.gutter + timeAtPointer * nextPps - input.pointerX);
130
+ }
131
+
132
+ export function getTimelinePlayheadLeft(time: number, pixelsPerSecond: number): number {
133
+ if (!Number.isFinite(time) || !Number.isFinite(pixelsPerSecond)) return GUTTER;
134
+ return GUTTER + Math.max(0, time) * Math.max(0, pixelsPerSecond);
135
+ }
136
+
137
+ export function getTimelineCanvasHeight(trackCount: number): number {
138
+ return RULER_H + Math.max(0, trackCount) * TRACK_H + TIMELINE_SCROLL_BUFFER;
139
+ }
140
+
141
+ export function shouldShowTimelineShortcutHint(
142
+ scrollHeight: number,
143
+ clientHeight: number,
144
+ ): boolean {
145
+ if (!Number.isFinite(scrollHeight) || !Number.isFinite(clientHeight)) return true;
146
+ return scrollHeight - clientHeight <= 1;
147
+ }
148
+
149
+ export function shouldHandleTimelineDeleteKey(input: {
150
+ key: string;
151
+ metaKey?: boolean;
152
+ ctrlKey?: boolean;
153
+ altKey?: boolean;
154
+ target?: EventTarget | null;
155
+ }): boolean {
156
+ if (input.key !== "Delete" && input.key !== "Backspace") return false;
157
+ if (input.metaKey || input.ctrlKey || input.altKey) return false;
158
+ const target =
159
+ input.target && typeof input.target === "object"
160
+ ? (input.target as {
161
+ tagName?: string;
162
+ isContentEditable?: boolean;
163
+ closest?: (selector: string) => Element | null;
164
+ })
165
+ : null;
166
+ if (target) {
167
+ const tag = target.tagName?.toLowerCase() ?? "";
168
+ if (target.isContentEditable) return false;
169
+ if (["input", "textarea", "select"].includes(tag)) return false;
170
+ if (typeof target.closest === "function" && target.closest("[contenteditable='true']")) {
171
+ return false;
172
+ }
173
+ }
174
+ return true;
175
+ }
176
+
177
+ export function getDefaultDroppedTrack(trackOrder: number[], rowIndex?: number): number {
178
+ if (trackOrder.length === 0) return 0;
179
+ if (rowIndex == null || rowIndex < 0) return trackOrder[0];
180
+ if (rowIndex >= trackOrder.length) {
181
+ return Math.max(...trackOrder) + 1;
182
+ }
183
+ return trackOrder[rowIndex] ?? trackOrder[trackOrder.length - 1] ?? 0;
184
+ }
185
+
186
+ export function resolveTimelineAssetDrop(
187
+ input: {
188
+ rectLeft: number;
189
+ rectTop: number;
190
+ scrollLeft: number;
191
+ scrollTop: number;
192
+ pixelsPerSecond: number;
193
+ duration: number;
194
+ trackHeight: number;
195
+ trackOrder: number[];
196
+ },
197
+ clientX: number,
198
+ clientY: number,
199
+ ): { start: number; track: number } {
200
+ const x = clientX - input.rectLeft + input.scrollLeft - GUTTER;
201
+ const y = clientY - input.rectTop + input.scrollTop - RULER_H;
202
+ const start = Math.max(
203
+ 0,
204
+ Math.min(input.duration, Math.round((x / Math.max(input.pixelsPerSecond, 1)) * 100) / 100),
205
+ );
206
+ const rowIndex = Math.floor(y / Math.max(input.trackHeight, 1));
207
+ return {
208
+ start,
209
+ track: getDefaultDroppedTrack(input.trackOrder, rowIndex),
210
+ };
211
+ }