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