@overtone-art/canvas-editor-core 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-MCBRZQ4M.mjs +261 -0
- package/dist/chunk-MCBRZQ4M.mjs.map +1 -0
- package/dist/index.d.mts +326 -5
- package/dist/index.d.ts +326 -5
- package/dist/index.global.js +71 -71
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1350 -72
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1136 -35
- 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-Dh1unrzT.d.mts} +107 -2
- package/dist/{types-bOY7oYVq.d.ts → types-Dh1unrzT.d.ts} +107 -2
- package/package.json +42 -5
- package/dist/chunk-NINRTPOJ.mjs +0 -104
- package/dist/chunk-NINRTPOJ.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -20,47 +20,71 @@ 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,
|
|
27
|
+
DEFAULT_LAYER_SHADOW: () => DEFAULT_LAYER_SHADOW,
|
|
26
28
|
DEFAULT_PATTERN_CONFIG: () => DEFAULT_PATTERN_CONFIG,
|
|
29
|
+
DEFAULT_TEXT_CURVE: () => DEFAULT_TEXT_CURVE,
|
|
27
30
|
EventEmitter: () => EventEmitter,
|
|
28
31
|
FontRegistry: () => FontRegistry,
|
|
29
32
|
HistoryManager: () => HistoryManager,
|
|
30
33
|
Layer: () => Layer,
|
|
31
34
|
LayerManager: () => LayerManager,
|
|
32
35
|
LicenseManager: () => LicenseManager,
|
|
36
|
+
MaskController: () => MaskController,
|
|
37
|
+
MaskPresetManager: () => MaskPresetManager,
|
|
38
|
+
MaskRefinementError: () => MaskRefinementError,
|
|
33
39
|
PatternManager: () => PatternManager,
|
|
34
40
|
ProjectManager: () => ProjectManager,
|
|
41
|
+
SHAPE_MASK_BOX: () => SHAPE_MASK_BOX,
|
|
42
|
+
SHAPE_MASK_IDS: () => SHAPE_MASK_IDS,
|
|
35
43
|
SnapManager: () => SnapManager,
|
|
44
|
+
TEXTURE_MASK_IDS: () => TEXTURE_MASK_IDS,
|
|
45
|
+
TEXTURE_MASK_SIZE: () => TEXTURE_MASK_SIZE,
|
|
46
|
+
TextCurveManager: () => TextCurveManager,
|
|
36
47
|
UnitConverter: () => UnitConverter,
|
|
48
|
+
applyAspectLock: () => applyAspectLock,
|
|
49
|
+
applyLayerShadow: () => applyLayerShadow,
|
|
37
50
|
applyPatternLocks: () => applyPatternLocks,
|
|
51
|
+
buildCurvePathData: () => buildCurvePathData,
|
|
38
52
|
buildPatternDataURL: () => buildPatternDataURL,
|
|
39
53
|
captureLocks: () => captureLocks,
|
|
40
54
|
clamp: () => clamp,
|
|
41
55
|
clearPatternImageCache: () => clearPatternImageCache,
|
|
56
|
+
clearTextureMaskCache: () => clearTextureMaskCache,
|
|
42
57
|
computeCoverPlacement: () => computeCoverPlacement,
|
|
43
58
|
computePrintAreaClip: () => computePrintAreaClip,
|
|
44
59
|
computeTilePositions: () => computeTilePositions,
|
|
45
60
|
deserializeEditor: () => deserializeEditor,
|
|
61
|
+
displaceRgba: () => displaceRgba,
|
|
46
62
|
drawTiles: () => drawTiles,
|
|
47
63
|
escapeXml: () => escapeXml,
|
|
48
64
|
exportDataURL: () => exportDataURL,
|
|
49
65
|
exportMockup: () => exportMockup,
|
|
50
66
|
exportPNG: () => exportPNG,
|
|
67
|
+
exportPrintArea: () => exportPrintArea,
|
|
51
68
|
exportSVG: () => exportSVG,
|
|
52
69
|
generateId: () => generateId,
|
|
53
70
|
isCssColor: () => isCssColor,
|
|
71
|
+
isMaskPresetId: () => isMaskPresetId,
|
|
72
|
+
isShapeMaskId: () => isShapeMaskId,
|
|
73
|
+
isTextureMaskId: () => isTextureMaskId,
|
|
54
74
|
loadPatternImage: () => loadPatternImage,
|
|
75
|
+
readLayerShadow: () => readLayerShadow,
|
|
76
|
+
renderTextureMask: () => renderTextureMask,
|
|
77
|
+
resetTransform: () => resetTransform,
|
|
55
78
|
restoreLocks: () => restoreLocks,
|
|
56
79
|
round2: () => round2,
|
|
57
80
|
sanitizeSvg: () => sanitizeSvg,
|
|
58
|
-
serializeEditor: () => serializeEditor
|
|
81
|
+
serializeEditor: () => serializeEditor,
|
|
82
|
+
shapeMaskPathData: () => shapeMaskPathData
|
|
59
83
|
});
|
|
60
84
|
module.exports = __toCommonJS(index_exports);
|
|
61
85
|
|
|
62
86
|
// src/editor.ts
|
|
63
|
-
var
|
|
87
|
+
var import_fabric9 = require("fabric");
|
|
64
88
|
|
|
65
89
|
// src/events.ts
|
|
66
90
|
var EventEmitter = class {
|
|
@@ -298,9 +322,13 @@ var LayerManager = class {
|
|
|
298
322
|
};
|
|
299
323
|
|
|
300
324
|
// src/history.ts
|
|
301
|
-
var HistoryManager = class {
|
|
325
|
+
var HistoryManager = class _HistoryManager {
|
|
326
|
+
static ASSET_KEY = "__canvasEditorHistoryAsset";
|
|
302
327
|
undoStack = [];
|
|
303
328
|
redoStack = [];
|
|
329
|
+
assets = /* @__PURE__ */ new Map();
|
|
330
|
+
assetIds = /* @__PURE__ */ new Map();
|
|
331
|
+
nextAssetId = 1;
|
|
304
332
|
maxSize;
|
|
305
333
|
maxBytes;
|
|
306
334
|
paused = false;
|
|
@@ -343,7 +371,8 @@ var HistoryManager = class {
|
|
|
343
371
|
return;
|
|
344
372
|
}
|
|
345
373
|
this.cancelPending();
|
|
346
|
-
const
|
|
374
|
+
const rawState = this.getState();
|
|
375
|
+
const state = this.compactState(rawState);
|
|
347
376
|
if (this.undoStack.at(-1) === state) {
|
|
348
377
|
this.emitChanged();
|
|
349
378
|
return;
|
|
@@ -355,7 +384,7 @@ var HistoryManager = class {
|
|
|
355
384
|
this.redoStack = [];
|
|
356
385
|
this.trimToBudget();
|
|
357
386
|
this.events.emit("history:snapshot", {
|
|
358
|
-
bytes:
|
|
387
|
+
bytes: rawState.length * 2,
|
|
359
388
|
totalBytes: this.snapshotBytes(),
|
|
360
389
|
entries: this.undoStack.length
|
|
361
390
|
});
|
|
@@ -383,15 +412,15 @@ var HistoryManager = class {
|
|
|
383
412
|
}
|
|
384
413
|
async undo() {
|
|
385
414
|
this.cancelPending();
|
|
386
|
-
const current = this.getState();
|
|
387
415
|
const committed = this.undoStack.at(-1);
|
|
388
416
|
if (!committed) return;
|
|
417
|
+
const current = this.compactState(this.getState());
|
|
389
418
|
const currentIsCommitted = current === committed;
|
|
390
419
|
if (currentIsCommitted && this.undoStack.length < 2) return;
|
|
391
420
|
const target = currentIsCommitted ? this.undoStack[this.undoStack.length - 2] : committed;
|
|
392
421
|
this.paused = true;
|
|
393
422
|
try {
|
|
394
|
-
await this.restoreState(target);
|
|
423
|
+
await this.restoreState(this.expandState(target));
|
|
395
424
|
if (currentIsCommitted) this.undoStack.pop();
|
|
396
425
|
this.redoStack.push(current);
|
|
397
426
|
this.trimToBudget();
|
|
@@ -409,7 +438,7 @@ var HistoryManager = class {
|
|
|
409
438
|
if (!state) return;
|
|
410
439
|
this.paused = true;
|
|
411
440
|
try {
|
|
412
|
-
await this.restoreState(state);
|
|
441
|
+
await this.restoreState(this.expandState(state));
|
|
413
442
|
this.redoStack.pop();
|
|
414
443
|
if (this.undoStack.at(-1) !== state) this.undoStack.push(state);
|
|
415
444
|
this.trimToBudget();
|
|
@@ -442,6 +471,8 @@ var HistoryManager = class {
|
|
|
442
471
|
this.cancelPending();
|
|
443
472
|
this.undoStack = [];
|
|
444
473
|
this.redoStack = [];
|
|
474
|
+
this.assets.clear();
|
|
475
|
+
this.assetIds.clear();
|
|
445
476
|
this.emitChanged();
|
|
446
477
|
}
|
|
447
478
|
getSnapshotBytes() {
|
|
@@ -457,6 +488,10 @@ var HistoryManager = class {
|
|
|
457
488
|
this.cancelPending();
|
|
458
489
|
this.transactionDepth = 0;
|
|
459
490
|
this.transactionDirty = false;
|
|
491
|
+
this.undoStack = [];
|
|
492
|
+
this.redoStack = [];
|
|
493
|
+
this.assets.clear();
|
|
494
|
+
this.assetIds.clear();
|
|
460
495
|
}
|
|
461
496
|
emitChanged() {
|
|
462
497
|
this.events.emit("history:changed", {
|
|
@@ -465,17 +500,91 @@ var HistoryManager = class {
|
|
|
465
500
|
});
|
|
466
501
|
}
|
|
467
502
|
snapshotBytes() {
|
|
468
|
-
|
|
503
|
+
const stackBytes = [...this.undoStack, ...this.redoStack].reduce(
|
|
469
504
|
(total, state) => total + state.length * 2,
|
|
470
505
|
0
|
|
471
506
|
);
|
|
507
|
+
const assetBytes = [...this.assets.values()].reduce(
|
|
508
|
+
(total, asset) => total + asset.length * 2,
|
|
509
|
+
0
|
|
510
|
+
);
|
|
511
|
+
return stackBytes + assetBytes;
|
|
472
512
|
}
|
|
473
513
|
trimToBudget() {
|
|
514
|
+
this.pruneAssets();
|
|
474
515
|
while (this.undoStack.length > 1 && this.snapshotBytes() > this.maxBytes) {
|
|
475
516
|
this.undoStack.shift();
|
|
517
|
+
this.pruneAssets();
|
|
476
518
|
}
|
|
477
519
|
while (this.redoStack.length > 1 && this.snapshotBytes() > this.maxBytes) {
|
|
478
520
|
this.redoStack.shift();
|
|
521
|
+
this.pruneAssets();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* History is internal and can content-address large raster strings without
|
|
526
|
+
* changing the public EditorState wire format. Unchanged images are retained
|
|
527
|
+
* once even when dozens of snapshots reference them.
|
|
528
|
+
*/
|
|
529
|
+
compactState(state) {
|
|
530
|
+
let parsed;
|
|
531
|
+
try {
|
|
532
|
+
parsed = JSON.parse(state);
|
|
533
|
+
} catch {
|
|
534
|
+
return state;
|
|
535
|
+
}
|
|
536
|
+
const visit = (value) => {
|
|
537
|
+
if (typeof value === "string" && /^data:image\/(?:png|jpeg|webp);base64,/i.test(value)) {
|
|
538
|
+
let id = this.assetIds.get(value);
|
|
539
|
+
if (!id) {
|
|
540
|
+
id = `a${this.nextAssetId++}`;
|
|
541
|
+
this.assetIds.set(value, id);
|
|
542
|
+
this.assets.set(id, value);
|
|
543
|
+
}
|
|
544
|
+
return { [_HistoryManager.ASSET_KEY]: id };
|
|
545
|
+
}
|
|
546
|
+
if (Array.isArray(value)) return value.map(visit);
|
|
547
|
+
if (!value || typeof value !== "object") return value;
|
|
548
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, visit(entry)]));
|
|
549
|
+
};
|
|
550
|
+
return JSON.stringify(visit(parsed));
|
|
551
|
+
}
|
|
552
|
+
expandState(state) {
|
|
553
|
+
let parsed;
|
|
554
|
+
try {
|
|
555
|
+
parsed = JSON.parse(state);
|
|
556
|
+
} catch {
|
|
557
|
+
return state;
|
|
558
|
+
}
|
|
559
|
+
const visit = (value) => {
|
|
560
|
+
if (Array.isArray(value)) return value.map(visit);
|
|
561
|
+
if (!value || typeof value !== "object") return value;
|
|
562
|
+
const record = value;
|
|
563
|
+
const id = record[_HistoryManager.ASSET_KEY];
|
|
564
|
+
if (typeof id === "string" && Object.keys(record).length === 1) {
|
|
565
|
+
const asset = this.assets.get(id);
|
|
566
|
+
if (!asset) throw new Error(`Missing history raster asset: ${id}`);
|
|
567
|
+
return asset;
|
|
568
|
+
}
|
|
569
|
+
return Object.fromEntries(Object.entries(record).map(([key, entry]) => [key, visit(entry)]));
|
|
570
|
+
};
|
|
571
|
+
return JSON.stringify(visit(parsed));
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Runs on every commit, so it scans for the serialized reference marker
|
|
575
|
+
* instead of re-parsing every snapshot — parsing multi-megabyte raster states
|
|
576
|
+
* per save would cost more than the retention it reclaims. `JSON.stringify`
|
|
577
|
+
* emits the marker verbatim; the same text inside user data is escaped and
|
|
578
|
+
* therefore cannot match.
|
|
579
|
+
*/
|
|
580
|
+
pruneAssets() {
|
|
581
|
+
if (this.assets.size === 0) return;
|
|
582
|
+
const states = [...this.undoStack, ...this.redoStack];
|
|
583
|
+
for (const [id, asset] of this.assets) {
|
|
584
|
+
const marker = `"${_HistoryManager.ASSET_KEY}":"${id}"`;
|
|
585
|
+
if (states.some((state) => state.includes(marker))) continue;
|
|
586
|
+
this.assets.delete(id);
|
|
587
|
+
this.assetIds.delete(asset);
|
|
479
588
|
}
|
|
480
589
|
}
|
|
481
590
|
};
|
|
@@ -1053,6 +1162,521 @@ function mod2(n) {
|
|
|
1053
1162
|
return (n % 2 + 2) % 2;
|
|
1054
1163
|
}
|
|
1055
1164
|
|
|
1165
|
+
// src/text-curve.ts
|
|
1166
|
+
var import_fabric3 = require("fabric");
|
|
1167
|
+
var DEFAULT_TEXT_CURVE = { arc: 0, wave: 0 };
|
|
1168
|
+
var MIN_ARC = 0.5;
|
|
1169
|
+
var FULL_CIRCLE_ARC = 99.5;
|
|
1170
|
+
var MIN_SWEEP = 0.12;
|
|
1171
|
+
var WAVE_PERIOD_EM = 4.1;
|
|
1172
|
+
var WAVE_AMPLITUDE_EM = 0.9;
|
|
1173
|
+
var WAVE_STEP = 6;
|
|
1174
|
+
var MEASURE_WIDTH = 1e5;
|
|
1175
|
+
var PATH_SLACK = 0.06;
|
|
1176
|
+
function isCurvable(object) {
|
|
1177
|
+
return !!object && typeof object.text === "string";
|
|
1178
|
+
}
|
|
1179
|
+
function measureText(text) {
|
|
1180
|
+
const authored = text.width;
|
|
1181
|
+
try {
|
|
1182
|
+
text.set({ width: MEASURE_WIDTH });
|
|
1183
|
+
text.initDimensions?.();
|
|
1184
|
+
return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
|
|
1185
|
+
} finally {
|
|
1186
|
+
if (authored !== void 0) text.set({ width: authored });
|
|
1187
|
+
text.initDimensions?.();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function arcPathData(width, arc) {
|
|
1191
|
+
const magnitude = Math.min(100, Math.abs(arc));
|
|
1192
|
+
const direction = arc < 0 ? -1 : 1;
|
|
1193
|
+
const full = magnitude >= FULL_CIRCLE_ARC;
|
|
1194
|
+
const sweep = full ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
|
|
1195
|
+
const radius = width / sweep;
|
|
1196
|
+
const centerX = width / 2;
|
|
1197
|
+
const point = (angle) => [
|
|
1198
|
+
centerX + radius * Math.sin(angle),
|
|
1199
|
+
direction * radius * (1 - Math.cos(angle))
|
|
1200
|
+
];
|
|
1201
|
+
const sweepFlag = direction > 0 ? 1 : 0;
|
|
1202
|
+
const format = ([x, y]) => `${round(x)} ${round(y)}`;
|
|
1203
|
+
if (full) {
|
|
1204
|
+
const start2 = point(-Math.PI);
|
|
1205
|
+
const top = point(0);
|
|
1206
|
+
return {
|
|
1207
|
+
data: [
|
|
1208
|
+
`M ${format(start2)}`,
|
|
1209
|
+
`A ${round(radius)} ${round(radius)} 0 0 ${sweepFlag} ${format(top)}`,
|
|
1210
|
+
`A ${round(radius)} ${round(radius)} 0 0 ${sweepFlag} ${format(start2)}`
|
|
1211
|
+
].join(" "),
|
|
1212
|
+
length: width
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
const start = point(-sweep / 2);
|
|
1216
|
+
const end = point(sweep / 2);
|
|
1217
|
+
const largeArc = sweep > Math.PI ? 1 : 0;
|
|
1218
|
+
return {
|
|
1219
|
+
data: `M ${format(start)} A ${round(radius)} ${round(radius)} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
|
|
1220
|
+
// radius = width / sweep, so the arc is exactly `width` long.
|
|
1221
|
+
length: width
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
function wavePathData(width, fontSize, wave) {
|
|
1225
|
+
const amplitude = clamp(wave, 0, 100) / 100 * fontSize * WAVE_AMPLITUDE_EM;
|
|
1226
|
+
const period = Math.max(1, fontSize * WAVE_PERIOD_EM);
|
|
1227
|
+
const steps = Math.max(2, Math.ceil(width / WAVE_STEP));
|
|
1228
|
+
const commands = [];
|
|
1229
|
+
let length = 0;
|
|
1230
|
+
let previous = null;
|
|
1231
|
+
for (let index = 0; index <= steps; index++) {
|
|
1232
|
+
const x = width * index / steps;
|
|
1233
|
+
const y = amplitude * Math.sin(x / period * Math.PI * 2);
|
|
1234
|
+
if (previous) length += Math.hypot(x - previous[0], y - previous[1]);
|
|
1235
|
+
previous = [x, y];
|
|
1236
|
+
commands.push(`${index === 0 ? "M" : "L"} ${round(x)} ${round(y)}`);
|
|
1237
|
+
}
|
|
1238
|
+
return { data: commands.join(" "), length };
|
|
1239
|
+
}
|
|
1240
|
+
function round(value) {
|
|
1241
|
+
return Math.round(value * 100) / 100;
|
|
1242
|
+
}
|
|
1243
|
+
function buildCurvePathData(config, width, fontSize) {
|
|
1244
|
+
if (Math.abs(config.arc) >= MIN_ARC) return arcPathData(width, config.arc);
|
|
1245
|
+
if (config.wave > 0) return wavePathData(width, fontSize, config.wave);
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
function normalize(config) {
|
|
1249
|
+
const arc = clamp(config.arc ?? 0, -100, 100);
|
|
1250
|
+
return { arc, wave: Math.abs(arc) >= MIN_ARC ? 0 : clamp(config.wave ?? 0, 0, 100) };
|
|
1251
|
+
}
|
|
1252
|
+
var TextCurveManager = class {
|
|
1253
|
+
constructor(canvas, layers, history, events) {
|
|
1254
|
+
this.canvas = canvas;
|
|
1255
|
+
this.layers = layers;
|
|
1256
|
+
this.history = history;
|
|
1257
|
+
this.events = events;
|
|
1258
|
+
}
|
|
1259
|
+
canvas;
|
|
1260
|
+
layers;
|
|
1261
|
+
history;
|
|
1262
|
+
events;
|
|
1263
|
+
/** Curve parameters for a layer, or null when it is not curved text. */
|
|
1264
|
+
get(layerId) {
|
|
1265
|
+
const layer = this.layers.get(layerId);
|
|
1266
|
+
if (!layer || !isCurvable(layer.fabricObject)) return null;
|
|
1267
|
+
return layer.meta.curve ?? { ...DEFAULT_TEXT_CURVE };
|
|
1268
|
+
}
|
|
1269
|
+
isCurved(layerId) {
|
|
1270
|
+
const curve = this.get(layerId);
|
|
1271
|
+
return !!curve && (Math.abs(curve.arc) >= MIN_ARC || curve.wave > 0);
|
|
1272
|
+
}
|
|
1273
|
+
/** Apply (or update) the curve on a text layer. Zeroed config clears it. */
|
|
1274
|
+
apply(layerId, config, save = true) {
|
|
1275
|
+
const layer = this.layers.get(layerId);
|
|
1276
|
+
if (!layer || !isCurvable(layer.fabricObject)) return false;
|
|
1277
|
+
const next = normalize(config);
|
|
1278
|
+
const text = layer.fabricObject;
|
|
1279
|
+
const run = measureText(text);
|
|
1280
|
+
const curve = buildCurvePathData(next, run * (1 + PATH_SLACK), text.fontSize);
|
|
1281
|
+
if (!curve) {
|
|
1282
|
+
this.detach(text, layer.meta.curveWidth);
|
|
1283
|
+
delete layer.meta.curve;
|
|
1284
|
+
delete layer.meta.curveWidth;
|
|
1285
|
+
} else {
|
|
1286
|
+
if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
|
|
1287
|
+
text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
|
|
1288
|
+
text.set({
|
|
1289
|
+
path: new import_fabric3.Path(curve.data, { visible: false, objectCaching: false }),
|
|
1290
|
+
pathAlign: "center",
|
|
1291
|
+
pathSide: "left",
|
|
1292
|
+
pathStartOffset: Math.max(0, (curve.length - run) / 2)
|
|
1293
|
+
});
|
|
1294
|
+
layer.meta.curve = next;
|
|
1295
|
+
}
|
|
1296
|
+
text.initDimensions?.();
|
|
1297
|
+
text.setCoords();
|
|
1298
|
+
text.dirty = true;
|
|
1299
|
+
this.canvas.requestRenderAll();
|
|
1300
|
+
this.events.emit("layer:modified", { layerId });
|
|
1301
|
+
if (save) this.history.save();
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
/** Remove the curve, restoring the authored text box width. */
|
|
1305
|
+
clear(layerId, save = true) {
|
|
1306
|
+
return this.apply(layerId, DEFAULT_TEXT_CURVE, save);
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Rebuild the path from the stored parameters. Text content, font family and
|
|
1310
|
+
* font size all change the run's width, and the path is sized to that width —
|
|
1311
|
+
* without this the curve keeps the geometry of the text it was created from.
|
|
1312
|
+
*/
|
|
1313
|
+
refresh(layerId, save = false) {
|
|
1314
|
+
const curve = this.layers.get(layerId)?.meta.curve;
|
|
1315
|
+
if (!curve) return false;
|
|
1316
|
+
return this.apply(layerId, curve, save);
|
|
1317
|
+
}
|
|
1318
|
+
/** Rebuild every curved layer — used after a state restore. */
|
|
1319
|
+
refreshAll() {
|
|
1320
|
+
for (const layer of this.layers.getAll()) {
|
|
1321
|
+
if (layer.meta.curve) this.refresh(layer.id);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
detach(text, authoredWidth) {
|
|
1325
|
+
text.set({ path: null, pathStartOffset: 0 });
|
|
1326
|
+
if (authoredWidth !== void 0 && authoredWidth > 0) text.set({ width: authoredWidth });
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
// src/mask-presets/manager.ts
|
|
1331
|
+
var import_fabric4 = require("fabric");
|
|
1332
|
+
|
|
1333
|
+
// src/mask-presets/shapes.ts
|
|
1334
|
+
var SHAPE_MASK_IDS = [
|
|
1335
|
+
"circle",
|
|
1336
|
+
"square",
|
|
1337
|
+
"triangle",
|
|
1338
|
+
"star",
|
|
1339
|
+
"heart",
|
|
1340
|
+
"octagram",
|
|
1341
|
+
"arch",
|
|
1342
|
+
"zigzag"
|
|
1343
|
+
];
|
|
1344
|
+
function isShapeMaskId(value) {
|
|
1345
|
+
return typeof value === "string" && SHAPE_MASK_IDS.includes(value);
|
|
1346
|
+
}
|
|
1347
|
+
function starPoints(points, outer, inner, start = -Math.PI / 2) {
|
|
1348
|
+
const step = Math.PI / points;
|
|
1349
|
+
const coordinates = [];
|
|
1350
|
+
for (let index = 0; index < points * 2; index++) {
|
|
1351
|
+
const radius = index % 2 === 0 ? outer : inner;
|
|
1352
|
+
const angle = start + index * step;
|
|
1353
|
+
const x = 50 + Math.cos(angle) * radius;
|
|
1354
|
+
const y = 50 + Math.sin(angle) * radius;
|
|
1355
|
+
coordinates.push(`${index === 0 ? "M" : "L"} ${round3(x)} ${round3(y)}`);
|
|
1356
|
+
}
|
|
1357
|
+
return `${coordinates.join(" ")} Z`;
|
|
1358
|
+
}
|
|
1359
|
+
function round3(value) {
|
|
1360
|
+
return Math.round(value * 100) / 100;
|
|
1361
|
+
}
|
|
1362
|
+
var SHAPE_PATHS = {
|
|
1363
|
+
circle: "M 96 50 A 46 46 0 1 1 4 50 A 46 46 0 1 1 96 50 Z",
|
|
1364
|
+
square: "M 4 4 H 96 V 96 H 4 Z",
|
|
1365
|
+
triangle: "M 50 4 L 96 96 L 4 96 Z",
|
|
1366
|
+
star: starPoints(5, 46, 22),
|
|
1367
|
+
heart: "M 50 96 C -8 55 12 4 50 28 C 88 4 108 55 50 96 Z",
|
|
1368
|
+
octagram: starPoints(8, 46, 27),
|
|
1369
|
+
arch: "M 4 96 V 50 A 46 46 0 0 1 96 50 V 96 Z",
|
|
1370
|
+
zigzag: starPoints(16, 46, 39)
|
|
1371
|
+
};
|
|
1372
|
+
function shapeMaskPathData(id) {
|
|
1373
|
+
return SHAPE_PATHS[id];
|
|
1374
|
+
}
|
|
1375
|
+
var SHAPE_MASK_BOX = 100;
|
|
1376
|
+
|
|
1377
|
+
// src/mask-presets/textures.ts
|
|
1378
|
+
var TEXTURE_MASK_IDS = [
|
|
1379
|
+
"vignette",
|
|
1380
|
+
"halftone",
|
|
1381
|
+
"spray",
|
|
1382
|
+
"grunge",
|
|
1383
|
+
"torn",
|
|
1384
|
+
"band"
|
|
1385
|
+
];
|
|
1386
|
+
function isTextureMaskId(value) {
|
|
1387
|
+
return typeof value === "string" && TEXTURE_MASK_IDS.includes(value);
|
|
1388
|
+
}
|
|
1389
|
+
var TEXTURE_MASK_SIZE = 320;
|
|
1390
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1391
|
+
function seeded(seed) {
|
|
1392
|
+
let state = seed;
|
|
1393
|
+
return () => {
|
|
1394
|
+
state = state * 16807 % 2147483647;
|
|
1395
|
+
return (state - 1) / 2147483646;
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
function traceRoughEdge(context, random, jitter, inset) {
|
|
1399
|
+
const size = TEXTURE_MASK_SIZE;
|
|
1400
|
+
const corners = [
|
|
1401
|
+
[inset, inset],
|
|
1402
|
+
[size - inset, inset],
|
|
1403
|
+
[size - inset, size - inset],
|
|
1404
|
+
[inset, size - inset]
|
|
1405
|
+
];
|
|
1406
|
+
context.beginPath();
|
|
1407
|
+
for (let corner = 0; corner < corners.length; corner++) {
|
|
1408
|
+
const [fromX, fromY] = corners[corner];
|
|
1409
|
+
const [toX, toY] = corners[(corner + 1) % corners.length];
|
|
1410
|
+
for (let step = 0; step <= 26; step++) {
|
|
1411
|
+
const ratio = step / 26;
|
|
1412
|
+
const x = fromX + (toX - fromX) * ratio + (random() - 0.5) * jitter;
|
|
1413
|
+
const y = fromY + (toY - fromY) * ratio + (random() - 0.5) * jitter;
|
|
1414
|
+
if (corner === 0 && step === 0) context.moveTo(x, y);
|
|
1415
|
+
else context.lineTo(x, y);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
context.closePath();
|
|
1419
|
+
context.fill();
|
|
1420
|
+
}
|
|
1421
|
+
function paint(id, context) {
|
|
1422
|
+
const size = TEXTURE_MASK_SIZE;
|
|
1423
|
+
const center = size / 2;
|
|
1424
|
+
context.fillStyle = "#ffffff";
|
|
1425
|
+
switch (id) {
|
|
1426
|
+
case "vignette": {
|
|
1427
|
+
const gradient = context.createRadialGradient(
|
|
1428
|
+
center,
|
|
1429
|
+
center,
|
|
1430
|
+
size * 0.18,
|
|
1431
|
+
center,
|
|
1432
|
+
center,
|
|
1433
|
+
size * 0.52
|
|
1434
|
+
);
|
|
1435
|
+
gradient.addColorStop(0, "rgba(255,255,255,1)");
|
|
1436
|
+
gradient.addColorStop(1, "rgba(255,255,255,0)");
|
|
1437
|
+
context.fillStyle = gradient;
|
|
1438
|
+
context.fillRect(0, 0, size, size);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
case "band": {
|
|
1442
|
+
const gradient = context.createLinearGradient(0, 0, 0, size);
|
|
1443
|
+
gradient.addColorStop(0, "rgba(255,255,255,0)");
|
|
1444
|
+
gradient.addColorStop(0.25, "rgba(255,255,255,1)");
|
|
1445
|
+
gradient.addColorStop(0.75, "rgba(255,255,255,1)");
|
|
1446
|
+
gradient.addColorStop(1, "rgba(255,255,255,0)");
|
|
1447
|
+
context.fillStyle = gradient;
|
|
1448
|
+
context.fillRect(0, 0, size, size);
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
case "halftone": {
|
|
1452
|
+
const pitch = 16;
|
|
1453
|
+
for (let y = pitch / 2; y < size; y += pitch) {
|
|
1454
|
+
for (let x = pitch / 2; x < size; x += pitch) {
|
|
1455
|
+
const distance = Math.hypot(x - center, y - center) / (size * 0.52);
|
|
1456
|
+
const radius = Math.max(0, pitch / 2 * (1 - distance) * 1.15);
|
|
1457
|
+
if (radius < 0.4) continue;
|
|
1458
|
+
context.beginPath();
|
|
1459
|
+
context.arc(x, y, radius, 0, Math.PI * 2);
|
|
1460
|
+
context.fill();
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
case "spray": {
|
|
1466
|
+
const random = seeded(42);
|
|
1467
|
+
const core = context.createRadialGradient(center, center, 10, center, center, size * 0.42);
|
|
1468
|
+
core.addColorStop(0, "rgba(255,255,255,1)");
|
|
1469
|
+
core.addColorStop(1, "rgba(255,255,255,0.85)");
|
|
1470
|
+
context.fillStyle = core;
|
|
1471
|
+
context.beginPath();
|
|
1472
|
+
context.arc(center, center, size * 0.42, 0, Math.PI * 2);
|
|
1473
|
+
context.fill();
|
|
1474
|
+
context.fillStyle = "rgba(255,255,255,0.9)";
|
|
1475
|
+
for (let dot = 0; dot < 900; dot++) {
|
|
1476
|
+
const angle = random() * Math.PI * 2;
|
|
1477
|
+
const distance = size * (0.3 + random() * 0.24);
|
|
1478
|
+
const x = center + Math.cos(angle) * distance;
|
|
1479
|
+
const y = center + Math.sin(angle) * distance;
|
|
1480
|
+
context.globalAlpha = 1 - (distance / size - 0.3) / 0.24;
|
|
1481
|
+
context.beginPath();
|
|
1482
|
+
context.arc(x, y, random() * 2.4, 0, Math.PI * 2);
|
|
1483
|
+
context.fill();
|
|
1484
|
+
}
|
|
1485
|
+
context.globalAlpha = 1;
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
case "grunge": {
|
|
1489
|
+
const random = seeded(7);
|
|
1490
|
+
traceRoughEdge(context, random, 14, size * 0.06);
|
|
1491
|
+
context.globalCompositeOperation = "destination-out";
|
|
1492
|
+
for (let speckle = 0; speckle < 240; speckle++) {
|
|
1493
|
+
const x = random() * size;
|
|
1494
|
+
const y = random() * size;
|
|
1495
|
+
const nearEdge = Math.min(x, y, size - x, size - y) < size * 0.12;
|
|
1496
|
+
if (!nearEdge && random() >= 0.12) continue;
|
|
1497
|
+
context.beginPath();
|
|
1498
|
+
context.arc(x, y, random() * 6 + 1, 0, Math.PI * 2);
|
|
1499
|
+
context.fill();
|
|
1500
|
+
}
|
|
1501
|
+
context.globalCompositeOperation = "source-over";
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
case "torn": {
|
|
1505
|
+
traceRoughEdge(context, seeded(99), 10, size * 0.08);
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function renderTextureMask(id) {
|
|
1511
|
+
const cached = cache.get(id);
|
|
1512
|
+
if (cached) return cached;
|
|
1513
|
+
if (typeof document === "undefined") {
|
|
1514
|
+
throw new Error("Texture masks require a DOM canvas");
|
|
1515
|
+
}
|
|
1516
|
+
const canvas = document.createElement("canvas");
|
|
1517
|
+
canvas.width = TEXTURE_MASK_SIZE;
|
|
1518
|
+
canvas.height = TEXTURE_MASK_SIZE;
|
|
1519
|
+
const context = canvas.getContext("2d");
|
|
1520
|
+
if (!context) throw new Error("Texture masks require a 2D canvas context");
|
|
1521
|
+
paint(id, context);
|
|
1522
|
+
cache.set(id, canvas);
|
|
1523
|
+
return canvas;
|
|
1524
|
+
}
|
|
1525
|
+
function clearTextureMaskCache() {
|
|
1526
|
+
cache.clear();
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// src/mask-presets/manager.ts
|
|
1530
|
+
function isMaskPresetId(value) {
|
|
1531
|
+
return isShapeMaskId(value) || isTextureMaskId(value);
|
|
1532
|
+
}
|
|
1533
|
+
var MaskPresetManager = class {
|
|
1534
|
+
constructor(canvas, layers, history, events) {
|
|
1535
|
+
this.canvas = canvas;
|
|
1536
|
+
this.layers = layers;
|
|
1537
|
+
this.history = history;
|
|
1538
|
+
this.events = events;
|
|
1539
|
+
}
|
|
1540
|
+
canvas;
|
|
1541
|
+
layers;
|
|
1542
|
+
history;
|
|
1543
|
+
events;
|
|
1544
|
+
get(layerId) {
|
|
1545
|
+
return this.layers.get(layerId)?.meta.maskPreset ?? null;
|
|
1546
|
+
}
|
|
1547
|
+
/** Clip the layer to `id`. Passing null (or an unknown id) clears the clip. */
|
|
1548
|
+
apply(layerId, id, save = true) {
|
|
1549
|
+
const layer = this.layers.get(layerId);
|
|
1550
|
+
if (!layer) return false;
|
|
1551
|
+
const object = layer.fabricObject;
|
|
1552
|
+
if (id === null) {
|
|
1553
|
+
if (!layer.meta.maskPreset) return false;
|
|
1554
|
+
object.clipPath = void 0;
|
|
1555
|
+
delete layer.meta.maskPreset;
|
|
1556
|
+
} else {
|
|
1557
|
+
if (!isMaskPresetId(id)) return false;
|
|
1558
|
+
object.clipPath = this.buildClip(id, object);
|
|
1559
|
+
layer.meta.maskPreset = id;
|
|
1560
|
+
}
|
|
1561
|
+
object.dirty = true;
|
|
1562
|
+
object.setCoords();
|
|
1563
|
+
this.canvas.requestRenderAll();
|
|
1564
|
+
this.events.emit("layer:modified", { layerId });
|
|
1565
|
+
if (save) this.history.save();
|
|
1566
|
+
return true;
|
|
1567
|
+
}
|
|
1568
|
+
clear(layerId, save = true) {
|
|
1569
|
+
return this.apply(layerId, null, save);
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Re-fit the clip to the layer's current size. The clip is built for the
|
|
1573
|
+
* object's dimensions at the time it was applied; editing text or replacing an
|
|
1574
|
+
* image changes them, and a stale clip would crop the wrong region.
|
|
1575
|
+
*/
|
|
1576
|
+
refresh(layerId, save = false) {
|
|
1577
|
+
const id = this.get(layerId);
|
|
1578
|
+
if (!id) return false;
|
|
1579
|
+
return this.apply(layerId, id, save);
|
|
1580
|
+
}
|
|
1581
|
+
/** Re-fit every masked layer — used after a state restore. */
|
|
1582
|
+
refreshAll() {
|
|
1583
|
+
for (const layer of this.layers.getAll()) {
|
|
1584
|
+
if (layer.meta.maskPreset) this.refresh(layer.id);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
buildClip(id, object) {
|
|
1588
|
+
const width = Math.max(1, object.width ?? 1);
|
|
1589
|
+
const height = Math.max(1, object.height ?? 1);
|
|
1590
|
+
const shared = {
|
|
1591
|
+
originX: "center",
|
|
1592
|
+
originY: "center",
|
|
1593
|
+
left: 0,
|
|
1594
|
+
top: 0,
|
|
1595
|
+
objectCaching: false
|
|
1596
|
+
};
|
|
1597
|
+
if (isShapeMaskId(id)) {
|
|
1598
|
+
return new import_fabric4.Path(shapeMaskPathData(id), {
|
|
1599
|
+
...shared,
|
|
1600
|
+
scaleX: width / SHAPE_MASK_BOX,
|
|
1601
|
+
scaleY: height / SHAPE_MASK_BOX
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
return new import_fabric4.FabricImage(renderTextureMask(id), {
|
|
1605
|
+
...shared,
|
|
1606
|
+
scaleX: width / TEXTURE_MASK_SIZE,
|
|
1607
|
+
scaleY: height / TEXTURE_MASK_SIZE
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1612
|
+
// src/shadow.ts
|
|
1613
|
+
var import_fabric5 = require("fabric");
|
|
1614
|
+
|
|
1615
|
+
// src/utils/color.ts
|
|
1616
|
+
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
1617
|
+
function isCssColor(value, allowEmpty = false) {
|
|
1618
|
+
if (typeof value !== "string") return false;
|
|
1619
|
+
const trimmed = value.trim();
|
|
1620
|
+
if (!trimmed) return allowEmpty;
|
|
1621
|
+
return CSS_COLOR.test(trimmed);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// src/shadow.ts
|
|
1625
|
+
var DEFAULT_LAYER_SHADOW = {
|
|
1626
|
+
enabled: false,
|
|
1627
|
+
color: "#000000",
|
|
1628
|
+
blur: 12,
|
|
1629
|
+
offsetX: 6,
|
|
1630
|
+
offsetY: 6
|
|
1631
|
+
};
|
|
1632
|
+
function readLayerShadow(object) {
|
|
1633
|
+
const shadow = object.shadow;
|
|
1634
|
+
if (!shadow || typeof shadow === "string") return { ...DEFAULT_LAYER_SHADOW };
|
|
1635
|
+
return {
|
|
1636
|
+
enabled: true,
|
|
1637
|
+
color: typeof shadow.color === "string" ? shadow.color : DEFAULT_LAYER_SHADOW.color,
|
|
1638
|
+
blur: shadow.blur ?? DEFAULT_LAYER_SHADOW.blur,
|
|
1639
|
+
offsetX: shadow.offsetX ?? DEFAULT_LAYER_SHADOW.offsetX,
|
|
1640
|
+
offsetY: shadow.offsetY ?? DEFAULT_LAYER_SHADOW.offsetY
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
function applyLayerShadow(object, config) {
|
|
1644
|
+
const next = { ...readLayerShadow(object), ...config };
|
|
1645
|
+
if (!next.enabled) {
|
|
1646
|
+
object.set({ shadow: null });
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
|
|
1650
|
+
object.set({
|
|
1651
|
+
shadow: new import_fabric5.Shadow({
|
|
1652
|
+
color,
|
|
1653
|
+
blur: Math.max(0, next.blur),
|
|
1654
|
+
offsetX: next.offsetX,
|
|
1655
|
+
offsetY: next.offsetY
|
|
1656
|
+
})
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// src/transform.ts
|
|
1661
|
+
var SIDE_CONTROLS = ["ml", "mr", "mt", "mb"];
|
|
1662
|
+
function applyAspectLock(object, locked) {
|
|
1663
|
+
for (const control of SIDE_CONTROLS) {
|
|
1664
|
+
object.setControlVisible(control, !locked);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
function resetTransform(object) {
|
|
1668
|
+
object.set({
|
|
1669
|
+
scaleX: 1,
|
|
1670
|
+
scaleY: 1,
|
|
1671
|
+
angle: 0,
|
|
1672
|
+
skewX: 0,
|
|
1673
|
+
skewY: 0,
|
|
1674
|
+
flipX: false,
|
|
1675
|
+
flipY: false
|
|
1676
|
+
});
|
|
1677
|
+
object.setCoords();
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1056
1680
|
// src/utils/units.ts
|
|
1057
1681
|
var MM_PER_INCH = 25.4;
|
|
1058
1682
|
var UnitConverter = class {
|
|
@@ -1101,18 +1725,7 @@ var UnitConverter = class {
|
|
|
1101
1725
|
};
|
|
1102
1726
|
|
|
1103
1727
|
// src/serialization.ts
|
|
1104
|
-
var
|
|
1105
|
-
|
|
1106
|
-
// src/utils/color.ts
|
|
1107
|
-
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
1108
|
-
function isCssColor(value, allowEmpty = false) {
|
|
1109
|
-
if (typeof value !== "string") return false;
|
|
1110
|
-
const trimmed = value.trim();
|
|
1111
|
-
if (!trimmed) return allowEmpty;
|
|
1112
|
-
return CSS_COLOR.test(trimmed);
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
// src/serialization.ts
|
|
1728
|
+
var import_fabric6 = require("fabric");
|
|
1116
1729
|
var VERSION = "2.0.0";
|
|
1117
1730
|
function serializeEditor(editor) {
|
|
1118
1731
|
return {
|
|
@@ -1128,6 +1741,7 @@ function serializeEditor(editor) {
|
|
|
1128
1741
|
// forced transparent while a mockup preview is active).
|
|
1129
1742
|
background: editor.getDesignBackground(),
|
|
1130
1743
|
backgroundImage: editor.getDesignBackgroundImage() ? editor.getDesignBackgroundImage().toObject() : null,
|
|
1744
|
+
backgroundImageOptions: editor.getBackgroundImageOptions(),
|
|
1131
1745
|
mockup: editor.getMockup()
|
|
1132
1746
|
};
|
|
1133
1747
|
}
|
|
@@ -1146,13 +1760,28 @@ async function deserializeEditor(editor, state) {
|
|
|
1146
1760
|
throw new Error("Invalid editor background color");
|
|
1147
1761
|
}
|
|
1148
1762
|
const staged = await Promise.all(
|
|
1149
|
-
state.layers.map(async (serialized) =>
|
|
1150
|
-
serialized
|
|
1151
|
-
|
|
1152
|
-
|
|
1763
|
+
state.layers.map(async (serialized) => {
|
|
1764
|
+
const fabricObject = (await import_fabric6.util.enlivenObjects([serialized.fabricObject]))[0];
|
|
1765
|
+
if (!fabricObject) {
|
|
1766
|
+
const source = serialized.fabricObject.src;
|
|
1767
|
+
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
1768
|
+
throw new Error(`Failed to restore expired object URL: ${source}`);
|
|
1769
|
+
}
|
|
1770
|
+
throw new Error(`Failed to restore layer: ${serialized.id}`);
|
|
1771
|
+
}
|
|
1772
|
+
return { serialized, fabricObject };
|
|
1773
|
+
})
|
|
1153
1774
|
);
|
|
1154
|
-
const stagedBackground = state.backgroundImage ? (await
|
|
1775
|
+
const stagedBackground = state.backgroundImage ? (await import_fabric6.util.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
1776
|
+
if (state.backgroundImage && !stagedBackground) {
|
|
1777
|
+
const source = state.backgroundImage.src;
|
|
1778
|
+
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
1779
|
+
throw new Error(`Failed to restore expired background object URL: ${source}`);
|
|
1780
|
+
}
|
|
1781
|
+
throw new Error("Failed to restore background image");
|
|
1782
|
+
}
|
|
1155
1783
|
editor.crop.cancel();
|
|
1784
|
+
editor.masks.detach();
|
|
1156
1785
|
editor.layers.clear();
|
|
1157
1786
|
if (state.canvas.unit) editor.units.setUnit(state.canvas.unit);
|
|
1158
1787
|
if (state.canvas.dpi !== void 0) editor.units.setDpi(state.canvas.dpi);
|
|
@@ -1160,7 +1789,11 @@ async function deserializeEditor(editor, state) {
|
|
|
1160
1789
|
if (state.background !== void 0) {
|
|
1161
1790
|
editor.setBackground(state.background);
|
|
1162
1791
|
}
|
|
1163
|
-
editor.setBackgroundImageObject(
|
|
1792
|
+
editor.setBackgroundImageObject(
|
|
1793
|
+
stagedBackground ?? null,
|
|
1794
|
+
false,
|
|
1795
|
+
state.backgroundImageOptions ?? null
|
|
1796
|
+
);
|
|
1164
1797
|
editor.setMockup(state.mockup ?? null);
|
|
1165
1798
|
for (const item of staged) {
|
|
1166
1799
|
restoreLayer(editor, item.serialized, item.fabricObject);
|
|
@@ -1171,6 +1804,7 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
1171
1804
|
const layer = editor.layers.add(serialized.type, fabricObject, serialized.name, serialized.id);
|
|
1172
1805
|
if (serialized.meta) {
|
|
1173
1806
|
layer.meta = serialized.meta;
|
|
1807
|
+
if (layer.meta.lockAspect) applyAspectLock(fabricObject, true);
|
|
1174
1808
|
}
|
|
1175
1809
|
if (!serialized.visible) {
|
|
1176
1810
|
editor.layers.setVisibility(layer.id, false);
|
|
@@ -1184,6 +1818,62 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
1184
1818
|
return layer;
|
|
1185
1819
|
}
|
|
1186
1820
|
|
|
1821
|
+
// src/export.ts
|
|
1822
|
+
var import_fabric7 = require("fabric");
|
|
1823
|
+
|
|
1824
|
+
// src/displacement.ts
|
|
1825
|
+
var CHANNEL_INDEX = {
|
|
1826
|
+
red: 0,
|
|
1827
|
+
green: 1,
|
|
1828
|
+
blue: 2,
|
|
1829
|
+
alpha: 3
|
|
1830
|
+
};
|
|
1831
|
+
function finiteScale(value, fallback, label) {
|
|
1832
|
+
const resolved = value ?? fallback;
|
|
1833
|
+
if (!Number.isFinite(resolved)) throw new Error(`${label} must be finite`);
|
|
1834
|
+
return resolved;
|
|
1835
|
+
}
|
|
1836
|
+
function sample(source, width, height, x, y, channel) {
|
|
1837
|
+
const clampedX = Math.max(0, Math.min(width - 1, x));
|
|
1838
|
+
const clampedY = Math.max(0, Math.min(height - 1, y));
|
|
1839
|
+
const x0 = Math.floor(clampedX);
|
|
1840
|
+
const y0 = Math.floor(clampedY);
|
|
1841
|
+
const x1 = Math.min(width - 1, x0 + 1);
|
|
1842
|
+
const y1 = Math.min(height - 1, y0 + 1);
|
|
1843
|
+
const tx = clampedX - x0;
|
|
1844
|
+
const ty = clampedY - y0;
|
|
1845
|
+
const top = source[(y0 * width + x0) * 4 + channel] * (1 - tx) + source[(y0 * width + x1) * 4 + channel] * tx;
|
|
1846
|
+
const bottom = source[(y1 * width + x0) * 4 + channel] * (1 - tx) + source[(y1 * width + x1) * 4 + channel] * tx;
|
|
1847
|
+
return top * (1 - ty) + bottom * ty;
|
|
1848
|
+
}
|
|
1849
|
+
function displaceRgba(source, map, width, height, options) {
|
|
1850
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
|
|
1851
|
+
throw new Error("Displacement dimensions must be positive integers");
|
|
1852
|
+
}
|
|
1853
|
+
const expectedLength = width * height * 4;
|
|
1854
|
+
if (source.length !== expectedLength || map.length !== expectedLength) {
|
|
1855
|
+
throw new Error("Displacement source and map must match the requested dimensions");
|
|
1856
|
+
}
|
|
1857
|
+
const scaleX = finiteScale(options.scaleX, 10, "Displacement scaleX");
|
|
1858
|
+
const scaleY = finiteScale(options.scaleY, 10, "Displacement scaleY");
|
|
1859
|
+
const channelX = CHANNEL_INDEX[options.channelX ?? "red"];
|
|
1860
|
+
const channelY = CHANNEL_INDEX[options.channelY ?? "green"];
|
|
1861
|
+
const output = new Uint8ClampedArray(expectedLength);
|
|
1862
|
+
for (let y = 0; y < height; y += 1) {
|
|
1863
|
+
for (let x = 0; x < width; x += 1) {
|
|
1864
|
+
const offset = (y * width + x) * 4;
|
|
1865
|
+
const sourceX = x + (map[offset + channelX] - 128) / 127 * scaleX;
|
|
1866
|
+
const sourceY = y + (map[offset + channelY] - 128) / 127 * scaleY;
|
|
1867
|
+
for (let channel = 0; channel < 4; channel += 1) {
|
|
1868
|
+
output[offset + channel] = Math.round(
|
|
1869
|
+
sample(source, width, height, sourceX, sourceY, channel)
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return output;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1187
1877
|
// src/export.ts
|
|
1188
1878
|
function computePrintAreaClip(area, scaleX, scaleY, targetWidth, targetHeight) {
|
|
1189
1879
|
const left = Math.max(0, Math.min(targetWidth, area.left * scaleX));
|
|
@@ -1221,6 +1911,55 @@ async function exportPNG(canvas, options = {}) {
|
|
|
1221
1911
|
const output = canvas.toCanvasElement(multiplier);
|
|
1222
1912
|
return canvasElementToBlob(output, format, quality);
|
|
1223
1913
|
}
|
|
1914
|
+
async function exportIsolatedPNG(source, objects, options = {}) {
|
|
1915
|
+
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
1916
|
+
const canvas = new import_fabric7.StaticCanvas(element, {
|
|
1917
|
+
width: options.width ?? source.getWidth(),
|
|
1918
|
+
height: options.height ?? source.getHeight(),
|
|
1919
|
+
backgroundColor: options.backgroundColor || void 0
|
|
1920
|
+
});
|
|
1921
|
+
try {
|
|
1922
|
+
const clones = options.cloneObjects === false ? objects : await Promise.all(objects.map((object) => object.clone()));
|
|
1923
|
+
if (clones.length) canvas.add(...clones);
|
|
1924
|
+
if (options.backgroundImage) canvas.backgroundImage = await options.backgroundImage.clone();
|
|
1925
|
+
canvas.requestRenderAll();
|
|
1926
|
+
return await exportPNG(canvas, options);
|
|
1927
|
+
} finally {
|
|
1928
|
+
canvas.dispose();
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
async function exportPrintArea(source, area, options = {}) {
|
|
1932
|
+
const { multiplier = 1, format = "png", quality = 1 } = options;
|
|
1933
|
+
const width = source.getWidth();
|
|
1934
|
+
const height = source.getHeight();
|
|
1935
|
+
const clip = computePrintAreaClip(
|
|
1936
|
+
area,
|
|
1937
|
+
multiplier,
|
|
1938
|
+
multiplier,
|
|
1939
|
+
width * multiplier,
|
|
1940
|
+
height * multiplier
|
|
1941
|
+
);
|
|
1942
|
+
if (clip.width <= 0 || clip.height <= 0) {
|
|
1943
|
+
throw new Error("Print area does not overlap the canvas");
|
|
1944
|
+
}
|
|
1945
|
+
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
1946
|
+
const canvas = new import_fabric7.StaticCanvas(element, { width, height });
|
|
1947
|
+
try {
|
|
1948
|
+
const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
|
|
1949
|
+
if (clones.length) canvas.add(...clones);
|
|
1950
|
+
canvas.requestRenderAll();
|
|
1951
|
+
const rendered = canvas.toCanvasElement(multiplier);
|
|
1952
|
+
const output = rendered.ownerDocument.createElement("canvas");
|
|
1953
|
+
output.width = Math.max(1, Math.round(clip.width));
|
|
1954
|
+
output.height = Math.max(1, Math.round(clip.height));
|
|
1955
|
+
const context = output.getContext("2d");
|
|
1956
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
1957
|
+
context.drawImage(rendered, -clip.left, -clip.top);
|
|
1958
|
+
return await canvasElementToBlob(output, format, quality);
|
|
1959
|
+
} finally {
|
|
1960
|
+
canvas.dispose();
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1224
1963
|
async function exportMockup(canvas, mockup, options = {}) {
|
|
1225
1964
|
const { multiplier = 1, format = "png", quality = 1 } = options;
|
|
1226
1965
|
const design = canvas.toCanvasElement(multiplier);
|
|
@@ -1236,41 +1975,92 @@ async function exportMockup(canvas, mockup, options = {}) {
|
|
|
1236
1975
|
element.onerror = () => reject(new Error(`Failed to load mockup image: ${url}`));
|
|
1237
1976
|
element.src = url;
|
|
1238
1977
|
});
|
|
1239
|
-
const drawCover = (image) => {
|
|
1978
|
+
const drawCover = (image, targetContext = context) => {
|
|
1240
1979
|
const placement = computeCoverPlacement(
|
|
1241
1980
|
image.naturalWidth || image.width,
|
|
1242
1981
|
image.naturalHeight || image.height,
|
|
1243
1982
|
output.width,
|
|
1244
1983
|
output.height
|
|
1245
1984
|
);
|
|
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
|
|
1985
|
+
targetContext.drawImage(
|
|
1986
|
+
image,
|
|
1987
|
+
placement.left,
|
|
1988
|
+
placement.top,
|
|
1989
|
+
placement.width,
|
|
1990
|
+
placement.height
|
|
1257
1991
|
);
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1992
|
+
};
|
|
1993
|
+
const scratch = [design];
|
|
1994
|
+
try {
|
|
1995
|
+
drawCover(await loadImage(mockup.image));
|
|
1996
|
+
let compositedDesign = design;
|
|
1997
|
+
if (mockup.displacement) {
|
|
1998
|
+
const sourceContext = design.getContext("2d");
|
|
1999
|
+
if (!sourceContext) throw new Error("2D design context is unavailable");
|
|
2000
|
+
const mapCanvas = design.ownerDocument.createElement("canvas");
|
|
2001
|
+
scratch.push(mapCanvas);
|
|
2002
|
+
mapCanvas.width = design.width;
|
|
2003
|
+
mapCanvas.height = design.height;
|
|
2004
|
+
const mapContext = mapCanvas.getContext("2d");
|
|
2005
|
+
if (!mapContext) throw new Error("2D displacement-map context is unavailable");
|
|
2006
|
+
drawCover(await loadImage(mockup.displacement.image), mapContext);
|
|
2007
|
+
const warped = design.ownerDocument.createElement("canvas");
|
|
2008
|
+
scratch.push(warped);
|
|
2009
|
+
warped.width = design.width;
|
|
2010
|
+
warped.height = design.height;
|
|
2011
|
+
const warpedContext = warped.getContext("2d");
|
|
2012
|
+
if (!warpedContext) throw new Error("2D displaced-design context is unavailable");
|
|
2013
|
+
let sourcePixels;
|
|
2014
|
+
let mapPixels;
|
|
2015
|
+
try {
|
|
2016
|
+
sourcePixels = sourceContext.getImageData(0, 0, design.width, design.height).data;
|
|
2017
|
+
mapPixels = mapContext.getImageData(0, 0, design.width, design.height).data;
|
|
2018
|
+
} catch (error) {
|
|
2019
|
+
throw new Error("Failed to apply mockup displacement map; verify image CORS access", {
|
|
2020
|
+
cause: error
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
const pixels = displaceRgba(sourcePixels, mapPixels, design.width, design.height, {
|
|
2024
|
+
...mockup.displacement,
|
|
2025
|
+
scaleX: (mockup.displacement.scaleX ?? 10) * multiplier,
|
|
2026
|
+
scaleY: (mockup.displacement.scaleY ?? 10) * multiplier
|
|
2027
|
+
});
|
|
2028
|
+
const imageData = warpedContext.createImageData(design.width, design.height);
|
|
2029
|
+
imageData.data.set(pixels);
|
|
2030
|
+
warpedContext.putImageData(imageData, 0, 0);
|
|
2031
|
+
compositedDesign = warped;
|
|
2032
|
+
}
|
|
1267
2033
|
context.save();
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
2034
|
+
if (mockup.printArea && mockup.clipToPrintArea !== false) {
|
|
2035
|
+
const clip = computePrintAreaClip(
|
|
2036
|
+
mockup.printArea,
|
|
2037
|
+
output.width / canvas.getWidth(),
|
|
2038
|
+
output.height / canvas.getHeight(),
|
|
2039
|
+
output.width,
|
|
2040
|
+
output.height
|
|
2041
|
+
);
|
|
2042
|
+
context.beginPath();
|
|
2043
|
+
context.rect(clip.left, clip.top, clip.width, clip.height);
|
|
2044
|
+
context.clip();
|
|
2045
|
+
}
|
|
2046
|
+
context.globalAlpha = Math.max(0, Math.min(1, mockup.designOpacity ?? 1));
|
|
2047
|
+
context.globalCompositeOperation = !mockup.designBlendMode || mockup.designBlendMode === "normal" ? "source-over" : mockup.designBlendMode;
|
|
2048
|
+
context.drawImage(compositedDesign, 0, 0);
|
|
1271
2049
|
context.restore();
|
|
2050
|
+
if (mockup.overlay) {
|
|
2051
|
+
context.save();
|
|
2052
|
+
context.globalAlpha = Math.max(0, Math.min(1, mockup.overlay.opacity ?? 1));
|
|
2053
|
+
context.globalCompositeOperation = mockup.overlay.blendMode === "normal" ? "source-over" : mockup.overlay.blendMode ?? "multiply";
|
|
2054
|
+
drawCover(await loadImage(mockup.overlay.image));
|
|
2055
|
+
context.restore();
|
|
2056
|
+
}
|
|
2057
|
+
return await canvasElementToBlob(output, format, quality);
|
|
2058
|
+
} finally {
|
|
2059
|
+
for (const element of scratch) {
|
|
2060
|
+
element.width = 0;
|
|
2061
|
+
element.height = 0;
|
|
2062
|
+
}
|
|
1272
2063
|
}
|
|
1273
|
-
return canvasElementToBlob(output, format, quality);
|
|
1274
2064
|
}
|
|
1275
2065
|
function exportSVG(canvas) {
|
|
1276
2066
|
return canvas.toSVG();
|
|
@@ -1592,9 +2382,270 @@ var ProjectManager = class {
|
|
|
1592
2382
|
}
|
|
1593
2383
|
};
|
|
1594
2384
|
|
|
2385
|
+
// src/mask.ts
|
|
2386
|
+
var import_fabric8 = require("fabric");
|
|
2387
|
+
var MaskRefinementError = class extends Error {
|
|
2388
|
+
constructor(code, message, cause) {
|
|
2389
|
+
super(message);
|
|
2390
|
+
this.code = code;
|
|
2391
|
+
this.cause = cause;
|
|
2392
|
+
this.name = "MaskRefinementError";
|
|
2393
|
+
}
|
|
2394
|
+
code;
|
|
2395
|
+
cause;
|
|
2396
|
+
};
|
|
2397
|
+
var MaskController = class {
|
|
2398
|
+
constructor(editor) {
|
|
2399
|
+
this.editor = editor;
|
|
2400
|
+
}
|
|
2401
|
+
editor;
|
|
2402
|
+
backing = null;
|
|
2403
|
+
context = null;
|
|
2404
|
+
layerId = null;
|
|
2405
|
+
brush = null;
|
|
2406
|
+
previousPoint = null;
|
|
2407
|
+
strokeBackup = null;
|
|
2408
|
+
strokeStartedAt = null;
|
|
2409
|
+
lastInteractionLatencyMs = 0;
|
|
2410
|
+
refinement = null;
|
|
2411
|
+
disposed = false;
|
|
2412
|
+
async create(width = this.editor.canvas.getWidth(), height = this.editor.canvas.getHeight()) {
|
|
2413
|
+
this.assertActive();
|
|
2414
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
|
|
2415
|
+
throw new Error("Mask dimensions must be positive integers");
|
|
2416
|
+
}
|
|
2417
|
+
const backing = this.makeCanvas(width, height);
|
|
2418
|
+
const image = new import_fabric8.FabricImage(backing, {
|
|
2419
|
+
left: 0,
|
|
2420
|
+
top: 0,
|
|
2421
|
+
originX: "left",
|
|
2422
|
+
originY: "top",
|
|
2423
|
+
selectable: false
|
|
2424
|
+
});
|
|
2425
|
+
const layer = this.editor.layers.add("mask", image, "Mask");
|
|
2426
|
+
layer.meta.mask = { width, height, revision: 0 };
|
|
2427
|
+
this.editor.history.saveImmediate();
|
|
2428
|
+
this.attachBacking(layer.id, backing);
|
|
2429
|
+
return layer;
|
|
2430
|
+
}
|
|
2431
|
+
attach(layerId) {
|
|
2432
|
+
this.assertActive();
|
|
2433
|
+
this.cancelStroke();
|
|
2434
|
+
const layer = this.requireMask(layerId);
|
|
2435
|
+
const image = layer.fabricObject;
|
|
2436
|
+
const width = layer.meta.mask?.width ?? image.width ?? this.editor.canvas.getWidth();
|
|
2437
|
+
const height = layer.meta.mask?.height ?? image.height ?? this.editor.canvas.getHeight();
|
|
2438
|
+
const backing = this.makeCanvas(width, height);
|
|
2439
|
+
const context = backing.getContext("2d");
|
|
2440
|
+
if (!context) throw new Error("2D mask context is unavailable");
|
|
2441
|
+
const element = image.getElement();
|
|
2442
|
+
if (element) context.drawImage(element, 0, 0, width, height);
|
|
2443
|
+
this.attachBacking(layerId, backing);
|
|
2444
|
+
}
|
|
2445
|
+
beginStroke(options) {
|
|
2446
|
+
this.assertActive();
|
|
2447
|
+
if (!this.context || !this.backing || !this.layerId) throw new Error("Attach a mask first");
|
|
2448
|
+
if (this.brush) throw new Error("A mask stroke is already active");
|
|
2449
|
+
if (!Number.isFinite(options.size) || options.size <= 0) {
|
|
2450
|
+
throw new Error("Mask brush size must be positive");
|
|
2451
|
+
}
|
|
2452
|
+
this.brush = { ...options, hardness: clamp(options.hardness, 0, 1) };
|
|
2453
|
+
this.previousPoint = null;
|
|
2454
|
+
this.strokeBackup = this.context.getImageData(0, 0, this.backing.width, this.backing.height);
|
|
2455
|
+
this.strokeStartedAt = performance.now();
|
|
2456
|
+
}
|
|
2457
|
+
addPoint(point) {
|
|
2458
|
+
if (!this.brush || !this.context || !this.backing || !this.layerId) {
|
|
2459
|
+
throw new Error("No active mask stroke");
|
|
2460
|
+
}
|
|
2461
|
+
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return;
|
|
2462
|
+
const previous = this.previousPoint ?? point;
|
|
2463
|
+
const distance = Math.hypot(point.x - previous.x, point.y - previous.y);
|
|
2464
|
+
const step = Math.max(1, this.brush.size / 4);
|
|
2465
|
+
const samples = Math.max(1, Math.ceil(distance / step));
|
|
2466
|
+
for (let index = 0; index <= samples; index += 1) {
|
|
2467
|
+
const ratio = index / samples;
|
|
2468
|
+
this.drawDot({
|
|
2469
|
+
x: previous.x + (point.x - previous.x) * ratio,
|
|
2470
|
+
y: previous.y + (point.y - previous.y) * ratio
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
this.previousPoint = point;
|
|
2474
|
+
this.requireMask(this.layerId).fabricObject.setElement(this.backing);
|
|
2475
|
+
this.editor.canvas.requestRenderAll();
|
|
2476
|
+
}
|
|
2477
|
+
async endStroke() {
|
|
2478
|
+
if (!this.brush || !this.backing || !this.layerId) return;
|
|
2479
|
+
const layerId = this.layerId;
|
|
2480
|
+
this.brush = null;
|
|
2481
|
+
this.previousPoint = null;
|
|
2482
|
+
this.strokeBackup = null;
|
|
2483
|
+
const dataUrl = this.backing.toDataURL("image/png");
|
|
2484
|
+
await this.editor.history.transaction(async () => {
|
|
2485
|
+
await this.editor.replaceImageSource(layerId, dataUrl);
|
|
2486
|
+
const layer = this.requireMask(layerId);
|
|
2487
|
+
if (layer.meta.mask) layer.meta.mask.revision += 1;
|
|
2488
|
+
});
|
|
2489
|
+
this.attach(layerId);
|
|
2490
|
+
if (this.strokeStartedAt !== null) {
|
|
2491
|
+
this.lastInteractionLatencyMs = performance.now() - this.strokeStartedAt;
|
|
2492
|
+
this.strokeStartedAt = null;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
cancelStroke() {
|
|
2496
|
+
if (this.strokeBackup && this.context && this.backing && this.layerId) {
|
|
2497
|
+
this.context.putImageData(this.strokeBackup, 0, 0);
|
|
2498
|
+
this.requireMask(this.layerId).fabricObject.setElement(this.backing);
|
|
2499
|
+
this.editor.canvas.requestRenderAll();
|
|
2500
|
+
}
|
|
2501
|
+
this.brush = null;
|
|
2502
|
+
this.previousPoint = null;
|
|
2503
|
+
this.strokeBackup = null;
|
|
2504
|
+
this.strokeStartedAt = null;
|
|
2505
|
+
}
|
|
2506
|
+
isStrokeActive() {
|
|
2507
|
+
return this.brush !== null;
|
|
2508
|
+
}
|
|
2509
|
+
activeLayerId() {
|
|
2510
|
+
return this.layerId;
|
|
2511
|
+
}
|
|
2512
|
+
detach(layerId) {
|
|
2513
|
+
if (layerId && this.layerId !== layerId) return;
|
|
2514
|
+
this.cancelStroke();
|
|
2515
|
+
this.cancelRefinement();
|
|
2516
|
+
this.backing = null;
|
|
2517
|
+
this.context = null;
|
|
2518
|
+
this.layerId = null;
|
|
2519
|
+
}
|
|
2520
|
+
async refine(layerId, provider, prompts, options = {}) {
|
|
2521
|
+
this.assertActive();
|
|
2522
|
+
const layer = this.editor.layers.get(layerId);
|
|
2523
|
+
if (!layer || layer.type !== "mask") {
|
|
2524
|
+
throw new MaskRefinementError("not-found", `Mask layer not found: ${layerId}`);
|
|
2525
|
+
}
|
|
2526
|
+
this.cancelRefinement();
|
|
2527
|
+
const controller = new AbortController();
|
|
2528
|
+
this.refinement = controller;
|
|
2529
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
2530
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
2531
|
+
if (options.signal?.aborted) abort();
|
|
2532
|
+
try {
|
|
2533
|
+
const image = layer.fabricObject;
|
|
2534
|
+
const result = await provider.refine(
|
|
2535
|
+
{
|
|
2536
|
+
mask: image.getSrc(),
|
|
2537
|
+
width: layer.meta.mask?.width ?? image.width ?? 1,
|
|
2538
|
+
height: layer.meta.mask?.height ?? image.height ?? 1,
|
|
2539
|
+
prompts: structuredClone(prompts)
|
|
2540
|
+
},
|
|
2541
|
+
{ signal: controller.signal, onProgress: options.onProgress }
|
|
2542
|
+
);
|
|
2543
|
+
if (controller.signal.aborted)
|
|
2544
|
+
throw new MaskRefinementError("cancelled", "Mask refinement cancelled");
|
|
2545
|
+
if (!/^data:image\/(?:png|jpeg|webp);base64,/i.test(result.dataUrl)) {
|
|
2546
|
+
throw new MaskRefinementError(
|
|
2547
|
+
"invalid-result",
|
|
2548
|
+
"Mask refinement must return a base64 PNG, JPEG, or WebP data URL"
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
await this.editor.history.transaction(async () => {
|
|
2552
|
+
await this.editor.replaceImageSource(layerId, result.dataUrl);
|
|
2553
|
+
if (layer.meta.mask) layer.meta.mask.revision += 1;
|
|
2554
|
+
});
|
|
2555
|
+
this.attach(layerId);
|
|
2556
|
+
return result;
|
|
2557
|
+
} catch (error) {
|
|
2558
|
+
if (error instanceof MaskRefinementError) throw error;
|
|
2559
|
+
if (controller.signal.aborted) {
|
|
2560
|
+
throw new MaskRefinementError("cancelled", "Mask refinement cancelled", error);
|
|
2561
|
+
}
|
|
2562
|
+
throw new MaskRefinementError("provider", "Mask refinement provider failed", error);
|
|
2563
|
+
} finally {
|
|
2564
|
+
options.signal?.removeEventListener("abort", abort);
|
|
2565
|
+
if (this.refinement === controller) this.refinement = null;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
cancelRefinement() {
|
|
2569
|
+
this.refinement?.abort(new DOMException("Cancelled", "AbortError"));
|
|
2570
|
+
this.refinement = null;
|
|
2571
|
+
}
|
|
2572
|
+
measure() {
|
|
2573
|
+
if (!this.backing) return null;
|
|
2574
|
+
const started = performance.now();
|
|
2575
|
+
this.context?.getImageData(0, 0, 1, 1);
|
|
2576
|
+
const backingBytes = this.backing.width * this.backing.height * 4;
|
|
2577
|
+
const strokeBackupBytes = this.strokeBackup ? backingBytes : 0;
|
|
2578
|
+
const memory = performance;
|
|
2579
|
+
return {
|
|
2580
|
+
width: this.backing.width,
|
|
2581
|
+
height: this.backing.height,
|
|
2582
|
+
backingBytes,
|
|
2583
|
+
strokeBackupBytes,
|
|
2584
|
+
// The backing store and rollback ImageData dominate interactive mask
|
|
2585
|
+
// memory. Encoded historical rasters are reported separately below.
|
|
2586
|
+
estimatedPeakBytes: backingBytes + strokeBackupBytes,
|
|
2587
|
+
historyBytes: this.editor.history.getSnapshotBytes(),
|
|
2588
|
+
interactionLatencyMs: this.lastInteractionLatencyMs,
|
|
2589
|
+
...typeof memory.memory?.usedJSHeapSize === "number" ? { usedJsHeapBytes: memory.memory.usedJSHeapSize } : {},
|
|
2590
|
+
elapsedMs: performance.now() - started
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
dispose() {
|
|
2594
|
+
this.detach();
|
|
2595
|
+
this.disposed = true;
|
|
2596
|
+
}
|
|
2597
|
+
drawDot(point) {
|
|
2598
|
+
const context = this.context;
|
|
2599
|
+
const brush = this.brush;
|
|
2600
|
+
const radius = brush.size / 2;
|
|
2601
|
+
context.save();
|
|
2602
|
+
context.globalCompositeOperation = brush.mode === "subtract" ? "destination-out" : "source-over";
|
|
2603
|
+
const gradient = context.createRadialGradient(
|
|
2604
|
+
point.x,
|
|
2605
|
+
point.y,
|
|
2606
|
+
radius * brush.hardness,
|
|
2607
|
+
point.x,
|
|
2608
|
+
point.y,
|
|
2609
|
+
radius
|
|
2610
|
+
);
|
|
2611
|
+
const color = brush.mode === "subtract" ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
|
|
2612
|
+
gradient.addColorStop(0, color);
|
|
2613
|
+
gradient.addColorStop(1, "rgba(255,255,255,0)");
|
|
2614
|
+
context.fillStyle = gradient;
|
|
2615
|
+
context.beginPath();
|
|
2616
|
+
context.arc(point.x, point.y, radius, 0, Math.PI * 2);
|
|
2617
|
+
context.fill();
|
|
2618
|
+
context.restore();
|
|
2619
|
+
}
|
|
2620
|
+
makeCanvas(width, height) {
|
|
2621
|
+
const canvas = this.editor.canvas.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
2622
|
+
canvas.width = width;
|
|
2623
|
+
canvas.height = height;
|
|
2624
|
+
return canvas;
|
|
2625
|
+
}
|
|
2626
|
+
attachBacking(layerId, backing) {
|
|
2627
|
+
const context = backing.getContext("2d");
|
|
2628
|
+
if (!context) throw new Error("2D mask context is unavailable");
|
|
2629
|
+
this.layerId = layerId;
|
|
2630
|
+
this.backing = backing;
|
|
2631
|
+
this.context = context;
|
|
2632
|
+
}
|
|
2633
|
+
requireMask(layerId) {
|
|
2634
|
+
const layer = this.editor.layers.get(layerId);
|
|
2635
|
+
if (!layer || layer.type !== "mask") throw new Error(`Mask layer not found: ${layerId}`);
|
|
2636
|
+
return layer;
|
|
2637
|
+
}
|
|
2638
|
+
assertActive() {
|
|
2639
|
+
if (this.disposed) throw new Error("Mask controller has been disposed");
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
|
|
1595
2643
|
// src/editor.ts
|
|
1596
2644
|
var MIN_ZOOM = 0.1;
|
|
1597
2645
|
var MAX_ZOOM = 8;
|
|
2646
|
+
function isTaintedCanvasError(error) {
|
|
2647
|
+
return error instanceof DOMException && error.name === "SecurityError" || error instanceof Error && /taint|cross-origin|insecure/i.test(error.message);
|
|
2648
|
+
}
|
|
1598
2649
|
var CanvasEditor = class {
|
|
1599
2650
|
canvas;
|
|
1600
2651
|
layers;
|
|
@@ -1604,9 +2655,12 @@ var CanvasEditor = class {
|
|
|
1604
2655
|
snapping;
|
|
1605
2656
|
crop;
|
|
1606
2657
|
patterns;
|
|
2658
|
+
curves;
|
|
2659
|
+
maskPresets;
|
|
1607
2660
|
fonts;
|
|
1608
2661
|
licensing;
|
|
1609
2662
|
pages;
|
|
2663
|
+
masks;
|
|
1610
2664
|
fileAdapter;
|
|
1611
2665
|
imageProvider;
|
|
1612
2666
|
zoomLevel = 1;
|
|
@@ -1616,6 +2670,7 @@ var CanvasEditor = class {
|
|
|
1616
2670
|
// for serialization and export — not the (possibly transient) canvas value.
|
|
1617
2671
|
designBackground;
|
|
1618
2672
|
designBackgroundImage = null;
|
|
2673
|
+
backgroundImageOptions = null;
|
|
1619
2674
|
constructor(canvasElement, config) {
|
|
1620
2675
|
this.events = new EventEmitter();
|
|
1621
2676
|
this.fonts = new FontRegistry();
|
|
@@ -1625,7 +2680,7 @@ var CanvasEditor = class {
|
|
|
1625
2680
|
const widthPx = this.units.toPixels(config.width);
|
|
1626
2681
|
const heightPx = this.units.toPixels(config.height);
|
|
1627
2682
|
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
1628
|
-
this.canvas = new
|
|
2683
|
+
this.canvas = new import_fabric9.Canvas(canvasElement, {
|
|
1629
2684
|
width: widthPx,
|
|
1630
2685
|
height: heightPx,
|
|
1631
2686
|
backgroundColor: this.designBackground,
|
|
@@ -1653,14 +2708,17 @@ var CanvasEditor = class {
|
|
|
1653
2708
|
this.events,
|
|
1654
2709
|
config.patternSourceResolver
|
|
1655
2710
|
);
|
|
2711
|
+
this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
|
|
2712
|
+
this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
|
|
1656
2713
|
this.setupCanvasEvents();
|
|
1657
2714
|
this.history.saveImmediate();
|
|
1658
2715
|
this.pages = new ProjectManager(this);
|
|
2716
|
+
this.masks = new MaskController(this);
|
|
1659
2717
|
}
|
|
1660
2718
|
// ─── Layer Operations ────────────────────────────────
|
|
1661
2719
|
async addImage(url, options) {
|
|
1662
2720
|
try {
|
|
1663
|
-
const img = await
|
|
2721
|
+
const img = await import_fabric9.FabricImage.fromURL(
|
|
1664
2722
|
url,
|
|
1665
2723
|
{},
|
|
1666
2724
|
{ originX: "left", originY: "top", ...options }
|
|
@@ -1676,14 +2734,16 @@ var CanvasEditor = class {
|
|
|
1676
2734
|
/** Replace an image source without changing its layer identity or visual transform. */
|
|
1677
2735
|
async replaceImageSource(layerId, url) {
|
|
1678
2736
|
const layer = this.layers.get(layerId);
|
|
1679
|
-
if (!layer || layer.type !== "image"
|
|
2737
|
+
if (!layer || layer.type !== "image" && layer.type !== "mask") {
|
|
2738
|
+
throw new Error(`Image or mask layer not found: ${layerId}`);
|
|
2739
|
+
}
|
|
1680
2740
|
if (layer.meta.pattern) {
|
|
1681
2741
|
throw new Error("Clear the pattern before replacing the image source");
|
|
1682
2742
|
}
|
|
1683
2743
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
1684
2744
|
const previous = layer.fabricObject;
|
|
1685
2745
|
try {
|
|
1686
|
-
const replacement = await
|
|
2746
|
+
const replacement = await import_fabric9.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
1687
2747
|
replacement.set({
|
|
1688
2748
|
left: previous.left,
|
|
1689
2749
|
top: previous.top,
|
|
@@ -1712,7 +2772,7 @@ var CanvasEditor = class {
|
|
|
1712
2772
|
}
|
|
1713
2773
|
}
|
|
1714
2774
|
addText(text, options) {
|
|
1715
|
-
const textbox = new
|
|
2775
|
+
const textbox = new import_fabric9.Textbox(text, {
|
|
1716
2776
|
fontSize: 32,
|
|
1717
2777
|
fontFamily: "Arial",
|
|
1718
2778
|
fill: "#000000",
|
|
@@ -1751,10 +2811,10 @@ var CanvasEditor = class {
|
|
|
1751
2811
|
return value === void 0 ? token : escapeXml(value);
|
|
1752
2812
|
})
|
|
1753
2813
|
);
|
|
1754
|
-
const { objects, options } = await (0,
|
|
2814
|
+
const { objects, options } = await (0, import_fabric9.loadSVGFromString)(resolved);
|
|
1755
2815
|
const validObjects = objects.filter((object) => object !== null);
|
|
1756
2816
|
if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
1757
|
-
const group =
|
|
2817
|
+
const group = import_fabric9.util.groupSVGElements(validObjects, options);
|
|
1758
2818
|
group.set({
|
|
1759
2819
|
left: this.canvas.getWidth() / 2,
|
|
1760
2820
|
top: this.canvas.getHeight() / 2,
|
|
@@ -1767,6 +2827,9 @@ var CanvasEditor = class {
|
|
|
1767
2827
|
}
|
|
1768
2828
|
removeLayer(id) {
|
|
1769
2829
|
if (this.crop.activeLayerId() === id) this.crop.cancel();
|
|
2830
|
+
if (this.masks.activeLayerId() === id) {
|
|
2831
|
+
this.masks.detach(id);
|
|
2832
|
+
}
|
|
1770
2833
|
if (this.layers.remove(id)) this.history.save();
|
|
1771
2834
|
}
|
|
1772
2835
|
selectLayer(id) {
|
|
@@ -1824,10 +2887,10 @@ var CanvasEditor = class {
|
|
|
1824
2887
|
const next = { ...previous, ...adjustments };
|
|
1825
2888
|
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
1826
2889
|
image.filters = [
|
|
1827
|
-
new
|
|
1828
|
-
new
|
|
1829
|
-
new
|
|
1830
|
-
new
|
|
2890
|
+
new import_fabric9.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
|
|
2891
|
+
new import_fabric9.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
|
|
2892
|
+
new import_fabric9.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
|
|
2893
|
+
new import_fabric9.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
|
|
1831
2894
|
];
|
|
1832
2895
|
layer.meta.imageAdjustments = next;
|
|
1833
2896
|
image.applyFilters();
|
|
@@ -1844,7 +2907,7 @@ var CanvasEditor = class {
|
|
|
1844
2907
|
const childData = children.map((layer) => structuredClone(layer.toData()));
|
|
1845
2908
|
const objects = children.map((layer) => layer.fabricObject);
|
|
1846
2909
|
for (const layer of children) this.layers.remove(layer.id);
|
|
1847
|
-
const group = new
|
|
2910
|
+
const group = new import_fabric9.Group(objects);
|
|
1848
2911
|
const grouped = this.layers.add("group", group, name);
|
|
1849
2912
|
grouped.meta.groupChildren = childData;
|
|
1850
2913
|
this.layers.select(grouped.id);
|
|
@@ -1863,7 +2926,7 @@ var CanvasEditor = class {
|
|
|
1863
2926
|
const objects = group.removeAll();
|
|
1864
2927
|
this.layers.remove(id);
|
|
1865
2928
|
const restored = objects.map((object, index) => {
|
|
1866
|
-
|
|
2929
|
+
import_fabric9.util.addTransformToObject(object, transform);
|
|
1867
2930
|
object.setCoords();
|
|
1868
2931
|
const data = childData[index];
|
|
1869
2932
|
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
@@ -1914,6 +2977,50 @@ var CanvasEditor = class {
|
|
|
1914
2977
|
async toWebP(options) {
|
|
1915
2978
|
return this.toRaster("webp", options);
|
|
1916
2979
|
}
|
|
2980
|
+
/** Export one layer in document coordinates or at its native image resolution. */
|
|
2981
|
+
async exportLayer(id, options = {}) {
|
|
2982
|
+
const layer = this.layers.get(id);
|
|
2983
|
+
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
2984
|
+
try {
|
|
2985
|
+
if (options.resolution === "source" && layer.fabricObject instanceof import_fabric9.FabricImage) {
|
|
2986
|
+
const image = await layer.fabricObject.clone();
|
|
2987
|
+
image.set({
|
|
2988
|
+
left: 0,
|
|
2989
|
+
top: 0,
|
|
2990
|
+
originX: "left",
|
|
2991
|
+
originY: "top",
|
|
2992
|
+
scaleX: 1,
|
|
2993
|
+
scaleY: 1,
|
|
2994
|
+
angle: 0,
|
|
2995
|
+
flipX: false,
|
|
2996
|
+
flipY: false
|
|
2997
|
+
});
|
|
2998
|
+
return await exportIsolatedPNG(this.canvas, [image], {
|
|
2999
|
+
...options,
|
|
3000
|
+
width: image.width || 1,
|
|
3001
|
+
height: image.height || 1,
|
|
3002
|
+
cloneObjects: false
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
return await exportIsolatedPNG(this.canvas, [layer.fabricObject], options);
|
|
3006
|
+
} catch (error) {
|
|
3007
|
+
this.events.emit("error", { message: `Failed to export layer: ${id}`, error });
|
|
3008
|
+
throw error;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
/** Export only the configured document background, excluding design layers. */
|
|
3012
|
+
async exportBackground(options = {}) {
|
|
3013
|
+
try {
|
|
3014
|
+
return await exportIsolatedPNG(this.canvas, [], {
|
|
3015
|
+
...options,
|
|
3016
|
+
backgroundColor: this.designBackground,
|
|
3017
|
+
backgroundImage: this.designBackgroundImage
|
|
3018
|
+
});
|
|
3019
|
+
} catch (error) {
|
|
3020
|
+
this.events.emit("error", { message: "Failed to export background", error });
|
|
3021
|
+
throw error;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
1917
3024
|
async toRaster(format, options) {
|
|
1918
3025
|
this.events.emit("export:start", { format });
|
|
1919
3026
|
try {
|
|
@@ -1925,7 +3032,10 @@ var CanvasEditor = class {
|
|
|
1925
3032
|
this.licensing.track(`export:${format}`);
|
|
1926
3033
|
return blob;
|
|
1927
3034
|
} catch (error) {
|
|
1928
|
-
this.events.emit("error", {
|
|
3035
|
+
this.events.emit("error", {
|
|
3036
|
+
message: isTaintedCanvasError(error) ? "Canvas export was blocked by cross-origin image data; load remote images with CORS enabled" : `Failed to export ${format.toUpperCase()}`,
|
|
3037
|
+
error
|
|
3038
|
+
});
|
|
1929
3039
|
throw error;
|
|
1930
3040
|
}
|
|
1931
3041
|
}
|
|
@@ -1956,6 +3066,23 @@ var CanvasEditor = class {
|
|
|
1956
3066
|
toDataURL(format = "png", multiplier = 1) {
|
|
1957
3067
|
return this.withDesignBackground(() => exportDataURL(this.canvas, format, multiplier));
|
|
1958
3068
|
}
|
|
3069
|
+
/**
|
|
3070
|
+
* Export the print file: the design cropped to the mockup's print area, on
|
|
3071
|
+
* transparency. Without a print area this is the whole canvas, still
|
|
3072
|
+
* transparent — a print file never carries the design background.
|
|
3073
|
+
*/
|
|
3074
|
+
async toPrintFile(options = {}) {
|
|
3075
|
+
await this.fonts.ready();
|
|
3076
|
+
try {
|
|
3077
|
+
const area = this.mockup?.printArea;
|
|
3078
|
+
const blob = area ? await exportPrintArea(this.canvas, area, options) : await exportIsolatedPNG(this.canvas, this.canvas.getObjects(), options);
|
|
3079
|
+
this.licensing.track(`export:print:${options.format ?? "png"}`);
|
|
3080
|
+
return blob;
|
|
3081
|
+
} catch (error) {
|
|
3082
|
+
this.events.emit("error", { message: "Failed to export print file", error });
|
|
3083
|
+
throw error;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
1959
3086
|
/** Export the current product-preview composite. Advanced warping is host-defined. */
|
|
1960
3087
|
async toMockupImage(options = {}) {
|
|
1961
3088
|
if (!this.mockup) throw new Error("No mockup is configured");
|
|
@@ -2118,13 +3245,20 @@ var CanvasEditor = class {
|
|
|
2118
3245
|
getDesignBackgroundImage() {
|
|
2119
3246
|
return this.designBackgroundImage;
|
|
2120
3247
|
}
|
|
3248
|
+
getBackgroundImageOptions() {
|
|
3249
|
+
return this.backgroundImageOptions ? { ...this.backgroundImageOptions } : null;
|
|
3250
|
+
}
|
|
2121
3251
|
async setBackgroundImage(url, options = {}) {
|
|
2122
3252
|
if (url === null) {
|
|
2123
3253
|
this.setBackgroundImageObject(null);
|
|
2124
3254
|
return;
|
|
2125
3255
|
}
|
|
2126
3256
|
try {
|
|
2127
|
-
const image = await
|
|
3257
|
+
const image = await import_fabric9.FabricImage.fromURL(
|
|
3258
|
+
url,
|
|
3259
|
+
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
3260
|
+
{ originX: "left", originY: "top" }
|
|
3261
|
+
);
|
|
2128
3262
|
const width = image.width || 1;
|
|
2129
3263
|
const height = image.height || 1;
|
|
2130
3264
|
const canvasWidth = this.canvas.getWidth();
|
|
@@ -2143,15 +3277,18 @@ var CanvasEditor = class {
|
|
|
2143
3277
|
selectable: false,
|
|
2144
3278
|
evented: false
|
|
2145
3279
|
});
|
|
2146
|
-
|
|
3280
|
+
const serializableOptions = { ...options };
|
|
3281
|
+
delete serializableOptions.signal;
|
|
3282
|
+
this.setBackgroundImageObject(image, true, serializableOptions);
|
|
2147
3283
|
} catch (error) {
|
|
2148
3284
|
this.events.emit("error", { message: "Failed to set background image", error });
|
|
2149
3285
|
throw error;
|
|
2150
3286
|
}
|
|
2151
3287
|
}
|
|
2152
3288
|
/** Used by state restoration and advanced integrations with an existing Fabric object. */
|
|
2153
|
-
setBackgroundImageObject(image, save = true) {
|
|
3289
|
+
setBackgroundImageObject(image, save = true, options = null) {
|
|
2154
3290
|
this.designBackgroundImage = image;
|
|
3291
|
+
this.backgroundImageOptions = image ? options : null;
|
|
2155
3292
|
this.canvas.backgroundImage = this.mockup ? void 0 : image ?? void 0;
|
|
2156
3293
|
this.canvas.requestRenderAll();
|
|
2157
3294
|
if (save) this.history.save();
|
|
@@ -2292,14 +3429,89 @@ var CanvasEditor = class {
|
|
|
2292
3429
|
clearPattern(layerId) {
|
|
2293
3430
|
return this.patterns.disable(layerId);
|
|
2294
3431
|
}
|
|
3432
|
+
// ─── Text curve ─────────────────────────────────────
|
|
3433
|
+
applyTextCurve(layerId, config) {
|
|
3434
|
+
return this.curves.apply(layerId, config);
|
|
3435
|
+
}
|
|
3436
|
+
clearTextCurve(layerId) {
|
|
3437
|
+
return this.curves.clear(layerId);
|
|
3438
|
+
}
|
|
3439
|
+
getTextCurve(layerId) {
|
|
3440
|
+
return this.curves.get(layerId);
|
|
3441
|
+
}
|
|
3442
|
+
// ─── Mask presets ───────────────────────────────────
|
|
3443
|
+
applyMaskPreset(layerId, id) {
|
|
3444
|
+
return this.maskPresets.apply(layerId, id);
|
|
3445
|
+
}
|
|
3446
|
+
clearMaskPreset(layerId) {
|
|
3447
|
+
return this.maskPresets.clear(layerId);
|
|
3448
|
+
}
|
|
3449
|
+
getMaskPreset(layerId) {
|
|
3450
|
+
return this.maskPresets.get(layerId);
|
|
3451
|
+
}
|
|
3452
|
+
// ─── Layer transform ────────────────────────────────
|
|
3453
|
+
/**
|
|
3454
|
+
* Constrain a layer to its current proportions. Persisted on the layer so a
|
|
3455
|
+
* reopened design still resizes the way it was set up to.
|
|
3456
|
+
*/
|
|
3457
|
+
setLayerAspectLock(layerId, locked) {
|
|
3458
|
+
const layer = this.layers.get(layerId);
|
|
3459
|
+
if (!layer) return false;
|
|
3460
|
+
applyAspectLock(layer.fabricObject, locked);
|
|
3461
|
+
if (locked) layer.meta.lockAspect = true;
|
|
3462
|
+
else delete layer.meta.lockAspect;
|
|
3463
|
+
this.canvas.requestRenderAll();
|
|
3464
|
+
this.events.emit("layer:modified", { layerId });
|
|
3465
|
+
this.history.save();
|
|
3466
|
+
return true;
|
|
3467
|
+
}
|
|
3468
|
+
getLayerAspectLock(layerId) {
|
|
3469
|
+
return this.layers.get(layerId)?.meta.lockAspect === true;
|
|
3470
|
+
}
|
|
3471
|
+
/** Re-apply every stored aspect lock — control visibility is not serialized. */
|
|
3472
|
+
restoreAspectLocks() {
|
|
3473
|
+
for (const layer of this.layers.getAll()) {
|
|
3474
|
+
if (layer.meta.lockAspect) applyAspectLock(layer.fabricObject, true);
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
/** Drop scale, rotation, skew and flips; the layer stays where it is. */
|
|
3478
|
+
resetLayerTransform(layerId) {
|
|
3479
|
+
const layer = this.layers.get(layerId);
|
|
3480
|
+
if (!layer) return false;
|
|
3481
|
+
resetTransform(layer.fabricObject);
|
|
3482
|
+
this.canvas.requestRenderAll();
|
|
3483
|
+
this.events.emit("layer:modified", { layerId });
|
|
3484
|
+
this.history.save();
|
|
3485
|
+
return true;
|
|
3486
|
+
}
|
|
3487
|
+
// ─── Shadow ─────────────────────────────────────────
|
|
3488
|
+
setLayerShadow(layerId, config) {
|
|
3489
|
+
const layer = this.layers.get(layerId);
|
|
3490
|
+
if (!layer) return false;
|
|
3491
|
+
applyLayerShadow(layer.fabricObject, config);
|
|
3492
|
+
layer.fabricObject.dirty = true;
|
|
3493
|
+
this.canvas.requestRenderAll();
|
|
3494
|
+
this.events.emit("layer:modified", { layerId });
|
|
3495
|
+
this.history.save();
|
|
3496
|
+
return true;
|
|
3497
|
+
}
|
|
3498
|
+
getLayerShadow(layerId) {
|
|
3499
|
+
const layer = this.layers.get(layerId);
|
|
3500
|
+
return layer ? readLayerShadow(layer.fabricObject) : null;
|
|
3501
|
+
}
|
|
2295
3502
|
// ─── Mockup (preview-only) ──────────────────────────
|
|
2296
|
-
|
|
3503
|
+
/**
|
|
3504
|
+
* Show (or clear) the product preview. Pass `history: false` for preview-only
|
|
3505
|
+
* changes such as swapping a colourway — those are not design edits and
|
|
3506
|
+
* should not fill the undo stack.
|
|
3507
|
+
*/
|
|
3508
|
+
setMockup(mockup, options = {}) {
|
|
2297
3509
|
this.mockup = mockup;
|
|
2298
3510
|
this.canvas.backgroundColor = mockup ? "" : this.designBackground;
|
|
2299
3511
|
this.canvas.backgroundImage = mockup ? void 0 : this.designBackgroundImage ?? void 0;
|
|
2300
3512
|
this.canvas.requestRenderAll();
|
|
2301
3513
|
this.events.emit("mockup:changed", { mockup });
|
|
2302
|
-
this.history.save();
|
|
3514
|
+
if (options.history !== false) this.history.save();
|
|
2303
3515
|
}
|
|
2304
3516
|
clearMockup() {
|
|
2305
3517
|
this.setMockup(null);
|
|
@@ -2309,6 +3521,7 @@ var CanvasEditor = class {
|
|
|
2309
3521
|
}
|
|
2310
3522
|
// ─── Cleanup ────────────────────────────────────────
|
|
2311
3523
|
dispose() {
|
|
3524
|
+
this.masks.dispose();
|
|
2312
3525
|
this.snapping.dispose();
|
|
2313
3526
|
this.crop.dispose();
|
|
2314
3527
|
this.history.dispose();
|
|
@@ -2363,43 +3576,108 @@ var CANVAS_SIZE_PRESETS = [
|
|
|
2363
3576
|
{ id: "instagram-square", name: "Social square", width: 1080, height: 1080, unit: "px", dpi: 72 },
|
|
2364
3577
|
{ id: "story", name: "Story", width: 1080, height: 1920, unit: "px", dpi: 72 }
|
|
2365
3578
|
];
|
|
3579
|
+
|
|
3580
|
+
// src/annotations.ts
|
|
3581
|
+
var AnnotationOverlay = class {
|
|
3582
|
+
items = /* @__PURE__ */ new Map();
|
|
3583
|
+
transform = { zoom: 1, panX: 0, panY: 0, devicePixelRatio: 1 };
|
|
3584
|
+
set(annotation) {
|
|
3585
|
+
this.items.set(annotation.id, structuredClone(annotation));
|
|
3586
|
+
}
|
|
3587
|
+
remove(id) {
|
|
3588
|
+
return this.items.delete(id);
|
|
3589
|
+
}
|
|
3590
|
+
clear() {
|
|
3591
|
+
this.items.clear();
|
|
3592
|
+
}
|
|
3593
|
+
getAll() {
|
|
3594
|
+
return [...this.items.values()].map((item) => structuredClone(item));
|
|
3595
|
+
}
|
|
3596
|
+
setTransform(transform) {
|
|
3597
|
+
if (!Number.isFinite(transform.zoom) || transform.zoom <= 0) {
|
|
3598
|
+
throw new Error("Annotation zoom must be positive");
|
|
3599
|
+
}
|
|
3600
|
+
this.transform = { ...transform, devicePixelRatio: transform.devicePixelRatio ?? 1 };
|
|
3601
|
+
}
|
|
3602
|
+
documentToViewport(point) {
|
|
3603
|
+
return {
|
|
3604
|
+
x: point.x * this.transform.zoom + this.transform.panX,
|
|
3605
|
+
y: point.y * this.transform.zoom + this.transform.panY
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
viewportToDocument(point) {
|
|
3609
|
+
return {
|
|
3610
|
+
x: (point.x - this.transform.panX) / this.transform.zoom,
|
|
3611
|
+
y: (point.y - this.transform.panY) / this.transform.zoom
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
documentToDevice(point) {
|
|
3615
|
+
const viewport = this.documentToViewport(point);
|
|
3616
|
+
const ratio = this.transform.devicePixelRatio ?? 1;
|
|
3617
|
+
return { x: viewport.x * ratio, y: viewport.y * ratio };
|
|
3618
|
+
}
|
|
3619
|
+
};
|
|
2366
3620
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2367
3621
|
0 && (module.exports = {
|
|
3622
|
+
AnnotationOverlay,
|
|
2368
3623
|
CANVAS_SIZE_PRESETS,
|
|
2369
3624
|
CanvasEditor,
|
|
2370
3625
|
CropController,
|
|
3626
|
+
DEFAULT_LAYER_SHADOW,
|
|
2371
3627
|
DEFAULT_PATTERN_CONFIG,
|
|
3628
|
+
DEFAULT_TEXT_CURVE,
|
|
2372
3629
|
EventEmitter,
|
|
2373
3630
|
FontRegistry,
|
|
2374
3631
|
HistoryManager,
|
|
2375
3632
|
Layer,
|
|
2376
3633
|
LayerManager,
|
|
2377
3634
|
LicenseManager,
|
|
3635
|
+
MaskController,
|
|
3636
|
+
MaskPresetManager,
|
|
3637
|
+
MaskRefinementError,
|
|
2378
3638
|
PatternManager,
|
|
2379
3639
|
ProjectManager,
|
|
3640
|
+
SHAPE_MASK_BOX,
|
|
3641
|
+
SHAPE_MASK_IDS,
|
|
2380
3642
|
SnapManager,
|
|
3643
|
+
TEXTURE_MASK_IDS,
|
|
3644
|
+
TEXTURE_MASK_SIZE,
|
|
3645
|
+
TextCurveManager,
|
|
2381
3646
|
UnitConverter,
|
|
3647
|
+
applyAspectLock,
|
|
3648
|
+
applyLayerShadow,
|
|
2382
3649
|
applyPatternLocks,
|
|
3650
|
+
buildCurvePathData,
|
|
2383
3651
|
buildPatternDataURL,
|
|
2384
3652
|
captureLocks,
|
|
2385
3653
|
clamp,
|
|
2386
3654
|
clearPatternImageCache,
|
|
3655
|
+
clearTextureMaskCache,
|
|
2387
3656
|
computeCoverPlacement,
|
|
2388
3657
|
computePrintAreaClip,
|
|
2389
3658
|
computeTilePositions,
|
|
2390
3659
|
deserializeEditor,
|
|
3660
|
+
displaceRgba,
|
|
2391
3661
|
drawTiles,
|
|
2392
3662
|
escapeXml,
|
|
2393
3663
|
exportDataURL,
|
|
2394
3664
|
exportMockup,
|
|
2395
3665
|
exportPNG,
|
|
3666
|
+
exportPrintArea,
|
|
2396
3667
|
exportSVG,
|
|
2397
3668
|
generateId,
|
|
2398
3669
|
isCssColor,
|
|
3670
|
+
isMaskPresetId,
|
|
3671
|
+
isShapeMaskId,
|
|
3672
|
+
isTextureMaskId,
|
|
2399
3673
|
loadPatternImage,
|
|
3674
|
+
readLayerShadow,
|
|
3675
|
+
renderTextureMask,
|
|
3676
|
+
resetTransform,
|
|
2400
3677
|
restoreLocks,
|
|
2401
3678
|
round2,
|
|
2402
3679
|
sanitizeSvg,
|
|
2403
|
-
serializeEditor
|
|
3680
|
+
serializeEditor,
|
|
3681
|
+
shapeMaskPathData
|
|
2404
3682
|
});
|
|
2405
3683
|
//# sourceMappingURL=index.js.map
|