@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.mjs CHANGED
@@ -1,4 +1,11 @@
1
1
  import {
2
+ TEXT_FIT_SLACK,
3
+ TextWrapManager,
4
+ applyMatrix,
5
+ applyRelativeMatrix,
6
+ applyTextWrapToObject,
7
+ asObject,
8
+ asText,
2
9
  computeCoverPlacement,
3
10
  computePrintAreaClip,
4
11
  displaceRgba,
@@ -7,11 +14,22 @@ import {
7
14
  exportMockup,
8
15
  exportPNG,
9
16
  exportPrintArea,
10
- exportSVG
11
- } from "./chunk-MCBRZQ4M.mjs";
17
+ exportSVG,
18
+ fitTextWidth,
19
+ fitToBox,
20
+ matrixOf,
21
+ preWrapWordSplit,
22
+ readTextWrap,
23
+ relativeMatrix,
24
+ textInk,
25
+ toCanvasSpace,
26
+ toHostSpace,
27
+ toMatrix,
28
+ unwrapGroup
29
+ } from "./chunk-XNGPX7FG.mjs";
12
30
 
13
31
  // src/editor.ts
14
- import { Canvas, FabricImage as FabricImage3, Group as Group7, Textbox as Textbox2, filters } from "fabric";
32
+ import { Canvas, FabricImage as FabricImage3, Group as Group7, Rect as Rect4, Textbox as Textbox2, filters } from "fabric";
15
33
 
16
34
  // src/events.ts
