UNPKG

react-perspective-crop

Version:

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

299 lines (292 loc) 8.32 kB
// 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