Crop Video
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx
index 5ee267a..847f932 100644
--- a/src/components/video-editor/VideoEditor.tsx
+++ b/src/components/video-editor/VideoEditor.tsx
@@ -265,24 +265,53 @@ export default function VideoEditor() {
return;
}
+ const aspectRatioValue = getAspectRatioValue(aspectRatio);
const sourceWidth = video.videoWidth || 1920;
const sourceHeight = video.videoHeight || 1080;
- const targetAspectRatio = 16 / 9;
- const sourceAspectRatio = sourceWidth / sourceHeight;
- let exportWidth: number;
- let exportHeight: number;
-
- if (sourceAspectRatio > targetAspectRatio) {
- exportHeight = sourceHeight;
- exportWidth = Math.round(exportHeight * targetAspectRatio);
+ let exportWidth: number = sourceWidth;
+ let exportHeight: number = sourceHeight;
+
+ if (aspectRatioValue === 1) {
+ // Square (1:1): use smaller dimension to avoid codec limits
+ const baseDimension = Math.floor(Math.min(sourceWidth, sourceHeight) / 2) * 2;
+ exportWidth = baseDimension;
+ exportHeight = baseDimension;
+ } else if (aspectRatioValue > 1) {
+ // Landscape: find largest even dimensions that exactly match aspect ratio
+ const baseWidth = Math.floor(sourceWidth / 2) * 2;
+ // Iterate down from baseWidth to find exact match
+ let found = false;
+ for (let w = baseWidth; w >= 100 && !found; w -= 2) {
+ const h = Math.round(w / aspectRatioValue);
+ if (h % 2 === 0 && Math.abs((w / h) - aspectRatioValue) < 0.0001) {
+ exportWidth = w;
+ exportHeight = h;
+ found = true;
+ }
+ }
+ if (!found) {
+ exportWidth = baseWidth;
+ exportHeight = Math.floor((baseWidth / aspectRatioValue) / 2) * 2;
+ }
} else {
- exportWidth = sourceWidth;
- exportHeight = Math.round(exportWidth / targetAspectRatio);
+ // Portrait: find largest even dimensions that exactly match aspect ratio
+ const baseHeight = Math.floor(sourceHeight / 2) * 2;
+ // Iterate down from baseHeight to find exact match
+ let found = false;
+ for (let h = baseHeight; h >= 100 && !found; h -= 2) {
+ const w = Math.round(h * aspectRatioValue);
+ if (w % 2 === 0 && Math.abs((w / h) - aspectRatioValue) < 0.0001) {
+ exportWidth = w;
+ exportHeight = h;
+ found = true;
+ }
+ }
+ if (!found) {
+ exportHeight = baseHeight;
+ exportWidth = Math.floor((baseHeight * aspectRatioValue) / 2) * 2;
+ }
}
-
- exportWidth = Math.round(exportWidth / 2) * 2;
- exportHeight = Math.round(exportHeight / 2) * 2;
// Calculate visually lossless bitrate matching screen recording optimization
const totalPixels = exportWidth * exportHeight;
@@ -350,7 +379,7 @@ export default function VideoEditor() {
setIsExporting(false);
exporterRef.current = null;
}
- }, [videoPath, wallpaper, zoomRegions, trimRegions, shadowIntensity, showBlur, motionBlurEnabled, borderRadius, padding, cropRegion, isPlaying]);
+ }, [videoPath, wallpaper, zoomRegions, trimRegions, shadowIntensity, showBlur, motionBlurEnabled, borderRadius, padding, cropRegion, isPlaying, aspectRatio]);
const handleCancelExport = useCallback(() => {
if (exporterRef.current) {
@@ -446,22 +475,24 @@ export default function VideoEditor() {
+ videoDuration={duration}
+ currentTime={currentTime}
+ onSeek={handleSeek}
+ zoomRegions={zoomRegions}
+ onZoomAdded={handleZoomAdded}
+ onZoomSpanChange={handleZoomSpanChange}
+ onZoomDelete={handleZoomDelete}
+ selectedZoomId={selectedZoomId}
+ onSelectZoom={handleSelectZoom}
+ trimRegions={trimRegions}
+ onTrimAdded={handleTrimAdded}
+ onTrimSpanChange={handleTrimSpanChange}
+ onTrimDelete={handleTrimDelete}
+ selectedTrimId={selectedTrimId}
+ onSelectTrim={handleSelectTrim}
+ aspectRatio={aspectRatio}
+ onAspectRatioChange={setAspectRatio}
+ />
@@ -488,7 +519,6 @@ export default function VideoEditor() {
cropRegion={cropRegion}
onCropChange={setCropRegion}
aspectRatio={aspectRatio}
- onAspectRatioChange={setAspectRatio}
videoElement={videoPlaybackRef.current?.video || null}
onExport={handleExport}
/>
diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx
index 14688ab..907081d 100644
--- a/src/components/video-editor/timeline/TimelineEditor.tsx
+++ b/src/components/video-editor/timeline/TimelineEditor.tsx
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTimelineContext } from "dnd-timeline";
import { Button } from "@/components/ui/button";
-import { Plus, Scissors, ZoomIn } from "lucide-react";
+import { Plus, Scissors, ZoomIn, ChevronDown, Check } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import TimelineWrapper from "./TimelineWrapper";
@@ -11,6 +11,13 @@ import KeyframeMarkers from "./KeyframeMarkers";
import type { Range, Span } from "dnd-timeline";
import type { ZoomRegion, TrimRegion } from "../types";
import { v4 as uuidv4 } from 'uuid';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { type AspectRatio, getAspectRatioLabel } from "@/utils/aspectRatioUtils";
const ZOOM_ROW_ID = "row-zoom";
const TRIM_ROW_ID = "row-trim";
@@ -34,6 +41,8 @@ interface TimelineEditorProps {
onTrimDelete?: (id: string) => void;
selectedTrimId?: string | null;
onSelectTrim?: (id: string | null) => void;
+ aspectRatio: AspectRatio;
+ onAspectRatioChange: (aspectRatio: AspectRatio) => void;
}
interface TimelineScaleConfig {
@@ -410,6 +419,8 @@ export default function TimelineEditor({
onTrimDelete,
selectedTrimId,
onSelectTrim,
+ aspectRatio,
+ onAspectRatioChange,
}: TimelineEditorProps) {
const totalMs = useMemo(() => Math.max(0, Math.round(videoDuration * 1000)), [videoDuration]);
const currentTimeMs = useMemo(() => Math.round(currentTime * 1000), [currentTime]);
@@ -683,6 +694,32 @@ export default function TimelineEditor({