UNPKG

react-perspective-crop

Version:

A flexible React image cropper with draggable custom-shaped bullets and export capabilities.

336 lines (327 loc) 10.4 kB
"use strict"; 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