@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
package/src/image-editor.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file image-editor.js
|
|
3
3
|
* @module image-editor
|
|
4
|
-
* @version 1.4.
|
|
4
|
+
* @version 1.4.2
|
|
5
5
|
* @author Ben Situ
|
|
6
6
|
* @license MIT
|
|
7
7
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
let fabric = null;
|
|
11
|
+
const INTERNAL_OPERATION_TOKEN = Symbol.for('ImageEditorInternalOperation');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Returns the ambient global scope used to discover a globally loaded Fabric.js namespace.
|
|
@@ -252,12 +253,16 @@ function ensureFabric() {
|
|
|
252
253
|
this.currentRotation = 0;
|
|
253
254
|
this.maskCounter = 0;
|
|
254
255
|
this.isAnimating = false;
|
|
256
|
+
this._isLoading = false;
|
|
257
|
+
this._activeOperationName = null;
|
|
258
|
+
this._activeOperationToken = null;
|
|
255
259
|
this.elements = {};
|
|
256
260
|
this.isImageLoadedToCanvas = false;
|
|
257
261
|
this.maxHistorySize = 50;
|
|
258
262
|
|
|
259
263
|
this._handlersByElementKey = {};
|
|
260
264
|
this._elementCache = {};
|
|
265
|
+
this._elementOriginalPointerEvents = new Map();
|
|
261
266
|
|
|
262
267
|
this._lastMask = null;
|
|
263
268
|
this._lastMaskInitialLeft = null;
|
|
@@ -357,6 +362,10 @@ function ensureFabric() {
|
|
|
357
362
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
358
363
|
this._visibilityStateByElement = new WeakMap();
|
|
359
364
|
this._activeAnimationRejectors = new Set();
|
|
365
|
+
this._isLoading = false;
|
|
366
|
+
this._activeOperationName = null;
|
|
367
|
+
this._activeOperationToken = null;
|
|
368
|
+
this._elementOriginalPointerEvents = new Map();
|
|
360
369
|
this._containerOriginalOverflow = null;
|
|
361
370
|
this._lastContainerViewportSize = null;
|
|
362
371
|
this._canvasElementOriginalStyle = null;
|
|
@@ -589,6 +598,13 @@ function ensureFabric() {
|
|
|
589
598
|
this.containerElement.style.overflowY = this._containerOriginalOverflow.overflowY;
|
|
590
599
|
}
|
|
591
600
|
|
|
601
|
+
_restoreContainerOverflowSnapshot(snapshot) {
|
|
602
|
+
if (!this.containerElement || !this.containerElement.style || !snapshot) return;
|
|
603
|
+
this.containerElement.style.overflow = snapshot.overflow || '';
|
|
604
|
+
this.containerElement.style.overflowX = snapshot.overflowX || '';
|
|
605
|
+
this.containerElement.style.overflowY = snapshot.overflowY || '';
|
|
606
|
+
}
|
|
607
|
+
|
|
592
608
|
/**
|
|
593
609
|
* DOM / UI bindings
|
|
594
610
|
* @private
|
|
@@ -740,8 +756,10 @@ function ensureFabric() {
|
|
|
740
756
|
if (!this._fabricLoaded) return;
|
|
741
757
|
if (!this.canvas || this._disposed) return;
|
|
742
758
|
if (!imageBase64 || typeof imageBase64 !== 'string' || !imageBase64.startsWith('data:image/')) return;
|
|
743
|
-
this._assertIdleForOperation('loadImage');
|
|
759
|
+
this._assertIdleForOperation('loadImage', options);
|
|
744
760
|
|
|
761
|
+
this._isLoading = true;
|
|
762
|
+
this._updateUI();
|
|
745
763
|
this._warnOnImageLayoutOptionConflict();
|
|
746
764
|
const transaction = this._captureLoadImageTransaction();
|
|
747
765
|
|
|
@@ -750,14 +768,16 @@ function ensureFabric() {
|
|
|
750
768
|
if (this._disposed || !this.canvas) throw new Error('Editor was disposed while loading image');
|
|
751
769
|
|
|
752
770
|
let loadSource = imageBase64;
|
|
753
|
-
|
|
771
|
+
const downsampleMaxWidth = Number(this.options.downsampleMaxWidth);
|
|
772
|
+
const downsampleMaxHeight = Number(this.options.downsampleMaxHeight);
|
|
773
|
+
if (this.options.downsampleOnLoad && downsampleMaxWidth > 0 && downsampleMaxHeight > 0) {
|
|
754
774
|
const shouldResize =
|
|
755
|
-
imageElement.naturalWidth >
|
|
756
|
-
imageElement.naturalHeight >
|
|
775
|
+
imageElement.naturalWidth > downsampleMaxWidth ||
|
|
776
|
+
imageElement.naturalHeight > downsampleMaxHeight;
|
|
757
777
|
if (shouldResize) {
|
|
758
778
|
const ratio = Math.min(
|
|
759
|
-
|
|
760
|
-
|
|
779
|
+
downsampleMaxWidth / imageElement.naturalWidth,
|
|
780
|
+
downsampleMaxHeight / imageElement.naturalHeight
|
|
761
781
|
);
|
|
762
782
|
const targetWidth = Math.round(imageElement.naturalWidth * ratio);
|
|
763
783
|
const targetHeight = Math.round(imageElement.naturalHeight * ratio);
|
|
@@ -765,10 +785,12 @@ function ensureFabric() {
|
|
|
765
785
|
imageElement,
|
|
766
786
|
targetWidth,
|
|
767
787
|
targetHeight,
|
|
768
|
-
this.options.downsampleQuality,
|
|
788
|
+
this._normalizeQuality(this.options.downsampleQuality),
|
|
769
789
|
imageBase64
|
|
770
790
|
);
|
|
771
791
|
}
|
|
792
|
+
} else if (this.options.downsampleOnLoad) {
|
|
793
|
+
this._reportWarning('loadImage: downsample limits must be positive numbers; using the original image');
|
|
772
794
|
}
|
|
773
795
|
|
|
774
796
|
const fabricImage = await this._createFabricImageFromURL(loadSource);
|
|
@@ -844,6 +866,9 @@ function ensureFabric() {
|
|
|
844
866
|
} catch (error) {
|
|
845
867
|
await this._rollbackLoadImageTransaction(transaction);
|
|
846
868
|
throw error;
|
|
869
|
+
} finally {
|
|
870
|
+
this._isLoading = false;
|
|
871
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
847
872
|
}
|
|
848
873
|
}
|
|
849
874
|
|
|
@@ -862,6 +887,22 @@ function ensureFabric() {
|
|
|
862
887
|
);
|
|
863
888
|
}
|
|
864
889
|
|
|
890
|
+
/**
|
|
891
|
+
* Checks whether the editor is in a temporary non-mutating state.
|
|
892
|
+
*
|
|
893
|
+
* @returns {boolean} True while loading, animating, cropping, or running a compound operation.
|
|
894
|
+
* @public
|
|
895
|
+
*/
|
|
896
|
+
isBusy() {
|
|
897
|
+
return !!(
|
|
898
|
+
this.isAnimating ||
|
|
899
|
+
this._cropMode ||
|
|
900
|
+
this._isLoading ||
|
|
901
|
+
this._activeOperationToken ||
|
|
902
|
+
(this.animationQueue && this.animationQueue.isBusy())
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
865
906
|
/**
|
|
866
907
|
* Creates an HTMLImageElement from a given data URL.
|
|
867
908
|
*
|
|
@@ -936,7 +977,6 @@ function ensureFabric() {
|
|
|
936
977
|
_captureLoadImageTransaction() {
|
|
937
978
|
return {
|
|
938
979
|
canvasState: this._serializeCanvasState(),
|
|
939
|
-
originalImage: this.originalImage,
|
|
940
980
|
baseImageScale: this.baseImageScale,
|
|
941
981
|
currentScale: this.currentScale,
|
|
942
982
|
currentRotation: this.currentRotation,
|
|
@@ -961,9 +1001,14 @@ function ensureFabric() {
|
|
|
961
1001
|
|
|
962
1002
|
async _rollbackLoadImageTransaction(transaction) {
|
|
963
1003
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
1004
|
+
let didRestoreCanvasState = false;
|
|
964
1005
|
try {
|
|
965
|
-
if (transaction.canvasState)
|
|
1006
|
+
if (transaction.canvasState) {
|
|
1007
|
+
await this.loadFromState(transaction.canvasState);
|
|
1008
|
+
didRestoreCanvasState = true;
|
|
1009
|
+
}
|
|
966
1010
|
} catch (error) {
|
|
1011
|
+
this._lastMask = null;
|
|
967
1012
|
this._reportError('loadImage rollback failed', error);
|
|
968
1013
|
}
|
|
969
1014
|
|
|
@@ -973,16 +1018,20 @@ function ensureFabric() {
|
|
|
973
1018
|
this.maskCounter = transaction.maskCounter;
|
|
974
1019
|
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
975
1020
|
this._lastSnapshot = transaction.lastSnapshot;
|
|
1021
|
+
if (didRestoreCanvasState) {
|
|
1022
|
+
this._restoreLastMaskReference(transaction.lastMask);
|
|
1023
|
+
} else {
|
|
1024
|
+
this._lastMask = null;
|
|
1025
|
+
}
|
|
976
1026
|
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
977
1027
|
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
978
1028
|
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
979
|
-
this._containerOriginalOverflow = transaction.containerOverflow;
|
|
980
1029
|
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
981
1030
|
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
982
1031
|
if (this.containerElement) {
|
|
983
1032
|
this.containerElement.scrollLeft = transaction.scrollLeft;
|
|
984
1033
|
this.containerElement.scrollTop = transaction.scrollTop;
|
|
985
|
-
this.
|
|
1034
|
+
this._restoreContainerOverflowSnapshot(transaction.containerOverflow);
|
|
986
1035
|
}
|
|
987
1036
|
this._updateInputs();
|
|
988
1037
|
this._updateMaskList();
|
|
@@ -990,6 +1039,22 @@ function ensureFabric() {
|
|
|
990
1039
|
if (this.canvas) this.canvas.renderAll();
|
|
991
1040
|
}
|
|
992
1041
|
|
|
1042
|
+
_restoreLastMaskReference(previousLastMask) {
|
|
1043
|
+
if (!this.canvas) {
|
|
1044
|
+
this._lastMask = null;
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
1049
|
+
const previousMaskId = previousLastMask && previousLastMask.maskId;
|
|
1050
|
+
this._lastMask = masks.find(mask => mask.maskId === previousMaskId) || masks[masks.length - 1] || null;
|
|
1051
|
+
if (!this._lastMask) {
|
|
1052
|
+
this._lastMaskInitialLeft = null;
|
|
1053
|
+
this._lastMaskInitialTop = null;
|
|
1054
|
+
this._lastMaskInitialWidth = null;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
993
1058
|
/**
|
|
994
1059
|
* Resamples the given image element to a new width and height and returns the result as a data URL.
|
|
995
1060
|
*
|
|
@@ -1002,12 +1067,20 @@ function ensureFabric() {
|
|
|
1002
1067
|
* @private
|
|
1003
1068
|
*/
|
|
1004
1069
|
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
|
|
1070
|
+
const sourceWidth = Math.max(1, Number(imageElement && (imageElement.naturalWidth || imageElement.width)) || 0);
|
|
1071
|
+
const sourceHeight = Math.max(1, Number(imageElement && (imageElement.naturalHeight || imageElement.height)) || 0);
|
|
1072
|
+
const safeTargetWidth = Math.round(Number(targetWidth));
|
|
1073
|
+
const safeTargetHeight = Math.round(Number(targetHeight));
|
|
1074
|
+
if (!Number.isFinite(safeTargetWidth) || !Number.isFinite(safeTargetHeight) || safeTargetWidth <= 0 || safeTargetHeight <= 0) {
|
|
1075
|
+
throw new Error('Invalid image resample target dimensions');
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1005
1078
|
const offscreenCanvas = document.createElement('canvas');
|
|
1006
|
-
offscreenCanvas.width =
|
|
1007
|
-
offscreenCanvas.height =
|
|
1079
|
+
offscreenCanvas.width = safeTargetWidth;
|
|
1080
|
+
offscreenCanvas.height = safeTargetHeight;
|
|
1008
1081
|
const context = offscreenCanvas.getContext('2d');
|
|
1009
1082
|
if (!context) throw new Error('2D canvas context is unavailable');
|
|
1010
|
-
context.drawImage(imageElement, 0, 0,
|
|
1083
|
+
context.drawImage(imageElement, 0, 0, sourceWidth, sourceHeight, 0, 0, safeTargetWidth, safeTargetHeight);
|
|
1011
1084
|
return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
|
|
1012
1085
|
}
|
|
1013
1086
|
|
|
@@ -1315,7 +1388,11 @@ function ensureFabric() {
|
|
|
1315
1388
|
maskStyleBackups.push(backup);
|
|
1316
1389
|
mask.set(stylePatch);
|
|
1317
1390
|
});
|
|
1318
|
-
|
|
1391
|
+
const result = callback();
|
|
1392
|
+
if (result && typeof result.then === 'function') {
|
|
1393
|
+
throw new Error('_withNormalizedMaskStyles callback must be synchronous');
|
|
1394
|
+
}
|
|
1395
|
+
return result;
|
|
1319
1396
|
} finally {
|
|
1320
1397
|
maskStyleBackups.forEach(backup => {
|
|
1321
1398
|
try {
|
|
@@ -1387,9 +1464,15 @@ function ensureFabric() {
|
|
|
1387
1464
|
* @returns {number} A finite quality value between 0 and 1.
|
|
1388
1465
|
* @private
|
|
1389
1466
|
*/
|
|
1390
|
-
_normalizeQuality(quality) {
|
|
1467
|
+
_normalizeQuality(quality, fallback = undefined) {
|
|
1468
|
+
const fallbackQuality = fallback == null ? this.options.downsampleQuality : fallback;
|
|
1469
|
+
const numericFallback = fallbackQuality == null ? NaN : Number(fallbackQuality);
|
|
1470
|
+
const safeFallback = Number.isFinite(numericFallback)
|
|
1471
|
+
? Math.max(0, Math.min(1, numericFallback))
|
|
1472
|
+
: 0.92;
|
|
1473
|
+
if (quality == null) return safeFallback;
|
|
1391
1474
|
const numericQuality = Number(quality);
|
|
1392
|
-
if (!Number.isFinite(numericQuality)) return
|
|
1475
|
+
if (!Number.isFinite(numericQuality)) return safeFallback;
|
|
1393
1476
|
return Math.max(0, Math.min(1, numericQuality));
|
|
1394
1477
|
}
|
|
1395
1478
|
|
|
@@ -1444,62 +1527,70 @@ function ensureFabric() {
|
|
|
1444
1527
|
};
|
|
1445
1528
|
}
|
|
1446
1529
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1530
|
+
_hasFractionalCanvasEdge(value) {
|
|
1531
|
+
const numericValue = Number(value);
|
|
1532
|
+
if (!Number.isFinite(numericValue)) return false;
|
|
1533
|
+
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
_getPartialExportEdges(bounds) {
|
|
1537
|
+
if (!bounds) return null;
|
|
1538
|
+
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
1539
|
+
const isAxisAligned = angle < 0.01 || Math.abs(angle - 90) < 0.01;
|
|
1540
|
+
if (!isAxisAligned) return null;
|
|
1541
|
+
|
|
1542
|
+
return {
|
|
1543
|
+
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1544
|
+
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1545
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1546
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
async _sealPartialTransparentEdges(dataUrl, edges) {
|
|
1551
|
+
if (!edges || !Object.values(edges).some(Boolean)) return dataUrl;
|
|
1552
|
+
|
|
1553
|
+
const imageElement = await this._createImageElement(dataUrl);
|
|
1554
|
+
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
1555
|
+
const height = Math.max(1, imageElement.naturalHeight || imageElement.height || 1);
|
|
1556
|
+
const offscreenCanvas = document.createElement('canvas');
|
|
1557
|
+
offscreenCanvas.width = width;
|
|
1558
|
+
offscreenCanvas.height = height;
|
|
1559
|
+
const context = offscreenCanvas.getContext('2d');
|
|
1560
|
+
if (!context) throw new Error('2D canvas context is unavailable');
|
|
1561
|
+
context.drawImage(imageElement, 0, 0, width, height);
|
|
1562
|
+
|
|
1563
|
+
const imageData = context.getImageData(0, 0, width, height);
|
|
1564
|
+
const pixels = imageData.data;
|
|
1565
|
+
const sealPixel = (x, y, fallbackX, fallbackY) => {
|
|
1566
|
+
const index = (y * width + x) * 4;
|
|
1567
|
+
const fallbackIndex = (fallbackY * width + fallbackX) * 4;
|
|
1568
|
+
if (pixels[index + 3] === 0 && pixels[fallbackIndex + 3] > 0) {
|
|
1569
|
+
pixels[index] = pixels[fallbackIndex];
|
|
1570
|
+
pixels[index + 1] = pixels[fallbackIndex + 1];
|
|
1571
|
+
pixels[index + 2] = pixels[fallbackIndex + 2];
|
|
1572
|
+
pixels[index + 3] = pixels[fallbackIndex + 3];
|
|
1573
|
+
}
|
|
1574
|
+
if (pixels[index + 3] > 0 && pixels[index + 3] < 255) {
|
|
1575
|
+
pixels[index + 3] = 255;
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
|
|
1579
|
+
if (edges.left && width > 1) {
|
|
1580
|
+
for (let y = 0; y < height; y += 1) sealPixel(0, y, 1, y);
|
|
1581
|
+
}
|
|
1582
|
+
if (edges.right && width > 1) {
|
|
1583
|
+
for (let y = 0; y < height; y += 1) sealPixel(width - 1, y, width - 2, y);
|
|
1584
|
+
}
|
|
1585
|
+
if (edges.top && height > 1) {
|
|
1586
|
+
for (let x = 0; x < width; x += 1) sealPixel(x, 0, x, 1);
|
|
1587
|
+
}
|
|
1588
|
+
if (edges.bottom && height > 1) {
|
|
1589
|
+
for (let x = 0; x < width; x += 1) sealPixel(x, height - 1, x, height - 2);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
context.putImageData(imageData, 0, 0);
|
|
1593
|
+
return offscreenCanvas.toDataURL('image/png');
|
|
1503
1594
|
}
|
|
1504
1595
|
|
|
1505
1596
|
/**
|
|
@@ -1513,13 +1604,16 @@ function ensureFabric() {
|
|
|
1513
1604
|
* @param {number} [region.multiplier=1] - Export multiplier.
|
|
1514
1605
|
* @param {number} [region.quality=0.92] - Output image quality for lossy formats.
|
|
1515
1606
|
* @param {'jpeg'|'png'|'webp'} [region.format='jpeg'] - Output image format.
|
|
1607
|
+
* @param {Object|null} [region.sealPartialEdges=null] - Fractional canvas edges whose alpha should be sealed.
|
|
1516
1608
|
* @returns {Promise<string>} Resolves with an image data URL for the cropped region.
|
|
1517
1609
|
* @private
|
|
1518
1610
|
*/
|
|
1519
|
-
_exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = 'jpeg' }) {
|
|
1611
|
+
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = 'jpeg', sealPartialEdges = null }) {
|
|
1520
1612
|
const safeMultiplier = Math.max(1, Number(multiplier) || 1);
|
|
1521
|
-
|
|
1522
|
-
|
|
1613
|
+
const safeFormat = this._normalizeImageFormat(format);
|
|
1614
|
+
const exportFormat = safeFormat === 'jpeg' ? 'png' : safeFormat;
|
|
1615
|
+
let regionDataUrl = this.canvas.toDataURL({
|
|
1616
|
+
format: exportFormat,
|
|
1523
1617
|
quality,
|
|
1524
1618
|
multiplier: safeMultiplier,
|
|
1525
1619
|
left: sourceX,
|
|
@@ -1527,6 +1621,71 @@ function ensureFabric() {
|
|
|
1527
1621
|
width: sourceWidth,
|
|
1528
1622
|
height: sourceHeight
|
|
1529
1623
|
});
|
|
1624
|
+
|
|
1625
|
+
regionDataUrl = await this._sealPartialTransparentEdges(regionDataUrl, sealPartialEdges);
|
|
1626
|
+
if (safeFormat !== 'jpeg') return regionDataUrl;
|
|
1627
|
+
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1631
|
+
const imageElement = await this._createImageElement(dataUrl);
|
|
1632
|
+
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
1633
|
+
const height = Math.max(1, imageElement.naturalHeight || imageElement.height || 1);
|
|
1634
|
+
const offscreenCanvas = document.createElement('canvas');
|
|
1635
|
+
offscreenCanvas.width = width;
|
|
1636
|
+
offscreenCanvas.height = height;
|
|
1637
|
+
const context = offscreenCanvas.getContext('2d');
|
|
1638
|
+
if (!context) throw new Error('2D canvas context is unavailable');
|
|
1639
|
+
context.fillStyle = this._getJpegBackgroundColor();
|
|
1640
|
+
context.fillRect(0, 0, width, height);
|
|
1641
|
+
context.drawImage(imageElement, 0, 0, width, height);
|
|
1642
|
+
return offscreenCanvas.toDataURL('image/jpeg', this._normalizeQuality(quality));
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
_getJpegBackgroundColor() {
|
|
1646
|
+
const backgroundColor = String(this.options.backgroundColor || '').trim();
|
|
1647
|
+
if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return '#ffffff';
|
|
1648
|
+
return backgroundColor;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
_isTransparentCssColor(color) {
|
|
1652
|
+
const normalizedColor = String(color || '').trim().toLowerCase();
|
|
1653
|
+
if (!normalizedColor || normalizedColor === 'transparent') return true;
|
|
1654
|
+
|
|
1655
|
+
const hexAlphaMatch = normalizedColor.match(/^#(?:[0-9a-f]{3}([0-9a-f])|[0-9a-f]{6}([0-9a-f]{2}))$/i);
|
|
1656
|
+
if (hexAlphaMatch) {
|
|
1657
|
+
const alpha = hexAlphaMatch[1] || hexAlphaMatch[2];
|
|
1658
|
+
return alpha === '0' || alpha === '00';
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const slashAlphaMatch = normalizedColor.match(/^(?:rgba?|hsla?)\([^)]*\/\s*([^)]+)\)$/i);
|
|
1662
|
+
if (slashAlphaMatch) return this._isZeroCssAlpha(slashAlphaMatch[1]);
|
|
1663
|
+
|
|
1664
|
+
const commaAlphaMatch = normalizedColor.match(/^(?:rgba|hsla)\((.*)\)$/i);
|
|
1665
|
+
if (commaAlphaMatch) {
|
|
1666
|
+
const parts = commaAlphaMatch[1].split(',');
|
|
1667
|
+
if (parts.length >= 4) return this._isZeroCssAlpha(parts[parts.length - 1]);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
return false;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
_isZeroCssAlpha(alphaValue) {
|
|
1674
|
+
const normalizedAlpha = String(alphaValue || '').trim();
|
|
1675
|
+
if (!normalizedAlpha) return false;
|
|
1676
|
+
if (normalizedAlpha.endsWith('%')) return Number.parseFloat(normalizedAlpha) === 0;
|
|
1677
|
+
return Number(normalizedAlpha) === 0;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
_decodeBase64Payload(base64Payload) {
|
|
1681
|
+
const payload = String(base64Payload || '');
|
|
1682
|
+
if (typeof atob === 'function') {
|
|
1683
|
+
return Uint8Array.from(atob(payload), char => char.charCodeAt(0));
|
|
1684
|
+
}
|
|
1685
|
+
if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
|
|
1686
|
+
return new Uint8Array(Buffer.from(payload, 'base64'));
|
|
1687
|
+
}
|
|
1688
|
+
throw new Error('Base64 decoding is unavailable');
|
|
1530
1689
|
}
|
|
1531
1690
|
|
|
1532
1691
|
/**
|
|
@@ -1698,19 +1857,80 @@ function ensureFabric() {
|
|
|
1698
1857
|
* @public
|
|
1699
1858
|
*/
|
|
1700
1859
|
scaleImage(factor, options = {}) {
|
|
1701
|
-
|
|
1860
|
+
try {
|
|
1861
|
+
this._assertCanQueueAnimation('scaleImage', options);
|
|
1862
|
+
} catch (error) {
|
|
1863
|
+
return Promise.reject(error);
|
|
1864
|
+
}
|
|
1865
|
+
return this.animationQueue.add(() => this._scaleImageImpl(factor, options))
|
|
1866
|
+
.finally(() => {
|
|
1867
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
_getInternalOperationToken(options) {
|
|
1872
|
+
return options && options[INTERNAL_OPERATION_TOKEN];
|
|
1702
1873
|
}
|
|
1703
1874
|
|
|
1704
|
-
|
|
1875
|
+
_isOwnInternalOperation(options) {
|
|
1876
|
+
const token = this._getInternalOperationToken(options);
|
|
1877
|
+
return !!token && token === this._activeOperationToken;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
_beginBusyOperation(operationName) {
|
|
1881
|
+
const token = Symbol(operationName);
|
|
1882
|
+
this._activeOperationName = operationName;
|
|
1883
|
+
this._activeOperationToken = token;
|
|
1884
|
+
this._updateUI();
|
|
1885
|
+
return token;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
_endBusyOperation(token) {
|
|
1889
|
+
if (token && token === this._activeOperationToken) {
|
|
1890
|
+
this._activeOperationName = null;
|
|
1891
|
+
this._activeOperationToken = null;
|
|
1892
|
+
this._updateUI();
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
_withInternalOperationOptions(token, options = {}) {
|
|
1897
|
+
return {
|
|
1898
|
+
...options,
|
|
1899
|
+
[INTERNAL_OPERATION_TOKEN]: token
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
_assertEditorAvailable(operationName) {
|
|
1705
1904
|
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
_assertIdleForOperation(operationName, options = {}) {
|
|
1908
|
+
this._assertEditorAvailable(operationName);
|
|
1909
|
+
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
1706
1910
|
if (this.isAnimating || (this.animationQueue && this.animationQueue.isBusy())) {
|
|
1707
1911
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1708
1912
|
}
|
|
1913
|
+
if (this._isLoading && !isOwnInternalOperation) {
|
|
1914
|
+
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1915
|
+
}
|
|
1916
|
+
if (this._activeOperationToken && !isOwnInternalOperation) {
|
|
1917
|
+
throw new Error(`${operationName} cannot run while ${this._activeOperationName || 'another operation'} is running`);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
_assertCanQueueAnimation(operationName, options = {}) {
|
|
1922
|
+
this._assertEditorAvailable(operationName);
|
|
1923
|
+
if (this._isLoading && !this._isOwnInternalOperation(options)) {
|
|
1924
|
+
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1925
|
+
}
|
|
1926
|
+
if (this._activeOperationToken && !this._isOwnInternalOperation(options)) {
|
|
1927
|
+
throw new Error(`${operationName} cannot run while ${this._activeOperationName || 'another operation'} is running`);
|
|
1928
|
+
}
|
|
1709
1929
|
}
|
|
1710
1930
|
|
|
1711
|
-
_canMutateNow(operationName) {
|
|
1931
|
+
_canMutateNow(operationName, options = {}) {
|
|
1712
1932
|
try {
|
|
1713
|
-
this._assertIdleForOperation(operationName);
|
|
1933
|
+
this._assertIdleForOperation(operationName, options);
|
|
1714
1934
|
return true;
|
|
1715
1935
|
} catch (error) {
|
|
1716
1936
|
this._reportError(`${operationName} blocked`, error);
|
|
@@ -1824,7 +2044,15 @@ function ensureFabric() {
|
|
|
1824
2044
|
* @public
|
|
1825
2045
|
*/
|
|
1826
2046
|
rotateImage(degrees, options = {}) {
|
|
1827
|
-
|
|
2047
|
+
try {
|
|
2048
|
+
this._assertCanQueueAnimation('rotateImage', options);
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
return Promise.reject(error);
|
|
2051
|
+
}
|
|
2052
|
+
return this.animationQueue.add(() => this._rotateImageImpl(degrees, options))
|
|
2053
|
+
.finally(() => {
|
|
2054
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
2055
|
+
});
|
|
1828
2056
|
}
|
|
1829
2057
|
|
|
1830
2058
|
/**
|
|
@@ -1894,6 +2122,11 @@ function ensureFabric() {
|
|
|
1894
2122
|
*/
|
|
1895
2123
|
resetImageTransform() {
|
|
1896
2124
|
if (!this.originalImage) return Promise.resolve();
|
|
2125
|
+
try {
|
|
2126
|
+
this._assertCanQueueAnimation('resetImageTransform');
|
|
2127
|
+
} catch (error) {
|
|
2128
|
+
return Promise.reject(error);
|
|
2129
|
+
}
|
|
1897
2130
|
|
|
1898
2131
|
return this.animationQueue.add(async () => {
|
|
1899
2132
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow('resetImageTransform');
|
|
@@ -1901,6 +2134,8 @@ function ensureFabric() {
|
|
|
1901
2134
|
await this._rotateImageImpl(0, { saveHistory: false });
|
|
1902
2135
|
const after = this._captureCanvasStateOrThrow('resetImageTransform');
|
|
1903
2136
|
this._pushStateTransition(before, after);
|
|
2137
|
+
}).finally(() => {
|
|
2138
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1904
2139
|
}).catch(error => {
|
|
1905
2140
|
this._reportError('resetImageTransform() failed', error);
|
|
1906
2141
|
throw error;
|
|
@@ -1942,6 +2177,13 @@ function ensureFabric() {
|
|
|
1942
2177
|
? JSON.parse(serializedState)
|
|
1943
2178
|
: serializedState;
|
|
1944
2179
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
2180
|
+
if (
|
|
2181
|
+
editorMetadata &&
|
|
2182
|
+
Object.prototype.hasOwnProperty.call(editorMetadata, 'version') &&
|
|
2183
|
+
Number(editorMetadata.version) !== 1
|
|
2184
|
+
) {
|
|
2185
|
+
this._reportWarning(`loadFromState: unsupported editor metadata version ${editorMetadata.version}`);
|
|
2186
|
+
}
|
|
1945
2187
|
|
|
1946
2188
|
this.canvas.loadFromJSON(state, async () => {
|
|
1947
2189
|
try {
|
|
@@ -2035,22 +2277,48 @@ function ensureFabric() {
|
|
|
2035
2277
|
|
|
2036
2278
|
_waitForImageElementReady(imageElement) {
|
|
2037
2279
|
if (!imageElement) return Promise.resolve();
|
|
2038
|
-
|
|
2280
|
+
const hasLoadedDimensions = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) &&
|
|
2281
|
+
(Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
2282
|
+
if (hasLoadedDimensions) return Promise.resolve();
|
|
2283
|
+
if (imageElement.complete) return Promise.reject(new Error('Image could not be loaded while restoring state'));
|
|
2039
2284
|
return new Promise((resolve, reject) => {
|
|
2040
2285
|
let isSettled = false;
|
|
2041
|
-
|
|
2042
|
-
settle(() => reject(new Error('Image load timed out while restoring state')));
|
|
2043
|
-
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
2286
|
+
let timerId;
|
|
2044
2287
|
const settle = (callback) => {
|
|
2045
2288
|
if (isSettled) return;
|
|
2046
2289
|
isSettled = true;
|
|
2047
2290
|
clearTimeout(timerId);
|
|
2048
|
-
imageElement.
|
|
2049
|
-
|
|
2291
|
+
if (typeof imageElement.removeEventListener === 'function') {
|
|
2292
|
+
imageElement.removeEventListener('load', handleLoad);
|
|
2293
|
+
imageElement.removeEventListener('error', handleError);
|
|
2294
|
+
} else {
|
|
2295
|
+
imageElement.onload = null;
|
|
2296
|
+
imageElement.onerror = null;
|
|
2297
|
+
}
|
|
2050
2298
|
callback();
|
|
2051
2299
|
};
|
|
2052
|
-
|
|
2053
|
-
|
|
2300
|
+
const handleLoad = () => {
|
|
2301
|
+
const didLoad = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) &&
|
|
2302
|
+
(Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
2303
|
+
settle(() => {
|
|
2304
|
+
if (didLoad) {
|
|
2305
|
+
resolve();
|
|
2306
|
+
} else {
|
|
2307
|
+
reject(new Error('Image could not be loaded while restoring state'));
|
|
2308
|
+
}
|
|
2309
|
+
});
|
|
2310
|
+
};
|
|
2311
|
+
const handleError = (error) => settle(() => reject(error instanceof Error ? error : new Error('Image could not be loaded while restoring state')));
|
|
2312
|
+
timerId = setTimeout(() => {
|
|
2313
|
+
settle(() => reject(new Error('Image load timed out while restoring state')));
|
|
2314
|
+
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
2315
|
+
if (typeof imageElement.addEventListener === 'function') {
|
|
2316
|
+
imageElement.addEventListener('load', handleLoad, { once: true });
|
|
2317
|
+
imageElement.addEventListener('error', handleError, { once: true });
|
|
2318
|
+
} else {
|
|
2319
|
+
imageElement.onload = handleLoad;
|
|
2320
|
+
imageElement.onerror = handleError;
|
|
2321
|
+
}
|
|
2054
2322
|
});
|
|
2055
2323
|
}
|
|
2056
2324
|
|
|
@@ -2336,6 +2604,11 @@ function ensureFabric() {
|
|
|
2336
2604
|
}
|
|
2337
2605
|
}
|
|
2338
2606
|
|
|
2607
|
+
if (!mask || typeof mask.set !== 'function' || typeof mask.setCoords !== 'function') {
|
|
2608
|
+
this._reportWarning('fabricGenerator returned an invalid Fabric object');
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2339
2612
|
const styles = maskConfig.styles || {};
|
|
2340
2613
|
const hasStyle = property => Object.prototype.hasOwnProperty.call(styles, property);
|
|
2341
2614
|
const maskSettings = {
|
|
@@ -2435,7 +2708,7 @@ function ensureFabric() {
|
|
|
2435
2708
|
*/
|
|
2436
2709
|
removeAllMasks(options = {}) {
|
|
2437
2710
|
if (!this.canvas) return;
|
|
2438
|
-
if (!this._canMutateNow('removeAllMasks')) return;
|
|
2711
|
+
if (!this._canMutateNow('removeAllMasks', options)) return;
|
|
2439
2712
|
const saveHistory = options.saveHistory !== false;
|
|
2440
2713
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
2441
2714
|
masks.forEach(mask => this._removeLabelForMask(mask));
|
|
@@ -2470,6 +2743,99 @@ function ensureFabric() {
|
|
|
2470
2743
|
}
|
|
2471
2744
|
}
|
|
2472
2745
|
|
|
2746
|
+
_captureMaskLabelBackups(masks) {
|
|
2747
|
+
if (!this.canvas) return [];
|
|
2748
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2749
|
+
return (masks || []).map(mask => {
|
|
2750
|
+
const label = mask && mask.__label ? mask.__label : null;
|
|
2751
|
+
return {
|
|
2752
|
+
mask,
|
|
2753
|
+
label,
|
|
2754
|
+
hadLabel: !!label,
|
|
2755
|
+
labelInCanvas: !!label && canvasObjects.has(label),
|
|
2756
|
+
visible: label ? label.visible : undefined
|
|
2757
|
+
};
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
_restoreMaskLabelBackups(labelBackups) {
|
|
2762
|
+
if (!this.canvas || !Array.isArray(labelBackups)) return;
|
|
2763
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2764
|
+
labelBackups.forEach(backup => {
|
|
2765
|
+
if (!backup || !backup.mask) return;
|
|
2766
|
+
try {
|
|
2767
|
+
if (!backup.hadLabel) {
|
|
2768
|
+
if (backup.mask.__label) this._removeLabelForMask(backup.mask);
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
backup.mask.__label = backup.label;
|
|
2772
|
+
if (!backup.label) return;
|
|
2773
|
+
if (backup.labelInCanvas && !canvasObjects.has(backup.label)) {
|
|
2774
|
+
this.canvas.add(backup.label);
|
|
2775
|
+
canvasObjects.add(backup.label);
|
|
2776
|
+
}
|
|
2777
|
+
if (backup.visible !== undefined) backup.label.set({ visible: backup.visible });
|
|
2778
|
+
if (backup.labelInCanvas) this.canvas.bringToFront(backup.label);
|
|
2779
|
+
this._syncMaskLabel(backup.mask);
|
|
2780
|
+
} catch (error) { void error; }
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
_captureActiveObjectBackup() {
|
|
2785
|
+
if (!this.canvas) return null;
|
|
2786
|
+
const activeObject = this.canvas.getActiveObject();
|
|
2787
|
+
if (!activeObject) return null;
|
|
2788
|
+
const selectedObjects = typeof activeObject.getObjects === 'function'
|
|
2789
|
+
? activeObject.getObjects()
|
|
2790
|
+
: [activeObject];
|
|
2791
|
+
return { activeObject, selectedObjects };
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
_restoreActiveObjectBackup(activeObjectBackup) {
|
|
2795
|
+
if (!this.canvas || !activeObjectBackup || !activeObjectBackup.activeObject) return;
|
|
2796
|
+
const canvasObjects = this.canvas.getObjects();
|
|
2797
|
+
const selectedObjects = Array.isArray(activeObjectBackup.selectedObjects)
|
|
2798
|
+
? activeObjectBackup.selectedObjects
|
|
2799
|
+
: [];
|
|
2800
|
+
const canRestore = selectedObjects.length
|
|
2801
|
+
? selectedObjects.every(object => canvasObjects.includes(object))
|
|
2802
|
+
: canvasObjects.includes(activeObjectBackup.activeObject);
|
|
2803
|
+
if (!canRestore) return;
|
|
2804
|
+
try {
|
|
2805
|
+
this.canvas.setActiveObject(activeObjectBackup.activeObject);
|
|
2806
|
+
} catch (error) { void error; }
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
_captureMaskExportBackups(masks) {
|
|
2810
|
+
return (masks || []).map(mask => ({
|
|
2811
|
+
object: mask,
|
|
2812
|
+
visible: mask.visible,
|
|
2813
|
+
opacity: mask.opacity,
|
|
2814
|
+
fill: mask.fill,
|
|
2815
|
+
strokeWidth: mask.strokeWidth,
|
|
2816
|
+
stroke: mask.stroke,
|
|
2817
|
+
selectable: mask.selectable,
|
|
2818
|
+
lockRotation: mask.lockRotation
|
|
2819
|
+
}));
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
_restoreMaskExportBackups(maskBackups) {
|
|
2823
|
+
(maskBackups || []).forEach(backup => {
|
|
2824
|
+
try {
|
|
2825
|
+
backup.object.set({
|
|
2826
|
+
visible: backup.visible,
|
|
2827
|
+
opacity: backup.opacity,
|
|
2828
|
+
fill: backup.fill,
|
|
2829
|
+
strokeWidth: backup.strokeWidth,
|
|
2830
|
+
stroke: backup.stroke,
|
|
2831
|
+
selectable: backup.selectable,
|
|
2832
|
+
lockRotation: backup.lockRotation
|
|
2833
|
+
});
|
|
2834
|
+
backup.object.setCoords();
|
|
2835
|
+
} catch (error) { void error; }
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2473
2839
|
/**
|
|
2474
2840
|
* Returns a stable zero-based creation index for label callbacks.
|
|
2475
2841
|
*
|
|
@@ -2547,10 +2913,14 @@ function ensureFabric() {
|
|
|
2547
2913
|
_hideAllMaskLabels() {
|
|
2548
2914
|
if (!this.canvas) return;
|
|
2549
2915
|
const canvasObjects = this.canvas.getObjects();
|
|
2916
|
+
const canvasObjectSet = new Set(canvasObjects);
|
|
2550
2917
|
const labels = canvasObjects.filter(object => object.maskLabel);
|
|
2551
2918
|
labels.forEach(label => {
|
|
2552
2919
|
try {
|
|
2553
|
-
if (
|
|
2920
|
+
if (canvasObjectSet.has(label)) {
|
|
2921
|
+
this.canvas.remove(label);
|
|
2922
|
+
canvasObjectSet.delete(label);
|
|
2923
|
+
}
|
|
2554
2924
|
} catch (error) { void error; }
|
|
2555
2925
|
});
|
|
2556
2926
|
canvasObjects.forEach(object => {
|
|
@@ -2714,20 +3084,38 @@ function ensureFabric() {
|
|
|
2714
3084
|
this._assertIdleForOperation('mergeMasks');
|
|
2715
3085
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
2716
3086
|
if (!masks.length) return;
|
|
3087
|
+
const beforeJson = this._serializeCanvasState();
|
|
3088
|
+
const operationToken = this._beginBusyOperation('mergeMasks');
|
|
2717
3089
|
|
|
2718
3090
|
this.canvas.discardActiveObject();
|
|
2719
3091
|
this.canvas.renderAll();
|
|
2720
3092
|
|
|
2721
3093
|
try {
|
|
2722
|
-
const
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
3094
|
+
const merged = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3095
|
+
exportImageArea: true,
|
|
3096
|
+
multiplier: this.options.exportMultiplier,
|
|
3097
|
+
fileType: 'png'
|
|
3098
|
+
}));
|
|
3099
|
+
this.removeAllMasks(this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
3100
|
+
if (this.canvas.getObjects().some(object => object.maskId)) {
|
|
3101
|
+
throw new Error('Masks could not be removed during merge');
|
|
3102
|
+
}
|
|
3103
|
+
await this.loadImage(merged, this._withInternalOperationOptions(operationToken, {
|
|
3104
|
+
preserveScroll: true,
|
|
3105
|
+
resetMaskCounter: false
|
|
3106
|
+
}));
|
|
2726
3107
|
const afterJson = this._serializeCanvasState();
|
|
2727
3108
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2728
3109
|
} catch (error) {
|
|
2729
3110
|
this._reportError('merge error', error);
|
|
3111
|
+
try {
|
|
3112
|
+
await this.loadFromState(beforeJson);
|
|
3113
|
+
} catch (restoreError) {
|
|
3114
|
+
this._reportError('mergeMasks rollback failed', restoreError);
|
|
3115
|
+
}
|
|
2730
3116
|
throw error;
|
|
3117
|
+
} finally {
|
|
3118
|
+
this._endBusyOperation(operationToken);
|
|
2731
3119
|
}
|
|
2732
3120
|
}
|
|
2733
3121
|
|
|
@@ -2783,7 +3171,7 @@ function ensureFabric() {
|
|
|
2783
3171
|
*/
|
|
2784
3172
|
async exportImageBase64(options = {}) {
|
|
2785
3173
|
if (!this.originalImage) throw new Error('No image loaded');
|
|
2786
|
-
this._assertIdleForOperation('exportImageBase64');
|
|
3174
|
+
this._assertIdleForOperation('exportImageBase64', options);
|
|
2787
3175
|
const exportImageArea = typeof options.exportImageArea === 'boolean' ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2788
3176
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2789
3177
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
@@ -2791,7 +3179,11 @@ function ensureFabric() {
|
|
|
2791
3179
|
|
|
2792
3180
|
if (!exportImageArea) {
|
|
2793
3181
|
const masks = this.canvas.getObjects().filter(object => object.maskId || object.maskLabel);
|
|
3182
|
+
const editableMasks = this.canvas.getObjects().filter(object => object.maskId);
|
|
2794
3183
|
const maskVisibilityBackups = masks.map(mask => ({ object: mask, visible: mask.visible }));
|
|
3184
|
+
const maskStyleBackups = this._captureMaskExportBackups(editableMasks);
|
|
3185
|
+
const labelBackups = this._captureMaskLabelBackups(editableMasks);
|
|
3186
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2795
3187
|
|
|
2796
3188
|
try {
|
|
2797
3189
|
masks.forEach(mask => { mask.set({ visible: false }); });
|
|
@@ -2800,32 +3192,30 @@ function ensureFabric() {
|
|
|
2800
3192
|
|
|
2801
3193
|
this.originalImage.setCoords();
|
|
2802
3194
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2803
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds
|
|
2804
|
-
return this._exportCanvasRegionToDataURL({
|
|
3195
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
3196
|
+
return await this._exportCanvasRegionToDataURL({
|
|
2805
3197
|
...exportRegion,
|
|
2806
3198
|
multiplier,
|
|
2807
3199
|
quality,
|
|
2808
|
-
format
|
|
3200
|
+
format,
|
|
3201
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2809
3202
|
});
|
|
2810
3203
|
} finally {
|
|
2811
3204
|
maskVisibilityBackups.forEach(backup => {
|
|
2812
3205
|
try { backup.object.set({ visible: backup.visible }); } catch (error) { void error; }
|
|
2813
3206
|
});
|
|
3207
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
3208
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
3209
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2814
3210
|
this.canvas.renderAll();
|
|
2815
3211
|
}
|
|
2816
3212
|
}
|
|
2817
3213
|
|
|
2818
3214
|
// Render masks as export shapes without mutating their editable styles.
|
|
2819
3215
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
2820
|
-
const maskStyleBackups =
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
fill: mask.fill,
|
|
2824
|
-
strokeWidth: mask.strokeWidth,
|
|
2825
|
-
stroke: mask.stroke,
|
|
2826
|
-
selectable: mask.selectable,
|
|
2827
|
-
lockRotation: mask.lockRotation
|
|
2828
|
-
}));
|
|
3216
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
3217
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
3218
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2829
3219
|
|
|
2830
3220
|
let finalBase64;
|
|
2831
3221
|
try {
|
|
@@ -2844,30 +3234,20 @@ function ensureFabric() {
|
|
|
2844
3234
|
// Compute an integer canvas region for the base image.
|
|
2845
3235
|
this.originalImage.setCoords();
|
|
2846
3236
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2847
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds
|
|
3237
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2848
3238
|
|
|
2849
3239
|
// Crop precisely in offscreen canvas
|
|
2850
|
-
finalBase64 = this._exportCanvasRegionToDataURL({
|
|
3240
|
+
finalBase64 = await this._exportCanvasRegionToDataURL({
|
|
2851
3241
|
...exportRegion,
|
|
2852
3242
|
multiplier,
|
|
2853
3243
|
quality,
|
|
2854
|
-
format
|
|
3244
|
+
format,
|
|
3245
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2855
3246
|
});
|
|
2856
3247
|
} finally {
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
opacity: backup.opacity,
|
|
2861
|
-
fill: backup.fill,
|
|
2862
|
-
strokeWidth: backup.strokeWidth,
|
|
2863
|
-
stroke: backup.stroke,
|
|
2864
|
-
selectable: backup.selectable,
|
|
2865
|
-
lockRotation: backup.lockRotation
|
|
2866
|
-
});
|
|
2867
|
-
backup.object.setCoords();
|
|
2868
|
-
} catch (error) { void error; }
|
|
2869
|
-
});
|
|
2870
|
-
|
|
3248
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
3249
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
3250
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2871
3251
|
this.canvas.renderAll();
|
|
2872
3252
|
}
|
|
2873
3253
|
|
|
@@ -2915,6 +3295,7 @@ function ensureFabric() {
|
|
|
2915
3295
|
} = options;
|
|
2916
3296
|
|
|
2917
3297
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
3298
|
+
const normalizedQuality = this._normalizeQuality(quality);
|
|
2918
3299
|
|
|
2919
3300
|
// Generate the data URL in the requested export mode.
|
|
2920
3301
|
let imageBase64;
|
|
@@ -2922,14 +3303,14 @@ function ensureFabric() {
|
|
|
2922
3303
|
imageBase64 = await this.exportImageBase64({
|
|
2923
3304
|
exportImageArea: true,
|
|
2924
3305
|
multiplier,
|
|
2925
|
-
quality,
|
|
3306
|
+
quality: normalizedQuality,
|
|
2926
3307
|
fileType: safeFileType
|
|
2927
3308
|
});
|
|
2928
3309
|
} else {
|
|
2929
3310
|
imageBase64 = await this.exportImageBase64({
|
|
2930
3311
|
exportImageArea: false,
|
|
2931
3312
|
multiplier,
|
|
2932
|
-
quality,
|
|
3313
|
+
quality: normalizedQuality,
|
|
2933
3314
|
fileType: safeFileType
|
|
2934
3315
|
});
|
|
2935
3316
|
}
|
|
@@ -2949,7 +3330,7 @@ function ensureFabric() {
|
|
|
2949
3330
|
const context = offscreenCanvas.getContext('2d');
|
|
2950
3331
|
if (!context) throw new Error('Unable to create 2D canvas context for export conversion');
|
|
2951
3332
|
context.drawImage(imageElement, 0, 0);
|
|
2952
|
-
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`,
|
|
3333
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
2953
3334
|
resolve(convertedDataUrl);
|
|
2954
3335
|
} catch (error) { reject(error); }
|
|
2955
3336
|
};
|
|
@@ -2959,13 +3340,8 @@ function ensureFabric() {
|
|
|
2959
3340
|
}
|
|
2960
3341
|
|
|
2961
3342
|
// Convert the final data URL to a File with the requested MIME type.
|
|
2962
|
-
const
|
|
3343
|
+
const bytes = this._decodeBase64Payload(imageDataUrl.split(',')[1]);
|
|
2963
3344
|
const mime = `image/${safeFileType}`;
|
|
2964
|
-
let byteIndex = binaryString.length;
|
|
2965
|
-
const bytes = new Uint8Array(byteIndex);
|
|
2966
|
-
while (byteIndex--) {
|
|
2967
|
-
bytes[byteIndex] = binaryString.charCodeAt(byteIndex);
|
|
2968
|
-
}
|
|
2969
3345
|
return new File([bytes], fileName, { type: mime });
|
|
2970
3346
|
}
|
|
2971
3347
|
|
|
@@ -3126,6 +3502,30 @@ function ensureFabric() {
|
|
|
3126
3502
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
3127
3503
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
3128
3504
|
cropRect.setCoords();
|
|
3505
|
+
const cropBounds = cropRect.getBoundingRect(true, true);
|
|
3506
|
+
const imageLeft = Number(imageBounds.left) || 0;
|
|
3507
|
+
const imageTop = Number(imageBounds.top) || 0;
|
|
3508
|
+
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
3509
|
+
const imageBottom = imageTop + (Number(imageBounds.height) || 0);
|
|
3510
|
+
let deltaX = 0;
|
|
3511
|
+
let deltaY = 0;
|
|
3512
|
+
if (cropBounds.left < imageLeft) {
|
|
3513
|
+
deltaX = imageLeft - cropBounds.left;
|
|
3514
|
+
} else if (cropBounds.left + cropBounds.width > imageRight) {
|
|
3515
|
+
deltaX = imageRight - (cropBounds.left + cropBounds.width);
|
|
3516
|
+
}
|
|
3517
|
+
if (cropBounds.top < imageTop) {
|
|
3518
|
+
deltaY = imageTop - cropBounds.top;
|
|
3519
|
+
} else if (cropBounds.top + cropBounds.height > imageBottom) {
|
|
3520
|
+
deltaY = imageBottom - (cropBounds.top + cropBounds.height);
|
|
3521
|
+
}
|
|
3522
|
+
if (deltaX || deltaY) {
|
|
3523
|
+
cropRect.set({
|
|
3524
|
+
left: (Number(cropRect.left) || 0) + deltaX,
|
|
3525
|
+
top: (Number(cropRect.top) || 0) + deltaY
|
|
3526
|
+
});
|
|
3527
|
+
cropRect.setCoords();
|
|
3528
|
+
}
|
|
3129
3529
|
this.canvas.requestRenderAll();
|
|
3130
3530
|
} catch (error) { void error; }
|
|
3131
3531
|
};
|
|
@@ -3205,23 +3605,19 @@ function ensureFabric() {
|
|
|
3205
3605
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
3206
3606
|
if (masks && masks.length) {
|
|
3207
3607
|
masks.forEach(mask => {
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
preservedMasks.push(mask);
|
|
3222
|
-
}
|
|
3223
|
-
} catch (error) {
|
|
3224
|
-
this._reportWarning('applyCrop: failed to remove mask', error);
|
|
3608
|
+
mask.setCoords();
|
|
3609
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
3610
|
+
const intersectsCrop =
|
|
3611
|
+
maskBounds.left < cropRegion.sourceX + cropRegion.sourceWidth &&
|
|
3612
|
+
maskBounds.left + maskBounds.width > cropRegion.sourceX &&
|
|
3613
|
+
maskBounds.top < cropRegion.sourceY + cropRegion.sourceHeight &&
|
|
3614
|
+
maskBounds.top + maskBounds.height > cropRegion.sourceY;
|
|
3615
|
+
this._removeLabelForMask(mask);
|
|
3616
|
+
this.canvas.remove(mask);
|
|
3617
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
3618
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3619
|
+
mask.set({ visible: true });
|
|
3620
|
+
preservedMasks.push(mask);
|
|
3225
3621
|
}
|
|
3226
3622
|
});
|
|
3227
3623
|
this._clearMaskPlacementMemory();
|
|
@@ -3229,7 +3625,8 @@ function ensureFabric() {
|
|
|
3229
3625
|
this.canvas.renderAll();
|
|
3230
3626
|
}
|
|
3231
3627
|
} catch (error) {
|
|
3232
|
-
this.
|
|
3628
|
+
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: failed to prepare masks', error);
|
|
3629
|
+
return;
|
|
3233
3630
|
}
|
|
3234
3631
|
|
|
3235
3632
|
this._removeCropRect();
|
|
@@ -3321,6 +3718,7 @@ function ensureFabric() {
|
|
|
3321
3718
|
const canUndo = this.historyManager?.canUndo();
|
|
3322
3719
|
const canRedo = this.historyManager?.canRedo();
|
|
3323
3720
|
const isInCropMode = !!this._cropMode;
|
|
3721
|
+
const isBusy = this.isBusy();
|
|
3324
3722
|
|
|
3325
3723
|
if (isInCropMode) {
|
|
3326
3724
|
// Disable all controls except the crop action buttons while crop mode is active.
|
|
@@ -3336,23 +3734,27 @@ function ensureFabric() {
|
|
|
3336
3734
|
return;
|
|
3337
3735
|
}
|
|
3338
3736
|
|
|
3339
|
-
this._setDisabled('zoomInBtn', !hasImage ||
|
|
3340
|
-
this._setDisabled('zoomOutBtn', !hasImage ||
|
|
3341
|
-
this._setDisabled('rotateLeftBtn', !hasImage ||
|
|
3342
|
-
this._setDisabled('rotateRightBtn', !hasImage ||
|
|
3343
|
-
this._setDisabled('addMaskBtn', !hasImage ||
|
|
3344
|
-
this._setDisabled('removeMaskBtn', !hasSelectedMask ||
|
|
3345
|
-
this._setDisabled('removeAllMasksBtn', !hasMasks ||
|
|
3346
|
-
this._setDisabled('mergeBtn', !hasImage || !hasMasks ||
|
|
3347
|
-
this._setDisabled('downloadBtn', !hasImage ||
|
|
3348
|
-
this._setDisabled('resetBtn', !hasImage || isDefaultTransform ||
|
|
3349
|
-
this._setDisabled('undoBtn', !hasImage ||
|
|
3350
|
-
this._setDisabled('redoBtn', !hasImage ||
|
|
3351
|
-
this._setDisabled('cropBtn', !hasImage ||
|
|
3737
|
+
this._setDisabled('zoomInBtn', !hasImage || isBusy || this.currentScale >= this.options.maxScale);
|
|
3738
|
+
this._setDisabled('zoomOutBtn', !hasImage || isBusy || this.currentScale <= this.options.minScale);
|
|
3739
|
+
this._setDisabled('rotateLeftBtn', !hasImage || isBusy);
|
|
3740
|
+
this._setDisabled('rotateRightBtn', !hasImage || isBusy);
|
|
3741
|
+
this._setDisabled('addMaskBtn', !hasImage || isBusy);
|
|
3742
|
+
this._setDisabled('removeMaskBtn', !hasSelectedMask || isBusy);
|
|
3743
|
+
this._setDisabled('removeAllMasksBtn', !hasMasks || isBusy);
|
|
3744
|
+
this._setDisabled('mergeBtn', !hasImage || !hasMasks || isBusy);
|
|
3745
|
+
this._setDisabled('downloadBtn', !hasImage || isBusy);
|
|
3746
|
+
this._setDisabled('resetBtn', !hasImage || isDefaultTransform || isBusy);
|
|
3747
|
+
this._setDisabled('undoBtn', !hasImage || isBusy || !canUndo);
|
|
3748
|
+
this._setDisabled('redoBtn', !hasImage || isBusy || !canRedo);
|
|
3749
|
+
this._setDisabled('cropBtn', !hasImage || isBusy);
|
|
3352
3750
|
this._setDisabled('applyCropBtn', true);
|
|
3353
3751
|
this._setDisabled('cancelCropBtn', true);
|
|
3354
|
-
this._setDisabled('
|
|
3355
|
-
this._setDisabled('
|
|
3752
|
+
this._setDisabled('scaleRate', !hasImage || isBusy);
|
|
3753
|
+
this._setDisabled('rotationLeftInput', !hasImage || isBusy);
|
|
3754
|
+
this._setDisabled('rotationRightInput', !hasImage || isBusy);
|
|
3755
|
+
this._setDisabled('maskList', !hasImage || isBusy);
|
|
3756
|
+
this._setDisabled('imageInput', isBusy);
|
|
3757
|
+
this._setDisabled('uploadArea', isBusy);
|
|
3356
3758
|
}
|
|
3357
3759
|
|
|
3358
3760
|
/**
|
|
@@ -3369,13 +3771,17 @@ function ensureFabric() {
|
|
|
3369
3771
|
element.disabled = !!disabled;
|
|
3370
3772
|
return;
|
|
3371
3773
|
}
|
|
3774
|
+
if (!this._elementOriginalPointerEvents) this._elementOriginalPointerEvents = new Map();
|
|
3775
|
+
if (!this._elementOriginalPointerEvents.has(key)) {
|
|
3776
|
+
this._elementOriginalPointerEvents.set(key, element.style.pointerEvents || '');
|
|
3777
|
+
}
|
|
3372
3778
|
|
|
3373
3779
|
if (disabled) {
|
|
3374
3780
|
element.setAttribute('aria-disabled', 'true');
|
|
3375
3781
|
element.style.pointerEvents = 'none';
|
|
3376
3782
|
} else {
|
|
3377
3783
|
element.removeAttribute('aria-disabled');
|
|
3378
|
-
element.style.pointerEvents = '';
|
|
3784
|
+
element.style.pointerEvents = this._elementOriginalPointerEvents.get(key) ?? '';
|
|
3379
3785
|
}
|
|
3380
3786
|
}
|
|
3381
3787
|
|
|
@@ -3474,6 +3880,9 @@ function ensureFabric() {
|
|
|
3474
3880
|
if (this.animationQueue) {
|
|
3475
3881
|
this.animationQueue.cancelAll(new Error('Editor disposed'));
|
|
3476
3882
|
}
|
|
3883
|
+
this._isLoading = false;
|
|
3884
|
+
this._activeOperationName = null;
|
|
3885
|
+
this._activeOperationToken = null;
|
|
3477
3886
|
|
|
3478
3887
|
// Remove bound DOM event listeners
|
|
3479
3888
|
try {
|
|
@@ -3520,17 +3929,20 @@ function ensureFabric() {
|
|
|
3520
3929
|
}
|
|
3521
3930
|
this._handlersByElementKey = {};
|
|
3522
3931
|
this._elementCache = {};
|
|
3932
|
+
this._elementOriginalPointerEvents = new Map();
|
|
3523
3933
|
this._clearMaskPlacementMemory();
|
|
3524
3934
|
this.originalImage = null;
|
|
3525
3935
|
this.baseImageScale = 1;
|
|
3526
3936
|
this.currentScale = 1;
|
|
3527
3937
|
this.currentRotation = 0;
|
|
3528
3938
|
this.isAnimating = false;
|
|
3939
|
+
this._isLoading = false;
|
|
3529
3940
|
this._cropMode = false;
|
|
3530
3941
|
this._cropRect = null;
|
|
3531
3942
|
this._cropHandlers = [];
|
|
3532
3943
|
this._cropPrevEvented = null;
|
|
3533
3944
|
this._prevSelectionSetting = undefined;
|
|
3945
|
+
this._lastContainerViewportSize = null;
|
|
3534
3946
|
this._initialized = false;
|
|
3535
3947
|
}
|
|
3536
3948
|
}
|
|
@@ -3585,6 +3997,7 @@ function ensureFabric() {
|
|
|
3585
3997
|
*/
|
|
3586
3998
|
this.isRunning = false;
|
|
3587
3999
|
this.currentTask = null;
|
|
4000
|
+
this._generation = 0;
|
|
3588
4001
|
}
|
|
3589
4002
|
|
|
3590
4003
|
/**
|
|
@@ -3607,6 +4020,7 @@ function ensureFabric() {
|
|
|
3607
4020
|
}
|
|
3608
4021
|
|
|
3609
4022
|
cancelAll(reason = new Error('Animation queue cancelled')) {
|
|
4023
|
+
this._generation += 1;
|
|
3610
4024
|
const cancellationError = reason instanceof Error ? reason : new Error(String(reason));
|
|
3611
4025
|
const tasks = [
|
|
3612
4026
|
...(this.currentTask ? [this.currentTask] : []),
|
|
@@ -3629,29 +4043,35 @@ function ensureFabric() {
|
|
|
3629
4043
|
*/
|
|
3630
4044
|
async _drainQueue() {
|
|
3631
4045
|
if (this.isRunning) return;
|
|
4046
|
+
const generation = this._generation;
|
|
3632
4047
|
this.isRunning = true;
|
|
3633
4048
|
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
4049
|
+
try {
|
|
4050
|
+
while (this.animationTasks.length > 0 && generation === this._generation) {
|
|
4051
|
+
const task = this.animationTasks.shift();
|
|
4052
|
+
this.currentTask = task;
|
|
3637
4053
|
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
4054
|
+
try {
|
|
4055
|
+
const result = await task.animationFn();
|
|
4056
|
+
if (generation === this._generation && !task.isSettled) {
|
|
4057
|
+
task.isSettled = true;
|
|
4058
|
+
task.resolve(result);
|
|
4059
|
+
}
|
|
4060
|
+
} catch (error) {
|
|
4061
|
+
if (generation === this._generation && !task.isSettled) {
|
|
4062
|
+
task.isSettled = true;
|
|
4063
|
+
task.reject(error);
|
|
4064
|
+
}
|
|
4065
|
+
} finally {
|
|
4066
|
+
if (generation === this._generation && this.currentTask === task) this.currentTask = null;
|
|
3648
4067
|
}
|
|
3649
|
-
}
|
|
3650
|
-
|
|
4068
|
+
}
|
|
4069
|
+
} finally {
|
|
4070
|
+
if (generation === this._generation) {
|
|
4071
|
+
this.isRunning = false;
|
|
4072
|
+
this.currentTask = null;
|
|
3651
4073
|
}
|
|
3652
4074
|
}
|
|
3653
|
-
|
|
3654
|
-
this.isRunning = false;
|
|
3655
4075
|
}
|
|
3656
4076
|
}
|
|
3657
4077
|
|
|
@@ -3722,9 +4142,9 @@ function ensureFabric() {
|
|
|
3722
4142
|
execute(command) {
|
|
3723
4143
|
const result = command.execute();
|
|
3724
4144
|
if (result && typeof result.then === 'function') {
|
|
3725
|
-
return Promise.resolve(result).then(() => {
|
|
4145
|
+
return this.enqueue(() => Promise.resolve(result).then(() => {
|
|
3726
4146
|
this.push(command);
|
|
3727
|
-
});
|
|
4147
|
+
}));
|
|
3728
4148
|
}
|
|
3729
4149
|
this.push(command);
|
|
3730
4150
|
return result;
|