@bensitu/image-editor 1.4.0 → 1.4.2
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 +551 -185
- package/dist/image-editor.esm.js.map +3 -3
- 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 +551 -185
- package/dist/image-editor.esm.mjs.map +3 -3
- package/dist/image-editor.js +551 -185
- package/dist/image-editor.js.map +3 -3
- package/dist/image-editor.min.js +2 -2
- package/dist/image-editor.min.js.map +3 -3
- package/image-editor.d.ts +24 -12
- package/package.json +3 -4
- package/src/image-editor.js +608 -188
|
@@ -5,12 +5,13 @@ import fabricModule from "fabric";
|
|
|
5
5
|
/**
|
|
6
6
|
* @file image-editor.js
|
|
7
7
|
* @module image-editor
|
|
8
|
-
* @version 1.4.
|
|
8
|
+
* @version 1.4.2
|
|
9
9
|
* @author Ben Situ
|
|
10
10
|
* @license MIT
|
|
11
11
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
12
12
|
*/
|
|
13
13
|
var fabric = null;
|
|
14
|
+
var INTERNAL_OPERATION_TOKEN = /* @__PURE__ */ Symbol.for("ImageEditorInternalOperation");
|
|
14
15
|
function getGlobalScope() {
|
|
15
16
|
if (typeof globalThis !== "undefined") return globalThis;
|
|
16
17
|
if (typeof self !== "undefined") return self;
|
|
@@ -118,11 +119,15 @@ var ImageEditor = class {
|
|
|
118
119
|
this.currentRotation = 0;
|
|
119
120
|
this.maskCounter = 0;
|
|
120
121
|
this.isAnimating = false;
|
|
122
|
+
this._isLoading = false;
|
|
123
|
+
this._activeOperationName = null;
|
|
124
|
+
this._activeOperationToken = null;
|
|
121
125
|
this.elements = {};
|
|
122
126
|
this.isImageLoadedToCanvas = false;
|
|
123
127
|
this.maxHistorySize = 50;
|
|
124
128
|
this._handlersByElementKey = {};
|
|
125
129
|
this._elementCache = {};
|
|
130
|
+
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
126
131
|
this._lastMask = null;
|
|
127
132
|
this._lastMaskInitialLeft = null;
|
|
128
133
|
this._lastMaskInitialTop = null;
|
|
@@ -211,6 +216,10 @@ var ImageEditor = class {
|
|
|
211
216
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
212
217
|
this._visibilityStateByElement = /* @__PURE__ */ new WeakMap();
|
|
213
218
|
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
219
|
+
this._isLoading = false;
|
|
220
|
+
this._activeOperationName = null;
|
|
221
|
+
this._activeOperationToken = null;
|
|
222
|
+
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
214
223
|
this._containerOriginalOverflow = null;
|
|
215
224
|
this._lastContainerViewportSize = null;
|
|
216
225
|
this._canvasElementOriginalStyle = null;
|
|
@@ -417,6 +426,12 @@ var ImageEditor = class {
|
|
|
417
426
|
this.containerElement.style.overflowX = this._containerOriginalOverflow.overflowX;
|
|
418
427
|
this.containerElement.style.overflowY = this._containerOriginalOverflow.overflowY;
|
|
419
428
|
}
|
|
429
|
+
_restoreContainerOverflowSnapshot(snapshot) {
|
|
430
|
+
if (!this.containerElement || !this.containerElement.style || !snapshot) return;
|
|
431
|
+
this.containerElement.style.overflow = snapshot.overflow || "";
|
|
432
|
+
this.containerElement.style.overflowX = snapshot.overflowX || "";
|
|
433
|
+
this.containerElement.style.overflowY = snapshot.overflowY || "";
|
|
434
|
+
}
|
|
420
435
|
/**
|
|
421
436
|
* DOM / UI bindings
|
|
422
437
|
* @private
|
|
@@ -551,19 +566,23 @@ var ImageEditor = class {
|
|
|
551
566
|
if (!this._fabricLoaded) return;
|
|
552
567
|
if (!this.canvas || this._disposed) return;
|
|
553
568
|
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/")) return;
|
|
554
|
-
this._assertIdleForOperation("loadImage");
|
|
569
|
+
this._assertIdleForOperation("loadImage", options);
|
|
570
|
+
this._isLoading = true;
|
|
571
|
+
this._updateUI();
|
|
555
572
|
this._warnOnImageLayoutOptionConflict();
|
|
556
573
|
const transaction = this._captureLoadImageTransaction();
|
|
557
574
|
try {
|
|
558
575
|
const imageElement = await this._createImageElement(imageBase64);
|
|
559
576
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
560
577
|
let loadSource = imageBase64;
|
|
561
|
-
|
|
562
|
-
|
|
578
|
+
const downsampleMaxWidth = Number(this.options.downsampleMaxWidth);
|
|
579
|
+
const downsampleMaxHeight = Number(this.options.downsampleMaxHeight);
|
|
580
|
+
if (this.options.downsampleOnLoad && downsampleMaxWidth > 0 && downsampleMaxHeight > 0) {
|
|
581
|
+
const shouldResize = imageElement.naturalWidth > downsampleMaxWidth || imageElement.naturalHeight > downsampleMaxHeight;
|
|
563
582
|
if (shouldResize) {
|
|
564
583
|
const ratio = Math.min(
|
|
565
|
-
|
|
566
|
-
|
|
584
|
+
downsampleMaxWidth / imageElement.naturalWidth,
|
|
585
|
+
downsampleMaxHeight / imageElement.naturalHeight
|
|
567
586
|
);
|
|
568
587
|
const targetWidth = Math.round(imageElement.naturalWidth * ratio);
|
|
569
588
|
const targetHeight = Math.round(imageElement.naturalHeight * ratio);
|
|
@@ -571,10 +590,12 @@ var ImageEditor = class {
|
|
|
571
590
|
imageElement,
|
|
572
591
|
targetWidth,
|
|
573
592
|
targetHeight,
|
|
574
|
-
this.options.downsampleQuality,
|
|
593
|
+
this._normalizeQuality(this.options.downsampleQuality),
|
|
575
594
|
imageBase64
|
|
576
595
|
);
|
|
577
596
|
}
|
|
597
|
+
} else if (this.options.downsampleOnLoad) {
|
|
598
|
+
this._reportWarning("loadImage: downsample limits must be positive numbers; using the original image");
|
|
578
599
|
}
|
|
579
600
|
const fabricImage = await this._createFabricImageFromURL(loadSource);
|
|
580
601
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
@@ -639,6 +660,9 @@ var ImageEditor = class {
|
|
|
639
660
|
} catch (error) {
|
|
640
661
|
await this._rollbackLoadImageTransaction(transaction);
|
|
641
662
|
throw error;
|
|
663
|
+
} finally {
|
|
664
|
+
this._isLoading = false;
|
|
665
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
642
666
|
}
|
|
643
667
|
}
|
|
644
668
|
/**
|
|
@@ -649,6 +673,15 @@ var ImageEditor = class {
|
|
|
649
673
|
const fabricInstance2 = ensureFabric();
|
|
650
674
|
return !!(this.originalImage && fabricInstance2 && this.originalImage instanceof fabricInstance2.Image && this.originalImage.width > 0 && this.originalImage.height > 0);
|
|
651
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Checks whether the editor is in a temporary non-mutating state.
|
|
678
|
+
*
|
|
679
|
+
* @returns {boolean} True while loading, animating, cropping, or running a compound operation.
|
|
680
|
+
* @public
|
|
681
|
+
*/
|
|
682
|
+
isBusy() {
|
|
683
|
+
return !!(this.isAnimating || this._cropMode || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
|
|
684
|
+
}
|
|
652
685
|
/**
|
|
653
686
|
* Creates an HTMLImageElement from a given data URL.
|
|
654
687
|
*
|
|
@@ -720,7 +753,6 @@ var ImageEditor = class {
|
|
|
720
753
|
_captureLoadImageTransaction() {
|
|
721
754
|
return {
|
|
722
755
|
canvasState: this._serializeCanvasState(),
|
|
723
|
-
originalImage: this.originalImage,
|
|
724
756
|
baseImageScale: this.baseImageScale,
|
|
725
757
|
currentScale: this.currentScale,
|
|
726
758
|
currentRotation: this.currentRotation,
|
|
@@ -744,9 +776,14 @@ var ImageEditor = class {
|
|
|
744
776
|
}
|
|
745
777
|
async _rollbackLoadImageTransaction(transaction) {
|
|
746
778
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
779
|
+
let didRestoreCanvasState = false;
|
|
747
780
|
try {
|
|
748
|
-
if (transaction.canvasState)
|
|
781
|
+
if (transaction.canvasState) {
|
|
782
|
+
await this.loadFromState(transaction.canvasState);
|
|
783
|
+
didRestoreCanvasState = true;
|
|
784
|
+
}
|
|
749
785
|
} catch (error) {
|
|
786
|
+
this._lastMask = null;
|
|
750
787
|
this._reportError("loadImage rollback failed", error);
|
|
751
788
|
}
|
|
752
789
|
this.baseImageScale = transaction.baseImageScale;
|
|
@@ -755,22 +792,40 @@ var ImageEditor = class {
|
|
|
755
792
|
this.maskCounter = transaction.maskCounter;
|
|
756
793
|
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
757
794
|
this._lastSnapshot = transaction.lastSnapshot;
|
|
795
|
+
if (didRestoreCanvasState) {
|
|
796
|
+
this._restoreLastMaskReference(transaction.lastMask);
|
|
797
|
+
} else {
|
|
798
|
+
this._lastMask = null;
|
|
799
|
+
}
|
|
758
800
|
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
759
801
|
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
760
802
|
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
761
|
-
this._containerOriginalOverflow = transaction.containerOverflow;
|
|
762
803
|
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
763
804
|
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
764
805
|
if (this.containerElement) {
|
|
765
806
|
this.containerElement.scrollLeft = transaction.scrollLeft;
|
|
766
807
|
this.containerElement.scrollTop = transaction.scrollTop;
|
|
767
|
-
this.
|
|
808
|
+
this._restoreContainerOverflowSnapshot(transaction.containerOverflow);
|
|
768
809
|
}
|
|
769
810
|
this._updateInputs();
|
|
770
811
|
this._updateMaskList();
|
|
771
812
|
this._updateUI();
|
|
772
813
|
if (this.canvas) this.canvas.renderAll();
|
|
773
814
|
}
|
|
815
|
+
_restoreLastMaskReference(previousLastMask) {
|
|
816
|
+
if (!this.canvas) {
|
|
817
|
+
this._lastMask = null;
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
821
|
+
const previousMaskId = previousLastMask && previousLastMask.maskId;
|
|
822
|
+
this._lastMask = masks.find((mask) => mask.maskId === previousMaskId) || masks[masks.length - 1] || null;
|
|
823
|
+
if (!this._lastMask) {
|
|
824
|
+
this._lastMaskInitialLeft = null;
|
|
825
|
+
this._lastMaskInitialTop = null;
|
|
826
|
+
this._lastMaskInitialWidth = null;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
774
829
|
/**
|
|
775
830
|
* Resamples the given image element to a new width and height and returns the result as a data URL.
|
|
776
831
|
*
|
|
@@ -783,12 +838,19 @@ var ImageEditor = class {
|
|
|
783
838
|
* @private
|
|
784
839
|
*/
|
|
785
840
|
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
|
|
841
|
+
const sourceWidth = Math.max(1, Number(imageElement && (imageElement.naturalWidth || imageElement.width)) || 0);
|
|
842
|
+
const sourceHeight = Math.max(1, Number(imageElement && (imageElement.naturalHeight || imageElement.height)) || 0);
|
|
843
|
+
const safeTargetWidth = Math.round(Number(targetWidth));
|
|
844
|
+
const safeTargetHeight = Math.round(Number(targetHeight));
|
|
845
|
+
if (!Number.isFinite(safeTargetWidth) || !Number.isFinite(safeTargetHeight) || safeTargetWidth <= 0 || safeTargetHeight <= 0) {
|
|
846
|
+
throw new Error("Invalid image resample target dimensions");
|
|
847
|
+
}
|
|
786
848
|
const offscreenCanvas = document.createElement("canvas");
|
|
787
|
-
offscreenCanvas.width =
|
|
788
|
-
offscreenCanvas.height =
|
|
849
|
+
offscreenCanvas.width = safeTargetWidth;
|
|
850
|
+
offscreenCanvas.height = safeTargetHeight;
|
|
789
851
|
const context = offscreenCanvas.getContext("2d");
|
|
790
852
|
if (!context) throw new Error("2D canvas context is unavailable");
|
|
791
|
-
context.drawImage(imageElement, 0, 0,
|
|
853
|
+
context.drawImage(imageElement, 0, 0, sourceWidth, sourceHeight, 0, 0, safeTargetWidth, safeTargetHeight);
|
|
792
854
|
return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
|
|
793
855
|
}
|
|
794
856
|
_getDataUrlMimeType(dataUrl) {
|
|
@@ -1054,7 +1116,11 @@ var ImageEditor = class {
|
|
|
1054
1116
|
maskStyleBackups.push(backup);
|
|
1055
1117
|
mask.set(stylePatch);
|
|
1056
1118
|
});
|
|
1057
|
-
|
|
1119
|
+
const result = callback();
|
|
1120
|
+
if (result && typeof result.then === "function") {
|
|
1121
|
+
throw new Error("_withNormalizedMaskStyles callback must be synchronous");
|
|
1122
|
+
}
|
|
1123
|
+
return result;
|
|
1058
1124
|
} finally {
|
|
1059
1125
|
maskStyleBackups.forEach((backup) => {
|
|
1060
1126
|
try {
|
|
@@ -1122,9 +1188,13 @@ var ImageEditor = class {
|
|
|
1122
1188
|
* @returns {number} A finite quality value between 0 and 1.
|
|
1123
1189
|
* @private
|
|
1124
1190
|
*/
|
|
1125
|
-
_normalizeQuality(quality) {
|
|
1191
|
+
_normalizeQuality(quality, fallback = void 0) {
|
|
1192
|
+
const fallbackQuality = fallback == null ? this.options.downsampleQuality : fallback;
|
|
1193
|
+
const numericFallback = fallbackQuality == null ? NaN : Number(fallbackQuality);
|
|
1194
|
+
const safeFallback = Number.isFinite(numericFallback) ? Math.max(0, Math.min(1, numericFallback)) : 0.92;
|
|
1195
|
+
if (quality == null) return safeFallback;
|
|
1126
1196
|
const numericQuality = Number(quality);
|
|
1127
|
-
if (!Number.isFinite(numericQuality)) return
|
|
1197
|
+
if (!Number.isFinite(numericQuality)) return safeFallback;
|
|
1128
1198
|
return Math.max(0, Math.min(1, numericQuality));
|
|
1129
1199
|
}
|
|
1130
1200
|
/**
|
|
@@ -1175,64 +1245,63 @@ var ImageEditor = class {
|
|
|
1175
1245
|
sourceHeight: Math.max(1, endY - sourceY)
|
|
1176
1246
|
};
|
|
1177
1247
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
});
|
|
1248
|
+
_hasFractionalCanvasEdge(value) {
|
|
1249
|
+
const numericValue = Number(value);
|
|
1250
|
+
if (!Number.isFinite(numericValue)) return false;
|
|
1251
|
+
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1252
|
+
}
|
|
1253
|
+
_getPartialExportEdges(bounds) {
|
|
1254
|
+
if (!bounds) return null;
|
|
1255
|
+
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
1256
|
+
const isAxisAligned = angle < 0.01 || Math.abs(angle - 90) < 0.01;
|
|
1257
|
+
if (!isAxisAligned) return null;
|
|
1258
|
+
return {
|
|
1259
|
+
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1260
|
+
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1261
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1262
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
async _sealPartialTransparentEdges(dataUrl, edges) {
|
|
1266
|
+
if (!edges || !Object.values(edges).some(Boolean)) return dataUrl;
|
|
1267
|
+
const imageElement = await this._createImageElement(dataUrl);
|
|
1268
|
+
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
1269
|
+
const height = Math.max(1, imageElement.naturalHeight || imageElement.height || 1);
|
|
1270
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
1271
|
+
offscreenCanvas.width = width;
|
|
1272
|
+
offscreenCanvas.height = height;
|
|
1273
|
+
const context = offscreenCanvas.getContext("2d");
|
|
1274
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
1275
|
+
context.drawImage(imageElement, 0, 0, width, height);
|
|
1276
|
+
const imageData = context.getImageData(0, 0, width, height);
|
|
1277
|
+
const pixels = imageData.data;
|
|
1278
|
+
const sealPixel = (x, y, fallbackX, fallbackY) => {
|
|
1279
|
+
const index = (y * width + x) * 4;
|
|
1280
|
+
const fallbackIndex = (fallbackY * width + fallbackX) * 4;
|
|
1281
|
+
if (pixels[index + 3] === 0 && pixels[fallbackIndex + 3] > 0) {
|
|
1282
|
+
pixels[index] = pixels[fallbackIndex];
|
|
1283
|
+
pixels[index + 1] = pixels[fallbackIndex + 1];
|
|
1284
|
+
pixels[index + 2] = pixels[fallbackIndex + 2];
|
|
1285
|
+
pixels[index + 3] = pixels[fallbackIndex + 3];
|
|
1286
|
+
}
|
|
1287
|
+
if (pixels[index + 3] > 0 && pixels[index + 3] < 255) {
|
|
1288
|
+
pixels[index + 3] = 255;
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
if (edges.left && width > 1) {
|
|
1292
|
+
for (let y = 0; y < height; y += 1) sealPixel(0, y, 1, y);
|
|
1293
|
+
}
|
|
1294
|
+
if (edges.right && width > 1) {
|
|
1295
|
+
for (let y = 0; y < height; y += 1) sealPixel(width - 1, y, width - 2, y);
|
|
1296
|
+
}
|
|
1297
|
+
if (edges.top && height > 1) {
|
|
1298
|
+
for (let x = 0; x < width; x += 1) sealPixel(x, 0, x, 1);
|
|
1299
|
+
}
|
|
1300
|
+
if (edges.bottom && height > 1) {
|
|
1301
|
+
for (let x = 0; x < width; x += 1) sealPixel(x, height - 1, x, height - 2);
|
|
1302
|
+
}
|
|
1303
|
+
context.putImageData(imageData, 0, 0);
|
|
1304
|
+
return offscreenCanvas.toDataURL("image/png");
|
|
1236
1305
|
}
|
|
1237
1306
|
/**
|
|
1238
1307
|
* Exports a source region directly through Fabric's region export options.
|
|
@@ -1245,13 +1314,16 @@ var ImageEditor = class {
|
|
|
1245
1314
|
* @param {number} [region.multiplier=1] - Export multiplier.
|
|
1246
1315
|
* @param {number} [region.quality=0.92] - Output image quality for lossy formats.
|
|
1247
1316
|
* @param {'jpeg'|'png'|'webp'} [region.format='jpeg'] - Output image format.
|
|
1317
|
+
* @param {Object|null} [region.sealPartialEdges=null] - Fractional canvas edges whose alpha should be sealed.
|
|
1248
1318
|
* @returns {Promise<string>} Resolves with an image data URL for the cropped region.
|
|
1249
1319
|
* @private
|
|
1250
1320
|
*/
|
|
1251
|
-
_exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg" }) {
|
|
1321
|
+
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg", sealPartialEdges = null }) {
|
|
1252
1322
|
const safeMultiplier = Math.max(1, Number(multiplier) || 1);
|
|
1253
|
-
|
|
1254
|
-
|
|
1323
|
+
const safeFormat = this._normalizeImageFormat(format);
|
|
1324
|
+
const exportFormat = safeFormat === "jpeg" ? "png" : safeFormat;
|
|
1325
|
+
let regionDataUrl = this.canvas.toDataURL({
|
|
1326
|
+
format: exportFormat,
|
|
1255
1327
|
quality,
|
|
1256
1328
|
multiplier: safeMultiplier,
|
|
1257
1329
|
left: sourceX,
|
|
@@ -1259,6 +1331,61 @@ var ImageEditor = class {
|
|
|
1259
1331
|
width: sourceWidth,
|
|
1260
1332
|
height: sourceHeight
|
|
1261
1333
|
});
|
|
1334
|
+
regionDataUrl = await this._sealPartialTransparentEdges(regionDataUrl, sealPartialEdges);
|
|
1335
|
+
if (safeFormat !== "jpeg") return regionDataUrl;
|
|
1336
|
+
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1337
|
+
}
|
|
1338
|
+
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1339
|
+
const imageElement = await this._createImageElement(dataUrl);
|
|
1340
|
+
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
1341
|
+
const height = Math.max(1, imageElement.naturalHeight || imageElement.height || 1);
|
|
1342
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
1343
|
+
offscreenCanvas.width = width;
|
|
1344
|
+
offscreenCanvas.height = height;
|
|
1345
|
+
const context = offscreenCanvas.getContext("2d");
|
|
1346
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
1347
|
+
context.fillStyle = this._getJpegBackgroundColor();
|
|
1348
|
+
context.fillRect(0, 0, width, height);
|
|
1349
|
+
context.drawImage(imageElement, 0, 0, width, height);
|
|
1350
|
+
return offscreenCanvas.toDataURL("image/jpeg", this._normalizeQuality(quality));
|
|
1351
|
+
}
|
|
1352
|
+
_getJpegBackgroundColor() {
|
|
1353
|
+
const backgroundColor = String(this.options.backgroundColor || "").trim();
|
|
1354
|
+
if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return "#ffffff";
|
|
1355
|
+
return backgroundColor;
|
|
1356
|
+
}
|
|
1357
|
+
_isTransparentCssColor(color) {
|
|
1358
|
+
const normalizedColor = String(color || "").trim().toLowerCase();
|
|
1359
|
+
if (!normalizedColor || normalizedColor === "transparent") return true;
|
|
1360
|
+
const hexAlphaMatch = normalizedColor.match(/^#(?:[0-9a-f]{3}([0-9a-f])|[0-9a-f]{6}([0-9a-f]{2}))$/i);
|
|
1361
|
+
if (hexAlphaMatch) {
|
|
1362
|
+
const alpha = hexAlphaMatch[1] || hexAlphaMatch[2];
|
|
1363
|
+
return alpha === "0" || alpha === "00";
|
|
1364
|
+
}
|
|
1365
|
+
const slashAlphaMatch = normalizedColor.match(/^(?:rgba?|hsla?)\([^)]*\/\s*([^)]+)\)$/i);
|
|
1366
|
+
if (slashAlphaMatch) return this._isZeroCssAlpha(slashAlphaMatch[1]);
|
|
1367
|
+
const commaAlphaMatch = normalizedColor.match(/^(?:rgba|hsla)\((.*)\)$/i);
|
|
1368
|
+
if (commaAlphaMatch) {
|
|
1369
|
+
const parts = commaAlphaMatch[1].split(",");
|
|
1370
|
+
if (parts.length >= 4) return this._isZeroCssAlpha(parts[parts.length - 1]);
|
|
1371
|
+
}
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
_isZeroCssAlpha(alphaValue) {
|
|
1375
|
+
const normalizedAlpha = String(alphaValue || "").trim();
|
|
1376
|
+
if (!normalizedAlpha) return false;
|
|
1377
|
+
if (normalizedAlpha.endsWith("%")) return Number.parseFloat(normalizedAlpha) === 0;
|
|
1378
|
+
return Number(normalizedAlpha) === 0;
|
|
1379
|
+
}
|
|
1380
|
+
_decodeBase64Payload(base64Payload) {
|
|
1381
|
+
const payload = String(base64Payload || "");
|
|
1382
|
+
if (typeof atob === "function") {
|
|
1383
|
+
return Uint8Array.from(atob(payload), (char) => char.charCodeAt(0));
|
|
1384
|
+
}
|
|
1385
|
+
if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
|
|
1386
|
+
return new Uint8Array(Buffer.from(payload, "base64"));
|
|
1387
|
+
}
|
|
1388
|
+
throw new Error("Base64 decoding is unavailable");
|
|
1262
1389
|
}
|
|
1263
1390
|
/**
|
|
1264
1391
|
* Gets the top-left corner coordinates of the given object.
|
|
@@ -1416,17 +1543,70 @@ var ImageEditor = class {
|
|
|
1416
1543
|
* @public
|
|
1417
1544
|
*/
|
|
1418
1545
|
scaleImage(factor, options = {}) {
|
|
1419
|
-
|
|
1546
|
+
try {
|
|
1547
|
+
this._assertCanQueueAnimation("scaleImage", options);
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
return Promise.reject(error);
|
|
1550
|
+
}
|
|
1551
|
+
return this.animationQueue.add(() => this._scaleImageImpl(factor, options)).finally(() => {
|
|
1552
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
_getInternalOperationToken(options) {
|
|
1556
|
+
return options && options[INTERNAL_OPERATION_TOKEN];
|
|
1557
|
+
}
|
|
1558
|
+
_isOwnInternalOperation(options) {
|
|
1559
|
+
const token = this._getInternalOperationToken(options);
|
|
1560
|
+
return !!token && token === this._activeOperationToken;
|
|
1420
1561
|
}
|
|
1421
|
-
|
|
1562
|
+
_beginBusyOperation(operationName) {
|
|
1563
|
+
const token = Symbol(operationName);
|
|
1564
|
+
this._activeOperationName = operationName;
|
|
1565
|
+
this._activeOperationToken = token;
|
|
1566
|
+
this._updateUI();
|
|
1567
|
+
return token;
|
|
1568
|
+
}
|
|
1569
|
+
_endBusyOperation(token) {
|
|
1570
|
+
if (token && token === this._activeOperationToken) {
|
|
1571
|
+
this._activeOperationName = null;
|
|
1572
|
+
this._activeOperationToken = null;
|
|
1573
|
+
this._updateUI();
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
_withInternalOperationOptions(token, options = {}) {
|
|
1577
|
+
return {
|
|
1578
|
+
...options,
|
|
1579
|
+
[INTERNAL_OPERATION_TOKEN]: token
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
_assertEditorAvailable(operationName) {
|
|
1422
1583
|
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1584
|
+
}
|
|
1585
|
+
_assertIdleForOperation(operationName, options = {}) {
|
|
1586
|
+
this._assertEditorAvailable(operationName);
|
|
1587
|
+
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
1423
1588
|
if (this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) {
|
|
1424
1589
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1425
1590
|
}
|
|
1591
|
+
if (this._isLoading && !isOwnInternalOperation) {
|
|
1592
|
+
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1593
|
+
}
|
|
1594
|
+
if (this._activeOperationToken && !isOwnInternalOperation) {
|
|
1595
|
+
throw new Error(`${operationName} cannot run while ${this._activeOperationName || "another operation"} is running`);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
_assertCanQueueAnimation(operationName, options = {}) {
|
|
1599
|
+
this._assertEditorAvailable(operationName);
|
|
1600
|
+
if (this._isLoading && !this._isOwnInternalOperation(options)) {
|
|
1601
|
+
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1602
|
+
}
|
|
1603
|
+
if (this._activeOperationToken && !this._isOwnInternalOperation(options)) {
|
|
1604
|
+
throw new Error(`${operationName} cannot run while ${this._activeOperationName || "another operation"} is running`);
|
|
1605
|
+
}
|
|
1426
1606
|
}
|
|
1427
|
-
_canMutateNow(operationName) {
|
|
1607
|
+
_canMutateNow(operationName, options = {}) {
|
|
1428
1608
|
try {
|
|
1429
|
-
this._assertIdleForOperation(operationName);
|
|
1609
|
+
this._assertIdleForOperation(operationName, options);
|
|
1430
1610
|
return true;
|
|
1431
1611
|
} catch (error) {
|
|
1432
1612
|
this._reportError(`${operationName} blocked`, error);
|
|
@@ -1531,7 +1711,14 @@ var ImageEditor = class {
|
|
|
1531
1711
|
* @public
|
|
1532
1712
|
*/
|
|
1533
1713
|
rotateImage(degrees, options = {}) {
|
|
1534
|
-
|
|
1714
|
+
try {
|
|
1715
|
+
this._assertCanQueueAnimation("rotateImage", options);
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
return Promise.reject(error);
|
|
1718
|
+
}
|
|
1719
|
+
return this.animationQueue.add(() => this._rotateImageImpl(degrees, options)).finally(() => {
|
|
1720
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1721
|
+
});
|
|
1535
1722
|
}
|
|
1536
1723
|
/**
|
|
1537
1724
|
* Rotates the original image by a given number of degrees, with animation.
|
|
@@ -1593,12 +1780,19 @@ var ImageEditor = class {
|
|
|
1593
1780
|
*/
|
|
1594
1781
|
resetImageTransform() {
|
|
1595
1782
|
if (!this.originalImage) return Promise.resolve();
|
|
1783
|
+
try {
|
|
1784
|
+
this._assertCanQueueAnimation("resetImageTransform");
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
return Promise.reject(error);
|
|
1787
|
+
}
|
|
1596
1788
|
return this.animationQueue.add(async () => {
|
|
1597
1789
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1598
1790
|
await this._scaleImageImpl(1, { saveHistory: false });
|
|
1599
1791
|
await this._rotateImageImpl(0, { saveHistory: false });
|
|
1600
1792
|
const after = this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1601
1793
|
this._pushStateTransition(before, after);
|
|
1794
|
+
}).finally(() => {
|
|
1795
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1602
1796
|
}).catch((error) => {
|
|
1603
1797
|
this._reportError("resetImageTransform() failed", error);
|
|
1604
1798
|
throw error;
|
|
@@ -1635,6 +1829,9 @@ var ImageEditor = class {
|
|
|
1635
1829
|
try {
|
|
1636
1830
|
const state = typeof serializedState === "string" ? JSON.parse(serializedState) : serializedState;
|
|
1637
1831
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
1832
|
+
if (editorMetadata && Object.prototype.hasOwnProperty.call(editorMetadata, "version") && Number(editorMetadata.version) !== 1) {
|
|
1833
|
+
this._reportWarning(`loadFromState: unsupported editor metadata version ${editorMetadata.version}`);
|
|
1834
|
+
}
|
|
1638
1835
|
this.canvas.loadFromJSON(state, async () => {
|
|
1639
1836
|
try {
|
|
1640
1837
|
if (this._disposed || !this.canvas) {
|
|
@@ -1713,22 +1910,46 @@ var ImageEditor = class {
|
|
|
1713
1910
|
}
|
|
1714
1911
|
_waitForImageElementReady(imageElement) {
|
|
1715
1912
|
if (!imageElement) return Promise.resolve();
|
|
1716
|
-
|
|
1913
|
+
const hasLoadedDimensions = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) && (Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
1914
|
+
if (hasLoadedDimensions) return Promise.resolve();
|
|
1915
|
+
if (imageElement.complete) return Promise.reject(new Error("Image could not be loaded while restoring state"));
|
|
1717
1916
|
return new Promise((resolve, reject) => {
|
|
1718
1917
|
let isSettled = false;
|
|
1719
|
-
|
|
1720
|
-
settle(() => reject(new Error("Image load timed out while restoring state")));
|
|
1721
|
-
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
1918
|
+
let timerId;
|
|
1722
1919
|
const settle = (callback) => {
|
|
1723
1920
|
if (isSettled) return;
|
|
1724
1921
|
isSettled = true;
|
|
1725
1922
|
clearTimeout(timerId);
|
|
1726
|
-
imageElement.
|
|
1727
|
-
|
|
1923
|
+
if (typeof imageElement.removeEventListener === "function") {
|
|
1924
|
+
imageElement.removeEventListener("load", handleLoad);
|
|
1925
|
+
imageElement.removeEventListener("error", handleError);
|
|
1926
|
+
} else {
|
|
1927
|
+
imageElement.onload = null;
|
|
1928
|
+
imageElement.onerror = null;
|
|
1929
|
+
}
|
|
1728
1930
|
callback();
|
|
1729
1931
|
};
|
|
1730
|
-
|
|
1731
|
-
|
|
1932
|
+
const handleLoad = () => {
|
|
1933
|
+
const didLoad = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) && (Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
1934
|
+
settle(() => {
|
|
1935
|
+
if (didLoad) {
|
|
1936
|
+
resolve();
|
|
1937
|
+
} else {
|
|
1938
|
+
reject(new Error("Image could not be loaded while restoring state"));
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
};
|
|
1942
|
+
const handleError = (error) => settle(() => reject(error instanceof Error ? error : new Error("Image could not be loaded while restoring state")));
|
|
1943
|
+
timerId = setTimeout(() => {
|
|
1944
|
+
settle(() => reject(new Error("Image load timed out while restoring state")));
|
|
1945
|
+
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
1946
|
+
if (typeof imageElement.addEventListener === "function") {
|
|
1947
|
+
imageElement.addEventListener("load", handleLoad, { once: true });
|
|
1948
|
+
imageElement.addEventListener("error", handleError, { once: true });
|
|
1949
|
+
} else {
|
|
1950
|
+
imageElement.onload = handleLoad;
|
|
1951
|
+
imageElement.onerror = handleError;
|
|
1952
|
+
}
|
|
1732
1953
|
});
|
|
1733
1954
|
}
|
|
1734
1955
|
/**
|
|
@@ -1993,6 +2214,10 @@ var ImageEditor = class {
|
|
|
1993
2214
|
});
|
|
1994
2215
|
}
|
|
1995
2216
|
}
|
|
2217
|
+
if (!mask || typeof mask.set !== "function" || typeof mask.setCoords !== "function") {
|
|
2218
|
+
this._reportWarning("fabricGenerator returned an invalid Fabric object");
|
|
2219
|
+
return null;
|
|
2220
|
+
}
|
|
1996
2221
|
const styles = maskConfig.styles || {};
|
|
1997
2222
|
const hasStyle = (property) => Object.prototype.hasOwnProperty.call(styles, property);
|
|
1998
2223
|
const maskSettings = {
|
|
@@ -2081,7 +2306,7 @@ var ImageEditor = class {
|
|
|
2081
2306
|
*/
|
|
2082
2307
|
removeAllMasks(options = {}) {
|
|
2083
2308
|
if (!this.canvas) return;
|
|
2084
|
-
if (!this._canMutateNow("removeAllMasks")) return;
|
|
2309
|
+
if (!this._canMutateNow("removeAllMasks", options)) return;
|
|
2085
2310
|
const saveHistory = options.saveHistory !== false;
|
|
2086
2311
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2087
2312
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
@@ -2120,6 +2345,93 @@ var ImageEditor = class {
|
|
|
2120
2345
|
}
|
|
2121
2346
|
}
|
|
2122
2347
|
}
|
|
2348
|
+
_captureMaskLabelBackups(masks) {
|
|
2349
|
+
if (!this.canvas) return [];
|
|
2350
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2351
|
+
return (masks || []).map((mask) => {
|
|
2352
|
+
const label = mask && mask.__label ? mask.__label : null;
|
|
2353
|
+
return {
|
|
2354
|
+
mask,
|
|
2355
|
+
label,
|
|
2356
|
+
hadLabel: !!label,
|
|
2357
|
+
labelInCanvas: !!label && canvasObjects.has(label),
|
|
2358
|
+
visible: label ? label.visible : void 0
|
|
2359
|
+
};
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
_restoreMaskLabelBackups(labelBackups) {
|
|
2363
|
+
if (!this.canvas || !Array.isArray(labelBackups)) return;
|
|
2364
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2365
|
+
labelBackups.forEach((backup) => {
|
|
2366
|
+
if (!backup || !backup.mask) return;
|
|
2367
|
+
try {
|
|
2368
|
+
if (!backup.hadLabel) {
|
|
2369
|
+
if (backup.mask.__label) this._removeLabelForMask(backup.mask);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
backup.mask.__label = backup.label;
|
|
2373
|
+
if (!backup.label) return;
|
|
2374
|
+
if (backup.labelInCanvas && !canvasObjects.has(backup.label)) {
|
|
2375
|
+
this.canvas.add(backup.label);
|
|
2376
|
+
canvasObjects.add(backup.label);
|
|
2377
|
+
}
|
|
2378
|
+
if (backup.visible !== void 0) backup.label.set({ visible: backup.visible });
|
|
2379
|
+
if (backup.labelInCanvas) this.canvas.bringToFront(backup.label);
|
|
2380
|
+
this._syncMaskLabel(backup.mask);
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
void error;
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
_captureActiveObjectBackup() {
|
|
2387
|
+
if (!this.canvas) return null;
|
|
2388
|
+
const activeObject = this.canvas.getActiveObject();
|
|
2389
|
+
if (!activeObject) return null;
|
|
2390
|
+
const selectedObjects = typeof activeObject.getObjects === "function" ? activeObject.getObjects() : [activeObject];
|
|
2391
|
+
return { activeObject, selectedObjects };
|
|
2392
|
+
}
|
|
2393
|
+
_restoreActiveObjectBackup(activeObjectBackup) {
|
|
2394
|
+
if (!this.canvas || !activeObjectBackup || !activeObjectBackup.activeObject) return;
|
|
2395
|
+
const canvasObjects = this.canvas.getObjects();
|
|
2396
|
+
const selectedObjects = Array.isArray(activeObjectBackup.selectedObjects) ? activeObjectBackup.selectedObjects : [];
|
|
2397
|
+
const canRestore = selectedObjects.length ? selectedObjects.every((object) => canvasObjects.includes(object)) : canvasObjects.includes(activeObjectBackup.activeObject);
|
|
2398
|
+
if (!canRestore) return;
|
|
2399
|
+
try {
|
|
2400
|
+
this.canvas.setActiveObject(activeObjectBackup.activeObject);
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
void error;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
_captureMaskExportBackups(masks) {
|
|
2406
|
+
return (masks || []).map((mask) => ({
|
|
2407
|
+
object: mask,
|
|
2408
|
+
visible: mask.visible,
|
|
2409
|
+
opacity: mask.opacity,
|
|
2410
|
+
fill: mask.fill,
|
|
2411
|
+
strokeWidth: mask.strokeWidth,
|
|
2412
|
+
stroke: mask.stroke,
|
|
2413
|
+
selectable: mask.selectable,
|
|
2414
|
+
lockRotation: mask.lockRotation
|
|
2415
|
+
}));
|
|
2416
|
+
}
|
|
2417
|
+
_restoreMaskExportBackups(maskBackups) {
|
|
2418
|
+
(maskBackups || []).forEach((backup) => {
|
|
2419
|
+
try {
|
|
2420
|
+
backup.object.set({
|
|
2421
|
+
visible: backup.visible,
|
|
2422
|
+
opacity: backup.opacity,
|
|
2423
|
+
fill: backup.fill,
|
|
2424
|
+
strokeWidth: backup.strokeWidth,
|
|
2425
|
+
stroke: backup.stroke,
|
|
2426
|
+
selectable: backup.selectable,
|
|
2427
|
+
lockRotation: backup.lockRotation
|
|
2428
|
+
});
|
|
2429
|
+
backup.object.setCoords();
|
|
2430
|
+
} catch (error) {
|
|
2431
|
+
void error;
|
|
2432
|
+
}
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2123
2435
|
/**
|
|
2124
2436
|
* Returns a stable zero-based creation index for label callbacks.
|
|
2125
2437
|
*
|
|
@@ -2192,10 +2504,14 @@ var ImageEditor = class {
|
|
|
2192
2504
|
_hideAllMaskLabels() {
|
|
2193
2505
|
if (!this.canvas) return;
|
|
2194
2506
|
const canvasObjects = this.canvas.getObjects();
|
|
2507
|
+
const canvasObjectSet = new Set(canvasObjects);
|
|
2195
2508
|
const labels = canvasObjects.filter((object) => object.maskLabel);
|
|
2196
2509
|
labels.forEach((label) => {
|
|
2197
2510
|
try {
|
|
2198
|
-
if (
|
|
2511
|
+
if (canvasObjectSet.has(label)) {
|
|
2512
|
+
this.canvas.remove(label);
|
|
2513
|
+
canvasObjectSet.delete(label);
|
|
2514
|
+
}
|
|
2199
2515
|
} catch (error) {
|
|
2200
2516
|
void error;
|
|
2201
2517
|
}
|
|
@@ -2354,18 +2670,36 @@ var ImageEditor = class {
|
|
|
2354
2670
|
this._assertIdleForOperation("mergeMasks");
|
|
2355
2671
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2356
2672
|
if (!masks.length) return;
|
|
2673
|
+
const beforeJson = this._serializeCanvasState();
|
|
2674
|
+
const operationToken = this._beginBusyOperation("mergeMasks");
|
|
2357
2675
|
this.canvas.discardActiveObject();
|
|
2358
2676
|
this.canvas.renderAll();
|
|
2359
2677
|
try {
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2678
|
+
const merged = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
2679
|
+
exportImageArea: true,
|
|
2680
|
+
multiplier: this.options.exportMultiplier,
|
|
2681
|
+
fileType: "png"
|
|
2682
|
+
}));
|
|
2683
|
+
this.removeAllMasks(this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2684
|
+
if (this.canvas.getObjects().some((object) => object.maskId)) {
|
|
2685
|
+
throw new Error("Masks could not be removed during merge");
|
|
2686
|
+
}
|
|
2687
|
+
await this.loadImage(merged, this._withInternalOperationOptions(operationToken, {
|
|
2688
|
+
preserveScroll: true,
|
|
2689
|
+
resetMaskCounter: false
|
|
2690
|
+
}));
|
|
2364
2691
|
const afterJson = this._serializeCanvasState();
|
|
2365
2692
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2366
2693
|
} catch (error) {
|
|
2367
2694
|
this._reportError("merge error", error);
|
|
2695
|
+
try {
|
|
2696
|
+
await this.loadFromState(beforeJson);
|
|
2697
|
+
} catch (restoreError) {
|
|
2698
|
+
this._reportError("mergeMasks rollback failed", restoreError);
|
|
2699
|
+
}
|
|
2368
2700
|
throw error;
|
|
2701
|
+
} finally {
|
|
2702
|
+
this._endBusyOperation(operationToken);
|
|
2369
2703
|
}
|
|
2370
2704
|
}
|
|
2371
2705
|
/**
|
|
@@ -2416,14 +2750,18 @@ var ImageEditor = class {
|
|
|
2416
2750
|
*/
|
|
2417
2751
|
async exportImageBase64(options = {}) {
|
|
2418
2752
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
2419
|
-
this._assertIdleForOperation("exportImageBase64");
|
|
2753
|
+
this._assertIdleForOperation("exportImageBase64", options);
|
|
2420
2754
|
const exportImageArea = typeof options.exportImageArea === "boolean" ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2421
2755
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2422
2756
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
2423
2757
|
const format = this._normalizeImageFormat(options.fileType || options.format);
|
|
2424
2758
|
if (!exportImageArea) {
|
|
2425
2759
|
const masks2 = this.canvas.getObjects().filter((object) => object.maskId || object.maskLabel);
|
|
2760
|
+
const editableMasks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2426
2761
|
const maskVisibilityBackups = masks2.map((mask) => ({ object: mask, visible: mask.visible }));
|
|
2762
|
+
const maskStyleBackups2 = this._captureMaskExportBackups(editableMasks);
|
|
2763
|
+
const labelBackups2 = this._captureMaskLabelBackups(editableMasks);
|
|
2764
|
+
const activeObjectBackup2 = this._captureActiveObjectBackup();
|
|
2427
2765
|
try {
|
|
2428
2766
|
masks2.forEach((mask) => {
|
|
2429
2767
|
mask.set({ visible: false });
|
|
@@ -2432,12 +2770,13 @@ var ImageEditor = class {
|
|
|
2432
2770
|
this.canvas.renderAll();
|
|
2433
2771
|
this.originalImage.setCoords();
|
|
2434
2772
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2435
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds
|
|
2436
|
-
return this._exportCanvasRegionToDataURL({
|
|
2773
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2774
|
+
return await this._exportCanvasRegionToDataURL({
|
|
2437
2775
|
...exportRegion,
|
|
2438
2776
|
multiplier,
|
|
2439
2777
|
quality,
|
|
2440
|
-
format
|
|
2778
|
+
format,
|
|
2779
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2441
2780
|
});
|
|
2442
2781
|
} finally {
|
|
2443
2782
|
maskVisibilityBackups.forEach((backup) => {
|
|
@@ -2447,19 +2786,16 @@ var ImageEditor = class {
|
|
|
2447
2786
|
void error;
|
|
2448
2787
|
}
|
|
2449
2788
|
});
|
|
2789
|
+
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
2790
|
+
this._restoreMaskLabelBackups(labelBackups2);
|
|
2791
|
+
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
2450
2792
|
this.canvas.renderAll();
|
|
2451
2793
|
}
|
|
2452
2794
|
}
|
|
2453
2795
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2454
|
-
const maskStyleBackups =
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
fill: mask.fill,
|
|
2458
|
-
strokeWidth: mask.strokeWidth,
|
|
2459
|
-
stroke: mask.stroke,
|
|
2460
|
-
selectable: mask.selectable,
|
|
2461
|
-
lockRotation: mask.lockRotation
|
|
2462
|
-
}));
|
|
2796
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
2797
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
2798
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2463
2799
|
let finalBase64;
|
|
2464
2800
|
try {
|
|
2465
2801
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
@@ -2472,29 +2808,18 @@ var ImageEditor = class {
|
|
|
2472
2808
|
this.canvas.renderAll();
|
|
2473
2809
|
this.originalImage.setCoords();
|
|
2474
2810
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2475
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds
|
|
2476
|
-
finalBase64 = this._exportCanvasRegionToDataURL({
|
|
2811
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2812
|
+
finalBase64 = await this._exportCanvasRegionToDataURL({
|
|
2477
2813
|
...exportRegion,
|
|
2478
2814
|
multiplier,
|
|
2479
2815
|
quality,
|
|
2480
|
-
format
|
|
2816
|
+
format,
|
|
2817
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2481
2818
|
});
|
|
2482
2819
|
} finally {
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
opacity: backup.opacity,
|
|
2487
|
-
fill: backup.fill,
|
|
2488
|
-
strokeWidth: backup.strokeWidth,
|
|
2489
|
-
stroke: backup.stroke,
|
|
2490
|
-
selectable: backup.selectable,
|
|
2491
|
-
lockRotation: backup.lockRotation
|
|
2492
|
-
});
|
|
2493
|
-
backup.object.setCoords();
|
|
2494
|
-
} catch (error) {
|
|
2495
|
-
void error;
|
|
2496
|
-
}
|
|
2497
|
-
});
|
|
2820
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
2821
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
2822
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2498
2823
|
this.canvas.renderAll();
|
|
2499
2824
|
}
|
|
2500
2825
|
return finalBase64;
|
|
@@ -2538,19 +2863,20 @@ var ImageEditor = class {
|
|
|
2538
2863
|
fileName = this.options.defaultDownloadFileName ?? "exported_image.jpg"
|
|
2539
2864
|
} = options;
|
|
2540
2865
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
2866
|
+
const normalizedQuality = this._normalizeQuality(quality);
|
|
2541
2867
|
let imageBase64;
|
|
2542
2868
|
if (mergeMask) {
|
|
2543
2869
|
imageBase64 = await this.exportImageBase64({
|
|
2544
2870
|
exportImageArea: true,
|
|
2545
2871
|
multiplier,
|
|
2546
|
-
quality,
|
|
2872
|
+
quality: normalizedQuality,
|
|
2547
2873
|
fileType: safeFileType
|
|
2548
2874
|
});
|
|
2549
2875
|
} else {
|
|
2550
2876
|
imageBase64 = await this.exportImageBase64({
|
|
2551
2877
|
exportImageArea: false,
|
|
2552
2878
|
multiplier,
|
|
2553
|
-
quality,
|
|
2879
|
+
quality: normalizedQuality,
|
|
2554
2880
|
fileType: safeFileType
|
|
2555
2881
|
});
|
|
2556
2882
|
}
|
|
@@ -2567,7 +2893,7 @@ var ImageEditor = class {
|
|
|
2567
2893
|
const context = offscreenCanvas.getContext("2d");
|
|
2568
2894
|
if (!context) throw new Error("Unable to create 2D canvas context for export conversion");
|
|
2569
2895
|
context.drawImage(imageElement, 0, 0);
|
|
2570
|
-
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`,
|
|
2896
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
2571
2897
|
resolve(convertedDataUrl);
|
|
2572
2898
|
} catch (error) {
|
|
2573
2899
|
reject(error);
|
|
@@ -2577,13 +2903,8 @@ var ImageEditor = class {
|
|
|
2577
2903
|
imageElement.src = imageBase64;
|
|
2578
2904
|
});
|
|
2579
2905
|
}
|
|
2580
|
-
const
|
|
2906
|
+
const bytes = this._decodeBase64Payload(imageDataUrl.split(",")[1]);
|
|
2581
2907
|
const mime = `image/${safeFileType}`;
|
|
2582
|
-
let byteIndex = binaryString.length;
|
|
2583
|
-
const bytes = new Uint8Array(byteIndex);
|
|
2584
|
-
while (byteIndex--) {
|
|
2585
|
-
bytes[byteIndex] = binaryString.charCodeAt(byteIndex);
|
|
2586
|
-
}
|
|
2587
2908
|
return new File([bytes], fileName, { type: mime });
|
|
2588
2909
|
}
|
|
2589
2910
|
_clearMaskPlacementMemory() {
|
|
@@ -2730,6 +3051,30 @@ var ImageEditor = class {
|
|
|
2730
3051
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
2731
3052
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
2732
3053
|
cropRect.setCoords();
|
|
3054
|
+
const cropBounds = cropRect.getBoundingRect(true, true);
|
|
3055
|
+
const imageLeft = Number(imageBounds.left) || 0;
|
|
3056
|
+
const imageTop = Number(imageBounds.top) || 0;
|
|
3057
|
+
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
3058
|
+
const imageBottom = imageTop + (Number(imageBounds.height) || 0);
|
|
3059
|
+
let deltaX = 0;
|
|
3060
|
+
let deltaY = 0;
|
|
3061
|
+
if (cropBounds.left < imageLeft) {
|
|
3062
|
+
deltaX = imageLeft - cropBounds.left;
|
|
3063
|
+
} else if (cropBounds.left + cropBounds.width > imageRight) {
|
|
3064
|
+
deltaX = imageRight - (cropBounds.left + cropBounds.width);
|
|
3065
|
+
}
|
|
3066
|
+
if (cropBounds.top < imageTop) {
|
|
3067
|
+
deltaY = imageTop - cropBounds.top;
|
|
3068
|
+
} else if (cropBounds.top + cropBounds.height > imageBottom) {
|
|
3069
|
+
deltaY = imageBottom - (cropBounds.top + cropBounds.height);
|
|
3070
|
+
}
|
|
3071
|
+
if (deltaX || deltaY) {
|
|
3072
|
+
cropRect.set({
|
|
3073
|
+
left: (Number(cropRect.left) || 0) + deltaX,
|
|
3074
|
+
top: (Number(cropRect.top) || 0) + deltaY
|
|
3075
|
+
});
|
|
3076
|
+
cropRect.setCoords();
|
|
3077
|
+
}
|
|
2733
3078
|
this.canvas.requestRenderAll();
|
|
2734
3079
|
} catch (error) {
|
|
2735
3080
|
void error;
|
|
@@ -2797,19 +3142,15 @@ var ImageEditor = class {
|
|
|
2797
3142
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2798
3143
|
if (masks && masks.length) {
|
|
2799
3144
|
masks.forEach((mask) => {
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
preservedMasks.push(mask);
|
|
2810
|
-
}
|
|
2811
|
-
} catch (error) {
|
|
2812
|
-
this._reportWarning("applyCrop: failed to remove mask", error);
|
|
3145
|
+
mask.setCoords();
|
|
3146
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
3147
|
+
const intersectsCrop = maskBounds.left < cropRegion.sourceX + cropRegion.sourceWidth && maskBounds.left + maskBounds.width > cropRegion.sourceX && maskBounds.top < cropRegion.sourceY + cropRegion.sourceHeight && maskBounds.top + maskBounds.height > cropRegion.sourceY;
|
|
3148
|
+
this._removeLabelForMask(mask);
|
|
3149
|
+
this.canvas.remove(mask);
|
|
3150
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
3151
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3152
|
+
mask.set({ visible: true });
|
|
3153
|
+
preservedMasks.push(mask);
|
|
2813
3154
|
}
|
|
2814
3155
|
});
|
|
2815
3156
|
this._clearMaskPlacementMemory();
|
|
@@ -2817,7 +3158,8 @@ var ImageEditor = class {
|
|
|
2817
3158
|
this.canvas.renderAll();
|
|
2818
3159
|
}
|
|
2819
3160
|
} catch (error) {
|
|
2820
|
-
this.
|
|
3161
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to prepare masks", error);
|
|
3162
|
+
return;
|
|
2821
3163
|
}
|
|
2822
3164
|
this._removeCropRect();
|
|
2823
3165
|
this._cropMode = false;
|
|
@@ -2893,6 +3235,7 @@ var ImageEditor = class {
|
|
|
2893
3235
|
const canUndo = this.historyManager?.canUndo();
|
|
2894
3236
|
const canRedo = this.historyManager?.canRedo();
|
|
2895
3237
|
const isInCropMode = !!this._cropMode;
|
|
3238
|
+
const isBusy = this.isBusy();
|
|
2896
3239
|
if (isInCropMode) {
|
|
2897
3240
|
for (const key of Object.keys(this.elements || {})) {
|
|
2898
3241
|
const element = this._getElement(key);
|
|
@@ -2905,23 +3248,27 @@ var ImageEditor = class {
|
|
|
2905
3248
|
}
|
|
2906
3249
|
return;
|
|
2907
3250
|
}
|
|
2908
|
-
this._setDisabled("zoomInBtn", !hasImage ||
|
|
2909
|
-
this._setDisabled("zoomOutBtn", !hasImage ||
|
|
2910
|
-
this._setDisabled("rotateLeftBtn", !hasImage ||
|
|
2911
|
-
this._setDisabled("rotateRightBtn", !hasImage ||
|
|
2912
|
-
this._setDisabled("addMaskBtn", !hasImage ||
|
|
2913
|
-
this._setDisabled("removeMaskBtn", !hasSelectedMask ||
|
|
2914
|
-
this._setDisabled("removeAllMasksBtn", !hasMasks ||
|
|
2915
|
-
this._setDisabled("mergeBtn", !hasImage || !hasMasks ||
|
|
2916
|
-
this._setDisabled("downloadBtn", !hasImage ||
|
|
2917
|
-
this._setDisabled("resetBtn", !hasImage || isDefaultTransform ||
|
|
2918
|
-
this._setDisabled("undoBtn", !hasImage ||
|
|
2919
|
-
this._setDisabled("redoBtn", !hasImage ||
|
|
2920
|
-
this._setDisabled("cropBtn", !hasImage ||
|
|
3251
|
+
this._setDisabled("zoomInBtn", !hasImage || isBusy || this.currentScale >= this.options.maxScale);
|
|
3252
|
+
this._setDisabled("zoomOutBtn", !hasImage || isBusy || this.currentScale <= this.options.minScale);
|
|
3253
|
+
this._setDisabled("rotateLeftBtn", !hasImage || isBusy);
|
|
3254
|
+
this._setDisabled("rotateRightBtn", !hasImage || isBusy);
|
|
3255
|
+
this._setDisabled("addMaskBtn", !hasImage || isBusy);
|
|
3256
|
+
this._setDisabled("removeMaskBtn", !hasSelectedMask || isBusy);
|
|
3257
|
+
this._setDisabled("removeAllMasksBtn", !hasMasks || isBusy);
|
|
3258
|
+
this._setDisabled("mergeBtn", !hasImage || !hasMasks || isBusy);
|
|
3259
|
+
this._setDisabled("downloadBtn", !hasImage || isBusy);
|
|
3260
|
+
this._setDisabled("resetBtn", !hasImage || isDefaultTransform || isBusy);
|
|
3261
|
+
this._setDisabled("undoBtn", !hasImage || isBusy || !canUndo);
|
|
3262
|
+
this._setDisabled("redoBtn", !hasImage || isBusy || !canRedo);
|
|
3263
|
+
this._setDisabled("cropBtn", !hasImage || isBusy);
|
|
2921
3264
|
this._setDisabled("applyCropBtn", true);
|
|
2922
3265
|
this._setDisabled("cancelCropBtn", true);
|
|
2923
|
-
this._setDisabled("
|
|
2924
|
-
this._setDisabled("
|
|
3266
|
+
this._setDisabled("scaleRate", !hasImage || isBusy);
|
|
3267
|
+
this._setDisabled("rotationLeftInput", !hasImage || isBusy);
|
|
3268
|
+
this._setDisabled("rotationRightInput", !hasImage || isBusy);
|
|
3269
|
+
this._setDisabled("maskList", !hasImage || isBusy);
|
|
3270
|
+
this._setDisabled("imageInput", isBusy);
|
|
3271
|
+
this._setDisabled("uploadArea", isBusy);
|
|
2925
3272
|
}
|
|
2926
3273
|
/**
|
|
2927
3274
|
* Enables or disables a specific UI element (typically a button) by its key.
|
|
@@ -2937,12 +3284,16 @@ var ImageEditor = class {
|
|
|
2937
3284
|
element.disabled = !!disabled;
|
|
2938
3285
|
return;
|
|
2939
3286
|
}
|
|
3287
|
+
if (!this._elementOriginalPointerEvents) this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
3288
|
+
if (!this._elementOriginalPointerEvents.has(key)) {
|
|
3289
|
+
this._elementOriginalPointerEvents.set(key, element.style.pointerEvents || "");
|
|
3290
|
+
}
|
|
2940
3291
|
if (disabled) {
|
|
2941
3292
|
element.setAttribute("aria-disabled", "true");
|
|
2942
3293
|
element.style.pointerEvents = "none";
|
|
2943
3294
|
} else {
|
|
2944
3295
|
element.removeAttribute("aria-disabled");
|
|
2945
|
-
element.style.pointerEvents = "";
|
|
3296
|
+
element.style.pointerEvents = this._elementOriginalPointerEvents.get(key) ?? "";
|
|
2946
3297
|
}
|
|
2947
3298
|
}
|
|
2948
3299
|
_isElementDisabled(element) {
|
|
@@ -3028,6 +3379,9 @@ var ImageEditor = class {
|
|
|
3028
3379
|
if (this.animationQueue) {
|
|
3029
3380
|
this.animationQueue.cancelAll(new Error("Editor disposed"));
|
|
3030
3381
|
}
|
|
3382
|
+
this._isLoading = false;
|
|
3383
|
+
this._activeOperationName = null;
|
|
3384
|
+
this._activeOperationToken = null;
|
|
3031
3385
|
try {
|
|
3032
3386
|
for (const [key, handlers] of Object.entries(this._handlersByElementKey || {})) {
|
|
3033
3387
|
const element = this._getElement(key);
|
|
@@ -3089,17 +3443,20 @@ var ImageEditor = class {
|
|
|
3089
3443
|
}
|
|
3090
3444
|
this._handlersByElementKey = {};
|
|
3091
3445
|
this._elementCache = {};
|
|
3446
|
+
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
3092
3447
|
this._clearMaskPlacementMemory();
|
|
3093
3448
|
this.originalImage = null;
|
|
3094
3449
|
this.baseImageScale = 1;
|
|
3095
3450
|
this.currentScale = 1;
|
|
3096
3451
|
this.currentRotation = 0;
|
|
3097
3452
|
this.isAnimating = false;
|
|
3453
|
+
this._isLoading = false;
|
|
3098
3454
|
this._cropMode = false;
|
|
3099
3455
|
this._cropRect = null;
|
|
3100
3456
|
this._cropHandlers = [];
|
|
3101
3457
|
this._cropPrevEvented = null;
|
|
3102
3458
|
this._prevSelectionSetting = void 0;
|
|
3459
|
+
this._lastContainerViewportSize = null;
|
|
3103
3460
|
this._initialized = false;
|
|
3104
3461
|
}
|
|
3105
3462
|
};
|
|
@@ -3111,6 +3468,7 @@ var AnimationQueue = class {
|
|
|
3111
3468
|
this.animationTasks = [];
|
|
3112
3469
|
this.isRunning = false;
|
|
3113
3470
|
this.currentTask = null;
|
|
3471
|
+
this._generation = 0;
|
|
3114
3472
|
}
|
|
3115
3473
|
/**
|
|
3116
3474
|
* Adds an animation function to the queue.
|
|
@@ -3130,6 +3488,7 @@ var AnimationQueue = class {
|
|
|
3130
3488
|
return this.isRunning || this.animationTasks.length > 0;
|
|
3131
3489
|
}
|
|
3132
3490
|
cancelAll(reason = new Error("Animation queue cancelled")) {
|
|
3491
|
+
this._generation += 1;
|
|
3133
3492
|
const cancellationError = reason instanceof Error ? reason : new Error(String(reason));
|
|
3134
3493
|
const tasks = [
|
|
3135
3494
|
...this.currentTask ? [this.currentTask] : [],
|
|
@@ -3151,26 +3510,33 @@ var AnimationQueue = class {
|
|
|
3151
3510
|
*/
|
|
3152
3511
|
async _drainQueue() {
|
|
3153
3512
|
if (this.isRunning) return;
|
|
3513
|
+
const generation = this._generation;
|
|
3154
3514
|
this.isRunning = true;
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
task.isSettled
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
task.isSettled
|
|
3167
|
-
|
|
3515
|
+
try {
|
|
3516
|
+
while (this.animationTasks.length > 0 && generation === this._generation) {
|
|
3517
|
+
const task = this.animationTasks.shift();
|
|
3518
|
+
this.currentTask = task;
|
|
3519
|
+
try {
|
|
3520
|
+
const result = await task.animationFn();
|
|
3521
|
+
if (generation === this._generation && !task.isSettled) {
|
|
3522
|
+
task.isSettled = true;
|
|
3523
|
+
task.resolve(result);
|
|
3524
|
+
}
|
|
3525
|
+
} catch (error) {
|
|
3526
|
+
if (generation === this._generation && !task.isSettled) {
|
|
3527
|
+
task.isSettled = true;
|
|
3528
|
+
task.reject(error);
|
|
3529
|
+
}
|
|
3530
|
+
} finally {
|
|
3531
|
+
if (generation === this._generation && this.currentTask === task) this.currentTask = null;
|
|
3168
3532
|
}
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3533
|
+
}
|
|
3534
|
+
} finally {
|
|
3535
|
+
if (generation === this._generation) {
|
|
3536
|
+
this.isRunning = false;
|
|
3537
|
+
this.currentTask = null;
|
|
3171
3538
|
}
|
|
3172
3539
|
}
|
|
3173
|
-
this.isRunning = false;
|
|
3174
3540
|
}
|
|
3175
3541
|
};
|
|
3176
3542
|
var Command = class {
|
|
@@ -3215,9 +3581,9 @@ var HistoryManager = class {
|
|
|
3215
3581
|
execute(command) {
|
|
3216
3582
|
const result = command.execute();
|
|
3217
3583
|
if (result && typeof result.then === "function") {
|
|
3218
|
-
return Promise.resolve(result).then(() => {
|
|
3584
|
+
return this.enqueue(() => Promise.resolve(result).then(() => {
|
|
3219
3585
|
this.push(command);
|
|
3220
|
-
});
|
|
3586
|
+
}));
|
|
3221
3587
|
}
|
|
3222
3588
|
this.push(command);
|
|
3223
3589
|
return result;
|