191 lines
6.3 KiB
React
191 lines
6.3 KiB
React
|
|
import { Image as KonvaImage, Transformer, Rect, Text } from "react-konva";
|
|||
|
|
import { useRef, useEffect, useState } from "react";
|
|||
|
|
import useEditorStore from "../useEditorStore";
|
|||
|
|
|
|||
|
|
export default function ImageElement({ element }) {
|
|||
|
|
const updateElement = useEditorStore((state) => state.updateElement);
|
|||
|
|
const setSelectedId = useEditorStore((state) => state.setSelectedId);
|
|||
|
|
const selectedId = useEditorStore((state) => state.selectedId);
|
|||
|
|
|
|||
|
|
const shapeRef = useRef();
|
|||
|
|
const trRef = useRef();
|
|||
|
|
const [image, setImage] = useState(null);
|
|||
|
|
const [isHovered, setIsHovered] = useState(false);
|
|||
|
|
|
|||
|
|
const isSelected = selectedId === element.id;
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (element.src) {
|
|||
|
|
const img = new window.Image();
|
|||
|
|
img.src = element.src;
|
|||
|
|
img.onload = () => {
|
|||
|
|
setImage(img);
|
|||
|
|
};
|
|||
|
|
} else {
|
|||
|
|
setImage(null);
|
|||
|
|
}
|
|||
|
|
}, [element.src]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (isSelected && trRef.current && shapeRef.current) {
|
|||
|
|
trRef.current.nodes([shapeRef.current]);
|
|||
|
|
trRef.current.getLayer().batchDraw();
|
|||
|
|
}
|
|||
|
|
}, [isSelected]);
|
|||
|
|
|
|||
|
|
const handleImageUpload = () => {
|
|||
|
|
const input = document.createElement("input");
|
|||
|
|
input.type = "file";
|
|||
|
|
input.accept = "image/*";
|
|||
|
|
input.onchange = (e) => {
|
|||
|
|
const file = e.target.files[0];
|
|||
|
|
if (file) {
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
reader.onload = (event) => {
|
|||
|
|
updateElement(element.id, { src: event.target.result });
|
|||
|
|
};
|
|||
|
|
reader.readAsDataURL(file);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
input.click();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDeleteImage = (e) => {
|
|||
|
|
e.cancelBubble = true;
|
|||
|
|
updateElement(element.id, { src: null });
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (!image) {
|
|||
|
|
// Placeholder when no image
|
|||
|
|
return (
|
|||
|
|
<>
|
|||
|
|
<Rect
|
|||
|
|
ref={shapeRef}
|
|||
|
|
x={element.x}
|
|||
|
|
y={element.y}
|
|||
|
|
width={element.width}
|
|||
|
|
height={element.height}
|
|||
|
|
fill="#f0f0f0"
|
|||
|
|
stroke="#ddd"
|
|||
|
|
strokeWidth={2}
|
|||
|
|
dash={[5, 5]}
|
|||
|
|
rotation={element.rotation}
|
|||
|
|
draggable
|
|||
|
|
onClick={() => {
|
|||
|
|
setSelectedId(element.id);
|
|||
|
|
handleImageUpload();
|
|||
|
|
}}
|
|||
|
|
onDragEnd={(e) => {
|
|||
|
|
const snap = (v) => Math.round(v / 10) * 10;
|
|||
|
|
updateElement(element.id, {
|
|||
|
|
x: snap(e.target.x()),
|
|||
|
|
y: snap(e.target.y()),
|
|||
|
|
});
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<Text
|
|||
|
|
text="📷 Click to Upload"
|
|||
|
|
x={element.x + element.width / 2 - 60}
|
|||
|
|
y={element.y + element.height / 2 - 10}
|
|||
|
|
fontSize={14}
|
|||
|
|
fill="#999"
|
|||
|
|
listening={false}
|
|||
|
|
/>
|
|||
|
|
{isSelected && (
|
|||
|
|
<Transformer
|
|||
|
|
ref={trRef}
|
|||
|
|
rotateEnabled={true}
|
|||
|
|
boundBoxFunc={(oldBox, newBox) => {
|
|||
|
|
if (newBox.width < 20 || newBox.height < 20) {
|
|||
|
|
return oldBox;
|
|||
|
|
}
|
|||
|
|
return newBox;
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<>
|
|||
|
|
<KonvaImage
|
|||
|
|
ref={shapeRef}
|
|||
|
|
image={image}
|
|||
|
|
x={element.x}
|
|||
|
|
y={element.y}
|
|||
|
|
width={element.width}
|
|||
|
|
height={element.height}
|
|||
|
|
rotation={element.rotation}
|
|||
|
|
draggable
|
|||
|
|
onClick={() => setSelectedId(element.id)}
|
|||
|
|
onMouseEnter={() => setIsHovered(true)}
|
|||
|
|
onMouseLeave={() => setIsHovered(false)}
|
|||
|
|
onDragEnd={(e) => {
|
|||
|
|
const snap = (v) => Math.round(v / 10) * 10;
|
|||
|
|
updateElement(element.id, {
|
|||
|
|
x: snap(e.target.x()),
|
|||
|
|
y: snap(e.target.y()),
|
|||
|
|
});
|
|||
|
|
}}
|
|||
|
|
onTransformEnd={() => {
|
|||
|
|
const node = shapeRef.current;
|
|||
|
|
const scaleX = node.scaleX();
|
|||
|
|
const scaleY = node.scaleY();
|
|||
|
|
|
|||
|
|
node.scaleX(1);
|
|||
|
|
node.scaleY(1);
|
|||
|
|
|
|||
|
|
updateElement(element.id, {
|
|||
|
|
x: node.x(),
|
|||
|
|
y: node.y(),
|
|||
|
|
width: Math.max(5, node.width() * scaleX),
|
|||
|
|
height: Math.max(5, node.height() * scaleY),
|
|||
|
|
rotation: node.rotation(),
|
|||
|
|
});
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{/* Delete button on hover */}
|
|||
|
|
{isHovered && (
|
|||
|
|
<Rect
|
|||
|
|
x={element.x + element.width - 30}
|
|||
|
|
y={element.y + 5}
|
|||
|
|
width={25}
|
|||
|
|
height={25}
|
|||
|
|
fill="#ff4444"
|
|||
|
|
cornerRadius={4}
|
|||
|
|
onClick={handleDeleteImage}
|
|||
|
|
onTap={handleDeleteImage}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
{isHovered && (
|
|||
|
|
<Text
|
|||
|
|
text="×"
|
|||
|
|
x={element.x + element.width - 22}
|
|||
|
|
y={element.y + 8}
|
|||
|
|
fontSize={18}
|
|||
|
|
fill="#fff"
|
|||
|
|
fontStyle="bold"
|
|||
|
|
onClick={handleDeleteImage}
|
|||
|
|
onTap={handleDeleteImage}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{isSelected && (
|
|||
|
|
<Transformer
|
|||
|
|
ref={trRef}
|
|||
|
|
rotateEnabled={true}
|
|||
|
|
boundBoxFunc={(oldBox, newBox) => {
|
|||
|
|
// Limit resize
|
|||
|
|
if (newBox.width < 20 || newBox.height < 20) {
|
|||
|
|
return oldBox;
|
|||
|
|
}
|
|||
|
|
return newBox;
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
}
|