@overtone-art/canvas-editor-core 0.5.1 → 0.6.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 +146 -15
- package/dist/index.d.ts +146 -15
- package/dist/index.global.js +52 -52
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +633 -167
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +584 -123
- 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;
|
|
@@ -1000,7 +1282,7 @@ var MAX_SNAPSHOT_PIXELS = 16e6;
|
|
|
1000
1282
|
var MAX_SNAPSHOT_SCALE = 8;
|
|
1001
1283
|
var SNAPSHOT_SHRINK_FACTOR = 2;
|
|
1002
1284
|
var TILE_BLEED_DEVICE_PX = 2;
|
|
1003
|
-
var TiledPatternObject = class _TiledPatternObject extends
|
|
1285
|
+
var TiledPatternObject = class _TiledPatternObject extends import_fabric4.FabricObject {
|
|
1004
1286
|
static type = "TiledPattern";
|
|
1005
1287
|
/** The layer's real object. Never rendered directly (it is kept at opacity 0). */
|
|
1006
1288
|
source;
|
|
@@ -1083,7 +1365,7 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1083
1365
|
const { tileW, tileH } = this.liveTile();
|
|
1084
1366
|
const centre = this.getCenterPoint();
|
|
1085
1367
|
const shift = this.shiftVector(tileW, tileH, this.liveAngle());
|
|
1086
|
-
return new
|
|
1368
|
+
return new import_fabric4.Point(centre.x - shift.x, centre.y - shift.y);
|
|
1087
1369
|
}
|
|
1088
1370
|
_render(ctx) {
|
|
1089
1371
|
const { width, height } = this.area;
|
|
@@ -1260,11 +1542,11 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1260
1542
|
const radians = angle * Math.PI / 180;
|
|
1261
1543
|
const cos = Math.cos(radians);
|
|
1262
1544
|
const sin = Math.sin(radians);
|
|
1263
|
-
return new
|
|
1545
|
+
return new import_fabric4.Point(dx * cos - dy * sin, dx * sin + dy * cos);
|
|
1264
1546
|
}
|
|
1265
1547
|
anchorFromOrigin(origin, tileW, tileH, angle) {
|
|
1266
1548
|
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1267
|
-
return new
|
|
1549
|
+
return new import_fabric4.Point(origin.x + shift.x, origin.y + shift.y);
|
|
1268
1550
|
}
|
|
1269
1551
|
/**
|
|
1270
1552
|
* Snapshot the source at (at least) `scale`, reusing the cached one while it
|
|
@@ -1524,7 +1806,7 @@ async function unbakeLegacyLayer(layer, state) {
|
|
|
1524
1806
|
cropY: state.original.cropY,
|
|
1525
1807
|
angle: state.original.angle
|
|
1526
1808
|
});
|
|
1527
|
-
image.clipPath = state.originalClip ? (await
|
|
1809
|
+
image.clipPath = state.originalClip ? (await import_fabric5.util.enlivenObjects([state.originalClip]))[0] : void 0;
|
|
1528
1810
|
restoreLocks(image, state.originalLocks);
|
|
1529
1811
|
image.setCoords();
|
|
1530
1812
|
}
|
|
@@ -1542,91 +1824,154 @@ function restoreLocks(obj, locks) {
|
|
|
1542
1824
|
}
|
|
1543
1825
|
|
|
1544
1826
|
// src/text-curve.ts
|
|
1545
|
-
var
|
|
1546
|
-
|
|
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
|
+
};
|
|
1547
1838
|
var MIN_ARC = 0.5;
|
|
1548
1839
|
var FULL_CIRCLE_ARC = 99.5;
|
|
1549
1840
|
var MIN_SWEEP = 0.12;
|
|
1550
|
-
var WAVE_PERIOD_EM = 4.1;
|
|
1551
1841
|
var WAVE_AMPLITUDE_EM = 0.9;
|
|
1842
|
+
var MIN_WAVE_LENGTH = 1;
|
|
1843
|
+
var MAX_WAVE_LENGTH = 12;
|
|
1552
1844
|
var WAVE_STEP = 6;
|
|
1553
|
-
var
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
return !!object && typeof object.text === "string";
|
|
1845
|
+
var MAX_WAVE_STEPS = 4e3;
|
|
1846
|
+
function round(value) {
|
|
1847
|
+
return Math.round(value * 100) / 100;
|
|
1557
1848
|
}
|
|
1558
|
-
function
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function bendsBaseline(config) {
|
|
1863
|
+
return config.shape === "arc" ? Math.abs(config.arc) >= MIN_ARC : config.wave > 0;
|
|
1568
1864
|
}
|
|
1569
|
-
function
|
|
1570
|
-
const magnitude = Math.min(100, Math.abs(arc));
|
|
1571
|
-
const direction = arc < 0 ? -1 : 1;
|
|
1572
|
-
const
|
|
1573
|
-
const
|
|
1574
|
-
const radius =
|
|
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;
|
|
1575
1877
|
const centerX = width / 2;
|
|
1576
1878
|
const point = (angle) => [
|
|
1577
1879
|
centerX + radius * Math.sin(angle),
|
|
1578
|
-
direction * radius *
|
|
1880
|
+
direction * (baseRadius - radius * Math.cos(angle))
|
|
1579
1881
|
];
|
|
1580
1882
|
const sweepFlag = direction > 0 ? 1 : 0;
|
|
1581
1883
|
const format = ([x, y]) => `${round(x)} ${round(y)}`;
|
|
1884
|
+
const r = round(radius);
|
|
1582
1885
|
if (full) {
|
|
1583
|
-
const start2 = point(-Math.PI);
|
|
1584
|
-
const
|
|
1886
|
+
const start2 = point(rotation - Math.PI);
|
|
1887
|
+
const opposite = point(rotation);
|
|
1585
1888
|
return {
|
|
1586
1889
|
data: [
|
|
1587
1890
|
`M ${format(start2)}`,
|
|
1588
|
-
`A ${
|
|
1589
|
-
`A ${
|
|
1891
|
+
`A ${r} ${r} 0 0 ${sweepFlag} ${format(opposite)}`,
|
|
1892
|
+
`A ${r} ${r} 0 0 ${sweepFlag} ${format(start2)}`
|
|
1590
1893
|
].join(" "),
|
|
1591
|
-
length:
|
|
1894
|
+
length: radius * Math.PI * 2
|
|
1592
1895
|
};
|
|
1593
1896
|
}
|
|
1594
|
-
const start = point(-sweep / 2);
|
|
1595
|
-
const end = point(sweep / 2);
|
|
1897
|
+
const start = point(rotation - sweep / 2);
|
|
1898
|
+
const end = point(rotation + sweep / 2);
|
|
1596
1899
|
const largeArc = sweep > Math.PI ? 1 : 0;
|
|
1597
1900
|
return {
|
|
1598
|
-
data: `M ${format(start)} A ${
|
|
1599
|
-
|
|
1600
|
-
length: width
|
|
1901
|
+
data: `M ${format(start)} A ${r} ${r} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
|
|
1902
|
+
length: radius * sweep
|
|
1601
1903
|
};
|
|
1602
1904
|
}
|
|
1603
|
-
function
|
|
1604
|
-
const amplitude =
|
|
1605
|
-
const period = Math.max(1, fontSize *
|
|
1606
|
-
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)));
|
|
1607
1911
|
const commands = [];
|
|
1608
1912
|
let length = 0;
|
|
1609
1913
|
let previous = null;
|
|
1610
1914
|
for (let index = 0; index <= steps; index++) {
|
|
1611
1915
|
const x = width * index / steps;
|
|
1612
|
-
const y = amplitude * Math.sin(x / period * Math.PI * 2);
|
|
1916
|
+
const y = dy + amplitude * Math.sin(x / period * Math.PI * 2 + phase);
|
|
1613
1917
|
if (previous) length += Math.hypot(x - previous[0], y - previous[1]);
|
|
1614
1918
|
previous = [x, y];
|
|
1615
1919
|
commands.push(`${index === 0 ? "M" : "L"} ${round(x)} ${round(y)}`);
|
|
1616
1920
|
}
|
|
1617
1921
|
return { data: commands.join(" "), length };
|
|
1618
1922
|
}
|
|
1619
|
-
function
|
|
1620
|
-
|
|
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;
|
|
1621
1941
|
}
|
|
1622
1942
|
function buildCurvePathData(config, width, fontSize) {
|
|
1623
|
-
|
|
1624
|
-
if (config.wave > 0) return wavePathData(width, fontSize, config.wave);
|
|
1625
|
-
return null;
|
|
1943
|
+
return buildCurveLinePaths(config, width, fontSize)?.[0] ?? null;
|
|
1626
1944
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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 });
|
|
1630
1975
|
}
|
|
1631
1976
|
var TextCurveManager = class {
|
|
1632
1977
|
constructor(canvas, layers, history, events) {
|
|
@@ -1643,33 +1988,35 @@ var TextCurveManager = class {
|
|
|
1643
1988
|
get(layerId) {
|
|
1644
1989
|
const layer = this.layers.get(layerId);
|
|
1645
1990
|
if (!layer || !isCurvable(layer.fabricObject)) return null;
|
|
1646
|
-
return layer.meta.curve ?? {
|
|
1991
|
+
return normalizeTextCurve(layer.meta.curve ?? {});
|
|
1647
1992
|
}
|
|
1648
1993
|
isCurved(layerId) {
|
|
1649
1994
|
const curve = this.get(layerId);
|
|
1650
|
-
return !!curve && (
|
|
1995
|
+
return !!curve && bendsBaseline(curve);
|
|
1651
1996
|
}
|
|
1652
|
-
/** Apply (or update) the curve on a text layer.
|
|
1997
|
+
/** Apply (or update) the curve on a text layer. A straight config clears it. */
|
|
1653
1998
|
apply(layerId, config, save = true) {
|
|
1654
1999
|
const layer = this.layers.get(layerId);
|
|
1655
2000
|
if (!layer || !isCurvable(layer.fabricObject)) return false;
|
|
1656
|
-
const next =
|
|
2001
|
+
const next = normalizeTextCurve(config);
|
|
1657
2002
|
const text = layer.fabricObject;
|
|
1658
2003
|
const run = measureText(text);
|
|
1659
|
-
|
|
1660
|
-
if (!curve) {
|
|
2004
|
+
if (!bendsBaseline(next)) {
|
|
1661
2005
|
this.detach(text, layer.meta.curveWidth);
|
|
1662
|
-
delete layer.meta.curve;
|
|
1663
2006
|
delete layer.meta.curveWidth;
|
|
2007
|
+
layer.meta.curve = next;
|
|
1664
2008
|
} else {
|
|
1665
2009
|
if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
|
|
1666
2010
|
text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
|
|
1667
|
-
text.
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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);
|
|
1673
2020
|
layer.meta.curve = next;
|
|
1674
2021
|
}
|
|
1675
2022
|
text.initDimensions?.();
|
|
@@ -1682,32 +2029,114 @@ var TextCurveManager = class {
|
|
|
1682
2029
|
}
|
|
1683
2030
|
/** Remove the curve, restoring the authored text box width. */
|
|
1684
2031
|
clear(layerId, save = true) {
|
|
1685
|
-
|
|
2032
|
+
const layer = this.layers.get(layerId);
|
|
2033
|
+
const shape = layer?.meta.curve?.shape;
|
|
2034
|
+
return this.apply(layerId, { shape }, save);
|
|
1686
2035
|
}
|
|
1687
2036
|
/**
|
|
1688
|
-
* Rebuild the
|
|
1689
|
-
* font size all change the
|
|
1690
|
-
* 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.
|
|
1691
2040
|
*/
|
|
1692
2041
|
refresh(layerId, save = false) {
|
|
1693
2042
|
const curve = this.layers.get(layerId)?.meta.curve;
|
|
1694
2043
|
if (!curve) return false;
|
|
1695
2044
|
return this.apply(layerId, curve, save);
|
|
1696
2045
|
}
|
|
1697
|
-
/**
|
|
2046
|
+
/**
|
|
2047
|
+
* Rebuild every curved layer — used after a state restore.
|
|
2048
|
+
*
|
|
2049
|
+
* `getAll` is the canvas stack, so this walks each group's children too:
|
|
2050
|
+
* curved text inside a group would otherwise keep the path geometry it was
|
|
2051
|
+
* restored with, and stop tracking its own text, font and size.
|
|
2052
|
+
*/
|
|
1698
2053
|
refreshAll() {
|
|
1699
|
-
|
|
1700
|
-
|
|
2054
|
+
const visit = (layers) => {
|
|
2055
|
+
for (const layer of layers) {
|
|
2056
|
+
if (layer.meta.curve) this.refresh(layer.id);
|
|
2057
|
+
if (layer.children.length > 0) visit(layer.children);
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
visit(this.layers.getAll());
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Puts every line's path on the object as one shape, and hooks per-line
|
|
2064
|
+
* measuring so each line is laid out on its own.
|
|
2065
|
+
*
|
|
2066
|
+
* The object's own path is the union of the lines: fabric sizes a curved text
|
|
2067
|
+
* object from it, and a single line's path would leave the rest of the block
|
|
2068
|
+
* outside the layer's box — where object caching clips it away.
|
|
2069
|
+
*/
|
|
2070
|
+
attach(text, curves) {
|
|
2071
|
+
const paths = curves.map(toFabricPath);
|
|
2072
|
+
const union = paths.length === 1 ? paths[0] : toFabricPath({ data: curves.map((c) => c.data).join(" "), length: 0 });
|
|
2073
|
+
text.set({
|
|
2074
|
+
path: union,
|
|
2075
|
+
pathAlign: "center",
|
|
2076
|
+
pathSide: "left",
|
|
2077
|
+
// Each line is centred on its own path by the measuring hook below, so
|
|
2078
|
+
// there is no single offset left to apply.
|
|
2079
|
+
pathStartOffset: 0
|
|
2080
|
+
});
|
|
2081
|
+
this.patchLineMeasure(text, paths, union);
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* Fabric lays every line of a text object along `this.path`, from one
|
|
2085
|
+
* `pathStartOffset` — so a multi-line run piles all its lines on top of each
|
|
2086
|
+
* other. Each line is measured against its own path instead, by swapping the
|
|
2087
|
+
* path in around fabric's own per-line measure pass.
|
|
2088
|
+
*
|
|
2089
|
+
* The paths all share the first one's `pathOffset`, which is the origin
|
|
2090
|
+
* fabric subtracts to place a glyph in the object — without that each line
|
|
2091
|
+
* would be re-centred on its own bounding box and land back in the same spot.
|
|
2092
|
+
*
|
|
2093
|
+
* `textAlign` is switched to centre for the pass, which is how fabric centres
|
|
2094
|
+
* a line on its path; the authored value is restored before anything else
|
|
2095
|
+
* reads it.
|
|
2096
|
+
*/
|
|
2097
|
+
patchLineMeasure(text, paths, union) {
|
|
2098
|
+
this.unpatchLineMeasure(text);
|
|
2099
|
+
const original = text._measureLine;
|
|
2100
|
+
if (typeof original !== "function") return;
|
|
2101
|
+
for (const path of paths) path.pathOffset = union.pathOffset;
|
|
2102
|
+
const installed = function(lineIndex) {
|
|
2103
|
+
const path = paths[lineIndex];
|
|
2104
|
+
if (!path) return original.call(this, lineIndex);
|
|
2105
|
+
const previousPath = this.path;
|
|
2106
|
+
const previousAlign = this.textAlign;
|
|
2107
|
+
this.path = path;
|
|
2108
|
+
this.textAlign = "center";
|
|
2109
|
+
this.setPathInfo?.();
|
|
2110
|
+
try {
|
|
2111
|
+
return original.call(this, lineIndex);
|
|
2112
|
+
} finally {
|
|
2113
|
+
this.path = previousPath;
|
|
2114
|
+
this.textAlign = previousAlign;
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
const ownedOriginal = Object.prototype.hasOwnProperty.call(text, "_measureLine");
|
|
2118
|
+
text._measureLine = installed;
|
|
2119
|
+
patches.set(text, { original, installed, ownedOriginal });
|
|
2120
|
+
}
|
|
2121
|
+
/** Restore fabric's own per-line measuring, if this manager replaced it. */
|
|
2122
|
+
unpatchLineMeasure(text) {
|
|
2123
|
+
const patch = patches.get(text);
|
|
2124
|
+
if (!patch) return;
|
|
2125
|
+
if (text._measureLine === patch.installed) {
|
|
2126
|
+
if (patch.ownedOriginal) text._measureLine = patch.original;
|
|
2127
|
+
else delete text._measureLine;
|
|
1701
2128
|
}
|
|
2129
|
+
patches.delete(text);
|
|
1702
2130
|
}
|
|
1703
2131
|
detach(text, authoredWidth) {
|
|
2132
|
+
this.unpatchLineMeasure(text);
|
|
1704
2133
|
text.set({ path: null, pathStartOffset: 0 });
|
|
1705
2134
|
if (authoredWidth !== void 0 && authoredWidth > 0) text.set({ width: authoredWidth });
|
|
1706
2135
|
}
|
|
1707
2136
|
};
|
|
1708
2137
|
|
|
1709
2138
|
// src/mask-presets/manager.ts
|
|
1710
|
-
var
|
|
2139
|
+
var import_fabric7 = require("fabric");
|
|
1711
2140
|
|
|
1712
2141
|
// src/mask-presets/shapes.ts
|
|
1713
2142
|
var SHAPE_MASK_IDS = [
|
|
@@ -1974,13 +2403,13 @@ var MaskPresetManager = class {
|
|
|
1974
2403
|
objectCaching: false
|
|
1975
2404
|
};
|
|
1976
2405
|
if (isShapeMaskId(id)) {
|
|
1977
|
-
return new
|
|
2406
|
+
return new import_fabric7.Path(shapeMaskPathData(id), {
|
|
1978
2407
|
...shared,
|
|
1979
2408
|
scaleX: width / SHAPE_MASK_BOX,
|
|
1980
2409
|
scaleY: height / SHAPE_MASK_BOX
|
|
1981
2410
|
});
|
|
1982
2411
|
}
|
|
1983
|
-
return new
|
|
2412
|
+
return new import_fabric7.FabricImage(renderTextureMask(id), {
|
|
1984
2413
|
...shared,
|
|
1985
2414
|
scaleX: width / TEXTURE_MASK_SIZE,
|
|
1986
2415
|
scaleY: height / TEXTURE_MASK_SIZE
|
|
@@ -1989,7 +2418,7 @@ var MaskPresetManager = class {
|
|
|
1989
2418
|
};
|
|
1990
2419
|
|
|
1991
2420
|
// src/shadow.ts
|
|
1992
|
-
var
|
|
2421
|
+
var import_fabric8 = require("fabric");
|
|
1993
2422
|
|
|
1994
2423
|
// src/utils/color.ts
|
|
1995
2424
|
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
@@ -2027,7 +2456,7 @@ function applyLayerShadow(object, config) {
|
|
|
2027
2456
|
}
|
|
2028
2457
|
const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
|
|
2029
2458
|
object.set({
|
|
2030
|
-
shadow: new
|
|
2459
|
+
shadow: new import_fabric8.Shadow({
|
|
2031
2460
|
color,
|
|
2032
2461
|
blur: Math.max(0, next.blur),
|
|
2033
2462
|
offsetX: next.offsetX,
|
|
@@ -2141,7 +2570,7 @@ var UnitConverter = class {
|
|
|
2141
2570
|
};
|
|
2142
2571
|
|
|
2143
2572
|
// src/serialization.ts
|
|
2144
|
-
var
|
|
2573
|
+
var import_fabric9 = require("fabric");
|
|
2145
2574
|
var VERSION = "2.0.0";
|
|
2146
2575
|
function serializeEditor(editor) {
|
|
2147
2576
|
return {
|
|
@@ -2177,7 +2606,7 @@ async function deserializeEditor(editor, state) {
|
|
|
2177
2606
|
}
|
|
2178
2607
|
const staged = await Promise.all(
|
|
2179
2608
|
state.layers.map(async (serialized) => {
|
|
2180
|
-
const fabricObject = (await
|
|
2609
|
+
const fabricObject = (await import_fabric9.util.enlivenObjects([serialized.fabricObject]))[0];
|
|
2181
2610
|
if (!fabricObject) {
|
|
2182
2611
|
const source = serialized.fabricObject.src;
|
|
2183
2612
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -2188,7 +2617,7 @@ async function deserializeEditor(editor, state) {
|
|
|
2188
2617
|
return { serialized, fabricObject };
|
|
2189
2618
|
})
|
|
2190
2619
|
);
|
|
2191
|
-
const stagedBackground = state.backgroundImage ? (await
|
|
2620
|
+
const stagedBackground = state.backgroundImage ? (await import_fabric9.util.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
2192
2621
|
if (state.backgroundImage && !stagedBackground) {
|
|
2193
2622
|
const source = state.backgroundImage.src;
|
|
2194
2623
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -2231,11 +2660,16 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
2231
2660
|
if (serialized.opacity !== 1) {
|
|
2232
2661
|
editor.layers.setOpacity(layer.id, serialized.opacity);
|
|
2233
2662
|
}
|
|
2663
|
+
const childData = serialized.children ?? serialized.meta?.groupChildren;
|
|
2664
|
+
if (layer.type === "group" && Array.isArray(childData)) {
|
|
2665
|
+
editor.layers.adoptGroupChildren(layer, childData);
|
|
2666
|
+
if (layer.meta.groupChildren) delete layer.meta.groupChildren;
|
|
2667
|
+
}
|
|
2234
2668
|
return layer;
|
|
2235
2669
|
}
|
|
2236
2670
|
|
|
2237
2671
|
// src/export.ts
|
|
2238
|
-
var
|
|
2672
|
+
var import_fabric10 = require("fabric");
|
|
2239
2673
|
|
|
2240
2674
|
// src/displacement.ts
|
|
2241
2675
|
var CHANNEL_INDEX = {
|
|
@@ -2329,7 +2763,7 @@ async function exportPNG(canvas, options = {}) {
|
|
|
2329
2763
|
}
|
|
2330
2764
|
async function exportIsolatedPNG(source, objects, options = {}) {
|
|
2331
2765
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
2332
|
-
const canvas = new
|
|
2766
|
+
const canvas = new import_fabric10.StaticCanvas(element, {
|
|
2333
2767
|
width: options.width ?? source.getWidth(),
|
|
2334
2768
|
height: options.height ?? source.getHeight(),
|
|
2335
2769
|
backgroundColor: options.backgroundColor || void 0
|
|
@@ -2359,7 +2793,7 @@ async function exportPrintArea(source, area, options = {}) {
|
|
|
2359
2793
|
throw new Error("Print area does not overlap the canvas");
|
|
2360
2794
|
}
|
|
2361
2795
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
2362
|
-
const canvas = new
|
|
2796
|
+
const canvas = new import_fabric10.StaticCanvas(element, { width, height });
|
|
2363
2797
|
try {
|
|
2364
2798
|
const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
|
|
2365
2799
|
if (clones.length) canvas.add(...clones);
|
|
@@ -2799,7 +3233,7 @@ var ProjectManager = class {
|
|
|
2799
3233
|
};
|
|
2800
3234
|
|
|
2801
3235
|
// src/mask.ts
|
|
2802
|
-
var
|
|
3236
|
+
var import_fabric11 = require("fabric");
|
|
2803
3237
|
var MaskRefinementError = class extends Error {
|
|
2804
3238
|
constructor(code, message, cause) {
|
|
2805
3239
|
super(message);
|
|
@@ -2831,7 +3265,7 @@ var MaskController = class {
|
|
|
2831
3265
|
throw new Error("Mask dimensions must be positive integers");
|
|
2832
3266
|
}
|
|
2833
3267
|
const backing = this.makeCanvas(width, height);
|
|
2834
|
-
const image = new
|
|
3268
|
+
const image = new import_fabric11.FabricImage(backing, {
|
|
2835
3269
|
left: 0,
|
|
2836
3270
|
top: 0,
|
|
2837
3271
|
originX: "left",
|
|
@@ -3057,10 +3491,10 @@ var MaskController = class {
|
|
|
3057
3491
|
};
|
|
3058
3492
|
|
|
3059
3493
|
// src/masks/manager.ts
|
|
3060
|
-
var
|
|
3494
|
+
var import_fabric18 = require("fabric");
|
|
3061
3495
|
|
|
3062
3496
|
// src/masks/compose.ts
|
|
3063
|
-
var
|
|
3497
|
+
var import_fabric12 = require("fabric");
|
|
3064
3498
|
var MODE_OPERATION = {
|
|
3065
3499
|
add: "source-over",
|
|
3066
3500
|
subtract: "destination-out",
|
|
@@ -3070,7 +3504,7 @@ function neutralize(child) {
|
|
|
3070
3504
|
child.set({ opacity: 0, globalCompositeOperation: "source-over" });
|
|
3071
3505
|
}
|
|
3072
3506
|
function baseRect(box) {
|
|
3073
|
-
return new
|
|
3507
|
+
return new import_fabric12.Rect({
|
|
3074
3508
|
left: box.left,
|
|
3075
3509
|
top: box.top,
|
|
3076
3510
|
width: Math.max(1, box.width),
|
|
@@ -3097,7 +3531,7 @@ function composeMaskGroup(children, entries, options) {
|
|
|
3097
3531
|
});
|
|
3098
3532
|
const first = entries.find((entry) => entry.visible);
|
|
3099
3533
|
const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
|
|
3100
|
-
return new
|
|
3534
|
+
return new import_fabric12.Group(withBase, {
|
|
3101
3535
|
absolutePositioned: options.absolute,
|
|
3102
3536
|
// Cached, so the children's compositing operations resolve against each
|
|
3103
3537
|
// other instead of against the page underneath the mask.
|
|
@@ -3111,15 +3545,15 @@ function needsAbsoluteSpace(entries) {
|
|
|
3111
3545
|
}
|
|
3112
3546
|
|
|
3113
3547
|
// src/masks/edit.ts
|
|
3114
|
-
var
|
|
3548
|
+
var import_fabric14 = require("fabric");
|
|
3115
3549
|
|
|
3116
3550
|
// src/masks/space.ts
|
|
3117
|
-
var
|
|
3551
|
+
var import_fabric13 = require("fabric");
|
|
3118
3552
|
function matrixOf(object) {
|
|
3119
3553
|
return object.calcTransformMatrix();
|
|
3120
3554
|
}
|
|
3121
3555
|
function applyMatrix(object, matrix) {
|
|
3122
|
-
const decomposed =
|
|
3556
|
+
const decomposed = import_fabric13.util.qrDecompose(matrix);
|
|
3123
3557
|
object.set({
|
|
3124
3558
|
flipX: false,
|
|
3125
3559
|
flipY: false,
|
|
@@ -3136,19 +3570,19 @@ function applyMatrix(object, matrix) {
|
|
|
3136
3570
|
object.setCoords();
|
|
3137
3571
|
}
|
|
3138
3572
|
function toCanvasSpace(object, host) {
|
|
3139
|
-
applyMatrix(object,
|
|
3573
|
+
applyMatrix(object, import_fabric13.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
|
|
3140
3574
|
}
|
|
3141
3575
|
function toHostSpace(object, host) {
|
|
3142
3576
|
applyMatrix(
|
|
3143
3577
|
object,
|
|
3144
|
-
|
|
3578
|
+
import_fabric13.util.multiplyTransformMatrices(import_fabric13.util.invertTransform(matrixOf(host)), matrixOf(object))
|
|
3145
3579
|
);
|
|
3146
3580
|
}
|
|
3147
3581
|
function relativeMatrix(object, host) {
|
|
3148
|
-
return
|
|
3582
|
+
return import_fabric13.util.multiplyTransformMatrices(import_fabric13.util.invertTransform(matrixOf(host)), matrixOf(object));
|
|
3149
3583
|
}
|
|
3150
3584
|
function applyRelativeMatrix(object, host, rel) {
|
|
3151
|
-
applyMatrix(object,
|
|
3585
|
+
applyMatrix(object, import_fabric13.util.multiplyTransformMatrices(matrixOf(host), rel));
|
|
3152
3586
|
}
|
|
3153
3587
|
function asObject(clip) {
|
|
3154
3588
|
return clip;
|
|
@@ -3261,8 +3695,8 @@ var MaskEditController = class {
|
|
|
3261
3695
|
if (!this.handle || !this.child || !this.group) return;
|
|
3262
3696
|
applyMatrix(
|
|
3263
3697
|
this.child,
|
|
3264
|
-
|
|
3265
|
-
|
|
3698
|
+
import_fabric14.util.multiplyTransformMatrices(
|
|
3699
|
+
import_fabric14.util.invertTransform(matrixOf(this.group)),
|
|
3266
3700
|
matrixOf(this.handle)
|
|
3267
3701
|
)
|
|
3268
3702
|
);
|
|
@@ -3273,15 +3707,15 @@ var MaskEditController = class {
|
|
|
3273
3707
|
};
|
|
3274
3708
|
|
|
3275
3709
|
// src/masks/store.ts
|
|
3276
|
-
var
|
|
3710
|
+
var import_fabric17 = require("fabric");
|
|
3277
3711
|
|
|
3278
3712
|
// src/masks/host.ts
|
|
3279
|
-
var
|
|
3713
|
+
var import_fabric15 = require("fabric");
|
|
3280
3714
|
function findCanvasHost(layers) {
|
|
3281
3715
|
return layers.getAll().find((layer) => layer.meta.canvasMask);
|
|
3282
3716
|
}
|
|
3283
3717
|
function createCanvasHost(canvas, layers) {
|
|
3284
|
-
const rect = new
|
|
3718
|
+
const rect = new import_fabric15.Rect({
|
|
3285
3719
|
left: 0,
|
|
3286
3720
|
top: 0,
|
|
3287
3721
|
width: canvas.getWidth(),
|
|
@@ -3330,9 +3764,9 @@ function hostBoxOf(canvas, host, absolute) {
|
|
|
3330
3764
|
}
|
|
3331
3765
|
|
|
3332
3766
|
// src/masks/install.ts
|
|
3333
|
-
var
|
|
3767
|
+
var import_fabric16 = require("fabric");
|
|
3334
3768
|
function convertSpace(host, sources, absolute) {
|
|
3335
|
-
const wasAbsolute = host.clipPath instanceof
|
|
3769
|
+
const wasAbsolute = host.clipPath instanceof import_fabric16.Group ? host.clipPath.absolutePositioned : absolute;
|
|
3336
3770
|
if (absolute === wasAbsolute) return;
|
|
3337
3771
|
for (const source of sources) {
|
|
3338
3772
|
if (absolute) toCanvasSpace(source, host);
|
|
@@ -3424,7 +3858,7 @@ var MaskStackStore = class {
|
|
|
3424
3858
|
const clip = host.clipPath;
|
|
3425
3859
|
if (!clip) return [];
|
|
3426
3860
|
const entries = this.list(target);
|
|
3427
|
-
if (entries.length === 0 || !(clip instanceof
|
|
3861
|
+
if (entries.length === 0 || !(clip instanceof import_fabric17.Group)) return [asObject(clip)];
|
|
3428
3862
|
const children = unwrapGroup(clip);
|
|
3429
3863
|
const extra = children.length - entries.length;
|
|
3430
3864
|
return extra > 0 ? children.slice(extra) : children;
|
|
@@ -3524,7 +3958,7 @@ var LayerMaskManager = class extends MaskStackStore {
|
|
|
3524
3958
|
if (index === -1) return false;
|
|
3525
3959
|
this.endEdit(false);
|
|
3526
3960
|
this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
|
|
3527
|
-
const group = host.clipPath instanceof
|
|
3961
|
+
const group = host.clipPath instanceof import_fabric18.Group ? host.clipPath : null;
|
|
3528
3962
|
if (!group) return false;
|
|
3529
3963
|
const children = group.getObjects();
|
|
3530
3964
|
const child = children[children.length - entries.length + index];
|
|
@@ -3830,7 +4264,7 @@ var CanvasEditor = class {
|
|
|
3830
4264
|
const widthPx = this.units.toPixels(config.width);
|
|
3831
4265
|
const heightPx = this.units.toPixels(config.height);
|
|
3832
4266
|
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
3833
|
-
this.canvas = new
|
|
4267
|
+
this.canvas = new import_fabric19.Canvas(canvasElement, {
|
|
3834
4268
|
width: widthPx,
|
|
3835
4269
|
height: heightPx,
|
|
3836
4270
|
backgroundColor: this.designBackground,
|
|
@@ -3864,7 +4298,7 @@ var CanvasEditor = class {
|
|
|
3864
4298
|
// ─── Layer Operations ────────────────────────────────
|
|
3865
4299
|
async addImage(url, options) {
|
|
3866
4300
|
try {
|
|
3867
|
-
const img = await
|
|
4301
|
+
const img = await import_fabric19.FabricImage.fromURL(
|
|
3868
4302
|
url,
|
|
3869
4303
|
{},
|
|
3870
4304
|
{ originX: "left", originY: "top", ...options }
|
|
@@ -3890,7 +4324,7 @@ var CanvasEditor = class {
|
|
|
3890
4324
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
3891
4325
|
const previous = layer.fabricObject;
|
|
3892
4326
|
try {
|
|
3893
|
-
const replacement = await
|
|
4327
|
+
const replacement = await import_fabric19.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
3894
4328
|
replacement.set({
|
|
3895
4329
|
left: previous.left,
|
|
3896
4330
|
top: previous.top,
|
|
@@ -3919,7 +4353,7 @@ var CanvasEditor = class {
|
|
|
3919
4353
|
}
|
|
3920
4354
|
}
|
|
3921
4355
|
addText(text, options) {
|
|
3922
|
-
const textbox = new
|
|
4356
|
+
const textbox = new import_fabric19.Textbox(text, {
|
|
3923
4357
|
fontSize: 32,
|
|
3924
4358
|
fontFamily: "Arial",
|
|
3925
4359
|
fill: "#000000",
|
|
@@ -3960,20 +4394,30 @@ var CanvasEditor = class {
|
|
|
3960
4394
|
return value === void 0 ? token : escapeXml(value);
|
|
3961
4395
|
})
|
|
3962
4396
|
);
|
|
3963
|
-
const
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
4397
|
+
const specs = resolved.includes("<textPath") ? [textPathSpec(template, values)] : await explodeTemplateSvg(template, resolved, values);
|
|
4398
|
+
if (specs.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
4399
|
+
return this.history.transaction(async () => {
|
|
4400
|
+
const created = specs.map((spec) => {
|
|
4401
|
+
const layer2 = this.layers.add(spec.type, spec.object, spec.name);
|
|
4402
|
+
if (spec.curve) this.curves.apply(layer2.id, spec.curve, false);
|
|
4403
|
+
return layer2;
|
|
4404
|
+
});
|
|
4405
|
+
const layer = created.length === 1 ? created[0] : await this.groupLayers(
|
|
4406
|
+
created.map((l) => l.id),
|
|
4407
|
+
template.name
|
|
4408
|
+
) ?? created[0];
|
|
4409
|
+
layer.fabricObject.set({
|
|
4410
|
+
left: this.canvas.getWidth() / 2,
|
|
4411
|
+
top: this.canvas.getHeight() / 2,
|
|
4412
|
+
originX: "center",
|
|
4413
|
+
originY: "center"
|
|
4414
|
+
});
|
|
4415
|
+
layer.fabricObject.setCoords();
|
|
4416
|
+
this.layers.select(layer.id);
|
|
4417
|
+
this.canvas.requestRenderAll();
|
|
4418
|
+
this.history.save();
|
|
4419
|
+
return layer;
|
|
3972
4420
|
});
|
|
3973
|
-
const layer = this.layers.add("template", group, template.name);
|
|
3974
|
-
this.layers.select(layer.id);
|
|
3975
|
-
this.history.save();
|
|
3976
|
-
return layer;
|
|
3977
4421
|
}
|
|
3978
4422
|
removeLayer(id) {
|
|
3979
4423
|
if (this.crop.activeLayerId() === id) this.crop.cancel();
|
|
@@ -3985,6 +4429,10 @@ var CanvasEditor = class {
|
|
|
3985
4429
|
selectLayer(id) {
|
|
3986
4430
|
this.layers.select(id);
|
|
3987
4431
|
}
|
|
4432
|
+
/** Select several layers at once — what a panel's shift/⌘ click drives. */
|
|
4433
|
+
selectLayers(ids) {
|
|
4434
|
+
this.layers.selectMany(ids);
|
|
4435
|
+
}
|
|
3988
4436
|
getSelectedLayer() {
|
|
3989
4437
|
const active = this.canvas.getActiveObject();
|
|
3990
4438
|
if (!active) return null;
|
|
@@ -4036,10 +4484,10 @@ var CanvasEditor = class {
|
|
|
4036
4484
|
const next = { ...previous, ...adjustments };
|
|
4037
4485
|
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
4038
4486
|
image.filters = [
|
|
4039
|
-
new
|
|
4040
|
-
new
|
|
4041
|
-
new
|
|
4042
|
-
new
|
|
4487
|
+
new import_fabric19.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
|
|
4488
|
+
new import_fabric19.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
|
|
4489
|
+
new import_fabric19.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
|
|
4490
|
+
new import_fabric19.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
|
|
4043
4491
|
];
|
|
4044
4492
|
layer.meta.imageAdjustments = next;
|
|
4045
4493
|
image.applyFilters();
|
|
@@ -4047,42 +4495,55 @@ var CanvasEditor = class {
|
|
|
4047
4495
|
this.history.save();
|
|
4048
4496
|
return true;
|
|
4049
4497
|
}
|
|
4050
|
-
/**
|
|
4498
|
+
/**
|
|
4499
|
+
* Combine two or more layers into one group layer. Picking a group among them
|
|
4500
|
+
* **absorbs** it — its children join the new group and the old group record is
|
|
4501
|
+
* dropped — because groups are exactly one level deep.
|
|
4502
|
+
*/
|
|
4051
4503
|
async groupLayers(ids, name = "Group") {
|
|
4052
4504
|
const uniqueIds = [...new Set(ids)];
|
|
4053
|
-
const
|
|
4054
|
-
if (
|
|
4505
|
+
const picked = uniqueIds.map((id) => this.layers.get(id)).filter((layer) => layer !== void 0 && layer.parentId === null);
|
|
4506
|
+
if (picked.length < 2 || picked.length !== uniqueIds.length) return null;
|
|
4507
|
+
const stack = this.layers.getAll();
|
|
4508
|
+
const topIndex = Math.max(...picked.map((layer) => stack.indexOf(layer)));
|
|
4509
|
+
const insertIndex = stack.slice(0, topIndex).filter((layer) => !picked.includes(layer)).length;
|
|
4055
4510
|
return this.history.transaction(() => {
|
|
4056
|
-
const
|
|
4057
|
-
const objects =
|
|
4058
|
-
for (const layer of
|
|
4059
|
-
|
|
4511
|
+
const children = [];
|
|
4512
|
+
const objects = [];
|
|
4513
|
+
for (const layer of picked) {
|
|
4514
|
+
if (layer.type === "group" && layer.children.length > 0) {
|
|
4515
|
+
const inner = layer.fabricObject.removeAll();
|
|
4516
|
+
for (const [index, object] of inner.entries()) {
|
|
4517
|
+
object.setCoords();
|
|
4518
|
+
const child = layer.children[index];
|
|
4519
|
+
if (!child) continue;
|
|
4520
|
+
children.push(child);
|
|
4521
|
+
objects.push(object);
|
|
4522
|
+
}
|
|
4523
|
+
layer.children = [];
|
|
4524
|
+
this.layers.detach(layer.id);
|
|
4525
|
+
} else {
|
|
4526
|
+
children.push(layer);
|
|
4527
|
+
objects.push(layer.fabricObject);
|
|
4528
|
+
this.layers.detach(layer.id);
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
const group = new import_fabric19.Group(objects);
|
|
4060
4532
|
const grouped = this.layers.add("group", group, name);
|
|
4061
|
-
|
|
4533
|
+
for (const child of children) child.parentId = grouped.id;
|
|
4534
|
+
grouped.children = children;
|
|
4535
|
+
this.layers.reorder(grouped.id, insertIndex);
|
|
4062
4536
|
this.layers.select(grouped.id);
|
|
4063
4537
|
this.history.save();
|
|
4064
4538
|
return grouped;
|
|
4065
4539
|
});
|
|
4066
4540
|
}
|
|
4067
|
-
/**
|
|
4541
|
+
/** Split a group back into the layers it holds, in place. */
|
|
4068
4542
|
async ungroupLayer(id) {
|
|
4069
4543
|
const grouped = this.layers.get(id);
|
|
4070
|
-
|
|
4071
|
-
if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
|
|
4072
|
-
const group = grouped.fabricObject;
|
|
4544
|
+
if (!grouped || grouped.type !== "group" || grouped.children.length === 0) return [];
|
|
4073
4545
|
return this.history.transaction(() => {
|
|
4074
|
-
const
|
|
4075
|
-
this.layers.remove(id);
|
|
4076
|
-
const restored = objects.map((object, index) => {
|
|
4077
|
-
object.setCoords();
|
|
4078
|
-
const data = childData[index];
|
|
4079
|
-
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
4080
|
-
if (data?.meta) layer.meta = structuredClone(data.meta);
|
|
4081
|
-
if (data && !data.visible) this.layers.setVisibility(layer.id, false);
|
|
4082
|
-
if (data?.locked) this.layers.setLocked(layer.id, true);
|
|
4083
|
-
if (data && data.opacity !== 1) this.layers.setOpacity(layer.id, data.opacity);
|
|
4084
|
-
return layer;
|
|
4085
|
-
});
|
|
4546
|
+
const restored = this.layers.explode(grouped);
|
|
4086
4547
|
this.history.save();
|
|
4087
4548
|
this.layers.select(restored[0]?.id ?? null);
|
|
4088
4549
|
return restored;
|
|
@@ -4130,7 +4591,7 @@ var CanvasEditor = class {
|
|
|
4130
4591
|
const layer = this.layers.get(id);
|
|
4131
4592
|
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
4132
4593
|
try {
|
|
4133
|
-
if (options.resolution === "source" && layer.fabricObject instanceof
|
|
4594
|
+
if (options.resolution === "source" && layer.fabricObject instanceof import_fabric19.FabricImage) {
|
|
4134
4595
|
const image = await layer.fabricObject.clone();
|
|
4135
4596
|
image.set({
|
|
4136
4597
|
left: 0,
|
|
@@ -4406,7 +4867,7 @@ var CanvasEditor = class {
|
|
|
4406
4867
|
return;
|
|
4407
4868
|
}
|
|
4408
4869
|
try {
|
|
4409
|
-
const image = await
|
|
4870
|
+
const image = await import_fabric19.FabricImage.fromURL(
|
|
4410
4871
|
url,
|
|
4411
4872
|
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
4412
4873
|
{ originX: "left", originY: "top" }
|
|
@@ -4830,6 +5291,7 @@ var AnnotationOverlay = class {
|
|
|
4830
5291
|
applyLayerShadow,
|
|
4831
5292
|
applyObjectSelectionStyle,
|
|
4832
5293
|
applySelectionStyle,
|
|
5294
|
+
buildCurveLinePaths,
|
|
4833
5295
|
buildCurvePathData,
|
|
4834
5296
|
clamp,
|
|
4835
5297
|
clearTextureMaskCache,
|
|
@@ -4841,6 +5303,7 @@ var AnnotationOverlay = class {
|
|
|
4841
5303
|
displaceRgba,
|
|
4842
5304
|
drawTiles,
|
|
4843
5305
|
escapeXml,
|
|
5306
|
+
explodeTemplateSvg,
|
|
4844
5307
|
exportDataURL,
|
|
4845
5308
|
exportMockup,
|
|
4846
5309
|
exportPNG,
|
|
@@ -4853,6 +5316,7 @@ var AnnotationOverlay = class {
|
|
|
4853
5316
|
isShapeMaskId,
|
|
4854
5317
|
isTextureMaskId,
|
|
4855
5318
|
needsAbsoluteSpace,
|
|
5319
|
+
normalizeTextCurve,
|
|
4856
5320
|
readLayerShadow,
|
|
4857
5321
|
renderTextureMask,
|
|
4858
5322
|
resetTransform,
|
|
@@ -4861,6 +5325,8 @@ var AnnotationOverlay = class {
|
|
|
4861
5325
|
sanitizeSvg,
|
|
4862
5326
|
serializeEditor,
|
|
4863
5327
|
shapeMaskPathData,
|
|
5328
|
+
textPathCurve,
|
|
5329
|
+
textPathSpec,
|
|
4864
5330
|
toCanvasSpace,
|
|
4865
5331
|
toHostSpace,
|
|
4866
5332
|
unwrapGroup
|