UNPKG

react-perspective-crop

Version:

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

1 lines 15 kB
{"version":3,"sources":["../src/App.tsx","../src/components/Bullet/index.tsx","../src/styled-components/index.ts","../src/utils/imageProcessor.ts","../src/utils/downloadImage.ts"],"sourcesContent":["import React, { useEffect, useRef, useState } from \"react\";\n\nimport Bullet from \"./components/Bullet\";\nimport { BulletContainer, MainImage, SelectedImage } from \"./styled-components\";\nimport type { IBulletData, IAppProps } from \"./App.types\";\nimport { imageProcessor, downloadImage } from \"./utils\";\n\nfunction App(\n props: IAppProps = {\n bulletsDefaultCordinates: [],\n imageSrc: \"\",\n onChange: () => {},\n downloadCroppedImg: () => {},\n }\n) {\n const container = useRef<any>(null);\n const invisibleItem = useRef<any>(null);\n\n const [bulletsData, setBulletsData] = useState<IBulletData[]>(\n props.bulletsDefaultCordinates\n );\n const [activeBulletIndex, setActiveBulletIndex] = useState<number>();\n const [isDragging, setIsDragging] = useState<boolean>(false);\n\n useEffect(() => {\n if (isDragging) {\n props.onChange(bulletsData);\n }\n\n if (props.downloadCroppedImg) {\n props.downloadCroppedImg(download);\n }\n }, [bulletsData]);\n\n const handleDragStart = (activeBulletId: number) => {\n const activeIndex = bulletsData.findIndex(\n (item) => item.id === activeBulletId\n );\n\n if (activeIndex !== -1) {\n setActiveBulletIndex(activeIndex);\n setIsDragging(true);\n }\n };\n\n const download = async () => {\n const containerBox = container.current.getBoundingClientRect();\n\n const blob = await imageProcessor(\n props.imageSrc,\n bulletsData,\n containerBox.width,\n containerBox.height\n );\n\n downloadImage(blob);\n };\n\n const handleBulletsData = (value: IBulletData) => {\n if (isDragging && activeBulletIndex !== undefined) {\n const localX = value.x - container?.current?.offsetLeft;\n const localY = value.y - container?.current?.offsetTop;\n\n if (\n localX > 0 &&\n localX < container.current.offsetWidth &&\n localY < container.current.offsetHeight &&\n localY > 0\n ) {\n const copyBulletsData = [...bulletsData];\n\n copyBulletsData[activeBulletIndex] = {\n ...value,\n x: localX,\n y: localY,\n };\n\n setBulletsData(copyBulletsData);\n }\n }\n };\n\n const handleOnDragEnd = (value: IBulletData) => {\n if (value.x === 0 && value.y === 0) {\n handleBulletsData(value);\n }\n\n setIsDragging(false);\n };\n\n return (\n <>\n <BulletContainer\n ref={container}\n $width={props.width}\n $height={props.height}\n >\n {bulletsData.map((bullet) => {\n return (\n <Bullet\n ref={invisibleItem}\n key={`${bullet.id}`}\n bulletData={bullet}\n bulletSize={20}\n onDragStart={handleDragStart}\n onDrag={handleBulletsData}\n onDragEnd={handleOnDragEnd}\n icon={`${bullet.id}`}\n />\n );\n })}\n <MainImage src={props.imageSrc} />\n <SelectedImage src={props.imageSrc} $cordinates={bulletsData} />\n </BulletContainer>\n\n <div\n ref={invisibleItem}\n id=\"invisible-drag-image\"\n style={{\n width: \"1px\",\n height: \"1px\",\n opacity: \"0\",\n position: \"absolute\",\n top: \"-1000px\",\n zIndex: \"-1\",\n }}\n />\n </>\n );\n}\n\nexport default App;\n","import React, { useRef, DragEvent } from \"react\";\n\nimport { BulletItem } from \"../../styled-components\";\nimport type { BulletsProps } from \"./index.types\";\n\nconst Bullets = React.forwardRef(\n (\n {\n bulletData,\n bulletSize,\n icon,\n onDragStart,\n onDrag,\n onDragEnd,\n }: BulletsProps,\n ref: any\n ) => {\n const R = useRef({\n x: 0,\n y: 0,\n });\n\n const point = useRef({\n x: bulletData.x,\n y: bulletData.y,\n });\n\n const handleDragStart = (e: any, BulletId: number) => {\n if (ref?.current) {\n e.dataTransfer.setDragImage(ref?.current, 0, 0);\n } //remove ghost effect\n\n const bounds = e.target.getBoundingClientRect();\n\n R.current.x =\n (Math.floor(bounds.left) + Math.floor(bounds.right)) / 2 - e.clientX;\n R.current.y =\n (Math.floor(bounds.bottom) + Math.floor(bounds.top)) / 2 - e.clientY;\n\n onDragStart(BulletId);\n };\n\n const handleDrag = (e: DragEvent<HTMLElement>) => {\n e.preventDefault();\n\n if (\n (e.clientX !== point.current.x || e.clientY !== point.current.y) &&\n (Math.abs(e.clientX - point.current.x) !== point.current.x ||\n Math.abs(e.clientY - point.current.y) !== point.current.y)\n ) {\n onDrag({\n id: bulletData.id,\n x: e.clientX + R.current.x,\n y: e.clientY + R.current.y,\n });\n\n point.current.x = e.clientX;\n point.current.y = e.clientY;\n }\n };\n\n const handleDragEnd = (e: DragEvent<HTMLElement>) => {\n e.preventDefault();\n\n onDragEnd({\n id: bulletData.id,\n x: e.clientX + R.current.x,\n y: e.clientY + R.current.y,\n });\n };\n\n const handleDragOver = (e: DragEvent<HTMLElement>) =>\n (e.dataTransfer.effectAllowed = \"move\");\n\n return (\n <BulletItem\n $cordinate={{\n left: bulletData.x - bulletSize / 2,\n top: bulletData.y - bulletSize / 2,\n }}\n $bulletSize={bulletSize}\n draggable\n onDragStart={(e: any) => handleDragStart(e, bulletData.id)}\n onDrag={handleDrag}\n onDragEnd={handleDragEnd}\n onDragOver={handleDragOver}\n >\n {icon ?? icon}\n </BulletItem>\n );\n }\n);\n\nexport default Bullets;\n","import styled from \"styled-components\";\n\nimport type { IBulletData } from \"../App.types\";\n\nexport const BulletContainer = styled.div<{\n $width?: number | string;\n $height?: number | string;\n}>`\n border: 1px solid black;\n width: ${({ $width }) =>\n !!$width ? (typeof $width === \"string\" ? $width : `${$width}px`) : `100%`};\n height: ${({ $height }) =>\n !!$height\n ? typeof $height === \"string\"\n ? $height\n : `${$height}px`\n : `100%`};\n position: relative;\n &::after {\n content: \"\";\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n right: 0;\n background-color: rgba(0, 0, 0, 0.5);\n z-index: 10;\n }\n\n & > * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n`;\n\nexport const BulletItem = styled.div<{\n $cordinate?: { left: number; top: number };\n $bulletSize?: number;\n}>`\n display: flex;\n justify-content: center;\n align-items: center;\n position: absolute;\n border: 1px solid red;\n border-radius: 50%;\n width: ${(props) => props.$bulletSize}px;\n height: ${(props) => props.$bulletSize}px;\n box-sizing: border-box;\n left: ${(props) => props.$cordinate?.left}px;\n top: ${(props) => props.$cordinate?.top}px;\n cursor: pointer;\n z-index: 100;\n`;\n\nexport const SelectedImage = styled.img<{\n $cordinates: IBulletData[];\n}>`\n width: 100%;\n height: 100%;\n clip-path: ${({ $cordinates }) =>\n `polygon(${$cordinates.map(\n (cordinate) => `${cordinate.x}px ${cordinate.y}px`\n )})`};\n z-index: 100;\n position: absolute;\n user-select: none;\n`;\n\nexport const MainImage = styled.img`\n width: 100%;\n height: 100%;\n position: absolute;\n z-index: -1;\n user-select: none;\n`;\n","import { IBulletData } from \"../App.types\";\n\n// imageProcessor.ts\nconst imageProcessor = async (\n imageSrc: string,\n bulletsData: IBulletData[],\n width: number,\n height: number\n): Promise<Blob> => {\n return new Promise((resolve, reject) => {\n const img = new Image();\n\n img.crossOrigin = \"anonymous\";\n\n img.onload = () => {\n const canvas = document.createElement(\"canvas\");\n const ctx = canvas.getContext(\"2d\");\n\n canvas.width = width;\n canvas.height = height;\n\n if (!ctx) {\n return reject(\"Canvas context not available\");\n }\n\n const path = new Path2D();\n const coordinates = bulletsData.map(\n (item) => [item.x, item.y] as [number, number]\n );\n\n if (coordinates.length > 0) {\n path.moveTo(coordinates[0][0], coordinates[0][1]);\n\n for (let i = 1; i < coordinates.length; i++) {\n path.lineTo(coordinates[i][0], coordinates[i][1]);\n }\n\n path.closePath();\n }\n\n ctx.save();\n ctx.clip(path);\n ctx.drawImage(img, 0, 0, width, height);\n ctx.restore();\n\n canvas.toBlob((blob) => {\n if (blob) {\n resolve(blob);\n } else {\n reject(\"Failed to create blob\");\n }\n }, \"image/jpeg\");\n };\n\n img.onerror = () => reject(\"Failed to load image\");\n img.src = imageSrc;\n });\n};\n\nexport default imageProcessor;\n","// داخل کامپوننت یا فانکشن\nconst downloadImage = async (value: Blob) => {\n const url = URL.createObjectURL(value);\n const a = document.createElement(\"a\");\n\n a.href = url;\n a.download = \"cropped-image.jpg\";\n a.click();\n};\n\nexport default downloadImage;\n"],"mappings":"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","names":["useRef","jsx","useRef"]}