react-perspective-crop
Version:
A flexible React image cropper with draggable custom-shaped bullets and export capabilities.
299 lines (292 loc) • 8.32 kB
JavaScript
// src/App.tsx
import { useEffect, useRef as useRef2, useState } from "react";
// src/components/Bullet/index.tsx
import React, { useRef } from "react";
// src/styled-components/index.ts
import styled from "styled-components";
var BulletContainer = styled.div`
border: 1px solid black;
width: ${({ $width }) => !!$width ? typeof $width === "string" ? $width : `${$width}px` : `100%`};
height: ${({ $height }) => !!$height ? typeof $height === "string" ? $height : `${$height}px` : `100%`};
position: relative;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 10;
}
& > * {
margin: 0;
padding: 0;
box-sizing: border-box;
}
`;
var BulletItem = styled.div`
display: flex;
justify-content: center;
align-items: center;
position: absolute;
border: 1px solid red;
border-radius: 50%;
width: ${(props) => props.$bulletSize}px;
height: ${(props) => props.$bulletSize}px;
box-sizing: border-box;
left: ${(props) => props.$cordinate?.left}px;
top: ${(props) => props.$cordinate?.top}px;
cursor: pointer;
z-index: 100;
`;
var SelectedImage = styled.img`
width: 100%;
height: 100%;
clip-path: ${({ $cordinates }) => `polygon(${$cordinates.map(
(cordinate) => `${cordinate.x}px ${cordinate.y}px`
)})`};
z-index: 100;
position: absolute;
user-select: none;
`;
var MainImage = styled.img`
width: 100%;
height: 100%;
position: absolute;
z-index: -1;
user-select: none;
`;
// src/components/Bullet/index.tsx
import { jsx } from "react/jsx-runtime";
var Bullets = React.forwardRef(
({
bulletData,
bulletSize,
icon,
onDragStart,
onDrag,
onDragEnd
}, ref) => {
const R = useRef({
x: 0,
y: 0
});
const point = useRef({
x: bulletData.x,
y: bulletData.y
});
const handleDragStart = (e, BulletId) => {
if (ref?.current) {
e.dataTransfer.setDragImage(ref?.current, 0, 0);
}
const bounds = e.target.getBoundingClientRect();
R.current.x = (Math.floor(bounds.left) + Math.floor(bounds.right)) / 2 - e.clientX;
R.current.y = (Math.floor(bounds.bottom) + Math.floor(bounds.top)) / 2 - e.clientY;
onDragStart(BulletId);
};
const handleDrag = (e) => {
e.preventDefault();
if ((e.clientX !== point.current.x || e.clientY !== point.current.y) && (Math.abs(e.clientX - point.current.x) !== point.current.x || Math.abs(e.clientY - point.current.y) !== point.current.y)) {
onDrag({
id: bulletData.id,
x: e.clientX + R.current.x,
y: e.clientY + R.current.y
});
point.current.x = e.clientX;
point.current.y = e.clientY;
}
};
const handleDragEnd = (e) => {
e.preventDefault();
onDragEnd({
id: bulletData.id,
x: e.clientX + R.current.x,
y: e.clientY + R.current.y
});
};
const handleDragOver = (e) => e.dataTransfer.effectAllowed = "move";
return /* @__PURE__ */ jsx(
BulletItem,
{
$cordinate: {
left: bulletData.x - bulletSize / 2,
top: bulletData.y - bulletSize / 2
},
$bulletSize: bulletSize,
draggable: true,
onDragStart: (e) => handleDragStart(e, bulletData.id),
onDrag: handleDrag,
onDragEnd: handleDragEnd,
onDragOver: handleDragOver,
children: icon ?? icon
}
);
}
);
var Bullet_default = Bullets;
// src/utils/imageProcessor.ts
var imageProcessor = async (imageSrc, bulletsData, width, height) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
if (!ctx) {
return reject("Canvas context not available");
}
const path = new Path2D();
const coordinates = bulletsData.map(
(item) => [item.x, item.y]
);
if (coordinates.length > 0) {
path.moveTo(coordinates[0][0], coordinates[0][1]);
for (let i = 1; i < coordinates.length; i++) {
path.lineTo(coordinates[i][0], coordinates[i][1]);
}
path.closePath();
}
ctx.save();
ctx.clip(path);
ctx.drawImage(img, 0, 0, width, height);
ctx.restore();
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
reject("Failed to create blob");
}
}, "image/jpeg");
};
img.onerror = () => reject("Failed to load image");
img.src = imageSrc;
});
};
var imageProcessor_default = imageProcessor;
// src/utils/downloadImage.ts
var downloadImage = async (value) => {
const url = URL.createObjectURL(value);
const a = document.createElement("a");
a.href = url;
a.download = "cropped-image.jpg";
a.click();
};
var downloadImage_default = downloadImage;
// src/App.tsx
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
function App(props = {
bulletsDefaultCordinates: [],
imageSrc: "",
onChange: () => {
},
downloadCroppedImg: () => {
}
}) {
const container = useRef2(null);
const invisibleItem = useRef2(null);
const [bulletsData, setBulletsData] = useState(
props.bulletsDefaultCordinates
);
const [activeBulletIndex, setActiveBulletIndex] = useState();
const [isDragging, setIsDragging] = useState(false);
useEffect(() => {
if (isDragging) {
props.onChange(bulletsData);
}
if (props.downloadCroppedImg) {
props.downloadCroppedImg(download);
}
}, [bulletsData]);
const handleDragStart = (activeBulletId) => {
const activeIndex = bulletsData.findIndex(
(item) => item.id === activeBulletId
);
if (activeIndex !== -1) {
setActiveBulletIndex(activeIndex);
setIsDragging(true);
}
};
const download = async () => {
const containerBox = container.current.getBoundingClientRect();
const blob = await imageProcessor_default(
props.imageSrc,
bulletsData,
containerBox.width,
containerBox.height
);
downloadImage_default(blob);
};
const handleBulletsData = (value) => {
if (isDragging && activeBulletIndex !== void 0) {
const localX = value.x - container?.current?.offsetLeft;
const localY = value.y - container?.current?.offsetTop;
if (localX > 0 && localX < container.current.offsetWidth && localY < container.current.offsetHeight && localY > 0) {
const copyBulletsData = [...bulletsData];
copyBulletsData[activeBulletIndex] = {
...value,
x: localX,
y: localY
};
setBulletsData(copyBulletsData);
}
}
};
const handleOnDragEnd = (value) => {
if (value.x === 0 && value.y === 0) {
handleBulletsData(value);
}
setIsDragging(false);
};
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsxs(
BulletContainer,
{
ref: container,
$width: props.width,
$height: props.height,
children: [
bulletsData.map((bullet) => {
return /* @__PURE__ */ jsx2(
Bullet_default,
{
ref: invisibleItem,
bulletData: bullet,
bulletSize: 20,
onDragStart: handleDragStart,
onDrag: handleBulletsData,
onDragEnd: handleOnDragEnd,
icon: `${bullet.id}`
},
`${bullet.id}`
);
}),
/* @__PURE__ */ jsx2(MainImage, { src: props.imageSrc }),
/* @__PURE__ */ jsx2(SelectedImage, { src: props.imageSrc, $cordinates: bulletsData })
]
}
),
/* @__PURE__ */ jsx2(
"div",
{
ref: invisibleItem,
id: "invisible-drag-image",
style: {
width: "1px",
height: "1px",
opacity: "0",
position: "absolute",
top: "-1000px",
zIndex: "-1"
}
}
)
] });
}
var App_default = App;
export {
App_default as ReactCropper
};
//# sourceMappingURL=index.mjs.map