@bensitu/image-editor 2.0.0 → 2.2.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 +346 -90
- package/dist/cjs/index.cjs +4883 -1191
- 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/annotation/annotation-lock.js +7 -0
- package/dist/esm/annotation/annotation-lock.js.map +1 -0
- package/dist/esm/annotation/annotation-manager.js +217 -0
- package/dist/esm/annotation/annotation-manager.js.map +1 -0
- package/dist/esm/annotation/annotation-style.js +50 -0
- package/dist/esm/annotation/annotation-style.js.map +1 -0
- package/dist/esm/annotation/draw-controller.js +114 -0
- package/dist/esm/annotation/draw-controller.js.map +1 -0
- package/dist/esm/annotation/text-controller.js +234 -0
- package/dist/esm/annotation/text-controller.js.map +1 -0
- package/dist/esm/core/default-options.js +447 -11
- package/dist/esm/core/default-options.js.map +1 -1
- package/dist/esm/core/editor-object-kind.js +37 -0
- package/dist/esm/core/editor-object-kind.js.map +1 -0
- package/dist/esm/core/errors.js +19 -0
- package/dist/esm/core/errors.js.map +1 -1
- package/dist/esm/core/layer-order.js +100 -0
- package/dist/esm/core/layer-order.js.map +1 -0
- 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 +34 -1
- package/dist/esm/core/public-types.js.map +1 -1
- package/dist/esm/core/state-serializer.js +108 -27
- package/dist/esm/core/state-serializer.js.map +1 -1
- package/dist/esm/crop/crop-controller.js +6 -2
- package/dist/esm/crop/crop-controller.js.map +1 -1
- package/dist/esm/export/export-format.js.map +1 -1
- package/dist/esm/export/export-service.js +140 -141
- package/dist/esm/export/export-service.js.map +1 -1
- package/dist/esm/export/overlay-merge-service.js +75 -0
- package/dist/esm/export/overlay-merge-service.js.map +1 -0
- package/dist/esm/fabric/fabric-animation.js +56 -4
- package/dist/esm/fabric/fabric-animation.js.map +1 -1
- package/dist/esm/history/history-manager.js +2 -2
- package/dist/esm/history/history-manager.js.map +1 -1
- package/dist/esm/image/image-loader.js +27 -65
- 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 +1474 -135
- package/dist/esm/image-editor.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/mask/mask-factory.js +92 -43
- package/dist/esm/mask/mask-factory.js.map +1 -1
- package/dist/esm/mask/mask-label-manager.js +2 -0
- package/dist/esm/mask/mask-label-manager.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/mask/mask-style.js.map +1 -1
- package/dist/esm/mosaic/mosaic-controller.js +666 -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/image-element-loader.js +55 -0
- package/dist/esm/utils/image-element-loader.js.map +1 -0
- package/dist/esm/utils/number.js.map +1 -1
- package/dist/esm/utils/pointer.js +28 -0
- package/dist/esm/utils/pointer.js.map +1 -0
- package/dist/types/animation/animation-queue.d.ts.map +1 -1
- package/dist/types/annotation/annotation-lock.d.ts +12 -0
- package/dist/types/annotation/annotation-lock.d.ts.map +1 -0
- package/dist/types/annotation/annotation-manager.d.ts +33 -0
- package/dist/types/annotation/annotation-manager.d.ts.map +1 -0
- package/dist/types/annotation/annotation-style.d.ts +13 -0
- package/dist/types/annotation/annotation-style.d.ts.map +1 -0
- package/dist/types/annotation/draw-controller.d.ts +43 -0
- package/dist/types/annotation/draw-controller.d.ts.map +1 -0
- package/dist/types/annotation/text-controller.d.ts +47 -0
- package/dist/types/annotation/text-controller.d.ts.map +1 -0
- package/dist/types/core/default-options.d.ts +46 -6
- package/dist/types/core/default-options.d.ts.map +1 -1
- package/dist/types/core/editor-object-kind.d.ts +29 -0
- package/dist/types/core/editor-object-kind.d.ts.map +1 -0
- package/dist/types/core/errors.d.ts +12 -2
- package/dist/types/core/errors.d.ts.map +1 -1
- package/dist/types/core/layer-order.d.ts +21 -0
- package/dist/types/core/layer-order.d.ts.map +1 -0
- 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 +341 -33
- package/dist/types/core/public-types.d.ts.map +1 -1
- package/dist/types/core/state-serializer.d.ts +32 -5
- package/dist/types/core/state-serializer.d.ts.map +1 -1
- package/dist/types/crop/crop-controller.d.ts +6 -7
- package/dist/types/crop/crop-controller.d.ts.map +1 -1
- package/dist/types/export/export-format.d.ts +5 -33
- package/dist/types/export/export-format.d.ts.map +1 -1
- package/dist/types/export/export-service.d.ts +24 -15
- package/dist/types/export/export-service.d.ts.map +1 -1
- package/dist/types/export/overlay-merge-service.d.ts +38 -0
- package/dist/types/export/overlay-merge-service.d.ts.map +1 -0
- package/dist/types/fabric/fabric-animation.d.ts.map +1 -1
- package/dist/types/history/history-manager.d.ts +11 -14
- package/dist/types/history/history-manager.d.ts.map +1 -1
- package/dist/types/image/image-loader.d.ts +24 -21
- package/dist/types/image/image-loader.d.ts.map +1 -1
- package/dist/types/image/image-resampler.d.ts +2 -2
- 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 +6 -9
- package/dist/types/image/transform-controller.d.ts.map +1 -1
- package/dist/types/image-editor.d.ts +93 -14
- package/dist/types/image-editor.d.ts.map +1 -1
- package/dist/types/index.d.cts +3 -3
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.ts +3 -3
- 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-label-manager.d.ts +10 -9
- package/dist/types/mask/mask-label-manager.d.ts.map +1 -1
- package/dist/types/mask/mask-list.d.ts +11 -12
- package/dist/types/mask/mask-list.d.ts.map +1 -1
- package/dist/types/mask/mask-style.d.ts +19 -20
- package/dist/types/mask/mask-style.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/ui/visibility-state.d.ts +2 -2
- package/dist/types/utils/image-element-loader.d.ts +19 -0
- package/dist/types/utils/image-element-loader.d.ts.map +1 -0
- package/dist/types/utils/number.d.ts +1 -2
- package/dist/types/utils/number.d.ts.map +1 -1
- package/dist/types/utils/pointer.d.ts +16 -0
- package/dist/types/utils/pointer.d.ts.map +1 -0
- package/dist/umd/image-editor.umd.js +1 -1
- package/dist/umd/image-editor.umd.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import { reportError, reportWarning } from '../core/callback-reporter.js';
|
|
2
|
+
import { markBaseImageObject, markSessionObject } from '../core/editor-object-kind.js';
|
|
3
|
+
import { mimeTypeFor, tryNormalizeImageFormat } from '../export/export-format.js';
|
|
4
|
+
import { Command } from '../history/history-manager.js';
|
|
5
|
+
import { detectSourceMimeType } from '../image/image-resampler.js';
|
|
6
|
+
import { getPointerFromFabricEvent } from '../utils/pointer.js';
|
|
7
|
+
import { withTimeout } from '../utils/timeout.js';
|
|
8
|
+
import { getMosaicImagePoint } from './mosaic-geometry.js';
|
|
9
|
+
import { applyCircularMosaicToImageData } from './mosaic-pixelate.js';
|
|
10
|
+
const MAX_PENDING_MOSAIC_POINTS = 4096;
|
|
11
|
+
function getCanvasDocument(context) {
|
|
12
|
+
var _a, _b, _c, _d, _e;
|
|
13
|
+
const element = (_b = (_a = context.canvas).getElement) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
14
|
+
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);
|
|
15
|
+
}
|
|
16
|
+
function safeRender(canvas) {
|
|
17
|
+
try {
|
|
18
|
+
canvas.requestRenderAll();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
try {
|
|
22
|
+
canvas.renderAll();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function createPreviewCircle(context) {
|
|
29
|
+
const config = context.getMosaicConfig();
|
|
30
|
+
const circle = new context.fabric.Circle({
|
|
31
|
+
left: 0,
|
|
32
|
+
top: 0,
|
|
33
|
+
radius: config.brushSize / 2,
|
|
34
|
+
originX: 'center',
|
|
35
|
+
originY: 'center',
|
|
36
|
+
fill: config.previewFill,
|
|
37
|
+
stroke: config.previewStroke,
|
|
38
|
+
strokeWidth: config.previewStrokeWidth,
|
|
39
|
+
strokeDashArray: config.previewStrokeDashArray
|
|
40
|
+
? [...config.previewStrokeDashArray]
|
|
41
|
+
: undefined,
|
|
42
|
+
selectable: false,
|
|
43
|
+
evented: false,
|
|
44
|
+
excludeFromExport: true,
|
|
45
|
+
objectCaching: false,
|
|
46
|
+
visible: false,
|
|
47
|
+
});
|
|
48
|
+
markSessionObject(circle, 'mosaicPreviewCircle');
|
|
49
|
+
circle.isMosaicPreview = true;
|
|
50
|
+
return circle;
|
|
51
|
+
}
|
|
52
|
+
function ensurePreviewCircle(context, session) {
|
|
53
|
+
var _a;
|
|
54
|
+
const { canvas } = context;
|
|
55
|
+
const circle = (_a = session.previewCircle) !== null && _a !== void 0 ? _a : createPreviewCircle(context);
|
|
56
|
+
session.previewCircle = circle;
|
|
57
|
+
if (!canvas.getObjects().includes(circle)) {
|
|
58
|
+
canvas.add(circle);
|
|
59
|
+
}
|
|
60
|
+
canvas.bringObjectToFront(circle);
|
|
61
|
+
updateMosaicPreview(context);
|
|
62
|
+
return circle;
|
|
63
|
+
}
|
|
64
|
+
function removePreviewCircle(context, session) {
|
|
65
|
+
const circle = session.previewCircle;
|
|
66
|
+
if (!circle)
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
context.canvas.remove(circle);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
}
|
|
73
|
+
session.previewCircle = null;
|
|
74
|
+
}
|
|
75
|
+
function createPreviewImage(context, sourceImage, rasterCache) {
|
|
76
|
+
const image = new context.fabric.FabricImage(rasterCache.offscreenCanvas, {
|
|
77
|
+
selectable: false,
|
|
78
|
+
evented: false,
|
|
79
|
+
excludeFromExport: true,
|
|
80
|
+
objectCaching: false,
|
|
81
|
+
visible: true,
|
|
82
|
+
});
|
|
83
|
+
copyBaseImageProperties(image, sourceImage);
|
|
84
|
+
image.set({
|
|
85
|
+
selectable: false,
|
|
86
|
+
evented: false,
|
|
87
|
+
excludeFromExport: true,
|
|
88
|
+
objectCaching: false,
|
|
89
|
+
visible: true,
|
|
90
|
+
});
|
|
91
|
+
markSessionObject(image, 'mosaicPreviewImage');
|
|
92
|
+
image.isMosaicPreview = true;
|
|
93
|
+
return image;
|
|
94
|
+
}
|
|
95
|
+
function placePreviewImageAfterBase(context, previewImage, sourceImage) {
|
|
96
|
+
var _a, _b;
|
|
97
|
+
const sourceIndex = context.canvas.getObjects().indexOf(sourceImage);
|
|
98
|
+
if (sourceIndex < 0)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
(_b = (_a = context.canvas).moveObjectTo) === null || _b === void 0 ? void 0 : _b.call(_a, previewImage, sourceIndex + 1);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function ensurePreviewImage(context, session, sourceImage) {
|
|
107
|
+
var _a;
|
|
108
|
+
const rasterCache = session.rasterCache;
|
|
109
|
+
if (!rasterCache)
|
|
110
|
+
return null;
|
|
111
|
+
const previewImage = (_a = session.previewImage) !== null && _a !== void 0 ? _a : createPreviewImage(context, sourceImage, rasterCache);
|
|
112
|
+
session.previewImage = previewImage;
|
|
113
|
+
copyBaseImageProperties(previewImage, sourceImage);
|
|
114
|
+
previewImage.set({
|
|
115
|
+
selectable: false,
|
|
116
|
+
evented: false,
|
|
117
|
+
excludeFromExport: true,
|
|
118
|
+
objectCaching: false,
|
|
119
|
+
visible: true,
|
|
120
|
+
});
|
|
121
|
+
previewImage.dirty = true;
|
|
122
|
+
if (!context.canvas.getObjects().includes(previewImage)) {
|
|
123
|
+
context.canvas.add(previewImage);
|
|
124
|
+
}
|
|
125
|
+
placePreviewImageAfterBase(context, previewImage, sourceImage);
|
|
126
|
+
const circle = session.previewCircle;
|
|
127
|
+
if (circle && context.canvas.getObjects().includes(circle)) {
|
|
128
|
+
context.canvas.bringObjectToFront(circle);
|
|
129
|
+
}
|
|
130
|
+
return previewImage;
|
|
131
|
+
}
|
|
132
|
+
function removePreviewImage(context, session) {
|
|
133
|
+
const image = session.previewImage;
|
|
134
|
+
if (!image)
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
context.canvas.remove(image);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
}
|
|
141
|
+
session.previewImage = null;
|
|
142
|
+
}
|
|
143
|
+
function releaseMosaicRasterCache(session) {
|
|
144
|
+
const cache = session.rasterCache;
|
|
145
|
+
if (!cache)
|
|
146
|
+
return;
|
|
147
|
+
try {
|
|
148
|
+
cache.offscreenCanvas.width = 0;
|
|
149
|
+
cache.offscreenCanvas.height = 0;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
}
|
|
153
|
+
session.rasterCache = null;
|
|
154
|
+
}
|
|
155
|
+
function hidePreview(context) {
|
|
156
|
+
var _a;
|
|
157
|
+
const circle = (_a = context.getMosaicSession()) === null || _a === void 0 ? void 0 : _a.previewCircle;
|
|
158
|
+
if (!circle)
|
|
159
|
+
return;
|
|
160
|
+
circle.set({ visible: false });
|
|
161
|
+
safeRender(context.canvas);
|
|
162
|
+
}
|
|
163
|
+
function movePreview(context, point) {
|
|
164
|
+
const session = context.getMosaicSession();
|
|
165
|
+
if (!session)
|
|
166
|
+
return;
|
|
167
|
+
const circle = ensurePreviewCircle(context, session);
|
|
168
|
+
circle.set({ left: point.x, top: point.y, visible: true });
|
|
169
|
+
safeRender(context.canvas);
|
|
170
|
+
}
|
|
171
|
+
function attachCanvasHandler(context, session, eventName, callback) {
|
|
172
|
+
context.canvas.on(eventName, callback);
|
|
173
|
+
session.handlers.push({ eventName, callback });
|
|
174
|
+
}
|
|
175
|
+
function detachCanvasHandlers(context, session) {
|
|
176
|
+
for (const record of session.handlers) {
|
|
177
|
+
try {
|
|
178
|
+
context.canvas.off(record.eventName, record.callback);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
session.handlers = [];
|
|
184
|
+
}
|
|
185
|
+
function restoreObjectStates(session) {
|
|
186
|
+
for (const record of session.prevObjectStates) {
|
|
187
|
+
try {
|
|
188
|
+
record.object.set({ evented: record.evented, selectable: record.selectable });
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
session.prevObjectStates = [];
|
|
194
|
+
}
|
|
195
|
+
function getImageSource(image) {
|
|
196
|
+
var _a;
|
|
197
|
+
const imageWithSource = image;
|
|
198
|
+
try {
|
|
199
|
+
const src = (_a = imageWithSource.getSrc) === null || _a === void 0 ? void 0 : _a.call(imageWithSource);
|
|
200
|
+
if (typeof src === 'string' && src.length > 0)
|
|
201
|
+
return src;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
}
|
|
205
|
+
return typeof imageWithSource.src === 'string' && imageWithSource.src.length > 0
|
|
206
|
+
? imageWithSource.src
|
|
207
|
+
: null;
|
|
208
|
+
}
|
|
209
|
+
function imageDimension(value) {
|
|
210
|
+
const numeric = Number(value);
|
|
211
|
+
return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : 0;
|
|
212
|
+
}
|
|
213
|
+
function decodeImageSource(ownerDocument, source) {
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
const imageElement = ownerDocument.createElement('img');
|
|
216
|
+
const cleanup = () => {
|
|
217
|
+
if (typeof imageElement.removeEventListener === 'function') {
|
|
218
|
+
imageElement.removeEventListener('load', handleLoad);
|
|
219
|
+
imageElement.removeEventListener('error', handleError);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
imageElement.onload = null;
|
|
223
|
+
imageElement.onerror = null;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const handleLoad = () => {
|
|
227
|
+
const width = imageDimension(imageElement.naturalWidth || imageElement.width);
|
|
228
|
+
const height = imageDimension(imageElement.naturalHeight || imageElement.height);
|
|
229
|
+
cleanup();
|
|
230
|
+
if (width <= 0 || height <= 0) {
|
|
231
|
+
reject(new Error('Mosaic image decode failed: source image has no dimensions.'));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
resolve({ element: imageElement, width, height });
|
|
235
|
+
};
|
|
236
|
+
const handleError = (event) => {
|
|
237
|
+
cleanup();
|
|
238
|
+
const message = typeof event === 'string'
|
|
239
|
+
? `Mosaic image decode failed: ${event}`
|
|
240
|
+
: 'Mosaic image decode failed.';
|
|
241
|
+
reject(new Error(message));
|
|
242
|
+
};
|
|
243
|
+
if (!source.startsWith('data:')) {
|
|
244
|
+
imageElement.crossOrigin = 'anonymous';
|
|
245
|
+
}
|
|
246
|
+
if (typeof imageElement.addEventListener === 'function') {
|
|
247
|
+
imageElement.addEventListener('load', handleLoad, { once: true });
|
|
248
|
+
imageElement.addEventListener('error', handleError, { once: true });
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
imageElement.onload = handleLoad;
|
|
252
|
+
imageElement.onerror = handleError;
|
|
253
|
+
}
|
|
254
|
+
imageElement.src = source;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function toSupportedMimeType(mimeType) {
|
|
258
|
+
return mimeType === 'image/jpeg' || mimeType === 'image/png' || mimeType === 'image/webp'
|
|
259
|
+
? mimeType
|
|
260
|
+
: null;
|
|
261
|
+
}
|
|
262
|
+
function mimeToFormat(mimeType) {
|
|
263
|
+
if (mimeType === 'image/jpeg')
|
|
264
|
+
return 'jpeg';
|
|
265
|
+
if (mimeType === 'image/webp')
|
|
266
|
+
return 'webp';
|
|
267
|
+
return 'png';
|
|
268
|
+
}
|
|
269
|
+
function resolveMosaicOutputFormat(context, source) {
|
|
270
|
+
var _a, _b, _c, _d;
|
|
271
|
+
const config = context.getMosaicConfig();
|
|
272
|
+
const requested = config.outputFileType;
|
|
273
|
+
const format = requested === 'source'
|
|
274
|
+
? mimeToFormat((_b = (_a = context.getCurrentImageMimeType()) !== null && _a !== void 0 ? _a : toSupportedMimeType(detectSourceMimeType(source))) !== null && _b !== void 0 ? _b : 'image/png')
|
|
275
|
+
: ((_c = tryNormalizeImageFormat(String(requested))) !== null && _c !== void 0 ? _c : 'png');
|
|
276
|
+
const mimeType = mimeTypeFor(format);
|
|
277
|
+
if (format === 'png')
|
|
278
|
+
return { mimeType };
|
|
279
|
+
return {
|
|
280
|
+
mimeType,
|
|
281
|
+
quality: (_d = config.outputQuality) !== null && _d !== void 0 ? _d : context.options.downsampleQuality,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function createFabricImageFromDataUrl(context, dataUrl) {
|
|
285
|
+
return await withTimeout(context.fabric.FabricImage.fromURL(dataUrl, { crossOrigin: 'anonymous' }), context.options.imageLoadTimeoutMs, 'Mosaic FabricImage.fromURL');
|
|
286
|
+
}
|
|
287
|
+
function copyBaseImageProperties(target, source) {
|
|
288
|
+
target.set({
|
|
289
|
+
left: source.left,
|
|
290
|
+
top: source.top,
|
|
291
|
+
scaleX: source.scaleX,
|
|
292
|
+
scaleY: source.scaleY,
|
|
293
|
+
angle: source.angle,
|
|
294
|
+
skewX: source.skewX,
|
|
295
|
+
skewY: source.skewY,
|
|
296
|
+
flipX: source.flipX,
|
|
297
|
+
flipY: source.flipY,
|
|
298
|
+
originX: source.originX,
|
|
299
|
+
originY: source.originY,
|
|
300
|
+
selectable: source.selectable,
|
|
301
|
+
evented: source.evented,
|
|
302
|
+
hasControls: source.hasControls,
|
|
303
|
+
hoverCursor: source.hoverCursor,
|
|
304
|
+
});
|
|
305
|
+
target.setCoords();
|
|
306
|
+
}
|
|
307
|
+
function replaceBaseImage(context, oldImage, newImage, mimeType) {
|
|
308
|
+
const { canvas } = context;
|
|
309
|
+
let oldRemoved = false;
|
|
310
|
+
let newAdded = false;
|
|
311
|
+
try {
|
|
312
|
+
copyBaseImageProperties(newImage, oldImage);
|
|
313
|
+
canvas.remove(oldImage);
|
|
314
|
+
oldRemoved = true;
|
|
315
|
+
canvas.add(newImage);
|
|
316
|
+
newAdded = true;
|
|
317
|
+
canvas.sendObjectToBack(newImage);
|
|
318
|
+
context.setOriginalImage(markBaseImageObject(newImage));
|
|
319
|
+
context.setCurrentImageMimeType(mimeType);
|
|
320
|
+
canvas.renderAll();
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
try {
|
|
324
|
+
if (newAdded)
|
|
325
|
+
canvas.remove(newImage);
|
|
326
|
+
if (oldRemoved && !canvas.getObjects().includes(oldImage)) {
|
|
327
|
+
canvas.add(oldImage);
|
|
328
|
+
canvas.sendObjectToBack(oldImage);
|
|
329
|
+
}
|
|
330
|
+
context.setOriginalImage(oldImage);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
}
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function pushMosaicHistory(context, after) {
|
|
338
|
+
var _a;
|
|
339
|
+
const before = (_a = context.getLastSnapshot()) !== null && _a !== void 0 ? _a : after;
|
|
340
|
+
if (!before || !after || before === after)
|
|
341
|
+
return;
|
|
342
|
+
context.historyManager.push(new Command(async () => {
|
|
343
|
+
await context.loadFromState(after);
|
|
344
|
+
}, async () => {
|
|
345
|
+
await context.loadFromState(before);
|
|
346
|
+
}));
|
|
347
|
+
context.setLastSnapshot(after);
|
|
348
|
+
}
|
|
349
|
+
async function getOrCreateRasterCache(context, session, source) {
|
|
350
|
+
if (session.rasterCache)
|
|
351
|
+
return session.rasterCache;
|
|
352
|
+
const ownerDocument = getCanvasDocument(context);
|
|
353
|
+
const decoded = await decodeImageSource(ownerDocument, source);
|
|
354
|
+
const offscreenCanvas = ownerDocument.createElement('canvas');
|
|
355
|
+
offscreenCanvas.width = decoded.width;
|
|
356
|
+
offscreenCanvas.height = decoded.height;
|
|
357
|
+
const renderingContext = offscreenCanvas.getContext('2d');
|
|
358
|
+
if (!renderingContext) {
|
|
359
|
+
reportError(context.options, new Error('Mosaic could not obtain a 2D canvas context.'), 'Mosaic apply failed.');
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
renderingContext.drawImage(decoded.element, 0, 0, decoded.width, decoded.height);
|
|
363
|
+
let imageData;
|
|
364
|
+
try {
|
|
365
|
+
imageData = renderingContext.getImageData(0, 0, decoded.width, decoded.height);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
reportError(context.options, error, 'Mosaic apply failed because the source image pixels could not be read.');
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
const rasterCache = {
|
|
372
|
+
offscreenCanvas,
|
|
373
|
+
renderingContext,
|
|
374
|
+
imageData,
|
|
375
|
+
source,
|
|
376
|
+
width: decoded.width,
|
|
377
|
+
height: decoded.height,
|
|
378
|
+
};
|
|
379
|
+
session.rasterCache = rasterCache;
|
|
380
|
+
return rasterCache;
|
|
381
|
+
}
|
|
382
|
+
function applyMosaicImagePoint(context, session, sourceImage, imagePoint) {
|
|
383
|
+
const rasterCache = session.rasterCache;
|
|
384
|
+
if (!rasterCache)
|
|
385
|
+
return false;
|
|
386
|
+
const config = context.getMosaicConfig();
|
|
387
|
+
const previousPoint = session.lastImagePoint;
|
|
388
|
+
const points = previousPoint
|
|
389
|
+
? interpolateMosaicPoints(previousPoint, imagePoint)
|
|
390
|
+
: [imagePoint];
|
|
391
|
+
let changed = false;
|
|
392
|
+
for (const point of points) {
|
|
393
|
+
changed =
|
|
394
|
+
applyCircularMosaicToImageData({
|
|
395
|
+
imageData: rasterCache.imageData,
|
|
396
|
+
centerX: point.sourceX,
|
|
397
|
+
centerY: point.sourceY,
|
|
398
|
+
radius: point.sourceRadius,
|
|
399
|
+
blockSize: config.blockSize,
|
|
400
|
+
}) || changed;
|
|
401
|
+
}
|
|
402
|
+
session.lastImagePoint = imagePoint;
|
|
403
|
+
if (changed) {
|
|
404
|
+
session.hasUncommittedChanges = true;
|
|
405
|
+
rasterCache.renderingContext.putImageData(rasterCache.imageData, 0, 0);
|
|
406
|
+
ensurePreviewImage(context, session, sourceImage);
|
|
407
|
+
safeRender(context.canvas);
|
|
408
|
+
}
|
|
409
|
+
return changed;
|
|
410
|
+
}
|
|
411
|
+
function interpolateMosaicPoints(start, end) {
|
|
412
|
+
const dx = end.sourceX - start.sourceX;
|
|
413
|
+
const dy = end.sourceY - start.sourceY;
|
|
414
|
+
const distance = Math.hypot(dx, dy);
|
|
415
|
+
const minRadius = Math.min(start.sourceRadius, end.sourceRadius);
|
|
416
|
+
const spacing = Math.max(1, minRadius / 2);
|
|
417
|
+
const steps = Math.max(1, Math.ceil(distance / spacing));
|
|
418
|
+
const points = [];
|
|
419
|
+
for (let index = 1; index <= steps; index += 1) {
|
|
420
|
+
const t = index / steps;
|
|
421
|
+
points.push({
|
|
422
|
+
sourceX: start.sourceX + dx * t,
|
|
423
|
+
sourceY: start.sourceY + dy * t,
|
|
424
|
+
sourceRadius: start.sourceRadius + (end.sourceRadius - start.sourceRadius) * t,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return points;
|
|
428
|
+
}
|
|
429
|
+
async function applyMosaicPointToCache(context, expectedSession, canvasPoint) {
|
|
430
|
+
const session = context.getMosaicSession();
|
|
431
|
+
if (!session || session !== expectedSession)
|
|
432
|
+
return;
|
|
433
|
+
const originalImage = context.getOriginalImage();
|
|
434
|
+
if (!originalImage || !context.isImageLoaded())
|
|
435
|
+
return;
|
|
436
|
+
const config = context.getMosaicConfig();
|
|
437
|
+
const imagePoint = getMosaicImagePoint(context.fabric, originalImage, canvasPoint, config.brushSize);
|
|
438
|
+
if (!imagePoint) {
|
|
439
|
+
session.lastImagePoint = null;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const source = getImageSource(originalImage);
|
|
443
|
+
if (!source) {
|
|
444
|
+
reportWarning(context.options, new Error('Mosaic cannot read the current image source.'), 'Mosaic skipped because the image source is unavailable.');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const rasterCache = await getOrCreateRasterCache(context, session, source);
|
|
448
|
+
if (!rasterCache)
|
|
449
|
+
return;
|
|
450
|
+
applyMosaicImagePoint(context, session, originalImage, imagePoint);
|
|
451
|
+
}
|
|
452
|
+
async function commitMosaicChanges(context, session, callbackContext) {
|
|
453
|
+
var _a;
|
|
454
|
+
session.commitRequested = false;
|
|
455
|
+
session.lastImagePoint = null;
|
|
456
|
+
if (!session.hasUncommittedChanges || !session.rasterCache)
|
|
457
|
+
return;
|
|
458
|
+
const originalImage = context.getOriginalImage();
|
|
459
|
+
if (!originalImage || !context.isImageLoaded())
|
|
460
|
+
return;
|
|
461
|
+
const source = (_a = getImageSource(originalImage)) !== null && _a !== void 0 ? _a : session.rasterCache.source;
|
|
462
|
+
const rasterCache = session.rasterCache;
|
|
463
|
+
rasterCache.renderingContext.putImageData(rasterCache.imageData, 0, 0);
|
|
464
|
+
const output = resolveMosaicOutputFormat(context, source);
|
|
465
|
+
const nextDataUrl = output.quality === undefined
|
|
466
|
+
? rasterCache.offscreenCanvas.toDataURL(output.mimeType)
|
|
467
|
+
: rasterCache.offscreenCanvas.toDataURL(output.mimeType, output.quality);
|
|
468
|
+
const nextImage = await createFabricImageFromDataUrl(context, nextDataUrl);
|
|
469
|
+
removePreviewCircle(context, session);
|
|
470
|
+
removePreviewImage(context, session);
|
|
471
|
+
try {
|
|
472
|
+
replaceBaseImage(context, originalImage, nextImage, output.mimeType);
|
|
473
|
+
const after = context.captureSnapshot();
|
|
474
|
+
pushMosaicHistory(context, after);
|
|
475
|
+
rasterCache.source = nextDataUrl;
|
|
476
|
+
session.hasUncommittedChanges = false;
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
if (context.getMosaicSession() === session) {
|
|
480
|
+
ensurePreviewCircle(context, session);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
context.updateInputs();
|
|
484
|
+
context.updateUi();
|
|
485
|
+
context.emitImageChanged(callbackContext);
|
|
486
|
+
}
|
|
487
|
+
async function drainMosaicQueue(context, expectedSession) {
|
|
488
|
+
const session = context.getMosaicSession();
|
|
489
|
+
if (!session || session !== expectedSession || session.isApplying)
|
|
490
|
+
return;
|
|
491
|
+
session.isApplying = true;
|
|
492
|
+
const callbackContext = context.buildCallbackContext('applyMosaic', false);
|
|
493
|
+
context.emitBusyChangeIfChanged(callbackContext);
|
|
494
|
+
context.updateUi();
|
|
495
|
+
try {
|
|
496
|
+
while (context.getMosaicSession() === session && session.pendingCanvasPoints.length > 0) {
|
|
497
|
+
const point = session.pendingCanvasPoints.shift();
|
|
498
|
+
if (point) {
|
|
499
|
+
await applyMosaicPointToCache(context, session, point);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (context.getMosaicSession() === session && session.commitRequested) {
|
|
503
|
+
await commitMosaicChanges(context, session, callbackContext);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
finally {
|
|
507
|
+
if (context.getMosaicSession() === session) {
|
|
508
|
+
session.isApplying = false;
|
|
509
|
+
}
|
|
510
|
+
context.emitBusyChangeIfChanged(callbackContext);
|
|
511
|
+
context.updateUi();
|
|
512
|
+
if (context.getMosaicSession() === session &&
|
|
513
|
+
(session.pendingCanvasPoints.length > 0 || session.commitRequested)) {
|
|
514
|
+
void drainMosaicQueue(context, session).catch((error) => {
|
|
515
|
+
reportError(context.options, error, 'Mosaic apply failed.');
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function enqueueMosaicPoint(context, canvasPoint) {
|
|
521
|
+
const session = context.getMosaicSession();
|
|
522
|
+
if (!session)
|
|
523
|
+
return;
|
|
524
|
+
session.pendingCanvasPoints.push(canvasPoint);
|
|
525
|
+
if (session.pendingCanvasPoints.length > MAX_PENDING_MOSAIC_POINTS) {
|
|
526
|
+
session.pendingCanvasPoints.splice(0, session.pendingCanvasPoints.length - MAX_PENDING_MOSAIC_POINTS);
|
|
527
|
+
}
|
|
528
|
+
void drainMosaicQueue(context, session).catch((error) => {
|
|
529
|
+
reportError(context.options, error, 'Mosaic apply failed.');
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
function requestMosaicCommit(context, session) {
|
|
533
|
+
session.commitRequested = true;
|
|
534
|
+
void drainMosaicQueue(context, session).catch((error) => {
|
|
535
|
+
reportError(context.options, error, 'Mosaic apply failed.');
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
function installMosaicHandlers(context, session) {
|
|
539
|
+
attachCanvasHandler(context, session, 'mouse:move', (event) => {
|
|
540
|
+
const pointer = getPointerFromFabricEvent(context.canvas, event);
|
|
541
|
+
if (!pointer) {
|
|
542
|
+
hidePreview(context);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
movePreview(context, pointer);
|
|
546
|
+
const currentSession = context.getMosaicSession();
|
|
547
|
+
if (currentSession === null || currentSession === void 0 ? void 0 : currentSession.isPointerDown) {
|
|
548
|
+
enqueueMosaicPoint(context, pointer);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
attachCanvasHandler(context, session, 'mouse:out', () => {
|
|
552
|
+
hidePreview(context);
|
|
553
|
+
const currentSession = context.getMosaicSession();
|
|
554
|
+
if (currentSession === null || currentSession === void 0 ? void 0 : currentSession.isPointerDown) {
|
|
555
|
+
currentSession.isPointerDown = false;
|
|
556
|
+
requestMosaicCommit(context, currentSession);
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
attachCanvasHandler(context, session, 'mouse:down', (event) => {
|
|
560
|
+
const pointer = getPointerFromFabricEvent(context.canvas, event);
|
|
561
|
+
if (!pointer)
|
|
562
|
+
return;
|
|
563
|
+
const currentSession = context.getMosaicSession();
|
|
564
|
+
if (!currentSession)
|
|
565
|
+
return;
|
|
566
|
+
currentSession.isPointerDown = true;
|
|
567
|
+
currentSession.lastImagePoint = null;
|
|
568
|
+
enqueueMosaicPoint(context, pointer);
|
|
569
|
+
});
|
|
570
|
+
attachCanvasHandler(context, session, 'mouse:up', (event) => {
|
|
571
|
+
const currentSession = context.getMosaicSession();
|
|
572
|
+
if (!currentSession)
|
|
573
|
+
return;
|
|
574
|
+
const pointer = getPointerFromFabricEvent(context.canvas, event);
|
|
575
|
+
if (pointer) {
|
|
576
|
+
movePreview(context, pointer);
|
|
577
|
+
enqueueMosaicPoint(context, pointer);
|
|
578
|
+
}
|
|
579
|
+
currentSession.isPointerDown = false;
|
|
580
|
+
requestMosaicCommit(context, currentSession);
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
export function enterMosaicMode(context) {
|
|
584
|
+
if (context.getMosaicSession())
|
|
585
|
+
return;
|
|
586
|
+
if (!context.isImageLoaded() || !context.getOriginalImage())
|
|
587
|
+
return;
|
|
588
|
+
const { canvas } = context;
|
|
589
|
+
context.hideAllMaskLabels();
|
|
590
|
+
canvas.discardActiveObject();
|
|
591
|
+
const prevSelection = !!canvas.selection;
|
|
592
|
+
const prevDefaultCursor = canvas.defaultCursor;
|
|
593
|
+
const prevObjectStates = canvas.getObjects().map((object) => {
|
|
594
|
+
var _a, _b;
|
|
595
|
+
return ({
|
|
596
|
+
object,
|
|
597
|
+
evented: (_a = object.evented) !== null && _a !== void 0 ? _a : true,
|
|
598
|
+
selectable: (_b = object.selectable) !== null && _b !== void 0 ? _b : true,
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
for (const record of prevObjectStates) {
|
|
602
|
+
try {
|
|
603
|
+
record.object.set({ evented: false, selectable: false });
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
canvas.selection = false;
|
|
609
|
+
canvas.defaultCursor = 'crosshair';
|
|
610
|
+
const session = {
|
|
611
|
+
previewCircle: null,
|
|
612
|
+
previewImage: null,
|
|
613
|
+
prevSelection,
|
|
614
|
+
prevDefaultCursor,
|
|
615
|
+
prevObjectStates,
|
|
616
|
+
handlers: [],
|
|
617
|
+
rasterCache: null,
|
|
618
|
+
pendingCanvasPoints: [],
|
|
619
|
+
isPointerDown: false,
|
|
620
|
+
isApplying: false,
|
|
621
|
+
commitRequested: false,
|
|
622
|
+
hasUncommittedChanges: false,
|
|
623
|
+
lastImagePoint: null,
|
|
624
|
+
};
|
|
625
|
+
context.setMosaicSession(session);
|
|
626
|
+
ensurePreviewCircle(context, session);
|
|
627
|
+
installMosaicHandlers(context, session);
|
|
628
|
+
canvas.renderAll();
|
|
629
|
+
}
|
|
630
|
+
export function exitMosaicMode(context) {
|
|
631
|
+
var _a;
|
|
632
|
+
const session = context.getMosaicSession();
|
|
633
|
+
if (!session)
|
|
634
|
+
return;
|
|
635
|
+
detachCanvasHandlers(context, session);
|
|
636
|
+
removePreviewCircle(context, session);
|
|
637
|
+
removePreviewImage(context, session);
|
|
638
|
+
releaseMosaicRasterCache(session);
|
|
639
|
+
restoreObjectStates(session);
|
|
640
|
+
context.canvas.selection = !!session.prevSelection;
|
|
641
|
+
context.canvas.defaultCursor = (_a = session.prevDefaultCursor) !== null && _a !== void 0 ? _a : 'default';
|
|
642
|
+
context.setMosaicSession(null);
|
|
643
|
+
context.canvas.renderAll();
|
|
644
|
+
}
|
|
645
|
+
export function updateMosaicPreview(context) {
|
|
646
|
+
const session = context.getMosaicSession();
|
|
647
|
+
const circle = session === null || session === void 0 ? void 0 : session.previewCircle;
|
|
648
|
+
if (!session || !circle)
|
|
649
|
+
return;
|
|
650
|
+
const config = context.getMosaicConfig();
|
|
651
|
+
circle.set({
|
|
652
|
+
radius: config.brushSize / 2,
|
|
653
|
+
fill: config.previewFill,
|
|
654
|
+
stroke: config.previewStroke,
|
|
655
|
+
strokeWidth: config.previewStrokeWidth,
|
|
656
|
+
strokeDashArray: config.previewStrokeDashArray
|
|
657
|
+
? [...config.previewStrokeDashArray]
|
|
658
|
+
: undefined,
|
|
659
|
+
});
|
|
660
|
+
context.canvas.bringObjectToFront(circle);
|
|
661
|
+
safeRender(context.canvas);
|
|
662
|
+
}
|
|
663
|
+
export function isMosaicPreviewObject(object) {
|
|
664
|
+
return object.isMosaicPreview === true;
|
|
665
|
+
}
|
|
666
|
+
//# sourceMappingURL=mosaic-controller.js.map
|