@overtone-art/canvas-editor-core 0.8.5 → 0.9.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
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,11 +17,21 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
33
+ ADJUST_KEYS: () => ADJUST_KEYS,
34
+ ADJUST_RANGES: () => ADJUST_RANGES,
23
35
  AnnotationOverlay: () => AnnotationOverlay,
24
36
  BLEND_GROUPS: () => BLEND_GROUPS,
25
37
  BLEND_MODES: () => BLEND_MODES,
@@ -35,7 +47,12 @@ __export(index_exports, {
35
47
  EventEmitter: () => EventEmitter,
36
48
  FontRegistry: () => FontRegistry,
37
49
  HistoryManager: () => HistoryManager,
50
+ IMAGE_FILTER_PRESETS: () => IMAGE_FILTER_PRESETS,
51
+ IMAGE_PROXY_MAX_EDGE: () => IMAGE_PROXY_MAX_EDGE,
52
+ ImageEffectsManager: () => ImageEffectsManager,
53
+ LAYER_EFFECT_DEFAULTS: () => LAYER_EFFECT_DEFAULTS,
38
54
  Layer: () => Layer,
55
+ LayerEffectsManager: () => LayerEffectsManager,
39
56
  LayerManager: () => LayerManager,
40
57
  LayerMaskManager: () => LayerMaskManager,
41
58
  LicenseManager: () => LicenseManager,
@@ -43,10 +60,12 @@ __export(index_exports, {
43
60
  MaskController: () => MaskController,
44
61
  MaskPresetManager: () => MaskPresetManager,
45
62
  MaskRefinementError: () => MaskRefinementError,
63
+ NEUTRAL_ADJUST: () => NEUTRAL_ADJUST,
46
64
  PatternManager: () => PatternManager,
47
65
  ProjectManager: () => ProjectManager,
48
66
  SHAPE_MASK_BOX: () => SHAPE_MASK_BOX,
49
67
  SHAPE_MASK_IDS: () => SHAPE_MASK_IDS,
68
+ ShapeManager: () => ShapeManager,
50
69
  SnapManager: () => SnapManager,
51
70
  TEXTURE_MASK_IDS: () => TEXTURE_MASK_IDS,
52
71
  TEXTURE_MASK_SIZE: () => TEXTURE_MASK_SIZE,
@@ -54,6 +73,8 @@ __export(index_exports, {
54
73
  TextWrapManager: () => TextWrapManager,
55
74
  TiledPatternObject: () => TiledPatternObject,
56
75
  UnitConverter: () => UnitConverter,
76
+ alignObjects: () => alignObjects,
77
+ alphaClipMatrix: () => alphaClipMatrix,
57
78
  angleFromCoords: () => angleFromCoords,
58
79
  applyAspectLock: () => applyAspectLock,
59
80
  applyLayerShadow: () => applyLayerShadow,
@@ -66,6 +87,8 @@ __export(index_exports, {
66
87
  buildCurveLinePaths: () => buildCurveLinePaths,
67
88
  buildCurvePathData: () => buildCurvePathData,
68
89
  clamp: () => clamp,
90
+ clampAdjust: () => clampAdjust,
91
+ clampAlphaClip: () => clampAlphaClip,
69
92
  clearTextureMaskCache: () => clearTextureMaskCache,
70
93
  composeMaskGroup: () => composeMaskGroup,
71
94
  computeCoverPlacement: () => computeCoverPlacement,
@@ -73,6 +96,7 @@ __export(index_exports, {
73
96
  computeTilePositions: () => computeTilePositions,
74
97
  deserializeEditor: () => deserializeEditor,
75
98
  displaceRgba: () => displaceRgba,
99
+ distributeObjects: () => distributeObjects,
76
100
  drawTiles: () => drawTiles,
77
101
  escapeXml: () => escapeXml,
78
102
  explodeTemplateSvg: () => explodeTemplateSvg,
@@ -84,12 +108,15 @@ __export(index_exports, {
84
108
  fitTextWidth: () => fitTextWidth,
85
109
  fitToBox: () => fitToBox,
86
110
  generateId: () => generateId,
111
+ imageEffectsFilterSpecs: () => imageEffectsFilterSpecs,
112
+ imageFilterPreset: () => imageFilterPreset,
87
113
  isCssColor: () => isCssColor,
88
114
  isMaskPresetId: () => isMaskPresetId,
89
115
  isShapeMaskId: () => isShapeMaskId,
90
116
  isTextureMaskId: () => isTextureMaskId,
91
117
  linearCoords: () => linearCoords,
92
118
  needsAbsoluteSpace: () => needsAbsoluteSpace,
119
+ normalizeImageEffects: () => normalizeImageEffects,
93
120
  normalizeTextCurve: () => normalizeTextCurve,
94
121
  preWrapWordSplit: () => preWrapWordSplit,
95
122
  readGradientConfig: () => readGradientConfig,
@@ -97,6 +124,7 @@ __export(index_exports, {
97
124
  readTextWrap: () => readTextWrap,
98
125
  renderTextureMask: () => renderTextureMask,
99
126
  resetTransform: () => resetTransform,
127
+ resolveShapeParams: () => resolveShapeParams,
100
128
  restoreLocks: () => restoreLocks,
101
129
  round2: () => round2,
102
130
  sanitizeSvg: () => sanitizeSvg,
@@ -107,12 +135,15 @@ __export(index_exports, {
107
135
  toCanvasSpace: () => toCanvasSpace,
108
136
  toFabricGradient: () => toFabricGradient,
109
137
  toHostSpace: () => toHostSpace,
138
+ unionBox: () => unionBox,
110
139
  unwrapGroup: () => unwrapGroup
111
140
  });
112
141
  module.exports = __toCommonJS(index_exports);
113
142
 
114
143
  // src/editor.ts
115
- var import_fabric22 = require("fabric");
144
+ var import_fabric25 = require("fabric");
145
+ var fabricModule = __toESM(require("fabric"));
146
+ var import_canvas_editor_effects2 = require("@overtone-art/canvas-editor-effects");
116
147
 
117
148
  // src/events.ts
118
149
  var EventEmitter = class {
@@ -264,9 +295,31 @@ var Layer = class {
264
295
  ...this.toData(),
265
296
  // A group's own object already carries its children's geometry, so the
266
297
  // child records in `toData` stay identity-only — nothing is duplicated.
267
- fabricObject: this.fabricObject.toObject()
298
+ fabricObject: this.serializeObject()
268
299
  };
269
300
  }
301
+ /**
302
+ * A member of an ActiveSelection holds selection-relative coordinates and
303
+ * `toObject()` writes them verbatim. A snapshot taken mid-selection — which
304
+ * `HistoryManager.undo()` takes — then held positions no restore could use,
305
+ * and the redo scattered the layers. Realise the selection's transform for
306
+ * the write and put the object's own transform back.
307
+ */
308
+ serializeObject() {
309
+ const object = this.fabricObject;
310
+ const selection = object.group;
311
+ if (!(selection instanceof import_fabric.ActiveSelection)) {
312
+ return object.toObject();
313
+ }
314
+ const saved = import_fabric.util.saveObjectTransform(object);
315
+ import_fabric.util.addTransformToObject(object, selection.calcOwnMatrix());
316
+ try {
317
+ return object.toObject();
318
+ } finally {
319
+ object.set(saved);
320
+ object.setCoords();
321
+ }
322
+ }
270
323
  };
271
324
  var LayerManager = class {
272
325
  constructor(canvas, events) {
@@ -1198,7 +1251,7 @@ var CropController = class {
1198
1251
  activeLayerId() {
1199
1252
  return this.session?.layerId ?? null;
1200
1253
  }
1201
- start(layer) {
1254
+ start(layer, options = {}) {
1202
1255
  if (layer.type !== "image") return;
1203
1256
  if (this.session) this.cancel();
1204
1257
  const image = layer.fabricObject;
@@ -1206,6 +1259,12 @@ var CropController = class {
1206
1259
  if (prevAngle) image.rotate(0);
1207
1260
  image.setCoords();
1208
1261
  const b = image.getBoundingRect();
1262
+ const prevTransform = {
1263
+ scaleX: image.scaleX,
1264
+ scaleY: image.scaleY,
1265
+ left: image.left,
1266
+ top: image.top
1267
+ };
1209
1268
  const rect = new import_fabric3.Rect({
1210
1269
  left: b.left,
1211
1270
  top: b.top,
@@ -1230,8 +1289,95 @@ var CropController = class {
1230
1289
  this.canvas.add(rect);
1231
1290
  this.canvas.setActiveObject(rect);
1232
1291
  this.canvas.requestRenderAll();
1233
- this.session = { layerId: layer.id, image, rect, prevSelectable, prevEvented, prevAngle };
1292
+ this.session = {
1293
+ layerId: layer.id,
1294
+ image,
1295
+ rect,
1296
+ prevSelectable,
1297
+ prevEvented,
1298
+ prevAngle,
1299
+ prevTransform,
1300
+ startRect: { left: b.left, top: b.top, width: b.width, height: b.height },
1301
+ aspect: null
1302
+ };
1234
1303
  this.events.emit("crop:changed", { active: true, layerId: layer.id });
1304
+ if (options.aspect) this.setAspect(options.aspect);
1305
+ }
1306
+ /**
1307
+ * The frame's own geometry, without its stroke.
1308
+ *
1309
+ * The rect draws a 1px dashed outline, and `getBoundingRect` /
1310
+ * `getScaledWidth` both count it — so reading the frame through them made the
1311
+ * aspect box, the zoom centre and the applied crop each about a pixel larger
1312
+ * than the box the user drew. The rect is axis-aligned (rotation is locked),
1313
+ * so its own left/top and scaled dimensions are exact.
1314
+ */
1315
+ frameBox(rect) {
1316
+ return {
1317
+ left: rect.left,
1318
+ top: rect.top,
1319
+ width: rect.width * rect.scaleX,
1320
+ height: rect.height * rect.scaleY
1321
+ };
1322
+ }
1323
+ /**
1324
+ * Lock the frame to a ratio: it becomes the largest box of that ratio inside
1325
+ * the current frame, centred, and only the corner handles stay — fabric's
1326
+ * default `uniformScaling` then keeps the ratio through a drag. `null` frees it.
1327
+ */
1328
+ setAspect(ratio) {
1329
+ const s = this.session;
1330
+ if (!s) return;
1331
+ s.aspect = ratio && ratio > 0 ? ratio : null;
1332
+ const { rect } = s;
1333
+ const sides = s.aspect === null;
1334
+ rect.setControlsVisibility({ ml: sides, mr: sides, mt: sides, mb: sides, mtr: false });
1335
+ if (s.aspect !== null) {
1336
+ rect.setCoords();
1337
+ const current = this.frameBox(rect);
1338
+ const width = Math.min(current.width, current.height * s.aspect);
1339
+ const height = width / s.aspect;
1340
+ rect.set({
1341
+ left: current.left + (current.width - width) / 2,
1342
+ top: current.top + (current.height - height) / 2,
1343
+ width,
1344
+ height,
1345
+ scaleX: 1,
1346
+ scaleY: 1
1347
+ });
1348
+ rect.setCoords();
1349
+ }
1350
+ this.canvas.requestRenderAll();
1351
+ }
1352
+ /** Zoom the image under a fixed frame, about the frame's centre. */
1353
+ setScale(factor) {
1354
+ const s = this.session;
1355
+ if (!s || !Number.isFinite(factor) || factor <= 0) return;
1356
+ const { image, rect } = s;
1357
+ rect.setCoords();
1358
+ const frame = this.frameBox(rect);
1359
+ const centre = { x: frame.left + frame.width / 2, y: frame.top + frame.height / 2 };
1360
+ const scaleX = Math.max(0.01, image.scaleX * factor);
1361
+ const scaleY = Math.max(0.01, image.scaleY * factor);
1362
+ const applied = scaleX / image.scaleX;
1363
+ image.set({
1364
+ scaleX,
1365
+ scaleY,
1366
+ left: centre.x - (centre.x - image.left) * applied,
1367
+ top: centre.y - (centre.y - image.top) * applied
1368
+ });
1369
+ image.setCoords();
1370
+ this.canvas.requestRenderAll();
1371
+ }
1372
+ /** Back to the frame and image transform the session started with; aspect freed. */
1373
+ reset() {
1374
+ const s = this.session;
1375
+ if (!s) return;
1376
+ s.image.set(s.prevTransform);
1377
+ s.image.setCoords();
1378
+ s.rect.set({ ...s.startRect, scaleX: 1, scaleY: 1 });
1379
+ s.rect.setCoords();
1380
+ this.setAspect(null);
1235
1381
  }
1236
1382
  apply() {
1237
1383
  const s = this.session;
@@ -1246,10 +1392,11 @@ var CropController = class {
1246
1392
  const el = image.getElement();
1247
1393
  const naturalW = el.naturalWidth || image.width || 0;
1248
1394
  const naturalH = el.naturalHeight || image.height || 0;
1249
- let cropX = (image.cropX ?? 0) + (rect.left - imgLeft) / scaleX;
1250
- let cropY = (image.cropY ?? 0) + (rect.top - imgTop) / scaleY;
1251
- let cropW = rect.getScaledWidth() / scaleX;
1252
- let cropH = rect.getScaledHeight() / scaleY;
1395
+ const frame = this.frameBox(rect);
1396
+ let cropX = (image.cropX ?? 0) + (frame.left - imgLeft) / scaleX;
1397
+ let cropY = (image.cropY ?? 0) + (frame.top - imgTop) / scaleY;
1398
+ let cropW = frame.width / scaleX;
1399
+ let cropH = frame.height / scaleY;
1253
1400
  cropX = clamp(cropX, 0, Math.max(0, naturalW - 1));
1254
1401
  cropY = clamp(cropY, 0, Math.max(0, naturalH - 1));
1255
1402
  cropW = clamp(cropW, 1, naturalW - cropX);
@@ -1274,6 +1421,8 @@ var CropController = class {
1274
1421
  cancel() {
1275
1422
  const s = this.session;
1276
1423
  if (!s) return;
1424
+ s.image.set(s.prevTransform);
1425
+ s.image.setCoords();
1277
1426
  if (s.prevAngle) {
1278
1427
  s.image.rotate(s.prevAngle);
1279
1428
  s.image.setCoords();
@@ -1298,14 +1447,70 @@ var CropController = class {
1298
1447
  };
1299
1448
  var STROKE2 = "#22c55e";
1300
1449
 
1450
+ // src/align.ts
1451
+ var import_fabric4 = require("fabric");
1452
+ function unionBox(boxes) {
1453
+ const left = Math.min(...boxes.map((b) => b.left));
1454
+ const top = Math.min(...boxes.map((b) => b.top));
1455
+ const right = Math.max(...boxes.map((b) => b.left + b.width));
1456
+ const bottom = Math.max(...boxes.map((b) => b.top + b.height));
1457
+ return { left, top, width: right - left, height: bottom - top };
1458
+ }
1459
+ function boxOf(object) {
1460
+ object.setCoords();
1461
+ return object.getBoundingRect();
1462
+ }
1463
+ function moveBy(object, dx, dy) {
1464
+ const parent = object.group;
1465
+ const vector = parent ? import_fabric4.util.sendVectorToPlane(new import_fabric4.Point(dx, dy), void 0, parent.calcTransformMatrix()) : new import_fabric4.Point(dx, dy);
1466
+ object.set({ left: (object.left ?? 0) + vector.x, top: (object.top ?? 0) + vector.y });
1467
+ object.setCoords();
1468
+ }
1469
+ function alignObjects(objects, target, frame) {
1470
+ for (const object of objects) {
1471
+ const b = boxOf(object);
1472
+ if (target === "left") moveBy(object, frame.left - b.left, 0);
1473
+ if (target === "center-h") moveBy(object, frame.left + (frame.width - b.width) / 2 - b.left, 0);
1474
+ if (target === "right") moveBy(object, frame.left + frame.width - b.width - b.left, 0);
1475
+ if (target === "top") moveBy(object, 0, frame.top - b.top);
1476
+ if (target === "middle") moveBy(object, 0, frame.top + (frame.height - b.height) / 2 - b.top);
1477
+ if (target === "bottom") moveBy(object, 0, frame.top + frame.height - b.height - b.top);
1478
+ }
1479
+ }
1480
+ function distributeObjects(objects, axis) {
1481
+ if (objects.length < 3) return false;
1482
+ const measured = objects.map((object) => ({ object, box: boxOf(object) }));
1483
+ const pos = (b) => axis === "h" ? b.left : b.top;
1484
+ const size = (b) => axis === "h" ? b.width : b.height;
1485
+ const ordered = [...measured].sort((a, b) => pos(a.box) - pos(b.box));
1486
+ const span = unionBox(measured.map((m) => m.box));
1487
+ const total = ordered.reduce((sum, m) => sum + size(m.box), 0);
1488
+ const gap = (size(span) - total) / (ordered.length - 1);
1489
+ let cursor = pos(span);
1490
+ for (const { object, box } of ordered) {
1491
+ const delta = cursor - pos(box);
1492
+ moveBy(object, axis === "h" ? delta : 0, axis === "h" ? 0 : delta);
1493
+ cursor += size(box) + gap;
1494
+ }
1495
+ return true;
1496
+ }
1497
+ function relayoutSelection(active) {
1498
+ if (!(active instanceof import_fabric4.ActiveSelection)) return;
1499
+ active.triggerLayout();
1500
+ active.setCoords();
1501
+ }
1502
+ function canvasBox(width, height) {
1503
+ return { left: 0, top: 0, width, height };
1504
+ }
1505
+
1301
1506
  // src/pattern/pattern-manager.ts
1302
- var import_fabric6 = require("fabric");
1507
+ var import_fabric7 = require("fabric");
1303
1508
 
1304
1509
  // src/pattern/tiled-pattern-object.ts
1305
- var import_fabric5 = require("fabric");
1510
+ var import_fabric6 = require("fabric");
1306
1511
 
1307
1512
  // src/text-fit.ts
1308
- var import_fabric4 = require("fabric");
1513
+ var import_fabric5 = require("fabric");
1309
1514
  var TEXT_FIT_SLACK = 0.5;
1310
1515
  var MEASURE_WIDTH = 1e5;
1311
1516
  var GROWTH_FACTOR = 1.1;
@@ -1394,7 +1599,7 @@ function fitTextWidth(object) {
1394
1599
  const after = textInk(text);
1395
1600
  const shift = (before.dx - after.dx) * (text.scaleX ?? 1);
1396
1601
  const radians = (text.angle ?? 0) * Math.PI / 180;
1397
- const moved = new import_fabric4.Point(
1602
+ const moved = new import_fabric5.Point(
1398
1603
  centre.x + shift * Math.cos(radians),
1399
1604
  centre.y + shift * Math.sin(radians)
1400
1605
  );
@@ -1418,9 +1623,9 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
1418
1623
  const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
1419
1624
  const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
1420
1625
  const cols = Math.ceil(span / tileW) + 2;
1421
- const rows = Math.ceil(span / tileH) + 2;
1626
+ const rows2 = Math.ceil(span / tileH) + 2;
1422
1627
  const [colFrom, colTo] = axisRange(config.horizontalLimit, Math.ceil(cols / 2));
1423
- const [rowFrom, rowTo] = axisRange(config.verticalLimit, Math.ceil(rows / 2));
1628
+ const [rowFrom, rowTo] = axisRange(config.verticalLimit, Math.ceil(rows2 / 2));
1424
1629
  const shiftX = tileW * (clampOffset(config.offsetX) / 100);
1425
1630
  const shiftY = tileH * (clampOffset(config.offsetY) / 100);
1426
1631
  const placements = [];
@@ -1479,7 +1684,7 @@ var MAX_SNAPSHOT_PIXELS = 16e6;
1479
1684
  var MAX_SNAPSHOT_SCALE = 8;
1480
1685
  var SNAPSHOT_SHRINK_FACTOR = 2;
1481
1686
  var TILE_BLEED_DEVICE_PX = 2;
1482
- var TiledPatternObject = class _TiledPatternObject extends import_fabric5.FabricObject {
1687
+ var TiledPatternObject = class _TiledPatternObject extends import_fabric6.FabricObject {
1483
1688
  static type = "TiledPattern";
1484
1689
  /** The layer's real object. Never rendered directly (it is kept at opacity 0). */
1485
1690
  source;
@@ -1563,7 +1768,7 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric5.Fabric
1563
1768
  const centre = this.getCenterPoint();
1564
1769
  const shift = this.shiftVector(tileW, tileH, this.liveAngle());
1565
1770
  const ink = this.inkOffset();
1566
- return new import_fabric5.Point(centre.x - shift.x - ink.x, centre.y - shift.y - ink.y);
1771
+ return new import_fabric6.Point(centre.x - shift.x - ink.x, centre.y - shift.y - ink.y);
1567
1772
  }
1568
1773
  _render(ctx) {
1569
1774
  const { width, height } = this.area;
@@ -1650,6 +1855,10 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric5.Fabric
1650
1855
  angle: this.angle,
1651
1856
  scaleX: this.scaleX,
1652
1857
  scaleY: this.scaleY,
1858
+ skewX: this.skewX,
1859
+ skewY: this.skewY,
1860
+ flipX: this.flipX,
1861
+ flipY: this.flipY,
1653
1862
  width: this.width,
1654
1863
  height: this.height,
1655
1864
  visible: this.visible,
@@ -1695,12 +1904,12 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric5.Fabric
1695
1904
  */
1696
1905
  inkOffset() {
1697
1906
  const text = asText(this.source);
1698
- if (!text) return new import_fabric5.Point(0, 0);
1907
+ if (!text) return new import_fabric6.Point(0, 0);
1699
1908
  const { dx } = textInk(text);
1700
- if (!dx) return new import_fabric5.Point(0, 0);
1909
+ if (!dx) return new import_fabric6.Point(0, 0);
1701
1910
  const scaled = dx * (this.source.scaleX ?? 1);
1702
1911
  const radians = (this.source.angle ?? 0) * Math.PI / 180;
1703
- return new import_fabric5.Point(scaled * Math.cos(radians), scaled * Math.sin(radians));
1912
+ return new import_fabric6.Point(scaled * Math.cos(radians), scaled * Math.sin(radians));
1704
1913
  }
1705
1914
  /** The source's on-canvas width, before the tile scale. */
1706
1915
  baseW() {
@@ -1761,11 +1970,11 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric5.Fabric
1761
1970
  const radians = angle * Math.PI / 180;
1762
1971
  const cos = Math.cos(radians);
1763
1972
  const sin = Math.sin(radians);
1764
- return new import_fabric5.Point(dx * cos - dy * sin, dx * sin + dy * cos);
1973
+ return new import_fabric6.Point(dx * cos - dy * sin, dx * sin + dy * cos);
1765
1974
  }
1766
1975
  anchorFromOrigin(origin, tileW, tileH, angle) {
1767
1976
  const shift = this.shiftVector(tileW, tileH, angle);
1768
- return new import_fabric5.Point(origin.x + shift.x, origin.y + shift.y);
1977
+ return new import_fabric6.Point(origin.x + shift.x, origin.y + shift.y);
1769
1978
  }
1770
1979
  /**
1771
1980
  * Snapshot the source at (at least) `scale`, reusing the cached one while it
@@ -1970,6 +2179,11 @@ var PatternManager = class {
1970
2179
  if (wasActive) this.canvas.setActiveObject(proxy);
1971
2180
  this.canvas.requestRenderAll();
1972
2181
  }
2182
+ /** Fold a programmatic move of the proxy into the config and the source, as a drop does. */
2183
+ commitProxy(layerId) {
2184
+ const proxy = this.proxies.get(layerId);
2185
+ if (proxy) this.commitGesture(proxy);
2186
+ }
1973
2187
  /**
1974
2188
  * Fold a finished drag / scale / rotate on the proxy back into the pattern:
1975
2189
  * position becomes the grid origin (carried by the source), scale becomes the
@@ -2037,7 +2251,7 @@ async function unbakeLegacyLayer(layer, state) {
2037
2251
  cropY: state.original.cropY,
2038
2252
  angle: state.original.angle
2039
2253
  });
2040
- image.clipPath = state.originalClip ? (await import_fabric6.util.enlivenObjects([state.originalClip]))[0] : void 0;
2254
+ image.clipPath = state.originalClip ? (await import_fabric7.util.enlivenObjects([state.originalClip]))[0] : void 0;
2041
2255
  restoreLocks(image, state.originalLocks);
2042
2256
  image.setCoords();
2043
2257
  }
@@ -2054,220 +2268,997 @@ function restoreLocks(obj, locks) {
2054
2268
  );
2055
2269
  }
2056
2270
 
2057
- // src/text-curve.ts
2058
- var import_fabric7 = require("fabric");
2059
-
2060
- // src/text-curve-geometry.ts
2061
- var DEFAULT_TEXT_CURVE = {
2062
- shape: "arc",
2063
- arc: 0,
2064
- wave: 0,
2065
- waveLength: 4.1,
2066
- offset: 0,
2067
- centerOffset: 0
2068
- };
2069
- var MIN_ARC = 0.5;
2070
- var FULL_CIRCLE_ARC = 99.5;
2071
- var MIN_SWEEP = 0.12;
2072
- var WAVE_AMPLITUDE_EM = 0.9;
2073
- var MIN_WAVE_LENGTH = 1;
2074
- var MAX_WAVE_LENGTH = 12;
2075
- var WAVE_STEP = 6;
2076
- var MAX_WAVE_STEPS = 4e3;
2077
- function round(value) {
2078
- return Math.round(value * 100) / 100;
2079
- }
2080
- function normalizeTextCurve(config) {
2081
- const arc = clamp(config.arc ?? 0, -100, 100);
2082
- const wave = clamp(config.wave ?? 0, 0, 100);
2083
- const inferred = Math.abs(arc) < MIN_ARC && wave > 0 ? "wave" : "arc";
2084
- return {
2085
- shape: config.shape ?? inferred,
2086
- arc,
2087
- wave,
2088
- waveLength: clamp(
2089
- config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength,
2090
- MIN_WAVE_LENGTH,
2091
- MAX_WAVE_LENGTH
2092
- ),
2093
- offset: clamp(config.offset ?? 0, -100, 100),
2094
- centerOffset: clamp(config.centerOffset ?? 0, -100, 100)
2095
- };
2096
- }
2097
- function bendsBaseline(config) {
2098
- return config.shape === "arc" ? Math.abs(config.arc) >= MIN_ARC : config.wave > 0;
2099
- }
2100
- function arcGeometry(width, config) {
2101
- const magnitude = Math.min(100, Math.abs(config.arc));
2102
- const direction = config.arc < 0 ? -1 : 1;
2103
- const baseSweep = magnitude >= FULL_CIRCLE_ARC ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
2104
- const baseRadius = width / baseSweep;
2105
- const radius = baseRadius * Math.pow(2, config.centerOffset / 100);
2106
- const sweep = radius >= baseRadius ? baseSweep : Math.min(Math.PI * 2, width / Math.max(radius, 1e-3));
2107
- return { radius, sweep, direction, baseRadius };
2108
- }
2109
- function arcLinePath(width, geometry, radius, rotation) {
2110
- const { sweep, direction, baseRadius } = geometry;
2111
- const full = sweep >= Math.PI * 2 - 1e-6;
2112
- const centerX = width / 2;
2113
- const point = (angle) => [
2114
- centerX + radius * Math.sin(angle),
2115
- direction * (baseRadius - radius * Math.cos(angle))
2116
- ];
2117
- const sweepFlag = direction > 0 ? 1 : 0;
2118
- const format = ([x, y]) => `${round(x)} ${round(y)}`;
2119
- const r = round(radius);
2120
- if (full) {
2121
- const start2 = point(rotation - Math.PI);
2122
- const opposite = point(rotation);
2123
- return {
2124
- data: [
2125
- `M ${format(start2)}`,
2126
- `A ${r} ${r} 0 0 ${sweepFlag} ${format(opposite)}`,
2127
- `A ${r} ${r} 0 0 ${sweepFlag} ${format(start2)}`
2128
- ].join(" "),
2129
- length: radius * Math.PI * 2
2130
- };
2131
- }
2132
- const start = point(rotation - sweep / 2);
2133
- const end = point(rotation + sweep / 2);
2134
- const largeArc = sweep > Math.PI ? 1 : 0;
2135
- return {
2136
- data: `M ${format(start)} A ${r} ${r} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
2137
- length: radius * sweep
2138
- };
2139
- }
2140
- function waveLinePath(width, fontSize, config, dy) {
2141
- const amplitude = config.wave / 100 * fontSize * WAVE_AMPLITUDE_EM;
2142
- const period = Math.max(1, fontSize * config.waveLength);
2143
- const phase = config.offset / 100 * Math.PI * 2;
2144
- const step = Math.min(WAVE_STEP, period / 8);
2145
- const steps = Math.min(MAX_WAVE_STEPS, Math.max(2, Math.ceil(width / step)));
2146
- const commands = [];
2147
- let length = 0;
2148
- let previous = null;
2149
- for (let index = 0; index <= steps; index++) {
2150
- const x = width * index / steps;
2151
- const y = dy + amplitude * Math.sin(x / period * Math.PI * 2 + phase);
2152
- if (previous) length += Math.hypot(x - previous[0], y - previous[1]);
2153
- previous = [x, y];
2154
- commands.push(`${index === 0 ? "M" : "L"} ${round(x)} ${round(y)}`);
2155
- }
2156
- return { data: commands.join(" "), length };
2157
- }
2158
- function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight = 0) {
2159
- const curve = normalizeTextCurve(config);
2160
- if (!bendsBaseline(curve)) return null;
2161
- const lines = Math.max(1, Math.floor(lineCount));
2162
- const paths = [];
2163
- if (curve.shape === "wave") {
2164
- for (let index = 0; index < lines; index++) {
2165
- paths.push(waveLinePath(width, fontSize, curve, (index - (lines - 1) / 2) * lineHeight));
2166
- }
2167
- return paths;
2168
- }
2169
- const geometry = arcGeometry(width, curve);
2170
- const rotation = curve.offset / 100 * geometry.sweep;
2171
- for (let index = 0; index < lines; index++) {
2172
- const radius = Math.max(1, geometry.radius + (index - (lines - 1) / 2) * lineHeight);
2173
- paths.push(arcLinePath(width, geometry, radius, rotation));
2174
- }
2175
- return paths;
2176
- }
2177
- function buildCurvePathData(config, width, fontSize) {
2178
- return buildCurveLinePaths(config, width, fontSize)?.[0] ?? null;
2179
- }
2180
-
2181
- // src/text-curve.ts
2182
- var MEASURE_WIDTH2 = 1e5;
2183
- var PATH_SLACK = 0.06;
2184
- var FALLBACK_LINE_HEIGHT = 1.16;
2185
- var patches = /* @__PURE__ */ new WeakMap();
2186
- var dimensionPatches = /* @__PURE__ */ new WeakMap();
2187
- var authoredCaching = /* @__PURE__ */ new WeakMap();
2188
- var GLYPH_HALF_HEIGHT = 0.7;
2189
- function curvedInkSize(text) {
2190
- const lineCount = text._textLines?.length ?? 0;
2191
- for (let index = 0; index < lineCount; index += 1) text.getLineWidth?.(index);
2192
- const lines = text.__charBounds;
2193
- if (!lines?.length) return null;
2194
- let halfWidth = 0;
2195
- let halfHeight = 0;
2196
- for (const line of lines) {
2197
- for (const box of line ?? []) {
2198
- const { renderLeft, renderTop } = box;
2199
- if (typeof renderLeft !== "number" || typeof renderTop !== "number") continue;
2200
- const halfGlyphWidth = (box.kernedWidth ?? box.width ?? 0) / 2;
2201
- const halfGlyphHeight = (box.height ?? text.fontSize) * GLYPH_HALF_HEIGHT;
2202
- const angle = box.angle ?? 0;
2203
- const cos = Math.abs(Math.cos(angle));
2204
- const sin = Math.abs(Math.sin(angle));
2205
- halfWidth = Math.max(
2206
- halfWidth,
2207
- Math.abs(renderLeft) + halfGlyphWidth * cos + halfGlyphHeight * sin
2208
- );
2209
- halfHeight = Math.max(
2210
- halfHeight,
2211
- Math.abs(renderTop) + halfGlyphWidth * sin + halfGlyphHeight * cos
2212
- );
2213
- }
2214
- }
2215
- if (halfWidth === 0 && halfHeight === 0) return null;
2216
- return { width: halfWidth * 2, height: halfHeight * 2 };
2217
- }
2218
- function growBoxToInk(text) {
2219
- const ink = curvedInkSize(text);
2220
- if (!ink) return;
2221
- if (ink.width > (text.width ?? 0)) text.width = ink.width;
2222
- if (ink.height > (text.height ?? 0)) text.height = ink.height;
2223
- }
2224
- function isCurvable(object) {
2225
- return !!object && typeof object.text === "string";
2226
- }
2227
- function measureText(text) {
2228
- const authored = text.width;
2229
- try {
2230
- text.set({ width: MEASURE_WIDTH2 });
2231
- text.initDimensions?.();
2232
- return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
2233
- } finally {
2234
- if (authored !== void 0) text.set({ width: authored });
2235
- text.initDimensions?.();
2271
+ // src/shapes.ts
2272
+ function resolveShapeParams(plugin, overrides = {}) {
2273
+ const params = {};
2274
+ for (const def of plugin.params ?? []) {
2275
+ const value = overrides[def.key];
2276
+ const wanted = typeof value === "number" && Number.isFinite(value) ? value : def.default;
2277
+ params[def.key] = clamp(wanted, def.min, def.max);
2236
2278
  }
2279
+ return params;
2237
2280
  }
2238
- function lineCountOf(text) {
2239
- return Math.max(1, text._textLines?.length ?? 1);
2240
- }
2241
- function lineHeightOf(text) {
2242
- const measured = text.getHeightOfLine?.(0);
2243
- if (typeof measured === "number" && Number.isFinite(measured) && measured > 0) return measured;
2244
- return text.fontSize * (text.lineHeight ?? 1) * FALLBACK_LINE_HEIGHT;
2245
- }
2246
- function toFabricPath(curve) {
2247
- return new import_fabric7.Path(curve.data, { visible: false, objectCaching: false });
2248
- }
2249
- var TextCurveManager = class {
2281
+ var ShapeManager = class {
2250
2282
  constructor(canvas, layers, history, events) {
2251
2283
  this.canvas = canvas;
2252
2284
  this.layers = layers;
2253
2285
  this.history = history;
2254
2286
  this.events = events;
2255
- this.canvas.on("text:changed", this.onTextChanged);
2287
+ this.canvas.on("object:modified", this.onObjectModified);
2256
2288
  }
2257
2289
  canvas;
2258
2290
  layers;
2259
2291
  history;
2260
2292
  events;
2293
+ plugins = /* @__PURE__ */ new Map();
2261
2294
  /**
2262
- * Typing on the canvas changes the run the paths were built for. Fabric drops
2263
- * a glyph whose centre falls past the end of the path, so without rebuilding
2264
- * here a longer replacement simply vanishes select-all-and-retype rendered
2265
- * an empty box — and a shorter one piles up on a path built for the old text.
2266
- *
2267
- * No history entry: fabric records the edit when editing exits, and one save
2268
- * per keystroke would bury every earlier step under the typing.
2295
+ * An arrow scaled 2×1 must not stretch its head: the new box is baked into
2296
+ * the points and the scale returns to 1. Runs before the editor's own
2297
+ * `object:modified` handler (constructed earlier), so history records the
2298
+ * baked state.
2269
2299
  */
2270
- onTextChanged = (event) => {
2300
+ onObjectModified = (event) => {
2301
+ const target = event.target;
2302
+ if (!target) return;
2303
+ const layer = this.layers.findByObject(target);
2304
+ const state = layer?.meta.shape;
2305
+ if (!layer || !state || target !== layer.fabricObject) return;
2306
+ const plugin = this.plugins.get(state.id);
2307
+ if (!plugin?.bakesScale || !plugin.regenerate) return;
2308
+ const { scaleX = 1, scaleY = 1 } = target;
2309
+ if (Math.abs(scaleX - scaleY) < 1e-6) return;
2310
+ target.set({
2311
+ width: target.width * scaleX,
2312
+ height: target.height * scaleY,
2313
+ scaleX: 1,
2314
+ scaleY: 1
2315
+ });
2316
+ plugin.regenerate(target, state.params);
2317
+ target.setCoords();
2318
+ target.dirty = true;
2319
+ this.canvas.requestRenderAll();
2320
+ };
2321
+ dispose() {
2322
+ this.canvas.off("object:modified", this.onObjectModified);
2323
+ }
2324
+ /** A restored layer names its plugin by id; only a registered one can regenerate. */
2325
+ register(plugin) {
2326
+ this.plugins.set(plugin.name, plugin);
2327
+ }
2328
+ read(layerId) {
2329
+ return this.layers.get(layerId)?.meta.shape ?? null;
2330
+ }
2331
+ /**
2332
+ * `commit: false` is a slider drag: geometry and `meta.shape` are written and
2333
+ * `layer:modified` is emitted so the Shape panel re-reads, but nothing reaches
2334
+ * history. The default commits one entry.
2335
+ */
2336
+ setParams(layerId, patch, commit = true) {
2337
+ const layer = this.layers.get(layerId);
2338
+ const state = layer?.meta.shape;
2339
+ if (!layer || !state) return;
2340
+ const plugin = this.plugins.get(state.id);
2341
+ if (!plugin?.regenerate) return;
2342
+ const params = resolveShapeParams(plugin, { ...state.params, ...patch });
2343
+ plugin.regenerate(layer.fabricObject, params);
2344
+ layer.fabricObject.setCoords();
2345
+ layer.fabricObject.dirty = true;
2346
+ const next = { ...state, params };
2347
+ if (commit) this.layers.setMeta(layerId, { shape: next });
2348
+ else layer.meta.shape = next;
2349
+ this.canvas.requestRenderAll();
2350
+ this.events.emit("layer:modified", { layerId });
2351
+ if (commit) this.history.save();
2352
+ }
2353
+ };
2354
+
2355
+ // src/image-effects/manager.ts
2356
+ var import_fabric9 = require("fabric");
2357
+
2358
+ // src/image-effects/presets.ts
2359
+ var LUMA = [0.299, 0.587, 0.114];
2360
+ var rows = (r, g, b) => [...r, ...g, ...b, 0, 0, 0, 1, 0];
2361
+ var IMAGE_FILTER_PRESETS = [
2362
+ { id: "original", name: "Original", version: 1, adjust: {} },
2363
+ {
2364
+ id: "black-white",
2365
+ name: "Black & White",
2366
+ version: 1,
2367
+ adjust: {},
2368
+ matrix: rows([...LUMA, 0, 0], [...LUMA, 0, 0], [...LUMA, 0, 0])
2369
+ },
2370
+ {
2371
+ id: "sepia",
2372
+ name: "Sepia",
2373
+ version: 1,
2374
+ adjust: {},
2375
+ matrix: rows(
2376
+ [0.393, 0.769, 0.189, 0, 0],
2377
+ [0.349, 0.686, 0.168, 0, 0],
2378
+ [0.272, 0.534, 0.131, 0, 0]
2379
+ )
2380
+ },
2381
+ {
2382
+ id: "fade",
2383
+ name: "Fade",
2384
+ version: 1,
2385
+ adjust: { contrast: -0.2, saturation: -0.25, brightness: 0.08 }
2386
+ },
2387
+ {
2388
+ id: "vivid",
2389
+ name: "Vivid",
2390
+ version: 1,
2391
+ adjust: { vibrance: 0.5, contrast: 0.15, saturation: 0.15 }
2392
+ },
2393
+ { id: "fuchsia", name: "Fuchsia", version: 1, adjust: { tint: 0.5, saturation: 0.2 } },
2394
+ { id: "cool", name: "Cool", version: 1, adjust: { temperature: -0.45 } },
2395
+ { id: "warm", name: "Warm", version: 1, adjust: { temperature: 0.45 } },
2396
+ {
2397
+ id: "duotone",
2398
+ name: "Duotone",
2399
+ version: 1,
2400
+ adjust: {},
2401
+ matrix: rows(
2402
+ [0.2544, 0.4995, 0.097, 0, 0.1059],
2403
+ [0.2216, 0.435, 0.0845, 0, 0.1647],
2404
+ [0.1453, 0.2853, 0.0554, 0, 0.2863]
2405
+ )
2406
+ },
2407
+ {
2408
+ id: "nashville",
2409
+ name: "Nashville",
2410
+ version: 1,
2411
+ adjust: { temperature: 0.25, tint: -0.1, contrast: 0.2, saturation: 0.15, brightness: 0.05 }
2412
+ },
2413
+ {
2414
+ id: "valencia",
2415
+ name: "Valencia",
2416
+ version: 1,
2417
+ adjust: { temperature: 0.2, contrast: 0.08, brightness: 0.08, saturation: -0.05 }
2418
+ },
2419
+ {
2420
+ id: "clarendon",
2421
+ name: "Clarendon",
2422
+ version: 1,
2423
+ adjust: { contrast: 0.2, saturation: 0.35, temperature: -0.1 }
2424
+ },
2425
+ {
2426
+ id: "reyes",
2427
+ name: "Reyes",
2428
+ version: 1,
2429
+ adjust: { brightness: 0.1, contrast: -0.15, saturation: -0.35, temperature: 0.15 }
2430
+ },
2431
+ {
2432
+ id: "lark",
2433
+ name: "Lark",
2434
+ version: 1,
2435
+ adjust: { brightness: 0.08, contrast: -0.05, saturation: 0.1, temperature: -0.15 }
2436
+ },
2437
+ {
2438
+ id: "juno",
2439
+ name: "Juno",
2440
+ version: 1,
2441
+ adjust: { saturation: 0.3, contrast: 0.1, temperature: 0.15, tint: 0.1 }
2442
+ }
2443
+ ];
2444
+ function imageFilterPreset(id) {
2445
+ return IMAGE_FILTER_PRESETS.find((preset) => preset.id === id);
2446
+ }
2447
+
2448
+ // src/image-effects/proxy.ts
2449
+ var import_fabric8 = require("fabric");
2450
+ function sourceSize(source) {
2451
+ const el = source;
2452
+ return { width: el.naturalWidth || el.width, height: el.naturalHeight || el.height };
2453
+ }
2454
+ function proxyScale(width, height, maxEdge) {
2455
+ const edge = Math.max(width, height);
2456
+ return edge > maxEdge ? maxEdge / edge : 1;
2457
+ }
2458
+ function buildProxySource(source, maxEdge) {
2459
+ const { width, height } = sourceSize(source);
2460
+ const scale = proxyScale(width, height, maxEdge);
2461
+ if (scale === 1) return null;
2462
+ const element = import_fabric8.util.createCanvasElement();
2463
+ element.width = Math.max(1, Math.round(width * scale));
2464
+ element.height = Math.max(1, Math.round(height * scale));
2465
+ const context = element.getContext("2d");
2466
+ if (!context) throw new Error("2D canvas context is unavailable");
2467
+ context.drawImage(source, 0, 0, element.width, element.height);
2468
+ return { element, width: element.width, height: element.height };
2469
+ }
2470
+
2471
+ // src/image-effects/types.ts
2472
+ var ADJUST_KEYS = [
2473
+ "temperature",
2474
+ "tint",
2475
+ "brightness",
2476
+ "contrast",
2477
+ "highlights",
2478
+ "shadows",
2479
+ "whites",
2480
+ "blacks",
2481
+ "invert",
2482
+ "vibrance",
2483
+ "saturation",
2484
+ "hue",
2485
+ "sharpness",
2486
+ "clarity",
2487
+ "vignette",
2488
+ "blur",
2489
+ "noise",
2490
+ "pixelate"
2491
+ ];
2492
+ var NEUTRAL_ADJUST = Object.fromEntries(
2493
+ ADJUST_KEYS.map((k) => [k, 0])
2494
+ );
2495
+ var ADJUST_RANGES = {
2496
+ invert: [0, 1],
2497
+ vignette: [0, 1],
2498
+ blur: [0, 1],
2499
+ noise: [0, 1],
2500
+ pixelate: [0, 1]
2501
+ };
2502
+ var IMAGE_PROXY_MAX_EDGE = 2048;
2503
+
2504
+ // src/image-effects/specs.ts
2505
+ var ORDER = [
2506
+ "highlights",
2507
+ "shadows",
2508
+ "whites",
2509
+ "blacks",
2510
+ "brightness",
2511
+ "contrast",
2512
+ "temperature",
2513
+ "tint",
2514
+ "vibrance",
2515
+ "saturation",
2516
+ "hue",
2517
+ "invert",
2518
+ "clarity",
2519
+ "sharpness",
2520
+ "noise",
2521
+ "pixelate",
2522
+ "blur",
2523
+ "vignette"
2524
+ ];
2525
+ function clampAdjust(adjust) {
2526
+ const out = {};
2527
+ for (const key of ADJUST_KEYS) {
2528
+ const value = adjust[key];
2529
+ if (typeof value !== "number" || !Number.isFinite(value)) continue;
2530
+ const [min, max] = ADJUST_RANGES[key] ?? [-1, 1];
2531
+ out[key] = key === "invert" ? value >= 0.5 ? 1 : 0 : clamp(value, min, max);
2532
+ }
2533
+ return out;
2534
+ }
2535
+ function clampAlphaClip(clip) {
2536
+ if (!clip || typeof clip !== "object") return void 0;
2537
+ const high = clamp(Number(clip.high) || 0, 1, 255);
2538
+ const low = clamp(Math.min(Number(clip.low) || 0, high - 1), 0, 254);
2539
+ return low === 0 && high === 255 ? void 0 : { low, high };
2540
+ }
2541
+ function alphaClipMatrix({ low, high }) {
2542
+ const span = Math.max(1, high - low);
2543
+ return [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 255 / span, -low / span];
2544
+ }
2545
+ function specFor(key, v) {
2546
+ if (v === 0) return null;
2547
+ switch (key) {
2548
+ case "highlights":
2549
+ return { type: "Highlights", amount: v };
2550
+ case "shadows":
2551
+ return { type: "Shadows", amount: v };
2552
+ case "whites":
2553
+ return { type: "Whites", amount: v };
2554
+ case "blacks":
2555
+ return { type: "Blacks", amount: v };
2556
+ case "brightness":
2557
+ return { type: "Brightness", brightness: v };
2558
+ case "contrast":
2559
+ return { type: "Contrast", contrast: v };
2560
+ case "temperature":
2561
+ return { type: "Temperature", amount: v };
2562
+ case "tint":
2563
+ return { type: "Tint", amount: v };
2564
+ case "vibrance":
2565
+ return { type: "Vibrance", vibrance: v };
2566
+ case "saturation":
2567
+ return { type: "Saturation", saturation: v };
2568
+ case "hue":
2569
+ return { type: "HueRotation", rotation: v };
2570
+ case "invert":
2571
+ return { type: "Invert", alpha: false, invert: true };
2572
+ case "clarity":
2573
+ return { type: "Clarity", amount: v };
2574
+ case "sharpness":
2575
+ return { type: "Convolute", opaque: false, matrix: [0, -v, 0, -v, 1 + 4 * v, -v, 0, -v, 0] };
2576
+ case "noise":
2577
+ return { type: "Noise", noise: Math.round(v * 300) };
2578
+ case "pixelate":
2579
+ return { type: "Pixelate", blocksize: Math.max(1, Math.round(1 + v * 40)) };
2580
+ case "blur":
2581
+ return { type: "Blur", blur: v };
2582
+ case "vignette":
2583
+ return { type: "Vignette", amount: v };
2584
+ }
2585
+ }
2586
+ function imageEffectsFilterSpecs(state) {
2587
+ const specs = [];
2588
+ const preset = state.preset ? imageFilterPreset(state.preset.id) : void 0;
2589
+ if (preset?.matrix)
2590
+ specs.push({ type: "ColorMatrix", matrix: [...preset.matrix], colorsOnly: true });
2591
+ const adjust = clampAdjust(state.adjust);
2592
+ for (const key of ORDER) {
2593
+ const spec = specFor(key, adjust[key] ?? 0);
2594
+ if (spec) specs.push(spec);
2595
+ }
2596
+ const clip = clampAlphaClip(state.alphaClip);
2597
+ if (clip) specs.push({ type: "ColorMatrix", matrix: alphaClipMatrix(clip), colorsOnly: false });
2598
+ return specs;
2599
+ }
2600
+ function normalizeImageEffects(meta) {
2601
+ const alphaClip = clampAlphaClip(meta.imageAlphaClip);
2602
+ const withClip = (state) => alphaClip ? { ...state, alphaClip } : state;
2603
+ const current = meta.imageEffects;
2604
+ if (current && typeof current === "object") {
2605
+ const preset = current.preset && imageFilterPreset(current.preset.id) ? { id: current.preset.id, version: current.preset.version } : void 0;
2606
+ return withClip({ ...preset ? { preset } : {}, adjust: clampAdjust(current.adjust ?? {}) });
2607
+ }
2608
+ const legacy = meta.imageAdjustments;
2609
+ return withClip({ adjust: clampAdjust(legacy ?? {}) });
2610
+ }
2611
+
2612
+ // src/image-effects/manager.ts
2613
+ var stable = (value) => JSON.stringify(
2614
+ value,
2615
+ (_k, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort()) : v
2616
+ );
2617
+ var ImageEffectsManager = class {
2618
+ constructor(canvas, layers, history, events) {
2619
+ this.canvas = canvas;
2620
+ this.layers = layers;
2621
+ this.history = history;
2622
+ this.events = events;
2623
+ this.events.on("layer:removed", this.onRemoved);
2624
+ }
2625
+ canvas;
2626
+ layers;
2627
+ history;
2628
+ events;
2629
+ proxies = /* @__PURE__ */ new Map();
2630
+ lastSpecs = /* @__PURE__ */ new Map();
2631
+ onRemoved = ({ layerId }) => this.drop(layerId);
2632
+ read(layerId) {
2633
+ const layer = this.imageLayer(layerId);
2634
+ return layer ? normalizeImageEffects(layer.meta) : { adjust: {} };
2635
+ }
2636
+ /** The filter instances a state derives to, in the D5 order, with no layer involved. */
2637
+ filtersFor(state) {
2638
+ return imageEffectsFilterSpecs(state).map(
2639
+ (spec) => new (import_fabric9.classRegistry.getClass(spec.type))(spec)
2640
+ );
2641
+ }
2642
+ preview(layerId, patch) {
2643
+ this.write(layerId, patch);
2644
+ }
2645
+ commit(layerId, patch) {
2646
+ if (this.write(layerId, patch)) this.history.save();
2647
+ }
2648
+ reset(layerId) {
2649
+ if (this.write(layerId, { preset: void 0, adjust: {} }, true)) this.history.save();
2650
+ }
2651
+ /** Re-derive after a source swap or a load. */
2652
+ refresh(layerId) {
2653
+ const layer = this.imageLayer(layerId);
2654
+ if (layer) this.apply(layer, this.read(layerId));
2655
+ }
2656
+ refreshAll() {
2657
+ for (const id of [...this.proxies.keys()]) if (!this.layers.get(id)) this.drop(id);
2658
+ for (const layer of this.layers.getAll()) {
2659
+ if (layer.type !== "image") continue;
2660
+ if ("imageAdjustments" in layer.meta) {
2661
+ const migrated = normalizeImageEffects(layer.meta);
2662
+ layer.meta.imageEffects = {
2663
+ ...migrated.preset ? { preset: migrated.preset } : {},
2664
+ adjust: migrated.adjust
2665
+ };
2666
+ delete layer.meta.imageAdjustments;
2667
+ }
2668
+ this.apply(layer, this.read(layer.id));
2669
+ }
2670
+ }
2671
+ hasProxy(layerId) {
2672
+ return this.proxies.has(layerId);
2673
+ }
2674
+ /** Run `fn` with every proxied image filtered at source resolution (export), then restore the proxies. */
2675
+ withSourceResolution(fn) {
2676
+ const swapped = [];
2677
+ for (const id of this.proxies.keys()) {
2678
+ const layer = this.imageLayer(id);
2679
+ if (!layer) continue;
2680
+ const image = layer.fabricObject;
2681
+ image._element = image._originalElement;
2682
+ image._filteredEl = void 0;
2683
+ image._filterScalingX = 1;
2684
+ image._filterScalingY = 1;
2685
+ image.applyFilters();
2686
+ swapped.push(layer);
2687
+ }
2688
+ try {
2689
+ return fn();
2690
+ } finally {
2691
+ for (const layer of swapped) {
2692
+ const image = layer.fabricObject;
2693
+ if (image._filteredEl) image._filteredEl.width = 0;
2694
+ image._filteredEl = void 0;
2695
+ image._element = image._originalElement;
2696
+ this.render(layer);
2697
+ }
2698
+ }
2699
+ }
2700
+ dispose() {
2701
+ this.events.off("layer:removed", this.onRemoved);
2702
+ for (const id of [...this.proxies.keys()]) this.drop(id);
2703
+ this.lastSpecs.clear();
2704
+ }
2705
+ imageLayer(layerId) {
2706
+ const layer = this.layers.get(layerId);
2707
+ return layer && layer.type === "image" ? layer : void 0;
2708
+ }
2709
+ write(layerId, patch, replace = false) {
2710
+ const layer = this.imageLayer(layerId);
2711
+ if (!layer) return false;
2712
+ const current = replace ? { adjust: {} } : this.read(layerId);
2713
+ const presetChanged = "preset" in patch && stable(patch.preset) !== stable(current.preset);
2714
+ const seed = presetChanged && patch.preset ? imageFilterPreset(patch.preset.id)?.adjust ?? {} : current.adjust;
2715
+ const preset = "preset" in patch ? patch.preset : current.preset;
2716
+ const alphaClip = clampAlphaClip("alphaClip" in patch ? patch.alphaClip : current.alphaClip);
2717
+ const next = {
2718
+ ...preset ? { preset } : {},
2719
+ adjust: { ...seed, ...patch.adjust ?? {} },
2720
+ ...alphaClip ? { alphaClip } : {}
2721
+ };
2722
+ layer.meta.imageEffects = { ...preset ? { preset } : {}, adjust: next.adjust };
2723
+ if (alphaClip) layer.meta.imageAlphaClip = alphaClip;
2724
+ else delete layer.meta.imageAlphaClip;
2725
+ delete layer.meta.imageAdjustments;
2726
+ this.apply(layer, next);
2727
+ this.events.emit("layer:modified", { layerId });
2728
+ return true;
2729
+ }
2730
+ apply(layer, state) {
2731
+ const image = layer.fabricObject;
2732
+ const specs = imageEffectsFilterSpecs(state);
2733
+ const previous = this.lastSpecs.get(layer.id) ?? specs;
2734
+ const clip = clampAlphaClip(state.alphaClip);
2735
+ const legacyClip = clip ? stable(alphaClipMatrix(clip)) : null;
2736
+ const foreign = image.filters.filter((f) => {
2737
+ if (f.isNeutralState()) return false;
2738
+ const plain = f.toObject();
2739
+ if (legacyClip && plain.type === "ColorMatrix" && stable(plain.matrix) === legacyClip)
2740
+ return false;
2741
+ return !previous.some((s) => stable(s) === stable(plain));
2742
+ });
2743
+ const managed = specs.map((spec) => new (import_fabric9.classRegistry.getClass(spec.type))(spec));
2744
+ image.filters = [...managed, ...foreign];
2745
+ this.lastSpecs.set(layer.id, specs);
2746
+ this.render(layer);
2747
+ }
2748
+ render(layer) {
2749
+ const image = layer.fabricObject;
2750
+ const record = this.proxyFor(layer.id, image);
2751
+ if (!record) {
2752
+ image.applyFilters();
2753
+ } else {
2754
+ const active = image.filters.filter((f) => !f.isNeutralState());
2755
+ if (active.length === 0) {
2756
+ image._element = image._originalElement;
2757
+ image._filterScalingX = 1;
2758
+ image._filterScalingY = 1;
2759
+ } else {
2760
+ (0, import_fabric9.getFilterBackend)().applyFilters(
2761
+ active,
2762
+ record.source.element,
2763
+ record.source.width,
2764
+ record.source.height,
2765
+ record.target,
2766
+ `${image.cacheKey}_proxy`
2767
+ );
2768
+ const { width, height } = sourceSize(image._originalElement);
2769
+ image._element = record.target;
2770
+ image._filterScalingX = record.source.width / width;
2771
+ image._filterScalingY = record.source.height / height;
2772
+ }
2773
+ image.set("dirty", true);
2774
+ }
2775
+ this.canvas.requestRenderAll();
2776
+ }
2777
+ proxyFor(layerId, image) {
2778
+ const existing = this.proxies.get(layerId);
2779
+ if (existing && existing.original === image._originalElement) return existing;
2780
+ if (existing) this.drop(layerId);
2781
+ const source = buildProxySource(image._originalElement, IMAGE_PROXY_MAX_EDGE);
2782
+ if (!source) return null;
2783
+ const target = source.element.ownerDocument.createElement("canvas");
2784
+ target.width = source.width;
2785
+ target.height = source.height;
2786
+ const record = { original: image._originalElement, source, target };
2787
+ this.proxies.set(layerId, record);
2788
+ return record;
2789
+ }
2790
+ drop(layerId) {
2791
+ const record = this.proxies.get(layerId);
2792
+ if (!record) return;
2793
+ const layer = this.layers.get(layerId);
2794
+ if (layer)
2795
+ layer.fabricObject.removeTexture(
2796
+ `${layer.fabricObject.cacheKey}_proxy`
2797
+ );
2798
+ record.source.element.width = 0;
2799
+ record.target.width = 0;
2800
+ this.proxies.delete(layerId);
2801
+ this.lastSpecs.delete(layerId);
2802
+ }
2803
+ };
2804
+
2805
+ // src/layer-effects.ts
2806
+ var import_canvas_editor_effects = require("@overtone-art/canvas-editor-effects");
2807
+
2808
+ // src/shadow.ts
2809
+ var import_fabric10 = require("fabric");
2810
+
2811
+ // src/utils/color.ts
2812
+ var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
2813
+ function isCssColor(value, allowEmpty = false) {
2814
+ if (typeof value !== "string") return false;
2815
+ const trimmed = value.trim();
2816
+ if (!trimmed) return allowEmpty;
2817
+ return CSS_COLOR.test(trimmed);
2818
+ }
2819
+
2820
+ // src/shadow.ts
2821
+ var DEFAULT_LAYER_SHADOW = {
2822
+ enabled: false,
2823
+ color: "#000000",
2824
+ blur: 12,
2825
+ offsetX: 6,
2826
+ offsetY: 6
2827
+ };
2828
+ function readLayerShadow(object) {
2829
+ const shadow = object.shadow;
2830
+ if (!shadow || typeof shadow === "string") return { ...DEFAULT_LAYER_SHADOW };
2831
+ return {
2832
+ enabled: true,
2833
+ color: typeof shadow.color === "string" ? shadow.color : DEFAULT_LAYER_SHADOW.color,
2834
+ blur: shadow.blur ?? DEFAULT_LAYER_SHADOW.blur,
2835
+ offsetX: shadow.offsetX ?? DEFAULT_LAYER_SHADOW.offsetX,
2836
+ offsetY: shadow.offsetY ?? DEFAULT_LAYER_SHADOW.offsetY
2837
+ };
2838
+ }
2839
+ function applyLayerShadow(object, config) {
2840
+ const next = { ...readLayerShadow(object), ...config };
2841
+ if (!next.enabled) {
2842
+ object.set({ shadow: null });
2843
+ return;
2844
+ }
2845
+ const color2 = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
2846
+ object.set({
2847
+ shadow: new import_fabric10.Shadow({
2848
+ color: color2,
2849
+ blur: Math.max(0, next.blur),
2850
+ offsetX: next.offsetX,
2851
+ offsetY: next.offsetY
2852
+ })
2853
+ });
2854
+ }
2855
+
2856
+ // src/layer-effects.ts
2857
+ var LAYER_EFFECT_DEFAULTS = {
2858
+ outline: { color: "#000000", width: 4 },
2859
+ hollow: { color: "#000000", width: 4 },
2860
+ drop: { color: "#000000", blur: 0, offsetX: 6, offsetY: 6 },
2861
+ splice: { color: "#ff4d6d", offsetX: 8, offsetY: 8 },
2862
+ echo: { color: "#000000", count: 3, stepX: 6, stepY: 6, fade: 0.5 },
2863
+ background: { color: "#ffe600", padding: 12, radius: 8 }
2864
+ };
2865
+ var num = (p, key, fallback) => {
2866
+ const value = p[key];
2867
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
2868
+ };
2869
+ var color = (p, key, fallback) => {
2870
+ const value = p[key];
2871
+ return typeof value === "string" && isCssColor(value) ? value : fallback;
2872
+ };
2873
+ var outlineProps = (p) => ({
2874
+ stroke: color(p, "color", "#000000"),
2875
+ strokeWidth: clamp(num(p, "width", 4), 0, 200),
2876
+ paintFirst: "stroke",
2877
+ strokeUniform: true
2878
+ });
2879
+ var LayerEffectsManager = class {
2880
+ constructor(canvas, layers, history, events) {
2881
+ this.canvas = canvas;
2882
+ this.layers = layers;
2883
+ this.history = history;
2884
+ this.events = events;
2885
+ }
2886
+ canvas;
2887
+ layers;
2888
+ history;
2889
+ events;
2890
+ read(layerId) {
2891
+ return this.layers.get(layerId)?.meta.effect ?? null;
2892
+ }
2893
+ /**
2894
+ * `commit: false` is a slider drag: props are written and `layer:modified`
2895
+ * is emitted so the panels re-read, but nothing reaches history. The default
2896
+ * commits ONE entry, setMeta's own save folded into the same transaction.
2897
+ */
2898
+ apply(layerId, kind, params = {}, commit = true) {
2899
+ const layer = this.layers.get(layerId);
2900
+ if (!layer || layer.type === "group") return;
2901
+ const object = layer.fabricObject;
2902
+ const write = () => {
2903
+ this.clear(object, layer.meta.effect);
2904
+ const next = kind === "none" ? void 0 : {
2905
+ kind,
2906
+ params: this.install(object, kind, { ...LAYER_EFFECT_DEFAULTS[kind], ...params }),
2907
+ version: 1
2908
+ };
2909
+ if (commit) this.layers.setMeta(layerId, { effect: next });
2910
+ else if (next) layer.meta.effect = next;
2911
+ else delete layer.meta.effect;
2912
+ object.dirty = true;
2913
+ object.setCoords();
2914
+ this.canvas.requestRenderAll();
2915
+ this.events.emit("layer:modified", { layerId });
2916
+ };
2917
+ if (!commit) {
2918
+ write();
2919
+ return;
2920
+ }
2921
+ this.history.beginTransaction();
2922
+ try {
2923
+ write();
2924
+ this.history.save();
2925
+ } finally {
2926
+ this.history.endTransaction();
2927
+ }
2928
+ }
2929
+ /** Undo what the previous kind wrote, so kinds never stack. */
2930
+ clear(object, previous) {
2931
+ if (!previous) return;
2932
+ switch (previous.kind) {
2933
+ case "outline":
2934
+ object.set({ strokeWidth: 0, paintFirst: "fill", strokeUniform: false });
2935
+ return;
2936
+ case "hollow": {
2937
+ const fill = previous.params.previousFill;
2938
+ object.set({
2939
+ strokeWidth: 0,
2940
+ paintFirst: "fill",
2941
+ strokeUniform: false,
2942
+ fill: typeof fill === "string" ? fill : "#000000"
2943
+ });
2944
+ return;
2945
+ }
2946
+ case "drop":
2947
+ applyLayerShadow(object, { enabled: false });
2948
+ return;
2949
+ case "splice":
2950
+ case "echo":
2951
+ case "background":
2952
+ object.set({ effect: void 0 });
2953
+ return;
2954
+ case "none":
2955
+ return;
2956
+ }
2957
+ }
2958
+ install(object, kind, p) {
2959
+ switch (kind) {
2960
+ case "outline": {
2961
+ const props = outlineProps(p);
2962
+ object.set(props);
2963
+ return { color: props.stroke, width: props.strokeWidth };
2964
+ }
2965
+ case "hollow": {
2966
+ const previousFill = typeof object.fill === "string" ? object.fill : "#000000";
2967
+ const props = outlineProps(p);
2968
+ object.set({ ...props, fill: "transparent" });
2969
+ return { color: props.stroke, width: props.strokeWidth, previousFill };
2970
+ }
2971
+ case "drop": {
2972
+ const shadow = {
2973
+ color: color(p, "color", "#000000"),
2974
+ blur: clamp(num(p, "blur", 0), 0, 200),
2975
+ offsetX: num(p, "offsetX", 6),
2976
+ offsetY: num(p, "offsetY", 6)
2977
+ };
2978
+ applyLayerShadow(object, { enabled: true, ...shadow });
2979
+ return shadow;
2980
+ }
2981
+ case "splice": {
2982
+ const effect = {
2983
+ kind: "splice",
2984
+ offsetX: num(p, "offsetX", 8),
2985
+ offsetY: num(p, "offsetY", 8),
2986
+ color: color(p, "color", "#ff4d6d"),
2987
+ version: 1
2988
+ };
2989
+ object.set({ effect });
2990
+ return { offsetX: effect.offsetX, offsetY: effect.offsetY, color: effect.color };
2991
+ }
2992
+ case "echo": {
2993
+ const effect = {
2994
+ kind: "echo",
2995
+ count: clamp(Math.round(num(p, "count", 3)), 1, import_canvas_editor_effects.ECHO_MAX_COUNT),
2996
+ stepX: num(p, "stepX", 6),
2997
+ stepY: num(p, "stepY", 6),
2998
+ color: color(p, "color", "#000000"),
2999
+ fade: clamp(num(p, "fade", 0.5), 0, 1),
3000
+ version: 1
3001
+ };
3002
+ object.set({ effect });
3003
+ return {
3004
+ count: effect.count,
3005
+ stepX: effect.stepX,
3006
+ stepY: effect.stepY,
3007
+ color: effect.color,
3008
+ fade: effect.fade
3009
+ };
3010
+ }
3011
+ case "background": {
3012
+ const effect = {
3013
+ kind: "background",
3014
+ padding: clamp(num(p, "padding", 12), 0, 400),
3015
+ radius: clamp(num(p, "radius", 8), 0, 400),
3016
+ color: color(p, "color", "#ffe600"),
3017
+ version: 1
3018
+ };
3019
+ object.set({ effect });
3020
+ return { padding: effect.padding, radius: effect.radius, color: effect.color };
3021
+ }
3022
+ }
3023
+ }
3024
+ };
3025
+
3026
+ // src/text-curve.ts
3027
+ var import_fabric11 = require("fabric");
3028
+
3029
+ // src/text-curve-geometry.ts
3030
+ var DEFAULT_TEXT_CURVE = {
3031
+ shape: "arc",
3032
+ arc: 0,
3033
+ wave: 0,
3034
+ waveLength: 4.1,
3035
+ offset: 0,
3036
+ centerOffset: 0,
3037
+ angle: 0
3038
+ };
3039
+ var MIN_ARC = 0.5;
3040
+ var MIN_ANGLE = 0.5;
3041
+ var MAX_ANGLE = 80;
3042
+ var FULL_CIRCLE_ARC = 99.5;
3043
+ var MIN_SWEEP = 0.12;
3044
+ var WAVE_AMPLITUDE_EM = 0.9;
3045
+ var MIN_WAVE_LENGTH = 1;
3046
+ var MAX_WAVE_LENGTH = 12;
3047
+ var WAVE_STEP = 6;
3048
+ var MAX_WAVE_STEPS = 4e3;
3049
+ function round(value) {
3050
+ return Math.round(value * 100) / 100;
3051
+ }
3052
+ function normalizeTextCurve(config) {
3053
+ const arc = clamp(config.arc ?? 0, -100, 100);
3054
+ const wave = clamp(config.wave ?? 0, 0, 100);
3055
+ const inferred = Math.abs(arc) < MIN_ARC && wave > 0 ? "wave" : "arc";
3056
+ return {
3057
+ shape: config.shape ?? inferred,
3058
+ arc,
3059
+ wave,
3060
+ waveLength: clamp(
3061
+ config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength,
3062
+ MIN_WAVE_LENGTH,
3063
+ MAX_WAVE_LENGTH
3064
+ ),
3065
+ offset: clamp(config.offset ?? 0, -100, 100),
3066
+ centerOffset: clamp(config.centerOffset ?? 0, -100, 100),
3067
+ angle: clamp(config.angle ?? 0, -MAX_ANGLE, MAX_ANGLE)
3068
+ };
3069
+ }
3070
+ function bendsBaseline(config) {
3071
+ if (config.shape === "angle") return Math.abs(config.angle) >= MIN_ANGLE;
3072
+ return config.shape === "arc" ? Math.abs(config.arc) >= MIN_ARC : config.wave > 0;
3073
+ }
3074
+ function arcGeometry(width, config) {
3075
+ const magnitude = Math.min(100, Math.abs(config.arc));
3076
+ const direction = config.arc < 0 ? -1 : 1;
3077
+ const baseSweep = magnitude >= FULL_CIRCLE_ARC ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
3078
+ const baseRadius = width / baseSweep;
3079
+ const radius = baseRadius * Math.pow(2, config.centerOffset / 100);
3080
+ const sweep = radius >= baseRadius ? baseSweep : Math.min(Math.PI * 2, width / Math.max(radius, 1e-3));
3081
+ return { radius, sweep, direction, baseRadius };
3082
+ }
3083
+ function arcLinePath(width, geometry, radius, rotation) {
3084
+ const { sweep, direction, baseRadius } = geometry;
3085
+ const full = sweep >= Math.PI * 2 - 1e-6;
3086
+ const centerX = width / 2;
3087
+ const point = (angle) => [
3088
+ centerX + radius * Math.sin(angle),
3089
+ direction * (baseRadius - radius * Math.cos(angle))
3090
+ ];
3091
+ const sweepFlag = direction > 0 ? 1 : 0;
3092
+ const format = ([x, y]) => `${round(x)} ${round(y)}`;
3093
+ const r = round(radius);
3094
+ if (full) {
3095
+ const start2 = point(rotation - Math.PI);
3096
+ const opposite = point(rotation);
3097
+ return {
3098
+ data: [
3099
+ `M ${format(start2)}`,
3100
+ `A ${r} ${r} 0 0 ${sweepFlag} ${format(opposite)}`,
3101
+ `A ${r} ${r} 0 0 ${sweepFlag} ${format(start2)}`
3102
+ ].join(" "),
3103
+ length: radius * Math.PI * 2
3104
+ };
3105
+ }
3106
+ const start = point(rotation - sweep / 2);
3107
+ const end = point(rotation + sweep / 2);
3108
+ const largeArc = sweep > Math.PI ? 1 : 0;
3109
+ return {
3110
+ data: `M ${format(start)} A ${r} ${r} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
3111
+ length: radius * sweep
3112
+ };
3113
+ }
3114
+ function waveLinePath(width, fontSize, config, dy) {
3115
+ const amplitude = config.wave / 100 * fontSize * WAVE_AMPLITUDE_EM;
3116
+ const period = Math.max(1, fontSize * config.waveLength);
3117
+ const phase = config.offset / 100 * Math.PI * 2;
3118
+ const step = Math.min(WAVE_STEP, period / 8);
3119
+ const steps = Math.min(MAX_WAVE_STEPS, Math.max(2, Math.ceil(width / step)));
3120
+ const commands = [];
3121
+ let length = 0;
3122
+ let previous = null;
3123
+ for (let index = 0; index <= steps; index++) {
3124
+ const x = width * index / steps;
3125
+ const y = dy + amplitude * Math.sin(x / period * Math.PI * 2 + phase);
3126
+ if (previous) length += Math.hypot(x - previous[0], y - previous[1]);
3127
+ previous = [x, y];
3128
+ commands.push(`${index === 0 ? "M" : "L"} ${round(x)} ${round(y)}`);
3129
+ }
3130
+ return { data: commands.join(" "), length };
3131
+ }
3132
+ function angleLinePath(width, config, dy) {
3133
+ const radians = config.angle * Math.PI / 180;
3134
+ const half = width / 2;
3135
+ const dx = half * Math.cos(radians);
3136
+ const rise = half * Math.sin(radians);
3137
+ const format = ([x, y]) => `${round(x)} ${round(y)}`;
3138
+ return {
3139
+ data: `M ${format([half - dx, dy + rise])} L ${format([half + dx, dy - rise])}`,
3140
+ length: width
3141
+ };
3142
+ }
3143
+ function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight = 0) {
3144
+ const curve = normalizeTextCurve(config);
3145
+ if (!bendsBaseline(curve)) return null;
3146
+ const lines = Math.max(1, Math.floor(lineCount));
3147
+ const paths = [];
3148
+ if (curve.shape === "wave") {
3149
+ for (let index = 0; index < lines; index++) {
3150
+ paths.push(waveLinePath(width, fontSize, curve, (index - (lines - 1) / 2) * lineHeight));
3151
+ }
3152
+ return paths;
3153
+ }
3154
+ if (curve.shape === "angle") {
3155
+ for (let index = 0; index < lines; index++) {
3156
+ paths.push(angleLinePath(width, curve, (index - (lines - 1) / 2) * lineHeight));
3157
+ }
3158
+ return paths;
3159
+ }
3160
+ const geometry = arcGeometry(width, curve);
3161
+ const rotation = curve.offset / 100 * geometry.sweep;
3162
+ for (let index = 0; index < lines; index++) {
3163
+ const radius = Math.max(1, geometry.radius + (index - (lines - 1) / 2) * lineHeight);
3164
+ paths.push(arcLinePath(width, geometry, radius, rotation));
3165
+ }
3166
+ return paths;
3167
+ }
3168
+ function buildCurvePathData(config, width, fontSize) {
3169
+ return buildCurveLinePaths(config, width, fontSize)?.[0] ?? null;
3170
+ }
3171
+
3172
+ // src/text-curve.ts
3173
+ var MEASURE_WIDTH2 = 1e5;
3174
+ var PATH_SLACK = 0.06;
3175
+ var FALLBACK_LINE_HEIGHT = 1.16;
3176
+ var patches = /* @__PURE__ */ new WeakMap();
3177
+ var dimensionPatches = /* @__PURE__ */ new WeakMap();
3178
+ var authoredCaching = /* @__PURE__ */ new WeakMap();
3179
+ var GLYPH_HALF_HEIGHT = 0.7;
3180
+ function curvedInkSize(text) {
3181
+ const lineCount = text._textLines?.length ?? 0;
3182
+ for (let index = 0; index < lineCount; index += 1) text.getLineWidth?.(index);
3183
+ const lines = text.__charBounds;
3184
+ if (!lines?.length) return null;
3185
+ let halfWidth = 0;
3186
+ let halfHeight = 0;
3187
+ for (const line of lines) {
3188
+ for (const box of line ?? []) {
3189
+ const { renderLeft, renderTop } = box;
3190
+ if (typeof renderLeft !== "number" || typeof renderTop !== "number") continue;
3191
+ const halfGlyphWidth = (box.kernedWidth ?? box.width ?? 0) / 2;
3192
+ const halfGlyphHeight = (box.height ?? text.fontSize) * GLYPH_HALF_HEIGHT;
3193
+ const angle = box.angle ?? 0;
3194
+ const cos = Math.abs(Math.cos(angle));
3195
+ const sin = Math.abs(Math.sin(angle));
3196
+ halfWidth = Math.max(
3197
+ halfWidth,
3198
+ Math.abs(renderLeft) + halfGlyphWidth * cos + halfGlyphHeight * sin
3199
+ );
3200
+ halfHeight = Math.max(
3201
+ halfHeight,
3202
+ Math.abs(renderTop) + halfGlyphWidth * sin + halfGlyphHeight * cos
3203
+ );
3204
+ }
3205
+ }
3206
+ if (halfWidth === 0 && halfHeight === 0) return null;
3207
+ return { width: halfWidth * 2, height: halfHeight * 2 };
3208
+ }
3209
+ function growBoxToInk(text) {
3210
+ const ink = curvedInkSize(text);
3211
+ if (!ink) return;
3212
+ if (ink.width > (text.width ?? 0)) text.width = ink.width;
3213
+ if (ink.height > (text.height ?? 0)) text.height = ink.height;
3214
+ }
3215
+ function isCurvable(object) {
3216
+ return !!object && typeof object.text === "string";
3217
+ }
3218
+ function measureText(text) {
3219
+ const authored = text.width;
3220
+ try {
3221
+ text.set({ width: MEASURE_WIDTH2 });
3222
+ text.initDimensions?.();
3223
+ return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
3224
+ } finally {
3225
+ if (authored !== void 0) text.set({ width: authored });
3226
+ text.initDimensions?.();
3227
+ }
3228
+ }
3229
+ function lineCountOf(text) {
3230
+ return Math.max(1, text._textLines?.length ?? 1);
3231
+ }
3232
+ function lineHeightOf(text) {
3233
+ const measured = text.getHeightOfLine?.(0);
3234
+ if (typeof measured === "number" && Number.isFinite(measured) && measured > 0) return measured;
3235
+ return text.fontSize * (text.lineHeight ?? 1) * FALLBACK_LINE_HEIGHT;
3236
+ }
3237
+ function toFabricPath(curve) {
3238
+ return new import_fabric11.Path(curve.data, { visible: false, objectCaching: false });
3239
+ }
3240
+ var TextCurveManager = class {
3241
+ constructor(canvas, layers, history, events) {
3242
+ this.canvas = canvas;
3243
+ this.layers = layers;
3244
+ this.history = history;
3245
+ this.events = events;
3246
+ this.canvas.on("text:changed", this.onTextChanged);
3247
+ }
3248
+ canvas;
3249
+ layers;
3250
+ history;
3251
+ events;
3252
+ /**
3253
+ * Typing on the canvas changes the run the paths were built for. Fabric drops
3254
+ * a glyph whose centre falls past the end of the path, so without rebuilding
3255
+ * here a longer replacement simply vanishes — select-all-and-retype rendered
3256
+ * an empty box — and a shorter one piles up on a path built for the old text.
3257
+ *
3258
+ * No history entry: fabric records the edit when editing exits, and one save
3259
+ * per keystroke would bury every earlier step under the typing.
3260
+ */
3261
+ onTextChanged = (event) => {
2271
3262
  const layer = event.target ? this.layers.findByObject(event.target) : void 0;
2272
3263
  if (!layer?.meta.curve) return;
2273
3264
  this.refresh(layer.id, false);
@@ -2464,7 +3455,7 @@ var TextCurveManager = class {
2464
3455
  };
2465
3456
 
2466
3457
  // src/text-wrap.ts
2467
- var import_fabric9 = require("fabric");
3458
+ var import_fabric13 = require("fabric");
2468
3459
 
2469
3460
  // src/text-wrap-split.ts
2470
3461
  var WHITESPACE = /[ \t\r]/;
@@ -2490,12 +3481,12 @@ function preWrapWordSplit(value) {
2490
3481
  }
2491
3482
 
2492
3483
  // src/masks/space.ts
2493
- var import_fabric8 = require("fabric");
3484
+ var import_fabric12 = require("fabric");
2494
3485
  function matrixOf(object) {
2495
3486
  return object.calcTransformMatrix();
2496
3487
  }
2497
3488
  function applyMatrix(object, matrix) {
2498
- const decomposed = import_fabric8.util.qrDecompose(matrix);
3489
+ const decomposed = import_fabric12.util.qrDecompose(matrix);
2499
3490
  object.set({
2500
3491
  flipX: false,
2501
3492
  flipY: false,
@@ -2512,19 +3503,19 @@ function applyMatrix(object, matrix) {
2512
3503
  object.setCoords();
2513
3504
  }
2514
3505
  function toCanvasSpace(object, host) {
2515
- applyMatrix(object, import_fabric8.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
3506
+ applyMatrix(object, import_fabric12.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
2516
3507
  }
2517
3508
  function toHostSpace(object, host) {
2518
3509
  applyMatrix(
2519
3510
  object,
2520
- import_fabric8.util.multiplyTransformMatrices(import_fabric8.util.invertTransform(matrixOf(host)), matrixOf(object))
3511
+ import_fabric12.util.multiplyTransformMatrices(import_fabric12.util.invertTransform(matrixOf(host)), matrixOf(object))
2521
3512
  );
2522
3513
  }
2523
3514
  function relativeMatrix(object, host) {
2524
- return import_fabric8.util.multiplyTransformMatrices(import_fabric8.util.invertTransform(matrixOf(host)), matrixOf(object));
3515
+ return import_fabric12.util.multiplyTransformMatrices(import_fabric12.util.invertTransform(matrixOf(host)), matrixOf(object));
2525
3516
  }
2526
3517
  function applyRelativeMatrix(object, host, rel) {
2527
- applyMatrix(object, import_fabric8.util.multiplyTransformMatrices(matrixOf(host), rel));
3518
+ applyMatrix(object, import_fabric12.util.multiplyTransformMatrices(matrixOf(host), rel));
2528
3519
  }
2529
3520
  function asObject(clip) {
2530
3521
  return clip;
@@ -2587,7 +3578,7 @@ function boxClip(text, makeRect) {
2587
3578
  function hasMask(meta) {
2588
3579
  return !!meta.maskPreset || (meta.maskStack?.length ?? 0) > 0;
2589
3580
  }
2590
- function applyTextWrapToObject(object, meta, makeRect = (options) => new import_fabric9.Rect(options)) {
3581
+ function applyTextWrapToObject(object, meta, makeRect = (options) => new import_fabric13.Rect(options)) {
2591
3582
  const text = asText(object);
2592
3583
  if (!text) return;
2593
3584
  const state = readTextWrap(meta);
@@ -2726,7 +3717,7 @@ var TextWrapManager = class {
2726
3717
  };
2727
3718
 
2728
3719
  // src/mask-presets/manager.ts
2729
- var import_fabric10 = require("fabric");
3720
+ var import_fabric14 = require("fabric");
2730
3721
 
2731
3722
  // src/mask-presets/shapes.ts
2732
3723
  var SHAPE_MASK_IDS = [
@@ -2994,13 +3985,13 @@ var MaskPresetManager = class {
2994
3985
  objectCaching: false
2995
3986
  };
2996
3987
  if (isShapeMaskId(id)) {
2997
- return new import_fabric10.Path(shapeMaskPathData(id), {
3988
+ return new import_fabric14.Path(shapeMaskPathData(id), {
2998
3989
  ...shared,
2999
3990
  scaleX: width / SHAPE_MASK_BOX,
3000
3991
  scaleY: height / SHAPE_MASK_BOX
3001
3992
  });
3002
3993
  }
3003
- return new import_fabric10.FabricImage(renderTextureMask(id), {
3994
+ return new import_fabric14.FabricImage(renderTextureMask(id), {
3004
3995
  ...shared,
3005
3996
  scaleX: width / TEXTURE_MASK_SIZE,
3006
3997
  scaleY: height / TEXTURE_MASK_SIZE
@@ -3008,54 +3999,6 @@ var MaskPresetManager = class {
3008
3999
  }
3009
4000
  };
3010
4001
 
3011
- // src/shadow.ts
3012
- var import_fabric11 = require("fabric");
3013
-
3014
- // src/utils/color.ts
3015
- var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
3016
- function isCssColor(value, allowEmpty = false) {
3017
- if (typeof value !== "string") return false;
3018
- const trimmed = value.trim();
3019
- if (!trimmed) return allowEmpty;
3020
- return CSS_COLOR.test(trimmed);
3021
- }
3022
-
3023
- // src/shadow.ts
3024
- var DEFAULT_LAYER_SHADOW = {
3025
- enabled: false,
3026
- color: "#000000",
3027
- blur: 12,
3028
- offsetX: 6,
3029
- offsetY: 6
3030
- };
3031
- function readLayerShadow(object) {
3032
- const shadow = object.shadow;
3033
- if (!shadow || typeof shadow === "string") return { ...DEFAULT_LAYER_SHADOW };
3034
- return {
3035
- enabled: true,
3036
- color: typeof shadow.color === "string" ? shadow.color : DEFAULT_LAYER_SHADOW.color,
3037
- blur: shadow.blur ?? DEFAULT_LAYER_SHADOW.blur,
3038
- offsetX: shadow.offsetX ?? DEFAULT_LAYER_SHADOW.offsetX,
3039
- offsetY: shadow.offsetY ?? DEFAULT_LAYER_SHADOW.offsetY
3040
- };
3041
- }
3042
- function applyLayerShadow(object, config) {
3043
- const next = { ...readLayerShadow(object), ...config };
3044
- if (!next.enabled) {
3045
- object.set({ shadow: null });
3046
- return;
3047
- }
3048
- const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
3049
- object.set({
3050
- shadow: new import_fabric11.Shadow({
3051
- color,
3052
- blur: Math.max(0, next.blur),
3053
- offsetX: next.offsetX,
3054
- offsetY: next.offsetY
3055
- })
3056
- });
3057
- }
3058
-
3059
4002
  // src/selection-style.ts
3060
4003
  var DEFAULT_SELECTION_STYLE = {
3061
4004
  borderColor: "#c9a96e",
@@ -3161,7 +4104,7 @@ var UnitConverter = class {
3161
4104
  };
3162
4105
 
3163
4106
  // src/serialization.ts
3164
- var import_fabric12 = require("fabric");
4107
+ var import_fabric15 = require("fabric");
3165
4108
  var VERSION = "2.0.0";
3166
4109
  function serializeEditor(editor) {
3167
4110
  return {
@@ -3197,7 +4140,7 @@ async function deserializeEditor(editor, state) {
3197
4140
  }
3198
4141
  const staged = await Promise.all(
3199
4142
  state.layers.map(async (serialized) => {
3200
- const fabricObject = (await import_fabric12.util.enlivenObjects([serialized.fabricObject]))[0];
4143
+ const fabricObject = (await import_fabric15.util.enlivenObjects([serialized.fabricObject]))[0];
3201
4144
  if (!fabricObject) {
3202
4145
  const source = serialized.fabricObject.src;
3203
4146
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -3208,7 +4151,7 @@ async function deserializeEditor(editor, state) {
3208
4151
  return { serialized, fabricObject };
3209
4152
  })
3210
4153
  );
3211
- const stagedBackground = state.backgroundImage ? (await import_fabric12.util.enlivenObjects([state.backgroundImage]))[0] : null;
4154
+ const stagedBackground = state.backgroundImage ? (await import_fabric15.util.enlivenObjects([state.backgroundImage]))[0] : null;
3212
4155
  if (state.backgroundImage && !stagedBackground) {
3213
4156
  const source = state.backgroundImage.src;
3214
4157
  if (typeof source === "string" && source.startsWith("blob:")) {
@@ -3261,7 +4204,7 @@ function restoreLayer(editor, serialized, fabricObject) {
3261
4204
  }
3262
4205
 
3263
4206
  // src/export.ts
3264
- var import_fabric13 = require("fabric");
4207
+ var import_fabric16 = require("fabric");
3265
4208
 
3266
4209
  // src/displacement.ts
3267
4210
  var CHANNEL_INDEX = {
@@ -3355,7 +4298,7 @@ async function exportPNG(canvas, options = {}) {
3355
4298
  }
3356
4299
  async function exportIsolatedPNG(source, objects, options = {}) {
3357
4300
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
3358
- const canvas = new import_fabric13.StaticCanvas(element, {
4301
+ const canvas = new import_fabric16.StaticCanvas(element, {
3359
4302
  width: options.width ?? source.getWidth(),
3360
4303
  height: options.height ?? source.getHeight(),
3361
4304
  backgroundColor: options.backgroundColor || void 0
@@ -3385,7 +4328,7 @@ async function exportPrintArea(source, area, options = {}) {
3385
4328
  throw new Error("Print area does not overlap the canvas");
3386
4329
  }
3387
4330
  const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
3388
- const canvas = new import_fabric13.StaticCanvas(element, { width, height });
4331
+ const canvas = new import_fabric16.StaticCanvas(element, { width, height });
3389
4332
  try {
3390
4333
  const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
3391
4334
  if (clones.length) canvas.add(...clones);
@@ -3825,7 +4768,7 @@ var ProjectManager = class {
3825
4768
  };
3826
4769
 
3827
4770
  // src/mask.ts
3828
- var import_fabric14 = require("fabric");
4771
+ var import_fabric17 = require("fabric");
3829
4772
  var MaskRefinementError = class extends Error {
3830
4773
  constructor(code, message, cause) {
3831
4774
  super(message);
@@ -3857,7 +4800,7 @@ var MaskController = class {
3857
4800
  throw new Error("Mask dimensions must be positive integers");
3858
4801
  }
3859
4802
  const backing = this.makeCanvas(width, height);
3860
- const image = new import_fabric14.FabricImage(backing, {
4803
+ const image = new import_fabric17.FabricImage(backing, {
3861
4804
  left: 0,
3862
4805
  top: 0,
3863
4806
  originX: "left",
@@ -4050,8 +4993,8 @@ var MaskController = class {
4050
4993
  point.y,
4051
4994
  radius
4052
4995
  );
4053
- const color = brush.mode === "subtract" ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
4054
- gradient.addColorStop(0, color);
4996
+ const color2 = brush.mode === "subtract" ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
4997
+ gradient.addColorStop(0, color2);
4055
4998
  gradient.addColorStop(1, "rgba(255,255,255,0)");
4056
4999
  context.fillStyle = gradient;
4057
5000
  context.beginPath();
@@ -4077,16 +5020,133 @@ var MaskController = class {
4077
5020
  if (!layer || layer.type !== "mask") throw new Error(`Mask layer not found: ${layerId}`);
4078
5021
  return layer;
4079
5022
  }
4080
- assertActive() {
4081
- if (this.disposed) throw new Error("Mask controller has been disposed");
5023
+ assertActive() {
5024
+ if (this.disposed) throw new Error("Mask controller has been disposed");
5025
+ }
5026
+ };
5027
+
5028
+ // src/masks/manager.ts
5029
+ var import_fabric24 = require("fabric");
5030
+
5031
+ // src/masks/compose.ts
5032
+ var import_fabric19 = require("fabric");
5033
+
5034
+ // src/gradient.ts
5035
+ var import_fabric18 = require("fabric");
5036
+ var DEFAULT_GRADIENT_CONFIG = {
5037
+ kind: "linear",
5038
+ angle: 90,
5039
+ center: { x: 0.5, y: 0.5 },
5040
+ radius: 0.5,
5041
+ stops: [
5042
+ { color: "#ffffff", opacity: 1, position: 0 },
5043
+ { color: "#000000", opacity: 1, position: 1 }
5044
+ ]
5045
+ };
5046
+ var MIN_RADIUS = 1e-3;
5047
+ var MAX_GRADIENT_STOPS = 256;
5048
+ var FALLBACK_STOP_COLOR = DEFAULT_GRADIENT_CONFIG.stops[0].color;
5049
+ function linearCoords(angle, box) {
5050
+ const radians = angle * Math.PI / 180;
5051
+ const sin = Math.sin(radians);
5052
+ const cos = Math.cos(radians);
5053
+ const vx = box.width * sin;
5054
+ const vy = -box.height * cos;
5055
+ const length = Math.abs(box.width * sin) + Math.abs(box.height * cos);
5056
+ const scale = length / (vx * vx + vy * vy || 1);
5057
+ return {
5058
+ x1: 0.5 - scale * vx / 2,
5059
+ y1: 0.5 - scale * vy / 2,
5060
+ x2: 0.5 + scale * vx / 2,
5061
+ y2: 0.5 + scale * vy / 2
5062
+ };
5063
+ }
5064
+ function angleFromCoords(coords, box) {
5065
+ const dx = (coords.x2 - coords.x1) / (box.width || 1);
5066
+ const dy = (coords.y2 - coords.y1) / (box.height || 1);
5067
+ const degrees = Math.atan2(dx, -dy) * 180 / Math.PI;
5068
+ return (degrees + 360) % 360;
5069
+ }
5070
+ function stopColor(stop) {
5071
+ const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
5072
+ const color2 = new import_fabric18.Color(source);
5073
+ const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
5074
+ color2.setAlpha(clamp(opacity, 0, 1));
5075
+ return color2.toRgba();
5076
+ }
5077
+ function toFabricGradient(config, box) {
5078
+ const colorStops = [...config.stops].sort((a, b) => a.position - b.position).slice(0, MAX_GRADIENT_STOPS).map((stop) => {
5079
+ const position = Number.isFinite(stop.position) ? stop.position : 0;
5080
+ return { offset: clamp(position, 0, 1), color: stopColor(stop) };
5081
+ });
5082
+ if (config.kind === "radial") {
5083
+ const radius = Math.max(MIN_RADIUS, config.radius);
5084
+ return new import_fabric18.Gradient({
5085
+ type: "radial",
5086
+ gradientUnits: "percentage",
5087
+ coords: {
5088
+ x1: config.center.x,
5089
+ y1: config.center.y,
5090
+ r1: 0,
5091
+ x2: config.center.x,
5092
+ y2: config.center.y,
5093
+ r2: radius
5094
+ },
5095
+ colorStops
5096
+ });
5097
+ }
5098
+ return new import_fabric18.Gradient({
5099
+ type: "linear",
5100
+ gradientUnits: "percentage",
5101
+ coords: linearCoords(config.angle, box),
5102
+ colorStops
5103
+ });
5104
+ }
5105
+ function toFabricMaskGradient(config, box) {
5106
+ const gradient = toFabricGradient(config, box);
5107
+ gradient.colorStops = gradient.colorStops.map((stop) => {
5108
+ const color2 = new import_fabric18.Color(stop.color);
5109
+ const [red, green, blue] = color2.getSource();
5110
+ const luminance = (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255;
5111
+ color2.setSource([255, 255, 255, clamp(luminance * color2.getAlpha(), 0, 1)]);
5112
+ return { offset: stop.offset, color: color2.toRgba() };
5113
+ });
5114
+ return gradient;
5115
+ }
5116
+ function readGradientConfig(object) {
5117
+ const fill = object.fill;
5118
+ if (!fill || typeof fill === "string" || !(fill instanceof import_fabric18.Gradient)) return null;
5119
+ const stops = fill.colorStops.map((stop) => {
5120
+ const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
5121
+ const color2 = new import_fabric18.Color(source);
5122
+ const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
5123
+ return {
5124
+ color: `#${color2.toHex().toLowerCase()}`,
5125
+ opacity: color2.getAlpha(),
5126
+ position: clamp(offset, 0, 1)
5127
+ };
5128
+ });
5129
+ if (fill.type === "radial") {
5130
+ const { x1, y1, r2 } = fill.coords;
5131
+ return {
5132
+ kind: "radial",
5133
+ angle: DEFAULT_GRADIENT_CONFIG.angle,
5134
+ center: { x: x1, y: y1 },
5135
+ radius: r2,
5136
+ stops
5137
+ };
4082
5138
  }
4083
- };
4084
-
4085
- // src/masks/manager.ts
4086
- var import_fabric20 = require("fabric");
5139
+ const box = { width: object.width || 1, height: object.height || 1 };
5140
+ return {
5141
+ kind: "linear",
5142
+ angle: angleFromCoords(fill.coords, box),
5143
+ center: { ...DEFAULT_GRADIENT_CONFIG.center },
5144
+ radius: DEFAULT_GRADIENT_CONFIG.radius,
5145
+ stops
5146
+ };
5147
+ }
4087
5148
 
4088
5149
  // src/masks/compose.ts
4089
- var import_fabric15 = require("fabric");
4090
5150
  var MODE_OPERATION = {
4091
5151
  add: "source-over",
4092
5152
  subtract: "destination-out",
@@ -4096,7 +5156,7 @@ function neutralize(child) {
4096
5156
  child.set({ opacity: 0, globalCompositeOperation: "source-over" });
4097
5157
  }
4098
5158
  function baseRect(box) {
4099
- return new import_fabric15.Rect({
5159
+ return new import_fabric19.Rect({
4100
5160
  left: box.left,
4101
5161
  top: box.top,
4102
5162
  width: Math.max(1, box.width),
@@ -4116,6 +5176,14 @@ function composeMaskGroup(children, entries, options) {
4116
5176
  neutralize(child);
4117
5177
  return;
4118
5178
  }
5179
+ if (entry.gradient) {
5180
+ child.set({
5181
+ fill: toFabricMaskGradient(entry.gradient, {
5182
+ width: Math.max(1, child.width ?? 1),
5183
+ height: Math.max(1, child.height ?? 1)
5184
+ })
5185
+ });
5186
+ }
4119
5187
  child.set({
4120
5188
  opacity: entry.opacity,
4121
5189
  globalCompositeOperation: MODE_OPERATION[entry.mode] ?? "source-over"
@@ -4123,7 +5191,7 @@ function composeMaskGroup(children, entries, options) {
4123
5191
  });
4124
5192
  const first = entries.find((entry) => entry.visible);
4125
5193
  const withBase = !first || first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
4126
- return new import_fabric15.Group(withBase, {
5194
+ return new import_fabric19.Group(withBase, {
4127
5195
  absolutePositioned: options.absolute,
4128
5196
  // Cached, so the children's compositing operations resolve against each
4129
5197
  // other instead of against the page underneath the mask.
@@ -4137,11 +5205,105 @@ function needsAbsoluteSpace(entries) {
4137
5205
  }
4138
5206
 
4139
5207
  // src/masks/edit.ts
4140
- var import_fabric16 = require("fabric");
5208
+ var import_fabric20 = require("fabric");
5209
+
5210
+ // src/masks/edit-viewport.ts
5211
+ var MIN_PADDING = 48;
5212
+ var MAX_PADDING = 512;
5213
+ var PADDING_FRACTION = 0.35;
5214
+ var OUTSIDE_SHADE = "rgba(15, 15, 15, 0.55)";
5215
+ var MaskEditViewport = class {
5216
+ constructor(canvas) {
5217
+ this.canvas = canvas;
5218
+ }
5219
+ canvas;
5220
+ state = null;
5221
+ open(handle) {
5222
+ if (this.state) return;
5223
+ const width = this.canvas.getWidth();
5224
+ const height = this.canvas.getHeight();
5225
+ const base = Math.min(
5226
+ MAX_PADDING,
5227
+ Math.max(MIN_PADDING, Math.max(width, height) * PADDING_FRACTION)
5228
+ );
5229
+ handle.setCoords();
5230
+ const bounds = handle.getBoundingRect();
5231
+ const left = Math.min(MAX_PADDING, Math.max(base, -bounds.left + MIN_PADDING));
5232
+ const top = Math.min(MAX_PADDING, Math.max(base, -bounds.top + MIN_PADDING));
5233
+ const right = Math.min(
5234
+ MAX_PADDING,
5235
+ Math.max(base, bounds.left + bounds.width - width + MIN_PADDING)
5236
+ );
5237
+ const bottom = Math.min(
5238
+ MAX_PADDING,
5239
+ Math.max(base, bounds.top + bounds.height - height + MIN_PADDING)
5240
+ );
5241
+ const viewport = [...this.canvas.viewportTransform];
5242
+ const wrapper = this.canvas.wrapperEl;
5243
+ this.state = {
5244
+ width,
5245
+ height,
5246
+ left,
5247
+ top,
5248
+ viewport,
5249
+ wrapperCss: wrapper.style.cssText
5250
+ };
5251
+ this.canvas.setDimensions({ width: width + left + right, height: height + top + bottom });
5252
+ const paddedViewport = [...viewport];
5253
+ paddedViewport[4] += left;
5254
+ paddedViewport[5] += top;
5255
+ this.canvas.setViewportTransform(paddedViewport);
5256
+ wrapper.style.position = "absolute";
5257
+ wrapper.style.left = `${-left}px`;
5258
+ wrapper.style.top = `${-top}px`;
5259
+ wrapper.style.zIndex = "1";
5260
+ this.canvas.on("after:render", this.onAfterRender);
5261
+ this.canvas.calcOffset();
5262
+ this.canvas.requestRenderAll();
5263
+ }
5264
+ close() {
5265
+ const state = this.state;
5266
+ if (!state) return;
5267
+ this.canvas.off("after:render", this.onAfterRender);
5268
+ this.canvas.setViewportTransform(state.viewport);
5269
+ this.canvas.setDimensions({ width: state.width, height: state.height });
5270
+ this.canvas.wrapperEl.style.cssText = state.wrapperCss;
5271
+ this.canvas.calcOffset();
5272
+ this.state = null;
5273
+ }
5274
+ /** Save with real document dimensions, then reopen the same editing view. */
5275
+ checkpoint(handle, save) {
5276
+ this.close();
5277
+ try {
5278
+ save();
5279
+ } finally {
5280
+ this.open(handle);
5281
+ }
5282
+ }
5283
+ onAfterRender = ({ ctx }) => {
5284
+ const state = this.state;
5285
+ if (!state || ctx !== this.canvas.getContext()) return;
5286
+ const fullWidth = this.canvas.getWidth();
5287
+ const fullHeight = this.canvas.getHeight();
5288
+ const right = state.left + state.width;
5289
+ const bottom = state.top + state.height;
5290
+ ctx.save();
5291
+ ctx.fillStyle = OUTSIDE_SHADE;
5292
+ ctx.fillRect(0, 0, fullWidth, state.top);
5293
+ ctx.fillRect(0, bottom, fullWidth, fullHeight - bottom);
5294
+ ctx.fillRect(0, state.top, state.left, state.height);
5295
+ ctx.fillRect(right, state.top, fullWidth - right, state.height);
5296
+ ctx.restore();
5297
+ this.canvas.drawControls(ctx);
5298
+ };
5299
+ };
5300
+
5301
+ // src/masks/edit.ts
4141
5302
  var MaskEditController = class {
4142
5303
  constructor(canvas, onCommit) {
4143
5304
  this.canvas = canvas;
4144
5305
  this.onCommit = onCommit;
5306
+ this.viewport = new MaskEditViewport(canvas);
4145
5307
  }
4146
5308
  canvas;
4147
5309
  onCommit;
@@ -4149,6 +5311,7 @@ var MaskEditController = class {
4149
5311
  editing = null;
4150
5312
  child = null;
4151
5313
  group = null;
5314
+ viewport;
4152
5315
  active() {
4153
5316
  return this.editing;
4154
5317
  }
@@ -4179,6 +5342,7 @@ var MaskEditController = class {
4179
5342
  this.group = group;
4180
5343
  this.canvas.add(handle);
4181
5344
  this.canvas.setActiveObject(handle);
5345
+ this.viewport.open(handle);
4182
5346
  this.canvas.on("object:moving", this.onTransform);
4183
5347
  this.canvas.on("object:scaling", this.onTransform);
4184
5348
  this.canvas.on("object:rotating", this.onTransform);
@@ -4193,6 +5357,7 @@ var MaskEditController = class {
4193
5357
  this.canvas.off("object:scaling", this.onTransform);
4194
5358
  this.canvas.off("object:rotating", this.onTransform);
4195
5359
  this.canvas.off("object:modified", this.onModified);
5360
+ this.viewport.close();
4196
5361
  if (this.canvas.getActiveObject() === this.handle) this.canvas.discardActiveObject();
4197
5362
  this.canvas.remove(this.handle);
4198
5363
  this.handle = null;
@@ -4211,15 +5376,15 @@ var MaskEditController = class {
4211
5376
  onModified = (event) => {
4212
5377
  if (!this.handle || event.target !== this.handle) return;
4213
5378
  this.write();
4214
- this.onCommit();
5379
+ this.viewport.checkpoint(this.handle, this.onCommit);
4215
5380
  };
4216
5381
  /** Handle transform (canvas space) → clip child transform (group-relative). */
4217
5382
  write() {
4218
5383
  if (!this.handle || !this.child || !this.group) return;
4219
5384
  applyMatrix(
4220
5385
  this.child,
4221
- import_fabric16.util.multiplyTransformMatrices(
4222
- import_fabric16.util.invertTransform(matrixOf(this.group)),
5386
+ import_fabric20.util.multiplyTransformMatrices(
5387
+ import_fabric20.util.invertTransform(matrixOf(this.group)),
4223
5388
  matrixOf(this.handle)
4224
5389
  )
4225
5390
  );
@@ -4230,15 +5395,15 @@ var MaskEditController = class {
4230
5395
  };
4231
5396
 
4232
5397
  // src/masks/store.ts
4233
- var import_fabric19 = require("fabric");
5398
+ var import_fabric23 = require("fabric");
4234
5399
 
4235
5400
  // src/masks/host.ts
4236
- var import_fabric17 = require("fabric");
5401
+ var import_fabric21 = require("fabric");
4237
5402
  function findCanvasHost(layers) {
4238
5403
  return layers.getAll().find((layer) => layer.meta.canvasMask);
4239
5404
  }
4240
5405
  function createCanvasHost(canvas, layers) {
4241
- const rect = new import_fabric17.Rect({
5406
+ const rect = new import_fabric21.Rect({
4242
5407
  left: 0,
4243
5408
  top: 0,
4244
5409
  width: canvas.getWidth(),
@@ -4287,9 +5452,9 @@ function hostBoxOf(canvas, host, absolute) {
4287
5452
  }
4288
5453
 
4289
5454
  // src/masks/install.ts
4290
- var import_fabric18 = require("fabric");
5455
+ var import_fabric22 = require("fabric");
4291
5456
  function convertSpace(host, sources, absolute) {
4292
- const wasAbsolute = host.clipPath instanceof import_fabric18.Group ? host.clipPath.absolutePositioned : absolute;
5457
+ const wasAbsolute = host.clipPath instanceof import_fabric22.Group ? host.clipPath.absolutePositioned : absolute;
4293
5458
  if (absolute === wasAbsolute) return;
4294
5459
  for (const source of sources) {
4295
5460
  if (absolute) toCanvasSpace(source, host);
@@ -4342,6 +5507,29 @@ var MaskStackStore = class {
4342
5507
  get(target, maskId) {
4343
5508
  return this.list(target).find((entry) => entry.id === maskId);
4344
5509
  }
5510
+ /** Repaint a gradient mask without replacing or re-fitting its geometry. */
5511
+ setGradient(target, maskId, gradient, options = {}) {
5512
+ const host = this.host(target);
5513
+ if (!host) return false;
5514
+ const entries = [...this.list(target)];
5515
+ const index = entries.findIndex((entry) => entry.id === maskId);
5516
+ if (index === -1) return false;
5517
+ const sources = this.unwrap(target, host);
5518
+ const source = sources[index];
5519
+ if (!source) return false;
5520
+ source.set({
5521
+ fill: toFabricGradient(gradient, {
5522
+ width: Math.max(1, source.width ?? 1),
5523
+ height: Math.max(1, source.height ?? 1)
5524
+ })
5525
+ });
5526
+ entries[index] = {
5527
+ ...entries[index],
5528
+ ...options.meta ? { meta: options.meta } : {}
5529
+ };
5530
+ this.commit(target, host, entries, sources, options.save ?? true);
5531
+ return true;
5532
+ }
4345
5533
  /** Every target that currently carries at least one mask. */
4346
5534
  targets() {
4347
5535
  return this.layers.getAll().filter((layer) => (layer.meta.maskStack ?? []).length > 0).map((layer) => layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
@@ -4402,14 +5590,14 @@ var MaskStackStore = class {
4402
5590
  const entries = this.list(target);
4403
5591
  const meta = this.hostLayer(target)?.meta;
4404
5592
  if (entries.length === 0 && this.holdsBoxClip(meta)) return [];
4405
- if (entries.length === 0 || !(clip instanceof import_fabric19.Group)) {
5593
+ if (entries.length === 0 || !(clip instanceof import_fabric23.Group)) {
4406
5594
  const source = asObject(clip);
4407
5595
  if (meta?.overflow === "hidden") source.clipPath = void 0;
4408
- return [source];
5596
+ return restoreGradientPaint(entries, [source]);
4409
5597
  }
4410
5598
  const children = unwrapGroup(clip);
4411
5599
  const extra = children.length - entries.length;
4412
- return extra > 0 ? children.slice(extra) : children;
5600
+ return restoreGradientPaint(entries, extra > 0 ? children.slice(extra) : children);
4413
5601
  }
4414
5602
  /**
4415
5603
  * Entries for a stack, adopting a pre-stack clip as the first one. Without
@@ -4442,7 +5630,8 @@ var MaskStackStore = class {
4442
5630
  if (!layer) return;
4443
5631
  const absolute = forceAbsolute || target === CANVAS_MASK_TARGET || needsAbsoluteSpace(entries);
4444
5632
  convertSpace(host, sources, absolute);
4445
- const next = withRelativeTransforms(entries, sources, host, absolute);
5633
+ const painted = captureGradientPaint(entries, sources);
5634
+ const next = withRelativeTransforms(painted, sources, host, absolute);
4446
5635
  installClip(host, next, sources, this.hostBox(target, absolute), absolute);
4447
5636
  if (next.length === 0) {
4448
5637
  delete layer.meta.maskStack;
@@ -4456,12 +5645,41 @@ var MaskStackStore = class {
4456
5645
  if (save) this.history.save();
4457
5646
  }
4458
5647
  };
5648
+ function restoreGradientPaint(entries, sources) {
5649
+ entries.forEach((entry, index) => {
5650
+ const source = sources[index];
5651
+ if (!source || !entry.gradient) return;
5652
+ const box = {
5653
+ width: Math.max(1, source.width ?? 1),
5654
+ height: Math.max(1, source.height ?? 1)
5655
+ };
5656
+ const fill = source.fill;
5657
+ const rendered = toFabricMaskGradient(entry.gradient, box);
5658
+ if (fill instanceof import_fabric23.Gradient && JSON.stringify(fill.toObject()) !== JSON.stringify(rendered.toObject())) {
5659
+ return;
5660
+ }
5661
+ source.set({
5662
+ fill: toFabricGradient(entry.gradient, box)
5663
+ });
5664
+ });
5665
+ return sources;
5666
+ }
5667
+ function captureGradientPaint(entries, sources) {
5668
+ return entries.map((entry, index) => {
5669
+ const gradient = sources[index] ? readGradientConfig(sources[index]) : null;
5670
+ if (gradient) return { ...entry, gradient };
5671
+ if (!entry.gradient) return entry;
5672
+ const next = { ...entry };
5673
+ delete next.gradient;
5674
+ return next;
5675
+ });
5676
+ }
4459
5677
 
4460
5678
  // src/masks/manager.ts
4461
5679
  var LayerMaskManager = class extends MaskStackStore {
4462
5680
  // Committing mid-drag would recompose the group the handle writes into and
4463
5681
  // leave it pointing at a discarded object; the geometry is settled on endEdit.
4464
- edits = new MaskEditController(this.canvas, () => this.history.save());
5682
+ edits = new MaskEditController(this.canvas, () => this.history.saveImmediate());
4465
5683
  onLayersChanged = () => this.pin();
4466
5684
  // Selecting a layer means the user has moved on from the mask they had open;
4467
5685
  // leaving both selected would show mask controls for an unrelated layer.
@@ -4506,7 +5724,7 @@ var LayerMaskManager = class extends MaskStackStore {
4506
5724
  if (index === -1) return false;
4507
5725
  this.endEdit(false);
4508
5726
  this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
4509
- const group = host.clipPath instanceof import_fabric20.Group ? host.clipPath : null;
5727
+ const group = host.clipPath instanceof import_fabric24.Group ? host.clipPath : null;
4510
5728
  if (!group) return false;
4511
5729
  const children = group.getObjects();
4512
5730
  const child = children[children.length - entries.length + index];
@@ -4848,110 +6066,6 @@ function layerTypeOf(object) {
4848
6066
  return "shape";
4849
6067
  }
4850
6068
 
4851
- // src/gradient.ts
4852
- var import_fabric21 = require("fabric");
4853
- var DEFAULT_GRADIENT_CONFIG = {
4854
- kind: "linear",
4855
- angle: 90,
4856
- center: { x: 0.5, y: 0.5 },
4857
- radius: 0.5,
4858
- stops: [
4859
- { color: "#ffffff", opacity: 1, position: 0 },
4860
- { color: "#000000", opacity: 1, position: 1 }
4861
- ]
4862
- };
4863
- var MIN_RADIUS = 1e-3;
4864
- var MAX_GRADIENT_STOPS = 256;
4865
- var FALLBACK_STOP_COLOR = DEFAULT_GRADIENT_CONFIG.stops[0].color;
4866
- function linearCoords(angle, box) {
4867
- const radians = angle * Math.PI / 180;
4868
- const sin = Math.sin(radians);
4869
- const cos = Math.cos(radians);
4870
- const vx = box.width * sin;
4871
- const vy = -box.height * cos;
4872
- const length = Math.abs(box.width * sin) + Math.abs(box.height * cos);
4873
- const scale = length / (vx * vx + vy * vy || 1);
4874
- return {
4875
- x1: 0.5 - scale * vx / 2,
4876
- y1: 0.5 - scale * vy / 2,
4877
- x2: 0.5 + scale * vx / 2,
4878
- y2: 0.5 + scale * vy / 2
4879
- };
4880
- }
4881
- function angleFromCoords(coords, box) {
4882
- const dx = (coords.x2 - coords.x1) / (box.width || 1);
4883
- const dy = (coords.y2 - coords.y1) / (box.height || 1);
4884
- const degrees = Math.atan2(dx, -dy) * 180 / Math.PI;
4885
- return (degrees + 360) % 360;
4886
- }
4887
- function stopColor(stop) {
4888
- const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4889
- const color = new import_fabric21.Color(source);
4890
- const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
4891
- color.setAlpha(clamp(opacity, 0, 1));
4892
- return color.toRgba();
4893
- }
4894
- function toFabricGradient(config, box) {
4895
- const colorStops = [...config.stops].sort((a, b) => a.position - b.position).slice(0, MAX_GRADIENT_STOPS).map((stop) => {
4896
- const position = Number.isFinite(stop.position) ? stop.position : 0;
4897
- return { offset: clamp(position, 0, 1), color: stopColor(stop) };
4898
- });
4899
- if (config.kind === "radial") {
4900
- const radius = Math.max(MIN_RADIUS, config.radius);
4901
- return new import_fabric21.Gradient({
4902
- type: "radial",
4903
- gradientUnits: "percentage",
4904
- coords: {
4905
- x1: config.center.x,
4906
- y1: config.center.y,
4907
- r1: 0,
4908
- x2: config.center.x,
4909
- y2: config.center.y,
4910
- r2: radius
4911
- },
4912
- colorStops
4913
- });
4914
- }
4915
- return new import_fabric21.Gradient({
4916
- type: "linear",
4917
- gradientUnits: "percentage",
4918
- coords: linearCoords(config.angle, box),
4919
- colorStops
4920
- });
4921
- }
4922
- function readGradientConfig(object) {
4923
- const fill = object.fill;
4924
- if (!fill || typeof fill === "string" || !(fill instanceof import_fabric21.Gradient)) return null;
4925
- const stops = fill.colorStops.map((stop) => {
4926
- const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4927
- const color = new import_fabric21.Color(source);
4928
- const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
4929
- return {
4930
- color: `#${color.toHex().toLowerCase()}`,
4931
- opacity: color.getAlpha(),
4932
- position: clamp(offset, 0, 1)
4933
- };
4934
- });
4935
- if (fill.type === "radial") {
4936
- const { x1, y1, r2 } = fill.coords;
4937
- return {
4938
- kind: "radial",
4939
- angle: DEFAULT_GRADIENT_CONFIG.angle,
4940
- center: { x: x1, y: y1 },
4941
- radius: r2,
4942
- stops
4943
- };
4944
- }
4945
- const box = { width: object.width || 1, height: object.height || 1 };
4946
- return {
4947
- kind: "linear",
4948
- angle: angleFromCoords(fill.coords, box),
4949
- center: { ...DEFAULT_GRADIENT_CONFIG.center },
4950
- radius: DEFAULT_GRADIENT_CONFIG.radius,
4951
- stops
4952
- };
4953
- }
4954
-
4955
6069
  // src/editor.ts
4956
6070
  var MIN_ZOOM = 0.01;
4957
6071
  var MAX_ZOOM = 8;
@@ -4967,11 +6081,17 @@ var CanvasEditor = class {
4967
6081
  snapping;
4968
6082
  crop;
4969
6083
  patterns;
6084
+ /** Parametric shape layers: `meta.shape` and in-place regeneration. */
6085
+ shapes;
4970
6086
  curves;
4971
6087
  wrap;
4972
6088
  maskPresets;
6089
+ /** Owner of every image layer's filter array (D5). */
6090
+ imageEffects;
4973
6091
  /** Stacked boolean masks, per layer and for the design as a whole. */
4974
6092
  layerMasks;
6093
+ /** One effect per text or shape layer (D6). */
6094
+ effects;
4975
6095
  fonts;
4976
6096
  licensing;
4977
6097
  pages;
@@ -4981,6 +6101,8 @@ var CanvasEditor = class {
4981
6101
  zoomLevel = 1;
4982
6102
  selectionStyle = { ...DEFAULT_SELECTION_STYLE };
4983
6103
  mockup = null;
6104
+ /** Alt was held when a drag began on this layer; the drop leaves a copy behind. */
6105
+ altDrag = null;
4984
6106
  // The design's configured background. The live canvas background is forced
4985
6107
  // transparent while a mockup preview is shown, so this is the source of truth
4986
6108
  // for serialization and export — not the (possibly transient) canvas value.
@@ -4988,6 +6110,7 @@ var CanvasEditor = class {
4988
6110
  designBackgroundImage = null;
4989
6111
  backgroundImageOptions = null;
4990
6112
  constructor(canvasElement, config) {
6113
+ (0, import_canvas_editor_effects2.registerEffects)(fabricModule);
4991
6114
  this.events = new EventEmitter();
4992
6115
  this.fonts = new FontRegistry();
4993
6116
  config.fonts?.forEach((font) => this.fonts.register(font));
@@ -4996,7 +6119,7 @@ var CanvasEditor = class {
4996
6119
  const widthPx = this.units.toPixels(config.width);
4997
6120
  const heightPx = this.units.toPixels(config.height);
4998
6121
  this.designBackground = config.backgroundColor ?? "#ffffff";
4999
- this.canvas = new import_fabric22.Canvas(canvasElement, {
6122
+ this.canvas = new import_fabric25.Canvas(canvasElement, {
5000
6123
  width: widthPx,
5001
6124
  height: heightPx,
5002
6125
  backgroundColor: this.designBackground,
@@ -5022,6 +6145,15 @@ var CanvasEditor = class {
5022
6145
  this.wrap = new TextWrapManager(this.canvas, this.layers, this.history, this.events);
5023
6146
  this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
5024
6147
  this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
6148
+ this.effects = new LayerEffectsManager(this.canvas, this.layers, this.history, this.events);
6149
+ this.imageEffects = new ImageEffectsManager(
6150
+ this.canvas,
6151
+ this.layers,
6152
+ this.history,
6153
+ this.events
6154
+ );
6155
+ this.shapes = new ShapeManager(this.canvas, this.layers, this.history, this.events);
6156
+ config.shapes?.forEach((plugin) => this.shapes.register(plugin));
5025
6157
  this.setupCanvasEvents();
5026
6158
  this.refreshSelectionStyle();
5027
6159
  this.history.saveImmediate();
@@ -5031,7 +6163,7 @@ var CanvasEditor = class {
5031
6163
  // ─── Layer Operations ────────────────────────────────
5032
6164
  async addImage(url, options) {
5033
6165
  try {
5034
- const img = await import_fabric22.FabricImage.fromURL(
6166
+ const img = await import_fabric25.FabricImage.fromURL(
5035
6167
  url,
5036
6168
  {},
5037
6169
  { originX: "left", originY: "top", ...options }
@@ -5046,7 +6178,7 @@ var CanvasEditor = class {
5046
6178
  }
5047
6179
  }
5048
6180
  /** Replace an image source without changing its layer identity or visual transform. */
5049
- async replaceImageSource(layerId, url) {
6181
+ async replaceImageSource(layerId, url, options = {}) {
5050
6182
  const layer = this.layers.get(layerId);
5051
6183
  if (!layer || layer.type !== "image" && layer.type !== "mask") {
5052
6184
  throw new Error(`Image or mask layer not found: ${layerId}`);
@@ -5057,7 +6189,11 @@ var CanvasEditor = class {
5057
6189
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
5058
6190
  const previous = layer.fabricObject;
5059
6191
  try {
5060
- const replacement = await import_fabric22.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
6192
+ const replacement = await import_fabric25.FabricImage.fromURL(
6193
+ url,
6194
+ { crossOrigin: options.crossOrigin },
6195
+ { originX: "left", originY: "top" }
6196
+ );
5061
6197
  replacement.set({
5062
6198
  left: previous.left,
5063
6199
  top: previous.top,
@@ -5079,14 +6215,28 @@ var CanvasEditor = class {
5079
6215
  replacement.applyFilters();
5080
6216
  replacement.setCoords();
5081
6217
  this.layers.replaceObject(layerId, replacement);
6218
+ this.imageEffects.refresh(layerId);
5082
6219
  return layer;
5083
6220
  } catch (error) {
5084
6221
  this.events.emit("error", { message: "Failed to replace image source", error });
5085
6222
  throw error;
5086
6223
  }
5087
6224
  }
6225
+ /**
6226
+ * The class the registry holds for a stock name.
6227
+ *
6228
+ * `registerEffects` (run in the constructor) overrides the stock names, but
6229
+ * registration only decides what an ENLIVEN produces — a `new Textbox(...)`
6230
+ * here would still be the stock class, which carries no `effect` in its
6231
+ * `customProperties` and so could never serialize one. Authoring and
6232
+ * restoring have to yield the same class.
6233
+ */
6234
+ registered(name, fallback) {
6235
+ return import_fabric25.classRegistry.getClass(name) ?? fallback;
6236
+ }
5088
6237
  addText(text, options) {
5089
- const textbox = new import_fabric22.Textbox(text, {
6238
+ const TextboxClass = this.registered("Textbox", import_fabric25.Textbox);
6239
+ const textbox = new TextboxClass(text, {
5090
6240
  fontSize: 32,
5091
6241
  fontFamily: "Arial",
5092
6242
  fill: "#000000",
@@ -5104,6 +6254,15 @@ var CanvasEditor = class {
5104
6254
  addShape(plugin, options) {
5105
6255
  const obj = plugin.create(options);
5106
6256
  const layer = this.layers.add("shape", obj, plugin.name);
6257
+ if (plugin.params) {
6258
+ this.shapes.register(plugin);
6259
+ const overrides = options?.params ?? {};
6260
+ layer.meta.shape = {
6261
+ id: plugin.name,
6262
+ params: resolveShapeParams(plugin, overrides),
6263
+ version: 1
6264
+ };
6265
+ }
5107
6266
  this.layers.select(layer.id);
5108
6267
  this.history.save();
5109
6268
  return layer;
@@ -5115,7 +6274,8 @@ var CanvasEditor = class {
5115
6274
  addGradient(config = DEFAULT_GRADIENT_CONFIG, box) {
5116
6275
  const width = box?.width ?? this.canvas.getWidth();
5117
6276
  const height = box?.height ?? this.canvas.getHeight();
5118
- const object = new import_fabric22.Rect({
6277
+ const RectClass = this.registered("Rect", import_fabric25.Rect);
6278
+ const object = new RectClass({
5119
6279
  left: box?.left ?? 0,
5120
6280
  top: box?.top ?? 0,
5121
6281
  originX: "left",
@@ -5244,6 +6404,49 @@ var CanvasEditor = class {
5244
6404
  this.canvas.requestRenderAll();
5245
6405
  this.history.save();
5246
6406
  }
6407
+ // ─── Align ──────────────────────────────────────────
6408
+ /** What the user is acting on: the proxy where a layer has one. */
6409
+ selectedTargets() {
6410
+ return this.canvas.getActiveObjects().filter((object) => this.layers.findByObject(object) !== void 0);
6411
+ }
6412
+ afterMove(objects) {
6413
+ relayoutSelection(this.canvas.getActiveObject());
6414
+ for (const object of objects) {
6415
+ const layer = this.layers.findByObject(object);
6416
+ if (!layer) continue;
6417
+ if (layer.renderProxy === object) this.patterns.commitProxy(layer.id);
6418
+ this.events.emit("layer:modified", { layerId: layer.id });
6419
+ }
6420
+ this.canvas.requestRenderAll();
6421
+ this.history.save();
6422
+ }
6423
+ /** One layer aligns to the canvas; several align to their common box. One history entry. */
6424
+ alignLayers(target) {
6425
+ const objects = this.selectedTargets();
6426
+ if (objects.length === 0) return false;
6427
+ const frame = objects.length === 1 ? canvasBox(this.canvas.getWidth(), this.canvas.getHeight()) : unionBox(objects.map((o) => (o.setCoords(), o.getBoundingRect())));
6428
+ this.history.beginTransaction();
6429
+ try {
6430
+ alignObjects(objects, target, frame);
6431
+ this.afterMove(objects);
6432
+ } finally {
6433
+ this.history.endTransaction();
6434
+ }
6435
+ return true;
6436
+ }
6437
+ /** Equal gaps along an axis. Needs three selected; returns false otherwise. */
6438
+ distributeLayers(axis) {
6439
+ const objects = this.selectedTargets();
6440
+ if (objects.length < 3) return false;
6441
+ this.history.beginTransaction();
6442
+ try {
6443
+ distributeObjects(objects, axis);
6444
+ this.afterMove(objects);
6445
+ } finally {
6446
+ this.history.endTransaction();
6447
+ }
6448
+ return true;
6449
+ }
5247
6450
  /** Clone a layer (offset slightly) and select the copy. */
5248
6451
  async duplicateLayer(id) {
5249
6452
  const layer = this.layers.get(id);
@@ -5268,24 +6471,19 @@ var CanvasEditor = class {
5268
6471
  this.history.save();
5269
6472
  return copy;
5270
6473
  }
5271
- applyImageAdjustments(layerId, adjustments) {
5272
- const layer = this.layers.get(layerId);
5273
- if (!layer || layer.type !== "image") return false;
5274
- const image = layer.fabricObject;
5275
- const previous = layer.meta.imageAdjustments ?? {};
5276
- const next = { ...previous, ...adjustments };
5277
- const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
5278
- image.filters = [
5279
- new import_fabric22.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
5280
- new import_fabric22.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
5281
- new import_fabric22.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
5282
- new import_fabric22.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
5283
- ];
5284
- layer.meta.imageAdjustments = next;
5285
- image.applyFilters();
5286
- this.canvas.requestRenderAll();
5287
- this.history.save();
5288
- return true;
6474
+ /** The drag moved the original; a copy takes the start position, just beneath it. */
6475
+ async leaveCopyBehind(layer, transform) {
6476
+ const now = import_fabric25.util.saveObjectTransform(layer.fabricObject);
6477
+ if (now.left === transform.left && now.top === transform.top) return;
6478
+ await this.history.transaction(async () => {
6479
+ const copy = await this.duplicateLayer(layer.id);
6480
+ if (!copy) return;
6481
+ copy.fabricObject.set(transform);
6482
+ copy.fabricObject.setCoords();
6483
+ this.layers.reorder(copy.id, this.layers.getAll().indexOf(layer));
6484
+ this.layers.select(layer.id);
6485
+ this.canvas.requestRenderAll();
6486
+ });
5289
6487
  }
5290
6488
  /**
5291
6489
  * Combine two or more layers into one group layer. Picking a group among them
@@ -5320,7 +6518,7 @@ var CanvasEditor = class {
5320
6518
  this.layers.detach(layer.id);
5321
6519
  }
5322
6520
  }
5323
- const group = new import_fabric22.Group(objects);
6521
+ const group = new import_fabric25.Group(objects);
5324
6522
  const grouped = this.layers.add("group", group, name);
5325
6523
  this.layers.adoptChildren(grouped, children);
5326
6524
  this.layers.reorder(grouped.id, insertIndex);
@@ -5354,6 +6552,7 @@ var CanvasEditor = class {
5354
6552
  await deserializeEditor(this, state);
5355
6553
  await this.patterns.rehydrateAll();
5356
6554
  this.layerMasks.refreshAll();
6555
+ this.imageEffects.refreshAll();
5357
6556
  } catch (error) {
5358
6557
  if (!managedByHistory) {
5359
6558
  this.events.emit("error", { message: "Failed to load editor state", error });
@@ -5382,7 +6581,7 @@ var CanvasEditor = class {
5382
6581
  const layer = this.layers.get(id);
5383
6582
  if (!layer) throw new Error(`Layer not found: ${id}`);
5384
6583
  try {
5385
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric22.FabricImage) {
6584
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric25.FabricImage) {
5386
6585
  const image = await layer.fabricObject.clone();
5387
6586
  image.set({
5388
6587
  left: 0,
@@ -5430,7 +6629,9 @@ var CanvasEditor = class {
5430
6629
  try {
5431
6630
  await this.fonts.ready();
5432
6631
  const blob = await this.withDesignBackground(
5433
- () => exportPNG(this.canvas, { ...options, format })
6632
+ () => this.imageEffects.withSourceResolution(
6633
+ () => exportPNG(this.canvas, { ...options, format })
6634
+ )
5434
6635
  );
5435
6636
  this.events.emit("export:complete", { format });
5436
6637
  this.licensing.track(`export:${format}`);
@@ -5468,7 +6669,9 @@ var CanvasEditor = class {
5468
6669
  }
5469
6670
  }
5470
6671
  toDataURL(format = "png", multiplier = 1) {
5471
- return this.withDesignBackground(() => exportDataURL(this.canvas, format, multiplier));
6672
+ return this.withDesignBackground(
6673
+ () => this.imageEffects.withSourceResolution(() => exportDataURL(this.canvas, format, multiplier))
6674
+ );
5472
6675
  }
5473
6676
  /**
5474
6677
  * Export the print file: the design cropped to the mockup's print area, on
@@ -5492,7 +6695,9 @@ var CanvasEditor = class {
5492
6695
  if (!this.mockup) throw new Error("No mockup is configured");
5493
6696
  await this.fonts.ready();
5494
6697
  try {
5495
- const blob = await exportMockup(this.canvas, this.mockup, options);
6698
+ const blob = await this.imageEffects.withSourceResolution(
6699
+ () => exportMockup(this.canvas, this.mockup, options)
6700
+ );
5496
6701
  this.licensing.track(`export:mockup:${options.format ?? "png"}`);
5497
6702
  return blob;
5498
6703
  } catch (error) {
@@ -5634,10 +6839,10 @@ var CanvasEditor = class {
5634
6839
  this.imageProvider = provider;
5635
6840
  }
5636
6841
  // ─── Canvas Operations ──────────────────────────────
5637
- setBackground(color) {
5638
- this.designBackground = color;
6842
+ setBackground(color2) {
6843
+ this.designBackground = color2;
5639
6844
  if (!this.mockup) {
5640
- this.canvas.backgroundColor = color;
6845
+ this.canvas.backgroundColor = color2;
5641
6846
  this.canvas.requestRenderAll();
5642
6847
  }
5643
6848
  this.history.save();
@@ -5658,7 +6863,7 @@ var CanvasEditor = class {
5658
6863
  return;
5659
6864
  }
5660
6865
  try {
5661
- const image = await import_fabric22.FabricImage.fromURL(
6866
+ const image = await import_fabric25.FabricImage.fromURL(
5662
6867
  url,
5663
6868
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
5664
6869
  { originX: "left", originY: "top" }
@@ -5944,9 +7149,11 @@ var CanvasEditor = class {
5944
7149
  }
5945
7150
  // ─── Cleanup ────────────────────────────────────────
5946
7151
  dispose() {
7152
+ this.imageEffects.dispose();
5947
7153
  this.masks.dispose();
5948
7154
  this.layerMasks.dispose();
5949
7155
  this.snapping.dispose();
7156
+ this.shapes.dispose();
5950
7157
  this.crop.dispose();
5951
7158
  this.history.dispose();
5952
7159
  this.patterns.dispose();
@@ -5980,12 +7187,26 @@ var CanvasEditor = class {
5980
7187
  const active = this.canvas.getActiveObject();
5981
7188
  if (active) applyObjectSelectionStyle(active, this.selectionStyle, this.zoomLevel);
5982
7189
  };
7190
+ this.canvas.on("mouse:down", (e) => {
7191
+ this.altDrag = null;
7192
+ const target = e.target;
7193
+ const pointer = e.e;
7194
+ if (!target || !pointer?.altKey || target instanceof import_fabric25.ActiveSelection) return;
7195
+ const layer = this.layers.findByObject(target);
7196
+ if (!layer || layer.locked || layer.renderProxy) return;
7197
+ this.altDrag = { layerId: layer.id, transform: import_fabric25.util.saveObjectTransform(target) };
7198
+ });
5983
7199
  this.canvas.on("object:modified", (e) => {
5984
7200
  if (!e.target) return;
5985
7201
  const layer = this.layers.findByObject(e.target);
5986
7202
  if (layer) {
5987
7203
  this.events.emit("layer:modified", { layerId: layer.id });
5988
7204
  this.history.save();
7205
+ const pending = this.altDrag;
7206
+ this.altDrag = null;
7207
+ if (pending && pending.layerId === layer.id) {
7208
+ void this.leaveCopyBehind(layer, pending.transform);
7209
+ }
5989
7210
  }
5990
7211
  });
5991
7212
  this.canvas.on("selection:created", (e) => {
@@ -6074,6 +7295,8 @@ var AnnotationOverlay = class {
6074
7295
  };
6075
7296
  // Annotate the CommonJS export names for ESM import in node:
6076
7297
  0 && (module.exports = {
7298
+ ADJUST_KEYS,
7299
+ ADJUST_RANGES,
6077
7300
  AnnotationOverlay,
6078
7301
  BLEND_GROUPS,
6079
7302
  BLEND_MODES,
@@ -6089,7 +7312,12 @@ var AnnotationOverlay = class {
6089
7312
  EventEmitter,
6090
7313
  FontRegistry,
6091
7314
  HistoryManager,
7315
+ IMAGE_FILTER_PRESETS,
7316
+ IMAGE_PROXY_MAX_EDGE,
7317
+ ImageEffectsManager,
7318
+ LAYER_EFFECT_DEFAULTS,
6092
7319
  Layer,
7320
+ LayerEffectsManager,
6093
7321
  LayerManager,
6094
7322
  LayerMaskManager,
6095
7323
  LicenseManager,
@@ -6097,10 +7325,12 @@ var AnnotationOverlay = class {
6097
7325
  MaskController,
6098
7326
  MaskPresetManager,
6099
7327
  MaskRefinementError,
7328
+ NEUTRAL_ADJUST,
6100
7329
  PatternManager,
6101
7330
  ProjectManager,
6102
7331
  SHAPE_MASK_BOX,
6103
7332
  SHAPE_MASK_IDS,
7333
+ ShapeManager,
6104
7334
  SnapManager,
6105
7335
  TEXTURE_MASK_IDS,
6106
7336
  TEXTURE_MASK_SIZE,
@@ -6108,6 +7338,8 @@ var AnnotationOverlay = class {
6108
7338
  TextWrapManager,
6109
7339
  TiledPatternObject,
6110
7340
  UnitConverter,
7341
+ alignObjects,
7342
+ alphaClipMatrix,
6111
7343
  angleFromCoords,
6112
7344
  applyAspectLock,
6113
7345
  applyLayerShadow,
@@ -6120,6 +7352,8 @@ var AnnotationOverlay = class {
6120
7352
  buildCurveLinePaths,
6121
7353
  buildCurvePathData,
6122
7354
  clamp,
7355
+ clampAdjust,
7356
+ clampAlphaClip,
6123
7357
  clearTextureMaskCache,
6124
7358
  composeMaskGroup,
6125
7359
  computeCoverPlacement,
@@ -6127,6 +7361,7 @@ var AnnotationOverlay = class {
6127
7361
  computeTilePositions,
6128
7362
  deserializeEditor,
6129
7363
  displaceRgba,
7364
+ distributeObjects,
6130
7365
  drawTiles,
6131
7366
  escapeXml,
6132
7367
  explodeTemplateSvg,
@@ -6138,12 +7373,15 @@ var AnnotationOverlay = class {
6138
7373
  fitTextWidth,
6139
7374
  fitToBox,
6140
7375
  generateId,
7376
+ imageEffectsFilterSpecs,
7377
+ imageFilterPreset,
6141
7378
  isCssColor,
6142
7379
  isMaskPresetId,
6143
7380
  isShapeMaskId,
6144
7381
  isTextureMaskId,
6145
7382
  linearCoords,
6146
7383
  needsAbsoluteSpace,
7384
+ normalizeImageEffects,
6147
7385
  normalizeTextCurve,
6148
7386
  preWrapWordSplit,
6149
7387
  readGradientConfig,
@@ -6151,6 +7389,7 @@ var AnnotationOverlay = class {
6151
7389
  readTextWrap,
6152
7390
  renderTextureMask,
6153
7391
  resetTransform,
7392
+ resolveShapeParams,
6154
7393
  restoreLocks,
6155
7394
  round2,
6156
7395
  sanitizeSvg,
@@ -6161,6 +7400,7 @@ var AnnotationOverlay = class {
6161
7400
  toCanvasSpace,
6162
7401
  toFabricGradient,
6163
7402
  toHostSpace,
7403
+ unionBox,
6164
7404
  unwrapGroup
6165
7405
  });
6166
7406
  //# sourceMappingURL=index.js.map