diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 1b6de7e4..cbf9b29b 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -1040,7 +1040,7 @@ export default function VideoEditor() { if (saveResult.canceled) { toast.info("Export canceled"); - } else if (saveResult.success) { + } else if (saveResult.success && saveResult.path) { handleExportSaved("GIF", saveResult.path); } else { setExportError(saveResult.message || "Failed to save GIF"); @@ -1167,7 +1167,7 @@ export default function VideoEditor() { if (saveResult.canceled) { toast.info("Export canceled"); - } else if (saveResult.success) { + } else if (saveResult.success && saveResult.path) { handleExportSaved("Video", saveResult.path); } else { setExportError(saveResult.message || "Failed to save video"); @@ -1232,11 +1232,16 @@ export default function VideoEditor() { // Build export settings from current state const sourceWidth = video.videoWidth || 1920; const sourceHeight = video.videoHeight || 1080; + const aspectRatioValue = + aspectRatio === "native" + ? getNativeAspectRatioValue(sourceWidth, sourceHeight, cropRegion) + : getAspectRatioValue(aspectRatio); const gifDimensions = calculateOutputDimensions( sourceWidth, sourceHeight, gifSizePreset, GIF_SIZE_PRESETS, + aspectRatioValue, ); const settings: ExportSettings = { @@ -1260,7 +1265,17 @@ export default function VideoEditor() { // Start export immediately handleExport(settings); - }, [videoPath, exportFormat, exportQuality, gifFrameRate, gifLoop, gifSizePreset, handleExport]); + }, [ + videoPath, + exportFormat, + exportQuality, + gifFrameRate, + gifLoop, + gifSizePreset, + aspectRatio, + cropRegion, + handleExport, + ]); const handleCancelExport = useCallback(() => { if (exporterRef.current) { @@ -1475,6 +1490,13 @@ export default function VideoEditor() { videoPlaybackRef.current?.video?.videoHeight || 1080, gifSizePreset, GIF_SIZE_PRESETS, + aspectRatio === "native" + ? getNativeAspectRatioValue( + videoPlaybackRef.current?.video?.videoWidth || 1920, + videoPlaybackRef.current?.video?.videoHeight || 1080, + cropRegion, + ) + : getAspectRatioValue(aspectRatio), )} onExport={handleOpenExportDialog} selectedAnnotationId={selectedAnnotationId} diff --git a/src/lib/exporter/gifExporter.test.ts b/src/lib/exporter/gifExporter.test.ts new file mode 100644 index 00000000..1ad16717 --- /dev/null +++ b/src/lib/exporter/gifExporter.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { calculateOutputDimensions } from "./gifExporter"; +import { GIF_SIZE_PRESETS } from "./types"; + +describe("calculateOutputDimensions", () => { + it("uses the selected aspect ratio for scaled GIF exports", () => { + expect(calculateOutputDimensions(1080, 1920, "medium", GIF_SIZE_PRESETS, 16 / 9)).toEqual({ + width: 1280, + height: 720, + }); + }); + + it("fits original-size GIF exports within the source bounds at the selected aspect ratio", () => { + expect(calculateOutputDimensions(1080, 1920, "original", GIF_SIZE_PRESETS, 16 / 9)).toEqual({ + width: 1080, + height: 606, + }); + }); +}); diff --git a/src/lib/exporter/gifExporter.ts b/src/lib/exporter/gifExporter.ts index af49ce25..b9067567 100644 --- a/src/lib/exporter/gifExporter.ts +++ b/src/lib/exporter/gifExporter.ts @@ -58,24 +58,39 @@ export function calculateOutputDimensions( sourceHeight: number, sizePreset: GifSizePreset, sizePresets: typeof GIF_SIZE_PRESETS, + targetAspectRatio = sourceWidth / sourceHeight, ): { width: number; height: number } { const preset = sizePresets[sizePreset]; const maxHeight = preset.maxHeight; + const aspectRatio = + Number.isFinite(targetAspectRatio) && targetAspectRatio > 0 + ? targetAspectRatio + : sourceWidth / sourceHeight; + + const toEven = (value: number) => { + const evenValue = Math.max(2, Math.floor(value / 2) * 2); + return evenValue; + }; + + if (sizePreset === "original") { + const sourceAspect = sourceWidth / sourceHeight; + if (aspectRatio >= sourceAspect) { + const width = toEven(sourceWidth); + const height = toEven(width / aspectRatio); + return { width, height }; + } - // If original is smaller than max height or preset is 'original', use source dimensions - if (sourceHeight <= maxHeight || sizePreset === "original") { - return { width: sourceWidth, height: sourceHeight }; + const height = toEven(sourceHeight); + const width = toEven(height * aspectRatio); + return { width, height }; } - // Calculate scaled dimensions preserving aspect ratio - const aspectRatio = sourceWidth / sourceHeight; - const newHeight = maxHeight; - const newWidth = Math.round(newHeight * aspectRatio); + const targetHeight = maxHeight; + const targetWidth = Math.round(targetHeight * aspectRatio); - // Ensure dimensions are even (required for some encoders) return { - width: newWidth % 2 === 0 ? newWidth : newWidth + 1, - height: newHeight % 2 === 0 ? newHeight : newHeight + 1, + width: toEven(targetWidth), + height: toEven(targetHeight), }; }