diff --git a/src/components/video-editor/CropControl.tsx b/src/components/video-editor/CropControl.tsx index e95b020..fc14894 100644 --- a/src/components/video-editor/CropControl.tsx +++ b/src/components/video-editor/CropControl.tsx @@ -16,7 +16,7 @@ interface CropControlProps { aspectRatio: AspectRatio; } -type DragHandle = 'top' | 'right' | 'bottom' | 'left' | null; +type DragHandle = 'top' | 'right' | 'bottom' | 'left' | 'move' | null; export function CropControl({ videoElement, cropRegion, onCropChange }: CropControlProps) { const canvasRef = useRef(null); @@ -97,6 +97,13 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont case 'right': newCrop.width = Math.max(0.1, Math.min(initialCrop.width + deltaX, 1 - initialCrop.x)); break; + case 'move': { + const newX = Math.max(0, Math.min(initialCrop.x + deltaX, 1 - initialCrop.width)); + const newY = Math.max(0, Math.min(initialCrop.y + deltaY, 1 - initialCrop.height)); + newCrop.x = newX; + newCrop.y = newY; + break; + } } onCropChange(newCrop); @@ -168,6 +175,18 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont +
handlePointerDown(e, 'move')} + /> +
(GRADIENTS[0]); const [showCropDropdown, setShowCropDropdown] = useState(false); + const [cropAspectLocked, setCropAspectLocked] = useState(false); + const [cropAspectRatio, setCropAspectRatio] = useState(''); + + const videoWidth = videoElement?.videoWidth || 1920; + const videoHeight = videoElement?.videoHeight || 1080; + + const handleCropNumericChange = useCallback((field: 'x' | 'y' | 'width' | 'height', pixelValue: number) => { + if (!cropRegion || !onCropChange) return; + const norm = { ...cropRegion }; + const maxW = videoWidth; + const maxH = videoHeight; + + switch (field) { + case 'x': + norm.x = Math.max(0, Math.min(pixelValue / maxW, 1 - norm.width)); + break; + case 'y': + norm.y = Math.max(0, Math.min(pixelValue / maxH, 1 - norm.height)); + break; + case 'width': { + const newW = Math.max(0.05, Math.min(pixelValue / maxW, 1 - norm.x)); + if (cropAspectLocked && norm.width > 0 && norm.height > 0) { + const ratio = norm.width / norm.height; + const newH = newW / ratio; + if (norm.y + newH <= 1) { + norm.width = newW; + norm.height = newH; + } + } else { + norm.width = newW; + } + break; + } + case 'height': { + const newH = Math.max(0.05, Math.min(pixelValue / maxH, 1 - norm.y)); + if (cropAspectLocked && norm.width > 0 && norm.height > 0) { + const ratio = norm.width / norm.height; + const newW = newH * ratio; + if (norm.x + newW <= 1) { + norm.height = newH; + norm.width = newW; + } + } else { + norm.height = newH; + } + break; + } + } + onCropChange(norm); + }, [cropRegion, onCropChange, videoWidth, videoHeight, cropAspectLocked]); + + const applyCropAspectPreset = useCallback((preset: string) => { + if (!cropRegion || !onCropChange) return; + setCropAspectRatio(preset); + if (preset === '') { + setCropAspectLocked(false); + return; + } + const [wStr, hStr] = preset.split(':'); + const targetRatio = Number(wStr) / Number(hStr); + const norm = { ...cropRegion }; + // Keep the current width, adjust height to match ratio + const newH = (norm.width * videoWidth) / (targetRatio * videoHeight); + if (norm.y + newH <= 1 && newH >= 0.05) { + norm.height = newH; + } else { + // Keep height, adjust width + const newW = (norm.height * videoHeight * targetRatio) / videoWidth; + if (norm.x + newW <= 1 && newW >= 0.05) { + norm.width = newW; + } + } + onCropChange(norm); + setCropAspectLocked(true); + }, [cropRegion, onCropChange, videoWidth, videoHeight]); const zoomEnabled = Boolean(selectedZoomDepth); const trimEnabled = Boolean(selectedTrimId); @@ -624,14 +699,72 @@ export function SettingsPanel({ onCropChange={onCropChange} aspectRatio={aspectRatio} /> -
- +
+
+ {[ + { label: 'X', field: 'x' as const, max: videoWidth }, + { label: 'Y', field: 'y' as const, max: videoHeight }, + { label: 'W', field: 'width' as const, max: videoWidth }, + { label: 'H', field: 'height' as const, max: videoHeight }, + ].map(({ label, field, max }) => ( +
+ + )[field] * (field === 'x' || field === 'width' ? videoWidth : videoHeight))} + onChange={(e) => handleCropNumericChange(field, Number(e.target.value))} + className="w-[90px] h-8 rounded-md border border-white/10 bg-white/5 px-2 text-xs text-slate-200 outline-none focus:border-[#34B27B]/50 focus:ring-1 focus:ring-[#34B27B]/30 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + /> +
+ ))} + +
+ +
+ + +
+
+ +

+ {videoWidth} × {videoHeight}px +

+
+ +
+ +