@bensitu/image-editor 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/image-editor.esm.js +946 -542
- 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 +946 -542
- package/dist/image-editor.esm.mjs.map +2 -2
- package/dist/image-editor.js +946 -542
- package/dist/image-editor.js.map +2 -2
- package/dist/image-editor.min.js +2 -2
- package/dist/image-editor.min.js.map +3 -3
- package/image-editor.d.ts +2 -0
- package/package.json +9 -4
- package/src/image-editor.js +899 -358
|
@@ -5,19 +5,16 @@ import fabricModule from "fabric";
|
|
|
5
5
|
/**
|
|
6
6
|
* @file image-editor.js
|
|
7
7
|
* @module image-editor
|
|
8
|
-
* @version 1.
|
|
8
|
+
* @version 1.4.0
|
|
9
9
|
* @author Ben Situ
|
|
10
10
|
* @license MIT
|
|
11
11
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
12
12
|
*/
|
|
13
13
|
var fabric = null;
|
|
14
14
|
function getGlobalScope() {
|
|
15
|
-
if (typeof globalThis !== "undefined")
|
|
16
|
-
|
|
17
|
-
if (typeof
|
|
18
|
-
return self;
|
|
19
|
-
if (typeof window !== "undefined")
|
|
20
|
-
return window;
|
|
15
|
+
if (typeof globalThis !== "undefined") return globalThis;
|
|
16
|
+
if (typeof self !== "undefined") return self;
|
|
17
|
+
if (typeof window !== "undefined") return window;
|
|
21
18
|
return null;
|
|
22
19
|
}
|
|
23
20
|
function getGlobalFabric() {
|
|
@@ -29,8 +26,7 @@ function setFabric(fabricInstance2) {
|
|
|
29
26
|
return fabric;
|
|
30
27
|
}
|
|
31
28
|
function ensureFabric() {
|
|
32
|
-
if (!fabric)
|
|
33
|
-
setFabric();
|
|
29
|
+
if (!fabric) setFabric();
|
|
34
30
|
return fabric;
|
|
35
31
|
}
|
|
36
32
|
var ImageEditor = class {
|
|
@@ -76,6 +72,8 @@ var ImageEditor = class {
|
|
|
76
72
|
downsampleMaxWidth: 4e3,
|
|
77
73
|
downsampleMaxHeight: 3e3,
|
|
78
74
|
downsampleQuality: 0.92,
|
|
75
|
+
preserveSourceFormat: true,
|
|
76
|
+
downsampleMimeType: null,
|
|
79
77
|
imageLoadTimeoutMs: 3e4,
|
|
80
78
|
exportMultiplier: 1,
|
|
81
79
|
exportImageAreaByDefault: true,
|
|
@@ -124,6 +122,7 @@ var ImageEditor = class {
|
|
|
124
122
|
this.isImageLoadedToCanvas = false;
|
|
125
123
|
this.maxHistorySize = 50;
|
|
126
124
|
this._handlersByElementKey = {};
|
|
125
|
+
this._elementCache = {};
|
|
127
126
|
this._lastMask = null;
|
|
128
127
|
this._lastMaskInitialLeft = null;
|
|
129
128
|
this._lastMaskInitialTop = null;
|
|
@@ -134,8 +133,14 @@ var ImageEditor = class {
|
|
|
134
133
|
this._cropHandlers = [];
|
|
135
134
|
this._cropPrevEvented = null;
|
|
136
135
|
this._prevSelectionSetting = void 0;
|
|
137
|
-
this._containerOriginalOverflow =
|
|
136
|
+
this._containerOriginalOverflow = null;
|
|
137
|
+
this._lastContainerViewportSize = null;
|
|
138
|
+
this._canvasElementOriginalStyle = null;
|
|
139
|
+
this._visibilityStateByElement = /* @__PURE__ */ new WeakMap();
|
|
138
140
|
this._scrollbarSizeCache = null;
|
|
141
|
+
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
142
|
+
this._disposed = false;
|
|
143
|
+
this._initialized = false;
|
|
139
144
|
this.onImageLoaded = typeof options.onImageLoaded === "function" ? options.onImageLoaded : null;
|
|
140
145
|
this.animationQueue = new AnimationQueue();
|
|
141
146
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
@@ -198,8 +203,17 @@ var ImageEditor = class {
|
|
|
198
203
|
* });
|
|
199
204
|
*/
|
|
200
205
|
init(idMap = {}) {
|
|
201
|
-
if (!this._fabricLoaded)
|
|
202
|
-
|
|
206
|
+
if (!this._fabricLoaded) return;
|
|
207
|
+
if (this._initialized || this.canvas) this.dispose();
|
|
208
|
+
this._disposed = false;
|
|
209
|
+
this._initialized = true;
|
|
210
|
+
this.animationQueue = new AnimationQueue();
|
|
211
|
+
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
212
|
+
this._visibilityStateByElement = /* @__PURE__ */ new WeakMap();
|
|
213
|
+
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
214
|
+
this._containerOriginalOverflow = null;
|
|
215
|
+
this._lastContainerViewportSize = null;
|
|
216
|
+
this._canvasElementOriginalStyle = null;
|
|
203
217
|
const defaults = {
|
|
204
218
|
canvas: "fabricCanvas",
|
|
205
219
|
canvasContainer: null,
|
|
@@ -227,6 +241,7 @@ var ImageEditor = class {
|
|
|
227
241
|
cancelCropBtn: "cancelCropBtn"
|
|
228
242
|
};
|
|
229
243
|
this.elements = { ...defaults, ...idMap };
|
|
244
|
+
this._elementCache = {};
|
|
230
245
|
this._initCanvas();
|
|
231
246
|
this._bindEvents();
|
|
232
247
|
this._updateInputs();
|
|
@@ -240,8 +255,7 @@ var ImageEditor = class {
|
|
|
240
255
|
}
|
|
241
256
|
_reportError(message, error = null) {
|
|
242
257
|
const handler = this.options && this.options.onError;
|
|
243
|
-
if (typeof handler !== "function")
|
|
244
|
-
return;
|
|
258
|
+
if (typeof handler !== "function") return;
|
|
245
259
|
try {
|
|
246
260
|
handler(error, message);
|
|
247
261
|
} catch {
|
|
@@ -249,8 +263,7 @@ var ImageEditor = class {
|
|
|
249
263
|
}
|
|
250
264
|
_reportWarning(message, error = null) {
|
|
251
265
|
const handler = this.options && this.options.onWarning;
|
|
252
|
-
if (typeof handler !== "function")
|
|
253
|
-
return;
|
|
266
|
+
if (typeof handler !== "function") return;
|
|
254
267
|
try {
|
|
255
268
|
handler(error, message);
|
|
256
269
|
} catch {
|
|
@@ -263,17 +276,22 @@ var ImageEditor = class {
|
|
|
263
276
|
* @private
|
|
264
277
|
*/
|
|
265
278
|
_initCanvas() {
|
|
266
|
-
const canvasElement =
|
|
267
|
-
if (!canvasElement)
|
|
268
|
-
throw new Error("Canvas is not found: " + this.elements.canvas);
|
|
279
|
+
const canvasElement = this._getElement("canvas");
|
|
280
|
+
if (!canvasElement) throw new Error("Canvas is not found: " + this.elements.canvas);
|
|
269
281
|
this.canvasElement = canvasElement;
|
|
282
|
+
this._canvasElementOriginalStyle = {
|
|
283
|
+
display: canvasElement.style.display || "",
|
|
284
|
+
width: canvasElement.style.width || "",
|
|
285
|
+
height: canvasElement.style.height || "",
|
|
286
|
+
maxWidth: canvasElement.style.maxWidth || ""
|
|
287
|
+
};
|
|
270
288
|
if (this.elements.canvasContainer) {
|
|
271
|
-
const containerElement =
|
|
289
|
+
const containerElement = this._getElement("canvasContainer");
|
|
272
290
|
this.containerElement = containerElement || canvasElement.parentElement;
|
|
273
291
|
} else {
|
|
274
292
|
this.containerElement = canvasElement.parentElement;
|
|
275
293
|
}
|
|
276
|
-
this.placeholderElement =
|
|
294
|
+
this.placeholderElement = this._getElement("imgPlaceholder") || null;
|
|
277
295
|
let initialWidth = this.options.canvasWidth;
|
|
278
296
|
let initialHeight = this.options.canvasHeight;
|
|
279
297
|
if (this.containerElement) {
|
|
@@ -282,6 +300,10 @@ var ImageEditor = class {
|
|
|
282
300
|
if (containerWidth > 0 && containerHeight > 0) {
|
|
283
301
|
initialWidth = containerWidth;
|
|
284
302
|
initialHeight = containerHeight;
|
|
303
|
+
this._lastContainerViewportSize = {
|
|
304
|
+
width: containerWidth,
|
|
305
|
+
height: containerHeight
|
|
306
|
+
};
|
|
285
307
|
}
|
|
286
308
|
}
|
|
287
309
|
this.canvas = new fabric.Canvas(canvasElement, {
|
|
@@ -295,20 +317,34 @@ var ImageEditor = class {
|
|
|
295
317
|
this.canvas.on("selection:updated", (event) => this._handleSelectionChanged(event.selected));
|
|
296
318
|
this.canvas.on("selection:cleared", () => this._handleSelectionChanged([]));
|
|
297
319
|
this.canvas.on("object:moving", (event) => {
|
|
298
|
-
if (event.target && event.target.maskId)
|
|
299
|
-
this._syncMaskLabel(event.target);
|
|
320
|
+
if (event.target && event.target.maskId) this._syncMaskLabel(event.target);
|
|
300
321
|
});
|
|
301
322
|
this.canvas.on("object:scaling", (event) => {
|
|
302
|
-
if (event.target && event.target.maskId)
|
|
303
|
-
this._syncMaskLabel(event.target);
|
|
323
|
+
if (event.target && event.target.maskId) this._syncMaskLabel(event.target);
|
|
304
324
|
});
|
|
305
325
|
this.canvas.on("object:rotating", (event) => {
|
|
306
|
-
if (event.target && event.target.maskId)
|
|
307
|
-
this._syncMaskLabel(event.target);
|
|
326
|
+
if (event.target && event.target.maskId) this._syncMaskLabel(event.target);
|
|
308
327
|
});
|
|
309
328
|
this.canvas.on("object:modified", (event) => this._handleObjectModified(event.target));
|
|
310
329
|
this.canvasElement.style.display = "block";
|
|
311
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Returns a configured DOM element and caches lookups for hot UI paths.
|
|
333
|
+
*
|
|
334
|
+
* @param {string} key - Key in the configured element map.
|
|
335
|
+
* @returns {HTMLElement|null} The configured element, or null when missing.
|
|
336
|
+
* @private
|
|
337
|
+
*/
|
|
338
|
+
_getElement(key) {
|
|
339
|
+
const id = this.elements && this.elements[key];
|
|
340
|
+
if (!id) return null;
|
|
341
|
+
if (this._elementCache && Object.prototype.hasOwnProperty.call(this._elementCache, key)) {
|
|
342
|
+
return this._elementCache[key];
|
|
343
|
+
}
|
|
344
|
+
const element = document.getElementById(id);
|
|
345
|
+
if (this._elementCache) this._elementCache[key] = element || null;
|
|
346
|
+
return element || null;
|
|
347
|
+
}
|
|
312
348
|
/**
|
|
313
349
|
* Records a history entry after Fabric finishes modifying one or more masks.
|
|
314
350
|
*
|
|
@@ -318,11 +354,9 @@ var ImageEditor = class {
|
|
|
318
354
|
*/
|
|
319
355
|
_handleObjectModified(target) {
|
|
320
356
|
const masks = this._getModifiedMasks(target);
|
|
321
|
-
if (!masks.length)
|
|
322
|
-
return;
|
|
357
|
+
if (!masks.length) return;
|
|
323
358
|
masks.forEach((mask) => {
|
|
324
|
-
if (typeof mask.setCoords === "function")
|
|
325
|
-
mask.setCoords();
|
|
359
|
+
if (typeof mask.setCoords === "function") mask.setCoords();
|
|
326
360
|
this._syncMaskLabel(mask);
|
|
327
361
|
});
|
|
328
362
|
this._expandCanvasToFitObjects(masks);
|
|
@@ -336,10 +370,8 @@ var ImageEditor = class {
|
|
|
336
370
|
* @private
|
|
337
371
|
*/
|
|
338
372
|
_getModifiedMasks(target) {
|
|
339
|
-
if (!target)
|
|
340
|
-
|
|
341
|
-
if (target.maskId)
|
|
342
|
-
return [target];
|
|
373
|
+
if (!target) return [];
|
|
374
|
+
if (target.maskId) return [target];
|
|
343
375
|
const objects = typeof target.getObjects === "function" ? target.getObjects() : [];
|
|
344
376
|
return Array.isArray(objects) ? objects.filter((object) => object && object.maskId) : [];
|
|
345
377
|
}
|
|
@@ -352,11 +384,8 @@ var ImageEditor = class {
|
|
|
352
384
|
* @private
|
|
353
385
|
*/
|
|
354
386
|
_syncContainerOverflow(options = {}) {
|
|
355
|
-
if (!this.containerElement || !this.containerElement.style)
|
|
356
|
-
|
|
357
|
-
if (this._containerOriginalOverflow === void 0) {
|
|
358
|
-
this._containerOriginalOverflow = this.containerElement.style.overflow || "";
|
|
359
|
-
}
|
|
387
|
+
if (!this.containerElement || !this.containerElement.style) return;
|
|
388
|
+
this._captureContainerOverflowState();
|
|
360
389
|
const shouldPreserveScroll = options.preserveScroll === true;
|
|
361
390
|
if (this.options.coverImageToCanvas) {
|
|
362
391
|
this.containerElement.style.overflow = "scroll";
|
|
@@ -371,62 +400,77 @@ var ImageEditor = class {
|
|
|
371
400
|
this.containerElement.scrollTop = 0;
|
|
372
401
|
}
|
|
373
402
|
} else {
|
|
374
|
-
this.
|
|
403
|
+
this._restoreContainerOverflowState();
|
|
375
404
|
}
|
|
376
405
|
}
|
|
406
|
+
_captureContainerOverflowState() {
|
|
407
|
+
if (!this.containerElement || !this.containerElement.style || this._containerOriginalOverflow) return;
|
|
408
|
+
this._containerOriginalOverflow = {
|
|
409
|
+
overflow: this.containerElement.style.overflow || "",
|
|
410
|
+
overflowX: this.containerElement.style.overflowX || "",
|
|
411
|
+
overflowY: this.containerElement.style.overflowY || ""
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
_restoreContainerOverflowState() {
|
|
415
|
+
if (!this.containerElement || !this.containerElement.style || !this._containerOriginalOverflow) return;
|
|
416
|
+
this.containerElement.style.overflow = this._containerOriginalOverflow.overflow;
|
|
417
|
+
this.containerElement.style.overflowX = this._containerOriginalOverflow.overflowX;
|
|
418
|
+
this.containerElement.style.overflowY = this._containerOriginalOverflow.overflowY;
|
|
419
|
+
}
|
|
377
420
|
/**
|
|
378
421
|
* DOM / UI bindings
|
|
379
422
|
* @private
|
|
380
423
|
*/
|
|
381
424
|
_bindEvents() {
|
|
382
425
|
this._bindIfExists("uploadArea", "click", () => {
|
|
383
|
-
const uploadAreaElement =
|
|
384
|
-
if (this._isElementDisabled(uploadAreaElement))
|
|
385
|
-
|
|
386
|
-
document.getElementById(this.elements.imageInput)?.click();
|
|
426
|
+
const uploadAreaElement = this._getElement("uploadArea");
|
|
427
|
+
if (this._isElementDisabled(uploadAreaElement)) return;
|
|
428
|
+
this._getElement("imageInput")?.click();
|
|
387
429
|
});
|
|
388
430
|
this._bindIfExists("imageInput", "change", (event) => {
|
|
389
431
|
const file = event.target.files && event.target.files[0];
|
|
390
|
-
if (file)
|
|
391
|
-
this._loadImageFile(file)
|
|
432
|
+
if (file) {
|
|
433
|
+
this._loadImageFile(file).catch((error) => this._reportError("Image file could not be loaded", error)).finally(() => {
|
|
434
|
+
event.target.value = "";
|
|
435
|
+
});
|
|
436
|
+
}
|
|
392
437
|
});
|
|
393
|
-
this._bindIfExists("zoomInBtn", "click", () => this.scaleImage(this.currentScale + this.options.scaleStep));
|
|
394
|
-
this._bindIfExists("zoomOutBtn", "click", () => this.scaleImage(this.currentScale - this.options.scaleStep));
|
|
438
|
+
this._bindIfExists("zoomInBtn", "click", () => this.scaleImage(this.currentScale + this.options.scaleStep).catch((error) => this._reportError("scaleImage failed", error)));
|
|
439
|
+
this._bindIfExists("zoomOutBtn", "click", () => this.scaleImage(this.currentScale - this.options.scaleStep).catch((error) => this._reportError("scaleImage failed", error)));
|
|
395
440
|
this._bindIfExists("resetBtn", "click", () => {
|
|
396
|
-
this.resetImageTransform();
|
|
441
|
+
this.resetImageTransform().catch((error) => this._reportError("resetImageTransform failed", error));
|
|
397
442
|
});
|
|
398
443
|
this._bindIfExists("addMaskBtn", "click", () => this.createMask());
|
|
399
444
|
this._bindIfExists("removeMaskBtn", "click", () => this.removeSelectedMask());
|
|
400
445
|
this._bindIfExists("removeAllMasksBtn", "click", () => this.removeAllMasks());
|
|
401
|
-
this._bindIfExists("mergeBtn", "click", () => this.mergeMasks());
|
|
446
|
+
this._bindIfExists("mergeBtn", "click", () => this.mergeMasks().catch((error) => this._reportError("merge error", error)));
|
|
402
447
|
this._bindIfExists("downloadBtn", "click", () => this.downloadImage());
|
|
403
|
-
this._bindIfExists("undoBtn", "click", () => this.undo());
|
|
404
|
-
this._bindIfExists("redoBtn", "click", () => this.redo());
|
|
448
|
+
this._bindIfExists("undoBtn", "click", () => this.undo().catch((error) => this._reportError("undo failed", error)));
|
|
449
|
+
this._bindIfExists("redoBtn", "click", () => this.redo().catch((error) => this._reportError("redo failed", error)));
|
|
405
450
|
this._bindIfExists("rotateLeftBtn", "click", () => {
|
|
406
|
-
const rotationInputElement =
|
|
451
|
+
const rotationInputElement = this._getElement("rotationLeftInput");
|
|
407
452
|
let step = this.options.rotationStep;
|
|
408
453
|
if (rotationInputElement) {
|
|
409
454
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
410
|
-
if (!isNaN(parsedStep))
|
|
411
|
-
step = parsedStep;
|
|
455
|
+
if (!isNaN(parsedStep)) step = parsedStep;
|
|
412
456
|
}
|
|
413
|
-
this.rotateImage(this.currentRotation - step);
|
|
457
|
+
this.rotateImage(this.currentRotation - step).catch((error) => this._reportError("rotateImage failed", error));
|
|
414
458
|
});
|
|
415
459
|
this._bindIfExists("rotateRightBtn", "click", () => {
|
|
416
|
-
const rotationInputElement =
|
|
460
|
+
const rotationInputElement = this._getElement("rotationRightInput");
|
|
417
461
|
let step = this.options.rotationStep;
|
|
418
462
|
if (rotationInputElement) {
|
|
419
463
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
420
|
-
if (!isNaN(parsedStep))
|
|
421
|
-
step = parsedStep;
|
|
464
|
+
if (!isNaN(parsedStep)) step = parsedStep;
|
|
422
465
|
}
|
|
423
|
-
this.rotateImage(this.currentRotation + step);
|
|
466
|
+
this.rotateImage(this.currentRotation + step).catch((error) => this._reportError("rotateImage failed", error));
|
|
424
467
|
});
|
|
425
468
|
this._bindIfExists("cropBtn", "click", () => this.enterCropMode());
|
|
426
469
|
this._bindIfExists("applyCropBtn", "click", () => {
|
|
427
470
|
this.applyCrop().catch((error) => this._reportError("applyCrop failed", error));
|
|
428
471
|
});
|
|
429
472
|
this._bindIfExists("cancelCropBtn", "click", () => this.cancelCrop());
|
|
473
|
+
this._bindIfExists("maskList", "click", (event) => this._handleMaskListClick(event));
|
|
430
474
|
}
|
|
431
475
|
/**
|
|
432
476
|
* Binds a DOM event listener when the configured element exists and records it for disposal.
|
|
@@ -437,12 +481,11 @@ var ImageEditor = class {
|
|
|
437
481
|
* @private
|
|
438
482
|
*/
|
|
439
483
|
_bindIfExists(key, eventName, handler) {
|
|
440
|
-
const element =
|
|
484
|
+
const element = this._getElement(key);
|
|
441
485
|
if (element) {
|
|
442
486
|
element.addEventListener(eventName, handler);
|
|
443
487
|
this._handlersByElementKey = this._handlersByElementKey || {};
|
|
444
|
-
if (!this._handlersByElementKey[key])
|
|
445
|
-
this._handlersByElementKey[key] = [];
|
|
488
|
+
if (!this._handlersByElementKey[key]) this._handlersByElementKey[key] = [];
|
|
446
489
|
this._handlersByElementKey[key].push({ eventName, handler });
|
|
447
490
|
}
|
|
448
491
|
}
|
|
@@ -450,17 +493,33 @@ var ImageEditor = class {
|
|
|
450
493
|
* Reads an image File as a data URL and loads it into the Fabric canvas.
|
|
451
494
|
*
|
|
452
495
|
* @param {File} file - Image file selected by the user.
|
|
496
|
+
* @returns {Promise<void>} Resolves after the selected file is loaded.
|
|
453
497
|
* @private
|
|
454
498
|
*/
|
|
455
499
|
_loadImageFile(file) {
|
|
456
|
-
if (!
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
500
|
+
if (!this._isSupportedImageFile(file)) {
|
|
501
|
+
const error = new Error("Selected file is not a supported image");
|
|
502
|
+
this._reportError("Selected file is not a supported image", error);
|
|
503
|
+
return Promise.reject(error);
|
|
504
|
+
}
|
|
505
|
+
return new Promise((resolve, reject) => {
|
|
506
|
+
const reader = new FileReader();
|
|
507
|
+
reader.onload = (event) => {
|
|
508
|
+
this.loadImage(event.target.result).then(resolve).catch(reject);
|
|
509
|
+
};
|
|
510
|
+
reader.onerror = (event) => {
|
|
511
|
+
const error = new Error("Image file could not be read");
|
|
512
|
+
this._reportError("Image file could not be read", event);
|
|
513
|
+
reject(error);
|
|
514
|
+
};
|
|
515
|
+
reader.readAsDataURL(file);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
_isSupportedImageFile(file) {
|
|
519
|
+
if (!file) return false;
|
|
520
|
+
if (typeof file.type === "string" && file.type.startsWith("image/")) return true;
|
|
521
|
+
const fileName = String(file.name || "");
|
|
522
|
+
return /\.(avif|bmp|gif|jpe?g|png|webp)$/i.test(fileName);
|
|
464
523
|
}
|
|
465
524
|
/**
|
|
466
525
|
* Warns when more than one mutually exclusive image layout mode is enabled.
|
|
@@ -474,8 +533,7 @@ var ImageEditor = class {
|
|
|
474
533
|
["coverImageToCanvas", this.options.coverImageToCanvas],
|
|
475
534
|
["expandCanvasToImage", this.options.expandCanvasToImage]
|
|
476
535
|
].filter(([, isEnabled]) => !!isEnabled).map(([name]) => name);
|
|
477
|
-
if (activeModes.length <= 1)
|
|
478
|
-
return;
|
|
536
|
+
if (activeModes.length <= 1) return;
|
|
479
537
|
this._reportWarning(
|
|
480
538
|
`Only one image layout mode should be enabled. Active modes: ${activeModes.join(", ")}.`
|
|
481
539
|
);
|
|
@@ -490,103 +548,98 @@ var ImageEditor = class {
|
|
|
490
548
|
* @public
|
|
491
549
|
*/
|
|
492
550
|
async loadImage(imageBase64, options = {}) {
|
|
493
|
-
if (!this._fabricLoaded)
|
|
494
|
-
|
|
495
|
-
if (!
|
|
496
|
-
|
|
497
|
-
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/"))
|
|
498
|
-
return;
|
|
551
|
+
if (!this._fabricLoaded) return;
|
|
552
|
+
if (!this.canvas || this._disposed) return;
|
|
553
|
+
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/")) return;
|
|
554
|
+
this._assertIdleForOperation("loadImage");
|
|
499
555
|
this._warnOnImageLayoutOptionConflict();
|
|
500
|
-
this.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
556
|
+
const transaction = this._captureLoadImageTransaction();
|
|
557
|
+
try {
|
|
558
|
+
const imageElement = await this._createImageElement(imageBase64);
|
|
559
|
+
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
560
|
+
let loadSource = imageBase64;
|
|
561
|
+
if (this.options.downsampleOnLoad) {
|
|
562
|
+
const shouldResize = imageElement.naturalWidth > this.options.downsampleMaxWidth || imageElement.naturalHeight > this.options.downsampleMaxHeight;
|
|
563
|
+
if (shouldResize) {
|
|
564
|
+
const ratio = Math.min(
|
|
565
|
+
this.options.downsampleMaxWidth / imageElement.naturalWidth,
|
|
566
|
+
this.options.downsampleMaxHeight / imageElement.naturalHeight
|
|
567
|
+
);
|
|
568
|
+
const targetWidth = Math.round(imageElement.naturalWidth * ratio);
|
|
569
|
+
const targetHeight = Math.round(imageElement.naturalHeight * ratio);
|
|
570
|
+
loadSource = this._resampleImageToDataURL(
|
|
571
|
+
imageElement,
|
|
572
|
+
targetWidth,
|
|
573
|
+
targetHeight,
|
|
574
|
+
this.options.downsampleQuality,
|
|
575
|
+
imageBase64
|
|
576
|
+
);
|
|
577
|
+
}
|
|
514
578
|
}
|
|
579
|
+
const fabricImage = await this._createFabricImageFromURL(loadSource);
|
|
580
|
+
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
581
|
+
this.canvas.discardActiveObject();
|
|
582
|
+
this._hideAllMaskLabels();
|
|
583
|
+
this.canvas.clear();
|
|
584
|
+
this.canvas.setBackgroundColor(this.options.backgroundColor, this.canvas.renderAll.bind(this.canvas));
|
|
585
|
+
fabricImage.set({ originX: "left", originY: "top", selectable: false, evented: false });
|
|
586
|
+
this._setPlaceholderVisible(false);
|
|
587
|
+
this._syncContainerOverflow({ preserveScroll: options.preserveScroll === true });
|
|
588
|
+
const imageWidth = fabricImage.width;
|
|
589
|
+
const imageHeight = fabricImage.height;
|
|
590
|
+
const viewport = this._getContainerViewportSize();
|
|
591
|
+
const minWidth = viewport.width;
|
|
592
|
+
const minHeight = viewport.height;
|
|
593
|
+
if (this.options.fitImageToCanvas) {
|
|
594
|
+
const canvasWidth = Math.max(1, minWidth - 1);
|
|
595
|
+
const canvasHeight = Math.max(1, minHeight - 1);
|
|
596
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
597
|
+
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
598
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
599
|
+
fabricImage.scale(fitScale);
|
|
600
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
601
|
+
} else if (this.options.coverImageToCanvas) {
|
|
602
|
+
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
603
|
+
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
604
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
605
|
+
fabricImage.scale(layout.scale);
|
|
606
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
607
|
+
} else if (this.options.expandCanvasToImage) {
|
|
608
|
+
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
609
|
+
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
610
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
611
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
612
|
+
fabricImage.scale(1);
|
|
613
|
+
this.baseImageScale = 1;
|
|
614
|
+
} else {
|
|
615
|
+
const canvasWidth = Math.max(this.options.canvasWidth, minWidth);
|
|
616
|
+
const canvasHeight = Math.max(this.options.canvasHeight, minHeight);
|
|
617
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
618
|
+
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
619
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
620
|
+
fabricImage.scale(fitScale);
|
|
621
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
622
|
+
}
|
|
623
|
+
this.originalImage = fabricImage;
|
|
624
|
+
this.canvas.add(fabricImage);
|
|
625
|
+
this.canvas.sendToBack(fabricImage);
|
|
626
|
+
this._clearMaskPlacementMemory();
|
|
627
|
+
if (options.resetMaskCounter !== false) this.maskCounter = 0;
|
|
628
|
+
this.currentScale = 1;
|
|
629
|
+
this.currentRotation = 0;
|
|
630
|
+
this._updateInputs();
|
|
631
|
+
this._updateMaskList();
|
|
632
|
+
this.isImageLoadedToCanvas = true;
|
|
633
|
+
this._updateUI();
|
|
634
|
+
this.canvas.renderAll();
|
|
635
|
+
this._lastSnapshot = this._captureCanvasStateOrThrow("loadImage");
|
|
636
|
+
if (typeof this.onImageLoaded === "function") {
|
|
637
|
+
this.onImageLoaded();
|
|
638
|
+
}
|
|
639
|
+
} catch (error) {
|
|
640
|
+
await this._rollbackLoadImageTransaction(transaction);
|
|
641
|
+
throw error;
|
|
515
642
|
}
|
|
516
|
-
return new Promise((resolve, reject) => {
|
|
517
|
-
fabric.Image.fromURL(loadSource, (fabricImage) => {
|
|
518
|
-
try {
|
|
519
|
-
if (!fabricImage)
|
|
520
|
-
throw new Error("Image could not be loaded");
|
|
521
|
-
this.canvas.discardActiveObject();
|
|
522
|
-
this._hideAllMaskLabels();
|
|
523
|
-
this.canvas.clear();
|
|
524
|
-
this.canvas.setBackgroundColor(this.options.backgroundColor, this.canvas.renderAll.bind(this.canvas));
|
|
525
|
-
fabricImage.set({ originX: "left", originY: "top", selectable: false, evented: false });
|
|
526
|
-
const imageWidth = fabricImage.width;
|
|
527
|
-
const imageHeight = fabricImage.height;
|
|
528
|
-
const viewport = this._getContainerViewportSize();
|
|
529
|
-
const minWidth = viewport.width;
|
|
530
|
-
const minHeight = viewport.height;
|
|
531
|
-
if (this.options.fitImageToCanvas) {
|
|
532
|
-
const canvasWidth = Math.max(1, minWidth - 1);
|
|
533
|
-
const canvasHeight = Math.max(1, minHeight - 1);
|
|
534
|
-
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
535
|
-
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
536
|
-
fabricImage.set({ left: 0, top: 0 });
|
|
537
|
-
fabricImage.scale(fitScale);
|
|
538
|
-
this.baseImageScale = fabricImage.scaleX || 1;
|
|
539
|
-
} else if (this.options.coverImageToCanvas) {
|
|
540
|
-
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
541
|
-
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
542
|
-
fabricImage.set({ left: 0, top: 0 });
|
|
543
|
-
fabricImage.scale(layout.scale);
|
|
544
|
-
this.baseImageScale = fabricImage.scaleX || 1;
|
|
545
|
-
} else if (this.options.expandCanvasToImage) {
|
|
546
|
-
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
547
|
-
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
548
|
-
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
549
|
-
fabricImage.set({ left: 0, top: 0 });
|
|
550
|
-
fabricImage.scale(1);
|
|
551
|
-
this.baseImageScale = 1;
|
|
552
|
-
} else {
|
|
553
|
-
const canvasWidth = Math.max(this.options.canvasWidth, minWidth);
|
|
554
|
-
const canvasHeight = Math.max(this.options.canvasHeight, minHeight);
|
|
555
|
-
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
556
|
-
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
557
|
-
fabricImage.set({ left: 0, top: 0 });
|
|
558
|
-
fabricImage.scale(fitScale);
|
|
559
|
-
this.baseImageScale = fabricImage.scaleX || 1;
|
|
560
|
-
}
|
|
561
|
-
this.originalImage = fabricImage;
|
|
562
|
-
this.canvas.add(fabricImage);
|
|
563
|
-
this.canvas.sendToBack(fabricImage);
|
|
564
|
-
this._lastMask = null;
|
|
565
|
-
this._lastMaskInitialLeft = null;
|
|
566
|
-
this._lastMaskInitialTop = null;
|
|
567
|
-
this._lastMaskInitialWidth = null;
|
|
568
|
-
this.maskCounter = 0;
|
|
569
|
-
this.currentScale = 1;
|
|
570
|
-
this.currentRotation = 0;
|
|
571
|
-
this._updateInputs();
|
|
572
|
-
this._updateMaskList();
|
|
573
|
-
this.isImageLoadedToCanvas = true;
|
|
574
|
-
this._updateUI();
|
|
575
|
-
this.canvas.renderAll();
|
|
576
|
-
try {
|
|
577
|
-
this._lastSnapshot = this._serializeCanvasState();
|
|
578
|
-
} catch (error) {
|
|
579
|
-
this._reportWarning("loadImage: failed to capture initial canvas snapshot", error);
|
|
580
|
-
}
|
|
581
|
-
if (typeof this.onImageLoaded === "function") {
|
|
582
|
-
this.onImageLoaded();
|
|
583
|
-
}
|
|
584
|
-
resolve();
|
|
585
|
-
} catch (error) {
|
|
586
|
-
reject(error);
|
|
587
|
-
}
|
|
588
|
-
}, { crossOrigin: "anonymous" });
|
|
589
|
-
});
|
|
590
643
|
}
|
|
591
644
|
/**
|
|
592
645
|
* Checks whether there is a loaded image on the current canvas.
|
|
@@ -611,8 +664,7 @@ var ImageEditor = class {
|
|
|
611
664
|
const safeTimeoutMs = Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) > 0 ? Number(timeoutMs) : 3e4;
|
|
612
665
|
let timerId;
|
|
613
666
|
const settle = (callback) => {
|
|
614
|
-
if (isSettled)
|
|
615
|
-
return;
|
|
667
|
+
if (isSettled) return;
|
|
616
668
|
isSettled = true;
|
|
617
669
|
clearTimeout(timerId);
|
|
618
670
|
imageElement.onload = null;
|
|
@@ -624,6 +676,7 @@ var ImageEditor = class {
|
|
|
624
676
|
try {
|
|
625
677
|
imageElement.src = "";
|
|
626
678
|
} catch (error) {
|
|
679
|
+
void error;
|
|
627
680
|
}
|
|
628
681
|
}, safeTimeoutMs);
|
|
629
682
|
imageElement.onload = () => settle(() => resolve(imageElement));
|
|
@@ -631,25 +684,132 @@ var ImageEditor = class {
|
|
|
631
684
|
imageElement.src = dataUrl;
|
|
632
685
|
});
|
|
633
686
|
}
|
|
687
|
+
_createFabricImageFromURL(dataUrl, timeoutMs = this.options.imageLoadTimeoutMs) {
|
|
688
|
+
return new Promise((resolve, reject) => {
|
|
689
|
+
const safeTimeoutMs = this._getSafeTimeoutMs(timeoutMs);
|
|
690
|
+
let isSettled = false;
|
|
691
|
+
let timerId;
|
|
692
|
+
const settle = (callback) => {
|
|
693
|
+
if (isSettled) return;
|
|
694
|
+
isSettled = true;
|
|
695
|
+
clearTimeout(timerId);
|
|
696
|
+
callback();
|
|
697
|
+
};
|
|
698
|
+
timerId = setTimeout(() => {
|
|
699
|
+
settle(() => reject(new Error("Fabric image load timed out")));
|
|
700
|
+
}, safeTimeoutMs);
|
|
701
|
+
try {
|
|
702
|
+
fabric.Image.fromURL(dataUrl, (fabricImage) => {
|
|
703
|
+
settle(() => {
|
|
704
|
+
if (!fabricImage) {
|
|
705
|
+
reject(new Error("Image could not be loaded"));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
resolve(fabricImage);
|
|
709
|
+
});
|
|
710
|
+
}, { crossOrigin: "anonymous" });
|
|
711
|
+
} catch (error) {
|
|
712
|
+
settle(() => reject(error));
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
_getSafeTimeoutMs(timeoutMs) {
|
|
717
|
+
const safeTimeoutMs = Number(timeoutMs);
|
|
718
|
+
return Number.isFinite(safeTimeoutMs) && safeTimeoutMs > 0 ? safeTimeoutMs : 3e4;
|
|
719
|
+
}
|
|
720
|
+
_captureLoadImageTransaction() {
|
|
721
|
+
return {
|
|
722
|
+
canvasState: this._serializeCanvasState(),
|
|
723
|
+
originalImage: this.originalImage,
|
|
724
|
+
baseImageScale: this.baseImageScale,
|
|
725
|
+
currentScale: this.currentScale,
|
|
726
|
+
currentRotation: this.currentRotation,
|
|
727
|
+
maskCounter: this.maskCounter,
|
|
728
|
+
isImageLoadedToCanvas: this.isImageLoadedToCanvas,
|
|
729
|
+
lastSnapshot: this._lastSnapshot,
|
|
730
|
+
lastMask: this._lastMask,
|
|
731
|
+
lastMaskInitialLeft: this._lastMaskInitialLeft,
|
|
732
|
+
lastMaskInitialTop: this._lastMaskInitialTop,
|
|
733
|
+
lastMaskInitialWidth: this._lastMaskInitialWidth,
|
|
734
|
+
containerOverflow: this.containerElement && this.containerElement.style ? {
|
|
735
|
+
overflow: this.containerElement.style.overflow || "",
|
|
736
|
+
overflowX: this.containerElement.style.overflowX || "",
|
|
737
|
+
overflowY: this.containerElement.style.overflowY || ""
|
|
738
|
+
} : null,
|
|
739
|
+
scrollLeft: this.containerElement ? this.containerElement.scrollLeft : 0,
|
|
740
|
+
scrollTop: this.containerElement ? this.containerElement.scrollTop : 0,
|
|
741
|
+
placeholderVisibility: this._captureElementVisibility(this.placeholderElement),
|
|
742
|
+
canvasVisibility: this._captureElementVisibility(this._getCanvasVisibilityElement())
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
async _rollbackLoadImageTransaction(transaction) {
|
|
746
|
+
if (!transaction || !this.canvas || this._disposed) return;
|
|
747
|
+
try {
|
|
748
|
+
if (transaction.canvasState) await this.loadFromState(transaction.canvasState);
|
|
749
|
+
} catch (error) {
|
|
750
|
+
this._reportError("loadImage rollback failed", error);
|
|
751
|
+
}
|
|
752
|
+
this.baseImageScale = transaction.baseImageScale;
|
|
753
|
+
this.currentScale = transaction.currentScale;
|
|
754
|
+
this.currentRotation = transaction.currentRotation;
|
|
755
|
+
this.maskCounter = transaction.maskCounter;
|
|
756
|
+
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
757
|
+
this._lastSnapshot = transaction.lastSnapshot;
|
|
758
|
+
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
759
|
+
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
760
|
+
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
761
|
+
this._containerOriginalOverflow = transaction.containerOverflow;
|
|
762
|
+
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
763
|
+
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
764
|
+
if (this.containerElement) {
|
|
765
|
+
this.containerElement.scrollLeft = transaction.scrollLeft;
|
|
766
|
+
this.containerElement.scrollTop = transaction.scrollTop;
|
|
767
|
+
this._restoreContainerOverflowState();
|
|
768
|
+
}
|
|
769
|
+
this._updateInputs();
|
|
770
|
+
this._updateMaskList();
|
|
771
|
+
this._updateUI();
|
|
772
|
+
if (this.canvas) this.canvas.renderAll();
|
|
773
|
+
}
|
|
634
774
|
/**
|
|
635
|
-
* Resamples the given image element to a new width and height and returns the result as a
|
|
775
|
+
* Resamples the given image element to a new width and height and returns the result as a data URL.
|
|
636
776
|
*
|
|
637
777
|
* @param {HTMLImageElement} imageElement - The image element to resample.
|
|
638
778
|
* @param {number} targetWidth - Target width (in pixels) for the resampled image.
|
|
639
779
|
* @param {number} targetHeight - Target height (in pixels) for the resampled image.
|
|
640
|
-
* @param {number} [quality=0.92] -
|
|
641
|
-
* @
|
|
780
|
+
* @param {number} [quality=0.92] - Image quality between 0 and 1 for lossy formats.
|
|
781
|
+
* @param {string|null} [sourceDataUrl=null] - Source data URL used to preserve alpha-capable formats.
|
|
782
|
+
* @returns {string} A data URL representing the resampled image.
|
|
642
783
|
* @private
|
|
643
784
|
*/
|
|
644
|
-
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92) {
|
|
785
|
+
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
|
|
645
786
|
const offscreenCanvas = document.createElement("canvas");
|
|
646
787
|
offscreenCanvas.width = targetWidth;
|
|
647
788
|
offscreenCanvas.height = targetHeight;
|
|
648
789
|
const context = offscreenCanvas.getContext("2d");
|
|
649
|
-
if (!context)
|
|
650
|
-
throw new Error("2D canvas context is unavailable");
|
|
790
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
651
791
|
context.drawImage(imageElement, 0, 0, imageElement.naturalWidth, imageElement.naturalHeight, 0, 0, targetWidth, targetHeight);
|
|
652
|
-
return offscreenCanvas.toDataURL(
|
|
792
|
+
return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
|
|
793
|
+
}
|
|
794
|
+
_getDataUrlMimeType(dataUrl) {
|
|
795
|
+
const match = String(dataUrl || "").match(/^data:([^;,]+)[;,]/i);
|
|
796
|
+
return match ? match[1].toLowerCase() : "";
|
|
797
|
+
}
|
|
798
|
+
_getDownsampleMimeType(sourceDataUrl) {
|
|
799
|
+
if (this.options.downsampleMimeType) {
|
|
800
|
+
const requestedFormat = this._normalizeImageFormat(this.options.downsampleMimeType);
|
|
801
|
+
return `image/${requestedFormat}`;
|
|
802
|
+
}
|
|
803
|
+
const sourceMimeType = this._getDataUrlMimeType(sourceDataUrl);
|
|
804
|
+
if (this.options.preserveSourceFormat !== false && (sourceMimeType === "image/png" || sourceMimeType === "image/webp")) {
|
|
805
|
+
return sourceMimeType;
|
|
806
|
+
}
|
|
807
|
+
return "image/jpeg";
|
|
808
|
+
}
|
|
809
|
+
_captureCanvasStateOrThrow(context) {
|
|
810
|
+
const snapshot = this._serializeCanvasState();
|
|
811
|
+
if (!snapshot) throw new Error(`${context}: canvas state is unavailable`);
|
|
812
|
+
return snapshot;
|
|
653
813
|
}
|
|
654
814
|
/**
|
|
655
815
|
* Sets canvas size to integer width and height values to prevent scrollbars due to sub-pixel rendering.
|
|
@@ -664,19 +824,16 @@ var ImageEditor = class {
|
|
|
664
824
|
const integerHeight = Math.max(1, Math.round(Number(height) || 1));
|
|
665
825
|
this.canvas.setWidth(integerWidth);
|
|
666
826
|
this.canvas.setHeight(integerHeight);
|
|
667
|
-
if (typeof this.canvas.calcOffset === "function")
|
|
668
|
-
this.canvas.calcOffset();
|
|
827
|
+
if (typeof this.canvas.calcOffset === "function") this.canvas.calcOffset();
|
|
669
828
|
if (this.canvasElement) {
|
|
670
829
|
this.canvasElement.style.width = integerWidth + "px";
|
|
671
830
|
this.canvasElement.style.height = integerHeight + "px";
|
|
672
|
-
this.canvasElement.style.maxWidth = "none";
|
|
673
831
|
}
|
|
674
832
|
}
|
|
675
833
|
_ceilCanvasDimension(value) {
|
|
676
834
|
const numericValue = Number(value) || 0;
|
|
677
835
|
const roundedValue = Math.round(numericValue);
|
|
678
|
-
if (Math.abs(numericValue - roundedValue) < 0.01)
|
|
679
|
-
return roundedValue;
|
|
836
|
+
if (Math.abs(numericValue - roundedValue) < 0.01) return roundedValue;
|
|
680
837
|
return Math.ceil(numericValue);
|
|
681
838
|
}
|
|
682
839
|
_getContainerViewportSize() {
|
|
@@ -686,19 +843,36 @@ var ImageEditor = class {
|
|
|
686
843
|
height: Math.max(1, Math.floor(this.options.canvasHeight || 1))
|
|
687
844
|
};
|
|
688
845
|
}
|
|
846
|
+
const measuredWidth = Math.floor(this.containerElement.clientWidth || 0);
|
|
847
|
+
const measuredHeight = Math.floor(this.containerElement.clientHeight || 0);
|
|
848
|
+
let width = Math.max(1, measuredWidth || this._lastContainerViewportSize?.width || this.options.canvasWidth || 1);
|
|
849
|
+
let height = Math.max(1, measuredHeight || this._lastContainerViewportSize?.height || this.options.canvasHeight || 1);
|
|
850
|
+
if (measuredWidth > 0 && measuredHeight > 0) {
|
|
851
|
+
this._lastContainerViewportSize = { width: measuredWidth, height: measuredHeight };
|
|
852
|
+
}
|
|
689
853
|
if (this._hasFixedContainerScrollbars()) {
|
|
690
|
-
return {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
854
|
+
return { width, height };
|
|
855
|
+
}
|
|
856
|
+
const overflow = this._getContainerOverflowValues();
|
|
857
|
+
const canScrollX = overflow.x.some((value) => value === "auto" || value === "scroll");
|
|
858
|
+
const canScrollY = overflow.y.some((value) => value === "auto" || value === "scroll");
|
|
859
|
+
const hasHorizontalScrollbar = canScrollX && this.containerElement.scrollWidth > this.containerElement.clientWidth;
|
|
860
|
+
const hasVerticalScrollbar = canScrollY && this.containerElement.scrollHeight > this.containerElement.clientHeight;
|
|
861
|
+
if (hasHorizontalScrollbar || hasVerticalScrollbar) {
|
|
862
|
+
const scrollbar = this._getScrollbarSize();
|
|
863
|
+
if (hasVerticalScrollbar) width += scrollbar.width;
|
|
864
|
+
if (hasHorizontalScrollbar) height += scrollbar.height;
|
|
694
865
|
}
|
|
695
|
-
const width = Math.max(1, Math.floor(this.containerElement.clientWidth || this.options.canvasWidth || 1));
|
|
696
|
-
const height = Math.max(1, Math.floor(this.containerElement.clientHeight || this.options.canvasHeight || 1));
|
|
697
866
|
return { width, height };
|
|
698
867
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
868
|
+
/**
|
|
869
|
+
* Reads inline and computed overflow values for both scroll axes.
|
|
870
|
+
*
|
|
871
|
+
* @returns {{x:string[], y:string[]}} Overflow values grouped by axis.
|
|
872
|
+
* @private
|
|
873
|
+
*/
|
|
874
|
+
_getContainerOverflowValues() {
|
|
875
|
+
if (!this.containerElement) return { x: [], y: [] };
|
|
702
876
|
const inlineOverflow = this.containerElement.style.overflow;
|
|
703
877
|
const inlineOverflowX = this.containerElement.style.overflowX;
|
|
704
878
|
const inlineOverflowY = this.containerElement.style.overflowY;
|
|
@@ -711,7 +885,15 @@ var ImageEditor = class {
|
|
|
711
885
|
computedOverflowX = style.overflowX;
|
|
712
886
|
computedOverflowY = style.overflowY;
|
|
713
887
|
}
|
|
714
|
-
return
|
|
888
|
+
return {
|
|
889
|
+
x: [inlineOverflow, inlineOverflowX, computedOverflow, computedOverflowX],
|
|
890
|
+
y: [inlineOverflow, inlineOverflowY, computedOverflow, computedOverflowY]
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
_hasFixedContainerScrollbars() {
|
|
894
|
+
if (!this.containerElement) return false;
|
|
895
|
+
const overflow = this._getContainerOverflowValues();
|
|
896
|
+
return [...overflow.x, ...overflow.y].some((value) => value === "scroll");
|
|
715
897
|
}
|
|
716
898
|
_getScrollbarSize() {
|
|
717
899
|
if (this._scrollbarSizeCache) {
|
|
@@ -754,15 +936,14 @@ var ImageEditor = class {
|
|
|
754
936
|
const scrollbar = this._getScrollbarSize();
|
|
755
937
|
let hasVertical = false;
|
|
756
938
|
let hasHorizontal = false;
|
|
757
|
-
let effectiveWidth
|
|
758
|
-
let effectiveHeight
|
|
939
|
+
let effectiveWidth;
|
|
940
|
+
let effectiveHeight;
|
|
759
941
|
for (let i = 0; i < 4; i += 1) {
|
|
760
942
|
effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
|
|
761
943
|
effectiveHeight = Math.max(1, viewport.height - (hasHorizontal ? scrollbar.height : 0));
|
|
762
944
|
const nextHasVertical = contentHeight > effectiveHeight + 0.5;
|
|
763
945
|
const nextHasHorizontal = contentWidth > effectiveWidth + 0.5;
|
|
764
|
-
if (nextHasVertical === hasVertical && nextHasHorizontal === hasHorizontal)
|
|
765
|
-
break;
|
|
946
|
+
if (nextHasVertical === hasVertical && nextHasHorizontal === hasHorizontal) break;
|
|
766
947
|
hasVertical = nextHasVertical;
|
|
767
948
|
hasHorizontal = nextHasHorizontal;
|
|
768
949
|
}
|
|
@@ -799,8 +980,8 @@ var ImageEditor = class {
|
|
|
799
980
|
let scale = 1;
|
|
800
981
|
let contentWidth = imageWidth;
|
|
801
982
|
let contentHeight = imageHeight;
|
|
802
|
-
let effectiveWidth
|
|
803
|
-
let effectiveHeight
|
|
983
|
+
let effectiveWidth;
|
|
984
|
+
let effectiveHeight;
|
|
804
985
|
for (let i = 0; i < 4; i += 1) {
|
|
805
986
|
effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
|
|
806
987
|
effectiveHeight = Math.max(1, viewport.height - (hasHorizontal ? scrollbar.height : 0));
|
|
@@ -809,8 +990,7 @@ var ImageEditor = class {
|
|
|
809
990
|
contentHeight = imageHeight * scale;
|
|
810
991
|
const nextHasVertical = contentHeight > effectiveHeight + 0.5;
|
|
811
992
|
const nextHasHorizontal = contentWidth > effectiveWidth + 0.5;
|
|
812
|
-
if (nextHasVertical === hasVertical && nextHasHorizontal === hasHorizontal)
|
|
813
|
-
break;
|
|
993
|
+
if (nextHasVertical === hasVertical && nextHasHorizontal === hasHorizontal) break;
|
|
814
994
|
hasVertical = nextHasVertical;
|
|
815
995
|
hasHorizontal = nextHasHorizontal;
|
|
816
996
|
}
|
|
@@ -849,41 +1029,48 @@ var ImageEditor = class {
|
|
|
849
1029
|
stroke: mask && mask.originalStroke || "#ccc",
|
|
850
1030
|
strokeWidth: Number.isFinite(strokeWidth) ? strokeWidth : 1
|
|
851
1031
|
};
|
|
852
|
-
if (Number.isFinite(opacity))
|
|
853
|
-
style.opacity = opacity;
|
|
1032
|
+
if (Number.isFinite(opacity)) style.opacity = opacity;
|
|
854
1033
|
return style;
|
|
855
1034
|
}
|
|
856
1035
|
_withNormalizedMaskStyles(callback) {
|
|
857
|
-
if (!this.canvas)
|
|
858
|
-
return callback();
|
|
1036
|
+
if (!this.canvas) return callback();
|
|
859
1037
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
860
|
-
const maskStyleBackups =
|
|
861
|
-
object: mask,
|
|
862
|
-
stroke: mask.stroke,
|
|
863
|
-
strokeWidth: mask.strokeWidth,
|
|
864
|
-
opacity: mask.opacity
|
|
865
|
-
}));
|
|
1038
|
+
const maskStyleBackups = [];
|
|
866
1039
|
try {
|
|
867
1040
|
masks.forEach((mask) => {
|
|
868
|
-
|
|
1041
|
+
const normalStyle = this._getMaskNormalStyle(mask);
|
|
1042
|
+
const stylePatch = {};
|
|
1043
|
+
Object.keys(normalStyle).forEach((property) => {
|
|
1044
|
+
if (mask[property] !== normalStyle[property]) {
|
|
1045
|
+
stylePatch[property] = normalStyle[property];
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
const changedProperties = Object.keys(stylePatch);
|
|
1049
|
+
if (!changedProperties.length) return;
|
|
1050
|
+
const backup = { object: mask };
|
|
1051
|
+
changedProperties.forEach((property) => {
|
|
1052
|
+
backup[property] = mask[property];
|
|
1053
|
+
});
|
|
1054
|
+
maskStyleBackups.push(backup);
|
|
1055
|
+
mask.set(stylePatch);
|
|
869
1056
|
});
|
|
870
1057
|
return callback();
|
|
871
1058
|
} finally {
|
|
872
1059
|
maskStyleBackups.forEach((backup) => {
|
|
873
1060
|
try {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
opacity: backup.opacity
|
|
1061
|
+
const restorePatch = {};
|
|
1062
|
+
Object.keys(backup).forEach((property) => {
|
|
1063
|
+
if (property !== "object") restorePatch[property] = backup[property];
|
|
878
1064
|
});
|
|
1065
|
+
backup.object.set(restorePatch);
|
|
879
1066
|
} catch (error) {
|
|
1067
|
+
void error;
|
|
880
1068
|
}
|
|
881
1069
|
});
|
|
882
1070
|
}
|
|
883
1071
|
}
|
|
884
1072
|
_restoreMaskControls(mask) {
|
|
885
|
-
if (!mask)
|
|
886
|
-
return;
|
|
1073
|
+
if (!mask) return;
|
|
887
1074
|
const cornerSize = Number(mask.cornerSize);
|
|
888
1075
|
mask.set({
|
|
889
1076
|
selectable: mask.selectable !== false,
|
|
@@ -896,8 +1083,7 @@ var ImageEditor = class {
|
|
|
896
1083
|
transparentCorners: mask.transparentCorners === true,
|
|
897
1084
|
strokeUniform: mask.strokeUniform !== false
|
|
898
1085
|
});
|
|
899
|
-
if (typeof mask.setCoords === "function")
|
|
900
|
-
mask.setCoords();
|
|
1086
|
+
if (typeof mask.setCoords === "function") mask.setCoords();
|
|
901
1087
|
}
|
|
902
1088
|
/**
|
|
903
1089
|
* Captures editor-owned runtime state that Fabric does not include in canvas JSON.
|
|
@@ -919,8 +1105,7 @@ var ImageEditor = class {
|
|
|
919
1105
|
};
|
|
920
1106
|
}
|
|
921
1107
|
_serializeCanvasState() {
|
|
922
|
-
if (!this.canvas)
|
|
923
|
-
return null;
|
|
1108
|
+
if (!this.canvas) return null;
|
|
924
1109
|
return this._withNormalizedMaskStyles(() => {
|
|
925
1110
|
const jsonObject = this.canvas.toJSON(this._getStateProperties());
|
|
926
1111
|
if (Array.isArray(jsonObject.objects)) {
|
|
@@ -939,8 +1124,7 @@ var ImageEditor = class {
|
|
|
939
1124
|
*/
|
|
940
1125
|
_normalizeQuality(quality) {
|
|
941
1126
|
const numericQuality = Number(quality);
|
|
942
|
-
if (!Number.isFinite(numericQuality))
|
|
943
|
-
return this.options.downsampleQuality ?? 0.92;
|
|
1127
|
+
if (!Number.isFinite(numericQuality)) return this.options.downsampleQuality ?? 0.92;
|
|
944
1128
|
return Math.max(0, Math.min(1, numericQuality));
|
|
945
1129
|
}
|
|
946
1130
|
/**
|
|
@@ -1013,8 +1197,7 @@ var ImageEditor = class {
|
|
|
1013
1197
|
const safeTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 3e4;
|
|
1014
1198
|
let timerId;
|
|
1015
1199
|
const settle = (callback) => {
|
|
1016
|
-
if (isSettled)
|
|
1017
|
-
return;
|
|
1200
|
+
if (isSettled) return;
|
|
1018
1201
|
isSettled = true;
|
|
1019
1202
|
clearTimeout(timerId);
|
|
1020
1203
|
imageElement.onload = null;
|
|
@@ -1026,6 +1209,7 @@ var ImageEditor = class {
|
|
|
1026
1209
|
try {
|
|
1027
1210
|
imageElement.src = "";
|
|
1028
1211
|
} catch (error) {
|
|
1212
|
+
void error;
|
|
1029
1213
|
}
|
|
1030
1214
|
}, safeTimeoutMs);
|
|
1031
1215
|
imageElement.onload = () => {
|
|
@@ -1039,8 +1223,7 @@ var ImageEditor = class {
|
|
|
1039
1223
|
offscreenCanvas.width = scaledSourceWidth;
|
|
1040
1224
|
offscreenCanvas.height = scaledSourceHeight;
|
|
1041
1225
|
const context = offscreenCanvas.getContext("2d");
|
|
1042
|
-
if (!context)
|
|
1043
|
-
throw new Error("2D canvas context is unavailable");
|
|
1226
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
1044
1227
|
context.drawImage(imageElement, scaledSourceX, scaledSourceY, scaledSourceWidth, scaledSourceHeight, 0, 0, scaledSourceWidth, scaledSourceHeight);
|
|
1045
1228
|
settle(() => resolve(offscreenCanvas.toDataURL(`image/${format}`, quality)));
|
|
1046
1229
|
} catch (error) {
|
|
@@ -1052,7 +1235,7 @@ var ImageEditor = class {
|
|
|
1052
1235
|
});
|
|
1053
1236
|
}
|
|
1054
1237
|
/**
|
|
1055
|
-
* Exports
|
|
1238
|
+
* Exports a source region directly through Fabric's region export options.
|
|
1056
1239
|
*
|
|
1057
1240
|
* @param {Object} region - Canvas source region and export options.
|
|
1058
1241
|
* @param {number} region.sourceX - Source region x coordinate.
|
|
@@ -1065,14 +1248,17 @@ var ImageEditor = class {
|
|
|
1065
1248
|
* @returns {Promise<string>} Resolves with an image data URL for the cropped region.
|
|
1066
1249
|
* @private
|
|
1067
1250
|
*/
|
|
1068
|
-
|
|
1251
|
+
_exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg" }) {
|
|
1069
1252
|
const safeMultiplier = Math.max(1, Number(multiplier) || 1);
|
|
1070
|
-
|
|
1253
|
+
return this.canvas.toDataURL({
|
|
1071
1254
|
format,
|
|
1072
1255
|
quality,
|
|
1073
|
-
multiplier: safeMultiplier
|
|
1256
|
+
multiplier: safeMultiplier,
|
|
1257
|
+
left: sourceX,
|
|
1258
|
+
top: sourceY,
|
|
1259
|
+
width: sourceWidth,
|
|
1260
|
+
height: sourceHeight
|
|
1074
1261
|
});
|
|
1075
|
-
return this._cropDataUrl(fullDataUrl, sourceX, sourceY, sourceWidth, sourceHeight, safeMultiplier, format, quality);
|
|
1076
1262
|
}
|
|
1077
1263
|
/**
|
|
1078
1264
|
* Gets the top-left corner coordinates of the given object.
|
|
@@ -1083,15 +1269,39 @@ var ImageEditor = class {
|
|
|
1083
1269
|
* @private
|
|
1084
1270
|
*/
|
|
1085
1271
|
_getObjectTopLeftPoint(fabricObject) {
|
|
1086
|
-
if (!fabricObject)
|
|
1087
|
-
return { x: 0, y: 0 };
|
|
1272
|
+
if (!fabricObject) return { x: 0, y: 0 };
|
|
1088
1273
|
fabricObject.setCoords();
|
|
1089
|
-
const coords = typeof fabricObject.getCoords === "function" ? fabricObject.getCoords() : null;
|
|
1090
|
-
if (coords && coords.length)
|
|
1091
|
-
return coords[0];
|
|
1092
1274
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1093
1275
|
return { x: boundingRect.left, y: boundingRect.top };
|
|
1094
1276
|
}
|
|
1277
|
+
_getObjectCoordinateTopLeftPoint(fabricObject) {
|
|
1278
|
+
if (!fabricObject) return { x: 0, y: 0 };
|
|
1279
|
+
fabricObject.setCoords();
|
|
1280
|
+
const coords = typeof fabricObject.getCoords === "function" ? fabricObject.getCoords() : null;
|
|
1281
|
+
if (coords && coords.length) return coords[0];
|
|
1282
|
+
return this._getObjectTopLeftPoint(fabricObject);
|
|
1283
|
+
}
|
|
1284
|
+
_getObjectOriginPoint(fabricObject, originX, originY) {
|
|
1285
|
+
if (!fabricObject) return { x: 0, y: 0 };
|
|
1286
|
+
if (typeof fabricObject.getPointByOrigin === "function") {
|
|
1287
|
+
return fabricObject.getPointByOrigin(originX, originY);
|
|
1288
|
+
}
|
|
1289
|
+
return this._getObjectTopLeftPoint(fabricObject);
|
|
1290
|
+
}
|
|
1291
|
+
_translateObjectByCanvasOffset(fabricObject, deltaX, deltaY) {
|
|
1292
|
+
if (!fabricObject) return;
|
|
1293
|
+
if (typeof fabricObject.getCenterPoint === "function" && typeof fabricObject.setPositionByOrigin === "function") {
|
|
1294
|
+
const center = fabricObject.getCenterPoint();
|
|
1295
|
+
const nextCenter = new fabric.Point(center.x + deltaX, center.y + deltaY);
|
|
1296
|
+
fabricObject.setPositionByOrigin(nextCenter, "center", "center");
|
|
1297
|
+
} else {
|
|
1298
|
+
fabricObject.set({
|
|
1299
|
+
left: (fabricObject.left || 0) + deltaX,
|
|
1300
|
+
top: (fabricObject.top || 0) + deltaY
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
fabricObject.setCoords();
|
|
1304
|
+
}
|
|
1095
1305
|
/**
|
|
1096
1306
|
* Sets the object's origin at the specified origin point, keeping a reference point fixed in position.
|
|
1097
1307
|
*
|
|
@@ -1102,8 +1312,7 @@ var ImageEditor = class {
|
|
|
1102
1312
|
* @private
|
|
1103
1313
|
*/
|
|
1104
1314
|
_setObjectOriginKeepingPosition(fabricObject, originX, originY, refPoint) {
|
|
1105
|
-
if (!fabricObject || !refPoint || !fabricObject.setPositionByOrigin)
|
|
1106
|
-
return;
|
|
1315
|
+
if (!fabricObject || !refPoint || !fabricObject.setPositionByOrigin) return;
|
|
1107
1316
|
fabricObject.set({ originX, originY });
|
|
1108
1317
|
fabricObject.setPositionByOrigin(refPoint, originX, originY);
|
|
1109
1318
|
fabricObject.setCoords();
|
|
@@ -1115,8 +1324,7 @@ var ImageEditor = class {
|
|
|
1115
1324
|
* @private
|
|
1116
1325
|
*/
|
|
1117
1326
|
_alignObjectBoundingBoxToCanvasTopLeft(fabricObject) {
|
|
1118
|
-
if (!fabricObject)
|
|
1119
|
-
return;
|
|
1327
|
+
if (!fabricObject) return;
|
|
1120
1328
|
fabricObject.setCoords();
|
|
1121
1329
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1122
1330
|
const deltaX = boundingRect.left;
|
|
@@ -1131,8 +1339,7 @@ var ImageEditor = class {
|
|
|
1131
1339
|
* @private
|
|
1132
1340
|
*/
|
|
1133
1341
|
_updateCanvasSizeToImageBounds() {
|
|
1134
|
-
if (!this.originalImage)
|
|
1135
|
-
return;
|
|
1342
|
+
if (!this.originalImage) return;
|
|
1136
1343
|
this.originalImage.setCoords();
|
|
1137
1344
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
1138
1345
|
const size = this._getScrollableCanvasSize(imageBounds.width, imageBounds.height);
|
|
@@ -1156,25 +1363,34 @@ var ImageEditor = class {
|
|
|
1156
1363
|
* @private
|
|
1157
1364
|
*/
|
|
1158
1365
|
_expandCanvasToFitObjects(fabricObjects, padding = 10) {
|
|
1159
|
-
if (!this.canvas || !Array.isArray(fabricObjects) || !fabricObjects.length || !this._shouldResizeCanvasToContentBounds())
|
|
1160
|
-
return;
|
|
1366
|
+
if (!this.canvas || !Array.isArray(fabricObjects) || !fabricObjects.length || !this._shouldResizeCanvasToContentBounds()) return;
|
|
1161
1367
|
try {
|
|
1162
|
-
|
|
1163
|
-
|
|
1368
|
+
const currentWidth = this.canvas.getWidth();
|
|
1369
|
+
const currentHeight = this.canvas.getHeight();
|
|
1370
|
+
let requiredWidth = currentWidth;
|
|
1371
|
+
let requiredHeight = currentHeight;
|
|
1164
1372
|
fabricObjects.forEach((fabricObject) => {
|
|
1165
|
-
if (!fabricObject)
|
|
1166
|
-
|
|
1167
|
-
if (typeof fabricObject.setCoords === "function")
|
|
1168
|
-
fabricObject.setCoords();
|
|
1373
|
+
if (!fabricObject) return;
|
|
1374
|
+
if (typeof fabricObject.setCoords === "function") fabricObject.setCoords();
|
|
1169
1375
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1170
1376
|
requiredWidth = Math.max(requiredWidth, Math.ceil(boundingRect.left + boundingRect.width + padding));
|
|
1171
1377
|
requiredHeight = Math.max(requiredHeight, Math.ceil(boundingRect.top + boundingRect.height + padding));
|
|
1172
1378
|
});
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1379
|
+
const shouldUseScrollSafeViewport = this.options.fitImageToCanvas || this.options.coverImageToCanvas;
|
|
1380
|
+
let minWidth = 0;
|
|
1381
|
+
let minHeight = 0;
|
|
1382
|
+
if (shouldUseScrollSafeViewport) {
|
|
1383
|
+
const viewport = this._getContainerViewportSize();
|
|
1384
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
1385
|
+
minWidth = Math.max(1, viewport.width - safetyMargin);
|
|
1386
|
+
minHeight = Math.max(1, viewport.height - safetyMargin);
|
|
1387
|
+
} else if (this.containerElement) {
|
|
1388
|
+
minWidth = Math.floor(this.containerElement.clientWidth || 0);
|
|
1389
|
+
minHeight = Math.floor(this.containerElement.clientHeight || 0);
|
|
1390
|
+
}
|
|
1391
|
+
const newWidth = Math.max(currentWidth, minWidth, requiredWidth);
|
|
1392
|
+
const newHeight = Math.max(currentHeight, minHeight, requiredHeight);
|
|
1393
|
+
if (newWidth !== currentWidth || newHeight !== currentHeight) {
|
|
1178
1394
|
this._setCanvasSizeInt(newWidth, newHeight);
|
|
1179
1395
|
}
|
|
1180
1396
|
} catch (error) {
|
|
@@ -1202,6 +1418,66 @@ var ImageEditor = class {
|
|
|
1202
1418
|
scaleImage(factor, options = {}) {
|
|
1203
1419
|
return this.animationQueue.add(() => this._scaleImageImpl(factor, options));
|
|
1204
1420
|
}
|
|
1421
|
+
_assertIdleForOperation(operationName) {
|
|
1422
|
+
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1423
|
+
if (this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) {
|
|
1424
|
+
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
_canMutateNow(operationName) {
|
|
1428
|
+
try {
|
|
1429
|
+
this._assertIdleForOperation(operationName);
|
|
1430
|
+
return true;
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
this._reportError(`${operationName} blocked`, error);
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
_rejectActiveAnimations(reason) {
|
|
1437
|
+
const error = reason instanceof Error ? reason : new Error(String(reason || "Animation cancelled"));
|
|
1438
|
+
this._activeAnimationRejectors.forEach((reject) => {
|
|
1439
|
+
try {
|
|
1440
|
+
reject(error);
|
|
1441
|
+
} catch (rejectError) {
|
|
1442
|
+
void rejectError;
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
this._activeAnimationRejectors.clear();
|
|
1446
|
+
}
|
|
1447
|
+
_animateFabricProperty(fabricObject, property, value) {
|
|
1448
|
+
return new Promise((resolve, reject) => {
|
|
1449
|
+
if (this._disposed || !this.canvas || !fabricObject) {
|
|
1450
|
+
reject(new Error("Animation cannot start after editor disposal"));
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
let isSettled = false;
|
|
1454
|
+
const duration = Math.max(0, Number(this.options.animationDuration) || 0);
|
|
1455
|
+
const timeoutMs = Math.max(1e3, duration + 1e3);
|
|
1456
|
+
let timerId;
|
|
1457
|
+
const settle = (callback) => {
|
|
1458
|
+
if (isSettled) return;
|
|
1459
|
+
isSettled = true;
|
|
1460
|
+
clearTimeout(timerId);
|
|
1461
|
+
this._activeAnimationRejectors.delete(reject);
|
|
1462
|
+
callback();
|
|
1463
|
+
};
|
|
1464
|
+
this._activeAnimationRejectors.add(reject);
|
|
1465
|
+
timerId = setTimeout(() => {
|
|
1466
|
+
settle(() => reject(new Error(`Animation timed out while changing ${property}`)));
|
|
1467
|
+
}, timeoutMs);
|
|
1468
|
+
try {
|
|
1469
|
+
fabricObject.animate(property, value, {
|
|
1470
|
+
duration,
|
|
1471
|
+
onChange: () => {
|
|
1472
|
+
if (!this._disposed && this.canvas) this.canvas.renderAll();
|
|
1473
|
+
},
|
|
1474
|
+
onComplete: () => settle(resolve)
|
|
1475
|
+
});
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
settle(() => reject(error));
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1205
1481
|
/**
|
|
1206
1482
|
* Scales the original image by a given factor, with animation.
|
|
1207
1483
|
* Returns a promise that resolves when the scale animation is complete.
|
|
@@ -1209,34 +1485,25 @@ var ImageEditor = class {
|
|
|
1209
1485
|
* @returns {Promise<void>} Promise that resolves once the scaling animation finishes.
|
|
1210
1486
|
* @private
|
|
1211
1487
|
*/
|
|
1212
|
-
_scaleImageImpl(factor, options = {}) {
|
|
1213
|
-
if (!this.originalImage)
|
|
1214
|
-
|
|
1215
|
-
if (this.isAnimating)
|
|
1216
|
-
return Promise.resolve();
|
|
1488
|
+
async _scaleImageImpl(factor, options = {}) {
|
|
1489
|
+
if (!this.originalImage || this._disposed) return;
|
|
1490
|
+
if (this.isAnimating) return;
|
|
1217
1491
|
const saveHistory = options.saveHistory !== false;
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
this.originalImage
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
this.originalImage.animate("scaleY", targetScale, {
|
|
1234
|
-
duration: this.options.animationDuration,
|
|
1235
|
-
onChange: this.canvas.renderAll.bind(this.canvas),
|
|
1236
|
-
onComplete: resolve
|
|
1237
|
-
});
|
|
1238
|
-
});
|
|
1239
|
-
return Promise.all([scaleXAnimation, scaleYAnimation]).then(() => {
|
|
1492
|
+
let didStartAnimation = false;
|
|
1493
|
+
try {
|
|
1494
|
+
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, factor));
|
|
1495
|
+
this.currentScale = factor;
|
|
1496
|
+
this.isAnimating = true;
|
|
1497
|
+
didStartAnimation = true;
|
|
1498
|
+
this._updateUI();
|
|
1499
|
+
const targetScale = this.baseImageScale * factor;
|
|
1500
|
+
const topLeft = this._getObjectTopLeftPoint(this.originalImage);
|
|
1501
|
+
this._setObjectOriginKeepingPosition(this.originalImage, "left", "top", topLeft);
|
|
1502
|
+
await Promise.all([
|
|
1503
|
+
this._animateFabricProperty(this.originalImage, "scaleX", targetScale),
|
|
1504
|
+
this._animateFabricProperty(this.originalImage, "scaleY", targetScale)
|
|
1505
|
+
]);
|
|
1506
|
+
if (this._disposed || !this.canvas || !this.originalImage) throw new Error("Editor was disposed during scale animation");
|
|
1240
1507
|
this.originalImage.set({ scaleX: targetScale, scaleY: targetScale });
|
|
1241
1508
|
this.originalImage.setCoords();
|
|
1242
1509
|
if (this._shouldResizeCanvasToContentBounds()) {
|
|
@@ -1244,18 +1511,17 @@ var ImageEditor = class {
|
|
|
1244
1511
|
}
|
|
1245
1512
|
this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
|
|
1246
1513
|
this.canvas.getObjects().forEach((object) => {
|
|
1247
|
-
if (object.maskId)
|
|
1248
|
-
this._syncMaskLabel(object);
|
|
1514
|
+
if (object.maskId) this._syncMaskLabel(object);
|
|
1249
1515
|
});
|
|
1250
|
-
this.isAnimating = false;
|
|
1251
1516
|
this._updateInputs();
|
|
1252
|
-
this.
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1517
|
+
if (saveHistory) this.saveState();
|
|
1518
|
+
} finally {
|
|
1519
|
+
if (didStartAnimation) {
|
|
1520
|
+
this.isAnimating = false;
|
|
1521
|
+
this._updateInputs();
|
|
1522
|
+
this._updateUI();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1259
1525
|
}
|
|
1260
1526
|
/**
|
|
1261
1527
|
* Rotates the original image by a given number of degrees, with animation.
|
|
@@ -1274,48 +1540,50 @@ var ImageEditor = class {
|
|
|
1274
1540
|
* @returns {Promise<void>} Promise that resolves once the rotation animation finishes.
|
|
1275
1541
|
* @private
|
|
1276
1542
|
*/
|
|
1277
|
-
_rotateImageImpl(degrees, options = {}) {
|
|
1278
|
-
if (!this.originalImage)
|
|
1279
|
-
|
|
1280
|
-
if (
|
|
1281
|
-
return Promise.resolve();
|
|
1282
|
-
if (isNaN(degrees))
|
|
1283
|
-
return Promise.resolve();
|
|
1543
|
+
async _rotateImageImpl(degrees, options = {}) {
|
|
1544
|
+
if (!this.originalImage || this._disposed) return;
|
|
1545
|
+
if (this.isAnimating) return;
|
|
1546
|
+
if (isNaN(degrees)) return;
|
|
1284
1547
|
const saveHistory = options.saveHistory !== false;
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1548
|
+
const image = this.originalImage;
|
|
1549
|
+
const previousOriginX = image.originX || "left";
|
|
1550
|
+
const previousOriginY = image.originY || "top";
|
|
1551
|
+
const previousOriginPoint = this._getObjectOriginPoint(image, previousOriginX, previousOriginY);
|
|
1552
|
+
let didStartAnimation = false;
|
|
1553
|
+
let didCompleteRotation = false;
|
|
1554
|
+
try {
|
|
1555
|
+
this.currentRotation = degrees;
|
|
1556
|
+
this.isAnimating = true;
|
|
1557
|
+
didStartAnimation = true;
|
|
1558
|
+
this._updateUI();
|
|
1559
|
+
const center = image.getCenterPoint();
|
|
1560
|
+
this._setObjectOriginKeepingPosition(image, "center", "center", center);
|
|
1561
|
+
await this._animateFabricProperty(image, "angle", degrees);
|
|
1562
|
+
if (this._disposed || !this.canvas || !this.originalImage) throw new Error("Editor was disposed during rotation animation");
|
|
1298
1563
|
this.originalImage.set("angle", degrees);
|
|
1299
1564
|
this.originalImage.setCoords();
|
|
1300
1565
|
if (this._shouldResizeCanvasToContentBounds()) {
|
|
1301
1566
|
this._updateCanvasSizeToImageBounds();
|
|
1302
1567
|
}
|
|
1303
1568
|
this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
|
|
1304
|
-
const newTopLeft = this.
|
|
1569
|
+
const newTopLeft = this._getObjectCoordinateTopLeftPoint(this.originalImage);
|
|
1305
1570
|
this._setObjectOriginKeepingPosition(this.originalImage, "left", "top", newTopLeft);
|
|
1306
1571
|
this.canvas.getObjects().forEach((object) => {
|
|
1307
|
-
if (object.maskId)
|
|
1308
|
-
this._syncMaskLabel(object);
|
|
1572
|
+
if (object.maskId) this._syncMaskLabel(object);
|
|
1309
1573
|
});
|
|
1310
|
-
this.isAnimating = false;
|
|
1311
1574
|
this._updateInputs();
|
|
1312
|
-
this.
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1575
|
+
if (saveHistory) this.saveState();
|
|
1576
|
+
didCompleteRotation = true;
|
|
1577
|
+
} finally {
|
|
1578
|
+
if (!didCompleteRotation && !this._disposed && image) {
|
|
1579
|
+
this._setObjectOriginKeepingPosition(image, previousOriginX, previousOriginY, previousOriginPoint);
|
|
1580
|
+
}
|
|
1581
|
+
if (didStartAnimation) {
|
|
1582
|
+
this.isAnimating = false;
|
|
1583
|
+
this._updateInputs();
|
|
1584
|
+
this._updateUI();
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1319
1587
|
}
|
|
1320
1588
|
/**
|
|
1321
1589
|
* Resets the image transform: scales to 1 and rotates to 0 degrees.
|
|
@@ -1324,16 +1592,16 @@ var ImageEditor = class {
|
|
|
1324
1592
|
* @public
|
|
1325
1593
|
*/
|
|
1326
1594
|
resetImageTransform() {
|
|
1327
|
-
if (!this.originalImage)
|
|
1328
|
-
return Promise.resolve();
|
|
1595
|
+
if (!this.originalImage) return Promise.resolve();
|
|
1329
1596
|
return this.animationQueue.add(async () => {
|
|
1330
|
-
const before = this._lastSnapshot || this.
|
|
1597
|
+
const before = this._lastSnapshot || this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1331
1598
|
await this._scaleImageImpl(1, { saveHistory: false });
|
|
1332
1599
|
await this._rotateImageImpl(0, { saveHistory: false });
|
|
1333
|
-
const after = this.
|
|
1600
|
+
const after = this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1334
1601
|
this._pushStateTransition(before, after);
|
|
1335
1602
|
}).catch((error) => {
|
|
1336
1603
|
this._reportError("resetImageTransform() failed", error);
|
|
1604
|
+
throw error;
|
|
1337
1605
|
});
|
|
1338
1606
|
}
|
|
1339
1607
|
/**
|
|
@@ -1353,14 +1621,31 @@ var ImageEditor = class {
|
|
|
1353
1621
|
* @public
|
|
1354
1622
|
*/
|
|
1355
1623
|
loadFromState(serializedState) {
|
|
1356
|
-
if (!serializedState || !this.canvas)
|
|
1357
|
-
|
|
1358
|
-
|
|
1624
|
+
if (!serializedState || !this.canvas || this._disposed) return Promise.resolve();
|
|
1625
|
+
if (this._cropMode || this._cropRect) {
|
|
1626
|
+
this._removeCropRect();
|
|
1627
|
+
this._restoreCropObjectState();
|
|
1628
|
+
this._cropMode = false;
|
|
1629
|
+
if (this._prevSelectionSetting !== void 0 && this.canvas) {
|
|
1630
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
1631
|
+
}
|
|
1632
|
+
this._prevSelectionSetting = void 0;
|
|
1633
|
+
}
|
|
1634
|
+
return new Promise((resolve, reject) => {
|
|
1359
1635
|
try {
|
|
1360
1636
|
const state = typeof serializedState === "string" ? JSON.parse(serializedState) : serializedState;
|
|
1361
1637
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
1362
|
-
this.canvas.loadFromJSON(state, () => {
|
|
1638
|
+
this.canvas.loadFromJSON(state, async () => {
|
|
1363
1639
|
try {
|
|
1640
|
+
if (this._disposed || !this.canvas) {
|
|
1641
|
+
reject(new Error("Editor was disposed while loading state"));
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
await this._waitForFabricImagesReady(this.canvas.getObjects());
|
|
1645
|
+
if (this._disposed || !this.canvas) {
|
|
1646
|
+
reject(new Error("Editor was disposed while loading state"));
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1364
1649
|
this._hideAllMaskLabels();
|
|
1365
1650
|
const canvasObjects = this.canvas.getObjects();
|
|
1366
1651
|
this.originalImage = canvasObjects.find((object) => object.type === "image" && !object.maskId) || null;
|
|
@@ -1408,18 +1693,44 @@ var ImageEditor = class {
|
|
|
1408
1693
|
this._updatePlaceholderStatus();
|
|
1409
1694
|
this._lastSnapshot = this._serializeCanvasState();
|
|
1410
1695
|
this._updateUI();
|
|
1696
|
+
resolve();
|
|
1411
1697
|
} catch (callbackError) {
|
|
1412
1698
|
this._reportError("loadFromState() failed", callbackError);
|
|
1413
|
-
|
|
1414
|
-
resolve();
|
|
1699
|
+
reject(callbackError);
|
|
1415
1700
|
}
|
|
1416
1701
|
});
|
|
1417
1702
|
} catch (error) {
|
|
1418
1703
|
this._reportError("loadFromState() failed", error);
|
|
1419
|
-
|
|
1704
|
+
reject(error);
|
|
1420
1705
|
}
|
|
1421
1706
|
});
|
|
1422
1707
|
}
|
|
1708
|
+
async _waitForFabricImagesReady(canvasObjects) {
|
|
1709
|
+
const imageObjects = (canvasObjects || []).filter((object) => object && object.type === "image");
|
|
1710
|
+
await Promise.all(imageObjects.map((object) => this._waitForImageElementReady(
|
|
1711
|
+
typeof object.getElement === "function" ? object.getElement() : object._element
|
|
1712
|
+
)));
|
|
1713
|
+
}
|
|
1714
|
+
_waitForImageElementReady(imageElement) {
|
|
1715
|
+
if (!imageElement) return Promise.resolve();
|
|
1716
|
+
if (imageElement.complete || imageElement.naturalWidth > 0 || imageElement.width > 0) return Promise.resolve();
|
|
1717
|
+
return new Promise((resolve, reject) => {
|
|
1718
|
+
let isSettled = false;
|
|
1719
|
+
const timerId = setTimeout(() => {
|
|
1720
|
+
settle(() => reject(new Error("Image load timed out while restoring state")));
|
|
1721
|
+
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
1722
|
+
const settle = (callback) => {
|
|
1723
|
+
if (isSettled) return;
|
|
1724
|
+
isSettled = true;
|
|
1725
|
+
clearTimeout(timerId);
|
|
1726
|
+
imageElement.onload = null;
|
|
1727
|
+
imageElement.onerror = null;
|
|
1728
|
+
callback();
|
|
1729
|
+
};
|
|
1730
|
+
imageElement.onload = () => settle(resolve);
|
|
1731
|
+
imageElement.onerror = (error) => settle(() => reject(error));
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1423
1734
|
/**
|
|
1424
1735
|
* Saves the current editable canvas state as an undoable history transition.
|
|
1425
1736
|
*
|
|
@@ -1430,14 +1741,11 @@ var ImageEditor = class {
|
|
|
1430
1741
|
* @public
|
|
1431
1742
|
*/
|
|
1432
1743
|
saveState() {
|
|
1433
|
-
if (!this.canvas)
|
|
1434
|
-
return;
|
|
1435
|
-
const activeObject = this.canvas.getActiveObject();
|
|
1744
|
+
if (!this.canvas) return;
|
|
1436
1745
|
try {
|
|
1437
|
-
const after = this.
|
|
1746
|
+
const after = this._captureCanvasStateOrThrow("saveState");
|
|
1438
1747
|
const before = this._lastSnapshot || after;
|
|
1439
|
-
if (after === before)
|
|
1440
|
-
return;
|
|
1748
|
+
if (after === before) return;
|
|
1441
1749
|
let executedOnce = false;
|
|
1442
1750
|
const command = new Command(
|
|
1443
1751
|
() => {
|
|
@@ -1454,9 +1762,6 @@ var ImageEditor = class {
|
|
|
1454
1762
|
} catch (error) {
|
|
1455
1763
|
this._reportWarning("saveState: failed to save canvas snapshot", error);
|
|
1456
1764
|
} finally {
|
|
1457
|
-
if (activeObject && activeObject.maskId && !activeObject.__label && this.canvas.getObjects().includes(activeObject)) {
|
|
1458
|
-
this._handleSelectionChanged([activeObject]);
|
|
1459
|
-
}
|
|
1460
1765
|
this._updateUI();
|
|
1461
1766
|
}
|
|
1462
1767
|
}
|
|
@@ -1472,12 +1777,12 @@ var ImageEditor = class {
|
|
|
1472
1777
|
* @private
|
|
1473
1778
|
*/
|
|
1474
1779
|
_pushStateTransition(before, after) {
|
|
1475
|
-
if (!before || !after)
|
|
1476
|
-
|
|
1477
|
-
if (before === after)
|
|
1780
|
+
if (!before || !after) {
|
|
1781
|
+
this._reportWarning("History transition skipped because a canvas snapshot is unavailable");
|
|
1478
1782
|
return;
|
|
1479
|
-
|
|
1480
|
-
|
|
1783
|
+
}
|
|
1784
|
+
if (before === after) return;
|
|
1785
|
+
if (!this.historyManager) this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
1481
1786
|
const command = new Command(
|
|
1482
1787
|
() => this.loadFromState(after),
|
|
1483
1788
|
() => this.loadFromState(before)
|
|
@@ -1497,6 +1802,7 @@ var ImageEditor = class {
|
|
|
1497
1802
|
this._updateUI();
|
|
1498
1803
|
}).catch((error) => {
|
|
1499
1804
|
this._reportError("undo failed", error);
|
|
1805
|
+
throw error;
|
|
1500
1806
|
});
|
|
1501
1807
|
}
|
|
1502
1808
|
/**
|
|
@@ -1510,48 +1816,40 @@ var ImageEditor = class {
|
|
|
1510
1816
|
this._updateUI();
|
|
1511
1817
|
}).catch((error) => {
|
|
1512
1818
|
this._reportError("redo failed", error);
|
|
1819
|
+
throw error;
|
|
1513
1820
|
});
|
|
1514
1821
|
}
|
|
1515
1822
|
_rebindMaskEvents(mask) {
|
|
1516
|
-
if (!mask)
|
|
1517
|
-
return;
|
|
1823
|
+
if (!mask) return;
|
|
1518
1824
|
if (mask.__imageEditorMaskHandlers) {
|
|
1519
1825
|
try {
|
|
1520
1826
|
mask.off("mouseover", mask.__imageEditorMaskHandlers.mouseover);
|
|
1521
1827
|
mask.off("mouseout", mask.__imageEditorMaskHandlers.mouseout);
|
|
1522
1828
|
} catch (error) {
|
|
1829
|
+
void error;
|
|
1523
1830
|
}
|
|
1524
1831
|
}
|
|
1525
1832
|
const metadata = {};
|
|
1526
1833
|
if (!Number.isFinite(Number(mask.originalAlpha))) {
|
|
1527
1834
|
metadata.originalAlpha = Number.isFinite(Number(mask.opacity)) ? Number(mask.opacity) : 0.5;
|
|
1528
1835
|
}
|
|
1529
|
-
if (!mask.originalStroke)
|
|
1530
|
-
metadata.originalStroke = mask.stroke || "#ccc";
|
|
1836
|
+
if (!mask.originalStroke) metadata.originalStroke = mask.stroke || "#ccc";
|
|
1531
1837
|
if (!Number.isFinite(Number(mask.originalStrokeWidth))) {
|
|
1532
1838
|
metadata.originalStrokeWidth = Number.isFinite(Number(mask.strokeWidth)) ? Number(mask.strokeWidth) : 1;
|
|
1533
1839
|
}
|
|
1534
|
-
if (Object.keys(metadata).length)
|
|
1535
|
-
mask.set(metadata);
|
|
1536
|
-
const normalStyle = {
|
|
1537
|
-
stroke: mask.originalStroke || "#ccc",
|
|
1538
|
-
strokeWidth: mask.originalStrokeWidth,
|
|
1539
|
-
opacity: mask.originalAlpha
|
|
1540
|
-
};
|
|
1541
|
-
const hoverStyle = {
|
|
1542
|
-
stroke: "#ff5500",
|
|
1543
|
-
strokeWidth: 2,
|
|
1544
|
-
opacity: Math.min(mask.originalAlpha + 0.2, 1)
|
|
1545
|
-
};
|
|
1840
|
+
if (Object.keys(metadata).length) mask.set(metadata);
|
|
1546
1841
|
const mouseover = () => {
|
|
1547
|
-
mask.
|
|
1548
|
-
|
|
1549
|
-
|
|
1842
|
+
const opacity = Number(mask.originalAlpha);
|
|
1843
|
+
mask.set({
|
|
1844
|
+
stroke: "#ff5500",
|
|
1845
|
+
strokeWidth: 2,
|
|
1846
|
+
opacity: Math.min((Number.isFinite(opacity) ? opacity : 0.5) + 0.2, 1)
|
|
1847
|
+
});
|
|
1848
|
+
if (mask.canvas) mask.canvas.requestRenderAll();
|
|
1550
1849
|
};
|
|
1551
1850
|
const mouseout = () => {
|
|
1552
|
-
mask.set(
|
|
1553
|
-
if (mask.canvas)
|
|
1554
|
-
mask.canvas.requestRenderAll();
|
|
1851
|
+
mask.set(this._getMaskNormalStyle(mask));
|
|
1852
|
+
if (mask.canvas) mask.canvas.requestRenderAll();
|
|
1555
1853
|
};
|
|
1556
1854
|
mask.on("mouseover", mouseover);
|
|
1557
1855
|
mask.on("mouseout", mouseout);
|
|
@@ -1586,8 +1884,8 @@ var ImageEditor = class {
|
|
|
1586
1884
|
* @public
|
|
1587
1885
|
*/
|
|
1588
1886
|
createMask(config = {}) {
|
|
1589
|
-
if (!this.canvas)
|
|
1590
|
-
|
|
1887
|
+
if (!this.canvas) return null;
|
|
1888
|
+
if (!this._canMutateNow("createMask")) return null;
|
|
1591
1889
|
const shapeType = config.shape || "rect";
|
|
1592
1890
|
const maskConfig = {
|
|
1593
1891
|
shape: shapeType,
|
|
@@ -1603,33 +1901,37 @@ var ImageEditor = class {
|
|
|
1603
1901
|
...config
|
|
1604
1902
|
};
|
|
1605
1903
|
const firstOffset = 10;
|
|
1606
|
-
let left
|
|
1607
|
-
let top
|
|
1608
|
-
const
|
|
1904
|
+
let left;
|
|
1905
|
+
let top;
|
|
1906
|
+
const getCanvasBasis = (axis) => {
|
|
1907
|
+
const canvasWidth = this.canvas ? this.canvas.getWidth() : 0;
|
|
1908
|
+
const canvasHeight = this.canvas ? this.canvas.getHeight() : 0;
|
|
1909
|
+
if (axis === "height") return canvasHeight;
|
|
1910
|
+
if (axis === "min") return Math.min(canvasWidth, canvasHeight);
|
|
1911
|
+
return canvasWidth;
|
|
1912
|
+
};
|
|
1913
|
+
const resolveValue = (value, fallback, axis = "width") => {
|
|
1609
1914
|
if (typeof value === "function")
|
|
1610
1915
|
return value(this.canvas, this.options);
|
|
1611
1916
|
if (typeof value === "string" && value.endsWith("%")) {
|
|
1612
|
-
const percent = parseFloat(value) / 100;
|
|
1613
|
-
|
|
1917
|
+
const percent = Number.parseFloat(value) / 100;
|
|
1918
|
+
if (!Number.isFinite(percent)) return fallback;
|
|
1919
|
+
return Math.floor(getCanvasBasis(axis) * percent);
|
|
1614
1920
|
}
|
|
1615
1921
|
return value != null ? value : fallback;
|
|
1616
1922
|
};
|
|
1617
1923
|
if (maskConfig.left === void 0 && this._lastMask) {
|
|
1618
1924
|
const previousMask = this._lastMask;
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
previousMaskRight += previousMask.width * (previousMask.scaleX ?? 1);
|
|
1624
|
-
}
|
|
1625
|
-
left = Math.round(previousMaskRight + maskConfig.gap);
|
|
1626
|
-
top = previousMask.top ?? firstOffset;
|
|
1925
|
+
if (typeof previousMask.setCoords === "function") previousMask.setCoords();
|
|
1926
|
+
const previousBounds = typeof previousMask.getBoundingRect === "function" ? previousMask.getBoundingRect(true, true) : { left: previousMask.left || firstOffset, top: previousMask.top || firstOffset, width: previousMask.width || 0 };
|
|
1927
|
+
left = Math.round(previousBounds.left + previousBounds.width + maskConfig.gap);
|
|
1928
|
+
top = Math.round(previousBounds.top ?? firstOffset);
|
|
1627
1929
|
} else {
|
|
1628
|
-
left = resolveValue(maskConfig.left, firstOffset);
|
|
1629
|
-
top = resolveValue(maskConfig.top, firstOffset);
|
|
1930
|
+
left = resolveValue(maskConfig.left, firstOffset, "width");
|
|
1931
|
+
top = resolveValue(maskConfig.top, firstOffset, "height");
|
|
1630
1932
|
}
|
|
1631
|
-
maskConfig.width = resolveValue(maskConfig.width, this.options.defaultMaskWidth);
|
|
1632
|
-
maskConfig.height = resolveValue(maskConfig.height, this.options.defaultMaskHeight);
|
|
1933
|
+
maskConfig.width = resolveValue(maskConfig.width, this.options.defaultMaskWidth, "width");
|
|
1934
|
+
maskConfig.height = resolveValue(maskConfig.height, this.options.defaultMaskHeight, "height");
|
|
1633
1935
|
maskConfig.left = left;
|
|
1634
1936
|
maskConfig.top = top;
|
|
1635
1937
|
let mask;
|
|
@@ -1641,7 +1943,7 @@ var ImageEditor = class {
|
|
|
1641
1943
|
mask = new fabric.Circle({
|
|
1642
1944
|
left,
|
|
1643
1945
|
top,
|
|
1644
|
-
radius: resolveValue(maskConfig.radius, Math.min(maskConfig.width, maskConfig.height) / 2),
|
|
1946
|
+
radius: resolveValue(maskConfig.radius, Math.min(maskConfig.width, maskConfig.height) / 2, "min"),
|
|
1645
1947
|
fill: maskConfig.color,
|
|
1646
1948
|
opacity: maskConfig.alpha,
|
|
1647
1949
|
angle: maskConfig.angle,
|
|
@@ -1652,8 +1954,8 @@ var ImageEditor = class {
|
|
|
1652
1954
|
mask = new fabric.Ellipse({
|
|
1653
1955
|
left,
|
|
1654
1956
|
top,
|
|
1655
|
-
rx: resolveValue(maskConfig.rx, maskConfig.width / 2),
|
|
1656
|
-
ry: resolveValue(maskConfig.ry, maskConfig.height / 2),
|
|
1957
|
+
rx: resolveValue(maskConfig.rx, maskConfig.width / 2, "width"),
|
|
1958
|
+
ry: resolveValue(maskConfig.ry, maskConfig.height / 2, "height"),
|
|
1657
1959
|
fill: maskConfig.color,
|
|
1658
1960
|
opacity: maskConfig.alpha,
|
|
1659
1961
|
angle: maskConfig.angle,
|
|
@@ -1680,8 +1982,8 @@ var ImageEditor = class {
|
|
|
1680
1982
|
mask = new fabric.Rect({
|
|
1681
1983
|
left,
|
|
1682
1984
|
top,
|
|
1683
|
-
width: resolveValue(maskConfig.width, this.options.defaultMaskWidth),
|
|
1684
|
-
height: resolveValue(maskConfig.height, this.options.defaultMaskHeight),
|
|
1985
|
+
width: resolveValue(maskConfig.width, this.options.defaultMaskWidth, "width"),
|
|
1986
|
+
height: resolveValue(maskConfig.height, this.options.defaultMaskHeight, "height"),
|
|
1685
1987
|
fill: maskConfig.color,
|
|
1686
1988
|
opacity: maskConfig.alpha,
|
|
1687
1989
|
angle: maskConfig.angle,
|
|
@@ -1706,8 +2008,7 @@ var ImageEditor = class {
|
|
|
1706
2008
|
opacity: hasStyle("opacity") ? styles.opacity : maskConfig.alpha,
|
|
1707
2009
|
strokeUniform: "strokeUniform" in maskConfig ? maskConfig.strokeUniform : hasStyle("strokeUniform") ? styles.strokeUniform : true
|
|
1708
2010
|
};
|
|
1709
|
-
if (hasStyle("strokeDashArray"))
|
|
1710
|
-
maskSettings.strokeDashArray = styles.strokeDashArray;
|
|
2011
|
+
if (hasStyle("strokeDashArray")) maskSettings.strokeDashArray = styles.strokeDashArray;
|
|
1711
2012
|
mask.set(maskSettings);
|
|
1712
2013
|
mask.setCoords();
|
|
1713
2014
|
mask.set({
|
|
@@ -1719,7 +2020,7 @@ var ImageEditor = class {
|
|
|
1719
2020
|
this._expandCanvasToFitObject(mask);
|
|
1720
2021
|
this._lastMaskInitialLeft = left;
|
|
1721
2022
|
this._lastMaskInitialTop = top;
|
|
1722
|
-
this._lastMaskInitialWidth = resolveValue(maskConfig.width, this.options.defaultMaskWidth);
|
|
2023
|
+
this._lastMaskInitialWidth = resolveValue(maskConfig.width, this.options.defaultMaskWidth, "width");
|
|
1723
2024
|
const maskId = ++this.maskCounter;
|
|
1724
2025
|
mask.set({
|
|
1725
2026
|
maskId,
|
|
@@ -1728,15 +2029,13 @@ var ImageEditor = class {
|
|
|
1728
2029
|
this._lastMask = mask;
|
|
1729
2030
|
this.canvas.add(mask);
|
|
1730
2031
|
this.canvas.bringToFront(mask);
|
|
1731
|
-
if (maskConfig.selectable)
|
|
1732
|
-
this.canvas.setActiveObject(mask);
|
|
2032
|
+
if (maskConfig.selectable) this.canvas.setActiveObject(mask);
|
|
1733
2033
|
this._handleSelectionChanged([mask]);
|
|
1734
2034
|
this._updateMaskList();
|
|
1735
2035
|
this._updateUI();
|
|
1736
2036
|
this.canvas.renderAll();
|
|
1737
2037
|
this.saveState();
|
|
1738
|
-
if (typeof maskConfig.onCreate === "function")
|
|
1739
|
-
maskConfig.onCreate(mask, this.canvas);
|
|
2038
|
+
if (typeof maskConfig.onCreate === "function") maskConfig.onCreate(mask, this.canvas);
|
|
1740
2039
|
return mask;
|
|
1741
2040
|
}
|
|
1742
2041
|
/**
|
|
@@ -1754,10 +2053,11 @@ var ImageEditor = class {
|
|
|
1754
2053
|
* The associated label is also removed. UI and mask list are updated.
|
|
1755
2054
|
*/
|
|
1756
2055
|
removeSelectedMask() {
|
|
2056
|
+
if (!this.canvas) return;
|
|
2057
|
+
if (!this._canMutateNow("removeSelectedMask")) return;
|
|
1757
2058
|
const activeObject = this.canvas.getActiveObject();
|
|
1758
2059
|
const selectedMasks = this._getModifiedMasks(activeObject);
|
|
1759
|
-
if (!selectedMasks.length)
|
|
1760
|
-
return;
|
|
2060
|
+
if (!selectedMasks.length) return;
|
|
1761
2061
|
this.canvas.discardActiveObject();
|
|
1762
2062
|
selectedMasks.forEach((mask) => {
|
|
1763
2063
|
this._removeLabelForMask(mask);
|
|
@@ -1780,6 +2080,8 @@ var ImageEditor = class {
|
|
|
1780
2080
|
* UI and internal mask placement memory are reset.
|
|
1781
2081
|
*/
|
|
1782
2082
|
removeAllMasks(options = {}) {
|
|
2083
|
+
if (!this.canvas) return;
|
|
2084
|
+
if (!this._canMutateNow("removeAllMasks")) return;
|
|
1783
2085
|
const saveHistory = options.saveHistory !== false;
|
|
1784
2086
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
1785
2087
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
@@ -1792,8 +2094,7 @@ var ImageEditor = class {
|
|
|
1792
2094
|
this._updateMaskList();
|
|
1793
2095
|
this._updateUI();
|
|
1794
2096
|
this.canvas.renderAll();
|
|
1795
|
-
if (saveHistory)
|
|
1796
|
-
this.saveState();
|
|
2097
|
+
if (saveHistory) this.saveState();
|
|
1797
2098
|
}
|
|
1798
2099
|
/**
|
|
1799
2100
|
* Removes the label associated with the specified mask object, if it exists.
|
|
@@ -1802,8 +2103,7 @@ var ImageEditor = class {
|
|
|
1802
2103
|
* @private
|
|
1803
2104
|
*/
|
|
1804
2105
|
_removeLabelForMask(mask) {
|
|
1805
|
-
if (!mask || !this.canvas)
|
|
1806
|
-
return;
|
|
2106
|
+
if (!mask || !this.canvas) return;
|
|
1807
2107
|
if (mask.__label) {
|
|
1808
2108
|
try {
|
|
1809
2109
|
const canvasObjects = this.canvas.getObjects();
|
|
@@ -1811,10 +2111,12 @@ var ImageEditor = class {
|
|
|
1811
2111
|
this.canvas.remove(mask.__label);
|
|
1812
2112
|
}
|
|
1813
2113
|
} catch (error) {
|
|
2114
|
+
void error;
|
|
1814
2115
|
}
|
|
1815
2116
|
try {
|
|
1816
2117
|
delete mask.__label;
|
|
1817
2118
|
} catch (error) {
|
|
2119
|
+
void error;
|
|
1818
2120
|
}
|
|
1819
2121
|
}
|
|
1820
2122
|
}
|
|
@@ -1830,8 +2132,7 @@ var ImageEditor = class {
|
|
|
1830
2132
|
*/
|
|
1831
2133
|
_getMaskCreationIndex(mask) {
|
|
1832
2134
|
const maskId = Number(mask && mask.maskId);
|
|
1833
|
-
if (Number.isFinite(maskId) && maskId > 0)
|
|
1834
|
-
return Math.floor(maskId) - 1;
|
|
2135
|
+
if (Number.isFinite(maskId) && maskId > 0) return Math.floor(maskId) - 1;
|
|
1835
2136
|
const masks = this.canvas ? this.canvas.getObjects().filter((object) => object.maskId) : [];
|
|
1836
2137
|
return Math.max(0, masks.indexOf(mask));
|
|
1837
2138
|
}
|
|
@@ -1843,12 +2144,15 @@ var ImageEditor = class {
|
|
|
1843
2144
|
* @private
|
|
1844
2145
|
*/
|
|
1845
2146
|
_createLabelForMask(mask) {
|
|
1846
|
-
if (!mask || !this.options.maskLabelOnSelect)
|
|
1847
|
-
return;
|
|
2147
|
+
if (!mask || !this.options.maskLabelOnSelect) return;
|
|
1848
2148
|
this._removeLabelForMask(mask);
|
|
1849
2149
|
let textObject = null;
|
|
1850
2150
|
if (this.options.label && typeof this.options.label.create === "function") {
|
|
1851
2151
|
textObject = this.options.label.create(mask, fabric);
|
|
2152
|
+
if (!textObject || typeof textObject.set !== "function") {
|
|
2153
|
+
this._reportWarning("label.create() returned an invalid Fabric object; using the default label");
|
|
2154
|
+
textObject = null;
|
|
2155
|
+
}
|
|
1852
2156
|
}
|
|
1853
2157
|
if (!textObject) {
|
|
1854
2158
|
let labelText = mask.maskName;
|
|
@@ -1886,15 +2190,14 @@ var ImageEditor = class {
|
|
|
1886
2190
|
* @private
|
|
1887
2191
|
*/
|
|
1888
2192
|
_hideAllMaskLabels() {
|
|
1889
|
-
if (!this.canvas)
|
|
1890
|
-
return;
|
|
2193
|
+
if (!this.canvas) return;
|
|
1891
2194
|
const canvasObjects = this.canvas.getObjects();
|
|
1892
2195
|
const labels = canvasObjects.filter((object) => object.maskLabel);
|
|
1893
2196
|
labels.forEach((label) => {
|
|
1894
2197
|
try {
|
|
1895
|
-
if (canvasObjects.includes(label))
|
|
1896
|
-
this.canvas.remove(label);
|
|
2198
|
+
if (canvasObjects.includes(label)) this.canvas.remove(label);
|
|
1897
2199
|
} catch (error) {
|
|
2200
|
+
void error;
|
|
1898
2201
|
}
|
|
1899
2202
|
});
|
|
1900
2203
|
canvasObjects.forEach((object) => {
|
|
@@ -1902,6 +2205,7 @@ var ImageEditor = class {
|
|
|
1902
2205
|
try {
|
|
1903
2206
|
delete object.__label;
|
|
1904
2207
|
} catch (error) {
|
|
2208
|
+
void error;
|
|
1905
2209
|
}
|
|
1906
2210
|
}
|
|
1907
2211
|
});
|
|
@@ -1913,16 +2217,13 @@ var ImageEditor = class {
|
|
|
1913
2217
|
* @private
|
|
1914
2218
|
*/
|
|
1915
2219
|
_syncMaskLabel(mask) {
|
|
1916
|
-
if (!mask)
|
|
1917
|
-
|
|
1918
|
-
if (!
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
const
|
|
1923
|
-
if (!coords || coords.length < 4)
|
|
1924
|
-
return;
|
|
1925
|
-
const tl = coords[0];
|
|
2220
|
+
if (!mask) return;
|
|
2221
|
+
if (!this.options.maskLabelOnSelect) return;
|
|
2222
|
+
if (!mask.__label) return;
|
|
2223
|
+
if (typeof mask.setCoords === "function") mask.setCoords();
|
|
2224
|
+
const bounds = mask.getBoundingRect ? mask.getBoundingRect(true, true) : null;
|
|
2225
|
+
if (!bounds) return;
|
|
2226
|
+
const tl = { x: bounds.left, y: bounds.top };
|
|
1926
2227
|
const center = mask.getCenterPoint();
|
|
1927
2228
|
const vx = center.x - tl.x;
|
|
1928
2229
|
const vy = center.y - tl.y;
|
|
@@ -1954,12 +2255,9 @@ var ImageEditor = class {
|
|
|
1954
2255
|
* @private
|
|
1955
2256
|
*/
|
|
1956
2257
|
_showLabelForMask(mask) {
|
|
1957
|
-
if (!mask)
|
|
1958
|
-
|
|
1959
|
-
if (!this.
|
|
1960
|
-
return;
|
|
1961
|
-
if (!mask.__label)
|
|
1962
|
-
this._createLabelForMask(mask);
|
|
2258
|
+
if (!mask) return;
|
|
2259
|
+
if (!this.options.maskLabelOnSelect) return;
|
|
2260
|
+
if (!mask.__label) this._createLabelForMask(mask);
|
|
1963
2261
|
mask.__label.set({ visible: true });
|
|
1964
2262
|
this._syncMaskLabel(mask);
|
|
1965
2263
|
}
|
|
@@ -1979,6 +2277,7 @@ var ImageEditor = class {
|
|
|
1979
2277
|
try {
|
|
1980
2278
|
this.canvas.remove(mask.__label);
|
|
1981
2279
|
} catch (error) {
|
|
2280
|
+
void error;
|
|
1982
2281
|
}
|
|
1983
2282
|
delete mask.__label;
|
|
1984
2283
|
}
|
|
@@ -1991,8 +2290,7 @@ var ImageEditor = class {
|
|
|
1991
2290
|
mask.set({ stroke: "#ff0000", strokeWidth: 1 });
|
|
1992
2291
|
}
|
|
1993
2292
|
});
|
|
1994
|
-
if (selectedMask)
|
|
1995
|
-
this._showLabelForMask(selectedMask);
|
|
2293
|
+
if (selectedMask) this._showLabelForMask(selectedMask);
|
|
1996
2294
|
this._updateMaskListSelection(selectedMask);
|
|
1997
2295
|
this.canvas.renderAll();
|
|
1998
2296
|
this._updateUI();
|
|
@@ -2003,22 +2301,28 @@ var ImageEditor = class {
|
|
|
2003
2301
|
* @private
|
|
2004
2302
|
*/
|
|
2005
2303
|
_updateMaskList() {
|
|
2006
|
-
const maskListElement =
|
|
2007
|
-
if (!maskListElement)
|
|
2008
|
-
return;
|
|
2304
|
+
const maskListElement = this._getElement("maskList");
|
|
2305
|
+
if (!maskListElement) return;
|
|
2009
2306
|
maskListElement.innerHTML = "";
|
|
2010
2307
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2011
2308
|
masks.forEach((mask) => {
|
|
2012
2309
|
const listItemElement = document.createElement("li");
|
|
2013
2310
|
listItemElement.className = "list-group-item mask-item";
|
|
2014
2311
|
listItemElement.textContent = mask.maskName;
|
|
2015
|
-
listItemElement.
|
|
2016
|
-
this.canvas.setActiveObject(mask);
|
|
2017
|
-
this._handleSelectionChanged([mask]);
|
|
2018
|
-
};
|
|
2312
|
+
listItemElement.dataset.maskId = String(mask.maskId);
|
|
2019
2313
|
maskListElement.appendChild(listItemElement);
|
|
2020
2314
|
});
|
|
2021
2315
|
}
|
|
2316
|
+
_handleMaskListClick(event) {
|
|
2317
|
+
if (!this.canvas) return;
|
|
2318
|
+
const itemElement = event.target && event.target.closest ? event.target.closest(".mask-item") : null;
|
|
2319
|
+
if (!itemElement || !itemElement.dataset) return;
|
|
2320
|
+
const maskId = Number(itemElement.dataset.maskId);
|
|
2321
|
+
const mask = this.canvas.getObjects().find((object) => Number(object.maskId) === maskId);
|
|
2322
|
+
if (!mask) return;
|
|
2323
|
+
this.canvas.setActiveObject(mask);
|
|
2324
|
+
this._handleSelectionChanged([mask]);
|
|
2325
|
+
}
|
|
2022
2326
|
/**
|
|
2023
2327
|
* Updates the visual selection (CSS 'active') state for the mask list in the DOM.
|
|
2024
2328
|
*
|
|
@@ -2026,13 +2330,13 @@ var ImageEditor = class {
|
|
|
2026
2330
|
* @private
|
|
2027
2331
|
*/
|
|
2028
2332
|
_updateMaskListSelection(selectedMask) {
|
|
2029
|
-
const maskListElement =
|
|
2030
|
-
if (!maskListElement)
|
|
2031
|
-
return;
|
|
2333
|
+
const maskListElement = this._getElement("maskList");
|
|
2334
|
+
if (!maskListElement) return;
|
|
2032
2335
|
const maskItems = maskListElement.querySelectorAll(".mask-item");
|
|
2033
2336
|
maskItems.forEach((item) => {
|
|
2034
|
-
const isSelected = !!selectedMask && item.
|
|
2337
|
+
const isSelected = !!selectedMask && Number(item.dataset.maskId) === Number(selectedMask.maskId);
|
|
2035
2338
|
item.classList.toggle("active", isSelected);
|
|
2339
|
+
item.classList.toggle("selected", isSelected);
|
|
2036
2340
|
});
|
|
2037
2341
|
}
|
|
2038
2342
|
/**
|
|
@@ -2046,22 +2350,22 @@ var ImageEditor = class {
|
|
|
2046
2350
|
* @public
|
|
2047
2351
|
*/
|
|
2048
2352
|
async mergeMasks() {
|
|
2049
|
-
if (!this.originalImage)
|
|
2050
|
-
|
|
2353
|
+
if (!this.originalImage) return;
|
|
2354
|
+
this._assertIdleForOperation("mergeMasks");
|
|
2051
2355
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2052
|
-
if (!masks.length)
|
|
2053
|
-
return;
|
|
2356
|
+
if (!masks.length) return;
|
|
2054
2357
|
this.canvas.discardActiveObject();
|
|
2055
2358
|
this.canvas.renderAll();
|
|
2056
2359
|
try {
|
|
2057
2360
|
const beforeJson = this._serializeCanvasState();
|
|
2058
2361
|
const merged = await this.exportImageBase64({ exportImageArea: true, multiplier: this.options.exportMultiplier });
|
|
2059
2362
|
this.removeAllMasks({ saveHistory: false });
|
|
2060
|
-
await this.loadImage(merged, { preserveScroll: true });
|
|
2363
|
+
await this.loadImage(merged, { preserveScroll: true, resetMaskCounter: false });
|
|
2061
2364
|
const afterJson = this._serializeCanvasState();
|
|
2062
2365
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2063
2366
|
} catch (error) {
|
|
2064
2367
|
this._reportError("merge error", error);
|
|
2368
|
+
throw error;
|
|
2065
2369
|
}
|
|
2066
2370
|
}
|
|
2067
2371
|
/**
|
|
@@ -2082,8 +2386,8 @@ var ImageEditor = class {
|
|
|
2082
2386
|
* @public
|
|
2083
2387
|
*/
|
|
2084
2388
|
downloadImage(fileName = this.options.defaultDownloadFileName) {
|
|
2085
|
-
if (!this.originalImage)
|
|
2086
|
-
|
|
2389
|
+
if (!this.originalImage) return;
|
|
2390
|
+
if (!this._canMutateNow("downloadImage")) return;
|
|
2087
2391
|
const exportImageArea = this.options.exportImageAreaByDefault;
|
|
2088
2392
|
this.exportImageBase64({ exportImageArea, multiplier: this.options.exportMultiplier }).then((imageBase64) => {
|
|
2089
2393
|
const link = document.createElement("a");
|
|
@@ -2111,8 +2415,8 @@ var ImageEditor = class {
|
|
|
2111
2415
|
* @public
|
|
2112
2416
|
*/
|
|
2113
2417
|
async exportImageBase64(options = {}) {
|
|
2114
|
-
if (!this.originalImage)
|
|
2115
|
-
|
|
2418
|
+
if (!this.originalImage) throw new Error("No image loaded");
|
|
2419
|
+
this._assertIdleForOperation("exportImageBase64");
|
|
2116
2420
|
const exportImageArea = typeof options.exportImageArea === "boolean" ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2117
2421
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2118
2422
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
@@ -2129,7 +2433,7 @@ var ImageEditor = class {
|
|
|
2129
2433
|
this.originalImage.setCoords();
|
|
2130
2434
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2131
2435
|
const exportRegion = this._getClampedCanvasRegion(imageBounds, { includePartialPixels: false });
|
|
2132
|
-
return
|
|
2436
|
+
return this._exportCanvasRegionToDataURL({
|
|
2133
2437
|
...exportRegion,
|
|
2134
2438
|
multiplier,
|
|
2135
2439
|
quality,
|
|
@@ -2140,6 +2444,7 @@ var ImageEditor = class {
|
|
|
2140
2444
|
try {
|
|
2141
2445
|
backup.object.set({ visible: backup.visible });
|
|
2142
2446
|
} catch (error) {
|
|
2447
|
+
void error;
|
|
2143
2448
|
}
|
|
2144
2449
|
});
|
|
2145
2450
|
this.canvas.renderAll();
|
|
@@ -2168,7 +2473,7 @@ var ImageEditor = class {
|
|
|
2168
2473
|
this.originalImage.setCoords();
|
|
2169
2474
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2170
2475
|
const exportRegion = this._getClampedCanvasRegion(imageBounds, { includePartialPixels: false });
|
|
2171
|
-
finalBase64 =
|
|
2476
|
+
finalBase64 = this._exportCanvasRegionToDataURL({
|
|
2172
2477
|
...exportRegion,
|
|
2173
2478
|
multiplier,
|
|
2174
2479
|
quality,
|
|
@@ -2187,6 +2492,7 @@ var ImageEditor = class {
|
|
|
2187
2492
|
});
|
|
2188
2493
|
backup.object.setCoords();
|
|
2189
2494
|
} catch (error) {
|
|
2495
|
+
void error;
|
|
2190
2496
|
}
|
|
2191
2497
|
});
|
|
2192
2498
|
this.canvas.renderAll();
|
|
@@ -2222,8 +2528,8 @@ var ImageEditor = class {
|
|
|
2222
2528
|
* const file = await this.exportImageFile({ mergeMask: false, fileType: 'png' });
|
|
2223
2529
|
*/
|
|
2224
2530
|
async exportImageFile(options = {}) {
|
|
2225
|
-
if (!this.originalImage)
|
|
2226
|
-
|
|
2531
|
+
if (!this.originalImage) throw new Error("No image loaded");
|
|
2532
|
+
this._assertIdleForOperation("exportImageFile");
|
|
2227
2533
|
const {
|
|
2228
2534
|
mergeMask = true,
|
|
2229
2535
|
fileType = "jpeg",
|
|
@@ -2259,6 +2565,7 @@ var ImageEditor = class {
|
|
|
2259
2565
|
offscreenCanvas.width = imageElement.width;
|
|
2260
2566
|
offscreenCanvas.height = imageElement.height;
|
|
2261
2567
|
const context = offscreenCanvas.getContext("2d");
|
|
2568
|
+
if (!context) throw new Error("Unable to create 2D canvas context for export conversion");
|
|
2262
2569
|
context.drawImage(imageElement, 0, 0);
|
|
2263
2570
|
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, quality);
|
|
2264
2571
|
resolve(convertedDataUrl);
|
|
@@ -2287,8 +2594,7 @@ var ImageEditor = class {
|
|
|
2287
2594
|
}
|
|
2288
2595
|
async _restoreStateAfterCropFailure(beforeJson, message, error) {
|
|
2289
2596
|
this._reportError(message, error);
|
|
2290
|
-
if (this._cropRect && this.canvas)
|
|
2291
|
-
this._removeCropRect();
|
|
2597
|
+
if (this._cropRect && this.canvas) this._removeCropRect();
|
|
2292
2598
|
this._cropRect = null;
|
|
2293
2599
|
this._cropMode = false;
|
|
2294
2600
|
if (this.canvas && this._prevSelectionSetting !== void 0) {
|
|
@@ -2303,8 +2609,7 @@ var ImageEditor = class {
|
|
|
2303
2609
|
}
|
|
2304
2610
|
}
|
|
2305
2611
|
this._updateUI();
|
|
2306
|
-
if (this.canvas)
|
|
2307
|
-
this.canvas.renderAll();
|
|
2612
|
+
if (this.canvas) this.canvas.renderAll();
|
|
2308
2613
|
}
|
|
2309
2614
|
_restoreCropObjectState() {
|
|
2310
2615
|
if (Array.isArray(this._cropPrevEvented)) {
|
|
@@ -2316,27 +2621,31 @@ var ImageEditor = class {
|
|
|
2316
2621
|
visible: state.visible
|
|
2317
2622
|
});
|
|
2318
2623
|
} catch (error) {
|
|
2624
|
+
void error;
|
|
2319
2625
|
}
|
|
2320
2626
|
});
|
|
2321
2627
|
}
|
|
2322
2628
|
this._cropPrevEvented = null;
|
|
2323
2629
|
}
|
|
2324
2630
|
_removeCropRect() {
|
|
2325
|
-
if (!this._cropRect)
|
|
2326
|
-
return;
|
|
2631
|
+
if (!this._cropRect) return;
|
|
2327
2632
|
try {
|
|
2328
2633
|
if (this._cropHandlers && this._cropHandlers.length) {
|
|
2329
2634
|
this._cropHandlers.forEach((targetHandlers) => {
|
|
2330
2635
|
targetHandlers.handlers.forEach((handlerRecord) => {
|
|
2331
|
-
targetHandlers.target.off
|
|
2636
|
+
if (targetHandlers.target && typeof targetHandlers.target.off === "function") {
|
|
2637
|
+
targetHandlers.target.off(handlerRecord.eventName, handlerRecord.handler);
|
|
2638
|
+
}
|
|
2332
2639
|
});
|
|
2333
2640
|
});
|
|
2334
2641
|
}
|
|
2335
2642
|
} catch (error) {
|
|
2643
|
+
void error;
|
|
2336
2644
|
}
|
|
2337
2645
|
try {
|
|
2338
|
-
this.canvas.remove(this._cropRect);
|
|
2646
|
+
if (this.canvas) this.canvas.remove(this._cropRect);
|
|
2339
2647
|
} catch (error) {
|
|
2648
|
+
void error;
|
|
2340
2649
|
}
|
|
2341
2650
|
this._cropRect = null;
|
|
2342
2651
|
this._cropHandlers = [];
|
|
@@ -2351,10 +2660,10 @@ var ImageEditor = class {
|
|
|
2351
2660
|
* @public
|
|
2352
2661
|
*/
|
|
2353
2662
|
enterCropMode() {
|
|
2354
|
-
if (!this.canvas || !this.originalImage || this._cropMode)
|
|
2355
|
-
|
|
2356
|
-
if (!this.isImageLoaded())
|
|
2357
|
-
|
|
2663
|
+
if (!this.canvas || !this.originalImage || this._cropMode) return;
|
|
2664
|
+
if (!this._canMutateNow("enterCropMode")) return;
|
|
2665
|
+
if (!this.isImageLoaded()) return;
|
|
2666
|
+
this._removeCropRect();
|
|
2358
2667
|
this._cropMode = true;
|
|
2359
2668
|
this._prevSelectionSetting = this.canvas.selection;
|
|
2360
2669
|
this.canvas.selection = false;
|
|
@@ -2406,10 +2715,10 @@ var ImageEditor = class {
|
|
|
2406
2715
|
evented: false,
|
|
2407
2716
|
selectable: false
|
|
2408
2717
|
};
|
|
2409
|
-
if (shouldHideMasks && (object.maskId || object.maskLabel))
|
|
2410
|
-
updates.visible = false;
|
|
2718
|
+
if (shouldHideMasks && (object.maskId || object.maskLabel)) updates.visible = false;
|
|
2411
2719
|
object.set(updates);
|
|
2412
2720
|
} catch (error) {
|
|
2721
|
+
void error;
|
|
2413
2722
|
}
|
|
2414
2723
|
}
|
|
2415
2724
|
});
|
|
@@ -2423,6 +2732,7 @@ var ImageEditor = class {
|
|
|
2423
2732
|
cropRect.setCoords();
|
|
2424
2733
|
this.canvas.requestRenderAll();
|
|
2425
2734
|
} catch (error) {
|
|
2735
|
+
void error;
|
|
2426
2736
|
}
|
|
2427
2737
|
};
|
|
2428
2738
|
cropRect.on("modified", handleCropRectModified);
|
|
@@ -2446,8 +2756,7 @@ var ImageEditor = class {
|
|
|
2446
2756
|
* @public
|
|
2447
2757
|
*/
|
|
2448
2758
|
cancelCrop() {
|
|
2449
|
-
if (!this.canvas || !this._cropMode)
|
|
2450
|
-
return;
|
|
2759
|
+
if (!this.canvas || !this._cropMode) return;
|
|
2451
2760
|
this._removeCropRect();
|
|
2452
2761
|
this._restoreCropObjectState();
|
|
2453
2762
|
this._cropMode = false;
|
|
@@ -2469,14 +2778,14 @@ var ImageEditor = class {
|
|
|
2469
2778
|
* @public
|
|
2470
2779
|
*/
|
|
2471
2780
|
async applyCrop() {
|
|
2472
|
-
if (!this.canvas || !this._cropMode || !this._cropRect)
|
|
2473
|
-
|
|
2781
|
+
if (!this.canvas || !this._cropMode || !this._cropRect) return;
|
|
2782
|
+
this._assertIdleForOperation("applyCrop");
|
|
2474
2783
|
this._cropRect.setCoords();
|
|
2475
2784
|
const rectBounds = this._cropRect.getBoundingRect(true, true);
|
|
2476
|
-
const cropRegion = this._getClampedCanvasRegion(rectBounds);
|
|
2785
|
+
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
2477
2786
|
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
2478
2787
|
this._restoreCropObjectState();
|
|
2479
|
-
let beforeJson
|
|
2788
|
+
let beforeJson;
|
|
2480
2789
|
try {
|
|
2481
2790
|
beforeJson = this._serializeCanvasState();
|
|
2482
2791
|
} catch (error) {
|
|
@@ -2495,12 +2804,8 @@ var ImageEditor = class {
|
|
|
2495
2804
|
this._removeLabelForMask(mask);
|
|
2496
2805
|
this.canvas.remove(mask);
|
|
2497
2806
|
if (shouldPreserveMasks && intersectsCrop) {
|
|
2498
|
-
mask.
|
|
2499
|
-
|
|
2500
|
-
top: (mask.top || 0) - cropRegion.sourceY,
|
|
2501
|
-
visible: true
|
|
2502
|
-
});
|
|
2503
|
-
mask.setCoords();
|
|
2807
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
2808
|
+
mask.set({ visible: true });
|
|
2504
2809
|
preservedMasks.push(mask);
|
|
2505
2810
|
}
|
|
2506
2811
|
} catch (error) {
|
|
@@ -2531,7 +2836,7 @@ var ImageEditor = class {
|
|
|
2531
2836
|
return;
|
|
2532
2837
|
}
|
|
2533
2838
|
try {
|
|
2534
|
-
await this.loadImage(croppedBase64);
|
|
2839
|
+
await this.loadImage(croppedBase64, { resetMaskCounter: false });
|
|
2535
2840
|
if (preservedMasks.length) {
|
|
2536
2841
|
preservedMasks.forEach((mask) => {
|
|
2537
2842
|
this._rebindMaskEvents(mask);
|
|
@@ -2547,9 +2852,9 @@ var ImageEditor = class {
|
|
|
2547
2852
|
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: loadImage(croppedBase64) failed", error);
|
|
2548
2853
|
return;
|
|
2549
2854
|
}
|
|
2550
|
-
let afterJson
|
|
2855
|
+
let afterJson;
|
|
2551
2856
|
try {
|
|
2552
|
-
afterJson = this._serializeCanvasState();
|
|
2857
|
+
afterJson = preservedMasks.length ? this._serializeCanvasState() : this._lastSnapshot;
|
|
2553
2858
|
} catch (error) {
|
|
2554
2859
|
this._reportWarning("applyCrop: failed to serialize after state", error);
|
|
2555
2860
|
afterJson = null;
|
|
@@ -2569,9 +2874,8 @@ var ImageEditor = class {
|
|
|
2569
2874
|
* @private
|
|
2570
2875
|
*/
|
|
2571
2876
|
_updateInputs() {
|
|
2572
|
-
const scaleInputElement =
|
|
2573
|
-
if (scaleInputElement)
|
|
2574
|
-
scaleInputElement.value = Math.round(this.currentScale * 100);
|
|
2877
|
+
const scaleInputElement = this._getElement("scaleRate");
|
|
2878
|
+
if (scaleInputElement) scaleInputElement.value = Math.round(this.currentScale * 100);
|
|
2575
2879
|
}
|
|
2576
2880
|
/**
|
|
2577
2881
|
* Updates the enabled/disabled state of various UI controls (buttons)
|
|
@@ -2579,6 +2883,7 @@ var ImageEditor = class {
|
|
|
2579
2883
|
* @private
|
|
2580
2884
|
*/
|
|
2581
2885
|
_updateUI() {
|
|
2886
|
+
if (!this.canvas) return;
|
|
2582
2887
|
const hasImage = !!this.originalImage;
|
|
2583
2888
|
const masks = hasImage ? this.canvas.getObjects().filter((object) => object.maskId) : [];
|
|
2584
2889
|
const hasMasks = masks.length > 0;
|
|
@@ -2590,9 +2895,8 @@ var ImageEditor = class {
|
|
|
2590
2895
|
const isInCropMode = !!this._cropMode;
|
|
2591
2896
|
if (isInCropMode) {
|
|
2592
2897
|
for (const key of Object.keys(this.elements || {})) {
|
|
2593
|
-
const element =
|
|
2594
|
-
if (!element)
|
|
2595
|
-
continue;
|
|
2898
|
+
const element = this._getElement(key);
|
|
2899
|
+
if (!element) continue;
|
|
2596
2900
|
if (key === "applyCropBtn" || key === "cancelCropBtn") {
|
|
2597
2901
|
this._setDisabled(key, false);
|
|
2598
2902
|
} else {
|
|
@@ -2627,9 +2931,8 @@ var ImageEditor = class {
|
|
|
2627
2931
|
* @private
|
|
2628
2932
|
*/
|
|
2629
2933
|
_setDisabled(key, disabled) {
|
|
2630
|
-
const element =
|
|
2631
|
-
if (!element)
|
|
2632
|
-
return;
|
|
2934
|
+
const element = this._getElement(key);
|
|
2935
|
+
if (!element) return;
|
|
2633
2936
|
if ("disabled" in element) {
|
|
2634
2937
|
element.disabled = !!disabled;
|
|
2635
2938
|
return;
|
|
@@ -2643,10 +2946,8 @@ var ImageEditor = class {
|
|
|
2643
2946
|
}
|
|
2644
2947
|
}
|
|
2645
2948
|
_isElementDisabled(element) {
|
|
2646
|
-
if (!element)
|
|
2647
|
-
|
|
2648
|
-
if ("disabled" in element)
|
|
2649
|
-
return !!element.disabled;
|
|
2949
|
+
if (!element) return false;
|
|
2950
|
+
if ("disabled" in element) return !!element.disabled;
|
|
2650
2951
|
return element.getAttribute("aria-disabled") === "true";
|
|
2651
2952
|
}
|
|
2652
2953
|
/**
|
|
@@ -2654,8 +2955,7 @@ var ImageEditor = class {
|
|
|
2654
2955
|
* @private
|
|
2655
2956
|
*/
|
|
2656
2957
|
_updatePlaceholderStatus() {
|
|
2657
|
-
if (!this.options.showPlaceholder)
|
|
2658
|
-
return;
|
|
2958
|
+
if (!this.options.showPlaceholder) return;
|
|
2659
2959
|
this._setPlaceholderVisible(!this.originalImage);
|
|
2660
2960
|
}
|
|
2661
2961
|
/**
|
|
@@ -2665,17 +2965,57 @@ var ImageEditor = class {
|
|
|
2665
2965
|
* @private
|
|
2666
2966
|
*/
|
|
2667
2967
|
_setPlaceholderVisible(show) {
|
|
2668
|
-
if (
|
|
2669
|
-
|
|
2670
|
-
if (
|
|
2671
|
-
this.
|
|
2672
|
-
|
|
2673
|
-
|
|
2968
|
+
if (this.placeholderElement) this._setElementVisible(this.placeholderElement, show);
|
|
2969
|
+
const canvasVisibilityElement = this._getCanvasVisibilityElement();
|
|
2970
|
+
if (canvasVisibilityElement && canvasVisibilityElement !== this.placeholderElement) {
|
|
2971
|
+
this._setElementVisible(canvasVisibilityElement, !show);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
_getCanvasVisibilityElement() {
|
|
2975
|
+
const wrapperElement = this.canvas && this.canvas.wrapperEl ? this.canvas.wrapperEl : null;
|
|
2976
|
+
if (this.containerElement && this.placeholderElement && (this.containerElement === this.placeholderElement || this.containerElement.contains(this.placeholderElement))) {
|
|
2977
|
+
return wrapperElement || this.canvasElement;
|
|
2978
|
+
}
|
|
2979
|
+
return this.containerElement || wrapperElement || this.canvasElement;
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Updates element visibility.
|
|
2983
|
+
*
|
|
2984
|
+
* @param {HTMLElement} element - Element whose visibility should be updated.
|
|
2985
|
+
* @param {boolean} isVisible - If true, removes the hidden state.
|
|
2986
|
+
* @returns {void}
|
|
2987
|
+
* @private
|
|
2988
|
+
*/
|
|
2989
|
+
_setElementVisible(element, isVisible) {
|
|
2990
|
+
if (!element) return;
|
|
2991
|
+
this._rememberElementVisibility(element);
|
|
2992
|
+
element.hidden = !isVisible;
|
|
2993
|
+
element.setAttribute("aria-hidden", isVisible ? "false" : "true");
|
|
2994
|
+
if (element.classList) {
|
|
2995
|
+
element.classList.toggle("d-none", !isVisible);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
_rememberElementVisibility(element) {
|
|
2999
|
+
if (!element || this._visibilityStateByElement.has(element)) return;
|
|
3000
|
+
this._visibilityStateByElement.set(element, this._captureElementVisibility(element));
|
|
3001
|
+
}
|
|
3002
|
+
_captureElementVisibility(element) {
|
|
3003
|
+
if (!element) return null;
|
|
3004
|
+
return {
|
|
3005
|
+
hidden: element.hidden,
|
|
3006
|
+
ariaHidden: element.getAttribute("aria-hidden"),
|
|
3007
|
+
className: element.className
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
_restoreElementVisibility(element, state) {
|
|
3011
|
+
if (!element || !state) return;
|
|
3012
|
+
element.hidden = !!state.hidden;
|
|
3013
|
+
if (state.ariaHidden === null) {
|
|
3014
|
+
element.removeAttribute("aria-hidden");
|
|
2674
3015
|
} else {
|
|
2675
|
-
|
|
2676
|
-
this.placeholderElement.classList.add("d-none");
|
|
2677
|
-
this.containerElement.classList.remove("d-none");
|
|
3016
|
+
element.setAttribute("aria-hidden", state.ariaHidden);
|
|
2678
3017
|
}
|
|
3018
|
+
element.className = state.className || "";
|
|
2679
3019
|
}
|
|
2680
3020
|
/**
|
|
2681
3021
|
* Cleans up and disposes of the canvas and related references.
|
|
@@ -2683,44 +3023,84 @@ var ImageEditor = class {
|
|
|
2683
3023
|
* @public
|
|
2684
3024
|
*/
|
|
2685
3025
|
dispose() {
|
|
3026
|
+
this._disposed = true;
|
|
3027
|
+
this._rejectActiveAnimations(new Error("Editor disposed during animation"));
|
|
3028
|
+
if (this.animationQueue) {
|
|
3029
|
+
this.animationQueue.cancelAll(new Error("Editor disposed"));
|
|
3030
|
+
}
|
|
2686
3031
|
try {
|
|
2687
|
-
for (const key
|
|
2688
|
-
const
|
|
2689
|
-
|
|
2690
|
-
if (!element)
|
|
2691
|
-
continue;
|
|
3032
|
+
for (const [key, handlers] of Object.entries(this._handlersByElementKey || {})) {
|
|
3033
|
+
const element = this._getElement(key);
|
|
3034
|
+
if (!element) continue;
|
|
2692
3035
|
handlers.forEach((handlerRecord) => {
|
|
2693
3036
|
try {
|
|
2694
3037
|
element.removeEventListener(handlerRecord.eventName, handlerRecord.handler);
|
|
2695
3038
|
} catch (error) {
|
|
3039
|
+
void error;
|
|
2696
3040
|
}
|
|
2697
3041
|
});
|
|
2698
3042
|
}
|
|
2699
3043
|
} catch (error) {
|
|
3044
|
+
void error;
|
|
2700
3045
|
}
|
|
2701
3046
|
if (this._cropRect) {
|
|
2702
3047
|
try {
|
|
2703
3048
|
this.canvas.remove(this._cropRect);
|
|
2704
3049
|
} catch (error) {
|
|
3050
|
+
void error;
|
|
2705
3051
|
}
|
|
2706
3052
|
this._cropRect = null;
|
|
2707
3053
|
}
|
|
2708
|
-
if (this.containerElement && this._containerOriginalOverflow
|
|
3054
|
+
if (this.containerElement && this._containerOriginalOverflow) {
|
|
3055
|
+
try {
|
|
3056
|
+
this._restoreContainerOverflowState();
|
|
3057
|
+
} catch (error) {
|
|
3058
|
+
void error;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
if (this._visibilityStateByElement) {
|
|
3062
|
+
try {
|
|
3063
|
+
[this.placeholderElement, this._getCanvasVisibilityElement()].forEach((element) => {
|
|
3064
|
+
const state = element ? this._visibilityStateByElement.get(element) : null;
|
|
3065
|
+
if (state) this._restoreElementVisibility(element, state);
|
|
3066
|
+
});
|
|
3067
|
+
} catch (error) {
|
|
3068
|
+
void error;
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
if (this.canvasElement && this._canvasElementOriginalStyle) {
|
|
2709
3072
|
try {
|
|
2710
|
-
this.
|
|
3073
|
+
this.canvasElement.style.display = this._canvasElementOriginalStyle.display;
|
|
3074
|
+
this.canvasElement.style.width = this._canvasElementOriginalStyle.width;
|
|
3075
|
+
this.canvasElement.style.height = this._canvasElementOriginalStyle.height;
|
|
2711
3076
|
} catch (error) {
|
|
3077
|
+
void error;
|
|
2712
3078
|
}
|
|
2713
3079
|
}
|
|
2714
3080
|
if (this.canvas) {
|
|
2715
3081
|
try {
|
|
2716
3082
|
this.canvas.dispose();
|
|
2717
3083
|
} catch (error) {
|
|
3084
|
+
void error;
|
|
2718
3085
|
}
|
|
2719
3086
|
this.canvas = null;
|
|
2720
3087
|
this.canvasElement = null;
|
|
2721
3088
|
this.isImageLoadedToCanvas = false;
|
|
2722
3089
|
}
|
|
2723
3090
|
this._handlersByElementKey = {};
|
|
3091
|
+
this._elementCache = {};
|
|
3092
|
+
this._clearMaskPlacementMemory();
|
|
3093
|
+
this.originalImage = null;
|
|
3094
|
+
this.baseImageScale = 1;
|
|
3095
|
+
this.currentScale = 1;
|
|
3096
|
+
this.currentRotation = 0;
|
|
3097
|
+
this.isAnimating = false;
|
|
3098
|
+
this._cropMode = false;
|
|
3099
|
+
this._cropRect = null;
|
|
3100
|
+
this._cropHandlers = [];
|
|
3101
|
+
this._cropPrevEvented = null;
|
|
3102
|
+
this._prevSelectionSetting = void 0;
|
|
3103
|
+
this._initialized = false;
|
|
2724
3104
|
}
|
|
2725
3105
|
};
|
|
2726
3106
|
var AnimationQueue = class {
|
|
@@ -2730,6 +3110,7 @@ var AnimationQueue = class {
|
|
|
2730
3110
|
constructor() {
|
|
2731
3111
|
this.animationTasks = [];
|
|
2732
3112
|
this.isRunning = false;
|
|
3113
|
+
this.currentTask = null;
|
|
2733
3114
|
}
|
|
2734
3115
|
/**
|
|
2735
3116
|
* Adds an animation function to the queue.
|
|
@@ -2739,12 +3120,29 @@ var AnimationQueue = class {
|
|
|
2739
3120
|
*/
|
|
2740
3121
|
async add(animationFn) {
|
|
2741
3122
|
return new Promise((resolve, reject) => {
|
|
2742
|
-
this.animationTasks.push({ animationFn, resolve, reject });
|
|
3123
|
+
this.animationTasks.push({ animationFn, resolve, reject, isSettled: false });
|
|
2743
3124
|
if (!this.isRunning) {
|
|
2744
3125
|
this._drainQueue();
|
|
2745
3126
|
}
|
|
2746
3127
|
});
|
|
2747
3128
|
}
|
|
3129
|
+
isBusy() {
|
|
3130
|
+
return this.isRunning || this.animationTasks.length > 0;
|
|
3131
|
+
}
|
|
3132
|
+
cancelAll(reason = new Error("Animation queue cancelled")) {
|
|
3133
|
+
const cancellationError = reason instanceof Error ? reason : new Error(String(reason));
|
|
3134
|
+
const tasks = [
|
|
3135
|
+
...this.currentTask ? [this.currentTask] : [],
|
|
3136
|
+
...this.animationTasks.splice(0)
|
|
3137
|
+
];
|
|
3138
|
+
tasks.forEach((task) => {
|
|
3139
|
+
if (!task || task.isSettled) return;
|
|
3140
|
+
task.isSettled = true;
|
|
3141
|
+
task.reject(cancellationError);
|
|
3142
|
+
});
|
|
3143
|
+
this.isRunning = false;
|
|
3144
|
+
this.currentTask = null;
|
|
3145
|
+
}
|
|
2748
3146
|
/**
|
|
2749
3147
|
* Runs queued animation tasks sequentially until the queue is empty.
|
|
2750
3148
|
*
|
|
@@ -2752,19 +3150,27 @@ var AnimationQueue = class {
|
|
|
2752
3150
|
* @returns {Promise<void>}
|
|
2753
3151
|
*/
|
|
2754
3152
|
async _drainQueue() {
|
|
2755
|
-
if (this.
|
|
2756
|
-
this.isRunning = false;
|
|
2757
|
-
return;
|
|
2758
|
-
}
|
|
3153
|
+
if (this.isRunning) return;
|
|
2759
3154
|
this.isRunning = true;
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
3155
|
+
while (this.animationTasks.length > 0) {
|
|
3156
|
+
const task = this.animationTasks.shift();
|
|
3157
|
+
this.currentTask = task;
|
|
3158
|
+
try {
|
|
3159
|
+
const result = await task.animationFn();
|
|
3160
|
+
if (!task.isSettled) {
|
|
3161
|
+
task.isSettled = true;
|
|
3162
|
+
task.resolve(result);
|
|
3163
|
+
}
|
|
3164
|
+
} catch (error) {
|
|
3165
|
+
if (!task.isSettled) {
|
|
3166
|
+
task.isSettled = true;
|
|
3167
|
+
task.reject(error);
|
|
3168
|
+
}
|
|
3169
|
+
} finally {
|
|
3170
|
+
if (this.currentTask === task) this.currentTask = null;
|
|
3171
|
+
}
|
|
2766
3172
|
}
|
|
2767
|
-
|
|
3173
|
+
this.isRunning = false;
|
|
2768
3174
|
}
|
|
2769
3175
|
};
|
|
2770
3176
|
var Command = class {
|
|
@@ -2795,15 +3201,8 @@ var HistoryManager = class {
|
|
|
2795
3201
|
* @private
|
|
2796
3202
|
*/
|
|
2797
3203
|
enqueue(task) {
|
|
2798
|
-
const nextTask = this.pending.then(
|
|
2799
|
-
|
|
2800
|
-
const resetPending = () => {
|
|
2801
|
-
if (this.pending === pendingAfterTask) {
|
|
2802
|
-
this.pending = Promise.resolve();
|
|
2803
|
-
}
|
|
2804
|
-
};
|
|
2805
|
-
pendingAfterTask = nextTask.then(resetPending, resetPending);
|
|
2806
|
-
this.pending = pendingAfterTask;
|
|
3204
|
+
const nextTask = this.pending.then(() => Promise.resolve().then(task));
|
|
3205
|
+
this.pending = nextTask.catch(() => void 0);
|
|
2807
3206
|
return nextTask;
|
|
2808
3207
|
}
|
|
2809
3208
|
/**
|
|
@@ -2814,8 +3213,14 @@ var HistoryManager = class {
|
|
|
2814
3213
|
* @returns {void}
|
|
2815
3214
|
*/
|
|
2816
3215
|
execute(command) {
|
|
2817
|
-
command.execute();
|
|
3216
|
+
const result = command.execute();
|
|
3217
|
+
if (result && typeof result.then === "function") {
|
|
3218
|
+
return Promise.resolve(result).then(() => {
|
|
3219
|
+
this.push(command);
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
2818
3222
|
this.push(command);
|
|
3223
|
+
return result;
|
|
2819
3224
|
}
|
|
2820
3225
|
/**
|
|
2821
3226
|
* Pushes an already-applied command onto the history stack.
|
|
@@ -2831,9 +3236,8 @@ var HistoryManager = class {
|
|
|
2831
3236
|
this.history.push(command);
|
|
2832
3237
|
if (this.history.length > this.maxSize) {
|
|
2833
3238
|
this.history.shift();
|
|
2834
|
-
} else {
|
|
2835
|
-
this.currentIndex++;
|
|
2836
3239
|
}
|
|
3240
|
+
this.currentIndex = this.history.length - 1;
|
|
2837
3241
|
}
|
|
2838
3242
|
/**
|
|
2839
3243
|
* Checks whether an undo operation is possible.
|