({ videoElement, cropRegion, onCropChange }: CropControlProps)
| 19 | type DragHandle = "top" | "right" | "bottom" | "left" | null; |
| 20 | |
| 21 | export function CropControl({ videoElement, cropRegion, onCropChange }: CropControlProps) { |
| 22 | const canvasRef = useRef<HTMLCanvasElement>(null); |
| 23 | const containerRef = useRef<HTMLDivElement>(null); |
| 24 | const [isDragging, setIsDragging] = useState<DragHandle>(null); |
| 25 | const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); |
| 26 | const [initialCrop, setInitialCrop] = useState<CropRegion>(cropRegion); |
| 27 | |
| 28 | useEffect(() => { |
| 29 | if (!videoElement || !canvasRef.current) return; |
| 30 | |
| 31 | const canvas = canvasRef.current; |
| 32 | const ctx = canvas.getContext("2d", { alpha: false }); |
| 33 | if (!ctx) return; |
| 34 | |
| 35 | canvas.width = videoElement.videoWidth || 1920; |
| 36 | canvas.height = videoElement.videoHeight || 1080; |
| 37 | |
| 38 | let animationFrameId = 0; |
| 39 | let isCancelled = false; |
| 40 | |
| 41 | const draw = () => { |
| 42 | if (isCancelled) { |
| 43 | return; |
| 44 | } |
| 45 | |
| 46 | if (videoElement.readyState >= 2) { |
| 47 | ctx.clearRect(0, 0, canvas.width, canvas.height); |
| 48 | ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height); |
| 49 | } |
| 50 | animationFrameId = requestAnimationFrame(draw); |
| 51 | }; |
| 52 | |
| 53 | animationFrameId = requestAnimationFrame(draw); |
| 54 | return () => { |
| 55 | isCancelled = true; |
| 56 | cancelAnimationFrame(animationFrameId); |
| 57 | }; |
| 58 | }, [videoElement]); |
| 59 | |
| 60 | const getContainerRect = () => { |
| 61 | return ( |
| 62 | containerRef.current?.getBoundingClientRect() || { |
| 63 | width: 0, |
| 64 | height: 0, |
| 65 | left: 0, |
| 66 | top: 0, |
| 67 | } |
| 68 | ); |
| 69 | }; |
| 70 | |
| 71 | const handlePointerDown = (e: React.PointerEvent, handle: DragHandle) => { |
| 72 | e.stopPropagation(); |
| 73 | e.preventDefault(); |
| 74 | const rect = getContainerRect(); |
| 75 | if (rect.width <= 0 || rect.height <= 0) { |
| 76 | return; |
| 77 | } |
| 78 |
nothing calls this directly
no test coverage detected