Files
web/frontend/src/pages/chat/ui/left/profile/ImageCropper.tsx
T
2025-10-11 00:35:58 +03:00

195 lines
6.2 KiB
TypeScript

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<HTMLCanvasElement>(null);
const imageRef = useRef<HTMLImageElement>(null);
const [src, setSrc] = useState<string | undefined>(undefined);
const [isLoaded, setIsLoaded] = useState(false);
const [cropArea, setCropArea] = useState<Rect>({ x: 0, y: 0, width: 200, height: 200 });
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState<Size2D>({ 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 (
<div className="cropper-container">
<canvas
ref={canvasRef}
width={400}
height={400}
style={{
cursor: isDragging ? "grabbing" : "grab",
border: "1px solid #ccc",
maxWidth: "100%",
height: "auto"
}}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
/>
<img
ref={imageRef}
src={src}
style={{ display: "none" }}
alt="Crop source"
/>
<div className="cropper-actions">
<mdui-button onClick={handleCrop} disabled={!isLoaded}>
Обрезать
</mdui-button>
<mdui-button variant="outlined" onClick={onCancel}>
Отмена
</mdui-button>
</div>
</div>
);
}