@overtone-art/canvas-editor-core 0.8.6 → 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,8 +2268,763 @@ function restoreLocks(obj, locks) {
2054
2268
  );
2055
2269
  }
2056
2270
 
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);
2278
+ }
2279
+ return params;
2280
+ }
2281
+ var ShapeManager = class {
2282
+ constructor(canvas, layers, history, events) {
2283
+ this.canvas = canvas;
2284
+ this.layers = layers;
2285
+ this.history = history;
2286
+ this.events = events;
2287
+ this.canvas.on("object:modified", this.onObjectModified);
2288
+ }
2289
+ canvas;
2290
+ layers;
2291
+ history;
2292
+ events;
2293
+ plugins = /* @__PURE__ */ new Map();
2294
+ /**
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.
2299
+ */
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
+
2057
3026
  // src/text-curve.ts
2058
- var import_fabric7 = require("fabric");
3027
+ var import_fabric11 = require("fabric");
2059
3028
 
2060
3029
  // src/text-curve-geometry.ts
2061
3030
  var DEFAULT_TEXT_CURVE = {
@@ -2064,9 +3033,12 @@ var DEFAULT_TEXT_CURVE = {
2064
3033
  wave: 0,
2065
3034
  waveLength: 4.1,
2066
3035
  offset: 0,
2067
- centerOffset: 0
3036
+ centerOffset: 0,
3037
+ angle: 0
2068
3038
  };
2069
3039
  var MIN_ARC = 0.5;
3040
+ var MIN_ANGLE = 0.5;
3041
+ var MAX_ANGLE = 80;
2070
3042
  var FULL_CIRCLE_ARC = 99.5;
2071
3043
  var MIN_SWEEP = 0.12;
2072
3044
  var WAVE_AMPLITUDE_EM = 0.9;
@@ -2091,10 +3063,12 @@ function normalizeTextCurve(config) {
2091
3063
  MAX_WAVE_LENGTH
2092
3064
  ),
2093
3065
  offset: clamp(config.offset ?? 0, -100, 100),
2094
- centerOffset: clamp(config.centerOffset ?? 0, -100, 100)
3066
+ centerOffset: clamp(config.centerOffset ?? 0, -100, 100),
3067
+ angle: clamp(config.angle ?? 0, -MAX_ANGLE, MAX_ANGLE)
2095
3068
  };
2096
3069
  }
2097
3070
  function bendsBaseline(config) {
3071
+ if (config.shape === "angle") return Math.abs(config.angle) >= MIN_ANGLE;
2098
3072
  return config.shape === "arc" ? Math.abs(config.arc) >= MIN_ARC : config.wave > 0;
2099
3073
  }
2100
3074
  function arcGeometry(width, config) {
@@ -2155,6 +3129,17 @@ function waveLinePath(width, fontSize, config, dy) {
2155
3129
  }
2156
3130
  return { data: commands.join(" "), length };
2157
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
+ }
2158
3143
  function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight = 0) {
2159
3144
  const curve = normalizeTextCurve(config);
2160
3145
  if (!bendsBaseline(curve)) return null;
@@ -2166,6 +3151,12 @@ function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight
2166
3151
  }
2167
3152
  return paths;
2168
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
+ }
2169
3160
  const geometry = arcGeometry(width, curve);
2170
3161
  const rotation = curve.offset / 100 * geometry.sweep;
