import interact from "interactjs"; import { CSSProperties, forwardRef, useCallback, useEffect, useRef, useState } from "react"; export interface Point { x: number; y: number; } interface PerspectiveCropperProps { src: string; width?: number; height?: number; paddingX?: number; paddingY?: number; backgroundColor?: string; draggableDotStyles?: CSSProperties; draggableDotSize?: number; startPoints?: Point[]; handleFinishedCrop?: (data: ImageData) => void; onPointsChange?: (points: Point[]) => void; onPointsFinishedChange?: (points: Point[]) => void; showGrid?: boolean; buttonContent?: React.ReactNode | string | number | null; customButtonStyles?: CSSProperties; } const PerspectiveCropper = ({ src, paddingX = 100, width, height, paddingY = 100, backgroundColor = "#202020", draggableDotStyles, draggableDotSize = 12, handleFinishedCrop, onPointsChange, onPointsFinishedChange, startPoints, showGrid = true, buttonContent, customButtonStyles, }: PerspectiveCropperProps) => { const firstDotRef = useRef(null); const secondDotRef = useRef(null); const thirdDotRef = useRef(null); const fourthDotRef = useRef(null); const polygonRef = useRef(null); const canvasRef = useRef(null); const insetPx = 10; const dotSizeOffsset = draggableDotSize / 2; const [computedWidth, setWidth] = useState(width || 0); const [computedHeight, setHeight] = useState(height || 0); const [currentPoints, setCurrentPoints] = useState([]); const [isDraggin, setIsDraggin] = useState(false); const startPosx1 = startPoints && startPoints[0] ? startPoints[0].x : insetPx + dotSizeOffsset; const startPosy1 = startPoints && startPoints[0] ? startPoints[0].y : insetPx + dotSizeOffsset; const startPosx2 = startPoints && startPoints[1] ? startPoints[1].x : computedWidth - insetPx - dotSizeOffsset; const startPosy2 = startPoints && startPoints[1] ? startPoints[1].y : insetPx + dotSizeOffsset; const startPosx3 = startPoints && startPoints[2] ? startPoints[2].x : computedWidth - insetPx - dotSizeOffsset; const startPosy3 = startPoints && startPoints[2] ? startPoints[2].y : computedHeight - insetPx - dotSizeOffsset; const startPosx4 = startPoints && startPoints[3] ? startPoints[3].x : insetPx + dotSizeOffsset; const startPosy4 = startPoints && startPoints[3] ? startPoints[3].y : computedHeight - insetPx - dotSizeOffsset; const getPoints = useCallback(() => { const fdot = firstDotRef.current; const sdot = secondDotRef.current; const tdot = thirdDotRef.current; const frdot = fourthDotRef.current; const x1 = parseInt(fdot?.style.left.replace("px", "") || "0") + parseInt(fdot?.getAttribute("data-x") || "0") + dotSizeOffsset; const y1 = parseInt(fdot?.style.top.replace("px", "") || "0") + parseInt(fdot?.getAttribute("data-y") || "0") + dotSizeOffsset; const x2 = parseInt(sdot?.style.left.replace("px", "") || "0") + parseInt(sdot?.getAttribute("data-x") || "0") + dotSizeOffsset; const y2 = parseInt(sdot?.style.top.replace("px", "") || "0") + parseInt(sdot?.getAttribute("data-y") || "0") + dotSizeOffsset; const x3 = parseInt(tdot?.style.left.replace("px", "") || "0") + parseInt(tdot?.getAttribute("data-x") || "0") + dotSizeOffsset; const y3 = parseInt(tdot?.style.top.replace("px", "") || "0") + parseInt(tdot?.getAttribute("data-y") || "0") + dotSizeOffsset; const x4 = parseInt(frdot?.style.left.replace("px", "") || "0") + parseInt(frdot?.getAttribute("data-x") || "0") + dotSizeOffsset; const y4 = parseInt(frdot?.style.top.replace("px", "") || "0") + parseInt(frdot?.getAttribute("data-y") || "0") + dotSizeOffsset; const maxX = Math.max(x1, x2, x3, x4); const maxY = Math.max(y1, y2, y3, y4); const minX = Math.min(x1, x2, x3, x4); const minY = Math.min(y1, y2, y3, y4); const width = maxX - minX; const height = maxY - minY; return { x1, y1, x2, y2, x3, y3, x4, y4, width, height }; }, [dotSizeOffsset]); const drawSVG = useCallback(() => { if (!polygonRef.current) return; const { x1, y1, x2, y2, x3, y3, x4, y4 } = getPoints(); if (typeof onPointsFinishedChange === "function") setCurrentPoints([ { x: x1, y: y1 }, { x: x2, y: y2 }, { x: x3, y: y3 }, { x: x4, y: y4 }, ]); if (typeof onPointsChange === "function") onPointsChange([ { x: x1, y: y1 }, { x: x2, y: y2 }, { x: x3, y: y3 }, { x: x4, y: y4 }, ]); const polygon = polygonRef.current; polygon.setAttribute("points", `${x1},${y1} ${x2},${y2} ${x3},${y3} ${x4},${y4}`); }, [getPoints, onPointsChange, onPointsFinishedChange]); const dragMoveListener = useCallback( (event: any) => { const target = event.target as HTMLDivElement; const x = parseFloat(target.getAttribute("data-x") || "0") + event.dx; const y = parseFloat(target.getAttribute("data-y") || "0") + event.dy; target.style.transform = `translate(${x}px, ${y}px)`; target.setAttribute("data-x", x); target.setAttribute("data-y", y); drawSVG(); }, [drawSVG] ); useEffect(() => { if (!firstDotRef.current || !secondDotRef.current || !thirdDotRef.current || !fourthDotRef.current) return; firstDotRef.current.style.top = startPosy1 + "px"; firstDotRef.current.style.left = startPosx1 + "px"; secondDotRef.current.style.top = startPosy2 + "px"; secondDotRef.current.style.left = startPosx2 + "px"; thirdDotRef.current.style.top = startPosy3 + "px"; thirdDotRef.current.style.left = startPosx3 + "px"; fourthDotRef.current.style.top = startPosy4 + "px"; fourthDotRef.current.style.left = startPosx4 + "px"; const interactListener = interact(".draggable").draggable({ modifiers: [ interact.modifiers.restrictRect({ restriction: "parent", endOnly: true, }), ], onend: () => setIsDraggin(false), onstart: () => setIsDraggin(true), onmove: dragMoveListener, }); return () => interactListener.unset(); }, [computedWidth, computedHeight, paddingX, paddingY, dragMoveListener, startPosy1, startPosx1, startPosy2, startPosx2, startPosy3, startPosx3, startPosy4, startPosx4]); const svgDefaultStyles: CSSProperties = { zIndex: 1, position: "absolute", }; const SVGPolygonElement: CSSProperties = { stroke: "white", strokeWidth: 2, }; const containerStyles: CSSProperties = { display: "flex", position: "relative", alignContent: "center", justifyContent: "center", backgroundColor, width: computedWidth + paddingX + "px", height: computedHeight + paddingY, alignItems: "center", }; const subContainerStyles: CSSProperties = { display: "flex", position: "relative", alignContent: "center", justifyContent: "center", width: computedWidth + "px", height: computedHeight, alignItems: "center", }; const buttonStyles: CSSProperties = { display: "flex", position: "absolute", bottom: "50px", right: computedWidth / 2 + "px", width: "150px", height: "50px", cursor: "pointer", background: "none", border: " 2px solid rgba(255,255,255,0.4)", borderRadius: "5px", alignItems: "center", justifyContent: "center", overflow: "hidden", color: "white", transform: "translate(50%, 50%)", zIndex: 1, }; const loadImageOnCanvas = useCallback(async () => { if (!src) return; const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; let img = new Image(); await new Promise((r, rej) => { img.src = src; img.onload = r; img.onerror = rej; }); if (!computedHeight) setHeight(img.height); if (!computedWidth) setWidth(img.width); ctx.drawImage(img, 0, 0, computedWidth, computedHeight); }, [computedHeight, src, computedWidth]); useEffect(() => { if (src === null || src === "") return; loadImageOnCanvas(); }, [loadImageOnCanvas, src]); const wrapImage = useCallback( (frame: any, dst: any, imagePoints: Point[], imageWidth: number, imageHeight: number) => { if (!cv) throw new Error("cv is not defined"); let arr1: number[] = []; for (let i = 0; i < imagePoints.length; i++) { arr1.push(imagePoints[i].x); arr1.push(imagePoints[i].y); } let arr2 = [0, 0, computedWidth, 0, 0, computedHeight, computedWidth, computedHeight]; let mat1 = cv.matFromArray(4, 2, cv.CV_32F, arr1); let mat2 = cv.matFromArray(4, 2, cv.CV_32F, arr2); let perspectiveMatrix = cv.getPerspectiveTransform(mat1, mat2); let size = new cv.Size(computedWidth, computedHeight); cv.warpPerspective(frame, dst, perspectiveMatrix, size); mat1.delete(); mat2.delete(); frame.delete(); perspectiveMatrix.delete(); size = new cv.Size(imageWidth, imageHeight); cv.resize(dst, dst, size); const intArray = new Uint8ClampedArray(dst.data, dst.cols, dst.rows); const imageData = new ImageData(intArray, dst.cols, dst.rows); return imageData; }, [computedHeight, computedWidth] ); useEffect(() => { if (isDraggin) { return; } if (!isDraggin) { onPointsFinishedChange(currentPoints); } }, [currentPoints, onPointsFinishedChange, isDraggin]); const cropImage = useCallback(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const { x1, y1, x2, y2, x3, y3, x4, y4, width: imageWidth, height: imageHeigth } = getPoints(); const imageData = ctx.getImageData(0, 0, computedWidth, computedHeight); let src = cv.matFromImageData(imageData); let dst = new cv.Mat(); const wrapMatrix = [ { x: x1, y: y1 }, { x: x2, y: y2 }, { x: x4, y: y4 }, { x: x3, y: y3 }, ]; const imageDataWrapped = wrapImage(src, dst, wrapMatrix, imageWidth, imageHeigth); handleFinishedCrop && handleFinishedCrop(imageDataWrapped); }, [getPoints, handleFinishedCrop, computedHeight, computedWidth, wrapImage]); return (
); }; const DraggableDot = forwardRef(({ draggableDotStyles, id, draggableDotSize }, ref) => { const defaultStyle: CSSProperties = { width: draggableDotSize + "px", height: draggableDotSize + "px", backgroundColor: "#14ff7e", borderRadius: "50%", position: "absolute", zIndex: 2, transform: `translate(-${draggableDotSize / 2}px, -${draggableDotSize / 2}px)`, }; return (
); }); export default PerspectiveCropper;