Add "Native" aspect ratio option to export at cropped video dimensions

Adds a "Native" option to the aspect ratio dropdown that uses the cropped
video's actual aspect ratio, so the video fills the entire frame with no
background visible. Selecting Native also sets padding to 0 automatically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hemkesh
2026-03-04 21:48:47 -06:00
parent 9eb362012b
commit c8ebef026b
3 changed files with 31 additions and 11 deletions
+9 -4
View File
@@ -42,7 +42,7 @@ import {
type PlaybackSpeed,
} from "./types";
import { VideoExporter, GifExporter, type ExportProgress, type ExportQuality, type ExportSettings, type ExportFormat, type GifFrameRate, type GifSizePreset, GIF_SIZE_PRESETS, calculateOutputDimensions } from "@/lib/exporter";
import { type AspectRatio, getAspectRatioValue } from "@/utils/aspectRatioUtils";
import { type AspectRatio, getAspectRatioValue, getNativeAspectRatioValue } from "@/utils/aspectRatioUtils";
import { getAssetPath } from "@/lib/assetPath";
import { useShortcuts } from "@/contexts/ShortcutsContext";
import { matchesShortcut } from "@/lib/shortcuts";
@@ -833,9 +833,11 @@ export default function VideoEditor() {
videoPlaybackRef.current?.pause();
}
const aspectRatioValue = getAspectRatioValue(aspectRatio);
const sourceWidth = video.videoWidth || 1920;
const sourceHeight = video.videoHeight || 1080;
const aspectRatioValue = aspectRatio === 'native'
? getNativeAspectRatioValue(sourceWidth, sourceHeight, cropRegion)
: getAspectRatioValue(aspectRatio);
// Get preview CONTAINER dimensions for scaling
const playbackRef = videoPlaybackRef.current;
@@ -1130,7 +1132,7 @@ export default function VideoEditor() {
<div className="w-full h-full flex flex-col items-center justify-center bg-black/40 rounded-2xl border border-white/5 shadow-2xl overflow-hidden">
{/* Video preview */}
<div className="w-full flex justify-center items-center" style={{ flex: '1 1 auto', margin: '6px 0 0' }}>
<div className="relative" style={{ width: 'auto', height: '100%', aspectRatio: getAspectRatioValue(aspectRatio), maxWidth: '100%', margin: '0 auto', boxSizing: 'border-box' }}>
<div className="relative" style={{ width: 'auto', height: '100%', aspectRatio: aspectRatio === 'native' ? getNativeAspectRatioValue(videoPlaybackRef.current?.video?.videoWidth || 1920, videoPlaybackRef.current?.video?.videoHeight || 1080, cropRegion) : getAspectRatioValue(aspectRatio), maxWidth: '100%', margin: '0 auto', boxSizing: 'border-box' }}>
<VideoPlayback
key={videoPath || 'no-video'}
aspectRatio={aspectRatio}
@@ -1217,7 +1219,10 @@ export default function VideoEditor() {
selectedAnnotationId={selectedAnnotationId}
onSelectAnnotation={handleSelectAnnotation}
aspectRatio={aspectRatio}
onAspectRatioChange={setAspectRatio}
onAspectRatioChange={(ratio) => {
setAspectRatio(ratio);
if (ratio === 'native') setPadding(0);
}}
/>
</div>
</Panel>
@@ -11,7 +11,7 @@ import { updateOverlayIndicator } from "./videoPlayback/overlayUtils";
import { layoutVideoContent as layoutVideoContentUtil } from "./videoPlayback/layoutUtils";
import { applyZoomTransform } from "./videoPlayback/zoomTransform";
import { createVideoEventHandlers } from "./videoPlayback/videoEventHandlers";
import { type AspectRatio, formatAspectRatioForCSS } from "@/utils/aspectRatioUtils";
import { type AspectRatio, formatAspectRatioForCSS, getNativeAspectRatioValue } from "@/utils/aspectRatioUtils";
import { AnnotationOverlay } from "./AnnotationOverlay";
interface VideoPlaybackProps {
@@ -797,7 +797,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
: { background: resolvedWallpaper || '' };
return (
<div className="relative rounded-sm overflow-hidden" style={{ width: '100%', aspectRatio: formatAspectRatioForCSS(aspectRatio) }}>
<div className="relative rounded-sm overflow-hidden" style={{ width: '100%', aspectRatio: formatAspectRatioForCSS(aspectRatio, aspectRatio === 'native' ? getNativeAspectRatioValue(lockedVideoDimensionsRef.current?.width || 1920, lockedVideoDimensionsRef.current?.height || 1080, cropRegion) : undefined) }}>
{/* Background layer - always render as DOM element with blur */}
<div
className="absolute inset-0 bg-cover bg-center"
+20 -5
View File
@@ -1,11 +1,11 @@
export const ASPECT_RATIOS = ['16:9', '9:16', '1:1', '4:3', '4:5', '16:10', '10:16'] as const;
export const ASPECT_RATIOS = ['16:9', '9:16', '1:1', '4:3', '4:5', '16:10', '10:16', 'native'] as const;
export type AspectRatio = typeof ASPECT_RATIOS[number];
/**
* Returns the numeric value of an aspect ratio.
* Uses exhaustive type checking to ensure all AspectRatio cases are handled.
* If TypeScript errors here, a new ratio was added to the type but not handled.
* For 'native', returns a fallback of 16/9 — callers with video/crop info
* should use getNativeAspectRatioValue() instead.
*/
export function getAspectRatioValue(aspectRatio: AspectRatio): number {
switch (aspectRatio) {
@@ -16,14 +16,27 @@ export function getAspectRatioValue(aspectRatio: AspectRatio): number {
case '4:5': return 4 / 5;
case '16:10': return 16 / 10;
case '10:16': return 10 / 16;
case 'native': return 16 / 9;
default: {
// Ensures all cases are handled - TypeScript errors if missing
const _exhaustiveCheck: never = aspectRatio;
return _exhaustiveCheck;
}
}
}
/**
* Returns the aspect ratio value for 'native' mode based on the cropped video dimensions.
*/
export function getNativeAspectRatioValue(
videoWidth: number,
videoHeight: number,
cropRegion?: { x: number; y: number; width: number; height: number },
): number {
const cropW = cropRegion?.width ?? 1;
const cropH = cropRegion?.height ?? 1;
return (videoWidth * cropW) / (videoHeight * cropH);
}
export function getAspectRatioDimensions(
aspectRatio: AspectRatio,
baseWidth: number
@@ -36,10 +49,12 @@ export function getAspectRatioDimensions(
}
export function getAspectRatioLabel(aspectRatio: AspectRatio): string {
if (aspectRatio === 'native') return 'Native';
return aspectRatio;
}
export function formatAspectRatioForCSS(aspectRatio: AspectRatio): string {
export function formatAspectRatioForCSS(aspectRatio: AspectRatio, nativeRatio?: number): string {
if (aspectRatio === 'native') return String(nativeRatio ?? 16 / 9);
return aspectRatio.replace(':', '/');
}