@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/chunk-ORZZ6MGQ.mjs +228 -0
- package/dist/chunk-ORZZ6MGQ.mjs.map +1 -0
- package/dist/index.d.mts +157 -4
- package/dist/index.d.ts +157 -4
- package/dist/index.global.js +59 -59
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +660 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +500 -21
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +51 -2
- package/dist/node.d.ts +51 -2
- package/dist/node.js +546 -13
- package/dist/node.js.map +1 -1
- package/dist/node.mjs +446 -4
- package/dist/node.mjs.map +1 -1
- package/dist/{types-bOY7oYVq.d.mts → types-D60CfxL9.d.mts} +80 -2
- package/dist/{types-bOY7oYVq.d.ts → types-D60CfxL9.d.ts} +80 -2
- package/package.json +37 -2
- package/dist/chunk-NINRTPOJ.mjs +0 -104
- package/dist/chunk-NINRTPOJ.mjs.map +0 -1
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
1828
|
-
new
|
|
1829
|
-
new
|
|
1830
|
-
new
|
|
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
|
|
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
|
-
|
|
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", {
|
|
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
|
|
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
|
-
|
|
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,
|