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