2171
3162
  for (let index = 0; index < lines; index++) {
@@ -2244,7 +3235,7 @@ function lineHeightOf(text) {
2244
3235
  return text.fontSize * (text.lineHeight ?? 1) * FALLBACK_LINE_HEIGHT;
2245
3236
  }
2246
3237
  function toFabricPath(curve) {
2247
- return new import_fabric7.Path(curve.data, { visible: false, objectCaching: false });
3238
+ return new import_fabric11.Path(curve.data, { visible: false, objectCaching: false });
2248
3239
  }
2249
3240
  var TextCurveManager = class {
2250
3241
  constructor(canvas, layers, history, events) {
@@ -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();
@@ -4083,13 +5026,13 @@ var MaskController = class {
4083
5026
  };
4084
5027
 
4085
5028
  // src/masks/manager.ts
4086
- var import_fabric21 = require("fabric");
5029
+ var import_fabric24 = require("fabric");
4087
5030
 
4088
5031
  // src/masks/compose.ts
4089
- var import_fabric16 = require("fabric");
5032
+ var import_fabric19 = require("fabric");
4090
5033
 
4091
5034
  // src/gradient.ts
4092
- var import_fabric15 = require("fabric");
5035
+ var import_fabric18 = require("fabric");
4093
5036
  var DEFAULT_GRADIENT_CONFIG = {
4094
5037
  kind: "linear",
4095
5038
  angle: 90,
@@ -4126,10 +5069,10 @@ function angleFromCoords(coords, box) {
4126
5069
  }
4127
5070
  function stopColor(stop) {
4128
5071
  const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4129
- const color = new import_fabric15.Color(source);
5072
+ const color2 = new import_fabric18.Color(source);
4130
5073
  const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
4131
- color.setAlpha(clamp(opacity, 0, 1));
4132
- return color.toRgba();
5074
+ color2.setAlpha(clamp(opacity, 0, 1));
5075
+ return color2.toRgba();
4133
5076
  }
4134
5077
  function toFabricGradient(config, box) {
4135
5078
  const colorStops = [...config.stops].sort((a, b) => a.position - b.position).slice(0, MAX_GRADIENT_STOPS).map((stop) => {
@@ -4138,7 +5081,7 @@ function toFabricGradient(config, box) {
4138
5081
  });
4139
5082
  if (config.kind === "radial") {
4140
5083
  const radius = Math.max(MIN_RADIUS, config.radius);
4141
- return new import_fabric15.Gradient({
5084
+ return new import_fabric18.Gradient({
4142
5085
  type: "radial",
4143
5086
  gradientUnits: "percentage",
4144
5087
  coords: {
@@ -4152,7 +5095,7 @@ function toFabricGradient(config, box) {
4152
5095
  colorStops
4153
5096
  });
4154
5097
  }
4155
- return new import_fabric15.Gradient({
5098
+ return new import_fabric18.Gradient({
4156
5099
  type: "linear",
4157
5100
  gradientUnits: "percentage",
4158
5101
  coords: linearCoords(config.angle, box),
@@ -4162,24 +5105,24 @@ function toFabricGradient(config, box) {
4162
5105
  function toFabricMaskGradient(config, box) {
4163
5106
  const gradient = toFabricGradient(config, box);
4164
5107
  gradient.colorStops = gradient.colorStops.map((stop) => {
4165
- const color = new import_fabric15.Color(stop.color);
4166
- const [red, green, blue] = color.getSource();
5108
+ const color2 = new import_fabric18.Color(stop.color);
5109
+ const [red, green, blue] = color2.getSource();
4167
5110
  const luminance = (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255;
4168
- color.setSource([255, 255, 255, clamp(luminance * color.getAlpha(), 0, 1)]);
4169
- return { offset: stop.offset, color: color.toRgba() };
5111
+ color2.setSource([255, 255, 255, clamp(luminance * color2.getAlpha(), 0, 1)]);
5112
+ return { offset: stop.offset, color: color2.toRgba() };
4170
5113
  });
4171
5114
  return gradient;
4172
5115
  }
4173
5116
  function readGradientConfig(object) {
4174
5117
  const fill = object.fill;
4175
- if (!fill || typeof fill === "string" || !(fill instanceof import_fabric15.Gradient)) return null;
5118
+ if (!fill || typeof fill === "string" || !(fill instanceof import_fabric18.Gradient)) return null;
4176
5119
  const stops = fill.colorStops.map((stop) => {
4177
5120
  const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4178
- const color = new import_fabric15.Color(source);
5121
+ const color2 = new import_fabric18.Color(source);
4179
5122
  const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
4180
5123
  return {
4181
- color: `#${color.toHex().toLowerCase()}`,
4182
- opacity: color.getAlpha(),
5124
+ color: `#${color2.toHex().toLowerCase()}`,
5125
+ opacity: color2.getAlpha(),
4183
5126
  position: clamp(offset, 0, 1)
4184
5127
  };
4185
5128
  });
@@ -4213,7 +5156,7 @@ function neutralize(child) {
4213
5156
  child.set({ opacity: 0, globalCompositeOperation: "source-over" });
4214
5157
  }
4215
5158
  function baseRect(box) {
4216
- return new import_fabric16.Rect({
5159
+ return new import_fabric19.Rect({
4217
5160
  left: box.left,
4218
5161
  top: box.top,
4219
5162
  width: Math.max(1, box.width),
@@ -4248,7 +5191,7 @@ function composeMaskGroup(children, entries, options) {
4248
5191
  });
4249
5192
  const first = entries.find((entry) => entry.visible);
4250
5193
  const withBase = !first || first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
4251
- return new import_fabric16.Group(withBase, {
5194
+ return new import_fabric19.Group(withBase, {
4252
5195
  absolutePositioned: options.absolute,
4253
5196
  // Cached, so the children's compositing operations resolve against each
4254
5197
  // other instead of against the page underneath the mask.
@@ -4262,7 +5205,7 @@ function needsAbsoluteSpace(entries) {
4262
5205
  }
4263
5206
 
4264
5207
  // src/masks/edit.ts
4265
- var import_fabric17 = require("fabric");
5208
+ var import_fabric20 = require("fabric");
4266
5209
 
4267
5210
  // src/masks/edit-viewport.ts
4268
5211
  var MIN_PADDING = 48;
@@ -4440,8 +5383,8 @@ var MaskEditController = class {
4440
5383
  if (!this.handle || !this.child || !this.group) return;
4441
5384
  applyMatrix(
4442
5385
  this.child,
4443
- import_fabric17.util.multiplyTransformMatrices(
4444
- import_fabric17.util.invertTransform(matrixOf(this.group)),
5386
+ import_fabric20.util.multiplyTransformMatrices(
5387
+ import_fabric20.util.invertTransform(matrixOf(this.group)),
4445
5388
  matrixOf(this.handle)
4446
5389
  )
4447
5390
  );
@@ -4452,15 +5395,15 @@ var MaskEditController = class {
4452
5395
  };
4453
5396
 
4454
5397
  // src/masks/store.ts
4455
- var import_fabric20 = require("fabric");
5398
+ var import_fabric23 = require("fabric");
4456
5399
 
4457
5400
  // src/masks/host.ts
4458
- var import_fabric18 = require("fabric");
5401
+ var import_fabric21 = require("fabric");
4459
5402
  function findCanvasHost(layers) {
4460
5403
  return layers.getAll().find((layer) => layer.meta.canvasMask);
4461
5404
  }
4462
5405
  function createCanvasHost(canvas, layers) {
4463
- const rect = new import_fabric18.Rect({
5406
+ const rect = new import_fabric21.Rect({
4464
5407
  left: 0,
4465
5408
  top: 0,
4466
5409
  width: canvas.getWidth(),
@@ -4509,9 +5452,9 @@ function hostBoxOf(canvas, host, absolute) {
4509
5452
  }
4510
5453
 
4511
5454
  // src/masks/install.ts
4512
- var import_fabric19 = require("fabric");
5455
+ var import_fabric22 = require("fabric");
4513
5456
  function convertSpace(host, sources, absolute) {
4514
- const wasAbsolute = host.clipPath instanceof import_fabric19.Group ? host.clipPath.absolutePositioned : absolute;
5457
+ const wasAbsolute = host.clipPath instanceof import_fabric22.Group ? host.clipPath.absolutePositioned : absolute;
4515
5458
  if (absolute === wasAbsolute) return;
4516
5459
  for (const source of sources) {
4517
5460
  if (absolute) toCanvasSpace(source, host);
@@ -4647,7 +5590,7 @@ var MaskStackStore = class {
4647
5590
  const entries = this.list(target);
4648
5591
  const meta = this.hostLayer(target)?.meta;
4649
5592
  if (entries.length === 0 && this.holdsBoxClip(meta)) return [];
4650
- if (entries.length === 0 || !(clip instanceof import_fabric20.Group)) {
5593
+ if (entries.length === 0 || !(clip instanceof import_fabric23.Group)) {
4651
5594
  const source = asObject(clip);
4652
5595
  if (meta?.overflow === "hidden") source.clipPath = void 0;
4653
5596
  return restoreGradientPaint(entries, [source]);
@@ -4712,7 +5655,7 @@ function restoreGradientPaint(entries, sources) {
4712
5655
  };
4713
5656
  const fill = source.fill;
4714
5657
  const rendered = toFabricMaskGradient(entry.gradient, box);
4715
- if (fill instanceof import_fabric20.Gradient && JSON.stringify(fill.toObject()) !== JSON.stringify(rendered.toObject())) {
5658
+ if (fill instanceof import_fabric23.Gradient && JSON.stringify(fill.toObject()) !== JSON.stringify(rendered.toObject())) {
4716
5659
  return;
4717
5660
  }
4718
5661
  source.set({
@@ -4781,7 +5724,7 @@ var LayerMaskManager = class extends MaskStackStore {
4781
5724
  if (index === -1) return false;
4782
5725
  this.endEdit(false);
4783
5726
  this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
4784
- const group = host.clipPath instanceof import_fabric21.Group ? host.clipPath : null;
5727
+ const group = host.clipPath instanceof import_fabric24.Group ? host.clipPath : null;
4785
5728
  if (!group) return false;
4786
5729
  const children = group.getObjects();
4787
5730
  const child = children[children.length - entries.length + index];
@@ -5138,11 +6081,17 @@ var CanvasEditor = class {
5138
6081
  snapping;
5139
6082
  crop;
5140
6083
  patterns;
6084
+ /** Parametric shape layers: `meta.shape` and in-place regeneration. */
6085
+ shapes;
5141
6086
  curves;
5142
6087
  wrap;
5143
6088
  maskPresets;
6089
+ /** Owner of every image layer's filter array (D5). */
6090
+ imageEffects;
5144
6091
  /** Stacked boolean masks, per layer and for the design as a whole. */
5145
6092
  layerMasks;
6093
+ /** One effect per text or shape layer (D6). */
6094
+ effects;
5146
6095
  fonts;
5147
6096
  licensing;
5148
6097
  pages;
@@ -5152,6 +6101,8 @@ var CanvasEditor = class {
5152
6101
  zoomLevel = 1;
5153
6102
  selectionStyle = { ...DEFAULT_SELECTION_STYLE };
5154
6103
  mockup = null;
6104
+ /** Alt was held when a drag began on this layer; the drop leaves a copy behind. */
6105
+ altDrag = null;
5155
6106
  // The design's configured background. The live canvas background is forced
5156
6107
  // transparent while a mockup preview is shown, so this is the source of truth
5157
6108
  // for serialization and export — not the (possibly transient) canvas value.
@@ -5159,6 +6110,7 @@ var CanvasEditor = class {
5159
6110
  designBackgroundImage = null;
5160
6111
  backgroundImageOptions = null;
5161
6112
  constructor(canvasElement, config) {
6113
+ (0, import_canvas_editor_effects2.registerEffects)(fabricModule);
5162
6114
  this.events = new EventEmitter();
5163
6115
  this.fonts = new FontRegistry();
5164
6116
  config.fonts?.forEach((font) => this.fonts.register(font));
@@ -5167,7 +6119,7 @@ var CanvasEditor = class {
5167
6119
  const widthPx = this.units.toPixels(config.width);
5168
6120
  const heightPx = this.units.toPixels(config.height);
5169
6121
  this.designBackground = config.backgroundColor ?? "#ffffff";
5170
- this.canvas = new import_fabric22.Canvas(canvasElement, {
6122
+ this.canvas = new import_fabric25.Canvas(canvasElement, {
5171
6123
  width: widthPx,
5172
6124
  height: heightPx,
5173
6125
  backgroundColor: this.designBackground,
@@ -5193,6 +6145,15 @@ var CanvasEditor = class {
5193
6145
  this.wrap = new TextWrapManager(this.canvas, this.layers, this.history, this.events);
5194
6146
  this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
5195
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));
5196
6157
  this.setupCanvasEvents();
5197
6158
  this.refreshSelectionStyle();
5198
6159
  this.history.saveImmediate();
@@ -5202,7 +6163,7 @@ var CanvasEditor = class {
5202
6163
  // ─── Layer Operations ────────────────────────────────
5203
6164
  async addImage(url, options) {
5204
6165
  try {
5205
- const img = await import_fabric22.FabricImage.fromURL(
6166
+ const img = await import_fabric25.FabricImage.fromURL(
5206
6167
  url,
5207
6168
  {},
5208
6169
  { originX: "left", originY: "top", ...options }
@@ -5217,7 +6178,7 @@ var CanvasEditor = class {
5217
6178
  }
5218
6179
  }
5219
6180
  /** Replace an image source without changing its layer identity or visual transform. */
5220
- async replaceImageSource(layerId, url) {
6181
+ async replaceImageSource(layerId, url, options = {}) {
5221
6182
  const layer = this.layers.get(layerId);
5222
6183
  if (!layer || layer.type !== "image" && layer.type !== "mask") {
5223
6184
  throw new Error(`Image or mask layer not found: ${layerId}`);
@@ -5228,7 +6189,11 @@ var CanvasEditor = class {
5228
6189
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
5229
6190
  const previous = layer.fabricObject;
5230
6191
  try {
5231
- 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
+ );
5232
6197
  replacement.set({
5233
6198
  left: previous.left,
5234
6199
  top: previous.top,
@@ -5250,14 +6215,28 @@ var CanvasEditor = class {
5250
6215
  replacement.applyFilters();
5251
6216
  replacement.setCoords();
5252
6217
  this.layers.replaceObject(layerId, replacement);
6218
+ this.imageEffects.refresh(layerId);
5253
6219
  return layer;
5254
6220
  } catch (error) {
5255
6221
  this.events.emit("error", { message: "Failed to replace image source", error });
5256
6222
  throw error;
5257
6223
  }
5258
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
+ }
5259
6237
  addText(text, options) {
5260
- const textbox = new import_fabric22.Textbox(text, {
6238
+ const TextboxClass = this.registered("Textbox", import_fabric25.Textbox);
6239
+ const textbox = new TextboxClass(text, {
5261
6240
  fontSize: 32,
5262
6241
  fontFamily: "Arial",
5263
6242
  fill: "#000000",
@@ -5275,6 +6254,15 @@ var CanvasEditor = class {
5275
6254
  addShape(plugin, options) {
5276
6255
  const obj = plugin.create(options);
5277
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
+ }
5278
6266
  this.layers.select(layer.id);
5279
6267
  this.history.save();
5280
6268
  return layer;
@@ -5286,7 +6274,8 @@ var CanvasEditor = class {
5286
6274
  addGradient(config = DEFAULT_GRADIENT_CONFIG, box) {
5287
6275
  const width = box?.width ?? this.canvas.getWidth();
5288
6276
  const height = box?.height ?? this.canvas.getHeight();
5289
- const object = new import_fabric22.Rect({
6277
+ const RectClass = this.registered("Rect", import_fabric25.Rect);
6278
+ const object = new RectClass({
5290
6279
  left: box?.left ?? 0,
5291
6280
  top: box?.top ?? 0,
5292
6281
  originX: "left",
@@ -5415,6 +6404,49 @@ var CanvasEditor = class {
5415
6404
  this.canvas.requestRenderAll();
5416
6405
  this.history.save();
5417
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
+ }
5418
6450
  /** Clone a layer (offset slightly) and select the copy. */
5419
6451
  async duplicateLayer(id) {
5420
6452
  const layer = this.layers.get(id);
@@ -5439,24 +6471,19 @@ var CanvasEditor = class {
5439
6471
  this.history.save();
5440
6472
  return copy;
5441
6473
  }
5442
- applyImageAdjustments(layerId, adjustments) {
5443
- const layer = this.layers.get(layerId);
5444
- if (!layer || layer.type !== "image") return false;
5445
- const image = layer.fabricObject;
5446
- const previous = layer.meta.imageAdjustments ?? {};
5447
- const next = { ...previous, ...adjustments };
5448
- const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
5449
- image.filters = [
5450
- new import_fabric22.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
5451
- new import_fabric22.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
5452
- new import_fabric22.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
5453
- new import_fabric22.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
5454
- ];
5455
- layer.meta.imageAdjustments = next;
5456
- image.applyFilters();
5457
- this.canvas.requestRenderAll();
5458
- this.history.save();
5459
- 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
+ });
5460
6487
  }
5461
6488
  /**
5462
6489
  * Combine two or more layers into one group layer. Picking a group among them
@@ -5491,7 +6518,7 @@ var CanvasEditor = class {
5491
6518
  this.layers.detach(layer.id);
5492
6519
  }
5493
6520
  }
5494
- const group = new import_fabric22.Group(objects);
6521
+ const group = new import_fabric25.Group(objects);
5495
6522
  const grouped = this.layers.add("group", group, name);
5496
6523
  this.layers.adoptChildren(grouped, children);
5497
6524
  this.layers.reorder(grouped.id, insertIndex);
@@ -5525,6 +6552,7 @@ var CanvasEditor = class {
5525
6552
  await deserializeEditor(this, state);
5526
6553
  await this.patterns.rehydrateAll();
5527
6554
  this.layerMasks.refreshAll();
6555
+ this.imageEffects.refreshAll();
5528
6556
  } catch (error) {
5529
6557
  if (!managedByHistory) {
5530
6558
  this.events.emit("error", { message: "Failed to load editor state", error });
@@ -5553,7 +6581,7 @@ var CanvasEditor = class {
5553
6581
  const layer = this.layers.get(id);
5554
6582
  if (!layer) throw new Error(`Layer not found: ${id}`);
5555
6583
  try {
5556
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric22.FabricImage) {
6584
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric25.FabricImage) {
5557
6585
  const image = await layer.fabricObject.clone();
5558
6586
  image.set({
5559
6587
  left: 0,
@@ -5601,7 +6629,9 @@ var CanvasEditor = class {
5601
6629
  try {
5602
6630
  await this.fonts.ready();
5603
6631
  const blob = await this.withDesignBackground(
5604
- () => exportPNG(this.canvas, { ...options, format })
6632
+ () => this.imageEffects.withSourceResolution(
6633
+ () => exportPNG(this.canvas, { ...options, format })
6634
+ )
5605
6635
  );
5606
6636
  this.events.emit("export:complete", { format });
5607
6637
  this.licensing.track(`export:${format}`);
@@ -5639,7 +6669,9 @@ var CanvasEditor = class {
5639
6669
  }
5640
6670
  }
5641
6671
  toDataURL(format = "png", multiplier = 1) {
5642
- return this.withDesignBackground(() => exportDataURL(this.canvas, format, multiplier));
6672
+ return this.withDesignBackground(
6673
+ () => this.imageEffects.withSourceResolution(() => exportDataURL(this.canvas, format, multiplier))
6674
+ );
5643
6675
  }
5644
6676
  /**
5645
6677
  * Export the print file: the design cropped to the mockup's print area, on
@@ -5663,7 +6695,9 @@ var CanvasEditor = class {
5663
6695
  if (!this.mockup) throw new Error("No mockup is configured");
5664
6696
  await this.fonts.ready();
5665
6697
  try {
5666
- const blob = await exportMockup(this.canvas, this.mockup, options);
6698
+ const blob = await this.imageEffects.withSourceResolution(
6699
+ () => exportMockup(this.canvas, this.mockup, options)
6700
+ );
5667
6701
  this.licensing.track(`export:mockup:${options.format ?? "png"}`);
5668
6702
  return blob;
5669
6703
  } catch (error) {
@@ -5805,10 +6839,10 @@ var CanvasEditor = class {
5805
6839
  this.imageProvider = provider;
5806
6840
  }
5807
6841
  // ─── Canvas Operations ──────────────────────────────
5808
- setBackground(color) {
5809
- this.designBackground = color;
6842
+ setBackground(color2) {
6843
+ this.designBackground = color2;
5810
6844
  if (!this.mockup) {
5811
- this.canvas.backgroundColor = color;
6845
+ this.canvas.backgroundColor = color2;
5812
6846
  this.canvas.requestRenderAll();
5813
6847
  }
5814
6848
  this.history.save();
@@ -5829,7 +6863,7 @@ var CanvasEditor = class {
5829
6863
  return;
5830
6864
  }
5831
6865
  try {
5832
- const image = await import_fabric22.FabricImage.fromURL(
6866
+ const image = await import_fabric25.FabricImage.fromURL(
5833
6867
  url,
5834
6868
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
5835
6869
  { originX: "left", originY: "top" }
@@ -6115,9 +7149,11 @@ var CanvasEditor = class {
6115
7149
  }
6116
7150
  // ─── Cleanup ────────────────────────────────────────
6117
7151
  dispose() {
7152
+ this.imageEffects.dispose();
6118
7153
  this.masks.dispose();
6119
7154
  this.layerMasks.dispose();
6120
7155
  this.snapping.dispose();
7156
+ this.shapes.dispose();
6121
7157
  this.crop.dispose();
6122
7158
  this.history.dispose();
6123
7159
  this.patterns.dispose();
@@ -6151,12 +7187,26 @@ var CanvasEditor = class {
6151
7187
  const active = this.canvas.getActiveObject();
6152
7188
  if (active) applyObjectSelectionStyle(active, this.selectionStyle, this.zoomLevel);
6153
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
+ });
6154
7199
  this.canvas.on("object:modified", (e) => {
6155
7200
  if (!e.target) return;
6156
7201
  const layer = this.layers.findByObject(e.target);
6157
7202
  if (layer) {
6158
7203
  this.events.emit("layer:modified", { layerId: layer.id });
6159
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
+ }
6160
7210
  }
6161
7211
  });
6162
7212
  this.canvas.on("selection:created", (e) => {
@@ -6245,6 +7295,8 @@ var AnnotationOverlay = class {
6245
7295
  };
6246
7296
  // Annotate the CommonJS export names for ESM import in node:
6247
7297
  0 && (module.exports = {
7298
+ ADJUST_KEYS,
7299
+ ADJUST_RANGES,
6248
7300
  AnnotationOverlay,
6249
7301
  BLEND_GROUPS,
6250
7302
  BLEND_MODES,
@@ -6260,7 +7312,12 @@ var AnnotationOverlay = class {
6260
7312
  EventEmitter,
6261
7313
  FontRegistry,
6262
7314
  HistoryManager,
7315
+ IMAGE_FILTER_PRESETS,
7316
+ IMAGE_PROXY_MAX_EDGE,
7317
+ ImageEffectsManager,
7318
+ LAYER_EFFECT_DEFAULTS,
6263
7319
  Layer,
7320
+ LayerEffectsManager,
6264
7321
  LayerManager,
6265
7322
  LayerMaskManager,
6266
7323
  LicenseManager,
@@ -6268,10 +7325,12 @@ var AnnotationOverlay = class {
6268
7325
  MaskController,
6269
7326
  MaskPresetManager,
6270
7327
  MaskRefinementError,
7328
+ NEUTRAL_ADJUST,
6271
7329
  PatternManager,
6272
7330
  ProjectManager,
6273
7331
  SHAPE_MASK_BOX,
6274
7332
  SHAPE_MASK_IDS,
7333
+ ShapeManager,
6275
7334
  SnapManager,
6276
7335
  TEXTURE_MASK_IDS,
6277
7336
  TEXTURE_MASK_SIZE,
@@ -6279,6 +7338,8 @@ var AnnotationOverlay = class {
6279
7338
  TextWrapManager,
6280
7339
  TiledPatternObject,
6281
7340
  UnitConverter,
7341
+ alignObjects,
7342
+ alphaClipMatrix,
6282
7343
  angleFromCoords,
6283
7344
  applyAspectLock,
6284
7345
  applyLayerShadow,
@@ -6291,6 +7352,8 @@ var AnnotationOverlay = class {
6291
7352
  buildCurveLinePaths,
6292
7353
  buildCurvePathData,
6293
7354
  clamp,
7355
+ clampAdjust,
7356
+ clampAlphaClip,
6294
7357
  clearTextureMaskCache,
6295
7358
  composeMaskGroup,
6296
7359
  computeCoverPlacement,
@@ -6298,6 +7361,7 @@ var AnnotationOverlay = class {
6298
7361
  computeTilePositions,
6299
7362
  deserializeEditor,
6300
7363
  displaceRgba,
7364
+ distributeObjects,
6301
7365
  drawTiles,
6302
7366
  escapeXml,
6303
7367
  explodeTemplateSvg,
@@ -6309,12 +7373,15 @@ var AnnotationOverlay = class {
6309
7373
  fitTextWidth,
6310
7374
  fitToBox,
6311
7375
  generateId,
7376
+ imageEffectsFilterSpecs,
7377
+ imageFilterPreset,
6312
7378
  isCssColor,
6313
7379
  isMaskPresetId,
6314
7380
  isShapeMaskId,
6315
7381
  isTextureMaskId,
6316
7382
  linearCoords,
6317
7383
  needsAbsoluteSpace,
7384
+ normalizeImageEffects,
6318
7385
  normalizeTextCurve,
6319
7386
  preWrapWordSplit,
6320
7387
  readGradientConfig,
@@ -6322,6 +7389,7 @@ var AnnotationOverlay = class {
6322
7389
  readTextWrap,
6323
7390
  renderTextureMask,
6324
7391
  resetTransform,
7392
+ resolveShapeParams,
6325
7393
  restoreLocks,
6326
7394
  round2,
6327
7395
  sanitizeSvg,
@@ -6332,6 +7400,7 @@ var AnnotationOverlay = class {
6332
7400
  toCanvasSpace,
6333
7401
  toFabricGradient,
6334
7402
  toHostSpace,
7403
+ unionBox,
6335
7404
  unwrapGroup
6336
7405
  });
6337
7406
  //# sourceMappingURL=index.js.map