@overtone-art/canvas-editor-core 0.2.7 → 0.2.8

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
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ AnnotationOverlay: () => AnnotationOverlay,
23
24
  CANVAS_SIZE_PRESETS: () => CANVAS_SIZE_PRESETS,
24
25
  CanvasEditor: () => CanvasEditor,
25
26
  CropController: () => CropController,
@@ -30,6 +31,8 @@ __export(index_exports, {
30
31
  Layer: () => Layer,
31
32
  LayerManager: () => LayerManager,
32
33
  LicenseManager: () => LicenseManager,
34
+ MaskController: () => MaskController,
35
+ MaskRefinementError: () => MaskRefinementError,
33
36
  PatternManager: () => PatternManager,
34
37
  ProjectManager: () => ProjectManager,
35
38
  SnapManager: () => SnapManager,
@@ -43,6 +46,7 @@ __export(index_exports, {
43
46
  computePrintAreaClip: () => computePrintAreaClip,
44
47
  computeTilePositions: () => computeTilePositions,
45
48
  deserializeEditor: () => deserializeEditor,
49
+ displaceRgba: () => displaceRgba,
46
50
  drawTiles: () => drawTiles,
47
51
  escapeXml: () => escapeXml,
48
52
  exportDataURL: () => exportDataURL,
@@ -60,7 +64,7 @@ __export(index_exports, {
60
64
  module.exports = __toCommonJS(index_exports);
61
65
 
62
66
  // src/editor.ts
63
- var import_fabric4 = require("fabric");
67
+ var import_fabric6 = require("fabric");
64
68
 
65
69
  // src/events.ts
66
70
  var EventEmitter = class {
@@ -298,9 +302,13 @@ var LayerManager = class {
298
302
  };
299
303
 
300
304
  // src/history.ts
301
- var HistoryManager = class {
305
+ var HistoryManager = class _HistoryManager {
306
+ static ASSET_KEY = "__canvasEditorHistoryAsset";
302
307
  undoStack = [];
303
308
  redoStack = [];
309
+ assets = /* @__PURE__ */ new Map();
310
+ assetIds = /* @__PURE__ */ new Map();
311
+ nextAssetId = 1;
304
312
  maxSize;
305
313
  maxBytes;
306
314
  paused = false;
@@ -343,7 +351,8 @@ var HistoryManager = class {
343
351
  return;
344
352
  }
345
353
  this.cancelPending();
346
- const state = this.getState();
354
+ const rawState = this.getState();
355
+ const state = this.compactState(rawState);
347
356
  if (this.undoStack.at(-1) === state) {
348
357
  this.emitChanged();
349
358
  return;
@@ -355,7 +364,7 @@ var HistoryManager = class {
355
364
  this.redoStack = [];
356
365
  this.trimToBudget();
357
366
  this.events.emit("history:snapshot", {
358
- bytes: state.length * 2,
367
+ bytes: rawState.length * 2,
359
368
  totalBytes: this.snapshotBytes(),
360
369
  entries: this.undoStack.length
361
370
  });
@@ -383,15 +392,15 @@ var HistoryManager = class {
383
392
  }
384
393
  async undo() {
385
394
  this.cancelPending();
386
- const current = this.getState();
387
395
  const committed = this.undoStack.at(-1);
388
396
  if (!committed) return;
397
+ const current = this.compactState(this.getState());
389
398
  const currentIsCommitted = current === committed;
390
399
  if (currentIsCommitted && this.undoStack.length < 2) return;
391
400
  const target = currentIsCommitted ? this.undoStack[this.undoStack.length - 2] : committed;
392
401
  this.paused = true;
393
402
  try {
394
- await this.restoreState(target);
403
+ await this.restoreState(this.expandState(target));
395
404
  if (currentIsCommitted) this.undoStack.pop();
396
405
  this.redoStack.push(current);
397
406
  this.trimToBudget();
@@ -409,7 +418,7 @@ var HistoryManager = class {
409
418
  if (!state) return;
410
419
  this.paused = true;
411
420
  try {
412
- await this.restoreState(state);
421
+ await this.restoreState(this.expandState(state));
413
422
  this.redoStack.pop();
414
423
  if (this.undoStack.at(-1) !== state) this.undoStack.push(state);
415
424
  this.trimToBudget();
@@ -442,6 +451,8 @@ var HistoryManager = class {
442
451
  this.cancelPending();
443
452
  this.undoStack = [];
444
453
  this.redoStack = [];
454
+ this.assets.clear();
455
+ this.assetIds.clear();
445
456
  this.emitChanged();
446
457
  }
447
458
  getSnapshotBytes() {
@@ -457,6 +468,10 @@ var HistoryManager = class {
457
468
  this.cancelPending();
458
469
  this.transactionDepth = 0;
459
470
  this.transactionDirty = false;
471
+ this.undoStack = [];
472
+ this.redoStack = [];
473
+ this.assets.clear();
474
+ this.assetIds.clear();
460
475
  }
461
476
  emitChanged() {
462
477
  this.events.emit("history:changed", {
@@ -465,17 +480,91 @@ var HistoryManager = class {
465
480
  });
466
481
  }
467
482
  snapshotBytes() {
468
- return [...this.undoStack, ...this.redoStack].reduce(
483
+ const stackBytes = [...this.undoStack, ...this.redoStack].reduce(
469
484
  (total, state) => total + state.length * 2,
470
485
  0
471
486
  );
487
+ const assetBytes = [...this.assets.values()].reduce(
488
+ (total, asset) => total + asset.length * 2,
489
+ 0
490
+ );
491
+ return stackBytes + assetBytes;
472
492
  }
473
493
  trimToBudget() {
494
+ this.pruneAssets();
474
495
  while (this.undoStack.length > 1 && this.snapshotBytes() > this.maxBytes) {
475
496
  this.undoStack.shift();
497
+ this.pruneAssets();
476
498
  }
477
499
  while (this.redoStack.length > 1 && this.snapshotBytes() > this.maxBytes) {
478
500
  this.redoStack.shift();
501
+ this.pruneAssets();
502
+ }
503
+ }
504
+ /**
505
+ * History is internal and can content-address large raster strings without
506
+ * changing the public EditorState wire format. Unchanged images are retained
507
+ * once even when dozens of snapshots reference them.
508
+ */
509
+ compactState(state) {
510
+ let parsed;
511
+ try {
512
+ parsed = JSON.parse(state);
513
+ } catch {
514
+ return state;
515
+ }
516
+ const visit = (value) => {
517
+ if (typeof value === "string" && /^data:image\/(?:png|jpeg|webp);base64,/i.test(value)) {
518
+ let id = this.assetIds.get(value);
519
+ if (!id) {
520
+ id = `a${this.nextAssetId++}`;
521
+ this.assetIds.set(value, id);
522
+ this.assets.set(id, value);
523
+ }
524
+ return { [_HistoryManager.ASSET_KEY]: id };
525
+ }
526
+ if (Array.isArray(value)) return value.map(visit);
527
+ if (!value || typeof value !== "object") return value;
528
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, visit(entry)]));
529
+ };
530
+ return JSON.stringify(visit(parsed));
531
+ }
532
+ expandState(state) {
533
+ let parsed;
534
+ try {
535
+ parsed = JSON.parse(state);
536
+ } catch {
537
+ return state;
538
+ }
539
+ const visit = (value) => {
540
+ if (Array.isArray(value)) return value.map(visit);
541
+ if (!value || typeof value !== "object") return value;
542
+ const record = value;
543
+ const id = record[_HistoryManager.ASSET_KEY];
544
+ if (typeof id === "string" && Object.keys(record).length === 1) {
545
+ const asset = this.assets.get(id);
546
+ if (!asset) throw new Error(`Missing history raster asset: ${id}`);
547
+ return asset;
548
+ }
549
+ return Object.fromEntries(Object.entries(record).map(([key, entry]) => [key, visit(entry)]));
550
+ };
551
+ return JSON.stringify(visit(parsed));
552
+ }
553
+ /**
554
+ * Runs on every commit, so it scans for the serialized reference marker
555
+ * instead of re-parsing every snapshot — parsing multi-megabyte raster states
556
+ * per save would cost more than the retention it reclaims. `JSON.stringify`
557
+ * emits the marker verbatim; the same text inside user data is escaped and
558
+ * therefore cannot match.
559
+ */
560
+ pruneAssets() {
561
+ if (this.assets.size === 0) return;
562
+ const states = [...this.undoStack, ...this.redoStack];
563
+ for (const [id, asset] of this.assets) {
564
+ const marker = `"${_HistoryManager.ASSET_KEY}":"${id}"`;
565
+ if (states.some((state) => state.includes(marker))) continue;
566
+ this.assets.delete(id);
567
+ this.assetIds.delete(asset);
479
568
  }
480
569
  }
481
570
  };
@@ -1128,6 +1217,7 @@ function serializeEditor(editor) {
1128
1217
  // forced transparent while a mockup preview is active).
1129
1218
  background: editor.getDesignBackground(),
1130
1219
  backgroundImage: editor.getDesignBackgroundImage() ? editor.getDesignBackgroundImage().toObject() : null,
1220
+ backgroundImageOptions: editor.getBackgroundImageOptions(),
1131
1221
  mockup: editor.getMockup()
1132
1222
  };
1133
1223
  }
@@ -1146,13 +1236,28 @@ async function deserializeEditor(editor, state) {
1146
1236
  throw new Error("Invalid editor background color");
1147
1237
  }
1148
1238
  const staged = await Promise.all(
1149
- state.layers.map(async (serialized) => ({
1150
- serialized,
1151
- fabricObject: (await import_fabric3.util.enlivenObjects([serialized.fabricObject]))[0]
1152
- }))
1239
+ state.layers.map(async (serialized) => {
1240
+ const fabricObject = (await import_fabric3.util.enlivenObjects([serialized.fabricObject]))[0];
1241
+ if (!fabricObject) {
1242
+ const source = serialized.fabricObject.src;
1243
+ if (typeof source === "string" && source.startsWith("blob:")) {
1244
+ throw new Error(`Failed to restore expired object URL: ${source}`);
1245
+ }
1246
+ throw new Error(`Failed to restore layer: ${serialized.id}`);
1247
+ }
1248
+ return { serialized, fabricObject };
1249
+ })
1153
1250
  );
1154
1251
  const stagedBackground = state.backgroundImage ? (await import_fabric3.util.enlivenObjects([state.backgroundImage]))[0] : null;
1252
+ if (state.backgroundImage && !stagedBackground) {
1253
+ const source = state.backgroundImage.src;
1254
+ if (typeof source === "string" && source.startsWith("blob:")) {
1255
+ throw new Error(`Failed to restore expired background object URL: ${source}`);
1256
+ }
1257
+ throw new Error("Failed to restore background image");
1258
+ }
1155
1259
  editor.crop.cancel();
1260
+ editor.masks.detach();
1156
1261
  editor.layers.clear();
1157
1262
  if (state.canvas.unit) editor.units.setUnit(state.canvas.unit);
1158
1263
  if (state.canvas.dpi !== void 0) editor.units.setDpi(state.canvas.dpi);
@@ -1160,7 +1265,11 @@ async function deserializeEditor(editor, state) {
1160
1265
  if (state.background !== void 0) {
1161
1266
  editor.setBackground(state.background);
1162
1267
  }
1163
- editor.setBackgroundImageObject(stagedBackground, false);
1268
+ editor.setBackgroundImageObject(
1269
+ stagedBackground ?? null,
1270
+ false,
1271
+ state.backgroundImageOptions ?? null
1272
+ );
1164
1273
  editor.setMockup(state.mockup ?? null);
1165
1274
  for (const item of staged) {
1166
1275
  restoreLayer(editor, item.serialized, item.fabricObject);
@@ -1184,6 +1293,62 @@ function restoreLayer(editor, serialized, fabricObject) {
1184
1293
  return layer;
1185
1294
  }
1186
1295
 
1296
+ // src/export.ts
1297
+ var import_fabric4 = require("fabric");
1298
+
1299
+ // src/displacement.ts
1300
+ var CHANNEL_INDEX = {
1301
+ red: 0,
1302
+ green: 1,
1303
+ blue: 2,
1304
+ alpha: 3
1305
+ };
1306
+ function finiteScale(value, fallback, label) {
1307
+ const resolved = value ?? fallback;
1308
+ if (!Number.isFinite(resolved)) throw new Error(`${label} must be finite`);
1309
+ return resolved;
1310
+ }
1311
+ function sample(source, width, height, x, y, channel) {
1312
+ const clampedX = Math.max(0, Math.min(width - 1, x));
1313
+ const clampedY = Math.max(0, Math.min(height - 1, y));
1314
+ const x0 = Math.floor(clampedX);
1315
+ const y0 = Math.floor(clampedY);
1316
+ const x1 = Math.min(width - 1, x0 + 1);
1317
+ const y1 = Math.min(height - 1, y0 + 1);
1318
+ const tx = clampedX - x0;
1319
+ const ty = clampedY - y0;
1320
+ const top = source[(y0 * width + x0) * 4 + channel] * (1 - tx) + source[(y0 * width + x1) * 4 + channel] * tx;
1321
+ const bottom = source[(y1 * width + x0) * 4 + channel] * (1 - tx) + source[(y1 * width + x1) * 4 + channel] * tx;
1322
+ return top * (1 - ty) + bottom * ty;
1323
+ }
1324
+ function displaceRgba(source, map, width, height, options) {
1325
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
1326
+ throw new Error("Displacement dimensions must be positive integers");
1327
+ }
1328
+ const expectedLength = width * height * 4;
1329
+ if (source.length !== expectedLength || map.length !== expectedLength) {
1330
+ throw new Error("Displacement source and map must match the requested dimensions");
1331
+ }
1332
+ const scaleX = finiteScale(options.scaleX, 10, "Displacement scaleX");
1333
+ const scaleY = finiteScale(options.scaleY, 10, "Displacement scaleY");
1334
+ const channelX = CHANNEL_INDEX[options.channelX ?? "red"];
1335
+ const channelY = CHANNEL_INDEX[options.channelY ?? "green"];
1336
+ const output = new Uint8ClampedArray(expectedLength);
1337
+ for (let y = 0; y < height; y += 1) {
1338
+ for (let x = 0; x < width; x += 1) {
1339
+ const offset = (y * width + x) * 4;
1340
+ const sourceX = x + (map[offset + channelX] - 128) / 127 * scaleX;
1341
+ const sourceY = y + (map[offset + channelY] - 128) / 127 * scaleY;
1342
+ for (let channel = 0; channel < 4; channel += 1) {
1343
+ output[offset + channel] = Math.round(
1344
+ sample(source, width, height, sourceX, sourceY, channel)
1345
+ );
1346
+ }
1347
+ }
1348
+ }
1349
+ return output;
1350
+ }
1351
+
1187
1352
  // src/export.ts
1188
1353
  function computePrintAreaClip(area, scaleX, scaleY, targetWidth, targetHeight) {
1189
1354
  const left = Math.max(0, Math.min(targetWidth, area.left * scaleX));
@@ -1221,6 +1386,23 @@ async function exportPNG(canvas, options = {}) {
1221
1386
  const output = canvas.toCanvasElement(multiplier);
1222
1387
  return canvasElementToBlob(output, format, quality);
1223
1388
  }
1389
+ async function exportIsolatedPNG(source, objects, options = {}) {
1390
+ const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
1391
+ const canvas = new import_fabric4.StaticCanvas(element, {
1392
+ width: options.width ?? source.getWidth(),
1393
+ height: options.height ?? source.getHeight(),
1394
+ backgroundColor: options.backgroundColor || void 0
1395
+ });
1396
+ try {
1397
+ const clones = options.cloneObjects === false ? objects : await Promise.all(objects.map((object) => object.clone()));
1398
+ if (clones.length) canvas.add(...clones);
1399
+ if (options.backgroundImage) canvas.backgroundImage = await options.backgroundImage.clone();
1400
+ canvas.requestRenderAll();
1401
+ return await exportPNG(canvas, options);
1402
+ } finally {
1403
+ canvas.dispose();
1404
+ }
1405
+ }
1224
1406
  async function exportMockup(canvas, mockup, options = {}) {
1225
1407
  const { multiplier = 1, format = "png", quality = 1 } = options;
1226
1408
  const design = canvas.toCanvasElement(multiplier);
@@ -1236,41 +1418,92 @@ async function exportMockup(canvas, mockup, options = {}) {
1236
1418
  element.onerror = () => reject(new Error(`Failed to load mockup image: ${url}`));
1237
1419
  element.src = url;
1238
1420
  });
1239
- const drawCover = (image) => {
1421
+ const drawCover = (image, targetContext = context) => {
1240
1422
  const placement = computeCoverPlacement(
1241
1423
  image.naturalWidth || image.width,
1242
1424
  image.naturalHeight || image.height,
1243
1425
  output.width,
1244
1426
  output.height
1245
1427
  );
1246
- context.drawImage(image, placement.left, placement.top, placement.width, placement.height);
1247
- };
1248
- drawCover(await loadImage(mockup.image));
1249
- context.save();
1250
- if (mockup.printArea && mockup.clipToPrintArea !== false) {
1251
- const clip = computePrintAreaClip(
1252
- mockup.printArea,
1253
- output.width / canvas.getWidth(),
1254
- output.height / canvas.getHeight(),
1255
- output.width,
1256
- output.height
1428
+ targetContext.drawImage(
1429
+ image,
1430
+ placement.left,
1431
+ placement.top,
1432
+ placement.width,
1433
+ placement.height
1257
1434
  );
1258
- context.beginPath();
1259
- context.rect(clip.left, clip.top, clip.width, clip.height);
1260
- context.clip();
1261
- }
1262
- context.globalAlpha = Math.max(0, Math.min(1, mockup.designOpacity ?? 1));
1263
- context.globalCompositeOperation = !mockup.designBlendMode || mockup.designBlendMode === "normal" ? "source-over" : mockup.designBlendMode;
1264
- context.drawImage(design, 0, 0);
1265
- context.restore();
1266
- if (mockup.overlay) {
1435
+ };
1436
+ const scratch = [design];
1437
+ try {
1438
+ drawCover(await loadImage(mockup.image));
1439
+ let compositedDesign = design;
1440
+ if (mockup.displacement) {
1441
+ const sourceContext = design.getContext("2d");
1442
+ if (!sourceContext) throw new Error("2D design context is unavailable");
1443
+ const mapCanvas = design.ownerDocument.createElement("canvas");
1444
+ scratch.push(mapCanvas);
1445
+ mapCanvas.width = design.width;
1446
+ mapCanvas.height = design.height;
1447
+ const mapContext = mapCanvas.getContext("2d");
1448
+ if (!mapContext) throw new Error("2D displacement-map context is unavailable");
1449
+ drawCover(await loadImage(mockup.displacement.image), mapContext);
1450
+ const warped = design.ownerDocument.createElement("canvas");
1451
+ scratch.push(warped);
1452
+ warped.width = design.width;
1453
+ warped.height = design.height;
1454
+ const warpedContext = warped.getContext("2d");
1455
+ if (!warpedContext) throw new Error("2D displaced-design context is unavailable");
1456
+ let sourcePixels;
1457
+ let mapPixels;
1458
+ try {
1459
+ sourcePixels = sourceContext.getImageData(0, 0, design.width, design.height).data;
1460
+ mapPixels = mapContext.getImageData(0, 0, design.width, design.height).data;
1461
+ } catch (error) {
1462
+ throw new Error("Failed to apply mockup displacement map; verify image CORS access", {
1463
+ cause: error
1464
+ });
1465
+ }
1466
+ const pixels = displaceRgba(sourcePixels, mapPixels, design.width, design.height, {
1467
+ ...mockup.displacement,
1468
+ scaleX: (mockup.displacement.scaleX ?? 10) * multiplier,
1469
+ scaleY: (mockup.displacement.scaleY ?? 10) * multiplier
1470
+ });
1471
+ const imageData = warpedContext.createImageData(design.width, design.height);
1472
+ imageData.data.set(pixels);
1473
+ warpedContext.putImageData(imageData, 0, 0);
1474
+ compositedDesign = warped;
1475
+ }
1267
1476
  context.save();
1268
- context.globalAlpha = Math.max(0, Math.min(1, mockup.overlay.opacity ?? 1));
1269
- context.globalCompositeOperation = mockup.overlay.blendMode === "normal" ? "source-over" : mockup.overlay.blendMode ?? "multiply";
1270
- drawCover(await loadImage(mockup.overlay.image));
1477
+ if (mockup.printArea && mockup.clipToPrintArea !== false) {
1478
+ const clip = computePrintAreaClip(
1479
+ mockup.printArea,
1480
+ output.width / canvas.getWidth(),
1481
+ output.height / canvas.getHeight(),
1482
+ output.width,
1483
+ output.height
1484
+ );
1485
+ context.beginPath();
1486
+ context.rect(clip.left, clip.top, clip.width, clip.height);
1487
+ context.clip();
1488
+ }
1489
+ context.globalAlpha = Math.max(0, Math.min(1, mockup.designOpacity ?? 1));
1490
+ context.globalCompositeOperation = !mockup.designBlendMode || mockup.designBlendMode === "normal" ? "source-over" : mockup.designBlendMode;
1491
+ context.drawImage(compositedDesign, 0, 0);
1271
1492
  context.restore();
1493
+ if (mockup.overlay) {
1494
+ context.save();
1495
+ context.globalAlpha = Math.max(0, Math.min(1, mockup.overlay.opacity ?? 1));
1496
+ context.globalCompositeOperation = mockup.overlay.blendMode === "normal" ? "source-over" : mockup.overlay.blendMode ?? "multiply";
1497
+ drawCover(await loadImage(mockup.overlay.image));
1498
+ context.restore();
1499
+ }
1500
+ return await canvasElementToBlob(output, format, quality);
1501
+ } finally {
1502
+ for (const element of scratch) {
1503
+ element.width = 0;
1504
+ element.height = 0;
1505
+ }
1272
1506
  }
1273
- return canvasElementToBlob(output, format, quality);
1274
1507
  }
1275
1508
  function exportSVG(canvas) {
1276
1509
  return canvas.toSVG();
@@ -1592,9 +1825,270 @@ var ProjectManager = class {
1592
1825
  }
1593
1826
  };
1594
1827
 
1828
+ // src/mask.ts
1829
+ var import_fabric5 = require("fabric");
1830
+ var MaskRefinementError = class extends Error {
1831
+ constructor(code, message, cause) {
1832
+ super(message);
1833
+ this.code = code;
1834
+ this.cause = cause;
1835
+ this.name = "MaskRefinementError";
1836
+ }
1837
+ code;
1838
+ cause;
1839
+ };
1840
+ var MaskController = class {
1841
+ constructor(editor) {
1842
+ this.editor = editor;
1843
+ }
1844
+ editor;
1845
+ backing = null;
1846
+ context = null;
1847
+ layerId = null;
1848
+ brush = null;
1849
+ previousPoint = null;
1850
+ strokeBackup = null;
1851
+ strokeStartedAt = null;
1852
+ lastInteractionLatencyMs = 0;
1853
+ refinement = null;
1854
+ disposed = false;
1855
+ async create(width = this.editor.canvas.getWidth(), height = this.editor.canvas.getHeight()) {
1856
+ this.assertActive();
1857
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
1858
+ throw new Error("Mask dimensions must be positive integers");
1859
+ }
1860
+ const backing = this.makeCanvas(width, height);
1861
+ const image = new import_fabric5.FabricImage(backing, {
1862
+ left: 0,
1863
+ top: 0,
1864
+ originX: "left",
1865
+ originY: "top",
1866
+ selectable: false
1867
+ });
1868
+ const layer = this.editor.layers.add("mask", image, "Mask");
1869
+ layer.meta.mask = { width, height, revision: 0 };
1870
+ this.editor.history.saveImmediate();
1871
+ this.attachBacking(layer.id, backing);
1872
+ return layer;
1873
+ }
1874
+ attach(layerId) {
1875
+ this.assertActive();
1876
+ this.cancelStroke();
1877
+ const layer = this.requireMask(layerId);
1878
+ const image = layer.fabricObject;
1879
+ const width = layer.meta.mask?.width ?? image.width ?? this.editor.canvas.getWidth();
1880
+ const height = layer.meta.mask?.height ?? image.height ?? this.editor.canvas.getHeight();
1881
+ const backing = this.makeCanvas(width, height);
1882
+ const context = backing.getContext("2d");
1883
+ if (!context) throw new Error("2D mask context is unavailable");
1884
+ const element = image.getElement();
1885
+ if (element) context.drawImage(element, 0, 0, width, height);
1886
+ this.attachBacking(layerId, backing);
1887
+ }
1888
+ beginStroke(options) {
1889
+ this.assertActive();
1890
+ if (!this.context || !this.backing || !this.layerId) throw new Error("Attach a mask first");
1891
+ if (this.brush) throw new Error("A mask stroke is already active");
1892
+ if (!Number.isFinite(options.size) || options.size <= 0) {
1893
+ throw new Error("Mask brush size must be positive");
1894
+ }
1895
+ this.brush = { ...options, hardness: clamp(options.hardness, 0, 1) };
1896
+ this.previousPoint = null;
1897
+ this.strokeBackup = this.context.getImageData(0, 0, this.backing.width, this.backing.height);
1898
+ this.strokeStartedAt = performance.now();
1899
+ }
1900
+ addPoint(point) {
1901
+ if (!this.brush || !this.context || !this.backing || !this.layerId) {
1902
+ throw new Error("No active mask stroke");
1903
+ }
1904
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return;
1905
+ const previous = this.previousPoint ?? point;
1906
+ const distance = Math.hypot(point.x - previous.x, point.y - previous.y);
1907
+ const step = Math.max(1, this.brush.size / 4);
1908
+ const samples = Math.max(1, Math.ceil(distance / step));
1909
+ for (let index = 0; index <= samples; index += 1) {
1910
+ const ratio = index / samples;
1911
+ this.drawDot({
1912
+ x: previous.x + (point.x - previous.x) * ratio,
1913
+ y: previous.y + (point.y - previous.y) * ratio
1914
+ });
1915
+ }
1916
+ this.previousPoint = point;
1917
+ this.requireMask(this.layerId).fabricObject.setElement(this.backing);
1918
+ this.editor.canvas.requestRenderAll();
1919
+ }
1920
+ async endStroke() {
1921
+ if (!this.brush || !this.backing || !this.layerId) return;
1922
+ const layerId = this.layerId;
1923
+ this.brush = null;
1924
+ this.previousPoint = null;
1925
+ this.strokeBackup = null;
1926
+ const dataUrl = this.backing.toDataURL("image/png");
1927
+ await this.editor.history.transaction(async () => {
1928
+ await this.editor.replaceImageSource(layerId, dataUrl);
1929
+ const layer = this.requireMask(layerId);
1930
+ if (layer.meta.mask) layer.meta.mask.revision += 1;
1931
+ });
1932
+ this.attach(layerId);
1933
+ if (this.strokeStartedAt !== null) {
1934
+ this.lastInteractionLatencyMs = performance.now() - this.strokeStartedAt;
1935
+ this.strokeStartedAt = null;
1936
+ }
1937
+ }
1938
+ cancelStroke() {
1939
+ if (this.strokeBackup && this.context && this.backing && this.layerId) {
1940
+ this.context.putImageData(this.strokeBackup, 0, 0);
1941
+ this.requireMask(this.layerId).fabricObject.setElement(this.backing);
1942
+ this.editor.canvas.requestRenderAll();
1943
+ }
1944
+ this.brush = null;
1945
+ this.previousPoint = null;
1946
+ this.strokeBackup = null;
1947
+ this.strokeStartedAt = null;
1948
+ }
1949
+ isStrokeActive() {
1950
+ return this.brush !== null;
1951
+ }
1952
+ activeLayerId() {
1953
+ return this.layerId;
1954
+ }
1955
+ detach(layerId) {
1956
+ if (layerId && this.layerId !== layerId) return;
1957
+ this.cancelStroke();
1958
+ this.cancelRefinement();
1959
+ this.backing = null;
1960
+ this.context = null;
1961
+ this.layerId = null;
1962
+ }
1963
+ async refine(layerId, provider, prompts, options = {}) {
1964
+ this.assertActive();
1965
+ const layer = this.editor.layers.get(layerId);
1966
+ if (!layer || layer.type !== "mask") {
1967
+ throw new MaskRefinementError("not-found", `Mask layer not found: ${layerId}`);
1968
+ }
1969
+ this.cancelRefinement();
1970
+ const controller = new AbortController();
1971
+ this.refinement = controller;
1972
+ const abort = () => controller.abort(options.signal?.reason);
1973
+ options.signal?.addEventListener("abort", abort, { once: true });
1974
+ if (options.signal?.aborted) abort();
1975
+ try {
1976
+ const image = layer.fabricObject;
1977
+ const result = await provider.refine(
1978
+ {
1979
+ mask: image.getSrc(),
1980
+ width: layer.meta.mask?.width ?? image.width ?? 1,
1981
+ height: layer.meta.mask?.height ?? image.height ?? 1,
1982
+ prompts: structuredClone(prompts)
1983
+ },
1984
+ { signal: controller.signal, onProgress: options.onProgress }
1985
+ );
1986
+ if (controller.signal.aborted)
1987
+ throw new MaskRefinementError("cancelled", "Mask refinement cancelled");
1988
+ if (!/^data:image\/(?:png|jpeg|webp);base64,/i.test(result.dataUrl)) {
1989
+ throw new MaskRefinementError(
1990
+ "invalid-result",
1991
+ "Mask refinement must return a base64 PNG, JPEG, or WebP data URL"
1992
+ );
1993
+ }
1994
+ await this.editor.history.transaction(async () => {
1995
+ await this.editor.replaceImageSource(layerId, result.dataUrl);
1996
+ if (layer.meta.mask) layer.meta.mask.revision += 1;
1997
+ });
1998
+ this.attach(layerId);
1999
+ return result;
2000
+ } catch (error) {
2001
+ if (error instanceof MaskRefinementError) throw error;
2002
+ if (controller.signal.aborted) {
2003
+ throw new MaskRefinementError("cancelled", "Mask refinement cancelled", error);
2004
+ }
2005
+ throw new MaskRefinementError("provider", "Mask refinement provider failed", error);
2006
+ } finally {
2007
+ options.signal?.removeEventListener("abort", abort);
2008
+ if (this.refinement === controller) this.refinement = null;
2009
+ }
2010
+ }
2011
+ cancelRefinement() {
2012
+ this.refinement?.abort(new DOMException("Cancelled", "AbortError"));
2013
+ this.refinement = null;
2014
+ }
2015
+ measure() {
2016
+ if (!this.backing) return null;
2017
+ const started = performance.now();
2018
+ this.context?.getImageData(0, 0, 1, 1);
2019
+ const backingBytes = this.backing.width * this.backing.height * 4;
2020
+ const strokeBackupBytes = this.strokeBackup ? backingBytes : 0;
2021
+ const memory = performance;
2022
+ return {
2023
+ width: this.backing.width,
2024
+ height: this.backing.height,
2025
+ backingBytes,
2026
+ strokeBackupBytes,
2027
+ // The backing store and rollback ImageData dominate interactive mask
2028
+ // memory. Encoded historical rasters are reported separately below.
2029
+ estimatedPeakBytes: backingBytes + strokeBackupBytes,
2030
+ historyBytes: this.editor.history.getSnapshotBytes(),
2031
+ interactionLatencyMs: this.lastInteractionLatencyMs,
2032
+ ...typeof memory.memory?.usedJSHeapSize === "number" ? { usedJsHeapBytes: memory.memory.usedJSHeapSize } : {},
2033
+ elapsedMs: performance.now() - started
2034
+ };
2035
+ }
2036
+ dispose() {
2037
+ this.detach();
2038
+ this.disposed = true;
2039
+ }
2040
+ drawDot(point) {
2041
+ const context = this.context;
2042
+ const brush = this.brush;
2043
+ const radius = brush.size / 2;
2044
+ context.save();
2045
+ context.globalCompositeOperation = brush.mode === "subtract" ? "destination-out" : "source-over";
2046
+ const gradient = context.createRadialGradient(
2047
+ point.x,
2048
+ point.y,
2049
+ radius * brush.hardness,
2050
+ point.x,
2051
+ point.y,
2052
+ radius
2053
+ );
2054
+ const color = brush.mode === "subtract" ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
2055
+ gradient.addColorStop(0, color);
2056
+ gradient.addColorStop(1, "rgba(255,255,255,0)");
2057
+ context.fillStyle = gradient;
2058
+ context.beginPath();
2059
+ context.arc(point.x, point.y, radius, 0, Math.PI * 2);
2060
+ context.fill();
2061
+ context.restore();
2062
+ }
2063
+ makeCanvas(width, height) {
2064
+ const canvas = this.editor.canvas.lowerCanvasEl.ownerDocument.createElement("canvas");
2065
+ canvas.width = width;
2066
+ canvas.height = height;
2067
+ return canvas;
2068
+ }
2069
+ attachBacking(layerId, backing) {
2070
+ const context = backing.getContext("2d");
2071
+ if (!context) throw new Error("2D mask context is unavailable");
2072
+ this.layerId = layerId;
2073
+ this.backing = backing;
2074
+ this.context = context;
2075
+ }
2076
+ requireMask(layerId) {
2077
+ const layer = this.editor.layers.get(layerId);
2078
+ if (!layer || layer.type !== "mask") throw new Error(`Mask layer not found: ${layerId}`);
2079
+ return layer;
2080
+ }
2081
+ assertActive() {
2082
+ if (this.disposed) throw new Error("Mask controller has been disposed");
2083
+ }
2084
+ };
2085
+
1595
2086
  // src/editor.ts
1596
2087
  var MIN_ZOOM = 0.1;
1597
2088
  var MAX_ZOOM = 8;
2089
+ function isTaintedCanvasError(error) {
2090
+ return error instanceof DOMException && error.name === "SecurityError" || error instanceof Error && /taint|cross-origin|insecure/i.test(error.message);
2091
+ }
1598
2092
  var CanvasEditor = class {
1599
2093
  canvas;
1600
2094
  layers;
@@ -1607,6 +2101,7 @@ var CanvasEditor = class {
1607
2101
  fonts;
1608
2102
  licensing;
1609
2103
  pages;
2104
+ masks;
1610
2105
  fileAdapter;
1611
2106
  imageProvider;
1612
2107
  zoomLevel = 1;
@@ -1616,6 +2111,7 @@ var CanvasEditor = class {
1616
2111
  // for serialization and export — not the (possibly transient) canvas value.
1617
2112
  designBackground;
1618
2113
  designBackgroundImage = null;
2114
+ backgroundImageOptions = null;
1619
2115
  constructor(canvasElement, config) {
1620
2116
  this.events = new EventEmitter();
1621
2117
  this.fonts = new FontRegistry();
@@ -1625,7 +2121,7 @@ var CanvasEditor = class {
1625
2121
  const widthPx = this.units.toPixels(config.width);
1626
2122
  const heightPx = this.units.toPixels(config.height);
1627
2123
  this.designBackground = config.backgroundColor ?? "#ffffff";
1628
- this.canvas = new import_fabric4.Canvas(canvasElement, {
2124
+ this.canvas = new import_fabric6.Canvas(canvasElement, {
1629
2125
  width: widthPx,
1630
2126
  height: heightPx,
1631
2127
  backgroundColor: this.designBackground,
@@ -1656,11 +2152,12 @@ var CanvasEditor = class {
1656
2152
  this.setupCanvasEvents();
1657
2153
  this.history.saveImmediate();
1658
2154
  this.pages = new ProjectManager(this);
2155
+ this.masks = new MaskController(this);
1659
2156
  }
1660
2157
  // ─── Layer Operations ────────────────────────────────
1661
2158
  async addImage(url, options) {
1662
2159
  try {
1663
- const img = await import_fabric4.FabricImage.fromURL(
2160
+ const img = await import_fabric6.FabricImage.fromURL(
1664
2161
  url,
1665
2162
  {},
1666
2163
  { originX: "left", originY: "top", ...options }
@@ -1676,14 +2173,16 @@ var CanvasEditor = class {
1676
2173
  /** Replace an image source without changing its layer identity or visual transform. */
1677
2174
  async replaceImageSource(layerId, url) {
1678
2175
  const layer = this.layers.get(layerId);
1679
- if (!layer || layer.type !== "image") throw new Error(`Image layer not found: ${layerId}`);
2176
+ if (!layer || layer.type !== "image" && layer.type !== "mask") {
2177
+ throw new Error(`Image or mask layer not found: ${layerId}`);
2178
+ }
1680
2179
  if (layer.meta.pattern) {
1681
2180
  throw new Error("Clear the pattern before replacing the image source");
1682
2181
  }
1683
2182
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
1684
2183
  const previous = layer.fabricObject;
1685
2184
  try {
1686
- const replacement = await import_fabric4.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
2185
+ const replacement = await import_fabric6.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
1687
2186
  replacement.set({
1688
2187
  left: previous.left,
1689
2188
  top: previous.top,
@@ -1712,7 +2211,7 @@ var CanvasEditor = class {
1712
2211
  }
1713
2212
  }
1714
2213
  addText(text, options) {
1715
- const textbox = new import_fabric4.Textbox(text, {
2214
+ const textbox = new import_fabric6.Textbox(text, {
1716
2215
  fontSize: 32,
1717
2216
  fontFamily: "Arial",
1718
2217
  fill: "#000000",
@@ -1751,10 +2250,10 @@ var CanvasEditor = class {
1751
2250
  return value === void 0 ? token : escapeXml(value);
1752
2251
  })
1753
2252
  );
1754
- const { objects, options } = await (0, import_fabric4.loadSVGFromString)(resolved);
2253
+ const { objects, options } = await (0, import_fabric6.loadSVGFromString)(resolved);
1755
2254
  const validObjects = objects.filter((object) => object !== null);
1756
2255
  if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
1757
- const group = import_fabric4.util.groupSVGElements(validObjects, options);
2256
+ const group = import_fabric6.util.groupSVGElements(validObjects, options);
1758
2257
  group.set({
1759
2258
  left: this.canvas.getWidth() / 2,
1760
2259
  top: this.canvas.getHeight() / 2,
@@ -1767,6 +2266,9 @@ var CanvasEditor = class {
1767
2266
  }
1768
2267
  removeLayer(id) {
1769
2268
  if (this.crop.activeLayerId() === id) this.crop.cancel();
2269
+ if (this.masks.activeLayerId() === id) {
2270
+ this.masks.detach(id);
2271
+ }
1770
2272
  if (this.layers.remove(id)) this.history.save();
1771
2273
  }
1772
2274
  selectLayer(id) {
@@ -1824,10 +2326,10 @@ var CanvasEditor = class {
1824
2326
  const next = { ...previous, ...adjustments };
1825
2327
  const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
1826
2328
  image.filters = [
1827
- new import_fabric4.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
1828
- new import_fabric4.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
1829
- new import_fabric4.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
1830
- new import_fabric4.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
2329
+ new import_fabric6.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
2330
+ new import_fabric6.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
2331
+ new import_fabric6.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
2332
+ new import_fabric6.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
1831
2333
  ];
1832
2334
  layer.meta.imageAdjustments = next;
1833
2335
  image.applyFilters();
@@ -1844,7 +2346,7 @@ var CanvasEditor = class {
1844
2346
  const childData = children.map((layer) => structuredClone(layer.toData()));
1845
2347
  const objects = children.map((layer) => layer.fabricObject);
1846
2348
  for (const layer of children) this.layers.remove(layer.id);
1847
- const group = new import_fabric4.Group(objects);
2349
+ const group = new import_fabric6.Group(objects);
1848
2350
  const grouped = this.layers.add("group", group, name);
1849
2351
  grouped.meta.groupChildren = childData;
1850
2352
  this.layers.select(grouped.id);
@@ -1863,7 +2365,7 @@ var CanvasEditor = class {
1863
2365
  const objects = group.removeAll();
1864
2366
  this.layers.remove(id);
1865
2367
  const restored = objects.map((object, index) => {
1866
- import_fabric4.util.addTransformToObject(object, transform);
2368
+ import_fabric6.util.addTransformToObject(object, transform);
1867
2369
  object.setCoords();
1868
2370
  const data = childData[index];
1869
2371
  const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
@@ -1914,6 +2416,50 @@ var CanvasEditor = class {
1914
2416
  async toWebP(options) {
1915
2417
  return this.toRaster("webp", options);
1916
2418
  }
2419
+ /** Export one layer in document coordinates or at its native image resolution. */
2420
+ async exportLayer(id, options = {}) {
2421
+ const layer = this.layers.get(id);
2422
+ if (!layer) throw new Error(`Layer not found: ${id}`);
2423
+ try {
2424
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric6.FabricImage) {
2425
+ const image = await layer.fabricObject.clone();
2426
+ image.set({
2427
+ left: 0,
2428
+ top: 0,
2429
+ originX: "left",
2430
+ originY: "top",
2431
+ scaleX: 1,
2432
+ scaleY: 1,
2433
+ angle: 0,
2434
+ flipX: false,
2435
+ flipY: false
2436
+ });
2437
+ return await exportIsolatedPNG(this.canvas, [image], {
2438
+ ...options,
2439
+ width: image.width || 1,
2440
+ height: image.height || 1,
2441
+ cloneObjects: false
2442
+ });
2443
+ }
2444
+ return await exportIsolatedPNG(this.canvas, [layer.fabricObject], options);
2445
+ } catch (error) {
2446
+ this.events.emit("error", { message: `Failed to export layer: ${id}`, error });
2447
+ throw error;
2448
+ }
2449
+ }
2450
+ /** Export only the configured document background, excluding design layers. */
2451
+ async exportBackground(options = {}) {
2452
+ try {
2453
+ return await exportIsolatedPNG(this.canvas, [], {
2454
+ ...options,
2455
+ backgroundColor: this.designBackground,
2456
+ backgroundImage: this.designBackgroundImage
2457
+ });
2458
+ } catch (error) {
2459
+ this.events.emit("error", { message: "Failed to export background", error });
2460
+ throw error;
2461
+ }
2462
+ }
1917
2463
  async toRaster(format, options) {
1918
2464
  this.events.emit("export:start", { format });
1919
2465
  try {
@@ -1925,7 +2471,10 @@ var CanvasEditor = class {
1925
2471
  this.licensing.track(`export:${format}`);
1926
2472
  return blob;
1927
2473
  } catch (error) {
1928
- this.events.emit("error", { message: `Failed to export ${format.toUpperCase()}`, error });
2474
+ this.events.emit("error", {
2475
+ message: isTaintedCanvasError(error) ? "Canvas export was blocked by cross-origin image data; load remote images with CORS enabled" : `Failed to export ${format.toUpperCase()}`,
2476
+ error
2477
+ });
1929
2478
  throw error;
1930
2479
  }
1931
2480
  }
@@ -2118,13 +2667,20 @@ var CanvasEditor = class {
2118
2667
  getDesignBackgroundImage() {
2119
2668
  return this.designBackgroundImage;
2120
2669
  }
2670
+ getBackgroundImageOptions() {
2671
+ return this.backgroundImageOptions ? { ...this.backgroundImageOptions } : null;
2672
+ }
2121
2673
  async setBackgroundImage(url, options = {}) {
2122
2674
  if (url === null) {
2123
2675
  this.setBackgroundImageObject(null);
2124
2676
  return;
2125
2677
  }
2126
2678
  try {
2127
- const image = await import_fabric4.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
2679
+ const image = await import_fabric6.FabricImage.fromURL(
2680
+ url,
2681
+ { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
2682
+ { originX: "left", originY: "top" }
2683
+ );
2128
2684
  const width = image.width || 1;
2129
2685
  const height = image.height || 1;
2130
2686
  const canvasWidth = this.canvas.getWidth();
@@ -2143,15 +2699,18 @@ var CanvasEditor = class {
2143
2699
  selectable: false,
2144
2700
  evented: false
2145
2701
  });
2146
- this.setBackgroundImageObject(image);
2702
+ const serializableOptions = { ...options };
2703
+ delete serializableOptions.signal;
2704
+ this.setBackgroundImageObject(image, true, serializableOptions);
2147
2705
  } catch (error) {
2148
2706
  this.events.emit("error", { message: "Failed to set background image", error });
2149
2707
  throw error;
2150
2708
  }
2151
2709
  }
2152
2710
  /** Used by state restoration and advanced integrations with an existing Fabric object. */
2153
- setBackgroundImageObject(image, save = true) {
2711
+ setBackgroundImageObject(image, save = true, options = null) {
2154
2712
  this.designBackgroundImage = image;
2713
+ this.backgroundImageOptions = image ? options : null;
2155
2714
  this.canvas.backgroundImage = this.mockup ? void 0 : image ?? void 0;
2156
2715
  this.canvas.requestRenderAll();
2157
2716
  if (save) this.history.save();
@@ -2309,6 +2868,7 @@ var CanvasEditor = class {
2309
2868
  }
2310
2869
  // ─── Cleanup ────────────────────────────────────────
2311
2870
  dispose() {
2871
+ this.masks.dispose();
2312
2872
  this.snapping.dispose();
2313
2873
  this.crop.dispose();
2314
2874
  this.history.dispose();
@@ -2363,8 +2923,50 @@ var CANVAS_SIZE_PRESETS = [
2363
2923
  { id: "instagram-square", name: "Social square", width: 1080, height: 1080, unit: "px", dpi: 72 },
2364
2924
  { id: "story", name: "Story", width: 1080, height: 1920, unit: "px", dpi: 72 }
2365
2925
  ];
2926
+
2927
+ // src/annotations.ts
2928
+ var AnnotationOverlay = class {
2929
+ items = /* @__PURE__ */ new Map();
2930
+ transform = { zoom: 1, panX: 0, panY: 0, devicePixelRatio: 1 };
2931
+ set(annotation) {
2932
+ this.items.set(annotation.id, structuredClone(annotation));
2933
+ }
2934
+ remove(id) {
2935
+ return this.items.delete(id);
2936
+ }
2937
+ clear() {
2938
+ this.items.clear();
2939
+ }
2940
+ getAll() {
2941
+ return [...this.items.values()].map((item) => structuredClone(item));
2942
+ }
2943
+ setTransform(transform) {
2944
+ if (!Number.isFinite(transform.zoom) || transform.zoom <= 0) {
2945
+ throw new Error("Annotation zoom must be positive");
2946
+ }
2947
+ this.transform = { ...transform, devicePixelRatio: transform.devicePixelRatio ?? 1 };
2948
+ }
2949
+ documentToViewport(point) {
2950
+ return {
2951
+ x: point.x * this.transform.zoom + this.transform.panX,
2952
+ y: point.y * this.transform.zoom + this.transform.panY
2953
+ };
2954
+ }
2955
+ viewportToDocument(point) {
2956
+ return {
2957
+ x: (point.x - this.transform.panX) / this.transform.zoom,
2958
+ y: (point.y - this.transform.panY) / this.transform.zoom
2959
+ };
2960
+ }
2961
+ documentToDevice(point) {
2962
+ const viewport = this.documentToViewport(point);
2963
+ const ratio = this.transform.devicePixelRatio ?? 1;
2964
+ return { x: viewport.x * ratio, y: viewport.y * ratio };
2965
+ }
2966
+ };
2366
2967
  // Annotate the CommonJS export names for ESM import in node:
2367
2968
  0 && (module.exports = {
2969
+ AnnotationOverlay,
2368
2970
  CANVAS_SIZE_PRESETS,
2369
2971
  CanvasEditor,
2370
2972
  CropController,
@@ -2375,6 +2977,8 @@ var CANVAS_SIZE_PRESETS = [
2375
2977
  Layer,
2376
2978
  LayerManager,
2377
2979
  LicenseManager,
2980
+ MaskController,
2981
+ MaskRefinementError,
2378
2982
  PatternManager,
2379
2983
  ProjectManager,
2380
2984
  SnapManager,
@@ -2388,6 +2992,7 @@ var CANVAS_SIZE_PRESETS = [
2388
2992
  computePrintAreaClip,
2389
2993
  computeTilePositions,
2390
2994
  deserializeEditor,
2995
+ displaceRgba,
2391
2996
  drawTiles,
2392
2997
  escapeXml,
2393
2998
  exportDataURL,