import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; import { type AspectRatio } from "@/utils/aspectRatioUtils"; interface CropRegion { x: number; // 0-1 normalized y: number; // 0-1 normalized width: number; // 0-1 normalized height: number; // 0-1 normalized } 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) { const canvasRef = useRef(null); const containerRef = useRef(null); const [isDragging, setIsDragging] = useState(null); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const [initialCrop, setInitialCrop] = useState(cropRegion); useEffect(() => { if (!videoElement || !canvasRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext('2d', { alpha: false }); if (!ctx) return; canvas.width = videoElement.videoWidth || 1920; canvas.height = videoElement.videoHeight || 1080; const draw = () => { if (videoElement.readyState >= 2) { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height); } requestAnimationFrame(draw); }; const rafId = requestAnimationFrame(draw); return () => cancelAnimationFrame(rafId); }, [videoElement]); const getContainerRect = () => { return containerRef.current?.getBoundingClientRect() || { width: 0, height: 0, left: 0, top: 0 }; }; const handlePointerDown = (e: React.PointerEvent, handle: DragHandle) => { e.stopPropagation(); e.preventDefault(); setIsDragging(handle); const rect = getContainerRect(); setDragStart({ x: (e.clientX - rect.left) / rect.width, y: (e.clientY - rect.top) / rect.height, }); setInitialCrop(cropRegion); e.currentTarget.setPointerCapture(e.pointerId); }; const handlePointerMove = (e: React.PointerEvent) => { if (!isDragging) return; const rect = getContainerRect(); const currentX = (e.clientX - rect.left) / rect.width; const currentY = (e.clientY - rect.top) / rect.height; const deltaX = currentX - dragStart.x; const deltaY = currentY - dragStart.y; let newCrop = { ...initialCrop }; switch (isDragging) { case 'top': { const newY = Math.max(0, initialCrop.y + deltaY); const bottom = initialCrop.y + initialCrop.height; newCrop.y = Math.min(newY, bottom - 0.1); newCrop.height = bottom - newCrop.y; break; } case 'bottom': newCrop.height = Math.max(0.1, Math.min(initialCrop.height + deltaY, 1 - initialCrop.y)); break; case 'left': { const newX = Math.max(0, initialCrop.x + deltaX); const right = initialCrop.x + initialCrop.width; newCrop.x = Math.min(newX, right - 0.1); newCrop.width = right - newCrop.x; break; } case 'right': newCrop.width = Math.max(0.1, Math.min(initialCrop.width + deltaX, 1 - initialCrop.x)); break; } onCropChange(newCrop); }; const handlePointerUp = (e: React.PointerEvent) => { if (isDragging) { try { e.currentTarget.releasePointerCapture(e.pointerId); } catch { } } setIsDragging(null); }; const cropPixelX = cropRegion.x * 100; const cropPixelY = cropRegion.y * 100; const cropPixelWidth = cropRegion.width * 100; const cropPixelHeight = cropRegion.height * 100; const videoAspectRatio = videoElement ? videoElement.videoWidth / videoElement.videoHeight : 16/9; const isVideoPortrait = videoAspectRatio < 1; const maxContainerWidth = isVideoPortrait ? '40vw' : '75vw'; const maxContainerHeight = '75vh'; return (
handlePointerDown(e, 'top')} />
handlePointerDown(e, 'bottom')} />
handlePointerDown(e, 'left')} />
handlePointerDown(e, 'right')} />
); }