@bensitu/image-editor 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ben Situ
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # ImageEditor
2
+ [![npm](https://img.shields.io/npm/l/image-editor.svg)](https://github.com/bensitu/image-editor)
3
+
4
+ A lightweight JavaScript wrapper around fabric.js that provides comprehensive image editing capabilities including loading, zooming, rotation, and mask management.
5
+
6
+ ## Overview
7
+
8
+ ImageEditor offers:
9
+
10
+ - Image loading from base64 or file input
11
+ - Zoom in/out and reset scale functionality
12
+ - Rotation (with custom degrees or step-based)
13
+ - Mask creation, selection, and removal
14
+ - Optional mask labels
15
+ - Merge/export helpers
16
+ - Basic UI element binding
17
+
18
+ **Note**: This library requires fabric.js v5.x to be loaded before instantiating the editor.
19
+
20
+ ## Features
21
+
22
+ - **Fabric.js-powered canvas** - Built on top of the robust fabric.js library
23
+ - **Image scaling** - Configurable min/max limits with smooth animation
24
+ - **Image rotation** - Step control and animated transitions
25
+ - **Auto-resizing** - Optional canvas resizing to match image or container size
26
+ - **Mask management** - Add, remove, remove all, with draggable/resizable masks
27
+ - **Mask labels** - Auto-sync with mask movement/scaling
28
+ - **Performance optimization** - Downsampling on load to prevent large image performance issues
29
+ - **Export & Download** - Base64 output or direct file save support
30
+ - **DOM/UI binding** - Easy integration with buttons, inputs, and placeholders
31
+
32
+ ## Installation
33
+
34
+ Include fabric.js and the ImageEditor class script in your HTML:
35
+
36
+ ```html
37
+ <!-- Fabric.js (required) -->
38
+ <script src="https://cdn.jsdelivr.net/npm/fabric@5.5.2/dist/fabric.min.js"></script>
39
+
40
+ <!-- ImageEditor -->
41
+ <script src="path/to/ImageEditor.js"></script>
42
+ <!-- or -->
43
+ <script src="https://cdn.jsdelivr.net/npm/@bensitu/image-editor/dist/image-editor.min.js"></script>
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ ### HTML Structure
49
+
50
+ ```html
51
+ <!-- Canvas -->
52
+ <canvas id="fabricCanvas"></canvas>
53
+
54
+ <!-- Optional Controls -->
55
+ <button id="zoomInBtn">Zoom In</button>
56
+ <button id="zoomOutBtn">Zoom Out</button>
57
+ <button id="rotateLeftBtn">Rotate Left</button>
58
+ <input id="rotationLeftInput" type="number" value="90">
59
+ <button id="rotateRightBtn">Rotate Right</button>
60
+ <input id="rotationRightInput" type="number" value="90">
61
+
62
+ <button id="addMaskBtn">Add Mask</button>
63
+ <button id="removeMaskBtn">Remove Mask</button>
64
+
65
+ <button id="mergeBtn">Merge</button>
66
+ <button id="downloadBtn">Download</button>
67
+
68
+ <input id="imageInput" type="file" accept="image/*">
69
+ ```
70
+
71
+ ### JavaScript Implementation
72
+
73
+ ```javascript
74
+ // Create instance
75
+ const editor = new ImageEditor({
76
+ canvasWidth: 800,
77
+ canvasHeight: 600,
78
+ backgroundColor: '#ffffff',
79
+ initialImageBase64: null // optional
80
+ });
81
+
82
+ // Initialize (binds to DOM elements)
83
+ editor.init({
84
+ canvas: 'fabricCanvas',
85
+ zoomInBtn: 'zoomInBtn',
86
+ zoomOutBtn: 'zoomOutBtn',
87
+ rotateLeftBtn: 'rotateLeftBtn',
88
+ rotationLeftInput: 'rotationLeftInput',
89
+ rotateRightBtn: 'rotateRightBtn',
90
+ rotationRightInput: 'rotationRightInput',
91
+ addMaskBtn: 'addMaskBtn',
92
+ mergeBtn: 'mergeBtn',
93
+ downloadBtn: 'downloadBtn',
94
+ imageInput: 'imageInput'
95
+ });
96
+
97
+ // Load an image manually (base64 string)
98
+ // editor.loadImage('data:image/jpeg;base64,...');
99
+ ```
100
+
101
+ ## Configuration Options
102
+
103
+ When creating the editor instance, you can pass an options object to override defaults:
104
+
105
+ | Option | Default | Description |
106
+ |--------|---------|-------------|
107
+ | `canvasWidth` | `800` | Initial canvas width (px) |
108
+ | `canvasHeight` | `600` | Initial canvas height (px) |
109
+ | `backgroundColor` | `#ffffff` | Canvas background color |
110
+ | `animationDuration` | `300` | Animation duration for scale/rotation (ms) |
111
+ | `minScale` | `0.1` | Minimum scale factor |
112
+ | `maxScale` | `5.0` | Maximum scale factor |
113
+ | `scaleStep` | `0.05` | Scale step for zoom buttons |
114
+ | `rotationStep` | `90` | Default rotation step in degrees |
115
+ | `expandCanvasToImage` | `true` | Expand canvas to image size on load |
116
+ | `fitImageToCanvas` | `false` | Fit image to current canvas size |
117
+ | `downsampleOnLoad` | `true` | Downsample large images before rendering |
118
+ | `downsampleMaxWidth` | `4000` | Max width count before downsampling |
119
+ | `downsampleMaxHeight` | `3000` | Max height count before downsampling |
120
+ | `downsampleQuality` | `0.92` | JPEG quality when downsampling |
121
+ | `exportMultiplier` | `1` | Scale factor for export |
122
+ | `exportImageAreaByDefault` | `true` | Export only the image area (clipped to masks) |
123
+ | `defaultMaskWidth` | `50` | Default mask width (px) |
124
+ | `defaultMaskHeight` | `80` | Default mask height (px) |
125
+ | `maskRotatable` | `false` | Whether masks can be rotated |
126
+ | `maskLabelOnSelect` | `true` | Show label when mask is selected |
127
+ | `maskLabelOffset` | `3` | Offset for mask labels from top-left corner |
128
+ | `maskName` | `mask` | Prefix for mask names/labels |
129
+ | `showPlaceholder` | `true` | Shows placeholder when no image is loaded |
130
+ | `initialImageBase64` | `null` | Base64 string to auto-load as initial image |
131
+ | `defaultDownloadFileName` | `edited_image.jpg` | Default file name for downloads |
132
+
133
+ ## API Methods
134
+
135
+ | Method | Description |
136
+ |--------|-------------|
137
+ | `init(idMap)` | Bind the editor to DOM elements. Pass IDs in an object (optional). |
138
+ | `loadImage(base64)` | Load an image from a base64 data string. |
139
+ | `scaleImage(factor)` | Scale image to the given factor (relative to base scale). |
140
+ | `rotateImage(degrees)` | Rotate image to the given angle in degrees. |
141
+ | `reset()` | Reset scale to 1 and rotation to 0. |
142
+ | `undo()` | Undo the last state change. |
143
+ | `redo()` | Redo the next state change. |
144
+ | `addMask(config)` | Add a mask to the canvas. Config can include width, height, color. |
145
+ | `removeSelectedMask()` | Remove the currently selected mask. |
146
+ | `removeAllMasks()` | Remove all masks from the canvas. |
147
+ | `merge()` | Merge masks with the base image in the canvas. |
148
+ | `downloadImage()` | Download the merged image as a file. |
149
+ | `exportImageFile(options)` | Exports the current canvas (with or without masks) as a `File` object. |
150
+
151
+ ## Example Workflow
152
+
153
+ 1. **Load an image** - Via file input or base64 string
154
+ 2. **Adjust positioning** - Zoom in/out or rotate as needed
155
+ 3. **Add masks** - Highlight or cover specific areas (drag, resize)
156
+ 4. **Merge result** - Create a flattened export (optional)
157
+ 5. **Download / export** - Save the final image
158
+
159
+ ## Installing
160
+
161
+ ### npm / pnpm / yarn
162
+ ```bash
163
+ npm i image-editor fabric
164
+ # or
165
+ pnpm add image-editor fabric
166
+ # or
167
+ yarn add image-editor fabric
168
+ ```
169
+
170
+ ### Local build
171
+ ```bash
172
+ npm run build
173
+ ```
174
+
175
+ ### Load UMD js file:
176
+ You can download image-editor from the dist folder.
177
+
178
+ ## Browser Support
179
+
180
+ * Chrome 100+
181
+ * Firefox 100+
182
+ * Safari 15+
183
+ * Edge 100+
184
+
185
+ The class uses modern DOM & ES2022 features (optional chaining, class, async/await).
186
+
187
+ If you need IE11 or old mobile Safari you will have to transpile.
188
+
189
+ ## Dependencies
190
+
191
+ - **fabric.js v5.x** — Must be loaded before ImageEditor
192
+
193
+ ## License
194
+
195
+ MIT © 2025 Ben Situ
196
+
197
+ Fabric.js is licensed under its own MIT license.
@@ -0,0 +1,14 @@
1
+ var B=(b,f)=>()=>(f||b((f={exports:{}}).exports,f),f.exports);var S=B((w,y)=>{/**
2
+ * @file image-editor.js
3
+ * @module image-editor
4
+ * @version 1.0.0
5
+ * @author Ben Situ
6
+ * @license MIT
7
+ * @description Lightweight canvas-based image editor with masking/transform/export support.
8
+ *
9
+ * This source file is free software, available under the MIT license.
10
+ * It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
11
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12
+ * See the license files for details.
13
+ */(function(b,f){typeof define=="function"&&define.amd?define([],f):typeof y=="object"&&y.exports?y.exports=f():b.ImageEditor=f()})(typeof self<"u"?self:w,function(){"use strict";class b{constructor(t={}){this._fabricLoaded=typeof fabric<"u",this._fabricLoaded||console.error("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted."),this.options={canvasWidth:800,canvasHeight:600,backgroundColor:"#ffffff",animationDuration:300,minScale:.1,maxScale:5,scaleStep:.05,rotationStep:90,expandCanvasToImage:!0,fitImageToCanvas:!1,downsampleOnLoad:!0,downsampleMaxWidth:4e3,downsampleMaxHeight:3e3,downsampleQuality:.92,exportMultiplier:1,exportImageAreaByDefault:!0,defaultMaskWidth:50,defaultMaskHeight:80,maskRotatable:!1,maskLabelOnSelect:!0,maskLabelOffset:3,maskName:"mask",groupSelection:!1,showPlaceholder:!0,initialImageBase64:null,defaultDownloadFileName:"edited_image.jpg",...t},this.options.label={getText:(i,e)=>i.maskName,textOptions:{fontSize:12,fill:"#fff",backgroundColor:"rgba(0,0,0,0.7)",padding:2,fontFamily:"monospace",fontWeight:"bold",selectable:!1,evented:!1,originX:"left",originY:"top"}},this.canvas=null,this.canvasEl=null,this.containerEl=null,this.placeholderEl=null,this.originalImage=null,this.baseImageScale=1,this.currentScale=1,this.currentRotation=0,this.maskCounter=0,this.isAnimating=!1,this.elements={},this.isImageLoadedToCanvas=!1,this.maxHistorySize=50,this._boundHandlers={},this._lastMaskInitialLeft=null,this._lastMaskInitialTop=null,this._lastMaskInitialWidth=null,this.onImageLoaded=typeof t.onImageLoaded=="function"?t.onImageLoaded:null,this.animQueue=new f,this.historyManager=new L(this.maxHistorySize)}init(t={}){if(!this._fabricLoaded)return;let i={canvas:"fabricCanvas",canvasContainer:null,imgPlaceholder:"imgPlaceholder",scaleRate:"scaleRate",rotationLeftInput:"rotationLeftInput",rotationRightInput:"rotationRightInput",rotateLeftBtn:"rotateLeftBtn",rotateRightBtn:"rotateRightBtn",addMaskBtn:"addMaskBtn",removeMaskBtn:"removeMaskBtn",removeAllMasksBtn:"removeAllMasksBtn",mergeBtn:"mergeBtn",downloadBtn:"downloadBtn",maskList:"maskList",zoomInBtn:"zoomInBtn",zoomOutBtn:"zoomOutBtn",resetBtn:"resetBtn",undoBtn:"undoBtn",redoBtn:"redoBtn",imageInput:"imageInput"};this.elements={...i,...t},this._initCanvas(),this._bindEvents(),this._updateInputs(),this._updateMaskList(),this._updateUI(),this.options.initialImageBase64?this.loadImage(this.options.initialImageBase64):this._updatePlaceholderStatus()}_initCanvas(){let t=document.getElementById(this.elements.canvas);if(!t)throw new Error("Canvas is not found: "+this.elements.canvas);if(this.canvasEl=t,this.elements.canvasContainer){let s=document.getElementById(this.elements.canvasContainer);this.containerEl=s||t.parentElement}else this.containerEl=t.parentElement;this.placeholderEl=document.getElementById(this.elements.imgPlaceholder)||null;let i=this.options.canvasWidth,e=this.options.canvasHeight;if(this.containerEl){let s=Math.floor(this.containerEl.clientWidth),a=Math.floor(this.containerEl.clientHeight);s>0&&a>0&&(i=s,e=a)}this.canvas=new fabric.Canvas(t,{width:i,height:e,backgroundColor:this.options.backgroundColor,selection:this.options.groupSelection,preserveObjectStacking:!0}),this.canvas.on("selection:created",s=>this._onSelectionChanged(s.selected)),this.canvas.on("selection:updated",s=>this._onSelectionChanged(s.selected)),this.canvas.on("selection:cleared",()=>this._onSelectionChanged([])),this.canvas.on("object:moving",s=>{s.target&&s.target.maskId&&this._syncMaskLabel(s.target)}),this.canvas.on("object:scaling",s=>{s.target&&s.target.maskId&&this._syncMaskLabel(s.target)}),this.canvas.on("object:rotating",s=>{s.target&&s.target.maskId&&this._syncMaskLabel(s.target)}),this.canvas.on("object:modified",s=>{s.target&&s.target.maskId&&this._syncMaskLabel(s.target)}),this.canvasEl.style.display="block"}_bindEvents(){this._bindIfExists("uploadArea","click",()=>document.getElementById(this.elements.imageInput)?.click());let t=document.getElementById(this.elements.imageInput);t&&t.addEventListener("change",s=>{let a=s.target.files&&s.target.files[0];a&&this._loadImageFile(a)}),this._bindIfExists("zoomInBtn","click",()=>this.scaleImage(this.currentScale+this.options.scaleStep)),this._bindIfExists("zoomOutBtn","click",()=>this.scaleImage(this.currentScale-this.options.scaleStep)),this._bindIfExists("resetBtn","click",()=>{this.reset()}),this._bindIfExists("addMaskBtn","click",()=>this.addMask()),this._bindIfExists("removeMaskBtn","click",()=>this.removeSelectedMask()),this._bindIfExists("removeAllMasksBtn","click",()=>this.removeAllMasks()),this._bindIfExists("mergeBtn","click",()=>this.merge()),this._bindIfExists("downloadBtn","click",()=>this.downloadImage()),this._bindIfExists("undoBtn","click",()=>this.undo()),this._bindIfExists("redoBtn","click",()=>this.redo());let i=document.getElementById(this.elements.rotateLeftBtn),e=document.getElementById(this.elements.rotateRightBtn);i&&i.addEventListener("click",()=>{let s=document.getElementById(this.elements.rotationLeftInput),a=this.options.rotationStep;if(s){let o=parseFloat(s.value);isNaN(o)||(a=o)}this.rotateImage(this.currentRotation-a)}),e&&e.addEventListener("click",()=>{let s=document.getElementById(this.elements.rotationRightInput),a=this.options.rotationStep;if(s){let o=parseFloat(s.value);isNaN(o)||(a=o)}this.rotateImage(this.currentRotation+a)})}_bindIfExists(t,i,e){let s=document.getElementById(this.elements[t]);s&&(s.addEventListener(i,e),this._boundHandlers=this._boundHandlers||{},this._boundHandlers[t]||(this._boundHandlers[t]=[]),this._boundHandlers[t].push({event:i,handler:e}))}_loadImageFile(t){if(!t||!t.type.startsWith("image/"))return;let i=new FileReader;i.onload=e=>this.loadImage(e.target.result),i.onerror=e=>{console.error("[ImageEditor: fileReadError]",e)},i.readAsDataURL(t)}async loadImage(t){if(!this._fabricLoaded||!t||typeof t!="string"||!t.startsWith("data:image/"))return;this._setPlaceholderVisible(!1);let i=await this._createImageElement(t),e=t;if(this.options.downsampleOnLoad&&(i.naturalWidth>this.options.downsampleMaxWidth||i.naturalHeight>this.options.downsampleMaxHeight)){let a=Math.min(this.options.downsampleMaxWidth/i.naturalWidth,this.options.downsampleMaxHeight/i.naturalHeight),o=Math.round(i.naturalWidth*a),h=Math.round(i.naturalHeight*a);e=this._resampleImageToDataURL(i,o,h,this.options.downsampleQuality)}fabric.Image.fromURL(e,s=>{this.canvas.discardActiveObject(),this._hideAllMaskLabels(),this.canvas.clear(),this.canvas.setBackgroundColor(this.options.backgroundColor,this.canvas.renderAll.bind(this.canvas)),s.set({originX:"left",originY:"top",selectable:!1,evented:!1});let a=s.width,o=s.height,h=this.containerEl?Math.floor(this.containerEl.clientWidth||this.options.canvasWidth):this.options.canvasWidth,n=this.containerEl?Math.floor(this.containerEl.clientHeight||this.options.canvasHeight):this.options.canvasHeight;if(this.options.fitImageToCanvas){let c=Math.max(this.options.canvasWidth,h),d=Math.max(this.options.canvasHeight,n);this._setCanvasSizeInt(c,d);let r=Math.min(c/a,d/o,1);s.set({left:(c-a*r)/2,top:(d-o*r)/2}),s.scale(r),this.baseImageScale=s.scaleX||1}else if(this.options.expandCanvasToImage){let c=Math.max(h,Math.floor(a)),d=Math.max(n,Math.floor(o));this._setCanvasSizeInt(c,d),s.set({left:0,top:0}),s.scale(1),this.baseImageScale=1}else{let c=Math.max(this.options.canvasWidth,h),d=Math.max(this.options.canvasHeight,n);this._setCanvasSizeInt(c,d);let r=Math.min(c/a,d/o,1);s.set({left:(c-a*r)/2,top:(d-o*r)/2}),s.scale(r),this.baseImageScale=s.scaleX||1}this.originalImage=s,this.canvas.add(s),this.canvas.sendToBack(s),this._lastMaskInitialLeft=null,this._lastMaskInitialTop=null,this._lastMaskInitialWidth=null,this.maskCounter=0,this.currentScale=1,this.currentRotation=0,this._updateInputs(),this._updateMaskList(),this._updateUI(),this.canvas.renderAll(),this.isImageLoadedToCanvas=!0,typeof this.onImageLoaded=="function"&&this.onImageLoaded()},{crossOrigin:"anonymous"})}isImageLoaded(){return!!(this.originalImage&&this.originalImage instanceof fabric.Image&&this.originalImage.width>0&&this.originalImage.height>0)}_createImageElement(t){return new Promise((i,e)=>{let s=new Image;s.onload=()=>{s.onload=null,s.onerror=null,i(s)},s.onerror=a=>{s.onload=null,s.onerror=null,e(a)},s.src=t})}_resampleImageToDataURL(t,i,e,s=.92){let a=document.createElement("canvas");return a.width=i,a.height=e,a.getContext("2d").drawImage(t,0,0,t.naturalWidth,t.naturalHeight,0,0,i,e),a.toDataURL("image/jpeg",s)}_setCanvasSizeInt(t,i){let e=Math.max(1,Math.round(Number(t)||1)),s=Math.max(1,Math.round(Number(i)||1));this.canvas.setWidth(e),this.canvas.setHeight(s),typeof this.canvas.calcOffset=="function"&&this.canvas.calcOffset(),this.canvasEl&&(this.canvasEl.style.width=e+"px",this.canvasEl.style.height=s+"px",this.canvasEl.style.maxWidth="none")}_getObjectTopLeftPoint(t){if(!t)return{x:0,y:0};t.setCoords();let i=typeof t.getCoords=="function"?t.getCoords():null;if(i&&i.length)return i[0];let e=t.getBoundingRect(!0,!0);return{x:e.left,y:e.top}}_setObjectOriginKeepingPosition(t,i,e,s){!t||!s||!t.setPositionByOrigin||(t.set({originX:i,originY:e}),t.setPositionByOrigin(s,i,e),t.setCoords())}_alignObjectBoundingBoxToCanvasTopLeft(t){if(!t)return;t.setCoords();let i=t.getBoundingRect(!0,!0),e=i.left,s=i.top;t.set({left:(t.left||0)-e,top:(t.top||0)-s}),t.setCoords(),this.canvas.renderAll()}_updateCanvasSizeToImageBounds(){if(!this.originalImage)return;this.originalImage.setCoords();let t=this.originalImage.getBoundingRect(!0,!0),i=this.containerEl?Math.ceil(this.containerEl.clientWidth||0):0,e=this.containerEl?Math.ceil(this.containerEl.clientHeight||0):0;if(i>0&&e>0&&t.width<=i&&t.height<=e){this._setCanvasSizeInt(i,e);return}let s=Math.max(i||0,Math.floor(t.width)),a=Math.max(e||0,Math.floor(t.height));this._setCanvasSizeInt(s,a)}scaleImage(t){return this.animQueue.add(()=>this._scaleImageImpl(t))}_scaleImageImpl(t){if(!this.originalImage||this.isAnimating)return Promise.resolve();t=Math.max(this.options.minScale,Math.min(this.options.maxScale,t)),this.currentScale=t,this.isAnimating=!0,this._updateUI();let i=this.baseImageScale*t,e=this._getObjectTopLeftPoint(this.originalImage);this._setObjectOriginKeepingPosition(this.originalImage,"left","top",e);let s=new Promise(o=>{this.originalImage.animate("scaleX",i,{duration:this.options.animationDuration,onChange:this.canvas.renderAll.bind(this.canvas),onComplete:o})}),a=new Promise(o=>{this.originalImage.animate("scaleY",i,{duration:this.options.animationDuration,onChange:this.canvas.renderAll.bind(this.canvas),onComplete:o})});return Promise.all([s,a]).then(()=>{this.originalImage.set({scaleX:i,scaleY:i}),this.originalImage.setCoords(),this.options.expandCanvasToImage&&this._updateCanvasSizeToImageBounds(),this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage),this.canvas.getObjects().forEach(o=>{o.maskId&&this._syncMaskLabel(o)}),this.isAnimating=!1,this._updateInputs(),this._updateUI(),this.saveState()}).catch(()=>{this.isAnimating=!1,this._updateUI()})}rotateImage(t){return this.animQueue.add(()=>this._rotateImageImpl(t))}_rotateImageImpl(t){if(!this.originalImage||this.isAnimating||isNaN(t))return Promise.resolve();this.currentRotation=t,this.isAnimating=!0,this._updateUI();let i=this.originalImage.getCenterPoint();return this._setObjectOriginKeepingPosition(this.originalImage,"center","center",i),new Promise(s=>{this.originalImage.animate("angle",t,{duration:this.options.animationDuration,onChange:this.canvas.renderAll.bind(this.canvas),onComplete:s})}).then(()=>{this.originalImage.set("angle",t),this.originalImage.setCoords(),this.options.expandCanvasToImage&&this._updateCanvasSizeToImageBounds(),this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);let s=this._getObjectTopLeftPoint(this.originalImage);this._setObjectOriginKeepingPosition(this.originalImage,"left","top",s),this.canvas.getObjects().forEach(a=>{a.maskId&&this._syncMaskLabel(a)}),this.isAnimating=!1,this._updateInputs(),this._updateUI(),this.saveState()}).catch(()=>{this.isAnimating=!1,this._updateUI()})}reset(){return this.originalImage?this.scaleImage(1).then(()=>this.rotateImage(0)).then(()=>{this.saveState()}).catch(t=>{console.error("reset() failed",t)}):Promise.resolve()}loadFromState(t){if(!(!t||!this.canvas))try{let i=typeof t=="string"?JSON.parse(t):t;this.canvas.loadFromJSON(i,()=>{this._hideAllMaskLabels();let e=this.canvas.getObjects();this.originalImage=e.find(a=>a.type==="image"&&!a.maskId)||null,this.originalImage.set({originX:"left",originY:"top",selectable:!1,evented:!1,hasControls:!1,hoverCursor:"default"}),this.canvas.sendToBack(this.originalImage);let s=e.filter(a=>a.maskId);this.maskCounter=s.reduce((a,o)=>Math.max(a,o.maskId),0),this.canvas.renderAll(),this._updateMaskList(),this._updateUI()})}catch(i){console.error("loadFromState() failed",i)}}saveState(){if(!this.canvas)return;let t=this.canvas.getActiveObject();this._hideAllMaskLabels();let i=JSON.stringify(this.canvas.toJSON(["maskId","maskName"])),e=this._lastSnapshot||i,s=!1,a=new E(()=>{s&&this.loadFromState(i),s=!0},()=>{this.loadFromState(e)});this.historyManager.execute(a),this._lastSnapshot=i,t&&t.maskId&&this._showLabelForMask(t),this._updateUI()}undo(){this.historyManager.undo()}redo(){this.historyManager.redo()}addMask(t={}){if(!this.canvas)return null;let i=t.shape||"rect",e={shape:i,width:this.options.defaultMaskWidth,height:this.options.defaultMaskHeight,color:"rgba(0,0,0,0.5)",alpha:.5,gap:5,left:void 0,top:void 0,angle:0,selectable:!0,...t},s=10,a=s,o=s,h=(r,l)=>{if(typeof r=="function")return r(this.canvas,this.options);if(typeof r=="string"&&r.endsWith("%")){let g=parseFloat(r)/100;return Math.floor((this.canvas?this.canvas.getWidth():0)*g)}return r??l};if(e.left===void 0&&this._lastMask){let r=this._lastMask,l=r.left;r.getScaledWidth?l+=r.getScaledWidth():r.width&&(l+=r.width*(r.scaleX??1)),a=Math.round(l+e.gap),o=r.top??s}else a=h(e.left,s),o=h(e.top,s);if(e.width=h(e.width,this.options.defaultMaskWidth),e.height=h(e.height,this.options.defaultMaskHeight),this.options.expandCanvasToImage&&i==="rect"){let r=Math.ceil(a+e.width+10),l=Math.ceil(o+e.height+10),g=this.containerEl?Math.floor(this.containerEl.clientWidth||0):0,u=this.containerEl?Math.floor(this.containerEl.clientHeight||0):0,m=Math.max(this.canvas.getWidth(),g,r),v=Math.max(this.canvas.getHeight(),u,l);this._setCanvasSizeInt(m,v)}let n;if(typeof e.fabricGenerator=="function")n=e.fabricGenerator(e,this.canvas,this.options);else switch(i){case"circle":n=new fabric.Circle({left:a,top:o,radius:h(e.radius,Math.min(e.width,e.height)/2),fill:e.color,opacity:e.alpha,angle:e.angle,...e.styles});break;case"ellipse":n=new fabric.Ellipse({left:a,top:o,rx:h(e.rx,e.width/2),ry:h(e.ry,e.height/2),fill:e.color,opacity:e.alpha,angle:e.angle,...e.styles});break;case"polygon":let r=e.points||[];Array.isArray(r)&&r.length&&typeof r[0]=="object"&&(r=r.map(l=>({x:Number(l.x),y:Number(l.y)}))),n=new fabric.Polygon(r,{left:a,top:o,fill:e.color,opacity:e.alpha,angle:e.angle,...e.styles});break;case"rect":default:n=new fabric.Rect({left:a,top:o,width:h(e.width,this.options.defaultMaskWidth),height:h(e.height,this.options.defaultMaskHeight),fill:e.color,opacity:e.alpha,angle:e.angle,rx:e.rx,ry:e.ry,...e.styles})}n.selectable=e.selectable!==!1,n.hasControls="hasControls"in e?e.hasControls:!0,n.lockRotation=!this.options.maskRotatable,n.borderColor=e.borderColor||"red",n.cornerColor=e.cornerColor||"black",n.cornerSize=e.cornerSize||8,n.transparentCorners="transparentCorners"in e?e.transparentCorners:!1,n.stroke=e.styles&&e.styles.stroke||"#ccc",n.strokeWidth=e.styles&&e.styles.strokeWidth||1,n.strokeUniform="strokeUniform"in e?e.strokeUniform:!0,e.styles&&e.styles.strokeDashArray&&(n.strokeDashArray=e.styles.strokeDashArray),n.originalAlpha=e.alpha;let c={stroke:n.stroke,strokeWidth:n.strokeWidth,opacity:n.originalAlpha},d={stroke:"#ff5500",strokeWidth:2,opacity:Math.min(n.originalAlpha+.2,1)};return n.on("mouseover",()=>{n.set(d),n.canvas.requestRenderAll()}),n.on("mouseout",()=>{n.set(c),n.canvas.requestRenderAll()}),this._lastMaskInitialLeft=a,this._lastMaskInitialTop=o,this._lastMaskInitialWidth=h(e.width,this.options.defaultMaskWidth),n.maskId=++this.maskCounter,n.maskName=`${this.options.maskName}${n.maskId}`,this._lastMask=n,this.canvas.add(n),this.canvas.bringToFront(n),e.selectable&&this.canvas.setActiveObject(n),this._onSelectionChanged([n]),this._updateMaskList(),this._updateUI(),this.canvas.renderAll(),this.saveState(),typeof e.onCreate=="function"&&e.onCreate(n,this.canvas),n}removeSelectedMask(){let t=this.canvas.getActiveObject();!t||!t.maskId||(this._removeLabelForMask(t),this.canvas.remove(t),this.canvas.discardActiveObject(),this._updateMaskList(),this._updateUI(),this.canvas.renderAll(),this.saveState())}removeAllMasks(){let t=this.canvas.getObjects().filter(i=>i.maskId);t.forEach(i=>this._removeLabelForMask(i)),t.forEach(i=>this.canvas.remove(i)),this.canvas.discardActiveObject(),this._lastMaskInitialLeft=null,this._lastMaskInitialTop=null,this._lastMaskInitialWidth=null,this._updateMaskList(),this._updateUI(),this.canvas.renderAll(),this.saveState()}_removeLabelForMask(t){if(!(!t||!this.canvas)&&t.__label){try{this.canvas.getObjects().includes(t.__label)&&this.canvas.remove(t.__label)}catch{}try{delete t.__label}catch{}}}_createLabelForMask(t){if(!t||!this.options.maskLabelOnSelect)return;this._removeLabelForMask(t);let i=null;if(this.options.label&&typeof this.options.label.create=="function"&&(i=this.options.label.create(t,fabric)),!i){let e=t.maskName,s={left:0,top:0,fontSize:12,fill:"#fff",backgroundColor:"rgba(0,0,0,0.7)",selectable:!1,evented:!1,padding:2,originX:"left",originY:"top"};this.options.label&&(typeof this.options.label.getText=="function"&&(e=this.options.label.getText(t,this.maskCounter)),this.options.label.textOptions&&Object.assign(s,this.options.label.textOptions)),i=new fabric.Text(e,s)}i.maskLabel=!0,t.__label=i,this.canvas.add(i),this.canvas.bringToFront(i),this._syncMaskLabel(t)}_hideAllMaskLabels(){if(!this.canvas)return;let t=this.canvas.getObjects();t.filter(e=>e.maskLabel).forEach(e=>{try{t.includes(e)&&this.canvas.remove(e)}catch{}}),t.forEach(e=>{if(e.maskId&&e.__label)try{delete e.__label}catch{}})}_syncMaskLabel(t){if(!t||!this.options.maskLabelOnSelect||!t.__label)return;let i=t.getCoords?t.getCoords():null;if(!i||i.length<4)return;let e=i[0],s=t.getCenterPoint(),a=s.x-e.x,o=s.y-e.y,h=Math.sqrt(a*a+o*o)||1,n=a/h,c=o/h,d=Math.max(0,this.options.maskLabelOffset??3),r=e.x+n*d,l=e.y+c*d;t.__label.set({left:Math.round(r),top:Math.round(l),angle:t.angle||0,originX:"left",originY:"top",visible:!0}),t.__label.setCoords(),this.canvas.renderAll()}_showLabelForMask(t){t&&this.options.maskLabelOnSelect&&(t.__label||this._createLabelForMask(t),t.__label.visible=!0,this._syncMaskLabel(t))}_onSelectionChanged(t){let i=(t||[]).find(s=>s.maskId);this.canvas.getObjects().filter(s=>s.maskId).forEach(s=>{if(s!==i){if(s.__label){try{this.canvas.remove(s.__label)}catch{}delete s.__label}s.set({stroke:"#ccc",strokeWidth:1})}else s.set({stroke:"#ff0000",strokeWidth:1})}),i&&this._showLabelForMask(i),this._updateMaskListSelection(i),this.canvas.renderAll(),this._updateUI()}_updateMaskList(){let t=document.getElementById(this.elements.maskList);if(!t)return;t.innerHTML="",this.canvas.getObjects().filter(e=>e.maskId).forEach(e=>{let s=document.createElement("li");s.className="list-group-item mask-item",s.textContent=e.maskName,s.onclick=()=>{this.canvas.setActiveObject(e),this._onSelectionChanged([e])},t.appendChild(s)})}_updateMaskListSelection(t){let i=document.getElementById(this.elements.maskList);if(!i)return;i.querySelectorAll(".mask-item").forEach(s=>{let a=!!t&&s.textContent===t.maskName;s.classList.toggle("active",a)})}async merge(){if(!(!this.originalImage||!this.canvas.getObjects().filter(i=>i.maskId).length)){this.canvas.discardActiveObject(),this.canvas.renderAll();try{let i=await this.getImageBase64({exportImageArea:!0,multiplier:this.options.exportMultiplier});this.removeAllMasks(),await this.loadImage(i),this.saveState()}catch(i){console.error("merge error",i),this.canvasEl&&(this.canvasEl.style.visibility="")}}}downloadImage(t=this.options.defaultDownloadFileName){if(!this.originalImage)return;let i=this.options.exportImageAreaByDefault;this.getImageBase64({exportImageArea:i,multiplier:this.options.exportMultiplier}).then(e=>{let s=document.createElement("a");s.download=t,s.href=e,document.body.appendChild(s),s.click(),document.body.removeChild(s)}).catch(e=>console.error("download error",e))}async getImageBase64(t={}){if(!this.originalImage)throw new Error("No image loaded");let i=typeof t.exportImageArea=="boolean"?t.exportImageArea:this.options.exportImageAreaByDefault,e=t.multiplier||this.options.exportMultiplier||1;if(!i){let l=this.originalImage.getElement?this.originalImage.getElement():this.originalImage._element||null;if(!l)return this.canvas.toDataURL({format:"jpeg",quality:this.options.downsampleQuality,multiplier:e});let g=this.originalImage.width,u=this.originalImage.height,m=document.createElement("canvas");return m.width=g,m.height=u,m.getContext("2d").drawImage(l,0,0,g,u),m.toDataURL("image/jpeg",this.options.downsampleQuality)}let s=this.canvas.getObjects().filter(l=>l.maskId),a=s.map(l=>({obj:l,opacity:l.opacity,fill:l.fill,strokeWidth:l.strokeWidth,stroke:l.stroke,selectable:l.selectable,lockRotation:l.lockRotation}));s.forEach(l=>this._removeLabelForMask(l)),this.canvas.discardActiveObject(),this.canvas.renderAll(),s.forEach(l=>{l.set({opacity:1,fill:"#000000",strokeWidth:0,stroke:null,selectable:!1}),l.setCoords()}),this.canvas.renderAll(),this.originalImage.setCoords();let o=this.originalImage.getBoundingRect(!0,!0),h=Math.max(0,Math.round(o.left)),n=Math.max(0,Math.round(o.top)),c=Math.max(1,Math.round(o.width)),d=Math.max(1,Math.round(o.height)),r=await new Promise((l,g)=>{try{let u=this.canvas.toDataURL({format:"jpeg",quality:this.options.downsampleQuality,multiplier:e}),m=new Image;m.onload=()=>{try{let v=Math.round(h*e),_=Math.round(n*e),p=Math.round(c*e),I=Math.round(d*e),k=document.createElement("canvas");k.width=p,k.height=I,k.getContext("2d").drawImage(m,v,_,p,I,0,0,p,I);let C=k.toDataURL("image/jpeg",this.options.downsampleQuality);l(C)}catch(v){g(v)}},m.onerror=g,m.src=u}catch(u){g(u)}});return a.forEach(l=>{try{l.obj.set({opacity:l.opacity,fill:l.fill,strokeWidth:l.strokeWidth,stroke:l.stroke,selectable:l.selectable,lockRotation:l.lockRotation}),l.obj.setCoords()}catch{}}),this.canvas.renderAll(),r}async exportImageFile(t={}){if(!this.originalImage)throw new Error("No image loaded");let{mergeMask:i=!0,fileType:e="jpeg",quality:s=this.options.downsampleQuality??.92,multiplier:a=this.options.exportMultiplier??1,fileName:o=this.options.defaultDownloadFileName??"exported_image.jpg"}=t,n={jpeg:"jpeg",jpg:"jpeg","image/jpeg":"jpeg",png:"png","image/png":"png",webp:"webp","image/webp":"webp"}[String(e).toLowerCase()]||"jpeg",c;i?c=await this.getImageBase64({exportImageArea:!0,multiplier:a}):c=await this.getImageBase64({exportImageArea:!1,multiplier:a});let d=c;d.startsWith(`data:image/${n}`)||(d=await new Promise((v,_)=>{let p=new window.Image;p.crossOrigin="Anonymous",p.onload=()=>{try{let I=document.createElement("canvas");I.width=p.width,I.height=p.height,I.getContext("2d").drawImage(p,0,0);let x=I.toDataURL(`image/${n}`,s);v(x)}catch(I){_(I)}},p.onerror=_,p.src=c}));let r=atob(d.split(",")[1]),l=`image/${n}`,g=r.length,u=new Uint8Array(g);for(;g--;)u[g]=r.charCodeAt(g);return new File([u],o,{type:l})}_updateInputs(){let t=document.getElementById(this.elements.scaleRate);t&&(t.value=Math.round(this.currentScale*100))}_updateUI(){let t=!!this.originalImage,e=(t?this.canvas.getObjects().filter(c=>c.maskId):[]).length>0,s=this.canvas.getActiveObject(),a=s&&s.maskId,o=this.currentScale===1&&this.currentRotation===0,h=this.historyManager?.canUndo(),n=this.historyManager?.canRedo();this._setDisabled("zoomInBtn",!t||this.isAnimating||this.currentScale>=this.options.maxScale),this._setDisabled("zoomOutBtn",!t||this.isAnimating||this.currentScale<=this.options.minScale),this._setDisabled("addMaskBtn",!t||this.isAnimating),this._setDisabled("removeMaskBtn",!a||this.isAnimating),this._setDisabled("removeAllMasksBtn",!e||this.isAnimating),this._setDisabled("mergeBtn",!t||!e||this.isAnimating),this._setDisabled("downloadBtn",!t||this.isAnimating),this._setDisabled("resetBtn",!t||o||this.isAnimating),this._setDisabled("undoBtn",!t||this.isAnimating||!h),this._setDisabled("redoBtn",!t||this.isAnimating||!n)}_setDisabled(t,i){let e=document.getElementById(this.elements[t]);e&&(e.disabled=!!i)}_updatePlaceholderStatus(){this.options.showPlaceholder&&this._setPlaceholderVisible(!this.originalImage)}_setPlaceholderVisible(t){this.placeholderEl&&(t?(this.placeholderEl.classList.remove("d-none"),this.placeholderEl.classList.add("d-flex"),this.containerEl.classList.add("d-none")):(this.placeholderEl.classList.remove("d-flex"),this.placeholderEl.classList.add("d-none"),this.containerEl.classList.remove("d-none")))}dispose(){try{for(let t in this._boundHandlers||{}){let i=this._boundHandlers[t]||[],e=document.getElementById(this.elements[t]);e&&i.forEach(s=>{try{e.removeEventListener(s.event,s.handler)}catch{}})}}catch{}if(this.canvas){try{this.canvas.dispose()}catch{}this.canvas=null,this.canvasEl=null,this.isImageLoadedToCanvas=!1}this._boundHandlers={}}}class f{constructor(){this.queue=[],this.running=!1}async add(t){return new Promise((i,e)=>{this.queue.push({fn:t,resolve:i,reject:e}),this.running||this.processQueue()})}async processQueue(){if(this.queue.length===0){this.running=!1;return}this.running=!0;let{fn:t,resolve:i,reject:e}=this.queue.shift();try{let s=await t();i(s)}catch(s){e(s)}this.processQueue()}}class E{constructor(t,i){this.execute=t,this.undo=i}}class L{constructor(t=50){this.history=[],this.currentIndex=-1,this.maxSize=t}execute(t){t.execute(),this.currentIndex<this.history.length-1&&(this.history=this.history.slice(0,this.currentIndex+1)),this.history.push(t),this.history.length>this.maxSize?this.history.shift():this.currentIndex++}canUndo(){return this.currentIndex>=0}canRedo(){return this.currentIndex<this.history.length-1}undo(){this.currentIndex>=0&&(this.history[this.currentIndex].undo(),this.currentIndex--)}redo(){this.currentIndex<this.history.length-1&&(this.currentIndex++,this.history[this.currentIndex].execute())}}return b})});export default S();
14
+ //# sourceMappingURL=image-editor.esm.min.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/image-editor.js"],
4
+ "sourcesContent": ["/**\n * @file image-editor.js\n * @module image-editor\n * @version 1.0.0\n * @author Ben Situ\n * @license MIT\n * @description Lightweight canvas-based image editor with masking/transform/export support.\n *\n * This source file is free software, available under the MIT license.\n * It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the license files for details.\n */\n\n(function (root, factory) {\n if (typeof define === 'function' && define.amd) {\n // AMD / RequireJS\n define([], factory)\n } else if (typeof module === 'object' && module.exports) {\n // CommonJS / Node / webpack (target=commonjs)\n module.exports = factory()\n } else {\n // Browser normal <script> method, hanging to the global\n root.ImageEditor = factory()\n }\n})(typeof self !== 'undefined' ? self : this, function () {\n 'use strict'\n /**\n * ImageEditor\n * \n * A lightweight wrapper around fabric.js providing masking, scaling, rotation,\n * merging/export helpers, and UI integrations for image editing.\n *\n * <b>Note:</b> Requires fabric.js (v5.x) to be loaded on the page before use.\n *\n * <pre>\n * Example usage:\n * const editor = new ImageEditor({ canvasWidth: 1024, canvasHeight: 768 });\n * editor.init();\n * </pre>\n *\n * @class ImageEditor\n * @classdesc Fabric.js-based image editor with simple mask, transform, export and UI features.\n *\n * @param {Object} [options={}] - Customization options to override defaults.\n * @param {number} [options.canvasWidth=800] - The initial canvas width in pixels.\n * @param {number} [options.canvasHeight=600] - The initial canvas height in pixels.\n * @param {string} [options.backgroundColor='#ffffff'] - The canvas background color.\n * @param {number} [options.animationDuration=300] - Duration in ms for scale/rotate animations.\n * @param {number} [options.minScale=0.1] - Minimum image scaling factor.\n * @param {number} [options.maxScale=5.0] - Maximum image scaling factor.\n * @param {number} [options.scaleStep=0.05] - Scale increment/decrement per step.\n * @param {number} [options.rotationStep=90] - Rotation step in degrees.\n * @param {boolean} [options.expandCanvasToImage=true] - If true, expands the canvas to fit image/mask.\n * @param {boolean} [options.fitImageToCanvas=false] - If true, fits loaded image inside canvas.\n * @param {boolean} [options.downsampleOnLoad=true] - Whether to downsample very large images on load.\n * @param {number} [options.downsampleMaxWidth=4000] - Max width for downsampling.\n * @param {number} [options.downsampleMaxHeight=3000] - Max height for downsampling.\n * @param {number} [options.downsampleQuality=0.92] - JPEG quality for downsampling/export.\n * @param {number} [options.exportMultiplier=1] - Scale output image by this multiplier on export.\n * @param {boolean} [options.exportImageAreaByDefault=true] - Export only the image area (clipped to masks).\n * @param {number} [options.defaultMaskWidth=50] - Default width of new mask rectangles.\n * @param {number} [options.defaultMaskHeight=80] - Default height of new masks.\n * @param {boolean} [options.maskRotatable=false] - If true, masks can be rotated.\n * @param {boolean} [options.maskLabelOnSelect=true] - Show label on selected mask.\n * @param {number} [options.maskLabelOffset=3] - Offset for mask labels from top-left corner.\n * @param {string} [options.maskName='mask'] - Prefix for mask names/labels.\n * @param {boolean} [options.showPlaceholder=true] - If true, shows placeholder when no image is loaded.\n * @param {string|null} [options.initialImageBase64=null] - Base64 string to auto-load as initial image, if any.\n * @param {string} [options.defaultDownloadFileName='edited_image.jpg'] - Default file name for downloads.\n * @param {function} [options.onImageLoaded] - Optional callback to invoke after an image loads.\n * \n * @constructor\n */\n class ImageEditor {\n constructor(options = {}) {\n // Verify that fabric.js is present\n this._fabricLoaded = typeof fabric !== 'undefined';\n if (!this._fabricLoaded) {\n console.error('fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.');\n }\n // Default options (can be overridden via ctor param)\n this.options = {\n canvasWidth: 800,\n canvasHeight: 600,\n backgroundColor: '#ffffff',\n\n animationDuration: 300,\n minScale: 0.1,\n maxScale: 5.0,\n scaleStep: 0.05,\n rotationStep: 90,\n\n expandCanvasToImage: true,\n fitImageToCanvas: false,\n\n downsampleOnLoad: true,\n downsampleMaxWidth: 4000,\n downsampleMaxHeight: 3000,\n downsampleQuality: 0.92,\n\n exportMultiplier: 1,\n exportImageAreaByDefault: true,\n\n defaultMaskWidth: 50,\n defaultMaskHeight: 80,\n maskRotatable: false,\n maskLabelOnSelect: true,\n maskLabelOffset: 3,\n maskName: 'mask',\n\n groupSelection: false,\n\n showPlaceholder: true,\n initialImageBase64: null, // Provide a base64 'data:image/...' string here if you want auto-load\n\n defaultDownloadFileName: 'edited_image.jpg',\n\n ...options\n };\n this.options.label = {\n getText: (mask, maskIndex) => mask.maskName,\n textOptions: {\n fontSize: 12,\n fill: '#fff',\n backgroundColor: 'rgba(0,0,0,0.7)',\n padding: 2,\n fontFamily: \"monospace\",\n fontWeight: \"bold\",\n selectable: false,\n evented: false,\n originX: 'left',\n originY: 'top',\n }\n };\n\n // Runtime state\n this.canvas = null;\n this.canvasEl = null;\n this.containerEl = null;\n this.placeholderEl = null;\n\n this.originalImage = null; // fabric.Image\n this.baseImageScale = 1;\n this.currentScale = 1;\n this.currentRotation = 0;\n this.maskCounter = 0;\n this.isAnimating = false;\n this.elements = {};\n this.isImageLoadedToCanvas = false;\n this.maxHistorySize = 50;\n\n this._boundHandlers = {};\n\n this._lastMaskInitialLeft = null;\n this._lastMaskInitialTop = null;\n this._lastMaskInitialWidth = null;\n\n this.onImageLoaded = typeof options.onImageLoaded === 'function' ? options.onImageLoaded : null;\n\n this.animQueue = new AnimationQueue();\n this.historyManager = new HistoryManager(this.maxHistorySize);\n }\n\n /**\n * Initializes the editor, binds to DOM elements, sets up event handlers,\n * and (optionally) loads an initial image.\n * Use this method to set up the editor UI before interacting with it.\n *\n * @param {Object} [idMap={}] - Optional mapping from logical element names to actual DOM element IDs.\n * Supported keys include: canvas, canvasContainer, imgPlaceholder, scaleRate, rotationLeftInput, rotationRightInput,\n * rotateLeftBtn, rotateRightBtn, addMaskBtn, removeMaskBtn, removeAllMasksBtn, mergeBtn, downloadBtn, maskList,\n * zoomInBtn, zoomOutBtn, resetBtn, imageInput. Unknown keys are ignored.\n *\n * @returns {void}\n *\n * @public\n *\n * @example\n * editor.init({\n * canvas: 'myFabricCanvasId',\n * downloadBtn: 'myDownloadButtonId'\n * });\n */\n init(idMap = {}) {\n if (!this._fabricLoaded) return;\n\n const defaults = {\n canvas: 'fabricCanvas',\n canvasContainer: null, // Pass an ID here if you have a scrollable viewport container\n imgPlaceholder: 'imgPlaceholder',\n scaleRate: 'scaleRate',\n rotationLeftInput: 'rotationLeftInput',\n rotationRightInput: 'rotationRightInput',\n rotateLeftBtn: 'rotateLeftBtn',\n rotateRightBtn: 'rotateRightBtn',\n addMaskBtn: 'addMaskBtn',\n removeMaskBtn: 'removeMaskBtn',\n removeAllMasksBtn: 'removeAllMasksBtn',\n mergeBtn: 'mergeBtn',\n downloadBtn: 'downloadBtn',\n maskList: 'maskList',\n zoomInBtn: 'zoomInBtn',\n zoomOutBtn: 'zoomOutBtn',\n resetBtn: 'resetBtn',\n undoBtn: 'undoBtn',\n redoBtn: 'redoBtn',\n imageInput: 'imageInput'\n };\n\n this.elements = { ...defaults, ...idMap };\n\n this._initCanvas();\n this._bindEvents();\n this._updateInputs();\n this._updateMaskList();\n this._updateUI();\n\n // Auto-load initial image if provided\n if (this.options.initialImageBase64) {\n this.loadImage(this.options.initialImageBase64);\n } else {\n this._updatePlaceholderStatus();\n }\n }\n\n /**\n * Canvas setup helpers\n * @private\n */\n _initCanvas() {\n const canvasEl = document.getElementById(this.elements.canvas);\n if (!canvasEl) throw new Error('Canvas is not found: ' + this.elements.canvas);\n this.canvasEl = canvasEl;\n\n // Decide which element acts as \"viewport\" (for width/height fallback)\n if (this.elements.canvasContainer) {\n const ce = document.getElementById(this.elements.canvasContainer);\n this.containerEl = ce || canvasEl.parentElement;\n } else {\n this.containerEl = canvasEl.parentElement;\n }\n\n this.placeholderEl = document.getElementById(this.elements.imgPlaceholder) || null;\n\n // Initial size \u2014 take container size if available\n let initialW = this.options.canvasWidth;\n let initialH = this.options.canvasHeight;\n if (this.containerEl) {\n const cw = Math.floor(this.containerEl.clientWidth);\n const ch = Math.floor(this.containerEl.clientHeight);\n if (cw > 0 && ch > 0) { initialW = cw; initialH = ch; }\n }\n\n this.canvas = new fabric.Canvas(canvasEl, {\n width: initialW,\n height: initialH,\n backgroundColor: this.options.backgroundColor,\n selection: this.options.groupSelection,\n preserveObjectStacking: true\n });\n\n // Fabric event wiring\n this.canvas.on('selection:created', (e) => this._onSelectionChanged(e.selected));\n this.canvas.on('selection:updated', (e) => this._onSelectionChanged(e.selected));\n this.canvas.on('selection:cleared', () => this._onSelectionChanged([]));\n this.canvas.on('object:moving', (e) => { if (e.target && e.target.maskId) this._syncMaskLabel(e.target); });\n this.canvas.on('object:scaling', (e) => { if (e.target && e.target.maskId) this._syncMaskLabel(e.target); });\n this.canvas.on('object:rotating', (e) => { if (e.target && e.target.maskId) this._syncMaskLabel(e.target); });\n this.canvas.on('object:modified', (e) => { if (e.target && e.target.maskId) this._syncMaskLabel(e.target); });\n\n // Avoid inline-element whitespace artefacts\n this.canvasEl.style.display = 'block';\n }\n\n /** \n * DOM / UI bindings\n * @private\n */\n _bindEvents() {\n // Click anywhere on the upload area opens the native file dialog\n this._bindIfExists('uploadArea', 'click', () => document.getElementById(this.elements.imageInput)?.click());\n // File-input change\n const inputEl = document.getElementById(this.elements.imageInput);\n if (inputEl) {\n inputEl.addEventListener('change', (e) => {\n const f = e.target.files && e.target.files[0];\n if (f) this._loadImageFile(f);\n });\n }\n // Zoom & reset\n this._bindIfExists('zoomInBtn', 'click', () => this.scaleImage(this.currentScale + this.options.scaleStep));\n this._bindIfExists('zoomOutBtn', 'click', () => this.scaleImage(this.currentScale - this.options.scaleStep));\n this._bindIfExists('resetBtn', 'click', () => { this.reset(); });\n // Mask management\n this._bindIfExists('addMaskBtn', 'click', () => this.addMask());\n this._bindIfExists('removeMaskBtn', 'click', () => this.removeSelectedMask());\n this._bindIfExists('removeAllMasksBtn', 'click', () => this.removeAllMasks());\n // Merge + download\n this._bindIfExists('mergeBtn', 'click', () => this.merge());\n this._bindIfExists('downloadBtn', 'click', () => this.downloadImage());\n // Undo + Redo\n this._bindIfExists('undoBtn', 'click', () => this.undo());\n this._bindIfExists('redoBtn', 'click', () => this.redo());\n\n // Rotation buttons (step can be overridden by two input fields)\n const rotLeftBtn = document.getElementById(this.elements.rotateLeftBtn);\n const rotRightBtn = document.getElementById(this.elements.rotateRightBtn);\n if (rotLeftBtn) rotLeftBtn.addEventListener('click', () => {\n const el = document.getElementById(this.elements.rotationLeftInput);\n let step = this.options.rotationStep;\n if (el) { const p = parseFloat(el.value); if (!isNaN(p)) step = p; }\n this.rotateImage(this.currentRotation - step);\n });\n if (rotRightBtn) rotRightBtn.addEventListener('click', () => {\n const el = document.getElementById(this.elements.rotationRightInput);\n let step = this.options.rotationStep;\n if (el) { const p = parseFloat(el.value); if (!isNaN(p)) step = p; }\n this.rotateImage(this.currentRotation + step);\n });\n }\n\n /** \n * Event binding element check\n * \n * @param {*} event \n * @param {*} handler \n * @param {*} key \n * @private\n */\n _bindIfExists(key, event, handler) {\n const el = document.getElementById(this.elements[key]);\n if (el) {\n el.addEventListener(event, handler);\n this._boundHandlers = this._boundHandlers || {};\n if (!this._boundHandlers[key]) this._boundHandlers[key] = [];\n this._boundHandlers[key].push({ event, handler });\n }\n }\n\n /** \n * Image loading helpers\n * \n * @param {File} file \n * @private\n */\n _loadImageFile(file) {\n if (!file || !file.type.startsWith('image/')) return;\n const reader = new FileReader();\n reader.onload = (e) => this.loadImage(e.target.result);\n reader.onerror = (e) => { console.error(`[ImageEditor: fileReadError]`, e); }\n reader.readAsDataURL(file);\n }\n\n /**\n * Load a base64 encoded image string into fabric.\n * @async\n * @param {String} base64 \n */\n async loadImage(base64) {\n if (!this._fabricLoaded) return;\n if (!base64 || typeof base64 !== 'string' || !base64.startsWith('data:image/')) return;\n\n this._setPlaceholderVisible(false);\n\n const imgEl = await this._createImageElement(base64);\n\n let loadSrc = base64;\n if (this.options.downsampleOnLoad) {\n const needResize =\n imgEl.naturalWidth > this.options.downsampleMaxWidth ||\n imgEl.naturalHeight > this.options.downsampleMaxHeight;\n if (needResize) {\n const ratio = Math.min(\n this.options.downsampleMaxWidth / imgEl.naturalWidth,\n this.options.downsampleMaxHeight / imgEl.naturalHeight\n );\n const tw = Math.round(imgEl.naturalWidth * ratio);\n const th = Math.round(imgEl.naturalHeight * ratio);\n loadSrc = this._resampleImageToDataURL(imgEl, tw, th, this.options.downsampleQuality);\n }\n }\n\n // Create fabric.Image from URL\n fabric.Image.fromURL(loadSrc, (fimg) => {\n this.canvas.discardActiveObject();\n this._hideAllMaskLabels();\n this.canvas.clear();\n this.canvas.setBackgroundColor(this.options.backgroundColor, this.canvas.renderAll.bind(this.canvas));\n\n fimg.set({ originX: 'left', originY: 'top', selectable: false, evented: false });\n\n const imgW = fimg.width;\n const imgH = fimg.height;\n\n const minW = this.containerEl ? Math.floor(this.containerEl.clientWidth || this.options.canvasWidth) : this.options.canvasWidth;\n const minH = this.containerEl ? Math.floor(this.containerEl.clientHeight || this.options.canvasHeight) : this.options.canvasHeight;\n\n if (this.options.fitImageToCanvas) {\n // Fit into current canvas (shrink only)\n const cw = Math.max(this.options.canvasWidth, minW);\n const ch = Math.max(this.options.canvasHeight, minH);\n this._setCanvasSizeInt(cw, ch);\n const fitScale = Math.min(cw / imgW, ch / imgH, 1);\n fimg.set({ left: (cw - imgW * fitScale) / 2, top: (ch - imgH * fitScale) / 2 });\n fimg.scale(fitScale);\n this.baseImageScale = fimg.scaleX || 1;\n } else if (this.options.expandCanvasToImage) {\n // Expand canvas so that it fully contains the image\n const cw = Math.max(minW, Math.floor(imgW));\n const ch = Math.max(minH, Math.floor(imgH));\n this._setCanvasSizeInt(cw, ch);\n fimg.set({ left: 0, top: 0 });\n fimg.scale(1);\n this.baseImageScale = 1;\n } else {\n // Keep existing canvas size and center the image\n const cw = Math.max(this.options.canvasWidth, minW);\n const ch = Math.max(this.options.canvasHeight, minH);\n this._setCanvasSizeInt(cw, ch);\n const fitScale = Math.min(cw / imgW, ch / imgH, 1);\n fimg.set({ left: (cw - imgW * fitScale) / 2, top: (ch - imgH * fitScale) / 2 });\n fimg.scale(fitScale);\n this.baseImageScale = fimg.scaleX || 1;\n }\n // Put the image onto the canvas\n this.originalImage = fimg;\n this.canvas.add(fimg);\n this.canvas.sendToBack(fimg);\n\n // Reset mask placement memory\n this._lastMaskInitialLeft = null;\n this._lastMaskInitialTop = null;\n this._lastMaskInitialWidth = null;\n\n this.maskCounter = 0;\n this.currentScale = 1;\n this.currentRotation = 0;\n\n this._updateInputs();\n this._updateMaskList();\n this._updateUI();\n this.canvas.renderAll();\n this.isImageLoadedToCanvas = true;\n\n if (typeof this.onImageLoaded === 'function') {\n this.onImageLoaded();\n }\n }, { crossOrigin: 'anonymous' });\n }\n\n /**\n * Checks whether there is a loaded image on the current canvas.\n * @returns {boolean} true if loaded, false if not\n */\n isImageLoaded() {\n return !!(\n this.originalImage &&\n this.originalImage instanceof fabric.Image &&\n this.originalImage.width > 0 &&\n this.originalImage.height > 0\n );\n }\n\n /**\n * Creates an HTMLImageElement from a given data URL.\n * \n * @param {string} dataURL - A data URL representing the image (e.g., \"data:image/png;base64,...\").\n * @returns {Promise<HTMLImageElement>} A promise that resolves to the created image element when loaded, or rejects on error.\n * @private\n */\n _createImageElement(dataURL) {\n return new Promise((res, rej) => {\n const img = new Image();\n img.onload = () => {\n img.onload = null;\n img.onerror = null;\n res(img);\n };\n img.onerror = (e) => {\n img.onload = null;\n img.onerror = null;\n rej(e);\n };\n img.src = dataURL;\n });\n }\n\n /**\n * Resamples the given image element to a new width and height and returns the result as a JPEG data URL.\n * \n * @param {HTMLImageElement} imgEl - The image element to resample.\n * @param {number} w - Target width (in pixels) for the resampled image.\n * @param {number} h - Target height (in pixels) for the resampled image.\n * @param {number} [quality=0.92] - JPEG image quality between 0 and 1 (optional, default 0.92).\n * @returns {string} A data URL representing the resampled image as JPEG.\n * @private\n */\n _resampleImageToDataURL(imgEl, w, h, quality = 0.92) {\n const oc = document.createElement('canvas');\n oc.width = w;\n oc.height = h;\n const ctx = oc.getContext('2d');\n ctx.drawImage(imgEl, 0, 0, imgEl.naturalWidth, imgEl.naturalHeight, 0, 0, w, h);\n return oc.toDataURL('image/jpeg', quality);\n }\n\n /** \n * Sets canvas size to integer width and height values to prevent scrollbars due to sub-pixel rendering.\n * Also updates the corresponding style attributes.\n * \n * @param {number} w - Canvas width (in pixels).\n * @param {number} h - Canvas height (in pixels).\n * @private\n */\n _setCanvasSizeInt(w, h) {\n const iw = Math.max(1, Math.round(Number(w) || 1));\n const ih = Math.max(1, Math.round(Number(h) || 1));\n // Set fabric internal and also style attributes to keep DOM consistent\n this.canvas.setWidth(iw);\n this.canvas.setHeight(ih);\n if (typeof this.canvas.calcOffset === 'function') this.canvas.calcOffset();\n // Keep DOM element in sync (avoid fractional CSS pixels)\n if (this.canvasEl) {\n this.canvasEl.style.width = iw + 'px';\n this.canvasEl.style.height = ih + 'px';\n this.canvasEl.style.maxWidth = 'none';\n }\n }\n\n /** \n * Gets the top-left corner coordinates of the given object.\n * Used for geometry calculations (e.g., scale, rotate).\n * \n * @param {Object} obj - The object for which to get the top-left coordinates. Should support setCoords and getCoords/getBoundingRect methods.\n * @returns {{x: number, y: number}} The top-left corner point as an object with x and y properties.\n * @private\n */\n _getObjectTopLeftPoint(obj) {\n if (!obj) return { x: 0, y: 0 };\n obj.setCoords();\n const coords = typeof obj.getCoords === 'function' ? obj.getCoords() : null;\n if (coords && coords.length) return coords[0];\n const br = obj.getBoundingRect(true, true);\n return { x: br.left, y: br.top };\n }\n\n /**\n * Sets the object's origin at the specified origin point, keeping a reference point fixed in position.\n * \n * @param {Object} obj - The object to modify. Should support set, setPositionByOrigin, and setCoords.\n * @param {string} originX - The new originX (\"left\", \"center\", \"right\", etc.).\n * @param {string} originY - The new originY (\"top\", \"center\", \"bottom\", etc.).\n * @param {{x: number, y: number}} refPoint - The point to keep fixed while setting the new origins.\n * @private\n */\n _setObjectOriginKeepingPosition(obj, originX, originY, refPoint) {\n if (!obj || !refPoint || !obj.setPositionByOrigin) return;\n obj.set({ originX, originY });\n obj.setPositionByOrigin(refPoint, originX, originY);\n obj.setCoords();\n }\n\n /**\n * Moves the object so its bounding box aligns with the canvas's top-left corner (0, 0).\n * \n * @param {Object} obj - The object to align.\n * @private\n */\n _alignObjectBoundingBoxToCanvasTopLeft(obj) {\n if (!obj) return;\n obj.setCoords();\n const br = obj.getBoundingRect(true, true);\n const dx = br.left;\n const dy = br.top;\n obj.set({ left: (obj.left || 0) - dx, top: (obj.top || 0) - dy });\n obj.setCoords();\n this.canvas.renderAll();\n }\n\n /**\n * Updates the canvas size to match the bounding box of the original image,\n * ensuring that the canvas is always at least as large as its container.\n * @private\n */\n _updateCanvasSizeToImageBounds() {\n if (!this.originalImage) return;\n this.originalImage.setCoords();\n const br = this.originalImage.getBoundingRect(true, true);\n\n // Container integer sizes\n const containerW = this.containerEl ? Math.ceil(this.containerEl.clientWidth || 0) : 0;\n const containerH = this.containerEl ? Math.ceil(this.containerEl.clientHeight || 0) : 0;\n\n // If image smaller or equal than container in BOTH dims => keep canvas equal to container\n if (containerW > 0 && containerH > 0 && br.width <= containerW && br.height <= containerH) {\n this._setCanvasSizeInt(containerW, containerH);\n return;\n }\n\n // Else canvas follows image bounding box but not smaller than container dims individually\n const newW = Math.max(containerW || 0, Math.floor(br.width));\n const newH = Math.max(containerH || 0, Math.floor(br.height));\n this._setCanvasSizeInt(newW, newH);\n }\n\n /** \n * Scales the original image by a given factor, with animation.\n * Returns a promise that resolves when the scale animation is complete.\n * @param {number} factor - The scaling factor (will be clamped between `options.minScale` and `options.maxScale`).\n * @returns {Promise<void>} Promise that resolves once the scaling animation finishes.\n * @public\n */\n scaleImage(factor) {\n return this.animQueue.add(() => this._scaleImageImpl(factor));\n }\n\n /** \n * Scales the original image by a given factor, with animation.\n * Returns a promise that resolves when the scale animation is complete.\n * @param {number} factor - The scaling factor (will be clamped between `options.minScale` and `options.maxScale`).\n * @returns {Promise<void>} Promise that resolves once the scaling animation finishes.\n * @private\n */\n _scaleImageImpl(factor) {\n if (!this.originalImage) return Promise.resolve();\n if (this.isAnimating) return Promise.resolve();\n factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, factor));\n this.currentScale = factor;\n this.isAnimating = true;\n this._updateUI();\n\n const targetAbs = this.baseImageScale * factor;\n\n // Scale around current top-left (recompute)\n const topLeft = this._getObjectTopLeftPoint(this.originalImage);\n this._setObjectOriginKeepingPosition(this.originalImage, 'left', 'top', topLeft);\n\n const p1 = new Promise((res) => {\n this.originalImage.animate('scaleX', targetAbs, {\n duration: this.options.animationDuration,\n onChange: this.canvas.renderAll.bind(this.canvas),\n onComplete: res\n });\n });\n const p2 = new Promise((res) => {\n this.originalImage.animate('scaleY', targetAbs, {\n duration: this.options.animationDuration,\n onChange: this.canvas.renderAll.bind(this.canvas),\n onComplete: res\n });\n });\n\n return Promise.all([p1, p2]).then(() => {\n this.originalImage.set({ scaleX: targetAbs, scaleY: targetAbs });\n this.originalImage.setCoords();\n\n if (this.options.expandCanvasToImage) this._updateCanvasSizeToImageBounds();\n\n this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);\n\n // Sync mask labels\n this.canvas.getObjects().forEach(o => { if (o.maskId) this._syncMaskLabel(o); });\n\n this.isAnimating = false;\n this._updateInputs();\n this._updateUI();\n this.saveState();\n }).catch(() => {\n this.isAnimating = false;\n this._updateUI();\n });\n }\n\n /** \n * Rotates the original image by a given number of degrees, with animation.\n * Returns a promise that resolves when the rotation animation is complete.\n * @param {number} degrees - The angle in degrees to rotate the image.\n * @returns {Promise<void>} Promise that resolves once the rotation animation finishes.\n * @public\n */\n rotateImage(deg) {\n return this.animQueue.add(() => this._rotateImageImpl(deg));\n }\n\n /** \n * Rotates the original image by a given number of degrees, with animation.\n * Returns a promise that resolves when the rotation animation is complete.\n * @param {number} degrees - The angle in degrees to rotate the image.\n * @returns {Promise<void>} Promise that resolves once the rotation animation finishes.\n * @private\n */\n _rotateImageImpl(degrees) {\n if (!this.originalImage) return Promise.resolve();\n if (this.isAnimating) return Promise.resolve();\n if (isNaN(degrees)) return Promise.resolve();\n this.currentRotation = degrees;\n this.isAnimating = true;\n this._updateUI();\n\n const center = this.originalImage.getCenterPoint();\n this._setObjectOriginKeepingPosition(this.originalImage, 'center', 'center', center);\n\n const p = new Promise((res) => {\n this.originalImage.animate('angle', degrees, {\n duration: this.options.animationDuration,\n onChange: this.canvas.renderAll.bind(this.canvas),\n onComplete: res\n });\n });\n\n return p.then(() => {\n this.originalImage.set('angle', degrees);\n this.originalImage.setCoords();\n\n if (this.options.expandCanvasToImage) this._updateCanvasSizeToImageBounds();\n\n this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);\n\n const newTopLeft = this._getObjectTopLeftPoint(this.originalImage);\n this._setObjectOriginKeepingPosition(this.originalImage, 'left', 'top', newTopLeft);\n\n // Sync mask labels\n this.canvas.getObjects().forEach(o => { if (o.maskId) this._syncMaskLabel(o); });\n\n this.isAnimating = false;\n this._updateInputs();\n this._updateUI();\n this.saveState();\n }).catch(() => {\n this.isAnimating = false;\n this._updateUI();\n });\n }\n\n /**\n * Resets the image: scales to 1 and rotates to 0 degrees.\n * @returns {Promise<void>} Promise that resolves when reset is complete.\n */\n reset() {\n if (!this.originalImage) return Promise.resolve();\n\n return this.scaleImage(1)\n .then(() => this.rotateImage(0))\n .then(() => {\n this.saveState();\n })\n .catch(err => {\n console.error('reset() failed', err);\n });\n }\n\n /**\n * Restores a canvas state that was previously stored by saveState().\n * @param {string} jsonString - the JSON string returned by fabric.toJSON().\n */\n loadFromState(jsonString) {\n if (!jsonString || !this.canvas) return;\n\n try {\n const json = (typeof jsonString === 'string')\n ? JSON.parse(jsonString)\n : jsonString;\n\n this.canvas.loadFromJSON(json, () => {\n this._hideAllMaskLabels();\n const objs = this.canvas.getObjects();\n this.originalImage = objs.find(o => o.type === 'image' && !o.maskId) || null;\n\n this.originalImage.set({ originX: 'left', originY: 'top', selectable: false, evented: false, hasControls: false, hoverCursor: 'default' });\n this.canvas.sendToBack(this.originalImage);\n\n const masks = objs.filter(o => o.maskId);\n this.maskCounter = masks.reduce((max, m) =>\n Math.max(max, m.maskId), 0);\n\n this.canvas.renderAll();\n this._updateMaskList();\n this._updateUI();\n });\n\n } catch (e) {\n console.error('loadFromState() failed', e);\n }\n }\n\n /**\n * Saves the current state of the canvas to history, storing any mask/raster label information.\n */\n saveState() {\n if (!this.canvas) return;\n const activeObj = this.canvas.getActiveObject();\n this._hideAllMaskLabels();\n const after = JSON.stringify(this.canvas.toJSON(['maskId', 'maskName']));\n const before = this._lastSnapshot || after;\n let executedOnce = false;\n\n const cmd = new Command(\n () => {\n if (executedOnce) {\n // this.canvas.clear();\n this.loadFromState(after);\n }\n executedOnce = true;\n },\n () => {\n // this.canvas.clear();\n this.loadFromState(before);\n }\n );\n\n this.historyManager.execute(cmd);\n this._lastSnapshot = after;\n if (activeObj && activeObj.maskId) {\n this._showLabelForMask(activeObj);\n }\n this._updateUI();\n }\n\n /**\n * Undo the last state change, if possible.\n */\n undo() {\n this.historyManager.undo();\n }\n\n /**\n * Redo the next state change, if possible.\n */\n redo() {\n this.historyManager.redo();\n }\n\n /** \n * Adds a rectangular mask to the canvas.\n * Mask placement and properties are determined by the provided config and instance options.\n * Canvas and list UI are updated accordingly.\n * @param {Object} [config={}] - Optional mask configuration overrides:\n * @param {string} [config.shape='rect'] - 'rect', 'circle', 'ellipse', 'polygon', ...\n * @param {Object|Array} [config.points] - Required for polygon: [{x, y}, ...] or [[x, y], ...]\n * @param {number|function} [config.width/height/rx/ry/radius] - Can be number or function(canvas, options) \n * @param {number|string|function} [config.left/top] - Absolute, %, or function\n * @param {number|string} [config.angle] - Rotation angle (degree)\n * @param {string} [config.color] - Fill color in CSS color format (default 'rgba(0,0,0,0.5)')\n * @param {number} [config.alpha] - Opacity, from 0 to 1 (default 0.5)\n * @param {boolean} [config.selectable=true]\n * @param {Object} [config.styles] - Custom styles (stroke, dashArray, etc)\n * @param {function} [config.onCreate] - Callback after mask created (receives Fabric object)\n * @param {function} [config.fabricGenerator] - (cfg) => new FabricObj\n * @returns {fabric.Rect|null} The created mask object, or null if canvas is not available.\n * @public\n */\n addMask(config = {}) {\n if (!this.canvas) return null;\n const shapeType = config.shape || 'rect';\n // Default config\n const cfg = {\n shape: shapeType,\n width: this.options.defaultMaskWidth,\n height: this.options.defaultMaskHeight,\n color: 'rgba(0,0,0,0.5)',\n alpha: 0.5,\n gap: 5,\n left: undefined,\n top: undefined,\n angle: 0,\n selectable: true,\n ...config\n };\n\n // Always start placement relative to canvas left/top.\n const firstOffset = 10;\n let left = firstOffset;\n let top = firstOffset;\n\n const resolveValue = (val, fallback) => {\n if (typeof val === 'function')\n return val(this.canvas, this.options); // This context is this of addMask\n if (typeof val === 'string' && val.endsWith('%')) {\n const percent = parseFloat(val) / 100;\n return Math.floor((this.canvas ? this.canvas.getWidth() : 0) * percent);\n }\n return val != null ? val : fallback;\n }\n\n if (cfg.left === undefined && this._lastMask) {\n const prev = this._lastMask;\n let prevRight = prev.left;\n\n if (prev.getScaledWidth) {\n prevRight += prev.getScaledWidth();\n } else if (prev.width) {\n prevRight += prev.width * (prev.scaleX ?? 1);\n }\n left = Math.round(prevRight + cfg.gap);\n top = prev.top ?? firstOffset;\n } else {\n left = resolveValue(cfg.left, firstOffset);\n top = resolveValue(cfg.top, firstOffset);\n }\n\n cfg.width = resolveValue(cfg.width, this.options.defaultMaskWidth);\n cfg.height = resolveValue(cfg.height, this.options.defaultMaskHeight);\n\n // If expandCanvasToImage mode, ensure canvas large enough to hold mask initial placement\n if (this.options.expandCanvasToImage && shapeType === 'rect') {\n const requiredW = Math.ceil(left + cfg.width + 10);\n const requiredH = Math.ceil(top + cfg.height + 10);\n const minW = this.containerEl ? Math.floor(this.containerEl.clientWidth || 0) : 0;\n const minH = this.containerEl ? Math.floor(this.containerEl.clientHeight || 0) : 0;\n const newW = Math.max(this.canvas.getWidth(), minW, requiredW);\n const newH = Math.max(this.canvas.getHeight(), minH, requiredH);\n this._setCanvasSizeInt(newW, newH);\n }\n\n let mask;\n if (typeof cfg.fabricGenerator === 'function') {\n mask = cfg.fabricGenerator(cfg, this.canvas, this.options);\n } else {\n switch (shapeType) {\n case 'circle':\n mask = new fabric.Circle({\n left, top,\n radius: resolveValue(cfg.radius, Math.min(cfg.width, cfg.height) / 2),\n fill: cfg.color,\n opacity: cfg.alpha,\n angle: cfg.angle,\n ...cfg.styles\n });\n break;\n case 'ellipse':\n mask = new fabric.Ellipse({\n left, top,\n rx: resolveValue(cfg.rx, cfg.width / 2),\n ry: resolveValue(cfg.ry, cfg.height / 2),\n fill: cfg.color,\n opacity: cfg.alpha,\n angle: cfg.angle,\n ...cfg.styles\n });\n break;\n case 'polygon':\n let polyPoints = cfg.points || [];\n if (Array.isArray(polyPoints) && polyPoints.length && typeof polyPoints[0] === 'object') {\n // Ensure numeric {x,y} objects for fabric.Polygon\n polyPoints = polyPoints.map(pt => ({ x: Number(pt.x), y: Number(pt.y) }));\n }\n mask = new fabric.Polygon(polyPoints, {\n left, top,\n fill: cfg.color,\n opacity: cfg.alpha,\n angle: cfg.angle,\n ...cfg.styles\n });\n break;\n case 'rect':\n default:\n mask = new fabric.Rect({\n left, top,\n width: resolveValue(cfg.width, this.options.defaultMaskWidth),\n height: resolveValue(cfg.height, this.options.defaultMaskHeight),\n fill: cfg.color,\n opacity: cfg.alpha,\n angle: cfg.angle,\n rx: cfg.rx, // Rounded Corners\n ry: cfg.ry,\n ...cfg.styles\n });\n }\n }\n\n mask.selectable = cfg.selectable !== false;\n mask.hasControls = ('hasControls' in cfg) ? cfg.hasControls : true;\n mask.lockRotation = !this.options.maskRotatable;\n mask.borderColor = cfg.borderColor || 'red';\n mask.cornerColor = cfg.cornerColor || 'black';\n mask.cornerSize = cfg.cornerSize || 8;\n mask.transparentCorners = ('transparentCorners' in cfg) ? cfg.transparentCorners : false;\n mask.stroke = (cfg.styles && cfg.styles.stroke) || '#ccc';\n mask.strokeWidth = (cfg.styles && cfg.styles.strokeWidth) || 1;\n mask.strokeUniform = ('strokeUniform' in cfg) ? cfg.strokeUniform : true;\n if (cfg.styles && cfg.styles.strokeDashArray) mask.strokeDashArray = cfg.styles.strokeDashArray;\n\n mask.originalAlpha = cfg.alpha;\n const normalStyle = { stroke: mask.stroke, strokeWidth: mask.strokeWidth, opacity: mask.originalAlpha };\n const hoverStyle = { stroke: '#ff5500', strokeWidth: 2, opacity: Math.min(mask.originalAlpha + 0.2, 1) };\n\n mask.on('mouseover', () => {\n mask.set(hoverStyle);\n mask.canvas.requestRenderAll();\n });\n\n mask.on('mouseout', () => {\n mask.set(normalStyle);\n mask.canvas.requestRenderAll();\n });\n\n // Remember initial for next one\n this._lastMaskInitialLeft = left;\n this._lastMaskInitialTop = top;\n this._lastMaskInitialWidth = resolveValue(cfg.width, this.options.defaultMaskWidth);\n\n mask.maskId = ++this.maskCounter;\n mask.maskName = `${this.options.maskName}${mask.maskId}`;\n this._lastMask = mask;\n\n this.canvas.add(mask);\n this.canvas.bringToFront(mask);\n if (cfg.selectable) this.canvas.setActiveObject(mask);\n this._onSelectionChanged([mask]);\n this._updateMaskList();\n this._updateUI();\n this.canvas.renderAll();\n this.saveState();\n\n if (typeof cfg.onCreate === 'function') cfg.onCreate(mask, this.canvas);\n return mask;\n }\n\n /**\n * Removes the currently selected mask from the canvas, if any.\n * The associated label is also removed. UI and mask list are updated.\n */\n removeSelectedMask() {\n const active = this.canvas.getActiveObject();\n if (!active || !active.maskId) return;\n this._removeLabelForMask(active);\n this.canvas.remove(active);\n this.canvas.discardActiveObject();\n this._updateMaskList();\n this._updateUI();\n this.canvas.renderAll();\n this.saveState();\n }\n\n /**\n * Removes all masks from the canvas, including their labels.\n * UI and internal mask placement memory are reset.\n */\n removeAllMasks() {\n const masks = this.canvas.getObjects().filter(o => o.maskId);\n masks.forEach(m => this._removeLabelForMask(m));\n masks.forEach(m => this.canvas.remove(m));\n this.canvas.discardActiveObject();\n this._lastMaskInitialLeft = null;\n this._lastMaskInitialTop = null;\n this._lastMaskInitialWidth = null;\n this._updateMaskList();\n this._updateUI();\n this.canvas.renderAll();\n this.saveState();\n }\n\n /**\n * Removes the label associated with the specified mask object, if it exists.\n * \n * @param {fabric.Object} mask - The mask object whose label should be removed.\n * @private\n */\n _removeLabelForMask(mask) {\n if (!mask || !this.canvas) return;\n if (mask.__label) {\n try {\n const objs = this.canvas.getObjects();\n if (objs.includes(mask.__label)) {\n this.canvas.remove(mask.__label);\n }\n } catch (e) { /* ignore */ }\n try { delete mask.__label; } catch (e) { }\n }\n }\n\n /**\n * Creates and adds a custom label (fabric.Text or fabric.IText) for the mask.\n * The label is default bound to the top-left of the mask and managed as a non-interactive overlay.\n * \n * @param {fabric.Object} mask - The mask to create a label for.\n * @private\n */\n _createLabelForMask(mask) {\n if (!mask || !this.options.maskLabelOnSelect) return;\n this._removeLabelForMask(mask);\n let textObj = null;\n if (this.options.label && typeof this.options.label.create === 'function') {\n textObj = this.options.label.create(mask, fabric);\n }\n if (!textObj) {\n let txt = mask.maskName; // Default\n let textOptions = {\n left: 0,\n top: 0,\n fontSize: 12,\n fill: '#fff',\n backgroundColor: 'rgba(0,0,0,0.7)',\n selectable: false,\n evented: false,\n padding: 2,\n originX: 'left',\n originY: 'top'\n };\n if (this.options.label) {\n if (typeof this.options.label.getText === 'function') {\n txt = this.options.label.getText(mask, this.maskCounter);\n }\n // Merge external styles\n if (this.options.label.textOptions) {\n Object.assign(textOptions, this.options.label.textOptions);\n }\n }\n textObj = new fabric.Text(txt, textOptions);\n }\n\n textObj.maskLabel = true;\n mask.__label = textObj;\n this.canvas.add(textObj);\n this.canvas.bringToFront(textObj);\n this._syncMaskLabel(mask);\n }\n\n /**\n * Hides (removes) all mask labels from the canvas.\n * Internal label references on mask objects are also deleted.\n * @private\n */\n _hideAllMaskLabels() {\n if (!this.canvas) return;\n const objs = this.canvas.getObjects();\n const labels = objs.filter(o => o.maskLabel);\n labels.forEach(l => {\n try {\n if (objs.includes(l)) this.canvas.remove(l);\n } catch (e) { }\n });\n objs.forEach(o => { if (o.maskId && o.__label) { try { delete o.__label; } catch (e) { } } });\n }\n\n /**\n * Synchronizes the position, angle, and visibility of the mask's label so that it appears properly above the mask.\n * \n * @param {fabric.Object} mask - The mask whose label should be repositioned.\n * @private\n */\n _syncMaskLabel(mask) {\n if (!mask) return;\n if (!this.options.maskLabelOnSelect) return;\n if (!mask.__label) return;\n\n const coords = mask.getCoords ? mask.getCoords() : null;\n if (!coords || coords.length < 4) return;\n\n const tl = coords[0];\n const center = mask.getCenterPoint();\n\n const vx = center.x - tl.x;\n const vy = center.y - tl.y;\n const dist = Math.sqrt(vx * vx + vy * vy) || 1;\n const ux = vx / dist;\n const uy = vy / dist;\n\n const offset = Math.max(0, this.options.maskLabelOffset ?? 3);\n\n const px = tl.x + ux * offset;\n const py = tl.y + uy * offset;\n\n mask.__label.set({\n left: Math.round(px),\n top: Math.round(py),\n angle: mask.angle || 0,\n originX: 'left',\n originY: 'top',\n visible: true\n });\n mask.__label.setCoords();\n this.canvas.renderAll();\n }\n\n /**\n * Shows the label for the given mask, creating it if necessary and synchronizing its position.\n * \n * @param {fabric.Object} mask - The mask whose label should be shown.\n * @private\n */\n _showLabelForMask(mask) {\n if (!mask) return;\n if (!this.options.maskLabelOnSelect) return;\n if (!mask.__label) this._createLabelForMask(mask);\n mask.__label.visible = true;\n this._syncMaskLabel(mask);\n }\n\n /**\n * Handles changes to the selection of canvas objects (masks),\n * updates mask stroke and label display, and syncs mask list selection.\n *\n * @param {Array<Object>} selected - The currently selected objects (e.g. [mask] or []).\n * @private\n */\n _onSelectionChanged(selected) {\n const selectedMask = (selected || []).find(o => o.maskId);\n const masks = this.canvas.getObjects().filter(o => o.maskId);\n masks.forEach(m => {\n if (m !== selectedMask) {\n if (m.__label) {\n try { this.canvas.remove(m.__label); } catch (e) { }\n delete m.__label;\n }\n m.set({ stroke: '#ccc', strokeWidth: 1 });\n } else {\n m.set({ stroke: '#ff0000', strokeWidth: 1 });\n }\n });\n\n if (selectedMask) this._showLabelForMask(selectedMask);\n\n this._updateMaskListSelection(selectedMask);\n this.canvas.renderAll();\n this._updateUI();\n }\n\n /**\n * Updates the mask list in the DOM to reflect the current masks on the canvas.\n * Each list entry becomes a clickable element for mask selection.\n * @private\n */\n _updateMaskList() {\n const listEl = document.getElementById(this.elements.maskList);\n if (!listEl) return;\n listEl.innerHTML = '';\n const masks = this.canvas.getObjects().filter(o => o.maskId);\n masks.forEach(mask => {\n const li = document.createElement('li');\n li.className = 'list-group-item mask-item';\n li.textContent = mask.maskName;\n li.onclick = () => { this.canvas.setActiveObject(mask); this._onSelectionChanged([mask]); };\n listEl.appendChild(li);\n });\n }\n\n /**\n * Updates the visual selection (CSS 'active') state for the mask list in the DOM.\n * \n * @param {Object|null} selectedMask - The currently selected mask, or null if none selected.\n * @private\n */\n _updateMaskListSelection(selectedMask) {\n const listEl = document.getElementById(this.elements.maskList);\n if (!listEl) return;\n const items = listEl.querySelectorAll('.mask-item');\n items.forEach(item => {\n const isSelected = !!selectedMask && item.textContent === selectedMask.maskName;\n item.classList.toggle('active', isSelected);\n });\n }\n\n /**\n * Merges current masks into the image: exports a masked/cropped image, removes all masks, and re-imports the merged image.\n * Will not run if no original image or no masks exist.\n * @async\n * @returns {Promise<void>} Resolves when merge and load are complete.\n */\n async merge() {\n if (!this.originalImage) return;\n const masks = this.canvas.getObjects().filter(o => o.maskId);\n if (!masks.length) return;\n\n this.canvas.discardActiveObject();\n this.canvas.renderAll();\n\n try {\n const merged = await this.getImageBase64({ exportImageArea: true, multiplier: this.options.exportMultiplier });\n this.removeAllMasks();\n await this.loadImage(merged);\n this.saveState();\n } catch (err) {\n console.error('merge error', err);\n if (this.canvasEl) this.canvasEl.style.visibility = '';\n }\n }\n\n /**\n * Triggers a JPEG image download of the current canvas (image plus masks if configured).\n * The image area and multiplier are controlled by options.\n * @param {string} [fileName=this.options.defaultDownloadFileName] - Desired download file name.\n */\n downloadImage(fileName = this.options.defaultDownloadFileName) {\n if (!this.originalImage) return;\n const exportImageArea = this.options.exportImageAreaByDefault;\n this.getImageBase64({ exportImageArea, multiplier: this.options.exportMultiplier })\n .then(base64 => {\n const link = document.createElement('a');\n link.download = fileName;\n link.href = base64;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n })\n .catch(err => console.error('download error', err));\n }\n\n /**\n * Exports the image as a Base64-encoded JPEG.\n * Can export either the original, or the current view including masks (clipped/cropped).\n * Will restore masks' state after temporary modifications for export.\n * @async\n * @param {Object} [opts={}] - Export options.\n * @param {boolean} [opts.exportImageArea] - If true, exports only the image bounding area with masks cropped and blended.\n * @param {number} [opts.multiplier=1] - Scaling multiplier for output (resolution).\n * @returns {Promise<string>} Promise resolving to a JPEG image data URL.\n * @throws {Error} If there is no image loaded.\n */\n async getImageBase64(opts = {}) {\n if (!this.originalImage) throw new Error('No image loaded');\n const exportImageArea = typeof opts.exportImageArea === 'boolean' ? opts.exportImageArea : this.options.exportImageAreaByDefault;\n const multiplier = opts.multiplier || this.options.exportMultiplier || 1;\n\n if (!exportImageArea) {\n // Export original image pixels\n const imgEl = this.originalImage.getElement ? this.originalImage.getElement() : (this.originalImage._element || null);\n if (!imgEl) return this.canvas.toDataURL({ format: 'jpeg', quality: this.options.downsampleQuality, multiplier });\n const w = this.originalImage.width;\n const h = this.originalImage.height;\n const oc = document.createElement('canvas');\n oc.width = w;\n oc.height = h;\n const ctx = oc.getContext('2d');\n ctx.drawImage(imgEl, 0, 0, w, h);\n return oc.toDataURL('image/jpeg', this.options.downsampleQuality);\n }\n\n // Export current scaled image area (masks clipped)\n const masks = this.canvas.getObjects().filter(o => o.maskId);\n const masksBackup = masks.map(m => ({\n obj: m,\n opacity: m.opacity,\n fill: m.fill,\n strokeWidth: m.strokeWidth,\n stroke: m.stroke,\n selectable: m.selectable,\n lockRotation: m.lockRotation\n }));\n\n // Remove labels, deselect\n masks.forEach(m => this._removeLabelForMask(m));\n this.canvas.discardActiveObject();\n this.canvas.renderAll();\n\n // Set masks to opaque black no border\n masks.forEach(m => {\n m.set({ opacity: 1, fill: '#000000', strokeWidth: 0, stroke: null, selectable: false });\n m.setCoords();\n });\n this.canvas.renderAll();\n\n // Compute integer bounding box for image\n this.originalImage.setCoords();\n const imgBr = this.originalImage.getBoundingRect(true, true);\n const sx = Math.max(0, Math.round(imgBr.left));\n const sy = Math.max(0, Math.round(imgBr.top));\n const sw = Math.max(1, Math.round(imgBr.width));\n const sh = Math.max(1, Math.round(imgBr.height));\n\n // Crop precisely in offscreen canvas\n const finalBase64 = await new Promise((resolve, reject) => {\n try {\n const fullDataUrl = this.canvas.toDataURL({\n format: 'jpeg',\n quality: this.options.downsampleQuality,\n multiplier: multiplier\n });\n\n const img = new Image();\n img.onload = () => {\n try {\n const sxM = Math.round(sx * multiplier);\n const syM = Math.round(sy * multiplier);\n const swM = Math.round(sw * multiplier);\n const shM = Math.round(sh * multiplier);\n\n const oc = document.createElement('canvas');\n oc.width = swM;\n oc.height = shM;\n const ctx = oc.getContext('2d');\n\n ctx.drawImage(img, sxM, syM, swM, shM, 0, 0, swM, shM);\n const out = oc.toDataURL('image/jpeg', this.options.downsampleQuality);\n resolve(out);\n } catch (e) { reject(e); }\n };\n img.onerror = reject;\n img.src = fullDataUrl;\n } catch (e) { reject(e); }\n });\n\n // Restore masks\n masksBackup.forEach(b => {\n try {\n b.obj.set({\n opacity: b.opacity,\n fill: b.fill,\n strokeWidth: b.strokeWidth,\n stroke: b.stroke,\n selectable: b.selectable,\n lockRotation: b.lockRotation\n });\n b.obj.setCoords();\n } catch (e) { }\n });\n\n this.canvas.renderAll();\n return finalBase64;\n }\n\n /**\n * Exports the current canvas (with or without masks) as a File object.\n * Allows you to choose whether to merge masks and specify file type (jpeg/png/webp).\n * \n * @async\n * @param {Object} [opts={}] - Export options.\n * @param {boolean} [opts.mergeMask=true] - If true, export image area with masks merged; if false, export the plain image without masks.\n * @param {string} [opts.fileType='jpeg'] - Output file type ('jpeg' | 'png' | 'webp'). Defaults to 'jpeg' on invalid input.\n * @param {number} [opts.quality=0.92] - Image quality for lossy types (0-1, default based on options.downsampleQuality).\n * @param {number} [opts.multiplier=1] - Output resolution multiplier.\n * @param {string} [opts.fileName] - Optional file name (only used for download).\n * @returns {Promise<File>} Resolves with the exported image as a File object.\n * \n * @example\n * const file = await this.exportImageFile({ mergeMask: false, fileType: 'png' });\n */\n async exportImageFile(opts = {}) {\n if (!this.originalImage) throw new Error('No image loaded');\n const {\n mergeMask = true,\n fileType = 'jpeg',\n quality = this.options.downsampleQuality ?? 0.92,\n multiplier = this.options.exportMultiplier ?? 1,\n fileName = this.options.defaultDownloadFileName ?? 'exported_image.jpg'\n } = opts;\n\n const typeMapping = {\n 'jpeg': 'jpeg',\n 'jpg': 'jpeg',\n 'image/jpeg': 'jpeg',\n 'png': 'png',\n 'image/png': 'png',\n 'webp': 'webp',\n 'image/webp': 'webp'\n };\n const safeFileType = typeMapping[String(fileType).toLowerCase()] || 'jpeg';\n\n // Get Base64\n let base64;\n if (mergeMask) {\n base64 = await this.getImageBase64({\n exportImageArea: true,\n multiplier,\n });\n } else {\n base64 = await this.getImageBase64({\n exportImageArea: false,\n multiplier,\n });\n }\n\n // Convert to the required image format\n let imageDataUrl = base64;\n if (!imageDataUrl.startsWith(`data:image/${safeFileType}`)) {\n // Redraw if not required format\n imageDataUrl = await new Promise((resolve, reject) => {\n const img = new window.Image();\n img.crossOrigin = \"Anonymous\";\n img.onload = () => {\n try {\n const oc = document.createElement('canvas');\n oc.width = img.width;\n oc.height = img.height;\n const ctx = oc.getContext('2d');\n ctx.drawImage(img, 0, 0);\n const durl = oc.toDataURL(`image/${safeFileType}`, quality);\n resolve(durl);\n } catch (e) { reject(e); }\n };\n img.onerror = reject;\n img.src = base64;\n });\n }\n\n // Convert DataURL to Blob and then to File\n const bstr = atob(imageDataUrl.split(',')[1]);\n const mime = `image/${safeFileType}`;\n let n = bstr.length;\n const u8arr = new Uint8Array(n);\n while (n--) {\n u8arr[n] = bstr.charCodeAt(n);\n }\n const file = new File([u8arr], fileName, { type: mime });\n return file;\n }\n\n /* ---------- Misc / UI ---------- */\n\n /**\n * Updates the scale input field in the UI to reflect the current scale.\n * Sets the value (as percentage) if the element is present.\n * @private\n */\n _updateInputs() {\n const scaleEl = document.getElementById(this.elements.scaleRate);\n if (scaleEl) scaleEl.value = Math.round(this.currentScale * 100);\n }\n\n /**\n * Updates the enabled/disabled state of various UI controls (buttons)\n * based on the current application state (image/mask presence, animation, etc).\n * @private\n */\n _updateUI() {\n const hasImg = !!this.originalImage;\n const masks = hasImg ? this.canvas.getObjects().filter(o => o.maskId) : [];\n const hasMasks = masks.length > 0;\n const active = this.canvas.getActiveObject();\n const hasSelectedMask = active && active.maskId;\n const isDefault = this.currentScale === 1 && this.currentRotation === 0;\n const canUndo = this.historyManager?.canUndo();\n const canRedo = this.historyManager?.canRedo();\n\n this._setDisabled('zoomInBtn', !hasImg || this.isAnimating || this.currentScale >= this.options.maxScale);\n this._setDisabled('zoomOutBtn', !hasImg || this.isAnimating || this.currentScale <= this.options.minScale);\n this._setDisabled('addMaskBtn', !hasImg || this.isAnimating);\n this._setDisabled('removeMaskBtn', !hasSelectedMask || this.isAnimating);\n this._setDisabled('removeAllMasksBtn', !hasMasks || this.isAnimating);\n this._setDisabled('mergeBtn', !hasImg || !hasMasks || this.isAnimating);\n this._setDisabled('downloadBtn', !hasImg || this.isAnimating);\n this._setDisabled('resetBtn', !hasImg || isDefault || this.isAnimating);\n this._setDisabled('undoBtn', !hasImg || this.isAnimating || !canUndo);\n this._setDisabled('redoBtn', !hasImg || this.isAnimating || !canRedo);\n }\n\n /**\n * Enables or disables a specific UI element (typically a button) by its key.\n * \n * @param {string} key - Key of the element in this.elements (e.g. 'zoomInBtn').\n * @param {boolean} disabled - If true, disables the element; otherwise enables.\n * @private\n */\n _setDisabled(key, disabled) {\n const el = document.getElementById(this.elements[key]);\n if (el) el.disabled = !!disabled;\n }\n\n /**\n * Automatically display and hide placeholders and containers based on the current image content\n * @private\n */\n _updatePlaceholderStatus() {\n if (!this.options.showPlaceholder) return;\n this._setPlaceholderVisible(!this.originalImage);\n }\n\n /**\n * Controls the display/hiding of the Placeholder and Canvas container.\n * @param {boolean} show - true displays the placeholder, false displays the canvas container\n * @private\n */\n _setPlaceholderVisible(show) {\n if (!this.placeholderEl) return;\n if (show) {\n this.placeholderEl.classList.remove('d-none');\n this.placeholderEl.classList.add('d-flex');\n this.containerEl.classList.add('d-none');\n } else {\n this.placeholderEl.classList.remove('d-flex');\n this.placeholderEl.classList.add('d-none');\n this.containerEl.classList.remove('d-none');\n }\n }\n\n /**\n * Cleans up and disposes of the canvas and related references.\n * Call this method to free memory and remove canvas listeners when the editor is no longer needed.\n * @public\n */\n dispose() {\n // Remove bound DOM event listeners\n try {\n for (const key in (this._boundHandlers || {})) {\n const handlers = this._boundHandlers[key] || [];\n const el = document.getElementById(this.elements[key]);\n if (!el) continue;\n handlers.forEach(h => {\n try { el.removeEventListener(h.event, h.handler); } catch (e) { }\n });\n }\n } catch (e) { }\n\n if (this.canvas) {\n try { this.canvas.dispose(); } catch (e) { }\n this.canvas = null;\n this.canvasEl = null;\n this.isImageLoadedToCanvas = false;\n }\n this._boundHandlers = {};\n }\n }\n\n /**\n * A simple FIFO queue that guarantees animations are executed sequentially.\n * @class AnimationQueue\n */\n class AnimationQueue {\n /**\n * Creates a new AnimationQueue.\n *\n * @constructor\n */\n constructor() {\n /**\n * Internal queue holding animation descriptors.\n * @type {Array<{fn: Function, resolve: Function, reject: Function}>}\n */\n this.queue = [];\n /**\n * Flag indicating whether an animation is currently running.\n * @type {boolean}\n */\n this.running = false;\n }\n\n /**\n * Adds an animation function to the queue.\n *\n * @param {Function} animationFn A function that returns a Promise or any await-able.\n * @returns {Promise<*>} A Promise that resolves/rejects with the animation result.\n */\n async add(animationFn) {\n return new Promise((resolve, reject) => {\n // Push the animation into the queue.\n this.queue.push({ fn: animationFn, resolve, reject });\n // Start processing if it's not already running.\n if (!this.running) {\n this.processQueue();\n }\n });\n }\n\n /**\n * Internal helper that processes the animation queue sequentially until it is empty.\n *\n * @private\n * @returns {Promise<void>}\n */\n async processQueue() {\n if (this.queue.length === 0) {\n this.running = false;\n return;\n }\n\n this.running = true;\n const { fn, resolve, reject } = this.queue.shift();\n\n try {\n const result = await fn();\n resolve(result);\n } catch (error) {\n reject(error);\n }\n\n this.processQueue();\n }\n }\n\n /**\n * Command object encapsulating an executable action and its corresponding undo operation.\n * @class Command\n */\n class Command {\n /**\n * @param {Function} execute The function that performs the action.\n * @param {Function} undo The function that reverts the action.\n */\n constructor(execute, undo) {\n /**\n * Executes the command.\n * @type {Function}\n */\n this.execute = execute;\n /**\n * Undoes the command.\n * @type {Function}\n */\n this.undo = undo;\n }\n }\n\n /**\n * Manages a history of Command objects enabling undo/redo functionality.\n * @class HistoryManager\n */\n class HistoryManager {\n /**\n * @param {number} [maxSize=50] Maximum number of commands to keep in history.\n */\n constructor(maxSize = 50) {\n this.history = [];\n this.currentIndex = -1;\n this.maxSize = maxSize;\n }\n\n /**\n * Executes a new command and pushes it onto the history stack.\n * Truncates any \"future\" history when branching.\n *\n * @param {Command} command The command to execute.\n * @returns {void}\n */\n execute(command) {\n // Perform the command.\n command.execute();\n\n // Remove any commands that are ahead of the current index.\n if (this.currentIndex < this.history.length - 1) {\n this.history = this.history.slice(0, this.currentIndex + 1);\n }\n\n // Add the new command.\n this.history.push(command);\n\n // Maintain the max size of the buffer.\n if (this.history.length > this.maxSize) {\n this.history.shift(); // Remove the oldest command.\n } else {\n this.currentIndex++;\n }\n }\n\n /**\n * Checks whether an undo operation is possible.\n *\n * @returns {boolean} True if undo can be performed.\n */\n canUndo() {\n return this.currentIndex >= 0;\n }\n\n /**\n * Checks whether a redo operation is possible.\n *\n * @returns {boolean} True if redo can be performed.\n */\n canRedo() {\n return this.currentIndex < this.history.length - 1;\n }\n\n /**\n * Undoes the last executed command if possible.\n *\n * @returns {void}\n */\n undo() {\n if (this.currentIndex >= 0) {\n this.history[this.currentIndex].undo();\n this.currentIndex--;\n }\n }\n\n /**\n * Redoes the next command in history if possible.\n *\n * @returns {void}\n */\n redo() {\n if (this.currentIndex < this.history.length - 1) {\n this.currentIndex++;\n this.history[this.currentIndex].execute();\n }\n }\n }\n\n return ImageEditor\n})\n"],
5
+ "mappings": "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",
6
+ "names": ["require_image_editor", "__commonJSMin", "exports", "module", "root", "factory", "ImageEditor", "options", "mask", "maskIndex", "AnimationQueue", "HistoryManager", "idMap", "defaults", "canvasEl", "ce", "initialW", "initialH", "cw", "ch", "e", "inputEl", "f", "rotLeftBtn", "rotRightBtn", "el", "step", "p", "key", "event", "handler", "file", "reader", "base64", "imgEl", "loadSrc", "ratio", "tw", "th", "fimg", "imgW", "imgH", "minW", "minH", "fitScale", "dataURL", "res", "rej", "img", "w", "h", "quality", "oc", "iw", "ih", "obj", "coords", "br", "originX", "originY", "refPoint", "dx", "dy", "containerW", "containerH", "newW", "newH", "factor", "targetAbs", "topLeft", "p1", "p2", "deg", "degrees", "center", "newTopLeft", "o", "err", "jsonString", "json", "objs", "masks", "max", "m", "activeObj", "after", "before", "executedOnce", "cmd", "Command", "config", "shapeType", "cfg", "firstOffset", "left", "top", "resolveValue", "val", "fallback", "percent", "prev", "prevRight", "requiredW", "requiredH", "polyPoints", "pt", "normalStyle", "hoverStyle", "active", "textObj", "txt", "textOptions", "l", "tl", "vx", "vy", "dist", "ux", "uy", "offset", "px", "py", "selected", "selectedMask", "listEl", "li", "item", "isSelected", "merged", "fileName", "exportImageArea", "link", "opts", "multiplier", "masksBackup", "imgBr", "sx", "sy", "sw", "sh", "finalBase64", "resolve", "reject", "fullDataUrl", "sxM", "syM", "swM", "shM", "out", "b", "mergeMask", "fileType", "safeFileType", "imageDataUrl", "durl", "bstr", "mime", "n", "u8arr", "scaleEl", "hasImg", "hasMasks", "hasSelectedMask", "isDefault", "canUndo", "canRedo", "disabled", "show", "handlers", "animationFn", "fn", "result", "error", "execute", "undo", "maxSize", "command"]
7
+ }
@@ -0,0 +1,14 @@
1
+ var ImageEditor=(()=>{var B=(b,f)=>()=>(f||b((f={exports:{}}).exports,f),f.exports);var S=B((w,y)=>{/**
2
+ * @file image-editor.js
3
+ * @module image-editor
4
+ * @version 1.0.0
5
+ * @author Ben Situ
6
+ * @license MIT
7
+ * @description Lightweight canvas-based image editor with masking/transform/export support.
8
+ *
9
+ * This source file is free software, available under the MIT license.
10
+ * It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
11
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12
+ * See the license files for details.
13
+ */(function(b,f){typeof define=="function"&&define.amd?define([],f):typeof y=="object"&&y.exports?y.exports=f():b.ImageEditor=f()})(typeof self<"u"?self:w,function(){"use strict";class b{constructor(t={}){this._fabricLoaded=typeof fabric<"u",this._fabricLoaded||console.error("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted."),this.options={canvasWidth:800,canvasHeight:600,backgroundColor:"#ffffff",animationDuration:300,minScale:.1,maxScale:5,scaleStep:.05,rotationStep:90,expandCanvasToImage:!0,fitImageToCanvas:!1,downsampleOnLoad:!0,downsampleMaxWidth:4e3,downsampleMaxHeight:3e3,downsampleQuality:.92,exportMultiplier:1,exportImageAreaByDefault:!0,defaultMaskWidth:50,defaultMaskHeight:80,maskRotatable:!1,maskLabelOnSelect:!0,maskLabelOffset:3,maskName:"mask",groupSelection:!1,showPlaceholder:!0,initialImageBase64:null,defaultDownloadFileName:"edited_image.jpg",...t},this.options.label={getText:(i,e)=>i.maskName,textOptions:{fontSize:12,fill:"#fff",backgroundColor:"rgba(0,0,0,0.7)",padding:2,fontFamily:"monospace",fontWeight:"bold",selectable:!1,evented:!1,originX:"left",originY:"top"}},this.canvas=null,this.canvasEl=null,this.containerEl=null,this.placeholderEl=null,this.originalImage=null,this.baseImageScale=1,this.currentScale=1,this.currentRotation=0,this.maskCounter=0,this.isAnimating=!1,this.elements={},this.isImageLoadedToCanvas=!1,this.maxHistorySize=50,this._boundHandlers={},this._lastMaskInitialLeft=null,this._lastMaskInitialTop=null,this._lastMaskInitialWidth=null,this.onImageLoaded=typeof t.onImageLoaded=="function"?t.onImageLoaded:null,this.animQueue=new f,this.historyManager=new L(this.maxHistorySize)}init(t={}){if(!this._fabricLoaded)return;let i={canvas:"fabricCanvas",canvasContainer:null,imgPlaceholder:"imgPlaceholder",scaleRate:"scaleRate",rotationLeftInput:"rotationLeftInput",rotationRightInput:"rotationRightInput",rotateLeftBtn:"rotateLeftBtn",rotateRightBtn:"rotateRightBtn",addMaskBtn:"addMaskBtn",removeMaskBtn:"removeMaskBtn",removeAllMasksBtn:"removeAllMasksBtn",mergeBtn:"mergeBtn",downloadBtn:"downloadBtn",maskList:"maskList",zoomInBtn:"zoomInBtn",zoomOutBtn:"zoomOutBtn",resetBtn:"resetBtn",undoBtn:"undoBtn",redoBtn:"redoBtn",imageInput:"imageInput"};this.elements={...i,...t},this._initCanvas(),this._bindEvents(),this._updateInputs(),this._updateMaskList(),this._updateUI(),this.options.initialImageBase64?this.loadImage(this.options.initialImageBase64):this._updatePlaceholderStatus()}_initCanvas(){let t=document.getElementById(this.elements.canvas);if(!t)throw new Error("Canvas is not found: "+this.elements.canvas);if(this.canvasEl=t,this.elements.canvasContainer){let s=document.getElementById(this.elements.canvasContainer);this.containerEl=s||t.parentElement}else this.containerEl=t.parentElement;this.placeholderEl=document.getElementById(this.elements.imgPlaceholder)||null;let i=this.options.canvasWidth,e=this.options.canvasHeight;if(this.containerEl){let s=Math.floor(this.containerEl.clientWidth),a=Math.floor(this.containerEl.clientHeight);s>0&&a>0&&(i=s,e=a)}this.canvas=new fabric.Canvas(t,{width:i,height:e,backgroundColor:this.options.backgroundColor,selection:this.options.groupSelection,preserveObjectStacking:!0}),this.canvas.on("selection:created",s=>this._onSelectionChanged(s.selected)),this.canvas.on("selection:updated",s=>this._onSelectionChanged(s.selected)),this.canvas.on("selection:cleared",()=>this._onSelectionChanged([])),this.canvas.on("object:moving",s=>{s.target&&s.target.maskId&&this._syncMaskLabel(s.target)}),this.canvas.on("object:scaling",s=>{s.target&&s.target.maskId&&this._syncMaskLabel(s.target)}),this.canvas.on("object:rotating",s=>{s.target&&s.target.maskId&&this._syncMaskLabel(s.target)}),this.canvas.on("object:modified",s=>{s.target&&s.target.maskId&&this._syncMaskLabel(s.target)}),this.canvasEl.style.display="block"}_bindEvents(){this._bindIfExists("uploadArea","click",()=>document.getElementById(this.elements.imageInput)?.click());let t=document.getElementById(this.elements.imageInput);t&&t.addEventListener("change",s=>{let a=s.target.files&&s.target.files[0];a&&this._loadImageFile(a)}),this._bindIfExists("zoomInBtn","click",()=>this.scaleImage(this.currentScale+this.options.scaleStep)),this._bindIfExists("zoomOutBtn","click",()=>this.scaleImage(this.currentScale-this.options.scaleStep)),this._bindIfExists("resetBtn","click",()=>{this.reset()}),this._bindIfExists("addMaskBtn","click",()=>this.addMask()),this._bindIfExists("removeMaskBtn","click",()=>this.removeSelectedMask()),this._bindIfExists("removeAllMasksBtn","click",()=>this.removeAllMasks()),this._bindIfExists("mergeBtn","click",()=>this.merge()),this._bindIfExists("downloadBtn","click",()=>this.downloadImage()),this._bindIfExists("undoBtn","click",()=>this.undo()),this._bindIfExists("redoBtn","click",()=>this.redo());let i=document.getElementById(this.elements.rotateLeftBtn),e=document.getElementById(this.elements.rotateRightBtn);i&&i.addEventListener("click",()=>{let s=document.getElementById(this.elements.rotationLeftInput),a=this.options.rotationStep;if(s){let o=parseFloat(s.value);isNaN(o)||(a=o)}this.rotateImage(this.currentRotation-a)}),e&&e.addEventListener("click",()=>{let s=document.getElementById(this.elements.rotationRightInput),a=this.options.rotationStep;if(s){let o=parseFloat(s.value);isNaN(o)||(a=o)}this.rotateImage(this.currentRotation+a)})}_bindIfExists(t,i,e){let s=document.getElementById(this.elements[t]);s&&(s.addEventListener(i,e),this._boundHandlers=this._boundHandlers||{},this._boundHandlers[t]||(this._boundHandlers[t]=[]),this._boundHandlers[t].push({event:i,handler:e}))}_loadImageFile(t){if(!t||!t.type.startsWith("image/"))return;let i=new FileReader;i.onload=e=>this.loadImage(e.target.result),i.onerror=e=>{console.error("[ImageEditor: fileReadError]",e)},i.readAsDataURL(t)}async loadImage(t){if(!this._fabricLoaded||!t||typeof t!="string"||!t.startsWith("data:image/"))return;this._setPlaceholderVisible(!1);let i=await this._createImageElement(t),e=t;if(this.options.downsampleOnLoad&&(i.naturalWidth>this.options.downsampleMaxWidth||i.naturalHeight>this.options.downsampleMaxHeight)){let a=Math.min(this.options.downsampleMaxWidth/i.naturalWidth,this.options.downsampleMaxHeight/i.naturalHeight),o=Math.round(i.naturalWidth*a),h=Math.round(i.naturalHeight*a);e=this._resampleImageToDataURL(i,o,h,this.options.downsampleQuality)}fabric.Image.fromURL(e,s=>{this.canvas.discardActiveObject(),this._hideAllMaskLabels(),this.canvas.clear(),this.canvas.setBackgroundColor(this.options.backgroundColor,this.canvas.renderAll.bind(this.canvas)),s.set({originX:"left",originY:"top",selectable:!1,evented:!1});let a=s.width,o=s.height,h=this.containerEl?Math.floor(this.containerEl.clientWidth||this.options.canvasWidth):this.options.canvasWidth,n=this.containerEl?Math.floor(this.containerEl.clientHeight||this.options.canvasHeight):this.options.canvasHeight;if(this.options.fitImageToCanvas){let c=Math.max(this.options.canvasWidth,h),d=Math.max(this.options.canvasHeight,n);this._setCanvasSizeInt(c,d);let r=Math.min(c/a,d/o,1);s.set({left:(c-a*r)/2,top:(d-o*r)/2}),s.scale(r),this.baseImageScale=s.scaleX||1}else if(this.options.expandCanvasToImage){let c=Math.max(h,Math.floor(a)),d=Math.max(n,Math.floor(o));this._setCanvasSizeInt(c,d),s.set({left:0,top:0}),s.scale(1),this.baseImageScale=1}else{let c=Math.max(this.options.canvasWidth,h),d=Math.max(this.options.canvasHeight,n);this._setCanvasSizeInt(c,d);let r=Math.min(c/a,d/o,1);s.set({left:(c-a*r)/2,top:(d-o*r)/2}),s.scale(r),this.baseImageScale=s.scaleX||1}this.originalImage=s,this.canvas.add(s),this.canvas.sendToBack(s),this._lastMaskInitialLeft=null,this._lastMaskInitialTop=null,this._lastMaskInitialWidth=null,this.maskCounter=0,this.currentScale=1,this.currentRotation=0,this._updateInputs(),this._updateMaskList(),this._updateUI(),this.canvas.renderAll(),this.isImageLoadedToCanvas=!0,typeof this.onImageLoaded=="function"&&this.onImageLoaded()},{crossOrigin:"anonymous"})}isImageLoaded(){return!!(this.originalImage&&this.originalImage instanceof fabric.Image&&this.originalImage.width>0&&this.originalImage.height>0)}_createImageElement(t){return new Promise((i,e)=>{let s=new Image;s.onload=()=>{s.onload=null,s.onerror=null,i(s)},s.onerror=a=>{s.onload=null,s.onerror=null,e(a)},s.src=t})}_resampleImageToDataURL(t,i,e,s=.92){let a=document.createElement("canvas");return a.width=i,a.height=e,a.getContext("2d").drawImage(t,0,0,t.naturalWidth,t.naturalHeight,0,0,i,e),a.toDataURL("image/jpeg",s)}_setCanvasSizeInt(t,i){let e=Math.max(1,Math.round(Number(t)||1)),s=Math.max(1,Math.round(Number(i)||1));this.canvas.setWidth(e),this.canvas.setHeight(s),typeof this.canvas.calcOffset=="function"&&this.canvas.calcOffset(),this.canvasEl&&(this.canvasEl.style.width=e+"px",this.canvasEl.style.height=s+"px",this.canvasEl.style.maxWidth="none")}_getObjectTopLeftPoint(t){if(!t)return{x:0,y:0};t.setCoords();let i=typeof t.getCoords=="function"?t.getCoords():null;if(i&&i.length)return i[0];let e=t.getBoundingRect(!0,!0);return{x:e.left,y:e.top}}_setObjectOriginKeepingPosition(t,i,e,s){!t||!s||!t.setPositionByOrigin||(t.set({originX:i,originY:e}),t.setPositionByOrigin(s,i,e),t.setCoords())}_alignObjectBoundingBoxToCanvasTopLeft(t){if(!t)return;t.setCoords();let i=t.getBoundingRect(!0,!0),e=i.left,s=i.top;t.set({left:(t.left||0)-e,top:(t.top||0)-s}),t.setCoords(),this.canvas.renderAll()}_updateCanvasSizeToImageBounds(){if(!this.originalImage)return;this.originalImage.setCoords();let t=this.originalImage.getBoundingRect(!0,!0),i=this.containerEl?Math.ceil(this.containerEl.clientWidth||0):0,e=this.containerEl?Math.ceil(this.containerEl.clientHeight||0):0;if(i>0&&e>0&&t.width<=i&&t.height<=e){this._setCanvasSizeInt(i,e);return}let s=Math.max(i||0,Math.floor(t.width)),a=Math.max(e||0,Math.floor(t.height));this._setCanvasSizeInt(s,a)}scaleImage(t){return this.animQueue.add(()=>this._scaleImageImpl(t))}_scaleImageImpl(t){if(!this.originalImage||this.isAnimating)return Promise.resolve();t=Math.max(this.options.minScale,Math.min(this.options.maxScale,t)),this.currentScale=t,this.isAnimating=!0,this._updateUI();let i=this.baseImageScale*t,e=this._getObjectTopLeftPoint(this.originalImage);this._setObjectOriginKeepingPosition(this.originalImage,"left","top",e);let s=new Promise(o=>{this.originalImage.animate("scaleX",i,{duration:this.options.animationDuration,onChange:this.canvas.renderAll.bind(this.canvas),onComplete:o})}),a=new Promise(o=>{this.originalImage.animate("scaleY",i,{duration:this.options.animationDuration,onChange:this.canvas.renderAll.bind(this.canvas),onComplete:o})});return Promise.all([s,a]).then(()=>{this.originalImage.set({scaleX:i,scaleY:i}),this.originalImage.setCoords(),this.options.expandCanvasToImage&&this._updateCanvasSizeToImageBounds(),this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage),this.canvas.getObjects().forEach(o=>{o.maskId&&this._syncMaskLabel(o)}),this.isAnimating=!1,this._updateInputs(),this._updateUI(),this.saveState()}).catch(()=>{this.isAnimating=!1,this._updateUI()})}rotateImage(t){return this.animQueue.add(()=>this._rotateImageImpl(t))}_rotateImageImpl(t){if(!this.originalImage||this.isAnimating||isNaN(t))return Promise.resolve();this.currentRotation=t,this.isAnimating=!0,this._updateUI();let i=this.originalImage.getCenterPoint();return this._setObjectOriginKeepingPosition(this.originalImage,"center","center",i),new Promise(s=>{this.originalImage.animate("angle",t,{duration:this.options.animationDuration,onChange:this.canvas.renderAll.bind(this.canvas),onComplete:s})}).then(()=>{this.originalImage.set("angle",t),this.originalImage.setCoords(),this.options.expandCanvasToImage&&this._updateCanvasSizeToImageBounds(),this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);let s=this._getObjectTopLeftPoint(this.originalImage);this._setObjectOriginKeepingPosition(this.originalImage,"left","top",s),this.canvas.getObjects().forEach(a=>{a.maskId&&this._syncMaskLabel(a)}),this.isAnimating=!1,this._updateInputs(),this._updateUI(),this.saveState()}).catch(()=>{this.isAnimating=!1,this._updateUI()})}reset(){return this.originalImage?this.scaleImage(1).then(()=>this.rotateImage(0)).then(()=>{this.saveState()}).catch(t=>{console.error("reset() failed",t)}):Promise.resolve()}loadFromState(t){if(!(!t||!this.canvas))try{let i=typeof t=="string"?JSON.parse(t):t;this.canvas.loadFromJSON(i,()=>{this._hideAllMaskLabels();let e=this.canvas.getObjects();this.originalImage=e.find(a=>a.type==="image"&&!a.maskId)||null,this.originalImage.set({originX:"left",originY:"top",selectable:!1,evented:!1,hasControls:!1,hoverCursor:"default"}),this.canvas.sendToBack(this.originalImage);let s=e.filter(a=>a.maskId);this.maskCounter=s.reduce((a,o)=>Math.max(a,o.maskId),0),this.canvas.renderAll(),this._updateMaskList(),this._updateUI()})}catch(i){console.error("loadFromState() failed",i)}}saveState(){if(!this.canvas)return;let t=this.canvas.getActiveObject();this._hideAllMaskLabels();let i=JSON.stringify(this.canvas.toJSON(["maskId","maskName"])),e=this._lastSnapshot||i,s=!1,a=new E(()=>{s&&this.loadFromState(i),s=!0},()=>{this.loadFromState(e)});this.historyManager.execute(a),this._lastSnapshot=i,t&&t.maskId&&this._showLabelForMask(t),this._updateUI()}undo(){this.historyManager.undo()}redo(){this.historyManager.redo()}addMask(t={}){if(!this.canvas)return null;let i=t.shape||"rect",e={shape:i,width:this.options.defaultMaskWidth,height:this.options.defaultMaskHeight,color:"rgba(0,0,0,0.5)",alpha:.5,gap:5,left:void 0,top:void 0,angle:0,selectable:!0,...t},s=10,a=s,o=s,h=(r,l)=>{if(typeof r=="function")return r(this.canvas,this.options);if(typeof r=="string"&&r.endsWith("%")){let g=parseFloat(r)/100;return Math.floor((this.canvas?this.canvas.getWidth():0)*g)}return r??l};if(e.left===void 0&&this._lastMask){let r=this._lastMask,l=r.left;r.getScaledWidth?l+=r.getScaledWidth():r.width&&(l+=r.width*(r.scaleX??1)),a=Math.round(l+e.gap),o=r.top??s}else a=h(e.left,s),o=h(e.top,s);if(e.width=h(e.width,this.options.defaultMaskWidth),e.height=h(e.height,this.options.defaultMaskHeight),this.options.expandCanvasToImage&&i==="rect"){let r=Math.ceil(a+e.width+10),l=Math.ceil(o+e.height+10),g=this.containerEl?Math.floor(this.containerEl.clientWidth||0):0,u=this.containerEl?Math.floor(this.containerEl.clientHeight||0):0,m=Math.max(this.canvas.getWidth(),g,r),v=Math.max(this.canvas.getHeight(),u,l);this._setCanvasSizeInt(m,v)}let n;if(typeof e.fabricGenerator=="function")n=e.fabricGenerator(e,this.canvas,this.options);else switch(i){case"circle":n=new fabric.Circle({left:a,top:o,radius:h(e.radius,Math.min(e.width,e.height)/2),fill:e.color,opacity:e.alpha,angle:e.angle,...e.styles});break;case"ellipse":n=new fabric.Ellipse({left:a,top:o,rx:h(e.rx,e.width/2),ry:h(e.ry,e.height/2),fill:e.color,opacity:e.alpha,angle:e.angle,...e.styles});break;case"polygon":let r=e.points||[];Array.isArray(r)&&r.length&&typeof r[0]=="object"&&(r=r.map(l=>({x:Number(l.x),y:Number(l.y)}))),n=new fabric.Polygon(r,{left:a,top:o,fill:e.color,opacity:e.alpha,angle:e.angle,...e.styles});break;case"rect":default:n=new fabric.Rect({left:a,top:o,width:h(e.width,this.options.defaultMaskWidth),height:h(e.height,this.options.defaultMaskHeight),fill:e.color,opacity:e.alpha,angle:e.angle,rx:e.rx,ry:e.ry,...e.styles})}n.selectable=e.selectable!==!1,n.hasControls="hasControls"in e?e.hasControls:!0,n.lockRotation=!this.options.maskRotatable,n.borderColor=e.borderColor||"red",n.cornerColor=e.cornerColor||"black",n.cornerSize=e.cornerSize||8,n.transparentCorners="transparentCorners"in e?e.transparentCorners:!1,n.stroke=e.styles&&e.styles.stroke||"#ccc",n.strokeWidth=e.styles&&e.styles.strokeWidth||1,n.strokeUniform="strokeUniform"in e?e.strokeUniform:!0,e.styles&&e.styles.strokeDashArray&&(n.strokeDashArray=e.styles.strokeDashArray),n.originalAlpha=e.alpha;let c={stroke:n.stroke,strokeWidth:n.strokeWidth,opacity:n.originalAlpha},d={stroke:"#ff5500",strokeWidth:2,opacity:Math.min(n.originalAlpha+.2,1)};return n.on("mouseover",()=>{n.set(d),n.canvas.requestRenderAll()}),n.on("mouseout",()=>{n.set(c),n.canvas.requestRenderAll()}),this._lastMaskInitialLeft=a,this._lastMaskInitialTop=o,this._lastMaskInitialWidth=h(e.width,this.options.defaultMaskWidth),n.maskId=++this.maskCounter,n.maskName=`${this.options.maskName}${n.maskId}`,this._lastMask=n,this.canvas.add(n),this.canvas.bringToFront(n),e.selectable&&this.canvas.setActiveObject(n),this._onSelectionChanged([n]),this._updateMaskList(),this._updateUI(),this.canvas.renderAll(),this.saveState(),typeof e.onCreate=="function"&&e.onCreate(n,this.canvas),n}removeSelectedMask(){let t=this.canvas.getActiveObject();!t||!t.maskId||(this._removeLabelForMask(t),this.canvas.remove(t),this.canvas.discardActiveObject(),this._updateMaskList(),this._updateUI(),this.canvas.renderAll(),this.saveState())}removeAllMasks(){let t=this.canvas.getObjects().filter(i=>i.maskId);t.forEach(i=>this._removeLabelForMask(i)),t.forEach(i=>this.canvas.remove(i)),this.canvas.discardActiveObject(),this._lastMaskInitialLeft=null,this._lastMaskInitialTop=null,this._lastMaskInitialWidth=null,this._updateMaskList(),this._updateUI(),this.canvas.renderAll(),this.saveState()}_removeLabelForMask(t){if(!(!t||!this.canvas)&&t.__label){try{this.canvas.getObjects().includes(t.__label)&&this.canvas.remove(t.__label)}catch{}try{delete t.__label}catch{}}}_createLabelForMask(t){if(!t||!this.options.maskLabelOnSelect)return;this._removeLabelForMask(t);let i=null;if(this.options.label&&typeof this.options.label.create=="function"&&(i=this.options.label.create(t,fabric)),!i){let e=t.maskName,s={left:0,top:0,fontSize:12,fill:"#fff",backgroundColor:"rgba(0,0,0,0.7)",selectable:!1,evented:!1,padding:2,originX:"left",originY:"top"};this.options.label&&(typeof this.options.label.getText=="function"&&(e=this.options.label.getText(t,this.maskCounter)),this.options.label.textOptions&&Object.assign(s,this.options.label.textOptions)),i=new fabric.Text(e,s)}i.maskLabel=!0,t.__label=i,this.canvas.add(i),this.canvas.bringToFront(i),this._syncMaskLabel(t)}_hideAllMaskLabels(){if(!this.canvas)return;let t=this.canvas.getObjects();t.filter(e=>e.maskLabel).forEach(e=>{try{t.includes(e)&&this.canvas.remove(e)}catch{}}),t.forEach(e=>{if(e.maskId&&e.__label)try{delete e.__label}catch{}})}_syncMaskLabel(t){if(!t||!this.options.maskLabelOnSelect||!t.__label)return;let i=t.getCoords?t.getCoords():null;if(!i||i.length<4)return;let e=i[0],s=t.getCenterPoint(),a=s.x-e.x,o=s.y-e.y,h=Math.sqrt(a*a+o*o)||1,n=a/h,c=o/h,d=Math.max(0,this.options.maskLabelOffset??3),r=e.x+n*d,l=e.y+c*d;t.__label.set({left:Math.round(r),top:Math.round(l),angle:t.angle||0,originX:"left",originY:"top",visible:!0}),t.__label.setCoords(),this.canvas.renderAll()}_showLabelForMask(t){t&&this.options.maskLabelOnSelect&&(t.__label||this._createLabelForMask(t),t.__label.visible=!0,this._syncMaskLabel(t))}_onSelectionChanged(t){let i=(t||[]).find(s=>s.maskId);this.canvas.getObjects().filter(s=>s.maskId).forEach(s=>{if(s!==i){if(s.__label){try{this.canvas.remove(s.__label)}catch{}delete s.__label}s.set({stroke:"#ccc",strokeWidth:1})}else s.set({stroke:"#ff0000",strokeWidth:1})}),i&&this._showLabelForMask(i),this._updateMaskListSelection(i),this.canvas.renderAll(),this._updateUI()}_updateMaskList(){let t=document.getElementById(this.elements.maskList);if(!t)return;t.innerHTML="",this.canvas.getObjects().filter(e=>e.maskId).forEach(e=>{let s=document.createElement("li");s.className="list-group-item mask-item",s.textContent=e.maskName,s.onclick=()=>{this.canvas.setActiveObject(e),this._onSelectionChanged([e])},t.appendChild(s)})}_updateMaskListSelection(t){let i=document.getElementById(this.elements.maskList);if(!i)return;i.querySelectorAll(".mask-item").forEach(s=>{let a=!!t&&s.textContent===t.maskName;s.classList.toggle("active",a)})}async merge(){if(!(!this.originalImage||!this.canvas.getObjects().filter(i=>i.maskId).length)){this.canvas.discardActiveObject(),this.canvas.renderAll();try{let i=await this.getImageBase64({exportImageArea:!0,multiplier:this.options.exportMultiplier});this.removeAllMasks(),await this.loadImage(i),this.saveState()}catch(i){console.error("merge error",i),this.canvasEl&&(this.canvasEl.style.visibility="")}}}downloadImage(t=this.options.defaultDownloadFileName){if(!this.originalImage)return;let i=this.options.exportImageAreaByDefault;this.getImageBase64({exportImageArea:i,multiplier:this.options.exportMultiplier}).then(e=>{let s=document.createElement("a");s.download=t,s.href=e,document.body.appendChild(s),s.click(),document.body.removeChild(s)}).catch(e=>console.error("download error",e))}async getImageBase64(t={}){if(!this.originalImage)throw new Error("No image loaded");let i=typeof t.exportImageArea=="boolean"?t.exportImageArea:this.options.exportImageAreaByDefault,e=t.multiplier||this.options.exportMultiplier||1;if(!i){let l=this.originalImage.getElement?this.originalImage.getElement():this.originalImage._element||null;if(!l)return this.canvas.toDataURL({format:"jpeg",quality:this.options.downsampleQuality,multiplier:e});let g=this.originalImage.width,u=this.originalImage.height,m=document.createElement("canvas");return m.width=g,m.height=u,m.getContext("2d").drawImage(l,0,0,g,u),m.toDataURL("image/jpeg",this.options.downsampleQuality)}let s=this.canvas.getObjects().filter(l=>l.maskId),a=s.map(l=>({obj:l,opacity:l.opacity,fill:l.fill,strokeWidth:l.strokeWidth,stroke:l.stroke,selectable:l.selectable,lockRotation:l.lockRotation}));s.forEach(l=>this._removeLabelForMask(l)),this.canvas.discardActiveObject(),this.canvas.renderAll(),s.forEach(l=>{l.set({opacity:1,fill:"#000000",strokeWidth:0,stroke:null,selectable:!1}),l.setCoords()}),this.canvas.renderAll(),this.originalImage.setCoords();let o=this.originalImage.getBoundingRect(!0,!0),h=Math.max(0,Math.round(o.left)),n=Math.max(0,Math.round(o.top)),c=Math.max(1,Math.round(o.width)),d=Math.max(1,Math.round(o.height)),r=await new Promise((l,g)=>{try{let u=this.canvas.toDataURL({format:"jpeg",quality:this.options.downsampleQuality,multiplier:e}),m=new Image;m.onload=()=>{try{let v=Math.round(h*e),_=Math.round(n*e),p=Math.round(c*e),I=Math.round(d*e),k=document.createElement("canvas");k.width=p,k.height=I,k.getContext("2d").drawImage(m,v,_,p,I,0,0,p,I);let C=k.toDataURL("image/jpeg",this.options.downsampleQuality);l(C)}catch(v){g(v)}},m.onerror=g,m.src=u}catch(u){g(u)}});return a.forEach(l=>{try{l.obj.set({opacity:l.opacity,fill:l.fill,strokeWidth:l.strokeWidth,stroke:l.stroke,selectable:l.selectable,lockRotation:l.lockRotation}),l.obj.setCoords()}catch{}}),this.canvas.renderAll(),r}async exportImageFile(t={}){if(!this.originalImage)throw new Error("No image loaded");let{mergeMask:i=!0,fileType:e="jpeg",quality:s=this.options.downsampleQuality??.92,multiplier:a=this.options.exportMultiplier??1,fileName:o=this.options.defaultDownloadFileName??"exported_image.jpg"}=t,n={jpeg:"jpeg",jpg:"jpeg","image/jpeg":"jpeg",png:"png","image/png":"png",webp:"webp","image/webp":"webp"}[String(e).toLowerCase()]||"jpeg",c;i?c=await this.getImageBase64({exportImageArea:!0,multiplier:a}):c=await this.getImageBase64({exportImageArea:!1,multiplier:a});let d=c;d.startsWith(`data:image/${n}`)||(d=await new Promise((v,_)=>{let p=new window.Image;p.crossOrigin="Anonymous",p.onload=()=>{try{let I=document.createElement("canvas");I.width=p.width,I.height=p.height,I.getContext("2d").drawImage(p,0,0);let x=I.toDataURL(`image/${n}`,s);v(x)}catch(I){_(I)}},p.onerror=_,p.src=c}));let r=atob(d.split(",")[1]),l=`image/${n}`,g=r.length,u=new Uint8Array(g);for(;g--;)u[g]=r.charCodeAt(g);return new File([u],o,{type:l})}_updateInputs(){let t=document.getElementById(this.elements.scaleRate);t&&(t.value=Math.round(this.currentScale*100))}_updateUI(){let t=!!this.originalImage,e=(t?this.canvas.getObjects().filter(c=>c.maskId):[]).length>0,s=this.canvas.getActiveObject(),a=s&&s.maskId,o=this.currentScale===1&&this.currentRotation===0,h=this.historyManager?.canUndo(),n=this.historyManager?.canRedo();this._setDisabled("zoomInBtn",!t||this.isAnimating||this.currentScale>=this.options.maxScale),this._setDisabled("zoomOutBtn",!t||this.isAnimating||this.currentScale<=this.options.minScale),this._setDisabled("addMaskBtn",!t||this.isAnimating),this._setDisabled("removeMaskBtn",!a||this.isAnimating),this._setDisabled("removeAllMasksBtn",!e||this.isAnimating),this._setDisabled("mergeBtn",!t||!e||this.isAnimating),this._setDisabled("downloadBtn",!t||this.isAnimating),this._setDisabled("resetBtn",!t||o||this.isAnimating),this._setDisabled("undoBtn",!t||this.isAnimating||!h),this._setDisabled("redoBtn",!t||this.isAnimating||!n)}_setDisabled(t,i){let e=document.getElementById(this.elements[t]);e&&(e.disabled=!!i)}_updatePlaceholderStatus(){this.options.showPlaceholder&&this._setPlaceholderVisible(!this.originalImage)}_setPlaceholderVisible(t){this.placeholderEl&&(t?(this.placeholderEl.classList.remove("d-none"),this.placeholderEl.classList.add("d-flex"),this.containerEl.classList.add("d-none")):(this.placeholderEl.classList.remove("d-flex"),this.placeholderEl.classList.add("d-none"),this.containerEl.classList.remove("d-none")))}dispose(){try{for(let t in this._boundHandlers||{}){let i=this._boundHandlers[t]||[],e=document.getElementById(this.elements[t]);e&&i.forEach(s=>{try{e.removeEventListener(s.event,s.handler)}catch{}})}}catch{}if(this.canvas){try{this.canvas.dispose()}catch{}this.canvas=null,this.canvasEl=null,this.isImageLoadedToCanvas=!1}this._boundHandlers={}}}class f{constructor(){this.queue=[],this.running=!1}async add(t){return new Promise((i,e)=>{this.queue.push({fn:t,resolve:i,reject:e}),this.running||this.processQueue()})}async processQueue(){if(this.queue.length===0){this.running=!1;return}this.running=!0;let{fn:t,resolve:i,reject:e}=this.queue.shift();try{let s=await t();i(s)}catch(s){e(s)}this.processQueue()}}class E{constructor(t,i){this.execute=t,this.undo=i}}class L{constructor(t=50){this.history=[],this.currentIndex=-1,this.maxSize=t}execute(t){t.execute(),this.currentIndex<this.history.length-1&&(this.history=this.history.slice(0,this.currentIndex+1)),this.history.push(t),this.history.length>this.maxSize?this.history.shift():this.currentIndex++}canUndo(){return this.currentIndex>=0}canRedo(){return this.currentIndex<this.history.length-1}undo(){this.currentIndex>=0&&(this.history[this.currentIndex].undo(),this.currentIndex--)}redo(){this.currentIndex<this.history.length-1&&(this.currentIndex++,this.history[this.currentIndex].execute())}}return b})});return S();})();
14
+ //# sourceMappingURL=image-editor.min.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/image-editor.js"],
4
+ "sourcesContent": ["/**\n * @file image-editor.js\n * @module image-editor\n * @version 1.0.0\n * @author Ben Situ\n * @license MIT\n * @description Lightweight canvas-based image editor with masking/transform/export support.\n *\n * This source file is free software, available under the MIT license.\n * It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n * See the license files for details.\n */\n\n(function (root, factory) {\n if (typeof define === 'function' && define.amd) {\n // AMD / RequireJS\n define([], factory)\n } else if (typeof module === 'object' && module.exports) {\n // CommonJS / Node / webpack (target=commonjs)\n module.exports = factory()\n } else {\n // Browser normal <script> method, hanging to the global\n root.ImageEditor = factory()\n }\n})(typeof self !== 'undefined' ? self : this, function () {\n 'use strict'\n /**\n * ImageEditor\n * \n * A lightweight wrapper around fabric.js providing masking, scaling, rotation,\n * merging/export helpers, and UI integrations for image editing.\n *\n * <b>Note:</b> Requires fabric.js (v5.x) to be loaded on the page before use.\n *\n * <pre>\n * Example usage:\n * const editor = new ImageEditor({ canvasWidth: 1024, canvasHeight: 768 });\n * editor.init();\n * </pre>\n *\n * @class ImageEditor\n * @classdesc Fabric.js-based image editor with simple mask, transform, export and UI features.\n *\n * @param {Object} [options={}] - Customization options to override defaults.\n * @param {number} [options.canvasWidth=800] - The initial canvas width in pixels.\n * @param {number} [options.canvasHeight=600] - The initial canvas height in pixels.\n * @param {string} [options.backgroundColor='#ffffff'] - The canvas background color.\n * @param {number} [options.animationDuration=300] - Duration in ms for scale/rotate animations.\n * @param {number} [options.minScale=0.1] - Minimum image scaling factor.\n * @param {number} [options.maxScale=5.0] - Maximum image scaling factor.\n * @param {number} [options.scaleStep=0.05] - Scale increment/decrement per step.\n * @param {number} [options.rotationStep=90] - Rotation step in degrees.\n * @param {boolean} [options.expandCanvasToImage=true] - If true, expands the canvas to fit image/mask.\n * @param {boolean} [options.fitImageToCanvas=false] - If true, fits loaded image inside canvas.\n * @param {boolean} [options.downsampleOnLoad=true] - Whether to downsample very large images on load.\n * @param {number} [options.downsampleMaxWidth=4000] - Max width for downsampling.\n * @param {number} [options.downsampleMaxHeight=3000] - Max height for downsampling.\n * @param {number} [options.downsampleQuality=0.92] - JPEG quality for downsampling/export.\n * @param {number} [options.exportMultiplier=1] - Scale output image by this multiplier on export.\n * @param {boolean} [options.exportImageAreaByDefault=true] - Export only the image area (clipped to masks).\n * @param {number} [options.defaultMaskWidth=50] - Default width of new mask rectangles.\n * @param {number} [options.defaultMaskHeight=80] - Default height of new masks.\n * @param {boolean} [options.maskRotatable=false] - If true, masks can be rotated.\n * @param {boolean} [options.maskLabelOnSelect=true] - Show label on selected mask.\n * @param {number} [options.maskLabelOffset=3] - Offset for mask labels from top-left corner.\n * @param {string} [options.maskName='mask'] - Prefix for mask names/labels.\n * @param {boolean} [options.showPlaceholder=true] - If true, shows placeholder when no image is loaded.\n * @param {string|null} [options.initialImageBase64=null] - Base64 string to auto-load as initial image, if any.\n * @param {string} [options.defaultDownloadFileName='edited_image.jpg'] - Default file name for downloads.\n * @param {function} [options.onImageLoaded] - Optional callback to invoke after an image loads.\n * \n * @constructor\n */\n class ImageEditor {\n constructor(options = {}) {\n // Verify that fabric.js is present\n this._fabricLoaded = typeof fabric !== 'undefined';\n if (!this._fabricLoaded) {\n console.error('fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.');\n }\n // Default options (can be overridden via ctor param)\n this.options = {\n canvasWidth: 800,\n canvasHeight: 600,\n backgroundColor: '#ffffff',\n\n animationDuration: 300,\n minScale: 0.1,\n maxScale: 5.0,\n scaleStep: 0.05,\n rotationStep: 90,\n\n expandCanvasToImage: true,\n fitImageToCanvas: false,\n\n downsampleOnLoad: true,\n downsampleMaxWidth: 4000,\n downsampleMaxHeight: 3000,\n downsampleQuality: 0.92,\n\n exportMultiplier: 1,\n exportImageAreaByDefault: true,\n\n defaultMaskWidth: 50,\n defaultMaskHeight: 80,\n maskRotatable: false,\n maskLabelOnSelect: true,\n maskLabelOffset: 3,\n maskName: 'mask',\n\n groupSelection: false,\n\n showPlaceholder: true,\n initialImageBase64: null, // Provide a base64 'data:image/...' string here if you want auto-load\n\n defaultDownloadFileName: 'edited_image.jpg',\n\n ...options\n };\n this.options.label = {\n getText: (mask, maskIndex) => mask.maskName,\n textOptions: {\n fontSize: 12,\n fill: '#fff',\n backgroundColor: 'rgba(0,0,0,0.7)',\n padding: 2,\n fontFamily: \"monospace\",\n fontWeight: \"bold\",\n selectable: false,\n evented: false,\n originX: 'left',\n originY: 'top',\n }\n };\n\n // Runtime state\n this.canvas = null;\n this.canvasEl = null;\n this.containerEl = null;\n this.placeholderEl = null;\n\n this.originalImage = null; // fabric.Image\n this.baseImageScale = 1;\n this.currentScale = 1;\n this.currentRotation = 0;\n this.maskCounter = 0;\n this.isAnimating = false;\n this.elements = {};\n this.isImageLoadedToCanvas = false;\n this.maxHistorySize = 50;\n\n this._boundHandlers = {};\n\n this._lastMaskInitialLeft = null;\n this._lastMaskInitialTop = null;\n this._lastMaskInitialWidth = null;\n\n this.onImageLoaded = typeof options.onImageLoaded === 'function' ? options.onImageLoaded : null;\n\n this.animQueue = new AnimationQueue();\n this.historyManager = new HistoryManager(this.maxHistorySize);\n }\n\n /**\n * Initializes the editor, binds to DOM elements, sets up event handlers,\n * and (optionally) loads an initial image.\n * Use this method to set up the editor UI before interacting with it.\n *\n * @param {Object} [idMap={}] - Optional mapping from logical element names to actual DOM element IDs.\n * Supported keys include: canvas, canvasContainer, imgPlaceholder, scaleRate, rotationLeftInput, rotationRightInput,\n * rotateLeftBtn, rotateRightBtn, addMaskBtn, removeMaskBtn, removeAllMasksBtn, mergeBtn, downloadBtn, maskList,\n * zoomInBtn, zoomOutBtn, resetBtn, imageInput. Unknown keys are ignored.\n *\n * @returns {void}\n *\n * @public\n *\n * @example\n * editor.init({\n * canvas: 'myFabricCanvasId',\n * downloadBtn: 'myDownloadButtonId'\n * });\n */\n init(idMap = {}) {\n if (!this._fabricLoaded) return;\n\n const defaults = {\n canvas: 'fabricCanvas',\n canvasContainer: null, // Pass an ID here if you have a scrollable viewport container\n imgPlaceholder: 'imgPlaceholder',\n scaleRate: 'scaleRate',\n rotationLeftInput: 'rotationLeftInput',\n rotationRightInput: 'rotationRightInput',\n rotateLeftBtn: 'rotateLeftBtn',\n rotateRightBtn: 'rotateRightBtn',\n addMaskBtn: 'addMaskBtn',\n removeMaskBtn: 'removeMaskBtn',\n removeAllMasksBtn: 'removeAllMasksBtn',\n mergeBtn: 'mergeBtn',\n downloadBtn: 'downloadBtn',\n maskList: 'maskList',\n zoomInBtn: 'zoomInBtn',\n zoomOutBtn: 'zoomOutBtn',\n resetBtn: 'resetBtn',\n undoBtn: 'undoBtn',\n redoBtn: 'redoBtn',\n imageInput: 'imageInput'\n };\n\n this.elements = { ...defaults, ...idMap };\n\n this._initCanvas();\n this._bindEvents();\n this._updateInputs();\n this._updateMaskList();\n this._updateUI();\n\n // Auto-load initial image if provided\n if (this.options.initialImageBase64) {\n this.loadImage(this.options.initialImageBase64);\n } else {\n this._updatePlaceholderStatus();\n }\n }\n\n /**\n * Canvas setup helpers\n * @private\n */\n _initCanvas() {\n const canvasEl = document.getElementById(this.elements.canvas);\n if (!canvasEl) throw new Error('Canvas is not found: ' + this.elements.canvas);\n this.canvasEl = canvasEl;\n\n // Decide which element acts as \"viewport\" (for width/height fallback)\n if (this.elements.canvasContainer) {\n const ce = document.getElementById(this.elements.canvasContainer);\n this.containerEl = ce || canvasEl.parentElement;\n } else {\n this.containerEl = canvasEl.parentElement;\n }\n\n this.placeholderEl = document.getElementById(this.elements.imgPlaceholder) || null;\n\n // Initial size \u2014 take container size if available\n let initialW = this.options.canvasWidth;\n let initialH = this.options.canvasHeight;\n if (this.containerEl) {\n const cw = Math.floor(this.containerEl.clientWidth);\n const ch = Math.floor(this.containerEl.clientHeight);\n if (cw > 0 && ch > 0) { initialW = cw; initialH = ch; }\n }\n\n this.canvas = new fabric.Canvas(canvasEl, {\n width: initialW,\n height: initialH,\n backgroundColor: this.options.backgroundColor,\n selection: this.options.groupSelection,\n preserveObjectStacking: true\n });\n\n // Fabric event wiring\n this.canvas.on('selection:created', (e) => this._onSelectionChanged(e.selected));\n this.canvas.on('selection:updated', (e) => this._onSelectionChanged(e.selected));\n this.canvas.on('selection:cleared', () => this._onSelectionChanged([]));\n this.canvas.on('object:moving', (e) => { if (e.target && e.target.maskId) this._syncMaskLabel(e.target); });\n this.canvas.on('object:scaling', (e) => { if (e.target && e.target.maskId) this._syncMaskLabel(e.target); });\n this.canvas.on('object:rotating', (e) => { if (e.target && e.target.maskId) this._syncMaskLabel(e.target); });\n this.canvas.on('object:modified', (e) => { if (e.target && e.target.maskId) this._syncMaskLabel(e.target); });\n\n // Avoid inline-element whitespace artefacts\n this.canvasEl.style.display = 'block';\n }\n\n /** \n * DOM / UI bindings\n * @private\n */\n _bindEvents() {\n // Click anywhere on the upload area opens the native file dialog\n this._bindIfExists('uploadArea', 'click', () => document.getElementById(this.elements.imageInput)?.click());\n // File-input change\n const inputEl = document.getElementById(this.elements.imageInput);\n if (inputEl) {\n inputEl.addEventListener('change', (e) => {\n const f = e.target.files && e.target.files[0];\n if (f) this._loadImageFile(f);\n });\n }\n // Zoom & reset\n this._bindIfExists('zoomInBtn', 'click', () => this.scaleImage(this.currentScale + this.options.scaleStep));\n this._bindIfExists('zoomOutBtn', 'click', () => this.scaleImage(this.currentScale - this.options.scaleStep));\n this._bindIfExists('resetBtn', 'click', () => { this.reset(); });\n // Mask management\n this._bindIfExists('addMaskBtn', 'click', () => this.addMask());\n this._bindIfExists('removeMaskBtn', 'click', () => this.removeSelectedMask());\n this._bindIfExists('removeAllMasksBtn', 'click', () => this.removeAllMasks());\n // Merge + download\n this._bindIfExists('mergeBtn', 'click', () => this.merge());\n this._bindIfExists('downloadBtn', 'click', () => this.downloadImage());\n // Undo + Redo\n this._bindIfExists('undoBtn', 'click', () => this.undo());\n this._bindIfExists('redoBtn', 'click', () => this.redo());\n\n // Rotation buttons (step can be overridden by two input fields)\n const rotLeftBtn = document.getElementById(this.elements.rotateLeftBtn);\n const rotRightBtn = document.getElementById(this.elements.rotateRightBtn);\n if (rotLeftBtn) rotLeftBtn.addEventListener('click', () => {\n const el = document.getElementById(this.elements.rotationLeftInput);\n let step = this.options.rotationStep;\n if (el) { const p = parseFloat(el.value); if (!isNaN(p)) step = p; }\n this.rotateImage(this.currentRotation - step);\n });\n if (rotRightBtn) rotRightBtn.addEventListener('click', () => {\n const el = document.getElementById(this.elements.rotationRightInput);\n let step = this.options.rotationStep;\n if (el) { const p = parseFloat(el.value); if (!isNaN(p)) step = p; }\n this.rotateImage(this.currentRotation + step);\n });\n }\n\n /** \n * Event binding element check\n * \n * @param {*} event \n * @param {*} handler \n * @param {*} key \n * @private\n */\n _bindIfExists(key, event, handler) {\n const el = document.getElementById(this.elements[key]);\n if (el) {\n el.addEventListener(event, handler);\n this._boundHandlers = this._boundHandlers || {};\n if (!this._boundHandlers[key]) this._boundHandlers[key] = [];\n this._boundHandlers[key].push({ event, handler });\n }\n }\n\n /** \n * Image loading helpers\n * \n * @param {File} file \n * @private\n */\n _loadImageFile(file) {\n if (!file || !file.type.startsWith('image/')) return;\n const reader = new FileReader();\n reader.onload = (e) => this.loadImage(e.target.result);\n reader.onerror = (e) => { console.error(`[ImageEditor: fileReadError]`, e); }\n reader.readAsDataURL(file);\n }\n\n /**\n * Load a base64 encoded image string into fabric.\n * @async\n * @param {String} base64 \n */\n async loadImage(base64) {\n if (!this._fabricLoaded) return;\n if (!base64 || typeof base64 !== 'string' || !base64.startsWith('data:image/')) return;\n\n this._setPlaceholderVisible(false);\n\n const imgEl = await this._createImageElement(base64);\n\n let loadSrc = base64;\n if (this.options.downsampleOnLoad) {\n const needResize =\n imgEl.naturalWidth > this.options.downsampleMaxWidth ||\n imgEl.naturalHeight > this.options.downsampleMaxHeight;\n if (needResize) {\n const ratio = Math.min(\n this.options.downsampleMaxWidth / imgEl.naturalWidth,\n this.options.downsampleMaxHeight / imgEl.naturalHeight\n );\n const tw = Math.round(imgEl.naturalWidth * ratio);\n const th = Math.round(imgEl.naturalHeight * ratio);\n loadSrc = this._resampleImageToDataURL(imgEl, tw, th, this.options.downsampleQuality);\n }\n }\n\n // Create fabric.Image from URL\n fabric.Image.fromURL(loadSrc, (fimg) => {\n this.canvas.discardActiveObject();\n this._hideAllMaskLabels();\n this.canvas.clear();\n this.canvas.setBackgroundColor(this.options.backgroundColor, this.canvas.renderAll.bind(this.canvas));\n\n fimg.set({ originX: 'left', originY: 'top', selectable: false, evented: false });\n\n const imgW = fimg.width;\n const imgH = fimg.height;\n\n const minW = this.containerEl ? Math.floor(this.containerEl.clientWidth || this.options.canvasWidth) : this.options.canvasWidth;\n const minH = this.containerEl ? Math.floor(this.containerEl.clientHeight || this.options.canvasHeight) : this.options.canvasHeight;\n\n if (this.options.fitImageToCanvas) {\n // Fit into current canvas (shrink only)\n const cw = Math.max(this.options.canvasWidth, minW);\n const ch = Math.max(this.options.canvasHeight, minH);\n this._setCanvasSizeInt(cw, ch);\n const fitScale = Math.min(cw / imgW, ch / imgH, 1);\n fimg.set({ left: (cw - imgW * fitScale) / 2, top: (ch - imgH * fitScale) / 2 });\n fimg.scale(fitScale);\n this.baseImageScale = fimg.scaleX || 1;\n } else if (this.options.expandCanvasToImage) {\n // Expand canvas so that it fully contains the image\n const cw = Math.max(minW, Math.floor(imgW));\n const ch = Math.max(minH, Math.floor(imgH));\n this._setCanvasSizeInt(cw, ch);\n fimg.set({ left: 0, top: 0 });\n fimg.scale(1);\n this.baseImageScale = 1;\n } else {\n // Keep existing canvas size and center the image\n const cw = Math.max(this.options.canvasWidth, minW);\n const ch = Math.max(this.options.canvasHeight, minH);\n this._setCanvasSizeInt(cw, ch);\n const fitScale = Math.min(cw / imgW, ch / imgH, 1);\n fimg.set({ left: (cw - imgW * fitScale) / 2, top: (ch - imgH * fitScale) / 2 });\n fimg.scale(fitScale);\n this.baseImageScale = fimg.scaleX || 1;\n }\n // Put the image onto the canvas\n this.originalImage = fimg;\n this.canvas.add(fimg);\n this.canvas.sendToBack(fimg);\n\n // Reset mask placement memory\n this._lastMaskInitialLeft = null;\n this._lastMaskInitialTop = null;\n this._lastMaskInitialWidth = null;\n\n this.maskCounter = 0;\n this.currentScale = 1;\n this.currentRotation = 0;\n\n this._updateInputs();\n this._updateMaskList();\n this._updateUI();\n this.canvas.renderAll();\n this.isImageLoadedToCanvas = true;\n\n if (typeof this.onImageLoaded === 'function') {\n this.onImageLoaded();\n }\n }, { crossOrigin: 'anonymous' });\n }\n\n /**\n * Checks whether there is a loaded image on the current canvas.\n * @returns {boolean} true if loaded, false if not\n */\n isImageLoaded() {\n return !!(\n this.originalImage &&\n this.originalImage instanceof fabric.Image &&\n this.originalImage.width > 0 &&\n this.originalImage.height > 0\n );\n }\n\n /**\n * Creates an HTMLImageElement from a given data URL.\n * \n * @param {string} dataURL - A data URL representing the image (e.g., \"data:image/png;base64,...\").\n * @returns {Promise<HTMLImageElement>} A promise that resolves to the created image element when loaded, or rejects on error.\n * @private\n */\n _createImageElement(dataURL) {\n return new Promise((res, rej) => {\n const img = new Image();\n img.onload = () => {\n img.onload = null;\n img.onerror = null;\n res(img);\n };\n img.onerror = (e) => {\n img.onload = null;\n img.onerror = null;\n rej(e);\n };\n img.src = dataURL;\n });\n }\n\n /**\n * Resamples the given image element to a new width and height and returns the result as a JPEG data URL.\n * \n * @param {HTMLImageElement} imgEl - The image element to resample.\n * @param {number} w - Target width (in pixels) for the resampled image.\n * @param {number} h - Target height (in pixels) for the resampled image.\n * @param {number} [quality=0.92] - JPEG image quality between 0 and 1 (optional, default 0.92).\n * @returns {string} A data URL representing the resampled image as JPEG.\n * @private\n */\n _resampleImageToDataURL(imgEl, w, h, quality = 0.92) {\n const oc = document.createElement('canvas');\n oc.width = w;\n oc.height = h;\n const ctx = oc.getContext('2d');\n ctx.drawImage(imgEl, 0, 0, imgEl.naturalWidth, imgEl.naturalHeight, 0, 0, w, h);\n return oc.toDataURL('image/jpeg', quality);\n }\n\n /** \n * Sets canvas size to integer width and height values to prevent scrollbars due to sub-pixel rendering.\n * Also updates the corresponding style attributes.\n * \n * @param {number} w - Canvas width (in pixels).\n * @param {number} h - Canvas height (in pixels).\n * @private\n */\n _setCanvasSizeInt(w, h) {\n const iw = Math.max(1, Math.round(Number(w) || 1));\n const ih = Math.max(1, Math.round(Number(h) || 1));\n // Set fabric internal and also style attributes to keep DOM consistent\n this.canvas.setWidth(iw);\n this.canvas.setHeight(ih);\n if (typeof this.canvas.calcOffset === 'function') this.canvas.calcOffset();\n // Keep DOM element in sync (avoid fractional CSS pixels)\n if (this.canvasEl) {\n this.canvasEl.style.width = iw + 'px';\n this.canvasEl.style.height = ih + 'px';\n this.canvasEl.style.maxWidth = 'none';\n }\n }\n\n /** \n * Gets the top-left corner coordinates of the given object.\n * Used for geometry calculations (e.g., scale, rotate).\n * \n * @param {Object} obj - The object for which to get the top-left coordinates. Should support setCoords and getCoords/getBoundingRect methods.\n * @returns {{x: number, y: number}} The top-left corner point as an object with x and y properties.\n * @private\n */\n _getObjectTopLeftPoint(obj) {\n if (!obj) return { x: 0, y: 0 };\n obj.setCoords();\n const coords = typeof obj.getCoords === 'function' ? obj.getCoords() : null;\n if (coords && coords.length) return coords[0];\n const br = obj.getBoundingRect(true, true);\n return { x: br.left, y: br.top };\n }\n\n /**\n * Sets the object's origin at the specified origin point, keeping a reference point fixed in position.\n * \n * @param {Object} obj - The object to modify. Should support set, setPositionByOrigin, and setCoords.\n * @param {string} originX - The new originX (\"left\", \"center\", \"right\", etc.).\n * @param {string} originY - The new originY (\"top\", \"center\", \"bottom\", etc.).\n * @param {{x: number, y: number}} refPoint - The point to keep fixed while setting the new origins.\n * @private\n */\n _setObjectOriginKeepingPosition(obj, originX, originY, refPoint) {\n if (!obj || !refPoint || !obj.setPositionByOrigin) return;\n obj.set({ originX, originY });\n obj.setPositionByOrigin(refPoint, originX, originY);\n obj.setCoords();\n }\n\n /**\n * Moves the object so its bounding box aligns with the canvas's top-left corner (0, 0).\n * \n * @param {Object} obj - The object to align.\n * @private\n */\n _alignObjectBoundingBoxToCanvasTopLeft(obj) {\n if (!obj) return;\n obj.setCoords();\n const br = obj.getBoundingRect(true, true);\n const dx = br.left;\n const dy = br.top;\n obj.set({ left: (obj.left || 0) - dx, top: (obj.top || 0) - dy });\n obj.setCoords();\n this.canvas.renderAll();\n }\n\n /**\n * Updates the canvas size to match the bounding box of the original image,\n * ensuring that the canvas is always at least as large as its container.\n * @private\n */\n _updateCanvasSizeToImageBounds() {\n if (!this.originalImage) return;\n this.originalImage.setCoords();\n const br = this.originalImage.getBoundingRect(true, true);\n\n // Container integer sizes\n const containerW = this.containerEl ? Math.ceil(this.containerEl.clientWidth || 0) : 0;\n const containerH = this.containerEl ? Math.ceil(this.containerEl.clientHeight || 0) : 0;\n\n // If image smaller or equal than container in BOTH dims => keep canvas equal to container\n if (containerW > 0 && containerH > 0 && br.width <= containerW && br.height <= containerH) {\n this._setCanvasSizeInt(containerW, containerH);\n return;\n }\n\n // Else canvas follows image bounding box but not smaller than container dims individually\n const newW = Math.max(containerW || 0, Math.floor(br.width));\n const newH = Math.max(containerH || 0, Math.floor(br.height));\n this._setCanvasSizeInt(newW, newH);\n }\n\n /** \n * Scales the original image by a given factor, with animation.\n * Returns a promise that resolves when the scale animation is complete.\n * @param {number} factor - The scaling factor (will be clamped between `options.minScale` and `options.maxScale`).\n * @returns {Promise<void>} Promise that resolves once the scaling animation finishes.\n * @public\n */\n scaleImage(factor) {\n return this.animQueue.add(() => this._scaleImageImpl(factor));\n }\n\n /** \n * Scales the original image by a given factor, with animation.\n * Returns a promise that resolves when the scale animation is complete.\n * @param {number} factor - The scaling factor (will be clamped between `options.minScale` and `options.maxScale`).\n * @returns {Promise<void>} Promise that resolves once the scaling animation finishes.\n * @private\n */\n _scaleImageImpl(factor) {\n if (!this.originalImage) return Promise.resolve();\n if (this.isAnimating) return Promise.resolve();\n factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, factor));\n this.currentScale = factor;\n this.isAnimating = true;\n this._updateUI();\n\n const targetAbs = this.baseImageScale * factor;\n\n // Scale around current top-left (recompute)\n const topLeft = this._getObjectTopLeftPoint(this.originalImage);\n this._setObjectOriginKeepingPosition(this.originalImage, 'left', 'top', topLeft);\n\n const p1 = new Promise((res) => {\n this.originalImage.animate('scaleX', targetAbs, {\n duration: this.options.animationDuration,\n onChange: this.canvas.renderAll.bind(this.canvas),\n onComplete: res\n });\n });\n const p2 = new Promise((res) => {\n this.originalImage.animate('scaleY', targetAbs, {\n duration: this.options.animationDuration,\n onChange: this.canvas.renderAll.bind(this.canvas),\n onComplete: res\n });\n });\n\n return Promise.all([p1, p2]).then(() => {\n this.originalImage.set({ scaleX: targetAbs, scaleY: targetAbs });\n this.originalImage.setCoords();\n\n if (this.options.expandCanvasToImage) this._updateCanvasSizeToImageBounds();\n\n this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);\n\n // Sync mask labels\n this.canvas.getObjects().forEach(o => { if (o.maskId) this._syncMaskLabel(o); });\n\n this.isAnimating = false;\n this._updateInputs();\n this._updateUI();\n this.saveState();\n }).catch(() => {\n this.isAnimating = false;\n this._updateUI();\n });\n }\n\n /** \n * Rotates the original image by a given number of degrees, with animation.\n * Returns a promise that resolves when the rotation animation is complete.\n * @param {number} degrees - The angle in degrees to rotate the image.\n * @returns {Promise<void>} Promise that resolves once the rotation animation finishes.\n * @public\n */\n rotateImage(deg) {\n return this.animQueue.add(() => this._rotateImageImpl(deg));\n }\n\n /** \n * Rotates the original image by a given number of degrees, with animation.\n * Returns a promise that resolves when the rotation animation is complete.\n * @param {number} degrees - The angle in degrees to rotate the image.\n * @returns {Promise<void>} Promise that resolves once the rotation animation finishes.\n * @private\n */\n _rotateImageImpl(degrees) {\n if (!this.originalImage) return Promise.resolve();\n if (this.isAnimating) return Promise.resolve();\n if (isNaN(degrees)) return Promise.resolve();\n this.currentRotation = degrees;\n this.isAnimating = true;\n this._updateUI();\n\n const center = this.originalImage.getCenterPoint();\n this._setObjectOriginKeepingPosition(this.originalImage, 'center', 'center', center);\n\n const p = new Promise((res) => {\n this.originalImage.animate('angle', degrees, {\n duration: this.options.animationDuration,\n onChange: this.canvas.renderAll.bind(this.canvas),\n onComplete: res\n });\n });\n\n return p.then(() => {\n this.originalImage.set('angle', degrees);\n this.originalImage.setCoords();\n\n if (this.options.expandCanvasToImage) this._updateCanvasSizeToImageBounds();\n\n this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);\n\n const newTopLeft = this._getObjectTopLeftPoint(this.originalImage);\n this._setObjectOriginKeepingPosition(this.originalImage, 'left', 'top', newTopLeft);\n\n // Sync mask labels\n this.canvas.getObjects().forEach(o => { if (o.maskId) this._syncMaskLabel(o); });\n\n this.isAnimating = false;\n this._updateInputs();\n this._updateUI();\n this.saveState();\n }).catch(() => {\n this.isAnimating = false;\n this._updateUI();\n });\n }\n\n /**\n * Resets the image: scales to 1 and rotates to 0 degrees.\n * @returns {Promise<void>} Promise that resolves when reset is complete.\n */\n reset() {\n if (!this.originalImage) return Promise.resolve();\n\n return this.scaleImage(1)\n .then(() => this.rotateImage(0))\n .then(() => {\n this.saveState();\n })\n .catch(err => {\n console.error('reset() failed', err);\n });\n }\n\n /**\n * Restores a canvas state that was previously stored by saveState().\n * @param {string} jsonString - the JSON string returned by fabric.toJSON().\n */\n loadFromState(jsonString) {\n if (!jsonString || !this.canvas) return;\n\n try {\n const json = (typeof jsonString === 'string')\n ? JSON.parse(jsonString)\n : jsonString;\n\n this.canvas.loadFromJSON(json, () => {\n this._hideAllMaskLabels();\n const objs = this.canvas.getObjects();\n this.originalImage = objs.find(o => o.type === 'image' && !o.maskId) || null;\n\n this.originalImage.set({ originX: 'left', originY: 'top', selectable: false, evented: false, hasControls: false, hoverCursor: 'default' });\n this.canvas.sendToBack(this.originalImage);\n\n const masks = objs.filter(o => o.maskId);\n this.maskCounter = masks.reduce((max, m) =>\n Math.max(max, m.maskId), 0);\n\n this.canvas.renderAll();\n this._updateMaskList();\n this._updateUI();\n });\n\n } catch (e) {\n console.error('loadFromState() failed', e);\n }\n }\n\n /**\n * Saves the current state of the canvas to history, storing any mask/raster label information.\n */\n saveState() {\n if (!this.canvas) return;\n const activeObj = this.canvas.getActiveObject();\n this._hideAllMaskLabels();\n const after = JSON.stringify(this.canvas.toJSON(['maskId', 'maskName']));\n const before = this._lastSnapshot || after;\n let executedOnce = false;\n\n const cmd = new Command(\n () => {\n if (executedOnce) {\n // this.canvas.clear();\n this.loadFromState(after);\n }\n executedOnce = true;\n },\n () => {\n // this.canvas.clear();\n this.loadFromState(before);\n }\n );\n\n this.historyManager.execute(cmd);\n this._lastSnapshot = after;\n if (activeObj && activeObj.maskId) {\n this._showLabelForMask(activeObj);\n }\n this._updateUI();\n }\n\n /**\n * Undo the last state change, if possible.\n */\n undo() {\n this.historyManager.undo();\n }\n\n /**\n * Redo the next state change, if possible.\n */\n redo() {\n this.historyManager.redo();\n }\n\n /** \n * Adds a rectangular mask to the canvas.\n * Mask placement and properties are determined by the provided config and instance options.\n * Canvas and list UI are updated accordingly.\n * @param {Object} [config={}] - Optional mask configuration overrides:\n * @param {string} [config.shape='rect'] - 'rect', 'circle', 'ellipse', 'polygon', ...\n * @param {Object|Array} [config.points] - Required for polygon: [{x, y}, ...] or [[x, y], ...]\n * @param {number|function} [config.width/height/rx/ry/radius] - Can be number or function(canvas, options) \n * @param {number|string|function} [config.left/top] - Absolute, %, or function\n * @param {number|string} [config.angle] - Rotation angle (degree)\n * @param {string} [config.color] - Fill color in CSS color format (default 'rgba(0,0,0,0.5)')\n * @param {number} [config.alpha] - Opacity, from 0 to 1 (default 0.5)\n * @param {boolean} [config.selectable=true]\n * @param {Object} [config.styles] - Custom styles (stroke, dashArray, etc)\n * @param {function} [config.onCreate] - Callback after mask created (receives Fabric object)\n * @param {function} [config.fabricGenerator] - (cfg) => new FabricObj\n * @returns {fabric.Rect|null} The created mask object, or null if canvas is not available.\n * @public\n */\n addMask(config = {}) {\n if (!this.canvas) return null;\n const shapeType = config.shape || 'rect';\n // Default config\n const cfg = {\n shape: shapeType,\n width: this.options.defaultMaskWidth,\n height: this.options.defaultMaskHeight,\n color: 'rgba(0,0,0,0.5)',\n alpha: 0.5,\n gap: 5,\n left: undefined,\n top: undefined,\n angle: 0,\n selectable: true,\n ...config\n };\n\n // Always start placement relative to canvas left/top.\n const firstOffset = 10;\n let left = firstOffset;\n let top = firstOffset;\n\n const resolveValue = (val, fallback) => {\n if (typeof val === 'function')\n return val(this.canvas, this.options); // This context is this of addMask\n if (typeof val === 'string' && val.endsWith('%')) {\n const percent = parseFloat(val) / 100;\n return Math.floor((this.canvas ? this.canvas.getWidth() : 0) * percent);\n }\n return val != null ? val : fallback;\n }\n\n if (cfg.left === undefined && this._lastMask) {\n const prev = this._lastMask;\n let prevRight = prev.left;\n\n if (prev.getScaledWidth) {\n prevRight += prev.getScaledWidth();\n } else if (prev.width) {\n prevRight += prev.width * (prev.scaleX ?? 1);\n }\n left = Math.round(prevRight + cfg.gap);\n top = prev.top ?? firstOffset;\n } else {\n left = resolveValue(cfg.left, firstOffset);\n top = resolveValue(cfg.top, firstOffset);\n }\n\n cfg.width = resolveValue(cfg.width, this.options.defaultMaskWidth);\n cfg.height = resolveValue(cfg.height, this.options.defaultMaskHeight);\n\n // If expandCanvasToImage mode, ensure canvas large enough to hold mask initial placement\n if (this.options.expandCanvasToImage && shapeType === 'rect') {\n const requiredW = Math.ceil(left + cfg.width + 10);\n const requiredH = Math.ceil(top + cfg.height + 10);\n const minW = this.containerEl ? Math.floor(this.containerEl.clientWidth || 0) : 0;\n const minH = this.containerEl ? Math.floor(this.containerEl.clientHeight || 0) : 0;\n const newW = Math.max(this.canvas.getWidth(), minW, requiredW);\n const newH = Math.max(this.canvas.getHeight(), minH, requiredH);\n this._setCanvasSizeInt(newW, newH);\n }\n\n let mask;\n if (typeof cfg.fabricGenerator === 'function') {\n mask = cfg.fabricGenerator(cfg, this.canvas, this.options);\n } else {\n switch (shapeType) {\n case 'circle':\n mask = new fabric.Circle({\n left, top,\n radius: resolveValue(cfg.radius, Math.min(cfg.width, cfg.height) / 2),\n fill: cfg.color,\n opacity: cfg.alpha,\n angle: cfg.angle,\n ...cfg.styles\n });\n break;\n case 'ellipse':\n mask = new fabric.Ellipse({\n left, top,\n rx: resolveValue(cfg.rx, cfg.width / 2),\n ry: resolveValue(cfg.ry, cfg.height / 2),\n fill: cfg.color,\n opacity: cfg.alpha,\n angle: cfg.angle,\n ...cfg.styles\n });\n break;\n case 'polygon':\n let polyPoints = cfg.points || [];\n if (Array.isArray(polyPoints) && polyPoints.length && typeof polyPoints[0] === 'object') {\n // Ensure numeric {x,y} objects for fabric.Polygon\n polyPoints = polyPoints.map(pt => ({ x: Number(pt.x), y: Number(pt.y) }));\n }\n mask = new fabric.Polygon(polyPoints, {\n left, top,\n fill: cfg.color,\n opacity: cfg.alpha,\n angle: cfg.angle,\n ...cfg.styles\n });\n break;\n case 'rect':\n default:\n mask = new fabric.Rect({\n left, top,\n width: resolveValue(cfg.width, this.options.defaultMaskWidth),\n height: resolveValue(cfg.height, this.options.defaultMaskHeight),\n fill: cfg.color,\n opacity: cfg.alpha,\n angle: cfg.angle,\n rx: cfg.rx, // Rounded Corners\n ry: cfg.ry,\n ...cfg.styles\n });\n }\n }\n\n mask.selectable = cfg.selectable !== false;\n mask.hasControls = ('hasControls' in cfg) ? cfg.hasControls : true;\n mask.lockRotation = !this.options.maskRotatable;\n mask.borderColor = cfg.borderColor || 'red';\n mask.cornerColor = cfg.cornerColor || 'black';\n mask.cornerSize = cfg.cornerSize || 8;\n mask.transparentCorners = ('transparentCorners' in cfg) ? cfg.transparentCorners : false;\n mask.stroke = (cfg.styles && cfg.styles.stroke) || '#ccc';\n mask.strokeWidth = (cfg.styles && cfg.styles.strokeWidth) || 1;\n mask.strokeUniform = ('strokeUniform' in cfg) ? cfg.strokeUniform : true;\n if (cfg.styles && cfg.styles.strokeDashArray) mask.strokeDashArray = cfg.styles.strokeDashArray;\n\n mask.originalAlpha = cfg.alpha;\n const normalStyle = { stroke: mask.stroke, strokeWidth: mask.strokeWidth, opacity: mask.originalAlpha };\n const hoverStyle = { stroke: '#ff5500', strokeWidth: 2, opacity: Math.min(mask.originalAlpha + 0.2, 1) };\n\n mask.on('mouseover', () => {\n mask.set(hoverStyle);\n mask.canvas.requestRenderAll();\n });\n\n mask.on('mouseout', () => {\n mask.set(normalStyle);\n mask.canvas.requestRenderAll();\n });\n\n // Remember initial for next one\n this._lastMaskInitialLeft = left;\n this._lastMaskInitialTop = top;\n this._lastMaskInitialWidth = resolveValue(cfg.width, this.options.defaultMaskWidth);\n\n mask.maskId = ++this.maskCounter;\n mask.maskName = `${this.options.maskName}${mask.maskId}`;\n this._lastMask = mask;\n\n this.canvas.add(mask);\n this.canvas.bringToFront(mask);\n if (cfg.selectable) this.canvas.setActiveObject(mask);\n this._onSelectionChanged([mask]);\n this._updateMaskList();\n this._updateUI();\n this.canvas.renderAll();\n this.saveState();\n\n if (typeof cfg.onCreate === 'function') cfg.onCreate(mask, this.canvas);\n return mask;\n }\n\n /**\n * Removes the currently selected mask from the canvas, if any.\n * The associated label is also removed. UI and mask list are updated.\n */\n removeSelectedMask() {\n const active = this.canvas.getActiveObject();\n if (!active || !active.maskId) return;\n this._removeLabelForMask(active);\n this.canvas.remove(active);\n this.canvas.discardActiveObject();\n this._updateMaskList();\n this._updateUI();\n this.canvas.renderAll();\n this.saveState();\n }\n\n /**\n * Removes all masks from the canvas, including their labels.\n * UI and internal mask placement memory are reset.\n */\n removeAllMasks() {\n const masks = this.canvas.getObjects().filter(o => o.maskId);\n masks.forEach(m => this._removeLabelForMask(m));\n masks.forEach(m => this.canvas.remove(m));\n this.canvas.discardActiveObject();\n this._lastMaskInitialLeft = null;\n this._lastMaskInitialTop = null;\n this._lastMaskInitialWidth = null;\n this._updateMaskList();\n this._updateUI();\n this.canvas.renderAll();\n this.saveState();\n }\n\n /**\n * Removes the label associated with the specified mask object, if it exists.\n * \n * @param {fabric.Object} mask - The mask object whose label should be removed.\n * @private\n */\n _removeLabelForMask(mask) {\n if (!mask || !this.canvas) return;\n if (mask.__label) {\n try {\n const objs = this.canvas.getObjects();\n if (objs.includes(mask.__label)) {\n this.canvas.remove(mask.__label);\n }\n } catch (e) { /* ignore */ }\n try { delete mask.__label; } catch (e) { }\n }\n }\n\n /**\n * Creates and adds a custom label (fabric.Text or fabric.IText) for the mask.\n * The label is default bound to the top-left of the mask and managed as a non-interactive overlay.\n * \n * @param {fabric.Object} mask - The mask to create a label for.\n * @private\n */\n _createLabelForMask(mask) {\n if (!mask || !this.options.maskLabelOnSelect) return;\n this._removeLabelForMask(mask);\n let textObj = null;\n if (this.options.label && typeof this.options.label.create === 'function') {\n textObj = this.options.label.create(mask, fabric);\n }\n if (!textObj) {\n let txt = mask.maskName; // Default\n let textOptions = {\n left: 0,\n top: 0,\n fontSize: 12,\n fill: '#fff',\n backgroundColor: 'rgba(0,0,0,0.7)',\n selectable: false,\n evented: false,\n padding: 2,\n originX: 'left',\n originY: 'top'\n };\n if (this.options.label) {\n if (typeof this.options.label.getText === 'function') {\n txt = this.options.label.getText(mask, this.maskCounter);\n }\n // Merge external styles\n if (this.options.label.textOptions) {\n Object.assign(textOptions, this.options.label.textOptions);\n }\n }\n textObj = new fabric.Text(txt, textOptions);\n }\n\n textObj.maskLabel = true;\n mask.__label = textObj;\n this.canvas.add(textObj);\n this.canvas.bringToFront(textObj);\n this._syncMaskLabel(mask);\n }\n\n /**\n * Hides (removes) all mask labels from the canvas.\n * Internal label references on mask objects are also deleted.\n * @private\n */\n _hideAllMaskLabels() {\n if (!this.canvas) return;\n const objs = this.canvas.getObjects();\n const labels = objs.filter(o => o.maskLabel);\n labels.forEach(l => {\n try {\n if (objs.includes(l)) this.canvas.remove(l);\n } catch (e) { }\n });\n objs.forEach(o => { if (o.maskId && o.__label) { try { delete o.__label; } catch (e) { } } });\n }\n\n /**\n * Synchronizes the position, angle, and visibility of the mask's label so that it appears properly above the mask.\n * \n * @param {fabric.Object} mask - The mask whose label should be repositioned.\n * @private\n */\n _syncMaskLabel(mask) {\n if (!mask) return;\n if (!this.options.maskLabelOnSelect) return;\n if (!mask.__label) return;\n\n const coords = mask.getCoords ? mask.getCoords() : null;\n if (!coords || coords.length < 4) return;\n\n const tl = coords[0];\n const center = mask.getCenterPoint();\n\n const vx = center.x - tl.x;\n const vy = center.y - tl.y;\n const dist = Math.sqrt(vx * vx + vy * vy) || 1;\n const ux = vx / dist;\n const uy = vy / dist;\n\n const offset = Math.max(0, this.options.maskLabelOffset ?? 3);\n\n const px = tl.x + ux * offset;\n const py = tl.y + uy * offset;\n\n mask.__label.set({\n left: Math.round(px),\n top: Math.round(py),\n angle: mask.angle || 0,\n originX: 'left',\n originY: 'top',\n visible: true\n });\n mask.__label.setCoords();\n this.canvas.renderAll();\n }\n\n /**\n * Shows the label for the given mask, creating it if necessary and synchronizing its position.\n * \n * @param {fabric.Object} mask - The mask whose label should be shown.\n * @private\n */\n _showLabelForMask(mask) {\n if (!mask) return;\n if (!this.options.maskLabelOnSelect) return;\n if (!mask.__label) this._createLabelForMask(mask);\n mask.__label.visible = true;\n this._syncMaskLabel(mask);\n }\n\n /**\n * Handles changes to the selection of canvas objects (masks),\n * updates mask stroke and label display, and syncs mask list selection.\n *\n * @param {Array<Object>} selected - The currently selected objects (e.g. [mask] or []).\n * @private\n */\n _onSelectionChanged(selected) {\n const selectedMask = (selected || []).find(o => o.maskId);\n const masks = this.canvas.getObjects().filter(o => o.maskId);\n masks.forEach(m => {\n if (m !== selectedMask) {\n if (m.__label) {\n try { this.canvas.remove(m.__label); } catch (e) { }\n delete m.__label;\n }\n m.set({ stroke: '#ccc', strokeWidth: 1 });\n } else {\n m.set({ stroke: '#ff0000', strokeWidth: 1 });\n }\n });\n\n if (selectedMask) this._showLabelForMask(selectedMask);\n\n this._updateMaskListSelection(selectedMask);\n this.canvas.renderAll();\n this._updateUI();\n }\n\n /**\n * Updates the mask list in the DOM to reflect the current masks on the canvas.\n * Each list entry becomes a clickable element for mask selection.\n * @private\n */\n _updateMaskList() {\n const listEl = document.getElementById(this.elements.maskList);\n if (!listEl) return;\n listEl.innerHTML = '';\n const masks = this.canvas.getObjects().filter(o => o.maskId);\n masks.forEach(mask => {\n const li = document.createElement('li');\n li.className = 'list-group-item mask-item';\n li.textContent = mask.maskName;\n li.onclick = () => { this.canvas.setActiveObject(mask); this._onSelectionChanged([mask]); };\n listEl.appendChild(li);\n });\n }\n\n /**\n * Updates the visual selection (CSS 'active') state for the mask list in the DOM.\n * \n * @param {Object|null} selectedMask - The currently selected mask, or null if none selected.\n * @private\n */\n _updateMaskListSelection(selectedMask) {\n const listEl = document.getElementById(this.elements.maskList);\n if (!listEl) return;\n const items = listEl.querySelectorAll('.mask-item');\n items.forEach(item => {\n const isSelected = !!selectedMask && item.textContent === selectedMask.maskName;\n item.classList.toggle('active', isSelected);\n });\n }\n\n /**\n * Merges current masks into the image: exports a masked/cropped image, removes all masks, and re-imports the merged image.\n * Will not run if no original image or no masks exist.\n * @async\n * @returns {Promise<void>} Resolves when merge and load are complete.\n */\n async merge() {\n if (!this.originalImage) return;\n const masks = this.canvas.getObjects().filter(o => o.maskId);\n if (!masks.length) return;\n\n this.canvas.discardActiveObject();\n this.canvas.renderAll();\n\n try {\n const merged = await this.getImageBase64({ exportImageArea: true, multiplier: this.options.exportMultiplier });\n this.removeAllMasks();\n await this.loadImage(merged);\n this.saveState();\n } catch (err) {\n console.error('merge error', err);\n if (this.canvasEl) this.canvasEl.style.visibility = '';\n }\n }\n\n /**\n * Triggers a JPEG image download of the current canvas (image plus masks if configured).\n * The image area and multiplier are controlled by options.\n * @param {string} [fileName=this.options.defaultDownloadFileName] - Desired download file name.\n */\n downloadImage(fileName = this.options.defaultDownloadFileName) {\n if (!this.originalImage) return;\n const exportImageArea = this.options.exportImageAreaByDefault;\n this.getImageBase64({ exportImageArea, multiplier: this.options.exportMultiplier })\n .then(base64 => {\n const link = document.createElement('a');\n link.download = fileName;\n link.href = base64;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n })\n .catch(err => console.error('download error', err));\n }\n\n /**\n * Exports the image as a Base64-encoded JPEG.\n * Can export either the original, or the current view including masks (clipped/cropped).\n * Will restore masks' state after temporary modifications for export.\n * @async\n * @param {Object} [opts={}] - Export options.\n * @param {boolean} [opts.exportImageArea] - If true, exports only the image bounding area with masks cropped and blended.\n * @param {number} [opts.multiplier=1] - Scaling multiplier for output (resolution).\n * @returns {Promise<string>} Promise resolving to a JPEG image data URL.\n * @throws {Error} If there is no image loaded.\n */\n async getImageBase64(opts = {}) {\n if (!this.originalImage) throw new Error('No image loaded');\n const exportImageArea = typeof opts.exportImageArea === 'boolean' ? opts.exportImageArea : this.options.exportImageAreaByDefault;\n const multiplier = opts.multiplier || this.options.exportMultiplier || 1;\n\n if (!exportImageArea) {\n // Export original image pixels\n const imgEl = this.originalImage.getElement ? this.originalImage.getElement() : (this.originalImage._element || null);\n if (!imgEl) return this.canvas.toDataURL({ format: 'jpeg', quality: this.options.downsampleQuality, multiplier });\n const w = this.originalImage.width;\n const h = this.originalImage.height;\n const oc = document.createElement('canvas');\n oc.width = w;\n oc.height = h;\n const ctx = oc.getContext('2d');\n ctx.drawImage(imgEl, 0, 0, w, h);\n return oc.toDataURL('image/jpeg', this.options.downsampleQuality);\n }\n\n // Export current scaled image area (masks clipped)\n const masks = this.canvas.getObjects().filter(o => o.maskId);\n const masksBackup = masks.map(m => ({\n obj: m,\n opacity: m.opacity,\n fill: m.fill,\n strokeWidth: m.strokeWidth,\n stroke: m.stroke,\n selectable: m.selectable,\n lockRotation: m.lockRotation\n }));\n\n // Remove labels, deselect\n masks.forEach(m => this._removeLabelForMask(m));\n this.canvas.discardActiveObject();\n this.canvas.renderAll();\n\n // Set masks to opaque black no border\n masks.forEach(m => {\n m.set({ opacity: 1, fill: '#000000', strokeWidth: 0, stroke: null, selectable: false });\n m.setCoords();\n });\n this.canvas.renderAll();\n\n // Compute integer bounding box for image\n this.originalImage.setCoords();\n const imgBr = this.originalImage.getBoundingRect(true, true);\n const sx = Math.max(0, Math.round(imgBr.left));\n const sy = Math.max(0, Math.round(imgBr.top));\n const sw = Math.max(1, Math.round(imgBr.width));\n const sh = Math.max(1, Math.round(imgBr.height));\n\n // Crop precisely in offscreen canvas\n const finalBase64 = await new Promise((resolve, reject) => {\n try {\n const fullDataUrl = this.canvas.toDataURL({\n format: 'jpeg',\n quality: this.options.downsampleQuality,\n multiplier: multiplier\n });\n\n const img = new Image();\n img.onload = () => {\n try {\n const sxM = Math.round(sx * multiplier);\n const syM = Math.round(sy * multiplier);\n const swM = Math.round(sw * multiplier);\n const shM = Math.round(sh * multiplier);\n\n const oc = document.createElement('canvas');\n oc.width = swM;\n oc.height = shM;\n const ctx = oc.getContext('2d');\n\n ctx.drawImage(img, sxM, syM, swM, shM, 0, 0, swM, shM);\n const out = oc.toDataURL('image/jpeg', this.options.downsampleQuality);\n resolve(out);\n } catch (e) { reject(e); }\n };\n img.onerror = reject;\n img.src = fullDataUrl;\n } catch (e) { reject(e); }\n });\n\n // Restore masks\n masksBackup.forEach(b => {\n try {\n b.obj.set({\n opacity: b.opacity,\n fill: b.fill,\n strokeWidth: b.strokeWidth,\n stroke: b.stroke,\n selectable: b.selectable,\n lockRotation: b.lockRotation\n });\n b.obj.setCoords();\n } catch (e) { }\n });\n\n this.canvas.renderAll();\n return finalBase64;\n }\n\n /**\n * Exports the current canvas (with or without masks) as a File object.\n * Allows you to choose whether to merge masks and specify file type (jpeg/png/webp).\n * \n * @async\n * @param {Object} [opts={}] - Export options.\n * @param {boolean} [opts.mergeMask=true] - If true, export image area with masks merged; if false, export the plain image without masks.\n * @param {string} [opts.fileType='jpeg'] - Output file type ('jpeg' | 'png' | 'webp'). Defaults to 'jpeg' on invalid input.\n * @param {number} [opts.quality=0.92] - Image quality for lossy types (0-1, default based on options.downsampleQuality).\n * @param {number} [opts.multiplier=1] - Output resolution multiplier.\n * @param {string} [opts.fileName] - Optional file name (only used for download).\n * @returns {Promise<File>} Resolves with the exported image as a File object.\n * \n * @example\n * const file = await this.exportImageFile({ mergeMask: false, fileType: 'png' });\n */\n async exportImageFile(opts = {}) {\n if (!this.originalImage) throw new Error('No image loaded');\n const {\n mergeMask = true,\n fileType = 'jpeg',\n quality = this.options.downsampleQuality ?? 0.92,\n multiplier = this.options.exportMultiplier ?? 1,\n fileName = this.options.defaultDownloadFileName ?? 'exported_image.jpg'\n } = opts;\n\n const typeMapping = {\n 'jpeg': 'jpeg',\n 'jpg': 'jpeg',\n 'image/jpeg': 'jpeg',\n 'png': 'png',\n 'image/png': 'png',\n 'webp': 'webp',\n 'image/webp': 'webp'\n };\n const safeFileType = typeMapping[String(fileType).toLowerCase()] || 'jpeg';\n\n // Get Base64\n let base64;\n if (mergeMask) {\n base64 = await this.getImageBase64({\n exportImageArea: true,\n multiplier,\n });\n } else {\n base64 = await this.getImageBase64({\n exportImageArea: false,\n multiplier,\n });\n }\n\n // Convert to the required image format\n let imageDataUrl = base64;\n if (!imageDataUrl.startsWith(`data:image/${safeFileType}`)) {\n // Redraw if not required format\n imageDataUrl = await new Promise((resolve, reject) => {\n const img = new window.Image();\n img.crossOrigin = \"Anonymous\";\n img.onload = () => {\n try {\n const oc = document.createElement('canvas');\n oc.width = img.width;\n oc.height = img.height;\n const ctx = oc.getContext('2d');\n ctx.drawImage(img, 0, 0);\n const durl = oc.toDataURL(`image/${safeFileType}`, quality);\n resolve(durl);\n } catch (e) { reject(e); }\n };\n img.onerror = reject;\n img.src = base64;\n });\n }\n\n // Convert DataURL to Blob and then to File\n const bstr = atob(imageDataUrl.split(',')[1]);\n const mime = `image/${safeFileType}`;\n let n = bstr.length;\n const u8arr = new Uint8Array(n);\n while (n--) {\n u8arr[n] = bstr.charCodeAt(n);\n }\n const file = new File([u8arr], fileName, { type: mime });\n return file;\n }\n\n /* ---------- Misc / UI ---------- */\n\n /**\n * Updates the scale input field in the UI to reflect the current scale.\n * Sets the value (as percentage) if the element is present.\n * @private\n */\n _updateInputs() {\n const scaleEl = document.getElementById(this.elements.scaleRate);\n if (scaleEl) scaleEl.value = Math.round(this.currentScale * 100);\n }\n\n /**\n * Updates the enabled/disabled state of various UI controls (buttons)\n * based on the current application state (image/mask presence, animation, etc).\n * @private\n */\n _updateUI() {\n const hasImg = !!this.originalImage;\n const masks = hasImg ? this.canvas.getObjects().filter(o => o.maskId) : [];\n const hasMasks = masks.length > 0;\n const active = this.canvas.getActiveObject();\n const hasSelectedMask = active && active.maskId;\n const isDefault = this.currentScale === 1 && this.currentRotation === 0;\n const canUndo = this.historyManager?.canUndo();\n const canRedo = this.historyManager?.canRedo();\n\n this._setDisabled('zoomInBtn', !hasImg || this.isAnimating || this.currentScale >= this.options.maxScale);\n this._setDisabled('zoomOutBtn', !hasImg || this.isAnimating || this.currentScale <= this.options.minScale);\n this._setDisabled('addMaskBtn', !hasImg || this.isAnimating);\n this._setDisabled('removeMaskBtn', !hasSelectedMask || this.isAnimating);\n this._setDisabled('removeAllMasksBtn', !hasMasks || this.isAnimating);\n this._setDisabled('mergeBtn', !hasImg || !hasMasks || this.isAnimating);\n this._setDisabled('downloadBtn', !hasImg || this.isAnimating);\n this._setDisabled('resetBtn', !hasImg || isDefault || this.isAnimating);\n this._setDisabled('undoBtn', !hasImg || this.isAnimating || !canUndo);\n this._setDisabled('redoBtn', !hasImg || this.isAnimating || !canRedo);\n }\n\n /**\n * Enables or disables a specific UI element (typically a button) by its key.\n * \n * @param {string} key - Key of the element in this.elements (e.g. 'zoomInBtn').\n * @param {boolean} disabled - If true, disables the element; otherwise enables.\n * @private\n */\n _setDisabled(key, disabled) {\n const el = document.getElementById(this.elements[key]);\n if (el) el.disabled = !!disabled;\n }\n\n /**\n * Automatically display and hide placeholders and containers based on the current image content\n * @private\n */\n _updatePlaceholderStatus() {\n if (!this.options.showPlaceholder) return;\n this._setPlaceholderVisible(!this.originalImage);\n }\n\n /**\n * Controls the display/hiding of the Placeholder and Canvas container.\n * @param {boolean} show - true displays the placeholder, false displays the canvas container\n * @private\n */\n _setPlaceholderVisible(show) {\n if (!this.placeholderEl) return;\n if (show) {\n this.placeholderEl.classList.remove('d-none');\n this.placeholderEl.classList.add('d-flex');\n this.containerEl.classList.add('d-none');\n } else {\n this.placeholderEl.classList.remove('d-flex');\n this.placeholderEl.classList.add('d-none');\n this.containerEl.classList.remove('d-none');\n }\n }\n\n /**\n * Cleans up and disposes of the canvas and related references.\n * Call this method to free memory and remove canvas listeners when the editor is no longer needed.\n * @public\n */\n dispose() {\n // Remove bound DOM event listeners\n try {\n for (const key in (this._boundHandlers || {})) {\n const handlers = this._boundHandlers[key] || [];\n const el = document.getElementById(this.elements[key]);\n if (!el) continue;\n handlers.forEach(h => {\n try { el.removeEventListener(h.event, h.handler); } catch (e) { }\n });\n }\n } catch (e) { }\n\n if (this.canvas) {\n try { this.canvas.dispose(); } catch (e) { }\n this.canvas = null;\n this.canvasEl = null;\n this.isImageLoadedToCanvas = false;\n }\n this._boundHandlers = {};\n }\n }\n\n /**\n * A simple FIFO queue that guarantees animations are executed sequentially.\n * @class AnimationQueue\n */\n class AnimationQueue {\n /**\n * Creates a new AnimationQueue.\n *\n * @constructor\n */\n constructor() {\n /**\n * Internal queue holding animation descriptors.\n * @type {Array<{fn: Function, resolve: Function, reject: Function}>}\n */\n this.queue = [];\n /**\n * Flag indicating whether an animation is currently running.\n * @type {boolean}\n */\n this.running = false;\n }\n\n /**\n * Adds an animation function to the queue.\n *\n * @param {Function} animationFn A function that returns a Promise or any await-able.\n * @returns {Promise<*>} A Promise that resolves/rejects with the animation result.\n */\n async add(animationFn) {\n return new Promise((resolve, reject) => {\n // Push the animation into the queue.\n this.queue.push({ fn: animationFn, resolve, reject });\n // Start processing if it's not already running.\n if (!this.running) {\n this.processQueue();\n }\n });\n }\n\n /**\n * Internal helper that processes the animation queue sequentially until it is empty.\n *\n * @private\n * @returns {Promise<void>}\n */\n async processQueue() {\n if (this.queue.length === 0) {\n this.running = false;\n return;\n }\n\n this.running = true;\n const { fn, resolve, reject } = this.queue.shift();\n\n try {\n const result = await fn();\n resolve(result);\n } catch (error) {\n reject(error);\n }\n\n this.processQueue();\n }\n }\n\n /**\n * Command object encapsulating an executable action and its corresponding undo operation.\n * @class Command\n */\n class Command {\n /**\n * @param {Function} execute The function that performs the action.\n * @param {Function} undo The function that reverts the action.\n */\n constructor(execute, undo) {\n /**\n * Executes the command.\n * @type {Function}\n */\n this.execute = execute;\n /**\n * Undoes the command.\n * @type {Function}\n */\n this.undo = undo;\n }\n }\n\n /**\n * Manages a history of Command objects enabling undo/redo functionality.\n * @class HistoryManager\n */\n class HistoryManager {\n /**\n * @param {number} [maxSize=50] Maximum number of commands to keep in history.\n */\n constructor(maxSize = 50) {\n this.history = [];\n this.currentIndex = -1;\n this.maxSize = maxSize;\n }\n\n /**\n * Executes a new command and pushes it onto the history stack.\n * Truncates any \"future\" history when branching.\n *\n * @param {Command} command The command to execute.\n * @returns {void}\n */\n execute(command) {\n // Perform the command.\n command.execute();\n\n // Remove any commands that are ahead of the current index.\n if (this.currentIndex < this.history.length - 1) {\n this.history = this.history.slice(0, this.currentIndex + 1);\n }\n\n // Add the new command.\n this.history.push(command);\n\n // Maintain the max size of the buffer.\n if (this.history.length > this.maxSize) {\n this.history.shift(); // Remove the oldest command.\n } else {\n this.currentIndex++;\n }\n }\n\n /**\n * Checks whether an undo operation is possible.\n *\n * @returns {boolean} True if undo can be performed.\n */\n canUndo() {\n return this.currentIndex >= 0;\n }\n\n /**\n * Checks whether a redo operation is possible.\n *\n * @returns {boolean} True if redo can be performed.\n */\n canRedo() {\n return this.currentIndex < this.history.length - 1;\n }\n\n /**\n * Undoes the last executed command if possible.\n *\n * @returns {void}\n */\n undo() {\n if (this.currentIndex >= 0) {\n this.history[this.currentIndex].undo();\n this.currentIndex--;\n }\n }\n\n /**\n * Redoes the next command in history if possible.\n *\n * @returns {void}\n */\n redo() {\n if (this.currentIndex < this.history.length - 1) {\n this.currentIndex++;\n this.history[this.currentIndex].execute();\n }\n }\n }\n\n return ImageEditor\n})\n"],
5
+ "mappings": "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",
6
+ "names": ["require_image_editor", "__commonJSMin", "exports", "module", "root", "factory", "ImageEditor", "options", "mask", "maskIndex", "AnimationQueue", "HistoryManager", "idMap", "defaults", "canvasEl", "ce", "initialW", "initialH", "cw", "ch", "e", "inputEl", "f", "rotLeftBtn", "rotRightBtn", "el", "step", "p", "key", "event", "handler", "file", "reader", "base64", "imgEl", "loadSrc", "ratio", "tw", "th", "fimg", "imgW", "imgH", "minW", "minH", "fitScale", "dataURL", "res", "rej", "img", "w", "h", "quality", "oc", "iw", "ih", "obj", "coords", "br", "originX", "originY", "refPoint", "dx", "dy", "containerW", "containerH", "newW", "newH", "factor", "targetAbs", "topLeft", "p1", "p2", "deg", "degrees", "center", "newTopLeft", "o", "err", "jsonString", "json", "objs", "masks", "max", "m", "activeObj", "after", "before", "executedOnce", "cmd", "Command", "config", "shapeType", "cfg", "firstOffset", "left", "top", "resolveValue", "val", "fallback", "percent", "prev", "prevRight", "requiredW", "requiredH", "polyPoints", "pt", "normalStyle", "hoverStyle", "active", "textObj", "txt", "textOptions", "l", "tl", "vx", "vy", "dist", "ux", "uy", "offset", "px", "py", "selected", "selectedMask", "listEl", "li", "item", "isSelected", "merged", "fileName", "exportImageArea", "link", "opts", "multiplier", "masksBackup", "imgBr", "sx", "sy", "sw", "sh", "finalBase64", "resolve", "reject", "fullDataUrl", "sxM", "syM", "swM", "shM", "out", "b", "mergeMask", "fileType", "safeFileType", "imageDataUrl", "durl", "bstr", "mime", "n", "u8arr", "scaleEl", "hasImg", "hasMasks", "hasSelectedMask", "isDefault", "canUndo", "canRedo", "disabled", "show", "handlers", "animationFn", "fn", "result", "error", "execute", "undo", "maxSize", "command"]
7
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@bensitu/image-editor",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight canvas-based image editor",
5
+ "main": "dist/image-editor.js",
6
+ "module": "dist/image-editor.esm.js",
7
+ "types": "image-editor.d.ts",
8
+ "exports": {
9
+ "import": "./dist/image-editor.esm.js",
10
+ "require": "./dist/image-editor.iife.js"
11
+ },
12
+ "scripts": {
13
+ "dev": "node esbuild.config.mjs --watch",
14
+ "build": "node esbuild.config.mjs",
15
+ "lint": "eslint src --ext .js"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/bensitu/image-editor.git"
20
+ },
21
+ "author": "Ben Situ",
22
+ "license": "MIT",
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "peerDependencies": {
27
+ "fabric": "^5.5.2"
28
+ },
29
+ "devDependencies": {
30
+ "esbuild": "^0.19.12",
31
+ "eslint": "^8.49.0"
32
+ },
33
+ "browserslist": [
34
+ "defaults",
35
+ "ie 11"
36
+ ],
37
+ "keywords": [
38
+ "fabricjs",
39
+ "image-editor",
40
+ "javascript",
41
+ "mask",
42
+ "open-source"
43
+ ],
44
+ "directories": {
45
+ "doc": "docs"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/bensitu/image-editor/issues"
49
+ },
50
+ "homepage": "https://github.com/bensitu/image-editor#readme",
51
+ "dependencies": {
52
+ "image-editor": "^0.2.0"
53
+ }
54
+ }