@bensitu/image-editor 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/image-editor.esm.js +739 -288
- package/dist/image-editor.esm.js.map +2 -2
- package/dist/image-editor.esm.min.js +3 -3
- package/dist/image-editor.esm.min.js.map +3 -3
- package/dist/image-editor.esm.min.mjs +3 -3
- package/dist/image-editor.esm.min.mjs.map +3 -3
- package/dist/image-editor.esm.mjs +739 -288
- package/dist/image-editor.esm.mjs.map +2 -2
- package/dist/image-editor.js +739 -288
- package/dist/image-editor.js.map +2 -2
- package/dist/image-editor.min.js +2 -2
- package/dist/image-editor.min.js.map +3 -3
- package/image-editor.d.ts +2 -0
- package/package.json +2 -2
- package/src/image-editor.js +803 -312
package/src/image-editor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file image-editor.js
|
|
3
3
|
* @module image-editor
|
|
4
|
-
* @version 1.
|
|
4
|
+
* @version 1.4.0
|
|
5
5
|
* @author Ben Situ
|
|
6
6
|
* @license MIT
|
|
7
7
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
@@ -196,6 +196,8 @@ function ensureFabric() {
|
|
|
196
196
|
downsampleMaxWidth: 4000,
|
|
197
197
|
downsampleMaxHeight: 3000,
|
|
198
198
|
downsampleQuality: 0.92,
|
|
199
|
+
preserveSourceFormat: true,
|
|
200
|
+
downsampleMimeType: null,
|
|
199
201
|
imageLoadTimeoutMs: 30000,
|
|
200
202
|
|
|
201
203
|
exportMultiplier: 1,
|
|
@@ -255,6 +257,7 @@ function ensureFabric() {
|
|
|
255
257
|
this.maxHistorySize = 50;
|
|
256
258
|
|
|
257
259
|
this._handlersByElementKey = {};
|
|
260
|
+
this._elementCache = {};
|
|
258
261
|
|
|
259
262
|
this._lastMask = null;
|
|
260
263
|
this._lastMaskInitialLeft = null;
|
|
@@ -267,8 +270,14 @@ function ensureFabric() {
|
|
|
267
270
|
this._cropHandlers = [];
|
|
268
271
|
this._cropPrevEvented = null;
|
|
269
272
|
this._prevSelectionSetting = undefined;
|
|
270
|
-
this._containerOriginalOverflow =
|
|
273
|
+
this._containerOriginalOverflow = null;
|
|
274
|
+
this._lastContainerViewportSize = null;
|
|
275
|
+
this._canvasElementOriginalStyle = null;
|
|
276
|
+
this._visibilityStateByElement = new WeakMap();
|
|
271
277
|
this._scrollbarSizeCache = null;
|
|
278
|
+
this._activeAnimationRejectors = new Set();
|
|
279
|
+
this._disposed = false;
|
|
280
|
+
this._initialized = false;
|
|
272
281
|
|
|
273
282
|
this.onImageLoaded = typeof options.onImageLoaded === 'function' ? options.onImageLoaded : null;
|
|
274
283
|
|
|
@@ -341,6 +350,16 @@ function ensureFabric() {
|
|
|
341
350
|
*/
|
|
342
351
|
init(idMap = {}) {
|
|
343
352
|
if (!this._fabricLoaded) return;
|
|
353
|
+
if (this._initialized || this.canvas) this.dispose();
|
|
354
|
+
this._disposed = false;
|
|
355
|
+
this._initialized = true;
|
|
356
|
+
this.animationQueue = new AnimationQueue();
|
|
357
|
+
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
358
|
+
this._visibilityStateByElement = new WeakMap();
|
|
359
|
+
this._activeAnimationRejectors = new Set();
|
|
360
|
+
this._containerOriginalOverflow = null;
|
|
361
|
+
this._lastContainerViewportSize = null;
|
|
362
|
+
this._canvasElementOriginalStyle = null;
|
|
344
363
|
|
|
345
364
|
const defaults = {
|
|
346
365
|
canvas: 'fabricCanvas',
|
|
@@ -369,6 +388,7 @@ function ensureFabric() {
|
|
|
369
388
|
};
|
|
370
389
|
|
|
371
390
|
this.elements = { ...defaults, ...idMap };
|
|
391
|
+
this._elementCache = {};
|
|
372
392
|
|
|
373
393
|
this._initCanvas();
|
|
374
394
|
this._bindEvents();
|
|
@@ -413,19 +433,25 @@ function ensureFabric() {
|
|
|
413
433
|
* @private
|
|
414
434
|
*/
|
|
415
435
|
_initCanvas() {
|
|
416
|
-
const canvasElement =
|
|
436
|
+
const canvasElement = this._getElement('canvas');
|
|
417
437
|
if (!canvasElement) throw new Error('Canvas is not found: ' + this.elements.canvas);
|
|
418
438
|
this.canvasElement = canvasElement;
|
|
439
|
+
this._canvasElementOriginalStyle = {
|
|
440
|
+
display: canvasElement.style.display || '',
|
|
441
|
+
width: canvasElement.style.width || '',
|
|
442
|
+
height: canvasElement.style.height || '',
|
|
443
|
+
maxWidth: canvasElement.style.maxWidth || ''
|
|
444
|
+
};
|
|
419
445
|
|
|
420
446
|
// Decide which element acts as the viewport for size fallback and scrolling.
|
|
421
447
|
if (this.elements.canvasContainer) {
|
|
422
|
-
const containerElement =
|
|
448
|
+
const containerElement = this._getElement('canvasContainer');
|
|
423
449
|
this.containerElement = containerElement || canvasElement.parentElement;
|
|
424
450
|
} else {
|
|
425
451
|
this.containerElement = canvasElement.parentElement;
|
|
426
452
|
}
|
|
427
453
|
|
|
428
|
-
this.placeholderElement =
|
|
454
|
+
this.placeholderElement = this._getElement('imgPlaceholder') || null;
|
|
429
455
|
|
|
430
456
|
// Prefer a measured container size when it is available.
|
|
431
457
|
let initialWidth = this.options.canvasWidth;
|
|
@@ -436,6 +462,11 @@ function ensureFabric() {
|
|
|
436
462
|
if (containerWidth > 0 && containerHeight > 0) {
|
|
437
463
|
initialWidth = containerWidth;
|
|
438
464
|
initialHeight = containerHeight;
|
|
465
|
+
|
|
466
|
+
this._lastContainerViewportSize = {
|
|
467
|
+
width: containerWidth,
|
|
468
|
+
height: containerHeight
|
|
469
|
+
};
|
|
439
470
|
}
|
|
440
471
|
}
|
|
441
472
|
|
|
@@ -460,6 +491,24 @@ function ensureFabric() {
|
|
|
460
491
|
this.canvasElement.style.display = 'block';
|
|
461
492
|
}
|
|
462
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Returns a configured DOM element and caches lookups for hot UI paths.
|
|
496
|
+
*
|
|
497
|
+
* @param {string} key - Key in the configured element map.
|
|
498
|
+
* @returns {HTMLElement|null} The configured element, or null when missing.
|
|
499
|
+
* @private
|
|
500
|
+
*/
|
|
501
|
+
_getElement(key) {
|
|
502
|
+
const id = this.elements && this.elements[key];
|
|
503
|
+
if (!id) return null;
|
|
504
|
+
if (this._elementCache && Object.prototype.hasOwnProperty.call(this._elementCache, key)) {
|
|
505
|
+
return this._elementCache[key];
|
|
506
|
+
}
|
|
507
|
+
const element = document.getElementById(id);
|
|
508
|
+
if (this._elementCache) this._elementCache[key] = element || null;
|
|
509
|
+
return element || null;
|
|
510
|
+
}
|
|
511
|
+
|
|
463
512
|
/**
|
|
464
513
|
* Records a history entry after Fabric finishes modifying one or more masks.
|
|
465
514
|
*
|
|
@@ -504,9 +553,7 @@ function ensureFabric() {
|
|
|
504
553
|
*/
|
|
505
554
|
_syncContainerOverflow(options = {}) {
|
|
506
555
|
if (!this.containerElement || !this.containerElement.style) return;
|
|
507
|
-
|
|
508
|
-
this._containerOriginalOverflow = this.containerElement.style.overflow || '';
|
|
509
|
-
}
|
|
556
|
+
this._captureContainerOverflowState();
|
|
510
557
|
|
|
511
558
|
const shouldPreserveScroll = options.preserveScroll === true;
|
|
512
559
|
if (this.options.coverImageToCanvas) {
|
|
@@ -522,10 +569,26 @@ function ensureFabric() {
|
|
|
522
569
|
this.containerElement.scrollTop = 0;
|
|
523
570
|
}
|
|
524
571
|
} else {
|
|
525
|
-
this.
|
|
572
|
+
this._restoreContainerOverflowState();
|
|
526
573
|
}
|
|
527
574
|
}
|
|
528
575
|
|
|
576
|
+
_captureContainerOverflowState() {
|
|
577
|
+
if (!this.containerElement || !this.containerElement.style || this._containerOriginalOverflow) return;
|
|
578
|
+
this._containerOriginalOverflow = {
|
|
579
|
+
overflow: this.containerElement.style.overflow || '',
|
|
580
|
+
overflowX: this.containerElement.style.overflowX || '',
|
|
581
|
+
overflowY: this.containerElement.style.overflowY || ''
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
_restoreContainerOverflowState() {
|
|
586
|
+
if (!this.containerElement || !this.containerElement.style || !this._containerOriginalOverflow) return;
|
|
587
|
+
this.containerElement.style.overflow = this._containerOriginalOverflow.overflow;
|
|
588
|
+
this.containerElement.style.overflowX = this._containerOriginalOverflow.overflowX;
|
|
589
|
+
this.containerElement.style.overflowY = this._containerOriginalOverflow.overflowY;
|
|
590
|
+
}
|
|
591
|
+
|
|
529
592
|
/**
|
|
530
593
|
* DOM / UI bindings
|
|
531
594
|
* @private
|
|
@@ -533,54 +596,61 @@ function ensureFabric() {
|
|
|
533
596
|
_bindEvents() {
|
|
534
597
|
// Click anywhere on the upload area opens the native file dialog
|
|
535
598
|
this._bindIfExists('uploadArea', 'click', () => {
|
|
536
|
-
const uploadAreaElement =
|
|
599
|
+
const uploadAreaElement = this._getElement('uploadArea');
|
|
537
600
|
if (this._isElementDisabled(uploadAreaElement)) return;
|
|
538
|
-
|
|
601
|
+
this._getElement('imageInput')?.click();
|
|
539
602
|
});
|
|
540
603
|
// File-input change
|
|
541
604
|
this._bindIfExists('imageInput', 'change', (event) => {
|
|
542
605
|
const file = event.target.files && event.target.files[0];
|
|
543
|
-
if (file)
|
|
606
|
+
if (file) {
|
|
607
|
+
this._loadImageFile(file)
|
|
608
|
+
.catch(error => this._reportError('Image file could not be loaded', error))
|
|
609
|
+
.finally(() => {
|
|
610
|
+
event.target.value = '';
|
|
611
|
+
});
|
|
612
|
+
}
|
|
544
613
|
});
|
|
545
614
|
// Zoom & reset
|
|
546
|
-
this._bindIfExists('zoomInBtn', 'click', () => this.scaleImage(this.currentScale + this.options.scaleStep));
|
|
547
|
-
this._bindIfExists('zoomOutBtn', 'click', () => this.scaleImage(this.currentScale - this.options.scaleStep));
|
|
548
|
-
this._bindIfExists('resetBtn', 'click', () => { this.resetImageTransform(); });
|
|
615
|
+
this._bindIfExists('zoomInBtn', 'click', () => this.scaleImage(this.currentScale + this.options.scaleStep).catch(error => this._reportError('scaleImage failed', error)));
|
|
616
|
+
this._bindIfExists('zoomOutBtn', 'click', () => this.scaleImage(this.currentScale - this.options.scaleStep).catch(error => this._reportError('scaleImage failed', error)));
|
|
617
|
+
this._bindIfExists('resetBtn', 'click', () => { this.resetImageTransform().catch(error => this._reportError('resetImageTransform failed', error)); });
|
|
549
618
|
// Mask management
|
|
550
619
|
this._bindIfExists('addMaskBtn', 'click', () => this.createMask());
|
|
551
620
|
this._bindIfExists('removeMaskBtn', 'click', () => this.removeSelectedMask());
|
|
552
621
|
this._bindIfExists('removeAllMasksBtn', 'click', () => this.removeAllMasks());
|
|
553
622
|
// Merge + download
|
|
554
|
-
this._bindIfExists('mergeBtn', 'click', () => this.mergeMasks());
|
|
623
|
+
this._bindIfExists('mergeBtn', 'click', () => this.mergeMasks().catch(error => this._reportError('merge error', error)));
|
|
555
624
|
this._bindIfExists('downloadBtn', 'click', () => this.downloadImage());
|
|
556
625
|
// Undo + Redo
|
|
557
|
-
this._bindIfExists('undoBtn', 'click', () => this.undo());
|
|
558
|
-
this._bindIfExists('redoBtn', 'click', () => this.redo());
|
|
626
|
+
this._bindIfExists('undoBtn', 'click', () => this.undo().catch(error => this._reportError('undo failed', error)));
|
|
627
|
+
this._bindIfExists('redoBtn', 'click', () => this.redo().catch(error => this._reportError('redo failed', error)));
|
|
559
628
|
|
|
560
629
|
// Rotation buttons (step can be overridden by two input fields)
|
|
561
630
|
this._bindIfExists('rotateLeftBtn', 'click', () => {
|
|
562
|
-
const rotationInputElement =
|
|
631
|
+
const rotationInputElement = this._getElement('rotationLeftInput');
|
|
563
632
|
let step = this.options.rotationStep;
|
|
564
633
|
if (rotationInputElement) {
|
|
565
634
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
566
635
|
if (!isNaN(parsedStep)) step = parsedStep;
|
|
567
636
|
}
|
|
568
|
-
this.rotateImage(this.currentRotation - step);
|
|
637
|
+
this.rotateImage(this.currentRotation - step).catch(error => this._reportError('rotateImage failed', error));
|
|
569
638
|
});
|
|
570
639
|
this._bindIfExists('rotateRightBtn', 'click', () => {
|
|
571
|
-
const rotationInputElement =
|
|
640
|
+
const rotationInputElement = this._getElement('rotationRightInput');
|
|
572
641
|
let step = this.options.rotationStep;
|
|
573
642
|
if (rotationInputElement) {
|
|
574
643
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
575
644
|
if (!isNaN(parsedStep)) step = parsedStep;
|
|
576
645
|
}
|
|
577
|
-
this.rotateImage(this.currentRotation + step);
|
|
646
|
+
this.rotateImage(this.currentRotation + step).catch(error => this._reportError('rotateImage failed', error));
|
|
578
647
|
});
|
|
579
648
|
|
|
580
649
|
// Crop bindings (optional: bound only if element IDs exist in elements)
|
|
581
650
|
this._bindIfExists('cropBtn', 'click', () => this.enterCropMode());
|
|
582
651
|
this._bindIfExists('applyCropBtn', 'click', () => { this.applyCrop().catch(error => this._reportError('applyCrop failed', error)); });
|
|
583
652
|
this._bindIfExists('cancelCropBtn', 'click', () => this.cancelCrop());
|
|
653
|
+
this._bindIfExists('maskList', 'click', (event) => this._handleMaskListClick(event));
|
|
584
654
|
}
|
|
585
655
|
|
|
586
656
|
/**
|
|
@@ -592,7 +662,7 @@ function ensureFabric() {
|
|
|
592
662
|
* @private
|
|
593
663
|
*/
|
|
594
664
|
_bindIfExists(key, eventName, handler) {
|
|
595
|
-
const element =
|
|
665
|
+
const element = this._getElement(key);
|
|
596
666
|
if (element) {
|
|
597
667
|
element.addEventListener(eventName, handler);
|
|
598
668
|
this._handlersByElementKey = this._handlersByElementKey || {};
|
|
@@ -605,14 +675,37 @@ function ensureFabric() {
|
|
|
605
675
|
* Reads an image File as a data URL and loads it into the Fabric canvas.
|
|
606
676
|
*
|
|
607
677
|
* @param {File} file - Image file selected by the user.
|
|
678
|
+
* @returns {Promise<void>} Resolves after the selected file is loaded.
|
|
608
679
|
* @private
|
|
609
680
|
*/
|
|
610
681
|
_loadImageFile(file) {
|
|
611
|
-
if (!
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
682
|
+
if (!this._isSupportedImageFile(file)) {
|
|
683
|
+
const error = new Error('Selected file is not a supported image');
|
|
684
|
+
this._reportError('Selected file is not a supported image', error);
|
|
685
|
+
return Promise.reject(error);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return new Promise((resolve, reject) => {
|
|
689
|
+
const reader = new FileReader();
|
|
690
|
+
reader.onload = (event) => {
|
|
691
|
+
this.loadImage(event.target.result)
|
|
692
|
+
.then(resolve)
|
|
693
|
+
.catch(reject);
|
|
694
|
+
};
|
|
695
|
+
reader.onerror = (event) => {
|
|
696
|
+
const error = new Error('Image file could not be read');
|
|
697
|
+
this._reportError('Image file could not be read', event);
|
|
698
|
+
reject(error);
|
|
699
|
+
};
|
|
700
|
+
reader.readAsDataURL(file);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
_isSupportedImageFile(file) {
|
|
705
|
+
if (!file) return false;
|
|
706
|
+
if (typeof file.type === 'string' && file.type.startsWith('image/')) return true;
|
|
707
|
+
const fileName = String(file.name || '');
|
|
708
|
+
return /\.(avif|bmp|gif|jpe?g|png|webp)$/i.test(fileName);
|
|
616
709
|
}
|
|
617
710
|
|
|
618
711
|
/**
|
|
@@ -645,120 +738,113 @@ function ensureFabric() {
|
|
|
645
738
|
*/
|
|
646
739
|
async loadImage(imageBase64, options = {}) {
|
|
647
740
|
if (!this._fabricLoaded) return;
|
|
648
|
-
if (!this.canvas) return;
|
|
741
|
+
if (!this.canvas || this._disposed) return;
|
|
649
742
|
if (!imageBase64 || typeof imageBase64 !== 'string' || !imageBase64.startsWith('data:image/')) return;
|
|
743
|
+
this._assertIdleForOperation('loadImage');
|
|
650
744
|
|
|
651
745
|
this._warnOnImageLayoutOptionConflict();
|
|
652
|
-
this.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
746
|
+
const transaction = this._captureLoadImageTransaction();
|
|
747
|
+
|
|
748
|
+
try {
|
|
749
|
+
const imageElement = await this._createImageElement(imageBase64);
|
|
750
|
+
if (this._disposed || !this.canvas) throw new Error('Editor was disposed while loading image');
|
|
751
|
+
|
|
752
|
+
let loadSource = imageBase64;
|
|
753
|
+
if (this.options.downsampleOnLoad) {
|
|
754
|
+
const shouldResize =
|
|
755
|
+
imageElement.naturalWidth > this.options.downsampleMaxWidth ||
|
|
756
|
+
imageElement.naturalHeight > this.options.downsampleMaxHeight;
|
|
757
|
+
if (shouldResize) {
|
|
758
|
+
const ratio = Math.min(
|
|
759
|
+
this.options.downsampleMaxWidth / imageElement.naturalWidth,
|
|
760
|
+
this.options.downsampleMaxHeight / imageElement.naturalHeight
|
|
761
|
+
);
|
|
762
|
+
const targetWidth = Math.round(imageElement.naturalWidth * ratio);
|
|
763
|
+
const targetHeight = Math.round(imageElement.naturalHeight * ratio);
|
|
764
|
+
loadSource = this._resampleImageToDataURL(
|
|
765
|
+
imageElement,
|
|
766
|
+
targetWidth,
|
|
767
|
+
targetHeight,
|
|
768
|
+
this.options.downsampleQuality,
|
|
769
|
+
imageBase64
|
|
770
|
+
);
|
|
771
|
+
}
|
|
670
772
|
}
|
|
671
|
-
}
|
|
672
773
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
fabricImage.set({ left: 0, top: 0 });
|
|
723
|
-
fabricImage.scale(fitScale);
|
|
724
|
-
this.baseImageScale = fabricImage.scaleX || 1;
|
|
725
|
-
}
|
|
726
|
-
// Put the image onto the canvas
|
|
727
|
-
this.originalImage = fabricImage;
|
|
728
|
-
this.canvas.add(fabricImage);
|
|
729
|
-
this.canvas.sendToBack(fabricImage);
|
|
730
|
-
|
|
731
|
-
// Reset mask placement memory
|
|
732
|
-
this._lastMask = null;
|
|
733
|
-
this._lastMaskInitialLeft = null;
|
|
734
|
-
this._lastMaskInitialTop = null;
|
|
735
|
-
this._lastMaskInitialWidth = null;
|
|
736
|
-
|
|
737
|
-
this.maskCounter = 0;
|
|
738
|
-
this.currentScale = 1;
|
|
739
|
-
this.currentRotation = 0;
|
|
740
|
-
|
|
741
|
-
this._updateInputs();
|
|
742
|
-
this._updateMaskList();
|
|
743
|
-
this.isImageLoadedToCanvas = true;
|
|
744
|
-
this._updateUI();
|
|
745
|
-
this.canvas.renderAll();
|
|
746
|
-
try {
|
|
747
|
-
this._lastSnapshot = this._serializeCanvasState();
|
|
748
|
-
} catch (error) {
|
|
749
|
-
this._reportWarning('loadImage: failed to capture initial canvas snapshot', error);
|
|
750
|
-
}
|
|
774
|
+
const fabricImage = await this._createFabricImageFromURL(loadSource);
|
|
775
|
+
if (this._disposed || !this.canvas) throw new Error('Editor was disposed while loading image');
|
|
776
|
+
|
|
777
|
+
this.canvas.discardActiveObject();
|
|
778
|
+
this._hideAllMaskLabels();
|
|
779
|
+
this.canvas.clear();
|
|
780
|
+
this.canvas.setBackgroundColor(this.options.backgroundColor, this.canvas.renderAll.bind(this.canvas));
|
|
781
|
+
|
|
782
|
+
fabricImage.set({ originX: 'left', originY: 'top', selectable: false, evented: false });
|
|
783
|
+
this._setPlaceholderVisible(false);
|
|
784
|
+
this._syncContainerOverflow({ preserveScroll: options.preserveScroll === true });
|
|
785
|
+
|
|
786
|
+
const imageWidth = fabricImage.width;
|
|
787
|
+
const imageHeight = fabricImage.height;
|
|
788
|
+
|
|
789
|
+
const viewport = this._getContainerViewportSize();
|
|
790
|
+
const minWidth = viewport.width;
|
|
791
|
+
const minHeight = viewport.height;
|
|
792
|
+
|
|
793
|
+
if (this.options.fitImageToCanvas) {
|
|
794
|
+
const canvasWidth = Math.max(1, minWidth - 1);
|
|
795
|
+
const canvasHeight = Math.max(1, minHeight - 1);
|
|
796
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
797
|
+
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
798
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
799
|
+
fabricImage.scale(fitScale);
|
|
800
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
801
|
+
} else if (this.options.coverImageToCanvas) {
|
|
802
|
+
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
803
|
+
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
804
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
805
|
+
fabricImage.scale(layout.scale);
|
|
806
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
807
|
+
} else if (this.options.expandCanvasToImage) {
|
|
808
|
+
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
809
|
+
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
810
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
811
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
812
|
+
fabricImage.scale(1);
|
|
813
|
+
this.baseImageScale = 1;
|
|
814
|
+
} else {
|
|
815
|
+
const canvasWidth = Math.max(this.options.canvasWidth, minWidth);
|
|
816
|
+
const canvasHeight = Math.max(this.options.canvasHeight, minHeight);
|
|
817
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
818
|
+
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
819
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
820
|
+
fabricImage.scale(fitScale);
|
|
821
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
822
|
+
}
|
|
751
823
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
824
|
+
this.originalImage = fabricImage;
|
|
825
|
+
this.canvas.add(fabricImage);
|
|
826
|
+
this.canvas.sendToBack(fabricImage);
|
|
755
827
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
828
|
+
this._clearMaskPlacementMemory();
|
|
829
|
+
if (options.resetMaskCounter !== false) this.maskCounter = 0;
|
|
830
|
+
this.currentScale = 1;
|
|
831
|
+
this.currentRotation = 0;
|
|
832
|
+
|
|
833
|
+
// this._setPlaceholderVisible(false);
|
|
834
|
+
this._updateInputs();
|
|
835
|
+
this._updateMaskList();
|
|
836
|
+
this.isImageLoadedToCanvas = true;
|
|
837
|
+
this._updateUI();
|
|
838
|
+
this.canvas.renderAll();
|
|
839
|
+
this._lastSnapshot = this._captureCanvasStateOrThrow('loadImage');
|
|
840
|
+
|
|
841
|
+
if (typeof this.onImageLoaded === 'function') {
|
|
842
|
+
this.onImageLoaded();
|
|
843
|
+
}
|
|
844
|
+
} catch (error) {
|
|
845
|
+
await this._rollbackLoadImageTransaction(transaction);
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
762
848
|
}
|
|
763
849
|
|
|
764
850
|
/**
|
|
@@ -810,24 +896,142 @@ function ensureFabric() {
|
|
|
810
896
|
});
|
|
811
897
|
}
|
|
812
898
|
|
|
899
|
+
_createFabricImageFromURL(dataUrl, timeoutMs = this.options.imageLoadTimeoutMs) {
|
|
900
|
+
return new Promise((resolve, reject) => {
|
|
901
|
+
const safeTimeoutMs = this._getSafeTimeoutMs(timeoutMs);
|
|
902
|
+
let isSettled = false;
|
|
903
|
+
let timerId;
|
|
904
|
+
const settle = (callback) => {
|
|
905
|
+
if (isSettled) return;
|
|
906
|
+
isSettled = true;
|
|
907
|
+
clearTimeout(timerId);
|
|
908
|
+
callback();
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
timerId = setTimeout(() => {
|
|
912
|
+
settle(() => reject(new Error('Fabric image load timed out')));
|
|
913
|
+
}, safeTimeoutMs);
|
|
914
|
+
|
|
915
|
+
try {
|
|
916
|
+
fabric.Image.fromURL(dataUrl, (fabricImage) => {
|
|
917
|
+
settle(() => {
|
|
918
|
+
if (!fabricImage) {
|
|
919
|
+
reject(new Error('Image could not be loaded'));
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
resolve(fabricImage);
|
|
923
|
+
});
|
|
924
|
+
}, { crossOrigin: 'anonymous' });
|
|
925
|
+
} catch (error) {
|
|
926
|
+
settle(() => reject(error));
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
_getSafeTimeoutMs(timeoutMs) {
|
|
932
|
+
const safeTimeoutMs = Number(timeoutMs);
|
|
933
|
+
return Number.isFinite(safeTimeoutMs) && safeTimeoutMs > 0 ? safeTimeoutMs : 30000;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
_captureLoadImageTransaction() {
|
|
937
|
+
return {
|
|
938
|
+
canvasState: this._serializeCanvasState(),
|
|
939
|
+
originalImage: this.originalImage,
|
|
940
|
+
baseImageScale: this.baseImageScale,
|
|
941
|
+
currentScale: this.currentScale,
|
|
942
|
+
currentRotation: this.currentRotation,
|
|
943
|
+
maskCounter: this.maskCounter,
|
|
944
|
+
isImageLoadedToCanvas: this.isImageLoadedToCanvas,
|
|
945
|
+
lastSnapshot: this._lastSnapshot,
|
|
946
|
+
lastMask: this._lastMask,
|
|
947
|
+
lastMaskInitialLeft: this._lastMaskInitialLeft,
|
|
948
|
+
lastMaskInitialTop: this._lastMaskInitialTop,
|
|
949
|
+
lastMaskInitialWidth: this._lastMaskInitialWidth,
|
|
950
|
+
containerOverflow: this.containerElement && this.containerElement.style ? {
|
|
951
|
+
overflow: this.containerElement.style.overflow || '',
|
|
952
|
+
overflowX: this.containerElement.style.overflowX || '',
|
|
953
|
+
overflowY: this.containerElement.style.overflowY || ''
|
|
954
|
+
} : null,
|
|
955
|
+
scrollLeft: this.containerElement ? this.containerElement.scrollLeft : 0,
|
|
956
|
+
scrollTop: this.containerElement ? this.containerElement.scrollTop : 0,
|
|
957
|
+
placeholderVisibility: this._captureElementVisibility(this.placeholderElement),
|
|
958
|
+
canvasVisibility: this._captureElementVisibility(this._getCanvasVisibilityElement())
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
async _rollbackLoadImageTransaction(transaction) {
|
|
963
|
+
if (!transaction || !this.canvas || this._disposed) return;
|
|
964
|
+
try {
|
|
965
|
+
if (transaction.canvasState) await this.loadFromState(transaction.canvasState);
|
|
966
|
+
} catch (error) {
|
|
967
|
+
this._reportError('loadImage rollback failed', error);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
this.baseImageScale = transaction.baseImageScale;
|
|
971
|
+
this.currentScale = transaction.currentScale;
|
|
972
|
+
this.currentRotation = transaction.currentRotation;
|
|
973
|
+
this.maskCounter = transaction.maskCounter;
|
|
974
|
+
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
975
|
+
this._lastSnapshot = transaction.lastSnapshot;
|
|
976
|
+
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
977
|
+
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
978
|
+
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
979
|
+
this._containerOriginalOverflow = transaction.containerOverflow;
|
|
980
|
+
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
981
|
+
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
982
|
+
if (this.containerElement) {
|
|
983
|
+
this.containerElement.scrollLeft = transaction.scrollLeft;
|
|
984
|
+
this.containerElement.scrollTop = transaction.scrollTop;
|
|
985
|
+
this._restoreContainerOverflowState();
|
|
986
|
+
}
|
|
987
|
+
this._updateInputs();
|
|
988
|
+
this._updateMaskList();
|
|
989
|
+
this._updateUI();
|
|
990
|
+
if (this.canvas) this.canvas.renderAll();
|
|
991
|
+
}
|
|
992
|
+
|
|
813
993
|
/**
|
|
814
|
-
* Resamples the given image element to a new width and height and returns the result as a
|
|
994
|
+
* Resamples the given image element to a new width and height and returns the result as a data URL.
|
|
815
995
|
*
|
|
816
996
|
* @param {HTMLImageElement} imageElement - The image element to resample.
|
|
817
997
|
* @param {number} targetWidth - Target width (in pixels) for the resampled image.
|
|
818
998
|
* @param {number} targetHeight - Target height (in pixels) for the resampled image.
|
|
819
|
-
* @param {number} [quality=0.92] -
|
|
820
|
-
* @
|
|
999
|
+
* @param {number} [quality=0.92] - Image quality between 0 and 1 for lossy formats.
|
|
1000
|
+
* @param {string|null} [sourceDataUrl=null] - Source data URL used to preserve alpha-capable formats.
|
|
1001
|
+
* @returns {string} A data URL representing the resampled image.
|
|
821
1002
|
* @private
|
|
822
1003
|
*/
|
|
823
|
-
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92) {
|
|
1004
|
+
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
|
|
824
1005
|
const offscreenCanvas = document.createElement('canvas');
|
|
825
1006
|
offscreenCanvas.width = targetWidth;
|
|
826
1007
|
offscreenCanvas.height = targetHeight;
|
|
827
1008
|
const context = offscreenCanvas.getContext('2d');
|
|
828
1009
|
if (!context) throw new Error('2D canvas context is unavailable');
|
|
829
1010
|
context.drawImage(imageElement, 0, 0, imageElement.naturalWidth, imageElement.naturalHeight, 0, 0, targetWidth, targetHeight);
|
|
830
|
-
return offscreenCanvas.toDataURL(
|
|
1011
|
+
return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
_getDataUrlMimeType(dataUrl) {
|
|
1015
|
+
const match = String(dataUrl || '').match(/^data:([^;,]+)[;,]/i);
|
|
1016
|
+
return match ? match[1].toLowerCase() : '';
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
_getDownsampleMimeType(sourceDataUrl) {
|
|
1020
|
+
if (this.options.downsampleMimeType) {
|
|
1021
|
+
const requestedFormat = this._normalizeImageFormat(this.options.downsampleMimeType);
|
|
1022
|
+
return `image/${requestedFormat}`;
|
|
1023
|
+
}
|
|
1024
|
+
const sourceMimeType = this._getDataUrlMimeType(sourceDataUrl);
|
|
1025
|
+
if (this.options.preserveSourceFormat !== false && (sourceMimeType === 'image/png' || sourceMimeType === 'image/webp')) {
|
|
1026
|
+
return sourceMimeType;
|
|
1027
|
+
}
|
|
1028
|
+
return 'image/jpeg';
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
_captureCanvasStateOrThrow(context) {
|
|
1032
|
+
const snapshot = this._serializeCanvasState();
|
|
1033
|
+
if (!snapshot) throw new Error(`${context}: canvas state is unavailable`);
|
|
1034
|
+
return snapshot;
|
|
831
1035
|
}
|
|
832
1036
|
|
|
833
1037
|
/**
|
|
@@ -849,7 +1053,6 @@ function ensureFabric() {
|
|
|
849
1053
|
if (this.canvasElement) {
|
|
850
1054
|
this.canvasElement.style.width = integerWidth + 'px';
|
|
851
1055
|
this.canvasElement.style.height = integerHeight + 'px';
|
|
852
|
-
this.canvasElement.style.maxWidth = 'none';
|
|
853
1056
|
}
|
|
854
1057
|
}
|
|
855
1058
|
|
|
@@ -868,8 +1071,14 @@ function ensureFabric() {
|
|
|
868
1071
|
};
|
|
869
1072
|
}
|
|
870
1073
|
|
|
871
|
-
|
|
872
|
-
|
|
1074
|
+
const measuredWidth = Math.floor(this.containerElement.clientWidth || 0);
|
|
1075
|
+
const measuredHeight = Math.floor(this.containerElement.clientHeight || 0);
|
|
1076
|
+
let width = Math.max(1, measuredWidth || this._lastContainerViewportSize?.width || this.options.canvasWidth || 1);
|
|
1077
|
+
let height = Math.max(1, measuredHeight || this._lastContainerViewportSize?.height || this.options.canvasHeight || 1);
|
|
1078
|
+
|
|
1079
|
+
if (measuredWidth > 0 && measuredHeight > 0) {
|
|
1080
|
+
this._lastContainerViewportSize = { width: measuredWidth, height: measuredHeight };
|
|
1081
|
+
}
|
|
873
1082
|
|
|
874
1083
|
if (this._hasFixedContainerScrollbars()) {
|
|
875
1084
|
return { width, height };
|
|
@@ -1266,6 +1475,7 @@ function ensureFabric() {
|
|
|
1266
1475
|
};
|
|
1267
1476
|
timerId = setTimeout(() => {
|
|
1268
1477
|
settle(() => reject(new Error('Image crop load timed out')));
|
|
1478
|
+
// Clearing src prevents later network/decode work; already-dispatched onload work is guarded by settle().
|
|
1269
1479
|
try { imageElement.src = ''; } catch (error) { void error; }
|
|
1270
1480
|
}, safeTimeoutMs);
|
|
1271
1481
|
imageElement.onload = () => {
|
|
@@ -1293,7 +1503,7 @@ function ensureFabric() {
|
|
|
1293
1503
|
}
|
|
1294
1504
|
|
|
1295
1505
|
/**
|
|
1296
|
-
* Exports
|
|
1506
|
+
* Exports a source region directly through Fabric's region export options.
|
|
1297
1507
|
*
|
|
1298
1508
|
* @param {Object} region - Canvas source region and export options.
|
|
1299
1509
|
* @param {number} region.sourceX - Source region x coordinate.
|
|
@@ -1306,15 +1516,17 @@ function ensureFabric() {
|
|
|
1306
1516
|
* @returns {Promise<string>} Resolves with an image data URL for the cropped region.
|
|
1307
1517
|
* @private
|
|
1308
1518
|
*/
|
|
1309
|
-
|
|
1519
|
+
_exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = 'jpeg' }) {
|
|
1310
1520
|
const safeMultiplier = Math.max(1, Number(multiplier) || 1);
|
|
1311
|
-
|
|
1521
|
+
return this.canvas.toDataURL({
|
|
1312
1522
|
format,
|
|
1313
1523
|
quality,
|
|
1314
|
-
multiplier: safeMultiplier
|
|
1524
|
+
multiplier: safeMultiplier,
|
|
1525
|
+
left: sourceX,
|
|
1526
|
+
top: sourceY,
|
|
1527
|
+
width: sourceWidth,
|
|
1528
|
+
height: sourceHeight
|
|
1315
1529
|
});
|
|
1316
|
-
|
|
1317
|
-
return this._cropDataUrl(fullDataUrl, sourceX, sourceY, sourceWidth, sourceHeight, safeMultiplier, format, quality);
|
|
1318
1530
|
}
|
|
1319
1531
|
|
|
1320
1532
|
/**
|
|
@@ -1328,12 +1540,41 @@ function ensureFabric() {
|
|
|
1328
1540
|
_getObjectTopLeftPoint(fabricObject) {
|
|
1329
1541
|
if (!fabricObject) return { x: 0, y: 0 };
|
|
1330
1542
|
fabricObject.setCoords();
|
|
1331
|
-
const coords = typeof fabricObject.getCoords === 'function' ? fabricObject.getCoords() : null;
|
|
1332
|
-
if (coords && coords.length) return coords[0];
|
|
1333
1543
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1334
1544
|
return { x: boundingRect.left, y: boundingRect.top };
|
|
1335
1545
|
}
|
|
1336
1546
|
|
|
1547
|
+
_getObjectCoordinateTopLeftPoint(fabricObject) {
|
|
1548
|
+
if (!fabricObject) return { x: 0, y: 0 };
|
|
1549
|
+
fabricObject.setCoords();
|
|
1550
|
+
const coords = typeof fabricObject.getCoords === 'function' ? fabricObject.getCoords() : null;
|
|
1551
|
+
if (coords && coords.length) return coords[0];
|
|
1552
|
+
return this._getObjectTopLeftPoint(fabricObject);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
_getObjectOriginPoint(fabricObject, originX, originY) {
|
|
1556
|
+
if (!fabricObject) return { x: 0, y: 0 };
|
|
1557
|
+
if (typeof fabricObject.getPointByOrigin === 'function') {
|
|
1558
|
+
return fabricObject.getPointByOrigin(originX, originY);
|
|
1559
|
+
}
|
|
1560
|
+
return this._getObjectTopLeftPoint(fabricObject);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
_translateObjectByCanvasOffset(fabricObject, deltaX, deltaY) {
|
|
1564
|
+
if (!fabricObject) return;
|
|
1565
|
+
if (typeof fabricObject.getCenterPoint === 'function' && typeof fabricObject.setPositionByOrigin === 'function') {
|
|
1566
|
+
const center = fabricObject.getCenterPoint();
|
|
1567
|
+
const nextCenter = new fabric.Point(center.x + deltaX, center.y + deltaY);
|
|
1568
|
+
fabricObject.setPositionByOrigin(nextCenter, 'center', 'center');
|
|
1569
|
+
} else {
|
|
1570
|
+
fabricObject.set({
|
|
1571
|
+
left: (fabricObject.left || 0) + deltaX,
|
|
1572
|
+
top: (fabricObject.top || 0) + deltaY
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
fabricObject.setCoords();
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1337
1578
|
/**
|
|
1338
1579
|
* Sets the object's origin at the specified origin point, keeping a reference point fixed in position.
|
|
1339
1580
|
*
|
|
@@ -1402,8 +1643,10 @@ function ensureFabric() {
|
|
|
1402
1643
|
_expandCanvasToFitObjects(fabricObjects, padding = 10) {
|
|
1403
1644
|
if (!this.canvas || !Array.isArray(fabricObjects) || !fabricObjects.length || !this._shouldResizeCanvasToContentBounds()) return;
|
|
1404
1645
|
try {
|
|
1405
|
-
|
|
1406
|
-
|
|
1646
|
+
const currentWidth = this.canvas.getWidth();
|
|
1647
|
+
const currentHeight = this.canvas.getHeight();
|
|
1648
|
+
let requiredWidth = currentWidth;
|
|
1649
|
+
let requiredHeight = currentHeight;
|
|
1407
1650
|
fabricObjects.forEach(fabricObject => {
|
|
1408
1651
|
if (!fabricObject) return;
|
|
1409
1652
|
if (typeof fabricObject.setCoords === 'function') fabricObject.setCoords();
|
|
@@ -1411,11 +1654,23 @@ function ensureFabric() {
|
|
|
1411
1654
|
requiredWidth = Math.max(requiredWidth, Math.ceil(boundingRect.left + boundingRect.width + padding));
|
|
1412
1655
|
requiredHeight = Math.max(requiredHeight, Math.ceil(boundingRect.top + boundingRect.height + padding));
|
|
1413
1656
|
});
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
if (
|
|
1657
|
+
const shouldUseScrollSafeViewport = this.options.fitImageToCanvas || this.options.coverImageToCanvas;
|
|
1658
|
+
|
|
1659
|
+
let minWidth = 0;
|
|
1660
|
+
let minHeight = 0;
|
|
1661
|
+
if (shouldUseScrollSafeViewport) {
|
|
1662
|
+
const viewport = this._getContainerViewportSize();
|
|
1663
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
1664
|
+
|
|
1665
|
+
minWidth = Math.max(1, viewport.width - safetyMargin);
|
|
1666
|
+
minHeight = Math.max(1, viewport.height - safetyMargin);
|
|
1667
|
+
} else if (this.containerElement) {
|
|
1668
|
+
minWidth = Math.floor(this.containerElement.clientWidth || 0);
|
|
1669
|
+
minHeight = Math.floor(this.containerElement.clientHeight || 0);
|
|
1670
|
+
}
|
|
1671
|
+
const newWidth = Math.max(currentWidth, minWidth, requiredWidth);
|
|
1672
|
+
const newHeight = Math.max(currentHeight, minHeight, requiredHeight);
|
|
1673
|
+
if (newWidth !== currentWidth || newHeight !== currentHeight) {
|
|
1419
1674
|
this._setCanvasSizeInt(newWidth, newHeight);
|
|
1420
1675
|
}
|
|
1421
1676
|
} catch (error) {
|
|
@@ -1446,6 +1701,69 @@ function ensureFabric() {
|
|
|
1446
1701
|
return this.animationQueue.add(() => this._scaleImageImpl(factor, options));
|
|
1447
1702
|
}
|
|
1448
1703
|
|
|
1704
|
+
_assertIdleForOperation(operationName) {
|
|
1705
|
+
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1706
|
+
if (this.isAnimating || (this.animationQueue && this.animationQueue.isBusy())) {
|
|
1707
|
+
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
_canMutateNow(operationName) {
|
|
1712
|
+
try {
|
|
1713
|
+
this._assertIdleForOperation(operationName);
|
|
1714
|
+
return true;
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
this._reportError(`${operationName} blocked`, error);
|
|
1717
|
+
return false;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
_rejectActiveAnimations(reason) {
|
|
1722
|
+
const error = reason instanceof Error ? reason : new Error(String(reason || 'Animation cancelled'));
|
|
1723
|
+
this._activeAnimationRejectors.forEach(reject => {
|
|
1724
|
+
try { reject(error); } catch (rejectError) { void rejectError; }
|
|
1725
|
+
});
|
|
1726
|
+
this._activeAnimationRejectors.clear();
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
_animateFabricProperty(fabricObject, property, value) {
|
|
1730
|
+
return new Promise((resolve, reject) => {
|
|
1731
|
+
if (this._disposed || !this.canvas || !fabricObject) {
|
|
1732
|
+
reject(new Error('Animation cannot start after editor disposal'));
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
let isSettled = false;
|
|
1737
|
+
const duration = Math.max(0, Number(this.options.animationDuration) || 0);
|
|
1738
|
+
const timeoutMs = Math.max(1000, duration + 1000);
|
|
1739
|
+
let timerId;
|
|
1740
|
+
const settle = (callback) => {
|
|
1741
|
+
if (isSettled) return;
|
|
1742
|
+
isSettled = true;
|
|
1743
|
+
clearTimeout(timerId);
|
|
1744
|
+
this._activeAnimationRejectors.delete(reject);
|
|
1745
|
+
callback();
|
|
1746
|
+
};
|
|
1747
|
+
|
|
1748
|
+
this._activeAnimationRejectors.add(reject);
|
|
1749
|
+
timerId = setTimeout(() => {
|
|
1750
|
+
settle(() => reject(new Error(`Animation timed out while changing ${property}`)));
|
|
1751
|
+
}, timeoutMs);
|
|
1752
|
+
|
|
1753
|
+
try {
|
|
1754
|
+
fabricObject.animate(property, value, {
|
|
1755
|
+
duration,
|
|
1756
|
+
onChange: () => {
|
|
1757
|
+
if (!this._disposed && this.canvas) this.canvas.renderAll();
|
|
1758
|
+
},
|
|
1759
|
+
onComplete: () => settle(resolve)
|
|
1760
|
+
});
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
settle(() => reject(error));
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1449
1767
|
/**
|
|
1450
1768
|
* Scales the original image by a given factor, with animation.
|
|
1451
1769
|
* Returns a promise that resolves when the scale animation is complete.
|
|
@@ -1453,37 +1771,29 @@ function ensureFabric() {
|
|
|
1453
1771
|
* @returns {Promise<void>} Promise that resolves once the scaling animation finishes.
|
|
1454
1772
|
* @private
|
|
1455
1773
|
*/
|
|
1456
|
-
_scaleImageImpl(factor, options = {}) {
|
|
1457
|
-
if (!this.originalImage
|
|
1458
|
-
if (this.isAnimating) return
|
|
1774
|
+
async _scaleImageImpl(factor, options = {}) {
|
|
1775
|
+
if (!this.originalImage || this._disposed) return;
|
|
1776
|
+
if (this.isAnimating) return;
|
|
1459
1777
|
const saveHistory = options.saveHistory !== false;
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1778
|
+
let didStartAnimation = false;
|
|
1779
|
+
try {
|
|
1780
|
+
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, factor));
|
|
1781
|
+
this.currentScale = factor;
|
|
1782
|
+
this.isAnimating = true;
|
|
1783
|
+
didStartAnimation = true;
|
|
1784
|
+
this._updateUI();
|
|
1464
1785
|
|
|
1465
|
-
|
|
1786
|
+
const targetScale = this.baseImageScale * factor;
|
|
1466
1787
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
this._setObjectOriginKeepingPosition(this.originalImage, 'left', 'top', topLeft);
|
|
1788
|
+
const topLeft = this._getObjectTopLeftPoint(this.originalImage);
|
|
1789
|
+
this._setObjectOriginKeepingPosition(this.originalImage, 'left', 'top', topLeft);
|
|
1470
1790
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
});
|
|
1477
|
-
});
|
|
1478
|
-
const scaleYAnimation = new Promise((resolve) => {
|
|
1479
|
-
this.originalImage.animate('scaleY', targetScale, {
|
|
1480
|
-
duration: this.options.animationDuration,
|
|
1481
|
-
onChange: this.canvas.renderAll.bind(this.canvas),
|
|
1482
|
-
onComplete: resolve
|
|
1483
|
-
});
|
|
1484
|
-
});
|
|
1791
|
+
await Promise.all([
|
|
1792
|
+
this._animateFabricProperty(this.originalImage, 'scaleX', targetScale),
|
|
1793
|
+
this._animateFabricProperty(this.originalImage, 'scaleY', targetScale)
|
|
1794
|
+
]);
|
|
1795
|
+
if (this._disposed || !this.canvas || !this.originalImage) throw new Error('Editor was disposed during scale animation');
|
|
1485
1796
|
|
|
1486
|
-
return Promise.all([scaleXAnimation, scaleYAnimation]).then(() => {
|
|
1487
1797
|
this.originalImage.set({ scaleX: targetScale, scaleY: targetScale });
|
|
1488
1798
|
this.originalImage.setCoords();
|
|
1489
1799
|
|
|
@@ -1493,17 +1803,17 @@ function ensureFabric() {
|
|
|
1493
1803
|
|
|
1494
1804
|
this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
|
|
1495
1805
|
|
|
1496
|
-
// Sync mask labels
|
|
1497
1806
|
this.canvas.getObjects().forEach(object => { if (object.maskId) this._syncMaskLabel(object); });
|
|
1498
1807
|
|
|
1499
|
-
this.isAnimating = false;
|
|
1500
1808
|
this._updateInputs();
|
|
1501
|
-
this._updateUI();
|
|
1502
1809
|
if (saveHistory) this.saveState();
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1810
|
+
} finally {
|
|
1811
|
+
if (didStartAnimation) {
|
|
1812
|
+
this.isAnimating = false;
|
|
1813
|
+
this._updateInputs();
|
|
1814
|
+
this._updateUI();
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1507
1817
|
}
|
|
1508
1818
|
|
|
1509
1819
|
/**
|
|
@@ -1524,27 +1834,29 @@ function ensureFabric() {
|
|
|
1524
1834
|
* @returns {Promise<void>} Promise that resolves once the rotation animation finishes.
|
|
1525
1835
|
* @private
|
|
1526
1836
|
*/
|
|
1527
|
-
_rotateImageImpl(degrees, options = {}) {
|
|
1528
|
-
if (!this.originalImage
|
|
1529
|
-
if (this.isAnimating) return
|
|
1530
|
-
if (isNaN(degrees)) return
|
|
1837
|
+
async _rotateImageImpl(degrees, options = {}) {
|
|
1838
|
+
if (!this.originalImage || this._disposed) return;
|
|
1839
|
+
if (this.isAnimating) return;
|
|
1840
|
+
if (isNaN(degrees)) return;
|
|
1531
1841
|
const saveHistory = options.saveHistory !== false;
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1842
|
+
const image = this.originalImage;
|
|
1843
|
+
const previousOriginX = image.originX || 'left';
|
|
1844
|
+
const previousOriginY = image.originY || 'top';
|
|
1845
|
+
const previousOriginPoint = this._getObjectOriginPoint(image, previousOriginX, previousOriginY);
|
|
1846
|
+
let didStartAnimation = false;
|
|
1847
|
+
let didCompleteRotation = false;
|
|
1848
|
+
try {
|
|
1849
|
+
this.currentRotation = degrees;
|
|
1850
|
+
this.isAnimating = true;
|
|
1851
|
+
didStartAnimation = true;
|
|
1852
|
+
this._updateUI();
|
|
1535
1853
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1854
|
+
const center = image.getCenterPoint();
|
|
1855
|
+
this._setObjectOriginKeepingPosition(image, 'center', 'center', center);
|
|
1538
1856
|
|
|
1539
|
-
|
|
1540
|
-
this.originalImage
|
|
1541
|
-
duration: this.options.animationDuration,
|
|
1542
|
-
onChange: this.canvas.renderAll.bind(this.canvas),
|
|
1543
|
-
onComplete: resolve
|
|
1544
|
-
});
|
|
1545
|
-
});
|
|
1857
|
+
await this._animateFabricProperty(image, 'angle', degrees);
|
|
1858
|
+
if (this._disposed || !this.canvas || !this.originalImage) throw new Error('Editor was disposed during rotation animation');
|
|
1546
1859
|
|
|
1547
|
-
return rotationAnimation.then(() => {
|
|
1548
1860
|
this.originalImage.set('angle', degrees);
|
|
1549
1861
|
this.originalImage.setCoords();
|
|
1550
1862
|
|
|
@@ -1554,20 +1866,24 @@ function ensureFabric() {
|
|
|
1554
1866
|
|
|
1555
1867
|
this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
|
|
1556
1868
|
|
|
1557
|
-
const newTopLeft = this.
|
|
1869
|
+
const newTopLeft = this._getObjectCoordinateTopLeftPoint(this.originalImage);
|
|
1558
1870
|
this._setObjectOriginKeepingPosition(this.originalImage, 'left', 'top', newTopLeft);
|
|
1559
1871
|
|
|
1560
|
-
// Sync mask labels
|
|
1561
1872
|
this.canvas.getObjects().forEach(object => { if (object.maskId) this._syncMaskLabel(object); });
|
|
1562
1873
|
|
|
1563
|
-
this.isAnimating = false;
|
|
1564
1874
|
this._updateInputs();
|
|
1565
|
-
this._updateUI();
|
|
1566
1875
|
if (saveHistory) this.saveState();
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
this.
|
|
1570
|
-
|
|
1876
|
+
didCompleteRotation = true;
|
|
1877
|
+
} finally {
|
|
1878
|
+
if (!didCompleteRotation && !this._disposed && image) {
|
|
1879
|
+
this._setObjectOriginKeepingPosition(image, previousOriginX, previousOriginY, previousOriginPoint);
|
|
1880
|
+
}
|
|
1881
|
+
if (didStartAnimation) {
|
|
1882
|
+
this.isAnimating = false;
|
|
1883
|
+
this._updateInputs();
|
|
1884
|
+
this._updateUI();
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1571
1887
|
}
|
|
1572
1888
|
|
|
1573
1889
|
/**
|
|
@@ -1580,13 +1896,14 @@ function ensureFabric() {
|
|
|
1580
1896
|
if (!this.originalImage) return Promise.resolve();
|
|
1581
1897
|
|
|
1582
1898
|
return this.animationQueue.add(async () => {
|
|
1583
|
-
const before = this._lastSnapshot || this.
|
|
1899
|
+
const before = this._lastSnapshot || this._captureCanvasStateOrThrow('resetImageTransform');
|
|
1584
1900
|
await this._scaleImageImpl(1, { saveHistory: false });
|
|
1585
1901
|
await this._rotateImageImpl(0, { saveHistory: false });
|
|
1586
|
-
const after = this.
|
|
1902
|
+
const after = this._captureCanvasStateOrThrow('resetImageTransform');
|
|
1587
1903
|
this._pushStateTransition(before, after);
|
|
1588
1904
|
}).catch(error => {
|
|
1589
1905
|
this._reportError('resetImageTransform() failed', error);
|
|
1906
|
+
throw error;
|
|
1590
1907
|
});
|
|
1591
1908
|
}
|
|
1592
1909
|
|
|
@@ -1608,17 +1925,35 @@ function ensureFabric() {
|
|
|
1608
1925
|
* @public
|
|
1609
1926
|
*/
|
|
1610
1927
|
loadFromState(serializedState) {
|
|
1611
|
-
if (!serializedState || !this.canvas) return Promise.resolve();
|
|
1928
|
+
if (!serializedState || !this.canvas || this._disposed) return Promise.resolve();
|
|
1929
|
+
if (this._cropMode || this._cropRect) {
|
|
1930
|
+
this._removeCropRect();
|
|
1931
|
+
this._restoreCropObjectState();
|
|
1932
|
+
this._cropMode = false;
|
|
1933
|
+
if (this._prevSelectionSetting !== undefined && this.canvas) {
|
|
1934
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
1935
|
+
}
|
|
1936
|
+
this._prevSelectionSetting = undefined;
|
|
1937
|
+
}
|
|
1612
1938
|
|
|
1613
|
-
return new Promise((resolve) => {
|
|
1939
|
+
return new Promise((resolve, reject) => {
|
|
1614
1940
|
try {
|
|
1615
1941
|
const state = (typeof serializedState === 'string')
|
|
1616
1942
|
? JSON.parse(serializedState)
|
|
1617
1943
|
: serializedState;
|
|
1618
1944
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
1619
1945
|
|
|
1620
|
-
this.canvas.loadFromJSON(state, () => {
|
|
1946
|
+
this.canvas.loadFromJSON(state, async () => {
|
|
1621
1947
|
try {
|
|
1948
|
+
if (this._disposed || !this.canvas) {
|
|
1949
|
+
reject(new Error('Editor was disposed while loading state'));
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
await this._waitForFabricImagesReady(this.canvas.getObjects());
|
|
1953
|
+
if (this._disposed || !this.canvas) {
|
|
1954
|
+
reject(new Error('Editor was disposed while loading state'));
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1622
1957
|
this._hideAllMaskLabels();
|
|
1623
1958
|
const canvasObjects = this.canvas.getObjects();
|
|
1624
1959
|
this.originalImage = canvasObjects.find(object => object.type === 'image' && !object.maskId) || null;
|
|
@@ -1677,20 +2012,48 @@ function ensureFabric() {
|
|
|
1677
2012
|
this._updatePlaceholderStatus();
|
|
1678
2013
|
this._lastSnapshot = this._serializeCanvasState();
|
|
1679
2014
|
this._updateUI();
|
|
2015
|
+
resolve();
|
|
1680
2016
|
} catch (callbackError) {
|
|
1681
2017
|
this._reportError('loadFromState() failed', callbackError);
|
|
1682
|
-
|
|
1683
|
-
resolve();
|
|
2018
|
+
reject(callbackError);
|
|
1684
2019
|
}
|
|
1685
2020
|
});
|
|
1686
2021
|
|
|
1687
2022
|
} catch (error) {
|
|
1688
2023
|
this._reportError('loadFromState() failed', error);
|
|
1689
|
-
|
|
2024
|
+
reject(error);
|
|
1690
2025
|
}
|
|
1691
2026
|
});
|
|
1692
2027
|
}
|
|
1693
2028
|
|
|
2029
|
+
async _waitForFabricImagesReady(canvasObjects) {
|
|
2030
|
+
const imageObjects = (canvasObjects || []).filter(object => object && object.type === 'image');
|
|
2031
|
+
await Promise.all(imageObjects.map(object => this._waitForImageElementReady(
|
|
2032
|
+
typeof object.getElement === 'function' ? object.getElement() : object._element
|
|
2033
|
+
)));
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
_waitForImageElementReady(imageElement) {
|
|
2037
|
+
if (!imageElement) return Promise.resolve();
|
|
2038
|
+
if (imageElement.complete || imageElement.naturalWidth > 0 || imageElement.width > 0) return Promise.resolve();
|
|
2039
|
+
return new Promise((resolve, reject) => {
|
|
2040
|
+
let isSettled = false;
|
|
2041
|
+
const timerId = setTimeout(() => {
|
|
2042
|
+
settle(() => reject(new Error('Image load timed out while restoring state')));
|
|
2043
|
+
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
2044
|
+
const settle = (callback) => {
|
|
2045
|
+
if (isSettled) return;
|
|
2046
|
+
isSettled = true;
|
|
2047
|
+
clearTimeout(timerId);
|
|
2048
|
+
imageElement.onload = null;
|
|
2049
|
+
imageElement.onerror = null;
|
|
2050
|
+
callback();
|
|
2051
|
+
};
|
|
2052
|
+
imageElement.onload = () => settle(resolve);
|
|
2053
|
+
imageElement.onerror = (error) => settle(() => reject(error));
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
|
|
1694
2057
|
/**
|
|
1695
2058
|
* Saves the current editable canvas state as an undoable history transition.
|
|
1696
2059
|
*
|
|
@@ -1702,10 +2065,9 @@ function ensureFabric() {
|
|
|
1702
2065
|
*/
|
|
1703
2066
|
saveState() {
|
|
1704
2067
|
if (!this.canvas) return;
|
|
1705
|
-
const activeObject = this.canvas.getActiveObject();
|
|
1706
2068
|
|
|
1707
2069
|
try {
|
|
1708
|
-
const after = this.
|
|
2070
|
+
const after = this._captureCanvasStateOrThrow('saveState');
|
|
1709
2071
|
const before = this._lastSnapshot || after;
|
|
1710
2072
|
if (after === before) return;
|
|
1711
2073
|
let executedOnce = false;
|
|
@@ -1726,9 +2088,6 @@ function ensureFabric() {
|
|
|
1726
2088
|
} catch (error) {
|
|
1727
2089
|
this._reportWarning('saveState: failed to save canvas snapshot', error);
|
|
1728
2090
|
} finally {
|
|
1729
|
-
if (activeObject && activeObject.maskId && !activeObject.__label && this.canvas.getObjects().includes(activeObject)) {
|
|
1730
|
-
this._handleSelectionChanged([activeObject]);
|
|
1731
|
-
}
|
|
1732
2091
|
this._updateUI();
|
|
1733
2092
|
}
|
|
1734
2093
|
}
|
|
@@ -1745,7 +2104,10 @@ function ensureFabric() {
|
|
|
1745
2104
|
* @private
|
|
1746
2105
|
*/
|
|
1747
2106
|
_pushStateTransition(before, after) {
|
|
1748
|
-
if (!before || !after)
|
|
2107
|
+
if (!before || !after) {
|
|
2108
|
+
this._reportWarning('History transition skipped because a canvas snapshot is unavailable');
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
1749
2111
|
if (before === after) return;
|
|
1750
2112
|
if (!this.historyManager) this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
1751
2113
|
|
|
@@ -1767,7 +2129,10 @@ function ensureFabric() {
|
|
|
1767
2129
|
undo() {
|
|
1768
2130
|
return this.historyManager.undo()
|
|
1769
2131
|
.then(() => { this._updateUI(); })
|
|
1770
|
-
.catch(error => {
|
|
2132
|
+
.catch(error => {
|
|
2133
|
+
this._reportError('undo failed', error);
|
|
2134
|
+
throw error;
|
|
2135
|
+
});
|
|
1771
2136
|
}
|
|
1772
2137
|
|
|
1773
2138
|
/**
|
|
@@ -1779,7 +2144,10 @@ function ensureFabric() {
|
|
|
1779
2144
|
redo() {
|
|
1780
2145
|
return this.historyManager.redo()
|
|
1781
2146
|
.then(() => { this._updateUI(); })
|
|
1782
|
-
.catch(error => {
|
|
2147
|
+
.catch(error => {
|
|
2148
|
+
this._reportError('redo failed', error);
|
|
2149
|
+
throw error;
|
|
2150
|
+
});
|
|
1783
2151
|
}
|
|
1784
2152
|
|
|
1785
2153
|
_rebindMaskEvents(mask) {
|
|
@@ -1801,23 +2169,17 @@ function ensureFabric() {
|
|
|
1801
2169
|
}
|
|
1802
2170
|
if (Object.keys(metadata).length) mask.set(metadata);
|
|
1803
2171
|
|
|
1804
|
-
const normalStyle = {
|
|
1805
|
-
stroke: mask.originalStroke || '#ccc',
|
|
1806
|
-
strokeWidth: mask.originalStrokeWidth,
|
|
1807
|
-
opacity: mask.originalAlpha
|
|
1808
|
-
};
|
|
1809
|
-
const hoverStyle = {
|
|
1810
|
-
stroke: '#ff5500',
|
|
1811
|
-
strokeWidth: 2,
|
|
1812
|
-
opacity: Math.min(mask.originalAlpha + 0.2, 1)
|
|
1813
|
-
};
|
|
1814
|
-
|
|
1815
2172
|
const mouseover = () => {
|
|
1816
|
-
mask.
|
|
2173
|
+
const opacity = Number(mask.originalAlpha);
|
|
2174
|
+
mask.set({
|
|
2175
|
+
stroke: '#ff5500',
|
|
2176
|
+
strokeWidth: 2,
|
|
2177
|
+
opacity: Math.min((Number.isFinite(opacity) ? opacity : 0.5) + 0.2, 1)
|
|
2178
|
+
});
|
|
1817
2179
|
if (mask.canvas) mask.canvas.requestRenderAll();
|
|
1818
2180
|
};
|
|
1819
2181
|
const mouseout = () => {
|
|
1820
|
-
mask.set(
|
|
2182
|
+
mask.set(this._getMaskNormalStyle(mask));
|
|
1821
2183
|
if (mask.canvas) mask.canvas.requestRenderAll();
|
|
1822
2184
|
};
|
|
1823
2185
|
|
|
@@ -1856,6 +2218,7 @@ function ensureFabric() {
|
|
|
1856
2218
|
*/
|
|
1857
2219
|
createMask(config = {}) {
|
|
1858
2220
|
if (!this.canvas) return null;
|
|
2221
|
+
if (!this._canMutateNow('createMask')) return null;
|
|
1859
2222
|
const shapeType = config.shape || 'rect';
|
|
1860
2223
|
// Normalize mask defaults before applying caller-provided overrides.
|
|
1861
2224
|
const maskConfig = {
|
|
@@ -1898,15 +2261,12 @@ function ensureFabric() {
|
|
|
1898
2261
|
|
|
1899
2262
|
if (maskConfig.left === undefined && this._lastMask) {
|
|
1900
2263
|
const previousMask = this._lastMask;
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
}
|
|
1908
|
-
left = Math.round(previousMaskRight + maskConfig.gap);
|
|
1909
|
-
top = previousMask.top ?? firstOffset;
|
|
2264
|
+
if (typeof previousMask.setCoords === 'function') previousMask.setCoords();
|
|
2265
|
+
const previousBounds = typeof previousMask.getBoundingRect === 'function'
|
|
2266
|
+
? previousMask.getBoundingRect(true, true)
|
|
2267
|
+
: { left: previousMask.left || firstOffset, top: previousMask.top || firstOffset, width: previousMask.width || 0 };
|
|
2268
|
+
left = Math.round(previousBounds.left + previousBounds.width + maskConfig.gap);
|
|
2269
|
+
top = Math.round(previousBounds.top ?? firstOffset);
|
|
1910
2270
|
} else {
|
|
1911
2271
|
left = resolveValue(maskConfig.left, firstOffset, 'width');
|
|
1912
2272
|
top = resolveValue(maskConfig.top, firstOffset, 'height');
|
|
@@ -2044,6 +2404,8 @@ function ensureFabric() {
|
|
|
2044
2404
|
* The associated label is also removed. UI and mask list are updated.
|
|
2045
2405
|
*/
|
|
2046
2406
|
removeSelectedMask() {
|
|
2407
|
+
if (!this.canvas) return;
|
|
2408
|
+
if (!this._canMutateNow('removeSelectedMask')) return;
|
|
2047
2409
|
const activeObject = this.canvas.getActiveObject();
|
|
2048
2410
|
const selectedMasks = this._getModifiedMasks(activeObject);
|
|
2049
2411
|
if (!selectedMasks.length) return;
|
|
@@ -2072,6 +2434,8 @@ function ensureFabric() {
|
|
|
2072
2434
|
* UI and internal mask placement memory are reset.
|
|
2073
2435
|
*/
|
|
2074
2436
|
removeAllMasks(options = {}) {
|
|
2437
|
+
if (!this.canvas) return;
|
|
2438
|
+
if (!this._canMutateNow('removeAllMasks')) return;
|
|
2075
2439
|
const saveHistory = options.saveHistory !== false;
|
|
2076
2440
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
2077
2441
|
masks.forEach(mask => this._removeLabelForMask(mask));
|
|
@@ -2137,6 +2501,10 @@ function ensureFabric() {
|
|
|
2137
2501
|
let textObject = null;
|
|
2138
2502
|
if (this.options.label && typeof this.options.label.create === 'function') {
|
|
2139
2503
|
textObject = this.options.label.create(mask, fabric);
|
|
2504
|
+
if (!textObject || typeof textObject.set !== 'function') {
|
|
2505
|
+
this._reportWarning('label.create() returned an invalid Fabric object; using the default label');
|
|
2506
|
+
textObject = null;
|
|
2507
|
+
}
|
|
2140
2508
|
}
|
|
2141
2509
|
if (!textObject) {
|
|
2142
2510
|
let labelText = mask.maskName;
|
|
@@ -2203,10 +2571,11 @@ function ensureFabric() {
|
|
|
2203
2571
|
if (!this.options.maskLabelOnSelect) return;
|
|
2204
2572
|
if (!mask.__label) return;
|
|
2205
2573
|
|
|
2206
|
-
|
|
2207
|
-
|
|
2574
|
+
if (typeof mask.setCoords === 'function') mask.setCoords();
|
|
2575
|
+
const bounds = mask.getBoundingRect ? mask.getBoundingRect(true, true) : null;
|
|
2576
|
+
if (!bounds) return;
|
|
2208
2577
|
|
|
2209
|
-
const tl =
|
|
2578
|
+
const tl = { x: bounds.left, y: bounds.top };
|
|
2210
2579
|
const center = mask.getCenterPoint();
|
|
2211
2580
|
|
|
2212
2581
|
const vx = center.x - tl.x;
|
|
@@ -2289,7 +2658,7 @@ function ensureFabric() {
|
|
|
2289
2658
|
* @private
|
|
2290
2659
|
*/
|
|
2291
2660
|
_updateMaskList() {
|
|
2292
|
-
const maskListElement =
|
|
2661
|
+
const maskListElement = this._getElement('maskList');
|
|
2293
2662
|
if (!maskListElement) return;
|
|
2294
2663
|
maskListElement.innerHTML = '';
|
|
2295
2664
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
@@ -2297,11 +2666,22 @@ function ensureFabric() {
|
|
|
2297
2666
|
const listItemElement = document.createElement('li');
|
|
2298
2667
|
listItemElement.className = 'list-group-item mask-item';
|
|
2299
2668
|
listItemElement.textContent = mask.maskName;
|
|
2300
|
-
listItemElement.
|
|
2669
|
+
listItemElement.dataset.maskId = String(mask.maskId);
|
|
2301
2670
|
maskListElement.appendChild(listItemElement);
|
|
2302
2671
|
});
|
|
2303
2672
|
}
|
|
2304
2673
|
|
|
2674
|
+
_handleMaskListClick(event) {
|
|
2675
|
+
if (!this.canvas) return;
|
|
2676
|
+
const itemElement = event.target && event.target.closest ? event.target.closest('.mask-item') : null;
|
|
2677
|
+
if (!itemElement || !itemElement.dataset) return;
|
|
2678
|
+
const maskId = Number(itemElement.dataset.maskId);
|
|
2679
|
+
const mask = this.canvas.getObjects().find(object => Number(object.maskId) === maskId);
|
|
2680
|
+
if (!mask) return;
|
|
2681
|
+
this.canvas.setActiveObject(mask);
|
|
2682
|
+
this._handleSelectionChanged([mask]);
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2305
2685
|
/**
|
|
2306
2686
|
* Updates the visual selection (CSS 'active') state for the mask list in the DOM.
|
|
2307
2687
|
*
|
|
@@ -2309,12 +2689,13 @@ function ensureFabric() {
|
|
|
2309
2689
|
* @private
|
|
2310
2690
|
*/
|
|
2311
2691
|
_updateMaskListSelection(selectedMask) {
|
|
2312
|
-
const maskListElement =
|
|
2692
|
+
const maskListElement = this._getElement('maskList');
|
|
2313
2693
|
if (!maskListElement) return;
|
|
2314
2694
|
const maskItems = maskListElement.querySelectorAll('.mask-item');
|
|
2315
2695
|
maskItems.forEach(item => {
|
|
2316
|
-
const isSelected = !!selectedMask && item.
|
|
2696
|
+
const isSelected = !!selectedMask && Number(item.dataset.maskId) === Number(selectedMask.maskId);
|
|
2317
2697
|
item.classList.toggle('active', isSelected);
|
|
2698
|
+
item.classList.toggle('selected', isSelected);
|
|
2318
2699
|
});
|
|
2319
2700
|
}
|
|
2320
2701
|
|
|
@@ -2330,6 +2711,7 @@ function ensureFabric() {
|
|
|
2330
2711
|
*/
|
|
2331
2712
|
async mergeMasks() {
|
|
2332
2713
|
if (!this.originalImage) return;
|
|
2714
|
+
this._assertIdleForOperation('mergeMasks');
|
|
2333
2715
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
2334
2716
|
if (!masks.length) return;
|
|
2335
2717
|
|
|
@@ -2340,11 +2722,12 @@ function ensureFabric() {
|
|
|
2340
2722
|
const beforeJson = this._serializeCanvasState();
|
|
2341
2723
|
const merged = await this.exportImageBase64({ exportImageArea: true, multiplier: this.options.exportMultiplier });
|
|
2342
2724
|
this.removeAllMasks({ saveHistory: false });
|
|
2343
|
-
await this.loadImage(merged, { preserveScroll: true });
|
|
2725
|
+
await this.loadImage(merged, { preserveScroll: true, resetMaskCounter: false });
|
|
2344
2726
|
const afterJson = this._serializeCanvasState();
|
|
2345
2727
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2346
2728
|
} catch (error) {
|
|
2347
2729
|
this._reportError('merge error', error);
|
|
2730
|
+
throw error;
|
|
2348
2731
|
}
|
|
2349
2732
|
}
|
|
2350
2733
|
|
|
@@ -2368,6 +2751,7 @@ function ensureFabric() {
|
|
|
2368
2751
|
*/
|
|
2369
2752
|
downloadImage(fileName = this.options.defaultDownloadFileName) {
|
|
2370
2753
|
if (!this.originalImage) return;
|
|
2754
|
+
if (!this._canMutateNow('downloadImage')) return;
|
|
2371
2755
|
const exportImageArea = this.options.exportImageAreaByDefault;
|
|
2372
2756
|
this.exportImageBase64({ exportImageArea, multiplier: this.options.exportMultiplier })
|
|
2373
2757
|
.then(imageBase64 => {
|
|
@@ -2399,6 +2783,7 @@ function ensureFabric() {
|
|
|
2399
2783
|
*/
|
|
2400
2784
|
async exportImageBase64(options = {}) {
|
|
2401
2785
|
if (!this.originalImage) throw new Error('No image loaded');
|
|
2786
|
+
this._assertIdleForOperation('exportImageBase64');
|
|
2402
2787
|
const exportImageArea = typeof options.exportImageArea === 'boolean' ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2403
2788
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2404
2789
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
@@ -2416,7 +2801,7 @@ function ensureFabric() {
|
|
|
2416
2801
|
this.originalImage.setCoords();
|
|
2417
2802
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2418
2803
|
const exportRegion = this._getClampedCanvasRegion(imageBounds, { includePartialPixels: false });
|
|
2419
|
-
return
|
|
2804
|
+
return this._exportCanvasRegionToDataURL({
|
|
2420
2805
|
...exportRegion,
|
|
2421
2806
|
multiplier,
|
|
2422
2807
|
quality,
|
|
@@ -2462,7 +2847,7 @@ function ensureFabric() {
|
|
|
2462
2847
|
const exportRegion = this._getClampedCanvasRegion(imageBounds, { includePartialPixels: false });
|
|
2463
2848
|
|
|
2464
2849
|
// Crop precisely in offscreen canvas
|
|
2465
|
-
finalBase64 =
|
|
2850
|
+
finalBase64 = this._exportCanvasRegionToDataURL({
|
|
2466
2851
|
...exportRegion,
|
|
2467
2852
|
multiplier,
|
|
2468
2853
|
quality,
|
|
@@ -2520,6 +2905,7 @@ function ensureFabric() {
|
|
|
2520
2905
|
*/
|
|
2521
2906
|
async exportImageFile(options = {}) {
|
|
2522
2907
|
if (!this.originalImage) throw new Error('No image loaded');
|
|
2908
|
+
this._assertIdleForOperation('exportImageFile');
|
|
2523
2909
|
const {
|
|
2524
2910
|
mergeMask = true,
|
|
2525
2911
|
fileType = 'jpeg',
|
|
@@ -2561,6 +2947,7 @@ function ensureFabric() {
|
|
|
2561
2947
|
offscreenCanvas.width = imageElement.width;
|
|
2562
2948
|
offscreenCanvas.height = imageElement.height;
|
|
2563
2949
|
const context = offscreenCanvas.getContext('2d');
|
|
2950
|
+
if (!context) throw new Error('Unable to create 2D canvas context for export conversion');
|
|
2564
2951
|
context.drawImage(imageElement, 0, 0);
|
|
2565
2952
|
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, quality);
|
|
2566
2953
|
resolve(convertedDataUrl);
|
|
@@ -2633,13 +3020,15 @@ function ensureFabric() {
|
|
|
2633
3020
|
if (this._cropHandlers && this._cropHandlers.length) {
|
|
2634
3021
|
this._cropHandlers.forEach(targetHandlers => {
|
|
2635
3022
|
targetHandlers.handlers.forEach(handlerRecord => {
|
|
2636
|
-
targetHandlers.target.off
|
|
3023
|
+
if (targetHandlers.target && typeof targetHandlers.target.off === 'function') {
|
|
3024
|
+
targetHandlers.target.off(handlerRecord.eventName, handlerRecord.handler);
|
|
3025
|
+
}
|
|
2637
3026
|
});
|
|
2638
3027
|
});
|
|
2639
3028
|
}
|
|
2640
3029
|
} catch (error) { void error; }
|
|
2641
3030
|
|
|
2642
|
-
try { this.canvas.remove(this._cropRect); } catch (error) { void error; }
|
|
3031
|
+
try { if (this.canvas) this.canvas.remove(this._cropRect); } catch (error) { void error; }
|
|
2643
3032
|
this._cropRect = null;
|
|
2644
3033
|
this._cropHandlers = [];
|
|
2645
3034
|
}
|
|
@@ -2655,7 +3044,9 @@ function ensureFabric() {
|
|
|
2655
3044
|
*/
|
|
2656
3045
|
enterCropMode() {
|
|
2657
3046
|
if (!this.canvas || !this.originalImage || this._cropMode) return;
|
|
3047
|
+
if (!this._canMutateNow('enterCropMode')) return;
|
|
2658
3048
|
if (!this.isImageLoaded()) return;
|
|
3049
|
+
this._removeCropRect();
|
|
2659
3050
|
this._cropMode = true;
|
|
2660
3051
|
|
|
2661
3052
|
// Disable group selection so only the crop rectangle can be manipulated.
|
|
@@ -2789,6 +3180,7 @@ function ensureFabric() {
|
|
|
2789
3180
|
*/
|
|
2790
3181
|
async applyCrop() {
|
|
2791
3182
|
if (!this.canvas || !this._cropMode || !this._cropRect) return;
|
|
3183
|
+
this._assertIdleForOperation('applyCrop');
|
|
2792
3184
|
|
|
2793
3185
|
// Fabric does not update control coordinates automatically after programmatic transforms.
|
|
2794
3186
|
this._cropRect.setCoords();
|
|
@@ -2824,12 +3216,8 @@ function ensureFabric() {
|
|
|
2824
3216
|
this._removeLabelForMask(mask);
|
|
2825
3217
|
this.canvas.remove(mask);
|
|
2826
3218
|
if (shouldPreserveMasks && intersectsCrop) {
|
|
2827
|
-
mask.
|
|
2828
|
-
|
|
2829
|
-
top: (mask.top || 0) - cropRegion.sourceY,
|
|
2830
|
-
visible: true
|
|
2831
|
-
});
|
|
2832
|
-
mask.setCoords();
|
|
3219
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3220
|
+
mask.set({ visible: true });
|
|
2833
3221
|
preservedMasks.push(mask);
|
|
2834
3222
|
}
|
|
2835
3223
|
} catch (error) {
|
|
@@ -2867,7 +3255,7 @@ function ensureFabric() {
|
|
|
2867
3255
|
|
|
2868
3256
|
// Load the cropped image as the new base image.
|
|
2869
3257
|
try {
|
|
2870
|
-
await this.loadImage(croppedBase64);
|
|
3258
|
+
await this.loadImage(croppedBase64, { resetMaskCounter: false });
|
|
2871
3259
|
if (preservedMasks.length) {
|
|
2872
3260
|
preservedMasks.forEach(mask => {
|
|
2873
3261
|
this._rebindMaskEvents(mask);
|
|
@@ -2887,7 +3275,7 @@ function ensureFabric() {
|
|
|
2887
3275
|
// Create an after snapshot and push one history command for the crop operation.
|
|
2888
3276
|
let afterJson;
|
|
2889
3277
|
try {
|
|
2890
|
-
afterJson = this._serializeCanvasState();
|
|
3278
|
+
afterJson = preservedMasks.length ? this._serializeCanvasState() : this._lastSnapshot;
|
|
2891
3279
|
} catch (error) {
|
|
2892
3280
|
this._reportWarning('applyCrop: failed to serialize after state', error);
|
|
2893
3281
|
afterJson = null;
|
|
@@ -2913,7 +3301,7 @@ function ensureFabric() {
|
|
|
2913
3301
|
* @private
|
|
2914
3302
|
*/
|
|
2915
3303
|
_updateInputs() {
|
|
2916
|
-
const scaleInputElement =
|
|
3304
|
+
const scaleInputElement = this._getElement('scaleRate');
|
|
2917
3305
|
if (scaleInputElement) scaleInputElement.value = Math.round(this.currentScale * 100);
|
|
2918
3306
|
}
|
|
2919
3307
|
|
|
@@ -2923,6 +3311,7 @@ function ensureFabric() {
|
|
|
2923
3311
|
* @private
|
|
2924
3312
|
*/
|
|
2925
3313
|
_updateUI() {
|
|
3314
|
+
if (!this.canvas) return;
|
|
2926
3315
|
const hasImage = !!this.originalImage;
|
|
2927
3316
|
const masks = hasImage ? this.canvas.getObjects().filter(object => object.maskId) : [];
|
|
2928
3317
|
const hasMasks = masks.length > 0;
|
|
@@ -2936,7 +3325,7 @@ function ensureFabric() {
|
|
|
2936
3325
|
if (isInCropMode) {
|
|
2937
3326
|
// Disable all controls except the crop action buttons while crop mode is active.
|
|
2938
3327
|
for (const key of Object.keys(this.elements || {})) {
|
|
2939
|
-
const element =
|
|
3328
|
+
const element = this._getElement(key);
|
|
2940
3329
|
if (!element) continue;
|
|
2941
3330
|
if (key === 'applyCropBtn' || key === 'cancelCropBtn') {
|
|
2942
3331
|
this._setDisabled(key, false);
|
|
@@ -2974,7 +3363,7 @@ function ensureFabric() {
|
|
|
2974
3363
|
* @private
|
|
2975
3364
|
*/
|
|
2976
3365
|
_setDisabled(key, disabled) {
|
|
2977
|
-
const element =
|
|
3366
|
+
const element = this._getElement(key);
|
|
2978
3367
|
if (!element) return;
|
|
2979
3368
|
if ('disabled' in element) {
|
|
2980
3369
|
element.disabled = !!disabled;
|
|
@@ -3012,9 +3401,23 @@ function ensureFabric() {
|
|
|
3012
3401
|
* @private
|
|
3013
3402
|
*/
|
|
3014
3403
|
_setPlaceholderVisible(show) {
|
|
3015
|
-
if (
|
|
3016
|
-
this.
|
|
3017
|
-
|
|
3404
|
+
if (this.placeholderElement) this._setElementVisible(this.placeholderElement, show);
|
|
3405
|
+
const canvasVisibilityElement = this._getCanvasVisibilityElement();
|
|
3406
|
+
if (canvasVisibilityElement && canvasVisibilityElement !== this.placeholderElement) {
|
|
3407
|
+
this._setElementVisible(canvasVisibilityElement, !show);
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
_getCanvasVisibilityElement() {
|
|
3412
|
+
const wrapperElement = this.canvas && this.canvas.wrapperEl ? this.canvas.wrapperEl : null;
|
|
3413
|
+
if (
|
|
3414
|
+
this.containerElement &&
|
|
3415
|
+
this.placeholderElement &&
|
|
3416
|
+
(this.containerElement === this.placeholderElement || this.containerElement.contains(this.placeholderElement))
|
|
3417
|
+
) {
|
|
3418
|
+
return wrapperElement || this.canvasElement;
|
|
3419
|
+
}
|
|
3420
|
+
return this.containerElement || wrapperElement || this.canvasElement;
|
|
3018
3421
|
}
|
|
3019
3422
|
|
|
3020
3423
|
/**
|
|
@@ -3027,9 +3430,37 @@ function ensureFabric() {
|
|
|
3027
3430
|
*/
|
|
3028
3431
|
_setElementVisible(element, isVisible) {
|
|
3029
3432
|
if (!element) return;
|
|
3433
|
+
this._rememberElementVisibility(element);
|
|
3030
3434
|
element.hidden = !isVisible;
|
|
3031
3435
|
element.setAttribute('aria-hidden', isVisible ? 'false' : 'true');
|
|
3032
|
-
if (
|
|
3436
|
+
if (element.classList) {
|
|
3437
|
+
element.classList.toggle('d-none', !isVisible);
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
_rememberElementVisibility(element) {
|
|
3442
|
+
if (!element || this._visibilityStateByElement.has(element)) return;
|
|
3443
|
+
this._visibilityStateByElement.set(element, this._captureElementVisibility(element));
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
_captureElementVisibility(element) {
|
|
3447
|
+
if (!element) return null;
|
|
3448
|
+
return {
|
|
3449
|
+
hidden: element.hidden,
|
|
3450
|
+
ariaHidden: element.getAttribute('aria-hidden'),
|
|
3451
|
+
className: element.className
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
_restoreElementVisibility(element, state) {
|
|
3456
|
+
if (!element || !state) return;
|
|
3457
|
+
element.hidden = !!state.hidden;
|
|
3458
|
+
if (state.ariaHidden === null) {
|
|
3459
|
+
element.removeAttribute('aria-hidden');
|
|
3460
|
+
} else {
|
|
3461
|
+
element.setAttribute('aria-hidden', state.ariaHidden);
|
|
3462
|
+
}
|
|
3463
|
+
element.className = state.className || '';
|
|
3033
3464
|
}
|
|
3034
3465
|
|
|
3035
3466
|
/**
|
|
@@ -3038,11 +3469,16 @@ function ensureFabric() {
|
|
|
3038
3469
|
* @public
|
|
3039
3470
|
*/
|
|
3040
3471
|
dispose() {
|
|
3472
|
+
this._disposed = true;
|
|
3473
|
+
this._rejectActiveAnimations(new Error('Editor disposed during animation'));
|
|
3474
|
+
if (this.animationQueue) {
|
|
3475
|
+
this.animationQueue.cancelAll(new Error('Editor disposed'));
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3041
3478
|
// Remove bound DOM event listeners
|
|
3042
3479
|
try {
|
|
3043
|
-
for (const key
|
|
3044
|
-
const
|
|
3045
|
-
const element = document.getElementById(this.elements[key]);
|
|
3480
|
+
for (const [key, handlers] of Object.entries(this._handlersByElementKey || {})) {
|
|
3481
|
+
const element = this._getElement(key);
|
|
3046
3482
|
if (!element) continue;
|
|
3047
3483
|
handlers.forEach(handlerRecord => {
|
|
3048
3484
|
try { element.removeEventListener(handlerRecord.eventName, handlerRecord.handler); } catch (error) { void error; }
|
|
@@ -3055,8 +3491,25 @@ function ensureFabric() {
|
|
|
3055
3491
|
this._cropRect = null;
|
|
3056
3492
|
}
|
|
3057
3493
|
|
|
3058
|
-
if (this.containerElement && this._containerOriginalOverflow
|
|
3059
|
-
try { this.
|
|
3494
|
+
if (this.containerElement && this._containerOriginalOverflow) {
|
|
3495
|
+
try { this._restoreContainerOverflowState(); } catch (error) { void error; }
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
if (this._visibilityStateByElement) {
|
|
3499
|
+
try {
|
|
3500
|
+
[this.placeholderElement, this._getCanvasVisibilityElement()].forEach(element => {
|
|
3501
|
+
const state = element ? this._visibilityStateByElement.get(element) : null;
|
|
3502
|
+
if (state) this._restoreElementVisibility(element, state);
|
|
3503
|
+
});
|
|
3504
|
+
} catch (error) { void error; }
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
if (this.canvasElement && this._canvasElementOriginalStyle) {
|
|
3508
|
+
try {
|
|
3509
|
+
this.canvasElement.style.display = this._canvasElementOriginalStyle.display;
|
|
3510
|
+
this.canvasElement.style.width = this._canvasElementOriginalStyle.width;
|
|
3511
|
+
this.canvasElement.style.height = this._canvasElementOriginalStyle.height;
|
|
3512
|
+
} catch (error) { void error; }
|
|
3060
3513
|
}
|
|
3061
3514
|
|
|
3062
3515
|
if (this.canvas) {
|
|
@@ -3066,6 +3519,19 @@ function ensureFabric() {
|
|
|
3066
3519
|
this.isImageLoadedToCanvas = false;
|
|
3067
3520
|
}
|
|
3068
3521
|
this._handlersByElementKey = {};
|
|
3522
|
+
this._elementCache = {};
|
|
3523
|
+
this._clearMaskPlacementMemory();
|
|
3524
|
+
this.originalImage = null;
|
|
3525
|
+
this.baseImageScale = 1;
|
|
3526
|
+
this.currentScale = 1;
|
|
3527
|
+
this.currentRotation = 0;
|
|
3528
|
+
this.isAnimating = false;
|
|
3529
|
+
this._cropMode = false;
|
|
3530
|
+
this._cropRect = null;
|
|
3531
|
+
this._cropHandlers = [];
|
|
3532
|
+
this._cropPrevEvented = null;
|
|
3533
|
+
this._prevSelectionSetting = undefined;
|
|
3534
|
+
this._initialized = false;
|
|
3069
3535
|
}
|
|
3070
3536
|
}
|
|
3071
3537
|
|
|
@@ -3118,6 +3584,7 @@ function ensureFabric() {
|
|
|
3118
3584
|
* @type {boolean}
|
|
3119
3585
|
*/
|
|
3120
3586
|
this.isRunning = false;
|
|
3587
|
+
this.currentTask = null;
|
|
3121
3588
|
}
|
|
3122
3589
|
|
|
3123
3590
|
/**
|
|
@@ -3128,13 +3595,32 @@ function ensureFabric() {
|
|
|
3128
3595
|
*/
|
|
3129
3596
|
async add(animationFn) {
|
|
3130
3597
|
return new Promise((resolve, reject) => {
|
|
3131
|
-
this.animationTasks.push({ animationFn, resolve, reject });
|
|
3598
|
+
this.animationTasks.push({ animationFn, resolve, reject, isSettled: false });
|
|
3132
3599
|
if (!this.isRunning) {
|
|
3133
3600
|
this._drainQueue();
|
|
3134
3601
|
}
|
|
3135
3602
|
});
|
|
3136
3603
|
}
|
|
3137
3604
|
|
|
3605
|
+
isBusy() {
|
|
3606
|
+
return this.isRunning || this.animationTasks.length > 0;
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
cancelAll(reason = new Error('Animation queue cancelled')) {
|
|
3610
|
+
const cancellationError = reason instanceof Error ? reason : new Error(String(reason));
|
|
3611
|
+
const tasks = [
|
|
3612
|
+
...(this.currentTask ? [this.currentTask] : []),
|
|
3613
|
+
...this.animationTasks.splice(0)
|
|
3614
|
+
];
|
|
3615
|
+
tasks.forEach(task => {
|
|
3616
|
+
if (!task || task.isSettled) return;
|
|
3617
|
+
task.isSettled = true;
|
|
3618
|
+
task.reject(cancellationError);
|
|
3619
|
+
});
|
|
3620
|
+
this.isRunning = false;
|
|
3621
|
+
this.currentTask = null;
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3138
3624
|
/**
|
|
3139
3625
|
* Runs queued animation tasks sequentially until the queue is empty.
|
|
3140
3626
|
*
|
|
@@ -3142,22 +3628,30 @@ function ensureFabric() {
|
|
|
3142
3628
|
* @returns {Promise<void>}
|
|
3143
3629
|
*/
|
|
3144
3630
|
async _drainQueue() {
|
|
3145
|
-
if (this.
|
|
3146
|
-
this.isRunning = false;
|
|
3147
|
-
return;
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3631
|
+
if (this.isRunning) return;
|
|
3150
3632
|
this.isRunning = true;
|
|
3151
|
-
const { animationFn, resolve, reject } = this.animationTasks.shift();
|
|
3152
3633
|
|
|
3153
|
-
|
|
3154
|
-
const
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3634
|
+
while (this.animationTasks.length > 0) {
|
|
3635
|
+
const task = this.animationTasks.shift();
|
|
3636
|
+
this.currentTask = task;
|
|
3637
|
+
|
|
3638
|
+
try {
|
|
3639
|
+
const result = await task.animationFn();
|
|
3640
|
+
if (!task.isSettled) {
|
|
3641
|
+
task.isSettled = true;
|
|
3642
|
+
task.resolve(result);
|
|
3643
|
+
}
|
|
3644
|
+
} catch (error) {
|
|
3645
|
+
if (!task.isSettled) {
|
|
3646
|
+
task.isSettled = true;
|
|
3647
|
+
task.reject(error);
|
|
3648
|
+
}
|
|
3649
|
+
} finally {
|
|
3650
|
+
if (this.currentTask === task) this.currentTask = null;
|
|
3651
|
+
}
|
|
3158
3652
|
}
|
|
3159
3653
|
|
|
3160
|
-
|
|
3654
|
+
this.isRunning = false;
|
|
3161
3655
|
}
|
|
3162
3656
|
}
|
|
3163
3657
|
|
|
@@ -3213,16 +3707,8 @@ function ensureFabric() {
|
|
|
3213
3707
|
* @private
|
|
3214
3708
|
*/
|
|
3215
3709
|
enqueue(task) {
|
|
3216
|
-
const nextTask = this.pending.then(
|
|
3217
|
-
|
|
3218
|
-
const resetPending = () => {
|
|
3219
|
-
if (this.pending === pendingAfterTask) {
|
|
3220
|
-
this.pending = Promise.resolve();
|
|
3221
|
-
}
|
|
3222
|
-
};
|
|
3223
|
-
|
|
3224
|
-
pendingAfterTask = nextTask.then(resetPending, resetPending);
|
|
3225
|
-
this.pending = pendingAfterTask;
|
|
3710
|
+
const nextTask = this.pending.then(() => Promise.resolve().then(task));
|
|
3711
|
+
this.pending = nextTask.catch(() => undefined);
|
|
3226
3712
|
return nextTask;
|
|
3227
3713
|
}
|
|
3228
3714
|
|
|
@@ -3234,8 +3720,14 @@ function ensureFabric() {
|
|
|
3234
3720
|
* @returns {void}
|
|
3235
3721
|
*/
|
|
3236
3722
|
execute(command) {
|
|
3237
|
-
command.execute();
|
|
3723
|
+
const result = command.execute();
|
|
3724
|
+
if (result && typeof result.then === 'function') {
|
|
3725
|
+
return Promise.resolve(result).then(() => {
|
|
3726
|
+
this.push(command);
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3238
3729
|
this.push(command);
|
|
3730
|
+
return result;
|
|
3239
3731
|
}
|
|
3240
3732
|
|
|
3241
3733
|
/**
|
|
@@ -3255,9 +3747,8 @@ function ensureFabric() {
|
|
|
3255
3747
|
|
|
3256
3748
|
if (this.history.length > this.maxSize) {
|
|
3257
3749
|
this.history.shift();
|
|
3258
|
-
} else {
|
|
3259
|
-
this.currentIndex++;
|
|
3260
3750
|
}
|
|
3751
|
+
this.currentIndex = this.history.length - 1;
|
|
3261
3752
|
}
|
|
3262
3753
|
|
|
3263
3754
|
/**
|