@h4md1/visual-image-tool 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -31
- package/dist/visual-image-tool.cjs +13 -3
- package/dist/visual-image-tool.cjs.map +1 -1
- package/dist/visual-image-tool.esm.js +13 -3
- package/dist/visual-image-tool.esm.js.map +1 -1
- package/dist/visual-image-tool.umd.js +1 -1
- package/dist/visual-image-tool.umd.js.map +1 -1
- package/package.json +3 -3
- package/src/visual-image-tool.js +13 -3
- package/src/visual-image-tool.test.js +52 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"visual-image-tool.umd.js","sources":["../src/visual-image-tool.js"],"sourcesContent":["/**\n * ImageTool - A lightweight tool for defining focus points and crop zones on images\n * @module visual-image-tool\n */\n\nclass VisualImageTool {\n\t/**\n\t * Creates an instance of the image tool.\n\t * The focus point is initialized to `{x:0, y:0}` in `this.state`. When `toggleFocusPoint(true)` is called for the first time, if the focus point is still `{x:0, y:0}`, it will be automatically centered on the image.\n\t * @param {Object} options - Configuration options\n\t * @param {HTMLElement|string} options.imageElement - Image element or CSS selector\n\t * @param {Object} [options.focusPoint] - Focus point configuration\n\t * @param {boolean} [options.focusPoint.enabled=true] - Enable focus point functionality\n\t * @param {Object} [options.focusPoint.style] - Custom styles for the focus point marker\n\t * @param {Object} [options.cropZone] - Crop zone configuration\n\t * @param {boolean} [options.cropZone.enabled=true] - Enable crop zone functionality\n\t * @param {Object} [options.cropZone.style] - Custom styles for the crop overlay\n\t * @param {Function} [options.onChange] - Callback called on changes\n\t */\n\tconstructor(options) {\n\t\t// Validate options\n\t\tif (!options || !options.imageElement) {\n\t\t\tthrow new Error(\"Image element is required\");\n\t\t}\n\n\t\t// Initialize properties\n\t\tthis.imageElement =\n\t\t\ttypeof options.imageElement === \"string\"\n\t\t\t\t? document.querySelector(options.imageElement)\n\t\t\t\t: options.imageElement;\n\n\t\tif (!this.imageElement || this.imageElement.tagName !== \"IMG\") {\n\t\t\tthrow new Error(\"Invalid image element\");\n\t\t}\n\n\t\t// Default options\n\t\tthis.options = {\n\t\t\tfocusPoint: {\n\t\t\t\tenabled: true,\n\t\t\t\tstyle: {\n\t\t\t\t\twidth: \"30px\",\n\t\t\t\t\theight: \"30px\",\n\t\t\t\t\tborder: \"3px solid white\",\n\t\t\t\t\tboxShadow: \"0 0 0 2px black, 0 0 5px rgba(0,0,0,0.5)\",\n\t\t\t\t\tbackgroundColor: \"rgba(255, 0, 0, 0.5)\",\n\t\t\t\t},\n\t\t\t\t...options.focusPoint,\n\t\t\t},\n\t\t\tcropZone: {\n\t\t\t\tenabled: true,\n\t\t\t\tstyle: {\n\t\t\t\t\tborder: \"1px dashed #fff\",\n\t\t\t\t\tbackgroundColor: \"rgba(0, 0, 0, 0.4)\",\n\t\t\t\t},\n\t\t\t\thandleStyle: {\n\t\t\t\t\twidth: \"14px\",\n\t\t\t\t\theight: \"14px\",\n\t\t\t\t\tbackgroundColor: \"white\",\n\t\t\t\t\tborder: \"2px solid black\",\n\t\t\t\t\tboxShadow: \"0 0 3px rgba(0,0,0,0.5)\",\n\t\t\t\t},\n\t\t\t\t...options.cropZone,\n\t\t\t},\n\t\t\tonChange: options.onChange || (() => {}),\n\t\t\tdebug: options.debug || false,\n\t\t};\n\n\t\t// Internal state\n\t\tthis.state = {\n\t\t\tfocusMarker: null,\n\t\t\tcropOverlay: null,\n\t\t\tfocusActive: false,\n\t\t\tcropActive: false,\n\t\t\tfocusPoint: { x: 0, y: 0 },\n\t\t\tcropZone: { x: 0, y: 0, width: 0, height: 0 },\n\t\t\toriginalWidth: 1,\n\t\t\toriginalHeight: 1,\n\t\t\tdisplayWidth: 1,\n\t\t\tdisplayHeight: 1,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t};\n\n\t\t// Handlers are bound once here: addEventListener and removeEventListener\n\t\t// must be handed the same function reference for removal to work.\n\t\tthis._boundUpdateScaling = this._updateScaling.bind(this);\n\t\tthis._boundHandleMouseUp = this._handleMouseUp.bind(this);\n\t\tthis._boundHandleMouseMove = this._handleMouseMove.bind(this);\n\t\tthis._boundFocusMarkerMouseDown =\n\t\t\tthis._handleFocusMarkerMouseDown.bind(this);\n\t\tthis._boundCropOverlayMouseDown =\n\t\t\tthis._handleCropOverlayMouseDown.bind(this);\n\t\tthis._boundCropHandleMouseDown = this._handleCropHandleMouseDown.bind(this);\n\n\t\t// Resize handles, tracked so their listeners can be removed on destroy\n\t\tthis.cropHandles = [];\n\n\t\t// Variables for tracking interactions\n\t\tthis.interaction = {\n\t\t\tfocusDragging: false,\n\t\t\tfocusDragOffsetX: 0,\n\t\t\tfocusDragOffsetY: 0,\n\t\t\tcropDragging: false,\n\t\t\tcropResizing: false,\n\t\t\tactiveHandle: null,\n\t\t\tstartX: 0,\n\t\t\tstartY: 0,\n\t\t\tstartWidth: 0,\n\t\t\tstartHeight: 0,\n\t\t\tstartMouseX: 0,\n\t\t\tstartMouseY: 0,\n\t\t};\n\n\t\tthis.spinnerElement = null;\n\t\tthis.initialLoadComplete = false;\n\n\t\t// Initialize the tool\n\t\tthis._init();\n\t}\n\n\t/**\n\t * Initializes the image tool\n\t * @private\n\t */\n\t_init() {\n\t\t// Prepare the parent container\n\t\tthis._prepareContainer();\n\n\t\t// Initialize dimensions\n\t\tthis._updateScaling();\n\n\t\t// Create UI elements if enabled\n\t\tif (this.options.focusPoint.enabled) {\n\t\t\tthis._createFocusMarker();\n\t\t}\n\n\t\tif (this.options.cropZone.enabled) {\n\t\t\tthis._createCropOverlay();\n\t\t}\n\n\t\t// Add event listeners\n\t\tthis._setupEventListeners();\n\t}\n\n\t/**\n\t * Prepares the image's parent container\n\t * @private\n\t */\n\t_prepareContainer() {\n\t\t// Ensure the parent is positioned\n\t\tif (this.imageElement.parentNode) {\n\t\t\tthis.imageElement.parentNode.style.position = \"relative\";\n\n\t\t\t// Create spinner element\n\t\t\tthis.spinnerElement = document.createElement(\"div\");\n\t\t\tthis.spinnerElement.style.position = \"absolute\";\n\t\t\tthis.spinnerElement.style.top = \"50%\";\n\t\t\tthis.spinnerElement.style.left = \"50%\";\n\t\t\tthis.spinnerElement.style.transform = \"translate(-50%, -50%)\";\n\t\t\tthis.spinnerElement.style.width = \"40px\";\n\t\t\tthis.spinnerElement.style.height = \"40px\";\n\t\t\tthis.spinnerElement.style.border = \"4px solid #f3f3f3\"; // Light grey\n\t\t\tthis.spinnerElement.style.borderTop = \"4px solid #3498db\"; // Blue\n\t\t\tthis.spinnerElement.style.borderRadius = \"50%\";\n\t\t\tthis.spinnerElement.style.boxSizing = \"border-box\";\n\t\t\tthis.spinnerElement.style.zIndex = \"1000\"; // Ensure it's above other elements\n\t\t\tthis.spinnerElement.style.display = \"block\"; // Initially visible\n\t\t\tthis.spinnerElement.style.animation = \"spin 1s linear infinite\";\n\n\t\t\tthis.imageElement.parentNode.appendChild(this.spinnerElement);\n\n\t\t\t// Add CSS keyframes for spinner animation\n\t\t\tif (!document.getElementById(\"visual-image-tool-spinner-styles\")) {\n\t\t\t\tconst styleElement = document.createElement(\"style\");\n\t\t\t\tstyleElement.id = \"visual-image-tool-spinner-styles\";\n\t\t\t\tstyleElement.innerHTML = `\n@keyframes spin {\n 0% { transform: translate(-50%, -50%) rotate(0deg); }\n 100% { transform: translate(-50%, -50%) rotate(360deg); }\n}\n `;\n\t\t\t\tdocument.head.appendChild(styleElement);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Updates scaling factors\n\t * @private\n\t */\n\t_updateScaling() {\n\t\tthis.state.originalWidth = this.imageElement.naturalWidth || 1;\n\t\tthis.state.originalHeight = this.imageElement.naturalHeight || 1;\n\n\t\t// Hide spinner once image is loaded and dimensions are available\n\t\tif (\n\t\t\t!this.initialLoadComplete &&\n\t\t\tthis.spinnerElement &&\n\t\t\tthis.state.originalWidth > 1 &&\n\t\t\tthis.state.originalHeight > 1\n\t\t) {\n\t\t\tthis.spinnerElement.style.display = \"none\";\n\t\t\tthis.initialLoadComplete = true;\n\t\t}\n\t\tthis.state.displayWidth = this.imageElement.offsetWidth;\n\t\tthis.state.displayHeight = this.imageElement.offsetHeight;\n\t\tthis.state.scaleX = this.state.displayWidth / this.state.originalWidth;\n\t\tthis.state.scaleY = this.state.displayHeight / this.state.originalHeight;\n\n\t\t// Update spinner position during initial load if still visible\n\t\tif (!this.initialLoadComplete && this.spinnerElement) {\n\t\t\tthis.spinnerElement.style.top = `${this.state.displayHeight / 2}px`;\n\t\t\tthis.spinnerElement.style.left = `${this.state.displayWidth / 2}px`;\n\t\t}\n\n\t\t// Reposition elements if active\n\t\tif (this.state.focusActive) {\n\t\t\tthis._updateFocusMarkerPosition();\n\t\t}\n\n\t\tif (this.state.cropActive) {\n\t\t\tthis._updateCropOverlayPosition();\n\t\t}\n\t}\n\n\t/**\n\t * Converts display coordinates to original coordinates\n\t * @private\n\t * @param {number} scaledX - X-coordinate at display scale\n\t * @param {number} scaledY - Y-coordinate at display scale\n\t * @returns {Object} Original coordinates {x, y}\n\t */\n\t_toOriginalCoords(scaledX, scaledY) {\n\t\treturn {\n\t\t\tx: scaledX / this.state.scaleX,\n\t\t\ty: scaledY / this.state.scaleY,\n\t\t};\n\t}\n\n\t/**\n\t * Converts original coordinates to display coordinates\n\t * @private\n\t * @param {number} originalX - Original X-coordinate\n\t * @param {number} originalY - Original Y-coordinate\n\t * @returns {Object} Display scale coordinates {x, y}\n\t */\n\t_toScaledCoords(originalX, originalY) {\n\t\treturn {\n\t\t\tx: originalX * this.state.scaleX,\n\t\t\ty: originalY * this.state.scaleY,\n\t\t};\n\t}\n\n\t/**\n\t * Creates the focus point marker\n\t * @private\n\t */\n\t_createFocusMarker() {\n\t\tif (this.state.focusMarker) return;\n\n\t\tconst marker = document.createElement(\"div\");\n\t\tmarker.style.position = \"absolute\";\n\t\tmarker.style.width = this.options.focusPoint.style.width;\n\t\tmarker.style.height = this.options.focusPoint.style.height;\n\t\tmarker.style.border = this.options.focusPoint.style.border;\n\t\tmarker.style.boxShadow = this.options.focusPoint.style.boxShadow;\n\t\tmarker.style.backgroundColor =\n\t\t\tthis.options.focusPoint.style.backgroundColor;\n\t\tmarker.style.cursor = \"move\";\n\t\tmarker.style.display = \"none\";\n\t\tmarker.style.zIndex = \"999\";\n\t\tmarker.style.clipPath =\n\t\t\t\"polygon(40% 0%, 60% 0%, 60% 40%, 100% 40%, 100% 60%, 60% 60%, 60% 100%, 40% 100%, 40% 60%, 0% 60%, 0% 40%, 40% 40%)\";\n\n\t\tthis.imageElement.parentNode.appendChild(marker);\n\t\tthis.state.focusMarker = marker;\n\n\t\t// Add event listeners to the marker\n\t\tmarker.addEventListener(\"mousedown\", this._boundFocusMarkerMouseDown);\n\t}\n\n\t/**\n\t * Handles mousedown event on the focus point marker\n\t * @private\n\t * @param {MouseEvent} e - Mousedown event\n\t */\n\t_handleFocusMarkerMouseDown(e) {\n\t\tthis.interaction.focusDragging = true;\n\t\tthis.state.focusMarker.style.cursor = \"grabbing\";\n\n\t\t// Calculate offset from marker center\n\t\tconst rect = this.state.focusMarker.getBoundingClientRect();\n\t\tthis.interaction.focusDragOffsetX =\n\t\t\te.clientX - (rect.left + rect.width / 2);\n\t\tthis.interaction.focusDragOffsetY =\n\t\t\te.clientY - (rect.top + rect.height / 2);\n\n\t\te.preventDefault();\n\t}\n\n\t/**\n\t * Creates the crop zone overlay\n\t * @private\n\t */\n\t_createCropOverlay() {\n\t\tif (this.state.cropOverlay) return;\n\n\t\tconst overlay = document.createElement(\"div\");\n\t\toverlay.style.position = \"absolute\";\n\t\toverlay.style.border = this.options.cropZone.style.border;\n\t\toverlay.style.backgroundColor = this.options.cropZone.style.backgroundColor;\n\t\toverlay.style.boxSizing = \"border-box\";\n\t\toverlay.style.cursor = \"move\";\n\t\toverlay.style.display = \"none\";\n\t\toverlay.style.zIndex = \"998\";\n\n\t\t// Add resize handles\n\t\tconst handles = [\"tl\", \"tm\", \"tr\", \"ml\", \"mr\", \"bl\", \"bm\", \"br\"];\n\t\tfor (const handleType of handles) {\n\t\t\tconst handle = document.createElement(\"div\");\n\t\t\thandle.style.position = \"absolute\";\n\t\t\thandle.style.width = this.options.cropZone.handleStyle.width;\n\t\t\thandle.style.height = this.options.cropZone.handleStyle.height;\n\t\t\thandle.style.backgroundColor =\n\t\t\t\tthis.options.cropZone.handleStyle.backgroundColor;\n\t\t\thandle.style.border = this.options.cropZone.handleStyle.border;\n\t\t\thandle.style.boxShadow = this.options.cropZone.handleStyle.boxShadow;\n\t\t\thandle.style.boxSizing = \"border-box\";\n\t\t\thandle.dataset.handle = handleType;\n\n\t\t\t// Position the handle\n\t\t\tswitch (handleType) {\n\t\t\t\tcase \"tl\": // Top-left\n\t\t\t\t\thandle.style.top = \"-7px\";\n\t\t\t\t\thandle.style.left = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"nwse-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"tm\": // Top-middle\n\t\t\t\t\thandle.style.top = \"-7px\";\n\t\t\t\t\thandle.style.left = \"50%\";\n\t\t\t\t\thandle.style.marginLeft = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"ns-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"tr\": // Top-right\n\t\t\t\t\thandle.style.top = \"-7px\";\n\t\t\t\t\thandle.style.right = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"nesw-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"ml\": // Middle-left\n\t\t\t\t\thandle.style.top = \"50%\";\n\t\t\t\t\thandle.style.left = \"-7px\";\n\t\t\t\t\thandle.style.marginTop = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"ew-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"mr\": // Middle-right\n\t\t\t\t\thandle.style.top = \"50%\";\n\t\t\t\t\thandle.style.right = \"-7px\";\n\t\t\t\t\thandle.style.marginTop = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"ew-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"bl\": // Bottom-left\n\t\t\t\t\thandle.style.bottom = \"-7px\";\n\t\t\t\t\thandle.style.left = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"nesw-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"bm\": // Bottom-middle\n\t\t\t\t\thandle.style.bottom = \"-7px\";\n\t\t\t\t\thandle.style.left = \"50%\";\n\t\t\t\t\thandle.style.marginLeft = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"ns-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"br\": // Bottom-right\n\t\t\t\t\thandle.style.bottom = \"-7px\";\n\t\t\t\t\thandle.style.right = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"nwse-resize\";\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Add event listener\n\t\t\thandle.addEventListener(\"mousedown\", this._boundCropHandleMouseDown);\n\n\t\t\toverlay.appendChild(handle);\n\t\t\tthis.cropHandles.push(handle);\n\t\t}\n\n\t\t// Add listener for overlay dragging\n\t\toverlay.addEventListener(\"mousedown\", this._boundCropOverlayMouseDown);\n\n\t\tthis.imageElement.parentNode.appendChild(overlay);\n\t\tthis.state.cropOverlay = overlay;\n\t}\n\n\t/**\n\t * Handles mousedown event on a resize handle\n\t * @private\n\t * @param {MouseEvent} e - Mousedown event\n\t */\n\t_handleCropHandleMouseDown(e) {\n\t\tconst handleType = e.currentTarget.dataset.handle;\n\n\t\tthis.interaction.cropResizing = true;\n\t\tthis.interaction.activeHandle = handleType;\n\n\t\t// Record initial dimensions and position\n\t\tthis.interaction.startX =\n\t\t\tNumber.parseInt(this.state.cropOverlay.style.left, 10) || 0;\n\t\tthis.interaction.startY =\n\t\t\tNumber.parseInt(this.state.cropOverlay.style.top, 10) || 0;\n\t\tthis.interaction.startWidth = this.state.cropOverlay.offsetWidth;\n\t\tthis.interaction.startHeight = this.state.cropOverlay.offsetHeight;\n\t\tthis.interaction.startMouseX = e.clientX;\n\t\tthis.interaction.startMouseY = e.clientY;\n\n\t\te.preventDefault();\n\t\te.stopPropagation();\n\t}\n\n\t/**\n\t * Handles mousedown event on the crop overlay\n\t * @private\n\t * @param {MouseEvent} e - Mousedown event\n\t */\n\t_handleCropOverlayMouseDown(e) {\n\t\t// Ignore if clicking on a handle\n\t\tif (e.target !== this.state.cropOverlay) return;\n\n\t\tthis.interaction.cropDragging = true;\n\t\tthis.state.cropOverlay.style.cursor = \"grabbing\";\n\n\t\t// Record initial position\n\t\tthis.interaction.startX =\n\t\t\tNumber.parseInt(this.state.cropOverlay.style.left, 10) || 0;\n\t\tthis.interaction.startY =\n\t\t\tNumber.parseInt(this.state.cropOverlay.style.top, 10) || 0;\n\t\tthis.interaction.startMouseX = e.clientX;\n\t\tthis.interaction.startMouseY = e.clientY;\n\n\t\te.preventDefault();\n\t}\n\n\t/**\n\t * Sets up global event listeners\n\t * @private\n\t */\n\t_setupEventListeners() {\n\t\t// Listener for window resize\n\t\twindow.addEventListener(\"resize\", this._boundUpdateScaling);\n\n\t\t// Listener for image load\n\t\tif (!this.imageElement.complete) {\n\t\t\tthis.imageElement.addEventListener(\"load\", this._boundUpdateScaling);\n\t\t}\n\n\t\t// Listeners for mouse interactions\n\t\tdocument.addEventListener(\"mouseup\", this._boundHandleMouseUp);\n\t\tdocument.addEventListener(\"mousemove\", this._boundHandleMouseMove);\n\t}\n\n\t/**\n\t * Handles global mouseup event\n\t * @private\n\t */\n\t_handleMouseUp() {\n\t\tif (this.interaction.focusDragging) {\n\t\t\tthis.interaction.focusDragging = false;\n\t\t\tif (this.state.focusMarker) this.state.focusMarker.style.cursor = \"move\";\n\t\t}\n\n\t\tif (this.interaction.cropDragging) {\n\t\t\tthis.interaction.cropDragging = false;\n\t\t\tif (this.state.cropOverlay) this.state.cropOverlay.style.cursor = \"move\";\n\t\t}\n\n\t\tthis.interaction.cropResizing = false;\n\t\tthis.interaction.activeHandle = null;\n\t\tdocument.body.style.cursor = \"default\";\n\t}\n\n\t/**\n\t * Handles global mousemove event\n\t * @private\n\t * @param {MouseEvent} e - Mousemove event\n\t */\n\t_handleMouseMove(e) {\n\t\t// Handle focus point dragging\n\t\tif (this.interaction.focusDragging && this.state.focusMarker) {\n\t\t\tthis._handleFocusMarkerDrag(e);\n\t\t}\n\n\t\t// Handle crop zone dragging\n\t\tif (this.interaction.cropDragging) {\n\t\t\tthis._handleCropOverlayDrag(e);\n\t\t}\n\n\t\t// Handle crop zone resizing\n\t\tif (this.interaction.cropResizing) {\n\t\t\tthis._handleCropOverlayResize(e);\n\t\t}\n\t}\n\n\t/**\n\t * Handles focus point marker drag\n\t * @private\n\t * @param {MouseEvent} e - Mousemove event\n\t */\n\t_handleFocusMarkerDrag(e) {\n\t\tconst imageRect = this.imageElement.getBoundingClientRect();\n\n\t\t// Calculate target position relative to the image\n\t\tconst targetScaledX =\n\t\t\te.clientX - imageRect.left - this.interaction.focusDragOffsetX;\n\t\tconst targetScaledY =\n\t\t\te.clientY - imageRect.top - this.interaction.focusDragOffsetY;\n\n\t\t// Convert to original coordinates\n\t\tconst original = this._toOriginalCoords(targetScaledX, targetScaledY);\n\n\t\t// Update focus point\n\t\tthis.setFocusPoint(original.x, original.y);\n\t}\n\n\t/**\n\t * Handles crop overlay drag\n\t * @private\n\t * @param {MouseEvent} e - Mousemove event\n\t */\n\t_handleCropOverlayDrag(e) {\n\t\tconst deltaX = e.clientX - this.interaction.startMouseX;\n\t\tconst deltaY = e.clientY - this.interaction.startMouseY;\n\n\t\t// Calculate new position in display pixels\n\t\tconst newX = this.interaction.startX + deltaX;\n\t\tconst newY = this.interaction.startY + deltaY;\n\n\t\t// Convert to original coordinates\n\t\tconst original = this._toOriginalCoords(newX, newY);\n\n\t\t// Get current dimensions in original coordinates\n\t\tconst { width, height } = this.state.cropZone;\n\n\t\t// Update crop zone\n\t\tthis.setCropZone(original.x, original.y, width, height);\n\t}\n\n\t/**\n\t * Handles crop overlay resize\n\t * @private\n\t * @param {MouseEvent} e - Mousemove event\n\t */\n\t_handleCropOverlayResize(e) {\n\t\tif (!this.interaction.activeHandle) return;\n\n\t\tconst deltaX = e.clientX - this.interaction.startMouseX;\n\t\tconst deltaY = e.clientY - this.interaction.startMouseY;\n\n\t\tlet newX = this.interaction.startX;\n\t\tlet newY = this.interaction.startY;\n\t\tlet newWidth = this.interaction.startWidth;\n\t\tlet newHeight = this.interaction.startHeight;\n\n\t\t// Adjust based on active handle\n\t\tif (this.interaction.activeHandle.includes(\"t\")) {\n\t\t\t// Top\n\t\t\tnewY = this.interaction.startY + deltaY;\n\t\t\tnewHeight = this.interaction.startHeight - deltaY;\n\t\t}\n\t\tif (this.interaction.activeHandle.includes(\"b\")) {\n\t\t\t// Bottom\n\t\t\tnewHeight = this.interaction.startHeight + deltaY;\n\t\t}\n\t\tif (this.interaction.activeHandle.includes(\"l\")) {\n\t\t\t// Left\n\t\t\tnewX = this.interaction.startX + deltaX;\n\t\t\tnewWidth = this.interaction.startWidth - deltaX;\n\t\t}\n\t\tif (this.interaction.activeHandle.includes(\"r\")) {\n\t\t\t// Right\n\t\t\tnewWidth = this.interaction.startWidth + deltaX;\n\t\t}\n\n\t\t// Prevent negative dimensions\n\t\tif (newWidth < 10) {\n\t\t\tif (this.interaction.activeHandle.includes(\"l\")) {\n\t\t\t\tnewX = this.interaction.startX + this.interaction.startWidth - 10;\n\t\t\t}\n\t\t\tnewWidth = 10;\n\t\t}\n\t\tif (newHeight < 10) {\n\t\t\tif (this.interaction.activeHandle.includes(\"t\")) {\n\t\t\t\tnewY = this.interaction.startY + this.interaction.startHeight - 10;\n\t\t\t}\n\t\t\tnewHeight = 10;\n\t\t}\n\n\t\t// Convert display coordinates to original coordinates\n\t\tconst originalX = newX / this.state.scaleX;\n\t\tconst originalY = newY / this.state.scaleY;\n\t\tconst originalWidth = newWidth / this.state.scaleX;\n\t\tconst originalHeight = newHeight / this.state.scaleY;\n\n\t\t// Update crop zone\n\t\tthis.setCropZone(originalX, originalY, originalWidth, originalHeight);\n\t}\n\n\t/**\n\t * Updates the focus point marker position\n\t * @private\n\t */\n\t_updateFocusMarkerPosition() {\n\t\tif (!this.state.focusMarker) return;\n\n\t\tconst { x, y } = this.state.focusPoint;\n\n\t\t// Clamp coordinates to image dimensions\n\t\tconst clampedX = Math.max(0, Math.min(x, this.state.originalWidth));\n\t\tconst clampedY = Math.max(0, Math.min(y, this.state.originalHeight));\n\n\t\t// Update state if coordinates were clamped\n\t\tif (x !== clampedX || y !== clampedY) {\n\t\t\tthis.state.focusPoint = { x: clampedX, y: clampedY };\n\t\t}\n\n\t\tconst scaled = this._toScaledCoords(clampedX, clampedY);\n\n\t\t// LOGGING: Validate padding offset\n\t\tif (this.options.debug) {\n\t\t\tconst container = this.imageElement.parentNode;\n\t\t\tconst computedStyle = window.getComputedStyle(container);\n\t\t\tconst paddingLeft = Number.parseFloat(computedStyle.paddingLeft);\n\t\t\tconst paddingTop = Number.parseFloat(computedStyle.paddingTop);\n\t\t\tconsole.log(\n\t\t\t\t\"[FocusMarker] scaled:\",\n\t\t\t\tscaled,\n\t\t\t\t\"paddingLeft:\",\n\t\t\t\tpaddingLeft,\n\t\t\t\t\"paddingTop:\",\n\t\t\t\tpaddingTop,\n\t\t\t\t\"containerRect:\",\n\t\t\t\tcontainer.getBoundingClientRect(),\n\t\t\t);\n\t\t}\n\n\t\t// Adjust to center the marker\n\t\t// Adjust for container padding (use unique variable names)\n\t\tlet focusPaddingLeft = 0;\n\t\tlet focusPaddingTop = 0;\n\t\t{\n\t\t\tconst container = this.imageElement.parentNode;\n\t\t\tconst computedStyle = window.getComputedStyle(container);\n\t\t\tfocusPaddingLeft = Number.parseFloat(computedStyle.paddingLeft);\n\t\t\tfocusPaddingTop = Number.parseFloat(computedStyle.paddingTop);\n\t\t}\n\n\t\tthis.state.focusMarker.style.left = `${\n\t\t\tscaled.x + focusPaddingLeft - this.state.focusMarker.offsetWidth / 2\n\t\t}px`;\n\t\tthis.state.focusMarker.style.top = `${\n\t\t\tscaled.y + focusPaddingTop - this.state.focusMarker.offsetHeight / 2\n\t\t}px`;\n\t\tif (this.options.debug) {\n\t\t\tconsole.log(\n\t\t\t\t\"[FocusMarker] style.left:\",\n\t\t\t\tthis.state.focusMarker.style.left,\n\t\t\t\t\"style.top:\",\n\t\t\t\tthis.state.focusMarker.style.top,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Updates the crop overlay position and dimensions\n\t * @private\n\t */\n\t_updateCropOverlayPosition() {\n\t\tif (!this.state.cropOverlay) return;\n\n\t\tconst { x, y, width, height } = this.state.cropZone;\n\n\t\t// Clamp to image dimensions\n\t\tconst clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));\n\t\tconst clampedY = Math.max(\n\t\t\t0,\n\t\t\tMath.min(y, this.state.originalHeight - height),\n\t\t);\n\t\tconst clampedWidth = Math.max(\n\t\t\t10,\n\t\t\tMath.min(width, this.state.originalWidth - clampedX),\n\t\t);\n\t\tconst clampedHeight = Math.max(\n\t\t\t10,\n\t\t\tMath.min(height, this.state.originalHeight - clampedY),\n\t\t);\n\n\t\t// Update state if values were clamped\n\t\tif (\n\t\t\tx !== clampedX ||\n\t\t\ty !== clampedY ||\n\t\t\twidth !== clampedWidth ||\n\t\t\theight !== clampedHeight\n\t\t) {\n\t\t\tthis.state.cropZone = {\n\t\t\t\tx: clampedX,\n\t\t\t\ty: clampedY,\n\t\t\t\twidth: clampedWidth,\n\t\t\t\theight: clampedHeight,\n\t\t\t};\n\t\t}\n\n\t\t// Convert to display coordinates\n\t\tconst scaled = this._toScaledCoords(clampedX, clampedY);\n\t\tconst scaledWidth = clampedWidth * this.state.scaleX;\n\t\tconst scaledHeight = clampedHeight * this.state.scaleY;\n\n\t\t// LOGGING: Validate padding offset\n\t\tif (this.options.debug) {\n\t\t\tconst container = this.imageElement.parentNode;\n\t\t\tconst computedStyle = window.getComputedStyle(container);\n\t\t\tconst paddingLeft = Number.parseFloat(computedStyle.paddingLeft);\n\t\t\tconst paddingTop = Number.parseFloat(computedStyle.paddingTop);\n\t\t\tconsole.log(\n\t\t\t\t\"[CropOverlay] scaled:\",\n\t\t\t\tscaled,\n\t\t\t\t\"paddingLeft:\",\n\t\t\t\tpaddingLeft,\n\t\t\t\t\"paddingTop:\",\n\t\t\t\tpaddingTop,\n\t\t\t\t\"containerRect:\",\n\t\t\t\tcontainer.getBoundingClientRect(),\n\t\t\t);\n\t\t}\n\n\t\t// Update overlay\n\t\t// Adjust for container padding (use unique variable names)\n\t\tlet cropPaddingLeft = 0;\n\t\tlet cropPaddingTop = 0;\n\t\t{\n\t\t\tconst container = this.imageElement.parentNode;\n\t\t\tconst computedStyle = window.getComputedStyle(container);\n\t\t\tcropPaddingLeft = Number.parseFloat(computedStyle.paddingLeft);\n\t\t\tcropPaddingTop = Number.parseFloat(computedStyle.paddingTop);\n\t\t}\n\n\t\tthis.state.cropOverlay.style.left = `${scaled.x + cropPaddingLeft}px`;\n\t\tthis.state.cropOverlay.style.top = `${scaled.y + cropPaddingTop}px`;\n\t\tthis.state.cropOverlay.style.width = `${scaledWidth}px`;\n\t\tthis.state.cropOverlay.style.height = `${scaledHeight}px`;\n\t\tif (this.options.debug) {\n\t\t\tconsole.log(\n\t\t\t\t\"[CropOverlay] style.left:\",\n\t\t\t\tthis.state.cropOverlay.style.left,\n\t\t\t\t\"style.top:\",\n\t\t\t\tthis.state.cropOverlay.style.top,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Enables or disables the focus point.\n\t * If the focus point is at its initial state `{x:0, y:0}` (top-left corner), it will be moved to the center of the image when this method is called with `active = true` for the first time.\n\t * @public\n\t * @param {boolean} active - Activation state\n\t * @returns {VisualImageTool} Instance for chaining\n\t */\n\ttoggleFocusPoint(active) {\n\t\tif (!this.options.focusPoint.enabled) return this;\n\n\t\tconst isActive = active === undefined ? !this.state.focusActive : active;\n\n\t\tif (isActive && !this.state.focusActive) {\n\t\t\t// Enable focus point\n\t\t\tif (!this.state.focusMarker) {\n\t\t\t\tthis._createFocusMarker();\n\t\t\t}\n\n\t\t\t// Position at image center by default if not already set\n\t\t\tif (this.state.focusPoint.x === 0 && this.state.focusPoint.y === 0) {\n\t\t\t\tthis.state.focusPoint = {\n\t\t\t\t\tx: this.state.originalWidth / 2,\n\t\t\t\t\ty: this.state.originalHeight / 2,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tthis._updateFocusMarkerPosition();\n\t\t\tthis.state.focusMarker.style.display = \"block\";\n\t\t\tthis.state.focusActive = true;\n\t\t} else if (!isActive && this.state.focusActive) {\n\t\t\t// Disable focus point\n\t\t\tif (this.state.focusMarker) {\n\t\t\t\tthis.state.focusMarker.style.display = \"none\";\n\t\t\t}\n\t\t\tthis.state.focusActive = false;\n\t\t}\n\n\t\t// Notify change\n\t\tthis._notifyChange();\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Enables or disables the crop zone\n\t * @public\n\t * @param {boolean} active - Activation state\n\t * @returns {VisualImageTool} Instance for chaining\n\t */\n\ttoggleCropZone(active) {\n\t\tif (!this.options.cropZone.enabled) return this;\n\n\t\tconst isActive = active === undefined ? !this.state.cropActive : active;\n\n\t\tif (isActive && !this.state.cropActive) {\n\t\t\t// Enable crop zone\n\t\t\tif (!this.state.cropOverlay) {\n\t\t\t\tthis._createCropOverlay();\n\t\t\t}\n\n\t\t\t// Define a default zone if not already set\n\t\t\tif (this.state.cropZone.width === 0 || this.state.cropZone.height === 0) {\n\t\t\t\tconst defaultWidth = this.state.originalWidth / 2;\n\t\t\t\tconst defaultHeight = this.state.originalHeight / 2;\n\t\t\t\tconst defaultX = (this.state.originalWidth - defaultWidth) / 2;\n\t\t\t\tconst defaultY = (this.state.originalHeight - defaultHeight) / 2;\n\n\t\t\t\tthis.state.cropZone = {\n\t\t\t\t\tx: defaultX,\n\t\t\t\t\ty: defaultY,\n\t\t\t\t\twidth: defaultWidth,\n\t\t\t\t\theight: defaultHeight,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tthis._updateCropOverlayPosition();\n\t\t\tthis.state.cropOverlay.style.display = \"block\";\n\t\t\tthis.state.cropActive = true;\n\t\t} else if (!isActive && this.state.cropActive) {\n\t\t\t// Disable crop zone\n\t\t\tif (this.state.cropOverlay) {\n\t\t\t\tthis.state.cropOverlay.style.display = \"none\";\n\t\t\t}\n\t\t\tthis.state.cropActive = false;\n\t\t}\n\n\t\t// Notify change\n\t\tthis._notifyChange();\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Sets the focus point position\n\t * @public\n\t * @param {number} x - X-coordinate in original pixels\n\t * @param {number} y - Y-coordinate in original pixels\n\t * @returns {VisualImageTool} Instance for chaining\n\t */\n\tsetFocusPoint(x, y) {\n\t\t// Clamp coordinates to image dimensions\n\t\tconst clampedX = Math.max(0, Math.min(x, this.state.originalWidth));\n\t\tconst clampedY = Math.max(0, Math.min(y, this.state.originalHeight));\n\n\t\tthis.state.focusPoint = { x: clampedX, y: clampedY };\n\n\t\tif (this.state.focusActive && this.state.focusMarker) {\n\t\t\tthis._updateFocusMarkerPosition();\n\t\t}\n\n\t\t// Notify change\n\t\tthis._notifyChange();\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Sets the crop zone position and dimensions\n\t * @public\n\t * @param {number} x - X-coordinate in original pixels\n\t * @param {number} y - Y-coordinate in original pixels\n\t * @param {number} width - Width in original pixels\n\t * @param {number} height - Height in original pixels\n\t * @returns {VisualImageTool} Instance for chaining\n\t */\n\tsetCropZone(x, y, width, height) {\n\t\t// Clamp to image dimensions\n\t\tconst clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));\n\t\tconst clampedY = Math.max(\n\t\t\t0,\n\t\t\tMath.min(y, this.state.originalHeight - height),\n\t\t);\n\t\tconst clampedWidth = Math.max(\n\t\t\t10,\n\t\t\tMath.min(width, this.state.originalWidth - clampedX),\n\t\t);\n\t\tconst clampedHeight = Math.max(\n\t\t\t10,\n\t\t\tMath.min(height, this.state.originalHeight - clampedY),\n\t\t);\n\n\t\tthis.state.cropZone = {\n\t\t\tx: clampedX,\n\t\t\ty: clampedY,\n\t\t\twidth: clampedWidth,\n\t\t\theight: clampedHeight,\n\t\t};\n\n\t\tif (this.state.cropActive && this.state.cropOverlay) {\n\t\t\tthis._updateCropOverlayPosition();\n\t\t}\n\n\t\t// Notify change\n\t\tthis._notifyChange();\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Gets the current focus point position\n\t * @public\n\t * @returns {Object} Focus point coordinates {x, y}\n\t */\n\tgetFocusPoint() {\n\t\treturn { ...this.state.focusPoint };\n\t}\n\n\t/**\n\t * Gets the current crop zone position and dimensions\n\t * @public\n\t * @returns {Object} Crop zone {x, y, width, height}\n\t */\n\tgetCropZone() {\n\t\treturn { ...this.state.cropZone };\n\t}\n\n\t/**\n\t * Gets the original image dimensions\n\t * @public\n\t * @returns {Object} Dimensions {width, height}\n\t */\n\tgetImageDimensions() {\n\t\treturn {\n\t\t\twidth: this.state.originalWidth,\n\t\t\theight: this.state.originalHeight,\n\t\t};\n\t}\n\n\t/**\n\t * Notifies changes via callback\n\t * @private\n\t */\n\t_notifyChange() {\n\t\tif (typeof this.options.onChange === \"function\") {\n\t\t\tthis.options.onChange({\n\t\t\t\tfocusPoint: this.getFocusPoint(),\n\t\t\t\tcropZone: this.getCropZone(),\n\t\t\t\tfocusActive: this.state.focusActive,\n\t\t\t\tcropActive: this.state.cropActive,\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Destroys the instance and cleans up resources\n\t * @public\n\t */\n\tdestroy() {\n\t\t// Already destroyed\n\t\tif (!this.state) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Remove element listeners\n\t\tif (this.state.focusMarker) {\n\t\t\tthis.state.focusMarker.removeEventListener(\n\t\t\t\t\"mousedown\",\n\t\t\t\tthis._boundFocusMarkerMouseDown,\n\t\t\t);\n\t\t}\n\n\t\tif (this.state.cropOverlay) {\n\t\t\tthis.state.cropOverlay.removeEventListener(\n\t\t\t\t\"mousedown\",\n\t\t\t\tthis._boundCropOverlayMouseDown,\n\t\t\t);\n\t\t}\n\n\t\tfor (const handle of this.cropHandles) {\n\t\t\thandle.removeEventListener(\"mousedown\", this._boundCropHandleMouseDown);\n\t\t}\n\t\tthis.cropHandles = [];\n\n\t\t// Remove DOM elements\n\t\tif (this.state.focusMarker?.parentNode) {\n\t\t\tthis.state.focusMarker.parentNode.removeChild(this.state.focusMarker);\n\t\t}\n\n\t\tif (this.state.cropOverlay?.parentNode) {\n\t\t\tthis.state.cropOverlay.parentNode.removeChild(this.state.cropOverlay);\n\t\t}\n\n\t\tif (this.spinnerElement?.parentNode) {\n\t\t\tthis.spinnerElement.parentNode.removeChild(this.spinnerElement);\n\t\t}\n\t\tthis.spinnerElement = null;\n\n\t\t// Remove global listeners\n\t\twindow.removeEventListener(\"resize\", this._boundUpdateScaling);\n\t\tdocument.removeEventListener(\"mouseup\", this._boundHandleMouseUp);\n\t\tdocument.removeEventListener(\"mousemove\", this._boundHandleMouseMove);\n\n\t\tif (this.imageElement) {\n\t\t\tthis.imageElement.removeEventListener(\"load\", this._boundUpdateScaling);\n\t\t}\n\n\t\t// Release bound handlers\n\t\tthis._boundUpdateScaling = null;\n\t\tthis._boundHandleMouseUp = null;\n\t\tthis._boundHandleMouseMove = null;\n\t\tthis._boundFocusMarkerMouseDown = null;\n\t\tthis._boundCropOverlayMouseDown = null;\n\t\tthis._boundCropHandleMouseDown = null;\n\n\t\t// Reset state\n\t\tthis.state = null;\n\t\tthis.interaction = null;\n\t\tthis.options = null;\n\t\tthis.imageElement = null;\n\t}\n}\n\n// Export class\nexport default VisualImageTool;\n"],"names":["constructor","options","imageElement","Error","this","document","querySelector","tagName","focusPoint","enabled","style","width","height","border","boxShadow","backgroundColor","cropZone","handleStyle","onChange","debug","state","focusMarker","cropOverlay","focusActive","cropActive","x","y","originalWidth","originalHeight","displayWidth","displayHeight","scaleX","scaleY","_boundUpdateScaling","_updateScaling","bind","_boundHandleMouseUp","_handleMouseUp","_boundHandleMouseMove","_handleMouseMove","_boundFocusMarkerMouseDown","_handleFocusMarkerMouseDown","_boundCropOverlayMouseDown","_handleCropOverlayMouseDown","_boundCropHandleMouseDown","_handleCropHandleMouseDown","cropHandles","interaction","focusDragging","focusDragOffsetX","focusDragOffsetY","cropDragging","cropResizing","activeHandle","startX","startY","startWidth","startHeight","startMouseX","startMouseY","spinnerElement","initialLoadComplete","_init","_prepareContainer","_createFocusMarker","_createCropOverlay","_setupEventListeners","parentNode","position","createElement","top","left","transform","borderTop","borderRadius","boxSizing","zIndex","display","animation","appendChild","getElementById","styleElement","id","innerHTML","head","naturalWidth","naturalHeight","offsetWidth","offsetHeight","_updateFocusMarkerPosition","_updateCropOverlayPosition","_toOriginalCoords","scaledX","scaledY","_toScaledCoords","originalX","originalY","marker","cursor","clipPath","addEventListener","e","rect","getBoundingClientRect","clientX","clientY","preventDefault","overlay","handles","handleType","handle","dataset","marginLeft","right","marginTop","bottom","push","currentTarget","Number","parseInt","stopPropagation","target","window","complete","body","_handleFocusMarkerDrag","_handleCropOverlayDrag","_handleCropOverlayResize","imageRect","targetScaledX","targetScaledY","original","setFocusPoint","deltaX","deltaY","newX","newY","setCropZone","newWidth","newHeight","includes","clampedX","Math","max","min","clampedY","scaled","container","computedStyle","getComputedStyle","paddingLeft","parseFloat","paddingTop","console","log","focusPaddingLeft","focusPaddingTop","clampedWidth","clampedHeight","scaledWidth","scaledHeight","cropPaddingLeft","cropPaddingTop","toggleFocusPoint","active","isActive","undefined","_notifyChange","toggleCropZone","defaultWidth","defaultHeight","defaultX","defaultY","getFocusPoint","getCropZone","getImageDimensions","destroy","removeEventListener","removeChild"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"visual-image-tool.umd.js","sources":["../src/visual-image-tool.js"],"sourcesContent":["/**\n * ImageTool - A lightweight tool for defining focus points and crop zones on images\n * @module visual-image-tool\n */\n\nclass VisualImageTool {\n\t/**\n\t * Creates an instance of the image tool.\n\t * The focus point is initialized to `{x:0, y:0}` in `this.state`. When `toggleFocusPoint(true)` is called for the first time, if the focus point is still `{x:0, y:0}`, it will be automatically centered on the image.\n\t * @param {Object} options - Configuration options\n\t * @param {HTMLElement|string} options.imageElement - Image element or CSS selector\n\t * @param {Object} [options.focusPoint] - Focus point configuration\n\t * @param {boolean} [options.focusPoint.enabled=true] - Enable focus point functionality\n\t * @param {Object} [options.focusPoint.style] - Custom styles for the focus point marker\n\t * @param {Object} [options.cropZone] - Crop zone configuration\n\t * @param {boolean} [options.cropZone.enabled=true] - Enable crop zone functionality\n\t * @param {Object} [options.cropZone.style] - Custom styles for the crop overlay\n\t * @param {Function} [options.onChange] - Callback called on changes\n\t */\n\tconstructor(options) {\n\t\t// Validate options\n\t\tif (!options || !options.imageElement) {\n\t\t\tthrow new Error(\"Image element is required\");\n\t\t}\n\n\t\t// Initialize properties\n\t\tthis.imageElement =\n\t\t\ttypeof options.imageElement === \"string\"\n\t\t\t\t? document.querySelector(options.imageElement)\n\t\t\t\t: options.imageElement;\n\n\t\tif (!this.imageElement || this.imageElement.tagName !== \"IMG\") {\n\t\t\tthrow new Error(\"Invalid image element\");\n\t\t}\n\n\t\t// Default options\n\t\tthis.options = {\n\t\t\tfocusPoint: {\n\t\t\t\tenabled: true,\n\t\t\t\tstyle: {\n\t\t\t\t\twidth: \"30px\",\n\t\t\t\t\theight: \"30px\",\n\t\t\t\t\tborder: \"3px solid white\",\n\t\t\t\t\tboxShadow: \"0 0 0 2px black, 0 0 5px rgba(0,0,0,0.5)\",\n\t\t\t\t\tbackgroundColor: \"rgba(255, 0, 0, 0.5)\",\n\t\t\t\t},\n\t\t\t\t...options.focusPoint,\n\t\t\t},\n\t\t\tcropZone: {\n\t\t\t\tenabled: true,\n\t\t\t\tstyle: {\n\t\t\t\t\tborder: \"1px dashed #fff\",\n\t\t\t\t\tbackgroundColor: \"rgba(0, 0, 0, 0.4)\",\n\t\t\t\t},\n\t\t\t\thandleStyle: {\n\t\t\t\t\twidth: \"14px\",\n\t\t\t\t\theight: \"14px\",\n\t\t\t\t\tbackgroundColor: \"white\",\n\t\t\t\t\tborder: \"2px solid black\",\n\t\t\t\t\tboxShadow: \"0 0 3px rgba(0,0,0,0.5)\",\n\t\t\t\t},\n\t\t\t\t...options.cropZone,\n\t\t\t},\n\t\t\tonChange: options.onChange || (() => {}),\n\t\t\tdebug: options.debug || false,\n\t\t};\n\n\t\t// Internal state\n\t\tthis.state = {\n\t\t\tfocusMarker: null,\n\t\t\tcropOverlay: null,\n\t\t\tfocusActive: false,\n\t\t\tcropActive: false,\n\t\t\tfocusPoint: { x: 0, y: 0 },\n\t\t\tcropZone: { x: 0, y: 0, width: 0, height: 0 },\n\t\t\toriginalWidth: 1,\n\t\t\toriginalHeight: 1,\n\t\t\tdisplayWidth: 1,\n\t\t\tdisplayHeight: 1,\n\t\t\tscaleX: 1,\n\t\t\tscaleY: 1,\n\t\t};\n\n\t\t// Handlers are bound once here: addEventListener and removeEventListener\n\t\t// must be handed the same function reference for removal to work.\n\t\tthis._boundUpdateScaling = this._updateScaling.bind(this);\n\t\tthis._boundHandleMouseUp = this._handleMouseUp.bind(this);\n\t\tthis._boundHandleMouseMove = this._handleMouseMove.bind(this);\n\t\tthis._boundFocusMarkerMouseDown =\n\t\t\tthis._handleFocusMarkerMouseDown.bind(this);\n\t\tthis._boundCropOverlayMouseDown =\n\t\t\tthis._handleCropOverlayMouseDown.bind(this);\n\t\tthis._boundCropHandleMouseDown = this._handleCropHandleMouseDown.bind(this);\n\n\t\t// Resize handles, tracked so their listeners can be removed on destroy\n\t\tthis.cropHandles = [];\n\n\t\t// Variables for tracking interactions\n\t\tthis.interaction = {\n\t\t\tfocusDragging: false,\n\t\t\tfocusDragOffsetX: 0,\n\t\t\tfocusDragOffsetY: 0,\n\t\t\tcropDragging: false,\n\t\t\tcropResizing: false,\n\t\t\tactiveHandle: null,\n\t\t\tstartX: 0,\n\t\t\tstartY: 0,\n\t\t\tstartWidth: 0,\n\t\t\tstartHeight: 0,\n\t\t\tstartMouseX: 0,\n\t\t\tstartMouseY: 0,\n\t\t};\n\n\t\tthis.spinnerElement = null;\n\t\tthis.initialLoadComplete = false;\n\n\t\t// Initialize the tool\n\t\tthis._init();\n\t}\n\n\t/**\n\t * Initializes the image tool\n\t * @private\n\t */\n\t_init() {\n\t\t// Prepare the parent container\n\t\tthis._prepareContainer();\n\n\t\t// Initialize dimensions\n\t\tthis._updateScaling();\n\n\t\t// Create UI elements if enabled\n\t\tif (this.options.focusPoint.enabled) {\n\t\t\tthis._createFocusMarker();\n\t\t}\n\n\t\tif (this.options.cropZone.enabled) {\n\t\t\tthis._createCropOverlay();\n\t\t}\n\n\t\t// Add event listeners\n\t\tthis._setupEventListeners();\n\t}\n\n\t/**\n\t * Prepares the image's parent container\n\t * @private\n\t */\n\t_prepareContainer() {\n\t\t// Ensure the parent is positioned\n\t\tif (this.imageElement.parentNode) {\n\t\t\tthis.imageElement.parentNode.style.position = \"relative\";\n\n\t\t\t// Create spinner element\n\t\t\tthis.spinnerElement = document.createElement(\"div\");\n\t\t\tthis.spinnerElement.style.position = \"absolute\";\n\t\t\tthis.spinnerElement.style.top = \"50%\";\n\t\t\tthis.spinnerElement.style.left = \"50%\";\n\t\t\tthis.spinnerElement.style.transform = \"translate(-50%, -50%)\";\n\t\t\tthis.spinnerElement.style.width = \"40px\";\n\t\t\tthis.spinnerElement.style.height = \"40px\";\n\t\t\tthis.spinnerElement.style.border = \"4px solid #f3f3f3\"; // Light grey\n\t\t\tthis.spinnerElement.style.borderTop = \"4px solid #3498db\"; // Blue\n\t\t\tthis.spinnerElement.style.borderRadius = \"50%\";\n\t\t\tthis.spinnerElement.style.boxSizing = \"border-box\";\n\t\t\tthis.spinnerElement.style.zIndex = \"1000\"; // Ensure it's above other elements\n\t\t\tthis.spinnerElement.style.display = \"block\"; // Initially visible\n\t\t\tthis.spinnerElement.style.animation =\n\t\t\t\t\"visual-image-tool-spin 1s linear infinite\";\n\n\t\t\tthis.imageElement.parentNode.appendChild(this.spinnerElement);\n\n\t\t\t// Add CSS keyframes for spinner animation.\n\t\t\t//\n\t\t\t// The animation name is namespaced: keyframes are global to the\n\t\t\t// document, so a bare name like `spin` would collide with one\n\t\t\t// defined by the host page, and whichever stylesheet was injected\n\t\t\t// last would silently win.\n\t\t\t//\n\t\t\t// This stylesheet is shared by every instance and deliberately\n\t\t\t// outlives destroy(): removing it would break the animation for\n\t\t\t// any instance still alive. The id guard keeps it to one element.\n\t\t\tif (!document.getElementById(\"visual-image-tool-spinner-styles\")) {\n\t\t\t\tconst styleElement = document.createElement(\"style\");\n\t\t\t\tstyleElement.id = \"visual-image-tool-spinner-styles\";\n\t\t\t\tstyleElement.innerHTML = `\n@keyframes visual-image-tool-spin {\n 0% { transform: translate(-50%, -50%) rotate(0deg); }\n 100% { transform: translate(-50%, -50%) rotate(360deg); }\n}\n `;\n\t\t\t\tdocument.head.appendChild(styleElement);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Updates scaling factors\n\t * @private\n\t */\n\t_updateScaling() {\n\t\tthis.state.originalWidth = this.imageElement.naturalWidth || 1;\n\t\tthis.state.originalHeight = this.imageElement.naturalHeight || 1;\n\n\t\t// Hide spinner once image is loaded and dimensions are available\n\t\tif (\n\t\t\t!this.initialLoadComplete &&\n\t\t\tthis.spinnerElement &&\n\t\t\tthis.state.originalWidth > 1 &&\n\t\t\tthis.state.originalHeight > 1\n\t\t) {\n\t\t\tthis.spinnerElement.style.display = \"none\";\n\t\t\tthis.initialLoadComplete = true;\n\t\t}\n\t\tthis.state.displayWidth = this.imageElement.offsetWidth;\n\t\tthis.state.displayHeight = this.imageElement.offsetHeight;\n\t\tthis.state.scaleX = this.state.displayWidth / this.state.originalWidth;\n\t\tthis.state.scaleY = this.state.displayHeight / this.state.originalHeight;\n\n\t\t// Update spinner position during initial load if still visible\n\t\tif (!this.initialLoadComplete && this.spinnerElement) {\n\t\t\tthis.spinnerElement.style.top = `${this.state.displayHeight / 2}px`;\n\t\t\tthis.spinnerElement.style.left = `${this.state.displayWidth / 2}px`;\n\t\t}\n\n\t\t// Reposition elements if active\n\t\tif (this.state.focusActive) {\n\t\t\tthis._updateFocusMarkerPosition();\n\t\t}\n\n\t\tif (this.state.cropActive) {\n\t\t\tthis._updateCropOverlayPosition();\n\t\t}\n\t}\n\n\t/**\n\t * Converts display coordinates to original coordinates\n\t * @private\n\t * @param {number} scaledX - X-coordinate at display scale\n\t * @param {number} scaledY - Y-coordinate at display scale\n\t * @returns {Object} Original coordinates {x, y}\n\t */\n\t_toOriginalCoords(scaledX, scaledY) {\n\t\treturn {\n\t\t\tx: scaledX / this.state.scaleX,\n\t\t\ty: scaledY / this.state.scaleY,\n\t\t};\n\t}\n\n\t/**\n\t * Converts original coordinates to display coordinates\n\t * @private\n\t * @param {number} originalX - Original X-coordinate\n\t * @param {number} originalY - Original Y-coordinate\n\t * @returns {Object} Display scale coordinates {x, y}\n\t */\n\t_toScaledCoords(originalX, originalY) {\n\t\treturn {\n\t\t\tx: originalX * this.state.scaleX,\n\t\t\ty: originalY * this.state.scaleY,\n\t\t};\n\t}\n\n\t/**\n\t * Creates the focus point marker\n\t * @private\n\t */\n\t_createFocusMarker() {\n\t\tif (this.state.focusMarker) return;\n\n\t\tconst marker = document.createElement(\"div\");\n\t\tmarker.style.position = \"absolute\";\n\t\tmarker.style.width = this.options.focusPoint.style.width;\n\t\tmarker.style.height = this.options.focusPoint.style.height;\n\t\tmarker.style.border = this.options.focusPoint.style.border;\n\t\tmarker.style.boxShadow = this.options.focusPoint.style.boxShadow;\n\t\tmarker.style.backgroundColor =\n\t\t\tthis.options.focusPoint.style.backgroundColor;\n\t\tmarker.style.cursor = \"move\";\n\t\tmarker.style.display = \"none\";\n\t\tmarker.style.zIndex = \"999\";\n\t\tmarker.style.clipPath =\n\t\t\t\"polygon(40% 0%, 60% 0%, 60% 40%, 100% 40%, 100% 60%, 60% 60%, 60% 100%, 40% 100%, 40% 60%, 0% 60%, 0% 40%, 40% 40%)\";\n\n\t\tthis.imageElement.parentNode.appendChild(marker);\n\t\tthis.state.focusMarker = marker;\n\n\t\t// Add event listeners to the marker\n\t\tmarker.addEventListener(\"mousedown\", this._boundFocusMarkerMouseDown);\n\t}\n\n\t/**\n\t * Handles mousedown event on the focus point marker\n\t * @private\n\t * @param {MouseEvent} e - Mousedown event\n\t */\n\t_handleFocusMarkerMouseDown(e) {\n\t\tthis.interaction.focusDragging = true;\n\t\tthis.state.focusMarker.style.cursor = \"grabbing\";\n\n\t\t// Calculate offset from marker center\n\t\tconst rect = this.state.focusMarker.getBoundingClientRect();\n\t\tthis.interaction.focusDragOffsetX =\n\t\t\te.clientX - (rect.left + rect.width / 2);\n\t\tthis.interaction.focusDragOffsetY =\n\t\t\te.clientY - (rect.top + rect.height / 2);\n\n\t\te.preventDefault();\n\t}\n\n\t/**\n\t * Creates the crop zone overlay\n\t * @private\n\t */\n\t_createCropOverlay() {\n\t\tif (this.state.cropOverlay) return;\n\n\t\tconst overlay = document.createElement(\"div\");\n\t\toverlay.style.position = \"absolute\";\n\t\toverlay.style.border = this.options.cropZone.style.border;\n\t\toverlay.style.backgroundColor = this.options.cropZone.style.backgroundColor;\n\t\toverlay.style.boxSizing = \"border-box\";\n\t\toverlay.style.cursor = \"move\";\n\t\toverlay.style.display = \"none\";\n\t\toverlay.style.zIndex = \"998\";\n\n\t\t// Add resize handles\n\t\tconst handles = [\"tl\", \"tm\", \"tr\", \"ml\", \"mr\", \"bl\", \"bm\", \"br\"];\n\t\tfor (const handleType of handles) {\n\t\t\tconst handle = document.createElement(\"div\");\n\t\t\thandle.style.position = \"absolute\";\n\t\t\thandle.style.width = this.options.cropZone.handleStyle.width;\n\t\t\thandle.style.height = this.options.cropZone.handleStyle.height;\n\t\t\thandle.style.backgroundColor =\n\t\t\t\tthis.options.cropZone.handleStyle.backgroundColor;\n\t\t\thandle.style.border = this.options.cropZone.handleStyle.border;\n\t\t\thandle.style.boxShadow = this.options.cropZone.handleStyle.boxShadow;\n\t\t\thandle.style.boxSizing = \"border-box\";\n\t\t\thandle.dataset.handle = handleType;\n\n\t\t\t// Position the handle\n\t\t\tswitch (handleType) {\n\t\t\t\tcase \"tl\": // Top-left\n\t\t\t\t\thandle.style.top = \"-7px\";\n\t\t\t\t\thandle.style.left = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"nwse-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"tm\": // Top-middle\n\t\t\t\t\thandle.style.top = \"-7px\";\n\t\t\t\t\thandle.style.left = \"50%\";\n\t\t\t\t\thandle.style.marginLeft = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"ns-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"tr\": // Top-right\n\t\t\t\t\thandle.style.top = \"-7px\";\n\t\t\t\t\thandle.style.right = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"nesw-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"ml\": // Middle-left\n\t\t\t\t\thandle.style.top = \"50%\";\n\t\t\t\t\thandle.style.left = \"-7px\";\n\t\t\t\t\thandle.style.marginTop = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"ew-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"mr\": // Middle-right\n\t\t\t\t\thandle.style.top = \"50%\";\n\t\t\t\t\thandle.style.right = \"-7px\";\n\t\t\t\t\thandle.style.marginTop = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"ew-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"bl\": // Bottom-left\n\t\t\t\t\thandle.style.bottom = \"-7px\";\n\t\t\t\t\thandle.style.left = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"nesw-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"bm\": // Bottom-middle\n\t\t\t\t\thandle.style.bottom = \"-7px\";\n\t\t\t\t\thandle.style.left = \"50%\";\n\t\t\t\t\thandle.style.marginLeft = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"ns-resize\";\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"br\": // Bottom-right\n\t\t\t\t\thandle.style.bottom = \"-7px\";\n\t\t\t\t\thandle.style.right = \"-7px\";\n\t\t\t\t\thandle.style.cursor = \"nwse-resize\";\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Add event listener\n\t\t\thandle.addEventListener(\"mousedown\", this._boundCropHandleMouseDown);\n\n\t\t\toverlay.appendChild(handle);\n\t\t\tthis.cropHandles.push(handle);\n\t\t}\n\n\t\t// Add listener for overlay dragging\n\t\toverlay.addEventListener(\"mousedown\", this._boundCropOverlayMouseDown);\n\n\t\tthis.imageElement.parentNode.appendChild(overlay);\n\t\tthis.state.cropOverlay = overlay;\n\t}\n\n\t/**\n\t * Handles mousedown event on a resize handle\n\t * @private\n\t * @param {MouseEvent} e - Mousedown event\n\t */\n\t_handleCropHandleMouseDown(e) {\n\t\tconst handleType = e.currentTarget.dataset.handle;\n\n\t\tthis.interaction.cropResizing = true;\n\t\tthis.interaction.activeHandle = handleType;\n\n\t\t// Record initial dimensions and position\n\t\tthis.interaction.startX =\n\t\t\tNumber.parseInt(this.state.cropOverlay.style.left, 10) || 0;\n\t\tthis.interaction.startY =\n\t\t\tNumber.parseInt(this.state.cropOverlay.style.top, 10) || 0;\n\t\tthis.interaction.startWidth = this.state.cropOverlay.offsetWidth;\n\t\tthis.interaction.startHeight = this.state.cropOverlay.offsetHeight;\n\t\tthis.interaction.startMouseX = e.clientX;\n\t\tthis.interaction.startMouseY = e.clientY;\n\n\t\te.preventDefault();\n\t\te.stopPropagation();\n\t}\n\n\t/**\n\t * Handles mousedown event on the crop overlay\n\t * @private\n\t * @param {MouseEvent} e - Mousedown event\n\t */\n\t_handleCropOverlayMouseDown(e) {\n\t\t// Ignore if clicking on a handle\n\t\tif (e.target !== this.state.cropOverlay) return;\n\n\t\tthis.interaction.cropDragging = true;\n\t\tthis.state.cropOverlay.style.cursor = \"grabbing\";\n\n\t\t// Record initial position\n\t\tthis.interaction.startX =\n\t\t\tNumber.parseInt(this.state.cropOverlay.style.left, 10) || 0;\n\t\tthis.interaction.startY =\n\t\t\tNumber.parseInt(this.state.cropOverlay.style.top, 10) || 0;\n\t\tthis.interaction.startMouseX = e.clientX;\n\t\tthis.interaction.startMouseY = e.clientY;\n\n\t\te.preventDefault();\n\t}\n\n\t/**\n\t * Sets up global event listeners\n\t * @private\n\t */\n\t_setupEventListeners() {\n\t\t// Listener for window resize\n\t\twindow.addEventListener(\"resize\", this._boundUpdateScaling);\n\n\t\t// Listener for image load\n\t\tif (!this.imageElement.complete) {\n\t\t\tthis.imageElement.addEventListener(\"load\", this._boundUpdateScaling);\n\t\t}\n\n\t\t// Listeners for mouse interactions\n\t\tdocument.addEventListener(\"mouseup\", this._boundHandleMouseUp);\n\t\tdocument.addEventListener(\"mousemove\", this._boundHandleMouseMove);\n\t}\n\n\t/**\n\t * Handles global mouseup event\n\t * @private\n\t */\n\t_handleMouseUp() {\n\t\tif (this.interaction.focusDragging) {\n\t\t\tthis.interaction.focusDragging = false;\n\t\t\tif (this.state.focusMarker) this.state.focusMarker.style.cursor = \"move\";\n\t\t}\n\n\t\tif (this.interaction.cropDragging) {\n\t\t\tthis.interaction.cropDragging = false;\n\t\t\tif (this.state.cropOverlay) this.state.cropOverlay.style.cursor = \"move\";\n\t\t}\n\n\t\tthis.interaction.cropResizing = false;\n\t\tthis.interaction.activeHandle = null;\n\t\tdocument.body.style.cursor = \"default\";\n\t}\n\n\t/**\n\t * Handles global mousemove event\n\t * @private\n\t * @param {MouseEvent} e - Mousemove event\n\t */\n\t_handleMouseMove(e) {\n\t\t// Handle focus point dragging\n\t\tif (this.interaction.focusDragging && this.state.focusMarker) {\n\t\t\tthis._handleFocusMarkerDrag(e);\n\t\t}\n\n\t\t// Handle crop zone dragging\n\t\tif (this.interaction.cropDragging) {\n\t\t\tthis._handleCropOverlayDrag(e);\n\t\t}\n\n\t\t// Handle crop zone resizing\n\t\tif (this.interaction.cropResizing) {\n\t\t\tthis._handleCropOverlayResize(e);\n\t\t}\n\t}\n\n\t/**\n\t * Handles focus point marker drag\n\t * @private\n\t * @param {MouseEvent} e - Mousemove event\n\t */\n\t_handleFocusMarkerDrag(e) {\n\t\tconst imageRect = this.imageElement.getBoundingClientRect();\n\n\t\t// Calculate target position relative to the image\n\t\tconst targetScaledX =\n\t\t\te.clientX - imageRect.left - this.interaction.focusDragOffsetX;\n\t\tconst targetScaledY =\n\t\t\te.clientY - imageRect.top - this.interaction.focusDragOffsetY;\n\n\t\t// Convert to original coordinates\n\t\tconst original = this._toOriginalCoords(targetScaledX, targetScaledY);\n\n\t\t// Update focus point\n\t\tthis.setFocusPoint(original.x, original.y);\n\t}\n\n\t/**\n\t * Handles crop overlay drag\n\t * @private\n\t * @param {MouseEvent} e - Mousemove event\n\t */\n\t_handleCropOverlayDrag(e) {\n\t\tconst deltaX = e.clientX - this.interaction.startMouseX;\n\t\tconst deltaY = e.clientY - this.interaction.startMouseY;\n\n\t\t// Calculate new position in display pixels\n\t\tconst newX = this.interaction.startX + deltaX;\n\t\tconst newY = this.interaction.startY + deltaY;\n\n\t\t// Convert to original coordinates\n\t\tconst original = this._toOriginalCoords(newX, newY);\n\n\t\t// Get current dimensions in original coordinates\n\t\tconst { width, height } = this.state.cropZone;\n\n\t\t// Update crop zone\n\t\tthis.setCropZone(original.x, original.y, width, height);\n\t}\n\n\t/**\n\t * Handles crop overlay resize\n\t * @private\n\t * @param {MouseEvent} e - Mousemove event\n\t */\n\t_handleCropOverlayResize(e) {\n\t\tif (!this.interaction.activeHandle) return;\n\n\t\tconst deltaX = e.clientX - this.interaction.startMouseX;\n\t\tconst deltaY = e.clientY - this.interaction.startMouseY;\n\n\t\tlet newX = this.interaction.startX;\n\t\tlet newY = this.interaction.startY;\n\t\tlet newWidth = this.interaction.startWidth;\n\t\tlet newHeight = this.interaction.startHeight;\n\n\t\t// Adjust based on active handle\n\t\tif (this.interaction.activeHandle.includes(\"t\")) {\n\t\t\t// Top\n\t\t\tnewY = this.interaction.startY + deltaY;\n\t\t\tnewHeight = this.interaction.startHeight - deltaY;\n\t\t}\n\t\tif (this.interaction.activeHandle.includes(\"b\")) {\n\t\t\t// Bottom\n\t\t\tnewHeight = this.interaction.startHeight + deltaY;\n\t\t}\n\t\tif (this.interaction.activeHandle.includes(\"l\")) {\n\t\t\t// Left\n\t\t\tnewX = this.interaction.startX + deltaX;\n\t\t\tnewWidth = this.interaction.startWidth - deltaX;\n\t\t}\n\t\tif (this.interaction.activeHandle.includes(\"r\")) {\n\t\t\t// Right\n\t\t\tnewWidth = this.interaction.startWidth + deltaX;\n\t\t}\n\n\t\t// Prevent negative dimensions\n\t\tif (newWidth < 10) {\n\t\t\tif (this.interaction.activeHandle.includes(\"l\")) {\n\t\t\t\tnewX = this.interaction.startX + this.interaction.startWidth - 10;\n\t\t\t}\n\t\t\tnewWidth = 10;\n\t\t}\n\t\tif (newHeight < 10) {\n\t\t\tif (this.interaction.activeHandle.includes(\"t\")) {\n\t\t\t\tnewY = this.interaction.startY + this.interaction.startHeight - 10;\n\t\t\t}\n\t\t\tnewHeight = 10;\n\t\t}\n\n\t\t// Convert display coordinates to original coordinates\n\t\tconst originalX = newX / this.state.scaleX;\n\t\tconst originalY = newY / this.state.scaleY;\n\t\tconst originalWidth = newWidth / this.state.scaleX;\n\t\tconst originalHeight = newHeight / this.state.scaleY;\n\n\t\t// Update crop zone\n\t\tthis.setCropZone(originalX, originalY, originalWidth, originalHeight);\n\t}\n\n\t/**\n\t * Updates the focus point marker position\n\t * @private\n\t */\n\t_updateFocusMarkerPosition() {\n\t\tif (!this.state.focusMarker) return;\n\n\t\tconst { x, y } = this.state.focusPoint;\n\n\t\t// Clamp coordinates to image dimensions\n\t\tconst clampedX = Math.max(0, Math.min(x, this.state.originalWidth));\n\t\tconst clampedY = Math.max(0, Math.min(y, this.state.originalHeight));\n\n\t\t// Update state if coordinates were clamped\n\t\tif (x !== clampedX || y !== clampedY) {\n\t\t\tthis.state.focusPoint = { x: clampedX, y: clampedY };\n\t\t}\n\n\t\tconst scaled = this._toScaledCoords(clampedX, clampedY);\n\n\t\t// LOGGING: Validate padding offset\n\t\tif (this.options.debug) {\n\t\t\tconst container = this.imageElement.parentNode;\n\t\t\tconst computedStyle = window.getComputedStyle(container);\n\t\t\tconst paddingLeft = Number.parseFloat(computedStyle.paddingLeft);\n\t\t\tconst paddingTop = Number.parseFloat(computedStyle.paddingTop);\n\t\t\tconsole.log(\n\t\t\t\t\"[FocusMarker] scaled:\",\n\t\t\t\tscaled,\n\t\t\t\t\"paddingLeft:\",\n\t\t\t\tpaddingLeft,\n\t\t\t\t\"paddingTop:\",\n\t\t\t\tpaddingTop,\n\t\t\t\t\"containerRect:\",\n\t\t\t\tcontainer.getBoundingClientRect(),\n\t\t\t);\n\t\t}\n\n\t\t// Adjust to center the marker\n\t\t// Adjust for container padding (use unique variable names)\n\t\tlet focusPaddingLeft = 0;\n\t\tlet focusPaddingTop = 0;\n\t\t{\n\t\t\tconst container = this.imageElement.parentNode;\n\t\t\tconst computedStyle = window.getComputedStyle(container);\n\t\t\tfocusPaddingLeft = Number.parseFloat(computedStyle.paddingLeft);\n\t\t\tfocusPaddingTop = Number.parseFloat(computedStyle.paddingTop);\n\t\t}\n\n\t\tthis.state.focusMarker.style.left = `${\n\t\t\tscaled.x + focusPaddingLeft - this.state.focusMarker.offsetWidth / 2\n\t\t}px`;\n\t\tthis.state.focusMarker.style.top = `${\n\t\t\tscaled.y + focusPaddingTop - this.state.focusMarker.offsetHeight / 2\n\t\t}px`;\n\t\tif (this.options.debug) {\n\t\t\tconsole.log(\n\t\t\t\t\"[FocusMarker] style.left:\",\n\t\t\t\tthis.state.focusMarker.style.left,\n\t\t\t\t\"style.top:\",\n\t\t\t\tthis.state.focusMarker.style.top,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Updates the crop overlay position and dimensions\n\t * @private\n\t */\n\t_updateCropOverlayPosition() {\n\t\tif (!this.state.cropOverlay) return;\n\n\t\tconst { x, y, width, height } = this.state.cropZone;\n\n\t\t// Clamp to image dimensions\n\t\tconst clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));\n\t\tconst clampedY = Math.max(\n\t\t\t0,\n\t\t\tMath.min(y, this.state.originalHeight - height),\n\t\t);\n\t\tconst clampedWidth = Math.max(\n\t\t\t10,\n\t\t\tMath.min(width, this.state.originalWidth - clampedX),\n\t\t);\n\t\tconst clampedHeight = Math.max(\n\t\t\t10,\n\t\t\tMath.min(height, this.state.originalHeight - clampedY),\n\t\t);\n\n\t\t// Update state if values were clamped\n\t\tif (\n\t\t\tx !== clampedX ||\n\t\t\ty !== clampedY ||\n\t\t\twidth !== clampedWidth ||\n\t\t\theight !== clampedHeight\n\t\t) {\n\t\t\tthis.state.cropZone = {\n\t\t\t\tx: clampedX,\n\t\t\t\ty: clampedY,\n\t\t\t\twidth: clampedWidth,\n\t\t\t\theight: clampedHeight,\n\t\t\t};\n\t\t}\n\n\t\t// Convert to display coordinates\n\t\tconst scaled = this._toScaledCoords(clampedX, clampedY);\n\t\tconst scaledWidth = clampedWidth * this.state.scaleX;\n\t\tconst scaledHeight = clampedHeight * this.state.scaleY;\n\n\t\t// LOGGING: Validate padding offset\n\t\tif (this.options.debug) {\n\t\t\tconst container = this.imageElement.parentNode;\n\t\t\tconst computedStyle = window.getComputedStyle(container);\n\t\t\tconst paddingLeft = Number.parseFloat(computedStyle.paddingLeft);\n\t\t\tconst paddingTop = Number.parseFloat(computedStyle.paddingTop);\n\t\t\tconsole.log(\n\t\t\t\t\"[CropOverlay] scaled:\",\n\t\t\t\tscaled,\n\t\t\t\t\"paddingLeft:\",\n\t\t\t\tpaddingLeft,\n\t\t\t\t\"paddingTop:\",\n\t\t\t\tpaddingTop,\n\t\t\t\t\"containerRect:\",\n\t\t\t\tcontainer.getBoundingClientRect(),\n\t\t\t);\n\t\t}\n\n\t\t// Update overlay\n\t\t// Adjust for container padding (use unique variable names)\n\t\tlet cropPaddingLeft = 0;\n\t\tlet cropPaddingTop = 0;\n\t\t{\n\t\t\tconst container = this.imageElement.parentNode;\n\t\t\tconst computedStyle = window.getComputedStyle(container);\n\t\t\tcropPaddingLeft = Number.parseFloat(computedStyle.paddingLeft);\n\t\t\tcropPaddingTop = Number.parseFloat(computedStyle.paddingTop);\n\t\t}\n\n\t\tthis.state.cropOverlay.style.left = `${scaled.x + cropPaddingLeft}px`;\n\t\tthis.state.cropOverlay.style.top = `${scaled.y + cropPaddingTop}px`;\n\t\tthis.state.cropOverlay.style.width = `${scaledWidth}px`;\n\t\tthis.state.cropOverlay.style.height = `${scaledHeight}px`;\n\t\tif (this.options.debug) {\n\t\t\tconsole.log(\n\t\t\t\t\"[CropOverlay] style.left:\",\n\t\t\t\tthis.state.cropOverlay.style.left,\n\t\t\t\t\"style.top:\",\n\t\t\t\tthis.state.cropOverlay.style.top,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Enables or disables the focus point.\n\t * If the focus point is at its initial state `{x:0, y:0}` (top-left corner), it will be moved to the center of the image when this method is called with `active = true` for the first time.\n\t * @public\n\t * @param {boolean} active - Activation state\n\t * @returns {VisualImageTool} Instance for chaining\n\t */\n\ttoggleFocusPoint(active) {\n\t\tif (!this.options.focusPoint.enabled) return this;\n\n\t\tconst isActive = active === undefined ? !this.state.focusActive : active;\n\n\t\tif (isActive && !this.state.focusActive) {\n\t\t\t// Enable focus point\n\t\t\tif (!this.state.focusMarker) {\n\t\t\t\tthis._createFocusMarker();\n\t\t\t}\n\n\t\t\t// Position at image center by default if not already set\n\t\t\tif (this.state.focusPoint.x === 0 && this.state.focusPoint.y === 0) {\n\t\t\t\tthis.state.focusPoint = {\n\t\t\t\t\tx: this.state.originalWidth / 2,\n\t\t\t\t\ty: this.state.originalHeight / 2,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tthis._updateFocusMarkerPosition();\n\t\t\tthis.state.focusMarker.style.display = \"block\";\n\t\t\tthis.state.focusActive = true;\n\t\t} else if (!isActive && this.state.focusActive) {\n\t\t\t// Disable focus point\n\t\t\tif (this.state.focusMarker) {\n\t\t\t\tthis.state.focusMarker.style.display = \"none\";\n\t\t\t}\n\t\t\tthis.state.focusActive = false;\n\t\t}\n\n\t\t// Notify change\n\t\tthis._notifyChange();\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Enables or disables the crop zone\n\t * @public\n\t * @param {boolean} active - Activation state\n\t * @returns {VisualImageTool} Instance for chaining\n\t */\n\ttoggleCropZone(active) {\n\t\tif (!this.options.cropZone.enabled) return this;\n\n\t\tconst isActive = active === undefined ? !this.state.cropActive : active;\n\n\t\tif (isActive && !this.state.cropActive) {\n\t\t\t// Enable crop zone\n\t\t\tif (!this.state.cropOverlay) {\n\t\t\t\tthis._createCropOverlay();\n\t\t\t}\n\n\t\t\t// Define a default zone if not already set\n\t\t\tif (this.state.cropZone.width === 0 || this.state.cropZone.height === 0) {\n\t\t\t\tconst defaultWidth = this.state.originalWidth / 2;\n\t\t\t\tconst defaultHeight = this.state.originalHeight / 2;\n\t\t\t\tconst defaultX = (this.state.originalWidth - defaultWidth) / 2;\n\t\t\t\tconst defaultY = (this.state.originalHeight - defaultHeight) / 2;\n\n\t\t\t\tthis.state.cropZone = {\n\t\t\t\t\tx: defaultX,\n\t\t\t\t\ty: defaultY,\n\t\t\t\t\twidth: defaultWidth,\n\t\t\t\t\theight: defaultHeight,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tthis._updateCropOverlayPosition();\n\t\t\tthis.state.cropOverlay.style.display = \"block\";\n\t\t\tthis.state.cropActive = true;\n\t\t} else if (!isActive && this.state.cropActive) {\n\t\t\t// Disable crop zone\n\t\t\tif (this.state.cropOverlay) {\n\t\t\t\tthis.state.cropOverlay.style.display = \"none\";\n\t\t\t}\n\t\t\tthis.state.cropActive = false;\n\t\t}\n\n\t\t// Notify change\n\t\tthis._notifyChange();\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Sets the focus point position\n\t * @public\n\t * @param {number} x - X-coordinate in original pixels\n\t * @param {number} y - Y-coordinate in original pixels\n\t * @returns {VisualImageTool} Instance for chaining\n\t */\n\tsetFocusPoint(x, y) {\n\t\t// Clamp coordinates to image dimensions\n\t\tconst clampedX = Math.max(0, Math.min(x, this.state.originalWidth));\n\t\tconst clampedY = Math.max(0, Math.min(y, this.state.originalHeight));\n\n\t\tthis.state.focusPoint = { x: clampedX, y: clampedY };\n\n\t\tif (this.state.focusActive && this.state.focusMarker) {\n\t\t\tthis._updateFocusMarkerPosition();\n\t\t}\n\n\t\t// Notify change\n\t\tthis._notifyChange();\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Sets the crop zone position and dimensions\n\t * @public\n\t * @param {number} x - X-coordinate in original pixels\n\t * @param {number} y - Y-coordinate in original pixels\n\t * @param {number} width - Width in original pixels\n\t * @param {number} height - Height in original pixels\n\t * @returns {VisualImageTool} Instance for chaining\n\t */\n\tsetCropZone(x, y, width, height) {\n\t\t// Clamp to image dimensions\n\t\tconst clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));\n\t\tconst clampedY = Math.max(\n\t\t\t0,\n\t\t\tMath.min(y, this.state.originalHeight - height),\n\t\t);\n\t\tconst clampedWidth = Math.max(\n\t\t\t10,\n\t\t\tMath.min(width, this.state.originalWidth - clampedX),\n\t\t);\n\t\tconst clampedHeight = Math.max(\n\t\t\t10,\n\t\t\tMath.min(height, this.state.originalHeight - clampedY),\n\t\t);\n\n\t\tthis.state.cropZone = {\n\t\t\tx: clampedX,\n\t\t\ty: clampedY,\n\t\t\twidth: clampedWidth,\n\t\t\theight: clampedHeight,\n\t\t};\n\n\t\tif (this.state.cropActive && this.state.cropOverlay) {\n\t\t\tthis._updateCropOverlayPosition();\n\t\t}\n\n\t\t// Notify change\n\t\tthis._notifyChange();\n\n\t\treturn this;\n\t}\n\n\t/**\n\t * Gets the current focus point position\n\t * @public\n\t * @returns {Object} Focus point coordinates {x, y}\n\t */\n\tgetFocusPoint() {\n\t\treturn { ...this.state.focusPoint };\n\t}\n\n\t/**\n\t * Gets the current crop zone position and dimensions\n\t * @public\n\t * @returns {Object} Crop zone {x, y, width, height}\n\t */\n\tgetCropZone() {\n\t\treturn { ...this.state.cropZone };\n\t}\n\n\t/**\n\t * Gets the original image dimensions\n\t * @public\n\t * @returns {Object} Dimensions {width, height}\n\t */\n\tgetImageDimensions() {\n\t\treturn {\n\t\t\twidth: this.state.originalWidth,\n\t\t\theight: this.state.originalHeight,\n\t\t};\n\t}\n\n\t/**\n\t * Notifies changes via callback\n\t * @private\n\t */\n\t_notifyChange() {\n\t\tif (typeof this.options.onChange === \"function\") {\n\t\t\tthis.options.onChange({\n\t\t\t\tfocusPoint: this.getFocusPoint(),\n\t\t\t\tcropZone: this.getCropZone(),\n\t\t\t\tfocusActive: this.state.focusActive,\n\t\t\t\tcropActive: this.state.cropActive,\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Destroys the instance and cleans up resources\n\t * @public\n\t */\n\tdestroy() {\n\t\t// Already destroyed\n\t\tif (!this.state) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Remove element listeners\n\t\tif (this.state.focusMarker) {\n\t\t\tthis.state.focusMarker.removeEventListener(\n\t\t\t\t\"mousedown\",\n\t\t\t\tthis._boundFocusMarkerMouseDown,\n\t\t\t);\n\t\t}\n\n\t\tif (this.state.cropOverlay) {\n\t\t\tthis.state.cropOverlay.removeEventListener(\n\t\t\t\t\"mousedown\",\n\t\t\t\tthis._boundCropOverlayMouseDown,\n\t\t\t);\n\t\t}\n\n\t\tfor (const handle of this.cropHandles) {\n\t\t\thandle.removeEventListener(\"mousedown\", this._boundCropHandleMouseDown);\n\t\t}\n\t\tthis.cropHandles = [];\n\n\t\t// Remove DOM elements\n\t\tif (this.state.focusMarker?.parentNode) {\n\t\t\tthis.state.focusMarker.parentNode.removeChild(this.state.focusMarker);\n\t\t}\n\n\t\tif (this.state.cropOverlay?.parentNode) {\n\t\t\tthis.state.cropOverlay.parentNode.removeChild(this.state.cropOverlay);\n\t\t}\n\n\t\tif (this.spinnerElement?.parentNode) {\n\t\t\tthis.spinnerElement.parentNode.removeChild(this.spinnerElement);\n\t\t}\n\t\tthis.spinnerElement = null;\n\n\t\t// Remove global listeners\n\t\twindow.removeEventListener(\"resize\", this._boundUpdateScaling);\n\t\tdocument.removeEventListener(\"mouseup\", this._boundHandleMouseUp);\n\t\tdocument.removeEventListener(\"mousemove\", this._boundHandleMouseMove);\n\n\t\tif (this.imageElement) {\n\t\t\tthis.imageElement.removeEventListener(\"load\", this._boundUpdateScaling);\n\t\t}\n\n\t\t// Release bound handlers\n\t\tthis._boundUpdateScaling = null;\n\t\tthis._boundHandleMouseUp = null;\n\t\tthis._boundHandleMouseMove = null;\n\t\tthis._boundFocusMarkerMouseDown = null;\n\t\tthis._boundCropOverlayMouseDown = null;\n\t\tthis._boundCropHandleMouseDown = null;\n\n\t\t// Reset state\n\t\tthis.state = null;\n\t\tthis.interaction = null;\n\t\tthis.options = null;\n\t\tthis.imageElement = null;\n\t}\n}\n\n// Export class\nexport default VisualImageTool;\n"],"names":["constructor","options","imageElement","Error","this","document","querySelector","tagName","focusPoint","enabled","style","width","height","border","boxShadow","backgroundColor","cropZone","handleStyle","onChange","debug","state","focusMarker","cropOverlay","focusActive","cropActive","x","y","originalWidth","originalHeight","displayWidth","displayHeight","scaleX","scaleY","_boundUpdateScaling","_updateScaling","bind","_boundHandleMouseUp","_handleMouseUp","_boundHandleMouseMove","_handleMouseMove","_boundFocusMarkerMouseDown","_handleFocusMarkerMouseDown","_boundCropOverlayMouseDown","_handleCropOverlayMouseDown","_boundCropHandleMouseDown","_handleCropHandleMouseDown","cropHandles","interaction","focusDragging","focusDragOffsetX","focusDragOffsetY","cropDragging","cropResizing","activeHandle","startX","startY","startWidth","startHeight","startMouseX","startMouseY","spinnerElement","initialLoadComplete","_init","_prepareContainer","_createFocusMarker","_createCropOverlay","_setupEventListeners","parentNode","position","createElement","top","left","transform","borderTop","borderRadius","boxSizing","zIndex","display","animation","appendChild","getElementById","styleElement","id","innerHTML","head","naturalWidth","naturalHeight","offsetWidth","offsetHeight","_updateFocusMarkerPosition","_updateCropOverlayPosition","_toOriginalCoords","scaledX","scaledY","_toScaledCoords","originalX","originalY","marker","cursor","clipPath","addEventListener","e","rect","getBoundingClientRect","clientX","clientY","preventDefault","overlay","handles","handleType","handle","dataset","marginLeft","right","marginTop","bottom","push","currentTarget","Number","parseInt","stopPropagation","target","window","complete","body","_handleFocusMarkerDrag","_handleCropOverlayDrag","_handleCropOverlayResize","imageRect","targetScaledX","targetScaledY","original","setFocusPoint","deltaX","deltaY","newX","newY","setCropZone","newWidth","newHeight","includes","clampedX","Math","max","min","clampedY","scaled","container","computedStyle","getComputedStyle","paddingLeft","parseFloat","paddingTop","console","log","focusPaddingLeft","focusPaddingTop","clampedWidth","clampedHeight","scaledWidth","scaledHeight","cropPaddingLeft","cropPaddingTop","toggleFocusPoint","active","isActive","undefined","_notifyChange","toggleCropZone","defaultWidth","defaultHeight","defaultX","defaultY","getFocusPoint","getCropZone","getImageDimensions","destroy","removeEventListener","removeChild"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h4md1/visual-image-tool",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.5",
|
|
4
|
+
"description": "A lightweight vanilla JavaScript tool to define focus points and crop zones on images",
|
|
5
5
|
"main": "dist/visual-image-tool.cjs",
|
|
6
6
|
"module": "dist/visual-image-tool.esm.js",
|
|
7
7
|
"browser": "dist/visual-image-tool.umd.js",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"bugs": {
|
|
63
63
|
"url": "https://github.com/killerwolf/visual-image-tool/issues"
|
|
64
64
|
},
|
|
65
|
-
"homepage": "https://
|
|
65
|
+
"homepage": "https://h4md1.fr/visual-image-tool/"
|
|
66
66
|
}
|
package/src/visual-image-tool.js
CHANGED
|
@@ -165,16 +165,26 @@ class VisualImageTool {
|
|
|
165
165
|
this.spinnerElement.style.boxSizing = "border-box";
|
|
166
166
|
this.spinnerElement.style.zIndex = "1000"; // Ensure it's above other elements
|
|
167
167
|
this.spinnerElement.style.display = "block"; // Initially visible
|
|
168
|
-
this.spinnerElement.style.animation =
|
|
168
|
+
this.spinnerElement.style.animation =
|
|
169
|
+
"visual-image-tool-spin 1s linear infinite";
|
|
169
170
|
|
|
170
171
|
this.imageElement.parentNode.appendChild(this.spinnerElement);
|
|
171
172
|
|
|
172
|
-
// Add CSS keyframes for spinner animation
|
|
173
|
+
// Add CSS keyframes for spinner animation.
|
|
174
|
+
//
|
|
175
|
+
// The animation name is namespaced: keyframes are global to the
|
|
176
|
+
// document, so a bare name like `spin` would collide with one
|
|
177
|
+
// defined by the host page, and whichever stylesheet was injected
|
|
178
|
+
// last would silently win.
|
|
179
|
+
//
|
|
180
|
+
// This stylesheet is shared by every instance and deliberately
|
|
181
|
+
// outlives destroy(): removing it would break the animation for
|
|
182
|
+
// any instance still alive. The id guard keeps it to one element.
|
|
173
183
|
if (!document.getElementById("visual-image-tool-spinner-styles")) {
|
|
174
184
|
const styleElement = document.createElement("style");
|
|
175
185
|
styleElement.id = "visual-image-tool-spinner-styles";
|
|
176
186
|
styleElement.innerHTML = `
|
|
177
|
-
@keyframes spin {
|
|
187
|
+
@keyframes visual-image-tool-spin {
|
|
178
188
|
0% { transform: translate(-50%, -50%) rotate(0deg); }
|
|
179
189
|
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
|
180
190
|
}
|
|
@@ -138,6 +138,58 @@ describe("VisualImageTool", () => {
|
|
|
138
138
|
instance = null;
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
+
it("should namespace the spinner keyframes to avoid colliding with the host page", () => {
|
|
142
|
+
const styleElement = document.getElementById(
|
|
143
|
+
"visual-image-tool-spinner-styles",
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(styleElement).not.toBeNull();
|
|
147
|
+
|
|
148
|
+
// A bare `spin` would collide with an animation of the same name on the
|
|
149
|
+
// consuming page, and the later stylesheet would silently win.
|
|
150
|
+
expect(styleElement.innerHTML).toContain(
|
|
151
|
+
"@keyframes visual-image-tool-spin",
|
|
152
|
+
);
|
|
153
|
+
expect(styleElement.innerHTML).not.toMatch(/@keyframes\s+spin\b/);
|
|
154
|
+
expect(instance.spinnerElement.style.animation).toContain(
|
|
155
|
+
"visual-image-tool-spin",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should inject the spinner stylesheet only once across instances", () => {
|
|
160
|
+
const second = new VisualImageTool({ imageElement });
|
|
161
|
+
|
|
162
|
+
expect(
|
|
163
|
+
document.querySelectorAll("#visual-image-tool-spinner-styles"),
|
|
164
|
+
).toHaveLength(1);
|
|
165
|
+
|
|
166
|
+
second.destroy();
|
|
167
|
+
|
|
168
|
+
// The stylesheet is shared, so it must survive one instance being torn
|
|
169
|
+
// down while another is still alive.
|
|
170
|
+
expect(
|
|
171
|
+
document.getElementById("visual-image-tool-spinner-styles"),
|
|
172
|
+
).not.toBeNull();
|
|
173
|
+
expect(instance.spinnerElement.style.animation).toContain(
|
|
174
|
+
"visual-image-tool-spin",
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should hide the spinner once the image dimensions are known", () => {
|
|
179
|
+
expect(instance.spinnerElement.style.display).toBe("none");
|
|
180
|
+
expect(instance.initialLoadComplete).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should remove the spinner from the DOM on destroy", () => {
|
|
184
|
+
const spinner = instance.spinnerElement;
|
|
185
|
+
expect(container.contains(spinner)).toBe(true);
|
|
186
|
+
|
|
187
|
+
instance.destroy();
|
|
188
|
+
instance = null;
|
|
189
|
+
|
|
190
|
+
expect(container.contains(spinner)).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
141
193
|
it("should resize the crop zone from the handle that was grabbed", () => {
|
|
142
194
|
instance.toggleCropZone(true);
|
|
143
195
|
const handle = instance.cropHandles.find((h) => h.dataset.handle === "br");
|