@overtone-art/canvas-editor-core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +180 -15
- package/dist/index.d.ts +180 -15
- package/dist/index.global.js +52 -52
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +697 -173
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +648 -129
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/{types-JP8MZsIs.d.mts → types-LM7pOkkj.d.mts} +34 -4
- package/dist/{types-JP8MZsIs.d.ts → types-LM7pOkkj.d.ts} +34 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -53,6 +53,7 @@ __export(index_exports, {
|
|
|
53
53
|
applyLayerShadow: () => applyLayerShadow,
|
|
54
54
|
applyObjectSelectionStyle: () => applyObjectSelectionStyle,
|
|
55
55
|
applySelectionStyle: () => applySelectionStyle,
|
|
56
|
+
buildCurveLinePaths: () => buildCurveLinePaths,
|
|
56
57
|
buildCurvePathData: () => buildCurvePathData,
|
|
57
58
|
clamp: () => clamp,
|
|
58
59
|
clearTextureMaskCache: () => clearTextureMaskCache,
|
|
@@ -64,6 +65,7 @@ __export(index_exports, {
|
|
|
64
65
|
displaceRgba: () => displaceRgba,
|
|
65
66
|
drawTiles: () => drawTiles,
|
|
66
67
|
escapeXml: () => escapeXml,
|
|
68
|
+
explodeTemplateSvg: () => explodeTemplateSvg,
|
|
67
69
|
exportDataURL: () => exportDataURL,
|
|
68
70
|
exportMockup: () => exportMockup,
|
|
69
71
|
exportPNG: () => exportPNG,
|
|
@@ -76,6 +78,7 @@ __export(index_exports, {
|
|
|
76
78
|
isShapeMaskId: () => isShapeMaskId,
|
|
77
79
|
isTextureMaskId: () => isTextureMaskId,
|
|
78
80
|
needsAbsoluteSpace: () => needsAbsoluteSpace,
|
|
81
|
+
normalizeTextCurve: () => normalizeTextCurve,
|
|
79
82
|
readLayerShadow: () => readLayerShadow,
|
|
80
83
|
renderTextureMask: () => renderTextureMask,
|
|
81
84
|
resetTransform: () => resetTransform,
|
|
@@ -84,6 +87,8 @@ __export(index_exports, {
|
|
|
84
87
|
sanitizeSvg: () => sanitizeSvg,
|
|
85
88
|
serializeEditor: () => serializeEditor,
|
|
86
89
|
shapeMaskPathData: () => shapeMaskPathData,
|
|
90
|
+
textPathCurve: () => textPathCurve,
|
|
91
|
+
textPathSpec: () => textPathSpec,
|
|
87
92
|
toCanvasSpace: () => toCanvasSpace,
|
|
88
93
|
toHostSpace: () => toHostSpace,
|
|
89
94
|
unwrapGroup: () => unwrapGroup
|
|
@@ -91,7 +96,7 @@ __export(index_exports, {
|
|
|
91
96
|
module.exports = __toCommonJS(index_exports);
|
|
92
97
|
|
|
93
98
|
// src/editor.ts
|
|
94
|
-
var
|
|
99
|
+
var import_fabric19 = require("fabric");
|
|
95
100
|
|
|
96
101
|
// src/events.ts
|
|
97
102
|
var EventEmitter = class {
|
|
@@ -128,6 +133,9 @@ var EventEmitter = class {
|
|
|
128
133
|
}
|
|
129
134
|
};
|
|
130
135
|
|
|
136
|
+
// src/layer.ts
|
|
137
|
+
var import_fabric = require("fabric");
|
|
138
|
+
|
|
131
139
|
// src/utils/id.ts
|
|
132
140
|
var import_nanoid = require("nanoid");
|
|
133
141
|
var generateId = () => (0, import_nanoid.nanoid)(12);
|
|
@@ -149,6 +157,14 @@ var Layer = class {
|
|
|
149
157
|
* the layer's real object stays the one every editor API talks to.
|
|
150
158
|
*/
|
|
151
159
|
renderProxy;
|
|
160
|
+
/**
|
|
161
|
+
* Layers nested inside this one, in draw order. Only a `group` layer has any:
|
|
162
|
+
* their fabric objects live inside this layer's fabric `Group`, not on the
|
|
163
|
+
* canvas, which is why the manager keeps them off the canvas stack.
|
|
164
|
+
*/
|
|
165
|
+
children;
|
|
166
|
+
/** Group this layer sits inside, or null when it is on the canvas stack. */
|
|
167
|
+
parentId;
|
|
152
168
|
constructor(type, fabricObject, name, id) {
|
|
153
169
|
this.id = id ?? generateId();
|
|
154
170
|
this.type = type;
|
|
@@ -159,6 +175,8 @@ var Layer = class {
|
|
|
159
175
|
this.meta = {};
|
|
160
176
|
this.fabricObject = fabricObject;
|
|
161
177
|
this.renderProxy = null;
|
|
178
|
+
this.children = [];
|
|
179
|
+
this.parentId = null;
|
|
162
180
|
this.fabricObject._layerId = this.id;
|
|
163
181
|
}
|
|
164
182
|
hasMeta() {
|
|
@@ -172,12 +190,15 @@ var Layer = class {
|
|
|
172
190
|
visible: this.visible,
|
|
173
191
|
locked: this.locked,
|
|
174
192
|
opacity: this.opacity,
|
|
175
|
-
...this.hasMeta() ? { meta: this.meta } : {}
|
|
193
|
+
...this.hasMeta() ? { meta: this.meta } : {},
|
|
194
|
+
...this.children.length > 0 ? { children: this.children.map((c) => c.toData()) } : {}
|
|
176
195
|
};
|
|
177
196
|
}
|
|
178
197
|
serialize() {
|
|
179
198
|
return {
|
|
180
199
|
...this.toData(),
|
|
200
|
+
// A group's own object already carries its children's geometry, so the
|
|
201
|
+
// child records in `toData` stay identity-only — nothing is duplicated.
|
|
181
202
|
fabricObject: this.fabricObject.toObject()
|
|
182
203
|
};
|
|
183
204
|
}
|
|
@@ -203,9 +224,23 @@ var LayerManager = class {
|
|
|
203
224
|
return layer;
|
|
204
225
|
}
|
|
205
226
|
remove(id) {
|
|
227
|
+
const layer = this.get(id);
|
|
228
|
+
if (!layer) return false;
|
|
229
|
+
if (layer.parentId) {
|
|
230
|
+
const parent = this.get(layer.parentId);
|
|
231
|
+
if (!parent) return false;
|
|
232
|
+
parent.fabricObject.remove(layer.fabricObject);
|
|
233
|
+
parent.children = parent.children.filter((c) => c.id !== id);
|
|
234
|
+
layer.parentId = null;
|
|
235
|
+
this.events.emit("layer:removed", { layerId: id });
|
|
236
|
+
if (parent.children.length < 2) this.dissolve(parent);
|
|
237
|
+
this.emitChanged();
|
|
238
|
+
this.onPropertyChanged?.();
|
|
239
|
+
this.canvas.requestRenderAll();
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
206
242
|
const index = this.layers.findIndex((l) => l.id === id);
|
|
207
243
|
if (index === -1) return false;
|
|
208
|
-
const layer = this.layers[index];
|
|
209
244
|
this.canvas.remove(layer.fabricObject);
|
|
210
245
|
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
211
246
|
this.layers.splice(index, 1);
|
|
@@ -214,6 +249,81 @@ var LayerManager = class {
|
|
|
214
249
|
this.onPropertyChanged?.();
|
|
215
250
|
return true;
|
|
216
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Take a top-level layer off the canvas stack **without** disposing it — it is
|
|
254
|
+
* about to become a group's child, so its object has to stay alive.
|
|
255
|
+
*/
|
|
256
|
+
detach(id) {
|
|
257
|
+
const index = this.layers.findIndex((l) => l.id === id);
|
|
258
|
+
if (index === -1) return void 0;
|
|
259
|
+
const [layer] = this.layers.splice(index, 1);
|
|
260
|
+
this.canvas.remove(layer.fabricObject);
|
|
261
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
262
|
+
return layer;
|
|
263
|
+
}
|
|
264
|
+
/** Put a group's children back on the canvas stack and drop the group. */
|
|
265
|
+
explode(group) {
|
|
266
|
+
const children = [...group.children];
|
|
267
|
+
this.dissolve(group);
|
|
268
|
+
this.events.emit("layer:removed", { layerId: group.id });
|
|
269
|
+
this.emitChanged();
|
|
270
|
+
this.onPropertyChanged?.();
|
|
271
|
+
this.canvas.requestRenderAll();
|
|
272
|
+
return children;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Empty a group onto the canvas stack, in the group's own place, and drop the
|
|
276
|
+
* group record. `removeAll` restores each child's absolute transform, so the
|
|
277
|
+
* group's matrix must NOT be re-applied on top of it.
|
|
278
|
+
*/
|
|
279
|
+
dissolve(group) {
|
|
280
|
+
const objects = group.fabricObject.removeAll();
|
|
281
|
+
const at = this.layers.findIndex((l) => l.id === group.id);
|
|
282
|
+
const survivors = group.children;
|
|
283
|
+
group.children = [];
|
|
284
|
+
for (const [index, object] of objects.entries()) {
|
|
285
|
+
object.setCoords();
|
|
286
|
+
const child = survivors[index];
|
|
287
|
+
if (!child) continue;
|
|
288
|
+
child.parentId = null;
|
|
289
|
+
this.layers.splice(at + index, 0, child);
|
|
290
|
+
this.canvas.add(object);
|
|
291
|
+
if (child.renderProxy) this.canvas.add(child.renderProxy);
|
|
292
|
+
}
|
|
293
|
+
this.canvas.remove(group.fabricObject);
|
|
294
|
+
this.layers = this.layers.filter((l) => l.id !== group.id);
|
|
295
|
+
this.syncZOrder();
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Re-attach child records to an enlivened group. `data[i]` describes
|
|
299
|
+
* `group._objects[i]` — the order fabric serializes and restores them in. A
|
|
300
|
+
* missing record still becomes a child, so a hand-edited state cannot drop
|
|
301
|
+
* artwork off the canvas.
|
|
302
|
+
*/
|
|
303
|
+
adoptGroupChildren(group, data) {
|
|
304
|
+
const objects = group.fabricObject.getObjects();
|
|
305
|
+
group.children = objects.map((object, index) => {
|
|
306
|
+
const record = data[index];
|
|
307
|
+
const child = new Layer(record?.type ?? "shape", object, record?.name, record?.id);
|
|
308
|
+
child.parentId = group.id;
|
|
309
|
+
if (record?.meta) child.meta = structuredClone(record.meta);
|
|
310
|
+
if (record && !record.visible) {
|
|
311
|
+
child.visible = false;
|
|
312
|
+
object.visible = false;
|
|
313
|
+
}
|
|
314
|
+
if (record?.locked) {
|
|
315
|
+
child.locked = true;
|
|
316
|
+
object.selectable = false;
|
|
317
|
+
object.evented = false;
|
|
318
|
+
}
|
|
319
|
+
if (record && record.opacity !== 1) {
|
|
320
|
+
child.opacity = record.opacity;
|
|
321
|
+
object.opacity = record.opacity;
|
|
322
|
+
}
|
|
323
|
+
return child;
|
|
324
|
+
});
|
|
325
|
+
this.emitChanged();
|
|
326
|
+
}
|
|
217
327
|
/** Replace a layer's render object while preserving its immutable ID and panel state. */
|
|
218
328
|
replaceObject(id, fabricObject) {
|
|
219
329
|
const layer = this.get(id);
|
|
@@ -278,16 +388,32 @@ var LayerManager = class {
|
|
|
278
388
|
if (layer.renderProxy) this.canvas.bringObjectToFront(layer.renderProxy);
|
|
279
389
|
}
|
|
280
390
|
}
|
|
391
|
+
/** The list a layer lives in: its group's children, or the canvas stack. */
|
|
392
|
+
siblingsOf(layer) {
|
|
393
|
+
if (!layer.parentId) return this.layers;
|
|
394
|
+
return this.get(layer.parentId)?.children ?? this.layers;
|
|
395
|
+
}
|
|
281
396
|
reorder(id, newIndex) {
|
|
282
|
-
const
|
|
397
|
+
const target = this.get(id);
|
|
398
|
+
if (!target) return false;
|
|
399
|
+
const list = this.siblingsOf(target);
|
|
400
|
+
const oldIndex = list.indexOf(target);
|
|
283
401
|
if (oldIndex === -1) return false;
|
|
284
402
|
if (!Number.isFinite(newIndex)) return false;
|
|
285
|
-
const clamped = Math.max(0, Math.min(
|
|
403
|
+
const clamped = Math.max(0, Math.min(list.length - 1, Math.round(newIndex)));
|
|
286
404
|
if (oldIndex === clamped) return false;
|
|
287
|
-
const [layer] =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
405
|
+
const [layer] = list.splice(oldIndex, 1);
|
|
406
|
+
list.splice(clamped, 0, layer);
|
|
407
|
+
if (layer.parentId) {
|
|
408
|
+
const group = this.get(layer.parentId)?.fabricObject;
|
|
409
|
+
if (group) {
|
|
410
|
+
group.remove(layer.fabricObject);
|
|
411
|
+
group.insertAt(clamped, layer.fabricObject);
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
this.syncZOrder();
|
|
415
|
+
}
|
|
416
|
+
this.events.emit("layer:reordered", { layerIds: list.map((l) => l.id) });
|
|
291
417
|
this.emitChanged();
|
|
292
418
|
this.onPropertyChanged?.();
|
|
293
419
|
return true;
|
|
@@ -295,9 +421,12 @@ var LayerManager = class {
|
|
|
295
421
|
select(id) {
|
|
296
422
|
if (id === null) {
|
|
297
423
|
this.canvas.discardActiveObject();
|
|
424
|
+
this.exitGroups();
|
|
298
425
|
} else {
|
|
299
426
|
const layer = this.get(id);
|
|
300
427
|
if (layer) {
|
|
428
|
+
if (layer.parentId) this.enterGroup(layer.parentId);
|
|
429
|
+
else this.exitGroups();
|
|
301
430
|
this.canvas.setActiveObject(layer.renderProxy ?? layer.fabricObject);
|
|
302
431
|
}
|
|
303
432
|
}
|
|
@@ -306,16 +435,67 @@ var LayerManager = class {
|
|
|
306
435
|
const selected = id && this.get(id) ? [id] : [];
|
|
307
436
|
this.events.emit("selection:changed", { selected });
|
|
308
437
|
}
|
|
438
|
+
/** Drill into a group so its children can be selected and clicked. */
|
|
439
|
+
enterGroup(groupId) {
|
|
440
|
+
const parent = this.get(groupId);
|
|
441
|
+
if (!parent) return;
|
|
442
|
+
const group = parent.fabricObject;
|
|
443
|
+
group.subTargetCheck = true;
|
|
444
|
+
group.interactive = true;
|
|
445
|
+
}
|
|
446
|
+
/** Leave every entered group — selecting anything else is done drilling in. */
|
|
447
|
+
exitGroups() {
|
|
448
|
+
for (const layer of this.layers) {
|
|
449
|
+
if (layer.type !== "group") continue;
|
|
450
|
+
const group = layer.fabricObject;
|
|
451
|
+
if (group.interactive) group.interactive = false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Select several layers at once, as one fabric `ActiveSelection`. Children are
|
|
456
|
+
* skipped: a selection spanning in and out of a group has no single transform.
|
|
457
|
+
*/
|
|
458
|
+
selectMany(ids) {
|
|
459
|
+
const layers = ids.flatMap((id) => {
|
|
460
|
+
const layer = this.get(id);
|
|
461
|
+
return layer && !layer.parentId ? [layer] : [];
|
|
462
|
+
});
|
|
463
|
+
if (layers.length === 0) return this.select(null);
|
|
464
|
+
if (layers.length === 1) return this.select(layers[0].id);
|
|
465
|
+
this.canvas.discardActiveObject();
|
|
466
|
+
this.exitGroups();
|
|
467
|
+
const selection = new import_fabric.ActiveSelection(
|
|
468
|
+
layers.map((l) => l.renderProxy ?? l.fabricObject),
|
|
469
|
+
{ canvas: this.canvas }
|
|
470
|
+
);
|
|
471
|
+
this.canvas.setActiveObject(selection);
|
|
472
|
+
this.canvas.requestRenderAll();
|
|
473
|
+
this.events.emit("selection:changed", { selected: layers.map((l) => l.id) });
|
|
474
|
+
}
|
|
475
|
+
/** Depth-first over the whole tree, each layer before its children. */
|
|
476
|
+
*walk(layers = this.layers) {
|
|
477
|
+
for (const layer of layers) {
|
|
478
|
+
yield layer;
|
|
479
|
+
if (layer.children.length > 0) yield* this.walk(layer.children);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
309
482
|
get(id) {
|
|
310
|
-
|
|
483
|
+
for (const layer of this.walk()) if (layer.id === id) return layer;
|
|
484
|
+
return void 0;
|
|
311
485
|
}
|
|
486
|
+
/** Top-level layers only — the canvas stack. Children hang off their group. */
|
|
312
487
|
getAll() {
|
|
313
488
|
return [...this.layers];
|
|
314
489
|
}
|
|
490
|
+
/** Top-level layers, each carrying its children: what the layer panel renders. */
|
|
491
|
+
getTree() {
|
|
492
|
+
return this.layers.map((l) => l.toData());
|
|
493
|
+
}
|
|
315
494
|
findByObject(obj) {
|
|
316
495
|
const layerId = obj._layerId;
|
|
317
496
|
if (layerId) return this.get(layerId);
|
|
318
|
-
|
|
497
|
+
for (const layer of this.walk()) if (layer.fabricObject === obj) return layer;
|
|
498
|
+
return void 0;
|
|
319
499
|
}
|
|
320
500
|
setVisibility(id, visible) {
|
|
321
501
|
const layer = this.get(id);
|
|
@@ -396,6 +576,7 @@ var LayerManager = class {
|
|
|
396
576
|
for (const layer of this.layers) {
|
|
397
577
|
this.canvas.remove(layer.fabricObject);
|
|
398
578
|
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
579
|
+
layer.children = [];
|
|
399
580
|
}
|
|
400
581
|
this.layers = [];
|
|
401
582
|
this.emitChanged();
|
|
@@ -410,6 +591,107 @@ var LayerManager = class {
|
|
|
410
591
|
}
|
|
411
592
|
};
|
|
412
593
|
|
|
594
|
+
// src/template-explode.ts
|
|
595
|
+
var import_fabric2 = require("fabric");
|
|
596
|
+
var NAME_MAX = 24;
|
|
597
|
+
function isText(object) {
|
|
598
|
+
return typeof object.text === "string";
|
|
599
|
+
}
|
|
600
|
+
function labelsByRun(template, values) {
|
|
601
|
+
const labels = new Map(template.parameters.map((p) => [p.key, p.label || p.key]));
|
|
602
|
+
const byRun = /* @__PURE__ */ new Map();
|
|
603
|
+
for (const match of template.svg.matchAll(/<text\b[^>]*>([\s\S]*?)<\/text>/g)) {
|
|
604
|
+
const token = /\{\{(\w+)(?:[|:]([^}]*))?\}\}/.exec(match[1]);
|
|
605
|
+
if (!token) continue;
|
|
606
|
+
const label = labels.get(token[1]);
|
|
607
|
+
if (!label) continue;
|
|
608
|
+
const resolved = String(values[token[1]] ?? token[2] ?? "").trim();
|
|
609
|
+
if (resolved) byRun.set(resolved, label);
|
|
610
|
+
}
|
|
611
|
+
return byRun;
|
|
612
|
+
}
|
|
613
|
+
function textName(run, labels) {
|
|
614
|
+
const trimmed = run.trim();
|
|
615
|
+
const label = labels.get(trimmed);
|
|
616
|
+
if (label) return label;
|
|
617
|
+
if (!trimmed) return "Text";
|
|
618
|
+
return trimmed.length > NAME_MAX ? `${trimmed.slice(0, NAME_MAX - 1)}\u2026` : trimmed;
|
|
619
|
+
}
|
|
620
|
+
function shapeName(object) {
|
|
621
|
+
const id = object.id;
|
|
622
|
+
if (typeof id === "string" && id) return id;
|
|
623
|
+
const type = object.type || "shape";
|
|
624
|
+
return type.charAt(0).toUpperCase() + type.slice(1);
|
|
625
|
+
}
|
|
626
|
+
function toTextbox(text) {
|
|
627
|
+
const box = new import_fabric2.Textbox(text.text, {
|
|
628
|
+
left: text.left,
|
|
629
|
+
top: text.top,
|
|
630
|
+
originX: text.originX,
|
|
631
|
+
originY: text.originY,
|
|
632
|
+
angle: text.angle,
|
|
633
|
+
scaleX: text.scaleX,
|
|
634
|
+
scaleY: text.scaleY,
|
|
635
|
+
fontSize: text.fontSize,
|
|
636
|
+
fontFamily: text.fontFamily,
|
|
637
|
+
fontWeight: text.fontWeight,
|
|
638
|
+
fontStyle: text.fontStyle,
|
|
639
|
+
textAlign: text.textAlign,
|
|
640
|
+
charSpacing: text.charSpacing,
|
|
641
|
+
lineHeight: text.lineHeight,
|
|
642
|
+
fill: text.fill,
|
|
643
|
+
stroke: text.stroke,
|
|
644
|
+
strokeWidth: text.strokeWidth,
|
|
645
|
+
opacity: text.opacity,
|
|
646
|
+
// A Textbox wraps at its box width; size it to the run the template drew so
|
|
647
|
+
// the authored copy never comes in pre-wrapped.
|
|
648
|
+
width: Math.max(1, (text.width ?? 0) + 2)
|
|
649
|
+
});
|
|
650
|
+
return box;
|
|
651
|
+
}
|
|
652
|
+
function textPathCurve(template, values, runWidth) {
|
|
653
|
+
if (template.id === "circular-text") return { arc: 100, wave: 0 };
|
|
654
|
+
if (template.id === "wave-text") return { arc: 0, wave: 50 };
|
|
655
|
+
const radius = Number(values.radius ?? 200);
|
|
656
|
+
if (!Number.isFinite(radius) || radius <= 0) return { arc: 0, wave: 0 };
|
|
657
|
+
const arc = 100 * runWidth / (Math.PI * radius);
|
|
658
|
+
return { arc: Math.max(-100, Math.min(100, arc)), wave: 0 };
|
|
659
|
+
}
|
|
660
|
+
function textPathSpec(template, values) {
|
|
661
|
+
const content = String(values.text ?? "Text");
|
|
662
|
+
const fontSize = template.id === "circular-text" ? 28 : 40;
|
|
663
|
+
const box = new import_fabric2.Textbox(content, {
|
|
664
|
+
fontSize,
|
|
665
|
+
fontFamily: String(values.font ?? "Arial"),
|
|
666
|
+
fontWeight: "bold",
|
|
667
|
+
fill: String(values.color ?? "#000000"),
|
|
668
|
+
originX: "left",
|
|
669
|
+
originY: "top",
|
|
670
|
+
// Wide enough that the run stays on one line before the curve widens it.
|
|
671
|
+
width: Math.max(1, content.length * fontSize * 0.62)
|
|
672
|
+
});
|
|
673
|
+
return {
|
|
674
|
+
type: "text",
|
|
675
|
+
name: template.parameters.find((p) => p.key === "text")?.label || "Text",
|
|
676
|
+
object: box,
|
|
677
|
+
curve: textPathCurve(template, values, box.width ?? 0)
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
async function explodeTemplateSvg(template, resolvedSvg, values) {
|
|
681
|
+
const labels = labelsByRun(template, values);
|
|
682
|
+
const { objects } = await (0, import_fabric2.loadSVGFromString)(resolvedSvg);
|
|
683
|
+
const specs = [];
|
|
684
|
+
for (const object of objects) {
|
|
685
|
+
if (object === null) continue;
|
|
686
|
+
if (isText(object)) {
|
|
687
|
+
specs.push({ type: "text", name: textName(object.text, labels), object: toTextbox(object) });
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
specs.push({ type: "shape", name: shapeName(object), object });
|
|
691
|
+
}
|
|
692
|
+
return specs;
|
|
693
|
+
}
|
|
694
|
+
|
|
413
695
|
// src/history.ts
|
|
414
696
|
var HistoryManager = class _HistoryManager {
|
|
415
697
|
static ASSET_KEY = "__canvasEditorHistoryAsset";
|
|
@@ -797,7 +1079,7 @@ function bestSnap(edges, candidates, threshold) {
|
|
|
797
1079
|
}
|
|
798
1080
|
|
|
799
1081
|
// src/crop.ts
|
|
800
|
-
var
|
|
1082
|
+
var import_fabric3 = require("fabric");
|
|
801
1083
|
|
|
802
1084
|
// src/utils/clamp.ts
|
|
803
1085
|
function clamp(v, min, max) {
|
|
@@ -837,7 +1119,7 @@ var CropController = class {
|
|
|
837
1119
|
if (prevAngle) image.rotate(0);
|
|
838
1120
|
image.setCoords();
|
|
839
1121
|
const b = image.getBoundingRect();
|
|
840
|
-
const rect = new
|
|
1122
|
+
const rect = new import_fabric3.Rect({
|
|
841
1123
|
left: b.left,
|
|
842
1124
|
top: b.top,
|
|
843
1125
|
width: b.width,
|
|
@@ -930,10 +1212,10 @@ var CropController = class {
|
|
|
930
1212
|
var STROKE2 = "#22c55e";
|
|
931
1213
|
|
|
932
1214
|
// src/pattern/pattern-manager.ts
|
|
933
|
-
var
|
|
1215
|
+
var import_fabric5 = require("fabric");
|
|
934
1216
|
|
|
935
1217
|
// src/pattern/tiled-pattern-object.ts
|
|
936
|
-
var
|
|
1218
|
+
var import_fabric4 = require("fabric");
|
|
937
1219
|
|
|
938
1220
|
// src/pattern/tile-geometry.ts
|
|
939
1221
|
var MAX_TILES_PER_AXIS = 200;
|
|
@@ -966,8 +1248,10 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
|
|
|
966
1248
|
}
|
|
967
1249
|
return placements;
|
|
968
1250
|
}
|
|
969
|
-
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
|
|
1251
|
+
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin, draw) {
|
|
970
1252
|
const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
|
|
1253
|
+
const drawW = draw && draw.width > 0 ? draw.width : baseW;
|
|
1254
|
+
const drawH = draw && draw.height > 0 ? draw.height : baseH;
|
|
971
1255
|
ctx.save();
|
|
972
1256
|
ctx.translate(anchor.x, anchor.y);
|
|
973
1257
|
ctx.rotate(config.angle * Math.PI / 180);
|
|
@@ -975,7 +1259,7 @@ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
|
|
|
975
1259
|
ctx.save();
|
|
976
1260
|
ctx.translate(tile.x, tile.y);
|
|
977
1261
|
ctx.rotate(tile.rotation);
|
|
978
|
-
ctx.drawImage(img, -
|
|
1262
|
+
ctx.drawImage(img, -drawW / 2, -drawH / 2, drawW, drawH);
|
|
979
1263
|
ctx.restore();
|
|
980
1264
|
}
|
|
981
1265
|
ctx.restore();
|
|
@@ -997,7 +1281,8 @@ function mod2(n) {
|
|
|
997
1281
|
var MAX_SNAPSHOT_PIXELS = 16e6;
|
|
998
1282
|
var MAX_SNAPSHOT_SCALE = 8;
|
|
999
1283
|
var SNAPSHOT_SHRINK_FACTOR = 2;
|
|
1000
|
-
var
|
|
1284
|
+
var TILE_BLEED_DEVICE_PX = 2;
|
|
1285
|
+
var TiledPatternObject = class _TiledPatternObject extends import_fabric4.FabricObject {
|
|
1001
1286
|
static type = "TiledPattern";
|
|
1002
1287
|
/** The layer's real object. Never rendered directly (it is kept at opacity 0). */
|
|
1003
1288
|
source;
|
|
@@ -1080,13 +1365,14 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1080
1365
|
const { tileW, tileH } = this.liveTile();
|
|
1081
1366
|
const centre = this.getCenterPoint();
|
|
1082
1367
|
const shift = this.shiftVector(tileW, tileH, this.liveAngle());
|
|
1083
|
-
return new
|
|
1368
|
+
return new import_fabric4.Point(centre.x - shift.x, centre.y - shift.y);
|
|
1084
1369
|
}
|
|
1085
1370
|
_render(ctx) {
|
|
1086
1371
|
const { width, height } = this.area;
|
|
1087
1372
|
if (width <= 0 || height <= 0) return;
|
|
1088
1373
|
const { tileW, tileH } = this.liveTile();
|
|
1089
|
-
const
|
|
1374
|
+
const scale = contextScale(ctx);
|
|
1375
|
+
const snapshot = this.ensureSnapshot(scale * (tileW / Math.max(1, this.baseW())));
|
|
1090
1376
|
if (!snapshot) return;
|
|
1091
1377
|
const centre = this.getCenterPoint();
|
|
1092
1378
|
const angle = this.liveAngle();
|
|
@@ -1097,7 +1383,17 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1097
1383
|
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1098
1384
|
const origin = { x: centre.x - shift.x, y: centre.y - shift.y };
|
|
1099
1385
|
const config = { ...this.config, angle, offsetX: 0, offsetY: 0 };
|
|
1100
|
-
drawTiles(
|
|
1386
|
+
drawTiles(
|
|
1387
|
+
ctx,
|
|
1388
|
+
snapshot,
|
|
1389
|
+
config,
|
|
1390
|
+
width,
|
|
1391
|
+
height,
|
|
1392
|
+
tileW,
|
|
1393
|
+
tileH,
|
|
1394
|
+
origin,
|
|
1395
|
+
this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX / scale)
|
|
1396
|
+
);
|
|
1101
1397
|
ctx.restore();
|
|
1102
1398
|
}
|
|
1103
1399
|
/** Raster fallback for SVG export — one `<image>` covering the print area. */
|
|
@@ -1123,7 +1419,9 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1123
1419
|
height,
|
|
1124
1420
|
tileW,
|
|
1125
1421
|
tileH,
|
|
1126
|
-
{ x: centre.x - shift.x, y: centre.y - shift.y }
|
|
1422
|
+
{ x: centre.x - shift.x, y: centre.y - shift.y },
|
|
1423
|
+
// The fallback raster is built at 1:1, so a device pixel is a canvas unit.
|
|
1424
|
+
this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX)
|
|
1127
1425
|
);
|
|
1128
1426
|
return [
|
|
1129
1427
|
`<g transform="rotate(${-angle}) translate(${-centre.x} ${-centre.y})">`,
|
|
@@ -1163,18 +1461,64 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1163
1461
|
});
|
|
1164
1462
|
return Promise.resolve(copy);
|
|
1165
1463
|
}
|
|
1464
|
+
/**
|
|
1465
|
+
* The source's tile footprint: its bounding box, minus a stroke it reserves
|
|
1466
|
+
* room for but never paints.
|
|
1467
|
+
*
|
|
1468
|
+
* fabric keeps `strokeWidth` inside an object's box whether or not a `stroke`
|
|
1469
|
+
* colour paints it, and the built-in shapes arrive with `strokeWidth: 1` and no
|
|
1470
|
+
* stroke. Stepping the grid by that box puts a transparent seam between every
|
|
1471
|
+
* pair of neighbours at zero spacing: the artwork is a pixel narrower than the
|
|
1472
|
+
* box it is stepped by.
|
|
1473
|
+
*/
|
|
1474
|
+
sourceBox() {
|
|
1475
|
+
const rect = this.source.getBoundingRect();
|
|
1476
|
+
if (paintsStroke(this.source)) return { width: rect.width, height: rect.height };
|
|
1477
|
+
const dims = this.source._getTransformedDimensions({ strokeWidth: 0 });
|
|
1478
|
+
const radians = (this.source.angle ?? 0) * Math.PI / 180;
|
|
1479
|
+
const cos = Math.abs(Math.cos(radians));
|
|
1480
|
+
const sin = Math.abs(Math.sin(radians));
|
|
1481
|
+
return {
|
|
1482
|
+
width: dims.x * cos + dims.y * sin,
|
|
1483
|
+
height: dims.x * sin + dims.y * cos
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1166
1486
|
/** The source's on-canvas width, before the tile scale. */
|
|
1167
1487
|
baseW() {
|
|
1168
|
-
return Math.max(1, this.
|
|
1488
|
+
return Math.max(1, this.sourceBox().width);
|
|
1489
|
+
}
|
|
1490
|
+
/** The source's on-canvas height, before the tile scale. */
|
|
1491
|
+
baseH() {
|
|
1492
|
+
return Math.max(1, this.sourceBox().height);
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Size to paint the snapshot at, for a tile of `tileW × tileH`.
|
|
1496
|
+
*
|
|
1497
|
+
* The snapshot is NOT exactly the source's box: `toCanvasElement` rounds the
|
|
1498
|
+
* raster up to whole pixels and pads it further for a shadow. Painting it into
|
|
1499
|
+
* the tile step would squeeze the artwork inside that padding — every tile
|
|
1500
|
+
* shrinks by up to a pixel and the grid shows transparent seams at zero
|
|
1501
|
+
* spacing. So the bitmap is painted at its own footprint, scaled by the same
|
|
1502
|
+
* factor the tile is, and the step stays the source's box.
|
|
1503
|
+
*/
|
|
1504
|
+
drawSize(tileW, tileH, bleed = 0) {
|
|
1505
|
+
const snapshot = this.snapshotEl;
|
|
1506
|
+
if (!snapshot || this.snapshotScale <= 0) {
|
|
1507
|
+
return { width: tileW + bleed, height: tileH + bleed };
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
width: snapshot.width / this.snapshotScale * (tileW / this.baseW()) + bleed,
|
|
1511
|
+
height: snapshot.height / this.snapshotScale * (tileH / this.baseH()) + bleed
|
|
1512
|
+
};
|
|
1169
1513
|
}
|
|
1170
1514
|
/** Tile size from the config alone, ignoring any in-flight gesture. */
|
|
1171
1515
|
baseTile() {
|
|
1172
|
-
const
|
|
1516
|
+
const box = this.sourceBox();
|
|
1173
1517
|
const scale = Math.max(1, this.config.scale ?? 100) / 100;
|
|
1174
1518
|
const floor = this.tileFloor();
|
|
1175
1519
|
return {
|
|
1176
|
-
tileW: Math.max(floor,
|
|
1177
|
-
tileH: Math.max(floor,
|
|
1520
|
+
tileW: Math.max(floor, box.width * scale),
|
|
1521
|
+
tileH: Math.max(floor, box.height * scale)
|
|
1178
1522
|
};
|
|
1179
1523
|
}
|
|
1180
1524
|
/** Tile size as drawn right now, including a live scale gesture. */
|
|
@@ -1198,11 +1542,11 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1198
1542
|
const radians = angle * Math.PI / 180;
|
|
1199
1543
|
const cos = Math.cos(radians);
|
|
1200
1544
|
const sin = Math.sin(radians);
|
|
1201
|
-
return new
|
|
1545
|
+
return new import_fabric4.Point(dx * cos - dy * sin, dx * sin + dy * cos);
|
|
1202
1546
|
}
|
|
1203
1547
|
anchorFromOrigin(origin, tileW, tileH, angle) {
|
|
1204
1548
|
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1205
|
-
return new
|
|
1549
|
+
return new import_fabric4.Point(origin.x + shift.x, origin.y + shift.y);
|
|
1206
1550
|
}
|
|
1207
1551
|
/**
|
|
1208
1552
|
* Snapshot the source at (at least) `scale`, reusing the cached one while it
|
|
@@ -1237,6 +1581,12 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1237
1581
|
return Math.max(0.05, Math.min(requested, budgeted));
|
|
1238
1582
|
}
|
|
1239
1583
|
};
|
|
1584
|
+
function paintsStroke(object) {
|
|
1585
|
+
if (!object.strokeWidth || object.strokeWidth <= 0) return false;
|
|
1586
|
+
const { stroke } = object;
|
|
1587
|
+
if (typeof stroke === "string") return stroke !== "" && stroke !== "transparent";
|
|
1588
|
+
return stroke !== null && stroke !== void 0;
|
|
1589
|
+
}
|
|
1240
1590
|
function clampPercent(value) {
|
|
1241
1591
|
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
1242
1592
|
return Math.max(-100, Math.min(100, value));
|
|
@@ -1456,7 +1806,7 @@ async function unbakeLegacyLayer(layer, state) {
|
|
|
1456
1806
|
cropY: state.original.cropY,
|
|
1457
1807
|
angle: state.original.angle
|
|
1458
1808
|
});
|
|
1459
|
-
image.clipPath = state.originalClip ? (await
|
|
1809
|
+
image.clipPath = state.originalClip ? (await import_fabric5.util.enlivenObjects([state.originalClip]))[0] : void 0;
|
|
1460
1810
|
restoreLocks(image, state.originalLocks);
|
|
1461
1811
|
image.setCoords();
|
|
1462
1812
|
}
|
|
@@ -1474,91 +1824,154 @@ function restoreLocks(obj, locks) {
|
|
|
1474
1824
|
}
|
|
1475
1825
|
|
|
1476
1826
|
// src/text-curve.ts
|
|
1477
|
-
var
|
|
1478
|
-
|
|
1827
|
+
var import_fabric6 = require("fabric");
|
|
1828
|
+
|
|
1829
|
+
// src/text-curve-geometry.ts
|
|
1830
|
+
var DEFAULT_TEXT_CURVE = {
|
|
1831
|
+
shape: "arc",
|
|
1832
|
+
arc: 0,
|
|
1833
|
+
wave: 0,
|
|
1834
|
+
waveLength: 4.1,
|
|
1835
|
+
offset: 0,
|
|
1836
|
+
centerOffset: 0
|
|
1837
|
+
};
|
|
1479
1838
|
var MIN_ARC = 0.5;
|
|
1480
1839
|
var FULL_CIRCLE_ARC = 99.5;
|
|
1481
1840
|
var MIN_SWEEP = 0.12;
|
|
1482
|
-
var WAVE_PERIOD_EM = 4.1;
|
|
1483
1841
|
var WAVE_AMPLITUDE_EM = 0.9;
|
|
1842
|
+
var MIN_WAVE_LENGTH = 1;
|
|
1843
|
+
var MAX_WAVE_LENGTH = 12;
|
|
1484
1844
|
var WAVE_STEP = 6;
|
|
1485
|
-
var
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
return !!object && typeof object.text === "string";
|
|
1845
|
+
var MAX_WAVE_STEPS = 4e3;
|
|
1846
|
+
function round(value) {
|
|
1847
|
+
return Math.round(value * 100) / 100;
|
|
1489
1848
|
}
|
|
1490
|
-
function
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1849
|
+
function normalizeTextCurve(config) {
|
|
1850
|
+
const arc = clamp(config.arc ?? 0, -100, 100);
|
|
1851
|
+
const wave = clamp(config.wave ?? 0, 0, 100);
|
|
1852
|
+
const inferred = Math.abs(arc) < MIN_ARC && wave > 0 ? "wave" : "arc";
|
|
1853
|
+
return {
|
|
1854
|
+
shape: config.shape ?? inferred,
|
|
1855
|
+
arc,
|
|
1856
|
+
wave,
|
|
1857
|
+
waveLength: clamp(config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength, MIN_WAVE_LENGTH, MAX_WAVE_LENGTH),
|
|
1858
|
+
offset: clamp(config.offset ?? 0, -100, 100),
|
|
1859
|
+
centerOffset: clamp(config.centerOffset ?? 0, -100, 100)
|
|
1860
|
+
};
|
|
1500
1861
|
}
|
|
1501
|
-
function
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const
|
|
1506
|
-
const
|
|
1862
|
+
function bendsBaseline(config) {
|
|
1863
|
+
return config.shape === "arc" ? Math.abs(config.arc) >= MIN_ARC : config.wave > 0;
|
|
1864
|
+
}
|
|
1865
|
+
function arcGeometry(width, config) {
|
|
1866
|
+
const magnitude = Math.min(100, Math.abs(config.arc));
|
|
1867
|
+
const direction = config.arc < 0 ? -1 : 1;
|
|
1868
|
+
const baseSweep = magnitude >= FULL_CIRCLE_ARC ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
|
|
1869
|
+
const baseRadius = width / baseSweep;
|
|
1870
|
+
const radius = baseRadius * Math.pow(2, config.centerOffset / 100);
|
|
1871
|
+
const sweep = radius >= baseRadius ? baseSweep : Math.min(Math.PI * 2, width / Math.max(radius, 1e-3));
|
|
1872
|
+
return { radius, sweep, direction, baseRadius };
|
|
1873
|
+
}
|
|
1874
|
+
function arcLinePath(width, geometry, radius, rotation) {
|
|
1875
|
+
const { sweep, direction, baseRadius } = geometry;
|
|
1876
|
+
const full = sweep >= Math.PI * 2 - 1e-6;
|
|
1507
1877
|
const centerX = width / 2;
|
|
1508
1878
|
const point = (angle) => [
|
|
1509
1879
|
centerX + radius * Math.sin(angle),
|
|
1510
|
-
direction * radius *
|
|
1880
|
+
direction * (baseRadius - radius * Math.cos(angle))
|
|
1511
1881
|
];
|
|
1512
1882
|
const sweepFlag = direction > 0 ? 1 : 0;
|
|
1513
1883
|
const format = ([x, y]) => `${round(x)} ${round(y)}`;
|
|
1884
|
+
const r = round(radius);
|
|
1514
1885
|
if (full) {
|
|
1515
|
-
const start2 = point(-Math.PI);
|
|
1516
|
-
const
|
|
1886
|
+
const start2 = point(rotation - Math.PI);
|
|
1887
|
+
const opposite = point(rotation);
|
|
1517
1888
|
return {
|
|
1518
1889
|
data: [
|
|
1519
1890
|
`M ${format(start2)}`,
|
|
1520
|
-
`A ${
|
|
1521
|
-
`A ${
|
|
1891
|
+
`A ${r} ${r} 0 0 ${sweepFlag} ${format(opposite)}`,
|
|
1892
|
+
`A ${r} ${r} 0 0 ${sweepFlag} ${format(start2)}`
|
|
1522
1893
|
].join(" "),
|
|
1523
|
-
length:
|
|
1894
|
+
length: radius * Math.PI * 2
|
|
1524
1895
|
};
|
|
1525
1896
|
}
|
|
1526
|
-
const start = point(-sweep / 2);
|
|
1527
|
-
const end = point(sweep / 2);
|
|
1897
|
+
const start = point(rotation - sweep / 2);
|
|
1898
|
+
const end = point(rotation + sweep / 2);
|
|
1528
1899
|
const largeArc = sweep > Math.PI ? 1 : 0;
|
|
1529
1900
|
return {
|
|
1530
|
-
data: `M ${format(start)} A ${
|
|
1531
|
-
|
|
1532
|
-
length: width
|
|
1901
|
+
data: `M ${format(start)} A ${r} ${r} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
|
|
1902
|
+
length: radius * sweep
|
|
1533
1903
|
};
|
|
1534
1904
|
}
|
|
1535
|
-
function
|
|
1536
|
-
const amplitude =
|
|
1537
|
-
const period = Math.max(1, fontSize *
|
|
1538
|
-
const
|
|
1905
|
+
function waveLinePath(width, fontSize, config, dy) {
|
|
1906
|
+
const amplitude = config.wave / 100 * fontSize * WAVE_AMPLITUDE_EM;
|
|
1907
|
+
const period = Math.max(1, fontSize * config.waveLength);
|
|
1908
|
+
const phase = config.offset / 100 * Math.PI * 2;
|
|
1909
|
+
const step = Math.min(WAVE_STEP, period / 8);
|
|
1910
|
+
const steps = Math.min(MAX_WAVE_STEPS, Math.max(2, Math.ceil(width / step)));
|
|
1539
1911
|
const commands = [];
|
|
1540
1912
|
let length = 0;
|
|
1541
1913
|
let previous = null;
|
|
1542
1914
|
for (let index = 0; index <= steps; index++) {
|
|
1543
1915
|
const x = width * index / steps;
|
|
1544
|
-
const y = amplitude * Math.sin(x / period * Math.PI * 2);
|
|
1916
|
+
const y = dy + amplitude * Math.sin(x / period * Math.PI * 2 + phase);
|
|
1545
1917
|
if (previous) length += Math.hypot(x - previous[0], y - previous[1]);
|
|
1546
1918
|
previous = [x, y];
|
|
1547
1919
|
commands.push(`${index === 0 ? "M" : "L"} ${round(x)} ${round(y)}`);
|
|
1548
1920
|
}
|
|
1549
1921
|
return { data: commands.join(" "), length };
|
|
1550
1922
|
}
|
|
1551
|
-
function
|
|
1552
|
-
|
|
1923
|
+
function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight = 0) {
|
|
1924
|
+
const curve = normalizeTextCurve(config);
|
|
1925
|
+
if (!bendsBaseline(curve)) return null;
|
|
1926
|
+
const lines = Math.max(1, Math.floor(lineCount));
|
|
1927
|
+
const paths = [];
|
|
1928
|
+
if (curve.shape === "wave") {
|
|
1929
|
+
for (let index = 0; index < lines; index++) {
|
|
1930
|
+
paths.push(waveLinePath(width, fontSize, curve, (index - (lines - 1) / 2) * lineHeight));
|
|
1931
|
+
}
|
|
1932
|
+
return paths;
|
|
1933
|
+
}
|
|
1934
|
+
const geometry = arcGeometry(width, curve);
|
|
1935
|
+
const rotation = curve.offset / 100 * geometry.sweep;
|
|
1936
|
+
for (let index = 0; index < lines; index++) {
|
|
1937
|
+
const radius = Math.max(1, geometry.radius + (index - (lines - 1) / 2) * lineHeight);
|
|
1938
|
+
paths.push(arcLinePath(width, geometry, radius, rotation));
|
|
1939
|
+
}
|
|
1940
|
+
return paths;
|
|
1553
1941
|
}
|
|
1554
1942
|
function buildCurvePathData(config, width, fontSize) {
|
|
1555
|
-
|
|
1556
|
-
if (config.wave > 0) return wavePathData(width, fontSize, config.wave);
|
|
1557
|
-
return null;
|
|
1943
|
+
return buildCurveLinePaths(config, width, fontSize)?.[0] ?? null;
|
|
1558
1944
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1945
|
+
|
|
1946
|
+
// src/text-curve.ts
|
|
1947
|
+
var MEASURE_WIDTH = 1e5;
|
|
1948
|
+
var PATH_SLACK = 0.06;
|
|
1949
|
+
var FALLBACK_LINE_HEIGHT = 1.16;
|
|
1950
|
+
var patches = /* @__PURE__ */ new WeakMap();
|
|
1951
|
+
function isCurvable(object) {
|
|
1952
|
+
return !!object && typeof object.text === "string";
|
|
1953
|
+
}
|
|
1954
|
+
function measureText(text) {
|
|
1955
|
+
const authored = text.width;
|
|
1956
|
+
try {
|
|
1957
|
+
text.set({ width: MEASURE_WIDTH });
|
|
1958
|
+
text.initDimensions?.();
|
|
1959
|
+
return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
|
|
1960
|
+
} finally {
|
|
1961
|
+
if (authored !== void 0) text.set({ width: authored });
|
|
1962
|
+
text.initDimensions?.();
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
function lineCountOf(text) {
|
|
1966
|
+
return Math.max(1, text._textLines?.length ?? 1);
|
|
1967
|
+
}
|
|
1968
|
+
function lineHeightOf(text) {
|
|
1969
|
+
const measured = text.getHeightOfLine?.(0);
|
|
1970
|
+
if (typeof measured === "number" && Number.isFinite(measured) && measured > 0) return measured;
|
|
1971
|
+
return text.fontSize * (text.lineHeight ?? 1) * FALLBACK_LINE_HEIGHT;
|
|
1972
|
+
}
|
|
1973
|
+
function toFabricPath(curve) {
|
|
1974
|
+
return new import_fabric6.Path(curve.data, { visible: false, objectCaching: false });
|
|
1562
1975
|
}
|
|
1563
1976
|
var TextCurveManager = class {
|
|
1564
1977
|
constructor(canvas, layers, history, events) {
|
|
@@ -1575,33 +1988,35 @@ var TextCurveManager = class {
|
|
|
1575
1988
|
get(layerId) {
|
|
1576
1989
|
const layer = this.layers.get(layerId);
|
|
1577
1990
|
if (!layer || !isCurvable(layer.fabricObject)) return null;
|
|
1578
|
-
return layer.meta.curve ?? {
|
|
1991
|
+
return normalizeTextCurve(layer.meta.curve ?? {});
|
|
1579
1992
|
}
|
|
1580
1993
|
isCurved(layerId) {
|
|
1581
1994
|
const curve = this.get(layerId);
|
|
1582
|
-
return !!curve && (
|
|
1995
|
+
return !!curve && bendsBaseline(curve);
|
|
1583
1996
|
}
|
|
1584
|
-
/** Apply (or update) the curve on a text layer.
|
|
1997
|
+
/** Apply (or update) the curve on a text layer. A straight config clears it. */
|
|
1585
1998
|
apply(layerId, config, save = true) {
|
|
1586
1999
|
const layer = this.layers.get(layerId);
|
|
1587
2000
|
if (!layer || !isCurvable(layer.fabricObject)) return false;
|
|
1588
|
-
const next =
|
|
2001
|
+
const next = normalizeTextCurve(config);
|
|
1589
2002
|
const text = layer.fabricObject;
|
|
1590
2003
|
const run = measureText(text);
|
|
1591
|
-
|
|
1592
|
-
if (!curve) {
|
|
2004
|
+
if (!bendsBaseline(next)) {
|
|
1593
2005
|
this.detach(text, layer.meta.curveWidth);
|
|
1594
|
-
delete layer.meta.curve;
|
|
1595
2006
|
delete layer.meta.curveWidth;
|
|
2007
|
+
layer.meta.curve = next;
|
|
1596
2008
|
} else {
|
|
1597
2009
|
if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
|
|
1598
2010
|
text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
|
|
1599
|
-
text.
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
2011
|
+
text.initDimensions?.();
|
|
2012
|
+
const curves = buildCurveLinePaths(
|
|
2013
|
+
next,
|
|
2014
|
+
run * (1 + PATH_SLACK),
|
|
2015
|
+
text.fontSize,
|
|
2016
|
+
lineCountOf(text),
|
|
2017
|
+
lineHeightOf(text)
|
|
2018
|
+
);
|
|
2019
|
+
if (curves) this.attach(text, curves);
|
|
1605
2020
|
layer.meta.curve = next;
|
|
1606
2021
|
}
|
|
1607
2022
|
text.initDimensions?.();
|
|
@@ -1614,12 +2029,14 @@ var TextCurveManager = class {
|
|
|
1614
2029
|
}
|
|
1615
2030
|
/** Remove the curve, restoring the authored text box width. */
|
|
1616
2031
|
clear(layerId, save = true) {
|
|
1617
|
-
|
|
2032
|
+
const layer = this.layers.get(layerId);
|
|
2033
|
+
const shape = layer?.meta.curve?.shape;
|
|
2034
|
+
return this.apply(layerId, { shape }, save);
|
|
1618
2035
|
}
|
|
1619
2036
|
/**
|
|
1620
|
-
* Rebuild the
|
|
1621
|
-
* font size all change the
|
|
1622
|
-
* without this the curve keeps
|
|
2037
|
+
* Rebuild the paths from the stored parameters. Text content, line count,
|
|
2038
|
+
* font family and font size all change the geometry the paths are sized to —
|
|
2039
|
+
* without this the curve keeps that of the text it was created from.
|
|
1623
2040
|
*/
|
|
1624
2041
|
refresh(layerId, save = false) {
|
|
1625
2042
|
const curve = this.layers.get(layerId)?.meta.curve;
|
|
@@ -1632,14 +2049,84 @@ var TextCurveManager = class {
|
|
|
1632
2049
|
if (layer.meta.curve) this.refresh(layer.id);
|
|
1633
2050
|
}
|
|
1634
2051
|
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Puts every line's path on the object as one shape, and hooks per-line
|
|
2054
|
+
* measuring so each line is laid out on its own.
|
|
2055
|
+
*
|
|
2056
|
+
* The object's own path is the union of the lines: fabric sizes a curved text
|
|
2057
|
+
* object from it, and a single line's path would leave the rest of the block
|
|
2058
|
+
* outside the layer's box — where object caching clips it away.
|
|
2059
|
+
*/
|
|
2060
|
+
attach(text, curves) {
|
|
2061
|
+
const paths = curves.map(toFabricPath);
|
|
2062
|
+
const union = paths.length === 1 ? paths[0] : toFabricPath({ data: curves.map((c) => c.data).join(" "), length: 0 });
|
|
2063
|
+
text.set({
|
|
2064
|
+
path: union,
|
|
2065
|
+
pathAlign: "center",
|
|
2066
|
+
pathSide: "left",
|
|
2067
|
+
// Each line is centred on its own path by the measuring hook below, so
|
|
2068
|
+
// there is no single offset left to apply.
|
|
2069
|
+
pathStartOffset: 0
|
|
2070
|
+
});
|
|
2071
|
+
this.patchLineMeasure(text, paths, union);
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Fabric lays every line of a text object along `this.path`, from one
|
|
2075
|
+
* `pathStartOffset` — so a multi-line run piles all its lines on top of each
|
|
2076
|
+
* other. Each line is measured against its own path instead, by swapping the
|
|
2077
|
+
* path in around fabric's own per-line measure pass.
|
|
2078
|
+
*
|
|
2079
|
+
* The paths all share the first one's `pathOffset`, which is the origin
|
|
2080
|
+
* fabric subtracts to place a glyph in the object — without that each line
|
|
2081
|
+
* would be re-centred on its own bounding box and land back in the same spot.
|
|
2082
|
+
*
|
|
2083
|
+
* `textAlign` is switched to centre for the pass, which is how fabric centres
|
|
2084
|
+
* a line on its path; the authored value is restored before anything else
|
|
2085
|
+
* reads it.
|
|
2086
|
+
*/
|
|
2087
|
+
patchLineMeasure(text, paths, union) {
|
|
2088
|
+
this.unpatchLineMeasure(text);
|
|
2089
|
+
const original = text._measureLine;
|
|
2090
|
+
if (typeof original !== "function") return;
|
|
2091
|
+
for (const path of paths) path.pathOffset = union.pathOffset;
|
|
2092
|
+
const installed = function(lineIndex) {
|
|
2093
|
+
const path = paths[lineIndex];
|
|
2094
|
+
if (!path) return original.call(this, lineIndex);
|
|
2095
|
+
const previousPath = this.path;
|
|
2096
|
+
const previousAlign = this.textAlign;
|
|
2097
|
+
this.path = path;
|
|
2098
|
+
this.textAlign = "center";
|
|
2099
|
+
this.setPathInfo?.();
|
|
2100
|
+
try {
|
|
2101
|
+
return original.call(this, lineIndex);
|
|
2102
|
+
} finally {
|
|
2103
|
+
this.path = previousPath;
|
|
2104
|
+
this.textAlign = previousAlign;
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
const ownedOriginal = Object.prototype.hasOwnProperty.call(text, "_measureLine");
|
|
2108
|
+
text._measureLine = installed;
|
|
2109
|
+
patches.set(text, { original, installed, ownedOriginal });
|
|
2110
|
+
}
|
|
2111
|
+
/** Restore fabric's own per-line measuring, if this manager replaced it. */
|
|
2112
|
+
unpatchLineMeasure(text) {
|
|
2113
|
+
const patch = patches.get(text);
|
|
2114
|
+
if (!patch) return;
|
|
2115
|
+
if (text._measureLine === patch.installed) {
|
|
2116
|
+
if (patch.ownedOriginal) text._measureLine = patch.original;
|
|
2117
|
+
else delete text._measureLine;
|
|
2118
|
+
}
|
|
2119
|
+
patches.delete(text);
|
|
2120
|
+
}
|
|
1635
2121
|
detach(text, authoredWidth) {
|
|
2122
|
+
this.unpatchLineMeasure(text);
|
|
1636
2123
|
text.set({ path: null, pathStartOffset: 0 });
|
|
1637
2124
|
if (authoredWidth !== void 0 && authoredWidth > 0) text.set({ width: authoredWidth });
|
|
1638
2125
|
}
|
|
1639
2126
|
};
|
|
1640
2127
|
|
|
1641
2128
|
// src/mask-presets/manager.ts
|
|
1642
|
-
var
|
|
2129
|
+
var import_fabric7 = require("fabric");
|
|
1643
2130
|
|
|
1644
2131
|
// src/mask-presets/shapes.ts
|
|
1645
2132
|
var SHAPE_MASK_IDS = [
|
|
@@ -1906,13 +2393,13 @@ var MaskPresetManager = class {
|
|
|
1906
2393
|
objectCaching: false
|
|
1907
2394
|
};
|
|
1908
2395
|
if (isShapeMaskId(id)) {
|
|
1909
|
-
return new
|
|
2396
|
+
return new import_fabric7.Path(shapeMaskPathData(id), {
|
|
1910
2397
|
...shared,
|
|
1911
2398
|
scaleX: width / SHAPE_MASK_BOX,
|
|
1912
2399
|
scaleY: height / SHAPE_MASK_BOX
|
|
1913
2400
|
});
|
|
1914
2401
|
}
|
|
1915
|
-
return new
|
|
2402
|
+
return new import_fabric7.FabricImage(renderTextureMask(id), {
|
|
1916
2403
|
...shared,
|
|
1917
2404
|
scaleX: width / TEXTURE_MASK_SIZE,
|
|
1918
2405
|
scaleY: height / TEXTURE_MASK_SIZE
|
|
@@ -1921,7 +2408,7 @@ var MaskPresetManager = class {
|
|
|
1921
2408
|
};
|
|
1922
2409
|
|
|
1923
2410
|
// src/shadow.ts
|
|
1924
|
-
var
|
|
2411
|
+
var import_fabric8 = require("fabric");
|
|
1925
2412
|
|
|
1926
2413
|
// src/utils/color.ts
|
|
1927
2414
|
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
@@ -1959,7 +2446,7 @@ function applyLayerShadow(object, config) {
|
|
|
1959
2446
|
}
|
|
1960
2447
|
const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
|
|
1961
2448
|
object.set({
|
|
1962
|
-
shadow: new
|
|
2449
|
+
shadow: new import_fabric8.Shadow({
|
|
1963
2450
|
color,
|
|
1964
2451
|
blur: Math.max(0, next.blur),
|
|
1965
2452
|
offsetX: next.offsetX,
|
|
@@ -2073,7 +2560,7 @@ var UnitConverter = class {
|
|
|
2073
2560
|
};
|
|
2074
2561
|
|
|
2075
2562
|
// src/serialization.ts
|
|
2076
|
-
var
|
|
2563
|
+
var import_fabric9 = require("fabric");
|
|
2077
2564
|
var VERSION = "2.0.0";
|
|
2078
2565
|
function serializeEditor(editor) {
|
|
2079
2566
|
return {
|
|
@@ -2109,7 +2596,7 @@ async function deserializeEditor(editor, state) {
|
|
|
2109
2596
|
}
|
|
2110
2597
|
const staged = await Promise.all(
|
|
2111
2598
|
state.layers.map(async (serialized) => {
|
|
2112
|
-
const fabricObject = (await
|
|
2599
|
+
const fabricObject = (await import_fabric9.util.enlivenObjects([serialized.fabricObject]))[0];
|
|
2113
2600
|
if (!fabricObject) {
|
|
2114
2601
|
const source = serialized.fabricObject.src;
|
|
2115
2602
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -2120,7 +2607,7 @@ async function deserializeEditor(editor, state) {
|
|
|
2120
2607
|
return { serialized, fabricObject };
|
|
2121
2608
|
})
|
|
2122
2609
|
);
|
|
2123
|
-
const stagedBackground = state.backgroundImage ? (await
|
|
2610
|
+
const stagedBackground = state.backgroundImage ? (await import_fabric9.util.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
2124
2611
|
if (state.backgroundImage && !stagedBackground) {
|
|
2125
2612
|
const source = state.backgroundImage.src;
|
|
2126
2613
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -2163,11 +2650,16 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
2163
2650
|
if (serialized.opacity !== 1) {
|
|
2164
2651
|
editor.layers.setOpacity(layer.id, serialized.opacity);
|
|
2165
2652
|
}
|
|
2653
|
+
const childData = serialized.children ?? serialized.meta?.groupChildren;
|
|
2654
|
+
if (layer.type === "group" && Array.isArray(childData)) {
|
|
2655
|
+
editor.layers.adoptGroupChildren(layer, childData);
|
|
2656
|
+
if (layer.meta.groupChildren) delete layer.meta.groupChildren;
|
|
2657
|
+
}
|
|
2166
2658
|
return layer;
|
|
2167
2659
|
}
|
|
2168
2660
|
|
|
2169
2661
|
// src/export.ts
|
|
2170
|
-
var
|
|
2662
|
+
var import_fabric10 = require("fabric");
|
|
2171
2663
|
|
|
2172
2664
|
// src/displacement.ts
|
|
2173
2665
|
var CHANNEL_INDEX = {
|
|
@@ -2261,7 +2753,7 @@ async function exportPNG(canvas, options = {}) {
|
|
|
2261
2753
|
}
|
|
2262
2754
|
async function exportIsolatedPNG(source, objects, options = {}) {
|
|
2263
2755
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
2264
|
-
const canvas = new
|
|
2756
|
+
const canvas = new import_fabric10.StaticCanvas(element, {
|
|
2265
2757
|
width: options.width ?? source.getWidth(),
|
|
2266
2758
|
height: options.height ?? source.getHeight(),
|
|
2267
2759
|
backgroundColor: options.backgroundColor || void 0
|
|
@@ -2291,7 +2783,7 @@ async function exportPrintArea(source, area, options = {}) {
|
|
|
2291
2783
|
throw new Error("Print area does not overlap the canvas");
|
|
2292
2784
|
}
|
|
2293
2785
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
2294
|
-
const canvas = new
|
|
2786
|
+
const canvas = new import_fabric10.StaticCanvas(element, { width, height });
|
|
2295
2787
|
try {
|
|
2296
2788
|
const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
|
|
2297
2789
|
if (clones.length) canvas.add(...clones);
|
|
@@ -2731,7 +3223,7 @@ var ProjectManager = class {
|
|
|
2731
3223
|
};
|
|
2732
3224
|
|
|
2733
3225
|
// src/mask.ts
|
|
2734
|
-
var
|
|
3226
|
+
var import_fabric11 = require("fabric");
|
|
2735
3227
|
var MaskRefinementError = class extends Error {
|
|
2736
3228
|
constructor(code, message, cause) {
|
|
2737
3229
|
super(message);
|
|
@@ -2763,7 +3255,7 @@ var MaskController = class {
|
|
|
2763
3255
|
throw new Error("Mask dimensions must be positive integers");
|
|
2764
3256
|
}
|
|
2765
3257
|
const backing = this.makeCanvas(width, height);
|
|
2766
|
-
const image = new
|
|
3258
|
+
const image = new import_fabric11.FabricImage(backing, {
|
|
2767
3259
|
left: 0,
|
|
2768
3260
|
top: 0,
|
|
2769
3261
|
originX: "left",
|
|
@@ -2989,10 +3481,10 @@ var MaskController = class {
|
|
|
2989
3481
|
};
|
|
2990
3482
|
|
|
2991
3483
|
// src/masks/manager.ts
|
|
2992
|
-
var
|
|
3484
|
+
var import_fabric18 = require("fabric");
|
|
2993
3485
|
|
|
2994
3486
|
// src/masks/compose.ts
|
|
2995
|
-
var
|
|
3487
|
+
var import_fabric12 = require("fabric");
|
|
2996
3488
|
var MODE_OPERATION = {
|
|
2997
3489
|
add: "source-over",
|
|
2998
3490
|
subtract: "destination-out",
|
|
@@ -3002,7 +3494,7 @@ function neutralize(child) {
|
|
|
3002
3494
|
child.set({ opacity: 0, globalCompositeOperation: "source-over" });
|
|
3003
3495
|
}
|
|
3004
3496
|
function baseRect(box) {
|
|
3005
|
-
return new
|
|
3497
|
+
return new import_fabric12.Rect({
|
|
3006
3498
|
left: box.left,
|
|
3007
3499
|
top: box.top,
|
|
3008
3500
|
width: Math.max(1, box.width),
|
|
@@ -3029,7 +3521,7 @@ function composeMaskGroup(children, entries, options) {
|
|
|
3029
3521
|
});
|
|
3030
3522
|
const first = entries.find((entry) => entry.visible);
|
|
3031
3523
|
const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
|
|
3032
|
-
return new
|
|
3524
|
+
return new import_fabric12.Group(withBase, {
|
|
3033
3525
|
absolutePositioned: options.absolute,
|
|
3034
3526
|
// Cached, so the children's compositing operations resolve against each
|
|
3035
3527
|
// other instead of against the page underneath the mask.
|
|
@@ -3043,15 +3535,15 @@ function needsAbsoluteSpace(entries) {
|
|
|
3043
3535
|
}
|
|
3044
3536
|
|
|
3045
3537
|
// src/masks/edit.ts
|
|
3046
|
-
var
|
|
3538
|
+
var import_fabric14 = require("fabric");
|
|
3047
3539
|
|
|
3048
3540
|
// src/masks/space.ts
|
|
3049
|
-
var
|
|
3541
|
+
var import_fabric13 = require("fabric");
|
|
3050
3542
|
function matrixOf(object) {
|
|
3051
3543
|
return object.calcTransformMatrix();
|
|
3052
3544
|
}
|
|
3053
3545
|
function applyMatrix(object, matrix) {
|
|
3054
|
-
const decomposed =
|
|
3546
|
+
const decomposed = import_fabric13.util.qrDecompose(matrix);
|
|
3055
3547
|
object.set({
|
|
3056
3548
|
flipX: false,
|
|
3057
3549
|
flipY: false,
|
|
@@ -3068,19 +3560,19 @@ function applyMatrix(object, matrix) {
|
|
|
3068
3560
|
object.setCoords();
|
|
3069
3561
|
}
|
|
3070
3562
|
function toCanvasSpace(object, host) {
|
|
3071
|
-
applyMatrix(object,
|
|
3563
|
+
applyMatrix(object, import_fabric13.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
|
|
3072
3564
|
}
|
|
3073
3565
|
function toHostSpace(object, host) {
|
|
3074
3566
|
applyMatrix(
|
|
3075
3567
|
object,
|
|
3076
|
-
|
|
3568
|
+
import_fabric13.util.multiplyTransformMatrices(import_fabric13.util.invertTransform(matrixOf(host)), matrixOf(object))
|
|
3077
3569
|
);
|
|
3078
3570
|
}
|
|
3079
3571
|
function relativeMatrix(object, host) {
|
|
3080
|
-
return
|
|
3572
|
+
return import_fabric13.util.multiplyTransformMatrices(import_fabric13.util.invertTransform(matrixOf(host)), matrixOf(object));
|
|
3081
3573
|
}
|
|
3082
3574
|
function applyRelativeMatrix(object, host, rel) {
|
|
3083
|
-
applyMatrix(object,
|
|
3575
|
+
applyMatrix(object, import_fabric13.util.multiplyTransformMatrices(matrixOf(host), rel));
|
|
3084
3576
|
}
|
|
3085
3577
|
function asObject(clip) {
|
|
3086
3578
|
return clip;
|
|
@@ -3193,8 +3685,8 @@ var MaskEditController = class {
|
|
|
3193
3685
|
if (!this.handle || !this.child || !this.group) return;
|
|
3194
3686
|
applyMatrix(
|
|
3195
3687
|
this.child,
|
|
3196
|
-
|
|
3197
|
-
|
|
3688
|
+
import_fabric14.util.multiplyTransformMatrices(
|
|
3689
|
+
import_fabric14.util.invertTransform(matrixOf(this.group)),
|
|
3198
3690
|
matrixOf(this.handle)
|
|
3199
3691
|
)
|
|
3200
3692
|
);
|
|
@@ -3205,15 +3697,15 @@ var MaskEditController = class {
|
|
|
3205
3697
|
};
|
|
3206
3698
|
|
|
3207
3699
|
// src/masks/store.ts
|
|
3208
|
-
var
|
|
3700
|
+
var import_fabric17 = require("fabric");
|
|
3209
3701
|
|
|
3210
3702
|
// src/masks/host.ts
|
|
3211
|
-
var
|
|
3703
|
+
var import_fabric15 = require("fabric");
|
|
3212
3704
|
function findCanvasHost(layers) {
|
|
3213
3705
|
return layers.getAll().find((layer) => layer.meta.canvasMask);
|
|
3214
3706
|
}
|
|
3215
3707
|
function createCanvasHost(canvas, layers) {
|
|
3216
|
-
const rect = new
|
|
3708
|
+
const rect = new import_fabric15.Rect({
|
|
3217
3709
|
left: 0,
|
|
3218
3710
|
top: 0,
|
|
3219
3711
|
width: canvas.getWidth(),
|
|
@@ -3262,9 +3754,9 @@ function hostBoxOf(canvas, host, absolute) {
|
|
|
3262
3754
|
}
|
|
3263
3755
|
|
|
3264
3756
|
// src/masks/install.ts
|
|
3265
|
-
var
|
|
3757
|
+
var import_fabric16 = require("fabric");
|
|
3266
3758
|
function convertSpace(host, sources, absolute) {
|
|
3267
|
-
const wasAbsolute = host.clipPath instanceof
|
|
3759
|
+
const wasAbsolute = host.clipPath instanceof import_fabric16.Group ? host.clipPath.absolutePositioned : absolute;
|
|
3268
3760
|
if (absolute === wasAbsolute) return;
|
|
3269
3761
|
for (const source of sources) {
|
|
3270
3762
|
if (absolute) toCanvasSpace(source, host);
|
|
@@ -3356,7 +3848,7 @@ var MaskStackStore = class {
|
|
|
3356
3848
|
const clip = host.clipPath;
|
|
3357
3849
|
if (!clip) return [];
|
|
3358
3850
|
const entries = this.list(target);
|
|
3359
|
-
if (entries.length === 0 || !(clip instanceof
|
|
3851
|
+
if (entries.length === 0 || !(clip instanceof import_fabric17.Group)) return [asObject(clip)];
|
|
3360
3852
|
const children = unwrapGroup(clip);
|
|
3361
3853
|
const extra = children.length - entries.length;
|
|
3362
3854
|
return extra > 0 ? children.slice(extra) : children;
|
|
@@ -3456,7 +3948,7 @@ var LayerMaskManager = class extends MaskStackStore {
|
|
|
3456
3948
|
if (index === -1) return false;
|
|
3457
3949
|
this.endEdit(false);
|
|
3458
3950
|
this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
|
|
3459
|
-
const group = host.clipPath instanceof
|
|
3951
|
+
const group = host.clipPath instanceof import_fabric18.Group ? host.clipPath : null;
|
|
3460
3952
|
if (!group) return false;
|
|
3461
3953
|
const children = group.getObjects();
|
|
3462
3954
|
const child = children[children.length - entries.length + index];
|
|
@@ -3762,7 +4254,7 @@ var CanvasEditor = class {
|
|
|
3762
4254
|
const widthPx = this.units.toPixels(config.width);
|
|
3763
4255
|
const heightPx = this.units.toPixels(config.height);
|
|
3764
4256
|
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
3765
|
-
this.canvas = new
|
|
4257
|
+
this.canvas = new import_fabric19.Canvas(canvasElement, {
|
|
3766
4258
|
width: widthPx,
|
|
3767
4259
|
height: heightPx,
|
|
3768
4260
|
backgroundColor: this.designBackground,
|
|
@@ -3796,7 +4288,7 @@ var CanvasEditor = class {
|
|
|
3796
4288
|
// ─── Layer Operations ────────────────────────────────
|
|
3797
4289
|
async addImage(url, options) {
|
|
3798
4290
|
try {
|
|
3799
|
-
const img = await
|
|
4291
|
+
const img = await import_fabric19.FabricImage.fromURL(
|
|
3800
4292
|
url,
|
|
3801
4293
|
{},
|
|
3802
4294
|
{ originX: "left", originY: "top", ...options }
|
|
@@ -3822,7 +4314,7 @@ var CanvasEditor = class {
|
|
|
3822
4314
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
3823
4315
|
const previous = layer.fabricObject;
|
|
3824
4316
|
try {
|
|
3825
|
-
const replacement = await
|
|
4317
|
+
const replacement = await import_fabric19.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
3826
4318
|
replacement.set({
|
|
3827
4319
|
left: previous.left,
|
|
3828
4320
|
top: previous.top,
|
|
@@ -3851,7 +4343,7 @@ var CanvasEditor = class {
|
|
|
3851
4343
|
}
|
|
3852
4344
|
}
|
|
3853
4345
|
addText(text, options) {
|
|
3854
|
-
const textbox = new
|
|
4346
|
+
const textbox = new import_fabric19.Textbox(text, {
|
|
3855
4347
|
fontSize: 32,
|
|
3856
4348
|
fontFamily: "Arial",
|
|
3857
4349
|
fill: "#000000",
|
|
@@ -3892,20 +4384,30 @@ var CanvasEditor = class {
|
|
|
3892
4384
|
return value === void 0 ? token : escapeXml(value);
|
|
3893
4385
|
})
|
|
3894
4386
|
);
|
|
3895
|
-
const
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
4387
|
+
const specs = resolved.includes("<textPath") ? [textPathSpec(template, values)] : await explodeTemplateSvg(template, resolved, values);
|
|
4388
|
+
if (specs.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
4389
|
+
return this.history.transaction(async () => {
|
|
4390
|
+
const created = specs.map((spec) => {
|
|
4391
|
+
const layer2 = this.layers.add(spec.type, spec.object, spec.name);
|
|
4392
|
+
if (spec.curve) this.curves.apply(layer2.id, spec.curve, false);
|
|
4393
|
+
return layer2;
|
|
4394
|
+
});
|
|
4395
|
+
const layer = created.length === 1 ? created[0] : await this.groupLayers(
|
|
4396
|
+
created.map((l) => l.id),
|
|
4397
|
+
template.name
|
|
4398
|
+
) ?? created[0];
|
|
4399
|
+
layer.fabricObject.set({
|
|
4400
|
+
left: this.canvas.getWidth() / 2,
|
|
4401
|
+
top: this.canvas.getHeight() / 2,
|
|
4402
|
+
originX: "center",
|
|
4403
|
+
originY: "center"
|
|
4404
|
+
});
|
|
4405
|
+
layer.fabricObject.setCoords();
|
|
4406
|
+
this.layers.select(layer.id);
|
|
4407
|
+
this.canvas.requestRenderAll();
|
|
4408
|
+
this.history.save();
|
|
4409
|
+
return layer;
|
|
3904
4410
|
});
|
|
3905
|
-
const layer = this.layers.add("template", group, template.name);
|
|
3906
|
-
this.layers.select(layer.id);
|
|
3907
|
-
this.history.save();
|
|
3908
|
-
return layer;
|
|
3909
4411
|
}
|
|
3910
4412
|
removeLayer(id) {
|
|
3911
4413
|
if (this.crop.activeLayerId() === id) this.crop.cancel();
|
|
@@ -3917,6 +4419,10 @@ var CanvasEditor = class {
|
|
|
3917
4419
|
selectLayer(id) {
|
|
3918
4420
|
this.layers.select(id);
|
|
3919
4421
|
}
|
|
4422
|
+
/** Select several layers at once — what a panel's shift/⌘ click drives. */
|
|
4423
|
+
selectLayers(ids) {
|
|
4424
|
+
this.layers.selectMany(ids);
|
|
4425
|
+
}
|
|
3920
4426
|
getSelectedLayer() {
|
|
3921
4427
|
const active = this.canvas.getActiveObject();
|
|
3922
4428
|
if (!active) return null;
|
|
@@ -3968,10 +4474,10 @@ var CanvasEditor = class {
|
|
|
3968
4474
|
const next = { ...previous, ...adjustments };
|
|
3969
4475
|
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
3970
4476
|
image.filters = [
|
|
3971
|
-
new
|
|
3972
|
-
new
|
|
3973
|
-
new
|
|
3974
|
-
new
|
|
4477
|
+
new import_fabric19.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
|
|
4478
|
+
new import_fabric19.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
|
|
4479
|
+
new import_fabric19.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
|
|
4480
|
+
new import_fabric19.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
|
|
3975
4481
|
];
|
|
3976
4482
|
layer.meta.imageAdjustments = next;
|
|
3977
4483
|
image.applyFilters();
|
|
@@ -3979,42 +4485,55 @@ var CanvasEditor = class {
|
|
|
3979
4485
|
this.history.save();
|
|
3980
4486
|
return true;
|
|
3981
4487
|
}
|
|
3982
|
-
/**
|
|
4488
|
+
/**
|
|
4489
|
+
* Combine two or more layers into one group layer. Picking a group among them
|
|
4490
|
+
* **absorbs** it — its children join the new group and the old group record is
|
|
4491
|
+
* dropped — because groups are exactly one level deep.
|
|
4492
|
+
*/
|
|
3983
4493
|
async groupLayers(ids, name = "Group") {
|
|
3984
4494
|
const uniqueIds = [...new Set(ids)];
|
|
3985
|
-
const
|
|
3986
|
-
if (
|
|
4495
|
+
const picked = uniqueIds.map((id) => this.layers.get(id)).filter((layer) => layer !== void 0 && layer.parentId === null);
|
|
4496
|
+
if (picked.length < 2 || picked.length !== uniqueIds.length) return null;
|
|
4497
|
+
const stack = this.layers.getAll();
|
|
4498
|
+
const topIndex = Math.max(...picked.map((layer) => stack.indexOf(layer)));
|
|
4499
|
+
const insertIndex = stack.slice(0, topIndex).filter((layer) => !picked.includes(layer)).length;
|
|
3987
4500
|
return this.history.transaction(() => {
|
|
3988
|
-
const
|
|
3989
|
-
const objects =
|
|
3990
|
-
for (const layer of
|
|
3991
|
-
|
|
4501
|
+
const children = [];
|
|
4502
|
+
const objects = [];
|
|
4503
|
+
for (const layer of picked) {
|
|
4504
|
+
if (layer.type === "group" && layer.children.length > 0) {
|
|
4505
|
+
const inner = layer.fabricObject.removeAll();
|
|
4506
|
+
for (const [index, object] of inner.entries()) {
|
|
4507
|
+
object.setCoords();
|
|
4508
|
+
const child = layer.children[index];
|
|
4509
|
+
if (!child) continue;
|
|
4510
|
+
children.push(child);
|
|
4511
|
+
objects.push(object);
|
|
4512
|
+
}
|
|
4513
|
+
layer.children = [];
|
|
4514
|
+
this.layers.detach(layer.id);
|
|
4515
|
+
} else {
|
|
4516
|
+
children.push(layer);
|
|
4517
|
+
objects.push(layer.fabricObject);
|
|
4518
|
+
this.layers.detach(layer.id);
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
const group = new import_fabric19.Group(objects);
|
|
3992
4522
|
const grouped = this.layers.add("group", group, name);
|
|
3993
|
-
|
|
4523
|
+
for (const child of children) child.parentId = grouped.id;
|
|
4524
|
+
grouped.children = children;
|
|
4525
|
+
this.layers.reorder(grouped.id, insertIndex);
|
|
3994
4526
|
this.layers.select(grouped.id);
|
|
3995
4527
|
this.history.save();
|
|
3996
4528
|
return grouped;
|
|
3997
4529
|
});
|
|
3998
4530
|
}
|
|
3999
|
-
/**
|
|
4531
|
+
/** Split a group back into the layers it holds, in place. */
|
|
4000
4532
|
async ungroupLayer(id) {
|
|
4001
4533
|
const grouped = this.layers.get(id);
|
|
4002
|
-
|
|
4003
|
-
if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
|
|
4004
|
-
const group = grouped.fabricObject;
|
|
4534
|
+
if (!grouped || grouped.type !== "group" || grouped.children.length === 0) return [];
|
|
4005
4535
|
return this.history.transaction(() => {
|
|
4006
|
-
const
|
|
4007
|
-
this.layers.remove(id);
|
|
4008
|
-
const restored = objects.map((object, index) => {
|
|
4009
|
-
object.setCoords();
|
|
4010
|
-
const data = childData[index];
|
|
4011
|
-
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
4012
|
-
if (data?.meta) layer.meta = structuredClone(data.meta);
|
|
4013
|
-
if (data && !data.visible) this.layers.setVisibility(layer.id, false);
|
|
4014
|
-
if (data?.locked) this.layers.setLocked(layer.id, true);
|
|
4015
|
-
if (data && data.opacity !== 1) this.layers.setOpacity(layer.id, data.opacity);
|
|
4016
|
-
return layer;
|
|
4017
|
-
});
|
|
4536
|
+
const restored = this.layers.explode(grouped);
|
|
4018
4537
|
this.history.save();
|
|
4019
4538
|
this.layers.select(restored[0]?.id ?? null);
|
|
4020
4539
|
return restored;
|
|
@@ -4062,7 +4581,7 @@ var CanvasEditor = class {
|
|
|
4062
4581
|
const layer = this.layers.get(id);
|
|
4063
4582
|
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
4064
4583
|
try {
|
|
4065
|
-
if (options.resolution === "source" && layer.fabricObject instanceof
|
|
4584
|
+
if (options.resolution === "source" && layer.fabricObject instanceof import_fabric19.FabricImage) {
|
|
4066
4585
|
const image = await layer.fabricObject.clone();
|
|
4067
4586
|
image.set({
|
|
4068
4587
|
left: 0,
|
|
@@ -4338,7 +4857,7 @@ var CanvasEditor = class {
|
|
|
4338
4857
|
return;
|
|
4339
4858
|
}
|
|
4340
4859
|
try {
|
|
4341
|
-
const image = await
|
|
4860
|
+
const image = await import_fabric19.FabricImage.fromURL(
|
|
4342
4861
|
url,
|
|
4343
4862
|
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
4344
4863
|
{ originX: "left", originY: "top" }
|
|
@@ -4762,6 +5281,7 @@ var AnnotationOverlay = class {
|
|
|
4762
5281
|
applyLayerShadow,
|
|
4763
5282
|
applyObjectSelectionStyle,
|
|
4764
5283
|
applySelectionStyle,
|
|
5284
|
+
buildCurveLinePaths,
|
|
4765
5285
|
buildCurvePathData,
|
|
4766
5286
|
clamp,
|
|
4767
5287
|
clearTextureMaskCache,
|
|
@@ -4773,6 +5293,7 @@ var AnnotationOverlay = class {
|
|
|
4773
5293
|
displaceRgba,
|
|
4774
5294
|
drawTiles,
|
|
4775
5295
|
escapeXml,
|
|
5296
|
+
explodeTemplateSvg,
|
|
4776
5297
|
exportDataURL,
|
|
4777
5298
|
exportMockup,
|
|
4778
5299
|
exportPNG,
|
|
@@ -4785,6 +5306,7 @@ var AnnotationOverlay = class {
|
|
|
4785
5306
|
isShapeMaskId,
|
|
4786
5307
|
isTextureMaskId,
|
|
4787
5308
|
needsAbsoluteSpace,
|
|
5309
|
+
normalizeTextCurve,
|
|
4788
5310
|
readLayerShadow,
|
|
4789
5311
|
renderTextureMask,
|
|
4790
5312
|
resetTransform,
|
|
@@ -4793,6 +5315,8 @@ var AnnotationOverlay = class {
|
|
|
4793
5315
|
sanitizeSvg,
|
|
4794
5316
|
serializeEditor,
|
|
4795
5317
|
shapeMaskPathData,
|
|
5318
|
+
textPathCurve,
|
|
5319
|
+
textPathSpec,
|
|
4796
5320
|
toCanvasSpace,
|
|
4797
5321
|
toHostSpace,
|
|
4798
5322
|
unwrapGroup
|