@hyperframes/studio 0.6.0-alpha.9 → 0.6.1
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-D1JDq7Gg.css +1 -0
- package/dist/assets/index-hYc4aP7M.js +117 -0
- package/dist/favicon.svg +14 -0
- package/dist/index.html +3 -2
- package/package.json +9 -9
- package/src/App.tsx +421 -4303
- package/src/captions/components/CaptionOverlay.tsx +13 -246
- package/src/captions/components/CaptionOverlayUtils.ts +221 -0
- package/src/components/AskAgentModal.tsx +120 -0
- package/src/components/StudioHeader.tsx +133 -0
- package/src/components/StudioLeftSidebar.tsx +125 -0
- package/src/components/StudioPreviewArea.tsx +167 -0
- package/src/components/StudioRightPanel.tsx +198 -0
- package/src/components/TimelineToolbar.tsx +89 -0
- package/src/components/editor/DomEditOverlay.tsx +88 -993
- 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/PropertyPanel.test.ts +0 -49
- package/src/components/editor/PropertyPanel.tsx +132 -2763
- 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 -1117
- 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/manualEditingAvailability.test.ts +2 -2
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/editor/manualEdits.ts +84 -1049
- 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/propertyPanelColor.tsx +371 -0
- package/src/components/editor/propertyPanelFill.tsx +421 -0
- package/src/components/editor/propertyPanelFont.tsx +455 -0
- package/src/components/editor/propertyPanelHelpers.ts +401 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
- package/src/components/editor/propertyPanelSections.tsx +453 -0
- package/src/components/editor/propertyPanelStyleSections.tsx +411 -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 +68 -155
- package/src/components/nle/NLEPreview.tsx +3 -0
- package/src/components/nle/useCompositionStack.ts +126 -0
- package/src/components/renders/RenderQueue.tsx +102 -31
- package/src/components/renders/useRenderQueue.ts +8 -2
- package/src/components/sidebar/LeftSidebar.tsx +186 -186
- package/src/contexts/DomEditContext.tsx +137 -0
- package/src/contexts/FileManagerContext.tsx +110 -0
- package/src/contexts/PanelLayoutContext.tsx +68 -0
- package/src/contexts/StudioContext.tsx +135 -0
- package/src/hooks/useAppHotkeys.ts +326 -0
- package/src/hooks/useAskAgentModal.ts +162 -0
- package/src/hooks/useCaptionDetection.ts +132 -0
- package/src/hooks/useCompositionDimensions.ts +25 -0
- package/src/hooks/useConsoleErrorCapture.ts +60 -0
- package/src/hooks/useDomEditCommits.ts +437 -0
- package/src/hooks/useDomEditSession.ts +342 -0
- package/src/hooks/useDomEditTextCommits.ts +330 -0
- package/src/hooks/useDomSelection.ts +398 -0
- package/src/hooks/useFileManager.ts +431 -0
- package/src/hooks/useFrameCapture.ts +77 -0
- package/src/hooks/useLintModal.ts +35 -0
- package/src/hooks/useManifestPersistence.ts +492 -0
- package/src/hooks/usePanelLayout.ts +68 -0
- package/src/hooks/usePreviewInteraction.ts +153 -0
- package/src/hooks/useRenderClipContent.ts +124 -0
- package/src/hooks/useTimelineEditing.ts +472 -0
- package/src/hooks/useToast.ts +20 -0
- package/src/player/components/Player.tsx +33 -2
- package/src/player/components/Timeline.test.ts +0 -8
- package/src/player/components/Timeline.tsx +196 -1518
- package/src/player/components/TimelineCanvas.tsx +434 -0
- package/src/player/components/TimelineClip.tsx +9 -244
- 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 +105 -1371
- 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/src/utils/domEditHelpers.ts +50 -0
- package/src/utils/studioFontHelpers.ts +83 -0
- package/src/utils/studioHelpers.ts +214 -0
- package/src/utils/studioPreviewHelpers.ts +185 -0
- package/src/utils/timelineDiscovery.ts +1 -1
- package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
- package/dist/assets/index-14zH9lqh.css +0 -1
- package/dist/assets/index-DYCiFGWQ.js +0 -108
- package/src/player/components/TimelineClip.test.ts +0 -92
|
@@ -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
|
+
}
|