UNPKG

intellicrop

Version:

Simple JavaScript solution to edge detection assisted image cropping.

1 lines 31.4 kB
{"mappings":"AAEA,UAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAElC;IACE,EAAE,EAAE,EAAE,CAAC;IACP,EAAE,EAAE,EAAE,CAAC;IACP,EAAE,EAAE,EAAE,CAAC;IACP,EAAE,EAAE,EAAE,CAAC;CACR;AAED;IACE,CAAC,EAAE,EAAE,CAAC;IACN,CAAC,EAAE,EAAE,CAAC;IACN,CAAC,EAAE,EAAE,CAAC;IACN,CAAC,EAAE,EAAE,CAAC;CACP;AAED;IACE,CAAC,EAAE,EAAE,CAAC;CACP;AEhBD;IACE,2CAA2C;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,uCAAuC;IACvC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,+BAA+B;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,uDAAuD;IACvD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qBAAqB;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,6BAA6B;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iEAAiE;IACjE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,8BAA8B;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sDAAsD;IACtD,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uEAAuE;IACvE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iEAAiE;IACjE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,0BAA0B;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+BAA+B;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2CAA2C;IAC3C,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,gCAAgC;IAChC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iCAAiC;IACjC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;IACE,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sCAAsC;IACtC,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,oDAAoD;IACpD,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;WA+BsB,cAAc;WAYpB,OAAO;gBAIT,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,cAAc;IAgPtF,+BAA+B;IACxB,OAAO;IAwPP,UAAU,CAAC,OAAO,EAAE,MAAM,OAAO,GAAG,MAAM,WAAW,GAAG,MAAM,UAAU,GAAG,SAAS;IAMpF,UAAU,CAAC,OAAO,EAAE,MAAM,OAAO,GAAG,MAAM,WAAW,GAAG,MAAM,UAAU,GAAG,SAAS;IAK3F,+EAA+E;IACxE,SAAS,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;IAwBzC,UAAU;IAIV,WAAW;IA+BX,YAAY;IAIZ,UAAU,IAOV,OAAO;CAwCf","sources":["src/cropper/src/util.ts","src/cropper/src/edge-detector/edgedetector.ts","src/cropper/src/cropper/cropper.ts","src/cropper/cropper.ts"],"sourcesContent":[null,null,null,"import cv from \"@techstark/opencv-js/\";\nimport EdgeDetector from \"../edge-detector/edgedetector\";\nimport Util, { Corners, EdgeCenters, Line, Pt, ViewCenter } from \"../util\";\n\nexport interface CropperTheme {\n /** Color used for background and margin */\n backgroundColor?: string;\n /** Color used for tinting the surroundings of the selected area */\n surroundingOverlayColor?: string;\n /** Radius of corner grabbers in rem */\n cornerGrabberRadius?: number;\n /** Color of corner grabbers */\n cornerGrabberColor?: string;\n /** Line thickness of edges in rem (0 to hide edges) */\n edgeThickness?: number;\n /** Color of edges */\n edgeColor?: string;\n /** Radius of edge grabbers in rem (0 to hide edge grabbers) */\n edgeGrabberRadius?: number;\n /** Color of edge grabbers */\n edgeGrabberColor?: string;\n /** Radius of center grabber in rem (0 to hide center grabber) */\n centerGrabberRadius?: number;\n /** Color of center grabber */\n centerGrabberColor?: string;\n /** Factor by which to expand grabbers when hovered */\n grabberExpansionFactor?: number;\n /** Factor for scaling grabber hitboxes (increase for touch devices) */\n grabberHitboxFactor?: number;\n /** Line thickness of crosslines in rem (0 to hide crosslines) */\n crossLineThickness?: number;\n /** Color of crosslines */\n crossLineColor?: string;\n /** Radius of zoom lens in rem (0 to hide zoom lens) */\n zoomLensRadius?: number;\n /** Margin between zoom lens and grabber in rem */\n zoomLensMargin?: number;\n /** Zoom factor of zoom lens */\n zoomLensFactor?: number;\n /** Border thickness of zoom lens in rem */\n zoomLensBorderThickness?: number;\n /** Border color of zoom lens */\n zoomLensBorderColor?: string;\n /** Overlay color of zoom lens */\n zoomLensOverlayColor?: string;\n}\n\nexport interface CropperOptions {\n /** Use edge detection for placing draggable corners */\n useEdgeDetection?: boolean;\n /** Theme to be used for Cropper UI */\n theme?: CropperTheme;\n /** Canvas element to show edge detection results */\n debugCanvas?: HTMLCanvasElement;\n /** Log debug information in console */\n debugLogs?: boolean;\n /** Initial placement draggable corners (overrides useEdgeDetection) */\n corners?: Corners;\n /** Initial amount of clockwise 90° rotations */\n rotations?: number;\n}\n\nexport default class Cropper {\n private readonly canvas: HTMLCanvasElement;\n private readonly ctx: CanvasRenderingContext2D;\n private readonly img: HTMLImageElement;\n private readonly imgMat: cv.Mat;\n private readonly options: CropperOptions;\n /** Current width of image as displayed on canvas (accounting for rotation) */\n private imgW: number;\n /** Current height of image as displayed on canvas (accounting for rotation) */\n private imgH: number;\n /** Current position of corner grabbers relative to unrotated source image */\n private corners: Corners | undefined;\n /** Current position of edge grabbers relative to unrotated source image */\n private edgeCenters: EdgeCenters | undefined;\n /** Current position of center grabber relative to unrotated source image */\n private viewCenter: ViewCenter | undefined;\n /** Amount of pixels per rem */\n private remPx: number;\n /** Amount of clockwise 90° rotations */\n private rotations = 0;\n /** Grabber that is currently being dragged by the user */\n private dragged: keyof Corners | keyof EdgeCenters | keyof ViewCenter | undefined;\n /** Grabber that is currently being hovered by the user */\n private hovered: keyof Corners | keyof EdgeCenters | keyof ViewCenter | undefined;\n\n private onPointerDown: (event: PointerEvent) => void;\n private onPointerMove: (event: PointerEvent) => void;\n private onPointerUp: (event: PointerEvent) => void;\n private onMouseMove: (event: MouseEvent) => void;\n private onResize = () => this.adjustDimensions();\n\n public static async initialization() {\n return new Promise<void>((resolve) => {\n if (cv.getBuildInformation) {\n resolve();\n } else {\n cv.onRuntimeInitialized = () => {\n resolve();\n };\n }\n });\n }\n\n public static isReady() {\n return !!cv.getBuildInformation;\n }\n\n constructor(canvas: HTMLCanvasElement, img: HTMLImageElement, options?: CropperOptions) {\n const defaultOptions: CropperOptions = {\n useEdgeDetection: true,\n theme: {\n backgroundColor: \"black\",\n surroundingOverlayColor: \"rgba(0,0,0,0.333)\",\n cornerGrabberRadius: 0.5,\n cornerGrabberColor: \"white\",\n edgeThickness: 0.2,\n edgeColor: \"white\",\n edgeGrabberRadius: options?.theme?.cornerGrabberRadius ? options.theme.cornerGrabberRadius * 0.8 : 0.4,\n edgeGrabberColor: options?.theme?.cornerGrabberColor ?? \"white\",\n centerGrabberRadius: options?.theme?.cornerGrabberRadius ? options.theme.cornerGrabberRadius * 1.2 : 0.6,\n centerGrabberColor: options?.theme?.cornerGrabberColor ?? \"rgba(255,255,255,0.5)\",\n grabberExpansionFactor: 1.2,\n grabberHitboxFactor: 1.5,\n crossLineThickness: options?.theme?.edgeThickness ?? 0.2,\n crossLineColor: options?.theme?.edgeColor ?? \"rgba(255,255,255,0.5)\",\n zoomLensRadius: options?.theme?.cornerGrabberRadius ? options.theme.cornerGrabberRadius * 8 : 4,\n zoomLensMargin: options?.theme?.cornerGrabberRadius ? options.theme.cornerGrabberRadius * 2 : 1,\n zoomLensFactor: 2,\n zoomLensBorderThickness: options?.theme?.edgeThickness ? options?.theme?.edgeThickness * 0.5 : 0.1,\n zoomLensBorderColor: \"black\",\n zoomLensOverlayColor: options?.theme?.surroundingOverlayColor ?? \"rgba(0,0,0,0.333)\",\n },\n };\n\n this.canvas = canvas;\n this.ctx = canvas.getContext(\"2d\");\n this.img = img;\n this.imgMat = cv.imread(img);\n this.options = {\n useEdgeDetection: !options.corners && (options.useEdgeDetection ?? defaultOptions.useEdgeDetection),\n theme: Object.assign(defaultOptions.theme, options?.theme),\n debugCanvas: options.debugCanvas,\n debugLogs: options.debugLogs,\n corners: options.corners,\n rotations: options.rotations,\n };\n\n if (this.options.rotations) {\n this.rotations = Util.rotateVal(options.rotations, 0, 4);\n }\n this.imgW = this.rotations % 2 == 0 ? this.imgMat.cols : this.imgMat.rows;\n this.imgH = this.rotations % 2 == 0 ? this.imgMat.rows : this.imgMat.cols;\n this.canvas.width = this.imgW;\n this.canvas.height = this.imgH;\n this.adjustDimensions();\n\n setTimeout(() => {\n if (this.options.useEdgeDetection) {\n if (this.options.debugLogs) console.time(\"edgedetection\");\n this.corners = EdgeDetector.detect(this.imgMat, this.options.debugCanvas);\n if (this.options.debugLogs) console.timeEnd(\"edgedetection\");\n }\n if (!this.corners) {\n this.corners = this.options.corners ?? {\n tl: [0, 0],\n tr: [this.imgW, 0],\n br: [this.imgW, this.imgH],\n bl: [0, this.imgH],\n };\n }\n this.updateCenters();\n this.registerListeners();\n this.render();\n }, 1);\n }\n\n private registerListeners() {\n let bounds: Pt | undefined;\n let edgeDragProps:\n | {\n dragLine: Line;\n edgeCenter: Pt;\n oppositeEdgeCenter: Pt;\n moveCorners: { key: keyof Corners; towardsKey: keyof Corners; deltaX: number; deltaY: number }[];\n scalarMin: number;\n scalarMax: number;\n }\n | undefined;\n let centerDragProps:\n | {\n cornerOffsets: Corners;\n }\n | undefined;\n\n this.onPointerDown = (event) => {\n event.preventDefault();\n event.stopPropagation();\n this.setDragged(this.getGrabber([event.clientX, event.clientY]));\n if (!this.dragged) return;\n\n bounds = this.rotations % 2 == 0 ? [this.imgW, this.imgH] : [this.imgH, this.imgW];\n if (this.dragged in this.edgeCenters) {\n const edgeCenter = this.edgeCenters[this.dragged];\n const oppositeEdgeCenter = this.edgeCenters[Util.rotateKey(this.edgeCenters, this.dragged, 2)];\n const dragLine = Util.lineThrough(edgeCenter, oppositeEdgeCenter);\n\n const cornerKeys = Object.keys(this.corners) as (keyof Corners)[];\n const edgeCenterKeys = Object.keys(this.edgeCenters) as (keyof EdgeCenters)[];\n const moveCorners = [];\n let scalarMin = -Infinity;\n let scalarMax = Infinity;\n\n for (let i = 0; i < 2; i++) {\n const edgeCenterKeyIndex = edgeCenterKeys.indexOf(this.dragged as keyof EdgeCenters);\n const moveCornerKey = cornerKeys[Util.rotateVal(edgeCenterKeyIndex, i, cornerKeys.length)];\n const towardsCornerKey = cornerKeys[Util.rotateVal(edgeCenterKeyIndex, -i - 1, cornerKeys.length)];\n const moveCornerPt = this.corners[moveCornerKey];\n const towardsCornerPt = this.corners[towardsCornerKey];\n const deltaX = moveCornerPt[0] - towardsCornerPt[0];\n const deltaY = moveCornerPt[1] - towardsCornerPt[1];\n\n let scalarLeft = -towardsCornerPt[0] / deltaX;\n let scalarRight = (bounds[0] - towardsCornerPt[0]) / deltaX;\n let scalarTop = -towardsCornerPt[1] / deltaY;\n let scalarBottom = (bounds[1] - towardsCornerPt[1]) / deltaY;\n\n scalarMin = Math.max(scalarMin, ...[scalarLeft, scalarRight, scalarTop, scalarBottom].filter((s) => s < 0));\n scalarMax = Math.min(scalarMax, ...[scalarLeft, scalarRight, scalarTop, scalarBottom].filter((s) => s > 1));\n\n moveCorners[i] = {\n key: moveCornerKey,\n towardsKey: towardsCornerKey,\n deltaX: deltaX,\n deltaY: deltaY,\n };\n }\n\n edgeDragProps = { dragLine, edgeCenter, oppositeEdgeCenter, moveCorners, scalarMin, scalarMax };\n } else if (this.dragged in this.viewCenter) {\n const cornerOffsets = Util.mapObj(this.corners, (c) => Util.ptDiff(this.viewCenter.c, c));\n\n centerDragProps = { cornerOffsets };\n }\n\n window.addEventListener(\"pointermove\", this.onPointerMove);\n window.addEventListener(\"pointerup\", this.onPointerUp);\n };\n\n let requestedAnimationFrame = false;\n this.onPointerMove = (event) => {\n event.preventDefault();\n event.stopPropagation();\n if (this.dragged && !requestedAnimationFrame) {\n window.requestAnimationFrame(() => {\n requestedAnimationFrame = false;\n if (this.dragged in this.corners) {\n this.corners[this.dragged] = Util.ptClipBounds(this.cl2imgPt([event.clientX, event.clientY]), bounds);\n } else if (this.dragged in this.edgeCenters) {\n const { dragLine, edgeCenter, oppositeEdgeCenter, moveCorners, scalarMin, scalarMax } = edgeDragProps;\n\n const newPt = Util.closestPoint(dragLine, this.cl2imgPt([event.clientX, event.clientY]));\n const newLenRelative = Math.min(\n Math.max(Util.ptRelativeDistance(newPt, oppositeEdgeCenter, edgeCenter), scalarMin),\n scalarMax\n );\n\n for (let moveCorner of moveCorners) {\n const towardsCornerPt = this.corners[moveCorner.towardsKey];\n this.corners[moveCorner.key] = [\n towardsCornerPt[0] + moveCorner.deltaX * newLenRelative,\n towardsCornerPt[1] + moveCorner.deltaY * newLenRelative,\n ];\n }\n } else if (this.dragged in this.viewCenter) {\n const offsetArray = Object.values(centerDragProps.cornerOffsets) as Pt[];\n\n const viewCenter = Util.ptClipBoundsRect(this.cl2imgPt([event.clientX, event.clientY]), {\n left: Math.max(...offsetArray.map((co) => -co[0])),\n right: Math.min(...offsetArray.map((co) => bounds[0] - co[0])),\n top: Math.max(...offsetArray.map((co) => -co[1])),\n bottom: Math.min(...offsetArray.map((co) => bounds[1] - co[1])),\n });\n this.corners = Util.mapObj(centerDragProps.cornerOffsets, (c) => [\n c[0] + viewCenter[0],\n c[1] + viewCenter[1],\n ]);\n }\n this.updateCenters();\n this.render();\n });\n requestedAnimationFrame = true;\n }\n };\n\n this.onPointerUp = (event) => {\n event.preventDefault();\n event.stopPropagation();\n this.setDragged(undefined);\n window.removeEventListener(\"pointermove\", this.onPointerMove);\n window.removeEventListener(\"pointerup\", this.onPointerUp);\n this.render();\n };\n\n this.onMouseMove = (event) => {\n if (this.dragged) return;\n event.preventDefault();\n event.stopPropagation();\n const prevHovered = this.hovered;\n this.setHovered(this.getGrabber([event.clientX, event.clientY]));\n if (this.hovered === prevHovered) return;\n this.render();\n };\n\n this.canvas.addEventListener(\"pointerdown\", this.onPointerDown);\n this.canvas.addEventListener(\"mousemove\", this.onMouseMove);\n window.addEventListener(\"resize\", this.onResize);\n }\n\n private getGrabber(clientPt: Pt): keyof Corners | keyof EdgeCenters | keyof ViewCenter | undefined {\n const cursorPt: Pt = this.cl2imgPt(clientPt);\n const fac = this.remPx * this.options.theme.grabberHitboxFactor;\n let radius: number;\n\n radius = this.options.theme.cornerGrabberRadius * fac;\n for (const key in this.corners) {\n const corner = this.corners[key];\n if (Util.withinRadius(cursorPt, corner, radius)) {\n return key as keyof Corners;\n }\n }\n\n radius = this.options.theme.edgeGrabberRadius * fac;\n for (const key in this.edgeCenters) {\n const corner = this.edgeCenters[key];\n if (Util.withinRadius(cursorPt, corner, radius)) {\n return key as keyof EdgeCenters;\n }\n }\n\n radius = this.options.theme.centerGrabberRadius * fac;\n if (Util.withinRadius(cursorPt, this.viewCenter.c, radius)) {\n return \"c\";\n }\n\n return undefined;\n }\n\n /** Discards event listeners */\n public discard() {\n window.removeEventListener(\"resize\", this.onResize);\n this.canvas.removeEventListener(\"pointerdown\", this.onPointerDown);\n window.removeEventListener(\"pointermove\", this.onPointerMove);\n window.removeEventListener(\"pointerup\", this.onPointerUp);\n }\n\n private adjustDimensions() {\n const bounds = this.canvas.getBoundingClientRect();\n const s = this.imgH / bounds.height;\n this.remPx = parseFloat(getComputedStyle(document.documentElement).fontSize) * s;\n this.canvas.width = this.imgW + this.options.theme.cornerGrabberRadius * this.remPx * 2;\n this.canvas.height = this.imgH + this.options.theme.cornerGrabberRadius * this.remPx * 2;\n this.render();\n }\n\n private render() {\n this.ctx.fillStyle = this.options.theme.backgroundColor;\n this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n\n //Draw image\n let xOffset = 0;\n let yOffset = 0;\n if (this.rotations) {\n this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2);\n this.ctx.rotate(this.rotations * (Math.PI / 2));\n xOffset = this.rotations % 2 == 0 ? -this.canvas.width / 2 : -this.canvas.height / 2;\n yOffset = this.rotations % 2 == 0 ? -this.canvas.height / 2 : -this.canvas.width / 2;\n }\n this.ctx.drawImage(\n this.img,\n xOffset + this.options.theme.cornerGrabberRadius * this.remPx,\n yOffset + this.options.theme.cornerGrabberRadius * this.remPx,\n this.imgMat.cols,\n this.imgMat.rows\n );\n if (this.rotations) {\n this.ctx.rotate(-this.rotations * (Math.PI / 2));\n this.ctx.translate(-this.canvas.width / 2, -this.canvas.height / 2);\n }\n\n //Draw UI\n if (this.corners) {\n const cornerKeys = Object.keys(this.corners);\n const edgeCenterKeys = Object.keys(this.edgeCenters);\n\n //Draw surrounding overlay\n if (this.options.theme.surroundingOverlayColor) {\n this.ctx.beginPath();\n this.ctx.fillStyle = this.options.theme.surroundingOverlayColor;\n\n //Path around image\n this.ctx.moveTo(...this.img2ctxPt([0, 0]));\n this.ctx.lineTo(...this.img2ctxPt([this.imgMat.cols, 0]));\n this.ctx.lineTo(...this.img2ctxPt([this.imgMat.cols, this.imgMat.rows]));\n this.ctx.lineTo(...this.img2ctxPt([0, this.imgMat.rows]));\n this.ctx.lineTo(...this.img2ctxPt([0, 0]));\n this.ctx.closePath();\n\n //Path around selected area\n this.ctx.moveTo(...this.img2ctxPt(this.corners.tl));\n this.ctx.lineTo(...this.img2ctxPt(this.corners.tr));\n this.ctx.lineTo(...this.img2ctxPt(this.corners.br));\n this.ctx.lineTo(...this.img2ctxPt(this.corners.bl));\n this.ctx.lineTo(...this.img2ctxPt(this.corners.tl));\n this.ctx.closePath();\n\n this.ctx.fill(\"evenodd\");\n }\n\n //Draw edges\n if (this.options.theme.edgeThickness) {\n for (let i = 0; i < cornerKeys.length; i++) {\n const cornerKey = cornerKeys[i];\n const nextCornerKey = cornerKeys[(i + 1) % cornerKeys.length];\n const corner = this.corners[cornerKey];\n const nextCorner = this.corners[nextCornerKey];\n const cornerPt = this.img2ctxPt(corner);\n const nextCornerPt = this.img2ctxPt(nextCorner);\n\n this.ctx.beginPath();\n this.ctx.strokeStyle = this.options.theme.edgeColor;\n this.ctx.lineWidth = this.options.theme.edgeThickness * this.remPx;\n this.ctx.moveTo(...cornerPt);\n this.ctx.lineTo(...nextCornerPt);\n this.ctx.stroke();\n }\n }\n\n //Draw crosslines\n if (this.options.theme.crossLineThickness) {\n for (const edgeCenterPair of [\n [this.edgeCenters.t, this.edgeCenters.b],\n [this.edgeCenters.l, this.edgeCenters.r],\n ]) {\n const edgeCenter0Pt = this.img2ctxPt(edgeCenterPair[0]);\n const edgeCenter1Pt = this.img2ctxPt(edgeCenterPair[1]);\n this.ctx.beginPath();\n this.ctx.strokeStyle = this.options.theme.crossLineColor;\n this.ctx.lineWidth = this.options.theme.crossLineThickness * this.remPx;\n this.ctx.moveTo(...edgeCenter0Pt);\n this.ctx.lineTo(...edgeCenter1Pt);\n this.ctx.stroke();\n }\n }\n\n //Draw grabbers\n for (let i = 0; i < cornerKeys.length; i++) {\n const cornerKey = cornerKeys[i];\n const edgeCenterKey = edgeCenterKeys[i];\n const corner = this.corners[cornerKey];\n const edgeCenter = this.edgeCenters[edgeCenterKey];\n const cornerPt = this.img2ctxPt(corner);\n const edgeCenterPt = this.img2ctxPt(edgeCenter);\n\n //Draw corner\n const cornerRadiusFactor = this.hovered === cornerKey ? this.options.theme.grabberExpansionFactor : 1.0;\n this.ctx.beginPath();\n this.ctx.fillStyle = this.options.theme.cornerGrabberColor;\n this.ctx.arc(\n ...cornerPt,\n this.options.theme.cornerGrabberRadius * this.remPx * cornerRadiusFactor,\n 0,\n 2 * Math.PI\n );\n this.ctx.fill();\n\n //Draw edge grabber\n if (this.options.theme.edgeGrabberRadius) {\n const edgeRadiusFactor = this.hovered === edgeCenterKey ? this.options.theme.grabberExpansionFactor : 1.0;\n this.ctx.beginPath();\n this.ctx.fillStyle = this.options.theme.edgeGrabberColor;\n this.ctx.arc(\n ...edgeCenterPt,\n this.options.theme.edgeGrabberRadius * this.remPx * edgeRadiusFactor,\n 0,\n 2 * Math.PI\n );\n this.ctx.fill();\n }\n }\n\n //Draw center grabber\n if (this.options.theme.centerGrabberRadius) {\n const centerRadiusFactor = this.hovered === \"c\" ? this.options.theme.grabberExpansionFactor : 1.0;\n const viewCenterPt = this.img2ctxPt(this.viewCenter.c);\n this.ctx.beginPath();\n this.ctx.fillStyle = this.options.theme.centerGrabberColor;\n this.ctx.arc(\n ...viewCenterPt,\n this.options.theme.centerGrabberRadius * this.remPx * centerRadiusFactor,\n 0,\n 2 * Math.PI\n );\n this.ctx.fill();\n }\n\n //Draw zoom lens\n if (this.options.theme.zoomLensRadius && this.dragged && !(this.dragged in this.viewCenter)) {\n let draggedPt: Pt;\n let lensOffset: [number, number];\n let angleFrom: number;\n let angleTo: number;\n\n if (this.dragged in this.corners) {\n draggedPt = this.corners[this.dragged];\n lensOffset = { tl: [1, 1], tr: [-1, 1], br: [-1, -1], bl: [1, -1] }[this.dragged];\n\n angleFrom = Util.angleBetween(\n this.corners[this.dragged],\n this.corners[Util.rotateKey(this.corners, this.dragged, -1)]\n );\n angleTo = Util.angleBetween(\n this.corners[this.dragged],\n this.corners[Util.rotateKey(this.corners, this.dragged, 1)]\n );\n } else if (this.dragged in this.edgeCenters) {\n draggedPt = this.edgeCenters[this.dragged];\n lensOffset = { t: [0, 1], r: [-1, 0], b: [0, -1], l: [1, 0] }[this.dragged];\n\n const cornerKeys = Object.keys(this.corners) as (keyof Corners)[];\n const edgeCenterKeys = Object.keys(this.edgeCenters) as (keyof EdgeCenters)[];\n const edgeCenterKeyIndex = edgeCenterKeys.indexOf(this.dragged as keyof EdgeCenters);\n const cornerFromKey = cornerKeys[Util.rotateVal(edgeCenterKeyIndex, 0, cornerKeys.length)];\n angleFrom = Util.angleBetween(this.edgeCenters[this.dragged], this.corners[cornerFromKey]);\n angleTo = angleFrom + Math.PI;\n }\n angleFrom += this.rotations * (Math.PI / 2);\n angleTo += this.rotations * (Math.PI / 2);\n\n let lensPt = this.img2ctxPt(draggedPt);\n lensPt = [\n lensPt[0] +\n lensOffset[0] * (this.options.theme.zoomLensRadius + this.options.theme.zoomLensMargin) * this.remPx,\n lensPt[1] +\n lensOffset[1] * (this.options.theme.zoomLensRadius + this.options.theme.zoomLensMargin) * this.remPx,\n ];\n\n const lensRadius = this.options.theme.zoomLensRadius * this.remPx;\n const magnifiedLensRadius = lensRadius / (this.options.theme.zoomLensFactor || 1);\n\n //Draw zoom lens border\n this.ctx.beginPath();\n this.ctx.strokeStyle = this.options.theme.zoomLensBorderColor;\n this.ctx.lineWidth = this.options.theme.zoomLensBorderThickness * this.remPx;\n this.ctx.arc(...lensPt, lensRadius, 0, 2 * Math.PI);\n this.ctx.stroke();\n\n //Draw zoom lens img\n this.ctx.save();\n this.ctx.beginPath();\n this.ctx.arc(...lensPt, this.options.theme.zoomLensRadius * this.remPx, 0, 2 * Math.PI);\n this.ctx.clip();\n this.ctx.fillStyle = this.options.theme.backgroundColor;\n this.ctx.fillRect(lensPt[0] - lensRadius, lensPt[1] - lensRadius, lensRadius * 2, lensRadius * 2);\n if (this.rotations) {\n this.ctx.translate(lensPt[0], lensPt[1]);\n this.ctx.rotate(this.rotations * (Math.PI / 2));\n this.ctx.translate(-lensPt[0], -lensPt[1]);\n }\n this.ctx.drawImage(\n this.img,\n draggedPt[0] - magnifiedLensRadius,\n draggedPt[1] - magnifiedLensRadius,\n magnifiedLensRadius * 2,\n magnifiedLensRadius * 2,\n lensPt[0] - lensRadius,\n lensPt[1] - lensRadius,\n lensRadius * 2,\n lensRadius * 2\n );\n this.ctx.restore();\n\n //Draw crop overlay\n this.ctx.beginPath();\n this.ctx.fillStyle = this.options.theme.zoomLensOverlayColor;\n this.ctx.moveTo(...lensPt);\n this.ctx.lineTo(...Util.ptAtAngle(lensPt, angleFrom, lensRadius));\n this.ctx.arc(...lensPt, lensRadius, angleFrom, angleTo);\n this.ctx.lineTo(...lensPt);\n this.ctx.fill();\n }\n } else {\n this.ctx.fillStyle = \"rgba(0,0,0,0.5)\";\n this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n }\n }\n\n public setDragged(dragged: keyof Corners | keyof EdgeCenters | keyof ViewCenter | undefined) {\n this.dragged = dragged;\n this.canvas.style.cursor = dragged ? \"grabbing\" : \"default\";\n this.setHovered(dragged);\n }\n\n public setHovered(hovered: keyof Corners | keyof EdgeCenters | keyof ViewCenter | undefined) {\n this.hovered = hovered;\n if (!this.dragged) this.canvas.style.cursor = hovered ? \"grab\" : \"default\";\n }\n\n /** Returns image cropped according to current position of draggable corners */\n public getResult(type?: string, quality?: number) {\n let dst = new cv.Mat();\n let srcTri = cv.matFromArray(4, 1, cv.CV_32FC2, [\n ...this.corners.tl,\n ...this.corners.tr,\n ...this.corners.bl,\n ...this.corners.br,\n ]); //Make new size from srcTri?\n const w =\n (Util.ptDistance(this.corners.tl, this.corners.tr) + Util.ptDistance(this.corners.bl, this.corners.br)) / 2;\n const h =\n (Util.ptDistance(this.corners.tl, this.corners.bl) + Util.ptDistance(this.corners.tr, this.corners.br)) / 2;\n let dstTri = cv.matFromArray(4, 1, cv.CV_32FC2, [0, 0, w, 0, 0, h, w, h]);\n let M = cv.getPerspectiveTransform(srcTri, dstTri);\n cv.warpPerspective(this.imgMat, dst, M, new cv.Size(w, h), cv.INTER_LINEAR, cv.BORDER_CONSTANT, new cv.Scalar());\n\n // Convert to image\n const image = new Image();\n const dstCanvas = document.createElement(\"canvas\");\n cv.imshow(dstCanvas, dst);\n image.src = dstCanvas.toDataURL(type, quality);\n return image;\n }\n\n public rotateLeft() {\n this.rotate(\"left\");\n }\n\n public rotateRight() {\n this.rotate(\"right\");\n }\n\n private rotate(dir: \"left\" | \"right\") {\n const left = dir === \"left\";\n const { tl, tr, br, bl } = this.corners;\n this.corners = {\n tl: left ? tr : bl,\n tr: left ? br : tl,\n br: left ? bl : tr,\n bl: left ? tl : br,\n };\n\n this.rotations = Util.rotateVal(this.rotations, left ? -1 : 1, 4);\n\n this.imgW = this.rotations % 2 == 0 ? this.imgMat.cols : this.imgMat.rows;\n this.imgH = this.rotations % 2 == 0 ? this.imgMat.rows : this.imgMat.cols;\n\n this.canvas.width = this.imgW;\n this.canvas.height = this.imgH;\n\n this.updateCenters();\n this.adjustDimensions();\n }\n\n private updateCenters() {\n this.edgeCenters = this.corners ? Util.edgeCenters(this.corners) : undefined;\n this.viewCenter = this.corners ? { c: Util.cornerCenter(this.corners) } : undefined;\n }\n\n public getRotations() {\n return this.rotations;\n }\n\n public getCorners() {\n const { tl, tr, br, bl } = this.corners;\n return {\n tl: [tl[0], tl[1]],\n tr: [tr[0], tr[1]],\n br: [br[0], br[1]],\n bl: [bl[0], bl[1]],\n } as Corners;\n }\n\n /** Converts coordinates on unrotated source image to coordinates on ctx */\n private img2ctxPt(imgPt: Pt): Pt {\n const cr = this.options.theme.cornerGrabberRadius;\n\n if (this.rotations === 0) {\n return [imgPt[0] + cr * this.remPx, imgPt[1] + cr * this.remPx];\n } else if (this.rotations === 1) {\n return [this.imgW - imgPt[1] + cr * this.remPx, imgPt[0] + cr * this.remPx];\n } else if (this.rotations === 2) {\n return [this.imgW - imgPt[0] + cr * this.remPx, this.imgH - imgPt[1] + cr * this.remPx];\n } else if (this.rotations === 3) {\n return [imgPt[1] + cr * this.remPx, this.imgH - imgPt[0] + cr * this.remPx];\n }\n }\n\n /** Converts coordinates on ctx to coordinates on unrotated source image */\n private ctx2imgPt(ctxPt: Pt): Pt {\n const cr = this.options.theme.cornerGrabberRadius;\n\n if (this.rotations === 0) {\n return [ctxPt[0] - cr * this.remPx, ctxPt[1] - cr * this.remPx];\n } else if (this.rotations === 1) {\n return [ctxPt[1] - cr * this.remPx, this.imgW - ctxPt[0] + cr * this.remPx];\n } else if (this.rotations === 2) {\n return [this.imgW - ctxPt[0] + cr * this.remPx, this.imgH - ctxPt[1] + cr * this.remPx];\n } else if (this.rotations === 3) {\n return [this.imgH - ctxPt[1] + cr * this.remPx, ctxPt[0] - cr * this.remPx];\n }\n }\n\n /** Converts cursor position on canvas to coordinates on unrotated source image */\n private cl2imgPt(clPt: Pt): Pt {\n const bounds = this.canvas.getBoundingClientRect();\n const sX = this.canvas.width / bounds.width;\n const sY = this.canvas.height / bounds.height;\n return this.ctx2imgPt([(clPt[0] - bounds.left) * sX, (clPt[1] - bounds.top) * sY]);\n }\n}\n"],"names":[],"version":3,"file":"intellicrop.d.ts.map"}