@bensitu/image-editor 1.3.1 → 1.4.1
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 +1019 -375
- 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 +1019 -375
- package/dist/image-editor.esm.mjs.map +2 -2
- package/dist/image-editor.js +1019 -375
- 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 +25 -12
- package/package.json +3 -5
- package/src/image-editor.js +1105 -396
|
@@ -5,12 +5,13 @@ import fabricModule from "fabric";
|
|
|
5
5
|
/**
|
|
6
6
|
* @file image-editor.js
|
|
7
7
|
* @module image-editor
|
|
8
|
-
* @version 1.
|
|
8
|
+
* @version 1.4.1
|
|
9
9
|
* @author Ben Situ
|
|
10
10
|
* @license MIT
|
|
11
11
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
12
12
|
*/
|
|
13
13
|
var fabric = null;
|
|
14
|
+
var INTERNAL_OPERATION_TOKEN = /* @__PURE__ */ Symbol("ImageEditorInternalOperation");
|
|
14
15
|
function getGlobalScope() {
|
|
15
16
|
if (typeof globalThis !== "undefined") return globalThis;
|
|
16
17
|
if (typeof self !== "undefined") return self;
|
|
@@ -72,6 +73,8 @@ var ImageEditor = class {
|
|
|
72
73
|
downsampleMaxWidth: 4e3,
|
|
73
74
|
downsampleMaxHeight: 3e3,
|
|
74
75
|
downsampleQuality: 0.92,
|
|
76
|
+
preserveSourceFormat: true,
|
|
77
|
+
downsampleMimeType: null,
|
|
75
78
|
imageLoadTimeoutMs: 3e4,
|
|
76
79
|
exportMultiplier: 1,
|
|
77
80
|
exportImageAreaByDefault: true,
|
|
@@ -116,10 +119,15 @@ var ImageEditor = class {
|
|
|
116
119
|
this.currentRotation = 0;
|
|
117
120
|
this.maskCounter = 0;
|
|
118
121
|
this.isAnimating = false;
|
|
122
|
+
this._isLoading = false;
|
|
123
|
+
this._activeOperationName = null;
|
|
124
|
+
this._activeOperationToken = null;
|
|
119
125
|
this.elements = {};
|
|
120
126
|
this.isImageLoadedToCanvas = false;
|
|
121
127
|
this.maxHistorySize = 50;
|
|
122
128
|
this._handlersByElementKey = {};
|
|
129
|
+
this._elementCache = {};
|
|
130
|
+
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
123
131
|
this._lastMask = null;
|
|
124
132
|
this._lastMaskInitialLeft = null;
|
|
125
133
|
this._lastMaskInitialTop = null;
|
|
@@ -130,8 +138,14 @@ var ImageEditor = class {
|
|
|
130
138
|
this._cropHandlers = [];
|
|
131
139
|
this._cropPrevEvented = null;
|
|
132
140
|
this._prevSelectionSetting = void 0;
|
|
133
|
-
this._containerOriginalOverflow =
|
|
141
|
+
this._containerOriginalOverflow = null;
|
|
142
|
+
this._lastContainerViewportSize = null;
|
|
143
|
+
this._canvasElementOriginalStyle = null;
|
|
144
|
+
this._visibilityStateByElement = /* @__PURE__ */ new WeakMap();
|
|
134
145
|
this._scrollbarSizeCache = null;
|
|
146
|
+
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
147
|
+
this._disposed = false;
|
|
148
|
+
this._initialized = false;
|
|
135
149
|
this.onImageLoaded = typeof options.onImageLoaded === "function" ? options.onImageLoaded : null;
|
|
136
150
|
this.animationQueue = new AnimationQueue();
|
|
137
151
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
@@ -195,6 +209,20 @@ var ImageEditor = class {
|
|
|
195
209
|
*/
|
|
196
210
|
init(idMap = {}) {
|
|
197
211
|
if (!this._fabricLoaded) return;
|
|
212
|
+
if (this._initialized || this.canvas) this.dispose();
|
|
213
|
+
this._disposed = false;
|
|
214
|
+
this._initialized = true;
|
|
215
|
+
this.animationQueue = new AnimationQueue();
|
|
216
|
+
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
217
|
+
this._visibilityStateByElement = /* @__PURE__ */ new WeakMap();
|
|
218
|
+
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
219
|
+
this._isLoading = false;
|
|
220
|
+
this._activeOperationName = null;
|
|
221
|
+
this._activeOperationToken = null;
|
|
222
|
+
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
223
|
+
this._containerOriginalOverflow = null;
|
|
224
|
+
this._lastContainerViewportSize = null;
|
|
225
|
+
this._canvasElementOriginalStyle = null;
|
|
198
226
|
const defaults = {
|
|
199
227
|
canvas: "fabricCanvas",
|
|
200
228
|
canvasContainer: null,
|
|
@@ -222,6 +250,7 @@ var ImageEditor = class {
|
|
|
222
250
|
cancelCropBtn: "cancelCropBtn"
|
|
223
251
|
};
|
|
224
252
|
this.elements = { ...defaults, ...idMap };
|
|
253
|
+
this._elementCache = {};
|
|
225
254
|
this._initCanvas();
|
|
226
255
|
this._bindEvents();
|
|
227
256
|
this._updateInputs();
|
|
@@ -256,16 +285,22 @@ var ImageEditor = class {
|
|
|
256
285
|
* @private
|
|
257
286
|
*/
|
|
258
287
|
_initCanvas() {
|
|
259
|
-
const canvasElement =
|
|
288
|
+
const canvasElement = this._getElement("canvas");
|
|
260
289
|
if (!canvasElement) throw new Error("Canvas is not found: " + this.elements.canvas);
|
|
261
290
|
this.canvasElement = canvasElement;
|
|
291
|
+
this._canvasElementOriginalStyle = {
|
|
292
|
+
display: canvasElement.style.display || "",
|
|
293
|
+
width: canvasElement.style.width || "",
|
|
294
|
+
height: canvasElement.style.height || "",
|
|
295
|
+
maxWidth: canvasElement.style.maxWidth || ""
|
|
296
|
+
};
|
|
262
297
|
if (this.elements.canvasContainer) {
|
|
263
|
-
const containerElement =
|
|
298
|
+
const containerElement = this._getElement("canvasContainer");
|
|
264
299
|
this.containerElement = containerElement || canvasElement.parentElement;
|
|
265
300
|
} else {
|
|
266
301
|
this.containerElement = canvasElement.parentElement;
|
|
267
302
|
}
|
|
268
|
-
this.placeholderElement =
|
|
303
|
+
this.placeholderElement = this._getElement("imgPlaceholder") || null;
|
|
269
304
|
let initialWidth = this.options.canvasWidth;
|
|
270
305
|
let initialHeight = this.options.canvasHeight;
|
|
271
306
|
if (this.containerElement) {
|
|
@@ -274,6 +309,10 @@ var ImageEditor = class {
|
|
|
274
309
|
if (containerWidth > 0 && containerHeight > 0) {
|
|
275
310
|
initialWidth = containerWidth;
|
|
276
311
|
initialHeight = containerHeight;
|
|
312
|
+
this._lastContainerViewportSize = {
|
|
313
|
+
width: containerWidth,
|
|
314
|
+
height: containerHeight
|
|
315
|
+
};
|
|
277
316
|
}
|
|
278
317
|
}
|
|
279
318
|
this.canvas = new fabric.Canvas(canvasElement, {
|
|
@@ -298,6 +337,23 @@ var ImageEditor = class {
|
|
|
298
337
|
this.canvas.on("object:modified", (event) => this._handleObjectModified(event.target));
|
|
299
338
|
this.canvasElement.style.display = "block";
|
|
300
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Returns a configured DOM element and caches lookups for hot UI paths.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} key - Key in the configured element map.
|
|
344
|
+
* @returns {HTMLElement|null} The configured element, or null when missing.
|
|
345
|
+
* @private
|
|
346
|
+
*/
|
|
347
|
+
_getElement(key) {
|
|
348
|
+
const id = this.elements && this.elements[key];
|
|
349
|
+
if (!id) return null;
|
|
350
|
+
if (this._elementCache && Object.prototype.hasOwnProperty.call(this._elementCache, key)) {
|
|
351
|
+
return this._elementCache[key];
|
|
352
|
+
}
|
|
353
|
+
const element = document.getElementById(id);
|
|
354
|
+
if (this._elementCache) this._elementCache[key] = element || null;
|
|
355
|
+
return element || null;
|
|
356
|
+
}
|
|
301
357
|
/**
|
|
302
358
|
* Records a history entry after Fabric finishes modifying one or more masks.
|
|
303
359
|
*
|
|
@@ -338,9 +394,7 @@ var ImageEditor = class {
|
|
|
338
394
|
*/
|
|
339
395
|
_syncContainerOverflow(options = {}) {
|
|
340
396
|
if (!this.containerElement || !this.containerElement.style) return;
|
|
341
|
-
|
|
342
|
-
this._containerOriginalOverflow = this.containerElement.style.overflow || "";
|
|
343
|
-
}
|
|
397
|
+
this._captureContainerOverflowState();
|
|
344
398
|
const shouldPreserveScroll = options.preserveScroll === true;
|
|
345
399
|
if (this.options.coverImageToCanvas) {
|
|
346
400
|
this.containerElement.style.overflow = "scroll";
|
|
@@ -355,58 +409,83 @@ var ImageEditor = class {
|
|
|
355
409
|
this.containerElement.scrollTop = 0;
|
|
356
410
|
}
|
|
357
411
|
} else {
|
|
358
|
-
this.
|
|
412
|
+
this._restoreContainerOverflowState();
|
|
359
413
|
}
|
|
360
414
|
}
|
|
415
|
+
_captureContainerOverflowState() {
|
|
416
|
+
if (!this.containerElement || !this.containerElement.style || this._containerOriginalOverflow) return;
|
|
417
|
+
this._containerOriginalOverflow = {
|
|
418
|
+
overflow: this.containerElement.style.overflow || "",
|
|
419
|
+
overflowX: this.containerElement.style.overflowX || "",
|
|
420
|
+
overflowY: this.containerElement.style.overflowY || ""
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
_restoreContainerOverflowState() {
|
|
424
|
+
if (!this.containerElement || !this.containerElement.style || !this._containerOriginalOverflow) return;
|
|
425
|
+
this.containerElement.style.overflow = this._containerOriginalOverflow.overflow;
|
|
426
|
+
this.containerElement.style.overflowX = this._containerOriginalOverflow.overflowX;
|
|
427
|
+
this.containerElement.style.overflowY = this._containerOriginalOverflow.overflowY;
|
|
428
|
+
}
|
|
429
|
+
_restoreContainerOverflowSnapshot(snapshot) {
|
|
430
|
+
if (!this.containerElement || !this.containerElement.style || !snapshot) return;
|
|
431
|
+
this.containerElement.style.overflow = snapshot.overflow || "";
|
|
432
|
+
this.containerElement.style.overflowX = snapshot.overflowX || "";
|
|
433
|
+
this.containerElement.style.overflowY = snapshot.overflowY || "";
|
|
434
|
+
}
|
|
361
435
|
/**
|
|
362
436
|
* DOM / UI bindings
|
|
363
437
|
* @private
|
|
364
438
|
*/
|
|
365
439
|
_bindEvents() {
|
|
366
440
|
this._bindIfExists("uploadArea", "click", () => {
|
|
367
|
-
const uploadAreaElement =
|
|
441
|
+
const uploadAreaElement = this._getElement("uploadArea");
|
|
368
442
|
if (this._isElementDisabled(uploadAreaElement)) return;
|
|
369
|
-
|
|
443
|
+
this._getElement("imageInput")?.click();
|
|
370
444
|
});
|
|
371
445
|
this._bindIfExists("imageInput", "change", (event) => {
|
|
372
446
|
const file = event.target.files && event.target.files[0];
|
|
373
|
-
if (file)
|
|
447
|
+
if (file) {
|
|
448
|
+
this._loadImageFile(file).catch((error) => this._reportError("Image file could not be loaded", error)).finally(() => {
|
|
449
|
+
event.target.value = "";
|
|
450
|
+
});
|
|
451
|
+
}
|
|
374
452
|
});
|
|
375
|
-
this._bindIfExists("zoomInBtn", "click", () => this.scaleImage(this.currentScale + this.options.scaleStep));
|
|
376
|
-
this._bindIfExists("zoomOutBtn", "click", () => this.scaleImage(this.currentScale - this.options.scaleStep));
|
|
453
|
+
this._bindIfExists("zoomInBtn", "click", () => this.scaleImage(this.currentScale + this.options.scaleStep).catch((error) => this._reportError("scaleImage failed", error)));
|
|
454
|
+
this._bindIfExists("zoomOutBtn", "click", () => this.scaleImage(this.currentScale - this.options.scaleStep).catch((error) => this._reportError("scaleImage failed", error)));
|
|
377
455
|
this._bindIfExists("resetBtn", "click", () => {
|
|
378
|
-
this.resetImageTransform();
|
|
456
|
+
this.resetImageTransform().catch((error) => this._reportError("resetImageTransform failed", error));
|
|
379
457
|
});
|
|
380
458
|
this._bindIfExists("addMaskBtn", "click", () => this.createMask());
|
|
381
459
|
this._bindIfExists("removeMaskBtn", "click", () => this.removeSelectedMask());
|
|
382
460
|
this._bindIfExists("removeAllMasksBtn", "click", () => this.removeAllMasks());
|
|
383
|
-
this._bindIfExists("mergeBtn", "click", () => this.mergeMasks());
|
|
461
|
+
this._bindIfExists("mergeBtn", "click", () => this.mergeMasks().catch((error) => this._reportError("merge error", error)));
|
|
384
462
|
this._bindIfExists("downloadBtn", "click", () => this.downloadImage());
|
|
385
|
-
this._bindIfExists("undoBtn", "click", () => this.undo());
|
|
386
|
-
this._bindIfExists("redoBtn", "click", () => this.redo());
|
|
463
|
+
this._bindIfExists("undoBtn", "click", () => this.undo().catch((error) => this._reportError("undo failed", error)));
|
|
464
|
+
this._bindIfExists("redoBtn", "click", () => this.redo().catch((error) => this._reportError("redo failed", error)));
|
|
387
465
|
this._bindIfExists("rotateLeftBtn", "click", () => {
|
|
388
|
-
const rotationInputElement =
|
|
466
|
+
const rotationInputElement = this._getElement("rotationLeftInput");
|
|
389
467
|
let step = this.options.rotationStep;
|
|
390
468
|
if (rotationInputElement) {
|
|
391
469
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
392
470
|
if (!isNaN(parsedStep)) step = parsedStep;
|
|
393
471
|
}
|
|
394
|
-
this.rotateImage(this.currentRotation - step);
|
|
472
|
+
this.rotateImage(this.currentRotation - step).catch((error) => this._reportError("rotateImage failed", error));
|
|
395
473
|
});
|
|
396
474
|
this._bindIfExists("rotateRightBtn", "click", () => {
|
|
397
|
-
const rotationInputElement =
|
|
475
|
+
const rotationInputElement = this._getElement("rotationRightInput");
|
|
398
476
|
let step = this.options.rotationStep;
|
|
399
477
|
if (rotationInputElement) {
|
|
400
478
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
401
479
|
if (!isNaN(parsedStep)) step = parsedStep;
|
|
402
480
|
}
|
|
403
|
-
this.rotateImage(this.currentRotation + step);
|
|
481
|
+
this.rotateImage(this.currentRotation + step).catch((error) => this._reportError("rotateImage failed", error));
|
|
404
482
|
});
|
|
405
483
|
this._bindIfExists("cropBtn", "click", () => this.enterCropMode());
|
|
406
484
|
this._bindIfExists("applyCropBtn", "click", () => {
|
|
407
485
|
this.applyCrop().catch((error) => this._reportError("applyCrop failed", error));
|
|
408
486
|
});
|
|
409
487
|
this._bindIfExists("cancelCropBtn", "click", () => this.cancelCrop());
|
|
488
|
+
this._bindIfExists("maskList", "click", (event) => this._handleMaskListClick(event));
|
|
410
489
|
}
|
|
411
490
|
/**
|
|
412
491
|
* Binds a DOM event listener when the configured element exists and records it for disposal.
|
|
@@ -417,7 +496,7 @@ var ImageEditor = class {
|
|
|
417
496
|
* @private
|
|
418
497
|
*/
|
|
419
498
|
_bindIfExists(key, eventName, handler) {
|
|
420
|
-
const element =
|
|
499
|
+
const element = this._getElement(key);
|
|
421
500
|
if (element) {
|
|
422
501
|
element.addEventListener(eventName, handler);
|
|
423
502
|
this._handlersByElementKey = this._handlersByElementKey || {};
|
|
@@ -429,16 +508,33 @@ var ImageEditor = class {
|
|
|
429
508
|
* Reads an image File as a data URL and loads it into the Fabric canvas.
|
|
430
509
|
*
|
|
431
510
|
* @param {File} file - Image file selected by the user.
|
|
511
|
+
* @returns {Promise<void>} Resolves after the selected file is loaded.
|
|
432
512
|
* @private
|
|
433
513
|
*/
|
|
434
514
|
_loadImageFile(file) {
|
|
435
|
-
if (!
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
515
|
+
if (!this._isSupportedImageFile(file)) {
|
|
516
|
+
const error = new Error("Selected file is not a supported image");
|
|
517
|
+
this._reportError("Selected file is not a supported image", error);
|
|
518
|
+
return Promise.reject(error);
|
|
519
|
+
}
|
|
520
|
+
return new Promise((resolve, reject) => {
|
|
521
|
+
const reader = new FileReader();
|
|
522
|
+
reader.onload = (event) => {
|
|
523
|
+
this.loadImage(event.target.result).then(resolve).catch(reject);
|
|
524
|
+
};
|
|
525
|
+
reader.onerror = (event) => {
|
|
526
|
+
const error = new Error("Image file could not be read");
|
|
527
|
+
this._reportError("Image file could not be read", event);
|
|
528
|
+
reject(error);
|
|
529
|
+
};
|
|
530
|
+
reader.readAsDataURL(file);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
_isSupportedImageFile(file) {
|
|
534
|
+
if (!file) return false;
|
|
535
|
+
if (typeof file.type === "string" && file.type.startsWith("image/")) return true;
|
|
536
|
+
const fileName = String(file.name || "");
|
|
537
|
+
return /\.(avif|bmp|gif|jpe?g|png|webp)$/i.test(fileName);
|
|
442
538
|
}
|
|
443
539
|
/**
|
|
444
540
|
* Warns when more than one mutually exclusive image layout mode is enabled.
|
|
@@ -468,98 +564,102 @@ var ImageEditor = class {
|
|
|
468
564
|
*/
|
|
469
565
|
async loadImage(imageBase64, options = {}) {
|
|
470
566
|
if (!this._fabricLoaded) return;
|
|
471
|
-
if (!this.canvas) return;
|
|
567
|
+
if (!this.canvas || this._disposed) return;
|
|
472
568
|
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/")) return;
|
|
569
|
+
this._assertIdleForOperation("loadImage", options);
|
|
570
|
+
this._isLoading = true;
|
|
571
|
+
this._updateUI();
|
|
473
572
|
this._warnOnImageLayoutOptionConflict();
|
|
474
|
-
this.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
573
|
+
const transaction = this._captureLoadImageTransaction();
|
|
574
|
+
try {
|
|
575
|
+
const imageElement = await this._createImageElement(imageBase64);
|
|
576
|
+
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
577
|
+
let loadSource = imageBase64;
|
|
578
|
+
if (this.options.downsampleOnLoad) {
|
|
579
|
+
const shouldResize = imageElement.naturalWidth > this.options.downsampleMaxWidth || imageElement.naturalHeight > this.options.downsampleMaxHeight;
|
|
580
|
+
if (shouldResize) {
|
|
581
|
+
const ratio = Math.min(
|
|
582
|
+
this.options.downsampleMaxWidth / imageElement.naturalWidth,
|
|
583
|
+
this.options.downsampleMaxHeight / imageElement.naturalHeight
|
|
584
|
+
);
|
|
585
|
+
const targetWidth = Math.round(imageElement.naturalWidth * ratio);
|
|
586
|
+
const targetHeight = Math.round(imageElement.naturalHeight * ratio);
|
|
587
|
+
loadSource = this._resampleImageToDataURL(
|
|
588
|
+
imageElement,
|
|
589
|
+
targetWidth,
|
|
590
|
+
targetHeight,
|
|
591
|
+
this._normalizeQuality(this.options.downsampleQuality),
|
|
592
|
+
imageBase64
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const fabricImage = await this._createFabricImageFromURL(loadSource);
|
|
597
|
+
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
598
|
+
this.canvas.discardActiveObject();
|
|
599
|
+
this._hideAllMaskLabels();
|
|
600
|
+
this.canvas.clear();
|
|
601
|
+
this.canvas.setBackgroundColor(this.options.backgroundColor, this.canvas.renderAll.bind(this.canvas));
|
|
602
|
+
fabricImage.set({ originX: "left", originY: "top", selectable: false, evented: false });
|
|
603
|
+
this._setPlaceholderVisible(false);
|
|
604
|
+
this._syncContainerOverflow({ preserveScroll: options.preserveScroll === true });
|
|
605
|
+
const imageWidth = fabricImage.width;
|
|
606
|
+
const imageHeight = fabricImage.height;
|
|
607
|
+
const viewport = this._getContainerViewportSize();
|
|
608
|
+
const minWidth = viewport.width;
|
|
609
|
+
const minHeight = viewport.height;
|
|
610
|
+
if (this.options.fitImageToCanvas) {
|
|
611
|
+
const canvasWidth = Math.max(1, minWidth - 1);
|
|
612
|
+
const canvasHeight = Math.max(1, minHeight - 1);
|
|
613
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
614
|
+
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
615
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
616
|
+
fabricImage.scale(fitScale);
|
|
617
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
618
|
+
} else if (this.options.coverImageToCanvas) {
|
|
619
|
+
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
620
|
+
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
621
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
622
|
+
fabricImage.scale(layout.scale);
|
|
623
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
624
|
+
} else if (this.options.expandCanvasToImage) {
|
|
625
|
+
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
626
|
+
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
627
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
628
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
629
|
+
fabricImage.scale(1);
|
|
630
|
+
this.baseImageScale = 1;
|
|
631
|
+
} else {
|
|
632
|
+
const canvasWidth = Math.max(this.options.canvasWidth, minWidth);
|
|
633
|
+
const canvasHeight = Math.max(this.options.canvasHeight, minHeight);
|
|
634
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
635
|
+
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
636
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
637
|
+
fabricImage.scale(fitScale);
|
|
638
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
639
|
+
}
|
|
640
|
+
this.originalImage = fabricImage;
|
|
641
|
+
this.canvas.add(fabricImage);
|
|
642
|
+
this.canvas.sendToBack(fabricImage);
|
|
643
|
+
this._clearMaskPlacementMemory();
|
|
644
|
+
if (options.resetMaskCounter !== false) this.maskCounter = 0;
|
|
645
|
+
this.currentScale = 1;
|
|
646
|
+
this.currentRotation = 0;
|
|
647
|
+
this._updateInputs();
|
|
648
|
+
this._updateMaskList();
|
|
649
|
+
this.isImageLoadedToCanvas = true;
|
|
650
|
+
this._updateUI();
|
|
651
|
+
this.canvas.renderAll();
|
|
652
|
+
this._lastSnapshot = this._captureCanvasStateOrThrow("loadImage");
|
|
653
|
+
if (typeof this.onImageLoaded === "function") {
|
|
654
|
+
this.onImageLoaded();
|
|
488
655
|
}
|
|
656
|
+
} catch (error) {
|
|
657
|
+
await this._rollbackLoadImageTransaction(transaction);
|
|
658
|
+
throw error;
|
|
659
|
+
} finally {
|
|
660
|
+
this._isLoading = false;
|
|
661
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
489
662
|
}
|
|
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
663
|
}
|
|
564
664
|
/**
|
|
565
665
|
* Checks whether there is a loaded image on the current canvas.
|
|
@@ -604,24 +704,155 @@ var ImageEditor = class {
|
|
|
604
704
|
imageElement.src = dataUrl;
|
|
605
705
|
});
|
|
606
706
|
}
|
|
707
|
+
_createFabricImageFromURL(dataUrl, timeoutMs = this.options.imageLoadTimeoutMs) {
|
|
708
|
+
return new Promise((resolve, reject) => {
|
|
709
|
+
const safeTimeoutMs = this._getSafeTimeoutMs(timeoutMs);
|
|
710
|
+
let isSettled = false;
|
|
711
|
+
let timerId;
|
|
712
|
+
const settle = (callback) => {
|
|
713
|
+
if (isSettled) return;
|
|
714
|
+
isSettled = true;
|
|
715
|
+
clearTimeout(timerId);
|
|
716
|
+
callback();
|
|
717
|
+
};
|
|
718
|
+
timerId = setTimeout(() => {
|
|
719
|
+
settle(() => reject(new Error("Fabric image load timed out")));
|
|
720
|
+
}, safeTimeoutMs);
|
|
721
|
+
try {
|
|
722
|
+
fabric.Image.fromURL(dataUrl, (fabricImage) => {
|
|
723
|
+
settle(() => {
|
|
724
|
+
if (!fabricImage) {
|
|
725
|
+
reject(new Error("Image could not be loaded"));
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
resolve(fabricImage);
|
|
729
|
+
});
|
|
730
|
+
}, { crossOrigin: "anonymous" });
|
|
731
|
+
} catch (error) {
|
|
732
|
+
settle(() => reject(error));
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
_getSafeTimeoutMs(timeoutMs) {
|
|
737
|
+
const safeTimeoutMs = Number(timeoutMs);
|
|
738
|
+
return Number.isFinite(safeTimeoutMs) && safeTimeoutMs > 0 ? safeTimeoutMs : 3e4;
|
|
739
|
+
}
|
|
740
|
+
_captureLoadImageTransaction() {
|
|
741
|
+
return {
|
|
742
|
+
canvasState: this._serializeCanvasState(),
|
|
743
|
+
originalImage: this.originalImage,
|
|
744
|
+
baseImageScale: this.baseImageScale,
|
|
745
|
+
currentScale: this.currentScale,
|
|
746
|
+
currentRotation: this.currentRotation,
|
|
747
|
+
maskCounter: this.maskCounter,
|
|
748
|
+
isImageLoadedToCanvas: this.isImageLoadedToCanvas,
|
|
749
|
+
lastSnapshot: this._lastSnapshot,
|
|
750
|
+
lastMask: this._lastMask,
|
|
751
|
+
lastMaskInitialLeft: this._lastMaskInitialLeft,
|
|
752
|
+
lastMaskInitialTop: this._lastMaskInitialTop,
|
|
753
|
+
lastMaskInitialWidth: this._lastMaskInitialWidth,
|
|
754
|
+
containerOverflow: this.containerElement && this.containerElement.style ? {
|
|
755
|
+
overflow: this.containerElement.style.overflow || "",
|
|
756
|
+
overflowX: this.containerElement.style.overflowX || "",
|
|
757
|
+
overflowY: this.containerElement.style.overflowY || ""
|
|
758
|
+
} : null,
|
|
759
|
+
scrollLeft: this.containerElement ? this.containerElement.scrollLeft : 0,
|
|
760
|
+
scrollTop: this.containerElement ? this.containerElement.scrollTop : 0,
|
|
761
|
+
placeholderVisibility: this._captureElementVisibility(this.placeholderElement),
|
|
762
|
+
canvasVisibility: this._captureElementVisibility(this._getCanvasVisibilityElement())
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
async _rollbackLoadImageTransaction(transaction) {
|
|
766
|
+
if (!transaction || !this.canvas || this._disposed) return;
|
|
767
|
+
let didRestoreCanvasState = false;
|
|
768
|
+
try {
|
|
769
|
+
if (transaction.canvasState) {
|
|
770
|
+
await this.loadFromState(transaction.canvasState);
|
|
771
|
+
didRestoreCanvasState = true;
|
|
772
|
+
}
|
|
773
|
+
} catch (error) {
|
|
774
|
+
this._lastMask = null;
|
|
775
|
+
this._reportError("loadImage rollback failed", error);
|
|
776
|
+
}
|
|
777
|
+
this.baseImageScale = transaction.baseImageScale;
|
|
778
|
+
this.currentScale = transaction.currentScale;
|
|
779
|
+
this.currentRotation = transaction.currentRotation;
|
|
780
|
+
this.maskCounter = transaction.maskCounter;
|
|
781
|
+
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
782
|
+
this._lastSnapshot = transaction.lastSnapshot;
|
|
783
|
+
if (didRestoreCanvasState) {
|
|
784
|
+
this._restoreLastMaskReference(transaction.lastMask);
|
|
785
|
+
} else {
|
|
786
|
+
this._lastMask = null;
|
|
787
|
+
}
|
|
788
|
+
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
789
|
+
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
790
|
+
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
791
|
+
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
792
|
+
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
793
|
+
if (this.containerElement) {
|
|
794
|
+
this.containerElement.scrollLeft = transaction.scrollLeft;
|
|
795
|
+
this.containerElement.scrollTop = transaction.scrollTop;
|
|
796
|
+
this._restoreContainerOverflowSnapshot(transaction.containerOverflow);
|
|
797
|
+
}
|
|
798
|
+
this._updateInputs();
|
|
799
|
+
this._updateMaskList();
|
|
800
|
+
this._updateUI();
|
|
801
|
+
if (this.canvas) this.canvas.renderAll();
|
|
802
|
+
}
|
|
803
|
+
_restoreLastMaskReference(previousLastMask) {
|
|
804
|
+
if (!this.canvas) {
|
|
805
|
+
this._lastMask = null;
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
809
|
+
const previousMaskId = previousLastMask && previousLastMask.maskId;
|
|
810
|
+
this._lastMask = masks.find((mask) => mask.maskId === previousMaskId) || masks[masks.length - 1] || null;
|
|
811
|
+
if (!this._lastMask) {
|
|
812
|
+
this._lastMaskInitialLeft = null;
|
|
813
|
+
this._lastMaskInitialTop = null;
|
|
814
|
+
this._lastMaskInitialWidth = null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
607
817
|
/**
|
|
608
|
-
* Resamples the given image element to a new width and height and returns the result as a
|
|
818
|
+
* Resamples the given image element to a new width and height and returns the result as a data URL.
|
|
609
819
|
*
|
|
610
820
|
* @param {HTMLImageElement} imageElement - The image element to resample.
|
|
611
821
|
* @param {number} targetWidth - Target width (in pixels) for the resampled image.
|
|
612
822
|
* @param {number} targetHeight - Target height (in pixels) for the resampled image.
|
|
613
|
-
* @param {number} [quality=0.92] -
|
|
614
|
-
* @
|
|
823
|
+
* @param {number} [quality=0.92] - Image quality between 0 and 1 for lossy formats.
|
|
824
|
+
* @param {string|null} [sourceDataUrl=null] - Source data URL used to preserve alpha-capable formats.
|
|
825
|
+
* @returns {string} A data URL representing the resampled image.
|
|
615
826
|
* @private
|
|
616
827
|
*/
|
|
617
|
-
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92) {
|
|
828
|
+
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
|
|
618
829
|
const offscreenCanvas = document.createElement("canvas");
|
|
619
830
|
offscreenCanvas.width = targetWidth;
|
|
620
831
|
offscreenCanvas.height = targetHeight;
|
|
621
832
|
const context = offscreenCanvas.getContext("2d");
|
|
622
833
|
if (!context) throw new Error("2D canvas context is unavailable");
|
|
623
834
|
context.drawImage(imageElement, 0, 0, imageElement.naturalWidth, imageElement.naturalHeight, 0, 0, targetWidth, targetHeight);
|
|
624
|
-
return offscreenCanvas.toDataURL(
|
|
835
|
+
return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
|
|
836
|
+
}
|
|
837
|
+
_getDataUrlMimeType(dataUrl) {
|
|
838
|
+
const match = String(dataUrl || "").match(/^data:([^;,]+)[;,]/i);
|
|
839
|
+
return match ? match[1].toLowerCase() : "";
|
|
840
|
+
}
|
|
841
|
+
_getDownsampleMimeType(sourceDataUrl) {
|
|
842
|
+
if (this.options.downsampleMimeType) {
|
|
843
|
+
const requestedFormat = this._normalizeImageFormat(this.options.downsampleMimeType);
|
|
844
|
+
return `image/${requestedFormat}`;
|
|
845
|
+
}
|
|
846
|
+
const sourceMimeType = this._getDataUrlMimeType(sourceDataUrl);
|
|
847
|
+
if (this.options.preserveSourceFormat !== false && (sourceMimeType === "image/png" || sourceMimeType === "image/webp")) {
|
|
848
|
+
return sourceMimeType;
|
|
849
|
+
}
|
|
850
|
+
return "image/jpeg";
|
|
851
|
+
}
|
|
852
|
+
_captureCanvasStateOrThrow(context) {
|
|
853
|
+
const snapshot = this._serializeCanvasState();
|
|
854
|
+
if (!snapshot) throw new Error(`${context}: canvas state is unavailable`);
|
|
855
|
+
return snapshot;
|
|
625
856
|
}
|
|
626
857
|
/**
|
|
627
858
|
* Sets canvas size to integer width and height values to prevent scrollbars due to sub-pixel rendering.
|
|
@@ -640,7 +871,6 @@ var ImageEditor = class {
|
|
|
640
871
|
if (this.canvasElement) {
|
|
641
872
|
this.canvasElement.style.width = integerWidth + "px";
|
|
642
873
|
this.canvasElement.style.height = integerHeight + "px";
|
|
643
|
-
this.canvasElement.style.maxWidth = "none";
|
|
644
874
|
}
|
|
645
875
|
}
|
|
646
876
|
_ceilCanvasDimension(value) {
|
|
@@ -656,8 +886,13 @@ var ImageEditor = class {
|
|
|
656
886
|
height: Math.max(1, Math.floor(this.options.canvasHeight || 1))
|
|
657
887
|
};
|
|
658
888
|
}
|
|
659
|
-
|
|
660
|
-
|
|
889
|
+
const measuredWidth = Math.floor(this.containerElement.clientWidth || 0);
|
|
890
|
+
const measuredHeight = Math.floor(this.containerElement.clientHeight || 0);
|
|
891
|
+
let width = Math.max(1, measuredWidth || this._lastContainerViewportSize?.width || this.options.canvasWidth || 1);
|
|
892
|
+
let height = Math.max(1, measuredHeight || this._lastContainerViewportSize?.height || this.options.canvasHeight || 1);
|
|
893
|
+
if (measuredWidth > 0 && measuredHeight > 0) {
|
|
894
|
+
this._lastContainerViewportSize = { width: measuredWidth, height: measuredHeight };
|
|
895
|
+
}
|
|
661
896
|
if (this._hasFixedContainerScrollbars()) {
|
|
662
897
|
return { width, height };
|
|
663
898
|
}
|
|
@@ -862,7 +1097,11 @@ var ImageEditor = class {
|
|
|
862
1097
|
maskStyleBackups.push(backup);
|
|
863
1098
|
mask.set(stylePatch);
|
|
864
1099
|
});
|
|
865
|
-
|
|
1100
|
+
const result = callback();
|
|
1101
|
+
if (result && typeof result.then === "function") {
|
|
1102
|
+
throw new Error("_withNormalizedMaskStyles callback must be synchronous");
|
|
1103
|
+
}
|
|
1104
|
+
return result;
|
|
866
1105
|
} finally {
|
|
867
1106
|
maskStyleBackups.forEach((backup) => {
|
|
868
1107
|
try {
|
|
@@ -930,9 +1169,13 @@ var ImageEditor = class {
|
|
|
930
1169
|
* @returns {number} A finite quality value between 0 and 1.
|
|
931
1170
|
* @private
|
|
932
1171
|
*/
|
|
933
|
-
_normalizeQuality(quality) {
|
|
1172
|
+
_normalizeQuality(quality, fallback = void 0) {
|
|
1173
|
+
const fallbackQuality = fallback == null ? this.options.downsampleQuality : fallback;
|
|
1174
|
+
const numericFallback = fallbackQuality == null ? NaN : Number(fallbackQuality);
|
|
1175
|
+
const safeFallback = Number.isFinite(numericFallback) ? Math.max(0, Math.min(1, numericFallback)) : 0.92;
|
|
1176
|
+
if (quality == null) return safeFallback;
|
|
934
1177
|
const numericQuality = Number(quality);
|
|
935
|
-
if (!Number.isFinite(numericQuality)) return
|
|
1178
|
+
if (!Number.isFinite(numericQuality)) return safeFallback;
|
|
936
1179
|
return Math.max(0, Math.min(1, numericQuality));
|
|
937
1180
|
}
|
|
938
1181
|
/**
|
|
@@ -983,67 +1226,66 @@ var ImageEditor = class {
|
|
|
983
1226
|
sourceHeight: Math.max(1, endY - sourceY)
|
|
984
1227
|
};
|
|
985
1228
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
});
|
|
1229
|
+
_hasFractionalCanvasEdge(value) {
|
|
1230
|
+
const numericValue = Number(value);
|
|
1231
|
+
if (!Number.isFinite(numericValue)) return false;
|
|
1232
|
+
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1233
|
+
}
|
|
1234
|
+
_getPartialExportEdges(bounds) {
|
|
1235
|
+
if (!bounds) return null;
|
|
1236
|
+
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
1237
|
+
const isAxisAligned = angle < 0.01 || Math.abs(angle - 90) < 0.01;
|
|
1238
|
+
if (!isAxisAligned) return null;
|
|
1239
|
+
return {
|
|
1240
|
+
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1241
|
+
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1242
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1243
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
async _sealPartialTransparentEdges(dataUrl, edges) {
|
|
1247
|
+
if (!edges || !Object.values(edges).some(Boolean)) return dataUrl;
|
|
1248
|
+
const imageElement = await this._createImageElement(dataUrl);
|
|
1249
|
+
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
1250
|
+
const height = Math.max(1, imageElement.naturalHeight || imageElement.height || 1);
|
|
1251
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
1252
|
+
offscreenCanvas.width = width;
|
|
1253
|
+
offscreenCanvas.height = height;
|
|
1254
|
+
const context = offscreenCanvas.getContext("2d");
|
|
1255
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
1256
|
+
context.drawImage(imageElement, 0, 0, width, height);
|
|
1257
|
+
const imageData = context.getImageData(0, 0, width, height);
|
|
1258
|
+
const pixels = imageData.data;
|
|
1259
|
+
const sealPixel = (x, y, fallbackX, fallbackY) => {
|
|
1260
|
+
const index = (y * width + x) * 4;
|
|
1261
|
+
const fallbackIndex = (fallbackY * width + fallbackX) * 4;
|
|
1262
|
+
if (pixels[index + 3] === 0 && pixels[fallbackIndex + 3] > 0) {
|
|
1263
|
+
pixels[index] = pixels[fallbackIndex];
|
|
1264
|
+
pixels[index + 1] = pixels[fallbackIndex + 1];
|
|
1265
|
+
pixels[index + 2] = pixels[fallbackIndex + 2];
|
|
1266
|
+
pixels[index + 3] = pixels[fallbackIndex + 3];
|
|
1267
|
+
}
|
|
1268
|
+
if (pixels[index + 3] > 0 && pixels[index + 3] < 255) {
|
|
1269
|
+
pixels[index + 3] = 255;
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
if (edges.left && width > 1) {
|
|
1273
|
+
for (let y = 0; y < height; y += 1) sealPixel(0, y, 1, y);
|
|
1274
|
+
}
|
|
1275
|
+
if (edges.right && width > 1) {
|
|
1276
|
+
for (let y = 0; y < height; y += 1) sealPixel(width - 1, y, width - 2, y);
|
|
1277
|
+
}
|
|
1278
|
+
if (edges.top && height > 1) {
|
|
1279
|
+
for (let x = 0; x < width; x += 1) sealPixel(x, 0, x, 1);
|
|
1280
|
+
}
|
|
1281
|
+
if (edges.bottom && height > 1) {
|
|
1282
|
+
for (let x = 0; x < width; x += 1) sealPixel(x, height - 1, x, height - 2);
|
|
1283
|
+
}
|
|
1284
|
+
context.putImageData(imageData, 0, 0);
|
|
1285
|
+
return offscreenCanvas.toDataURL("image/png");
|
|
1044
1286
|
}
|
|
1045
1287
|
/**
|
|
1046
|
-
* Exports
|
|
1288
|
+
* Exports a source region directly through Fabric's region export options.
|
|
1047
1289
|
*
|
|
1048
1290
|
* @param {Object} region - Canvas source region and export options.
|
|
1049
1291
|
* @param {number} region.sourceX - Source region x coordinate.
|
|
@@ -1053,17 +1295,46 @@ var ImageEditor = class {
|
|
|
1053
1295
|
* @param {number} [region.multiplier=1] - Export multiplier.
|
|
1054
1296
|
* @param {number} [region.quality=0.92] - Output image quality for lossy formats.
|
|
1055
1297
|
* @param {'jpeg'|'png'|'webp'} [region.format='jpeg'] - Output image format.
|
|
1298
|
+
* @param {Object|null} [region.sealPartialEdges=null] - Fractional canvas edges whose alpha should be sealed.
|
|
1056
1299
|
* @returns {Promise<string>} Resolves with an image data URL for the cropped region.
|
|
1057
1300
|
* @private
|
|
1058
1301
|
*/
|
|
1059
|
-
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg" }) {
|
|
1302
|
+
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg", sealPartialEdges = null }) {
|
|
1060
1303
|
const safeMultiplier = Math.max(1, Number(multiplier) || 1);
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1304
|
+
const safeFormat = this._normalizeImageFormat(format);
|
|
1305
|
+
const exportFormat = safeFormat === "jpeg" ? "png" : safeFormat;
|
|
1306
|
+
let regionDataUrl = this.canvas.toDataURL({
|
|
1307
|
+
format: exportFormat,
|
|
1063
1308
|
quality,
|
|
1064
|
-
multiplier: safeMultiplier
|
|
1309
|
+
multiplier: safeMultiplier,
|
|
1310
|
+
left: sourceX,
|
|
1311
|
+
top: sourceY,
|
|
1312
|
+
width: sourceWidth,
|
|
1313
|
+
height: sourceHeight
|
|
1065
1314
|
});
|
|
1066
|
-
|
|
1315
|
+
regionDataUrl = await this._sealPartialTransparentEdges(regionDataUrl, sealPartialEdges);
|
|
1316
|
+
if (safeFormat !== "jpeg") return regionDataUrl;
|
|
1317
|
+
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1318
|
+
}
|
|
1319
|
+
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1320
|
+
const imageElement = await this._createImageElement(dataUrl);
|
|
1321
|
+
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
1322
|
+
const height = Math.max(1, imageElement.naturalHeight || imageElement.height || 1);
|
|
1323
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
1324
|
+
offscreenCanvas.width = width;
|
|
1325
|
+
offscreenCanvas.height = height;
|
|
1326
|
+
const context = offscreenCanvas.getContext("2d");
|
|
1327
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
1328
|
+
context.fillStyle = this._getJpegBackgroundColor();
|
|
1329
|
+
context.fillRect(0, 0, width, height);
|
|
1330
|
+
context.drawImage(imageElement, 0, 0, width, height);
|
|
1331
|
+
return offscreenCanvas.toDataURL("image/jpeg", this._normalizeQuality(quality));
|
|
1332
|
+
}
|
|
1333
|
+
_getJpegBackgroundColor() {
|
|
1334
|
+
const backgroundColor = String(this.options.backgroundColor || "").trim();
|
|
1335
|
+
if (!backgroundColor || backgroundColor === "transparent") return "#ffffff";
|
|
1336
|
+
if (/^rgba\([^)]*,\s*0(?:\.0+)?\s*\)$/i.test(backgroundColor)) return "#ffffff";
|
|
1337
|
+
return backgroundColor;
|
|
1067
1338
|
}
|
|
1068
1339
|
/**
|
|
1069
1340
|
* Gets the top-left corner coordinates of the given object.
|
|
@@ -1076,11 +1347,37 @@ var ImageEditor = class {
|
|
|
1076
1347
|
_getObjectTopLeftPoint(fabricObject) {
|
|
1077
1348
|
if (!fabricObject) return { x: 0, y: 0 };
|
|
1078
1349
|
fabricObject.setCoords();
|
|
1079
|
-
const coords = typeof fabricObject.getCoords === "function" ? fabricObject.getCoords() : null;
|
|
1080
|
-
if (coords && coords.length) return coords[0];
|
|
1081
1350
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1082
1351
|
return { x: boundingRect.left, y: boundingRect.top };
|
|
1083
1352
|
}
|
|
1353
|
+
_getObjectCoordinateTopLeftPoint(fabricObject) {
|
|
1354
|
+
if (!fabricObject) return { x: 0, y: 0 };
|
|
1355
|
+
fabricObject.setCoords();
|
|
1356
|
+
const coords = typeof fabricObject.getCoords === "function" ? fabricObject.getCoords() : null;
|
|
1357
|
+
if (coords && coords.length) return coords[0];
|
|
1358
|
+
return this._getObjectTopLeftPoint(fabricObject);
|
|
1359
|
+
}
|
|
1360
|
+
_getObjectOriginPoint(fabricObject, originX, originY) {
|
|
1361
|
+
if (!fabricObject) return { x: 0, y: 0 };
|
|
1362
|
+
if (typeof fabricObject.getPointByOrigin === "function") {
|
|
1363
|
+
return fabricObject.getPointByOrigin(originX, originY);
|
|
1364
|
+
}
|
|
1365
|
+
return this._getObjectTopLeftPoint(fabricObject);
|
|
1366
|
+
}
|
|
1367
|
+
_translateObjectByCanvasOffset(fabricObject, deltaX, deltaY) {
|
|
1368
|
+
if (!fabricObject) return;
|
|
1369
|
+
if (typeof fabricObject.getCenterPoint === "function" && typeof fabricObject.setPositionByOrigin === "function") {
|
|
1370
|
+
const center = fabricObject.getCenterPoint();
|
|
1371
|
+
const nextCenter = new fabric.Point(center.x + deltaX, center.y + deltaY);
|
|
1372
|
+
fabricObject.setPositionByOrigin(nextCenter, "center", "center");
|
|
1373
|
+
} else {
|
|
1374
|
+
fabricObject.set({
|
|
1375
|
+
left: (fabricObject.left || 0) + deltaX,
|
|
1376
|
+
top: (fabricObject.top || 0) + deltaY
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
fabricObject.setCoords();
|
|
1380
|
+
}
|
|
1084
1381
|
/**
|
|
1085
1382
|
* Sets the object's origin at the specified origin point, keeping a reference point fixed in position.
|
|
1086
1383
|
*
|
|
@@ -1144,8 +1441,10 @@ var ImageEditor = class {
|
|
|
1144
1441
|
_expandCanvasToFitObjects(fabricObjects, padding = 10) {
|
|
1145
1442
|
if (!this.canvas || !Array.isArray(fabricObjects) || !fabricObjects.length || !this._shouldResizeCanvasToContentBounds()) return;
|
|
1146
1443
|
try {
|
|
1147
|
-
|
|
1148
|
-
|
|
1444
|
+
const currentWidth = this.canvas.getWidth();
|
|
1445
|
+
const currentHeight = this.canvas.getHeight();
|
|
1446
|
+
let requiredWidth = currentWidth;
|
|
1447
|
+
let requiredHeight = currentHeight;
|
|
1149
1448
|
fabricObjects.forEach((fabricObject) => {
|
|
1150
1449
|
if (!fabricObject) return;
|
|
1151
1450
|
if (typeof fabricObject.setCoords === "function") fabricObject.setCoords();
|
|
@@ -1153,11 +1452,21 @@ var ImageEditor = class {
|
|
|
1153
1452
|
requiredWidth = Math.max(requiredWidth, Math.ceil(boundingRect.left + boundingRect.width + padding));
|
|
1154
1453
|
requiredHeight = Math.max(requiredHeight, Math.ceil(boundingRect.top + boundingRect.height + padding));
|
|
1155
1454
|
});
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1455
|
+
const shouldUseScrollSafeViewport = this.options.fitImageToCanvas || this.options.coverImageToCanvas;
|
|
1456
|
+
let minWidth = 0;
|
|
1457
|
+
let minHeight = 0;
|
|
1458
|
+
if (shouldUseScrollSafeViewport) {
|
|
1459
|
+
const viewport = this._getContainerViewportSize();
|
|
1460
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
1461
|
+
minWidth = Math.max(1, viewport.width - safetyMargin);
|
|
1462
|
+
minHeight = Math.max(1, viewport.height - safetyMargin);
|
|
1463
|
+
} else if (this.containerElement) {
|
|
1464
|
+
minWidth = Math.floor(this.containerElement.clientWidth || 0);
|
|
1465
|
+
minHeight = Math.floor(this.containerElement.clientHeight || 0);
|
|
1466
|
+
}
|
|
1467
|
+
const newWidth = Math.max(currentWidth, minWidth, requiredWidth);
|
|
1468
|
+
const newHeight = Math.max(currentHeight, minHeight, requiredHeight);
|
|
1469
|
+
if (newWidth !== currentWidth || newHeight !== currentHeight) {
|
|
1161
1470
|
this._setCanvasSizeInt(newWidth, newHeight);
|
|
1162
1471
|
}
|
|
1163
1472
|
} catch (error) {
|
|
@@ -1183,7 +1492,120 @@ var ImageEditor = class {
|
|
|
1183
1492
|
* @public
|
|
1184
1493
|
*/
|
|
1185
1494
|
scaleImage(factor, options = {}) {
|
|
1186
|
-
|
|
1495
|
+
try {
|
|
1496
|
+
this._assertCanQueueAnimation("scaleImage", options);
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
return Promise.reject(error);
|
|
1499
|
+
}
|
|
1500
|
+
return this.animationQueue.add(() => this._scaleImageImpl(factor, options)).finally(() => {
|
|
1501
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
_getInternalOperationToken(options) {
|
|
1505
|
+
return options && options[INTERNAL_OPERATION_TOKEN];
|
|
1506
|
+
}
|
|
1507
|
+
_isOwnInternalOperation(options) {
|
|
1508
|
+
const token = this._getInternalOperationToken(options);
|
|
1509
|
+
return !!token && token === this._activeOperationToken;
|
|
1510
|
+
}
|
|
1511
|
+
_beginBusyOperation(operationName) {
|
|
1512
|
+
const token = Symbol(operationName);
|
|
1513
|
+
this._activeOperationName = operationName;
|
|
1514
|
+
this._activeOperationToken = token;
|
|
1515
|
+
this._updateUI();
|
|
1516
|
+
return token;
|
|
1517
|
+
}
|
|
1518
|
+
_endBusyOperation(token) {
|
|
1519
|
+
if (token && token === this._activeOperationToken) {
|
|
1520
|
+
this._activeOperationName = null;
|
|
1521
|
+
this._activeOperationToken = null;
|
|
1522
|
+
this._updateUI();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
_withInternalOperationOptions(token, options = {}) {
|
|
1526
|
+
return {
|
|
1527
|
+
...options,
|
|
1528
|
+
[INTERNAL_OPERATION_TOKEN]: token
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
_assertEditorAvailable(operationName) {
|
|
1532
|
+
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1533
|
+
}
|
|
1534
|
+
_assertIdleForOperation(operationName, options = {}) {
|
|
1535
|
+
this._assertEditorAvailable(operationName);
|
|
1536
|
+
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
1537
|
+
if (this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) {
|
|
1538
|
+
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1539
|
+
}
|
|
1540
|
+
if (this._isLoading && !isOwnInternalOperation) {
|
|
1541
|
+
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1542
|
+
}
|
|
1543
|
+
if (this._activeOperationToken && !isOwnInternalOperation) {
|
|
1544
|
+
throw new Error(`${operationName} cannot run while ${this._activeOperationName || "another operation"} is running`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
_assertCanQueueAnimation(operationName, options = {}) {
|
|
1548
|
+
this._assertEditorAvailable(operationName);
|
|
1549
|
+
if (this._isLoading && !this._isOwnInternalOperation(options)) {
|
|
1550
|
+
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1551
|
+
}
|
|
1552
|
+
if (this._activeOperationToken && !this._isOwnInternalOperation(options)) {
|
|
1553
|
+
throw new Error(`${operationName} cannot run while ${this._activeOperationName || "another operation"} is running`);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
_canMutateNow(operationName, options = {}) {
|
|
1557
|
+
try {
|
|
1558
|
+
this._assertIdleForOperation(operationName, options);
|
|
1559
|
+
return true;
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
this._reportError(`${operationName} blocked`, error);
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
_rejectActiveAnimations(reason) {
|
|
1566
|
+
const error = reason instanceof Error ? reason : new Error(String(reason || "Animation cancelled"));
|
|
1567
|
+
this._activeAnimationRejectors.forEach((reject) => {
|
|
1568
|
+
try {
|
|
1569
|
+
reject(error);
|
|
1570
|
+
} catch (rejectError) {
|
|
1571
|
+
void rejectError;
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
this._activeAnimationRejectors.clear();
|
|
1575
|
+
}
|
|
1576
|
+
_animateFabricProperty(fabricObject, property, value) {
|
|
1577
|
+
return new Promise((resolve, reject) => {
|
|
1578
|
+
if (this._disposed || !this.canvas || !fabricObject) {
|
|
1579
|
+
reject(new Error("Animation cannot start after editor disposal"));
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
let isSettled = false;
|
|
1583
|
+
const duration = Math.max(0, Number(this.options.animationDuration) || 0);
|
|
1584
|
+
const timeoutMs = Math.max(1e3, duration + 1e3);
|
|
1585
|
+
let timerId;
|
|
1586
|
+
const settle = (callback) => {
|
|
1587
|
+
if (isSettled) return;
|
|
1588
|
+
isSettled = true;
|
|
1589
|
+
clearTimeout(timerId);
|
|
1590
|
+
this._activeAnimationRejectors.delete(reject);
|
|
1591
|
+
callback();
|
|
1592
|
+
};
|
|
1593
|
+
this._activeAnimationRejectors.add(reject);
|
|
1594
|
+
timerId = setTimeout(() => {
|
|
1595
|
+
settle(() => reject(new Error(`Animation timed out while changing ${property}`)));
|
|
1596
|
+
}, timeoutMs);
|
|
1597
|
+
try {
|
|
1598
|
+
fabricObject.animate(property, value, {
|
|
1599
|
+
duration,
|
|
1600
|
+
onChange: () => {
|
|
1601
|
+
if (!this._disposed && this.canvas) this.canvas.renderAll();
|
|
1602
|
+
},
|
|
1603
|
+
onComplete: () => settle(resolve)
|
|
1604
|
+
});
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
settle(() => reject(error));
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1187
1609
|
}
|
|
1188
1610
|
/**
|
|
1189
1611
|
* Scales the original image by a given factor, with animation.
|
|
@@ -1192,32 +1614,25 @@ var ImageEditor = class {
|
|
|
1192
1614
|
* @returns {Promise<void>} Promise that resolves once the scaling animation finishes.
|
|
1193
1615
|
* @private
|
|
1194
1616
|
*/
|
|
1195
|
-
_scaleImageImpl(factor, options = {}) {
|
|
1196
|
-
if (!this.originalImage
|
|
1197
|
-
if (this.isAnimating) return
|
|
1617
|
+
async _scaleImageImpl(factor, options = {}) {
|
|
1618
|
+
if (!this.originalImage || this._disposed) return;
|
|
1619
|
+
if (this.isAnimating) return;
|
|
1198
1620
|
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(() => {
|
|
1621
|
+
let didStartAnimation = false;
|
|
1622
|
+
try {
|
|
1623
|
+
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, factor));
|
|
1624
|
+
this.currentScale = factor;
|
|
1625
|
+
this.isAnimating = true;
|
|
1626
|
+
didStartAnimation = true;
|
|
1627
|
+
this._updateUI();
|
|
1628
|
+
const targetScale = this.baseImageScale * factor;
|
|
1629
|
+
const topLeft = this._getObjectTopLeftPoint(this.originalImage);
|
|
1630
|
+
this._setObjectOriginKeepingPosition(this.originalImage, "left", "top", topLeft);
|
|
1631
|
+
await Promise.all([
|
|
1632
|
+
this._animateFabricProperty(this.originalImage, "scaleX", targetScale),
|
|
1633
|
+
this._animateFabricProperty(this.originalImage, "scaleY", targetScale)
|
|
1634
|
+
]);
|
|
1635
|
+
if (this._disposed || !this.canvas || !this.originalImage) throw new Error("Editor was disposed during scale animation");
|
|
1221
1636
|
this.originalImage.set({ scaleX: targetScale, scaleY: targetScale });
|
|
1222
1637
|
this.originalImage.setCoords();
|
|
1223
1638
|
if (this._shouldResizeCanvasToContentBounds()) {
|
|
@@ -1227,14 +1642,15 @@ var ImageEditor = class {
|
|
|
1227
1642
|
this.canvas.getObjects().forEach((object) => {
|
|
1228
1643
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1229
1644
|
});
|
|
1230
|
-
this.isAnimating = false;
|
|
1231
1645
|
this._updateInputs();
|
|
1232
|
-
this._updateUI();
|
|
1233
1646
|
if (saveHistory) this.saveState();
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1647
|
+
} finally {
|
|
1648
|
+
if (didStartAnimation) {
|
|
1649
|
+
this.isAnimating = false;
|
|
1650
|
+
this._updateInputs();
|
|
1651
|
+
this._updateUI();
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1238
1654
|
}
|
|
1239
1655
|
/**
|
|
1240
1656
|
* Rotates the original image by a given number of degrees, with animation.
|
|
@@ -1244,7 +1660,14 @@ var ImageEditor = class {
|
|
|
1244
1660
|
* @public
|
|
1245
1661
|
*/
|
|
1246
1662
|
rotateImage(degrees, options = {}) {
|
|
1247
|
-
|
|
1663
|
+
try {
|
|
1664
|
+
this._assertCanQueueAnimation("rotateImage", options);
|
|
1665
|
+
} catch (error) {
|
|
1666
|
+
return Promise.reject(error);
|
|
1667
|
+
}
|
|
1668
|
+
return this.animationQueue.add(() => this._rotateImageImpl(degrees, options)).finally(() => {
|
|
1669
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1670
|
+
});
|
|
1248
1671
|
}
|
|
1249
1672
|
/**
|
|
1250
1673
|
* Rotates the original image by a given number of degrees, with animation.
|
|
@@ -1253,43 +1676,50 @@ var ImageEditor = class {
|
|
|
1253
1676
|
* @returns {Promise<void>} Promise that resolves once the rotation animation finishes.
|
|
1254
1677
|
* @private
|
|
1255
1678
|
*/
|
|
1256
|
-
_rotateImageImpl(degrees, options = {}) {
|
|
1257
|
-
if (!this.originalImage
|
|
1258
|
-
if (this.isAnimating) return
|
|
1259
|
-
if (isNaN(degrees)) return
|
|
1679
|
+
async _rotateImageImpl(degrees, options = {}) {
|
|
1680
|
+
if (!this.originalImage || this._disposed) return;
|
|
1681
|
+
if (this.isAnimating) return;
|
|
1682
|
+
if (isNaN(degrees)) return;
|
|
1260
1683
|
const saveHistory = options.saveHistory !== false;
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1684
|
+
const image = this.originalImage;
|
|
1685
|
+
const previousOriginX = image.originX || "left";
|
|
1686
|
+
const previousOriginY = image.originY || "top";
|
|
1687
|
+
const previousOriginPoint = this._getObjectOriginPoint(image, previousOriginX, previousOriginY);
|
|
1688
|
+
let didStartAnimation = false;
|
|
1689
|
+
let didCompleteRotation = false;
|
|
1690
|
+
try {
|
|
1691
|
+
this.currentRotation = degrees;
|
|
1692
|
+
this.isAnimating = true;
|
|
1693
|
+
didStartAnimation = true;
|
|
1694
|
+
this._updateUI();
|
|
1695
|
+
const center = image.getCenterPoint();
|
|
1696
|
+
this._setObjectOriginKeepingPosition(image, "center", "center", center);
|
|
1697
|
+
await this._animateFabricProperty(image, "angle", degrees);
|
|
1698
|
+
if (this._disposed || !this.canvas || !this.originalImage) throw new Error("Editor was disposed during rotation animation");
|
|
1274
1699
|
this.originalImage.set("angle", degrees);
|
|
1275
1700
|
this.originalImage.setCoords();
|
|
1276
1701
|
if (this._shouldResizeCanvasToContentBounds()) {
|
|
1277
1702
|
this._updateCanvasSizeToImageBounds();
|
|
1278
1703
|
}
|
|
1279
1704
|
this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
|
|
1280
|
-
const newTopLeft = this.
|
|
1705
|
+
const newTopLeft = this._getObjectCoordinateTopLeftPoint(this.originalImage);
|
|
1281
1706
|
this._setObjectOriginKeepingPosition(this.originalImage, "left", "top", newTopLeft);
|
|
1282
1707
|
this.canvas.getObjects().forEach((object) => {
|
|
1283
1708
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1284
1709
|
});
|
|
1285
|
-
this.isAnimating = false;
|
|
1286
1710
|
this._updateInputs();
|
|
1287
|
-
this._updateUI();
|
|
1288
1711
|
if (saveHistory) this.saveState();
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
this.
|
|
1292
|
-
|
|
1712
|
+
didCompleteRotation = true;
|
|
1713
|
+
} finally {
|
|
1714
|
+
if (!didCompleteRotation && !this._disposed && image) {
|
|
1715
|
+
this._setObjectOriginKeepingPosition(image, previousOriginX, previousOriginY, previousOriginPoint);
|
|
1716
|
+
}
|
|
1717
|
+
if (didStartAnimation) {
|
|
1718
|
+
this.isAnimating = false;
|
|
1719
|
+
this._updateInputs();
|
|
1720
|
+
this._updateUI();
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1293
1723
|
}
|
|
1294
1724
|
/**
|
|
1295
1725
|
* Resets the image transform: scales to 1 and rotates to 0 degrees.
|
|
@@ -1299,14 +1729,22 @@ var ImageEditor = class {
|
|
|
1299
1729
|
*/
|
|
1300
1730
|
resetImageTransform() {
|
|
1301
1731
|
if (!this.originalImage) return Promise.resolve();
|
|
1732
|
+
try {
|
|
1733
|
+
this._assertCanQueueAnimation("resetImageTransform");
|
|
1734
|
+
} catch (error) {
|
|
1735
|
+
return Promise.reject(error);
|
|
1736
|
+
}
|
|
1302
1737
|
return this.animationQueue.add(async () => {
|
|
1303
|
-
const before = this._lastSnapshot || this.
|
|
1738
|
+
const before = this._lastSnapshot || this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1304
1739
|
await this._scaleImageImpl(1, { saveHistory: false });
|
|
1305
1740
|
await this._rotateImageImpl(0, { saveHistory: false });
|
|
1306
|
-
const after = this.
|
|
1741
|
+
const after = this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1307
1742
|
this._pushStateTransition(before, after);
|
|
1743
|
+
}).finally(() => {
|
|
1744
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1308
1745
|
}).catch((error) => {
|
|
1309
1746
|
this._reportError("resetImageTransform() failed", error);
|
|
1747
|
+
throw error;
|
|
1310
1748
|
});
|
|
1311
1749
|
}
|
|
1312
1750
|
/**
|
|
@@ -1326,13 +1764,31 @@ var ImageEditor = class {
|
|
|
1326
1764
|
* @public
|
|
1327
1765
|
*/
|
|
1328
1766
|
loadFromState(serializedState) {
|
|
1329
|
-
if (!serializedState || !this.canvas) return Promise.resolve();
|
|
1330
|
-
|
|
1767
|
+
if (!serializedState || !this.canvas || this._disposed) return Promise.resolve();
|
|
1768
|
+
if (this._cropMode || this._cropRect) {
|
|
1769
|
+
this._removeCropRect();
|
|
1770
|
+
this._restoreCropObjectState();
|
|
1771
|
+
this._cropMode = false;
|
|
1772
|
+
if (this._prevSelectionSetting !== void 0 && this.canvas) {
|
|
1773
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
1774
|
+
}
|
|
1775
|
+
this._prevSelectionSetting = void 0;
|
|
1776
|
+
}
|
|
1777
|
+
return new Promise((resolve, reject) => {
|
|
1331
1778
|
try {
|
|
1332
1779
|
const state = typeof serializedState === "string" ? JSON.parse(serializedState) : serializedState;
|
|
1333
1780
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
1334
|
-
this.canvas.loadFromJSON(state, () => {
|
|
1781
|
+
this.canvas.loadFromJSON(state, async () => {
|
|
1335
1782
|
try {
|
|
1783
|
+
if (this._disposed || !this.canvas) {
|
|
1784
|
+
reject(new Error("Editor was disposed while loading state"));
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
await this._waitForFabricImagesReady(this.canvas.getObjects());
|
|
1788
|
+
if (this._disposed || !this.canvas) {
|
|
1789
|
+
reject(new Error("Editor was disposed while loading state"));
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1336
1792
|
this._hideAllMaskLabels();
|
|
1337
1793
|
const canvasObjects = this.canvas.getObjects();
|
|
1338
1794
|
this.originalImage = canvasObjects.find((object) => object.type === "image" && !object.maskId) || null;
|
|
@@ -1380,15 +1836,53 @@ var ImageEditor = class {
|
|
|
1380
1836
|
this._updatePlaceholderStatus();
|
|
1381
1837
|
this._lastSnapshot = this._serializeCanvasState();
|
|
1382
1838
|
this._updateUI();
|
|
1839
|
+
resolve();
|
|
1383
1840
|
} catch (callbackError) {
|
|
1384
1841
|
this._reportError("loadFromState() failed", callbackError);
|
|
1385
|
-
|
|
1386
|
-
resolve();
|
|
1842
|
+
reject(callbackError);
|
|
1387
1843
|
}
|
|
1388
1844
|
});
|
|
1389
1845
|
} catch (error) {
|
|
1390
1846
|
this._reportError("loadFromState() failed", error);
|
|
1391
|
-
|
|
1847
|
+
reject(error);
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
async _waitForFabricImagesReady(canvasObjects) {
|
|
1852
|
+
const imageObjects = (canvasObjects || []).filter((object) => object && object.type === "image");
|
|
1853
|
+
await Promise.all(imageObjects.map((object) => this._waitForImageElementReady(
|
|
1854
|
+
typeof object.getElement === "function" ? object.getElement() : object._element
|
|
1855
|
+
)));
|
|
1856
|
+
}
|
|
1857
|
+
_waitForImageElementReady(imageElement) {
|
|
1858
|
+
if (!imageElement) return Promise.resolve();
|
|
1859
|
+
if (imageElement.complete || imageElement.naturalWidth > 0 || imageElement.width > 0) return Promise.resolve();
|
|
1860
|
+
return new Promise((resolve, reject) => {
|
|
1861
|
+
let isSettled = false;
|
|
1862
|
+
const timerId = setTimeout(() => {
|
|
1863
|
+
settle(() => reject(new Error("Image load timed out while restoring state")));
|
|
1864
|
+
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
1865
|
+
const settle = (callback) => {
|
|
1866
|
+
if (isSettled) return;
|
|
1867
|
+
isSettled = true;
|
|
1868
|
+
clearTimeout(timerId);
|
|
1869
|
+
if (typeof imageElement.removeEventListener === "function") {
|
|
1870
|
+
imageElement.removeEventListener("load", handleLoad);
|
|
1871
|
+
imageElement.removeEventListener("error", handleError);
|
|
1872
|
+
} else {
|
|
1873
|
+
imageElement.onload = null;
|
|
1874
|
+
imageElement.onerror = null;
|
|
1875
|
+
}
|
|
1876
|
+
callback();
|
|
1877
|
+
};
|
|
1878
|
+
const handleLoad = () => settle(resolve);
|
|
1879
|
+
const handleError = (error) => settle(() => reject(error));
|
|
1880
|
+
if (typeof imageElement.addEventListener === "function") {
|
|
1881
|
+
imageElement.addEventListener("load", handleLoad, { once: true });
|
|
1882
|
+
imageElement.addEventListener("error", handleError, { once: true });
|
|
1883
|
+
} else {
|
|
1884
|
+
imageElement.onload = handleLoad;
|
|
1885
|
+
imageElement.onerror = handleError;
|
|
1392
1886
|
}
|
|
1393
1887
|
});
|
|
1394
1888
|
}
|
|
@@ -1403,9 +1897,8 @@ var ImageEditor = class {
|
|
|
1403
1897
|
*/
|
|
1404
1898
|
saveState() {
|
|
1405
1899
|
if (!this.canvas) return;
|
|
1406
|
-
const activeObject = this.canvas.getActiveObject();
|
|
1407
1900
|
try {
|
|
1408
|
-
const after = this.
|
|
1901
|
+
const after = this._captureCanvasStateOrThrow("saveState");
|
|
1409
1902
|
const before = this._lastSnapshot || after;
|
|
1410
1903
|
if (after === before) return;
|
|
1411
1904
|
let executedOnce = false;
|
|
@@ -1424,9 +1917,6 @@ var ImageEditor = class {
|
|
|
1424
1917
|
} catch (error) {
|
|
1425
1918
|
this._reportWarning("saveState: failed to save canvas snapshot", error);
|
|
1426
1919
|
} finally {
|
|
1427
|
-
if (activeObject && activeObject.maskId && !activeObject.__label && this.canvas.getObjects().includes(activeObject)) {
|
|
1428
|
-
this._handleSelectionChanged([activeObject]);
|
|
1429
|
-
}
|
|
1430
1920
|
this._updateUI();
|
|
1431
1921
|
}
|
|
1432
1922
|
}
|
|
@@ -1442,7 +1932,10 @@ var ImageEditor = class {
|
|
|
1442
1932
|
* @private
|
|
1443
1933
|
*/
|
|
1444
1934
|
_pushStateTransition(before, after) {
|
|
1445
|
-
if (!before || !after)
|
|
1935
|
+
if (!before || !after) {
|
|
1936
|
+
this._reportWarning("History transition skipped because a canvas snapshot is unavailable");
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1446
1939
|
if (before === after) return;
|
|
1447
1940
|
if (!this.historyManager) this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
1448
1941
|
const command = new Command(
|
|
@@ -1464,6 +1957,7 @@ var ImageEditor = class {
|
|
|
1464
1957
|
this._updateUI();
|
|
1465
1958
|
}).catch((error) => {
|
|
1466
1959
|
this._reportError("undo failed", error);
|
|
1960
|
+
throw error;
|
|
1467
1961
|
});
|
|
1468
1962
|
}
|
|
1469
1963
|
/**
|
|
@@ -1477,6 +1971,7 @@ var ImageEditor = class {
|
|
|
1477
1971
|
this._updateUI();
|
|
1478
1972
|
}).catch((error) => {
|
|
1479
1973
|
this._reportError("redo failed", error);
|
|
1974
|
+
throw error;
|
|
1480
1975
|
});
|
|
1481
1976
|
}
|
|
1482
1977
|
_rebindMaskEvents(mask) {
|
|
@@ -1498,22 +1993,17 @@ var ImageEditor = class {
|
|
|
1498
1993
|
metadata.originalStrokeWidth = Number.isFinite(Number(mask.strokeWidth)) ? Number(mask.strokeWidth) : 1;
|
|
1499
1994
|
}
|
|
1500
1995
|
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
1996
|
const mouseover = () => {
|
|
1512
|
-
mask.
|
|
1997
|
+
const opacity = Number(mask.originalAlpha);
|
|
1998
|
+
mask.set({
|
|
1999
|
+
stroke: "#ff5500",
|
|
2000
|
+
strokeWidth: 2,
|
|
2001
|
+
opacity: Math.min((Number.isFinite(opacity) ? opacity : 0.5) + 0.2, 1)
|
|
2002
|
+
});
|
|
1513
2003
|
if (mask.canvas) mask.canvas.requestRenderAll();
|
|
1514
2004
|
};
|
|
1515
2005
|
const mouseout = () => {
|
|
1516
|
-
mask.set(
|
|
2006
|
+
mask.set(this._getMaskNormalStyle(mask));
|
|
1517
2007
|
if (mask.canvas) mask.canvas.requestRenderAll();
|
|
1518
2008
|
};
|
|
1519
2009
|
mask.on("mouseover", mouseover);
|
|
@@ -1550,6 +2040,7 @@ var ImageEditor = class {
|
|
|
1550
2040
|
*/
|
|
1551
2041
|
createMask(config = {}) {
|
|
1552
2042
|
if (!this.canvas) return null;
|
|
2043
|
+
if (!this._canMutateNow("createMask")) return null;
|
|
1553
2044
|
const shapeType = config.shape || "rect";
|
|
1554
2045
|
const maskConfig = {
|
|
1555
2046
|
shape: shapeType,
|
|
@@ -1586,14 +2077,10 @@ var ImageEditor = class {
|
|
|
1586
2077
|
};
|
|
1587
2078
|
if (maskConfig.left === void 0 && this._lastMask) {
|
|
1588
2079
|
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;
|
|
2080
|
+
if (typeof previousMask.setCoords === "function") previousMask.setCoords();
|
|
2081
|
+
const previousBounds = typeof previousMask.getBoundingRect === "function" ? previousMask.getBoundingRect(true, true) : { left: previousMask.left || firstOffset, top: previousMask.top || firstOffset, width: previousMask.width || 0 };
|
|
2082
|
+
left = Math.round(previousBounds.left + previousBounds.width + maskConfig.gap);
|
|
2083
|
+
top = Math.round(previousBounds.top ?? firstOffset);
|
|
1597
2084
|
} else {
|
|
1598
2085
|
left = resolveValue(maskConfig.left, firstOffset, "width");
|
|
1599
2086
|
top = resolveValue(maskConfig.top, firstOffset, "height");
|
|
@@ -1721,6 +2208,8 @@ var ImageEditor = class {
|
|
|
1721
2208
|
* The associated label is also removed. UI and mask list are updated.
|
|
1722
2209
|
*/
|
|
1723
2210
|
removeSelectedMask() {
|
|
2211
|
+
if (!this.canvas) return;
|
|
2212
|
+
if (!this._canMutateNow("removeSelectedMask")) return;
|
|
1724
2213
|
const activeObject = this.canvas.getActiveObject();
|
|
1725
2214
|
const selectedMasks = this._getModifiedMasks(activeObject);
|
|
1726
2215
|
if (!selectedMasks.length) return;
|
|
@@ -1746,6 +2235,8 @@ var ImageEditor = class {
|
|
|
1746
2235
|
* UI and internal mask placement memory are reset.
|
|
1747
2236
|
*/
|
|
1748
2237
|
removeAllMasks(options = {}) {
|
|
2238
|
+
if (!this.canvas) return;
|
|
2239
|
+
if (!this._canMutateNow("removeAllMasks", options)) return;
|
|
1749
2240
|
const saveHistory = options.saveHistory !== false;
|
|
1750
2241
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
1751
2242
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
@@ -1813,6 +2304,10 @@ var ImageEditor = class {
|
|
|
1813
2304
|
let textObject = null;
|
|
1814
2305
|
if (this.options.label && typeof this.options.label.create === "function") {
|
|
1815
2306
|
textObject = this.options.label.create(mask, fabric);
|
|
2307
|
+
if (!textObject || typeof textObject.set !== "function") {
|
|
2308
|
+
this._reportWarning("label.create() returned an invalid Fabric object; using the default label");
|
|
2309
|
+
textObject = null;
|
|
2310
|
+
}
|
|
1816
2311
|
}
|
|
1817
2312
|
if (!textObject) {
|
|
1818
2313
|
let labelText = mask.maskName;
|
|
@@ -1880,9 +2375,10 @@ var ImageEditor = class {
|
|
|
1880
2375
|
if (!mask) return;
|
|
1881
2376
|
if (!this.options.maskLabelOnSelect) return;
|
|
1882
2377
|
if (!mask.__label) return;
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2378
|
+
if (typeof mask.setCoords === "function") mask.setCoords();
|
|
2379
|
+
const bounds = mask.getBoundingRect ? mask.getBoundingRect(true, true) : null;
|
|
2380
|
+
if (!bounds) return;
|
|
2381
|
+
const tl = { x: bounds.left, y: bounds.top };
|
|
1886
2382
|
const center = mask.getCenterPoint();
|
|
1887
2383
|
const vx = center.x - tl.x;
|
|
1888
2384
|
const vy = center.y - tl.y;
|
|
@@ -1960,7 +2456,7 @@ var ImageEditor = class {
|
|
|
1960
2456
|
* @private
|
|
1961
2457
|
*/
|
|
1962
2458
|
_updateMaskList() {
|
|
1963
|
-
const maskListElement =
|
|
2459
|
+
const maskListElement = this._getElement("maskList");
|
|
1964
2460
|
if (!maskListElement) return;
|
|
1965
2461
|
maskListElement.innerHTML = "";
|
|
1966
2462
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
@@ -1968,13 +2464,20 @@ var ImageEditor = class {
|
|
|
1968
2464
|
const listItemElement = document.createElement("li");
|
|
1969
2465
|
listItemElement.className = "list-group-item mask-item";
|
|
1970
2466
|
listItemElement.textContent = mask.maskName;
|
|
1971
|
-
listItemElement.
|
|
1972
|
-
this.canvas.setActiveObject(mask);
|
|
1973
|
-
this._handleSelectionChanged([mask]);
|
|
1974
|
-
};
|
|
2467
|
+
listItemElement.dataset.maskId = String(mask.maskId);
|
|
1975
2468
|
maskListElement.appendChild(listItemElement);
|
|
1976
2469
|
});
|
|
1977
2470
|
}
|
|
2471
|
+
_handleMaskListClick(event) {
|
|
2472
|
+
if (!this.canvas) return;
|
|
2473
|
+
const itemElement = event.target && event.target.closest ? event.target.closest(".mask-item") : null;
|
|
2474
|
+
if (!itemElement || !itemElement.dataset) return;
|
|
2475
|
+
const maskId = Number(itemElement.dataset.maskId);
|
|
2476
|
+
const mask = this.canvas.getObjects().find((object) => Number(object.maskId) === maskId);
|
|
2477
|
+
if (!mask) return;
|
|
2478
|
+
this.canvas.setActiveObject(mask);
|
|
2479
|
+
this._handleSelectionChanged([mask]);
|
|
2480
|
+
}
|
|
1978
2481
|
/**
|
|
1979
2482
|
* Updates the visual selection (CSS 'active') state for the mask list in the DOM.
|
|
1980
2483
|
*
|
|
@@ -1982,12 +2485,13 @@ var ImageEditor = class {
|
|
|
1982
2485
|
* @private
|
|
1983
2486
|
*/
|
|
1984
2487
|
_updateMaskListSelection(selectedMask) {
|
|
1985
|
-
const maskListElement =
|
|
2488
|
+
const maskListElement = this._getElement("maskList");
|
|
1986
2489
|
if (!maskListElement) return;
|
|
1987
2490
|
const maskItems = maskListElement.querySelectorAll(".mask-item");
|
|
1988
2491
|
maskItems.forEach((item) => {
|
|
1989
|
-
const isSelected = !!selectedMask && item.
|
|
2492
|
+
const isSelected = !!selectedMask && Number(item.dataset.maskId) === Number(selectedMask.maskId);
|
|
1990
2493
|
item.classList.toggle("active", isSelected);
|
|
2494
|
+
item.classList.toggle("selected", isSelected);
|
|
1991
2495
|
});
|
|
1992
2496
|
}
|
|
1993
2497
|
/**
|
|
@@ -2002,19 +2506,36 @@ var ImageEditor = class {
|
|
|
2002
2506
|
*/
|
|
2003
2507
|
async mergeMasks() {
|
|
2004
2508
|
if (!this.originalImage) return;
|
|
2509
|
+
this._assertIdleForOperation("mergeMasks");
|
|
2005
2510
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2006
2511
|
if (!masks.length) return;
|
|
2512
|
+
const beforeJson = this._serializeCanvasState();
|
|
2513
|
+
const operationToken = this._beginBusyOperation("mergeMasks");
|
|
2007
2514
|
this.canvas.discardActiveObject();
|
|
2008
2515
|
this.canvas.renderAll();
|
|
2009
2516
|
try {
|
|
2010
|
-
const
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2517
|
+
const merged = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
2518
|
+
exportImageArea: true,
|
|
2519
|
+
multiplier: this.options.exportMultiplier,
|
|
2520
|
+
fileType: "png"
|
|
2521
|
+
}));
|
|
2522
|
+
this.removeAllMasks(this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2523
|
+
await this.loadImage(merged, this._withInternalOperationOptions(operationToken, {
|
|
2524
|
+
preserveScroll: true,
|
|
2525
|
+
resetMaskCounter: false
|
|
2526
|
+
}));
|
|
2014
2527
|
const afterJson = this._serializeCanvasState();
|
|
2015
2528
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2016
2529
|
} catch (error) {
|
|
2017
2530
|
this._reportError("merge error", error);
|
|
2531
|
+
try {
|
|
2532
|
+
await this.loadFromState(beforeJson);
|
|
2533
|
+
} catch (restoreError) {
|
|
2534
|
+
this._reportError("mergeMasks rollback failed", restoreError);
|
|
2535
|
+
}
|
|
2536
|
+
throw error;
|
|
2537
|
+
} finally {
|
|
2538
|
+
this._endBusyOperation(operationToken);
|
|
2018
2539
|
}
|
|
2019
2540
|
}
|
|
2020
2541
|
/**
|
|
@@ -2036,6 +2557,7 @@ var ImageEditor = class {
|
|
|
2036
2557
|
*/
|
|
2037
2558
|
downloadImage(fileName = this.options.defaultDownloadFileName) {
|
|
2038
2559
|
if (!this.originalImage) return;
|
|
2560
|
+
if (!this._canMutateNow("downloadImage")) return;
|
|
2039
2561
|
const exportImageArea = this.options.exportImageAreaByDefault;
|
|
2040
2562
|
this.exportImageBase64({ exportImageArea, multiplier: this.options.exportMultiplier }).then((imageBase64) => {
|
|
2041
2563
|
const link = document.createElement("a");
|
|
@@ -2064,6 +2586,7 @@ var ImageEditor = class {
|
|
|
2064
2586
|
*/
|
|
2065
2587
|
async exportImageBase64(options = {}) {
|
|
2066
2588
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
2589
|
+
this._assertIdleForOperation("exportImageBase64", options);
|
|
2067
2590
|
const exportImageArea = typeof options.exportImageArea === "boolean" ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2068
2591
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2069
2592
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
@@ -2079,12 +2602,13 @@ var ImageEditor = class {
|
|
|
2079
2602
|
this.canvas.renderAll();
|
|
2080
2603
|
this.originalImage.setCoords();
|
|
2081
2604
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2082
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds
|
|
2605
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2083
2606
|
return await this._exportCanvasRegionToDataURL({
|
|
2084
2607
|
...exportRegion,
|
|
2085
2608
|
multiplier,
|
|
2086
2609
|
quality,
|
|
2087
|
-
format
|
|
2610
|
+
format,
|
|
2611
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2088
2612
|
});
|
|
2089
2613
|
} finally {
|
|
2090
2614
|
maskVisibilityBackups.forEach((backup) => {
|
|
@@ -2119,12 +2643,13 @@ var ImageEditor = class {
|
|
|
2119
2643
|
this.canvas.renderAll();
|
|
2120
2644
|
this.originalImage.setCoords();
|
|
2121
2645
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2122
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds
|
|
2646
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2123
2647
|
finalBase64 = await this._exportCanvasRegionToDataURL({
|
|
2124
2648
|
...exportRegion,
|
|
2125
2649
|
multiplier,
|
|
2126
2650
|
quality,
|
|
2127
|
-
format
|
|
2651
|
+
format,
|
|
2652
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2128
2653
|
});
|
|
2129
2654
|
} finally {
|
|
2130
2655
|
maskStyleBackups.forEach((backup) => {
|
|
@@ -2176,6 +2701,7 @@ var ImageEditor = class {
|
|
|
2176
2701
|
*/
|
|
2177
2702
|
async exportImageFile(options = {}) {
|
|
2178
2703
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
2704
|
+
this._assertIdleForOperation("exportImageFile");
|
|
2179
2705
|
const {
|
|
2180
2706
|
mergeMask = true,
|
|
2181
2707
|
fileType = "jpeg",
|
|
@@ -2184,19 +2710,20 @@ var ImageEditor = class {
|
|
|
2184
2710
|
fileName = this.options.defaultDownloadFileName ?? "exported_image.jpg"
|
|
2185
2711
|
} = options;
|
|
2186
2712
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
2713
|
+
const normalizedQuality = this._normalizeQuality(quality);
|
|
2187
2714
|
let imageBase64;
|
|
2188
2715
|
if (mergeMask) {
|
|
2189
2716
|
imageBase64 = await this.exportImageBase64({
|
|
2190
2717
|
exportImageArea: true,
|
|
2191
2718
|
multiplier,
|
|
2192
|
-
quality,
|
|
2719
|
+
quality: normalizedQuality,
|
|
2193
2720
|
fileType: safeFileType
|
|
2194
2721
|
});
|
|
2195
2722
|
} else {
|
|
2196
2723
|
imageBase64 = await this.exportImageBase64({
|
|
2197
2724
|
exportImageArea: false,
|
|
2198
2725
|
multiplier,
|
|
2199
|
-
quality,
|
|
2726
|
+
quality: normalizedQuality,
|
|
2200
2727
|
fileType: safeFileType
|
|
2201
2728
|
});
|
|
2202
2729
|
}
|
|
@@ -2211,8 +2738,9 @@ var ImageEditor = class {
|
|
|
2211
2738
|
offscreenCanvas.width = imageElement.width;
|
|
2212
2739
|
offscreenCanvas.height = imageElement.height;
|
|
2213
2740
|
const context = offscreenCanvas.getContext("2d");
|
|
2741
|
+
if (!context) throw new Error("Unable to create 2D canvas context for export conversion");
|
|
2214
2742
|
context.drawImage(imageElement, 0, 0);
|
|
2215
|
-
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`,
|
|
2743
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
2216
2744
|
resolve(convertedDataUrl);
|
|
2217
2745
|
} catch (error) {
|
|
2218
2746
|
reject(error);
|
|
@@ -2278,7 +2806,9 @@ var ImageEditor = class {
|
|
|
2278
2806
|
if (this._cropHandlers && this._cropHandlers.length) {
|
|
2279
2807
|
this._cropHandlers.forEach((targetHandlers) => {
|
|
2280
2808
|
targetHandlers.handlers.forEach((handlerRecord) => {
|
|
2281
|
-
targetHandlers.target.off
|
|
2809
|
+
if (targetHandlers.target && typeof targetHandlers.target.off === "function") {
|
|
2810
|
+
targetHandlers.target.off(handlerRecord.eventName, handlerRecord.handler);
|
|
2811
|
+
}
|
|
2282
2812
|
});
|
|
2283
2813
|
});
|
|
2284
2814
|
}
|
|
@@ -2286,7 +2816,7 @@ var ImageEditor = class {
|
|
|
2286
2816
|
void error;
|
|
2287
2817
|
}
|
|
2288
2818
|
try {
|
|
2289
|
-
this.canvas.remove(this._cropRect);
|
|
2819
|
+
if (this.canvas) this.canvas.remove(this._cropRect);
|
|
2290
2820
|
} catch (error) {
|
|
2291
2821
|
void error;
|
|
2292
2822
|
}
|
|
@@ -2304,7 +2834,9 @@ var ImageEditor = class {
|
|
|
2304
2834
|
*/
|
|
2305
2835
|
enterCropMode() {
|
|
2306
2836
|
if (!this.canvas || !this.originalImage || this._cropMode) return;
|
|
2837
|
+
if (!this._canMutateNow("enterCropMode")) return;
|
|
2307
2838
|
if (!this.isImageLoaded()) return;
|
|
2839
|
+
this._removeCropRect();
|
|
2308
2840
|
this._cropMode = true;
|
|
2309
2841
|
this._prevSelectionSetting = this.canvas.selection;
|
|
2310
2842
|
this.canvas.selection = false;
|
|
@@ -2420,6 +2952,7 @@ var ImageEditor = class {
|
|
|
2420
2952
|
*/
|
|
2421
2953
|
async applyCrop() {
|
|
2422
2954
|
if (!this.canvas || !this._cropMode || !this._cropRect) return;
|
|
2955
|
+
this._assertIdleForOperation("applyCrop");
|
|
2423
2956
|
this._cropRect.setCoords();
|
|
2424
2957
|
const rectBounds = this._cropRect.getBoundingRect(true, true);
|
|
2425
2958
|
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
@@ -2444,12 +2977,8 @@ var ImageEditor = class {
|
|
|
2444
2977
|
this._removeLabelForMask(mask);
|
|
2445
2978
|
this.canvas.remove(mask);
|
|
2446
2979
|
if (shouldPreserveMasks && intersectsCrop) {
|
|
2447
|
-
mask.
|
|
2448
|
-
|
|
2449
|
-
top: (mask.top || 0) - cropRegion.sourceY,
|
|
2450
|
-
visible: true
|
|
2451
|
-
});
|
|
2452
|
-
mask.setCoords();
|
|
2980
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
2981
|
+
mask.set({ visible: true });
|
|
2453
2982
|
preservedMasks.push(mask);
|
|
2454
2983
|
}
|
|
2455
2984
|
} catch (error) {
|
|
@@ -2480,7 +3009,7 @@ var ImageEditor = class {
|
|
|
2480
3009
|
return;
|
|
2481
3010
|
}
|
|
2482
3011
|
try {
|
|
2483
|
-
await this.loadImage(croppedBase64);
|
|
3012
|
+
await this.loadImage(croppedBase64, { resetMaskCounter: false });
|
|
2484
3013
|
if (preservedMasks.length) {
|
|
2485
3014
|
preservedMasks.forEach((mask) => {
|
|
2486
3015
|
this._rebindMaskEvents(mask);
|
|
@@ -2498,7 +3027,7 @@ var ImageEditor = class {
|
|
|
2498
3027
|
}
|
|
2499
3028
|
let afterJson;
|
|
2500
3029
|
try {
|
|
2501
|
-
afterJson = this._serializeCanvasState();
|
|
3030
|
+
afterJson = preservedMasks.length ? this._serializeCanvasState() : this._lastSnapshot;
|
|
2502
3031
|
} catch (error) {
|
|
2503
3032
|
this._reportWarning("applyCrop: failed to serialize after state", error);
|
|
2504
3033
|
afterJson = null;
|
|
@@ -2518,7 +3047,7 @@ var ImageEditor = class {
|
|
|
2518
3047
|
* @private
|
|
2519
3048
|
*/
|
|
2520
3049
|
_updateInputs() {
|
|
2521
|
-
const scaleInputElement =
|
|
3050
|
+
const scaleInputElement = this._getElement("scaleRate");
|
|
2522
3051
|
if (scaleInputElement) scaleInputElement.value = Math.round(this.currentScale * 100);
|
|
2523
3052
|
}
|
|
2524
3053
|
/**
|
|
@@ -2527,6 +3056,7 @@ var ImageEditor = class {
|
|
|
2527
3056
|
* @private
|
|
2528
3057
|
*/
|
|
2529
3058
|
_updateUI() {
|
|
3059
|
+
if (!this.canvas) return;
|
|
2530
3060
|
const hasImage = !!this.originalImage;
|
|
2531
3061
|
const masks = hasImage ? this.canvas.getObjects().filter((object) => object.maskId) : [];
|
|
2532
3062
|
const hasMasks = masks.length > 0;
|
|
@@ -2536,9 +3066,10 @@ var ImageEditor = class {
|
|
|
2536
3066
|
const canUndo = this.historyManager?.canUndo();
|
|
2537
3067
|
const canRedo = this.historyManager?.canRedo();
|
|
2538
3068
|
const isInCropMode = !!this._cropMode;
|
|
3069
|
+
const isBusy = this.isAnimating || this._isLoading || !!this._activeOperationToken || !!(this.animationQueue && this.animationQueue.isBusy());
|
|
2539
3070
|
if (isInCropMode) {
|
|
2540
3071
|
for (const key of Object.keys(this.elements || {})) {
|
|
2541
|
-
const element =
|
|
3072
|
+
const element = this._getElement(key);
|
|
2542
3073
|
if (!element) continue;
|
|
2543
3074
|
if (key === "applyCropBtn" || key === "cancelCropBtn") {
|
|
2544
3075
|
this._setDisabled(key, false);
|
|
@@ -2548,23 +3079,23 @@ var ImageEditor = class {
|
|
|
2548
3079
|
}
|
|
2549
3080
|
return;
|
|
2550
3081
|
}
|
|
2551
|
-
this._setDisabled("zoomInBtn", !hasImage ||
|
|
2552
|
-
this._setDisabled("zoomOutBtn", !hasImage ||
|
|
2553
|
-
this._setDisabled("rotateLeftBtn", !hasImage ||
|
|
2554
|
-
this._setDisabled("rotateRightBtn", !hasImage ||
|
|
2555
|
-
this._setDisabled("addMaskBtn", !hasImage ||
|
|
2556
|
-
this._setDisabled("removeMaskBtn", !hasSelectedMask ||
|
|
2557
|
-
this._setDisabled("removeAllMasksBtn", !hasMasks ||
|
|
2558
|
-
this._setDisabled("mergeBtn", !hasImage || !hasMasks ||
|
|
2559
|
-
this._setDisabled("downloadBtn", !hasImage ||
|
|
2560
|
-
this._setDisabled("resetBtn", !hasImage || isDefaultTransform ||
|
|
2561
|
-
this._setDisabled("undoBtn", !hasImage ||
|
|
2562
|
-
this._setDisabled("redoBtn", !hasImage ||
|
|
2563
|
-
this._setDisabled("cropBtn", !hasImage ||
|
|
3082
|
+
this._setDisabled("zoomInBtn", !hasImage || isBusy || this.currentScale >= this.options.maxScale);
|
|
3083
|
+
this._setDisabled("zoomOutBtn", !hasImage || isBusy || this.currentScale <= this.options.minScale);
|
|
3084
|
+
this._setDisabled("rotateLeftBtn", !hasImage || isBusy);
|
|
3085
|
+
this._setDisabled("rotateRightBtn", !hasImage || isBusy);
|
|
3086
|
+
this._setDisabled("addMaskBtn", !hasImage || isBusy);
|
|
3087
|
+
this._setDisabled("removeMaskBtn", !hasSelectedMask || isBusy);
|
|
3088
|
+
this._setDisabled("removeAllMasksBtn", !hasMasks || isBusy);
|
|
3089
|
+
this._setDisabled("mergeBtn", !hasImage || !hasMasks || isBusy);
|
|
3090
|
+
this._setDisabled("downloadBtn", !hasImage || isBusy);
|
|
3091
|
+
this._setDisabled("resetBtn", !hasImage || isDefaultTransform || isBusy);
|
|
3092
|
+
this._setDisabled("undoBtn", !hasImage || isBusy || !canUndo);
|
|
3093
|
+
this._setDisabled("redoBtn", !hasImage || isBusy || !canRedo);
|
|
3094
|
+
this._setDisabled("cropBtn", !hasImage || isBusy);
|
|
2564
3095
|
this._setDisabled("applyCropBtn", true);
|
|
2565
3096
|
this._setDisabled("cancelCropBtn", true);
|
|
2566
|
-
this._setDisabled("imageInput",
|
|
2567
|
-
this._setDisabled("uploadArea",
|
|
3097
|
+
this._setDisabled("imageInput", isBusy);
|
|
3098
|
+
this._setDisabled("uploadArea", isBusy);
|
|
2568
3099
|
}
|
|
2569
3100
|
/**
|
|
2570
3101
|
* Enables or disables a specific UI element (typically a button) by its key.
|
|
@@ -2574,18 +3105,22 @@ var ImageEditor = class {
|
|
|
2574
3105
|
* @private
|
|
2575
3106
|
*/
|
|
2576
3107
|
_setDisabled(key, disabled) {
|
|
2577
|
-
const element =
|
|
3108
|
+
const element = this._getElement(key);
|
|
2578
3109
|
if (!element) return;
|
|
2579
3110
|
if ("disabled" in element) {
|
|
2580
3111
|
element.disabled = !!disabled;
|
|
2581
3112
|
return;
|
|
2582
3113
|
}
|
|
3114
|
+
if (!this._elementOriginalPointerEvents) this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
3115
|
+
if (!this._elementOriginalPointerEvents.has(key)) {
|
|
3116
|
+
this._elementOriginalPointerEvents.set(key, element.style.pointerEvents || "");
|
|
3117
|
+
}
|
|
2583
3118
|
if (disabled) {
|
|
2584
3119
|
element.setAttribute("aria-disabled", "true");
|
|
2585
3120
|
element.style.pointerEvents = "none";
|
|
2586
3121
|
} else {
|
|
2587
3122
|
element.removeAttribute("aria-disabled");
|
|
2588
|
-
element.style.pointerEvents = "";
|
|
3123
|
+
element.style.pointerEvents = this._elementOriginalPointerEvents.get(key) ?? "";
|
|
2589
3124
|
}
|
|
2590
3125
|
}
|
|
2591
3126
|
_isElementDisabled(element) {
|
|
@@ -2608,9 +3143,18 @@ var ImageEditor = class {
|
|
|
2608
3143
|
* @private
|
|
2609
3144
|
*/
|
|
2610
3145
|
_setPlaceholderVisible(show) {
|
|
2611
|
-
if (
|
|
2612
|
-
this.
|
|
2613
|
-
|
|
3146
|
+
if (this.placeholderElement) this._setElementVisible(this.placeholderElement, show);
|
|
3147
|
+
const canvasVisibilityElement = this._getCanvasVisibilityElement();
|
|
3148
|
+
if (canvasVisibilityElement && canvasVisibilityElement !== this.placeholderElement) {
|
|
3149
|
+
this._setElementVisible(canvasVisibilityElement, !show);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
_getCanvasVisibilityElement() {
|
|
3153
|
+
const wrapperElement = this.canvas && this.canvas.wrapperEl ? this.canvas.wrapperEl : null;
|
|
3154
|
+
if (this.containerElement && this.placeholderElement && (this.containerElement === this.placeholderElement || this.containerElement.contains(this.placeholderElement))) {
|
|
3155
|
+
return wrapperElement || this.canvasElement;
|
|
3156
|
+
}
|
|
3157
|
+
return this.containerElement || wrapperElement || this.canvasElement;
|
|
2614
3158
|
}
|
|
2615
3159
|
/**
|
|
2616
3160
|
* Updates element visibility.
|
|
@@ -2622,9 +3166,34 @@ var ImageEditor = class {
|
|
|
2622
3166
|
*/
|
|
2623
3167
|
_setElementVisible(element, isVisible) {
|
|
2624
3168
|
if (!element) return;
|
|
3169
|
+
this._rememberElementVisibility(element);
|
|
2625
3170
|
element.hidden = !isVisible;
|
|
2626
3171
|
element.setAttribute("aria-hidden", isVisible ? "false" : "true");
|
|
2627
|
-
if (
|
|
3172
|
+
if (element.classList) {
|
|
3173
|
+
element.classList.toggle("d-none", !isVisible);
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
_rememberElementVisibility(element) {
|
|
3177
|
+
if (!element || this._visibilityStateByElement.has(element)) return;
|
|
3178
|
+
this._visibilityStateByElement.set(element, this._captureElementVisibility(element));
|
|
3179
|
+
}
|
|
3180
|
+
_captureElementVisibility(element) {
|
|
3181
|
+
if (!element) return null;
|
|
3182
|
+
return {
|
|
3183
|
+
hidden: element.hidden,
|
|
3184
|
+
ariaHidden: element.getAttribute("aria-hidden"),
|
|
3185
|
+
className: element.className
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
_restoreElementVisibility(element, state) {
|
|
3189
|
+
if (!element || !state) return;
|
|
3190
|
+
element.hidden = !!state.hidden;
|
|
3191
|
+
if (state.ariaHidden === null) {
|
|
3192
|
+
element.removeAttribute("aria-hidden");
|
|
3193
|
+
} else {
|
|
3194
|
+
element.setAttribute("aria-hidden", state.ariaHidden);
|
|
3195
|
+
}
|
|
3196
|
+
element.className = state.className || "";
|
|
2628
3197
|
}
|
|
2629
3198
|
/**
|
|
2630
3199
|
* Cleans up and disposes of the canvas and related references.
|
|
@@ -2632,10 +3201,17 @@ var ImageEditor = class {
|
|
|
2632
3201
|
* @public
|
|
2633
3202
|
*/
|
|
2634
3203
|
dispose() {
|
|
3204
|
+
this._disposed = true;
|
|
3205
|
+
this._rejectActiveAnimations(new Error("Editor disposed during animation"));
|
|
3206
|
+
if (this.animationQueue) {
|
|
3207
|
+
this.animationQueue.cancelAll(new Error("Editor disposed"));
|
|
3208
|
+
}
|
|
3209
|
+
this._isLoading = false;
|
|
3210
|
+
this._activeOperationName = null;
|
|
3211
|
+
this._activeOperationToken = null;
|
|
2635
3212
|
try {
|
|
2636
|
-
for (const key
|
|
2637
|
-
const
|
|
2638
|
-
const element = document.getElementById(this.elements[key]);
|
|
3213
|
+
for (const [key, handlers] of Object.entries(this._handlersByElementKey || {})) {
|
|
3214
|
+
const element = this._getElement(key);
|
|
2639
3215
|
if (!element) continue;
|
|
2640
3216
|
handlers.forEach((handlerRecord) => {
|
|
2641
3217
|
try {
|
|
@@ -2656,9 +3232,28 @@ var ImageEditor = class {
|
|
|
2656
3232
|
}
|
|
2657
3233
|
this._cropRect = null;
|
|
2658
3234
|
}
|
|
2659
|
-
if (this.containerElement && this._containerOriginalOverflow
|
|
3235
|
+
if (this.containerElement && this._containerOriginalOverflow) {
|
|
3236
|
+
try {
|
|
3237
|
+
this._restoreContainerOverflowState();
|
|
3238
|
+
} catch (error) {
|
|
3239
|
+
void error;
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
if (this._visibilityStateByElement) {
|
|
3243
|
+
try {
|
|
3244
|
+
[this.placeholderElement, this._getCanvasVisibilityElement()].forEach((element) => {
|
|
3245
|
+
const state = element ? this._visibilityStateByElement.get(element) : null;
|
|
3246
|
+
if (state) this._restoreElementVisibility(element, state);
|
|
3247
|
+
});
|
|
3248
|
+
} catch (error) {
|
|
3249
|
+
void error;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
if (this.canvasElement && this._canvasElementOriginalStyle) {
|
|
2660
3253
|
try {
|
|
2661
|
-
this.
|
|
3254
|
+
this.canvasElement.style.display = this._canvasElementOriginalStyle.display;
|
|
3255
|
+
this.canvasElement.style.width = this._canvasElementOriginalStyle.width;
|
|
3256
|
+
this.canvasElement.style.height = this._canvasElementOriginalStyle.height;
|
|
2662
3257
|
} catch (error) {
|
|
2663
3258
|
void error;
|
|
2664
3259
|
}
|
|
@@ -2674,6 +3269,22 @@ var ImageEditor = class {
|
|
|
2674
3269
|
this.isImageLoadedToCanvas = false;
|
|
2675
3270
|
}
|
|
2676
3271
|
this._handlersByElementKey = {};
|
|
3272
|
+
this._elementCache = {};
|
|
3273
|
+
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
3274
|
+
this._clearMaskPlacementMemory();
|
|
3275
|
+
this.originalImage = null;
|
|
3276
|
+
this.baseImageScale = 1;
|
|
3277
|
+
this.currentScale = 1;
|
|
3278
|
+
this.currentRotation = 0;
|
|
3279
|
+
this.isAnimating = false;
|
|
3280
|
+
this._isLoading = false;
|
|
3281
|
+
this._cropMode = false;
|
|
3282
|
+
this._cropRect = null;
|
|
3283
|
+
this._cropHandlers = [];
|
|
3284
|
+
this._cropPrevEvented = null;
|
|
3285
|
+
this._prevSelectionSetting = void 0;
|
|
3286
|
+
this._lastContainerViewportSize = null;
|
|
3287
|
+
this._initialized = false;
|
|
2677
3288
|
}
|
|
2678
3289
|
};
|
|
2679
3290
|
var AnimationQueue = class {
|
|
@@ -2683,6 +3294,8 @@ var AnimationQueue = class {
|
|
|
2683
3294
|
constructor() {
|
|
2684
3295
|
this.animationTasks = [];
|
|
2685
3296
|
this.isRunning = false;
|
|
3297
|
+
this.currentTask = null;
|
|
3298
|
+
this._generation = 0;
|
|
2686
3299
|
}
|
|
2687
3300
|
/**
|
|
2688
3301
|
* Adds an animation function to the queue.
|
|
@@ -2692,12 +3305,30 @@ var AnimationQueue = class {
|
|
|
2692
3305
|
*/
|
|
2693
3306
|
async add(animationFn) {
|
|
2694
3307
|
return new Promise((resolve, reject) => {
|
|
2695
|
-
this.animationTasks.push({ animationFn, resolve, reject });
|
|
3308
|
+
this.animationTasks.push({ animationFn, resolve, reject, isSettled: false });
|
|
2696
3309
|
if (!this.isRunning) {
|
|
2697
3310
|
this._drainQueue();
|
|
2698
3311
|
}
|
|
2699
3312
|
});
|
|
2700
3313
|
}
|
|
3314
|
+
isBusy() {
|
|
3315
|
+
return this.isRunning || this.animationTasks.length > 0;
|
|
3316
|
+
}
|
|
3317
|
+
cancelAll(reason = new Error("Animation queue cancelled")) {
|
|
3318
|
+
this._generation += 1;
|
|
3319
|
+
const cancellationError = reason instanceof Error ? reason : new Error(String(reason));
|
|
3320
|
+
const tasks = [
|
|
3321
|
+
...this.currentTask ? [this.currentTask] : [],
|
|
3322
|
+
...this.animationTasks.splice(0)
|
|
3323
|
+
];
|
|
3324
|
+
tasks.forEach((task) => {
|
|
3325
|
+
if (!task || task.isSettled) return;
|
|
3326
|
+
task.isSettled = true;
|
|
3327
|
+
task.reject(cancellationError);
|
|
3328
|
+
});
|
|
3329
|
+
this.isRunning = false;
|
|
3330
|
+
this.currentTask = null;
|
|
3331
|
+
}
|
|
2701
3332
|
/**
|
|
2702
3333
|
* Runs queued animation tasks sequentially until the queue is empty.
|
|
2703
3334
|
*
|
|
@@ -2705,19 +3336,34 @@ var AnimationQueue = class {
|
|
|
2705
3336
|
* @returns {Promise<void>}
|
|
2706
3337
|
*/
|
|
2707
3338
|
async _drainQueue() {
|
|
2708
|
-
if (this.
|
|
2709
|
-
|
|
2710
|
-
return;
|
|
2711
|
-
}
|
|
3339
|
+
if (this.isRunning) return;
|
|
3340
|
+
const generation = this._generation;
|
|
2712
3341
|
this.isRunning = true;
|
|
2713
|
-
const { animationFn, resolve, reject } = this.animationTasks.shift();
|
|
2714
3342
|
try {
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
3343
|
+
while (this.animationTasks.length > 0 && generation === this._generation) {
|
|
3344
|
+
const task = this.animationTasks.shift();
|
|
3345
|
+
this.currentTask = task;
|
|
3346
|
+
try {
|
|
3347
|
+
const result = await task.animationFn();
|
|
3348
|
+
if (generation === this._generation && !task.isSettled) {
|
|
3349
|
+
task.isSettled = true;
|
|
3350
|
+
task.resolve(result);
|
|
3351
|
+
}
|
|
3352
|
+
} catch (error) {
|
|
3353
|
+
if (generation === this._generation && !task.isSettled) {
|
|
3354
|
+
task.isSettled = true;
|
|
3355
|
+
task.reject(error);
|
|
3356
|
+
}
|
|
3357
|
+
} finally {
|
|
3358
|
+
if (generation === this._generation && this.currentTask === task) this.currentTask = null;
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
} finally {
|
|
3362
|
+
if (generation === this._generation) {
|
|
3363
|
+
this.isRunning = false;
|
|
3364
|
+
this.currentTask = null;
|
|
3365
|
+
}
|
|
2719
3366
|
}
|
|
2720
|
-
await this._drainQueue();
|
|
2721
3367
|
}
|
|
2722
3368
|
};
|
|
2723
3369
|
var Command = class {
|
|
@@ -2748,15 +3394,8 @@ var HistoryManager = class {
|
|
|
2748
3394
|
* @private
|
|
2749
3395
|
*/
|
|
2750
3396
|
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;
|
|
3397
|
+
const nextTask = this.pending.then(() => Promise.resolve().then(task));
|
|
3398
|
+
this.pending = nextTask.catch(() => void 0);
|
|
2760
3399
|
return nextTask;
|
|
2761
3400
|
}
|
|
2762
3401
|
/**
|
|
@@ -2767,8 +3406,14 @@ var HistoryManager = class {
|
|
|
2767
3406
|
* @returns {void}
|
|
2768
3407
|
*/
|
|
2769
3408
|
execute(command) {
|
|
2770
|
-
command.execute();
|
|
3409
|
+
const result = command.execute();
|
|
3410
|
+
if (result && typeof result.then === "function") {
|
|
3411
|
+
return Promise.resolve(result).then(() => {
|
|
3412
|
+
this.push(command);
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
2771
3415
|
this.push(command);
|
|
3416
|
+
return result;
|
|
2772
3417
|
}
|
|
2773
3418
|
/**
|
|
2774
3419
|
* Pushes an already-applied command onto the history stack.
|
|
@@ -2784,9 +3429,8 @@ var HistoryManager = class {
|
|
|
2784
3429
|
this.history.push(command);
|
|
2785
3430
|
if (this.history.length > this.maxSize) {
|
|
2786
3431
|
this.history.shift();
|
|
2787
|
-
} else {
|
|
2788
|
-
this.currentIndex++;
|
|
2789
3432
|
}
|
|
3433
|
+
this.currentIndex = this.history.length - 1;
|
|
2790
3434
|
}
|
|
2791
3435
|
/**
|
|
2792
3436
|
* Checks whether an undo operation is possible.
|