@overtone-art/canvas-editor-core 0.6.4 → 0.8.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.js CHANGED
@@ -21,10 +21,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AnnotationOverlay: () => AnnotationOverlay,
24
+ BLEND_GROUPS: () => BLEND_GROUPS,
25
+ BLEND_MODES: () => BLEND_MODES,
24
26
  CANVAS_MASK_TARGET: () => CANVAS_MASK_TARGET,
25
27
  CANVAS_SIZE_PRESETS: () => CANVAS_SIZE_PRESETS,
26
28
  CanvasEditor: () => CanvasEditor,
27
29
  CropController: () => CropController,
30
+ DEFAULT_GRADIENT_CONFIG: () => DEFAULT_GRADIENT_CONFIG,
28
31
  DEFAULT_LAYER_SHADOW: () => DEFAULT_LAYER_SHADOW,
29
32
  DEFAULT_PATTERN_CONFIG: () => DEFAULT_PATTERN_CONFIG,
30
33
  DEFAULT_SELECTION_STYLE: () => DEFAULT_SELECTION_STYLE,
@@ -47,12 +50,18 @@ __export(index_exports, {
47
50
  TEXTURE_MASK_IDS: () => TEXTURE_MASK_IDS,
48
51
  TEXTURE_MASK_SIZE: () => TEXTURE_MASK_SIZE,
49
52
  TextCurveManager: () => TextCurveManager,
53
+ TextWrapManager: () => TextWrapManager,
50
54
  TiledPatternObject: () => TiledPatternObject,
51
55
  UnitConverter: () => UnitConverter,
56
+ angleFromCoords: () => angleFromCoords,
52
57
  applyAspectLock: () => applyAspectLock,
53
58
  applyLayerShadow: () => applyLayerShadow,
54
59
  applyObjectSelectionStyle: () => applyObjectSelectionStyle,
55
60
  applySelectionStyle: () => applySelectionStyle,
61
+ applyTextWrapToObject: () => applyTextWrapToObject,
62
+ blendModeLabel: () => blendModeLabel,
63
+ blendModeOf: () => blendModeOf,
64
+ blendOperation: () => blendOperation,
56
65
  buildCurveLinePaths: () => buildCurveLinePaths,
57
66
  buildCurvePathData: () => buildCurvePathData,
58
67
  clamp: () => clamp,
@@ -78,9 +87,13 @@ __export(index_exports, {
78
87
  isMaskPresetId: () => isMaskPresetId,
79
88
  isShapeMaskId: () => isShapeMaskId,
80
89
  isTextureMaskId: () => isTextureMaskId,
90
+ linearCoords: () => linearCoords,
81
91
  needsAbsoluteSpace: () => needsAbsoluteSpace,
82
92
  normalizeTextCurve: () => normalizeTextCurve,
93
+ preWrapWordSplit: () => preWrapWordSplit,
94
+ readGradientConfig: () => readGradientConfig,
83
95
  readLayerShadow: () => readLayerShadow,
96
+ readTextWrap: () => readTextWrap,
84
97
  renderTextureMask: () => renderTextureMask,
85
98
  resetTransform: () => resetTransform,
86
99
  restoreLocks: () => restoreLocks,
@@ -91,13 +104,14 @@ __export(index_exports, {
91
104
  textPathCurve: () => textPathCurve,
92
105
  textPathSpec: () => textPathSpec,
93
106
  toCanvasSpace: () => toCanvasSpace,
107
+ toFabricGradient: () => toFabricGradient,
94
108
  toHostSpace: () => toHostSpace,
95
109
  unwrapGroup: () => unwrapGroup
96
110
  });
97
111
  module.exports = __toCommonJS(index_exports);
98
112
 
99
113
  // src/editor.ts
100
- var import_fabric20 = require("fabric");
114
+ var import_fabric22 = require("fabric");
101
115
 
102
116
  // src/events.ts
103
117
  var EventEmitter = class {
@@ -141,6 +155,53 @@ var import_fabric = require("fabric");
141
155
  var import_nanoid = require("nanoid");
142
156
  var generateId = () => (0, import_nanoid.nanoid)(12);
143
157
 
158
+ // src/blend.ts
159
+ var BLEND_MODES = [
160
+ "normal",
161
+ "darken",
162
+ "multiply",
163
+ "color-burn",
164
+ "lighten",
165
+ "screen",
166
+ "color-dodge",
167
+ "linear-dodge",
168
+ "overlay",
169
+ "soft-light",
170
+ "hard-light",
171
+ "difference",
172
+ "exclusion",
173
+ "hue",
174
+ "saturation",
175
+ "color",
176
+ "luminosity"
177
+ ];
178
+ var OPERATION_EXCEPTIONS = {
179
+ normal: "source-over",
180
+ "linear-dodge": "lighter"
181
+ };
182
+ function blendOperation(mode) {
183
+ return OPERATION_EXCEPTIONS[mode] ?? mode;
184
+ }
185
+ function blendModeOf(operation) {
186
+ if (!operation) return "normal";
187
+ for (const mode of BLEND_MODES) {
188
+ if (blendOperation(mode) === operation) return mode;
189
+ }
190
+ return "normal";
191
+ }
192
+ var BLEND_GROUPS = [
193
+ { label: "Normal", modes: ["normal"] },
194
+ { label: "Darken", modes: ["darken", "multiply", "color-burn"] },
195
+ { label: "Lighten", modes: ["lighten", "screen", "color-dodge", "linear-dodge"] },
196
+ { label: "Contrast", modes: ["overlay", "soft-light", "hard-light"] },
197
+ { label: "Comparative", modes: ["difference", "exclusion"] },
198
+ { label: "Composite", modes: ["hue", "saturation", "color", "luminosity"] }
199
+ ];
200
+ function blendModeLabel(mode) {
201
+ if (mode === "linear-dodge") return "Linear Dodge (Add)";
202
+ return mode.split("-").map((word) => (word[0] ?? "").toUpperCase() + word.slice(1)).join(" ");
203
+ }
204
+
144
205
  // src/layer.ts
145
206
  var Layer = class {
146
207
  id;
@@ -184,6 +245,7 @@ var Layer = class {
184
245
  return Object.keys(this.meta).length > 0;
185
246
  }
186
247
  toData() {
248
+ const blend = blendModeOf(this.fabricObject.globalCompositeOperation);
187
249
  return {
188
250
  id: this.id,
189
251
  type: this.type,
@@ -191,6 +253,7 @@ var Layer = class {
191
253
  visible: this.visible,
192
254
  locked: this.locked,
193
255
  opacity: this.opacity,
256
+ ...blend !== "normal" ? { blend } : {},
194
257
  ...this.hasMeta() ? { meta: this.meta } : {},
195
258
  ...this.children.length > 0 ? { children: this.children.map((c) => c.toData()) } : {}
196
259
  };
@@ -352,7 +415,11 @@ var LayerManager = class {
352
415
  visible: layer.visible,
353
416
  opacity: layer.opacity,
354
417
  selectable: !layer.locked,
355
- evented: !layer.locked
418
+ evented: !layer.locked,
419
+ // Compositing lives only on the fabric object (see `blendModeOf`); carry
420
+ // it from the object being replaced or a blend set before this call
421
+ // silently reverts to normal.
422
+ globalCompositeOperation: previous.globalCompositeOperation
356
423
  });
357
424
  this.canvas.insertAt(Math.max(0, stackIndex), fabricObject);
358
425
  if (layer.renderProxy) {
@@ -382,7 +449,12 @@ var LayerManager = class {
382
449
  visible: layer.visible,
383
450
  opacity: layer.opacity,
384
451
  selectable: !layer.locked,
385
- evented: !layer.locked
452
+ evented: !layer.locked,
453
+ // The proxy is what actually draws once a layer has one (see
454
+ // `LayerManager`'s class doc); a blend set before the proxy existed
455
+ // must not silently stop applying. Read off the layer's own object —
456
+ // that stays the single source of truth (see `blendModeOf`).
457
+ globalCompositeOperation: layer.fabricObject.globalCompositeOperation
386
458
  });
387
459
  this.canvas.add(proxy);
388
460
  this.syncZOrder();
@@ -1235,6 +1307,9 @@ var import_fabric5 = require("fabric");
1235
1307
  var import_fabric4 = require("fabric");
1236
1308
  var TEXT_FIT_SLACK = 0.5;
1237
1309
  var MEASURE_WIDTH = 1e5;
1310
+ var GROWTH_FACTOR = 1.1;
1311
+ var GROWTH_TRIES = 8;
1312
+ var SEARCH_TRIES = 24;
1238
1313
  function unwrappedWidth(text) {
1239
1314
  const authored = text.width;
1240
1315
  try {
@@ -1262,26 +1337,74 @@ function textInk(text) {
1262
1337
  const flip = text.flipX ? -1 : 1;
1263
1338
  return { width, dx: flip * (align.includes("right") ? slack : -slack) };
1264
1339
  }
1340
+ function wrapsAt(text, width) {
1341
+ text.set({ width });
1342
+ text.initDimensions?.();
1343
+ const authored = text.text.split("\n").length;
1344
+ return (text._textLines?.length ?? 0) > authored;
1345
+ }
1346
+ function widenPastSoftWrap(text, fitted) {
1347
+ if (!wrapsAt(text, fitted)) return fitted;
1348
+ let low = fitted;
1349
+ let high = fitted;
1350
+ let bracketed = false;
1351
+ for (let tries = 0; tries < GROWTH_TRIES; tries += 1) {
1352
+ low = high;
1353
+ high = high * GROWTH_FACTOR + 1;
1354
+ if (!wrapsAt(text, high)) {
1355
+ bracketed = true;
1356
+ break;
1357
+ }
1358
+ }
1359
+ if (!bracketed) return high;
1360
+ for (let tries = 0; tries < SEARCH_TRIES && high - low > TEXT_FIT_SLACK; tries += 1) {
1361
+ const mid = (low + high) / 2;
1362
+ if (wrapsAt(text, mid)) low = mid;
1363
+ else high = mid;
1364
+ }
1365
+ if (text.width !== high) wrapsAt(text, high);
1366
+ return high;
1367
+ }
1368
+ function restoreWidth(text, width) {
1369
+ try {
1370
+ text.set({ width });
1371
+ text.initDimensions?.();
1372
+ } catch {
1373
+ }
1374
+ }
1375
+ function isSettled(text, width) {
1376
+ if (wrapsAt(text, width)) return false;
1377
+ const tight = wrapsAt(text, width - TEXT_FIT_SLACK);
1378
+ wrapsAt(text, width);
1379
+ return tight;
1380
+ }
1265
1381
  function fitTextWidth(object) {
1266
1382
  const text = asText(object);
1267
1383
  if (!text || text.path) return false;
1268
- const before = textInk(text);
1269
- const fitted = unwrappedWidth(text) + TEXT_FIT_SLACK;
1270
- if (Math.abs(fitted - text.width) < TEXT_FIT_SLACK) return false;
1271
- const centre = text.getCenterPoint();
1272
- text.set({ width: fitted });
1273
- text.initDimensions?.();
1274
- const after = textInk(text);
1275
- const shift = (before.dx - after.dx) * (text.scaleX ?? 1);
1276
- const radians = (text.angle ?? 0) * Math.PI / 180;
1277
- const moved = new import_fabric4.Point(
1278
- centre.x + shift * Math.cos(radians),
1279
- centre.y + shift * Math.sin(radians)
1280
- );
1281
- text.setPositionByOrigin(moved, "center", "center");
1282
- text.setCoords();
1283
- text.dirty = true;
1284
- return true;
1384
+ const authored = text.width;
1385
+ try {
1386
+ const before = textInk(text);
1387
+ const fitted = unwrappedWidth(text) + TEXT_FIT_SLACK;
1388
+ if (Math.abs(fitted - authored) < TEXT_FIT_SLACK) return false;
1389
+ const nearFit = authored > fitted && authored <= fitted * GROWTH_FACTOR + 1;
1390
+ if (nearFit && isSettled(text, authored)) return false;
1391
+ const centre = text.getCenterPoint();
1392
+ const settled = widenPastSoftWrap(text, fitted);
1393
+ const after = textInk(text);
1394
+ const shift = (before.dx - after.dx) * (text.scaleX ?? 1);
1395
+ const radians = (text.angle ?? 0) * Math.PI / 180;
1396
+ const moved = new import_fabric4.Point(
1397
+ centre.x + shift * Math.cos(radians),
1398
+ centre.y + shift * Math.sin(radians)
1399
+ );
1400
+ text.setPositionByOrigin(moved, "center", "center");
1401
+ text.setCoords();
1402
+ text.dirty = true;
1403
+ return Math.abs(settled - authored) >= TEXT_FIT_SLACK;
1404
+ } catch (error) {
1405
+ restoreWidth(text, authored);
1406
+ throw error;
1407
+ }
1285
1408
  }
1286
1409
 
1287
1410
  // src/pattern/tile-geometry.ts
@@ -2339,8 +2462,268 @@ var TextCurveManager = class {
2339
2462
  }
2340
2463
  };
2341
2464
 
2342
- // src/mask-presets/manager.ts
2465
+ // src/text-wrap.ts
2466
+ var import_fabric9 = require("fabric");
2467
+
2468
+ // src/text-wrap-split.ts
2469
+ var WHITESPACE = /[ \t\r]/;
2470
+ function preWrapWordSplit(value) {
2471
+ const tokens = [];
2472
+ let index = 0;
2473
+ let first = true;
2474
+ while (index < value.length) {
2475
+ let space = "";
2476
+ while (index < value.length && WHITESPACE.test(value[index])) {
2477
+ space += value[index];
2478
+ index += 1;
2479
+ }
2480
+ let word = "";
2481
+ while (index < value.length && !WHITESPACE.test(value[index])) {
2482
+ word += value[index];
2483
+ index += 1;
2484
+ }
2485
+ tokens.push((first ? space : space.slice(1)) + word);
2486
+ first = false;
2487
+ }
2488
+ return tokens.length > 0 ? tokens : [""];
2489
+ }
2490
+
2491
+ // src/masks/space.ts
2343
2492
  var import_fabric8 = require("fabric");
2493
+ function matrixOf(object) {
2494
+ return object.calcTransformMatrix();
2495
+ }
2496
+ function applyMatrix(object, matrix) {
2497
+ const decomposed = import_fabric8.util.qrDecompose(matrix);
2498
+ object.set({
2499
+ flipX: false,
2500
+ flipY: false,
2501
+ originX: "center",
2502
+ originY: "center",
2503
+ left: decomposed.translateX,
2504
+ top: decomposed.translateY,
2505
+ scaleX: decomposed.scaleX,
2506
+ scaleY: decomposed.scaleY,
2507
+ angle: decomposed.angle,
2508
+ skewX: decomposed.skewX,
2509
+ skewY: 0
2510
+ });
2511
+ object.setCoords();
2512
+ }
2513
+ function toCanvasSpace(object, host) {
2514
+ applyMatrix(object, import_fabric8.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
2515
+ }
2516
+ function toHostSpace(object, host) {
2517
+ applyMatrix(
2518
+ object,
2519
+ import_fabric8.util.multiplyTransformMatrices(import_fabric8.util.invertTransform(matrixOf(host)), matrixOf(object))
2520
+ );
2521
+ }
2522
+ function relativeMatrix(object, host) {
2523
+ return import_fabric8.util.multiplyTransformMatrices(import_fabric8.util.invertTransform(matrixOf(host)), matrixOf(object));
2524
+ }
2525
+ function applyRelativeMatrix(object, host, rel) {
2526
+ applyMatrix(object, import_fabric8.util.multiplyTransformMatrices(matrixOf(host), rel));
2527
+ }
2528
+ function asObject(clip) {
2529
+ return clip;
2530
+ }
2531
+ function toMatrix(values) {
2532
+ if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
2533
+ return null;
2534
+ return [values[0], values[1], values[2], values[3], values[4], values[5]];
2535
+ }
2536
+ function fitToBox(object, box, zoom = 1) {
2537
+ const width = Math.max(1, box.width) * zoom;
2538
+ const height = Math.max(1, box.height) * zoom;
2539
+ object.set({
2540
+ originX: "center",
2541
+ originY: "center",
2542
+ angle: 0,
2543
+ skewX: 0,
2544
+ skewY: 0,
2545
+ left: box.left + box.width / 2,
2546
+ top: box.top + box.height / 2,
2547
+ scaleX: width / Math.max(1, object.width ?? 1),
2548
+ scaleY: height / Math.max(1, object.height ?? 1)
2549
+ });
2550
+ object.setCoords();
2551
+ }
2552
+ function unwrapGroup(group) {
2553
+ const children = group.removeAll();
2554
+ for (const child of children) child.setCoords();
2555
+ return children;
2556
+ }
2557
+
2558
+ // src/text-wrap.ts
2559
+ function patchWordSplit(text) {
2560
+ if (Object.prototype.hasOwnProperty.call(text, "wordSplit")) return;
2561
+ Object.defineProperty(text, "wordSplit", {
2562
+ value: preWrapWordSplit,
2563
+ configurable: true,
2564
+ writable: true
2565
+ });
2566
+ }
2567
+ function unpatchWordSplit(text) {
2568
+ if (Object.prototype.hasOwnProperty.call(text, "wordSplit")) {
2569
+ delete text.wordSplit;
2570
+ }
2571
+ }
2572
+ function readTextWrap(meta) {
2573
+ return { wrap: meta?.wrap ?? "none", overflow: meta?.overflow ?? "visible" };
2574
+ }
2575
+ function boxClip(text, makeRect) {
2576
+ return makeRect({
2577
+ width: Math.max(1, text.width ?? 1),
2578
+ height: Math.max(1, text.height ?? 1),
2579
+ originX: "center",
2580
+ originY: "center",
2581
+ left: 0,
2582
+ top: 0,
2583
+ objectCaching: false
2584
+ });
2585
+ }
2586
+ function hasMask(meta) {
2587
+ return !!meta.maskPreset || (meta.maskStack?.length ?? 0) > 0;
2588
+ }
2589
+ function applyTextWrapToObject(object, meta, makeRect = (options) => new import_fabric9.Rect(options)) {
2590
+ const text = asText(object);
2591
+ if (!text) return;
2592
+ const state = readTextWrap(meta);
2593
+ if (!text.path) {
2594
+ text.set({ splitByGrapheme: state.wrap === "break-all" });
2595
+ if (state.wrap === "pre-wrap") patchWordSplit(text);
2596
+ else unpatchWordSplit(text);
2597
+ text.initDimensions?.();
2598
+ }
2599
+ const clip = state.overflow === "hidden" ? boxClip(text, makeRect) : void 0;
2600
+ if (meta && hasMask(meta)) {
2601
+ const host = text.clipPath;
2602
+ if (host) {
2603
+ if (clip) toHostSpace(clip, asObject(host));
2604
+ host.clipPath = clip;
2605
+ }
2606
+ } else {
2607
+ text.clipPath = clip;
2608
+ }
2609
+ }
2610
+ var TextWrapManager = class {
2611
+ constructor(canvas, layers, history, events) {
2612
+ this.canvas = canvas;
2613
+ this.layers = layers;
2614
+ this.history = history;
2615
+ this.events = events;
2616
+ this.canvas.on("text:changed", this.onTextChanged);
2617
+ this.events.on("masks:changed", this.onMasksChanged);
2618
+ }
2619
+ canvas;
2620
+ layers;
2621
+ history;
2622
+ events;
2623
+ /**
2624
+ * Typing changes the run the box was fitted to, so an auto-width layer has to
2625
+ * re-fit. No history entry: fabric records the edit when editing exits, and a
2626
+ * save per keystroke would bury every earlier step.
2627
+ */
2628
+ onTextChanged = (event) => {
2629
+ const layer = event.target ? this.layers.findByObject(event.target) : void 0;
2630
+ if (!layer) return;
2631
+ this.refresh(layer.id, false);
2632
+ };
2633
+ /**
2634
+ * Both mask owners — the preset manager and every mask-stack mutation —
2635
+ * install their clip straight onto `clipPath`, dropping whatever was there,
2636
+ * and neither knows this layer had a box clip. Re-deriving on the one event
2637
+ * they both announce puts it back where it now belongs: nested under the new
2638
+ * mask, or at the top level when the last mask leaves. Waiting for the next
2639
+ * keystroke instead would leave a layer that should clip inside its box
2640
+ * serialized unclipped — which is what the print renderer reads.
2641
+ */
2642
+ onMasksChanged = ({ target }) => {
2643
+ this.refresh(target);
2644
+ };
2645
+ dispose() {
2646
+ this.canvas.off("text:changed", this.onTextChanged);
2647
+ this.events.off("masks:changed", this.onMasksChanged);
2648
+ }
2649
+ /** Both properties for a text layer, or null when it is not text. */
2650
+ get(layerId) {
2651
+ const layer = this.layers.get(layerId);
2652
+ if (!layer || !asText(layer.fabricObject)) return null;
2653
+ return readTextWrap(layer.meta);
2654
+ }
2655
+ apply(layerId, wrap, save = true) {
2656
+ const layer = this.layers.get(layerId);
2657
+ const text = layer ? asText(layer.fabricObject) : null;
2658
+ if (!layer || !text) return false;
2659
+ if (wrap === "none") {
2660
+ const curved = !!text.path;
2661
+ if (layer.meta.wrapWidth === void 0 && !curved) {
2662
+ layer.meta.wrapWidth = text.width ?? 0;
2663
+ }
2664
+ }
2665
+ layer.meta.wrap = wrap;
2666
+ return this.derive(layerId, save);
2667
+ }
2668
+ setOverflow(layerId, overflow, save = true) {
2669
+ const layer = this.layers.get(layerId);
2670
+ if (!layer || !asText(layer.fabricObject)) return false;
2671
+ layer.meta.overflow = overflow;
2672
+ return this.derive(layerId, save);
2673
+ }
2674
+ /** Back to the defaults: auto-width, unclipped. */
2675
+ clear(layerId, save = true) {
2676
+ const layer = this.layers.get(layerId);
2677
+ if (!layer || !asText(layer.fabricObject)) return false;
2678
+ delete layer.meta.overflow;
2679
+ return this.apply(layerId, "none", save);
2680
+ }
2681
+ /** Re-derive from the stored mode — after a text, font or size change. */
2682
+ refresh(layerId, save = false) {
2683
+ const layer = this.layers.get(layerId);
2684
+ if (!layer || !asText(layer.fabricObject)) return false;
2685
+ return this.derive(layerId, save);
2686
+ }
2687
+ /**
2688
+ * Re-derive every text layer — used after a state restore.
2689
+ *
2690
+ * Recursive because a template inserts as a group of real child layers, and
2691
+ * text inside one would otherwise keep whatever box it was restored with.
2692
+ */
2693
+ refreshAll() {
2694
+ const visit = (layers) => {
2695
+ for (const layer of layers) {
2696
+ if (asText(layer.fabricObject)) this.refresh(layer.id);
2697
+ if (layer.children.length > 0) visit(layer.children);
2698
+ }
2699
+ };
2700
+ visit(this.layers.getAll());
2701
+ }
2702
+ derive(layerId, save) {
2703
+ const layer = this.layers.get(layerId);
2704
+ const text = layer ? asText(layer.fabricObject) : null;
2705
+ if (!layer || !text) return false;
2706
+ const state = readTextWrap(layer.meta);
2707
+ if (!text.path) {
2708
+ if (state.wrap === "none") {
2709
+ fitTextWidth(text);
2710
+ } else if (layer.meta.wrapWidth !== void 0) {
2711
+ text.set({ width: layer.meta.wrapWidth });
2712
+ delete layer.meta.wrapWidth;
2713
+ }
2714
+ }
2715
+ applyTextWrapToObject(text, layer.meta);
2716
+ text.dirty = true;
2717
+ text.setCoords();
2718
+ this.canvas.requestRenderAll();
2719
+ this.events.emit("layer:modified", { layerId });
2720
+ if (save) this.history.save();
2721
+ return true;
2722
+ }
2723
+ };
2724
+
2725
+ // src/mask-presets/manager.ts
2726
+ var import_fabric10 = require("fabric");
2344
2727
 
2345
2728
  // src/mask-presets/shapes.ts
2346
2729
  var SHAPE_MASK_IDS = [
@@ -2573,6 +2956,7 @@ var MaskPresetManager = class {
2573
2956
  object.dirty = true;
2574
2957
  object.setCoords();
2575
2958
  this.canvas.requestRenderAll();
2959
+ this.events.emit("masks:changed", { target: layerId });
2576
2960
  this.events.emit("layer:modified", { layerId });
2577
2961
  if (save) this.history.save();
2578
2962
  return true;
@@ -2607,13 +2991,13 @@ var MaskPresetManager = class {
2607
2991
  objectCaching: false
2608
2992
  };
2609
2993
  if (isShapeMaskId(id)) {
2610
- return new import_fabric8.Path(shapeMaskPathData(id), {
2994
+ return new import_fabric10.Path(shapeMaskPathData(id), {
2611
2995
  ...shared,
2612
2996
  scaleX: width / SHAPE_MASK_BOX,
2613
2997
  scaleY: height / SHAPE_MASK_BOX
2614
2998
  });
2615
2999
  }
2616
- return new import_fabric8.FabricImage(renderTextureMask(id), {
3000
+ return new import_fabric10.FabricImage(renderTextureMask(id), {
2617
3001
  ...shared,
2618
3002
  scaleX: width / TEXTURE_MASK_SIZE,
2619
3003
  scaleY: height / TEXTURE_MASK_SIZE
@@ -2622,7 +3006,7 @@ var MaskPresetManager = class {
2622
3006
  };
2623
3007
 
2624
3008
  // src/shadow.ts
2625
- var import_fabric9 = require("fabric");
3009
+ var import_fabric11 = require("fabric");
2626
3010
 
2627
3011
  // src/utils/color.ts
2628
3012
  var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
@@ -2660,7 +3044,7 @@ function applyLayerShadow(object, config) {
2660
3044
  }
2661
3045
  const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
2662
3046
  object.set({
2663
- shadow: new import_fabric9.Shadow({
3047
+ shadow: new import_fabric11.Shadow({
2664
3048
  color,
2665
3049
  blur: Math.max(0, next.blur),
2666
3050
  offsetX: next.offsetX,
@@ -2774,7 +3158,7 @@ var UnitConverter = class {
2774
3158
  };
2775
3159
 
2776
3160
  // src/serialization.ts
2777
- var import_fabric10 = require("fabric");
3161
+ var import_fabric12 = require("fabric");
2778
3162
  var VERSION = "2.0.0";
2779
3163
  function serializeEditor(editor) {
2780
3164
  return {
@@ -2810,7 +3194,7 @@ async function deserializeEditor(editor, state) {
2810
3194
  }
2811
3195
  const staged = await Promise.all(
2812
3196
  state.layers.map(async (serialized) => {
2813
- const fabricObject = (await import_fabric10.util.enlivenObjects([serialized.fabricObject]))[0];
3197
+ const fabricObject = (await import_fabric12.util.enlivenObjects([serialized.fabricObject]))[0];
2814
3198
  if (!fabricObject) {
2815
3199
  const source = serialized.fabricObject.src;
2816
3200
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -2821,7 +3205,7 @@ async function deserializeEditor(editor, state) {
2821
3205
  return { serialized, fabricObject };
2822
3206
  })
2823
3207
  );
2824
- const stagedBackground = state.backgroundImage ? (await import_fabric10.util.enlivenObjects([state.backgroundImage]))[0] : null;
3208
+ const stagedBackground = state.backgroundImage ? (await import_fabric12.util.enlivenObjects([state.backgroundImage]))[0] : null;
2825
3209
  if (state.backgroundImage && !stagedBackground) {
2826
3210
  const source = state.backgroundImage.src;
2827
3211
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -2847,6 +3231,7 @@ async function deserializeEditor(editor, state) {
2847
3231
  for (const item of staged) {
2848
3232
  restoreLayer(editor, item.serialized, item.fabricObject);
2849
3233
  }
3234
+ editor.wrap.refreshAll();
2850
3235
  editor.canvas.requestRenderAll();
2851
3236
  }
2852
3237
  function restoreLayer(editor, serialized, fabricObject) {
@@ -2873,7 +3258,7 @@ function restoreLayer(editor, serialized, fabricObject) {
2873
3258
  }
2874
3259
 
2875
3260
  // src/export.ts
2876
- var import_fabric11 = require("fabric");
3261
+ var import_fabric13 = require("fabric");
2877
3262
 
2878
3263
  // src/displacement.ts
2879
3264
  var CHANNEL_INDEX = {
@@ -2967,7 +3352,7 @@ async function exportPNG(canvas, options = {}) {
2967
3352
  }
2968
3353
  async function exportIsolatedPNG(source, objects, options = {}) {
2969
3354
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
2970
- const canvas = new import_fabric11.StaticCanvas(element, {
3355
+ const canvas = new import_fabric13.StaticCanvas(element, {
2971
3356
  width: options.width ?? source.getWidth(),
2972
3357
  height: options.height ?? source.getHeight(),
2973
3358
  backgroundColor: options.backgroundColor || void 0
@@ -2997,7 +3382,7 @@ async function exportPrintArea(source, area, options = {}) {
2997
3382
  throw new Error("Print area does not overlap the canvas");
2998
3383
  }
2999
3384
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
3000
- const canvas = new import_fabric11.StaticCanvas(element, { width, height });
3385
+ const canvas = new import_fabric13.StaticCanvas(element, { width, height });
3001
3386
  try {
3002
3387
  const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
3003
3388
  if (clones.length) canvas.add(...clones);
@@ -3437,7 +3822,7 @@ var ProjectManager = class {
3437
3822
  };
3438
3823
 
3439
3824
  // src/mask.ts
3440
- var import_fabric12 = require("fabric");
3825
+ var import_fabric14 = require("fabric");
3441
3826
  var MaskRefinementError = class extends Error {
3442
3827
  constructor(code, message, cause) {
3443
3828
  super(message);
@@ -3469,7 +3854,7 @@ var MaskController = class {
3469
3854
  throw new Error("Mask dimensions must be positive integers");
3470
3855
  }
3471
3856
  const backing = this.makeCanvas(width, height);
3472
- const image = new import_fabric12.FabricImage(backing, {
3857
+ const image = new import_fabric14.FabricImage(backing, {
3473
3858
  left: 0,
3474
3859
  top: 0,
3475
3860
  originX: "left",
@@ -3695,10 +4080,10 @@ var MaskController = class {
3695
4080
  };
3696
4081
 
3697
4082
  // src/masks/manager.ts
3698
- var import_fabric19 = require("fabric");
4083
+ var import_fabric20 = require("fabric");
3699
4084
 
3700
4085
  // src/masks/compose.ts
3701
- var import_fabric13 = require("fabric");
4086
+ var import_fabric15 = require("fabric");
3702
4087
  var MODE_OPERATION = {
3703
4088
  add: "source-over",
3704
4089
  subtract: "destination-out",
@@ -3708,7 +4093,7 @@ function neutralize(child) {
3708
4093
  child.set({ opacity: 0, globalCompositeOperation: "source-over" });
3709
4094
  }
3710
4095
  function baseRect(box) {
3711
- return new import_fabric13.Rect({
4096
+ return new import_fabric15.Rect({
3712
4097
  left: box.left,
3713
4098
  top: box.top,
3714
4099
  width: Math.max(1, box.width),
@@ -3735,7 +4120,7 @@ function composeMaskGroup(children, entries, options) {
3735
4120
  });
3736
4121
  const first = entries.find((entry) => entry.visible);
3737
4122
  const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
3738
- return new import_fabric13.Group(withBase, {
4123
+ return new import_fabric15.Group(withBase, {
3739
4124
  absolutePositioned: options.absolute,
3740
4125
  // Cached, so the children's compositing operations resolve against each
3741
4126
  // other instead of against the page underneath the mask.
@@ -3749,76 +4134,7 @@ function needsAbsoluteSpace(entries) {
3749
4134
  }
3750
4135
 
3751
4136
  // src/masks/edit.ts
3752
- var import_fabric15 = require("fabric");
3753
-
3754
- // src/masks/space.ts
3755
- var import_fabric14 = require("fabric");
3756
- function matrixOf(object) {
3757
- return object.calcTransformMatrix();
3758
- }
3759
- function applyMatrix(object, matrix) {
3760
- const decomposed = import_fabric14.util.qrDecompose(matrix);
3761
- object.set({
3762
- flipX: false,
3763
- flipY: false,
3764
- originX: "center",
3765
- originY: "center",
3766
- left: decomposed.translateX,
3767
- top: decomposed.translateY,
3768
- scaleX: decomposed.scaleX,
3769
- scaleY: decomposed.scaleY,
3770
- angle: decomposed.angle,
3771
- skewX: decomposed.skewX,
3772
- skewY: 0
3773
- });
3774
- object.setCoords();
3775
- }
3776
- function toCanvasSpace(object, host) {
3777
- applyMatrix(object, import_fabric14.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3778
- }
3779
- function toHostSpace(object, host) {
3780
- applyMatrix(
3781
- object,
3782
- import_fabric14.util.multiplyTransformMatrices(import_fabric14.util.invertTransform(matrixOf(host)), matrixOf(object))
3783
- );
3784
- }
3785
- function relativeMatrix(object, host) {
3786
- return import_fabric14.util.multiplyTransformMatrices(import_fabric14.util.invertTransform(matrixOf(host)), matrixOf(object));
3787
- }
3788
- function applyRelativeMatrix(object, host, rel) {
3789
- applyMatrix(object, import_fabric14.util.multiplyTransformMatrices(matrixOf(host), rel));
3790
- }
3791
- function asObject(clip) {
3792
- return clip;
3793
- }
3794
- function toMatrix(values) {
3795
- if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
3796
- return null;
3797
- return [values[0], values[1], values[2], values[3], values[4], values[5]];
3798
- }
3799
- function fitToBox(object, box, zoom = 1) {
3800
- const width = Math.max(1, box.width) * zoom;
3801
- const height = Math.max(1, box.height) * zoom;
3802
- object.set({
3803
- originX: "center",
3804
- originY: "center",
3805
- angle: 0,
3806
- skewX: 0,
3807
- skewY: 0,
3808
- left: box.left + box.width / 2,
3809
- top: box.top + box.height / 2,
3810
- scaleX: width / Math.max(1, object.width ?? 1),
3811
- scaleY: height / Math.max(1, object.height ?? 1)
3812
- });
3813
- object.setCoords();
3814
- }
3815
- function unwrapGroup(group) {
3816
- const children = group.removeAll();
3817
- for (const child of children) child.setCoords();
3818
- return children;
3819
- }
3820
-
3821
- // src/masks/edit.ts
4137
+ var import_fabric16 = require("fabric");
3822
4138
  var MaskEditController = class {
3823
4139
  constructor(canvas, onCommit) {
3824
4140
  this.canvas = canvas;
@@ -3899,8 +4215,8 @@ var MaskEditController = class {
3899
4215
  if (!this.handle || !this.child || !this.group) return;
3900
4216
  applyMatrix(
3901
4217
  this.child,
3902
- import_fabric15.util.multiplyTransformMatrices(
3903
- import_fabric15.util.invertTransform(matrixOf(this.group)),
4218
+ import_fabric16.util.multiplyTransformMatrices(
4219
+ import_fabric16.util.invertTransform(matrixOf(this.group)),
3904
4220
  matrixOf(this.handle)
3905
4221
  )
3906
4222
  );
@@ -3911,15 +4227,15 @@ var MaskEditController = class {
3911
4227
  };
3912
4228
 
3913
4229
  // src/masks/store.ts
3914
- var import_fabric18 = require("fabric");
4230
+ var import_fabric19 = require("fabric");
3915
4231
 
3916
4232
  // src/masks/host.ts
3917
- var import_fabric16 = require("fabric");
4233
+ var import_fabric17 = require("fabric");
3918
4234
  function findCanvasHost(layers) {
3919
4235
  return layers.getAll().find((layer) => layer.meta.canvasMask);
3920
4236
  }
3921
4237
  function createCanvasHost(canvas, layers) {
3922
- const rect = new import_fabric16.Rect({
4238
+ const rect = new import_fabric17.Rect({
3923
4239
  left: 0,
3924
4240
  top: 0,
3925
4241
  width: canvas.getWidth(),
@@ -3968,9 +4284,9 @@ function hostBoxOf(canvas, host, absolute) {
3968
4284
  }
3969
4285
 
3970
4286
  // src/masks/install.ts
3971
- var import_fabric17 = require("fabric");
4287
+ var import_fabric18 = require("fabric");
3972
4288
  function convertSpace(host, sources, absolute) {
3973
- const wasAbsolute = host.clipPath instanceof import_fabric17.Group ? host.clipPath.absolutePositioned : absolute;
4289
+ const wasAbsolute = host.clipPath instanceof import_fabric18.Group ? host.clipPath.absolutePositioned : absolute;
3974
4290
  if (absolute === wasAbsolute) return;
3975
4291
  for (const source of sources) {
3976
4292
  if (absolute) toCanvasSpace(source, host);
@@ -4050,6 +4366,24 @@ var MaskStackStore = class {
4050
4366
  this.pinning = false;
4051
4367
  }
4052
4368
  }
4369
+ /**
4370
+ * Is the host's own `clipPath` the wrap manager's box clip rather than a mask?
4371
+ *
4372
+ * Adoption rescues a clip whose provenance is UNKNOWN — a mask preset, or a
4373
+ * hand-set `clipPath` from before stacks existed — so the first add() cannot
4374
+ * silently throw it away. A text layer's box clip is the opposite case: its
4375
+ * provenance is known exactly (the wrap manager derives it from
4376
+ * `meta.overflow` on every pass and re-installs it under whatever owns the
4377
+ * property next), and it is not a mask. Adopted, it becomes a stack row the
4378
+ * user never made whose 'add' union covers the whole box — swallowing the mask
4379
+ * they actually added.
4380
+ *
4381
+ * Only meaningful with an empty stack: once there are entries the top-level
4382
+ * clip is the composed group and the box clip is nested underneath it.
4383
+ */
4384
+ holdsBoxClip(meta) {
4385
+ return meta?.overflow === "hidden" && !meta.maskPreset;
4386
+ }
4053
4387
  /**
4054
4388
  * Take the current geometry back out of the composed clip, entry-aligned.
4055
4389
  *
@@ -4062,7 +4396,13 @@ var MaskStackStore = class {
4062
4396
  const clip = host.clipPath;
4063
4397
  if (!clip) return [];
4064
4398
  const entries = this.list(target);
4065
- if (entries.length === 0 || !(clip instanceof import_fabric18.Group)) return [asObject(clip)];
4399
+ const meta = this.hostLayer(target)?.meta;
4400
+ if (entries.length === 0 && this.holdsBoxClip(meta)) return [];
4401
+ if (entries.length === 0 || !(clip instanceof import_fabric19.Group)) {
4402
+ const source = asObject(clip);
4403
+ if (meta?.overflow === "hidden") source.clipPath = void 0;
4404
+ return [source];
4405
+ }
4066
4406
  const children = unwrapGroup(clip);
4067
4407
  const extra = children.length - entries.length;
4068
4408
  return extra > 0 ? children.slice(extra) : children;
@@ -4162,7 +4502,7 @@ var LayerMaskManager = class extends MaskStackStore {
4162
4502
  if (index === -1) return false;
4163
4503
  this.endEdit(false);
4164
4504
  this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
4165
- const group = host.clipPath instanceof import_fabric19.Group ? host.clipPath : null;
4505
+ const group = host.clipPath instanceof import_fabric20.Group ? host.clipPath : null;
4166
4506
  if (!group) return false;
4167
4507
  const children = group.getObjects();
4168
4508
  const child = children[children.length - entries.length + index];
@@ -4393,6 +4733,7 @@ var LayerMaskManager = class extends MaskStackStore {
4393
4733
  const clip = layer?.fabricObject.clipPath;
4394
4734
  if (!layer || !clip || layer.meta.pattern) return null;
4395
4735
  if ((layer.meta.maskStack ?? []).length > 0) return null;
4736
+ if (this.holdsBoxClip(layer.meta)) return null;
4396
4737
  const entry = {
4397
4738
  ...DEFAULT_ENTRY,
4398
4739
  id: generateId(),
@@ -4429,6 +4770,109 @@ var LayerMaskManager = class extends MaskStackStore {
4429
4770
  }
4430
4771
  };
4431
4772
 
4773
+ // src/gradient.ts
4774
+ var import_fabric21 = require("fabric");
4775
+ var DEFAULT_GRADIENT_CONFIG = {
4776
+ kind: "linear",
4777
+ angle: 90,
4778
+ center: { x: 0.5, y: 0.5 },
4779
+ radius: 0.5,
4780
+ stops: [
4781
+ { color: "#ffffff", opacity: 1, position: 0 },
4782
+ { color: "#000000", opacity: 1, position: 1 }
4783
+ ]
4784
+ };
4785
+ var MIN_RADIUS = 1e-3;
4786
+ var FALLBACK_STOP_COLOR = DEFAULT_GRADIENT_CONFIG.stops[0].color;
4787
+ function linearCoords(angle, box) {
4788
+ const radians = angle * Math.PI / 180;
4789
+ const sin = Math.sin(radians);
4790
+ const cos = Math.cos(radians);
4791
+ const vx = box.width * sin;
4792
+ const vy = -box.height * cos;
4793
+ const length = Math.abs(box.width * sin) + Math.abs(box.height * cos);
4794
+ const scale = length / (vx * vx + vy * vy || 1);
4795
+ return {
4796
+ x1: 0.5 - scale * vx / 2,
4797
+ y1: 0.5 - scale * vy / 2,
4798
+ x2: 0.5 + scale * vx / 2,
4799
+ y2: 0.5 + scale * vy / 2
4800
+ };
4801
+ }
4802
+ function angleFromCoords(coords, box) {
4803
+ const dx = (coords.x2 - coords.x1) / (box.width || 1);
4804
+ const dy = (coords.y2 - coords.y1) / (box.height || 1);
4805
+ const degrees = Math.atan2(dx, -dy) * 180 / Math.PI;
4806
+ return (degrees + 360) % 360;
4807
+ }
4808
+ function stopColor(stop) {
4809
+ const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4810
+ const color = new import_fabric21.Color(source);
4811
+ const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
4812
+ color.setAlpha(clamp(opacity, 0, 1));
4813
+ return color.toRgba();
4814
+ }
4815
+ function toFabricGradient(config, box) {
4816
+ const colorStops = [...config.stops].sort((a, b) => a.position - b.position).map((stop) => {
4817
+ const position = Number.isFinite(stop.position) ? stop.position : 0;
4818
+ return { offset: clamp(position, 0, 1), color: stopColor(stop) };
4819
+ });
4820
+ if (config.kind === "radial") {
4821
+ const radius = Math.max(MIN_RADIUS, config.radius);
4822
+ return new import_fabric21.Gradient({
4823
+ type: "radial",
4824
+ gradientUnits: "percentage",
4825
+ coords: {
4826
+ x1: config.center.x,
4827
+ y1: config.center.y,
4828
+ r1: 0,
4829
+ x2: config.center.x,
4830
+ y2: config.center.y,
4831
+ r2: radius
4832
+ },
4833
+ colorStops
4834
+ });
4835
+ }
4836
+ return new import_fabric21.Gradient({
4837
+ type: "linear",
4838
+ gradientUnits: "percentage",
4839
+ coords: linearCoords(config.angle, box),
4840
+ colorStops
4841
+ });
4842
+ }
4843
+ function readGradientConfig(object) {
4844
+ const fill = object.fill;
4845
+ if (!fill || typeof fill === "string" || !(fill instanceof import_fabric21.Gradient)) return null;
4846
+ const stops = fill.colorStops.map((stop) => {
4847
+ const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4848
+ const color = new import_fabric21.Color(source);
4849
+ const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
4850
+ return {
4851
+ color: `#${color.toHex().toLowerCase()}`,
4852
+ opacity: color.getAlpha(),
4853
+ position: clamp(offset, 0, 1)
4854
+ };
4855
+ });
4856
+ if (fill.type === "radial") {
4857
+ const { x1, y1, r2 } = fill.coords;
4858
+ return {
4859
+ kind: "radial",
4860
+ angle: DEFAULT_GRADIENT_CONFIG.angle,
4861
+ center: { x: x1, y: y1 },
4862
+ radius: r2,
4863
+ stops
4864
+ };
4865
+ }
4866
+ const box = { width: object.width || 1, height: object.height || 1 };
4867
+ return {
4868
+ kind: "linear",
4869
+ angle: angleFromCoords(fill.coords, box),
4870
+ center: { ...DEFAULT_GRADIENT_CONFIG.center },
4871
+ radius: DEFAULT_GRADIENT_CONFIG.radius,
4872
+ stops
4873
+ };
4874
+ }
4875
+
4432
4876
  // src/editor.ts
4433
4877
  var MIN_ZOOM = 0.1;
4434
4878
  var MAX_ZOOM = 8;
@@ -4445,6 +4889,7 @@ var CanvasEditor = class {
4445
4889
  crop;
4446
4890
  patterns;
4447
4891
  curves;
4892
+ wrap;
4448
4893
  maskPresets;
4449
4894
  /** Stacked boolean masks, per layer and for the design as a whole. */
4450
4895
  layerMasks;
@@ -4472,7 +4917,7 @@ var CanvasEditor = class {
4472
4917
  const widthPx = this.units.toPixels(config.width);
4473
4918
  const heightPx = this.units.toPixels(config.height);
4474
4919
  this.designBackground = config.backgroundColor ?? "#ffffff";
4475
- this.canvas = new import_fabric20.Canvas(canvasElement, {
4920
+ this.canvas = new import_fabric22.Canvas(canvasElement, {
4476
4921
  width: widthPx,
4477
4922
  height: heightPx,
4478
4923
  backgroundColor: this.designBackground,
@@ -4495,6 +4940,7 @@ var CanvasEditor = class {
4495
4940
  this.crop = new CropController(this.canvas, this.history, this.events);
4496
4941
  this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
4497
4942
  this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
4943
+ this.wrap = new TextWrapManager(this.canvas, this.layers, this.history, this.events);
4498
4944
  this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
4499
4945
  this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
4500
4946
  this.setupCanvasEvents();
@@ -4506,7 +4952,7 @@ var CanvasEditor = class {
4506
4952
  // ─── Layer Operations ────────────────────────────────
4507
4953
  async addImage(url, options) {
4508
4954
  try {
4509
- const img = await import_fabric20.FabricImage.fromURL(
4955
+ const img = await import_fabric22.FabricImage.fromURL(
4510
4956
  url,
4511
4957
  {},
4512
4958
  { originX: "left", originY: "top", ...options }
@@ -4532,7 +4978,7 @@ var CanvasEditor = class {
4532
4978
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
4533
4979
  const previous = layer.fabricObject;
4534
4980
  try {
4535
- const replacement = await import_fabric20.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4981
+ const replacement = await import_fabric22.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4536
4982
  replacement.set({
4537
4983
  left: previous.left,
4538
4984
  top: previous.top,
@@ -4561,7 +5007,7 @@ var CanvasEditor = class {
4561
5007
  }
4562
5008
  }
4563
5009
  addText(text, options) {
4564
- const textbox = new import_fabric20.Textbox(text, {
5010
+ const textbox = new import_fabric22.Textbox(text, {
4565
5011
  fontSize: 32,
4566
5012
  fontFamily: "Arial",
4567
5013
  fill: "#000000",
@@ -4583,6 +5029,65 @@ var CanvasEditor = class {
4583
5029
  this.history.save();
4584
5030
  return layer;
4585
5031
  }
5032
+ /**
5033
+ * Insert a gradient layer. It is an ordinary `Rect` with a gradient fill —
5034
+ * which is exactly why the Node print compositor needs no code for it.
5035
+ */
5036
+ addGradient(config = DEFAULT_GRADIENT_CONFIG, box) {
5037
+ const width = box?.width ?? this.canvas.getWidth();
5038
+ const height = box?.height ?? this.canvas.getHeight();
5039
+ const object = new import_fabric22.Rect({
5040
+ left: box?.left ?? 0,
5041
+ top: box?.top ?? 0,
5042
+ originX: "left",
5043
+ originY: "top",
5044
+ width,
5045
+ height,
5046
+ // A gradient fill IS the artwork; an outline is not part of this
5047
+ // layer's design, only its own gradient (unlike a plain shape, where a
5048
+ // stroke is a legitimate look).
5049
+ strokeWidth: 0,
5050
+ fill: toFabricGradient(config, { width, height })
5051
+ });
5052
+ const layer = this.layers.add("gradient", object, "Gradient");
5053
+ this.layers.select(layer.id);
5054
+ this.history.save();
5055
+ return layer;
5056
+ }
5057
+ /**
5058
+ * Repaint a layer's gradient. `save: false` while a slider is being dragged —
5059
+ * the settled value is the one worth an undo step.
5060
+ */
5061
+ setGradient(layerId, config, save = true) {
5062
+ const layer = this.layers.get(layerId);
5063
+ if (!layer) return;
5064
+ const object = layer.fabricObject;
5065
+ object.set({
5066
+ fill: toFabricGradient(config, {
5067
+ width: object.width || 1,
5068
+ height: object.height || 1
5069
+ })
5070
+ });
5071
+ object.dirty = true;
5072
+ this.canvas.requestRenderAll();
5073
+ this.events.emit("layer:modified", { layerId });
5074
+ if (save) this.history.save();
5075
+ }
5076
+ /** Set a layer's blend mode. Applied to the render proxy too, like opacity. */
5077
+ setBlend(layerId, mode) {
5078
+ const layer = this.layers.get(layerId);
5079
+ if (!layer) return;
5080
+ const operation = blendOperation(mode);
5081
+ layer.fabricObject.set({ globalCompositeOperation: operation });
5082
+ layer.fabricObject.dirty = true;
5083
+ if (layer.renderProxy) {
5084
+ layer.renderProxy.set({ globalCompositeOperation: operation });
5085
+ layer.renderProxy.dirty = true;
5086
+ }
5087
+ this.canvas.requestRenderAll();
5088
+ this.events.emit("layer:modified", { layerId });
5089
+ this.history.save();
5090
+ }
4586
5091
  async addTemplate(template, params) {
4587
5092
  const values = {};
4588
5093
  for (const parameter of template.parameters) {
@@ -4692,10 +5197,10 @@ var CanvasEditor = class {
4692
5197
  const next = { ...previous, ...adjustments };
4693
5198
  const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
4694
5199
  image.filters = [
4695
- new import_fabric20.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
4696
- new import_fabric20.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
4697
- new import_fabric20.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
4698
- new import_fabric20.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
5200
+ new import_fabric22.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
5201
+ new import_fabric22.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
5202
+ new import_fabric22.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
5203
+ new import_fabric22.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
4699
5204
  ];
4700
5205
  layer.meta.imageAdjustments = next;
4701
5206
  image.applyFilters();
@@ -4736,7 +5241,7 @@ var CanvasEditor = class {
4736
5241
  this.layers.detach(layer.id);
4737
5242
  }
4738
5243
  }
4739
- const group = new import_fabric20.Group(objects);
5244
+ const group = new import_fabric22.Group(objects);
4740
5245
  const grouped = this.layers.add("group", group, name);
4741
5246
  this.layers.adoptChildren(grouped, children);
4742
5247
  this.layers.reorder(grouped.id, insertIndex);
@@ -4798,7 +5303,7 @@ var CanvasEditor = class {
4798
5303
  const layer = this.layers.get(id);
4799
5304
  if (!layer) throw new Error(`Layer not found: ${id}`);
4800
5305
  try {
4801
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric20.FabricImage) {
5306
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric22.FabricImage) {
4802
5307
  const image = await layer.fabricObject.clone();
4803
5308
  image.set({
4804
5309
  left: 0,
@@ -5074,7 +5579,7 @@ var CanvasEditor = class {
5074
5579
  return;
5075
5580
  }
5076
5581
  try {
5077
- const image = await import_fabric20.FabricImage.fromURL(
5582
+ const image = await import_fabric22.FabricImage.fromURL(
5078
5583
  url,
5079
5584
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
5080
5585
  { originX: "left", originY: "top" }
@@ -5251,15 +5756,34 @@ var CanvasEditor = class {
5251
5756
  }
5252
5757
  // ─── Text curve ─────────────────────────────────────
5253
5758
  applyTextCurve(layerId, config) {
5254
- return this.curves.apply(layerId, config);
5759
+ const applied = this.curves.apply(layerId, config);
5760
+ if (applied) this.wrap.refresh(layerId);
5761
+ return applied;
5255
5762
  }
5256
5763
  clearTextCurve(layerId) {
5257
- return this.curves.clear(layerId);
5764
+ const cleared = this.curves.clear(layerId);
5765
+ if (cleared) this.wrap.refresh(layerId);
5766
+ return cleared;
5258
5767
  }
5259
5768
  getTextCurve(layerId) {
5260
5769
  return this.curves.get(layerId);
5261
5770
  }
5771
+ // ─── Text wrap ──────────────────────────────────────
5772
+ applyTextWrap(layerId, wrap) {
5773
+ return this.wrap.apply(layerId, wrap);
5774
+ }
5775
+ setTextOverflow(layerId, overflow) {
5776
+ return this.wrap.setOverflow(layerId, overflow);
5777
+ }
5778
+ getTextWrap(layerId) {
5779
+ return this.wrap.get(layerId);
5780
+ }
5262
5781
  // ─── Mask presets ───────────────────────────────────
5782
+ // A preset assigns `clipPath` wholesale, so a text layer clipping to its box
5783
+ // loses that clip the moment a mask lands on it. Putting it back is NOT wired
5784
+ // here: the preset manager announces `masks:changed` and the wrap manager
5785
+ // re-derives from that, which covers this facade, the manager called directly,
5786
+ // and `refreshAll()` on a restore — all with one mechanism.
5263
5787
  applyMaskPreset(layerId, id) {
5264
5788
  return this.maskPresets.apply(layerId, id);
5265
5789
  }
@@ -5348,6 +5872,7 @@ var CanvasEditor = class {
5348
5872
  this.history.dispose();
5349
5873
  this.patterns.dispose();
5350
5874
  this.curves.dispose();
5875
+ this.wrap.dispose();
5351
5876
  this.events.removeAllListeners();
5352
5877
  this.canvas.dispose();
5353
5878
  }
@@ -5471,10 +5996,13 @@ var AnnotationOverlay = class {
5471
5996
  // Annotate the CommonJS export names for ESM import in node:
5472
5997
  0 && (module.exports = {
5473
5998
  AnnotationOverlay,
5999
+ BLEND_GROUPS,
6000
+ BLEND_MODES,
5474
6001
  CANVAS_MASK_TARGET,
5475
6002
  CANVAS_SIZE_PRESETS,
5476
6003
  CanvasEditor,
5477
6004
  CropController,
6005
+ DEFAULT_GRADIENT_CONFIG,
5478
6006
  DEFAULT_LAYER_SHADOW,
5479
6007
  DEFAULT_PATTERN_CONFIG,
5480
6008
  DEFAULT_SELECTION_STYLE,
@@ -5497,12 +6025,18 @@ var AnnotationOverlay = class {
5497
6025
  TEXTURE_MASK_IDS,
5498
6026
  TEXTURE_MASK_SIZE,
5499
6027
  TextCurveManager,
6028
+ TextWrapManager,
5500
6029
  TiledPatternObject,
5501
6030
  UnitConverter,
6031
+ angleFromCoords,
5502
6032
  applyAspectLock,
5503
6033
  applyLayerShadow,
5504
6034
  applyObjectSelectionStyle,
5505
6035
  applySelectionStyle,
6036
+ applyTextWrapToObject,
6037
+ blendModeLabel,
6038
+ blendModeOf,
6039
+ blendOperation,
5506
6040
  buildCurveLinePaths,
5507
6041
  buildCurvePathData,
5508
6042
  clamp,
@@ -5528,9 +6062,13 @@ var AnnotationOverlay = class {
5528
6062
  isMaskPresetId,
5529
6063
  isShapeMaskId,
5530
6064
  isTextureMaskId,
6065
+ linearCoords,
5531
6066
  needsAbsoluteSpace,
5532
6067
  normalizeTextCurve,
6068
+ preWrapWordSplit,
6069
+ readGradientConfig,
5533
6070
  readLayerShadow,
6071
+ readTextWrap,
5534
6072
  renderTextureMask,
5535
6073
  resetTransform,
5536
6074
  restoreLocks,
@@ -5541,6 +6079,7 @@ var AnnotationOverlay = class {
5541
6079
  textPathCurve,
5542
6080
  textPathSpec,
5543
6081
  toCanvasSpace,
6082
+ toFabricGradient,
5544
6083
  toHostSpace,
5545
6084
  unwrapGroup
5546
6085
  });