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" | "move" | 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; case "move": { newCrop.x = Math.max(0, Math.min(initialCrop.x + deltaX, 1 - initialCrop.width)); newCrop.y = Math.max(0, Math.min(initialCrop.y + deltaY, 1 - initialCrop.height)); break; } } onCropChange(newCrop); }; const handlePointerUp = (e: React.PointerEvent) => { if (isDragging) { try { e.currentTarget.releasePointerCapture(e.pointerId); } catch { // Pointer may already be released; ignore. } } 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, "move")} />
handlePointerDown(e, "top")} />
handlePointerDown(e, "bottom")} />
handlePointerDown(e, "left")} />
handlePointerDown(e, "right")} />
); }