From 0d27f4fc3694dc347a3f5398c2ce6bc276adf7c4 Mon Sep 17 00:00:00 2001 From: Idris Gadi Date: Tue, 27 Jan 2026 15:59:01 +0530 Subject: [PATCH] feat: add support for 16:10 aspect ratio --- .../video-editor/timeline/TimelineEditor.tsx | 64 +++++++++---------- src/utils/aspectRatioUtils.ts | 7 +- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx index e516cfc..4ea851b 100644 --- a/src/components/video-editor/timeline/TimelineEditor.tsx +++ b/src/components/video-editor/timeline/TimelineEditor.tsx @@ -155,13 +155,13 @@ function formatTimeLabel(milliseconds: number, intervalMs: number) { return `${minutes}:${Math.floor(seconds).toString().padStart(2, "0")}`; } -function PlaybackCursor({ - currentTimeMs, +function PlaybackCursor({ + currentTimeMs, videoDurationMs, onSeek, timelineRef, -}: { - currentTimeMs: number; +}: { + currentTimeMs: number; videoDurationMs: number; onSeek?: (time: number) => void; timelineRef: React.RefObject; @@ -175,14 +175,14 @@ function PlaybackCursor({ const handleMouseMove = (e: MouseEvent) => { if (!timelineRef.current || !onSeek) return; - + const rect = timelineRef.current.getBoundingClientRect(); const clickX = e.clientX - rect.left - sidebarWidth; - + // Allow dragging outside to 0 or max, but clamp the value const relativeMs = pixelsToValue(clickX); const absoluteMs = Math.max(0, Math.min(range.start + relativeMs, videoDurationMs)); - + onSeek(absoluteMs / 1000); }; @@ -207,7 +207,7 @@ function PlaybackCursor({ } const clampedTime = Math.min(currentTimeMs, videoDurationMs); - + if (clampedTime < range.start || clampedTime > range.end) { return null; } @@ -276,7 +276,7 @@ function TimelineAxis({ if (visibleStart <= maxTime) { markerTimes.add(Math.round(visibleStart)); } - + if (videoDurationMs > 0) { markerTimes.add(Math.round(videoDurationMs)); } @@ -288,7 +288,7 @@ function TimelineAxis({ // Generate minor ticks (4 ticks between major intervals) const minorTicks = []; const minorInterval = intervalMs / 5; - + for (let time = firstMarker; time <= maxTime; time += minorInterval) { if (time >= visibleStart && time <= visibleEnd) { // Skip if it's close to a major marker @@ -299,12 +299,12 @@ function TimelineAxis({ } } - return { + return { markers: sorted.map((time) => ({ time, label: formatTimeLabel(time, intervalMs), - })), - minorTicks + })), + minorTicks }; }, [intervalMs, range.end, range.start, videoDurationMs]); @@ -395,7 +395,7 @@ function Timeline({ const handleTimelineClick = useCallback((e: React.MouseEvent) => { if (!onSeek || videoDurationMs <= 0) return; - + // Only clear selection if clicking on empty space (not on items) // This is handled by event propagation - items stop propagation onSelectZoom?.(null); @@ -404,13 +404,13 @@ function Timeline({ const rect = e.currentTarget.getBoundingClientRect(); const clickX = e.clientX - rect.left - sidebarWidth; - + if (clickX < 0) return; - + const relativeMs = pixelsToValue(clickX); const absoluteMs = Math.max(0, Math.min(range.start + relativeMs, videoDurationMs)); const timeInSeconds = absoluteMs / 1000; - + onSeek(timeInSeconds); }, [onSeek, onSelectZoom, onSelectTrim, onSelectAnnotation, videoDurationMs, sidebarWidth, range.start, pixelsToValue]); @@ -427,13 +427,13 @@ function Timeline({ >
- - + {zoomItems.map((item) => ( 0) { const currentTimeMs = Math.round(currentTime * 1000); const overlapping = annotationRegions .filter(a => currentTimeMs >= a.startMs && currentTimeMs <= a.endMs) .sort((a, b) => a.zIndex - b.zIndex); // Sort by z-index - + if (overlapping.length > 0) { - e.preventDefault(); - + e.preventDefault(); + if (!selectedAnnotationId || !overlapping.some(a => a.id === selectedAnnotationId)) { onSelectAnnotation?.(overlapping[0].id); } else { // Cycle to next annotation const currentIndex = overlapping.findIndex(a => a.id === selectedAnnotationId); - const nextIndex = e.shiftKey + const nextIndex = e.shiftKey ? (currentIndex - 1 + overlapping.length) % overlapping.length // Shift+Tab = backward : (currentIndex + 1) % overlapping.length; // Tab = forward onSelectAnnotation?.(overlapping[nextIndex].id); } } - } + } if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey)) { if (selectedKeyframeId) { deleteSelectedKeyframe(); @@ -803,7 +803,7 @@ export default function TimelineEditor({ const annotations: TimelineRenderItem[] = annotationRegions.map((region) => { let label: string; - + if (region.type === 'text') { // Show text preview const preview = region.content.trim() || 'Empty text'; @@ -813,7 +813,7 @@ export default function TimelineEditor({ } else { label = 'Annotation'; } - + return { id: region.id, rowId: ANNOTATION_ROW_ID, @@ -896,7 +896,7 @@ export default function TimelineEditor({ - {(['16:9', '9:16', '1:1', '4:3', '4:5'] as AspectRatio[]).map((ratio) => ( + {(['16:9', '9:16', '1:1', '4:3', '4:5', '16:10', "10:16"] as AspectRatio[]).map((ratio) => ( onAspectRatioChange(ratio)} @@ -918,7 +918,7 @@ export default function TimelineEditor({ Pan - {shortcuts.zoom} + {shortcuts.zoom} Zoom
diff --git a/src/utils/aspectRatioUtils.ts b/src/utils/aspectRatioUtils.ts index aaad804..f00167c 100644 --- a/src/utils/aspectRatioUtils.ts +++ b/src/utils/aspectRatioUtils.ts @@ -1,4 +1,4 @@ -export type AspectRatio = '16:9' | '9:16' | '1:1' | '4:3' | '4:5'; +export type AspectRatio = '16:9' | '9:16' | '1:1' | '4:3' | '4:5' | '16:10' | '10:16'; export function getAspectRatioValue(aspectRatio: AspectRatio): number { switch (aspectRatio) { @@ -7,6 +7,9 @@ export function getAspectRatioValue(aspectRatio: AspectRatio): number { case '1:1': return 1; case '4:3': return 4 / 3; case '4:5': return 4 / 5; + case '16:10': return 16 / 10; + case '10:16': return 10 / 16; + default: return 1; } } @@ -28,4 +31,4 @@ export function getAspectRatioLabel(aspectRatio: AspectRatio): string { export function formatAspectRatioForCSS(aspectRatio: AspectRatio): string { return aspectRatio.replace(':', '/'); -} \ No newline at end of file +}