StickerAI-Front/src/screens/CropPhoto.tsx

349 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useRef, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import styles from './CropPhoto.module.css';
const CropPhoto: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const imageRef = useRef<HTMLImageElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const viewportRef = useRef<HTMLDivElement>(null);
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [scale, setScale] = useState(1);
const [minScale, setMinScale] = useState(1);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const [initialTouch, setInitialTouch] = useState<{ distance: number; scale: number } | null>(null);
// Эффект для создания и очистки blob URL
useEffect(() => {
const file = (location.state as any)?.file;
if (!file) {
navigate('/');
return;
}
// Создаем URL только если его еще нет
if (!imageUrl) {
const url = URL.createObjectURL(file);
setImageUrl(url);
}
// Очищаем URL при размонтировании компонента
return () => {
if (imageUrl) {
URL.revokeObjectURL(imageUrl);
setImageUrl(null);
}
};
}, [location.state, navigate, imageUrl]);
// Отдельный эффект для обработки загруженного изображения
useEffect(() => {
if (!imageUrl || !viewportRef.current) return;
const img = new Image();
img.onload = () => {
const viewport = viewportRef.current;
if (!viewport) return;
const frame = viewport.querySelector(`.${styles.frame}`);
if (!frame) return;
const viewportRect = viewport.getBoundingClientRect();
const frameRect = frame.getBoundingClientRect();
// Используем размеры рамки обрезки
const frameWidth = frameRect.width;
const frameHeight = frameRect.height;
// Вычисляем масштаб, чтобы изображение полностью покрывало область обрезки
const scaleX = frameWidth / img.naturalWidth;
const scaleY = frameHeight / img.naturalHeight;
const baseScale = Math.max(scaleX, scaleY);
setMinScale(baseScale);
setScale(baseScale);
// Центрируем изображение относительно рамки
const scaledWidth = img.naturalWidth * baseScale;
const scaledHeight = img.naturalHeight * baseScale;
// Учитываем отступы от рамки до viewport
const frameLeft = frameRect.left - viewportRect.left;
const frameTop = frameRect.top - viewportRect.top;
setPosition({
x: frameLeft + (frameWidth - scaledWidth) / 2,
y: frameTop + (frameHeight - scaledHeight) / 2
});
};
img.onerror = () => {
// В случае ошибки загрузки изображения
console.error('Ошибка загрузки изображения');
navigate('/');
};
img.src = imageUrl;
}, [imageUrl, navigate]);
const handleStart = (e: React.MouseEvent | React.TouchEvent) => {
if ('touches' in e) {
if (e.touches.length === 2) {
const distance = getDistance(e.touches);
setInitialTouch({ distance, scale });
setIsDragging(false);
} else {
const touch = e.touches[0];
setIsDragging(true);
setDragStart({ x: touch.clientX - position.x, y: touch.clientY - position.y });
}
} else {
setIsDragging(true);
setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y });
}
};
const handleMove = (e: React.MouseEvent | React.TouchEvent) => {
if ('touches' in e) {
if (e.touches.length === 2 && initialTouch) {
// Масштабирование двумя пальцами
const distance = getDistance(e.touches);
const newScale = Math.min(
Math.max(
(initialTouch.scale * distance) / initialTouch.distance,
minScale
),
minScale * 3
);
handleScale(newScale);
} else if (isDragging) {
const touch = e.touches[0];
handleDrag(touch.clientX, touch.clientY);
}
} else if (isDragging) {
handleDrag(e.clientX, e.clientY);
}
};
const handleEnd = () => {
setIsDragging(false);
setInitialTouch(null);
};
const getDistance = (touches: React.TouchList) => {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
};
const handleDrag = (clientX: number, clientY: number) => {
if (!viewportRef.current || !imageRef.current) return;
const viewport = viewportRef.current;
const frame = viewport.querySelector(`.${styles.frame}`);
if (!frame) return;
const viewportRect = viewport.getBoundingClientRect();
const frameRect = frame.getBoundingClientRect();
// Получаем позицию рамки относительно viewport
const frameLeft = frameRect.left - viewportRect.left;
const frameTop = frameRect.top - viewportRect.top;
// Вычисляем размеры масштабированного изображения
const scaledWidth = imageRef.current.naturalWidth * scale;
const scaledHeight = imageRef.current.naturalHeight * scale;
// Вычисляем новую позицию
const newX = clientX - dragStart.x;
const newY = clientY - dragStart.y;
// Ограничиваем перемещение
const minX = frameLeft - (scaledWidth - frameRect.width);
const maxX = frameLeft;
const minY = frameTop - (scaledHeight - frameRect.height);
const maxY = frameTop;
setPosition({
x: Math.max(minX, Math.min(maxX, newX)),
y: Math.max(minY, Math.min(maxY, newY))
});
};
const handleScale = (newScale: number) => {
if (!viewportRef.current || !imageRef.current) return;
const viewport = viewportRef.current;
const frame = viewport.querySelector(`.${styles.frame}`);
if (!frame) return;
const viewportRect = viewport.getBoundingClientRect();
const frameRect = frame.getBoundingClientRect();
// Получаем позицию рамки относительно viewport
const frameLeft = frameRect.left - viewportRect.left;
const frameTop = frameRect.top - viewportRect.top;
// Вычисляем центр рамки
const frameCenterX = frameLeft + frameRect.width / 2;
const frameCenterY = frameTop + frameRect.height / 2;
// Вычисляем текущий центр изображения
const currentCenterX = position.x + (imageRef.current.naturalWidth * scale) / 2;
const currentCenterY = position.y + (imageRef.current.naturalHeight * scale) / 2;
// Вычисляем смещение для сохранения центра
const dx = (frameCenterX - currentCenterX) * (newScale / scale - 1);
const dy = (frameCenterY - currentCenterY) * (newScale / scale - 1);
// Вычисляем новые размеры изображения
const newScaledWidth = imageRef.current.naturalWidth * newScale;
const newScaledHeight = imageRef.current.naturalHeight * newScale;
// Вычисляем новую позицию
let newX = position.x + dx;
let newY = position.y + dy;
// Ограничиваем позицию
const minX = frameLeft - (newScaledWidth - frameRect.width);
const maxX = frameLeft;
const minY = frameTop - (newScaledHeight - frameRect.height);
const maxY = frameTop;
setScale(newScale);
setPosition({
x: Math.max(minX, Math.min(maxX, newX)),
y: Math.max(minY, Math.min(maxY, newY))
});
};
const handleZoom = (e: React.ChangeEvent<HTMLInputElement>) => {
handleScale(parseFloat(e.target.value));
};
const createPreview = () => {
if (!imageRef.current || !viewportRef.current) return null;
const canvas = document.createElement('canvas');
canvas.width = 768;
canvas.height = 768;
const ctx = canvas.getContext('2d');
if (!ctx) return null;
const viewport = viewportRef.current;
const frame = viewport.querySelector(`.${styles.frame}`);
if (!frame) return null;
const frameRect = frame.getBoundingClientRect();
const image = imageRef.current;
ctx.drawImage(
image,
-position.x / scale,
-position.y / scale,
frameRect.width / scale,
frameRect.height / scale,
0,
0,
768,
768
);
return canvas.toDataURL('image/jpeg', 0.95);
};
const handleConfirm = () => {
const previewUrl = createPreview();
if (previewUrl) {
// Передаем не только URL, но и base64 данные
// Убираем префикс data:image/jpeg;base64, оставляем только данные
const imageData = previewUrl.split(',')[1];
// Сохраняем данные в localStorage для сохранения между сеансами навигации
localStorage.setItem('stickerPreviewUrl', previewUrl);
localStorage.setItem('stickerImageData', imageData);
navigate('/', {
state: {
previewUrl,
imageData
}
});
}
};
return (
<div className={styles.container}>
<div className={styles.header}>
<button className={styles.backButton} onClick={() => navigate('/')}>
</button>
<h1 className={styles.title}>Обрезка фото</h1>
<button className={styles.confirmButton} onClick={handleConfirm}>
Готово
</button>
</div>
<div className={styles.cropContainer}>
<div className={styles.viewportContainer}>
<div
ref={viewportRef}
className={styles.viewport}
onTouchStart={handleStart}
onTouchMove={handleMove}
onTouchEnd={handleEnd}
onTouchCancel={handleEnd}
onMouseDown={handleStart}
onMouseMove={handleMove}
onMouseUp={handleEnd}
onMouseLeave={handleEnd}
onContextMenu={(e) => e.preventDefault()}
>
{imageUrl && (
<div
ref={wrapperRef}
className={styles.imageWrapper}
>
<img
ref={imageRef}
src={imageUrl}
alt="Crop"
className={styles.image}
draggable={false}
style={{
transform: `matrix(${scale}, 0, 0, ${scale}, ${position.x}, ${position.y})`
}}
/>
</div>
)}
<div className={styles.frame} />
<div className={styles.frameCross} />
</div>
</div>
<div className={styles.controls}>
<input
type="range"
min={minScale}
max={minScale * 3}
step="0.01"
value={scale}
onChange={handleZoom}
className={styles.zoomSlider}
/>
<div className={styles.hint}>
Отрегулируйте масштаб и положение фото
</div>
</div>
</div>
</div>
);
};
export default CropPhoto;