@bensitu/image-editor 2.0.0 → 2.1.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/README.md +118 -16
- package/dist/cjs/index.cjs +1800 -330
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/animation/animation-queue.js +16 -9
- package/dist/esm/animation/animation-queue.js.map +1 -1
- package/dist/esm/core/default-options.js +216 -9
- package/dist/esm/core/default-options.js.map +1 -1
- package/dist/esm/core/operation-guard.js +28 -0
- package/dist/esm/core/operation-guard.js.map +1 -1
- package/dist/esm/core/public-types.js.map +1 -1
- package/dist/esm/core/state-serializer.js +5 -4
- package/dist/esm/core/state-serializer.js.map +1 -1
- package/dist/esm/crop/crop-controller.js +4 -2
- package/dist/esm/crop/crop-controller.js.map +1 -1
- package/dist/esm/export/export-service.js +21 -10
- package/dist/esm/export/export-service.js.map +1 -1
- package/dist/esm/fabric/fabric-animation.js +56 -4
- package/dist/esm/fabric/fabric-animation.js.map +1 -1
- package/dist/esm/image/image-loader.js +9 -16
- package/dist/esm/image/image-loader.js.map +1 -1
- package/dist/esm/image/image-resampler.js +7 -2
- package/dist/esm/image/image-resampler.js.map +1 -1
- package/dist/esm/image/layout-manager.js +2 -20
- package/dist/esm/image/layout-manager.js.map +1 -1
- package/dist/esm/image/transform-controller.js.map +1 -1
- package/dist/esm/image-editor.js +383 -47
- package/dist/esm/image-editor.js.map +1 -1
- package/dist/esm/mask/mask-factory.js +53 -29
- package/dist/esm/mask/mask-factory.js.map +1 -1
- package/dist/esm/mask/mask-list.js +9 -3
- package/dist/esm/mask/mask-list.js.map +1 -1
- package/dist/esm/mosaic/mosaic-controller.js +670 -0
- package/dist/esm/mosaic/mosaic-controller.js.map +1 -0
- package/dist/esm/mosaic/mosaic-geometry.js +81 -0
- package/dist/esm/mosaic/mosaic-geometry.js.map +1 -0
- package/dist/esm/mosaic/mosaic-pixelate.js +71 -0
- package/dist/esm/mosaic/mosaic-pixelate.js.map +1 -0
- package/dist/esm/ui/dom-bindings.js +10 -3
- package/dist/esm/ui/dom-bindings.js.map +1 -1
- package/dist/esm/utils/number.js.map +1 -1
- package/dist/types/animation/animation-queue.d.ts.map +1 -1
- package/dist/types/core/default-options.d.ts +34 -6
- package/dist/types/core/default-options.d.ts.map +1 -1
- package/dist/types/core/errors.d.ts +1 -1
- package/dist/types/core/operation-guard.d.ts +2 -0
- package/dist/types/core/operation-guard.d.ts.map +1 -1
- package/dist/types/core/public-types.d.ts +123 -13
- package/dist/types/core/public-types.d.ts.map +1 -1
- package/dist/types/core/state-serializer.d.ts +3 -1
- package/dist/types/core/state-serializer.d.ts.map +1 -1
- package/dist/types/crop/crop-controller.d.ts.map +1 -1
- package/dist/types/export/export-service.d.ts.map +1 -1
- package/dist/types/fabric/fabric-animation.d.ts.map +1 -1
- package/dist/types/image/image-loader.d.ts +2 -4
- package/dist/types/image/image-loader.d.ts.map +1 -1
- package/dist/types/image/image-resampler.d.ts +1 -1
- package/dist/types/image/image-resampler.d.ts.map +1 -1
- package/dist/types/image/layout-manager.d.ts +5 -49
- package/dist/types/image/layout-manager.d.ts.map +1 -1
- package/dist/types/image/transform-controller.d.ts +1 -2
- package/dist/types/image/transform-controller.d.ts.map +1 -1
- package/dist/types/image-editor.d.ts +20 -9
- package/dist/types/image-editor.d.ts.map +1 -1
- package/dist/types/index.d.cts +1 -1
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/mask/mask-factory.d.ts +24 -21
- package/dist/types/mask/mask-factory.d.ts.map +1 -1
- package/dist/types/mask/mask-list.d.ts.map +1 -1
- package/dist/types/mosaic/mosaic-controller.d.ts +82 -0
- package/dist/types/mosaic/mosaic-controller.d.ts.map +1 -0
- package/dist/types/mosaic/mosaic-geometry.d.ts +29 -0
- package/dist/types/mosaic/mosaic-geometry.d.ts.map +1 -0
- package/dist/types/mosaic/mosaic-pixelate.d.ts +23 -0
- package/dist/types/mosaic/mosaic-pixelate.d.ts.map +1 -0
- package/dist/types/ui/dom-bindings.d.ts +3 -1
- package/dist/types/ui/dom-bindings.d.ts.map +1 -1
- package/dist/types/utils/number.d.ts +1 -2
- package/dist/types/utils/number.d.ts.map +1 -1
- package/dist/umd/image-editor.umd.js +1 -1
- package/dist/umd/image-editor.umd.js.map +1 -1
- package/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -52,20 +52,27 @@ class AnimationQueue {
|
|
|
52
52
|
return this.add(() => Promise.resolve()).then(() => undefined, () => undefined);
|
|
53
53
|
}
|
|
54
54
|
async drainQueue() {
|
|
55
|
-
if (this.
|
|
56
|
-
this.running = false;
|
|
55
|
+
if (this.running)
|
|
57
56
|
return;
|
|
58
|
-
}
|
|
59
57
|
this.running = true;
|
|
60
|
-
const entry = this.queue.shift();
|
|
61
58
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
while (this.queue.length > 0) {
|
|
60
|
+
const entry = this.queue.shift();
|
|
61
|
+
try {
|
|
62
|
+
await entry.run();
|
|
63
|
+
entry.resolve();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
entry.reject(error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
64
69
|
}
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
finally {
|
|
71
|
+
this.running = false;
|
|
72
|
+
if (this.queue.length > 0) {
|
|
73
|
+
void this.drainQueue();
|
|
74
|
+
}
|
|
67
75
|
}
|
|
68
|
-
void this.drainQueue();
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
78
|
|
|
@@ -92,6 +99,61 @@ function reportError(options, error, message) {
|
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
const FORMAT_ALIAS_TABLE = Object.freeze({
|
|
103
|
+
jpeg: 'jpeg',
|
|
104
|
+
jpg: 'jpeg',
|
|
105
|
+
'image/jpeg': 'jpeg',
|
|
106
|
+
png: 'png',
|
|
107
|
+
'image/png': 'png',
|
|
108
|
+
webp: 'webp',
|
|
109
|
+
'image/webp': 'webp',
|
|
110
|
+
});
|
|
111
|
+
const MIME_TABLE = Object.freeze({
|
|
112
|
+
jpeg: 'image/jpeg',
|
|
113
|
+
png: 'image/png',
|
|
114
|
+
webp: 'image/webp',
|
|
115
|
+
});
|
|
116
|
+
function normalizeImageFormat(input) {
|
|
117
|
+
var _a;
|
|
118
|
+
return (_a = tryNormalizeImageFormat(input)) !== null && _a !== void 0 ? _a : 'jpeg';
|
|
119
|
+
}
|
|
120
|
+
function tryNormalizeImageFormat(input) {
|
|
121
|
+
var _a;
|
|
122
|
+
if (!input)
|
|
123
|
+
return null;
|
|
124
|
+
const key = String(input).toLowerCase();
|
|
125
|
+
if (Object.prototype.hasOwnProperty.call(FORMAT_ALIAS_TABLE, key)) {
|
|
126
|
+
return (_a = FORMAT_ALIAS_TABLE[key]) !== null && _a !== void 0 ? _a : null;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
function mimeTypeFor(format) {
|
|
131
|
+
return MIME_TABLE[format];
|
|
132
|
+
}
|
|
133
|
+
function clampQuality(quality, fallback) {
|
|
134
|
+
const numeric = Number(quality);
|
|
135
|
+
if (!Number.isFinite(numeric))
|
|
136
|
+
return fallback;
|
|
137
|
+
return Math.max(0, Math.min(1, numeric));
|
|
138
|
+
}
|
|
139
|
+
function resolveExportFormat(options, downsampleQuality) {
|
|
140
|
+
var _a;
|
|
141
|
+
const providedOptions = options !== null && options !== void 0 ? options : {};
|
|
142
|
+
const fileType = providedOptions.fileType;
|
|
143
|
+
const formatAlias = providedOptions.format;
|
|
144
|
+
const requested = fileType || formatAlias;
|
|
145
|
+
const format = normalizeImageFormat(requested);
|
|
146
|
+
const mimeType = mimeTypeFor(format);
|
|
147
|
+
if (format === 'png') {
|
|
148
|
+
return { format, mimeType, quality: undefined };
|
|
149
|
+
}
|
|
150
|
+
const rawQuality = (_a = providedOptions.quality) !== null && _a !== void 0 ? _a : downsampleQuality;
|
|
151
|
+
const quality = clampQuality(rawQuality, downsampleQuality);
|
|
152
|
+
return { format, mimeType, quality };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const EMPTY_DEFAULT_MASK_CONFIG = Object.freeze({});
|
|
156
|
+
const DEFAULT_LAYOUT_MODE = 'expand';
|
|
95
157
|
const DEFAULT_OPTIONS = {
|
|
96
158
|
canvasWidth: 800,
|
|
97
159
|
canvasHeight: 600,
|
|
@@ -101,9 +163,8 @@ const DEFAULT_OPTIONS = {
|
|
|
101
163
|
maxScale: 5.0,
|
|
102
164
|
scaleStep: 0.05,
|
|
103
165
|
rotationStep: 90,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
coverImageToCanvas: false,
|
|
166
|
+
defaultLayoutMode: DEFAULT_LAYOUT_MODE,
|
|
167
|
+
layoutMode: DEFAULT_LAYOUT_MODE,
|
|
107
168
|
downsampleOnLoad: true,
|
|
108
169
|
downsampleMaxWidth: 4000,
|
|
109
170
|
downsampleMaxHeight: 3000,
|
|
@@ -118,6 +179,7 @@ const DEFAULT_OPTIONS = {
|
|
|
118
179
|
mergeMaskByDefault: true,
|
|
119
180
|
defaultMaskWidth: 50,
|
|
120
181
|
defaultMaskHeight: 80,
|
|
182
|
+
defaultMaskConfig: EMPTY_DEFAULT_MASK_CONFIG,
|
|
121
183
|
maskRotatable: false,
|
|
122
184
|
maskLabelOnSelect: true,
|
|
123
185
|
maskLabelOffset: 3,
|
|
@@ -159,6 +221,16 @@ const DEFAULT_CROP = {
|
|
|
159
221
|
preserveMasksAfterCrop: false,
|
|
160
222
|
allowRotationOfCropRect: false,
|
|
161
223
|
exportFileType: 'source'};
|
|
224
|
+
const DEFAULT_MOSAIC_CONFIG = Object.freeze({
|
|
225
|
+
brushSize: 48,
|
|
226
|
+
blockSize: 8,
|
|
227
|
+
previewStroke: '#333',
|
|
228
|
+
previewStrokeWidth: 1,
|
|
229
|
+
previewStrokeDashArray: Object.freeze([4, 4]),
|
|
230
|
+
previewFill: 'rgba(0,0,0,0)',
|
|
231
|
+
outputFileType: 'source',
|
|
232
|
+
outputQuality: undefined,
|
|
233
|
+
});
|
|
162
234
|
const KNOWN_TOP_LEVEL_KEYS = new Set([
|
|
163
235
|
'canvasWidth',
|
|
164
236
|
'canvasHeight',
|
|
@@ -168,9 +240,7 @@ const KNOWN_TOP_LEVEL_KEYS = new Set([
|
|
|
168
240
|
'maxScale',
|
|
169
241
|
'scaleStep',
|
|
170
242
|
'rotationStep',
|
|
171
|
-
'
|
|
172
|
-
'fitImageToCanvas',
|
|
173
|
-
'coverImageToCanvas',
|
|
243
|
+
'defaultLayoutMode',
|
|
174
244
|
'downsampleOnLoad',
|
|
175
245
|
'downsampleMaxWidth',
|
|
176
246
|
'downsampleMaxHeight',
|
|
@@ -185,6 +255,7 @@ const KNOWN_TOP_LEVEL_KEYS = new Set([
|
|
|
185
255
|
'mergeMaskByDefault',
|
|
186
256
|
'defaultMaskWidth',
|
|
187
257
|
'defaultMaskHeight',
|
|
258
|
+
'defaultMaskConfig',
|
|
188
259
|
'maskRotatable',
|
|
189
260
|
'maskLabelOnSelect',
|
|
190
261
|
'maskLabelOffset',
|
|
@@ -205,10 +276,44 @@ const KNOWN_TOP_LEVEL_KEYS = new Set([
|
|
|
205
276
|
'onWarning',
|
|
206
277
|
'label',
|
|
207
278
|
'crop',
|
|
279
|
+
'defaultMosaicConfig',
|
|
208
280
|
]);
|
|
209
281
|
function normalizeCallback(value) {
|
|
210
282
|
return typeof value === 'function' ? value : null;
|
|
211
283
|
}
|
|
284
|
+
function isLayoutMode(value) {
|
|
285
|
+
return value === 'fit' || value === 'cover' || value === 'expand';
|
|
286
|
+
}
|
|
287
|
+
function normalizeLayoutMode(value) {
|
|
288
|
+
return isLayoutMode(value) ? value : DEFAULT_LAYOUT_MODE;
|
|
289
|
+
}
|
|
290
|
+
function isConfigObject(value) {
|
|
291
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
292
|
+
}
|
|
293
|
+
function copyDefaultMaskConfigValue(value) {
|
|
294
|
+
return Array.isArray(value) ? [...value] : value;
|
|
295
|
+
}
|
|
296
|
+
function normalizeDefaultMaskConfig(value) {
|
|
297
|
+
if (!isConfigObject(value))
|
|
298
|
+
return EMPTY_DEFAULT_MASK_CONFIG;
|
|
299
|
+
const normalized = {};
|
|
300
|
+
for (const [key, optionValue] of Object.entries(value)) {
|
|
301
|
+
if (key === 'onCreate' || key === 'fabricGenerator' || key === 'styles')
|
|
302
|
+
continue;
|
|
303
|
+
normalized[key] = copyDefaultMaskConfigValue(optionValue);
|
|
304
|
+
}
|
|
305
|
+
const styles = value.styles;
|
|
306
|
+
if (isConfigObject(styles)) {
|
|
307
|
+
const copiedStyles = {};
|
|
308
|
+
for (const [key, styleValue] of Object.entries(styles)) {
|
|
309
|
+
copiedStyles[key] = copyDefaultMaskConfigValue(styleValue);
|
|
310
|
+
}
|
|
311
|
+
Object.freeze(copiedStyles);
|
|
312
|
+
normalized.styles = copiedStyles;
|
|
313
|
+
}
|
|
314
|
+
Object.freeze(normalized);
|
|
315
|
+
return normalized;
|
|
316
|
+
}
|
|
212
317
|
function normalizePositiveInteger(value, fallback) {
|
|
213
318
|
const numeric = Number(value);
|
|
214
319
|
if (!Number.isFinite(numeric) || numeric <= 0)
|
|
@@ -264,6 +369,151 @@ function normalizeOptionalQuality(value) {
|
|
|
264
369
|
return undefined;
|
|
265
370
|
return Math.max(0, Math.min(1, numeric));
|
|
266
371
|
}
|
|
372
|
+
function hasOwn(object, key) {
|
|
373
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
374
|
+
}
|
|
375
|
+
function isFiniteNumber$1(value) {
|
|
376
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
377
|
+
}
|
|
378
|
+
function normalizeMosaicPositiveNumber(value, fallback) {
|
|
379
|
+
return isFiniteNumber$1(value) && value > 0 ? value : fallback;
|
|
380
|
+
}
|
|
381
|
+
function normalizeMosaicBlockSize(value, fallback) {
|
|
382
|
+
return isFiniteNumber$1(value) && value > 0 ? Math.max(1, Math.floor(value)) : fallback;
|
|
383
|
+
}
|
|
384
|
+
function normalizeMosaicNonNegativeNumber(value, fallback) {
|
|
385
|
+
return isFiniteNumber$1(value) && value >= 0 ? value : fallback;
|
|
386
|
+
}
|
|
387
|
+
function normalizeMosaicDashArray(value, fallback) {
|
|
388
|
+
if (value === null)
|
|
389
|
+
return null;
|
|
390
|
+
if (Array.isArray(value) &&
|
|
391
|
+
value.every((entry) => typeof entry === 'number' && Number.isFinite(entry) && entry >= 0)) {
|
|
392
|
+
return [...value];
|
|
393
|
+
}
|
|
394
|
+
return fallback ? [...fallback] : null;
|
|
395
|
+
}
|
|
396
|
+
function normalizeMosaicOutputFileType(value, fallback) {
|
|
397
|
+
var _a;
|
|
398
|
+
if (value === 'source')
|
|
399
|
+
return 'source';
|
|
400
|
+
if (typeof value !== 'string')
|
|
401
|
+
return fallback;
|
|
402
|
+
return (_a = tryNormalizeImageFormat(value)) !== null && _a !== void 0 ? _a : fallback;
|
|
403
|
+
}
|
|
404
|
+
function normalizeMosaicOutputQuality(value, fallback) {
|
|
405
|
+
if (value === undefined || value === null)
|
|
406
|
+
return undefined;
|
|
407
|
+
if (!isFiniteNumber$1(value))
|
|
408
|
+
return fallback;
|
|
409
|
+
return Math.max(0, Math.min(1, value));
|
|
410
|
+
}
|
|
411
|
+
function cloneResolvedMosaicConfig(config) {
|
|
412
|
+
return {
|
|
413
|
+
...config,
|
|
414
|
+
previewStrokeDashArray: config.previewStrokeDashArray
|
|
415
|
+
? [...config.previewStrokeDashArray]
|
|
416
|
+
: null,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function normalizeMosaicConfig(input, fallback) {
|
|
420
|
+
if (!isConfigObject(input))
|
|
421
|
+
return cloneResolvedMosaicConfig(fallback);
|
|
422
|
+
return mergeMosaicConfigPatch(fallback, input);
|
|
423
|
+
}
|
|
424
|
+
function mergeMosaicConfigPatch(current, patch, fallback = current) {
|
|
425
|
+
const raw = isConfigObject(patch) ? patch : {};
|
|
426
|
+
const next = cloneResolvedMosaicConfig(current);
|
|
427
|
+
if (hasOwn(raw, 'brushSize')) {
|
|
428
|
+
next.brushSize = normalizeMosaicPositiveNumber(raw.brushSize, fallback.brushSize);
|
|
429
|
+
}
|
|
430
|
+
if (hasOwn(raw, 'blockSize')) {
|
|
431
|
+
next.blockSize = normalizeMosaicBlockSize(raw.blockSize, fallback.blockSize);
|
|
432
|
+
}
|
|
433
|
+
if (hasOwn(raw, 'previewStroke')) {
|
|
434
|
+
next.previewStroke =
|
|
435
|
+
typeof raw.previewStroke === 'string' ? raw.previewStroke : fallback.previewStroke;
|
|
436
|
+
}
|
|
437
|
+
if (hasOwn(raw, 'previewStrokeWidth')) {
|
|
438
|
+
next.previewStrokeWidth = normalizeMosaicNonNegativeNumber(raw.previewStrokeWidth, fallback.previewStrokeWidth);
|
|
439
|
+
}
|
|
440
|
+
if (hasOwn(raw, 'previewStrokeDashArray')) {
|
|
441
|
+
next.previewStrokeDashArray = normalizeMosaicDashArray(raw.previewStrokeDashArray, fallback.previewStrokeDashArray);
|
|
442
|
+
}
|
|
443
|
+
if (hasOwn(raw, 'previewFill')) {
|
|
444
|
+
next.previewFill =
|
|
445
|
+
typeof raw.previewFill === 'string' ? raw.previewFill : fallback.previewFill;
|
|
446
|
+
}
|
|
447
|
+
if (hasOwn(raw, 'outputFileType')) {
|
|
448
|
+
next.outputFileType = normalizeMosaicOutputFileType(raw.outputFileType, fallback.outputFileType);
|
|
449
|
+
}
|
|
450
|
+
if (hasOwn(raw, 'outputQuality')) {
|
|
451
|
+
next.outputQuality = normalizeMosaicOutputQuality(raw.outputQuality, fallback.outputQuality);
|
|
452
|
+
}
|
|
453
|
+
return next;
|
|
454
|
+
}
|
|
455
|
+
function getInvalidMosaicConfigFields(input) {
|
|
456
|
+
const raw = isConfigObject(input) ? input : {};
|
|
457
|
+
const invalid = [];
|
|
458
|
+
if (hasOwn(raw, 'brushSize') &&
|
|
459
|
+
!(typeof raw.brushSize === 'number' && Number.isFinite(raw.brushSize) && raw.brushSize > 0)) {
|
|
460
|
+
invalid.push('brushSize');
|
|
461
|
+
}
|
|
462
|
+
if (hasOwn(raw, 'blockSize') &&
|
|
463
|
+
!(typeof raw.blockSize === 'number' && Number.isFinite(raw.blockSize) && raw.blockSize > 0)) {
|
|
464
|
+
invalid.push('blockSize');
|
|
465
|
+
}
|
|
466
|
+
if (hasOwn(raw, 'previewStroke') && typeof raw.previewStroke !== 'string') {
|
|
467
|
+
invalid.push('previewStroke');
|
|
468
|
+
}
|
|
469
|
+
if (hasOwn(raw, 'previewStrokeWidth') &&
|
|
470
|
+
!(typeof raw.previewStrokeWidth === 'number' &&
|
|
471
|
+
Number.isFinite(raw.previewStrokeWidth) &&
|
|
472
|
+
raw.previewStrokeWidth >= 0)) {
|
|
473
|
+
invalid.push('previewStrokeWidth');
|
|
474
|
+
}
|
|
475
|
+
if (hasOwn(raw, 'previewStrokeDashArray')) {
|
|
476
|
+
const value = raw.previewStrokeDashArray;
|
|
477
|
+
const valid = value === null ||
|
|
478
|
+
(Array.isArray(value) &&
|
|
479
|
+
value.every((entry) => typeof entry === 'number' && Number.isFinite(entry) && entry >= 0));
|
|
480
|
+
if (!valid)
|
|
481
|
+
invalid.push('previewStrokeDashArray');
|
|
482
|
+
}
|
|
483
|
+
if (hasOwn(raw, 'previewFill') && typeof raw.previewFill !== 'string') {
|
|
484
|
+
invalid.push('previewFill');
|
|
485
|
+
}
|
|
486
|
+
if (hasOwn(raw, 'outputFileType')) {
|
|
487
|
+
const value = raw.outputFileType;
|
|
488
|
+
const valid = value === 'source' || (typeof value === 'string' && tryNormalizeImageFormat(value));
|
|
489
|
+
if (!valid)
|
|
490
|
+
invalid.push('outputFileType');
|
|
491
|
+
}
|
|
492
|
+
if (hasOwn(raw, 'outputQuality') &&
|
|
493
|
+
raw.outputQuality !== undefined &&
|
|
494
|
+
raw.outputQuality !== null &&
|
|
495
|
+
!(typeof raw.outputQuality === 'number' && Number.isFinite(raw.outputQuality))) {
|
|
496
|
+
invalid.push('outputQuality');
|
|
497
|
+
}
|
|
498
|
+
return invalid;
|
|
499
|
+
}
|
|
500
|
+
function areResolvedMosaicConfigsEqual(left, right) {
|
|
501
|
+
const leftDash = left.previewStrokeDashArray;
|
|
502
|
+
const rightDash = right.previewStrokeDashArray;
|
|
503
|
+
const dashEqual = leftDash === rightDash ||
|
|
504
|
+
(Array.isArray(leftDash) &&
|
|
505
|
+
Array.isArray(rightDash) &&
|
|
506
|
+
leftDash.length === rightDash.length &&
|
|
507
|
+
leftDash.every((value, index) => value === rightDash[index]));
|
|
508
|
+
return (left.brushSize === right.brushSize &&
|
|
509
|
+
left.blockSize === right.blockSize &&
|
|
510
|
+
left.previewStroke === right.previewStroke &&
|
|
511
|
+
left.previewStrokeWidth === right.previewStrokeWidth &&
|
|
512
|
+
dashEqual &&
|
|
513
|
+
left.previewFill === right.previewFill &&
|
|
514
|
+
left.outputFileType === right.outputFileType &&
|
|
515
|
+
left.outputQuality === right.outputQuality);
|
|
516
|
+
}
|
|
267
517
|
function resolveOptions(input) {
|
|
268
518
|
var _a, _b, _c, _d;
|
|
269
519
|
const raw = input !== null && input !== void 0 ? input : {};
|
|
@@ -271,7 +521,7 @@ function resolveOptions(input) {
|
|
|
271
521
|
for (const key of Object.keys(raw)) {
|
|
272
522
|
if (!KNOWN_TOP_LEVEL_KEYS.has(key))
|
|
273
523
|
continue;
|
|
274
|
-
if (key === 'label' || key === 'crop')
|
|
524
|
+
if (key === 'label' || key === 'crop' || key === 'defaultMosaicConfig')
|
|
275
525
|
continue;
|
|
276
526
|
if (key === 'onImageLoadStart' ||
|
|
277
527
|
key === 'onImageLoaded' ||
|
|
@@ -300,6 +550,12 @@ function resolveOptions(input) {
|
|
|
300
550
|
resolved.exportAreaByDefault = normalizeExportArea(value);
|
|
301
551
|
continue;
|
|
302
552
|
}
|
|
553
|
+
if (key === 'defaultLayoutMode') {
|
|
554
|
+
const layoutMode = normalizeLayoutMode(value);
|
|
555
|
+
resolved.defaultLayoutMode = layoutMode;
|
|
556
|
+
resolved.layoutMode = layoutMode;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
303
559
|
if (key === 'canvasWidth') {
|
|
304
560
|
resolved.canvasWidth = normalizePositiveInteger(value, DEFAULT_OPTIONS.canvasWidth);
|
|
305
561
|
continue;
|
|
@@ -352,6 +608,10 @@ function resolveOptions(input) {
|
|
|
352
608
|
resolved.defaultMaskHeight = normalizePositiveFiniteNumber(value, DEFAULT_OPTIONS.defaultMaskHeight);
|
|
353
609
|
continue;
|
|
354
610
|
}
|
|
611
|
+
if (key === 'defaultMaskConfig') {
|
|
612
|
+
resolved.defaultMaskConfig = normalizeDefaultMaskConfig(value);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
355
615
|
if (key === 'maskLabelOffset') {
|
|
356
616
|
resolved.maskLabelOffset = normalizeNonNegativeFiniteNumber(value, DEFAULT_OPTIONS.maskLabelOffset);
|
|
357
617
|
continue;
|
|
@@ -403,11 +663,17 @@ function resolveOptions(input) {
|
|
|
403
663
|
exportQuality: normalizeOptionalQuality(userCrop.exportQuality),
|
|
404
664
|
};
|
|
405
665
|
Object.freeze(crop);
|
|
406
|
-
|
|
666
|
+
const defaultMosaicConfig = normalizeMosaicConfig(raw.defaultMosaicConfig, DEFAULT_MOSAIC_CONFIG);
|
|
667
|
+
if (defaultMosaicConfig.previewStrokeDashArray) {
|
|
668
|
+
Object.freeze(defaultMosaicConfig.previewStrokeDashArray);
|
|
669
|
+
}
|
|
670
|
+
Object.freeze(defaultMosaicConfig);
|
|
671
|
+
return Object.freeze({
|
|
407
672
|
...resolved,
|
|
408
673
|
label,
|
|
409
674
|
crop,
|
|
410
|
-
|
|
675
|
+
defaultMosaicConfig,
|
|
676
|
+
});
|
|
411
677
|
}
|
|
412
678
|
|
|
413
679
|
class OperationGuard {
|
|
@@ -442,6 +708,12 @@ class OperationGuard {
|
|
|
442
708
|
writable: true,
|
|
443
709
|
value: null
|
|
444
710
|
});
|
|
711
|
+
Object.defineProperty(this, "animationAborters", {
|
|
712
|
+
enumerable: true,
|
|
713
|
+
configurable: true,
|
|
714
|
+
writable: true,
|
|
715
|
+
value: new Set()
|
|
716
|
+
});
|
|
445
717
|
}
|
|
446
718
|
isAnimating() {
|
|
447
719
|
return this.isAnimationActive;
|
|
@@ -470,6 +742,28 @@ class OperationGuard {
|
|
|
470
742
|
this.isLoadingActive = false;
|
|
471
743
|
this.currentOperationName = null;
|
|
472
744
|
this.currentOperationToken = null;
|
|
745
|
+
for (const abort of this.animationAborters) {
|
|
746
|
+
try {
|
|
747
|
+
abort();
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
this.animationAborters.clear();
|
|
753
|
+
}
|
|
754
|
+
registerAnimationAborter(abort) {
|
|
755
|
+
if (this.isDisposedFlag) {
|
|
756
|
+
try {
|
|
757
|
+
abort();
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
}
|
|
761
|
+
return () => undefined;
|
|
762
|
+
}
|
|
763
|
+
this.animationAborters.add(abort);
|
|
764
|
+
return () => {
|
|
765
|
+
this.animationAborters.delete(abort);
|
|
766
|
+
};
|
|
473
767
|
}
|
|
474
768
|
beginLoading() {
|
|
475
769
|
this.isLoadingActive = true;
|
|
@@ -560,6 +854,7 @@ const SNAPSHOT_CUSTOM_KEYS = [
|
|
|
560
854
|
'borderColor',
|
|
561
855
|
'cornerColor',
|
|
562
856
|
'cornerSize',
|
|
857
|
+
'isMosaicPreview',
|
|
563
858
|
];
|
|
564
859
|
function copySnapshotCustomPropsFromCanvas(canvasObjects, jsonObjects) {
|
|
565
860
|
if (!Array.isArray(jsonObjects))
|
|
@@ -611,6 +906,8 @@ function copySnapshotCustomPropsFromCanvas(canvasObjects, jsonObjects) {
|
|
|
611
906
|
jsonObject.isCropRect = true;
|
|
612
907
|
if (liveObject.maskLabel === true)
|
|
613
908
|
jsonObject.maskLabel = true;
|
|
909
|
+
if (liveObject.isMosaicPreview === true)
|
|
910
|
+
jsonObject.isMosaicPreview = true;
|
|
614
911
|
}
|
|
615
912
|
}
|
|
616
913
|
function isActiveSelectionObject(object) {
|
|
@@ -621,9 +918,7 @@ function isActiveSelectionObject(object) {
|
|
|
621
918
|
return true;
|
|
622
919
|
const isType = object.isType;
|
|
623
920
|
return (typeof isType === 'function' &&
|
|
624
|
-
(isType.call(object, 'ActiveSelection') ||
|
|
625
|
-
isType.call(object, 'activeSelection') ||
|
|
626
|
-
isType.call(object, 'activeselection')));
|
|
921
|
+
(isType.call(object, 'ActiveSelection') || isType.call(object, 'activeSelection')));
|
|
627
922
|
}
|
|
628
923
|
function saveState(input) {
|
|
629
924
|
var _a, _b, _c;
|
|
@@ -640,7 +935,7 @@ function saveState(input) {
|
|
|
640
935
|
const jsonObj = canvas.toJSON(SNAPSHOT_CUSTOM_KEYS);
|
|
641
936
|
copySnapshotCustomPropsFromCanvas(canvas.getObjects(), jsonObj.objects);
|
|
642
937
|
if (Array.isArray(jsonObj.objects)) {
|
|
643
|
-
jsonObj.objects = jsonObj.objects.filter((o) => o.isCropRect !== true && o.maskLabel !== true);
|
|
938
|
+
jsonObj.objects = jsonObj.objects.filter((o) => o.isCropRect !== true && o.maskLabel !== true && o.isMosaicPreview !== true);
|
|
644
939
|
}
|
|
645
940
|
jsonObj._editorState = {
|
|
646
941
|
currentScale,
|
|
@@ -1312,59 +1607,6 @@ function getObjectBBox(object) {
|
|
|
1312
1607
|
};
|
|
1313
1608
|
}
|
|
1314
1609
|
|
|
1315
|
-
const FORMAT_ALIAS_TABLE = Object.freeze({
|
|
1316
|
-
jpeg: 'jpeg',
|
|
1317
|
-
jpg: 'jpeg',
|
|
1318
|
-
'image/jpeg': 'jpeg',
|
|
1319
|
-
png: 'png',
|
|
1320
|
-
'image/png': 'png',
|
|
1321
|
-
webp: 'webp',
|
|
1322
|
-
'image/webp': 'webp',
|
|
1323
|
-
});
|
|
1324
|
-
const MIME_TABLE = Object.freeze({
|
|
1325
|
-
jpeg: 'image/jpeg',
|
|
1326
|
-
png: 'image/png',
|
|
1327
|
-
webp: 'image/webp',
|
|
1328
|
-
});
|
|
1329
|
-
function normalizeImageFormat(input) {
|
|
1330
|
-
var _a;
|
|
1331
|
-
return (_a = tryNormalizeImageFormat(input)) !== null && _a !== void 0 ? _a : 'jpeg';
|
|
1332
|
-
}
|
|
1333
|
-
function tryNormalizeImageFormat(input) {
|
|
1334
|
-
var _a;
|
|
1335
|
-
if (!input)
|
|
1336
|
-
return null;
|
|
1337
|
-
const key = String(input).toLowerCase();
|
|
1338
|
-
if (Object.prototype.hasOwnProperty.call(FORMAT_ALIAS_TABLE, key)) {
|
|
1339
|
-
return (_a = FORMAT_ALIAS_TABLE[key]) !== null && _a !== void 0 ? _a : null;
|
|
1340
|
-
}
|
|
1341
|
-
return null;
|
|
1342
|
-
}
|
|
1343
|
-
function mimeTypeFor(format) {
|
|
1344
|
-
return MIME_TABLE[format];
|
|
1345
|
-
}
|
|
1346
|
-
function clampQuality(quality, fallback) {
|
|
1347
|
-
const numeric = Number(quality);
|
|
1348
|
-
if (!Number.isFinite(numeric))
|
|
1349
|
-
return fallback;
|
|
1350
|
-
return Math.max(0, Math.min(1, numeric));
|
|
1351
|
-
}
|
|
1352
|
-
function resolveExportFormat(options, downsampleQuality) {
|
|
1353
|
-
var _a;
|
|
1354
|
-
const providedOptions = options !== null && options !== void 0 ? options : {};
|
|
1355
|
-
const fileType = providedOptions.fileType;
|
|
1356
|
-
const formatAlias = providedOptions.format;
|
|
1357
|
-
const requested = fileType || formatAlias;
|
|
1358
|
-
const format = normalizeImageFormat(requested);
|
|
1359
|
-
const mimeType = mimeTypeFor(format);
|
|
1360
|
-
if (format === 'png') {
|
|
1361
|
-
return { format, mimeType, quality: undefined };
|
|
1362
|
-
}
|
|
1363
|
-
const rawQuality = (_a = providedOptions.quality) !== null && _a !== void 0 ? _a : downsampleQuality;
|
|
1364
|
-
const quality = clampQuality(rawQuality, downsampleQuality);
|
|
1365
|
-
return { format, mimeType, quality };
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
1610
|
const CROP_RECT_FILL = 'rgba(0,0,0,0.12)';
|
|
1369
1611
|
const CROP_RECT_STROKE = '#00aaff';
|
|
1370
1612
|
const CROP_RECT_DASH = [6, 4];
|
|
@@ -1479,14 +1721,16 @@ function maskIntersectsRegion(mask, region) {
|
|
|
1479
1721
|
function capturePreservedMasks(canvas, cropRegion, maskBackups = []) {
|
|
1480
1722
|
var _a;
|
|
1481
1723
|
const records = [];
|
|
1482
|
-
const styleBackupByMask =
|
|
1724
|
+
const styleBackupByMask = maskBackups.length > 0
|
|
1725
|
+
? new Map(maskBackups.map((backup) => [backup.object, backup]))
|
|
1726
|
+
: null;
|
|
1483
1727
|
const masks = canvas.getObjects().filter(isMaskObject);
|
|
1484
1728
|
for (const mask of masks) {
|
|
1485
1729
|
try {
|
|
1486
1730
|
mask.setCoords();
|
|
1487
1731
|
const intersects = maskIntersectsRegion(mask, cropRegion);
|
|
1488
1732
|
if (intersects) {
|
|
1489
|
-
const styleBackup = (_a = styleBackupByMask.get(mask)) !== null && _a !== void 0 ? _a : captureMaskStyleBackup(mask);
|
|
1733
|
+
const styleBackup = (_a = styleBackupByMask === null || styleBackupByMask === void 0 ? void 0 : styleBackupByMask.get(mask)) !== null && _a !== void 0 ? _a : captureMaskStyleBackup(mask);
|
|
1490
1734
|
records.push({
|
|
1491
1735
|
mask,
|
|
1492
1736
|
left: finiteNumberOrFallback(mask.left, 0),
|
|
@@ -1762,62 +2006,948 @@ async function applyCrop(context) {
|
|
|
1762
2006
|
}
|
|
1763
2007
|
}
|
|
1764
2008
|
|
|
1765
|
-
function
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2009
|
+
function computeDownsampleDimensions(srcWidth, srcHeight, maxWidth, maxHeight) {
|
|
2010
|
+
if (!isPositiveFinite$1(srcWidth) ||
|
|
2011
|
+
!isPositiveFinite$1(srcHeight) ||
|
|
2012
|
+
!isPositiveFinite$1(maxWidth) ||
|
|
2013
|
+
!isPositiveFinite$1(maxHeight)) {
|
|
2014
|
+
return {
|
|
2015
|
+
width: Math.max(1, Math.round(srcWidth) || 1),
|
|
2016
|
+
height: Math.max(1, Math.round(srcHeight) || 1),
|
|
2017
|
+
needsResize: false,
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
const needsResize = srcWidth > maxWidth || srcHeight > maxHeight;
|
|
2021
|
+
if (!needsResize) {
|
|
2022
|
+
return { width: srcWidth, height: srcHeight, needsResize: false };
|
|
2023
|
+
}
|
|
2024
|
+
const ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight);
|
|
1779
2025
|
return {
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
: context.options.mergeMaskByDefault,
|
|
1784
|
-
multiplier: resolveMultiplier(providedOptions.multiplier, context.options.exportMultiplier),
|
|
1785
|
-
format: resolveExportFormat(providedOptions, context.options.downsampleQuality),
|
|
2026
|
+
width: Math.max(1, Math.round(srcWidth * ratio)),
|
|
2027
|
+
height: Math.max(1, Math.round(srcHeight * ratio)),
|
|
2028
|
+
needsResize: true,
|
|
1786
2029
|
};
|
|
1787
2030
|
}
|
|
1788
|
-
function
|
|
1789
|
-
|
|
1790
|
-
const getter = canvasLike[getterName];
|
|
1791
|
-
const value = typeof getter === 'function' ? getter.call(canvasLike) : canvasLike[propertyName];
|
|
1792
|
-
return Math.max(1, Math.ceil(Number.isFinite(value) ? Number(value) : 1));
|
|
2031
|
+
function isPositiveFinite$1(value) {
|
|
2032
|
+
return Number.isFinite(value) && value > 0;
|
|
1793
2033
|
}
|
|
1794
|
-
function
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
const outputHeight = Math.max(1, Math.ceil(sourceHeight * multiplier));
|
|
1800
|
-
const pixelCount = outputWidth * outputHeight;
|
|
1801
|
-
const maxPixels = context.options.maxExportPixels;
|
|
1802
|
-
if (!Number.isFinite(pixelCount) || pixelCount > maxPixels) {
|
|
1803
|
-
throw new RangeError(`[ImageEditor] Export size ${outputWidth}x${outputHeight} ` +
|
|
1804
|
-
`(${pixelCount} pixels) exceeds maxExportPixels (${maxPixels}).`);
|
|
2034
|
+
function selectDownsampleMimeType(sourceMime, preserveSourceFormat, downsampleMimeType) {
|
|
2035
|
+
if (downsampleMimeType)
|
|
2036
|
+
return downsampleMimeType;
|
|
2037
|
+
if (preserveSourceFormat && (sourceMime === 'image/png' || sourceMime === 'image/webp')) {
|
|
2038
|
+
return sourceMime;
|
|
1805
2039
|
}
|
|
2040
|
+
return 'image/jpeg';
|
|
1806
2041
|
}
|
|
1807
|
-
function
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
const
|
|
1814
|
-
const
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
throw new ExportError('exportImageBase64 failed: image export region is empty.');
|
|
2042
|
+
function detectSourceMimeType(dataUrl) {
|
|
2043
|
+
const match = /^data:(image\/[a-z0-9+\-.]+)\s*;/i.exec(dataUrl);
|
|
2044
|
+
return match ? match[1].toLowerCase() : null;
|
|
2045
|
+
}
|
|
2046
|
+
function resampleImage(imageElement, maxWidth, maxHeight, sourceMime, preserveSourceFormat, downsampleMimeType, quality, ownerDocument) {
|
|
2047
|
+
var _a;
|
|
2048
|
+
const { width, height } = computeDownsampleDimensions(imageElement.naturalWidth, imageElement.naturalHeight, maxWidth, maxHeight);
|
|
2049
|
+
const mimeType = selectDownsampleMimeType(sourceMime, preserveSourceFormat, downsampleMimeType);
|
|
2050
|
+
const documentForCanvas = (_a = ownerDocument !== null && ownerDocument !== void 0 ? ownerDocument : imageElement.ownerDocument) !== null && _a !== void 0 ? _a : (typeof document !== 'undefined' ? document : null);
|
|
2051
|
+
if (!documentForCanvas) {
|
|
2052
|
+
throw new DownsampleError('Failed to obtain an owner document for downsampling.');
|
|
1819
2053
|
}
|
|
1820
|
-
|
|
2054
|
+
const offscreenCanvas = documentForCanvas.createElement('canvas');
|
|
2055
|
+
offscreenCanvas.width = width;
|
|
2056
|
+
offscreenCanvas.height = height;
|
|
2057
|
+
const context = offscreenCanvas.getContext('2d');
|
|
2058
|
+
if (!context) {
|
|
2059
|
+
throw new DownsampleError('Failed to obtain a 2D context for downsampling.');
|
|
2060
|
+
}
|
|
2061
|
+
context.drawImage(imageElement, 0, 0, imageElement.naturalWidth, imageElement.naturalHeight, 0, 0, width, height);
|
|
2062
|
+
const dataUrl = mimeType === 'image/png'
|
|
2063
|
+
? offscreenCanvas.toDataURL(mimeType)
|
|
2064
|
+
: offscreenCanvas.toDataURL(mimeType, quality);
|
|
2065
|
+
return { dataUrl, width, height, mimeType };
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
function withTimeout(promise, ms, label) {
|
|
2069
|
+
return new Promise((resolve, reject) => {
|
|
2070
|
+
const start = Date.now();
|
|
2071
|
+
const timeoutId = setTimeout(() => {
|
|
2072
|
+
reject(new ImageLoadTimeoutError(label, Date.now() - start));
|
|
2073
|
+
}, ms);
|
|
2074
|
+
promise.then((value) => {
|
|
2075
|
+
clearTimeout(timeoutId);
|
|
2076
|
+
resolve(value);
|
|
2077
|
+
}, (err) => {
|
|
2078
|
+
clearTimeout(timeoutId);
|
|
2079
|
+
reject(err);
|
|
2080
|
+
});
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
const MATRIX_DETERMINANT_EPSILON = 1e-8;
|
|
2085
|
+
const MATRIX_SCALE_EPSILON = 1e-8;
|
|
2086
|
+
function toMatrix2D(matrix) {
|
|
2087
|
+
if (matrix.length < 6)
|
|
2088
|
+
return null;
|
|
2089
|
+
const a = matrix[0];
|
|
2090
|
+
const b = matrix[1];
|
|
2091
|
+
const c = matrix[2];
|
|
2092
|
+
const d = matrix[3];
|
|
2093
|
+
const e = matrix[4];
|
|
2094
|
+
const f = matrix[5];
|
|
2095
|
+
if (!Number.isFinite(a) ||
|
|
2096
|
+
!Number.isFinite(b) ||
|
|
2097
|
+
!Number.isFinite(c) ||
|
|
2098
|
+
!Number.isFinite(d) ||
|
|
2099
|
+
!Number.isFinite(e) ||
|
|
2100
|
+
!Number.isFinite(f)) {
|
|
2101
|
+
return null;
|
|
2102
|
+
}
|
|
2103
|
+
return { a: a, b: b, c: c, d: d, e: e, f: f };
|
|
2104
|
+
}
|
|
2105
|
+
function invertMatrix(matrix) {
|
|
2106
|
+
const determinant = matrix.a * matrix.d - matrix.b * matrix.c;
|
|
2107
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) < MATRIX_DETERMINANT_EPSILON) {
|
|
2108
|
+
return null;
|
|
2109
|
+
}
|
|
2110
|
+
return {
|
|
2111
|
+
a: matrix.d / determinant,
|
|
2112
|
+
b: -matrix.b / determinant,
|
|
2113
|
+
c: -matrix.c / determinant,
|
|
2114
|
+
d: matrix.a / determinant,
|
|
2115
|
+
e: (matrix.c * matrix.f - matrix.d * matrix.e) / determinant,
|
|
2116
|
+
f: (matrix.b * matrix.e - matrix.a * matrix.f) / determinant,
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
function transformPoint(point, matrix) {
|
|
2120
|
+
return {
|
|
2121
|
+
x: matrix.a * point.x + matrix.c * point.y + matrix.e,
|
|
2122
|
+
y: matrix.b * point.x + matrix.d * point.y + matrix.f,
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
function getSourceRadiusFromMatrix(matrix, canvasRadius) {
|
|
2126
|
+
const scaleX = Math.hypot(matrix.a, matrix.b);
|
|
2127
|
+
const scaleY = Math.hypot(matrix.c, matrix.d);
|
|
2128
|
+
const minScale = Math.min(scaleX > MATRIX_SCALE_EPSILON ? scaleX : Number.POSITIVE_INFINITY, scaleY > MATRIX_SCALE_EPSILON ? scaleY : Number.POSITIVE_INFINITY);
|
|
2129
|
+
if (!Number.isFinite(minScale) || minScale <= 0)
|
|
2130
|
+
return canvasRadius;
|
|
2131
|
+
return canvasRadius / minScale;
|
|
2132
|
+
}
|
|
2133
|
+
function getMosaicImagePoint(fabric, image, canvasPoint, brushDiameterCanvasPx) {
|
|
2134
|
+
const width = Number(image.width) || 0;
|
|
2135
|
+
const height = Number(image.height) || 0;
|
|
2136
|
+
const brushDiameter = Number(brushDiameterCanvasPx);
|
|
2137
|
+
if (width <= 0 ||
|
|
2138
|
+
height <= 0 ||
|
|
2139
|
+
!Number.isFinite(canvasPoint.x) ||
|
|
2140
|
+
!Number.isFinite(canvasPoint.y) ||
|
|
2141
|
+
!Number.isFinite(brushDiameter) ||
|
|
2142
|
+
brushDiameter <= 0) {
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
const matrix = toMatrix2D(image.calcTransformMatrix());
|
|
2146
|
+
if (!matrix)
|
|
2147
|
+
return null;
|
|
2148
|
+
const inverse = invertMatrix(matrix);
|
|
2149
|
+
if (!inverse)
|
|
2150
|
+
return null;
|
|
2151
|
+
const localPoint = transformPoint(canvasPoint, inverse);
|
|
2152
|
+
const sourceX = localPoint.x + width / 2;
|
|
2153
|
+
const sourceY = localPoint.y + height / 2;
|
|
2154
|
+
if (sourceX < 0 || sourceY < 0 || sourceX > width || sourceY > height) {
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
return {
|
|
2158
|
+
sourceX,
|
|
2159
|
+
sourceY,
|
|
2160
|
+
sourceRadius: getSourceRadiusFromMatrix(matrix, brushDiameter / 2),
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
function normalizeBlockSize(value) {
|
|
2165
|
+
return Number.isFinite(value) && value > 0 ? Math.max(1, Math.floor(value)) : 1;
|
|
2166
|
+
}
|
|
2167
|
+
function isInsideCircle(x, y, centerX, centerY, radiusSquared) {
|
|
2168
|
+
const dx = x - centerX;
|
|
2169
|
+
const dy = y - centerY;
|
|
2170
|
+
return dx * dx + dy * dy <= radiusSquared;
|
|
2171
|
+
}
|
|
2172
|
+
function pixelOffset(width, x, y) {
|
|
2173
|
+
return (y * width + x) * 4;
|
|
2174
|
+
}
|
|
2175
|
+
function applyCircularMosaicToImageData(options) {
|
|
2176
|
+
var _a, _b, _c, _d;
|
|
2177
|
+
const { imageData } = options;
|
|
2178
|
+
const { width, height, data } = imageData;
|
|
2179
|
+
const centerX = Number(options.centerX);
|
|
2180
|
+
const centerY = Number(options.centerY);
|
|
2181
|
+
const radius = Number(options.radius);
|
|
2182
|
+
if (!Number.isFinite(centerX) ||
|
|
2183
|
+
!Number.isFinite(centerY) ||
|
|
2184
|
+
!Number.isFinite(radius) ||
|
|
2185
|
+
radius <= 0 ||
|
|
2186
|
+
width <= 0 ||
|
|
2187
|
+
height <= 0) {
|
|
2188
|
+
return false;
|
|
2189
|
+
}
|
|
2190
|
+
const blockSize = normalizeBlockSize(options.blockSize);
|
|
2191
|
+
const minX = Math.max(0, Math.floor(centerX - radius));
|
|
2192
|
+
const maxX = Math.min(width - 1, Math.ceil(centerX + radius));
|
|
2193
|
+
const minY = Math.max(0, Math.floor(centerY - radius));
|
|
2194
|
+
const maxY = Math.min(height - 1, Math.ceil(centerY + radius));
|
|
2195
|
+
if (minX > maxX || minY > maxY)
|
|
2196
|
+
return false;
|
|
2197
|
+
const radiusSquared = radius * radius;
|
|
2198
|
+
let processed = false;
|
|
2199
|
+
for (let blockY = minY; blockY <= maxY; blockY += blockSize) {
|
|
2200
|
+
for (let blockX = minX; blockX <= maxX; blockX += blockSize) {
|
|
2201
|
+
const blockMaxX = Math.min(maxX, blockX + blockSize - 1);
|
|
2202
|
+
const blockMaxY = Math.min(maxY, blockY + blockSize - 1);
|
|
2203
|
+
let sampleOffset = -1;
|
|
2204
|
+
for (let y = blockY; y <= blockMaxY && sampleOffset < 0; y += 1) {
|
|
2205
|
+
for (let x = blockX; x <= blockMaxX; x += 1) {
|
|
2206
|
+
if (!isInsideCircle(x, y, centerX, centerY, radiusSquared))
|
|
2207
|
+
continue;
|
|
2208
|
+
sampleOffset = pixelOffset(width, x, y);
|
|
2209
|
+
break;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
if (sampleOffset < 0)
|
|
2213
|
+
continue;
|
|
2214
|
+
const red = (_a = data[sampleOffset]) !== null && _a !== void 0 ? _a : 0;
|
|
2215
|
+
const green = (_b = data[sampleOffset + 1]) !== null && _b !== void 0 ? _b : 0;
|
|
2216
|
+
const blue = (_c = data[sampleOffset + 2]) !== null && _c !== void 0 ? _c : 0;
|
|
2217
|
+
const alpha = (_d = data[sampleOffset + 3]) !== null && _d !== void 0 ? _d : 0;
|
|
2218
|
+
for (let y = blockY; y <= blockMaxY; y += 1) {
|
|
2219
|
+
for (let x = blockX; x <= blockMaxX; x += 1) {
|
|
2220
|
+
if (!isInsideCircle(x, y, centerX, centerY, radiusSquared))
|
|
2221
|
+
continue;
|
|
2222
|
+
const offset = pixelOffset(width, x, y);
|
|
2223
|
+
data[offset] = red;
|
|
2224
|
+
data[offset + 1] = green;
|
|
2225
|
+
data[offset + 2] = blue;
|
|
2226
|
+
data[offset + 3] = alpha;
|
|
2227
|
+
processed = true;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
return processed;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
const MAX_PENDING_MOSAIC_POINTS = 4096;
|
|
2236
|
+
function getCanvasDocument$2(context) {
|
|
2237
|
+
var _a, _b, _c, _d, _e;
|
|
2238
|
+
const element = (_b = (_a = context.canvas).getElement) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
2239
|
+
return ((_e = (_c = element === null || element === void 0 ? void 0 : element.ownerDocument) !== null && _c !== void 0 ? _c : (_d = context.canvas.lowerCanvasEl) === null || _d === void 0 ? void 0 : _d.ownerDocument) !== null && _e !== void 0 ? _e : document);
|
|
2240
|
+
}
|
|
2241
|
+
function isFinitePoint(value) {
|
|
2242
|
+
const point = value;
|
|
2243
|
+
return (!!point &&
|
|
2244
|
+
typeof point.x === 'number' &&
|
|
2245
|
+
Number.isFinite(point.x) &&
|
|
2246
|
+
typeof point.y === 'number' &&
|
|
2247
|
+
Number.isFinite(point.y));
|
|
2248
|
+
}
|
|
2249
|
+
function getPointerFromFabricEvent(canvas, event) {
|
|
2250
|
+
const fabricEvent = event;
|
|
2251
|
+
if (isFinitePoint(fabricEvent.scenePoint))
|
|
2252
|
+
return fabricEvent.scenePoint;
|
|
2253
|
+
if (isFinitePoint(fabricEvent.pointer))
|
|
2254
|
+
return fabricEvent.pointer;
|
|
2255
|
+
if (isFinitePoint(fabricEvent.absolutePointer))
|
|
2256
|
+
return fabricEvent.absolutePointer;
|
|
2257
|
+
if (fabricEvent.e && typeof canvas.getPointer === 'function') {
|
|
2258
|
+
const pointer = canvas.getPointer(fabricEvent.e);
|
|
2259
|
+
if (isFinitePoint(pointer))
|
|
2260
|
+
return pointer;
|
|
2261
|
+
}
|
|
2262
|
+
return null;
|
|
2263
|
+
}
|
|
2264
|
+
function safeRender(canvas) {
|
|
2265
|
+
try {
|
|
2266
|
+
canvas.requestRenderAll();
|
|
2267
|
+
}
|
|
2268
|
+
catch {
|
|
2269
|
+
try {
|
|
2270
|
+
canvas.renderAll();
|
|
2271
|
+
}
|
|
2272
|
+
catch {
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
function createPreviewCircle(context) {
|
|
2277
|
+
var _a;
|
|
2278
|
+
const config = context.getMosaicConfig();
|
|
2279
|
+
const circle = new context.fabric.Circle({
|
|
2280
|
+
left: 0,
|
|
2281
|
+
top: 0,
|
|
2282
|
+
radius: config.brushSize / 2,
|
|
2283
|
+
originX: 'center',
|
|
2284
|
+
originY: 'center',
|
|
2285
|
+
fill: config.previewFill,
|
|
2286
|
+
stroke: config.previewStroke,
|
|
2287
|
+
strokeWidth: config.previewStrokeWidth,
|
|
2288
|
+
strokeDashArray: (_a = config.previewStrokeDashArray) !== null && _a !== void 0 ? _a : undefined,
|
|
2289
|
+
selectable: false,
|
|
2290
|
+
evented: false,
|
|
2291
|
+
excludeFromExport: true,
|
|
2292
|
+
objectCaching: false,
|
|
2293
|
+
visible: false,
|
|
2294
|
+
});
|
|
2295
|
+
circle.isMosaicPreview = true;
|
|
2296
|
+
return circle;
|
|
2297
|
+
}
|
|
2298
|
+
function ensurePreviewCircle(context, session) {
|
|
2299
|
+
var _a;
|
|
2300
|
+
const { canvas } = context;
|
|
2301
|
+
const circle = (_a = session.previewCircle) !== null && _a !== void 0 ? _a : createPreviewCircle(context);
|
|
2302
|
+
session.previewCircle = circle;
|
|
2303
|
+
if (!canvas.getObjects().includes(circle)) {
|
|
2304
|
+
canvas.add(circle);
|
|
2305
|
+
}
|
|
2306
|
+
canvas.bringObjectToFront(circle);
|
|
2307
|
+
updateMosaicPreview(context);
|
|
2308
|
+
return circle;
|
|
2309
|
+
}
|
|
2310
|
+
function removePreviewCircle(context, session) {
|
|
2311
|
+
const circle = session.previewCircle;
|
|
2312
|
+
if (!circle)
|
|
2313
|
+
return;
|
|
2314
|
+
try {
|
|
2315
|
+
context.canvas.remove(circle);
|
|
2316
|
+
}
|
|
2317
|
+
catch {
|
|
2318
|
+
}
|
|
2319
|
+
session.previewCircle = null;
|
|
2320
|
+
}
|
|
2321
|
+
function createPreviewImage(context, sourceImage, rasterCache) {
|
|
2322
|
+
const image = new context.fabric.FabricImage(rasterCache.offscreenCanvas, {
|
|
2323
|
+
selectable: false,
|
|
2324
|
+
evented: false,
|
|
2325
|
+
excludeFromExport: true,
|
|
2326
|
+
objectCaching: false,
|
|
2327
|
+
visible: true,
|
|
2328
|
+
});
|
|
2329
|
+
copyBaseImageProperties(image, sourceImage);
|
|
2330
|
+
image.set({
|
|
2331
|
+
selectable: false,
|
|
2332
|
+
evented: false,
|
|
2333
|
+
excludeFromExport: true,
|
|
2334
|
+
objectCaching: false,
|
|
2335
|
+
visible: true,
|
|
2336
|
+
});
|
|
2337
|
+
image.isMosaicPreview = true;
|
|
2338
|
+
return image;
|
|
2339
|
+
}
|
|
2340
|
+
function placePreviewImageAfterBase(context, previewImage, sourceImage) {
|
|
2341
|
+
var _a, _b;
|
|
2342
|
+
const sourceIndex = context.canvas.getObjects().indexOf(sourceImage);
|
|
2343
|
+
if (sourceIndex < 0)
|
|
2344
|
+
return;
|
|
2345
|
+
try {
|
|
2346
|
+
(_b = (_a = context.canvas).moveObjectTo) === null || _b === void 0 ? void 0 : _b.call(_a, previewImage, sourceIndex + 1);
|
|
2347
|
+
}
|
|
2348
|
+
catch {
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
function ensurePreviewImage(context, session, sourceImage) {
|
|
2352
|
+
var _a;
|
|
2353
|
+
const rasterCache = session.rasterCache;
|
|
2354
|
+
if (!rasterCache)
|
|
2355
|
+
return null;
|
|
2356
|
+
const previewImage = (_a = session.previewImage) !== null && _a !== void 0 ? _a : createPreviewImage(context, sourceImage, rasterCache);
|
|
2357
|
+
session.previewImage = previewImage;
|
|
2358
|
+
copyBaseImageProperties(previewImage, sourceImage);
|
|
2359
|
+
previewImage.set({
|
|
2360
|
+
selectable: false,
|
|
2361
|
+
evented: false,
|
|
2362
|
+
excludeFromExport: true,
|
|
2363
|
+
objectCaching: false,
|
|
2364
|
+
visible: true,
|
|
2365
|
+
});
|
|
2366
|
+
previewImage.dirty = true;
|
|
2367
|
+
if (!context.canvas.getObjects().includes(previewImage)) {
|
|
2368
|
+
context.canvas.add(previewImage);
|
|
2369
|
+
}
|
|
2370
|
+
placePreviewImageAfterBase(context, previewImage, sourceImage);
|
|
2371
|
+
const circle = session.previewCircle;
|
|
2372
|
+
if (circle && context.canvas.getObjects().includes(circle)) {
|
|
2373
|
+
context.canvas.bringObjectToFront(circle);
|
|
2374
|
+
}
|
|
2375
|
+
return previewImage;
|
|
2376
|
+
}
|
|
2377
|
+
function removePreviewImage(context, session) {
|
|
2378
|
+
const image = session.previewImage;
|
|
2379
|
+
if (!image)
|
|
2380
|
+
return;
|
|
2381
|
+
try {
|
|
2382
|
+
context.canvas.remove(image);
|
|
2383
|
+
}
|
|
2384
|
+
catch {
|
|
2385
|
+
}
|
|
2386
|
+
session.previewImage = null;
|
|
2387
|
+
}
|
|
2388
|
+
function hidePreview(context) {
|
|
2389
|
+
var _a;
|
|
2390
|
+
const circle = (_a = context.getMosaicSession()) === null || _a === void 0 ? void 0 : _a.previewCircle;
|
|
2391
|
+
if (!circle)
|
|
2392
|
+
return;
|
|
2393
|
+
circle.set({ visible: false });
|
|
2394
|
+
safeRender(context.canvas);
|
|
2395
|
+
}
|
|
2396
|
+
function movePreview(context, point) {
|
|
2397
|
+
const session = context.getMosaicSession();
|
|
2398
|
+
if (!session)
|
|
2399
|
+
return;
|
|
2400
|
+
const circle = ensurePreviewCircle(context, session);
|
|
2401
|
+
circle.set({ left: point.x, top: point.y, visible: true });
|
|
2402
|
+
safeRender(context.canvas);
|
|
2403
|
+
}
|
|
2404
|
+
function attachCanvasHandler(context, session, eventName, callback) {
|
|
2405
|
+
context.canvas.on(eventName, callback);
|
|
2406
|
+
session.handlers.push({ eventName, callback });
|
|
2407
|
+
}
|
|
2408
|
+
function detachCanvasHandlers(context, session) {
|
|
2409
|
+
for (const record of session.handlers) {
|
|
2410
|
+
try {
|
|
2411
|
+
context.canvas.off(record.eventName, record.callback);
|
|
2412
|
+
}
|
|
2413
|
+
catch {
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
session.handlers = [];
|
|
2417
|
+
}
|
|
2418
|
+
function restoreObjectStates(session) {
|
|
2419
|
+
for (const record of session.prevObjectStates) {
|
|
2420
|
+
try {
|
|
2421
|
+
record.object.set({ evented: record.evented, selectable: record.selectable });
|
|
2422
|
+
}
|
|
2423
|
+
catch {
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
session.prevObjectStates = [];
|
|
2427
|
+
}
|
|
2428
|
+
function getImageSource(image) {
|
|
2429
|
+
var _a;
|
|
2430
|
+
const imageWithSource = image;
|
|
2431
|
+
try {
|
|
2432
|
+
const src = (_a = imageWithSource.getSrc) === null || _a === void 0 ? void 0 : _a.call(imageWithSource);
|
|
2433
|
+
if (typeof src === 'string' && src.length > 0)
|
|
2434
|
+
return src;
|
|
2435
|
+
}
|
|
2436
|
+
catch {
|
|
2437
|
+
}
|
|
2438
|
+
return typeof imageWithSource.src === 'string' && imageWithSource.src.length > 0
|
|
2439
|
+
? imageWithSource.src
|
|
2440
|
+
: null;
|
|
2441
|
+
}
|
|
2442
|
+
function imageDimension(value) {
|
|
2443
|
+
const numeric = Number(value);
|
|
2444
|
+
return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : 0;
|
|
2445
|
+
}
|
|
2446
|
+
function decodeImageSource(ownerDocument, source) {
|
|
2447
|
+
return new Promise((resolve, reject) => {
|
|
2448
|
+
const imageElement = ownerDocument.createElement('img');
|
|
2449
|
+
const cleanup = () => {
|
|
2450
|
+
if (typeof imageElement.removeEventListener === 'function') {
|
|
2451
|
+
imageElement.removeEventListener('load', handleLoad);
|
|
2452
|
+
imageElement.removeEventListener('error', handleError);
|
|
2453
|
+
}
|
|
2454
|
+
else {
|
|
2455
|
+
imageElement.onload = null;
|
|
2456
|
+
imageElement.onerror = null;
|
|
2457
|
+
}
|
|
2458
|
+
};
|
|
2459
|
+
const handleLoad = () => {
|
|
2460
|
+
const width = imageDimension(imageElement.naturalWidth || imageElement.width);
|
|
2461
|
+
const height = imageDimension(imageElement.naturalHeight || imageElement.height);
|
|
2462
|
+
cleanup();
|
|
2463
|
+
if (width <= 0 || height <= 0) {
|
|
2464
|
+
reject(new Error('Mosaic image decode failed: source image has no dimensions.'));
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
resolve({ element: imageElement, width, height });
|
|
2468
|
+
};
|
|
2469
|
+
const handleError = (event) => {
|
|
2470
|
+
cleanup();
|
|
2471
|
+
const message = typeof event === 'string'
|
|
2472
|
+
? `Mosaic image decode failed: ${event}`
|
|
2473
|
+
: 'Mosaic image decode failed.';
|
|
2474
|
+
reject(new Error(message));
|
|
2475
|
+
};
|
|
2476
|
+
if (!source.startsWith('data:')) {
|
|
2477
|
+
imageElement.crossOrigin = 'anonymous';
|
|
2478
|
+
}
|
|
2479
|
+
if (typeof imageElement.addEventListener === 'function') {
|
|
2480
|
+
imageElement.addEventListener('load', handleLoad, { once: true });
|
|
2481
|
+
imageElement.addEventListener('error', handleError, { once: true });
|
|
2482
|
+
}
|
|
2483
|
+
else {
|
|
2484
|
+
imageElement.onload = handleLoad;
|
|
2485
|
+
imageElement.onerror = handleError;
|
|
2486
|
+
}
|
|
2487
|
+
imageElement.src = source;
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
function toSupportedMimeType(mimeType) {
|
|
2491
|
+
return mimeType === 'image/jpeg' || mimeType === 'image/png' || mimeType === 'image/webp'
|
|
2492
|
+
? mimeType
|
|
2493
|
+
: null;
|
|
2494
|
+
}
|
|
2495
|
+
function mimeToFormat(mimeType) {
|
|
2496
|
+
if (mimeType === 'image/jpeg')
|
|
2497
|
+
return 'jpeg';
|
|
2498
|
+
if (mimeType === 'image/webp')
|
|
2499
|
+
return 'webp';
|
|
2500
|
+
return 'png';
|
|
2501
|
+
}
|
|
2502
|
+
function resolveMosaicOutputFormat(context, source) {
|
|
2503
|
+
var _a, _b, _c, _d;
|
|
2504
|
+
const config = context.getMosaicConfig();
|
|
2505
|
+
const requested = config.outputFileType;
|
|
2506
|
+
const format = requested === 'source'
|
|
2507
|
+
? mimeToFormat((_b = (_a = context.getCurrentImageMimeType()) !== null && _a !== void 0 ? _a : toSupportedMimeType(detectSourceMimeType(source))) !== null && _b !== void 0 ? _b : 'image/png')
|
|
2508
|
+
: ((_c = tryNormalizeImageFormat(String(requested))) !== null && _c !== void 0 ? _c : 'png');
|
|
2509
|
+
const mimeType = mimeTypeFor(format);
|
|
2510
|
+
if (format === 'png')
|
|
2511
|
+
return { mimeType };
|
|
2512
|
+
return {
|
|
2513
|
+
mimeType,
|
|
2514
|
+
quality: (_d = config.outputQuality) !== null && _d !== void 0 ? _d : context.options.downsampleQuality,
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
async function createFabricImageFromDataUrl(context, dataUrl) {
|
|
2518
|
+
return await withTimeout(context.fabric.FabricImage.fromURL(dataUrl, { crossOrigin: 'anonymous' }), context.options.imageLoadTimeoutMs, 'Mosaic FabricImage.fromURL');
|
|
2519
|
+
}
|
|
2520
|
+
function copyBaseImageProperties(target, source) {
|
|
2521
|
+
target.set({
|
|
2522
|
+
left: source.left,
|
|
2523
|
+
top: source.top,
|
|
2524
|
+
scaleX: source.scaleX,
|
|
2525
|
+
scaleY: source.scaleY,
|
|
2526
|
+
angle: source.angle,
|
|
2527
|
+
skewX: source.skewX,
|
|
2528
|
+
skewY: source.skewY,
|
|
2529
|
+
flipX: source.flipX,
|
|
2530
|
+
flipY: source.flipY,
|
|
2531
|
+
originX: source.originX,
|
|
2532
|
+
originY: source.originY,
|
|
2533
|
+
selectable: source.selectable,
|
|
2534
|
+
evented: source.evented,
|
|
2535
|
+
hasControls: source.hasControls,
|
|
2536
|
+
hoverCursor: source.hoverCursor,
|
|
2537
|
+
});
|
|
2538
|
+
target.setCoords();
|
|
2539
|
+
}
|
|
2540
|
+
function replaceBaseImage(context, oldImage, newImage, mimeType) {
|
|
2541
|
+
const { canvas } = context;
|
|
2542
|
+
let oldRemoved = false;
|
|
2543
|
+
let newAdded = false;
|
|
2544
|
+
try {
|
|
2545
|
+
copyBaseImageProperties(newImage, oldImage);
|
|
2546
|
+
canvas.remove(oldImage);
|
|
2547
|
+
oldRemoved = true;
|
|
2548
|
+
canvas.add(newImage);
|
|
2549
|
+
newAdded = true;
|
|
2550
|
+
canvas.sendObjectToBack(newImage);
|
|
2551
|
+
context.setOriginalImage(newImage);
|
|
2552
|
+
context.setCurrentImageMimeType(mimeType);
|
|
2553
|
+
canvas.renderAll();
|
|
2554
|
+
}
|
|
2555
|
+
catch (error) {
|
|
2556
|
+
try {
|
|
2557
|
+
if (newAdded)
|
|
2558
|
+
canvas.remove(newImage);
|
|
2559
|
+
if (oldRemoved && !canvas.getObjects().includes(oldImage)) {
|
|
2560
|
+
canvas.add(oldImage);
|
|
2561
|
+
canvas.sendObjectToBack(oldImage);
|
|
2562
|
+
}
|
|
2563
|
+
context.setOriginalImage(oldImage);
|
|
2564
|
+
}
|
|
2565
|
+
catch {
|
|
2566
|
+
}
|
|
2567
|
+
throw error;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
function pushMosaicHistory(context, after) {
|
|
2571
|
+
var _a;
|
|
2572
|
+
const before = (_a = context.getLastSnapshot()) !== null && _a !== void 0 ? _a : after;
|
|
2573
|
+
if (!before || !after || before === after)
|
|
2574
|
+
return;
|
|
2575
|
+
context.historyManager.push(new Command(async () => {
|
|
2576
|
+
await context.loadFromState(after);
|
|
2577
|
+
}, async () => {
|
|
2578
|
+
await context.loadFromState(before);
|
|
2579
|
+
}));
|
|
2580
|
+
context.setLastSnapshot(after);
|
|
2581
|
+
}
|
|
2582
|
+
async function getOrCreateRasterCache(context, session, source) {
|
|
2583
|
+
if (session.rasterCache)
|
|
2584
|
+
return session.rasterCache;
|
|
2585
|
+
const ownerDocument = getCanvasDocument$2(context);
|
|
2586
|
+
const decoded = await decodeImageSource(ownerDocument, source);
|
|
2587
|
+
const offscreenCanvas = ownerDocument.createElement('canvas');
|
|
2588
|
+
offscreenCanvas.width = decoded.width;
|
|
2589
|
+
offscreenCanvas.height = decoded.height;
|
|
2590
|
+
const renderingContext = offscreenCanvas.getContext('2d');
|
|
2591
|
+
if (!renderingContext) {
|
|
2592
|
+
reportError(context.options, new Error('Mosaic could not obtain a 2D canvas context.'), 'Mosaic apply failed.');
|
|
2593
|
+
return null;
|
|
2594
|
+
}
|
|
2595
|
+
renderingContext.drawImage(decoded.element, 0, 0, decoded.width, decoded.height);
|
|
2596
|
+
let imageData;
|
|
2597
|
+
try {
|
|
2598
|
+
imageData = renderingContext.getImageData(0, 0, decoded.width, decoded.height);
|
|
2599
|
+
}
|
|
2600
|
+
catch (error) {
|
|
2601
|
+
reportError(context.options, error, 'Mosaic apply failed because the source image pixels could not be read.');
|
|
2602
|
+
return null;
|
|
2603
|
+
}
|
|
2604
|
+
const rasterCache = {
|
|
2605
|
+
offscreenCanvas,
|
|
2606
|
+
renderingContext,
|
|
2607
|
+
imageData,
|
|
2608
|
+
source,
|
|
2609
|
+
width: decoded.width,
|
|
2610
|
+
height: decoded.height,
|
|
2611
|
+
};
|
|
2612
|
+
session.rasterCache = rasterCache;
|
|
2613
|
+
return rasterCache;
|
|
2614
|
+
}
|
|
2615
|
+
function applyMosaicImagePoint(context, session, sourceImage, imagePoint) {
|
|
2616
|
+
const rasterCache = session.rasterCache;
|
|
2617
|
+
if (!rasterCache)
|
|
2618
|
+
return false;
|
|
2619
|
+
const config = context.getMosaicConfig();
|
|
2620
|
+
const previousPoint = session.lastImagePoint;
|
|
2621
|
+
const points = previousPoint
|
|
2622
|
+
? interpolateMosaicPoints(previousPoint, imagePoint)
|
|
2623
|
+
: [imagePoint];
|
|
2624
|
+
let changed = false;
|
|
2625
|
+
for (const point of points) {
|
|
2626
|
+
changed =
|
|
2627
|
+
applyCircularMosaicToImageData({
|
|
2628
|
+
imageData: rasterCache.imageData,
|
|
2629
|
+
centerX: point.sourceX,
|
|
2630
|
+
centerY: point.sourceY,
|
|
2631
|
+
radius: point.sourceRadius,
|
|
2632
|
+
blockSize: config.blockSize,
|
|
2633
|
+
}) || changed;
|
|
2634
|
+
}
|
|
2635
|
+
session.lastImagePoint = imagePoint;
|
|
2636
|
+
if (changed) {
|
|
2637
|
+
session.hasUncommittedChanges = true;
|
|
2638
|
+
rasterCache.renderingContext.putImageData(rasterCache.imageData, 0, 0);
|
|
2639
|
+
ensurePreviewImage(context, session, sourceImage);
|
|
2640
|
+
safeRender(context.canvas);
|
|
2641
|
+
}
|
|
2642
|
+
return changed;
|
|
2643
|
+
}
|
|
2644
|
+
function interpolateMosaicPoints(start, end) {
|
|
2645
|
+
const dx = end.sourceX - start.sourceX;
|
|
2646
|
+
const dy = end.sourceY - start.sourceY;
|
|
2647
|
+
const distance = Math.hypot(dx, dy);
|
|
2648
|
+
const minRadius = Math.min(start.sourceRadius, end.sourceRadius);
|
|
2649
|
+
const spacing = Math.max(1, minRadius / 2);
|
|
2650
|
+
const steps = Math.max(1, Math.ceil(distance / spacing));
|
|
2651
|
+
const points = [];
|
|
2652
|
+
for (let index = 1; index <= steps; index += 1) {
|
|
2653
|
+
const t = index / steps;
|
|
2654
|
+
points.push({
|
|
2655
|
+
sourceX: start.sourceX + dx * t,
|
|
2656
|
+
sourceY: start.sourceY + dy * t,
|
|
2657
|
+
sourceRadius: start.sourceRadius + (end.sourceRadius - start.sourceRadius) * t,
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
return points;
|
|
2661
|
+
}
|
|
2662
|
+
async function applyMosaicPointToCache(context, expectedSession, canvasPoint) {
|
|
2663
|
+
const session = context.getMosaicSession();
|
|
2664
|
+
if (!session || session !== expectedSession)
|
|
2665
|
+
return;
|
|
2666
|
+
const originalImage = context.getOriginalImage();
|
|
2667
|
+
if (!originalImage || !context.isImageLoaded())
|
|
2668
|
+
return;
|
|
2669
|
+
const config = context.getMosaicConfig();
|
|
2670
|
+
const imagePoint = getMosaicImagePoint(context.fabric, originalImage, canvasPoint, config.brushSize);
|
|
2671
|
+
if (!imagePoint) {
|
|
2672
|
+
session.lastImagePoint = null;
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
const source = getImageSource(originalImage);
|
|
2676
|
+
if (!source) {
|
|
2677
|
+
reportWarning(context.options, new Error('Mosaic cannot read the current image source.'), 'Mosaic skipped because the image source is unavailable.');
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
const rasterCache = await getOrCreateRasterCache(context, session, source);
|
|
2681
|
+
if (!rasterCache)
|
|
2682
|
+
return;
|
|
2683
|
+
applyMosaicImagePoint(context, session, originalImage, imagePoint);
|
|
2684
|
+
}
|
|
2685
|
+
async function commitMosaicChanges(context, session, callbackContext) {
|
|
2686
|
+
var _a;
|
|
2687
|
+
session.commitRequested = false;
|
|
2688
|
+
session.lastImagePoint = null;
|
|
2689
|
+
if (!session.hasUncommittedChanges || !session.rasterCache)
|
|
2690
|
+
return;
|
|
2691
|
+
const originalImage = context.getOriginalImage();
|
|
2692
|
+
if (!originalImage || !context.isImageLoaded())
|
|
2693
|
+
return;
|
|
2694
|
+
const source = (_a = getImageSource(originalImage)) !== null && _a !== void 0 ? _a : session.rasterCache.source;
|
|
2695
|
+
const rasterCache = session.rasterCache;
|
|
2696
|
+
rasterCache.renderingContext.putImageData(rasterCache.imageData, 0, 0);
|
|
2697
|
+
const output = resolveMosaicOutputFormat(context, source);
|
|
2698
|
+
const nextDataUrl = output.quality === undefined
|
|
2699
|
+
? rasterCache.offscreenCanvas.toDataURL(output.mimeType)
|
|
2700
|
+
: rasterCache.offscreenCanvas.toDataURL(output.mimeType, output.quality);
|
|
2701
|
+
const nextImage = await createFabricImageFromDataUrl(context, nextDataUrl);
|
|
2702
|
+
removePreviewCircle(context, session);
|
|
2703
|
+
removePreviewImage(context, session);
|
|
2704
|
+
try {
|
|
2705
|
+
replaceBaseImage(context, originalImage, nextImage, output.mimeType);
|
|
2706
|
+
const after = context.captureSnapshot();
|
|
2707
|
+
pushMosaicHistory(context, after);
|
|
2708
|
+
rasterCache.source = nextDataUrl;
|
|
2709
|
+
session.hasUncommittedChanges = false;
|
|
2710
|
+
}
|
|
2711
|
+
finally {
|
|
2712
|
+
if (context.getMosaicSession() === session) {
|
|
2713
|
+
ensurePreviewCircle(context, session);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
context.updateInputs();
|
|
2717
|
+
context.updateUi();
|
|
2718
|
+
context.emitImageChanged(callbackContext);
|
|
2719
|
+
}
|
|
2720
|
+
async function drainMosaicQueue(context, expectedSession) {
|
|
2721
|
+
const session = context.getMosaicSession();
|
|
2722
|
+
if (!session || session !== expectedSession || session.isApplying)
|
|
2723
|
+
return;
|
|
2724
|
+
session.isApplying = true;
|
|
2725
|
+
const callbackContext = context.buildCallbackContext('applyMosaic', false);
|
|
2726
|
+
context.emitBusyChangeIfChanged(callbackContext);
|
|
2727
|
+
context.updateUi();
|
|
2728
|
+
try {
|
|
2729
|
+
while (context.getMosaicSession() === session && session.pendingCanvasPoints.length > 0) {
|
|
2730
|
+
const point = session.pendingCanvasPoints.shift();
|
|
2731
|
+
if (point) {
|
|
2732
|
+
await applyMosaicPointToCache(context, session, point);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
if (context.getMosaicSession() === session && session.commitRequested) {
|
|
2736
|
+
await commitMosaicChanges(context, session, callbackContext);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
finally {
|
|
2740
|
+
if (context.getMosaicSession() === session) {
|
|
2741
|
+
session.isApplying = false;
|
|
2742
|
+
}
|
|
2743
|
+
context.emitBusyChangeIfChanged(callbackContext);
|
|
2744
|
+
context.updateUi();
|
|
2745
|
+
if (context.getMosaicSession() === session &&
|
|
2746
|
+
(session.pendingCanvasPoints.length > 0 || session.commitRequested)) {
|
|
2747
|
+
void drainMosaicQueue(context, session).catch((error) => {
|
|
2748
|
+
reportError(context.options, error, 'Mosaic apply failed.');
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
function enqueueMosaicPoint(context, canvasPoint) {
|
|
2754
|
+
const session = context.getMosaicSession();
|
|
2755
|
+
if (!session)
|
|
2756
|
+
return;
|
|
2757
|
+
session.pendingCanvasPoints.push(canvasPoint);
|
|
2758
|
+
if (session.pendingCanvasPoints.length > MAX_PENDING_MOSAIC_POINTS) {
|
|
2759
|
+
session.pendingCanvasPoints.splice(0, session.pendingCanvasPoints.length - MAX_PENDING_MOSAIC_POINTS);
|
|
2760
|
+
}
|
|
2761
|
+
void drainMosaicQueue(context, session).catch((error) => {
|
|
2762
|
+
reportError(context.options, error, 'Mosaic apply failed.');
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
function requestMosaicCommit(context, session) {
|
|
2766
|
+
session.commitRequested = true;
|
|
2767
|
+
void drainMosaicQueue(context, session).catch((error) => {
|
|
2768
|
+
reportError(context.options, error, 'Mosaic apply failed.');
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
function installMosaicHandlers(context, session) {
|
|
2772
|
+
attachCanvasHandler(context, session, 'mouse:move', (event) => {
|
|
2773
|
+
const pointer = getPointerFromFabricEvent(context.canvas, event);
|
|
2774
|
+
if (!pointer) {
|
|
2775
|
+
hidePreview(context);
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
movePreview(context, pointer);
|
|
2779
|
+
const currentSession = context.getMosaicSession();
|
|
2780
|
+
if (currentSession === null || currentSession === void 0 ? void 0 : currentSession.isPointerDown) {
|
|
2781
|
+
enqueueMosaicPoint(context, pointer);
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
attachCanvasHandler(context, session, 'mouse:out', () => {
|
|
2785
|
+
hidePreview(context);
|
|
2786
|
+
const currentSession = context.getMosaicSession();
|
|
2787
|
+
if (currentSession === null || currentSession === void 0 ? void 0 : currentSession.isPointerDown) {
|
|
2788
|
+
currentSession.isPointerDown = false;
|
|
2789
|
+
requestMosaicCommit(context, currentSession);
|
|
2790
|
+
}
|
|
2791
|
+
});
|
|
2792
|
+
attachCanvasHandler(context, session, 'mouse:down', (event) => {
|
|
2793
|
+
const pointer = getPointerFromFabricEvent(context.canvas, event);
|
|
2794
|
+
if (!pointer)
|
|
2795
|
+
return;
|
|
2796
|
+
const currentSession = context.getMosaicSession();
|
|
2797
|
+
if (!currentSession)
|
|
2798
|
+
return;
|
|
2799
|
+
currentSession.isPointerDown = true;
|
|
2800
|
+
currentSession.lastImagePoint = null;
|
|
2801
|
+
enqueueMosaicPoint(context, pointer);
|
|
2802
|
+
});
|
|
2803
|
+
attachCanvasHandler(context, session, 'mouse:up', (event) => {
|
|
2804
|
+
const currentSession = context.getMosaicSession();
|
|
2805
|
+
if (!currentSession)
|
|
2806
|
+
return;
|
|
2807
|
+
const pointer = getPointerFromFabricEvent(context.canvas, event);
|
|
2808
|
+
if (pointer) {
|
|
2809
|
+
movePreview(context, pointer);
|
|
2810
|
+
enqueueMosaicPoint(context, pointer);
|
|
2811
|
+
}
|
|
2812
|
+
currentSession.isPointerDown = false;
|
|
2813
|
+
requestMosaicCommit(context, currentSession);
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
function enterMosaicMode(context) {
|
|
2817
|
+
if (context.getMosaicSession())
|
|
2818
|
+
return;
|
|
2819
|
+
if (!context.isImageLoaded() || !context.getOriginalImage())
|
|
2820
|
+
return;
|
|
2821
|
+
const { canvas } = context;
|
|
2822
|
+
context.hideAllMaskLabels();
|
|
2823
|
+
canvas.discardActiveObject();
|
|
2824
|
+
const prevSelection = !!canvas.selection;
|
|
2825
|
+
const prevDefaultCursor = canvas.defaultCursor;
|
|
2826
|
+
const prevObjectStates = canvas.getObjects().map((object) => {
|
|
2827
|
+
var _a, _b;
|
|
2828
|
+
return ({
|
|
2829
|
+
object,
|
|
2830
|
+
evented: (_a = object.evented) !== null && _a !== void 0 ? _a : true,
|
|
2831
|
+
selectable: (_b = object.selectable) !== null && _b !== void 0 ? _b : true,
|
|
2832
|
+
});
|
|
2833
|
+
});
|
|
2834
|
+
for (const record of prevObjectStates) {
|
|
2835
|
+
try {
|
|
2836
|
+
record.object.set({ evented: false, selectable: false });
|
|
2837
|
+
}
|
|
2838
|
+
catch {
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
canvas.selection = false;
|
|
2842
|
+
canvas.defaultCursor = 'crosshair';
|
|
2843
|
+
const session = {
|
|
2844
|
+
previewCircle: null,
|
|
2845
|
+
previewImage: null,
|
|
2846
|
+
prevSelection,
|
|
2847
|
+
prevDefaultCursor,
|
|
2848
|
+
prevObjectStates,
|
|
2849
|
+
handlers: [],
|
|
2850
|
+
rasterCache: null,
|
|
2851
|
+
pendingCanvasPoints: [],
|
|
2852
|
+
isPointerDown: false,
|
|
2853
|
+
isApplying: false,
|
|
2854
|
+
commitRequested: false,
|
|
2855
|
+
hasUncommittedChanges: false,
|
|
2856
|
+
lastImagePoint: null,
|
|
2857
|
+
};
|
|
2858
|
+
context.setMosaicSession(session);
|
|
2859
|
+
ensurePreviewCircle(context, session);
|
|
2860
|
+
installMosaicHandlers(context, session);
|
|
2861
|
+
canvas.renderAll();
|
|
2862
|
+
}
|
|
2863
|
+
function exitMosaicMode(context) {
|
|
2864
|
+
var _a;
|
|
2865
|
+
const session = context.getMosaicSession();
|
|
2866
|
+
if (!session)
|
|
2867
|
+
return;
|
|
2868
|
+
detachCanvasHandlers(context, session);
|
|
2869
|
+
removePreviewCircle(context, session);
|
|
2870
|
+
removePreviewImage(context, session);
|
|
2871
|
+
restoreObjectStates(session);
|
|
2872
|
+
context.canvas.selection = !!session.prevSelection;
|
|
2873
|
+
context.canvas.defaultCursor = (_a = session.prevDefaultCursor) !== null && _a !== void 0 ? _a : 'default';
|
|
2874
|
+
context.setMosaicSession(null);
|
|
2875
|
+
context.canvas.renderAll();
|
|
2876
|
+
}
|
|
2877
|
+
function updateMosaicPreview(context) {
|
|
2878
|
+
var _a;
|
|
2879
|
+
const session = context.getMosaicSession();
|
|
2880
|
+
const circle = session === null || session === void 0 ? void 0 : session.previewCircle;
|
|
2881
|
+
if (!session || !circle)
|
|
2882
|
+
return;
|
|
2883
|
+
const config = context.getMosaicConfig();
|
|
2884
|
+
circle.set({
|
|
2885
|
+
radius: config.brushSize / 2,
|
|
2886
|
+
fill: config.previewFill,
|
|
2887
|
+
stroke: config.previewStroke,
|
|
2888
|
+
strokeWidth: config.previewStrokeWidth,
|
|
2889
|
+
strokeDashArray: (_a = config.previewStrokeDashArray) !== null && _a !== void 0 ? _a : undefined,
|
|
2890
|
+
});
|
|
2891
|
+
context.canvas.bringObjectToFront(circle);
|
|
2892
|
+
safeRender(context.canvas);
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
function resolveMultiplier(requested, fallback) {
|
|
2896
|
+
const num = Number(requested);
|
|
2897
|
+
if (Number.isFinite(num) && num > 0)
|
|
2898
|
+
return num;
|
|
2899
|
+
const fallbackValue = Number(fallback);
|
|
2900
|
+
return Number.isFinite(fallbackValue) && fallbackValue > 0 ? fallbackValue : 1;
|
|
2901
|
+
}
|
|
2902
|
+
function resolveExportArea(requested, fallback) {
|
|
2903
|
+
if (requested === 'canvas' || requested === 'image')
|
|
2904
|
+
return requested;
|
|
2905
|
+
return fallback === 'canvas' ? 'canvas' : 'image';
|
|
2906
|
+
}
|
|
2907
|
+
function resolveExportOptions(context, options) {
|
|
2908
|
+
const providedOptions = options !== null && options !== void 0 ? options : {};
|
|
2909
|
+
return {
|
|
2910
|
+
exportArea: resolveExportArea(providedOptions.exportArea, context.options.exportAreaByDefault),
|
|
2911
|
+
mergeMask: typeof providedOptions.mergeMask === 'boolean'
|
|
2912
|
+
? providedOptions.mergeMask
|
|
2913
|
+
: context.options.mergeMaskByDefault,
|
|
2914
|
+
multiplier: resolveMultiplier(providedOptions.multiplier, context.options.exportMultiplier),
|
|
2915
|
+
format: resolveExportFormat(providedOptions, context.options.downsampleQuality),
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
function readCanvasDimension(canvas, getterName, propertyName) {
|
|
2919
|
+
const canvasLike = canvas;
|
|
2920
|
+
const getter = canvasLike[getterName];
|
|
2921
|
+
const value = typeof getter === 'function' ? getter.call(canvasLike) : canvasLike[propertyName];
|
|
2922
|
+
return Math.max(1, Math.ceil(Number.isFinite(value) ? Number(value) : 1));
|
|
2923
|
+
}
|
|
2924
|
+
function assertExportPixelBudget(context, multiplier, region) {
|
|
2925
|
+
var _a, _b;
|
|
2926
|
+
const sourceWidth = (_a = region === null || region === void 0 ? void 0 : region.width) !== null && _a !== void 0 ? _a : readCanvasDimension(context.canvas, 'getWidth', 'width');
|
|
2927
|
+
const sourceHeight = (_b = region === null || region === void 0 ? void 0 : region.height) !== null && _b !== void 0 ? _b : readCanvasDimension(context.canvas, 'getHeight', 'height');
|
|
2928
|
+
const outputWidth = Math.max(1, Math.ceil(sourceWidth * multiplier));
|
|
2929
|
+
const outputHeight = Math.max(1, Math.ceil(sourceHeight * multiplier));
|
|
2930
|
+
const pixelCount = outputWidth * outputHeight;
|
|
2931
|
+
const maxPixels = context.options.maxExportPixels;
|
|
2932
|
+
if (!Number.isFinite(pixelCount) || pixelCount > maxPixels) {
|
|
2933
|
+
throw new RangeError(`[ImageEditor] Export size ${outputWidth}x${outputHeight} ` +
|
|
2934
|
+
`(${pixelCount} pixels) exceeds maxExportPixels (${maxPixels}).`);
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
function computeExportRegion(context, exportArea) {
|
|
2938
|
+
if (exportArea === 'canvas')
|
|
2939
|
+
return { region: null, partialEdges: null };
|
|
2940
|
+
const originalImage = context.getOriginalImage();
|
|
2941
|
+
if (!originalImage)
|
|
2942
|
+
return { region: null, partialEdges: null };
|
|
2943
|
+
const bounds = getObjectBBox(originalImage);
|
|
2944
|
+
const canvasLike = context.canvas;
|
|
2945
|
+
const canvasWidth = typeof canvasLike.getWidth === 'function' ? canvasLike.getWidth() : canvasLike.width;
|
|
2946
|
+
const canvasHeight = typeof canvasLike.getHeight === 'function' ? canvasLike.getHeight() : canvasLike.height;
|
|
2947
|
+
if (!hasMeaningfulCanvasRegion(bounds, canvasWidth, canvasHeight)) {
|
|
2948
|
+
throw new ExportError('exportImageBase64 failed: image export region is empty.');
|
|
2949
|
+
}
|
|
2950
|
+
return {
|
|
1821
2951
|
region: getClampedCanvasRegion(bounds, canvasWidth, canvasHeight, {
|
|
1822
2952
|
includePartialPixels: true,
|
|
1823
2953
|
}),
|
|
@@ -2030,7 +3160,7 @@ function loadImageElement(dataUrl) {
|
|
|
2030
3160
|
imageElement.src = dataUrl;
|
|
2031
3161
|
});
|
|
2032
3162
|
}
|
|
2033
|
-
async function sealPartialTransparentEdges(dataUrl, edges) {
|
|
3163
|
+
async function sealPartialTransparentEdges(dataUrl, edges, target) {
|
|
2034
3164
|
if (!hasPartialEdges(edges))
|
|
2035
3165
|
return dataUrl;
|
|
2036
3166
|
const imageElement = await loadImageElement(dataUrl);
|
|
@@ -2078,7 +3208,9 @@ async function sealPartialTransparentEdges(dataUrl, edges) {
|
|
|
2078
3208
|
sealPixel(x, height - 1, x, height - 2);
|
|
2079
3209
|
}
|
|
2080
3210
|
canvasContext.putImageData(imageData, 0, 0);
|
|
2081
|
-
return
|
|
3211
|
+
return target.quality === undefined
|
|
3212
|
+
? offscreenCanvas.toDataURL(target.mimeType)
|
|
3213
|
+
: offscreenCanvas.toDataURL(target.mimeType, target.quality);
|
|
2082
3214
|
}
|
|
2083
3215
|
function getJpegBackgroundColor(backgroundColor) {
|
|
2084
3216
|
return resolveCanvasFillStyle(backgroundColor);
|
|
@@ -2115,6 +3247,11 @@ function createColorValidationContext() {
|
|
|
2115
3247
|
return null;
|
|
2116
3248
|
}
|
|
2117
3249
|
}
|
|
3250
|
+
function getCanvasDocument$1(canvas) {
|
|
3251
|
+
var _a, _b, _c, _d, _e;
|
|
3252
|
+
const canvasLike = canvas;
|
|
3253
|
+
return ((_e = (_c = (_b = (_a = canvasLike.getElement) === null || _a === void 0 ? void 0 : _a.call(canvasLike)) === null || _b === void 0 ? void 0 : _b.ownerDocument) !== null && _c !== void 0 ? _c : (_d = canvasLike.lowerCanvasEl) === null || _d === void 0 ? void 0 : _d.ownerDocument) !== null && _e !== void 0 ? _e : document);
|
|
3254
|
+
}
|
|
2118
3255
|
function isTransparentCssColor(value) {
|
|
2119
3256
|
const normalized = value.trim().toLowerCase();
|
|
2120
3257
|
if (normalized === 'transparent')
|
|
@@ -2158,12 +3295,11 @@ async function convertDataUrlToOpaqueJpeg(dataUrl, backgroundColor, quality) {
|
|
|
2158
3295
|
}
|
|
2159
3296
|
function dataUrlToBytes(dataUrl) {
|
|
2160
3297
|
var _a;
|
|
2161
|
-
const match = /^data:image\/[a-z0-9.+-]+;base64,([A-Za-z0-9
|
|
2162
|
-
|
|
3298
|
+
const match = /^data:image\/[a-z0-9.+-]+;base64,([A-Za-z0-9+/=]+)$/i.exec(dataUrl);
|
|
3299
|
+
const base64 = (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : '';
|
|
3300
|
+
if (!base64) {
|
|
2163
3301
|
throw new Error('exportImageFile received a malformed or empty image data URL.');
|
|
2164
3302
|
}
|
|
2165
|
-
const commaAt = dataUrl.indexOf(',');
|
|
2166
|
-
const base64 = dataUrl.slice(commaAt + 1).replace(/\s/g, '');
|
|
2167
3303
|
if (typeof globalThis.atob === 'function') {
|
|
2168
3304
|
const binary = globalThis.atob(base64);
|
|
2169
3305
|
const buffer = new ArrayBuffer(binary.length);
|
|
@@ -2222,7 +3358,10 @@ async function exportImageBase64(context, options) {
|
|
|
2222
3358
|
const renderQuality = renderFormat === 'png' ? undefined : resolved.format.quality;
|
|
2223
3359
|
let dataUrl = await withMaskExportState(context, resolved.mergeMask, async () => renderCanvasToDataUrl(context.canvas, renderFormat, renderQuality, resolved.multiplier, region));
|
|
2224
3360
|
if (region) {
|
|
2225
|
-
|
|
3361
|
+
const sealedFormat = resolved.format.format === 'jpeg'
|
|
3362
|
+
? { format: 'png', mimeType: 'image/png', quality: undefined }
|
|
3363
|
+
: resolved.format;
|
|
3364
|
+
dataUrl = await sealPartialTransparentEdges(dataUrl, partialEdges, sealedFormat);
|
|
2226
3365
|
if (resolved.format.format === 'jpeg') {
|
|
2227
3366
|
dataUrl = await convertDataUrlToOpaqueJpeg(dataUrl, context.options.backgroundColor, resolved.format.quality);
|
|
2228
3367
|
}
|
|
@@ -2278,15 +3417,17 @@ function downloadImage(context, fileName) {
|
|
|
2278
3417
|
.then((dataUrl) => {
|
|
2279
3418
|
if (!dataUrl)
|
|
2280
3419
|
return;
|
|
2281
|
-
const
|
|
3420
|
+
const ownerDocument = getCanvasDocument$1(context.canvas);
|
|
3421
|
+
const link = ownerDocument.createElement('a');
|
|
2282
3422
|
link.download = resolvedFileName;
|
|
2283
3423
|
link.href = dataUrl;
|
|
2284
|
-
|
|
3424
|
+
const body = ownerDocument.body;
|
|
3425
|
+
body.appendChild(link);
|
|
2285
3426
|
try {
|
|
2286
3427
|
link.click();
|
|
2287
3428
|
}
|
|
2288
3429
|
finally {
|
|
2289
|
-
|
|
3430
|
+
body.removeChild(link);
|
|
2290
3431
|
}
|
|
2291
3432
|
})
|
|
2292
3433
|
.catch((error) => {
|
|
@@ -2345,26 +3486,10 @@ async function mergeMasks(context) {
|
|
|
2345
3486
|
console.warn('[ImageEditor] mergeMasks: rollback failed', rollbackError);
|
|
2346
3487
|
}
|
|
2347
3488
|
if (error instanceof MergeMasksError)
|
|
2348
|
-
throw error;
|
|
2349
|
-
const message = error instanceof Error ? `mergeMasks failed: ${error.message}` : 'mergeMasks failed';
|
|
2350
|
-
throw new MergeMasksError(message, error);
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
function withTimeout(promise, ms, label) {
|
|
2355
|
-
return new Promise((resolve, reject) => {
|
|
2356
|
-
const start = Date.now();
|
|
2357
|
-
const timeoutId = setTimeout(() => {
|
|
2358
|
-
reject(new ImageLoadTimeoutError(label, Date.now() - start));
|
|
2359
|
-
}, ms);
|
|
2360
|
-
promise.then((value) => {
|
|
2361
|
-
clearTimeout(timeoutId);
|
|
2362
|
-
resolve(value);
|
|
2363
|
-
}, (err) => {
|
|
2364
|
-
clearTimeout(timeoutId);
|
|
2365
|
-
reject(err);
|
|
2366
|
-
});
|
|
2367
|
-
});
|
|
3489
|
+
throw error;
|
|
3490
|
+
const message = error instanceof Error ? `mergeMasks failed: ${error.message}` : 'mergeMasks failed';
|
|
3491
|
+
throw new MergeMasksError(message, error);
|
|
3492
|
+
}
|
|
2368
3493
|
}
|
|
2369
3494
|
|
|
2370
3495
|
function forceReflow(element) {
|
|
@@ -2373,26 +3498,8 @@ function forceReflow(element) {
|
|
|
2373
3498
|
void element.offsetWidth;
|
|
2374
3499
|
}
|
|
2375
3500
|
|
|
2376
|
-
function selectLayoutStrategy(
|
|
2377
|
-
|
|
2378
|
-
return 'fit';
|
|
2379
|
-
if (options.coverImageToCanvas)
|
|
2380
|
-
return 'cover';
|
|
2381
|
-
return 'expand';
|
|
2382
|
-
}
|
|
2383
|
-
function detectLayoutConflict(options) {
|
|
2384
|
-
if (!options.fitImageToCanvas || !options.coverImageToCanvas)
|
|
2385
|
-
return null;
|
|
2386
|
-
const enabled = ['fit', 'cover'];
|
|
2387
|
-
if (options.expandCanvasToImage)
|
|
2388
|
-
enabled.push('expand');
|
|
2389
|
-
const selected = selectLayoutStrategy(options);
|
|
2390
|
-
return {
|
|
2391
|
-
enabled,
|
|
2392
|
-
selected,
|
|
2393
|
-
message: `Layout flags ${enabled.map((s) => `\`${s}\``).join(', ')} are enabled simultaneously. ` +
|
|
2394
|
-
`Using precedence \`fit > cover > expand\`; selected \`${selected}\`.`,
|
|
2395
|
-
};
|
|
3501
|
+
function selectLayoutStrategy(mode) {
|
|
3502
|
+
return mode;
|
|
2396
3503
|
}
|
|
2397
3504
|
class ViewportCache {
|
|
2398
3505
|
constructor() {
|
|
@@ -2596,60 +3703,6 @@ function applyCanvasDimensions(canvas, width, height, containerElement) {
|
|
|
2596
3703
|
forceReflow(containerElement);
|
|
2597
3704
|
}
|
|
2598
3705
|
|
|
2599
|
-
function computeDownsampleDimensions(srcWidth, srcHeight, maxWidth, maxHeight) {
|
|
2600
|
-
if (!isPositiveFinite$1(srcWidth) ||
|
|
2601
|
-
!isPositiveFinite$1(srcHeight) ||
|
|
2602
|
-
!isPositiveFinite$1(maxWidth) ||
|
|
2603
|
-
!isPositiveFinite$1(maxHeight)) {
|
|
2604
|
-
return {
|
|
2605
|
-
width: Math.max(1, Math.round(srcWidth) || 1),
|
|
2606
|
-
height: Math.max(1, Math.round(srcHeight) || 1),
|
|
2607
|
-
needsResize: false,
|
|
2608
|
-
};
|
|
2609
|
-
}
|
|
2610
|
-
const needsResize = srcWidth > maxWidth || srcHeight > maxHeight;
|
|
2611
|
-
if (!needsResize) {
|
|
2612
|
-
return { width: srcWidth, height: srcHeight, needsResize: false };
|
|
2613
|
-
}
|
|
2614
|
-
const ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight);
|
|
2615
|
-
return {
|
|
2616
|
-
width: Math.max(1, Math.round(srcWidth * ratio)),
|
|
2617
|
-
height: Math.max(1, Math.round(srcHeight * ratio)),
|
|
2618
|
-
needsResize: true,
|
|
2619
|
-
};
|
|
2620
|
-
}
|
|
2621
|
-
function isPositiveFinite$1(value) {
|
|
2622
|
-
return Number.isFinite(value) && value > 0;
|
|
2623
|
-
}
|
|
2624
|
-
function selectDownsampleMimeType(sourceMime, preserveSourceFormat, downsampleMimeType) {
|
|
2625
|
-
if (downsampleMimeType)
|
|
2626
|
-
return downsampleMimeType;
|
|
2627
|
-
if (preserveSourceFormat && (sourceMime === 'image/png' || sourceMime === 'image/webp')) {
|
|
2628
|
-
return sourceMime;
|
|
2629
|
-
}
|
|
2630
|
-
return 'image/jpeg';
|
|
2631
|
-
}
|
|
2632
|
-
function detectSourceMimeType(dataUrl) {
|
|
2633
|
-
const match = /^data:(image\/[a-z0-9+\-.]+)\s*;/i.exec(dataUrl);
|
|
2634
|
-
return match ? match[1].toLowerCase() : null;
|
|
2635
|
-
}
|
|
2636
|
-
function resampleImage(imageElement, maxWidth, maxHeight, sourceMime, preserveSourceFormat, downsampleMimeType, quality) {
|
|
2637
|
-
const { width, height } = computeDownsampleDimensions(imageElement.naturalWidth, imageElement.naturalHeight, maxWidth, maxHeight);
|
|
2638
|
-
const mimeType = selectDownsampleMimeType(sourceMime, preserveSourceFormat, downsampleMimeType);
|
|
2639
|
-
const offscreenCanvas = document.createElement('canvas');
|
|
2640
|
-
offscreenCanvas.width = width;
|
|
2641
|
-
offscreenCanvas.height = height;
|
|
2642
|
-
const context = offscreenCanvas.getContext('2d');
|
|
2643
|
-
if (!context) {
|
|
2644
|
-
throw new DownsampleError('Failed to obtain a 2D context for downsampling.');
|
|
2645
|
-
}
|
|
2646
|
-
context.drawImage(imageElement, 0, 0, imageElement.naturalWidth, imageElement.naturalHeight, 0, 0, width, height);
|
|
2647
|
-
const dataUrl = mimeType === 'image/png'
|
|
2648
|
-
? offscreenCanvas.toDataURL(mimeType)
|
|
2649
|
-
: offscreenCanvas.toDataURL(mimeType, quality);
|
|
2650
|
-
return { dataUrl, width, height, mimeType };
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
3706
|
async function loadImage(context, imageBase64, loadOptions = {}) {
|
|
2654
3707
|
if (typeof imageBase64 !== 'string' || !imageBase64.startsWith('data:image/')) {
|
|
2655
3708
|
return;
|
|
@@ -2661,14 +3714,10 @@ async function loadImage(context, imageBase64, loadOptions = {}) {
|
|
|
2661
3714
|
const containerScrollLeft = context.containerElement
|
|
2662
3715
|
? context.containerElement.scrollLeft
|
|
2663
3716
|
: null;
|
|
2664
|
-
const containerOverflow = context.containerElement
|
|
2665
|
-
? context.containerElement.style.overflow
|
|
2666
|
-
: null;
|
|
2667
3717
|
const bundle = {
|
|
2668
3718
|
placeholderHidden,
|
|
2669
3719
|
containerScrollTop,
|
|
2670
3720
|
containerScrollLeft,
|
|
2671
|
-
containerOverflow,
|
|
2672
3721
|
originalImage: context.getOriginalImage(),
|
|
2673
3722
|
isImageLoadedToCanvas: context.getIsImageLoadedToCanvas(),
|
|
2674
3723
|
lastSnapshot: context.getLastSnapshot(),
|
|
@@ -2690,7 +3739,7 @@ async function loadImage(context, imageBase64, loadOptions = {}) {
|
|
|
2690
3739
|
decode.cleanup(true);
|
|
2691
3740
|
throw error;
|
|
2692
3741
|
}
|
|
2693
|
-
const loadSource = maybeDownsample(imageElement, imageBase64, context.options);
|
|
3742
|
+
const loadSource = maybeDownsample(imageElement, imageBase64, context.options, getCanvasDocument(context.canvas));
|
|
2694
3743
|
const fabricImage = await withTimeout(context.fabric.FabricImage.fromURL(loadSource.dataUrl, { crossOrigin: 'anonymous' }), context.options.imageLoadTimeoutMs, 'FabricImage.fromURL');
|
|
2695
3744
|
context.canvas.discardActiveObject();
|
|
2696
3745
|
context.canvas.clear();
|
|
@@ -2802,7 +3851,7 @@ function toSupportedImageMimeType(mimeType) {
|
|
|
2802
3851
|
? mimeType
|
|
2803
3852
|
: null;
|
|
2804
3853
|
}
|
|
2805
|
-
function maybeDownsample(imageElement, originalDataUrl, options) {
|
|
3854
|
+
function maybeDownsample(imageElement, originalDataUrl, options, ownerDocument) {
|
|
2806
3855
|
const originalMimeType = toSupportedImageMimeType(detectSourceMimeType(originalDataUrl));
|
|
2807
3856
|
if (!options.downsampleOnLoad) {
|
|
2808
3857
|
return { dataUrl: originalDataUrl, mimeType: originalMimeType };
|
|
@@ -2817,13 +3866,18 @@ function maybeDownsample(imageElement, originalDataUrl, options) {
|
|
|
2817
3866
|
return { dataUrl: originalDataUrl, mimeType: originalMimeType };
|
|
2818
3867
|
}
|
|
2819
3868
|
const sourceMime = detectSourceMimeType(originalDataUrl);
|
|
2820
|
-
const resampledImage = resampleImage(imageElement, options.downsampleMaxWidth, options.downsampleMaxHeight, sourceMime, options.preserveSourceFormat, options.downsampleMimeType, options.downsampleQuality);
|
|
3869
|
+
const resampledImage = resampleImage(imageElement, options.downsampleMaxWidth, options.downsampleMaxHeight, sourceMime, options.preserveSourceFormat, options.downsampleMimeType, options.downsampleQuality, ownerDocument);
|
|
2821
3870
|
const actualMimeType = toSupportedImageMimeType(detectSourceMimeType(resampledImage.dataUrl));
|
|
2822
3871
|
return {
|
|
2823
3872
|
dataUrl: resampledImage.dataUrl,
|
|
2824
3873
|
mimeType: actualMimeType !== null && actualMimeType !== void 0 ? actualMimeType : resampledImage.mimeType,
|
|
2825
3874
|
};
|
|
2826
3875
|
}
|
|
3876
|
+
function getCanvasDocument(canvas) {
|
|
3877
|
+
var _a, _b, _c, _d, _e;
|
|
3878
|
+
const canvasLike = canvas;
|
|
3879
|
+
return ((_e = (_c = (_b = (_a = canvasLike.getElement) === null || _a === void 0 ? void 0 : _a.call(canvasLike)) === null || _b === void 0 ? void 0 : _b.ownerDocument) !== null && _c !== void 0 ? _c : (_d = canvasLike.lowerCanvasEl) === null || _d === void 0 ? void 0 : _d.ownerDocument) !== null && _e !== void 0 ? _e : (typeof document !== 'undefined' ? document : undefined));
|
|
3880
|
+
}
|
|
2827
3881
|
function computeLayout(context, fabricImage) {
|
|
2828
3882
|
var _a, _b, _c, _d;
|
|
2829
3883
|
const imageWidth = (_a = fabricImage.width) !== null && _a !== void 0 ? _a : 0;
|
|
@@ -2833,7 +3887,7 @@ function computeLayout(context, fabricImage) {
|
|
|
2833
3887
|
width: context.options.canvasWidth,
|
|
2834
3888
|
height: context.options.canvasHeight,
|
|
2835
3889
|
}, scrollbarSize);
|
|
2836
|
-
const strategy = selectLayoutStrategy(context.options);
|
|
3890
|
+
const strategy = selectLayoutStrategy(context.options.layoutMode);
|
|
2837
3891
|
if (strategy === 'fit') {
|
|
2838
3892
|
return computeFitLayout(imageWidth, imageHeight, context.options.canvasWidth, context.options.canvasHeight, viewport);
|
|
2839
3893
|
}
|
|
@@ -2848,14 +3902,6 @@ function serializeCanvas(canvas) {
|
|
|
2848
3902
|
return JSON.stringify(json);
|
|
2849
3903
|
}
|
|
2850
3904
|
async function replayRollback(context, bundle) {
|
|
2851
|
-
if (context.containerElement && bundle.containerOverflow !== null) {
|
|
2852
|
-
try {
|
|
2853
|
-
context.containerElement.style.overflow = bundle.containerOverflow;
|
|
2854
|
-
}
|
|
2855
|
-
catch (rollbackError) {
|
|
2856
|
-
console.warn('[ImageEditor] rollback: overflow restore failed', rollbackError);
|
|
2857
|
-
}
|
|
2858
|
-
}
|
|
2859
3905
|
try {
|
|
2860
3906
|
await context.canvas.loadFromJSON(JSON.parse(bundle.canvasJson));
|
|
2861
3907
|
context.canvas.renderAll();
|
|
@@ -2889,16 +3935,56 @@ async function replayRollback(context, bundle) {
|
|
|
2889
3935
|
}
|
|
2890
3936
|
}
|
|
2891
3937
|
|
|
3938
|
+
const ANIMATION_SETTLE_GRACE_MS = 1000;
|
|
2892
3939
|
function animateProps(object, props, options, guard) {
|
|
2893
3940
|
return new Promise((resolve, reject) => {
|
|
2894
3941
|
const propCount = Object.keys(props).length;
|
|
2895
|
-
if (propCount === 0) {
|
|
3942
|
+
if (propCount === 0 || guard.isDisposed()) {
|
|
2896
3943
|
resolve();
|
|
2897
3944
|
return;
|
|
2898
3945
|
}
|
|
2899
3946
|
let completed = 0;
|
|
3947
|
+
let settled = false;
|
|
3948
|
+
let aborters = [];
|
|
3949
|
+
let timeoutId = null;
|
|
3950
|
+
let unregisterAborter = null;
|
|
3951
|
+
const cleanup = () => {
|
|
3952
|
+
if (timeoutId !== null) {
|
|
3953
|
+
clearTimeout(timeoutId);
|
|
3954
|
+
timeoutId = null;
|
|
3955
|
+
}
|
|
3956
|
+
unregisterAborter === null || unregisterAborter === void 0 ? void 0 : unregisterAborter();
|
|
3957
|
+
unregisterAborter = null;
|
|
3958
|
+
};
|
|
3959
|
+
const settle = () => {
|
|
3960
|
+
if (settled)
|
|
3961
|
+
return;
|
|
3962
|
+
settled = true;
|
|
3963
|
+
cleanup();
|
|
3964
|
+
resolve();
|
|
3965
|
+
};
|
|
3966
|
+
const fail = (error) => {
|
|
3967
|
+
if (settled)
|
|
3968
|
+
return;
|
|
3969
|
+
settled = true;
|
|
3970
|
+
cleanup();
|
|
3971
|
+
reject(error);
|
|
3972
|
+
};
|
|
3973
|
+
const abortAndSettle = () => {
|
|
3974
|
+
for (const abort of aborters) {
|
|
3975
|
+
try {
|
|
3976
|
+
abort();
|
|
3977
|
+
}
|
|
3978
|
+
catch {
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
settle();
|
|
3982
|
+
};
|
|
3983
|
+
const duration = Number.isFinite(options.duration) ? Math.max(0, options.duration) : 0;
|
|
3984
|
+
timeoutId = setTimeout(abortAndSettle, duration + ANIMATION_SETTLE_GRACE_MS);
|
|
3985
|
+
unregisterAborter = guard.registerAnimationAborter(abortAndSettle);
|
|
2900
3986
|
try {
|
|
2901
|
-
object.animate(props, {
|
|
3987
|
+
const animationResult = object.animate(props, {
|
|
2902
3988
|
duration: options.duration,
|
|
2903
3989
|
onChange: () => {
|
|
2904
3990
|
var _a;
|
|
@@ -2908,15 +3994,27 @@ function animateProps(object, props, options, guard) {
|
|
|
2908
3994
|
},
|
|
2909
3995
|
onComplete: () => {
|
|
2910
3996
|
if (++completed >= propCount)
|
|
2911
|
-
|
|
3997
|
+
settle();
|
|
2912
3998
|
},
|
|
2913
3999
|
});
|
|
4000
|
+
aborters = collectAnimationAborters(animationResult);
|
|
2914
4001
|
}
|
|
2915
4002
|
catch (error) {
|
|
2916
|
-
|
|
4003
|
+
fail(error);
|
|
2917
4004
|
}
|
|
2918
4005
|
});
|
|
2919
4006
|
}
|
|
4007
|
+
function collectAnimationAborters(animationResult) {
|
|
4008
|
+
const handles = Array.isArray(animationResult)
|
|
4009
|
+
? animationResult
|
|
4010
|
+
: animationResult && typeof animationResult === 'object'
|
|
4011
|
+
? Object.values(animationResult)
|
|
4012
|
+
: [animationResult];
|
|
4013
|
+
return handles.flatMap((handle) => {
|
|
4014
|
+
const abort = handle === null || handle === void 0 ? void 0 : handle.abort;
|
|
4015
|
+
return typeof abort === 'function' ? [() => abort.call(handle)] : [];
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
2920
4018
|
function restoreOrigin(object, originX, originY) {
|
|
2921
4019
|
try {
|
|
2922
4020
|
object.set({ originX, originY });
|
|
@@ -3082,10 +4180,8 @@ function coercePoint(pt) {
|
|
|
3082
4180
|
}
|
|
3083
4181
|
|
|
3084
4182
|
const POLYGON_AREA_EPSILON = 1e-6;
|
|
3085
|
-
let nextMaskUid = 0;
|
|
3086
4183
|
function createMaskUid(maskId) {
|
|
3087
|
-
|
|
3088
|
-
return `mask-${maskId}-${nextMaskUid}`;
|
|
4184
|
+
return `mask-${maskId}`;
|
|
3089
4185
|
}
|
|
3090
4186
|
function isFabricObjectLike(value) {
|
|
3091
4187
|
if (!value || typeof value !== 'object')
|
|
@@ -3093,6 +4189,26 @@ function isFabricObjectLike(value) {
|
|
|
3093
4189
|
const candidate = value;
|
|
3094
4190
|
return typeof candidate.set === 'function' && typeof candidate.on === 'function';
|
|
3095
4191
|
}
|
|
4192
|
+
function isStyleObject(value) {
|
|
4193
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
4194
|
+
}
|
|
4195
|
+
function mergeMaskConfig(defaultMaskConfig, config) {
|
|
4196
|
+
const safeDefaultConfig = { ...defaultMaskConfig };
|
|
4197
|
+
const defaultStyles = safeDefaultConfig.styles;
|
|
4198
|
+
delete safeDefaultConfig.onCreate;
|
|
4199
|
+
delete safeDefaultConfig.fabricGenerator;
|
|
4200
|
+
delete safeDefaultConfig.styles;
|
|
4201
|
+
const configStyles = isStyleObject(config.styles) ? config.styles : {};
|
|
4202
|
+
const safeDefaultStyles = isStyleObject(defaultStyles) ? defaultStyles : {};
|
|
4203
|
+
return {
|
|
4204
|
+
...safeDefaultConfig,
|
|
4205
|
+
...config,
|
|
4206
|
+
styles: {
|
|
4207
|
+
...safeDefaultStyles,
|
|
4208
|
+
...configStyles,
|
|
4209
|
+
},
|
|
4210
|
+
};
|
|
4211
|
+
}
|
|
3096
4212
|
function warnInvalidMask(options, reason) {
|
|
3097
4213
|
reportWarning(options, null, `createMask skipped: ${reason}.`);
|
|
3098
4214
|
}
|
|
@@ -3178,11 +4294,11 @@ function createMask(context, config = {}) {
|
|
|
3178
4294
|
const { canvas, options, fabric: fabricModule } = context;
|
|
3179
4295
|
if (!canvas)
|
|
3180
4296
|
return null;
|
|
3181
|
-
const
|
|
3182
|
-
|
|
4297
|
+
const mergedConfig = mergeMaskConfig(options.defaultMaskConfig, config);
|
|
4298
|
+
const shapeType = (_a = mergedConfig.shape) !== null && _a !== void 0 ? _a : 'rect';
|
|
4299
|
+
if (!validateNumericInputs(options, mergedConfig))
|
|
3183
4300
|
return null;
|
|
3184
4301
|
const resolvedConfig = {
|
|
3185
|
-
shape: shapeType,
|
|
3186
4302
|
width: options.defaultMaskWidth,
|
|
3187
4303
|
height: options.defaultMaskHeight,
|
|
3188
4304
|
color: 'rgba(0,0,0,0.5)',
|
|
@@ -3192,13 +4308,14 @@ function createMask(context, config = {}) {
|
|
|
3192
4308
|
top: undefined,
|
|
3193
4309
|
angle: 0,
|
|
3194
4310
|
selectable: true,
|
|
3195
|
-
...
|
|
4311
|
+
...mergedConfig,
|
|
4312
|
+
shape: shapeType,
|
|
3196
4313
|
};
|
|
3197
4314
|
const firstOffset = 10;
|
|
3198
4315
|
let left;
|
|
3199
4316
|
let top;
|
|
3200
4317
|
const previousMask = context.getLastMask();
|
|
3201
|
-
if (
|
|
4318
|
+
if (mergedConfig.left === undefined && previousMask) {
|
|
3202
4319
|
const previousRight = ((_b = previousMask.left) !== null && _b !== void 0 ? _b : 0) +
|
|
3203
4320
|
(typeof previousMask.getScaledWidth === 'function'
|
|
3204
4321
|
? previousMask.getScaledWidth()
|
|
@@ -3207,17 +4324,21 @@ function createMask(context, config = {}) {
|
|
|
3207
4324
|
top = (_f = previousMask.top) !== null && _f !== void 0 ? _f : firstOffset;
|
|
3208
4325
|
}
|
|
3209
4326
|
else {
|
|
3210
|
-
left = resolveNumeric(
|
|
3211
|
-
top = resolveNumeric(
|
|
4327
|
+
left = resolveNumeric(mergedConfig.left, 'x', firstOffset, canvas, options);
|
|
4328
|
+
top = resolveNumeric(mergedConfig.top, 'y', firstOffset, canvas, options);
|
|
3212
4329
|
}
|
|
3213
|
-
resolvedConfig.width = resolveNumeric(
|
|
3214
|
-
resolvedConfig.height = resolveNumeric(
|
|
3215
|
-
const rx =
|
|
3216
|
-
|
|
4330
|
+
resolvedConfig.width = resolveNumeric(mergedConfig.width, 'x', options.defaultMaskWidth, canvas, options);
|
|
4331
|
+
resolvedConfig.height = resolveNumeric(mergedConfig.height, 'y', options.defaultMaskHeight, canvas, options);
|
|
4332
|
+
const rx = mergedConfig.rx !== undefined
|
|
4333
|
+
? resolveNumeric(mergedConfig.rx, 'x', 0, canvas, options)
|
|
4334
|
+
: undefined;
|
|
4335
|
+
const ry = mergedConfig.ry !== undefined
|
|
4336
|
+
? resolveNumeric(mergedConfig.ry, 'y', 0, canvas, options)
|
|
4337
|
+
: undefined;
|
|
3217
4338
|
const radius = shapeType === 'circle'
|
|
3218
|
-
? resolveNumeric(
|
|
4339
|
+
? resolveNumeric(mergedConfig.radius, 'x', Math.min(resolvedConfig.width, resolvedConfig.height) / 2, canvas, options)
|
|
3219
4340
|
: undefined;
|
|
3220
|
-
const polygonPoints = shapeType === 'polygon' ? resolvePolygonPoints(options,
|
|
4341
|
+
const polygonPoints = shapeType === 'polygon' ? resolvePolygonPoints(options, mergedConfig.points) : null;
|
|
3221
4342
|
if (!validateFiniteField(options, 'left', left) ||
|
|
3222
4343
|
!validateFiniteField(options, 'top', top) ||
|
|
3223
4344
|
!validatePositiveField(options, 'width', resolvedConfig.width) ||
|
|
@@ -3233,7 +4354,7 @@ function createMask(context, config = {}) {
|
|
|
3233
4354
|
(shapeType === 'polygon' && polygonPoints === null)) {
|
|
3234
4355
|
return null;
|
|
3235
4356
|
}
|
|
3236
|
-
if (options.
|
|
4357
|
+
if (options.layoutMode === 'expand') {
|
|
3237
4358
|
const requiredWidth = Math.ceil(left + resolvedConfig.width + 10);
|
|
3238
4359
|
const requiredHeight = Math.ceil(top + resolvedConfig.height + 10);
|
|
3239
4360
|
const nextWidth = Math.max(canvas.getWidth(), requiredWidth);
|
|
@@ -3248,8 +4369,8 @@ function createMask(context, config = {}) {
|
|
|
3248
4369
|
}
|
|
3249
4370
|
}
|
|
3250
4371
|
let mask;
|
|
3251
|
-
if (typeof
|
|
3252
|
-
const generated =
|
|
4372
|
+
if (typeof config.fabricGenerator === 'function') {
|
|
4373
|
+
const generated = config.fabricGenerator(resolvedConfig, canvas, options);
|
|
3253
4374
|
if (!isFabricObjectLike(generated)) {
|
|
3254
4375
|
reportWarning(options, generated, 'createMask skipped: fabricGenerator did not return a Fabric object.');
|
|
3255
4376
|
return null;
|
|
@@ -3325,16 +4446,17 @@ function createMask(context, config = {}) {
|
|
|
3325
4446
|
}
|
|
3326
4447
|
}
|
|
3327
4448
|
const maskObject = mask;
|
|
3328
|
-
maskObject.selectable = 'selectable' in
|
|
3329
|
-
maskObject.evented = 'evented' in
|
|
3330
|
-
maskObject.hasControls = 'hasControls' in
|
|
4449
|
+
maskObject.selectable = 'selectable' in mergedConfig ? !!mergedConfig.selectable : true;
|
|
4450
|
+
maskObject.evented = 'evented' in mergedConfig ? !!mergedConfig.evented : true;
|
|
4451
|
+
maskObject.hasControls = 'hasControls' in mergedConfig ? !!mergedConfig.hasControls : true;
|
|
3331
4452
|
maskObject.transparentCorners =
|
|
3332
|
-
'transparentCorners' in
|
|
3333
|
-
maskObject.strokeUniform =
|
|
4453
|
+
'transparentCorners' in mergedConfig ? !!mergedConfig.transparentCorners : false;
|
|
4454
|
+
maskObject.strokeUniform =
|
|
4455
|
+
'strokeUniform' in mergedConfig ? !!mergedConfig.strokeUniform : true;
|
|
3334
4456
|
maskObject.lockRotation = !options.maskRotatable;
|
|
3335
|
-
maskObject.borderColor = (_o =
|
|
3336
|
-
maskObject.cornerColor = (_p =
|
|
3337
|
-
maskObject.cornerSize = (_q =
|
|
4457
|
+
maskObject.borderColor = (_o = mergedConfig.borderColor) !== null && _o !== void 0 ? _o : 'red';
|
|
4458
|
+
maskObject.cornerColor = (_p = mergedConfig.cornerColor) !== null && _p !== void 0 ? _p : 'black';
|
|
4459
|
+
maskObject.cornerSize = (_q = mergedConfig.cornerSize) !== null && _q !== void 0 ? _q : 8;
|
|
3338
4460
|
const styles = ((_r = resolvedConfig.styles) !== null && _r !== void 0 ? _r : {});
|
|
3339
4461
|
if ('stroke' in styles) {
|
|
3340
4462
|
maskObject.stroke = styles.stroke;
|
|
@@ -3369,9 +4491,9 @@ function createMask(context, config = {}) {
|
|
|
3369
4491
|
}
|
|
3370
4492
|
canvas.renderAll();
|
|
3371
4493
|
context.saveCanvasState();
|
|
3372
|
-
if (typeof
|
|
4494
|
+
if (typeof config.onCreate === 'function') {
|
|
3373
4495
|
try {
|
|
3374
|
-
|
|
4496
|
+
config.onCreate(maskObject, canvas);
|
|
3375
4497
|
}
|
|
3376
4498
|
catch (error) {
|
|
3377
4499
|
reportWarning(options, error, 'createMask onCreate callback threw.');
|
|
@@ -3527,11 +4649,17 @@ function hideAllMaskLabels(context) {
|
|
|
3527
4649
|
});
|
|
3528
4650
|
}
|
|
3529
4651
|
|
|
4652
|
+
function getMaskListDocument(context) {
|
|
4653
|
+
var _a, _b, _c, _d, _e;
|
|
4654
|
+
const canvasLike = context.canvas;
|
|
4655
|
+
return ((_e = (_c = (_b = (_a = canvasLike === null || canvasLike === void 0 ? void 0 : canvasLike.getElement) === null || _a === void 0 ? void 0 : _a.call(canvasLike)) === null || _b === void 0 ? void 0 : _b.ownerDocument) !== null && _c !== void 0 ? _c : (_d = canvasLike === null || canvasLike === void 0 ? void 0 : canvasLike.lowerCanvasEl) === null || _d === void 0 ? void 0 : _d.ownerDocument) !== null && _e !== void 0 ? _e : document);
|
|
4656
|
+
}
|
|
3530
4657
|
function renderMaskList(context) {
|
|
3531
4658
|
const listId = context.getListElementId();
|
|
3532
4659
|
if (!listId)
|
|
3533
4660
|
return;
|
|
3534
|
-
const
|
|
4661
|
+
const ownerDocument = getMaskListDocument(context);
|
|
4662
|
+
const listEl = ownerDocument.getElementById(listId);
|
|
3535
4663
|
if (!listEl || !context.canvas)
|
|
3536
4664
|
return;
|
|
3537
4665
|
listEl.innerHTML = '';
|
|
@@ -3540,7 +4668,7 @@ function renderMaskList(context) {
|
|
|
3540
4668
|
.getObjects()
|
|
3541
4669
|
.filter(isMaskObject)
|
|
3542
4670
|
.forEach((mask) => {
|
|
3543
|
-
const listItemElement =
|
|
4671
|
+
const listItemElement = ownerDocument.createElement('li');
|
|
3544
4672
|
listItemElement.className = 'list-group-item mask-item';
|
|
3545
4673
|
listItemElement.textContent = mask.maskName;
|
|
3546
4674
|
listItemElement.dataset.maskId = String(mask.maskId);
|
|
@@ -3563,7 +4691,7 @@ function updateMaskListSelection(context, selectedMask) {
|
|
|
3563
4691
|
const listId = context.getListElementId();
|
|
3564
4692
|
if (!listId)
|
|
3565
4693
|
return;
|
|
3566
|
-
const listEl =
|
|
4694
|
+
const listEl = getMaskListDocument(context).getElementById(listId);
|
|
3567
4695
|
if (!listEl)
|
|
3568
4696
|
return;
|
|
3569
4697
|
const selectedId = selectedMask ? String(selectedMask.maskId) : null;
|
|
@@ -3574,7 +4702,7 @@ function updateMaskListSelection(context, selectedMask) {
|
|
|
3574
4702
|
}
|
|
3575
4703
|
|
|
3576
4704
|
class DomBindings {
|
|
3577
|
-
constructor(resolveElementId, isDisposed) {
|
|
4705
|
+
constructor(resolveElementId, isDisposed, resolveDocument = () => document) {
|
|
3578
4706
|
Object.defineProperty(this, "registry", {
|
|
3579
4707
|
enumerable: true,
|
|
3580
4708
|
configurable: true,
|
|
@@ -3593,14 +4721,21 @@ class DomBindings {
|
|
|
3593
4721
|
writable: true,
|
|
3594
4722
|
value: void 0
|
|
3595
4723
|
});
|
|
4724
|
+
Object.defineProperty(this, "resolveDocument", {
|
|
4725
|
+
enumerable: true,
|
|
4726
|
+
configurable: true,
|
|
4727
|
+
writable: true,
|
|
4728
|
+
value: void 0
|
|
4729
|
+
});
|
|
3596
4730
|
this.resolveElementId = resolveElementId;
|
|
3597
4731
|
this.isDisposed = isDisposed;
|
|
4732
|
+
this.resolveDocument = resolveDocument;
|
|
3598
4733
|
}
|
|
3599
4734
|
bindIfExists(key, eventType, handler) {
|
|
3600
4735
|
const id = this.resolveElementId(key);
|
|
3601
4736
|
if (!id)
|
|
3602
4737
|
return false;
|
|
3603
|
-
const element =
|
|
4738
|
+
const element = this.resolveDocument().getElementById(id);
|
|
3604
4739
|
if (!element)
|
|
3605
4740
|
return false;
|
|
3606
4741
|
const wrapped = (event) => {
|
|
@@ -3617,7 +4752,7 @@ class DomBindings {
|
|
|
3617
4752
|
const id = this.resolveElementId(entry.elementKey);
|
|
3618
4753
|
if (!id)
|
|
3619
4754
|
continue;
|
|
3620
|
-
const element =
|
|
4755
|
+
const element = this.resolveDocument().getElementById(id);
|
|
3621
4756
|
if (!element)
|
|
3622
4757
|
continue;
|
|
3623
4758
|
try {
|
|
@@ -3698,8 +4833,8 @@ function resetFileInput(input) {
|
|
|
3698
4833
|
}
|
|
3699
4834
|
|
|
3700
4835
|
const LAYOUT_EPSILON = 0.5;
|
|
3701
|
-
const INTERNAL_OPERATION_TOKEN = Symbol
|
|
3702
|
-
const INTERNAL_ALLOW_DURING_ANIMATION_QUEUE = Symbol
|
|
4836
|
+
const INTERNAL_OPERATION_TOKEN = Symbol('ImageEditorInternalOperation');
|
|
4837
|
+
const INTERNAL_ALLOW_DURING_ANIMATION_QUEUE = Symbol('ImageEditorAllowDuringAnimationQueue');
|
|
3703
4838
|
const CROP_MODE_CONTROL_KEYS = [
|
|
3704
4839
|
'scalePercentageInput',
|
|
3705
4840
|
'rotateLeftDegreesInput',
|
|
@@ -3720,9 +4855,85 @@ const CROP_MODE_CONTROL_KEYS = [
|
|
|
3720
4855
|
'enterCropModeButton',
|
|
3721
4856
|
'applyCropButton',
|
|
3722
4857
|
'cancelCropButton',
|
|
4858
|
+
'enterMosaicModeButton',
|
|
4859
|
+
'exitMosaicModeButton',
|
|
4860
|
+
'mosaicBrushSizeInput',
|
|
4861
|
+
'mosaicBlockSizeInput',
|
|
3723
4862
|
];
|
|
3724
4863
|
const CROP_MODE_ENABLED_KEYS = ['applyCropButton', 'cancelCropButton'];
|
|
3725
4864
|
const CROP_SESSION_ALLOWED_OPERATIONS = new Set(['applyCrop', 'cancelCrop']);
|
|
4865
|
+
const MOSAIC_MODE_CONTROL_KEYS = [
|
|
4866
|
+
'scalePercentageInput',
|
|
4867
|
+
'rotateLeftDegreesInput',
|
|
4868
|
+
'rotateRightDegreesInput',
|
|
4869
|
+
'rotateLeftButton',
|
|
4870
|
+
'rotateRightButton',
|
|
4871
|
+
'createMaskButton',
|
|
4872
|
+
'removeSelectedMaskButton',
|
|
4873
|
+
'removeAllMasksButton',
|
|
4874
|
+
'mergeMasksButton',
|
|
4875
|
+
'downloadImageButton',
|
|
4876
|
+
'zoomInButton',
|
|
4877
|
+
'zoomOutButton',
|
|
4878
|
+
'resetImageTransformButton',
|
|
4879
|
+
'undoButton',
|
|
4880
|
+
'redoButton',
|
|
4881
|
+
'imageInput',
|
|
4882
|
+
'enterCropModeButton',
|
|
4883
|
+
'applyCropButton',
|
|
4884
|
+
'cancelCropButton',
|
|
4885
|
+
'enterMosaicModeButton',
|
|
4886
|
+
'exitMosaicModeButton',
|
|
4887
|
+
'mosaicBrushSizeInput',
|
|
4888
|
+
'mosaicBlockSizeInput',
|
|
4889
|
+
];
|
|
4890
|
+
const MOSAIC_MODE_ENABLED_KEYS = [
|
|
4891
|
+
'exitMosaicModeButton',
|
|
4892
|
+
'mosaicBrushSizeInput',
|
|
4893
|
+
'mosaicBlockSizeInput',
|
|
4894
|
+
];
|
|
4895
|
+
const MOSAIC_SESSION_ALLOWED_OPERATIONS = new Set([
|
|
4896
|
+
'exitMosaicMode',
|
|
4897
|
+
'applyMosaic',
|
|
4898
|
+
'setMosaicConfig',
|
|
4899
|
+
'resetMosaicConfig',
|
|
4900
|
+
'setMosaicBrushSize',
|
|
4901
|
+
'setMosaicBlockSize',
|
|
4902
|
+
'saveState',
|
|
4903
|
+
]);
|
|
4904
|
+
const SCROLLBAR_SETTLE_EPSILON = 1;
|
|
4905
|
+
const IMAGE_EDITOR_OPERATIONS = new Set([
|
|
4906
|
+
'init',
|
|
4907
|
+
'loadImage',
|
|
4908
|
+
'loadFromState',
|
|
4909
|
+
'saveState',
|
|
4910
|
+
'scaleImage',
|
|
4911
|
+
'rotateImage',
|
|
4912
|
+
'resetImageTransform',
|
|
4913
|
+
'createMask',
|
|
4914
|
+
'removeSelectedMask',
|
|
4915
|
+
'removeAllMasks',
|
|
4916
|
+
'mergeMasks',
|
|
4917
|
+
'enterCropMode',
|
|
4918
|
+
'applyCrop',
|
|
4919
|
+
'cancelCrop',
|
|
4920
|
+
'enterMosaicMode',
|
|
4921
|
+
'exitMosaicMode',
|
|
4922
|
+
'applyMosaic',
|
|
4923
|
+
'setMosaicConfig',
|
|
4924
|
+
'resetMosaicConfig',
|
|
4925
|
+
'setMosaicBrushSize',
|
|
4926
|
+
'setMosaicBlockSize',
|
|
4927
|
+
'undo',
|
|
4928
|
+
'redo',
|
|
4929
|
+
'exportImageBase64',
|
|
4930
|
+
'exportImageFile',
|
|
4931
|
+
'downloadImage',
|
|
4932
|
+
'dispose',
|
|
4933
|
+
]);
|
|
4934
|
+
function isImageEditorOperation(value) {
|
|
4935
|
+
return value !== null && IMAGE_EDITOR_OPERATIONS.has(value);
|
|
4936
|
+
}
|
|
3726
4937
|
class ImageEditor {
|
|
3727
4938
|
constructor(fabricModuleOrOptions = {}, options = {}) {
|
|
3728
4939
|
var _a;
|
|
@@ -3744,6 +4955,24 @@ class ImageEditor {
|
|
|
3744
4955
|
writable: true,
|
|
3745
4956
|
value: void 0
|
|
3746
4957
|
});
|
|
4958
|
+
Object.defineProperty(this, "currentLayoutMode", {
|
|
4959
|
+
enumerable: true,
|
|
4960
|
+
configurable: true,
|
|
4961
|
+
writable: true,
|
|
4962
|
+
value: 'expand'
|
|
4963
|
+
});
|
|
4964
|
+
Object.defineProperty(this, "defaultMosaicConfig", {
|
|
4965
|
+
enumerable: true,
|
|
4966
|
+
configurable: true,
|
|
4967
|
+
writable: true,
|
|
4968
|
+
value: void 0
|
|
4969
|
+
});
|
|
4970
|
+
Object.defineProperty(this, "currentMosaicConfig", {
|
|
4971
|
+
enumerable: true,
|
|
4972
|
+
configurable: true,
|
|
4973
|
+
writable: true,
|
|
4974
|
+
value: void 0
|
|
4975
|
+
});
|
|
3747
4976
|
Object.defineProperty(this, "canvas", {
|
|
3748
4977
|
enumerable: true,
|
|
3749
4978
|
configurable: true,
|
|
@@ -3882,6 +5111,12 @@ class ImageEditor {
|
|
|
3882
5111
|
writable: true,
|
|
3883
5112
|
value: null
|
|
3884
5113
|
});
|
|
5114
|
+
Object.defineProperty(this, "mosaicSession", {
|
|
5115
|
+
enumerable: true,
|
|
5116
|
+
configurable: true,
|
|
5117
|
+
writable: true,
|
|
5118
|
+
value: null
|
|
5119
|
+
});
|
|
3885
5120
|
Object.defineProperty(this, "domBindings", {
|
|
3886
5121
|
enumerable: true,
|
|
3887
5122
|
configurable: true,
|
|
@@ -3922,9 +5157,15 @@ class ImageEditor {
|
|
|
3922
5157
|
this.fabricModule = (_a = detected.fabric) !== null && _a !== void 0 ? _a : {};
|
|
3923
5158
|
this.isFabricLoaded = detected.isFabricLoaded;
|
|
3924
5159
|
this.options = resolveOptions(detected.options);
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
5160
|
+
this.currentLayoutMode = this.options.layoutMode;
|
|
5161
|
+
this.defaultMosaicConfig = this.options.defaultMosaicConfig;
|
|
5162
|
+
this.currentMosaicConfig = cloneResolvedMosaicConfig(this.defaultMosaicConfig);
|
|
5163
|
+
const rawDefaultLayoutMode = detected.options
|
|
5164
|
+
.defaultLayoutMode;
|
|
5165
|
+
if (rawDefaultLayoutMode !== undefined && !isLayoutMode(rawDefaultLayoutMode)) {
|
|
5166
|
+
reportWarning(this.options, new TypeError(`[ImageEditor] Unsupported defaultLayoutMode ` +
|
|
5167
|
+
`${JSON.stringify(rawDefaultLayoutMode)}. ` +
|
|
5168
|
+
'Expected "fit", "cover", or "expand".'), 'Invalid defaultLayoutMode fell back to "expand".');
|
|
3928
5169
|
}
|
|
3929
5170
|
this.operationGuard = new OperationGuard();
|
|
3930
5171
|
this.animQueue = new AnimationQueue();
|
|
@@ -3966,10 +5207,14 @@ class ImageEditor {
|
|
|
3966
5207
|
enterCropModeButton: 'enterCropModeButton',
|
|
3967
5208
|
applyCropButton: 'applyCropButton',
|
|
3968
5209
|
cancelCropButton: 'cancelCropButton',
|
|
5210
|
+
enterMosaicModeButton: 'enterMosaicModeButton',
|
|
5211
|
+
exitMosaicModeButton: 'exitMosaicModeButton',
|
|
5212
|
+
mosaicBrushSizeInput: 'mosaicBrushSizeInput',
|
|
5213
|
+
mosaicBlockSizeInput: 'mosaicBlockSizeInput',
|
|
3969
5214
|
uploadArea: 'uploadArea',
|
|
3970
5215
|
};
|
|
3971
5216
|
this.elements = { ...defaults, ...idMap };
|
|
3972
|
-
this.domBindings = new DomBindings((key) => this.elements[key], () => this.isDisposed);
|
|
5217
|
+
this.domBindings = new DomBindings((key) => this.elements[key], () => this.isDisposed, () => { var _a, _b; return (_b = (_a = this.canvasElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : document; });
|
|
3973
5218
|
this.initCanvas();
|
|
3974
5219
|
this.transformController = new TransformController(this.buildTransformContext());
|
|
3975
5220
|
this.bindDomEvents();
|
|
@@ -4120,6 +5365,26 @@ class ImageEditor {
|
|
|
4120
5365
|
this.bindElementIfExists('cancelCropButton', 'click', () => {
|
|
4121
5366
|
this.cancelCrop();
|
|
4122
5367
|
});
|
|
5368
|
+
this.bindElementIfExists('enterMosaicModeButton', 'click', () => {
|
|
5369
|
+
this.enterMosaicMode();
|
|
5370
|
+
});
|
|
5371
|
+
this.bindElementIfExists('exitMosaicModeButton', 'click', () => {
|
|
5372
|
+
this.exitMosaicMode();
|
|
5373
|
+
});
|
|
5374
|
+
const bindMosaicSizeInput = (key, applyValue) => {
|
|
5375
|
+
const handler = (event) => {
|
|
5376
|
+
const parsed = parseFloat(event.target.value);
|
|
5377
|
+
applyValue(parsed);
|
|
5378
|
+
};
|
|
5379
|
+
this.bindElementIfExists(key, 'input', handler);
|
|
5380
|
+
this.bindElementIfExists(key, 'change', handler);
|
|
5381
|
+
};
|
|
5382
|
+
bindMosaicSizeInput('mosaicBrushSizeInput', (value) => {
|
|
5383
|
+
this.setMosaicBrushSize(value);
|
|
5384
|
+
});
|
|
5385
|
+
bindMosaicSizeInput('mosaicBlockSizeInput', (value) => {
|
|
5386
|
+
this.setMosaicBlockSize(value);
|
|
5387
|
+
});
|
|
4123
5388
|
}
|
|
4124
5389
|
bindElementIfExists(key, event, handler) {
|
|
4125
5390
|
var _a;
|
|
@@ -4155,6 +5420,9 @@ class ImageEditor {
|
|
|
4155
5420
|
}
|
|
4156
5421
|
}
|
|
4157
5422
|
async loadImage(base64, options = {}) {
|
|
5423
|
+
return this.loadImageInternal(base64, options);
|
|
5424
|
+
}
|
|
5425
|
+
async loadImageInternal(base64, options = {}) {
|
|
4158
5426
|
if (!this.isFabricLoaded || !this.canvas)
|
|
4159
5427
|
return;
|
|
4160
5428
|
if (this.isDisposed)
|
|
@@ -4174,7 +5442,7 @@ class ImageEditor {
|
|
|
4174
5442
|
const loadImageContext = {
|
|
4175
5443
|
fabric: this.fabricModule,
|
|
4176
5444
|
canvas: this.canvas,
|
|
4177
|
-
options: this.
|
|
5445
|
+
options: this.getRuntimeOptions(),
|
|
4178
5446
|
containerElement: this.containerElement,
|
|
4179
5447
|
placeholderElement: this.placeholderElement,
|
|
4180
5448
|
viewportCache: this.viewportCache,
|
|
@@ -4266,6 +5534,11 @@ class ImageEditor {
|
|
|
4266
5534
|
!CROP_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
|
|
4267
5535
|
throw new Error(`[ImageEditor] Cannot run "${operationName}" while crop mode is active.`);
|
|
4268
5536
|
}
|
|
5537
|
+
if (this.mosaicSession &&
|
|
5538
|
+
!this.operationGuard.isOwnOperation(token) &&
|
|
5539
|
+
!MOSAIC_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
|
|
5540
|
+
throw new Error(`[ImageEditor] Cannot run "${operationName}" while mosaic mode is active.`);
|
|
5541
|
+
}
|
|
4269
5542
|
if (this.animQueue.isBusy() && !this.canRunDuringAnimationQueue(options)) {
|
|
4270
5543
|
throw new Error(`[ImageEditor] Cannot run "${operationName}" while an animation is queued.`);
|
|
4271
5544
|
}
|
|
@@ -4275,10 +5548,17 @@ class ImageEditor {
|
|
|
4275
5548
|
this.assertIdleForOperation(operationName, options);
|
|
4276
5549
|
return true;
|
|
4277
5550
|
}
|
|
4278
|
-
catch {
|
|
5551
|
+
catch (error) {
|
|
5552
|
+
if (!this.isExpectedIdleGuardError(error, operationName)) {
|
|
5553
|
+
throw error;
|
|
5554
|
+
}
|
|
4279
5555
|
return false;
|
|
4280
5556
|
}
|
|
4281
5557
|
}
|
|
5558
|
+
isExpectedIdleGuardError(error, operationName) {
|
|
5559
|
+
return (error instanceof Error &&
|
|
5560
|
+
error.message.startsWith(`[ImageEditor] Cannot run "${operationName}" `));
|
|
5561
|
+
}
|
|
4282
5562
|
assertCanQueueAnimation(operationName, options) {
|
|
4283
5563
|
this.operationGuard.assertCanQueueAnimation(operationName, this.getInternalOperationToken(options));
|
|
4284
5564
|
}
|
|
@@ -4290,17 +5570,26 @@ class ImageEditor {
|
|
|
4290
5570
|
((_b = this.originalImage.height) !== null && _b !== void 0 ? _b : 0) > 0);
|
|
4291
5571
|
}
|
|
4292
5572
|
isBusy() {
|
|
4293
|
-
return this.operationGuard.isBusy() ||
|
|
5573
|
+
return (this.operationGuard.isBusy() ||
|
|
5574
|
+
this.animQueue.isBusy() ||
|
|
5575
|
+
this.cropSession !== null ||
|
|
5576
|
+
this.mosaicSession !== null);
|
|
4294
5577
|
}
|
|
4295
5578
|
setLayoutMode(mode) {
|
|
4296
|
-
if (mode
|
|
5579
|
+
if (!isLayoutMode(mode)) {
|
|
4297
5580
|
reportWarning(this.options, new TypeError(`[ImageEditor] Unsupported layout mode ${JSON.stringify(mode)}. ` +
|
|
4298
5581
|
'Expected "fit", "cover", or "expand".'), 'Ignored invalid layout mode.');
|
|
4299
5582
|
return;
|
|
4300
5583
|
}
|
|
4301
|
-
this.
|
|
4302
|
-
|
|
4303
|
-
|
|
5584
|
+
this.currentLayoutMode = mode;
|
|
5585
|
+
}
|
|
5586
|
+
getRuntimeOptions() {
|
|
5587
|
+
if (this.currentLayoutMode === this.options.layoutMode)
|
|
5588
|
+
return this.options;
|
|
5589
|
+
return Object.freeze({
|
|
5590
|
+
...this.options,
|
|
5591
|
+
layoutMode: this.currentLayoutMode,
|
|
5592
|
+
});
|
|
4304
5593
|
}
|
|
4305
5594
|
buildCallbackContext(operation, isInternalOperation = false) {
|
|
4306
5595
|
return { operation, isInternalOperation };
|
|
@@ -4309,7 +5598,7 @@ class ImageEditor {
|
|
|
4309
5598
|
const internal = this.getInternalOperationToken(options);
|
|
4310
5599
|
const activeOperation = this.operationGuard.activeOperationName();
|
|
4311
5600
|
if (internal && activeOperation) {
|
|
4312
|
-
return this.buildCallbackContext(activeOperation, true);
|
|
5601
|
+
return this.buildCallbackContext(isImageEditorOperation(activeOperation) ? activeOperation : fallback, true);
|
|
4313
5602
|
}
|
|
4314
5603
|
return this.buildCallbackContext(fallback, false);
|
|
4315
5604
|
}
|
|
@@ -4376,6 +5665,7 @@ class ImageEditor {
|
|
|
4376
5665
|
currentRotation: this.currentRotation,
|
|
4377
5666
|
isBusy: this.isBusy(),
|
|
4378
5667
|
isCropMode: this.cropSession !== null,
|
|
5668
|
+
isMosaicMode: this.mosaicSession !== null,
|
|
4379
5669
|
canUndo: this.historyManager.canUndo(),
|
|
4380
5670
|
canRedo: this.historyManager.canRedo(),
|
|
4381
5671
|
canvasWidth,
|
|
@@ -4463,7 +5753,7 @@ class ImageEditor {
|
|
|
4463
5753
|
const boundingRect = this.originalImage.getBoundingRect();
|
|
4464
5754
|
const scrollbarSize = measureScrollbarSize((_b = (_a = this.containerElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : null);
|
|
4465
5755
|
const viewport = this.measureLayoutViewport(scrollbarSize);
|
|
4466
|
-
if (this.
|
|
5756
|
+
if (this.currentLayoutMode === 'fit' || this.currentLayoutMode === 'cover') {
|
|
4467
5757
|
const canvasSize = computeScrollableCanvasSize(boundingRect.width, boundingRect.height, viewport, scrollbarSize);
|
|
4468
5758
|
this.setCanvasSizePx(canvasSize.width, canvasSize.height);
|
|
4469
5759
|
return;
|
|
@@ -4485,14 +5775,14 @@ class ImageEditor {
|
|
|
4485
5775
|
const canvasH = Math.ceil(this.canvas.getHeight());
|
|
4486
5776
|
const clipsImage = boundingRect.width > canvasW + LAYOUT_EPSILON ||
|
|
4487
5777
|
boundingRect.height > canvasH + LAYOUT_EPSILON;
|
|
4488
|
-
if (this.
|
|
5778
|
+
if (this.currentLayoutMode === 'fit' || this.currentLayoutMode === 'cover') {
|
|
4489
5779
|
const staleOverflowWidth = canvasW > viewport.width + LAYOUT_EPSILON &&
|
|
4490
5780
|
boundingRect.width <= viewport.width + LAYOUT_EPSILON;
|
|
4491
5781
|
const staleOverflowHeight = canvasH > viewport.height + LAYOUT_EPSILON &&
|
|
4492
5782
|
boundingRect.height <= viewport.height + LAYOUT_EPSILON;
|
|
4493
5783
|
return clipsImage || staleOverflowWidth || staleOverflowHeight;
|
|
4494
5784
|
}
|
|
4495
|
-
if (this.
|
|
5785
|
+
if (this.currentLayoutMode === 'expand') {
|
|
4496
5786
|
const expectedW = Math.max(viewport.width, Math.ceil(boundingRect.width));
|
|
4497
5787
|
const expectedH = Math.max(viewport.height, Math.ceil(boundingRect.height));
|
|
4498
5788
|
return (Math.abs(canvasW - expectedW) > LAYOUT_EPSILON ||
|
|
@@ -4500,6 +5790,33 @@ class ImageEditor {
|
|
|
4500
5790
|
}
|
|
4501
5791
|
return clipsImage;
|
|
4502
5792
|
}
|
|
5793
|
+
settleFitCoverScrollbarsAfterStateRestore() {
|
|
5794
|
+
if (!this.canvas ||
|
|
5795
|
+
!this.containerElement ||
|
|
5796
|
+
(this.currentLayoutMode !== 'fit' && this.currentLayoutMode !== 'cover')) {
|
|
5797
|
+
return;
|
|
5798
|
+
}
|
|
5799
|
+
const canvasW = Math.ceil(this.canvas.getWidth());
|
|
5800
|
+
const canvasH = Math.ceil(this.canvas.getHeight());
|
|
5801
|
+
if (canvasW <= 1 || canvasH <= 1)
|
|
5802
|
+
return;
|
|
5803
|
+
const clientW = Math.floor(this.containerElement.clientWidth || 0);
|
|
5804
|
+
const clientH = Math.floor(this.containerElement.clientHeight || 0);
|
|
5805
|
+
if (clientW <= 0 || clientH <= 0)
|
|
5806
|
+
return;
|
|
5807
|
+
const scrollW = Math.ceil(this.containerElement.scrollWidth || 0);
|
|
5808
|
+
const scrollH = Math.ceil(this.containerElement.scrollHeight || 0);
|
|
5809
|
+
const hasHorizontalScrollbar = scrollW > clientW + LAYOUT_EPSILON;
|
|
5810
|
+
const hasVerticalScrollbar = scrollH > clientH + LAYOUT_EPSILON;
|
|
5811
|
+
if (!hasHorizontalScrollbar && !hasVerticalScrollbar)
|
|
5812
|
+
return;
|
|
5813
|
+
const nudgeWidth = hasVerticalScrollbar && Math.abs(canvasW - clientW) <= SCROLLBAR_SETTLE_EPSILON;
|
|
5814
|
+
const nudgeHeight = hasHorizontalScrollbar && Math.abs(canvasH - clientH) <= SCROLLBAR_SETTLE_EPSILON;
|
|
5815
|
+
if (!nudgeWidth && !nudgeHeight)
|
|
5816
|
+
return;
|
|
5817
|
+
this.setCanvasSizePx(nudgeWidth ? canvasW - 1 : canvasW, nudgeHeight ? canvasH - 1 : canvasH);
|
|
5818
|
+
this.setCanvasSizePx(canvasW, canvasH);
|
|
5819
|
+
}
|
|
4503
5820
|
captureImageDisplayGeometry() {
|
|
4504
5821
|
if (!this.canvas || !this.originalImage)
|
|
4505
5822
|
return null;
|
|
@@ -4564,11 +5881,7 @@ class ImageEditor {
|
|
|
4564
5881
|
afterTransformSnap: () => {
|
|
4565
5882
|
if (this.isDisposed || !this.canvas || !this.originalImage)
|
|
4566
5883
|
return;
|
|
4567
|
-
|
|
4568
|
-
this.options.coverImageToCanvas ||
|
|
4569
|
-
this.options.fitImageToCanvas) {
|
|
4570
|
-
this.updateCanvasSizeToImageBounds();
|
|
4571
|
-
}
|
|
5884
|
+
this.updateCanvasSizeToImageBounds();
|
|
4572
5885
|
this.alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
|
|
4573
5886
|
this.canvas
|
|
4574
5887
|
.getObjects()
|
|
@@ -4736,14 +6049,13 @@ class ImageEditor {
|
|
|
4736
6049
|
this.currentImageMimeType = null;
|
|
4737
6050
|
}
|
|
4738
6051
|
this.isImageLoadedToCanvas = !!this.originalImage;
|
|
4739
|
-
if (this.originalImage &&
|
|
4740
|
-
(this.options.expandCanvasToImage ||
|
|
4741
|
-
this.options.coverImageToCanvas ||
|
|
4742
|
-
this.options.fitImageToCanvas) &&
|
|
4743
|
-
this.shouldNormalizeCanvasSizeAfterStateRestore()) {
|
|
6052
|
+
if (this.originalImage && this.shouldNormalizeCanvasSizeAfterStateRestore()) {
|
|
4744
6053
|
this.updateCanvasSizeToImageBounds();
|
|
4745
6054
|
this.alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
|
|
4746
6055
|
}
|
|
6056
|
+
if (this.originalImage) {
|
|
6057
|
+
this.settleFitCoverScrollbarsAfterStateRestore();
|
|
6058
|
+
}
|
|
4747
6059
|
const restoredMasks = restoredState.objects.filter(isMaskObject);
|
|
4748
6060
|
this.lastMask = restoredMasks.reduce((lastMask, maskObject) => !lastMask || maskObject.maskId > lastMask.maskId ? maskObject : lastMask, null);
|
|
4749
6061
|
restoredMasks.forEach((maskObject) => {
|
|
@@ -4803,16 +6115,12 @@ class ImageEditor {
|
|
|
4803
6115
|
if (after === before) {
|
|
4804
6116
|
return;
|
|
4805
6117
|
}
|
|
4806
|
-
let executedOnce = false;
|
|
4807
6118
|
const cmd = new Command(async () => {
|
|
4808
|
-
|
|
4809
|
-
await this.loadFromStateInternal(after, this.withAnimationQueueBypass());
|
|
4810
|
-
}
|
|
4811
|
-
executedOnce = true;
|
|
6119
|
+
await this.loadFromStateInternal(after, this.withAnimationQueueBypass());
|
|
4812
6120
|
}, async () => {
|
|
4813
6121
|
await this.loadFromStateInternal(before, this.withAnimationQueueBypass());
|
|
4814
6122
|
});
|
|
4815
|
-
this.historyManager.
|
|
6123
|
+
this.historyManager.push(cmd);
|
|
4816
6124
|
this.lastSnapshot = after;
|
|
4817
6125
|
}
|
|
4818
6126
|
catch (error) {
|
|
@@ -4927,7 +6235,7 @@ class ImageEditor {
|
|
|
4927
6235
|
return {
|
|
4928
6236
|
fabric: this.fabricModule,
|
|
4929
6237
|
canvas: this.canvas,
|
|
4930
|
-
options: this.
|
|
6238
|
+
options: this.getRuntimeOptions(),
|
|
4931
6239
|
getLastMask: () => this.lastMask,
|
|
4932
6240
|
setLastMask: (maskObject) => {
|
|
4933
6241
|
this.lastMask = maskObject;
|
|
@@ -5128,7 +6436,7 @@ class ImageEditor {
|
|
|
5128
6436
|
containerElement: this.containerElement,
|
|
5129
6437
|
loadImage: async (base64, providedOptions) => {
|
|
5130
6438
|
const geometry = this.captureImageDisplayGeometry();
|
|
5131
|
-
await this.
|
|
6439
|
+
await this.loadImageInternal(base64, this.withInternalOperationOptions(operationToken, providedOptions !== null && providedOptions !== void 0 ? providedOptions : {}));
|
|
5132
6440
|
this.restoreMergedImageDisplayGeometry(geometry);
|
|
5133
6441
|
},
|
|
5134
6442
|
saveState: () => this.captureSnapshotInternal(),
|
|
@@ -5141,8 +6449,9 @@ class ImageEditor {
|
|
|
5141
6449
|
}
|
|
5142
6450
|
captureSnapshotInternal() {
|
|
5143
6451
|
var _a;
|
|
5144
|
-
if (!this.canvas)
|
|
5145
|
-
|
|
6452
|
+
if (!this.canvas) {
|
|
6453
|
+
throw new Error('[ImageEditor] Cannot capture canvas snapshot before init or after dispose.');
|
|
6454
|
+
}
|
|
5146
6455
|
const activeMask = this.getActiveMaskForSnapshot();
|
|
5147
6456
|
this.hideAllMaskLabels();
|
|
5148
6457
|
return saveState({
|
|
@@ -5161,9 +6470,134 @@ class ImageEditor {
|
|
|
5161
6470
|
const activeObject = this.canvas.getActiveObject();
|
|
5162
6471
|
if (activeObject && isMaskObject(activeObject))
|
|
5163
6472
|
return activeObject;
|
|
5164
|
-
|
|
6473
|
+
const labeledMasks = this.canvas
|
|
5165
6474
|
.getObjects()
|
|
5166
|
-
.
|
|
6475
|
+
.filter((object) => isMaskObject(object) && !!object.labelObject);
|
|
6476
|
+
return labeledMasks.length === 1 ? ((_a = labeledMasks[0]) !== null && _a !== void 0 ? _a : null) : null;
|
|
6477
|
+
}
|
|
6478
|
+
enterMosaicMode() {
|
|
6479
|
+
if (!this.canvas || !this.originalImage)
|
|
6480
|
+
return;
|
|
6481
|
+
if (this.mosaicSession)
|
|
6482
|
+
return;
|
|
6483
|
+
if (!this.isImageLoaded())
|
|
6484
|
+
return;
|
|
6485
|
+
if (!this.canRunIdleOperation('enterMosaicMode'))
|
|
6486
|
+
return;
|
|
6487
|
+
enterMosaicMode(this.buildMosaicControllerContext());
|
|
6488
|
+
this.updateInputs();
|
|
6489
|
+
this.updateUi();
|
|
6490
|
+
const callbackContext = this.buildCallbackContext('enterMosaicMode', false);
|
|
6491
|
+
this.emitBusyChangeIfChanged(callbackContext);
|
|
6492
|
+
this.emitImageChanged(callbackContext);
|
|
6493
|
+
}
|
|
6494
|
+
exitMosaicMode() {
|
|
6495
|
+
if (!this.canvas || !this.mosaicSession)
|
|
6496
|
+
return;
|
|
6497
|
+
if (!this.canRunIdleOperation('exitMosaicMode'))
|
|
6498
|
+
return;
|
|
6499
|
+
exitMosaicMode(this.buildMosaicControllerContext());
|
|
6500
|
+
this.updateInputs();
|
|
6501
|
+
this.updateUi();
|
|
6502
|
+
const callbackContext = this.buildCallbackContext('exitMosaicMode', false);
|
|
6503
|
+
this.emitBusyChangeIfChanged(callbackContext);
|
|
6504
|
+
this.emitImageChanged(callbackContext);
|
|
6505
|
+
}
|
|
6506
|
+
isMosaicMode() {
|
|
6507
|
+
return this.mosaicSession !== null;
|
|
6508
|
+
}
|
|
6509
|
+
getMosaicConfig() {
|
|
6510
|
+
return cloneResolvedMosaicConfig(this.currentMosaicConfig);
|
|
6511
|
+
}
|
|
6512
|
+
setMosaicConfig(config) {
|
|
6513
|
+
this.applyMosaicConfigPatch(config, 'setMosaicConfig');
|
|
6514
|
+
}
|
|
6515
|
+
resetMosaicConfig() {
|
|
6516
|
+
if (this.isDisposed)
|
|
6517
|
+
return;
|
|
6518
|
+
const nextConfig = cloneResolvedMosaicConfig(this.defaultMosaicConfig);
|
|
6519
|
+
if (areResolvedMosaicConfigsEqual(this.currentMosaicConfig, nextConfig))
|
|
6520
|
+
return;
|
|
6521
|
+
this.currentMosaicConfig = nextConfig;
|
|
6522
|
+
if (this.mosaicSession && this.canvas) {
|
|
6523
|
+
updateMosaicPreview(this.buildMosaicControllerContext());
|
|
6524
|
+
}
|
|
6525
|
+
this.updateInputs();
|
|
6526
|
+
this.updateUi();
|
|
6527
|
+
this.emitImageChanged(this.buildCallbackContext('resetMosaicConfig', false));
|
|
6528
|
+
}
|
|
6529
|
+
setMosaicBrushSize(size) {
|
|
6530
|
+
this.applyMosaicConfigPatch({ brushSize: size }, 'setMosaicBrushSize');
|
|
6531
|
+
}
|
|
6532
|
+
setMosaicBlockSize(size) {
|
|
6533
|
+
this.applyMosaicConfigPatch({ blockSize: size }, 'setMosaicBlockSize');
|
|
6534
|
+
}
|
|
6535
|
+
applyMosaicConfigPatch(config, operation) {
|
|
6536
|
+
if (this.isDisposed)
|
|
6537
|
+
return;
|
|
6538
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
6539
|
+
reportWarning(this.options, new TypeError('[ImageEditor] Invalid Mosaic config object.'), 'Ignored invalid Mosaic config.');
|
|
6540
|
+
return;
|
|
6541
|
+
}
|
|
6542
|
+
const invalidFields = getInvalidMosaicConfigFields(config);
|
|
6543
|
+
if (invalidFields.length > 0) {
|
|
6544
|
+
reportWarning(this.options, new TypeError(`[ImageEditor] Ignored invalid Mosaic config field(s): ` +
|
|
6545
|
+
`${invalidFields.join(', ')}.`), 'Ignored invalid Mosaic config fields.');
|
|
6546
|
+
}
|
|
6547
|
+
const nextConfig = mergeMosaicConfigPatch(this.currentMosaicConfig, config);
|
|
6548
|
+
if (areResolvedMosaicConfigsEqual(this.currentMosaicConfig, nextConfig))
|
|
6549
|
+
return;
|
|
6550
|
+
this.currentMosaicConfig = nextConfig;
|
|
6551
|
+
if (this.mosaicSession && this.canvas) {
|
|
6552
|
+
updateMosaicPreview(this.buildMosaicControllerContext());
|
|
6553
|
+
}
|
|
6554
|
+
this.updateInputs();
|
|
6555
|
+
this.updateUi();
|
|
6556
|
+
this.emitImageChanged(this.buildCallbackContext(operation, false));
|
|
6557
|
+
}
|
|
6558
|
+
buildMosaicControllerContext() {
|
|
6559
|
+
return {
|
|
6560
|
+
fabric: this.fabricModule,
|
|
6561
|
+
canvas: this.canvas,
|
|
6562
|
+
options: this.options,
|
|
6563
|
+
historyManager: this.historyManager,
|
|
6564
|
+
getMosaicConfig: () => cloneResolvedMosaicConfig(this.currentMosaicConfig),
|
|
6565
|
+
isImageLoaded: () => this.isImageLoaded(),
|
|
6566
|
+
getOriginalImage: () => this.originalImage,
|
|
6567
|
+
setOriginalImage: (image) => {
|
|
6568
|
+
this.originalImage = image;
|
|
6569
|
+
},
|
|
6570
|
+
getCurrentImageMimeType: () => this.currentImageMimeType,
|
|
6571
|
+
setCurrentImageMimeType: (mimeType) => {
|
|
6572
|
+
this.currentImageMimeType = mimeType;
|
|
6573
|
+
},
|
|
6574
|
+
getLastSnapshot: () => this.lastSnapshot,
|
|
6575
|
+
setLastSnapshot: (snapshot) => {
|
|
6576
|
+
this.lastSnapshot = snapshot;
|
|
6577
|
+
},
|
|
6578
|
+
captureSnapshot: () => this.captureSnapshotInternal(),
|
|
6579
|
+
loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withAnimationQueueBypass()),
|
|
6580
|
+
updateUi: () => {
|
|
6581
|
+
this.updateUi();
|
|
6582
|
+
},
|
|
6583
|
+
updateInputs: () => {
|
|
6584
|
+
this.updateInputs();
|
|
6585
|
+
},
|
|
6586
|
+
hideAllMaskLabels: () => {
|
|
6587
|
+
this.hideAllMaskLabels();
|
|
6588
|
+
},
|
|
6589
|
+
emitImageChanged: (context) => {
|
|
6590
|
+
this.emitImageChanged(context);
|
|
6591
|
+
},
|
|
6592
|
+
emitBusyChangeIfChanged: (context) => {
|
|
6593
|
+
this.emitBusyChangeIfChanged(context);
|
|
6594
|
+
},
|
|
6595
|
+
buildCallbackContext: (operation, isInternal) => this.buildCallbackContext(operation, isInternal),
|
|
6596
|
+
getMosaicSession: () => this.mosaicSession,
|
|
6597
|
+
setMosaicSession: (session) => {
|
|
6598
|
+
this.mosaicSession = session;
|
|
6599
|
+
},
|
|
6600
|
+
};
|
|
5167
6601
|
}
|
|
5168
6602
|
enterCropMode() {
|
|
5169
6603
|
if (!this.canvas || !this.originalImage)
|
|
@@ -5236,7 +6670,7 @@ class ImageEditor {
|
|
|
5236
6670
|
},
|
|
5237
6671
|
saveState: () => this.captureSnapshotInternal(),
|
|
5238
6672
|
loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withInternalOperationOptions(operationToken, this.withAnimationQueueBypass())),
|
|
5239
|
-
loadImage: (base64, providedOptions) => this.
|
|
6673
|
+
loadImage: (base64, providedOptions) => this.loadImageInternal(base64, this.withInternalOperationOptions(operationToken, providedOptions !== null && providedOptions !== void 0 ? providedOptions : {})),
|
|
5240
6674
|
getMaskCounter: () => this.maskCounter,
|
|
5241
6675
|
setMaskCounter: (n) => {
|
|
5242
6676
|
this.maskCounter = n;
|
|
@@ -5248,13 +6682,28 @@ class ImageEditor {
|
|
|
5248
6682
|
}
|
|
5249
6683
|
updateInputs() {
|
|
5250
6684
|
const scaleId = this.elements.scalePercentageInput;
|
|
5251
|
-
if (
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
6685
|
+
if (scaleId) {
|
|
6686
|
+
const scaleInputElement = document.getElementById(scaleId);
|
|
6687
|
+
if (scaleInputElement) {
|
|
6688
|
+
scaleInputElement.value = String(Math.round(this.currentScale * 100));
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6691
|
+
const mosaicConfig = this.getMosaicConfig();
|
|
6692
|
+
const mosaicBrushSizeInputId = this.elements.mosaicBrushSizeInput;
|
|
6693
|
+
if (mosaicBrushSizeInputId) {
|
|
6694
|
+
const brushInput = document.getElementById(mosaicBrushSizeInputId);
|
|
6695
|
+
if (brushInput)
|
|
6696
|
+
brushInput.value = String(mosaicConfig.brushSize);
|
|
6697
|
+
}
|
|
6698
|
+
const mosaicBlockSizeInputId = this.elements.mosaicBlockSizeInput;
|
|
6699
|
+
if (mosaicBlockSizeInputId) {
|
|
6700
|
+
const blockInput = document.getElementById(mosaicBlockSizeInputId);
|
|
6701
|
+
if (blockInput)
|
|
6702
|
+
blockInput.value = String(mosaicConfig.blockSize);
|
|
6703
|
+
}
|
|
5256
6704
|
}
|
|
5257
6705
|
updateUi() {
|
|
6706
|
+
var _a;
|
|
5258
6707
|
if (!this.canvas)
|
|
5259
6708
|
return;
|
|
5260
6709
|
const hasImage = !!this.originalImage;
|
|
@@ -5266,13 +6715,22 @@ class ImageEditor {
|
|
|
5266
6715
|
const canUndo = this.historyManager.canUndo();
|
|
5267
6716
|
const canRedo = this.historyManager.canRedo();
|
|
5268
6717
|
const isInCropMode = this.cropSession !== null;
|
|
6718
|
+
const isInMosaicMode = this.mosaicSession !== null;
|
|
5269
6719
|
const isBusy = this.operationGuard.isBusy() || this.animQueue.isBusy();
|
|
6720
|
+
const isMosaicApplying = ((_a = this.mosaicSession) === null || _a === void 0 ? void 0 : _a.isApplying) === true;
|
|
5270
6721
|
if (isInCropMode) {
|
|
5271
6722
|
CROP_MODE_CONTROL_KEYS.forEach((key) => {
|
|
5272
6723
|
this.setControlEnabled(key, !isBusy && CROP_MODE_ENABLED_KEYS.includes(key));
|
|
5273
6724
|
});
|
|
5274
6725
|
return;
|
|
5275
6726
|
}
|
|
6727
|
+
if (isInMosaicMode) {
|
|
6728
|
+
MOSAIC_MODE_CONTROL_KEYS.forEach((key) => {
|
|
6729
|
+
this.setControlEnabled(key, !isBusy && !isMosaicApplying && MOSAIC_MODE_ENABLED_KEYS.includes(key));
|
|
6730
|
+
});
|
|
6731
|
+
this.setControlEnabled('imageInput', false);
|
|
6732
|
+
return;
|
|
6733
|
+
}
|
|
5276
6734
|
this.setControlEnabled('scalePercentageInput', hasImage && !isBusy);
|
|
5277
6735
|
this.setControlEnabled('rotateLeftDegreesInput', hasImage && !isBusy);
|
|
5278
6736
|
this.setControlEnabled('rotateRightDegreesInput', hasImage && !isBusy);
|
|
@@ -5289,6 +6747,10 @@ class ImageEditor {
|
|
|
5289
6747
|
this.setControlEnabled('undoButton', hasImage && !isBusy && canUndo);
|
|
5290
6748
|
this.setControlEnabled('redoButton', hasImage && !isBusy && canRedo);
|
|
5291
6749
|
this.setControlEnabled('enterCropModeButton', hasImage && !isBusy);
|
|
6750
|
+
this.setControlEnabled('enterMosaicModeButton', hasImage && !isBusy);
|
|
6751
|
+
this.setControlEnabled('exitMosaicModeButton', false);
|
|
6752
|
+
this.setControlEnabled('mosaicBrushSizeInput', !this.isDisposed);
|
|
6753
|
+
this.setControlEnabled('mosaicBlockSizeInput', !this.isDisposed);
|
|
5292
6754
|
this.setControlEnabled('imageInput', !isBusy);
|
|
5293
6755
|
this.setControlEnabled('applyCropButton', false);
|
|
5294
6756
|
this.setControlEnabled('cancelCropButton', false);
|
|
@@ -5386,6 +6848,14 @@ class ImageEditor {
|
|
|
5386
6848
|
}
|
|
5387
6849
|
this.cropSession = null;
|
|
5388
6850
|
}
|
|
6851
|
+
if (this.mosaicSession && this.canvas) {
|
|
6852
|
+
try {
|
|
6853
|
+
exitMosaicMode(this.buildMosaicControllerContext());
|
|
6854
|
+
}
|
|
6855
|
+
catch {
|
|
6856
|
+
}
|
|
6857
|
+
this.mosaicSession = null;
|
|
6858
|
+
}
|
|
5389
6859
|
if (this.canvas) {
|
|
5390
6860
|
try {
|
|
5391
6861
|
void Promise.resolve(this.canvas.dispose()).catch(() => {
|