preview aspect ratio
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type AspectRatio, formatAspectRatioForCSS } from "@/utils/aspectRatioUtils";
|
||||
|
||||
interface CropRegion {
|
||||
x: number; // 0-1 normalized
|
||||
@@ -12,11 +13,12 @@ interface CropControlProps {
|
||||
videoElement: HTMLVideoElement | null;
|
||||
cropRegion: CropRegion;
|
||||
onCropChange: (region: CropRegion) => void;
|
||||
aspectRatio: AspectRatio;
|
||||
}
|
||||
|
||||
type DragHandle = 'top' | 'right' | 'bottom' | 'left' | null;
|
||||
|
||||
export function CropControl({ videoElement, cropRegion, onCropChange }: CropControlProps) {
|
||||
export function CropControl({ videoElement, cropRegion, onCropChange, aspectRatio }: CropControlProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isDragging, setIsDragging] = useState<DragHandle>(null);
|
||||
@@ -119,7 +121,8 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont
|
||||
<div className="w-full p-8">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative w-full aspect-video bg-black rounded-lg overflow-visible cursor-default select-none shadow-2xl"
|
||||
className="relative w-full bg-black rounded-lg overflow-visible cursor-default select-none shadow-2xl"
|
||||
style={{ aspectRatio: formatAspectRatioForCSS(aspectRatio) }}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerLeave={handlePointerUp}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { toast } from "sonner";
|
||||
import type { ZoomDepth, CropRegion } from "./types";
|
||||
import { CropControl } from "./CropControl";
|
||||
import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp";
|
||||
import { type AspectRatio } from "@/utils/aspectRatioUtils";
|
||||
|
||||
const WALLPAPER_COUNT = 18;
|
||||
const WALLPAPER_RELATIVE = Array.from({ length: WALLPAPER_COUNT }, (_, i) => `wallpapers/wallpaper${i + 1}.jpg`);
|
||||
@@ -63,6 +64,8 @@ interface SettingsPanelProps {
|
||||
onPaddingChange?: (padding: number) => void;
|
||||
cropRegion?: CropRegion;
|
||||
onCropChange?: (region: CropRegion) => void;
|
||||
aspectRatio: AspectRatio;
|
||||
onAspectRatioChange: (aspectRatio: AspectRatio) => void;
|
||||
videoElement?: HTMLVideoElement | null;
|
||||
onExport?: () => void;
|
||||
}
|
||||
@@ -78,7 +81,7 @@ const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string }> = [
|
||||
{ depth: 6, label: "5×" },
|
||||
];
|
||||
|
||||
export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, onZoomDepthChange, selectedZoomId, onZoomDelete, shadowIntensity = 0, onShadowChange, showBlur, onBlurChange, motionBlurEnabled = true, onMotionBlurChange, borderRadius = 0, onBorderRadiusChange, padding = 50, onPaddingChange, cropRegion, onCropChange, videoElement, onExport }: SettingsPanelProps) {
|
||||
export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, onZoomDepthChange, selectedZoomId, onZoomDelete, shadowIntensity = 0, onShadowChange, showBlur, onBlurChange, motionBlurEnabled = true, onMotionBlurChange, borderRadius = 0, onBorderRadiusChange, padding = 50, onPaddingChange, cropRegion, onCropChange, aspectRatio, onAspectRatioChange, videoElement, onExport }: SettingsPanelProps) {
|
||||
const [wallpaperPaths, setWallpaperPaths] = useState<string[]>([]);
|
||||
const [customImages, setCustomImages] = useState<string[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -230,11 +233,37 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<span className="text-sm font-medium text-slate-200 mb-3 block">Aspect Ratio</span>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{(['16:9', '9:16', '1:1', '4:3'] as AspectRatio[]).map((ratio) => {
|
||||
const isActive = aspectRatio === ratio;
|
||||
return (
|
||||
<Button
|
||||
key={ratio}
|
||||
type="button"
|
||||
onClick={() => onAspectRatioChange(ratio)}
|
||||
className={cn(
|
||||
"h-auto w-full rounded-xl border px-2 py-3 text-center shadow-sm transition-all flex items-center justify-center",
|
||||
"duration-200 ease-out",
|
||||
isActive
|
||||
? "border-[#34B27B] bg-[#34B27B] text-white shadow-[#34B27B]/20 scale-105 ring-2 ring-[#34B27B]/20"
|
||||
: "border-white/5 bg-white/5 text-slate-400 hover:bg-white/10 hover:border-white/10 hover:text-slate-200"
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-semibold tracking-tight">{ratio}</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Drop Shadow Slider */}
|
||||
<div className="p-3 rounded-xl bg-white/5 border border-white/5 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between">\n
|
||||
<div className="text-xs font-medium text-slate-200">Shadow</div>
|
||||
<span className="text-[10px] text-slate-400 font-mono">{Math.round(shadowIntensity * 100)}%</span>
|
||||
</div>
|
||||
@@ -319,6 +348,7 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth,
|
||||
videoElement={videoElement || null}
|
||||
cropRegion={cropRegion}
|
||||
onCropChange={onCropChange}
|
||||
aspectRatio={aspectRatio}
|
||||
/>
|
||||
<div className="mt-6 flex justify-end">
|
||||
<Button
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type CropRegion,
|
||||
} from "./types";
|
||||
import { VideoExporter, type ExportProgress } from "@/lib/exporter";
|
||||
import { type AspectRatio, getAspectRatioValue } from "@/utils/aspectRatioUtils";
|
||||
|
||||
const WALLPAPER_COUNT = 18;
|
||||
const WALLPAPER_PATHS = Array.from({ length: WALLPAPER_COUNT }, (_, i) => `/wallpapers/wallpaper${i + 1}.jpg`);
|
||||
@@ -49,6 +50,7 @@ export default function VideoEditor() {
|
||||
const [exportProgress, setExportProgress] = useState<ExportProgress | null>(null);
|
||||
const [exportError, setExportError] = useState<string | null>(null);
|
||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||
const [aspectRatio, setAspectRatio] = useState<AspectRatio>('16:9');
|
||||
|
||||
const videoPlaybackRef = useRef<VideoPlaybackRef>(null);
|
||||
const nextZoomIdRef = useRef(1);
|
||||
@@ -395,8 +397,9 @@ 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: '16/9', maxWidth: '100%', margin: '0 auto', boxSizing: 'border-box' }}>
|
||||
<div className="relative" style={{ width: 'auto', height: '100%', aspectRatio: getAspectRatioValue(aspectRatio), maxWidth: '100%', margin: '0 auto', boxSizing: 'border-box' }}>
|
||||
<VideoPlayback
|
||||
aspectRatio={aspectRatio}
|
||||
ref={videoPlaybackRef}
|
||||
videoPath={videoPath || ''}
|
||||
onDurationChange={setDuration}
|
||||
@@ -484,6 +487,8 @@ export default function VideoEditor() {
|
||||
onPaddingChange={setPadding}
|
||||
cropRegion={cropRegion}
|
||||
onCropChange={setCropRegion}
|
||||
aspectRatio={aspectRatio}
|
||||
onAspectRatioChange={setAspectRatio}
|
||||
videoElement={videoPlaybackRef.current?.video || null}
|
||||
onExport={handleExport}
|
||||
/>
|
||||
|
||||
@@ -11,6 +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";
|
||||
|
||||
interface VideoPlaybackProps {
|
||||
videoPath: string;
|
||||
@@ -32,6 +33,7 @@ interface VideoPlaybackProps {
|
||||
padding?: number;
|
||||
cropRegion?: import('./types').CropRegion;
|
||||
trimRegions?: TrimRegion[];
|
||||
aspectRatio: AspectRatio;
|
||||
}
|
||||
|
||||
export interface VideoPlaybackRef {
|
||||
@@ -63,6 +65,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
padding = 50,
|
||||
cropRegion,
|
||||
trimRegions = [],
|
||||
aspectRatio,
|
||||
}, ref) => {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -747,7 +750,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
|
||||
: { background: resolvedWallpaper || '' };
|
||||
|
||||
return (
|
||||
<div className="relative aspect-video rounded-sm overflow-hidden" style={{ width: '100%' }}>
|
||||
<div className="relative rounded-sm overflow-hidden" style={{ width: '100%', aspectRatio: formatAspectRatioForCSS(aspectRatio) }}>
|
||||
{/* Background layer - always render as DOM element with blur */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
export type AspectRatio = '16:9' | '9:16' | '1:1' | '4:3';
|
||||
|
||||
/**
|
||||
* Converts aspect ratio string to numeric value
|
||||
* @param aspectRatio - Aspect ratio as string (e.g., '16:9')
|
||||
* @returns Numeric aspect ratio value (e.g., 1.777... for 16:9)
|
||||
*/
|
||||
export function getAspectRatioValue(aspectRatio: AspectRatio): number {
|
||||
switch (aspectRatio) {
|
||||
case '16:9':
|
||||
return 16 / 9;
|
||||
case '9:16':
|
||||
return 9 / 16;
|
||||
case '1:1':
|
||||
return 1;
|
||||
case '4:3':
|
||||
return 4 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates dimensions for a given aspect ratio based on a base width
|
||||
* @param aspectRatio - Aspect ratio as string
|
||||
* @param baseWidth - Base width to calculate from
|
||||
* @returns Object with width and height
|
||||
*/
|
||||
export function getAspectRatioDimensions(
|
||||
aspectRatio: AspectRatio,
|
||||
baseWidth: number
|
||||
): { width: number; height: number } {
|
||||
const ratio = getAspectRatioValue(aspectRatio);
|
||||
return {
|
||||
width: baseWidth,
|
||||
height: baseWidth / ratio,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats aspect ratio for CSS
|
||||
* @param aspectRatio - Aspect ratio as string
|
||||
* @returns CSS-compatible aspect ratio string
|
||||
*/
|
||||
export function formatAspectRatioForCSS(aspectRatio: AspectRatio): string {
|
||||
return aspectRatio.replace(':', '/');
|
||||
}
|
||||
Reference in New Issue
Block a user