@overtone-art/canvas-editor-core 0.2.6 → 0.2.7
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/LICENSE +21 -0
- package/dist/chunk-NINRTPOJ.mjs +104 -0
- package/dist/chunk-NINRTPOJ.mjs.map +1 -0
- package/dist/index.d.mts +144 -225
- package/dist/index.d.ts +144 -225
- package/dist/index.global.js +506 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +1109 -94
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1020 -92
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +37 -0
- package/dist/node.d.ts +37 -0
- package/dist/node.js +281 -0
- package/dist/node.js.map +1 -0
- package/dist/node.mjs +233 -0
- package/dist/node.mjs.map +1 -0
- package/dist/types-bOY7oYVq.d.mts +383 -0
- package/dist/types-bOY7oYVq.d.ts +383 -0
- package/package.json +13 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeCoverPlacement,
|
|
3
|
+
computePrintAreaClip,
|
|
4
|
+
exportDataURL,
|
|
5
|
+
exportMockup,
|
|
6
|
+
exportPNG,
|
|
7
|
+
exportSVG
|
|
8
|
+
} from "./chunk-NINRTPOJ.mjs";
|
|
9
|
+
|
|
1
10
|
// src/editor.ts
|
|
2
|
-
import { Canvas, FabricImage as FabricImage2, Textbox } from "fabric";
|
|
11
|
+
import { Canvas, FabricImage as FabricImage2, Group, Textbox, filters, loadSVGFromString, util as util3 } from "fabric";
|
|
3
12
|
|
|
4
13
|
// src/events.ts
|
|
5
14
|
var EventEmitter = class {
|
|
@@ -91,6 +100,10 @@ var LayerManager = class {
|
|
|
91
100
|
canvas;
|
|
92
101
|
events;
|
|
93
102
|
layers = [];
|
|
103
|
+
onPropertyChanged;
|
|
104
|
+
setHistoryCallback(callback) {
|
|
105
|
+
this.onPropertyChanged = callback;
|
|
106
|
+
}
|
|
94
107
|
add(type, fabricObject, name, id) {
|
|
95
108
|
const layer = new Layer(type, fabricObject, name, id);
|
|
96
109
|
this.layers.push(layer);
|
|
@@ -107,12 +120,39 @@ var LayerManager = class {
|
|
|
107
120
|
this.layers.splice(index, 1);
|
|
108
121
|
this.events.emit("layer:removed", { layerId: id });
|
|
109
122
|
this.emitChanged();
|
|
123
|
+
this.onPropertyChanged?.();
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
/** Replace a layer's render object while preserving its immutable ID and panel state. */
|
|
127
|
+
replaceObject(id, fabricObject) {
|
|
128
|
+
const layer = this.get(id);
|
|
129
|
+
if (!layer || layer.fabricObject === fabricObject) return false;
|
|
130
|
+
const previous = layer.fabricObject;
|
|
131
|
+
const stackIndex = this.canvas.getObjects().indexOf(previous);
|
|
132
|
+
const wasActive = this.canvas.getActiveObject() === previous;
|
|
133
|
+
this.canvas.remove(previous);
|
|
134
|
+
layer.fabricObject = fabricObject;
|
|
135
|
+
fabricObject._layerId = id;
|
|
136
|
+
fabricObject.set({
|
|
137
|
+
visible: layer.visible,
|
|
138
|
+
opacity: layer.opacity,
|
|
139
|
+
selectable: !layer.locked,
|
|
140
|
+
evented: !layer.locked
|
|
141
|
+
});
|
|
142
|
+
this.canvas.insertAt(Math.max(0, stackIndex), fabricObject);
|
|
143
|
+
if (wasActive) this.canvas.setActiveObject(fabricObject);
|
|
144
|
+
this.canvas.requestRenderAll();
|
|
145
|
+
this.events.emit("layer:modified", { layerId: id });
|
|
146
|
+
this.emitChanged();
|
|
147
|
+
this.onPropertyChanged?.();
|
|
110
148
|
return true;
|
|
111
149
|
}
|
|
112
150
|
reorder(id, newIndex) {
|
|
113
151
|
const oldIndex = this.layers.findIndex((l) => l.id === id);
|
|
114
152
|
if (oldIndex === -1) return false;
|
|
115
|
-
|
|
153
|
+
if (!Number.isFinite(newIndex)) return false;
|
|
154
|
+
const clamped = Math.max(0, Math.min(this.layers.length - 1, Math.round(newIndex)));
|
|
155
|
+
if (oldIndex === clamped) return false;
|
|
116
156
|
const [layer] = this.layers.splice(oldIndex, 1);
|
|
117
157
|
this.layers.splice(clamped, 0, layer);
|
|
118
158
|
this.layers.forEach((l, i) => {
|
|
@@ -120,6 +160,7 @@ var LayerManager = class {
|
|
|
120
160
|
});
|
|
121
161
|
this.events.emit("layer:reordered", { layerIds: this.layers.map((l) => l.id) });
|
|
122
162
|
this.emitChanged();
|
|
163
|
+
this.onPropertyChanged?.();
|
|
123
164
|
return true;
|
|
124
165
|
}
|
|
125
166
|
select(id) {
|
|
@@ -150,33 +191,42 @@ var LayerManager = class {
|
|
|
150
191
|
setVisibility(id, visible) {
|
|
151
192
|
const layer = this.get(id);
|
|
152
193
|
if (!layer) return;
|
|
194
|
+
if (layer.visible === visible) return;
|
|
153
195
|
layer.visible = visible;
|
|
154
196
|
layer.fabricObject.visible = visible;
|
|
155
197
|
this.canvas.requestRenderAll();
|
|
156
198
|
this.emitChanged();
|
|
199
|
+
this.onPropertyChanged?.();
|
|
157
200
|
}
|
|
158
201
|
setLocked(id, locked) {
|
|
159
202
|
const layer = this.get(id);
|
|
160
203
|
if (!layer) return;
|
|
204
|
+
if (layer.locked === locked) return;
|
|
161
205
|
layer.locked = locked;
|
|
162
206
|
layer.fabricObject.selectable = !locked;
|
|
163
207
|
layer.fabricObject.evented = !locked;
|
|
164
208
|
this.canvas.requestRenderAll();
|
|
165
209
|
this.emitChanged();
|
|
210
|
+
this.onPropertyChanged?.();
|
|
166
211
|
}
|
|
167
212
|
setOpacity(id, opacity) {
|
|
168
213
|
const layer = this.get(id);
|
|
169
|
-
if (!layer) return;
|
|
170
|
-
|
|
171
|
-
layer.
|
|
214
|
+
if (!layer || !Number.isFinite(opacity)) return;
|
|
215
|
+
const next = Math.max(0, Math.min(1, opacity));
|
|
216
|
+
if (layer.opacity === next) return;
|
|
217
|
+
layer.opacity = next;
|
|
218
|
+
layer.fabricObject.opacity = next;
|
|
172
219
|
this.canvas.requestRenderAll();
|
|
173
220
|
this.emitChanged();
|
|
221
|
+
this.onPropertyChanged?.();
|
|
174
222
|
}
|
|
175
223
|
setName(id, name) {
|
|
176
224
|
const layer = this.get(id);
|
|
177
225
|
if (!layer) return;
|
|
226
|
+
if (layer.name === name) return;
|
|
178
227
|
layer.name = name;
|
|
179
228
|
this.emitChanged();
|
|
229
|
+
this.onPropertyChanged?.();
|
|
180
230
|
}
|
|
181
231
|
clear() {
|
|
182
232
|
for (const layer of this.layers) {
|
|
@@ -200,57 +250,124 @@ var HistoryManager = class {
|
|
|
200
250
|
undoStack = [];
|
|
201
251
|
redoStack = [];
|
|
202
252
|
maxSize;
|
|
253
|
+
maxBytes;
|
|
203
254
|
paused = false;
|
|
204
255
|
debounceTimer = null;
|
|
205
256
|
debounceMs;
|
|
206
257
|
getState;
|
|
207
258
|
restoreState;
|
|
208
259
|
events;
|
|
260
|
+
transactionDepth = 0;
|
|
261
|
+
transactionDirty = false;
|
|
209
262
|
constructor(opts) {
|
|
210
263
|
this.getState = opts.getState;
|
|
211
264
|
this.restoreState = opts.restoreState;
|
|
212
265
|
this.events = opts.events;
|
|
213
266
|
this.maxSize = opts.maxSize ?? 50;
|
|
267
|
+
this.maxBytes = opts.maxBytes ?? 50 * 1024 * 1024;
|
|
214
268
|
this.debounceMs = opts.debounceMs ?? 300;
|
|
215
269
|
}
|
|
216
270
|
save() {
|
|
217
271
|
if (this.paused) return;
|
|
272
|
+
if (this.transactionDepth > 0) {
|
|
273
|
+
this.transactionDirty = true;
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
218
276
|
if (this.debounceTimer) {
|
|
219
277
|
clearTimeout(this.debounceTimer);
|
|
220
278
|
}
|
|
221
279
|
this.debounceTimer = setTimeout(() => {
|
|
222
280
|
this.saveImmediate();
|
|
223
281
|
}, this.debounceMs);
|
|
282
|
+
this.emitChanged();
|
|
224
283
|
}
|
|
225
284
|
saveImmediate() {
|
|
226
|
-
if (this.paused)
|
|
285
|
+
if (this.paused) {
|
|
286
|
+
this.cancelPending();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (this.transactionDepth > 0) {
|
|
290
|
+
this.transactionDirty = true;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.cancelPending();
|
|
227
294
|
const state = this.getState();
|
|
295
|
+
if (this.undoStack.at(-1) === state) {
|
|
296
|
+
this.emitChanged();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
228
299
|
this.undoStack.push(state);
|
|
229
|
-
|
|
300
|
+
while (this.undoStack.length > this.maxSize) {
|
|
230
301
|
this.undoStack.shift();
|
|
231
302
|
}
|
|
232
303
|
this.redoStack = [];
|
|
304
|
+
this.trimToBudget();
|
|
305
|
+
this.events.emit("history:snapshot", {
|
|
306
|
+
bytes: state.length * 2,
|
|
307
|
+
totalBytes: this.snapshotBytes(),
|
|
308
|
+
entries: this.undoStack.length
|
|
309
|
+
});
|
|
233
310
|
this.emitChanged();
|
|
234
311
|
}
|
|
312
|
+
beginTransaction() {
|
|
313
|
+
if (this.transactionDepth === 0 && this.debounceTimer) this.saveImmediate();
|
|
314
|
+
this.transactionDepth += 1;
|
|
315
|
+
}
|
|
316
|
+
endTransaction() {
|
|
317
|
+
if (this.transactionDepth === 0) return;
|
|
318
|
+
this.transactionDepth -= 1;
|
|
319
|
+
if (this.transactionDepth === 0 && this.transactionDirty) {
|
|
320
|
+
this.transactionDirty = false;
|
|
321
|
+
this.saveImmediate();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async transaction(operation) {
|
|
325
|
+
this.beginTransaction();
|
|
326
|
+
try {
|
|
327
|
+
return await operation();
|
|
328
|
+
} finally {
|
|
329
|
+
this.endTransaction();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
235
332
|
async undo() {
|
|
236
333
|
this.cancelPending();
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
334
|
+
const current = this.getState();
|
|
335
|
+
const committed = this.undoStack.at(-1);
|
|
336
|
+
if (!committed) return;
|
|
337
|
+
const currentIsCommitted = current === committed;
|
|
338
|
+
if (currentIsCommitted && this.undoStack.length < 2) return;
|
|
339
|
+
const target = currentIsCommitted ? this.undoStack[this.undoStack.length - 2] : committed;
|
|
240
340
|
this.paused = true;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
341
|
+
try {
|
|
342
|
+
await this.restoreState(target);
|
|
343
|
+
if (currentIsCommitted) this.undoStack.pop();
|
|
344
|
+
this.redoStack.push(current);
|
|
345
|
+
this.trimToBudget();
|
|
346
|
+
} catch (error) {
|
|
347
|
+
this.events.emit("error", { message: "Failed to undo the last change", error });
|
|
348
|
+
throw error;
|
|
349
|
+
} finally {
|
|
350
|
+
this.paused = false;
|
|
351
|
+
this.emitChanged();
|
|
352
|
+
}
|
|
244
353
|
}
|
|
245
354
|
async redo() {
|
|
246
355
|
this.cancelPending();
|
|
247
|
-
const state = this.redoStack.
|
|
356
|
+
const state = this.redoStack.at(-1);
|
|
248
357
|
if (!state) return;
|
|
249
|
-
this.undoStack.push(this.getState());
|
|
250
358
|
this.paused = true;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
359
|
+
try {
|
|
360
|
+
await this.restoreState(state);
|
|
361
|
+
this.redoStack.pop();
|
|
362
|
+
if (this.undoStack.at(-1) !== state) this.undoStack.push(state);
|
|
363
|
+
this.trimToBudget();
|
|
364
|
+
} catch (error) {
|
|
365
|
+
this.events.emit("error", { message: "Failed to redo the last change", error });
|
|
366
|
+
throw error;
|
|
367
|
+
} finally {
|
|
368
|
+
this.paused = false;
|
|
369
|
+
this.emitChanged();
|
|
370
|
+
}
|
|
254
371
|
}
|
|
255
372
|
/** True while a restore (undo/redo/deserialize) is in flight. Managers that
|
|
256
373
|
* react to canvas events should skip mutating history during this window. */
|
|
@@ -264,16 +381,20 @@ var HistoryManager = class {
|
|
|
264
381
|
this.paused = false;
|
|
265
382
|
}
|
|
266
383
|
canUndo() {
|
|
267
|
-
return this.undoStack.length >
|
|
384
|
+
return this.undoStack.length > 1 || this.debounceTimer !== null;
|
|
268
385
|
}
|
|
269
386
|
canRedo() {
|
|
270
387
|
return this.redoStack.length > 0;
|
|
271
388
|
}
|
|
272
389
|
clear() {
|
|
390
|
+
this.cancelPending();
|
|
273
391
|
this.undoStack = [];
|
|
274
392
|
this.redoStack = [];
|
|
275
393
|
this.emitChanged();
|
|
276
394
|
}
|
|
395
|
+
getSnapshotBytes() {
|
|
396
|
+
return this.snapshotBytes();
|
|
397
|
+
}
|
|
277
398
|
cancelPending() {
|
|
278
399
|
if (this.debounceTimer) {
|
|
279
400
|
clearTimeout(this.debounceTimer);
|
|
@@ -281,9 +402,9 @@ var HistoryManager = class {
|
|
|
281
402
|
}
|
|
282
403
|
}
|
|
283
404
|
dispose() {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
405
|
+
this.cancelPending();
|
|
406
|
+
this.transactionDepth = 0;
|
|
407
|
+
this.transactionDirty = false;
|
|
287
408
|
}
|
|
288
409
|
emitChanged() {
|
|
289
410
|
this.events.emit("history:changed", {
|
|
@@ -291,6 +412,20 @@ var HistoryManager = class {
|
|
|
291
412
|
canRedo: this.canRedo()
|
|
292
413
|
});
|
|
293
414
|
}
|
|
415
|
+
snapshotBytes() {
|
|
416
|
+
return [...this.undoStack, ...this.redoStack].reduce(
|
|
417
|
+
(total, state) => total + state.length * 2,
|
|
418
|
+
0
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
trimToBudget() {
|
|
422
|
+
while (this.undoStack.length > 1 && this.snapshotBytes() > this.maxBytes) {
|
|
423
|
+
this.undoStack.shift();
|
|
424
|
+
}
|
|
425
|
+
while (this.redoStack.length > 1 && this.snapshotBytes() > this.maxBytes) {
|
|
426
|
+
this.redoStack.shift();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
294
429
|
};
|
|
295
430
|
|
|
296
431
|
// src/snapping.ts
|
|
@@ -510,6 +645,10 @@ var CropController = class {
|
|
|
510
645
|
top: imgTop + (cropY - (image.cropY ?? 0)) * scaleY
|
|
511
646
|
});
|
|
512
647
|
image.setCoords();
|
|
648
|
+
if (s.prevAngle) {
|
|
649
|
+
image.rotate(s.prevAngle);
|
|
650
|
+
image.setCoords();
|
|
651
|
+
}
|
|
513
652
|
this.finish();
|
|
514
653
|
this.history.save();
|
|
515
654
|
}
|
|
@@ -544,14 +683,18 @@ var STROKE2 = "#22c55e";
|
|
|
544
683
|
import { util } from "fabric";
|
|
545
684
|
var MAX_TILES_PER_AXIS = 200;
|
|
546
685
|
var PatternManager = class {
|
|
547
|
-
constructor(canvas, layers, history) {
|
|
686
|
+
constructor(canvas, layers, history, events, sourceResolver) {
|
|
548
687
|
this.canvas = canvas;
|
|
549
688
|
this.layers = layers;
|
|
550
689
|
this.history = history;
|
|
690
|
+
this.events = events;
|
|
691
|
+
this.sourceResolver = sourceResolver;
|
|
551
692
|
}
|
|
552
693
|
canvas;
|
|
553
694
|
layers;
|
|
554
695
|
history;
|
|
696
|
+
events;
|
|
697
|
+
sourceResolver;
|
|
555
698
|
// Per-layer task chain. apply()/disable() both await an async setSrc on the
|
|
556
699
|
// same fabric image; running two concurrently lets their setSrc resolutions
|
|
557
700
|
// interleave (wrong image installed, original lost). Serialising per layer
|
|
@@ -599,6 +742,9 @@ var PatternManager = class {
|
|
|
599
742
|
throw err;
|
|
600
743
|
}
|
|
601
744
|
this.history.save();
|
|
745
|
+
}).catch((error) => {
|
|
746
|
+
this.events.emit("error", { message: "Failed to apply image pattern", error });
|
|
747
|
+
throw error;
|
|
602
748
|
});
|
|
603
749
|
}
|
|
604
750
|
/**
|
|
@@ -654,6 +800,9 @@ var PatternManager = class {
|
|
|
654
800
|
delete layer.meta.pattern;
|
|
655
801
|
this.canvas.requestRenderAll();
|
|
656
802
|
this.history.save();
|
|
803
|
+
}).catch((error) => {
|
|
804
|
+
this.events.emit("error", { message: "Failed to clear image pattern", error });
|
|
805
|
+
throw error;
|
|
657
806
|
});
|
|
658
807
|
}
|
|
659
808
|
/** Run `task` after any in-flight work for this layer, regardless of outcome. */
|
|
@@ -683,7 +832,8 @@ var PatternManager = class {
|
|
|
683
832
|
cw,
|
|
684
833
|
ch,
|
|
685
834
|
tileW,
|
|
686
|
-
tileH
|
|
835
|
+
tileH,
|
|
836
|
+
this.sourceResolver
|
|
687
837
|
);
|
|
688
838
|
await image.setSrc(dataUrl);
|
|
689
839
|
image.set({
|
|
@@ -754,14 +904,23 @@ function elementToDataURL(image) {
|
|
|
754
904
|
}
|
|
755
905
|
var IMAGE_CACHE_MAX = 16;
|
|
756
906
|
var imageCache = /* @__PURE__ */ new Map();
|
|
757
|
-
function
|
|
907
|
+
function loadPatternImage(src, resolver) {
|
|
758
908
|
const cached = imageCache.get(src);
|
|
759
909
|
if (cached) {
|
|
760
910
|
imageCache.delete(src);
|
|
761
911
|
imageCache.set(src, cached);
|
|
762
912
|
return cached;
|
|
763
913
|
}
|
|
764
|
-
const promise = decodeImage(src)
|
|
914
|
+
const promise = decodeImage(src).catch(async (originalError) => {
|
|
915
|
+
if (!resolver) throw originalError;
|
|
916
|
+
const resolved = await resolver(src);
|
|
917
|
+
if (!resolved || resolved === src) {
|
|
918
|
+
throw new Error("Pattern source resolver did not return a usable alternate URL", {
|
|
919
|
+
cause: originalError
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
return decodeImage(resolved);
|
|
923
|
+
});
|
|
765
924
|
promise.catch(() => {
|
|
766
925
|
if (imageCache.get(src) === promise) imageCache.delete(src);
|
|
767
926
|
});
|
|
@@ -784,8 +943,8 @@ function decodeImage(src) {
|
|
|
784
943
|
function clearPatternImageCache() {
|
|
785
944
|
imageCache.clear();
|
|
786
945
|
}
|
|
787
|
-
async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH) {
|
|
788
|
-
const img = await
|
|
946
|
+
async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH, sourceResolver) {
|
|
947
|
+
const img = await loadPatternImage(src, sourceResolver);
|
|
789
948
|
const off = document.createElement("canvas");
|
|
790
949
|
off.width = Math.max(1, Math.round(targetW));
|
|
791
950
|
off.height = Math.max(1, Math.round(targetH));
|
|
@@ -845,16 +1004,18 @@ function mod2(n) {
|
|
|
845
1004
|
// src/utils/units.ts
|
|
846
1005
|
var MM_PER_INCH = 25.4;
|
|
847
1006
|
var UnitConverter = class {
|
|
1007
|
+
unit;
|
|
1008
|
+
dpi;
|
|
848
1009
|
constructor(unit = "px", dpi = 72) {
|
|
849
1010
|
this.unit = unit;
|
|
850
|
-
this.dpi =
|
|
1011
|
+
this.dpi = 72;
|
|
1012
|
+
this.setDpi(dpi);
|
|
851
1013
|
}
|
|
852
|
-
unit;
|
|
853
|
-
dpi;
|
|
854
1014
|
setUnit(unit) {
|
|
855
1015
|
this.unit = unit;
|
|
856
1016
|
}
|
|
857
1017
|
setDpi(dpi) {
|
|
1018
|
+
if (!Number.isFinite(dpi) || dpi <= 0) throw new Error("DPI must be a positive number");
|
|
858
1019
|
this.dpi = dpi;
|
|
859
1020
|
}
|
|
860
1021
|
getUnit() {
|
|
@@ -889,38 +1050,72 @@ var UnitConverter = class {
|
|
|
889
1050
|
|
|
890
1051
|
// src/serialization.ts
|
|
891
1052
|
import { util as util2 } from "fabric";
|
|
892
|
-
|
|
1053
|
+
|
|
1054
|
+
// src/utils/color.ts
|
|
1055
|
+
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
1056
|
+
function isCssColor(value, allowEmpty = false) {
|
|
1057
|
+
if (typeof value !== "string") return false;
|
|
1058
|
+
const trimmed = value.trim();
|
|
1059
|
+
if (!trimmed) return allowEmpty;
|
|
1060
|
+
return CSS_COLOR.test(trimmed);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/serialization.ts
|
|
1064
|
+
var VERSION = "2.0.0";
|
|
893
1065
|
function serializeEditor(editor) {
|
|
894
1066
|
return {
|
|
895
1067
|
version: VERSION,
|
|
896
1068
|
canvas: {
|
|
897
1069
|
width: editor.canvas.getWidth(),
|
|
898
|
-
height: editor.canvas.getHeight()
|
|
1070
|
+
height: editor.canvas.getHeight(),
|
|
1071
|
+
unit: editor.units.getUnit(),
|
|
1072
|
+
dpi: editor.units.getDpi()
|
|
899
1073
|
},
|
|
900
1074
|
layers: editor.layers.getAll().map((layer) => layer.serialize()),
|
|
901
1075
|
// The configured design background, not the live canvas value (which is
|
|
902
1076
|
// forced transparent while a mockup preview is active).
|
|
903
1077
|
background: editor.getDesignBackground(),
|
|
1078
|
+
backgroundImage: editor.getDesignBackgroundImage() ? editor.getDesignBackgroundImage().toObject() : null,
|
|
904
1079
|
mockup: editor.getMockup()
|
|
905
1080
|
};
|
|
906
1081
|
}
|
|
907
1082
|
async function deserializeEditor(editor, state) {
|
|
1083
|
+
if (!state || !state.canvas || !Array.isArray(state.layers)) {
|
|
1084
|
+
throw new Error("Invalid editor state");
|
|
1085
|
+
}
|
|
1086
|
+
if (!Number.isFinite(state.canvas.width) || !Number.isFinite(state.canvas.height) || state.canvas.width <= 0 || state.canvas.height <= 0 || state.canvas.unit !== void 0 && !["px", "mm", "in"].includes(state.canvas.unit) || state.canvas.dpi !== void 0 && (!Number.isFinite(state.canvas.dpi) || state.canvas.dpi <= 0)) {
|
|
1087
|
+
throw new Error("Invalid editor canvas settings");
|
|
1088
|
+
}
|
|
1089
|
+
const major = Number.parseInt(state.version?.split(".")[0] ?? "1", 10);
|
|
1090
|
+
if (!Number.isFinite(major) || major > 2) {
|
|
1091
|
+
throw new Error(`Unsupported editor state version: ${state.version}`);
|
|
1092
|
+
}
|
|
1093
|
+
if (state.background !== void 0 && !isCssColor(state.background, true)) {
|
|
1094
|
+
throw new Error("Invalid editor background color");
|
|
1095
|
+
}
|
|
1096
|
+
const staged = await Promise.all(
|
|
1097
|
+
state.layers.map(async (serialized) => ({
|
|
1098
|
+
serialized,
|
|
1099
|
+
fabricObject: (await util2.enlivenObjects([serialized.fabricObject]))[0]
|
|
1100
|
+
}))
|
|
1101
|
+
);
|
|
1102
|
+
const stagedBackground = state.backgroundImage ? (await util2.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
1103
|
+
editor.crop.cancel();
|
|
908
1104
|
editor.layers.clear();
|
|
1105
|
+
if (state.canvas.unit) editor.units.setUnit(state.canvas.unit);
|
|
1106
|
+
if (state.canvas.dpi !== void 0) editor.units.setDpi(state.canvas.dpi);
|
|
909
1107
|
editor.canvas.setDimensions({ width: state.canvas.width, height: state.canvas.height });
|
|
910
|
-
if (state.background) {
|
|
1108
|
+
if (state.background !== void 0) {
|
|
911
1109
|
editor.setBackground(state.background);
|
|
912
1110
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
await restoreLayer(editor, serializedLayer);
|
|
1111
|
+
editor.setBackgroundImageObject(stagedBackground, false);
|
|
1112
|
+
editor.setMockup(state.mockup ?? null);
|
|
1113
|
+
for (const item of staged) {
|
|
1114
|
+
restoreLayer(editor, item.serialized, item.fabricObject);
|
|
918
1115
|
}
|
|
919
1116
|
editor.canvas.requestRenderAll();
|
|
920
1117
|
}
|
|
921
|
-
|
|
922
|
-
const objects = await util2.enlivenObjects([serialized.fabricObject]);
|
|
923
|
-
const fabricObject = objects[0];
|
|
1118
|
+
function restoreLayer(editor, serialized, fabricObject) {
|
|
924
1119
|
const layer = editor.layers.add(serialized.type, fabricObject, serialized.name, serialized.id);
|
|
925
1120
|
if (serialized.meta) {
|
|
926
1121
|
layer.meta = serialized.meta;
|
|
@@ -937,23 +1132,318 @@ async function restoreLayer(editor, serialized) {
|
|
|
937
1132
|
return layer;
|
|
938
1133
|
}
|
|
939
1134
|
|
|
940
|
-
// src/
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1135
|
+
// src/utils/svg.ts
|
|
1136
|
+
function escapeXml(value) {
|
|
1137
|
+
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1138
|
+
}
|
|
1139
|
+
function sanitizeSvg(svg) {
|
|
1140
|
+
const document2 = new DOMParser().parseFromString(svg, "image/svg+xml");
|
|
1141
|
+
if (document2.querySelector("parsererror")) throw new Error("Invalid template SVG");
|
|
1142
|
+
document2.querySelectorAll("script, foreignObject, iframe, object, embed, link, style").forEach((node) => node.remove());
|
|
1143
|
+
document2.querySelectorAll("*").forEach((node) => {
|
|
1144
|
+
for (const attribute of [...node.attributes]) {
|
|
1145
|
+
const name = attribute.name.toLowerCase();
|
|
1146
|
+
const value = attribute.value.trim().toLowerCase();
|
|
1147
|
+
const isLink = name === "href" || name === "xlink:href" || name === "src";
|
|
1148
|
+
const safeLink = value.startsWith("#") || /^data:image\/(?:png|jpeg|webp|gif);base64,/.test(value);
|
|
1149
|
+
if (name.startsWith("on") || isLink && !safeLink || /url\s*\(/.test(value) && !/url\s*\(\s*['"]?#/.test(value) || /(?:javascript:|expression\s*\()/.test(value)) {
|
|
1150
|
+
node.removeAttribute(attribute.name);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
947
1153
|
});
|
|
948
|
-
|
|
949
|
-
return response.blob();
|
|
1154
|
+
return new XMLSerializer().serializeToString(document2.documentElement);
|
|
950
1155
|
}
|
|
951
|
-
|
|
952
|
-
|
|
1156
|
+
|
|
1157
|
+
// src/fonts.ts
|
|
1158
|
+
function fontSource(source) {
|
|
1159
|
+
return /^(?:url|local)\(/.test(source.trim()) ? source : `url(${JSON.stringify(source)})`;
|
|
1160
|
+
}
|
|
1161
|
+
function mimeForSource(source) {
|
|
1162
|
+
const path = source.split(/[?#]/)[0].toLowerCase();
|
|
1163
|
+
if (path.endsWith(".woff2")) return "font/woff2";
|
|
1164
|
+
if (path.endsWith(".woff")) return "font/woff";
|
|
1165
|
+
if (path.endsWith(".otf")) return "font/otf";
|
|
1166
|
+
return "font/ttf";
|
|
1167
|
+
}
|
|
1168
|
+
function arrayBufferToBase64(buffer) {
|
|
1169
|
+
const bytes = new Uint8Array(buffer);
|
|
1170
|
+
let binary = "";
|
|
1171
|
+
for (let index = 0; index < bytes.length; index += 32768) {
|
|
1172
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
|
|
1173
|
+
}
|
|
1174
|
+
return btoa(binary);
|
|
1175
|
+
}
|
|
1176
|
+
function cssString(value) {
|
|
1177
|
+
return JSON.stringify(value).replace(/[<>&]/g, (char) => `\\${char.charCodeAt(0).toString(16)} `);
|
|
1178
|
+
}
|
|
1179
|
+
var SAFE_WEIGHT = /^(?:normal|bold|bolder|lighter|[1-9]\d{0,2}(?:\s+[1-9]\d{0,2})?)$/i;
|
|
1180
|
+
var SAFE_STYLE = /^(?:normal|italic|oblique(?:\s+-?\d+(?:\.\d+)?deg)?)$/i;
|
|
1181
|
+
var SAFE_DISPLAY = /^(?:auto|block|swap|fallback|optional)$/i;
|
|
1182
|
+
function cssKeyword(value, pattern, fallback) {
|
|
1183
|
+
const trimmed = value?.trim();
|
|
1184
|
+
return trimmed && pattern.test(trimmed) ? trimmed : fallback;
|
|
1185
|
+
}
|
|
1186
|
+
function sourceUrl(source) {
|
|
1187
|
+
const trimmed = source.trim();
|
|
1188
|
+
if (trimmed.startsWith("data:")) return trimmed;
|
|
1189
|
+
const match = trimmed.match(/^url\(\s*(['"]?)(.*?)\1\s*\)/i);
|
|
1190
|
+
if (match) return match[2];
|
|
1191
|
+
if (/^local\(/i.test(trimmed)) return null;
|
|
1192
|
+
return trimmed;
|
|
1193
|
+
}
|
|
1194
|
+
function localName(source) {
|
|
1195
|
+
const match = source.trim().match(/^local\(\s*(['"]?)([^)"'{};]*)\1\s*\)$/i);
|
|
1196
|
+
return match ? match[2].trim() || null : null;
|
|
1197
|
+
}
|
|
1198
|
+
var FontRegistry = class {
|
|
1199
|
+
definitions = /* @__PURE__ */ new Map();
|
|
1200
|
+
loads = /* @__PURE__ */ new Map();
|
|
1201
|
+
register(definition) {
|
|
1202
|
+
if (!definition.family.trim() || !definition.source.trim()) {
|
|
1203
|
+
throw new Error("Font family and source are required");
|
|
1204
|
+
}
|
|
1205
|
+
this.definitions.set(definition.family, { ...definition });
|
|
1206
|
+
this.loads.delete(definition.family);
|
|
1207
|
+
}
|
|
1208
|
+
unregister(family) {
|
|
1209
|
+
this.loads.delete(family);
|
|
1210
|
+
return this.definitions.delete(family);
|
|
1211
|
+
}
|
|
1212
|
+
getAll() {
|
|
1213
|
+
return [...this.definitions.values()].map((definition) => ({ ...definition }));
|
|
1214
|
+
}
|
|
1215
|
+
load(family) {
|
|
1216
|
+
const cached = this.loads.get(family);
|
|
1217
|
+
if (cached) return cached;
|
|
1218
|
+
const definition = this.definitions.get(family);
|
|
1219
|
+
if (!definition) return Promise.reject(new Error(`Font is not registered: ${family}`));
|
|
1220
|
+
if (typeof FontFace === "undefined" || typeof document === "undefined") {
|
|
1221
|
+
return Promise.reject(new Error("Font loading requires a browser FontFace API"));
|
|
1222
|
+
}
|
|
1223
|
+
const promise = new FontFace(definition.family, fontSource(definition.source), {
|
|
1224
|
+
weight: definition.weight,
|
|
1225
|
+
style: definition.style,
|
|
1226
|
+
display: definition.display
|
|
1227
|
+
}).load().then((font) => {
|
|
1228
|
+
document.fonts.add(font);
|
|
1229
|
+
return font;
|
|
1230
|
+
});
|
|
1231
|
+
promise.catch(() => {
|
|
1232
|
+
if (this.loads.get(family) === promise) this.loads.delete(family);
|
|
1233
|
+
});
|
|
1234
|
+
this.loads.set(family, promise);
|
|
1235
|
+
return promise;
|
|
1236
|
+
}
|
|
1237
|
+
async ready() {
|
|
1238
|
+
if (this.definitions.size === 0) return;
|
|
1239
|
+
await Promise.all([...this.definitions.keys()].map((family) => this.load(family)));
|
|
1240
|
+
await document.fonts.ready;
|
|
1241
|
+
}
|
|
1242
|
+
async getEmbeddedCss() {
|
|
1243
|
+
const rules = await Promise.all(
|
|
1244
|
+
this.getAll().map(async (definition) => {
|
|
1245
|
+
const url = sourceUrl(definition.source);
|
|
1246
|
+
let cssSource;
|
|
1247
|
+
if (url && !url.startsWith("data:")) {
|
|
1248
|
+
const response = await fetch(url);
|
|
1249
|
+
if (!response.ok) throw new Error(`Failed to fetch font: ${url}`);
|
|
1250
|
+
const data = arrayBufferToBase64(await response.arrayBuffer());
|
|
1251
|
+
const mime = response.headers.get("content-type") || mimeForSource(url);
|
|
1252
|
+
cssSource = `url(${cssString(`data:${mime};base64,${data}`)})`;
|
|
1253
|
+
} else if (url) {
|
|
1254
|
+
cssSource = `url(${cssString(url)})`;
|
|
1255
|
+
} else {
|
|
1256
|
+
const name = localName(definition.source);
|
|
1257
|
+
if (!name) {
|
|
1258
|
+
throw new Error(`Unsupported font source for embedding: ${definition.family}`);
|
|
1259
|
+
}
|
|
1260
|
+
cssSource = `local(${cssString(name)})`;
|
|
1261
|
+
}
|
|
1262
|
+
const weight = cssKeyword(definition.weight, SAFE_WEIGHT, "normal");
|
|
1263
|
+
const style = cssKeyword(definition.style, SAFE_STYLE, "normal");
|
|
1264
|
+
const display = cssKeyword(definition.display, SAFE_DISPLAY, "swap");
|
|
1265
|
+
return `@font-face{font-family:${cssString(definition.family)};src:${cssSource};font-weight:${weight};font-style:${style};font-display:${display}}`;
|
|
1266
|
+
})
|
|
1267
|
+
);
|
|
1268
|
+
return rules.join("\n");
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
// src/licensing.ts
|
|
1273
|
+
function domainMatches(hostname, pattern) {
|
|
1274
|
+
const host = hostname.toLowerCase();
|
|
1275
|
+
const expected = pattern.toLowerCase();
|
|
1276
|
+
if (expected.startsWith("*.")) {
|
|
1277
|
+
const suffix = expected.slice(1);
|
|
1278
|
+
return host.endsWith(suffix) && host.length > suffix.length;
|
|
1279
|
+
}
|
|
1280
|
+
return host === expected;
|
|
953
1281
|
}
|
|
954
|
-
function
|
|
955
|
-
return
|
|
1282
|
+
function isLocal(hostname) {
|
|
1283
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname.endsWith(".localhost");
|
|
956
1284
|
}
|
|
1285
|
+
var LicenseManager = class {
|
|
1286
|
+
constructor(config = {}) {
|
|
1287
|
+
this.config = config;
|
|
1288
|
+
const hostname = config.hostname ?? globalThis.location?.hostname ?? "localhost";
|
|
1289
|
+
const environment = config.environment ?? "development";
|
|
1290
|
+
if (environment !== "production" || isLocal(hostname)) {
|
|
1291
|
+
this.status = { state: "exempt", payload: null };
|
|
1292
|
+
this.readyPromise = Promise.resolve(this.status);
|
|
1293
|
+
} else if (!config.key) {
|
|
1294
|
+
this.status = { state: "community", payload: null };
|
|
1295
|
+
this.readyPromise = Promise.resolve(this.status);
|
|
1296
|
+
} else {
|
|
1297
|
+
this.status = { state: "checking", payload: null };
|
|
1298
|
+
this.readyPromise = this.validate(config.key, hostname);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
config;
|
|
1302
|
+
status;
|
|
1303
|
+
readyPromise;
|
|
1304
|
+
getStatus() {
|
|
1305
|
+
return this.status;
|
|
1306
|
+
}
|
|
1307
|
+
ready() {
|
|
1308
|
+
return this.readyPromise;
|
|
1309
|
+
}
|
|
1310
|
+
hasFeature(feature) {
|
|
1311
|
+
return this.status.state === "valid" && (this.status.payload.features ?? []).includes(feature);
|
|
1312
|
+
}
|
|
1313
|
+
track(name) {
|
|
1314
|
+
this.config.onUsage?.({
|
|
1315
|
+
name,
|
|
1316
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1317
|
+
licenseId: this.status.state === "valid" ? this.status.payload.id : void 0
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
async validate(key, hostname) {
|
|
1321
|
+
const payload = await this.config.verifyOffline?.(key) ?? null;
|
|
1322
|
+
if (!payload) return this.status = { state: "invalid", payload: null };
|
|
1323
|
+
if (payload.expiresAt && Date.parse(payload.expiresAt) < Date.now()) {
|
|
1324
|
+
return this.status = { state: "expired", payload };
|
|
1325
|
+
}
|
|
1326
|
+
if (!payload.domains.some((domain) => domainMatches(hostname, domain))) {
|
|
1327
|
+
return this.status = { state: "domain-mismatch", payload };
|
|
1328
|
+
}
|
|
1329
|
+
return this.status = { state: "valid", payload };
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
// src/project.ts
|
|
1334
|
+
var ProjectManager = class {
|
|
1335
|
+
constructor(editor) {
|
|
1336
|
+
this.editor = editor;
|
|
1337
|
+
const first = { id: generateId(), name: "Page 1", state: editor.toJSON() };
|
|
1338
|
+
this.pages = [first];
|
|
1339
|
+
this.activePageId = first.id;
|
|
1340
|
+
}
|
|
1341
|
+
editor;
|
|
1342
|
+
pages;
|
|
1343
|
+
activePageId;
|
|
1344
|
+
getAll() {
|
|
1345
|
+
return this.pages.map(({ id, name }) => ({ id, name }));
|
|
1346
|
+
}
|
|
1347
|
+
getActivePageId() {
|
|
1348
|
+
return this.activePageId;
|
|
1349
|
+
}
|
|
1350
|
+
add(name = `Page ${this.pages.length + 1}`, cloneCurrent = false) {
|
|
1351
|
+
this.saveCurrent();
|
|
1352
|
+
const source = structuredClone(this.editor.toJSON());
|
|
1353
|
+
const state = cloneCurrent ? source : this.blankState(source);
|
|
1354
|
+
const page = { id: generateId(), name, state };
|
|
1355
|
+
this.pages.push(page);
|
|
1356
|
+
this.emitChanged();
|
|
1357
|
+
return page.id;
|
|
1358
|
+
}
|
|
1359
|
+
async switchTo(id) {
|
|
1360
|
+
if (id === this.activePageId) return true;
|
|
1361
|
+
const page = this.pages.find((candidate) => candidate.id === id);
|
|
1362
|
+
if (!page) return false;
|
|
1363
|
+
this.saveCurrent();
|
|
1364
|
+
await this.editor.fromJSON(structuredClone(page.state));
|
|
1365
|
+
this.activePageId = id;
|
|
1366
|
+
this.emitChanged();
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
async duplicate(id) {
|
|
1370
|
+
this.saveCurrent();
|
|
1371
|
+
const source = this.pages.find((page2) => page2.id === id);
|
|
1372
|
+
if (!source) return null;
|
|
1373
|
+
const page = {
|
|
1374
|
+
id: generateId(),
|
|
1375
|
+
name: `${source.name} copy`,
|
|
1376
|
+
state: structuredClone(source.state)
|
|
1377
|
+
};
|
|
1378
|
+
const index = this.pages.indexOf(source);
|
|
1379
|
+
this.pages.splice(index + 1, 0, page);
|
|
1380
|
+
this.emitChanged();
|
|
1381
|
+
return page.id;
|
|
1382
|
+
}
|
|
1383
|
+
async remove(id) {
|
|
1384
|
+
if (this.pages.length === 1) return false;
|
|
1385
|
+
const index = this.pages.findIndex((page) => page.id === id);
|
|
1386
|
+
if (index < 0) return false;
|
|
1387
|
+
if (id === this.activePageId) {
|
|
1388
|
+
const next = this.pages[index + 1] ?? this.pages[index - 1];
|
|
1389
|
+
await this.editor.fromJSON(structuredClone(next.state));
|
|
1390
|
+
this.activePageId = next.id;
|
|
1391
|
+
}
|
|
1392
|
+
this.pages.splice(index, 1);
|
|
1393
|
+
this.emitChanged();
|
|
1394
|
+
return true;
|
|
1395
|
+
}
|
|
1396
|
+
rename(id, name) {
|
|
1397
|
+
const page = this.pages.find((candidate) => candidate.id === id);
|
|
1398
|
+
const trimmed = name.trim();
|
|
1399
|
+
if (!page || !trimmed || page.name === trimmed) return false;
|
|
1400
|
+
page.name = trimmed;
|
|
1401
|
+
this.emitChanged();
|
|
1402
|
+
return true;
|
|
1403
|
+
}
|
|
1404
|
+
reorder(id, newIndex) {
|
|
1405
|
+
const index = this.pages.findIndex((page2) => page2.id === id);
|
|
1406
|
+
if (index < 0 || !Number.isFinite(newIndex)) return false;
|
|
1407
|
+
const target = Math.max(0, Math.min(this.pages.length - 1, Math.round(newIndex)));
|
|
1408
|
+
if (target === index) return false;
|
|
1409
|
+
const [page] = this.pages.splice(index, 1);
|
|
1410
|
+
this.pages.splice(target, 0, page);
|
|
1411
|
+
this.emitChanged();
|
|
1412
|
+
return true;
|
|
1413
|
+
}
|
|
1414
|
+
toJSON() {
|
|
1415
|
+
this.saveCurrent();
|
|
1416
|
+
return {
|
|
1417
|
+
version: "1.0.0",
|
|
1418
|
+
activePageId: this.activePageId,
|
|
1419
|
+
pages: structuredClone(this.pages)
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
async fromJSON(project) {
|
|
1423
|
+
if (project?.version !== "1.0.0" || !Array.isArray(project.pages) || project.pages.length === 0 || !project.pages.some((page) => page.id === project.activePageId)) {
|
|
1424
|
+
throw new Error("Invalid project state");
|
|
1425
|
+
}
|
|
1426
|
+
const pages = structuredClone(project.pages);
|
|
1427
|
+
const active = pages.find((page) => page.id === project.activePageId);
|
|
1428
|
+
await this.editor.fromJSON(structuredClone(active.state));
|
|
1429
|
+
this.pages = pages;
|
|
1430
|
+
this.activePageId = active.id;
|
|
1431
|
+
this.emitChanged();
|
|
1432
|
+
}
|
|
1433
|
+
saveCurrent() {
|
|
1434
|
+
const page = this.pages.find((candidate) => candidate.id === this.activePageId);
|
|
1435
|
+
if (page) page.state = structuredClone(this.editor.toJSON());
|
|
1436
|
+
}
|
|
1437
|
+
blankState(source) {
|
|
1438
|
+
return { ...source, layers: [], mockup: null };
|
|
1439
|
+
}
|
|
1440
|
+
emitChanged() {
|
|
1441
|
+
this.editor.events.emit("project:changed", {
|
|
1442
|
+
activePageId: this.activePageId,
|
|
1443
|
+
pages: this.getAll()
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
957
1447
|
|
|
958
1448
|
// src/editor.ts
|
|
959
1449
|
var MIN_ZOOM = 0.1;
|
|
@@ -967,6 +1457,9 @@ var CanvasEditor = class {
|
|
|
967
1457
|
snapping;
|
|
968
1458
|
crop;
|
|
969
1459
|
patterns;
|
|
1460
|
+
fonts;
|
|
1461
|
+
licensing;
|
|
1462
|
+
pages;
|
|
970
1463
|
fileAdapter;
|
|
971
1464
|
imageProvider;
|
|
972
1465
|
zoomLevel = 1;
|
|
@@ -975,8 +1468,12 @@ var CanvasEditor = class {
|
|
|
975
1468
|
// transparent while a mockup preview is shown, so this is the source of truth
|
|
976
1469
|
// for serialization and export — not the (possibly transient) canvas value.
|
|
977
1470
|
designBackground;
|
|
1471
|
+
designBackgroundImage = null;
|
|
978
1472
|
constructor(canvasElement, config) {
|
|
979
1473
|
this.events = new EventEmitter();
|
|
1474
|
+
this.fonts = new FontRegistry();
|
|
1475
|
+
config.fonts?.forEach((font) => this.fonts.register(font));
|
|
1476
|
+
this.licensing = new LicenseManager(config.license);
|
|
980
1477
|
this.units = new UnitConverter(config.unit ?? "px", config.dpi ?? 72);
|
|
981
1478
|
const widthPx = this.units.toPixels(config.width);
|
|
982
1479
|
const heightPx = this.units.toPixels(config.height);
|
|
@@ -999,18 +1496,73 @@ var CanvasEditor = class {
|
|
|
999
1496
|
},
|
|
1000
1497
|
events: this.events
|
|
1001
1498
|
});
|
|
1499
|
+
this.layers.setHistoryCallback(() => this.history.save());
|
|
1002
1500
|
this.snapping = new SnapManager(this.canvas, this.events);
|
|
1003
1501
|
this.crop = new CropController(this.canvas, this.history, this.events);
|
|
1004
|
-
this.patterns = new PatternManager(
|
|
1502
|
+
this.patterns = new PatternManager(
|
|
1503
|
+
this.canvas,
|
|
1504
|
+
this.layers,
|
|
1505
|
+
this.history,
|
|
1506
|
+
this.events,
|
|
1507
|
+
config.patternSourceResolver
|
|
1508
|
+
);
|
|
1005
1509
|
this.setupCanvasEvents();
|
|
1006
1510
|
this.history.saveImmediate();
|
|
1511
|
+
this.pages = new ProjectManager(this);
|
|
1007
1512
|
}
|
|
1008
1513
|
// ─── Layer Operations ────────────────────────────────
|
|
1009
1514
|
async addImage(url, options) {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1515
|
+
try {
|
|
1516
|
+
const img = await FabricImage2.fromURL(
|
|
1517
|
+
url,
|
|
1518
|
+
{},
|
|
1519
|
+
{ originX: "left", originY: "top", ...options }
|
|
1520
|
+
);
|
|
1521
|
+
const layer = this.layers.add("image", img);
|
|
1522
|
+
this.history.save();
|
|
1523
|
+
return layer;
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
this.events.emit("error", { message: "Failed to add image", error });
|
|
1526
|
+
throw error;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/** Replace an image source without changing its layer identity or visual transform. */
|
|
1530
|
+
async replaceImageSource(layerId, url) {
|
|
1531
|
+
const layer = this.layers.get(layerId);
|
|
1532
|
+
if (!layer || layer.type !== "image") throw new Error(`Image layer not found: ${layerId}`);
|
|
1533
|
+
if (layer.meta.pattern) {
|
|
1534
|
+
throw new Error("Clear the pattern before replacing the image source");
|
|
1535
|
+
}
|
|
1536
|
+
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
1537
|
+
const previous = layer.fabricObject;
|
|
1538
|
+
try {
|
|
1539
|
+
const replacement = await FabricImage2.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
1540
|
+
replacement.set({
|
|
1541
|
+
left: previous.left,
|
|
1542
|
+
top: previous.top,
|
|
1543
|
+
originX: previous.originX,
|
|
1544
|
+
originY: previous.originY,
|
|
1545
|
+
width: previous.width,
|
|
1546
|
+
height: previous.height,
|
|
1547
|
+
cropX: previous.cropX,
|
|
1548
|
+
cropY: previous.cropY,
|
|
1549
|
+
scaleX: previous.scaleX,
|
|
1550
|
+
scaleY: previous.scaleY,
|
|
1551
|
+
angle: previous.angle,
|
|
1552
|
+
skewX: previous.skewX,
|
|
1553
|
+
skewY: previous.skewY,
|
|
1554
|
+
flipX: previous.flipX,
|
|
1555
|
+
flipY: previous.flipY
|
|
1556
|
+
});
|
|
1557
|
+
replacement.filters = [...previous.filters];
|
|
1558
|
+
replacement.applyFilters();
|
|
1559
|
+
replacement.setCoords();
|
|
1560
|
+
this.layers.replaceObject(layerId, replacement);
|
|
1561
|
+
return layer;
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
this.events.emit("error", { message: "Failed to replace image source", error });
|
|
1564
|
+
throw error;
|
|
1565
|
+
}
|
|
1014
1566
|
}
|
|
1015
1567
|
addText(text, options) {
|
|
1016
1568
|
const textbox = new Textbox(text, {
|
|
@@ -1033,27 +1585,42 @@ var CanvasEditor = class {
|
|
|
1033
1585
|
this.history.save();
|
|
1034
1586
|
return layer;
|
|
1035
1587
|
}
|
|
1036
|
-
async addTemplate(template,
|
|
1037
|
-
const
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1588
|
+
async addTemplate(template, params) {
|
|
1589
|
+
const values = {};
|
|
1590
|
+
for (const parameter of template.parameters) {
|
|
1591
|
+
const value = params[parameter.key] ?? parameter.default;
|
|
1592
|
+
if (value === void 0) throw new Error(`Missing template parameter: ${parameter.key}`);
|
|
1593
|
+
if (parameter.type === "number" && !Number.isFinite(Number(value))) {
|
|
1594
|
+
throw new Error(`Invalid number for template parameter: ${parameter.key}`);
|
|
1595
|
+
}
|
|
1596
|
+
if (parameter.type === "color" && !isCssColor(value)) {
|
|
1597
|
+
throw new Error(`Invalid color for template parameter: ${parameter.key}`);
|
|
1598
|
+
}
|
|
1599
|
+
values[parameter.key] = value;
|
|
1600
|
+
}
|
|
1601
|
+
const resolved = sanitizeSvg(
|
|
1602
|
+
template.svg.replace(/\{\{(\w+)(?:[|:]([^}]*))?\}\}/g, (token, key, fallback) => {
|
|
1603
|
+
const value = values[key] ?? fallback;
|
|
1604
|
+
return value === void 0 ? token : escapeXml(value);
|
|
1605
|
+
})
|
|
1050
1606
|
);
|
|
1607
|
+
const { objects, options } = await loadSVGFromString(resolved);
|
|
1608
|
+
const validObjects = objects.filter((object) => object !== null);
|
|
1609
|
+
if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
1610
|
+
const group = util3.groupSVGElements(validObjects, options);
|
|
1611
|
+
group.set({
|
|
1612
|
+
left: this.canvas.getWidth() / 2,
|
|
1613
|
+
top: this.canvas.getHeight() / 2,
|
|
1614
|
+
originX: "center",
|
|
1615
|
+
originY: "center"
|
|
1616
|
+
});
|
|
1617
|
+
const layer = this.layers.add("template", group, template.name);
|
|
1051
1618
|
this.history.save();
|
|
1052
1619
|
return layer;
|
|
1053
1620
|
}
|
|
1054
1621
|
removeLayer(id) {
|
|
1055
|
-
this.
|
|
1056
|
-
this.history.save();
|
|
1622
|
+
if (this.crop.activeLayerId() === id) this.crop.cancel();
|
|
1623
|
+
if (this.layers.remove(id)) this.history.save();
|
|
1057
1624
|
}
|
|
1058
1625
|
selectLayer(id) {
|
|
1059
1626
|
this.layers.select(id);
|
|
@@ -1088,35 +1655,173 @@ var CanvasEditor = class {
|
|
|
1088
1655
|
clone.setCoords();
|
|
1089
1656
|
const copy = this.layers.add(layer.type, clone, `${layer.name} copy`);
|
|
1090
1657
|
copy.meta = structuredClone(layer.meta);
|
|
1658
|
+
copy.visible = layer.visible;
|
|
1659
|
+
copy.locked = layer.locked;
|
|
1660
|
+
copy.opacity = layer.opacity;
|
|
1661
|
+
clone.set({
|
|
1662
|
+
visible: layer.visible,
|
|
1663
|
+
selectable: !layer.locked,
|
|
1664
|
+
evented: !layer.locked,
|
|
1665
|
+
opacity: layer.opacity
|
|
1666
|
+
});
|
|
1091
1667
|
this.canvas.setActiveObject(clone);
|
|
1092
1668
|
this.canvas.requestRenderAll();
|
|
1093
1669
|
this.history.save();
|
|
1094
1670
|
return copy;
|
|
1095
1671
|
}
|
|
1672
|
+
applyImageAdjustments(layerId, adjustments) {
|
|
1673
|
+
const layer = this.layers.get(layerId);
|
|
1674
|
+
if (!layer || layer.type !== "image") return false;
|
|
1675
|
+
const image = layer.fabricObject;
|
|
1676
|
+
const previous = layer.meta.imageAdjustments ?? {};
|
|
1677
|
+
const next = { ...previous, ...adjustments };
|
|
1678
|
+
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
1679
|
+
image.filters = [
|
|
1680
|
+
new filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
|
|
1681
|
+
new filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
|
|
1682
|
+
new filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
|
|
1683
|
+
new filters.Blur({ blur: clampAdjustment(next.blur, 0) })
|
|
1684
|
+
];
|
|
1685
|
+
layer.meta.imageAdjustments = next;
|
|
1686
|
+
image.applyFilters();
|
|
1687
|
+
this.canvas.requestRenderAll();
|
|
1688
|
+
this.history.save();
|
|
1689
|
+
return true;
|
|
1690
|
+
}
|
|
1691
|
+
/** Combine two or more layers into a single editable group layer. */
|
|
1692
|
+
async groupLayers(ids, name = "Group") {
|
|
1693
|
+
const uniqueIds = [...new Set(ids)];
|
|
1694
|
+
const children = uniqueIds.map((id) => this.layers.get(id)).filter((layer) => layer !== void 0);
|
|
1695
|
+
if (children.length < 2 || children.length !== uniqueIds.length) return null;
|
|
1696
|
+
return this.history.transaction(() => {
|
|
1697
|
+
const childData = children.map((layer) => structuredClone(layer.toData()));
|
|
1698
|
+
const objects = children.map((layer) => layer.fabricObject);
|
|
1699
|
+
for (const layer of children) this.layers.remove(layer.id);
|
|
1700
|
+
const group = new Group(objects);
|
|
1701
|
+
const grouped = this.layers.add("group", group, name);
|
|
1702
|
+
grouped.meta.groupChildren = childData;
|
|
1703
|
+
this.layers.select(grouped.id);
|
|
1704
|
+
this.history.save();
|
|
1705
|
+
return grouped;
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
/** Restore a group created by groupLayers back to its original layer records. */
|
|
1709
|
+
async ungroupLayer(id) {
|
|
1710
|
+
const grouped = this.layers.get(id);
|
|
1711
|
+
const childData = grouped?.meta.groupChildren;
|
|
1712
|
+
if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
|
|
1713
|
+
const group = grouped.fabricObject;
|
|
1714
|
+
return this.history.transaction(() => {
|
|
1715
|
+
const transform = group.calcTransformMatrix();
|
|
1716
|
+
const objects = group.removeAll();
|
|
1717
|
+
this.layers.remove(id);
|
|
1718
|
+
const restored = objects.map((object, index) => {
|
|
1719
|
+
util3.addTransformToObject(object, transform);
|
|
1720
|
+
object.setCoords();
|
|
1721
|
+
const data = childData[index];
|
|
1722
|
+
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
1723
|
+
if (data?.meta) layer.meta = structuredClone(data.meta);
|
|
1724
|
+
if (data && !data.visible) this.layers.setVisibility(layer.id, false);
|
|
1725
|
+
if (data?.locked) this.layers.setLocked(layer.id, true);
|
|
1726
|
+
if (data && data.opacity !== 1) this.layers.setOpacity(layer.id, data.opacity);
|
|
1727
|
+
return layer;
|
|
1728
|
+
});
|
|
1729
|
+
this.history.save();
|
|
1730
|
+
this.layers.select(restored[0]?.id ?? null);
|
|
1731
|
+
return restored;
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1096
1734
|
// ─── Serialization ──────────────────────────────────
|
|
1097
1735
|
toJSON() {
|
|
1098
1736
|
return serializeEditor(this);
|
|
1099
1737
|
}
|
|
1100
1738
|
async fromJSON(state) {
|
|
1101
|
-
|
|
1102
|
-
|
|
1739
|
+
const managedByHistory = this.history.isRestoring();
|
|
1740
|
+
if (!managedByHistory) {
|
|
1741
|
+
this.history.saveImmediate();
|
|
1742
|
+
this.history.pause();
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
await deserializeEditor(this, state);
|
|
1746
|
+
this.patterns.repinAll();
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
if (!managedByHistory) {
|
|
1749
|
+
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
1750
|
+
}
|
|
1751
|
+
throw error;
|
|
1752
|
+
} finally {
|
|
1753
|
+
if (!managedByHistory) this.history.resume();
|
|
1754
|
+
}
|
|
1755
|
+
if (!managedByHistory) {
|
|
1756
|
+
this.history.clear();
|
|
1757
|
+
this.history.saveImmediate();
|
|
1758
|
+
}
|
|
1103
1759
|
}
|
|
1104
1760
|
// ─── Export ──────────────────────────────────────────
|
|
1105
1761
|
async toPNG(options) {
|
|
1106
|
-
this.
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
return
|
|
1762
|
+
return this.toRaster(options?.format ?? "png", options);
|
|
1763
|
+
}
|
|
1764
|
+
async toJPEG(options) {
|
|
1765
|
+
return this.toRaster("jpeg", options);
|
|
1766
|
+
}
|
|
1767
|
+
async toWebP(options) {
|
|
1768
|
+
return this.toRaster("webp", options);
|
|
1769
|
+
}
|
|
1770
|
+
async toRaster(format, options) {
|
|
1771
|
+
this.events.emit("export:start", { format });
|
|
1772
|
+
try {
|
|
1773
|
+
await this.fonts.ready();
|
|
1774
|
+
const blob = await this.withDesignBackground(
|
|
1775
|
+
() => exportPNG(this.canvas, { ...options, format })
|
|
1776
|
+
);
|
|
1777
|
+
this.events.emit("export:complete", { format });
|
|
1778
|
+
this.licensing.track(`export:${format}`);
|
|
1779
|
+
return blob;
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
this.events.emit("error", { message: `Failed to export ${format.toUpperCase()}`, error });
|
|
1782
|
+
throw error;
|
|
1783
|
+
}
|
|
1110
1784
|
}
|
|
1111
1785
|
toSVG() {
|
|
1112
1786
|
this.events.emit("export:start", { format: "svg" });
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1787
|
+
try {
|
|
1788
|
+
const svg = this.withDesignBackground(() => exportSVG(this.canvas));
|
|
1789
|
+
this.events.emit("export:complete", { format: "svg" });
|
|
1790
|
+
this.licensing.track("export:svg");
|
|
1791
|
+
return svg;
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
this.events.emit("error", { message: "Failed to export SVG", error });
|
|
1794
|
+
throw error;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
async toSVGAsync(options = {}) {
|
|
1798
|
+
await this.fonts.ready();
|
|
1799
|
+
const svg = this.toSVG();
|
|
1800
|
+
if (options.embedFonts === false || this.fonts.getAll().length === 0) return svg;
|
|
1801
|
+
try {
|
|
1802
|
+
const css = await this.fonts.getEmbeddedCss();
|
|
1803
|
+
return svg.replace(/(<svg\b[^>]*>)/i, `$1<defs><style>${css}</style></defs>`);
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
this.events.emit("error", { message: "Failed to embed fonts in SVG", error });
|
|
1806
|
+
throw error;
|
|
1807
|
+
}
|
|
1116
1808
|
}
|
|
1117
1809
|
toDataURL(format = "png", multiplier = 1) {
|
|
1118
1810
|
return this.withDesignBackground(() => exportDataURL(this.canvas, format, multiplier));
|
|
1119
1811
|
}
|
|
1812
|
+
/** Export the current product-preview composite. Advanced warping is host-defined. */
|
|
1813
|
+
async toMockupImage(options = {}) {
|
|
1814
|
+
if (!this.mockup) throw new Error("No mockup is configured");
|
|
1815
|
+
await this.fonts.ready();
|
|
1816
|
+
try {
|
|
1817
|
+
const blob = await exportMockup(this.canvas, this.mockup, options);
|
|
1818
|
+
this.licensing.track(`export:mockup:${options.format ?? "png"}`);
|
|
1819
|
+
return blob;
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
this.events.emit("error", { message: "Failed to export mockup", error });
|
|
1822
|
+
throw error;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1120
1825
|
/**
|
|
1121
1826
|
* Run an export with the configured design background applied, even when a
|
|
1122
1827
|
* mockup preview has forced the live canvas transparent — so exports reflect
|
|
@@ -1125,11 +1830,14 @@ var CanvasEditor = class {
|
|
|
1125
1830
|
withDesignBackground(fn) {
|
|
1126
1831
|
if (!this.mockup) return fn();
|
|
1127
1832
|
const previewBg = this.canvas.backgroundColor;
|
|
1833
|
+
const previewImage = this.canvas.backgroundImage;
|
|
1128
1834
|
this.canvas.backgroundColor = this.designBackground;
|
|
1835
|
+
this.canvas.backgroundImage = this.designBackgroundImage ?? void 0;
|
|
1129
1836
|
try {
|
|
1130
1837
|
return fn();
|
|
1131
1838
|
} finally {
|
|
1132
1839
|
this.canvas.backgroundColor = previewBg;
|
|
1840
|
+
this.canvas.backgroundImage = previewImage;
|
|
1133
1841
|
}
|
|
1134
1842
|
}
|
|
1135
1843
|
toPrintifyPositioning() {
|
|
@@ -1144,7 +1852,8 @@ var CanvasEditor = class {
|
|
|
1144
1852
|
for (const layer of imageLayers) {
|
|
1145
1853
|
const obj = layer.fabricObject;
|
|
1146
1854
|
const center = obj.getCenterPoint();
|
|
1147
|
-
result[layer.name]
|
|
1855
|
+
const key = result[layer.name] ? `${layer.name}-${layer.id}` : layer.name;
|
|
1856
|
+
result[key] = {
|
|
1148
1857
|
x: round2(center.x / canvasW),
|
|
1149
1858
|
y: round2(center.y / canvasH),
|
|
1150
1859
|
scale: round2(obj.scaleX ?? 1),
|
|
@@ -1153,26 +1862,92 @@ var CanvasEditor = class {
|
|
|
1153
1862
|
}
|
|
1154
1863
|
return result;
|
|
1155
1864
|
}
|
|
1865
|
+
/** Provider-neutral, ID-stable placement data in 0..1 canvas coordinates. */
|
|
1866
|
+
toNormalizedPositioning() {
|
|
1867
|
+
const canvasWidth = this.canvas.getWidth();
|
|
1868
|
+
const canvasHeight = this.canvas.getHeight();
|
|
1869
|
+
return this.layers.getAll().map((layer) => {
|
|
1870
|
+
const object = layer.fabricObject;
|
|
1871
|
+
const center = object.getCenterPoint();
|
|
1872
|
+
const bounds = object.getBoundingRect();
|
|
1873
|
+
return {
|
|
1874
|
+
layerId: layer.id,
|
|
1875
|
+
name: layer.name,
|
|
1876
|
+
type: layer.type,
|
|
1877
|
+
centerX: round2(center.x / canvasWidth),
|
|
1878
|
+
centerY: round2(center.y / canvasHeight),
|
|
1879
|
+
width: round2(bounds.width / canvasWidth),
|
|
1880
|
+
height: round2(bounds.height / canvasHeight),
|
|
1881
|
+
scaleX: round2(object.scaleX ?? 1),
|
|
1882
|
+
scaleY: round2(object.scaleY ?? 1),
|
|
1883
|
+
angle: round2(object.angle ?? 0)
|
|
1884
|
+
};
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
toProviderPositioning(adapter) {
|
|
1888
|
+
return adapter.map(this.toNormalizedPositioning(), {
|
|
1889
|
+
width: this.canvas.getWidth(),
|
|
1890
|
+
height: this.canvas.getHeight()
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1156
1893
|
// ─── File Operations ────────────────────────────────
|
|
1157
1894
|
async save(filename, format) {
|
|
1158
1895
|
if (!this.fileAdapter) return void 0;
|
|
1159
1896
|
let data;
|
|
1160
1897
|
if (format === "png") {
|
|
1161
1898
|
data = await this.toPNG();
|
|
1899
|
+
} else if (format === "jpeg") {
|
|
1900
|
+
data = await this.toJPEG();
|
|
1901
|
+
} else if (format === "webp") {
|
|
1902
|
+
data = await this.toWebP();
|
|
1162
1903
|
} else if (format === "svg") {
|
|
1163
|
-
data = this.
|
|
1904
|
+
data = await this.toSVGAsync();
|
|
1164
1905
|
} else {
|
|
1165
1906
|
data = JSON.stringify(this.toJSON(), null, 2);
|
|
1166
1907
|
}
|
|
1167
|
-
|
|
1908
|
+
try {
|
|
1909
|
+
return await this.fileAdapter.save(data, filename, format);
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
this.events.emit("error", { message: `Failed to save ${format}`, error });
|
|
1912
|
+
throw error;
|
|
1913
|
+
}
|
|
1168
1914
|
}
|
|
1169
1915
|
async uploadImage(file) {
|
|
1170
|
-
if (!this.imageProvider) return void 0;
|
|
1171
|
-
|
|
1916
|
+
if (!this.imageProvider?.upload) return void 0;
|
|
1917
|
+
try {
|
|
1918
|
+
return await this.imageProvider.upload(file);
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
this.events.emit("error", { message: "Failed to upload image", error });
|
|
1921
|
+
throw error;
|
|
1922
|
+
}
|
|
1172
1923
|
}
|
|
1173
1924
|
async browseImages() {
|
|
1174
1925
|
if (!this.imageProvider?.browse) return null;
|
|
1175
|
-
|
|
1926
|
+
try {
|
|
1927
|
+
return await this.imageProvider.browse();
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
this.events.emit("error", { message: "Failed to browse images", error });
|
|
1930
|
+
throw error;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
async searchImages(query, options) {
|
|
1934
|
+
if (!this.imageProvider?.search) return null;
|
|
1935
|
+
try {
|
|
1936
|
+
return await this.imageProvider.search(query, options);
|
|
1937
|
+
} catch (error) {
|
|
1938
|
+
this.events.emit("error", { message: "Failed to search images", error });
|
|
1939
|
+
throw error;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
/** Track provider usage, then insert its hotlinked image as a normal layer. */
|
|
1943
|
+
async addProviderImage(image, options) {
|
|
1944
|
+
try {
|
|
1945
|
+
await this.imageProvider?.trackUse?.(image);
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
this.events.emit("error", { message: "Failed to record image use", error });
|
|
1948
|
+
throw error;
|
|
1949
|
+
}
|
|
1950
|
+
return this.addImage(image.url, options);
|
|
1176
1951
|
}
|
|
1177
1952
|
setFileAdapter(adapter) {
|
|
1178
1953
|
this.fileAdapter = adapter;
|
|
@@ -1188,15 +1963,130 @@ var CanvasEditor = class {
|
|
|
1188
1963
|
this.canvas.requestRenderAll();
|
|
1189
1964
|
}
|
|
1190
1965
|
this.history.save();
|
|
1966
|
+
this.events.emit("canvas:modified", {});
|
|
1191
1967
|
}
|
|
1192
1968
|
getDesignBackground() {
|
|
1193
1969
|
return this.designBackground;
|
|
1194
1970
|
}
|
|
1195
|
-
|
|
1971
|
+
getDesignBackgroundImage() {
|
|
1972
|
+
return this.designBackgroundImage;
|
|
1973
|
+
}
|
|
1974
|
+
async setBackgroundImage(url, options = {}) {
|
|
1975
|
+
if (url === null) {
|
|
1976
|
+
this.setBackgroundImageObject(null);
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
try {
|
|
1980
|
+
const image = await FabricImage2.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
1981
|
+
const width = image.width || 1;
|
|
1982
|
+
const height = image.height || 1;
|
|
1983
|
+
const canvasWidth = this.canvas.getWidth();
|
|
1984
|
+
const canvasHeight = this.canvas.getHeight();
|
|
1985
|
+
const fit = options.fit ?? "cover";
|
|
1986
|
+
const sx = canvasWidth / width;
|
|
1987
|
+
const sy = canvasHeight / height;
|
|
1988
|
+
const scaleX = fit === "stretch" ? sx : fit === "contain" ? Math.min(sx, sy) : Math.max(sx, sy);
|
|
1989
|
+
const scaleY = fit === "stretch" ? sy : scaleX;
|
|
1990
|
+
image.set({
|
|
1991
|
+
left: (canvasWidth - width * scaleX) / 2,
|
|
1992
|
+
top: (canvasHeight - height * scaleY) / 2,
|
|
1993
|
+
scaleX,
|
|
1994
|
+
scaleY,
|
|
1995
|
+
opacity: clamp(options.opacity ?? 1, 0, 1),
|
|
1996
|
+
selectable: false,
|
|
1997
|
+
evented: false
|
|
1998
|
+
});
|
|
1999
|
+
this.setBackgroundImageObject(image);
|
|
2000
|
+
} catch (error) {
|
|
2001
|
+
this.events.emit("error", { message: "Failed to set background image", error });
|
|
2002
|
+
throw error;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
/** Used by state restoration and advanced integrations with an existing Fabric object. */
|
|
2006
|
+
setBackgroundImageObject(image, save = true) {
|
|
2007
|
+
this.designBackgroundImage = image;
|
|
2008
|
+
this.canvas.backgroundImage = this.mockup ? void 0 : image ?? void 0;
|
|
2009
|
+
this.canvas.requestRenderAll();
|
|
2010
|
+
if (save) this.history.save();
|
|
2011
|
+
this.events.emit("canvas:modified", {});
|
|
2012
|
+
}
|
|
2013
|
+
setTransparentBackground() {
|
|
2014
|
+
this.setBackground("");
|
|
2015
|
+
}
|
|
2016
|
+
resize(width, height, options = {}) {
|
|
1196
2017
|
const widthPx = this.units.toPixels(width);
|
|
1197
2018
|
const heightPx = this.units.toPixels(height);
|
|
2019
|
+
const oldWidth = this.canvas.getWidth();
|
|
2020
|
+
const oldHeight = this.canvas.getHeight();
|
|
2021
|
+
const scaleContent = options.scaleContent ?? true;
|
|
2022
|
+
if (scaleContent && oldWidth > 0 && oldHeight > 0) {
|
|
2023
|
+
const sx = widthPx / oldWidth;
|
|
2024
|
+
const sy = heightPx / oldHeight;
|
|
2025
|
+
for (const layer of this.layers.getAll()) {
|
|
2026
|
+
if (layer.meta.pattern) continue;
|
|
2027
|
+
const object = layer.fabricObject;
|
|
2028
|
+
object.set({
|
|
2029
|
+
left: (object.left ?? 0) * sx,
|
|
2030
|
+
top: (object.top ?? 0) * sy,
|
|
2031
|
+
scaleX: (object.scaleX ?? 1) * sx,
|
|
2032
|
+
scaleY: (object.scaleY ?? 1) * sy
|
|
2033
|
+
});
|
|
2034
|
+
object.setCoords();
|
|
2035
|
+
}
|
|
2036
|
+
if (this.designBackgroundImage) {
|
|
2037
|
+
this.designBackgroundImage.set({
|
|
2038
|
+
left: (this.designBackgroundImage.left ?? 0) * sx,
|
|
2039
|
+
top: (this.designBackgroundImage.top ?? 0) * sy,
|
|
2040
|
+
scaleX: (this.designBackgroundImage.scaleX ?? 1) * sx,
|
|
2041
|
+
scaleY: (this.designBackgroundImage.scaleY ?? 1) * sy
|
|
2042
|
+
});
|
|
2043
|
+
this.designBackgroundImage.setCoords();
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
1198
2046
|
this.canvas.setDimensions({ width: widthPx, height: heightPx });
|
|
2047
|
+
this.patterns.repinAll();
|
|
1199
2048
|
this.canvas.requestRenderAll();
|
|
2049
|
+
this.history.save();
|
|
2050
|
+
this.events.emit("canvas:modified", {});
|
|
2051
|
+
}
|
|
2052
|
+
/** Effective source resolution for an image at its current physical size. */
|
|
2053
|
+
getImageDpi(layerId) {
|
|
2054
|
+
const layer = this.layers.get(layerId);
|
|
2055
|
+
if (!layer || layer.type !== "image") return null;
|
|
2056
|
+
const image = layer.fabricObject;
|
|
2057
|
+
const sourcePixels = image.width ?? 0;
|
|
2058
|
+
const displayedPixels = image.getScaledWidth();
|
|
2059
|
+
if (sourcePixels <= 0 || displayedPixels <= 0) return null;
|
|
2060
|
+
return round2(sourcePixels / (displayedPixels / this.units.getDpi()));
|
|
2061
|
+
}
|
|
2062
|
+
validateImageDpi(minimumDpi = 300) {
|
|
2063
|
+
return this.layers.getAll().filter((layer) => layer.type === "image" && !layer.meta.pattern).flatMap((layer) => {
|
|
2064
|
+
const effectiveDpi = this.getImageDpi(layer.id);
|
|
2065
|
+
return effectiveDpi !== null && effectiveDpi < minimumDpi ? [{ layerId: layer.id, layerName: layer.name, effectiveDpi, minimumDpi }] : [];
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
/** Render a small data-URL preview without changing document dimensions. */
|
|
2069
|
+
toThumbnail(maxWidth = 320, maxHeight = 320, format = "png") {
|
|
2070
|
+
const multiplier = Math.min(
|
|
2071
|
+
1,
|
|
2072
|
+
maxWidth / this.canvas.getWidth(),
|
|
2073
|
+
maxHeight / this.canvas.getHeight()
|
|
2074
|
+
);
|
|
2075
|
+
return this.toDataURL(format, multiplier);
|
|
2076
|
+
}
|
|
2077
|
+
bringToFront(id) {
|
|
2078
|
+
return this.layers.reorder(id, this.layers.count() - 1);
|
|
2079
|
+
}
|
|
2080
|
+
sendToBack(id) {
|
|
2081
|
+
return this.layers.reorder(id, 0);
|
|
2082
|
+
}
|
|
2083
|
+
bringForward(id) {
|
|
2084
|
+
const index = this.layers.getAll().findIndex((layer) => layer.id === id);
|
|
2085
|
+
return index >= 0 && this.layers.reorder(id, index + 1);
|
|
2086
|
+
}
|
|
2087
|
+
sendBackward(id) {
|
|
2088
|
+
const index = this.layers.getAll().findIndex((layer) => layer.id === id);
|
|
2089
|
+
return index >= 0 && this.layers.reorder(id, index - 1);
|
|
1200
2090
|
}
|
|
1201
2091
|
// ─── Zoom ───────────────────────────────────────────
|
|
1202
2092
|
//
|
|
@@ -1234,6 +2124,20 @@ var CanvasEditor = class {
|
|
|
1234
2124
|
const sy = (viewportHeight - padding * 2) / h;
|
|
1235
2125
|
this.setZoom(Math.min(sx, sy));
|
|
1236
2126
|
}
|
|
2127
|
+
/** Zoom until the current selection fills the artboard viewport. */
|
|
2128
|
+
zoomToSelection(padding = 24) {
|
|
2129
|
+
const active = this.canvas.getActiveObject();
|
|
2130
|
+
if (!active) return;
|
|
2131
|
+
active.setCoords();
|
|
2132
|
+
const bounds = active.getBoundingRect();
|
|
2133
|
+
if (bounds.width <= 0 || bounds.height <= 0) return;
|
|
2134
|
+
this.setZoom(
|
|
2135
|
+
Math.min(
|
|
2136
|
+
(this.canvas.getWidth() - padding * 2) / bounds.width,
|
|
2137
|
+
(this.canvas.getHeight() - padding * 2) / bounds.height
|
|
2138
|
+
)
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
1237
2141
|
// ─── Patterns ───────────────────────────────────────
|
|
1238
2142
|
applyPattern(layerId, config) {
|
|
1239
2143
|
return this.patterns.apply(layerId, config);
|
|
@@ -1245,8 +2149,10 @@ var CanvasEditor = class {
|
|
|
1245
2149
|
setMockup(mockup) {
|
|
1246
2150
|
this.mockup = mockup;
|
|
1247
2151
|
this.canvas.backgroundColor = mockup ? "" : this.designBackground;
|
|
2152
|
+
this.canvas.backgroundImage = mockup ? void 0 : this.designBackgroundImage ?? void 0;
|
|
1248
2153
|
this.canvas.requestRenderAll();
|
|
1249
2154
|
this.events.emit("mockup:changed", { mockup });
|
|
2155
|
+
this.history.save();
|
|
1250
2156
|
}
|
|
1251
2157
|
clearMockup() {
|
|
1252
2158
|
this.setMockup(null);
|
|
@@ -1259,6 +2165,7 @@ var CanvasEditor = class {
|
|
|
1259
2165
|
this.snapping.dispose();
|
|
1260
2166
|
this.crop.dispose();
|
|
1261
2167
|
this.history.dispose();
|
|
2168
|
+
clearPatternImageCache();
|
|
1262
2169
|
this.events.removeAllListeners();
|
|
1263
2170
|
this.canvas.dispose();
|
|
1264
2171
|
}
|
|
@@ -1299,15 +2206,29 @@ var DEFAULT_PATTERN_CONFIG = {
|
|
|
1299
2206
|
rotationStepH: 0,
|
|
1300
2207
|
rotationStepV: 0
|
|
1301
2208
|
};
|
|
2209
|
+
|
|
2210
|
+
// src/presets.ts
|
|
2211
|
+
var CANVAS_SIZE_PRESETS = [
|
|
2212
|
+
{ id: "a4-portrait", name: "A4 portrait", width: 210, height: 297, unit: "mm", dpi: 300 },
|
|
2213
|
+
{ id: "a4-landscape", name: "A4 landscape", width: 297, height: 210, unit: "mm", dpi: 300 },
|
|
2214
|
+
{ id: "us-letter", name: "US Letter", width: 8.5, height: 11, unit: "in", dpi: 300 },
|
|
2215
|
+
{ id: "shirt-front", name: "Garment front", width: 12, height: 16, unit: "in", dpi: 300 },
|
|
2216
|
+
{ id: "instagram-square", name: "Social square", width: 1080, height: 1080, unit: "px", dpi: 72 },
|
|
2217
|
+
{ id: "story", name: "Story", width: 1080, height: 1920, unit: "px", dpi: 72 }
|
|
2218
|
+
];
|
|
1302
2219
|
export {
|
|
2220
|
+
CANVAS_SIZE_PRESETS,
|
|
1303
2221
|
CanvasEditor,
|
|
1304
2222
|
CropController,
|
|
1305
2223
|
DEFAULT_PATTERN_CONFIG,
|
|
1306
2224
|
EventEmitter,
|
|
2225
|
+
FontRegistry,
|
|
1307
2226
|
HistoryManager,
|
|
1308
2227
|
Layer,
|
|
1309
2228
|
LayerManager,
|
|
2229
|
+
LicenseManager,
|
|
1310
2230
|
PatternManager,
|
|
2231
|
+
ProjectManager,
|
|
1311
2232
|
SnapManager,
|
|
1312
2233
|
UnitConverter,
|
|
1313
2234
|
applyPatternLocks,
|
|
@@ -1315,15 +2236,22 @@ export {
|
|
|
1315
2236
|
captureLocks,
|
|
1316
2237
|
clamp,
|
|
1317
2238
|
clearPatternImageCache,
|
|
2239
|
+
computeCoverPlacement,
|
|
2240
|
+
computePrintAreaClip,
|
|
1318
2241
|
computeTilePositions,
|
|
1319
2242
|
deserializeEditor,
|
|
1320
2243
|
drawTiles,
|
|
2244
|
+
escapeXml,
|
|
1321
2245
|
exportDataURL,
|
|
2246
|
+
exportMockup,
|
|
1322
2247
|
exportPNG,
|
|
1323
2248
|
exportSVG,
|
|
1324
2249
|
generateId,
|
|
2250
|
+
isCssColor,
|
|
2251
|
+
loadPatternImage,
|
|
1325
2252
|
restoreLocks,
|
|
1326
2253
|
round2,
|
|
2254
|
+
sanitizeSvg,
|
|
1327
2255
|
serializeEditor
|
|
1328
2256
|
};
|
|
1329
2257
|
//# sourceMappingURL=index.mjs.map
|