@overtone-art/canvas-editor-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +250 -0
- package/dist/index.d.ts +250 -0
- package/dist/index.js +657 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +607 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
CanvasEditor: () => CanvasEditor,
|
|
34
|
+
EventEmitter: () => EventEmitter,
|
|
35
|
+
HistoryManager: () => HistoryManager,
|
|
36
|
+
Layer: () => Layer,
|
|
37
|
+
LayerManager: () => LayerManager,
|
|
38
|
+
UnitConverter: () => UnitConverter,
|
|
39
|
+
clamp: () => clamp,
|
|
40
|
+
deserializeEditor: () => deserializeEditor,
|
|
41
|
+
exportDataURL: () => exportDataURL,
|
|
42
|
+
exportPNG: () => exportPNG,
|
|
43
|
+
exportSVG: () => exportSVG,
|
|
44
|
+
generateId: () => generateId,
|
|
45
|
+
round2: () => round2,
|
|
46
|
+
serializeEditor: () => serializeEditor
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(index_exports);
|
|
49
|
+
|
|
50
|
+
// src/editor.ts
|
|
51
|
+
var import_fabric2 = require("fabric");
|
|
52
|
+
|
|
53
|
+
// src/events.ts
|
|
54
|
+
var EventEmitter = class {
|
|
55
|
+
listeners = /* @__PURE__ */ new Map();
|
|
56
|
+
on(event, handler) {
|
|
57
|
+
if (!this.listeners.has(event)) {
|
|
58
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
59
|
+
}
|
|
60
|
+
this.listeners.get(event).add(handler);
|
|
61
|
+
}
|
|
62
|
+
off(event, handler) {
|
|
63
|
+
this.listeners.get(event)?.delete(handler);
|
|
64
|
+
}
|
|
65
|
+
once(event, handler) {
|
|
66
|
+
const wrapped = (data) => {
|
|
67
|
+
this.off(event, wrapped);
|
|
68
|
+
handler(data);
|
|
69
|
+
};
|
|
70
|
+
this.on(event, wrapped);
|
|
71
|
+
}
|
|
72
|
+
emit(event, data) {
|
|
73
|
+
const handlers = this.listeners.get(event);
|
|
74
|
+
if (!handlers) return;
|
|
75
|
+
for (const handler of handlers) {
|
|
76
|
+
handler(data);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
removeAllListeners(event) {
|
|
80
|
+
if (event) {
|
|
81
|
+
this.listeners.delete(event);
|
|
82
|
+
} else {
|
|
83
|
+
this.listeners.clear();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/utils/id.ts
|
|
89
|
+
var import_nanoid = require("nanoid");
|
|
90
|
+
var generateId = () => (0, import_nanoid.nanoid)(12);
|
|
91
|
+
|
|
92
|
+
// src/layer.ts
|
|
93
|
+
var Layer = class {
|
|
94
|
+
id;
|
|
95
|
+
type;
|
|
96
|
+
name;
|
|
97
|
+
visible;
|
|
98
|
+
locked;
|
|
99
|
+
opacity;
|
|
100
|
+
fabricObject;
|
|
101
|
+
constructor(type, fabricObject, name, id) {
|
|
102
|
+
this.id = id ?? generateId();
|
|
103
|
+
this.type = type;
|
|
104
|
+
this.name = name ?? `${type}-${this.id.slice(0, 4)}`;
|
|
105
|
+
this.visible = true;
|
|
106
|
+
this.locked = false;
|
|
107
|
+
this.opacity = 1;
|
|
108
|
+
this.fabricObject = fabricObject;
|
|
109
|
+
this.fabricObject._layerId = this.id;
|
|
110
|
+
}
|
|
111
|
+
toData() {
|
|
112
|
+
return {
|
|
113
|
+
id: this.id,
|
|
114
|
+
type: this.type,
|
|
115
|
+
name: this.name,
|
|
116
|
+
visible: this.visible,
|
|
117
|
+
locked: this.locked,
|
|
118
|
+
opacity: this.opacity
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
serialize() {
|
|
122
|
+
return {
|
|
123
|
+
...this.toData(),
|
|
124
|
+
fabricObject: this.fabricObject.toObject()
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
var LayerManager = class {
|
|
129
|
+
constructor(canvas, events) {
|
|
130
|
+
this.canvas = canvas;
|
|
131
|
+
this.events = events;
|
|
132
|
+
}
|
|
133
|
+
canvas;
|
|
134
|
+
events;
|
|
135
|
+
layers = [];
|
|
136
|
+
add(type, fabricObject, name, id) {
|
|
137
|
+
const layer = new Layer(type, fabricObject, name, id);
|
|
138
|
+
this.layers.push(layer);
|
|
139
|
+
this.canvas.add(fabricObject);
|
|
140
|
+
this.events.emit("layer:added", { layer: layer.toData() });
|
|
141
|
+
this.emitChanged();
|
|
142
|
+
return layer;
|
|
143
|
+
}
|
|
144
|
+
remove(id) {
|
|
145
|
+
const index = this.layers.findIndex((l) => l.id === id);
|
|
146
|
+
if (index === -1) return false;
|
|
147
|
+
const layer = this.layers[index];
|
|
148
|
+
this.canvas.remove(layer.fabricObject);
|
|
149
|
+
this.layers.splice(index, 1);
|
|
150
|
+
this.events.emit("layer:removed", { layerId: id });
|
|
151
|
+
this.emitChanged();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
reorder(id, newIndex) {
|
|
155
|
+
const oldIndex = this.layers.findIndex((l) => l.id === id);
|
|
156
|
+
if (oldIndex === -1) return false;
|
|
157
|
+
const clamped = Math.max(0, Math.min(this.layers.length - 1, newIndex));
|
|
158
|
+
const [layer] = this.layers.splice(oldIndex, 1);
|
|
159
|
+
this.layers.splice(clamped, 0, layer);
|
|
160
|
+
this.layers.forEach((l, i) => {
|
|
161
|
+
this.canvas.moveObjectTo(l.fabricObject, i);
|
|
162
|
+
});
|
|
163
|
+
this.events.emit("layer:reordered", { layerIds: this.layers.map((l) => l.id) });
|
|
164
|
+
this.emitChanged();
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
select(id) {
|
|
168
|
+
if (id === null) {
|
|
169
|
+
this.canvas.discardActiveObject();
|
|
170
|
+
} else {
|
|
171
|
+
const layer = this.get(id);
|
|
172
|
+
if (layer) {
|
|
173
|
+
this.canvas.setActiveObject(layer.fabricObject);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
this.canvas.requestRenderAll();
|
|
177
|
+
this.events.emit("layer:selected", { layerId: id });
|
|
178
|
+
}
|
|
179
|
+
get(id) {
|
|
180
|
+
return this.layers.find((l) => l.id === id);
|
|
181
|
+
}
|
|
182
|
+
getAll() {
|
|
183
|
+
return [...this.layers];
|
|
184
|
+
}
|
|
185
|
+
findByObject(obj) {
|
|
186
|
+
const layerId = obj._layerId;
|
|
187
|
+
if (layerId) return this.get(layerId);
|
|
188
|
+
return this.layers.find((l) => l.fabricObject === obj);
|
|
189
|
+
}
|
|
190
|
+
setVisibility(id, visible) {
|
|
191
|
+
const layer = this.get(id);
|
|
192
|
+
if (!layer) return;
|
|
193
|
+
layer.visible = visible;
|
|
194
|
+
layer.fabricObject.visible = visible;
|
|
195
|
+
this.canvas.requestRenderAll();
|
|
196
|
+
this.emitChanged();
|
|
197
|
+
}
|
|
198
|
+
setLocked(id, locked) {
|
|
199
|
+
const layer = this.get(id);
|
|
200
|
+
if (!layer) return;
|
|
201
|
+
layer.locked = locked;
|
|
202
|
+
layer.fabricObject.selectable = !locked;
|
|
203
|
+
layer.fabricObject.evented = !locked;
|
|
204
|
+
this.canvas.requestRenderAll();
|
|
205
|
+
this.emitChanged();
|
|
206
|
+
}
|
|
207
|
+
setOpacity(id, opacity) {
|
|
208
|
+
const layer = this.get(id);
|
|
209
|
+
if (!layer) return;
|
|
210
|
+
layer.opacity = opacity;
|
|
211
|
+
layer.fabricObject.opacity = opacity;
|
|
212
|
+
this.canvas.requestRenderAll();
|
|
213
|
+
this.emitChanged();
|
|
214
|
+
}
|
|
215
|
+
setName(id, name) {
|
|
216
|
+
const layer = this.get(id);
|
|
217
|
+
if (!layer) return;
|
|
218
|
+
layer.name = name;
|
|
219
|
+
this.emitChanged();
|
|
220
|
+
}
|
|
221
|
+
clear() {
|
|
222
|
+
for (const layer of this.layers) {
|
|
223
|
+
this.canvas.remove(layer.fabricObject);
|
|
224
|
+
}
|
|
225
|
+
this.layers = [];
|
|
226
|
+
this.emitChanged();
|
|
227
|
+
}
|
|
228
|
+
count() {
|
|
229
|
+
return this.layers.length;
|
|
230
|
+
}
|
|
231
|
+
emitChanged() {
|
|
232
|
+
this.events.emit("layers:changed", {
|
|
233
|
+
layers: this.layers.map((l) => l.toData())
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// src/history.ts
|
|
239
|
+
var HistoryManager = class {
|
|
240
|
+
undoStack = [];
|
|
241
|
+
redoStack = [];
|
|
242
|
+
maxSize;
|
|
243
|
+
paused = false;
|
|
244
|
+
debounceTimer = null;
|
|
245
|
+
debounceMs;
|
|
246
|
+
getState;
|
|
247
|
+
restoreState;
|
|
248
|
+
events;
|
|
249
|
+
constructor(opts) {
|
|
250
|
+
this.getState = opts.getState;
|
|
251
|
+
this.restoreState = opts.restoreState;
|
|
252
|
+
this.events = opts.events;
|
|
253
|
+
this.maxSize = opts.maxSize ?? 50;
|
|
254
|
+
this.debounceMs = opts.debounceMs ?? 300;
|
|
255
|
+
}
|
|
256
|
+
save() {
|
|
257
|
+
if (this.paused) return;
|
|
258
|
+
if (this.debounceTimer) {
|
|
259
|
+
clearTimeout(this.debounceTimer);
|
|
260
|
+
}
|
|
261
|
+
this.debounceTimer = setTimeout(() => {
|
|
262
|
+
this.saveImmediate();
|
|
263
|
+
}, this.debounceMs);
|
|
264
|
+
}
|
|
265
|
+
saveImmediate() {
|
|
266
|
+
if (this.paused) return;
|
|
267
|
+
const state = this.getState();
|
|
268
|
+
this.undoStack.push(state);
|
|
269
|
+
if (this.undoStack.length > this.maxSize) {
|
|
270
|
+
this.undoStack.shift();
|
|
271
|
+
}
|
|
272
|
+
this.redoStack = [];
|
|
273
|
+
this.emitChanged();
|
|
274
|
+
}
|
|
275
|
+
async undo() {
|
|
276
|
+
const state = this.undoStack.pop();
|
|
277
|
+
if (!state) return;
|
|
278
|
+
this.redoStack.push(this.getState());
|
|
279
|
+
this.paused = true;
|
|
280
|
+
await this.restoreState(state);
|
|
281
|
+
this.paused = false;
|
|
282
|
+
this.emitChanged();
|
|
283
|
+
}
|
|
284
|
+
async redo() {
|
|
285
|
+
const state = this.redoStack.pop();
|
|
286
|
+
if (!state) return;
|
|
287
|
+
this.undoStack.push(this.getState());
|
|
288
|
+
this.paused = true;
|
|
289
|
+
await this.restoreState(state);
|
|
290
|
+
this.paused = false;
|
|
291
|
+
this.emitChanged();
|
|
292
|
+
}
|
|
293
|
+
pause() {
|
|
294
|
+
this.paused = true;
|
|
295
|
+
}
|
|
296
|
+
resume() {
|
|
297
|
+
this.paused = false;
|
|
298
|
+
}
|
|
299
|
+
canUndo() {
|
|
300
|
+
return this.undoStack.length > 0;
|
|
301
|
+
}
|
|
302
|
+
canRedo() {
|
|
303
|
+
return this.redoStack.length > 0;
|
|
304
|
+
}
|
|
305
|
+
clear() {
|
|
306
|
+
this.undoStack = [];
|
|
307
|
+
this.redoStack = [];
|
|
308
|
+
this.emitChanged();
|
|
309
|
+
}
|
|
310
|
+
dispose() {
|
|
311
|
+
if (this.debounceTimer) {
|
|
312
|
+
clearTimeout(this.debounceTimer);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
emitChanged() {
|
|
316
|
+
this.events.emit("history:changed", {
|
|
317
|
+
canUndo: this.canUndo(),
|
|
318
|
+
canRedo: this.canRedo()
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// src/utils/units.ts
|
|
324
|
+
var MM_PER_INCH = 25.4;
|
|
325
|
+
var UnitConverter = class {
|
|
326
|
+
constructor(unit = "px", dpi = 72) {
|
|
327
|
+
this.unit = unit;
|
|
328
|
+
this.dpi = dpi;
|
|
329
|
+
}
|
|
330
|
+
unit;
|
|
331
|
+
dpi;
|
|
332
|
+
setUnit(unit) {
|
|
333
|
+
this.unit = unit;
|
|
334
|
+
}
|
|
335
|
+
setDpi(dpi) {
|
|
336
|
+
this.dpi = dpi;
|
|
337
|
+
}
|
|
338
|
+
getUnit() {
|
|
339
|
+
return this.unit;
|
|
340
|
+
}
|
|
341
|
+
getDpi() {
|
|
342
|
+
return this.dpi;
|
|
343
|
+
}
|
|
344
|
+
toPixels(value, fromUnit) {
|
|
345
|
+
const u = fromUnit ?? this.unit;
|
|
346
|
+
switch (u) {
|
|
347
|
+
case "px":
|
|
348
|
+
return value;
|
|
349
|
+
case "in":
|
|
350
|
+
return value * this.dpi;
|
|
351
|
+
case "mm":
|
|
352
|
+
return value / MM_PER_INCH * this.dpi;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
fromPixels(px, toUnit) {
|
|
356
|
+
const u = toUnit ?? this.unit;
|
|
357
|
+
switch (u) {
|
|
358
|
+
case "px":
|
|
359
|
+
return px;
|
|
360
|
+
case "in":
|
|
361
|
+
return px / this.dpi;
|
|
362
|
+
case "mm":
|
|
363
|
+
return px / this.dpi * MM_PER_INCH;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// src/serialization.ts
|
|
369
|
+
var import_fabric = require("fabric");
|
|
370
|
+
var VERSION = "1.0.0";
|
|
371
|
+
function serializeEditor(editor) {
|
|
372
|
+
return {
|
|
373
|
+
version: VERSION,
|
|
374
|
+
canvas: {
|
|
375
|
+
width: editor.canvas.getWidth(),
|
|
376
|
+
height: editor.canvas.getHeight()
|
|
377
|
+
},
|
|
378
|
+
layers: editor.layers.getAll().map((layer) => layer.serialize()),
|
|
379
|
+
background: editor.canvas.backgroundColor
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
async function deserializeEditor(editor, state) {
|
|
383
|
+
editor.layers.clear();
|
|
384
|
+
editor.canvas.setDimensions({ width: state.canvas.width, height: state.canvas.height });
|
|
385
|
+
if (state.background) {
|
|
386
|
+
editor.canvas.backgroundColor = state.background;
|
|
387
|
+
}
|
|
388
|
+
for (const serializedLayer of state.layers) {
|
|
389
|
+
await restoreLayer(editor, serializedLayer);
|
|
390
|
+
}
|
|
391
|
+
editor.canvas.requestRenderAll();
|
|
392
|
+
}
|
|
393
|
+
async function restoreLayer(editor, serialized) {
|
|
394
|
+
const objects = await import_fabric.util.enlivenObjects([serialized.fabricObject]);
|
|
395
|
+
const fabricObject = objects[0];
|
|
396
|
+
const layer = editor.layers.add(
|
|
397
|
+
serialized.type,
|
|
398
|
+
fabricObject,
|
|
399
|
+
serialized.name,
|
|
400
|
+
serialized.id
|
|
401
|
+
);
|
|
402
|
+
if (!serialized.visible) {
|
|
403
|
+
editor.layers.setVisibility(layer.id, false);
|
|
404
|
+
}
|
|
405
|
+
if (serialized.locked) {
|
|
406
|
+
editor.layers.setLocked(layer.id, true);
|
|
407
|
+
}
|
|
408
|
+
if (serialized.opacity !== 1) {
|
|
409
|
+
editor.layers.setOpacity(layer.id, serialized.opacity);
|
|
410
|
+
}
|
|
411
|
+
return layer;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/export.ts
|
|
415
|
+
async function exportPNG(canvas, options = {}) {
|
|
416
|
+
const { multiplier = 1, format = "png", quality = 1 } = options;
|
|
417
|
+
const dataUrl = canvas.toDataURL({
|
|
418
|
+
format,
|
|
419
|
+
multiplier,
|
|
420
|
+
quality
|
|
421
|
+
});
|
|
422
|
+
const response = await fetch(dataUrl);
|
|
423
|
+
return response.blob();
|
|
424
|
+
}
|
|
425
|
+
function exportSVG(canvas) {
|
|
426
|
+
return canvas.toSVG();
|
|
427
|
+
}
|
|
428
|
+
function exportDataURL(canvas, format = "png", multiplier = 1) {
|
|
429
|
+
return canvas.toDataURL({ format, multiplier });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/utils/clamp.ts
|
|
433
|
+
function clamp(v, min, max) {
|
|
434
|
+
return Math.min(max, Math.max(min, v));
|
|
435
|
+
}
|
|
436
|
+
function round2(v) {
|
|
437
|
+
return Math.round(v * 100) / 100;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/editor.ts
|
|
441
|
+
var CanvasEditor = class {
|
|
442
|
+
canvas;
|
|
443
|
+
layers;
|
|
444
|
+
history;
|
|
445
|
+
events;
|
|
446
|
+
units;
|
|
447
|
+
fileAdapter;
|
|
448
|
+
imageProvider;
|
|
449
|
+
constructor(canvasElement, config) {
|
|
450
|
+
this.events = new EventEmitter();
|
|
451
|
+
this.units = new UnitConverter(config.unit ?? "px", config.dpi ?? 72);
|
|
452
|
+
const widthPx = this.units.toPixels(config.width);
|
|
453
|
+
const heightPx = this.units.toPixels(config.height);
|
|
454
|
+
this.canvas = new import_fabric2.Canvas(canvasElement, {
|
|
455
|
+
width: widthPx,
|
|
456
|
+
height: heightPx,
|
|
457
|
+
backgroundColor: config.backgroundColor ?? "#ffffff",
|
|
458
|
+
preserveObjectStacking: config.preserveObjectStacking ?? true,
|
|
459
|
+
selection: true
|
|
460
|
+
});
|
|
461
|
+
this.layers = new LayerManager(this.canvas, this.events);
|
|
462
|
+
this.fileAdapter = config.fileAdapter;
|
|
463
|
+
this.imageProvider = config.imageProvider;
|
|
464
|
+
this.history = new HistoryManager({
|
|
465
|
+
getState: () => JSON.stringify(this.toJSON()),
|
|
466
|
+
restoreState: async (state) => {
|
|
467
|
+
const parsed = JSON.parse(state);
|
|
468
|
+
await this.fromJSON(parsed);
|
|
469
|
+
},
|
|
470
|
+
events: this.events
|
|
471
|
+
});
|
|
472
|
+
this.setupCanvasEvents();
|
|
473
|
+
this.history.saveImmediate();
|
|
474
|
+
}
|
|
475
|
+
// ─── Layer Operations ────────────────────────────────
|
|
476
|
+
async addImage(url, options) {
|
|
477
|
+
const img = await import_fabric2.FabricImage.fromURL(url, {}, options);
|
|
478
|
+
const layer = this.layers.add("image", img);
|
|
479
|
+
this.history.save();
|
|
480
|
+
return layer;
|
|
481
|
+
}
|
|
482
|
+
addText(text, options) {
|
|
483
|
+
const textbox = new import_fabric2.Textbox(text, {
|
|
484
|
+
fontSize: 32,
|
|
485
|
+
fontFamily: "Arial",
|
|
486
|
+
fill: "#000000",
|
|
487
|
+
width: 200,
|
|
488
|
+
...options
|
|
489
|
+
});
|
|
490
|
+
const layer = this.layers.add("text", textbox);
|
|
491
|
+
this.history.save();
|
|
492
|
+
return layer;
|
|
493
|
+
}
|
|
494
|
+
addShape(plugin, options) {
|
|
495
|
+
const obj = plugin.create(options);
|
|
496
|
+
const layer = this.layers.add("shape", obj, plugin.name);
|
|
497
|
+
this.history.save();
|
|
498
|
+
return layer;
|
|
499
|
+
}
|
|
500
|
+
async addTemplate(template, _params) {
|
|
501
|
+
const { Textbox: TextboxClass } = await import("fabric");
|
|
502
|
+
const placeholder = new TextboxClass(`[Template: ${template.name}]`, {
|
|
503
|
+
fontSize: 24,
|
|
504
|
+
fontFamily: "Arial",
|
|
505
|
+
fill: "#666666",
|
|
506
|
+
width: 300
|
|
507
|
+
});
|
|
508
|
+
const layer = this.layers.add("template", placeholder, template.name);
|
|
509
|
+
this.history.save();
|
|
510
|
+
return layer;
|
|
511
|
+
}
|
|
512
|
+
removeLayer(id) {
|
|
513
|
+
this.layers.remove(id);
|
|
514
|
+
this.history.save();
|
|
515
|
+
}
|
|
516
|
+
selectLayer(id) {
|
|
517
|
+
this.layers.select(id);
|
|
518
|
+
}
|
|
519
|
+
getSelectedLayer() {
|
|
520
|
+
const active = this.canvas.getActiveObject();
|
|
521
|
+
if (!active) return null;
|
|
522
|
+
return this.layers.findByObject(active) ?? null;
|
|
523
|
+
}
|
|
524
|
+
// ─── Serialization ──────────────────────────────────
|
|
525
|
+
toJSON() {
|
|
526
|
+
return serializeEditor(this);
|
|
527
|
+
}
|
|
528
|
+
async fromJSON(state) {
|
|
529
|
+
await deserializeEditor(this, state);
|
|
530
|
+
}
|
|
531
|
+
// ─── Export ──────────────────────────────────────────
|
|
532
|
+
async toPNG(options) {
|
|
533
|
+
this.events.emit("export:start", { format: "png" });
|
|
534
|
+
const blob = await exportPNG(this.canvas, options);
|
|
535
|
+
this.events.emit("export:complete", { format: "png" });
|
|
536
|
+
return blob;
|
|
537
|
+
}
|
|
538
|
+
toSVG() {
|
|
539
|
+
this.events.emit("export:start", { format: "svg" });
|
|
540
|
+
const svg = exportSVG(this.canvas);
|
|
541
|
+
this.events.emit("export:complete", { format: "svg" });
|
|
542
|
+
return svg;
|
|
543
|
+
}
|
|
544
|
+
toDataURL(format = "png", multiplier = 1) {
|
|
545
|
+
return exportDataURL(this.canvas, format, multiplier);
|
|
546
|
+
}
|
|
547
|
+
toPrintifyPositioning() {
|
|
548
|
+
const result = {};
|
|
549
|
+
const canvasW = this.canvas.getWidth();
|
|
550
|
+
const canvasH = this.canvas.getHeight();
|
|
551
|
+
const imageLayers = this.layers.getAll().filter((l) => l.type === "image");
|
|
552
|
+
if (imageLayers.length === 0) {
|
|
553
|
+
result["front"] = { x: 0.5, y: 0.5, scale: 1, angle: 0 };
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
for (const layer of imageLayers) {
|
|
557
|
+
const obj = layer.fabricObject;
|
|
558
|
+
const center = obj.getCenterPoint();
|
|
559
|
+
result[layer.name] = {
|
|
560
|
+
x: round2(center.x / canvasW),
|
|
561
|
+
y: round2(center.y / canvasH),
|
|
562
|
+
scale: round2(obj.scaleX ?? 1),
|
|
563
|
+
angle: round2(obj.angle ?? 0)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
return result;
|
|
567
|
+
}
|
|
568
|
+
// ─── File Operations ────────────────────────────────
|
|
569
|
+
async save(filename, format) {
|
|
570
|
+
if (!this.fileAdapter) return void 0;
|
|
571
|
+
let data;
|
|
572
|
+
if (format === "png") {
|
|
573
|
+
data = await this.toPNG();
|
|
574
|
+
} else if (format === "svg") {
|
|
575
|
+
data = this.toSVG();
|
|
576
|
+
} else {
|
|
577
|
+
data = JSON.stringify(this.toJSON(), null, 2);
|
|
578
|
+
}
|
|
579
|
+
return this.fileAdapter.save(data, filename, format);
|
|
580
|
+
}
|
|
581
|
+
async uploadImage(file) {
|
|
582
|
+
if (!this.imageProvider) return void 0;
|
|
583
|
+
return this.imageProvider.upload(file);
|
|
584
|
+
}
|
|
585
|
+
async browseImages() {
|
|
586
|
+
if (!this.imageProvider?.browse) return null;
|
|
587
|
+
return this.imageProvider.browse();
|
|
588
|
+
}
|
|
589
|
+
setFileAdapter(adapter) {
|
|
590
|
+
this.fileAdapter = adapter;
|
|
591
|
+
}
|
|
592
|
+
setImageProvider(provider) {
|
|
593
|
+
this.imageProvider = provider;
|
|
594
|
+
}
|
|
595
|
+
// ─── Canvas Operations ──────────────────────────────
|
|
596
|
+
setBackground(color) {
|
|
597
|
+
this.canvas.backgroundColor = color;
|
|
598
|
+
this.canvas.requestRenderAll();
|
|
599
|
+
this.history.save();
|
|
600
|
+
}
|
|
601
|
+
resize(width, height) {
|
|
602
|
+
const widthPx = this.units.toPixels(width);
|
|
603
|
+
const heightPx = this.units.toPixels(height);
|
|
604
|
+
this.canvas.setDimensions({ width: widthPx, height: heightPx });
|
|
605
|
+
this.canvas.requestRenderAll();
|
|
606
|
+
}
|
|
607
|
+
zoom(level) {
|
|
608
|
+
this.canvas.setZoom(level);
|
|
609
|
+
this.canvas.requestRenderAll();
|
|
610
|
+
}
|
|
611
|
+
// ─── Cleanup ────────────────────────────────────────
|
|
612
|
+
dispose() {
|
|
613
|
+
this.history.dispose();
|
|
614
|
+
this.events.removeAllListeners();
|
|
615
|
+
this.canvas.dispose();
|
|
616
|
+
}
|
|
617
|
+
// ─── Private ────────────────────────────────────────
|
|
618
|
+
setupCanvasEvents() {
|
|
619
|
+
this.canvas.on("object:modified", (e) => {
|
|
620
|
+
if (!e.target) return;
|
|
621
|
+
const layer = this.layers.findByObject(e.target);
|
|
622
|
+
if (layer) {
|
|
623
|
+
this.events.emit("layer:modified", { layerId: layer.id });
|
|
624
|
+
this.history.save();
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
this.canvas.on("selection:created", (e) => {
|
|
628
|
+
const selected = (e.selected ?? []).map((obj) => this.layers.findByObject(obj)?.id).filter((id) => id !== void 0);
|
|
629
|
+
this.events.emit("selection:changed", { selected });
|
|
630
|
+
});
|
|
631
|
+
this.canvas.on("selection:updated", (e) => {
|
|
632
|
+
const selected = (e.selected ?? []).map((obj) => this.layers.findByObject(obj)?.id).filter((id) => id !== void 0);
|
|
633
|
+
this.events.emit("selection:changed", { selected });
|
|
634
|
+
});
|
|
635
|
+
this.canvas.on("selection:cleared", () => {
|
|
636
|
+
this.events.emit("selection:changed", { selected: [] });
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
641
|
+
0 && (module.exports = {
|
|
642
|
+
CanvasEditor,
|
|
643
|
+
EventEmitter,
|
|
644
|
+
HistoryManager,
|
|
645
|
+
Layer,
|
|
646
|
+
LayerManager,
|
|
647
|
+
UnitConverter,
|
|
648
|
+
clamp,
|
|
649
|
+
deserializeEditor,
|
|
650
|
+
exportDataURL,
|
|
651
|
+
exportPNG,
|
|
652
|
+
exportSVG,
|
|
653
|
+
generateId,
|
|
654
|
+
round2,
|
|
655
|
+
serializeEditor
|
|
656
|
+
});
|
|
657
|
+
//# sourceMappingURL=index.js.map
|