17
35
  var EventEmitter = class {
@@ -55,6 +73,53 @@ import { ActiveSelection } from "fabric";
55
73
  import { nanoid } from "nanoid";
56
74
  var generateId = () => nanoid(12);
57
75
 
76
+ // src/blend.ts
77
+ var BLEND_MODES = [
78
+ "normal",
79
+ "darken",
80
+ "multiply",
81
+ "color-burn",
82
+ "lighten",
83
+ "screen",
84
+ "color-dodge",
85
+ "linear-dodge",
86
+ "overlay",
87
+ "soft-light",
88
+ "hard-light",
89
+ "difference",
90
+ "exclusion",
91
+ "hue",
92
+ "saturation",
93
+ "color",
94
+ "luminosity"
95
+ ];
96
+ var OPERATION_EXCEPTIONS = {
97
+ normal: "source-over",
98
+ "linear-dodge": "lighter"
99
+ };
100
+ function blendOperation(mode) {
101
+ return OPERATION_EXCEPTIONS[mode] ?? mode;
102
+ }
103
+ function blendModeOf(operation) {
104
+ if (!operation) return "normal";
105
+ for (const mode of BLEND_MODES) {
106
+ if (blendOperation(mode) === operation) return mode;
107
+ }
108
+ return "normal";
109
+ }
110
+ var BLEND_GROUPS = [
111
+ { label: "Normal", modes: ["normal"] },
112
+ { label: "Darken", modes: ["darken", "multiply", "color-burn"] },
113
+ { label: "Lighten", modes: ["lighten", "screen", "color-dodge", "linear-dodge"] },
114
+ { label: "Contrast", modes: ["overlay", "soft-light", "hard-light"] },
115
+ { label: "Comparative", modes: ["difference", "exclusion"] },
116
+ { label: "Composite", modes: ["hue", "saturation", "color", "luminosity"] }
117
+ ];
118
+ function blendModeLabel(mode) {
119
+ if (mode === "linear-dodge") return "Linear Dodge (Add)";
120
+ return mode.split("-").map((word) => (word[0] ?? "").toUpperCase() + word.slice(1)).join(" ");
121
+ }
122
+
58
123
  // src/layer.ts
59
124
  var Layer = class {
60
125
  id;
@@ -98,6 +163,7 @@ var Layer = class {
98
163
  return Object.keys(this.meta).length > 0;
99
164
  }
100
165
  toData() {
166
+ const blend = blendModeOf(this.fabricObject.globalCompositeOperation);
101
167
  return {
102
168
  id: this.id,
103
169
  type: this.type,
@@ -105,6 +171,7 @@ var Layer = class {
105
171
  visible: this.visible,
106
172
  locked: this.locked,
107
173
  opacity: this.opacity,
174
+ ...blend !== "normal" ? { blend } : {},
108
175
  ...this.hasMeta() ? { meta: this.meta } : {},
109
176
  ...this.children.length > 0 ? { children: this.children.map((c) => c.toData()) } : {}
110
177
  };
@@ -266,7 +333,11 @@ var LayerManager = class {
266
333
  visible: layer.visible,
267
334
  opacity: layer.opacity,
268
335
  selectable: !layer.locked,
269
- evented: !layer.locked
336
+ evented: !layer.locked,
337
+ // Compositing lives only on the fabric object (see `blendModeOf`); carry
338
+ // it from the object being replaced or a blend set before this call
339
+ // silently reverts to normal.
340
+ globalCompositeOperation: previous.globalCompositeOperation
270
341
  });
271
342
  this.canvas.insertAt(Math.max(0, stackIndex), fabricObject);
272
343
  if (layer.renderProxy) {
@@ -296,7 +367,12 @@ var LayerManager = class {
296
367
  visible: layer.visible,
297
368
  opacity: layer.opacity,
298
369
  selectable: !layer.locked,
299
- evented: !layer.locked
370
+ evented: !layer.locked,
371
+ // The proxy is what actually draws once a layer has one (see
372
+ // `LayerManager`'s class doc); a blend set before the proxy existed
373
+ // must not silently stop applying. Read off the layer's own object —
374
+ // that stays the single source of truth (see `blendModeOf`).
375
+ globalCompositeOperation: layer.fabricObject.globalCompositeOperation
300
376
  });
301
377
  this.canvas.add(proxy);
302
378
  this.syncZOrder();
@@ -1143,60 +1219,7 @@ var STROKE2 = "#22c55e";
1143
1219
  import { util } from "fabric";
1144
1220
 
1145
1221
  // src/pattern/tiled-pattern-object.ts
1146
- import { FabricObject, Point as Point2 } from "fabric";
1147
-
1148
- // src/text-fit.ts
1149
- import { Point } from "fabric";
1150
- var TEXT_FIT_SLACK = 0.5;
1151
- var MEASURE_WIDTH = 1e5;
1152
- function unwrappedWidth(text) {
1153
- const authored = text.width;
1154
- try {
1155
- text.set({ width: MEASURE_WIDTH });
1156
- text.initDimensions?.();
1157
- const measured = text.calcTextWidth?.() ?? authored;
1158
- return Number.isFinite(measured) && measured > 0 ? measured : authored;
1159
- } finally {
1160
- text.set({ width: authored });
1161
- text.initDimensions?.();
1162
- }
1163
- }
1164
- function asText(object) {
1165
- if (!object) return null;
1166
- const text = object;
1167
- return typeof text.text === "string" && typeof text.calcTextWidth === "function" ? text : null;
1168
- }
1169
- function textInk(text) {
1170
- const boxWidth = Math.max(0, text.width ?? 0);
1171
- const measured = text.calcTextWidth?.() ?? boxWidth;
1172
- const width = Math.max(1, Math.min(boxWidth, Number.isFinite(measured) ? measured : boxWidth));
1173
- const slack = (boxWidth - width) / 2;
1174
- const align = text.textAlign ?? "left";
1175
- if (align.includes("center")) return { width, dx: 0 };
1176
- const flip = text.flipX ? -1 : 1;
1177
- return { width, dx: flip * (align.includes("right") ? slack : -slack) };
1178
- }
1179
- function fitTextWidth(object) {
1180
- const text = asText(object);
1181
- if (!text || text.path) return false;
1182
- const before = textInk(text);
1183
- const fitted = unwrappedWidth(text) + TEXT_FIT_SLACK;
1184
- if (Math.abs(fitted - text.width) < TEXT_FIT_SLACK) return false;
1185
- const centre = text.getCenterPoint();
1186
- text.set({ width: fitted });
1187
- text.initDimensions?.();
1188
- const after = textInk(text);
1189
- const shift = (before.dx - after.dx) * (text.scaleX ?? 1);
1190
- const radians = (text.angle ?? 0) * Math.PI / 180;
1191
- const moved = new Point(
1192
- centre.x + shift * Math.cos(radians),
1193
- centre.y + shift * Math.sin(radians)
1194
- );
1195
- text.setPositionByOrigin(moved, "center", "center");
1196
- text.setCoords();
1197
- text.dirty = true;
1198
- return true;
1199
- }
1222
+ import { FabricObject, Point } from "fabric";
1200
1223
 
1201
1224
  // src/pattern/tile-geometry.ts
1202
1225
  var MAX_TILES_PER_AXIS = 200;
@@ -1353,7 +1376,7 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1353
1376
  const centre = this.getCenterPoint();
1354
1377
  const shift = this.shiftVector(tileW, tileH, this.liveAngle());
1355
1378
  const ink = this.inkOffset();
1356
- return new Point2(centre.x - shift.x - ink.x, centre.y - shift.y - ink.y);
1379
+ return new Point(centre.x - shift.x - ink.x, centre.y - shift.y - ink.y);
1357
1380
  }
1358
1381
  _render(ctx) {
1359
1382
  const { width, height } = this.area;
@@ -1485,12 +1508,12 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1485
1508
  */
1486
1509
  inkOffset() {
1487
1510
  const text = asText(this.source);
1488
- if (!text) return new Point2(0, 0);
1511
+ if (!text) return new Point(0, 0);
1489
1512
  const { dx } = textInk(text);
1490
- if (!dx) return new Point2(0, 0);
1513
+ if (!dx) return new Point(0, 0);
1491
1514
  const scaled = dx * (this.source.scaleX ?? 1);
1492
1515
  const radians = (this.source.angle ?? 0) * Math.PI / 180;
1493
- return new Point2(scaled * Math.cos(radians), scaled * Math.sin(radians));
1516
+ return new Point(scaled * Math.cos(radians), scaled * Math.sin(radians));
1494
1517
  }
1495
1518
  /** The source's on-canvas width, before the tile scale. */
1496
1519
  baseW() {
@@ -1551,11 +1574,11 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1551
1574
  const radians = angle * Math.PI / 180;
1552
1575
  const cos = Math.cos(radians);
1553
1576
  const sin = Math.sin(radians);
1554
- return new Point2(dx * cos - dy * sin, dx * sin + dy * cos);
1577
+ return new Point(dx * cos - dy * sin, dx * sin + dy * cos);
1555
1578
  }
1556
1579
  anchorFromOrigin(origin, tileW, tileH, angle) {
1557
1580
  const shift = this.shiftVector(tileW, tileH, angle);
1558
- return new Point2(origin.x + shift.x, origin.y + shift.y);
1581
+ return new Point(origin.x + shift.x, origin.y + shift.y);
1559
1582
  }
1560
1583
  /**
1561
1584
  * Snapshot the source at (at least) `scale`, reusing the cached one while it
@@ -1969,7 +1992,7 @@ function buildCurvePathData(config, width, fontSize) {
1969
1992
  }
1970
1993
 
1971
1994
  // src/text-curve.ts
1972
- var MEASURE_WIDTH2 = 1e5;
1995
+ var MEASURE_WIDTH = 1e5;
1973
1996
  var PATH_SLACK = 0.06;
1974
1997
  var FALLBACK_LINE_HEIGHT = 1.16;
1975
1998
  var patches = /* @__PURE__ */ new WeakMap();
@@ -2017,7 +2040,7 @@ function isCurvable(object) {
2017
2040
  function measureText(text) {
2018
2041
  const authored = text.width;
2019
2042
  try {
2020
- text.set({ width: MEASURE_WIDTH2 });
2043
+ text.set({ width: MEASURE_WIDTH });
2021
2044
  text.initDimensions?.();
2022
2045
  return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
2023
2046
  } finally {
@@ -2487,6 +2510,7 @@ var MaskPresetManager = class {
2487
2510
  object.dirty = true;
2488
2511
  object.setCoords();
2489
2512
  this.canvas.requestRenderAll();
2513
+ this.events.emit("masks:changed", { target: layerId });
2490
2514
  this.events.emit("layer:modified", { layerId });
2491
2515
  if (save) this.history.save();
2492
2516
  return true;
@@ -2761,6 +2785,7 @@ async function deserializeEditor(editor, state) {
2761
2785
  for (const item of staged) {
2762
2786
  restoreLayer(editor, item.serialized, item.fabricObject);
2763
2787
  }
2788
+ editor.wrap.refreshAll();
2764
2789
  editor.canvas.requestRenderAll();
2765
2790
  }
2766
2791
  function restoreLayer(editor, serialized, fabricObject) {
@@ -3412,76 +3437,7 @@ function needsAbsoluteSpace(entries) {
3412
3437
  }
3413
3438
 
3414
3439
  // src/masks/edit.ts
3415
- import { util as util4 } from "fabric";
3416
-
3417
- // src/masks/space.ts
3418
3440
  import { util as util3 } from "fabric";
3419
- function matrixOf(object) {
3420
- return object.calcTransformMatrix();
3421
- }
3422
- function applyMatrix(object, matrix) {
3423
- const decomposed = util3.qrDecompose(matrix);
3424
- object.set({
3425
- flipX: false,
3426
- flipY: false,
3427
- originX: "center",
3428
- originY: "center",
3429
- left: decomposed.translateX,
3430
- top: decomposed.translateY,
3431
- scaleX: decomposed.scaleX,
3432
- scaleY: decomposed.scaleY,
3433
- angle: decomposed.angle,
3434
- skewX: decomposed.skewX,
3435
- skewY: 0
3436
- });
3437
- object.setCoords();
3438
- }
3439
- function toCanvasSpace(object, host) {
3440
- applyMatrix(object, util3.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3441
- }
3442
- function toHostSpace(object, host) {
3443
- applyMatrix(
3444
- object,
3445
- util3.multiplyTransformMatrices(util3.invertTransform(matrixOf(host)), matrixOf(object))
3446
- );
3447
- }
3448
- function relativeMatrix(object, host) {
3449
- return util3.multiplyTransformMatrices(util3.invertTransform(matrixOf(host)), matrixOf(object));
3450
- }
3451
- function applyRelativeMatrix(object, host, rel) {
3452
- applyMatrix(object, util3.multiplyTransformMatrices(matrixOf(host), rel));
3453
- }
3454
- function asObject(clip) {
3455
- return clip;
3456
- }
3457
- function toMatrix(values) {
3458
- if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
3459
- return null;
3460
- return [values[0], values[1], values[2], values[3], values[4], values[5]];
3461
- }
3462
- function fitToBox(object, box, zoom = 1) {
3463
- const width = Math.max(1, box.width) * zoom;
3464
- const height = Math.max(1, box.height) * zoom;
3465
- object.set({
3466
- originX: "center",
3467
- originY: "center",
3468
- angle: 0,
3469
- skewX: 0,
3470
- skewY: 0,
3471
- left: box.left + box.width / 2,
3472
- top: box.top + box.height / 2,
3473
- scaleX: width / Math.max(1, object.width ?? 1),
3474
- scaleY: height / Math.max(1, object.height ?? 1)
3475
- });
3476
- object.setCoords();
3477
- }
3478
- function unwrapGroup(group) {
3479
- const children = group.removeAll();
3480
- for (const child of children) child.setCoords();
3481
- return children;
3482
- }
3483
-
3484
- // src/masks/edit.ts
3485
3441
  var MaskEditController = class {
3486
3442
  constructor(canvas, onCommit) {
3487
3443
  this.canvas = canvas;
@@ -3562,8 +3518,8 @@ var MaskEditController = class {
3562
3518
  if (!this.handle || !this.child || !this.group) return;
3563
3519
  applyMatrix(
3564
3520
  this.child,
3565
- util4.multiplyTransformMatrices(
3566
- util4.invertTransform(matrixOf(this.group)),
3521
+ util3.multiplyTransformMatrices(
3522
+ util3.invertTransform(matrixOf(this.group)),
3567
3523
  matrixOf(this.handle)
3568
3524
  )
3569
3525
  );
@@ -3713,6 +3669,24 @@ var MaskStackStore = class {
3713
3669
  this.pinning = false;
3714
3670
  }
3715
3671
  }
3672
+ /**
3673
+ * Is the host's own `clipPath` the wrap manager's box clip rather than a mask?
3674
+ *
3675
+ * Adoption rescues a clip whose provenance is UNKNOWN — a mask preset, or a
3676
+ * hand-set `clipPath` from before stacks existed — so the first add() cannot
3677
+ * silently throw it away. A text layer's box clip is the opposite case: its
3678
+ * provenance is known exactly (the wrap manager derives it from
3679
+ * `meta.overflow` on every pass and re-installs it under whatever owns the
3680
+ * property next), and it is not a mask. Adopted, it becomes a stack row the
3681
+ * user never made whose 'add' union covers the whole box — swallowing the mask
3682
+ * they actually added.
3683
+ *
3684
+ * Only meaningful with an empty stack: once there are entries the top-level
3685
+ * clip is the composed group and the box clip is nested underneath it.
3686
+ */
3687
+ holdsBoxClip(meta) {
3688
+ return meta?.overflow === "hidden" && !meta.maskPreset;
3689
+ }
3716
3690
  /**
3717
3691
  * Take the current geometry back out of the composed clip, entry-aligned.
3718
3692
  *
@@ -3725,7 +3699,13 @@ var MaskStackStore = class {
3725
3699
  const clip = host.clipPath;
3726
3700
  if (!clip) return [];
3727
3701
  const entries = this.list(target);
3728
- if (entries.length === 0 || !(clip instanceof Group5)) return [asObject(clip)];
3702
+ const meta = this.hostLayer(target)?.meta;
3703
+ if (entries.length === 0 && this.holdsBoxClip(meta)) return [];
3704
+ if (entries.length === 0 || !(clip instanceof Group5)) {
3705
+ const source = asObject(clip);
3706
+ if (meta?.overflow === "hidden") source.clipPath = void 0;
3707
+ return [source];
3708
+ }
3729
3709
  const children = unwrapGroup(clip);
3730
3710
  const extra = children.length - entries.length;
3731
3711
  return extra > 0 ? children.slice(extra) : children;
@@ -4056,6 +4036,7 @@ var LayerMaskManager = class extends MaskStackStore {
4056
4036
  const clip = layer?.fabricObject.clipPath;
4057
4037
  if (!layer || !clip || layer.meta.pattern) return null;
4058
4038
  if ((layer.meta.maskStack ?? []).length > 0) return null;
4039
+ if (this.holdsBoxClip(layer.meta)) return null;
4059
4040
  const entry = {
4060
4041
  ...DEFAULT_ENTRY,
4061
4042
  id: generateId(),
@@ -4092,6 +4073,109 @@ var LayerMaskManager = class extends MaskStackStore {
4092
4073
  }
4093
4074
  };
4094
4075
 
4076
+ // src/gradient.ts
4077
+ import { Color, Gradient } from "fabric";
4078
+ var DEFAULT_GRADIENT_CONFIG = {
4079
+ kind: "linear",
4080
+ angle: 90,
4081
+ center: { x: 0.5, y: 0.5 },
4082
+ radius: 0.5,
4083
+ stops: [
4084
+ { color: "#ffffff", opacity: 1, position: 0 },
4085
+ { color: "#000000", opacity: 1, position: 1 }
4086
+ ]
4087
+ };
4088
+ var MIN_RADIUS = 1e-3;
4089
+ var FALLBACK_STOP_COLOR = DEFAULT_GRADIENT_CONFIG.stops[0].color;
4090
+ function linearCoords(angle, box) {
4091
+ const radians = angle * Math.PI / 180;
4092
+ const sin = Math.sin(radians);
4093
+ const cos = Math.cos(radians);
4094
+ const vx = box.width * sin;
4095
+ const vy = -box.height * cos;
4096
+ const length = Math.abs(box.width * sin) + Math.abs(box.height * cos);
4097
+ const scale = length / (vx * vx + vy * vy || 1);
4098
+ return {
4099
+ x1: 0.5 - scale * vx / 2,
4100
+ y1: 0.5 - scale * vy / 2,
4101
+ x2: 0.5 + scale * vx / 2,
4102
+ y2: 0.5 + scale * vy / 2
4103
+ };
4104
+ }
4105
+ function angleFromCoords(coords, box) {
4106
+ const dx = (coords.x2 - coords.x1) / (box.width || 1);
4107
+ const dy = (coords.y2 - coords.y1) / (box.height || 1);
4108
+ const degrees = Math.atan2(dx, -dy) * 180 / Math.PI;
4109
+ return (degrees + 360) % 360;
4110
+ }
4111
+ function stopColor(stop) {
4112
+ const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4113
+ const color = new Color(source);
4114
+ const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
4115
+ color.setAlpha(clamp(opacity, 0, 1));
4116
+ return color.toRgba();
4117
+ }
4118
+ function toFabricGradient(config, box) {
4119
+ const colorStops = [...config.stops].sort((a, b) => a.position - b.position).map((stop) => {
4120
+ const position = Number.isFinite(stop.position) ? stop.position : 0;
4121
+ return { offset: clamp(position, 0, 1), color: stopColor(stop) };
4122
+ });
4123
+ if (config.kind === "radial") {
4124
+ const radius = Math.max(MIN_RADIUS, config.radius);
4125
+ return new Gradient({
4126
+ type: "radial",
4127
+ gradientUnits: "percentage",
4128
+ coords: {
4129
+ x1: config.center.x,
4130
+ y1: config.center.y,
4131
+ r1: 0,
4132
+ x2: config.center.x,
4133
+ y2: config.center.y,
4134
+ r2: radius
4135
+ },
4136
+ colorStops
4137
+ });
4138
+ }
4139
+ return new Gradient({
4140
+ type: "linear",
4141
+ gradientUnits: "percentage",
4142
+ coords: linearCoords(config.angle, box),
4143
+ colorStops
4144
+ });
4145
+ }
4146
+ function readGradientConfig(object) {
4147
+ const fill = object.fill;
4148
+ if (!fill || typeof fill === "string" || !(fill instanceof Gradient)) return null;
4149
+ const stops = fill.colorStops.map((stop) => {
4150
+ const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4151
+ const color = new Color(source);
4152
+ const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
4153
+ return {
4154
+ color: `#${color.toHex().toLowerCase()}`,
4155
+ opacity: color.getAlpha(),
4156
+ position: clamp(offset, 0, 1)
4157
+ };
4158
+ });
4159
+ if (fill.type === "radial") {
4160
+ const { x1, y1, r2 } = fill.coords;
4161
+ return {
4162
+ kind: "radial",
4163
+ angle: DEFAULT_GRADIENT_CONFIG.angle,
4164
+ center: { x: x1, y: y1 },
4165
+ radius: r2,
4166
+ stops
4167
+ };
4168
+ }
4169
+ const box = { width: object.width || 1, height: object.height || 1 };
4170
+ return {
4171
+ kind: "linear",
4172
+ angle: angleFromCoords(fill.coords, box),
4173
+ center: { ...DEFAULT_GRADIENT_CONFIG.center },
4174
+ radius: DEFAULT_GRADIENT_CONFIG.radius,
4175
+ stops
4176
+ };
4177
+ }
4178
+
4095
4179
  // src/editor.ts
4096
4180
  var MIN_ZOOM = 0.1;
4097
4181
  var MAX_ZOOM = 8;
@@ -4108,6 +4192,7 @@ var CanvasEditor = class {
4108
4192
  crop;
4109
4193
  patterns;
4110
4194
  curves;
4195
+ wrap;
4111
4196
  maskPresets;
4112
4197
  /** Stacked boolean masks, per layer and for the design as a whole. */
4113
4198
  layerMasks;
@@ -4158,6 +4243,7 @@ var CanvasEditor = class {
4158
4243
  this.crop = new CropController(this.canvas, this.history, this.events);
4159
4244
  this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
4160
4245
  this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
4246
+ this.wrap = new TextWrapManager(this.canvas, this.layers, this.history, this.events);
4161
4247
  this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
4162
4248
  this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
4163
4249
  this.setupCanvasEvents();
@@ -4246,6 +4332,65 @@ var CanvasEditor = class {
4246
4332
  this.history.save();
4247
4333
  return layer;
4248
4334
  }
4335
+ /**
4336
+ * Insert a gradient layer. It is an ordinary `Rect` with a gradient fill —
4337
+ * which is exactly why the Node print compositor needs no code for it.
4338
+ */
4339
+ addGradient(config = DEFAULT_GRADIENT_CONFIG, box) {
4340
+ const width = box?.width ?? this.canvas.getWidth();
4341
+ const height = box?.height ?? this.canvas.getHeight();
4342
+ const object = new Rect4({
4343
+ left: box?.left ?? 0,
4344
+ top: box?.top ?? 0,
4345
+ originX: "left",
4346
+ originY: "top",
4347
+ width,
4348
+ height,
4349
+ // A gradient fill IS the artwork; an outline is not part of this
4350
+ // layer's design, only its own gradient (unlike a plain shape, where a
4351
+ // stroke is a legitimate look).
4352
+ strokeWidth: 0,
4353
+ fill: toFabricGradient(config, { width, height })
4354
+ });
4355
+ const layer = this.layers.add("gradient", object, "Gradient");
4356
+ this.layers.select(layer.id);
4357
+ this.history.save();
4358
+ return layer;
4359
+ }
4360
+ /**
4361
+ * Repaint a layer's gradient. `save: false` while a slider is being dragged —
4362
+ * the settled value is the one worth an undo step.
4363
+ */
4364
+ setGradient(layerId, config, save = true) {
4365
+ const layer = this.layers.get(layerId);
4366
+ if (!layer) return;
4367
+ const object = layer.fabricObject;
4368
+ object.set({
4369
+ fill: toFabricGradient(config, {
4370
+ width: object.width || 1,
4371
+ height: object.height || 1
4372
+ })
4373
+ });
4374
+ object.dirty = true;
4375
+ this.canvas.requestRenderAll();
4376
+ this.events.emit("layer:modified", { layerId });
4377
+ if (save) this.history.save();
4378
+ }
4379
+ /** Set a layer's blend mode. Applied to the render proxy too, like opacity. */
4380
+ setBlend(layerId, mode) {
4381
+ const layer = this.layers.get(layerId);
4382
+ if (!layer) return;
4383
+ const operation = blendOperation(mode);
4384
+ layer.fabricObject.set({ globalCompositeOperation: operation });
4385
+ layer.fabricObject.dirty = true;
4386
+ if (layer.renderProxy) {
4387
+ layer.renderProxy.set({ globalCompositeOperation: operation });
4388
+ layer.renderProxy.dirty = true;
4389
+ }
4390
+ this.canvas.requestRenderAll();
4391
+ this.events.emit("layer:modified", { layerId });
4392
+ this.history.save();
4393
+ }
4249
4394
  async addTemplate(template, params) {
4250
4395
  const values = {};
4251
4396
  for (const parameter of template.parameters) {
@@ -4914,15 +5059,34 @@ var CanvasEditor = class {
4914
5059
  }
4915
5060
  // ─── Text curve ─────────────────────────────────────
4916
5061
  applyTextCurve(layerId, config) {
4917
- return this.curves.apply(layerId, config);
5062
+ const applied = this.curves.apply(layerId, config);
5063
+ if (applied) this.wrap.refresh(layerId);
5064
+ return applied;
4918
5065
  }
4919
5066
  clearTextCurve(layerId) {
4920
- return this.curves.clear(layerId);
5067
+ const cleared = this.curves.clear(layerId);
5068
+ if (cleared) this.wrap.refresh(layerId);
5069
+ return cleared;
4921
5070
  }
4922
5071
  getTextCurve(layerId) {
4923
5072
  return this.curves.get(layerId);
4924
5073
  }
5074
+ // ─── Text wrap ──────────────────────────────────────
5075
+ applyTextWrap(layerId, wrap) {
5076
+ return this.wrap.apply(layerId, wrap);
5077
+ }
5078
+ setTextOverflow(layerId, overflow) {
5079
+ return this.wrap.setOverflow(layerId, overflow);
5080
+ }
5081
+ getTextWrap(layerId) {
5082
+ return this.wrap.get(layerId);
5083
+ }
4925
5084
  // ─── Mask presets ───────────────────────────────────
5085
+ // A preset assigns `clipPath` wholesale, so a text layer clipping to its box
5086
+ // loses that clip the moment a mask lands on it. Putting it back is NOT wired
5087
+ // here: the preset manager announces `masks:changed` and the wrap manager
5088
+ // re-derives from that, which covers this facade, the manager called directly,
5089
+ // and `refreshAll()` on a restore — all with one mechanism.
4926
5090
  applyMaskPreset(layerId, id) {
4927
5091
  return this.maskPresets.apply(layerId, id);
4928
5092
  }
@@ -5011,6 +5175,7 @@ var CanvasEditor = class {
5011
5175
  this.history.dispose();
5012
5176
  this.patterns.dispose();
5013
5177
  this.curves.dispose();
5178
+ this.wrap.dispose();
5014
5179
  this.events.removeAllListeners();
5015
5180
  this.canvas.dispose();
5016
5181
  }
@@ -5133,10 +5298,13 @@ var AnnotationOverlay = class {
5133
5298
  };
5134
5299
  export {
5135
5300
  AnnotationOverlay,
5301
+ BLEND_GROUPS,
5302
+ BLEND_MODES,
5136
5303
  CANVAS_MASK_TARGET,
5137
5304
  CANVAS_SIZE_PRESETS,
5138
5305
  CanvasEditor,
5139
5306
  CropController,
5307
+ DEFAULT_GRADIENT_CONFIG,
5140
5308
  DEFAULT_LAYER_SHADOW,
5141
5309
  DEFAULT_PATTERN_CONFIG,
5142
5310
  DEFAULT_SELECTION_STYLE,
@@ -5159,12 +5327,18 @@ export {
5159
5327
  TEXTURE_MASK_IDS,
5160
5328
  TEXTURE_MASK_SIZE,
5161
5329
  TextCurveManager,
5330
+ TextWrapManager,
5162
5331
  TiledPatternObject,
5163
5332
  UnitConverter,
5333
+ angleFromCoords,
5164
5334
  applyAspectLock,
5165
5335
  applyLayerShadow,
5166
5336
  applyObjectSelectionStyle,
5167
5337
  applySelectionStyle,
5338
+ applyTextWrapToObject,
5339
+ blendModeLabel,
5340
+ blendModeOf,
5341
+ blendOperation,
5168
5342
  buildCurveLinePaths,
5169
5343
  buildCurvePathData,
5170
5344
  clamp,
@@ -5190,9 +5364,13 @@ export {
5190
5364
  isMaskPresetId,
5191
5365
  isShapeMaskId,
5192
5366
  isTextureMaskId,
5367
+ linearCoords,
5193
5368
  needsAbsoluteSpace,
5194
5369
  normalizeTextCurve,
5370
+ preWrapWordSplit,
5371
+ readGradientConfig,
5195
5372
  readLayerShadow,
5373
+ readTextWrap,
5196
5374
  renderTextureMask,
5197
5375
  resetTransform,
5198
5376
  restoreLocks,
@@ -5203,6 +5381,7 @@ export {
5203
5381
  textPathCurve,
5204
5382
  textPathSpec,
5205
5383
  toCanvasSpace,
5384
+ toFabricGradient,
5206
5385
  toHostSpace,
5207
5386
  unwrapGroup
5208
5387
  };