import { useCallback, useEffect, useRef, useState } from "react"; import type { Size2D, Rect } from "@/core/types"; interface ImageCropperProps { onCrop: (croppedImageData: string) => void; onCancel: () => void; imageFile: File | null; } export function ImageCropper({ onCrop, onCancel, imageFile }: ImageCropperProps) { const canvasRef = useRef(null); const imageRef = useRef(null); const [src, setSrc] = useState(undefined); const [isLoaded, setIsLoaded] = useState(false); const [cropArea, setCropArea] = useState({ x: 0, y: 0, width: 200, height: 200 }); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); useEffect(() => { const img = imageRef.current; if (imageFile) { const reader = new FileReader(); function handleImageLoad() { setIsLoaded(true); if (img) { const size = Math.min(img.naturalWidth, img.naturalHeight) * 0.8; setCropArea({ x: (img.naturalWidth - size) / 2, y: (img.naturalHeight - size) / 2, width: size, height: size }); } } function handleReaderLoad() { if (img) { setSrc(reader.result as string); img.addEventListener("load", handleImageLoad); } } reader.addEventListener("load", handleReaderLoad); reader.readAsDataURL(imageFile); return () => { reader.abort(); reader.removeEventListener("load", handleReaderLoad); img?.removeEventListener("load", handleImageLoad); } } }, [imageFile, imageRef]); function handleMouseDown(e: React.MouseEvent) { if (!isLoaded) return; const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Check if click is within crop area if (x >= cropArea.x && x <= cropArea.x + cropArea.width && y >= cropArea.y && y <= cropArea.y + cropArea.height) { setIsDragging(true); setDragStart({ x: x - cropArea.x, y: y - cropArea.y }); } }; function handleMouseMove(e: React.MouseEvent) { const rect = canvasRef.current?.getBoundingClientRect(); if (isDragging && isLoaded && rect && imageRef.current) { const x = e.clientX - rect.left; const y = e.clientY - rect.top; const newX = Math.max( 0, Math.min( x - dragStart.x, imageRef.current.naturalWidth - cropArea.width ) ); const newY = Math.max( 0, Math.min( y - dragStart.y, imageRef.current.naturalHeight - cropArea.height ) ); setCropArea(prev => ({ ...prev, x: newX, y: newY })); } }; function handleMouseUp() { setIsDragging(false); }; function handleCrop() { const img = imageRef.current; if (!canvasRef.current || !img || !isLoaded) return; const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); if (!ctx) return; // Set canvas size to crop area canvas.width = cropArea.width; canvas.height = cropArea.height; // Draw cropped portion ctx.drawImage( img, cropArea.x, cropArea.y, cropArea.width, cropArea.height, 0, 0, cropArea.width, cropArea.height ); // Convert to data URL const croppedImageData = canvas.toDataURL("image/jpeg", 0.9); onCrop(croppedImageData); }; const drawCropArea = useCallback(() => { const img = imageRef.current; if (!canvasRef.current || !img || !isLoaded) return; const canvas = canvasRef.current; const ctx = canvas.getContext("2d"); if (!ctx) return; // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw image ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // Draw crop overlay ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; ctx.fillRect(0, 0, canvas.width, canvas.height); // Clear crop area ctx.globalCompositeOperation = "destination-out"; ctx.fillRect(cropArea.x, cropArea.y, cropArea.width, cropArea.height); // Draw crop border ctx.globalCompositeOperation = "source-over"; ctx.strokeStyle = "#fff"; ctx.lineWidth = 2; ctx.strokeRect(cropArea.x, cropArea.y, cropArea.width, cropArea.height); }, [cropArea, isLoaded, imageRef]); useEffect(() => { drawCropArea(); }, [cropArea, isLoaded, drawCropArea]); if (!imageFile) return null; return (
Crop source
Обрезать Отмена
); }