@bensitu/image-editor 1.3.1 → 1.4.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/dist/image-editor.esm.js +739 -288
- package/dist/image-editor.esm.js.map +2 -2
- package/dist/image-editor.esm.min.js +3 -3
- package/dist/image-editor.esm.min.js.map +3 -3
- package/dist/image-editor.esm.min.mjs +3 -3
- package/dist/image-editor.esm.min.mjs.map +3 -3
- package/dist/image-editor.esm.mjs +739 -288
- package/dist/image-editor.esm.mjs.map +2 -2
- package/dist/image-editor.js +739 -288
- package/dist/image-editor.js.map +2 -2
- package/dist/image-editor.min.js +2 -2
- package/dist/image-editor.min.js.map +3 -3
- package/image-editor.d.ts +2 -0
- package/package.json +2 -2
- package/src/image-editor.js +803 -312
package/dist/image-editor.esm.js
CHANGED
|
@@ -5,7 +5,7 @@ import fabricModule from "fabric";
|
|
|
5
5
|
/**
|
|
6
6
|
* @file image-editor.js
|
|
7
7
|
* @module image-editor
|
|
8
|
-
* @version 1.
|
|
8
|
+
* @version 1.4.0
|
|
9
9
|
* @author Ben Situ
|
|
10
10
|
* @license MIT
|
|
11
11
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
@@ -72,6 +72,8 @@ var ImageEditor = class {
|
|
|
72
72
|
downsampleMaxWidth: 4e3,
|
|
73
73
|
downsampleMaxHeight: 3e3,
|
|
74
74
|
downsampleQuality: 0.92,
|
|
75
|
+
preserveSourceFormat: true,
|
|
76
|
+
downsampleMimeType: null,
|
|
75
77
|
imageLoadTimeoutMs: 3e4,
|
|
76
78
|
exportMultiplier: 1,
|
|
77
79
|
exportImageAreaByDefault: true,
|
|
@@ -120,6 +122,7 @@ var ImageEditor = class {
|
|
|
120
122
|
this.isImageLoadedToCanvas = false;
|
|
121
123
|
this.maxHistorySize = 50;
|
|
122
124
|
this._handlersByElementKey = {};
|
|
125
|
+
this._elementCache = {};
|
|
123
126
|
this._lastMask = null;
|
|
124
127
|
this._lastMaskInitialLeft = null;
|
|
125
128
|
this._lastMaskInitialTop = null;
|
|
@@ -130,8 +133,14 @@ var ImageEditor = class {
|
|
|
130
133
|
this._cropHandlers = [];
|
|
131
134
|
this._cropPrevEvented = null;
|
|
132
135
|
this._prevSelectionSetting = void 0;
|
|
133
|
-
this._containerOriginalOverflow =
|
|
136
|
+
this._containerOriginalOverflow = null;
|
|
137
|
+
this._lastContainerViewportSize = null;
|
|
138
|
+
this._canvasElementOriginalStyle = null;
|
|
139
|
+
this._visibilityStateByElement = /* @__PURE__ */ new WeakMap();
|
|
134
140
|
this._scrollbarSizeCache = null;
|
|
141
|
+
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
142
|
+
this._disposed = false;
|
|
143
|
+
this._initialized = false;
|
|
135
144
|
this.onImageLoaded = typeof options.onImageLoaded === "function" ? options.onImageLoaded : null;
|
|
136
145
|
this.animationQueue = new AnimationQueue();
|
|
137
146
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
@@ -195,6 +204,16 @@ var ImageEditor = class {
|
|
|
195
204
|
*/
|
|
196
205
|
init(idMap = {}) {
|
|
197
206
|
if (!this._fabricLoaded) return;
|
|
207
|
+
if (this._initialized || this.canvas) this.dispose();
|
|
208
|
+
this._disposed = false;
|
|
209
|
+
this._initialized = true;
|
|
210
|
+
this.animationQueue = new AnimationQueue();
|
|
211
|
+
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
212
|
+
this._visibilityStateByElement = /* @__PURE__ */ new WeakMap();
|
|
213
|
+
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
214
|
+
this._containerOriginalOverflow = null;
|
|
215
|
+
this._lastContainerViewportSize = null;
|
|
216
|
+
this._canvasElementOriginalStyle = null;
|
|
198
217
|
const defaults = {
|
|
199
218
|
canvas: "fabricCanvas",
|
|
200
219
|
canvasContainer: null,
|
|
@@ -222,6 +241,7 @@ var ImageEditor = class {
|
|
|
222
241
|
cancelCropBtn: "cancelCropBtn"
|
|
223
242
|
};
|
|
224
243
|
this.elements = { ...defaults, ...idMap };
|
|
244
|
+
this._elementCache = {};
|
|
225
245
|
this._initCanvas();
|
|
226
246
|
this._bindEvents();
|
|
227
247
|
this._updateInputs();
|
|
@@ -256,16 +276,22 @@ var ImageEditor = class {
|
|
|
256
276
|
* @private
|
|
257
277
|
*/
|
|
258
278
|
_initCanvas() {
|
|
259
|
-
const canvasElement =
|
|
279
|
+
const canvasElement = this._getElement("canvas");
|
|
260
280
|
if (!canvasElement) throw new Error("Canvas is not found: " + this.elements.canvas);
|
|
261
281
|
this.canvasElement = canvasElement;
|
|
282
|
+
this._canvasElementOriginalStyle = {
|
|
283
|
+
display: canvasElement.style.display || "",
|
|
284
|
+
width: canvasElement.style.width || "",
|
|
285
|
+
height: canvasElement.style.height || "",
|
|
286
|
+
maxWidth: canvasElement.style.maxWidth || ""
|
|
287
|
+
};
|
|
262
288
|
if (this.elements.canvasContainer) {
|
|
263
|
-
const containerElement =
|
|
289
|
+
const containerElement = this._getElement("canvasContainer");
|
|
264
290
|
this.containerElement = containerElement || canvasElement.parentElement;
|
|
265
291
|
} else {
|
|
266
292
|
this.containerElement = canvasElement.parentElement;
|
|
267
293
|
}
|
|
268
|
-
this.placeholderElement =
|
|
294
|
+
this.placeholderElement = this._getElement("imgPlaceholder") || null;
|
|
269
295
|
let initialWidth = this.options.canvasWidth;
|
|
270
296
|
let initialHeight = this.options.canvasHeight;
|
|
271
297
|
if (this.containerElement) {
|
|
@@ -274,6 +300,10 @@ var ImageEditor = class {
|
|
|
274
300
|
if (containerWidth > 0 && containerHeight > 0) {
|
|
275
301
|
initialWidth = containerWidth;
|
|
276
302
|
initialHeight = containerHeight;
|
|
303
|
+
this._lastContainerViewportSize = {
|
|
304
|
+
width: containerWidth,
|
|
305
|
+
height: containerHeight
|
|
306
|
+
};
|
|
277
307
|
}
|
|
278
308
|
}
|
|
279
309
|
this.canvas = new fabric.Canvas(canvasElement, {
|
|
@@ -298,6 +328,23 @@ var ImageEditor = class {
|
|
|
298
328
|
this.canvas.on("object:modified", (event) => this._handleObjectModified(event.target));
|
|
299
329
|
this.canvasElement.style.display = "block";
|
|
300
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Returns a configured DOM element and caches lookups for hot UI paths.
|
|
333
|
+
*
|
|
334
|
+
* @param {string} key - Key in the configured element map.
|
|
335
|
+
* @returns {HTMLElement|null} The configured element, or null when missing.
|
|
336
|
+
* @private
|
|
337
|
+
*/
|
|
338
|
+
_getElement(key) {
|
|
339
|
+
const id = this.elements && this.elements[key];
|
|
340
|
+
if (!id) return null;
|
|
341
|
+
if (this._elementCache && Object.prototype.hasOwnProperty.call(this._elementCache, key)) {
|
|
342
|
+
return this._elementCache[key];
|
|
343
|
+
}
|
|
344
|
+
const element = document.getElementById(id);
|
|
345
|
+
if (this._elementCache) this._elementCache[key] = element || null;
|
|
346
|
+
return element || null;
|
|
347
|
+
}
|
|
301
348
|
/**
|
|
302
349
|
* Records a history entry after Fabric finishes modifying one or more masks.
|
|
303
350
|
*
|
|
@@ -338,9 +385,7 @@ var ImageEditor = class {
|
|
|
338
385
|
*/
|
|
339
386
|
_syncContainerOverflow(options = {}) {
|
|
340
387
|
if (!this.containerElement || !this.containerElement.style) return;
|
|
341
|
-
|
|
342
|
-
this._containerOriginalOverflow = this.containerElement.style.overflow || "";
|
|
343
|
-
}
|
|
388
|
+
this._captureContainerOverflowState();
|
|
344
389
|
const shouldPreserveScroll = options.preserveScroll === true;
|
|
345
390
|
if (this.options.coverImageToCanvas) {
|
|
346
391
|
this.containerElement.style.overflow = "scroll";
|
|
@@ -355,58 +400,77 @@ var ImageEditor = class {
|
|
|
355
400
|
this.containerElement.scrollTop = 0;
|
|
356
401
|
}
|
|
357
402
|
} else {
|
|
358
|
-
this.
|
|
403
|
+
this._restoreContainerOverflowState();
|
|
359
404
|
}
|
|
360
405
|
}
|
|
406
|
+
_captureContainerOverflowState() {
|
|
407
|
+
if (!this.containerElement || !this.containerElement.style || this._containerOriginalOverflow) return;
|
|
408
|
+
this._containerOriginalOverflow = {
|
|
409
|
+
overflow: this.containerElement.style.overflow || "",
|
|
410
|
+
overflowX: this.containerElement.style.overflowX || "",
|
|
411
|
+
overflowY: this.containerElement.style.overflowY || ""
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
_restoreContainerOverflowState() {
|
|
415
|
+
if (!this.containerElement || !this.containerElement.style || !this._containerOriginalOverflow) return;
|
|
416
|
+
this.containerElement.style.overflow = this._containerOriginalOverflow.overflow;
|
|
417
|
+
this.containerElement.style.overflowX = this._containerOriginalOverflow.overflowX;
|
|
418
|
+
this.containerElement.style.overflowY = this._containerOriginalOverflow.overflowY;
|
|
419
|
+
}
|
|
361
420
|
/**
|
|
362
421
|
* DOM / UI bindings
|
|
363
422
|
* @private
|
|
364
423
|
*/
|
|
365
424
|
_bindEvents() {
|
|
366
425
|
this._bindIfExists("uploadArea", "click", () => {
|
|
367
|
-
const uploadAreaElement =
|
|
426
|
+
const uploadAreaElement = this._getElement("uploadArea");
|
|
368
427
|
if (this._isElementDisabled(uploadAreaElement)) return;
|
|
369
|
-
|
|
428
|
+
this._getElement("imageInput")?.click();
|
|
370
429
|
});
|
|
371
430
|
this._bindIfExists("imageInput", "change", (event) => {
|
|
372
431
|
const file = event.target.files && event.target.files[0];
|
|
373
|
-
if (file)
|
|
432
|
+
if (file) {
|
|
433
|
+
this._loadImageFile(file).catch((error) => this._reportError("Image file could not be loaded", error)).finally(() => {
|
|
434
|
+
event.target.value = "";
|
|
435
|
+
});
|
|
436
|
+
}
|
|
374
437
|
});
|
|
375
|
-
this._bindIfExists("zoomInBtn", "click", () => this.scaleImage(this.currentScale + this.options.scaleStep));
|
|
376
|
-
this._bindIfExists("zoomOutBtn", "click", () => this.scaleImage(this.currentScale - this.options.scaleStep));
|
|
438
|
+
this._bindIfExists("zoomInBtn", "click", () => this.scaleImage(this.currentScale + this.options.scaleStep).catch((error) => this._reportError("scaleImage failed", error)));
|
|
439
|
+
this._bindIfExists("zoomOutBtn", "click", () => this.scaleImage(this.currentScale - this.options.scaleStep).catch((error) => this._reportError("scaleImage failed", error)));
|
|
377
440
|
this._bindIfExists("resetBtn", "click", () => {
|
|
378
|
-
this.resetImageTransform();
|
|
441
|
+
this.resetImageTransform().catch((error) => this._reportError("resetImageTransform failed", error));
|
|
379
442
|
});
|
|
380
443
|
this._bindIfExists("addMaskBtn", "click", () => this.createMask());
|
|
381
444
|
this._bindIfExists("removeMaskBtn", "click", () => this.removeSelectedMask());
|
|
382
445
|
this._bindIfExists("removeAllMasksBtn", "click", () => this.removeAllMasks());
|
|
383
|
-
this._bindIfExists("mergeBtn", "click", () => this.mergeMasks());
|
|
446
|
+
this._bindIfExists("mergeBtn", "click", () => this.mergeMasks().catch((error) => this._reportError("merge error", error)));
|
|
384
447
|
this._bindIfExists("downloadBtn", "click", () => this.downloadImage());
|
|
385
|
-
this._bindIfExists("undoBtn", "click", () => this.undo());
|
|
386
|
-
this._bindIfExists("redoBtn", "click", () => this.redo());
|
|
448
|
+
this._bindIfExists("undoBtn", "click", () => this.undo().catch((error) => this._reportError("undo failed", error)));
|
|
449
|
+
this._bindIfExists("redoBtn", "click", () => this.redo().catch((error) => this._reportError("redo failed", error)));
|
|
387
450
|
this._bindIfExists("rotateLeftBtn", "click", () => {
|
|
388
|
-
const rotationInputElement =
|
|
451
|
+
const rotationInputElement = this._getElement("rotationLeftInput");
|
|
389
452
|
let step = this.options.rotationStep;
|
|
390
453
|
if (rotationInputElement) {
|
|
391
454
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
392
455
|
if (!isNaN(parsedStep)) step = parsedStep;
|
|
393
456
|
}
|
|
394
|
-
this.rotateImage(this.currentRotation - step);
|
|
457
|
+
this.rotateImage(this.currentRotation - step).catch((error) => this._reportError("rotateImage failed", error));
|
|
395
458
|
});
|
|
396
459
|
this._bindIfExists("rotateRightBtn", "click", () => {
|
|
397
|
-
const rotationInputElement =
|
|
460
|
+
const rotationInputElement = this._getElement("rotationRightInput");
|
|
398
461
|
let step = this.options.rotationStep;
|
|
399
462
|
if (rotationInputElement) {
|
|
400
463
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
401
464
|
if (!isNaN(parsedStep)) step = parsedStep;
|
|
402
465
|
}
|
|
403
|
-
this.rotateImage(this.currentRotation + step);
|
|
466
|
+
this.rotateImage(this.currentRotation + step).catch((error) => this._reportError("rotateImage failed", error));
|
|
404
467
|
});
|
|
405
468
|
this._bindIfExists("cropBtn", "click", () => this.enterCropMode());
|
|
406
469
|
this._bindIfExists("applyCropBtn", "click", () => {
|
|
407
470
|
this.applyCrop().catch((error) => this._reportError("applyCrop failed", error));
|
|
408
471
|
});
|
|
409
472
|
this._bindIfExists("cancelCropBtn", "click", () => this.cancelCrop());
|
|
473
|
+
this._bindIfExists("maskList", "click", (event) => this._handleMaskListClick(event));
|
|
410
474
|
}
|
|
411
475
|
/**
|
|
412
476
|
* Binds a DOM event listener when the configured element exists and records it for disposal.
|
|
@@ -417,7 +481,7 @@ var ImageEditor = class {
|
|
|
417
481
|
* @private
|
|
418
482
|
*/
|
|
419
483
|
_bindIfExists(key, eventName, handler) {
|
|
420
|
-
const element =
|
|
484
|
+
const element = this._getElement(key);
|
|
421
485
|
if (element) {
|
|
422
486
|
element.addEventListener(eventName, handler);
|
|
423
487
|
this._handlersByElementKey = this._handlersByElementKey || {};
|
|
@@ -429,16 +493,33 @@ var ImageEditor = class {
|
|
|
429
493
|
* Reads an image File as a data URL and loads it into the Fabric canvas.
|
|
430
494
|
*
|
|
431
495
|
* @param {File} file - Image file selected by the user.
|
|
496
|
+
* @returns {Promise<void>} Resolves after the selected file is loaded.
|
|
432
497
|
* @private
|
|
433
498
|
*/
|
|
434
499
|
_loadImageFile(file) {
|
|
435
|
-
if (!
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
500
|
+
if (!this._isSupportedImageFile(file)) {
|
|
501
|
+
const error = new Error("Selected file is not a supported image");
|
|
502
|
+
this._reportError("Selected file is not a supported image", error);
|
|
503
|
+
return Promise.reject(error);
|
|
504
|
+
}
|
|
505
|
+
return new Promise((resolve, reject) => {
|
|
506
|
+
const reader = new FileReader();
|
|
507
|
+
reader.onload = (event) => {
|
|
508
|
+
this.loadImage(event.target.result).then(resolve).catch(reject);
|
|
509
|
+
};
|
|
510
|
+
reader.onerror = (event) => {
|
|
511
|
+
const error = new Error("Image file could not be read");
|
|
512
|
+
this._reportError("Image file could not be read", event);
|
|
513
|
+
reject(error);
|
|
514
|
+
};
|
|
515
|
+
reader.readAsDataURL(file);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
_isSupportedImageFile(file) {
|
|
519
|
+
if (!file) return false;
|
|
520
|
+
if (typeof file.type === "string" && file.type.startsWith("image/")) return true;
|
|
521
|
+
const fileName = String(file.name || "");
|
|
522
|
+
return /\.(avif|bmp|gif|jpe?g|png|webp)$/i.test(fileName);
|
|
442
523
|
}
|
|
443
524
|
/**
|
|
444
525
|
* Warns when more than one mutually exclusive image layout mode is enabled.
|
|
@@ -468,98 +549,97 @@ var ImageEditor = class {
|
|
|
468
549
|
*/
|
|
469
550
|
async loadImage(imageBase64, options = {}) {
|
|
470
551
|
if (!this._fabricLoaded) return;
|
|
471
|
-
if (!this.canvas) return;
|
|
552
|
+
if (!this.canvas || this._disposed) return;
|
|
472
553
|
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/")) return;
|
|
554
|
+
this._assertIdleForOperation("loadImage");
|
|
473
555
|
this._warnOnImageLayoutOptionConflict();
|
|
474
|
-
this.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
556
|
+
const transaction = this._captureLoadImageTransaction();
|
|
557
|
+
try {
|
|
558
|
+
const imageElement = await this._createImageElement(imageBase64);
|
|
559
|
+
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
560
|
+
let loadSource = imageBase64;
|
|
561
|
+
if (this.options.downsampleOnLoad) {
|
|
562
|
+
const shouldResize = imageElement.naturalWidth > this.options.downsampleMaxWidth || imageElement.naturalHeight > this.options.downsampleMaxHeight;
|
|
563
|
+
if (shouldResize) {
|
|
564
|
+
const ratio = Math.min(
|
|
565
|
+
this.options.downsampleMaxWidth / imageElement.naturalWidth,
|
|
566
|
+
this.options.downsampleMaxHeight / imageElement.naturalHeight
|
|
567
|
+
);
|
|
568
|
+
const targetWidth = Math.round(imageElement.naturalWidth * ratio);
|
|
569
|
+
const targetHeight = Math.round(imageElement.naturalHeight * ratio);
|
|
570
|
+
loadSource = this._resampleImageToDataURL(
|
|
571
|
+
imageElement,
|
|
572
|
+
targetWidth,
|
|
573
|
+
targetHeight,
|
|
574
|
+
this.options.downsampleQuality,
|
|
575
|
+
imageBase64
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const fabricImage = await this._createFabricImageFromURL(loadSource);
|
|
580
|
+
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
581
|
+
this.canvas.discardActiveObject();
|
|
582
|
+
this._hideAllMaskLabels();
|
|
583
|
+
this.canvas.clear();
|
|
584
|
+
this.canvas.setBackgroundColor(this.options.backgroundColor, this.canvas.renderAll.bind(this.canvas));
|
|
585
|
+
fabricImage.set({ originX: "left", originY: "top", selectable: false, evented: false });
|
|
586
|
+
this._setPlaceholderVisible(false);
|
|
587
|
+
this._syncContainerOverflow({ preserveScroll: options.preserveScroll === true });
|
|
588
|
+
const imageWidth = fabricImage.width;
|
|
589
|
+
const imageHeight = fabricImage.height;
|
|
590
|
+
const viewport = this._getContainerViewportSize();
|
|
591
|
+
const minWidth = viewport.width;
|
|
592
|
+
const minHeight = viewport.height;
|
|
593
|
+
if (this.options.fitImageToCanvas) {
|
|
594
|
+
const canvasWidth = Math.max(1, minWidth - 1);
|
|
595
|
+
const canvasHeight = Math.max(1, minHeight - 1);
|
|
596
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
597
|
+
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
598
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
599
|
+
fabricImage.scale(fitScale);
|
|
600
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
601
|
+
} else if (this.options.coverImageToCanvas) {
|
|
602
|
+
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
603
|
+
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
604
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
605
|
+
fabricImage.scale(layout.scale);
|
|
606
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
607
|
+
} else if (this.options.expandCanvasToImage) {
|
|
608
|
+
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
609
|
+
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
610
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
611
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
612
|
+
fabricImage.scale(1);
|
|
613
|
+
this.baseImageScale = 1;
|
|
614
|
+
} else {
|
|
615
|
+
const canvasWidth = Math.max(this.options.canvasWidth, minWidth);
|
|
616
|
+
const canvasHeight = Math.max(this.options.canvasHeight, minHeight);
|
|
617
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
618
|
+
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
619
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
620
|
+
fabricImage.scale(fitScale);
|
|
621
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
488
622
|
}
|
|
623
|
+
this.originalImage = fabricImage;
|
|
624
|
+
this.canvas.add(fabricImage);
|
|
625
|
+
this.canvas.sendToBack(fabricImage);
|
|
626
|
+
this._clearMaskPlacementMemory();
|
|
627
|
+
if (options.resetMaskCounter !== false) this.maskCounter = 0;
|
|
628
|
+
this.currentScale = 1;
|
|
629
|
+
this.currentRotation = 0;
|
|
630
|
+
this._updateInputs();
|
|
631
|
+
this._updateMaskList();
|
|
632
|
+
this.isImageLoadedToCanvas = true;
|
|
633
|
+
this._updateUI();
|
|
634
|
+
this.canvas.renderAll();
|
|
635
|
+
this._lastSnapshot = this._captureCanvasStateOrThrow("loadImage");
|
|
636
|
+
if (typeof this.onImageLoaded === "function") {
|
|
637
|
+
this.onImageLoaded();
|
|
638
|
+
}
|
|
639
|
+
} catch (error) {
|
|
640
|
+
await this._rollbackLoadImageTransaction(transaction);
|
|
641
|
+
throw error;
|
|
489
642
|
}
|
|
490
|
-
return new Promise((resolve, reject) => {
|
|
491
|
-
fabric.Image.fromURL(loadSource, (fabricImage) => {
|
|
492
|
-
try {
|
|
493
|
-
if (!fabricImage) throw new Error("Image could not be loaded");
|
|
494
|
-
this.canvas.discardActiveObject();
|
|
495
|
-
this._hideAllMaskLabels();
|
|
496
|
-
this.canvas.clear();
|
|
497
|
-
this.canvas.setBackgroundColor(this.options.backgroundColor, this.canvas.renderAll.bind(this.canvas));
|
|
498
|
-
fabricImage.set({ originX: "left", originY: "top", selectable: false, evented: false });
|
|
499
|
-
const imageWidth = fabricImage.width;
|
|
500
|
-
const imageHeight = fabricImage.height;
|
|
501
|
-
const viewport = this._getContainerViewportSize();
|
|
502
|
-
const minWidth = viewport.width;
|
|
503
|
-
const minHeight = viewport.height;
|
|
504
|
-
if (this.options.fitImageToCanvas) {
|
|
505
|
-
const canvasWidth = Math.max(1, minWidth - 1);
|
|
506
|
-
const canvasHeight = Math.max(1, minHeight - 1);
|
|
507
|
-
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
508
|
-
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
509
|
-
fabricImage.set({ left: 0, top: 0 });
|
|
510
|
-
fabricImage.scale(fitScale);
|
|
511
|
-
this.baseImageScale = fabricImage.scaleX || 1;
|
|
512
|
-
} else if (this.options.coverImageToCanvas) {
|
|
513
|
-
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
514
|
-
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
515
|
-
fabricImage.set({ left: 0, top: 0 });
|
|
516
|
-
fabricImage.scale(layout.scale);
|
|
517
|
-
this.baseImageScale = fabricImage.scaleX || 1;
|
|
518
|
-
} else if (this.options.expandCanvasToImage) {
|
|
519
|
-
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
520
|
-
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
521
|
-
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
522
|
-
fabricImage.set({ left: 0, top: 0 });
|
|
523
|
-
fabricImage.scale(1);
|
|
524
|
-
this.baseImageScale = 1;
|
|
525
|
-
} else {
|
|
526
|
-
const canvasWidth = Math.max(this.options.canvasWidth, minWidth);
|
|
527
|
-
const canvasHeight = Math.max(this.options.canvasHeight, minHeight);
|
|
528
|
-
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
529
|
-
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
530
|
-
fabricImage.set({ left: 0, top: 0 });
|
|
531
|
-
fabricImage.scale(fitScale);
|
|
532
|
-
this.baseImageScale = fabricImage.scaleX || 1;
|
|
533
|
-
}
|
|
534
|
-
this.originalImage = fabricImage;
|
|
535
|
-
this.canvas.add(fabricImage);
|
|
536
|
-
this.canvas.sendToBack(fabricImage);
|
|
537
|
-
this._lastMask = null;
|
|
538
|
-
this._lastMaskInitialLeft = null;
|
|
539
|
-
this._lastMaskInitialTop = null;
|
|
540
|
-
this._lastMaskInitialWidth = null;
|
|
541
|
-
this.maskCounter = 0;
|
|
542
|
-
this.currentScale = 1;
|
|
543
|
-
this.currentRotation = 0;
|
|
544
|
-
this._updateInputs();
|
|
545
|
-
this._updateMaskList();
|
|
546
|
-
this.isImageLoadedToCanvas = true;
|
|
547
|
-
this._updateUI();
|
|
548
|
-
this.canvas.renderAll();
|
|
549
|
-
try {
|
|
550
|
-
this._lastSnapshot = this._serializeCanvasState();
|
|
551
|
-
} catch (error) {
|
|
552
|
-
this._reportWarning("loadImage: failed to capture initial canvas snapshot", error);
|
|
553
|
-
}
|
|
554
|
-
if (typeof this.onImageLoaded === "function") {
|
|
555
|
-
this.onImageLoaded();
|
|
556
|
-
}
|
|
557
|
-
resolve();
|
|
558
|
-
} catch (error) {
|
|
559
|
-
reject(error);
|
|
560
|
-
}
|
|
561
|
-
}, { crossOrigin: "anonymous" });
|
|
562
|
-
});
|
|
563
643
|
}
|
|
564
644
|
/**
|
|
565
645
|
* Checks whether there is a loaded image on the current canvas.
|
|
@@ -604,24 +684,132 @@ var ImageEditor = class {
|
|
|
604
684
|
imageElement.src = dataUrl;
|
|
605
685
|
});
|
|
606
686
|
}
|
|
687
|
+
_createFabricImageFromURL(dataUrl, timeoutMs = this.options.imageLoadTimeoutMs) {
|
|
688
|
+
return new Promise((resolve, reject) => {
|
|
689
|
+
const safeTimeoutMs = this._getSafeTimeoutMs(timeoutMs);
|
|
690
|
+
let isSettled = false;
|
|
691
|
+
let timerId;
|
|
692
|
+
const settle = (callback) => {
|
|
693
|
+
if (isSettled) return;
|
|
694
|
+
isSettled = true;
|
|
695
|
+
clearTimeout(timerId);
|
|
696
|
+
callback();
|
|
697
|
+
};
|
|
698
|
+
timerId = setTimeout(() => {
|
|
699
|
+
settle(() => reject(new Error("Fabric image load timed out")));
|
|
700
|
+
}, safeTimeoutMs);
|
|
701
|
+
try {
|
|
702
|
+
fabric.Image.fromURL(dataUrl, (fabricImage) => {
|
|
703
|
+
settle(() => {
|
|
704
|
+
if (!fabricImage) {
|
|
705
|
+
reject(new Error("Image could not be loaded"));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
resolve(fabricImage);
|
|
709
|
+
});
|
|
710
|
+
}, { crossOrigin: "anonymous" });
|
|
711
|
+
} catch (error) {
|
|
712
|
+
settle(() => reject(error));
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
_getSafeTimeoutMs(timeoutMs) {
|
|
717
|
+
const safeTimeoutMs = Number(timeoutMs);
|
|
718
|
+
return Number.isFinite(safeTimeoutMs) && safeTimeoutMs > 0 ? safeTimeoutMs : 3e4;
|
|
719
|
+
}
|
|
720
|
+
_captureLoadImageTransaction() {
|
|
721
|
+
return {
|
|
722
|
+
canvasState: this._serializeCanvasState(),
|
|
723
|
+
originalImage: this.originalImage,
|
|
724
|
+
baseImageScale: this.baseImageScale,
|
|
725
|
+
currentScale: this.currentScale,
|
|
726
|
+
currentRotation: this.currentRotation,
|
|
727
|
+
maskCounter: this.maskCounter,
|
|
728
|
+
isImageLoadedToCanvas: this.isImageLoadedToCanvas,
|
|
729
|
+
lastSnapshot: this._lastSnapshot,
|
|
730
|
+
lastMask: this._lastMask,
|
|
731
|
+
lastMaskInitialLeft: this._lastMaskInitialLeft,
|
|
732
|
+
lastMaskInitialTop: this._lastMaskInitialTop,
|
|
733
|
+
lastMaskInitialWidth: this._lastMaskInitialWidth,
|
|
734
|
+
containerOverflow: this.containerElement && this.containerElement.style ? {
|
|
735
|
+
overflow: this.containerElement.style.overflow || "",
|
|
736
|
+
overflowX: this.containerElement.style.overflowX || "",
|
|
737
|
+
overflowY: this.containerElement.style.overflowY || ""
|
|
738
|
+
} : null,
|
|
739
|
+
scrollLeft: this.containerElement ? this.containerElement.scrollLeft : 0,
|
|
740
|
+
scrollTop: this.containerElement ? this.containerElement.scrollTop : 0,
|
|
741
|
+
placeholderVisibility: this._captureElementVisibility(this.placeholderElement),
|
|
742
|
+
canvasVisibility: this._captureElementVisibility(this._getCanvasVisibilityElement())
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
async _rollbackLoadImageTransaction(transaction) {
|
|
746
|
+
if (!transaction || !this.canvas || this._disposed) return;
|
|
747
|
+
try {
|
|
748
|
+
if (transaction.canvasState) await this.loadFromState(transaction.canvasState);
|
|
749
|
+
} catch (error) {
|
|
750
|
+
this._reportError("loadImage rollback failed", error);
|
|
751
|
+
}
|
|
752
|
+
this.baseImageScale = transaction.baseImageScale;
|
|
753
|
+
this.currentScale = transaction.currentScale;
|
|
754
|
+
this.currentRotation = transaction.currentRotation;
|
|
755
|
+
this.maskCounter = transaction.maskCounter;
|
|
756
|
+
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
757
|
+
this._lastSnapshot = transaction.lastSnapshot;
|
|
758
|
+
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
759
|
+
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
760
|
+
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
761
|
+
this._containerOriginalOverflow = transaction.containerOverflow;
|
|
762
|
+
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
763
|
+
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
764
|
+
if (this.containerElement) {
|
|
765
|
+
this.containerElement.scrollLeft = transaction.scrollLeft;
|
|
766
|
+
this.containerElement.scrollTop = transaction.scrollTop;
|
|
767
|
+
this._restoreContainerOverflowState();
|
|
768
|
+
}
|
|
769
|
+
this._updateInputs();
|
|
770
|
+
this._updateMaskList();
|
|
771
|
+
this._updateUI();
|
|
772
|
+
if (this.canvas) this.canvas.renderAll();
|
|
773
|
+
}
|
|
607
774
|
/**
|
|
608
|
-
* Resamples the given image element to a new width and height and returns the result as a
|
|
775
|
+
* Resamples the given image element to a new width and height and returns the result as a data URL.
|
|
609
776
|
*
|
|
610
777
|
* @param {HTMLImageElement} imageElement - The image element to resample.
|
|
611
778
|
* @param {number} targetWidth - Target width (in pixels) for the resampled image.
|
|
612
779
|
* @param {number} targetHeight - Target height (in pixels) for the resampled image.
|
|
613
|
-
* @param {number} [quality=0.92] -
|
|
614
|
-
* @
|
|
780
|
+
* @param {number} [quality=0.92] - Image quality between 0 and 1 for lossy formats.
|
|
781
|
+
* @param {string|null} [sourceDataUrl=null] - Source data URL used to preserve alpha-capable formats.
|
|
782
|
+
* @returns {string} A data URL representing the resampled image.
|
|
615
783
|
* @private
|
|
616
784
|
*/
|
|
617
|
-
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92) {
|
|
785
|
+
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
|
|
618
786
|
const offscreenCanvas = document.createElement("canvas");
|
|
619
787
|
offscreenCanvas.width = targetWidth;
|
|
620
788
|
offscreenCanvas.height = targetHeight;
|
|
621
789
|
const context = offscreenCanvas.getContext("2d");
|
|
622
790
|
if (!context) throw new Error("2D canvas context is unavailable");
|
|
623
791
|
context.drawImage(imageElement, 0, 0, imageElement.naturalWidth, imageElement.naturalHeight, 0, 0, targetWidth, targetHeight);
|
|
624
|
-
return offscreenCanvas.toDataURL(
|
|
792
|
+
return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
|
|
793
|
+
}
|
|
794
|
+
_getDataUrlMimeType(dataUrl) {
|
|
795
|
+
const match = String(dataUrl || "").match(/^data:([^;,]+)[;,]/i);
|
|
796
|
+
return match ? match[1].toLowerCase() : "";
|
|
797
|
+
}
|
|
798
|
+
_getDownsampleMimeType(sourceDataUrl) {
|
|
799
|
+
if (this.options.downsampleMimeType) {
|
|
800
|
+
const requestedFormat = this._normalizeImageFormat(this.options.downsampleMimeType);
|
|
801
|
+
return `image/${requestedFormat}`;
|
|
802
|
+
}
|
|
803
|
+
const sourceMimeType = this._getDataUrlMimeType(sourceDataUrl);
|
|
804
|
+
if (this.options.preserveSourceFormat !== false && (sourceMimeType === "image/png" || sourceMimeType === "image/webp")) {
|
|
805
|
+
return sourceMimeType;
|
|
806
|
+
}
|
|
807
|
+
return "image/jpeg";
|
|
808
|
+
}
|
|
809
|
+
_captureCanvasStateOrThrow(context) {
|
|
810
|
+
const snapshot = this._serializeCanvasState();
|
|
811
|
+
if (!snapshot) throw new Error(`${context}: canvas state is unavailable`);
|
|
812
|
+
return snapshot;
|
|
625
813
|
}
|
|
626
814
|
/**
|
|
627
815
|
* Sets canvas size to integer width and height values to prevent scrollbars due to sub-pixel rendering.
|
|
@@ -640,7 +828,6 @@ var ImageEditor = class {
|
|
|
640
828
|
if (this.canvasElement) {
|
|
641
829
|
this.canvasElement.style.width = integerWidth + "px";
|
|
642
830
|
this.canvasElement.style.height = integerHeight + "px";
|
|
643
|
-
this.canvasElement.style.maxWidth = "none";
|
|
644
831
|
}
|
|
645
832
|
}
|
|
646
833
|
_ceilCanvasDimension(value) {
|
|
@@ -656,8 +843,13 @@ var ImageEditor = class {
|
|
|
656
843
|
height: Math.max(1, Math.floor(this.options.canvasHeight || 1))
|
|
657
844
|
};
|
|
658
845
|
}
|
|
659
|
-
|
|
660
|
-
|
|
846
|
+
const measuredWidth = Math.floor(this.containerElement.clientWidth || 0);
|
|
847
|
+
const measuredHeight = Math.floor(this.containerElement.clientHeight || 0);
|
|
848
|
+
let width = Math.max(1, measuredWidth || this._lastContainerViewportSize?.width || this.options.canvasWidth || 1);
|
|
849
|
+
let height = Math.max(1, measuredHeight || this._lastContainerViewportSize?.height || this.options.canvasHeight || 1);
|
|
850
|
+
if (measuredWidth > 0 && measuredHeight > 0) {
|
|
851
|
+
this._lastContainerViewportSize = { width: measuredWidth, height: measuredHeight };
|
|
852
|
+
}
|
|
661
853
|
if (this._hasFixedContainerScrollbars()) {
|
|
662
854
|
return { width, height };
|
|
663
855
|
}
|
|
@@ -1043,7 +1235,7 @@ var ImageEditor = class {
|
|
|
1043
1235
|
});
|
|
1044
1236
|
}
|
|
1045
1237
|
/**
|
|
1046
|
-
* Exports
|
|
1238
|
+
* Exports a source region directly through Fabric's region export options.
|
|
1047
1239
|
*
|
|
1048
1240
|
* @param {Object} region - Canvas source region and export options.
|
|
1049
1241
|
* @param {number} region.sourceX - Source region x coordinate.
|
|
@@ -1056,14 +1248,17 @@ var ImageEditor = class {
|
|
|
1056
1248
|
* @returns {Promise<string>} Resolves with an image data URL for the cropped region.
|
|
1057
1249
|
* @private
|
|
1058
1250
|
*/
|
|
1059
|
-
|
|
1251
|
+
_exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg" }) {
|
|
1060
1252
|
const safeMultiplier = Math.max(1, Number(multiplier) || 1);
|
|
1061
|
-
|
|
1253
|
+
return this.canvas.toDataURL({
|
|
1062
1254
|
format,
|
|
1063
1255
|
quality,
|
|
1064
|
-
multiplier: safeMultiplier
|
|
1256
|
+
multiplier: safeMultiplier,
|
|
1257
|
+
left: sourceX,
|
|
1258
|
+
top: sourceY,
|
|
1259
|
+
width: sourceWidth,
|
|
1260
|
+
height: sourceHeight
|
|
1065
1261
|
});
|
|
1066
|
-
return this._cropDataUrl(fullDataUrl, sourceX, sourceY, sourceWidth, sourceHeight, safeMultiplier, format, quality);
|
|
1067
1262
|
}
|
|
1068
1263
|
/**
|
|
1069
1264
|
* Gets the top-left corner coordinates of the given object.
|
|
@@ -1076,11 +1271,37 @@ var ImageEditor = class {
|
|
|
1076
1271
|
_getObjectTopLeftPoint(fabricObject) {
|
|
1077
1272
|
if (!fabricObject) return { x: 0, y: 0 };
|
|
1078
1273
|
fabricObject.setCoords();
|
|
1079
|
-
const coords = typeof fabricObject.getCoords === "function" ? fabricObject.getCoords() : null;
|
|
1080
|
-
if (coords && coords.length) return coords[0];
|
|
1081
1274
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1082
1275
|
return { x: boundingRect.left, y: boundingRect.top };
|
|
1083
1276
|
}
|
|
1277
|
+
_getObjectCoordinateTopLeftPoint(fabricObject) {
|
|
1278
|
+
if (!fabricObject) return { x: 0, y: 0 };
|
|
1279
|
+
fabricObject.setCoords();
|
|
1280
|
+
const coords = typeof fabricObject.getCoords === "function" ? fabricObject.getCoords() : null;
|
|
1281
|
+
if (coords && coords.length) return coords[0];
|
|
1282
|
+
return this._getObjectTopLeftPoint(fabricObject);
|
|
1283
|
+
}
|
|
1284
|
+
_getObjectOriginPoint(fabricObject, originX, originY) {
|
|
1285
|
+
if (!fabricObject) return { x: 0, y: 0 };
|
|
1286
|
+
if (typeof fabricObject.getPointByOrigin === "function") {
|
|
1287
|
+
return fabricObject.getPointByOrigin(originX, originY);
|
|
1288
|
+
}
|
|
1289
|
+
return this._getObjectTopLeftPoint(fabricObject);
|
|
1290
|
+
}
|
|
1291
|
+
_translateObjectByCanvasOffset(fabricObject, deltaX, deltaY) {
|
|
1292
|
+
if (!fabricObject) return;
|
|
1293
|
+
if (typeof fabricObject.getCenterPoint === "function" && typeof fabricObject.setPositionByOrigin === "function") {
|
|
1294
|
+
const center = fabricObject.getCenterPoint();
|
|
1295
|
+
const nextCenter = new fabric.Point(center.x + deltaX, center.y + deltaY);
|
|
1296
|
+
fabricObject.setPositionByOrigin(nextCenter, "center", "center");
|
|
1297
|
+
} else {
|
|
1298
|
+
fabricObject.set({
|
|
1299
|
+
left: (fabricObject.left || 0) + deltaX,
|
|
1300
|
+
top: (fabricObject.top || 0) + deltaY
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
fabricObject.setCoords();
|
|
1304
|
+
}
|
|
1084
1305
|
/**
|
|
1085
1306
|
* Sets the object's origin at the specified origin point, keeping a reference point fixed in position.
|
|
1086
1307
|
*
|
|
@@ -1144,8 +1365,10 @@ var ImageEditor = class {
|
|
|
1144
1365
|
_expandCanvasToFitObjects(fabricObjects, padding = 10) {
|
|
1145
1366
|
if (!this.canvas || !Array.isArray(fabricObjects) || !fabricObjects.length || !this._shouldResizeCanvasToContentBounds()) return;
|
|
1146
1367
|
try {
|
|
1147
|
-
|
|
1148
|
-
|
|
1368
|
+
const currentWidth = this.canvas.getWidth();
|
|
1369
|
+
const currentHeight = this.canvas.getHeight();
|
|
1370
|
+
let requiredWidth = currentWidth;
|
|
1371
|
+
let requiredHeight = currentHeight;
|
|
1149
1372
|
fabricObjects.forEach((fabricObject) => {
|
|
1150
1373
|
if (!fabricObject) return;
|
|
1151
1374
|
if (typeof fabricObject.setCoords === "function") fabricObject.setCoords();
|
|
@@ -1153,11 +1376,21 @@ var ImageEditor = class {
|
|
|
1153
1376
|
requiredWidth = Math.max(requiredWidth, Math.ceil(boundingRect.left + boundingRect.width + padding));
|
|
1154
1377
|
requiredHeight = Math.max(requiredHeight, Math.ceil(boundingRect.top + boundingRect.height + padding));
|
|
1155
1378
|
});
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1379
|
+
const shouldUseScrollSafeViewport = this.options.fitImageToCanvas || this.options.coverImageToCanvas;
|
|
1380
|
+
let minWidth = 0;
|
|
1381
|
+
let minHeight = 0;
|
|
1382
|
+
if (shouldUseScrollSafeViewport) {
|
|
1383
|
+
const viewport = this._getContainerViewportSize();
|
|
1384
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
1385
|
+
minWidth = Math.max(1, viewport.width - safetyMargin);
|
|
1386
|
+
minHeight = Math.max(1, viewport.height - safetyMargin);
|
|
1387
|
+
} else if (this.containerElement) {
|
|
1388
|
+
minWidth = Math.floor(this.containerElement.clientWidth || 0);
|
|
1389
|
+
minHeight = Math.floor(this.containerElement.clientHeight || 0);
|
|
1390
|
+
}
|
|
1391
|
+
const newWidth = Math.max(currentWidth, minWidth, requiredWidth);
|
|
1392
|
+
const newHeight = Math.max(currentHeight, minHeight, requiredHeight);
|
|
1393
|
+
if (newWidth !== currentWidth || newHeight !== currentHeight) {
|
|
1161
1394
|
this._setCanvasSizeInt(newWidth, newHeight);
|
|
1162
1395
|
}
|
|
1163
1396
|
} catch (error) {
|
|
@@ -1185,6 +1418,66 @@ var ImageEditor = class {
|
|
|
1185
1418
|
scaleImage(factor, options = {}) {
|
|
1186
1419
|
return this.animationQueue.add(() => this._scaleImageImpl(factor, options));
|
|
1187
1420
|
}
|
|
1421
|
+
_assertIdleForOperation(operationName) {
|
|
1422
|
+
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1423
|
+
if (this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) {
|
|
1424
|
+
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
_canMutateNow(operationName) {
|
|
1428
|
+
try {
|
|
1429
|
+
this._assertIdleForOperation(operationName);
|
|
1430
|
+
return true;
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
this._reportError(`${operationName} blocked`, error);
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
_rejectActiveAnimations(reason) {
|
|
1437
|
+
const error = reason instanceof Error ? reason : new Error(String(reason || "Animation cancelled"));
|
|
1438
|
+
this._activeAnimationRejectors.forEach((reject) => {
|
|
1439
|
+
try {
|
|
1440
|
+
reject(error);
|
|
1441
|
+
} catch (rejectError) {
|
|
1442
|
+
void rejectError;
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
this._activeAnimationRejectors.clear();
|
|
1446
|
+
}
|
|
1447
|
+
_animateFabricProperty(fabricObject, property, value) {
|
|
1448
|
+
return new Promise((resolve, reject) => {
|
|
1449
|
+
if (this._disposed || !this.canvas || !fabricObject) {
|
|
1450
|
+
reject(new Error("Animation cannot start after editor disposal"));
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
let isSettled = false;
|
|
1454
|
+
const duration = Math.max(0, Number(this.options.animationDuration) || 0);
|
|
1455
|
+
const timeoutMs = Math.max(1e3, duration + 1e3);
|
|
1456
|
+
let timerId;
|
|
1457
|
+
const settle = (callback) => {
|
|
1458
|
+
if (isSettled) return;
|
|
1459
|
+
isSettled = true;
|
|
1460
|
+
clearTimeout(timerId);
|
|
1461
|
+
this._activeAnimationRejectors.delete(reject);
|
|
1462
|
+
callback();
|
|
1463
|
+
};
|
|
1464
|
+
this._activeAnimationRejectors.add(reject);
|
|
1465
|
+
timerId = setTimeout(() => {
|
|
1466
|
+
settle(() => reject(new Error(`Animation timed out while changing ${property}`)));
|
|
1467
|
+
}, timeoutMs);
|
|
1468
|
+
try {
|
|
1469
|
+
fabricObject.animate(property, value, {
|
|
1470
|
+
duration,
|
|
1471
|
+
onChange: () => {
|
|
1472
|
+
if (!this._disposed && this.canvas) this.canvas.renderAll();
|
|
1473
|
+
},
|
|
1474
|
+
onComplete: () => settle(resolve)
|
|
1475
|
+
});
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
settle(() => reject(error));
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1188
1481
|
/**
|
|
1189
1482
|
* Scales the original image by a given factor, with animation.
|
|
1190
1483
|
* Returns a promise that resolves when the scale animation is complete.
|
|
@@ -1192,32 +1485,25 @@ var ImageEditor = class {
|
|
|
1192
1485
|
* @returns {Promise<void>} Promise that resolves once the scaling animation finishes.
|
|
1193
1486
|
* @private
|
|
1194
1487
|
*/
|
|
1195
|
-
_scaleImageImpl(factor, options = {}) {
|
|
1196
|
-
if (!this.originalImage
|
|
1197
|
-
if (this.isAnimating) return
|
|
1488
|
+
async _scaleImageImpl(factor, options = {}) {
|
|
1489
|
+
if (!this.originalImage || this._disposed) return;
|
|
1490
|
+
if (this.isAnimating) return;
|
|
1198
1491
|
const saveHistory = options.saveHistory !== false;
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
this.originalImage
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
this.originalImage.animate("scaleY", targetScale, {
|
|
1215
|
-
duration: this.options.animationDuration,
|
|
1216
|
-
onChange: this.canvas.renderAll.bind(this.canvas),
|
|
1217
|
-
onComplete: resolve
|
|
1218
|
-
});
|
|
1219
|
-
});
|
|
1220
|
-
return Promise.all([scaleXAnimation, scaleYAnimation]).then(() => {
|
|
1492
|
+
let didStartAnimation = false;
|
|
1493
|
+
try {
|
|
1494
|
+
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, factor));
|
|
1495
|
+
this.currentScale = factor;
|
|
1496
|
+
this.isAnimating = true;
|
|
1497
|
+
didStartAnimation = true;
|
|
1498
|
+
this._updateUI();
|
|
1499
|
+
const targetScale = this.baseImageScale * factor;
|
|
1500
|
+
const topLeft = this._getObjectTopLeftPoint(this.originalImage);
|
|
1501
|
+
this._setObjectOriginKeepingPosition(this.originalImage, "left", "top", topLeft);
|
|
1502
|
+
await Promise.all([
|
|
1503
|
+
this._animateFabricProperty(this.originalImage, "scaleX", targetScale),
|
|
1504
|
+
this._animateFabricProperty(this.originalImage, "scaleY", targetScale)
|
|
1505
|
+
]);
|
|
1506
|
+
if (this._disposed || !this.canvas || !this.originalImage) throw new Error("Editor was disposed during scale animation");
|
|
1221
1507
|
this.originalImage.set({ scaleX: targetScale, scaleY: targetScale });
|
|
1222
1508
|
this.originalImage.setCoords();
|
|
1223
1509
|
if (this._shouldResizeCanvasToContentBounds()) {
|
|
@@ -1227,14 +1513,15 @@ var ImageEditor = class {
|
|
|
1227
1513
|
this.canvas.getObjects().forEach((object) => {
|
|
1228
1514
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1229
1515
|
});
|
|
1230
|
-
this.isAnimating = false;
|
|
1231
1516
|
this._updateInputs();
|
|
1232
|
-
this._updateUI();
|
|
1233
1517
|
if (saveHistory) this.saveState();
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1518
|
+
} finally {
|
|
1519
|
+
if (didStartAnimation) {
|
|
1520
|
+
this.isAnimating = false;
|
|
1521
|
+
this._updateInputs();
|
|
1522
|
+
this._updateUI();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1238
1525
|
}
|
|
1239
1526
|
/**
|
|
1240
1527
|
* Rotates the original image by a given number of degrees, with animation.
|
|
@@ -1253,43 +1540,50 @@ var ImageEditor = class {
|
|
|
1253
1540
|
* @returns {Promise<void>} Promise that resolves once the rotation animation finishes.
|
|
1254
1541
|
* @private
|
|
1255
1542
|
*/
|
|
1256
|
-
_rotateImageImpl(degrees, options = {}) {
|
|
1257
|
-
if (!this.originalImage
|
|
1258
|
-
if (this.isAnimating) return
|
|
1259
|
-
if (isNaN(degrees)) return
|
|
1543
|
+
async _rotateImageImpl(degrees, options = {}) {
|
|
1544
|
+
if (!this.originalImage || this._disposed) return;
|
|
1545
|
+
if (this.isAnimating) return;
|
|
1546
|
+
if (isNaN(degrees)) return;
|
|
1260
1547
|
const saveHistory = options.saveHistory !== false;
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1548
|
+
const image = this.originalImage;
|
|
1549
|
+
const previousOriginX = image.originX || "left";
|
|
1550
|
+
const previousOriginY = image.originY || "top";
|
|
1551
|
+
const previousOriginPoint = this._getObjectOriginPoint(image, previousOriginX, previousOriginY);
|
|
1552
|
+
let didStartAnimation = false;
|
|
1553
|
+
let didCompleteRotation = false;
|
|
1554
|
+
try {
|
|
1555
|
+
this.currentRotation = degrees;
|
|
1556
|
+
this.isAnimating = true;
|
|
1557
|
+
didStartAnimation = true;
|
|
1558
|
+
this._updateUI();
|
|
1559
|
+
const center = image.getCenterPoint();
|
|
1560
|
+
this._setObjectOriginKeepingPosition(image, "center", "center", center);
|
|
1561
|
+
await this._animateFabricProperty(image, "angle", degrees);
|
|
1562
|
+
if (this._disposed || !this.canvas || !this.originalImage) throw new Error("Editor was disposed during rotation animation");
|
|
1274
1563
|
this.originalImage.set("angle", degrees);
|
|
1275
1564
|
this.originalImage.setCoords();
|
|
1276
1565
|
if (this._shouldResizeCanvasToContentBounds()) {
|
|
1277
1566
|
this._updateCanvasSizeToImageBounds();
|
|
1278
1567
|
}
|
|
1279
1568
|
this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
|
|
1280
|
-
const newTopLeft = this.
|
|
1569
|
+
const newTopLeft = this._getObjectCoordinateTopLeftPoint(this.originalImage);
|
|
1281
1570
|
this._setObjectOriginKeepingPosition(this.originalImage, "left", "top", newTopLeft);
|
|
1282
1571
|
this.canvas.getObjects().forEach((object) => {
|
|
1283
1572
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1284
1573
|
});
|
|
1285
|
-
this.isAnimating = false;
|
|
1286
1574
|
this._updateInputs();
|
|
1287
|
-
this._updateUI();
|
|
1288
1575
|
if (saveHistory) this.saveState();
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
this.
|
|
1292
|
-
|
|
1576
|
+
didCompleteRotation = true;
|
|
1577
|
+
} finally {
|
|
1578
|
+
if (!didCompleteRotation && !this._disposed && image) {
|
|
1579
|
+
this._setObjectOriginKeepingPosition(image, previousOriginX, previousOriginY, previousOriginPoint);
|
|
1580
|
+
}
|
|
1581
|
+
if (didStartAnimation) {
|
|
1582
|
+
this.isAnimating = false;
|
|
1583
|
+
this._updateInputs();
|
|
1584
|
+
this._updateUI();
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1293
1587
|
}
|
|
1294
1588
|
/**
|
|
1295
1589
|
* Resets the image transform: scales to 1 and rotates to 0 degrees.
|
|
@@ -1300,13 +1594,14 @@ var ImageEditor = class {
|
|
|
1300
1594
|
resetImageTransform() {
|
|
1301
1595
|
if (!this.originalImage) return Promise.resolve();
|
|
1302
1596
|
return this.animationQueue.add(async () => {
|
|
1303
|
-
const before = this._lastSnapshot || this.
|
|
1597
|
+
const before = this._lastSnapshot || this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1304
1598
|
await this._scaleImageImpl(1, { saveHistory: false });
|
|
1305
1599
|
await this._rotateImageImpl(0, { saveHistory: false });
|
|
1306
|
-
const after = this.
|
|
1600
|
+
const after = this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1307
1601
|
this._pushStateTransition(before, after);
|
|
1308
1602
|
}).catch((error) => {
|
|
1309
1603
|
this._reportError("resetImageTransform() failed", error);
|
|
1604
|
+
throw error;
|
|
1310
1605
|
});
|
|
1311
1606
|
}
|
|
1312
1607
|
/**
|
|
@@ -1326,13 +1621,31 @@ var ImageEditor = class {
|
|
|
1326
1621
|
* @public
|
|
1327
1622
|
*/
|
|
1328
1623
|
loadFromState(serializedState) {
|
|
1329
|
-
if (!serializedState || !this.canvas) return Promise.resolve();
|
|
1330
|
-
|
|
1624
|
+
if (!serializedState || !this.canvas || this._disposed) return Promise.resolve();
|
|
1625
|
+
if (this._cropMode || this._cropRect) {
|
|
1626
|
+
this._removeCropRect();
|
|
1627
|
+
this._restoreCropObjectState();
|
|
1628
|
+
this._cropMode = false;
|
|
1629
|
+
if (this._prevSelectionSetting !== void 0 && this.canvas) {
|
|
1630
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
1631
|
+
}
|
|
1632
|
+
this._prevSelectionSetting = void 0;
|
|
1633
|
+
}
|
|
1634
|
+
return new Promise((resolve, reject) => {
|
|
1331
1635
|
try {
|
|
1332
1636
|
const state = typeof serializedState === "string" ? JSON.parse(serializedState) : serializedState;
|
|
1333
1637
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
1334
|
-
this.canvas.loadFromJSON(state, () => {
|
|
1638
|
+
this.canvas.loadFromJSON(state, async () => {
|
|
1335
1639
|
try {
|
|
1640
|
+
if (this._disposed || !this.canvas) {
|
|
1641
|
+
reject(new Error("Editor was disposed while loading state"));
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
await this._waitForFabricImagesReady(this.canvas.getObjects());
|
|
1645
|
+
if (this._disposed || !this.canvas) {
|
|
1646
|
+
reject(new Error("Editor was disposed while loading state"));
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1336
1649
|
this._hideAllMaskLabels();
|
|
1337
1650
|
const canvasObjects = this.canvas.getObjects();
|
|
1338
1651
|
this.originalImage = canvasObjects.find((object) => object.type === "image" && !object.maskId) || null;
|
|
@@ -1380,18 +1693,44 @@ var ImageEditor = class {
|
|
|
1380
1693
|
this._updatePlaceholderStatus();
|
|
1381
1694
|
this._lastSnapshot = this._serializeCanvasState();
|
|
1382
1695
|
this._updateUI();
|
|
1696
|
+
resolve();
|
|
1383
1697
|
} catch (callbackError) {
|
|
1384
1698
|
this._reportError("loadFromState() failed", callbackError);
|
|
1385
|
-
|
|
1386
|
-
resolve();
|
|
1699
|
+
reject(callbackError);
|
|
1387
1700
|
}
|
|
1388
1701
|
});
|
|
1389
1702
|
} catch (error) {
|
|
1390
1703
|
this._reportError("loadFromState() failed", error);
|
|
1391
|
-
|
|
1704
|
+
reject(error);
|
|
1392
1705
|
}
|
|
1393
1706
|
});
|
|
1394
1707
|
}
|
|
1708
|
+
async _waitForFabricImagesReady(canvasObjects) {
|
|
1709
|
+
const imageObjects = (canvasObjects || []).filter((object) => object && object.type === "image");
|
|
1710
|
+
await Promise.all(imageObjects.map((object) => this._waitForImageElementReady(
|
|
1711
|
+
typeof object.getElement === "function" ? object.getElement() : object._element
|
|
1712
|
+
)));
|
|
1713
|
+
}
|
|
1714
|
+
_waitForImageElementReady(imageElement) {
|
|
1715
|
+
if (!imageElement) return Promise.resolve();
|
|
1716
|
+
if (imageElement.complete || imageElement.naturalWidth > 0 || imageElement.width > 0) return Promise.resolve();
|
|
1717
|
+
return new Promise((resolve, reject) => {
|
|
1718
|
+
let isSettled = false;
|
|
1719
|
+
const timerId = setTimeout(() => {
|
|
1720
|
+
settle(() => reject(new Error("Image load timed out while restoring state")));
|
|
1721
|
+
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
1722
|
+
const settle = (callback) => {
|
|
1723
|
+
if (isSettled) return;
|
|
1724
|
+
isSettled = true;
|
|
1725
|
+
clearTimeout(timerId);
|
|
1726
|
+
imageElement.onload = null;
|
|
1727
|
+
imageElement.onerror = null;
|
|
1728
|
+
callback();
|
|
1729
|
+
};
|
|
1730
|
+
imageElement.onload = () => settle(resolve);
|
|
1731
|
+
imageElement.onerror = (error) => settle(() => reject(error));
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1395
1734
|
/**
|
|
1396
1735
|
* Saves the current editable canvas state as an undoable history transition.
|
|
1397
1736
|
*
|
|
@@ -1403,9 +1742,8 @@ var ImageEditor = class {
|
|
|
1403
1742
|
*/
|
|
1404
1743
|
saveState() {
|
|
1405
1744
|
if (!this.canvas) return;
|
|
1406
|
-
const activeObject = this.canvas.getActiveObject();
|
|
1407
1745
|
try {
|
|
1408
|
-
const after = this.
|
|
1746
|
+
const after = this._captureCanvasStateOrThrow("saveState");
|
|
1409
1747
|
const before = this._lastSnapshot || after;
|
|
1410
1748
|
if (after === before) return;
|
|
1411
1749
|
let executedOnce = false;
|
|
@@ -1424,9 +1762,6 @@ var ImageEditor = class {
|
|
|
1424
1762
|
} catch (error) {
|
|
1425
1763
|
this._reportWarning("saveState: failed to save canvas snapshot", error);
|
|
1426
1764
|
} finally {
|
|
1427
|
-
if (activeObject && activeObject.maskId && !activeObject.__label && this.canvas.getObjects().includes(activeObject)) {
|
|
1428
|
-
this._handleSelectionChanged([activeObject]);
|
|
1429
|
-
}
|
|
1430
1765
|
this._updateUI();
|
|
1431
1766
|
}
|
|
1432
1767
|
}
|
|
@@ -1442,7 +1777,10 @@ var ImageEditor = class {
|
|
|
1442
1777
|
* @private
|
|
1443
1778
|
*/
|
|
1444
1779
|
_pushStateTransition(before, after) {
|
|
1445
|
-
if (!before || !after)
|
|
1780
|
+
if (!before || !after) {
|
|
1781
|
+
this._reportWarning("History transition skipped because a canvas snapshot is unavailable");
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1446
1784
|
if (before === after) return;
|
|
1447
1785
|
if (!this.historyManager) this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
1448
1786
|
const command = new Command(
|
|
@@ -1464,6 +1802,7 @@ var ImageEditor = class {
|
|
|
1464
1802
|
this._updateUI();
|
|
1465
1803
|
}).catch((error) => {
|
|
1466
1804
|
this._reportError("undo failed", error);
|
|
1805
|
+
throw error;
|
|
1467
1806
|
});
|
|
1468
1807
|
}
|
|
1469
1808
|
/**
|
|
@@ -1477,6 +1816,7 @@ var ImageEditor = class {
|
|
|
1477
1816
|
this._updateUI();
|
|
1478
1817
|
}).catch((error) => {
|
|
1479
1818
|
this._reportError("redo failed", error);
|
|
1819
|
+
throw error;
|
|
1480
1820
|
});
|
|
1481
1821
|
}
|
|
1482
1822
|
_rebindMaskEvents(mask) {
|
|
@@ -1498,22 +1838,17 @@ var ImageEditor = class {
|
|
|
1498
1838
|
metadata.originalStrokeWidth = Number.isFinite(Number(mask.strokeWidth)) ? Number(mask.strokeWidth) : 1;
|
|
1499
1839
|
}
|
|
1500
1840
|
if (Object.keys(metadata).length) mask.set(metadata);
|
|
1501
|
-
const normalStyle = {
|
|
1502
|
-
stroke: mask.originalStroke || "#ccc",
|
|
1503
|
-
strokeWidth: mask.originalStrokeWidth,
|
|
1504
|
-
opacity: mask.originalAlpha
|
|
1505
|
-
};
|
|
1506
|
-
const hoverStyle = {
|
|
1507
|
-
stroke: "#ff5500",
|
|
1508
|
-
strokeWidth: 2,
|
|
1509
|
-
opacity: Math.min(mask.originalAlpha + 0.2, 1)
|
|
1510
|
-
};
|
|
1511
1841
|
const mouseover = () => {
|
|
1512
|
-
mask.
|
|
1842
|
+
const opacity = Number(mask.originalAlpha);
|
|
1843
|
+
mask.set({
|
|
1844
|
+
stroke: "#ff5500",
|
|
1845
|
+
strokeWidth: 2,
|
|
1846
|
+
opacity: Math.min((Number.isFinite(opacity) ? opacity : 0.5) + 0.2, 1)
|
|
1847
|
+
});
|
|
1513
1848
|
if (mask.canvas) mask.canvas.requestRenderAll();
|
|
1514
1849
|
};
|
|
1515
1850
|
const mouseout = () => {
|
|
1516
|
-
mask.set(
|
|
1851
|
+
mask.set(this._getMaskNormalStyle(mask));
|
|
1517
1852
|
if (mask.canvas) mask.canvas.requestRenderAll();
|
|
1518
1853
|
};
|
|
1519
1854
|
mask.on("mouseover", mouseover);
|
|
@@ -1550,6 +1885,7 @@ var ImageEditor = class {
|
|
|
1550
1885
|
*/
|
|
1551
1886
|
createMask(config = {}) {
|
|
1552
1887
|
if (!this.canvas) return null;
|
|
1888
|
+
if (!this._canMutateNow("createMask")) return null;
|
|
1553
1889
|
const shapeType = config.shape || "rect";
|
|
1554
1890
|
const maskConfig = {
|
|
1555
1891
|
shape: shapeType,
|
|
@@ -1586,14 +1922,10 @@ var ImageEditor = class {
|
|
|
1586
1922
|
};
|
|
1587
1923
|
if (maskConfig.left === void 0 && this._lastMask) {
|
|
1588
1924
|
const previousMask = this._lastMask;
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
previousMaskRight += previousMask.width * (previousMask.scaleX ?? 1);
|
|
1594
|
-
}
|
|
1595
|
-
left = Math.round(previousMaskRight + maskConfig.gap);
|
|
1596
|
-
top = previousMask.top ?? firstOffset;
|
|
1925
|
+
if (typeof previousMask.setCoords === "function") previousMask.setCoords();
|
|
1926
|
+
const previousBounds = typeof previousMask.getBoundingRect === "function" ? previousMask.getBoundingRect(true, true) : { left: previousMask.left || firstOffset, top: previousMask.top || firstOffset, width: previousMask.width || 0 };
|
|
1927
|
+
left = Math.round(previousBounds.left + previousBounds.width + maskConfig.gap);
|
|
1928
|
+
top = Math.round(previousBounds.top ?? firstOffset);
|
|
1597
1929
|
} else {
|
|
1598
1930
|
left = resolveValue(maskConfig.left, firstOffset, "width");
|
|
1599
1931
|
top = resolveValue(maskConfig.top, firstOffset, "height");
|
|
@@ -1721,6 +2053,8 @@ var ImageEditor = class {
|
|
|
1721
2053
|
* The associated label is also removed. UI and mask list are updated.
|
|
1722
2054
|
*/
|
|
1723
2055
|
removeSelectedMask() {
|
|
2056
|
+
if (!this.canvas) return;
|
|
2057
|
+
if (!this._canMutateNow("removeSelectedMask")) return;
|
|
1724
2058
|
const activeObject = this.canvas.getActiveObject();
|
|
1725
2059
|
const selectedMasks = this._getModifiedMasks(activeObject);
|
|
1726
2060
|
if (!selectedMasks.length) return;
|
|
@@ -1746,6 +2080,8 @@ var ImageEditor = class {
|
|
|
1746
2080
|
* UI and internal mask placement memory are reset.
|
|
1747
2081
|
*/
|
|
1748
2082
|
removeAllMasks(options = {}) {
|
|
2083
|
+
if (!this.canvas) return;
|
|
2084
|
+
if (!this._canMutateNow("removeAllMasks")) return;
|
|
1749
2085
|
const saveHistory = options.saveHistory !== false;
|
|
1750
2086
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
1751
2087
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
@@ -1813,6 +2149,10 @@ var ImageEditor = class {
|
|
|
1813
2149
|
let textObject = null;
|
|
1814
2150
|
if (this.options.label && typeof this.options.label.create === "function") {
|
|
1815
2151
|
textObject = this.options.label.create(mask, fabric);
|
|
2152
|
+
if (!textObject || typeof textObject.set !== "function") {
|
|
2153
|
+
this._reportWarning("label.create() returned an invalid Fabric object; using the default label");
|
|
2154
|
+
textObject = null;
|
|
2155
|
+
}
|
|
1816
2156
|
}
|
|
1817
2157
|
if (!textObject) {
|
|
1818
2158
|
let labelText = mask.maskName;
|
|
@@ -1880,9 +2220,10 @@ var ImageEditor = class {
|
|
|
1880
2220
|
if (!mask) return;
|
|
1881
2221
|
if (!this.options.maskLabelOnSelect) return;
|
|
1882
2222
|
if (!mask.__label) return;
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2223
|
+
if (typeof mask.setCoords === "function") mask.setCoords();
|
|
2224
|
+
const bounds = mask.getBoundingRect ? mask.getBoundingRect(true, true) : null;
|
|
2225
|
+
if (!bounds) return;
|
|
2226
|
+
const tl = { x: bounds.left, y: bounds.top };
|
|
1886
2227
|
const center = mask.getCenterPoint();
|
|
1887
2228
|
const vx = center.x - tl.x;
|
|
1888
2229
|
const vy = center.y - tl.y;
|
|
@@ -1960,7 +2301,7 @@ var ImageEditor = class {
|
|
|
1960
2301
|
* @private
|
|
1961
2302
|
*/
|
|
1962
2303
|
_updateMaskList() {
|
|
1963
|
-
const maskListElement =
|
|
2304
|
+
const maskListElement = this._getElement("maskList");
|
|
1964
2305
|
if (!maskListElement) return;
|
|
1965
2306
|
maskListElement.innerHTML = "";
|
|
1966
2307
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
@@ -1968,13 +2309,20 @@ var ImageEditor = class {
|
|
|
1968
2309
|
const listItemElement = document.createElement("li");
|
|
1969
2310
|
listItemElement.className = "list-group-item mask-item";
|
|
1970
2311
|
listItemElement.textContent = mask.maskName;
|
|
1971
|
-
listItemElement.
|
|
1972
|
-
this.canvas.setActiveObject(mask);
|
|
1973
|
-
this._handleSelectionChanged([mask]);
|
|
1974
|
-
};
|
|
2312
|
+
listItemElement.dataset.maskId = String(mask.maskId);
|
|
1975
2313
|
maskListElement.appendChild(listItemElement);
|
|
1976
2314
|
});
|
|
1977
2315
|
}
|
|
2316
|
+
_handleMaskListClick(event) {
|
|
2317
|
+
if (!this.canvas) return;
|
|
2318
|
+
const itemElement = event.target && event.target.closest ? event.target.closest(".mask-item") : null;
|
|
2319
|
+
if (!itemElement || !itemElement.dataset) return;
|
|
2320
|
+
const maskId = Number(itemElement.dataset.maskId);
|
|
2321
|
+
const mask = this.canvas.getObjects().find((object) => Number(object.maskId) === maskId);
|
|
2322
|
+
if (!mask) return;
|
|
2323
|
+
this.canvas.setActiveObject(mask);
|
|
2324
|
+
this._handleSelectionChanged([mask]);
|
|
2325
|
+
}
|
|
1978
2326
|
/**
|
|
1979
2327
|
* Updates the visual selection (CSS 'active') state for the mask list in the DOM.
|
|
1980
2328
|
*
|
|
@@ -1982,12 +2330,13 @@ var ImageEditor = class {
|
|
|
1982
2330
|
* @private
|
|
1983
2331
|
*/
|
|
1984
2332
|
_updateMaskListSelection(selectedMask) {
|
|
1985
|
-
const maskListElement =
|
|
2333
|
+
const maskListElement = this._getElement("maskList");
|
|
1986
2334
|
if (!maskListElement) return;
|
|
1987
2335
|
const maskItems = maskListElement.querySelectorAll(".mask-item");
|
|
1988
2336
|
maskItems.forEach((item) => {
|
|
1989
|
-
const isSelected = !!selectedMask && item.
|
|
2337
|
+
const isSelected = !!selectedMask && Number(item.dataset.maskId) === Number(selectedMask.maskId);
|
|
1990
2338
|
item.classList.toggle("active", isSelected);
|
|
2339
|
+
item.classList.toggle("selected", isSelected);
|
|
1991
2340
|
});
|
|
1992
2341
|
}
|
|
1993
2342
|
/**
|
|
@@ -2002,6 +2351,7 @@ var ImageEditor = class {
|
|
|
2002
2351
|
*/
|
|
2003
2352
|
async mergeMasks() {
|
|
2004
2353
|
if (!this.originalImage) return;
|
|
2354
|
+
this._assertIdleForOperation("mergeMasks");
|
|
2005
2355
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2006
2356
|
if (!masks.length) return;
|
|
2007
2357
|
this.canvas.discardActiveObject();
|
|
@@ -2010,11 +2360,12 @@ var ImageEditor = class {
|
|
|
2010
2360
|
const beforeJson = this._serializeCanvasState();
|
|
2011
2361
|
const merged = await this.exportImageBase64({ exportImageArea: true, multiplier: this.options.exportMultiplier });
|
|
2012
2362
|
this.removeAllMasks({ saveHistory: false });
|
|
2013
|
-
await this.loadImage(merged, { preserveScroll: true });
|
|
2363
|
+
await this.loadImage(merged, { preserveScroll: true, resetMaskCounter: false });
|
|
2014
2364
|
const afterJson = this._serializeCanvasState();
|
|
2015
2365
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2016
2366
|
} catch (error) {
|
|
2017
2367
|
this._reportError("merge error", error);
|
|
2368
|
+
throw error;
|
|
2018
2369
|
}
|
|
2019
2370
|
}
|
|
2020
2371
|
/**
|
|
@@ -2036,6 +2387,7 @@ var ImageEditor = class {
|
|
|
2036
2387
|
*/
|
|
2037
2388
|
downloadImage(fileName = this.options.defaultDownloadFileName) {
|
|
2038
2389
|
if (!this.originalImage) return;
|
|
2390
|
+
if (!this._canMutateNow("downloadImage")) return;
|
|
2039
2391
|
const exportImageArea = this.options.exportImageAreaByDefault;
|
|
2040
2392
|
this.exportImageBase64({ exportImageArea, multiplier: this.options.exportMultiplier }).then((imageBase64) => {
|
|
2041
2393
|
const link = document.createElement("a");
|
|
@@ -2064,6 +2416,7 @@ var ImageEditor = class {
|
|
|
2064
2416
|
*/
|
|
2065
2417
|
async exportImageBase64(options = {}) {
|
|
2066
2418
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
2419
|
+
this._assertIdleForOperation("exportImageBase64");
|
|
2067
2420
|
const exportImageArea = typeof options.exportImageArea === "boolean" ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2068
2421
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2069
2422
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
@@ -2080,7 +2433,7 @@ var ImageEditor = class {
|
|
|
2080
2433
|
this.originalImage.setCoords();
|
|
2081
2434
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2082
2435
|
const exportRegion = this._getClampedCanvasRegion(imageBounds, { includePartialPixels: false });
|
|
2083
|
-
return
|
|
2436
|
+
return this._exportCanvasRegionToDataURL({
|
|
2084
2437
|
...exportRegion,
|
|
2085
2438
|
multiplier,
|
|
2086
2439
|
quality,
|
|
@@ -2120,7 +2473,7 @@ var ImageEditor = class {
|
|
|
2120
2473
|
this.originalImage.setCoords();
|
|
2121
2474
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2122
2475
|
const exportRegion = this._getClampedCanvasRegion(imageBounds, { includePartialPixels: false });
|
|
2123
|
-
finalBase64 =
|
|
2476
|
+
finalBase64 = this._exportCanvasRegionToDataURL({
|
|
2124
2477
|
...exportRegion,
|
|
2125
2478
|
multiplier,
|
|
2126
2479
|
quality,
|
|
@@ -2176,6 +2529,7 @@ var ImageEditor = class {
|
|
|
2176
2529
|
*/
|
|
2177
2530
|
async exportImageFile(options = {}) {
|
|
2178
2531
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
2532
|
+
this._assertIdleForOperation("exportImageFile");
|
|
2179
2533
|
const {
|
|
2180
2534
|
mergeMask = true,
|
|
2181
2535
|
fileType = "jpeg",
|
|
@@ -2211,6 +2565,7 @@ var ImageEditor = class {
|
|
|
2211
2565
|
offscreenCanvas.width = imageElement.width;
|
|
2212
2566
|
offscreenCanvas.height = imageElement.height;
|
|
2213
2567
|
const context = offscreenCanvas.getContext("2d");
|
|
2568
|
+
if (!context) throw new Error("Unable to create 2D canvas context for export conversion");
|
|
2214
2569
|
context.drawImage(imageElement, 0, 0);
|
|
2215
2570
|
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, quality);
|
|
2216
2571
|
resolve(convertedDataUrl);
|
|
@@ -2278,7 +2633,9 @@ var ImageEditor = class {
|
|
|
2278
2633
|
if (this._cropHandlers && this._cropHandlers.length) {
|
|
2279
2634
|
this._cropHandlers.forEach((targetHandlers) => {
|
|
2280
2635
|
targetHandlers.handlers.forEach((handlerRecord) => {
|
|
2281
|
-
targetHandlers.target.off
|
|
2636
|
+
if (targetHandlers.target && typeof targetHandlers.target.off === "function") {
|
|
2637
|
+
targetHandlers.target.off(handlerRecord.eventName, handlerRecord.handler);
|
|
2638
|
+
}
|
|
2282
2639
|
});
|
|
2283
2640
|
});
|
|
2284
2641
|
}
|
|
@@ -2286,7 +2643,7 @@ var ImageEditor = class {
|
|
|
2286
2643
|
void error;
|
|
2287
2644
|
}
|
|
2288
2645
|
try {
|
|
2289
|
-
this.canvas.remove(this._cropRect);
|
|
2646
|
+
if (this.canvas) this.canvas.remove(this._cropRect);
|
|
2290
2647
|
} catch (error) {
|
|
2291
2648
|
void error;
|
|
2292
2649
|
}
|
|
@@ -2304,7 +2661,9 @@ var ImageEditor = class {
|
|
|
2304
2661
|
*/
|
|
2305
2662
|
enterCropMode() {
|
|
2306
2663
|
if (!this.canvas || !this.originalImage || this._cropMode) return;
|
|
2664
|
+
if (!this._canMutateNow("enterCropMode")) return;
|
|
2307
2665
|
if (!this.isImageLoaded()) return;
|
|
2666
|
+
this._removeCropRect();
|
|
2308
2667
|
this._cropMode = true;
|
|
2309
2668
|
this._prevSelectionSetting = this.canvas.selection;
|
|
2310
2669
|
this.canvas.selection = false;
|
|
@@ -2420,6 +2779,7 @@ var ImageEditor = class {
|
|
|
2420
2779
|
*/
|
|
2421
2780
|
async applyCrop() {
|
|
2422
2781
|
if (!this.canvas || !this._cropMode || !this._cropRect) return;
|
|
2782
|
+
this._assertIdleForOperation("applyCrop");
|
|
2423
2783
|
this._cropRect.setCoords();
|
|
2424
2784
|
const rectBounds = this._cropRect.getBoundingRect(true, true);
|
|
2425
2785
|
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
@@ -2444,12 +2804,8 @@ var ImageEditor = class {
|
|
|
2444
2804
|
this._removeLabelForMask(mask);
|
|
2445
2805
|
this.canvas.remove(mask);
|
|
2446
2806
|
if (shouldPreserveMasks && intersectsCrop) {
|
|
2447
|
-
mask.
|
|
2448
|
-
|
|
2449
|
-
top: (mask.top || 0) - cropRegion.sourceY,
|
|
2450
|
-
visible: true
|
|
2451
|
-
});
|
|
2452
|
-
mask.setCoords();
|
|
2807
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
2808
|
+
mask.set({ visible: true });
|
|
2453
2809
|
preservedMasks.push(mask);
|
|
2454
2810
|
}
|
|
2455
2811
|
} catch (error) {
|
|
@@ -2480,7 +2836,7 @@ var ImageEditor = class {
|
|
|
2480
2836
|
return;
|
|
2481
2837
|
}
|
|
2482
2838
|
try {
|
|
2483
|
-
await this.loadImage(croppedBase64);
|
|
2839
|
+
await this.loadImage(croppedBase64, { resetMaskCounter: false });
|
|
2484
2840
|
if (preservedMasks.length) {
|
|
2485
2841
|
preservedMasks.forEach((mask) => {
|
|
2486
2842
|
this._rebindMaskEvents(mask);
|
|
@@ -2498,7 +2854,7 @@ var ImageEditor = class {
|
|
|
2498
2854
|
}
|
|
2499
2855
|
let afterJson;
|
|
2500
2856
|
try {
|
|
2501
|
-
afterJson = this._serializeCanvasState();
|
|
2857
|
+
afterJson = preservedMasks.length ? this._serializeCanvasState() : this._lastSnapshot;
|
|
2502
2858
|
} catch (error) {
|
|
2503
2859
|
this._reportWarning("applyCrop: failed to serialize after state", error);
|
|
2504
2860
|
afterJson = null;
|
|
@@ -2518,7 +2874,7 @@ var ImageEditor = class {
|
|
|
2518
2874
|
* @private
|
|
2519
2875
|
*/
|
|
2520
2876
|
_updateInputs() {
|
|
2521
|
-
const scaleInputElement =
|
|
2877
|
+
const scaleInputElement = this._getElement("scaleRate");
|
|
2522
2878
|
if (scaleInputElement) scaleInputElement.value = Math.round(this.currentScale * 100);
|
|
2523
2879
|
}
|
|
2524
2880
|
/**
|
|
@@ -2527,6 +2883,7 @@ var ImageEditor = class {
|
|
|
2527
2883
|
* @private
|
|
2528
2884
|
*/
|
|
2529
2885
|
_updateUI() {
|
|
2886
|
+
if (!this.canvas) return;
|
|
2530
2887
|
const hasImage = !!this.originalImage;
|
|
2531
2888
|
const masks = hasImage ? this.canvas.getObjects().filter((object) => object.maskId) : [];
|
|
2532
2889
|
const hasMasks = masks.length > 0;
|
|
@@ -2538,7 +2895,7 @@ var ImageEditor = class {
|
|
|
2538
2895
|
const isInCropMode = !!this._cropMode;
|
|
2539
2896
|
if (isInCropMode) {
|
|
2540
2897
|
for (const key of Object.keys(this.elements || {})) {
|
|
2541
|
-
const element =
|
|
2898
|
+
const element = this._getElement(key);
|
|
2542
2899
|
if (!element) continue;
|
|
2543
2900
|
if (key === "applyCropBtn" || key === "cancelCropBtn") {
|
|
2544
2901
|
this._setDisabled(key, false);
|
|
@@ -2574,7 +2931,7 @@ var ImageEditor = class {
|
|
|
2574
2931
|
* @private
|
|
2575
2932
|
*/
|
|
2576
2933
|
_setDisabled(key, disabled) {
|
|
2577
|
-
const element =
|
|
2934
|
+
const element = this._getElement(key);
|
|
2578
2935
|
if (!element) return;
|
|
2579
2936
|
if ("disabled" in element) {
|
|
2580
2937
|
element.disabled = !!disabled;
|
|
@@ -2608,9 +2965,18 @@ var ImageEditor = class {
|
|
|
2608
2965
|
* @private
|
|
2609
2966
|
*/
|
|
2610
2967
|
_setPlaceholderVisible(show) {
|
|
2611
|
-
if (
|
|
2612
|
-
this.
|
|
2613
|
-
|
|
2968
|
+
if (this.placeholderElement) this._setElementVisible(this.placeholderElement, show);
|
|
2969
|
+
const canvasVisibilityElement = this._getCanvasVisibilityElement();
|
|
2970
|
+
if (canvasVisibilityElement && canvasVisibilityElement !== this.placeholderElement) {
|
|
2971
|
+
this._setElementVisible(canvasVisibilityElement, !show);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
_getCanvasVisibilityElement() {
|
|
2975
|
+
const wrapperElement = this.canvas && this.canvas.wrapperEl ? this.canvas.wrapperEl : null;
|
|
2976
|
+
if (this.containerElement && this.placeholderElement && (this.containerElement === this.placeholderElement || this.containerElement.contains(this.placeholderElement))) {
|
|
2977
|
+
return wrapperElement || this.canvasElement;
|
|
2978
|
+
}
|
|
2979
|
+
return this.containerElement || wrapperElement || this.canvasElement;
|
|
2614
2980
|
}
|
|
2615
2981
|
/**
|
|
2616
2982
|
* Updates element visibility.
|
|
@@ -2622,9 +2988,34 @@ var ImageEditor = class {
|
|
|
2622
2988
|
*/
|
|
2623
2989
|
_setElementVisible(element, isVisible) {
|
|
2624
2990
|
if (!element) return;
|
|
2991
|
+
this._rememberElementVisibility(element);
|
|
2625
2992
|
element.hidden = !isVisible;
|
|
2626
2993
|
element.setAttribute("aria-hidden", isVisible ? "false" : "true");
|
|
2627
|
-
if (
|
|
2994
|
+
if (element.classList) {
|
|
2995
|
+
element.classList.toggle("d-none", !isVisible);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
_rememberElementVisibility(element) {
|
|
2999
|
+
if (!element || this._visibilityStateByElement.has(element)) return;
|
|
3000
|
+
this._visibilityStateByElement.set(element, this._captureElementVisibility(element));
|
|
3001
|
+
}
|
|
3002
|
+
_captureElementVisibility(element) {
|
|
3003
|
+
if (!element) return null;
|
|
3004
|
+
return {
|
|
3005
|
+
hidden: element.hidden,
|
|
3006
|
+
ariaHidden: element.getAttribute("aria-hidden"),
|
|
3007
|
+
className: element.className
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
_restoreElementVisibility(element, state) {
|
|
3011
|
+
if (!element || !state) return;
|
|
3012
|
+
element.hidden = !!state.hidden;
|
|
3013
|
+
if (state.ariaHidden === null) {
|
|
3014
|
+
element.removeAttribute("aria-hidden");
|
|
3015
|
+
} else {
|
|
3016
|
+
element.setAttribute("aria-hidden", state.ariaHidden);
|
|
3017
|
+
}
|
|
3018
|
+
element.className = state.className || "";
|
|
2628
3019
|
}
|
|
2629
3020
|
/**
|
|
2630
3021
|
* Cleans up and disposes of the canvas and related references.
|
|
@@ -2632,10 +3023,14 @@ var ImageEditor = class {
|
|
|
2632
3023
|
* @public
|
|
2633
3024
|
*/
|
|
2634
3025
|
dispose() {
|
|
3026
|
+
this._disposed = true;
|
|
3027
|
+
this._rejectActiveAnimations(new Error("Editor disposed during animation"));
|
|
3028
|
+
if (this.animationQueue) {
|
|
3029
|
+
this.animationQueue.cancelAll(new Error("Editor disposed"));
|
|
3030
|
+
}
|
|
2635
3031
|
try {
|
|
2636
|
-
for (const key
|
|
2637
|
-
const
|
|
2638
|
-
const element = document.getElementById(this.elements[key]);
|
|
3032
|
+
for (const [key, handlers] of Object.entries(this._handlersByElementKey || {})) {
|
|
3033
|
+
const element = this._getElement(key);
|
|
2639
3034
|
if (!element) continue;
|
|
2640
3035
|
handlers.forEach((handlerRecord) => {
|
|
2641
3036
|
try {
|
|
@@ -2656,9 +3051,28 @@ var ImageEditor = class {
|
|
|
2656
3051
|
}
|
|
2657
3052
|
this._cropRect = null;
|
|
2658
3053
|
}
|
|
2659
|
-
if (this.containerElement && this._containerOriginalOverflow
|
|
3054
|
+
if (this.containerElement && this._containerOriginalOverflow) {
|
|
3055
|
+
try {
|
|
3056
|
+
this._restoreContainerOverflowState();
|
|
3057
|
+
} catch (error) {
|
|
3058
|
+
void error;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
if (this._visibilityStateByElement) {
|
|
3062
|
+
try {
|
|
3063
|
+
[this.placeholderElement, this._getCanvasVisibilityElement()].forEach((element) => {
|
|
3064
|
+
const state = element ? this._visibilityStateByElement.get(element) : null;
|
|
3065
|
+
if (state) this._restoreElementVisibility(element, state);
|
|
3066
|
+
});
|
|
3067
|
+
} catch (error) {
|
|
3068
|
+
void error;
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
if (this.canvasElement && this._canvasElementOriginalStyle) {
|
|
2660
3072
|
try {
|
|
2661
|
-
this.
|
|
3073
|
+
this.canvasElement.style.display = this._canvasElementOriginalStyle.display;
|
|
3074
|
+
this.canvasElement.style.width = this._canvasElementOriginalStyle.width;
|
|
3075
|
+
this.canvasElement.style.height = this._canvasElementOriginalStyle.height;
|
|
2662
3076
|
} catch (error) {
|
|
2663
3077
|
void error;
|
|
2664
3078
|
}
|
|
@@ -2674,6 +3088,19 @@ var ImageEditor = class {
|
|
|
2674
3088
|
this.isImageLoadedToCanvas = false;
|
|
2675
3089
|
}
|
|
2676
3090
|
this._handlersByElementKey = {};
|
|
3091
|
+
this._elementCache = {};
|
|
3092
|
+
this._clearMaskPlacementMemory();
|
|
3093
|
+
this.originalImage = null;
|
|
3094
|
+
this.baseImageScale = 1;
|
|
3095
|
+
this.currentScale = 1;
|
|
3096
|
+
this.currentRotation = 0;
|
|
3097
|
+
this.isAnimating = false;
|
|
3098
|
+
this._cropMode = false;
|
|
3099
|
+
this._cropRect = null;
|
|
3100
|
+
this._cropHandlers = [];
|
|
3101
|
+
this._cropPrevEvented = null;
|
|
3102
|
+
this._prevSelectionSetting = void 0;
|
|
3103
|
+
this._initialized = false;
|
|
2677
3104
|
}
|
|
2678
3105
|
};
|
|
2679
3106
|
var AnimationQueue = class {
|
|
@@ -2683,6 +3110,7 @@ var AnimationQueue = class {
|
|
|
2683
3110
|
constructor() {
|
|
2684
3111
|
this.animationTasks = [];
|
|
2685
3112
|
this.isRunning = false;
|
|
3113
|
+
this.currentTask = null;
|
|
2686
3114
|
}
|
|
2687
3115
|
/**
|
|
2688
3116
|
* Adds an animation function to the queue.
|
|
@@ -2692,12 +3120,29 @@ var AnimationQueue = class {
|
|
|
2692
3120
|
*/
|
|
2693
3121
|
async add(animationFn) {
|
|
2694
3122
|
return new Promise((resolve, reject) => {
|
|
2695
|
-
this.animationTasks.push({ animationFn, resolve, reject });
|
|
3123
|
+
this.animationTasks.push({ animationFn, resolve, reject, isSettled: false });
|
|
2696
3124
|
if (!this.isRunning) {
|
|
2697
3125
|
this._drainQueue();
|
|
2698
3126
|
}
|
|
2699
3127
|
});
|
|
2700
3128
|
}
|
|
3129
|
+
isBusy() {
|
|
3130
|
+
return this.isRunning || this.animationTasks.length > 0;
|
|
3131
|
+
}
|
|
3132
|
+
cancelAll(reason = new Error("Animation queue cancelled")) {
|
|
3133
|
+
const cancellationError = reason instanceof Error ? reason : new Error(String(reason));
|
|
3134
|
+
const tasks = [
|
|
3135
|
+
...this.currentTask ? [this.currentTask] : [],
|
|
3136
|
+
...this.animationTasks.splice(0)
|
|
3137
|
+
];
|
|
3138
|
+
tasks.forEach((task) => {
|
|
3139
|
+
if (!task || task.isSettled) return;
|
|
3140
|
+
task.isSettled = true;
|
|
3141
|
+
task.reject(cancellationError);
|
|
3142
|
+
});
|
|
3143
|
+
this.isRunning = false;
|
|
3144
|
+
this.currentTask = null;
|
|
3145
|
+
}
|
|
2701
3146
|
/**
|
|
2702
3147
|
* Runs queued animation tasks sequentially until the queue is empty.
|
|
2703
3148
|
*
|
|
@@ -2705,19 +3150,27 @@ var AnimationQueue = class {
|
|
|
2705
3150
|
* @returns {Promise<void>}
|
|
2706
3151
|
*/
|
|
2707
3152
|
async _drainQueue() {
|
|
2708
|
-
if (this.
|
|
2709
|
-
this.isRunning = false;
|
|
2710
|
-
return;
|
|
2711
|
-
}
|
|
3153
|
+
if (this.isRunning) return;
|
|
2712
3154
|
this.isRunning = true;
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
3155
|
+
while (this.animationTasks.length > 0) {
|
|
3156
|
+
const task = this.animationTasks.shift();
|
|
3157
|
+
this.currentTask = task;
|
|
3158
|
+
try {
|
|
3159
|
+
const result = await task.animationFn();
|
|
3160
|
+
if (!task.isSettled) {
|
|
3161
|
+
task.isSettled = true;
|
|
3162
|
+
task.resolve(result);
|
|
3163
|
+
}
|
|
3164
|
+
} catch (error) {
|
|
3165
|
+
if (!task.isSettled) {
|
|
3166
|
+
task.isSettled = true;
|
|
3167
|
+
task.reject(error);
|
|
3168
|
+
}
|
|
3169
|
+
} finally {
|
|
3170
|
+
if (this.currentTask === task) this.currentTask = null;
|
|
3171
|
+
}
|
|
2719
3172
|
}
|
|
2720
|
-
|
|
3173
|
+
this.isRunning = false;
|
|
2721
3174
|
}
|
|
2722
3175
|
};
|
|
2723
3176
|
var Command = class {
|
|
@@ -2748,15 +3201,8 @@ var HistoryManager = class {
|
|
|
2748
3201
|
* @private
|
|
2749
3202
|
*/
|
|
2750
3203
|
enqueue(task) {
|
|
2751
|
-
const nextTask = this.pending.then(
|
|
2752
|
-
|
|
2753
|
-
const resetPending = () => {
|
|
2754
|
-
if (this.pending === pendingAfterTask) {
|
|
2755
|
-
this.pending = Promise.resolve();
|
|
2756
|
-
}
|
|
2757
|
-
};
|
|
2758
|
-
pendingAfterTask = nextTask.then(resetPending, resetPending);
|
|
2759
|
-
this.pending = pendingAfterTask;
|
|
3204
|
+
const nextTask = this.pending.then(() => Promise.resolve().then(task));
|
|
3205
|
+
this.pending = nextTask.catch(() => void 0);
|
|
2760
3206
|
return nextTask;
|
|
2761
3207
|
}
|
|
2762
3208
|
/**
|
|
@@ -2767,8 +3213,14 @@ var HistoryManager = class {
|
|
|
2767
3213
|
* @returns {void}
|
|
2768
3214
|
*/
|
|
2769
3215
|
execute(command) {
|
|
2770
|
-
command.execute();
|
|
3216
|
+
const result = command.execute();
|
|
3217
|
+
if (result && typeof result.then === "function") {
|
|
3218
|
+
return Promise.resolve(result).then(() => {
|
|
3219
|
+
this.push(command);
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
2771
3222
|
this.push(command);
|
|
3223
|
+
return result;
|
|
2772
3224
|
}
|
|
2773
3225
|
/**
|
|
2774
3226
|
* Pushes an already-applied command onto the history stack.
|
|
@@ -2784,9 +3236,8 @@ var HistoryManager = class {
|
|
|
2784
3236
|
this.history.push(command);
|
|
2785
3237
|
if (this.history.length > this.maxSize) {
|
|
2786
3238
|
this.history.shift();
|
|
2787
|
-
} else {
|
|
2788
|
-
this.currentIndex++;
|
|
2789
3239
|
}
|
|
3240
|
+
this.currentIndex = this.history.length - 1;
|
|
2790
3241
|
}
|
|
2791
3242
|
/**
|
|
2792
3243
|
* Checks whether an undo operation is possible.
|