react-perspective-crop
Version:
A flexible React image cropper with draggable custom-shaped bullets and export capabilities.
336 lines (327 loc) • 10.4 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
ReactCropper: () => App_default
});
module.exports = __toCommonJS(index_exports);
// src/App.tsx
var import_react2 = require("react");
// src/components/Bullet/index.tsx
var import_react = __toESM(require("react"));
// src/styled-components/index.ts
var import_styled_components = __toESM(require("styled-components"));
var BulletContainer = import_styled_components.default.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 = import_styled_components.default.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 = import_styled_components.default.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 = import_styled_components.default.img`
width: 100%;
height: 100%;
position: absolute;
z-index: -1;
user-select: none;
`;
// src/components/Bullet/index.tsx
var import_jsx_runtime = require("react/jsx-runtime");
var Bullets = import_react.default.forwardRef(
({
bulletData,
bulletSize,
icon,
onDragStart,
onDrag,
onDragEnd
}, ref) => {
const R = (0, import_react.useRef)({
x: 0,
y: 0
});
const point = (0, import_react.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__ */ (0, import_jsx_runtime.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
var import_jsx_runtime2 = require("react/jsx-runtime");
function App(props = {
bulletsDefaultCordinates: [],
imageSrc: "",
onChange: () => {
},
downloadCroppedImg: () => {
}
}) {
const container = (0, import_react2.useRef)(null);
const invisibleItem = (0, import_react2.useRef)(null);
const [bulletsData, setBulletsData] = (0, import_react2.useState)(
props.bulletsDefaultCordinates
);
const [activeBulletIndex, setActiveBulletIndex] = (0, import_react2.useState)();
const [isDragging, setIsDragging] = (0, import_react2.useState)(false);
(0, import_react2.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__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
BulletContainer,
{
ref: container,
$width: props.width,
$height: props.height,
children: [
bulletsData.map((bullet) => {
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
Bullet_default,
{
ref: invisibleItem,
bulletData: bullet,
bulletSize: 20,
onDragStart: handleDragStart,
onDrag: handleBulletsData,
onDragEnd: handleOnDragEnd,
icon: `${bullet.id}`
},
`${bullet.id}`
);
}),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MainImage, { src: props.imageSrc }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SelectedImage, { src: props.imageSrc, $cordinates: bulletsData })
]
}
),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"div",
{
ref: invisibleItem,
id: "invisible-drag-image",
style: {
width: "1px",
height: "1px",
opacity: "0",
position: "absolute",
top: "-1000px",
zIndex: "-1"
}
}
)
] });
}
var App_default = App;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ReactCropper
});
//# sourceMappingURL=index.js.map