@overtone-art/canvas-editor-core 0.2.8 → 0.3.1
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 → chunk-MCBRZQ4M.mjs} +34 -1
- package/dist/chunk-MCBRZQ4M.mjs.map +1 -0
- package/dist/index.d.mts +184 -4
- package/dist/index.d.ts +184 -4
- package/dist/index.global.js +67 -67
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +742 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +676 -22
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js.map +1 -1
- package/dist/node.mjs +1 -1
- package/dist/{types-D60CfxL9.d.mts → types-Dh1unrzT.d.mts} +28 -1
- package/dist/{types-D60CfxL9.d.ts → types-Dh1unrzT.d.ts} +28 -1
- package/package.json +6 -4
- package/dist/chunk-ORZZ6MGQ.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -24,7 +24,9 @@ __export(index_exports, {
|
|
|
24
24
|
CANVAS_SIZE_PRESETS: () => CANVAS_SIZE_PRESETS,
|
|
25
25
|
CanvasEditor: () => CanvasEditor,
|
|
26
26
|
CropController: () => CropController,
|
|
27
|
+
DEFAULT_LAYER_SHADOW: () => DEFAULT_LAYER_SHADOW,
|
|
27
28
|
DEFAULT_PATTERN_CONFIG: () => DEFAULT_PATTERN_CONFIG,
|
|
29
|
+
DEFAULT_TEXT_CURVE: () => DEFAULT_TEXT_CURVE,
|
|
28
30
|
EventEmitter: () => EventEmitter,
|
|
29
31
|
FontRegistry: () => FontRegistry,
|
|
30
32
|
HistoryManager: () => HistoryManager,
|
|
@@ -32,16 +34,26 @@ __export(index_exports, {
|
|
|
32
34
|
LayerManager: () => LayerManager,
|
|
33
35
|
LicenseManager: () => LicenseManager,
|
|
34
36
|
MaskController: () => MaskController,
|
|
37
|
+
MaskPresetManager: () => MaskPresetManager,
|
|
35
38
|
MaskRefinementError: () => MaskRefinementError,
|
|
36
39
|
PatternManager: () => PatternManager,
|
|
37
40
|
ProjectManager: () => ProjectManager,
|
|
41
|
+
SHAPE_MASK_BOX: () => SHAPE_MASK_BOX,
|
|
42
|
+
SHAPE_MASK_IDS: () => SHAPE_MASK_IDS,
|
|
38
43
|
SnapManager: () => SnapManager,
|
|
44
|
+
TEXTURE_MASK_IDS: () => TEXTURE_MASK_IDS,
|
|
45
|
+
TEXTURE_MASK_SIZE: () => TEXTURE_MASK_SIZE,
|
|
46
|
+
TextCurveManager: () => TextCurveManager,
|
|
39
47
|
UnitConverter: () => UnitConverter,
|
|
48
|
+
applyAspectLock: () => applyAspectLock,
|
|
49
|
+
applyLayerShadow: () => applyLayerShadow,
|
|
40
50
|
applyPatternLocks: () => applyPatternLocks,
|
|
51
|
+
buildCurvePathData: () => buildCurvePathData,
|
|
41
52
|
buildPatternDataURL: () => buildPatternDataURL,
|
|
42
53
|
captureLocks: () => captureLocks,
|
|
43
54
|
clamp: () => clamp,
|
|
44
55
|
clearPatternImageCache: () => clearPatternImageCache,
|
|
56
|
+
clearTextureMaskCache: () => clearTextureMaskCache,
|
|
45
57
|
computeCoverPlacement: () => computeCoverPlacement,
|
|
46
58
|
computePrintAreaClip: () => computePrintAreaClip,
|
|
47
59
|
computeTilePositions: () => computeTilePositions,
|
|
@@ -52,19 +64,27 @@ __export(index_exports, {
|
|
|
52
64
|
exportDataURL: () => exportDataURL,
|
|
53
65
|
exportMockup: () => exportMockup,
|
|
54
66
|
exportPNG: () => exportPNG,
|
|
67
|
+
exportPrintArea: () => exportPrintArea,
|
|
55
68
|
exportSVG: () => exportSVG,
|
|
56
69
|
generateId: () => generateId,
|
|
57
70
|
isCssColor: () => isCssColor,
|
|
71
|
+
isMaskPresetId: () => isMaskPresetId,
|
|
72
|
+
isShapeMaskId: () => isShapeMaskId,
|
|
73
|
+
isTextureMaskId: () => isTextureMaskId,
|
|
58
74
|
loadPatternImage: () => loadPatternImage,
|
|
75
|
+
readLayerShadow: () => readLayerShadow,
|
|
76
|
+
renderTextureMask: () => renderTextureMask,
|
|
77
|
+
resetTransform: () => resetTransform,
|
|
59
78
|
restoreLocks: () => restoreLocks,
|
|
60
79
|
round2: () => round2,
|
|
61
80
|
sanitizeSvg: () => sanitizeSvg,
|
|
62
|
-
serializeEditor: () => serializeEditor
|
|
81
|
+
serializeEditor: () => serializeEditor,
|
|
82
|
+
shapeMaskPathData: () => shapeMaskPathData
|
|
63
83
|
});
|
|
64
84
|
module.exports = __toCommonJS(index_exports);
|
|
65
85
|
|
|
66
86
|
// src/editor.ts
|
|
67
|
-
var
|
|
87
|
+
var import_fabric9 = require("fabric");
|
|
68
88
|
|
|
69
89
|
// src/events.ts
|
|
70
90
|
var EventEmitter = class {
|
|
@@ -284,6 +304,38 @@ var LayerManager = class {
|
|
|
284
304
|
this.emitChanged();
|
|
285
305
|
this.onPropertyChanged?.();
|
|
286
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Patch a layer's host metadata. A key whose value is `undefined` is removed.
|
|
309
|
+
*
|
|
310
|
+
* `meta` is part of `LayerData` and is serialized, so a host that assigns
|
|
311
|
+
* `layer.meta.x` directly gets neither of the two things every other setter
|
|
312
|
+
* here provides: `layers:changed` (so `useLayers()` keeps returning the old
|
|
313
|
+
* meta) and the history checkpoint (so an undo silently reverts the write —
|
|
314
|
+
* the state IS versioned, it was just never committed at the moment it
|
|
315
|
+
* changed). Going through this method is what makes metadata behave like
|
|
316
|
+
* every other layer property.
|
|
317
|
+
*/
|
|
318
|
+
setMeta(id, patch) {
|
|
319
|
+
const layer = this.get(id);
|
|
320
|
+
if (!layer) return;
|
|
321
|
+
const next = { ...layer.meta };
|
|
322
|
+
let changed = false;
|
|
323
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
324
|
+
if (value === void 0) {
|
|
325
|
+
if (key in next) {
|
|
326
|
+
delete next[key];
|
|
327
|
+
changed = true;
|
|
328
|
+
}
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (next[key] !== value) changed = true;
|
|
332
|
+
next[key] = value;
|
|
333
|
+
}
|
|
334
|
+
if (!changed) return;
|
|
335
|
+
layer.meta = next;
|
|
336
|
+
this.emitChanged();
|
|
337
|
+
this.onPropertyChanged?.();
|
|
338
|
+
}
|
|
287
339
|
clear() {
|
|
288
340
|
for (const layer of this.layers) {
|
|
289
341
|
this.canvas.remove(layer.fabricObject);
|
|
@@ -1142,6 +1194,521 @@ function mod2(n) {
|
|
|
1142
1194
|
return (n % 2 + 2) % 2;
|
|
1143
1195
|
}
|
|
1144
1196
|
|
|
1197
|
+
// src/text-curve.ts
|
|
1198
|
+
var import_fabric3 = require("fabric");
|
|
1199
|
+
var DEFAULT_TEXT_CURVE = { arc: 0, wave: 0 };
|
|
1200
|
+
var MIN_ARC = 0.5;
|
|
1201
|
+
var FULL_CIRCLE_ARC = 99.5;
|
|
1202
|
+
var MIN_SWEEP = 0.12;
|
|
1203
|
+
var WAVE_PERIOD_EM = 4.1;
|
|
1204
|
+
var WAVE_AMPLITUDE_EM = 0.9;
|
|
1205
|
+
var WAVE_STEP = 6;
|
|
1206
|
+
var MEASURE_WIDTH = 1e5;
|
|
1207
|
+
var PATH_SLACK = 0.06;
|
|
1208
|
+
function isCurvable(object) {
|
|
1209
|
+
return !!object && typeof object.text === "string";
|
|
1210
|
+
}
|
|
1211
|
+
function measureText(text) {
|
|
1212
|
+
const authored = text.width;
|
|
1213
|
+
try {
|
|
1214
|
+
text.set({ width: MEASURE_WIDTH });
|
|
1215
|
+
text.initDimensions?.();
|
|
1216
|
+
return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
|
|
1217
|
+
} finally {
|
|
1218
|
+
if (authored !== void 0) text.set({ width: authored });
|
|
1219
|
+
text.initDimensions?.();
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function arcPathData(width, arc) {
|
|
1223
|
+
const magnitude = Math.min(100, Math.abs(arc));
|
|
1224
|
+
const direction = arc < 0 ? -1 : 1;
|
|
1225
|
+
const full = magnitude >= FULL_CIRCLE_ARC;
|
|
1226
|
+
const sweep = full ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
|
|
1227
|
+
const radius = width / sweep;
|
|
1228
|
+
const centerX = width / 2;
|
|
1229
|
+
const point = (angle) => [
|
|
1230
|
+
centerX + radius * Math.sin(angle),
|
|
1231
|
+
direction * radius * (1 - Math.cos(angle))
|
|
1232
|
+
];
|
|
1233
|
+
const sweepFlag = direction > 0 ? 1 : 0;
|
|
1234
|
+
const format = ([x, y]) => `${round(x)} ${round(y)}`;
|
|
1235
|
+
if (full) {
|
|
1236
|
+
const start2 = point(-Math.PI);
|
|
1237
|
+
const top = point(0);
|
|
1238
|
+
return {
|
|
1239
|
+
data: [
|
|
1240
|
+
`M ${format(start2)}`,
|
|
1241
|
+
`A ${round(radius)} ${round(radius)} 0 0 ${sweepFlag} ${format(top)}`,
|
|
1242
|
+
`A ${round(radius)} ${round(radius)} 0 0 ${sweepFlag} ${format(start2)}`
|
|
1243
|
+
].join(" "),
|
|
1244
|
+
length: width
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
const start = point(-sweep / 2);
|
|
1248
|
+
const end = point(sweep / 2);
|
|
1249
|
+
const largeArc = sweep > Math.PI ? 1 : 0;
|
|
1250
|
+
return {
|
|
1251
|
+
data: `M ${format(start)} A ${round(radius)} ${round(radius)} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
|
|
1252
|
+
// radius = width / sweep, so the arc is exactly `width` long.
|
|
1253
|
+
length: width
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function wavePathData(width, fontSize, wave) {
|
|
1257
|
+
const amplitude = clamp(wave, 0, 100) / 100 * fontSize * WAVE_AMPLITUDE_EM;
|
|
1258
|
+
const period = Math.max(1, fontSize * WAVE_PERIOD_EM);
|
|
1259
|
+
const steps = Math.max(2, Math.ceil(width / WAVE_STEP));
|
|
1260
|
+
const commands = [];
|
|
1261
|
+
let length = 0;
|
|
1262
|
+
let previous = null;
|
|
1263
|
+
for (let index = 0; index <= steps; index++) {
|
|
1264
|
+
const x = width * index / steps;
|
|
1265
|
+
const y = amplitude * Math.sin(x / period * Math.PI * 2);
|
|
1266
|
+
if (previous) length += Math.hypot(x - previous[0], y - previous[1]);
|
|
1267
|
+
previous = [x, y];
|
|
1268
|
+
commands.push(`${index === 0 ? "M" : "L"} ${round(x)} ${round(y)}`);
|
|
1269
|
+
}
|
|
1270
|
+
return { data: commands.join(" "), length };
|
|
1271
|
+
}
|
|
1272
|
+
function round(value) {
|
|
1273
|
+
return Math.round(value * 100) / 100;
|
|
1274
|
+
}
|
|
1275
|
+
function buildCurvePathData(config, width, fontSize) {
|
|
1276
|
+
if (Math.abs(config.arc) >= MIN_ARC) return arcPathData(width, config.arc);
|
|
1277
|
+
if (config.wave > 0) return wavePathData(width, fontSize, config.wave);
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
function normalize(config) {
|
|
1281
|
+
const arc = clamp(config.arc ?? 0, -100, 100);
|
|
1282
|
+
return { arc, wave: Math.abs(arc) >= MIN_ARC ? 0 : clamp(config.wave ?? 0, 0, 100) };
|
|
1283
|
+
}
|
|
1284
|
+
var TextCurveManager = class {
|
|
1285
|
+
constructor(canvas, layers, history, events) {
|
|
1286
|
+
this.canvas = canvas;
|
|
1287
|
+
this.layers = layers;
|
|
1288
|
+
this.history = history;
|
|
1289
|
+
this.events = events;
|
|
1290
|
+
}
|
|
1291
|
+
canvas;
|
|
1292
|
+
layers;
|
|
1293
|
+
history;
|
|
1294
|
+
events;
|
|
1295
|
+
/** Curve parameters for a layer, or null when it is not curved text. */
|
|
1296
|
+
get(layerId) {
|
|
1297
|
+
const layer = this.layers.get(layerId);
|
|
1298
|
+
if (!layer || !isCurvable(layer.fabricObject)) return null;
|
|
1299
|
+
return layer.meta.curve ?? { ...DEFAULT_TEXT_CURVE };
|
|
1300
|
+
}
|
|
1301
|
+
isCurved(layerId) {
|
|
1302
|
+
const curve = this.get(layerId);
|
|
1303
|
+
return !!curve && (Math.abs(curve.arc) >= MIN_ARC || curve.wave > 0);
|
|
1304
|
+
}
|
|
1305
|
+
/** Apply (or update) the curve on a text layer. Zeroed config clears it. */
|
|
1306
|
+
apply(layerId, config, save = true) {
|
|
1307
|
+
const layer = this.layers.get(layerId);
|
|
1308
|
+
if (!layer || !isCurvable(layer.fabricObject)) return false;
|
|
1309
|
+
const next = normalize(config);
|
|
1310
|
+
const text = layer.fabricObject;
|
|
1311
|
+
const run = measureText(text);
|
|
1312
|
+
const curve = buildCurvePathData(next, run * (1 + PATH_SLACK), text.fontSize);
|
|
1313
|
+
if (!curve) {
|
|
1314
|
+
this.detach(text, layer.meta.curveWidth);
|
|
1315
|
+
delete layer.meta.curve;
|
|
1316
|
+
delete layer.meta.curveWidth;
|
|
1317
|
+
} else {
|
|
1318
|
+
if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
|
|
1319
|
+
text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
|
|
1320
|
+
text.set({
|
|
1321
|
+
path: new import_fabric3.Path(curve.data, { visible: false, objectCaching: false }),
|
|
1322
|
+
pathAlign: "center",
|
|
1323
|
+
pathSide: "left",
|
|
1324
|
+
pathStartOffset: Math.max(0, (curve.length - run) / 2)
|
|
1325
|
+
});
|
|
1326
|
+
layer.meta.curve = next;
|
|
1327
|
+
}
|
|
1328
|
+
text.initDimensions?.();
|
|
1329
|
+
text.setCoords();
|
|
1330
|
+
text.dirty = true;
|
|
1331
|
+
this.canvas.requestRenderAll();
|
|
1332
|
+
this.events.emit("layer:modified", { layerId });
|
|
1333
|
+
if (save) this.history.save();
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
/** Remove the curve, restoring the authored text box width. */
|
|
1337
|
+
clear(layerId, save = true) {
|
|
1338
|
+
return this.apply(layerId, DEFAULT_TEXT_CURVE, save);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Rebuild the path from the stored parameters. Text content, font family and
|
|
1342
|
+
* font size all change the run's width, and the path is sized to that width —
|
|
1343
|
+
* without this the curve keeps the geometry of the text it was created from.
|
|
1344
|
+
*/
|
|
1345
|
+
refresh(layerId, save = false) {
|
|
1346
|
+
const curve = this.layers.get(layerId)?.meta.curve;
|
|
1347
|
+
if (!curve) return false;
|
|
1348
|
+
return this.apply(layerId, curve, save);
|
|
1349
|
+
}
|
|
1350
|
+
/** Rebuild every curved layer — used after a state restore. */
|
|
1351
|
+
refreshAll() {
|
|
1352
|
+
for (const layer of this.layers.getAll()) {
|
|
1353
|
+
if (layer.meta.curve) this.refresh(layer.id);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
detach(text, authoredWidth) {
|
|
1357
|
+
text.set({ path: null, pathStartOffset: 0 });
|
|
1358
|
+
if (authoredWidth !== void 0 && authoredWidth > 0) text.set({ width: authoredWidth });
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
// src/mask-presets/manager.ts
|
|
1363
|
+
var import_fabric4 = require("fabric");
|
|
1364
|
+
|
|
1365
|
+
// src/mask-presets/shapes.ts
|
|
1366
|
+
var SHAPE_MASK_IDS = [
|
|
1367
|
+
"circle",
|
|
1368
|
+
"square",
|
|
1369
|
+
"triangle",
|
|
1370
|
+
"star",
|
|
1371
|
+
"heart",
|
|
1372
|
+
"octagram",
|
|
1373
|
+
"arch",
|
|
1374
|
+
"zigzag"
|
|
1375
|
+
];
|
|
1376
|
+
function isShapeMaskId(value) {
|
|
1377
|
+
return typeof value === "string" && SHAPE_MASK_IDS.includes(value);
|
|
1378
|
+
}
|
|
1379
|
+
function starPoints(points, outer, inner, start = -Math.PI / 2) {
|
|
1380
|
+
const step = Math.PI / points;
|
|
1381
|
+
const coordinates = [];
|
|
1382
|
+
for (let index = 0; index < points * 2; index++) {
|
|
1383
|
+
const radius = index % 2 === 0 ? outer : inner;
|
|
1384
|
+
const angle = start + index * step;
|
|
1385
|
+
const x = 50 + Math.cos(angle) * radius;
|
|
1386
|
+
const y = 50 + Math.sin(angle) * radius;
|
|
1387
|
+
coordinates.push(`${index === 0 ? "M" : "L"} ${round3(x)} ${round3(y)}`);
|
|
1388
|
+
}
|
|
1389
|
+
return `${coordinates.join(" ")} Z`;
|
|
1390
|
+
}
|
|
1391
|
+
function round3(value) {
|
|
1392
|
+
return Math.round(value * 100) / 100;
|
|
1393
|
+
}
|
|
1394
|
+
var SHAPE_PATHS = {
|
|
1395
|
+
circle: "M 96 50 A 46 46 0 1 1 4 50 A 46 46 0 1 1 96 50 Z",
|
|
1396
|
+
square: "M 4 4 H 96 V 96 H 4 Z",
|
|
1397
|
+
triangle: "M 50 4 L 96 96 L 4 96 Z",
|
|
1398
|
+
star: starPoints(5, 46, 22),
|
|
1399
|
+
heart: "M 50 96 C -8 55 12 4 50 28 C 88 4 108 55 50 96 Z",
|
|
1400
|
+
octagram: starPoints(8, 46, 27),
|
|
1401
|
+
arch: "M 4 96 V 50 A 46 46 0 0 1 96 50 V 96 Z",
|
|
1402
|
+
zigzag: starPoints(16, 46, 39)
|
|
1403
|
+
};
|
|
1404
|
+
function shapeMaskPathData(id) {
|
|
1405
|
+
return SHAPE_PATHS[id];
|
|
1406
|
+
}
|
|
1407
|
+
var SHAPE_MASK_BOX = 100;
|
|
1408
|
+
|
|
1409
|
+
// src/mask-presets/textures.ts
|
|
1410
|
+
var TEXTURE_MASK_IDS = [
|
|
1411
|
+
"vignette",
|
|
1412
|
+
"halftone",
|
|
1413
|
+
"spray",
|
|
1414
|
+
"grunge",
|
|
1415
|
+
"torn",
|
|
1416
|
+
"band"
|
|
1417
|
+
];
|
|
1418
|
+
function isTextureMaskId(value) {
|
|
1419
|
+
return typeof value === "string" && TEXTURE_MASK_IDS.includes(value);
|
|
1420
|
+
}
|
|
1421
|
+
var TEXTURE_MASK_SIZE = 320;
|
|
1422
|
+
var cache = /* @__PURE__ */ new Map();
|
|
1423
|
+
function seeded(seed) {
|
|
1424
|
+
let state = seed;
|
|
1425
|
+
return () => {
|
|
1426
|
+
state = state * 16807 % 2147483647;
|
|
1427
|
+
return (state - 1) / 2147483646;
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
function traceRoughEdge(context, random, jitter, inset) {
|
|
1431
|
+
const size = TEXTURE_MASK_SIZE;
|
|
1432
|
+
const corners = [
|
|
1433
|
+
[inset, inset],
|
|
1434
|
+
[size - inset, inset],
|
|
1435
|
+
[size - inset, size - inset],
|
|
1436
|
+
[inset, size - inset]
|
|
1437
|
+
];
|
|
1438
|
+
context.beginPath();
|
|
1439
|
+
for (let corner = 0; corner < corners.length; corner++) {
|
|
1440
|
+
const [fromX, fromY] = corners[corner];
|
|
1441
|
+
const [toX, toY] = corners[(corner + 1) % corners.length];
|
|
1442
|
+
for (let step = 0; step <= 26; step++) {
|
|
1443
|
+
const ratio = step / 26;
|
|
1444
|
+
const x = fromX + (toX - fromX) * ratio + (random() - 0.5) * jitter;
|
|
1445
|
+
const y = fromY + (toY - fromY) * ratio + (random() - 0.5) * jitter;
|
|
1446
|
+
if (corner === 0 && step === 0) context.moveTo(x, y);
|
|
1447
|
+
else context.lineTo(x, y);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
context.closePath();
|
|
1451
|
+
context.fill();
|
|
1452
|
+
}
|
|
1453
|
+
function paint(id, context) {
|
|
1454
|
+
const size = TEXTURE_MASK_SIZE;
|
|
1455
|
+
const center = size / 2;
|
|
1456
|
+
context.fillStyle = "#ffffff";
|
|
1457
|
+
switch (id) {
|
|
1458
|
+
case "vignette": {
|
|
1459
|
+
const gradient = context.createRadialGradient(
|
|
1460
|
+
center,
|
|
1461
|
+
center,
|
|
1462
|
+
size * 0.18,
|
|
1463
|
+
center,
|
|
1464
|
+
center,
|
|
1465
|
+
size * 0.52
|
|
1466
|
+
);
|
|
1467
|
+
gradient.addColorStop(0, "rgba(255,255,255,1)");
|
|
1468
|
+
gradient.addColorStop(1, "rgba(255,255,255,0)");
|
|
1469
|
+
context.fillStyle = gradient;
|
|
1470
|
+
context.fillRect(0, 0, size, size);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
case "band": {
|
|
1474
|
+
const gradient = context.createLinearGradient(0, 0, 0, size);
|
|
1475
|
+
gradient.addColorStop(0, "rgba(255,255,255,0)");
|
|
1476
|
+
gradient.addColorStop(0.25, "rgba(255,255,255,1)");
|
|
1477
|
+
gradient.addColorStop(0.75, "rgba(255,255,255,1)");
|
|
1478
|
+
gradient.addColorStop(1, "rgba(255,255,255,0)");
|
|
1479
|
+
context.fillStyle = gradient;
|
|
1480
|
+
context.fillRect(0, 0, size, size);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
case "halftone": {
|
|
1484
|
+
const pitch = 16;
|
|
1485
|
+
for (let y = pitch / 2; y < size; y += pitch) {
|
|
1486
|
+
for (let x = pitch / 2; x < size; x += pitch) {
|
|
1487
|
+
const distance = Math.hypot(x - center, y - center) / (size * 0.52);
|
|
1488
|
+
const radius = Math.max(0, pitch / 2 * (1 - distance) * 1.15);
|
|
1489
|
+
if (radius < 0.4) continue;
|
|
1490
|
+
context.beginPath();
|
|
1491
|
+
context.arc(x, y, radius, 0, Math.PI * 2);
|
|
1492
|
+
context.fill();
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
case "spray": {
|
|
1498
|
+
const random = seeded(42);
|
|
1499
|
+
const core = context.createRadialGradient(center, center, 10, center, center, size * 0.42);
|
|
1500
|
+
core.addColorStop(0, "rgba(255,255,255,1)");
|
|
1501
|
+
core.addColorStop(1, "rgba(255,255,255,0.85)");
|
|
1502
|
+
context.fillStyle = core;
|
|
1503
|
+
context.beginPath();
|
|
1504
|
+
context.arc(center, center, size * 0.42, 0, Math.PI * 2);
|
|
1505
|
+
context.fill();
|
|
1506
|
+
context.fillStyle = "rgba(255,255,255,0.9)";
|
|
1507
|
+
for (let dot = 0; dot < 900; dot++) {
|
|
1508
|
+
const angle = random() * Math.PI * 2;
|
|
1509
|
+
const distance = size * (0.3 + random() * 0.24);
|
|
1510
|
+
const x = center + Math.cos(angle) * distance;
|
|
1511
|
+
const y = center + Math.sin(angle) * distance;
|
|
1512
|
+
context.globalAlpha = 1 - (distance / size - 0.3) / 0.24;
|
|
1513
|
+
context.beginPath();
|
|
1514
|
+
context.arc(x, y, random() * 2.4, 0, Math.PI * 2);
|
|
1515
|
+
context.fill();
|
|
1516
|
+
}
|
|
1517
|
+
context.globalAlpha = 1;
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
case "grunge": {
|
|
1521
|
+
const random = seeded(7);
|
|
1522
|
+
traceRoughEdge(context, random, 14, size * 0.06);
|
|
1523
|
+
context.globalCompositeOperation = "destination-out";
|
|
1524
|
+
for (let speckle = 0; speckle < 240; speckle++) {
|
|
1525
|
+
const x = random() * size;
|
|
1526
|
+
const y = random() * size;
|
|
1527
|
+
const nearEdge = Math.min(x, y, size - x, size - y) < size * 0.12;
|
|
1528
|
+
if (!nearEdge && random() >= 0.12) continue;
|
|
1529
|
+
context.beginPath();
|
|
1530
|
+
context.arc(x, y, random() * 6 + 1, 0, Math.PI * 2);
|
|
1531
|
+
context.fill();
|
|
1532
|
+
}
|
|
1533
|
+
context.globalCompositeOperation = "source-over";
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
case "torn": {
|
|
1537
|
+
traceRoughEdge(context, seeded(99), 10, size * 0.08);
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
function renderTextureMask(id) {
|
|
1543
|
+
const cached = cache.get(id);
|
|
1544
|
+
if (cached) return cached;
|
|
1545
|
+
if (typeof document === "undefined") {
|
|
1546
|
+
throw new Error("Texture masks require a DOM canvas");
|
|
1547
|
+
}
|
|
1548
|
+
const canvas = document.createElement("canvas");
|
|
1549
|
+
canvas.width = TEXTURE_MASK_SIZE;
|
|
1550
|
+
canvas.height = TEXTURE_MASK_SIZE;
|
|
1551
|
+
const context = canvas.getContext("2d");
|
|
1552
|
+
if (!context) throw new Error("Texture masks require a 2D canvas context");
|
|
1553
|
+
paint(id, context);
|
|
1554
|
+
cache.set(id, canvas);
|
|
1555
|
+
return canvas;
|
|
1556
|
+
}
|
|
1557
|
+
function clearTextureMaskCache() {
|
|
1558
|
+
cache.clear();
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/mask-presets/manager.ts
|
|
1562
|
+
function isMaskPresetId(value) {
|
|
1563
|
+
return isShapeMaskId(value) || isTextureMaskId(value);
|
|
1564
|
+
}
|
|
1565
|
+
var MaskPresetManager = class {
|
|
1566
|
+
constructor(canvas, layers, history, events) {
|
|
1567
|
+
this.canvas = canvas;
|
|
1568
|
+
this.layers = layers;
|
|
1569
|
+
this.history = history;
|
|
1570
|
+
this.events = events;
|
|
1571
|
+
}
|
|
1572
|
+
canvas;
|
|
1573
|
+
layers;
|
|
1574
|
+
history;
|
|
1575
|
+
events;
|
|
1576
|
+
get(layerId) {
|
|
1577
|
+
return this.layers.get(layerId)?.meta.maskPreset ?? null;
|
|
1578
|
+
}
|
|
1579
|
+
/** Clip the layer to `id`. Passing null (or an unknown id) clears the clip. */
|
|
1580
|
+
apply(layerId, id, save = true) {
|
|
1581
|
+
const layer = this.layers.get(layerId);
|
|
1582
|
+
if (!layer) return false;
|
|
1583
|
+
const object = layer.fabricObject;
|
|
1584
|
+
if (id === null) {
|
|
1585
|
+
if (!layer.meta.maskPreset) return false;
|
|
1586
|
+
object.clipPath = void 0;
|
|
1587
|
+
delete layer.meta.maskPreset;
|
|
1588
|
+
} else {
|
|
1589
|
+
if (!isMaskPresetId(id)) return false;
|
|
1590
|
+
object.clipPath = this.buildClip(id, object);
|
|
1591
|
+
layer.meta.maskPreset = id;
|
|
1592
|
+
}
|
|
1593
|
+
object.dirty = true;
|
|
1594
|
+
object.setCoords();
|
|
1595
|
+
this.canvas.requestRenderAll();
|
|
1596
|
+
this.events.emit("layer:modified", { layerId });
|
|
1597
|
+
if (save) this.history.save();
|
|
1598
|
+
return true;
|
|
1599
|
+
}
|
|
1600
|
+
clear(layerId, save = true) {
|
|
1601
|
+
return this.apply(layerId, null, save);
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Re-fit the clip to the layer's current size. The clip is built for the
|
|
1605
|
+
* object's dimensions at the time it was applied; editing text or replacing an
|
|
1606
|
+
* image changes them, and a stale clip would crop the wrong region.
|
|
1607
|
+
*/
|
|
1608
|
+
refresh(layerId, save = false) {
|
|
1609
|
+
const id = this.get(layerId);
|
|
1610
|
+
if (!id) return false;
|
|
1611
|
+
return this.apply(layerId, id, save);
|
|
1612
|
+
}
|
|
1613
|
+
/** Re-fit every masked layer — used after a state restore. */
|
|
1614
|
+
refreshAll() {
|
|
1615
|
+
for (const layer of this.layers.getAll()) {
|
|
1616
|
+
if (layer.meta.maskPreset) this.refresh(layer.id);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
buildClip(id, object) {
|
|
1620
|
+
const width = Math.max(1, object.width ?? 1);
|
|
1621
|
+
const height = Math.max(1, object.height ?? 1);
|
|
1622
|
+
const shared = {
|
|
1623
|
+
originX: "center",
|
|
1624
|
+
originY: "center",
|
|
1625
|
+
left: 0,
|
|
1626
|
+
top: 0,
|
|
1627
|
+
objectCaching: false
|
|
1628
|
+
};
|
|
1629
|
+
if (isShapeMaskId(id)) {
|
|
1630
|
+
return new import_fabric4.Path(shapeMaskPathData(id), {
|
|
1631
|
+
...shared,
|
|
1632
|
+
scaleX: width / SHAPE_MASK_BOX,
|
|
1633
|
+
scaleY: height / SHAPE_MASK_BOX
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
return new import_fabric4.FabricImage(renderTextureMask(id), {
|
|
1637
|
+
...shared,
|
|
1638
|
+
scaleX: width / TEXTURE_MASK_SIZE,
|
|
1639
|
+
scaleY: height / TEXTURE_MASK_SIZE
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
// src/shadow.ts
|
|
1645
|
+
var import_fabric5 = require("fabric");
|
|
1646
|
+
|
|
1647
|
+
// src/utils/color.ts
|
|
1648
|
+
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
1649
|
+
function isCssColor(value, allowEmpty = false) {
|
|
1650
|
+
if (typeof value !== "string") return false;
|
|
1651
|
+
const trimmed = value.trim();
|
|
1652
|
+
if (!trimmed) return allowEmpty;
|
|
1653
|
+
return CSS_COLOR.test(trimmed);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/shadow.ts
|
|
1657
|
+
var DEFAULT_LAYER_SHADOW = {
|
|
1658
|
+
enabled: false,
|
|
1659
|
+
color: "#000000",
|
|
1660
|
+
blur: 12,
|
|
1661
|
+
offsetX: 6,
|
|
1662
|
+
offsetY: 6
|
|
1663
|
+
};
|
|
1664
|
+
function readLayerShadow(object) {
|
|
1665
|
+
const shadow = object.shadow;
|
|
1666
|
+
if (!shadow || typeof shadow === "string") return { ...DEFAULT_LAYER_SHADOW };
|
|
1667
|
+
return {
|
|
1668
|
+
enabled: true,
|
|
1669
|
+
color: typeof shadow.color === "string" ? shadow.color : DEFAULT_LAYER_SHADOW.color,
|
|
1670
|
+
blur: shadow.blur ?? DEFAULT_LAYER_SHADOW.blur,
|
|
1671
|
+
offsetX: shadow.offsetX ?? DEFAULT_LAYER_SHADOW.offsetX,
|
|
1672
|
+
offsetY: shadow.offsetY ?? DEFAULT_LAYER_SHADOW.offsetY
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
function applyLayerShadow(object, config) {
|
|
1676
|
+
const next = { ...readLayerShadow(object), ...config };
|
|
1677
|
+
if (!next.enabled) {
|
|
1678
|
+
object.set({ shadow: null });
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
|
|
1682
|
+
object.set({
|
|
1683
|
+
shadow: new import_fabric5.Shadow({
|
|
1684
|
+
color,
|
|
1685
|
+
blur: Math.max(0, next.blur),
|
|
1686
|
+
offsetX: next.offsetX,
|
|
1687
|
+
offsetY: next.offsetY
|
|
1688
|
+
})
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// src/transform.ts
|
|
1693
|
+
var SIDE_CONTROLS = ["ml", "mr", "mt", "mb"];
|
|
1694
|
+
function applyAspectLock(object, locked) {
|
|
1695
|
+
for (const control of SIDE_CONTROLS) {
|
|
1696
|
+
object.setControlVisible(control, !locked);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function resetTransform(object) {
|
|
1700
|
+
object.set({
|
|
1701
|
+
scaleX: 1,
|
|
1702
|
+
scaleY: 1,
|
|
1703
|
+
angle: 0,
|
|
1704
|
+
skewX: 0,
|
|
1705
|
+
skewY: 0,
|
|
1706
|
+
flipX: false,
|
|
1707
|
+
flipY: false
|
|
1708
|
+
});
|
|
1709
|
+
object.setCoords();
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1145
1712
|
// src/utils/units.ts
|
|
1146
1713
|
var MM_PER_INCH = 25.4;
|
|
1147
1714
|
var UnitConverter = class {
|
|
@@ -1190,18 +1757,7 @@ var UnitConverter = class {
|
|
|
1190
1757
|
};
|
|
1191
1758
|
|
|
1192
1759
|
// src/serialization.ts
|
|
1193
|
-
var
|
|
1194
|
-
|
|
1195
|
-
// src/utils/color.ts
|
|
1196
|
-
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
1197
|
-
function isCssColor(value, allowEmpty = false) {
|
|
1198
|
-
if (typeof value !== "string") return false;
|
|
1199
|
-
const trimmed = value.trim();
|
|
1200
|
-
if (!trimmed) return allowEmpty;
|
|
1201
|
-
return CSS_COLOR.test(trimmed);
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// src/serialization.ts
|
|
1760
|
+
var import_fabric6 = require("fabric");
|
|
1205
1761
|
var VERSION = "2.0.0";
|
|
1206
1762
|
function serializeEditor(editor) {
|
|
1207
1763
|
return {
|
|
@@ -1237,7 +1793,7 @@ async function deserializeEditor(editor, state) {
|
|
|
1237
1793
|
}
|
|
1238
1794
|
const staged = await Promise.all(
|
|
1239
1795
|
state.layers.map(async (serialized) => {
|
|
1240
|
-
const fabricObject = (await
|
|
1796
|
+
const fabricObject = (await import_fabric6.util.enlivenObjects([serialized.fabricObject]))[0];
|
|
1241
1797
|
if (!fabricObject) {
|
|
1242
1798
|
const source = serialized.fabricObject.src;
|
|
1243
1799
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -1248,7 +1804,7 @@ async function deserializeEditor(editor, state) {
|
|
|
1248
1804
|
return { serialized, fabricObject };
|
|
1249
1805
|
})
|
|
1250
1806
|
);
|
|
1251
|
-
const stagedBackground = state.backgroundImage ? (await
|
|
1807
|
+
const stagedBackground = state.backgroundImage ? (await import_fabric6.util.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
1252
1808
|
if (state.backgroundImage && !stagedBackground) {
|
|
1253
1809
|
const source = state.backgroundImage.src;
|
|
1254
1810
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -1280,6 +1836,7 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
1280
1836
|
const layer = editor.layers.add(serialized.type, fabricObject, serialized.name, serialized.id);
|
|
1281
1837
|
if (serialized.meta) {
|
|
1282
1838
|
layer.meta = serialized.meta;
|
|
1839
|
+
if (layer.meta.lockAspect) applyAspectLock(fabricObject, true);
|
|
1283
1840
|
}
|
|
1284
1841
|
if (!serialized.visible) {
|
|
1285
1842
|
editor.layers.setVisibility(layer.id, false);
|
|
@@ -1294,7 +1851,7 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
1294
1851
|
}
|
|
1295
1852
|
|
|
1296
1853
|
// src/export.ts
|
|
1297
|
-
var
|
|
1854
|
+
var import_fabric7 = require("fabric");
|
|
1298
1855
|
|
|
1299
1856
|
// src/displacement.ts
|
|
1300
1857
|
var CHANNEL_INDEX = {
|
|
@@ -1388,7 +1945,7 @@ async function exportPNG(canvas, options = {}) {
|
|
|
1388
1945
|
}
|
|
1389
1946
|
async function exportIsolatedPNG(source, objects, options = {}) {
|
|
1390
1947
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
1391
|
-
const canvas = new
|
|
1948
|
+
const canvas = new import_fabric7.StaticCanvas(element, {
|
|
1392
1949
|
width: options.width ?? source.getWidth(),
|
|
1393
1950
|
height: options.height ?? source.getHeight(),
|
|
1394
1951
|
backgroundColor: options.backgroundColor || void 0
|
|
@@ -1403,6 +1960,38 @@ async function exportIsolatedPNG(source, objects, options = {}) {
|
|
|
1403
1960
|
canvas.dispose();
|
|
1404
1961
|
}
|
|
1405
1962
|
}
|
|
1963
|
+
async function exportPrintArea(source, area, options = {}) {
|
|
1964
|
+
const { multiplier = 1, format = "png", quality = 1 } = options;
|
|
1965
|
+
const width = source.getWidth();
|
|
1966
|
+
const height = source.getHeight();
|
|
1967
|
+
const clip = computePrintAreaClip(
|
|
1968
|
+
area,
|
|
1969
|
+
multiplier,
|
|
1970
|
+
multiplier,
|
|
1971
|
+
width * multiplier,
|
|
1972
|
+
height * multiplier
|
|
1973
|
+
);
|
|
1974
|
+
if (clip.width <= 0 || clip.height <= 0) {
|
|
1975
|
+
throw new Error("Print area does not overlap the canvas");
|
|
1976
|
+
}
|
|
1977
|
+
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
1978
|
+
const canvas = new import_fabric7.StaticCanvas(element, { width, height });
|
|
1979
|
+
try {
|
|
1980
|
+
const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
|
|
1981
|
+
if (clones.length) canvas.add(...clones);
|
|
1982
|
+
canvas.requestRenderAll();
|
|
1983
|
+
const rendered = canvas.toCanvasElement(multiplier);
|
|
1984
|
+
const output = rendered.ownerDocument.createElement("canvas");
|
|
1985
|
+
output.width = Math.max(1, Math.round(clip.width));
|
|
1986
|
+
output.height = Math.max(1, Math.round(clip.height));
|
|
1987
|
+
const context = output.getContext("2d");
|
|
1988
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
1989
|
+
context.drawImage(rendered, -clip.left, -clip.top);
|
|
1990
|
+
return await canvasElementToBlob(output, format, quality);
|
|
1991
|
+
} finally {
|
|
1992
|
+
canvas.dispose();
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1406
1995
|
async function exportMockup(canvas, mockup, options = {}) {
|
|
1407
1996
|
const { multiplier = 1, format = "png", quality = 1 } = options;
|
|
1408
1997
|
const design = canvas.toCanvasElement(multiplier);
|
|
@@ -1826,7 +2415,7 @@ var ProjectManager = class {
|
|
|
1826
2415
|
};
|
|
1827
2416
|
|
|
1828
2417
|
// src/mask.ts
|
|
1829
|
-
var
|
|
2418
|
+
var import_fabric8 = require("fabric");
|
|
1830
2419
|
var MaskRefinementError = class extends Error {
|
|
1831
2420
|
constructor(code, message, cause) {
|
|
1832
2421
|
super(message);
|
|
@@ -1858,7 +2447,7 @@ var MaskController = class {
|
|
|
1858
2447
|
throw new Error("Mask dimensions must be positive integers");
|
|
1859
2448
|
}
|
|
1860
2449
|
const backing = this.makeCanvas(width, height);
|
|
1861
|
-
const image = new
|
|
2450
|
+
const image = new import_fabric8.FabricImage(backing, {
|
|
1862
2451
|
left: 0,
|
|
1863
2452
|
top: 0,
|
|
1864
2453
|
originX: "left",
|
|
@@ -2098,6 +2687,8 @@ var CanvasEditor = class {
|
|
|
2098
2687
|
snapping;
|
|
2099
2688
|
crop;
|
|
2100
2689
|
patterns;
|
|
2690
|
+
curves;
|
|
2691
|
+
maskPresets;
|
|
2101
2692
|
fonts;
|
|
2102
2693
|
licensing;
|
|
2103
2694
|
pages;
|
|
@@ -2121,7 +2712,7 @@ var CanvasEditor = class {
|
|
|
2121
2712
|
const widthPx = this.units.toPixels(config.width);
|
|
2122
2713
|
const heightPx = this.units.toPixels(config.height);
|
|
2123
2714
|
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
2124
|
-
this.canvas = new
|
|
2715
|
+
this.canvas = new import_fabric9.Canvas(canvasElement, {
|
|
2125
2716
|
width: widthPx,
|
|
2126
2717
|
height: heightPx,
|
|
2127
2718
|
backgroundColor: this.designBackground,
|
|
@@ -2149,6 +2740,8 @@ var CanvasEditor = class {
|
|
|
2149
2740
|
this.events,
|
|
2150
2741
|
config.patternSourceResolver
|
|
2151
2742
|
);
|
|
2743
|
+
this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
|
|
2744
|
+
this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
|
|
2152
2745
|
this.setupCanvasEvents();
|
|
2153
2746
|
this.history.saveImmediate();
|
|
2154
2747
|
this.pages = new ProjectManager(this);
|
|
@@ -2157,7 +2750,7 @@ var CanvasEditor = class {
|
|
|
2157
2750
|
// ─── Layer Operations ────────────────────────────────
|
|
2158
2751
|
async addImage(url, options) {
|
|
2159
2752
|
try {
|
|
2160
|
-
const img = await
|
|
2753
|
+
const img = await import_fabric9.FabricImage.fromURL(
|
|
2161
2754
|
url,
|
|
2162
2755
|
{},
|
|
2163
2756
|
{ originX: "left", originY: "top", ...options }
|
|
@@ -2182,7 +2775,7 @@ var CanvasEditor = class {
|
|
|
2182
2775
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
2183
2776
|
const previous = layer.fabricObject;
|
|
2184
2777
|
try {
|
|
2185
|
-
const replacement = await
|
|
2778
|
+
const replacement = await import_fabric9.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
2186
2779
|
replacement.set({
|
|
2187
2780
|
left: previous.left,
|
|
2188
2781
|
top: previous.top,
|
|
@@ -2211,7 +2804,7 @@ var CanvasEditor = class {
|
|
|
2211
2804
|
}
|
|
2212
2805
|
}
|
|
2213
2806
|
addText(text, options) {
|
|
2214
|
-
const textbox = new
|
|
2807
|
+
const textbox = new import_fabric9.Textbox(text, {
|
|
2215
2808
|
fontSize: 32,
|
|
2216
2809
|
fontFamily: "Arial",
|
|
2217
2810
|
fill: "#000000",
|
|
@@ -2250,10 +2843,10 @@ var CanvasEditor = class {
|
|
|
2250
2843
|
return value === void 0 ? token : escapeXml(value);
|
|
2251
2844
|
})
|
|
2252
2845
|
);
|
|
2253
|
-
const { objects, options } = await (0,
|
|
2846
|
+
const { objects, options } = await (0, import_fabric9.loadSVGFromString)(resolved);
|
|
2254
2847
|
const validObjects = objects.filter((object) => object !== null);
|
|
2255
2848
|
if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
2256
|
-
const group =
|
|
2849
|
+
const group = import_fabric9.util.groupSVGElements(validObjects, options);
|
|
2257
2850
|
group.set({
|
|
2258
2851
|
left: this.canvas.getWidth() / 2,
|
|
2259
2852
|
top: this.canvas.getHeight() / 2,
|
|
@@ -2326,10 +2919,10 @@ var CanvasEditor = class {
|
|
|
2326
2919
|
const next = { ...previous, ...adjustments };
|
|
2327
2920
|
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
2328
2921
|
image.filters = [
|
|
2329
|
-
new
|
|
2330
|
-
new
|
|
2331
|
-
new
|
|
2332
|
-
new
|
|
2922
|
+
new import_fabric9.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
|
|
2923
|
+
new import_fabric9.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
|
|
2924
|
+
new import_fabric9.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
|
|
2925
|
+
new import_fabric9.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
|
|
2333
2926
|
];
|
|
2334
2927
|
layer.meta.imageAdjustments = next;
|
|
2335
2928
|
image.applyFilters();
|
|
@@ -2346,7 +2939,7 @@ var CanvasEditor = class {
|
|
|
2346
2939
|
const childData = children.map((layer) => structuredClone(layer.toData()));
|
|
2347
2940
|
const objects = children.map((layer) => layer.fabricObject);
|
|
2348
2941
|
for (const layer of children) this.layers.remove(layer.id);
|
|
2349
|
-
const group = new
|
|
2942
|
+
const group = new import_fabric9.Group(objects);
|
|
2350
2943
|
const grouped = this.layers.add("group", group, name);
|
|
2351
2944
|
grouped.meta.groupChildren = childData;
|
|
2352
2945
|
this.layers.select(grouped.id);
|
|
@@ -2365,7 +2958,7 @@ var CanvasEditor = class {
|
|
|
2365
2958
|
const objects = group.removeAll();
|
|
2366
2959
|
this.layers.remove(id);
|
|
2367
2960
|
const restored = objects.map((object, index) => {
|
|
2368
|
-
|
|
2961
|
+
import_fabric9.util.addTransformToObject(object, transform);
|
|
2369
2962
|
object.setCoords();
|
|
2370
2963
|
const data = childData[index];
|
|
2371
2964
|
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
@@ -2421,7 +3014,7 @@ var CanvasEditor = class {
|
|
|
2421
3014
|
const layer = this.layers.get(id);
|
|
2422
3015
|
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
2423
3016
|
try {
|
|
2424
|
-
if (options.resolution === "source" && layer.fabricObject instanceof
|
|
3017
|
+
if (options.resolution === "source" && layer.fabricObject instanceof import_fabric9.FabricImage) {
|
|
2425
3018
|
const image = await layer.fabricObject.clone();
|
|
2426
3019
|
image.set({
|
|
2427
3020
|
left: 0,
|
|
@@ -2505,6 +3098,23 @@ var CanvasEditor = class {
|
|
|
2505
3098
|
toDataURL(format = "png", multiplier = 1) {
|
|
2506
3099
|
return this.withDesignBackground(() => exportDataURL(this.canvas, format, multiplier));
|
|
2507
3100
|
}
|
|
3101
|
+
/**
|
|
3102
|
+
* Export the print file: the design cropped to the mockup's print area, on
|
|
3103
|
+
* transparency. Without a print area this is the whole canvas, still
|
|
3104
|
+
* transparent — a print file never carries the design background.
|
|
3105
|
+
*/
|
|
3106
|
+
async toPrintFile(options = {}) {
|
|
3107
|
+
await this.fonts.ready();
|
|
3108
|
+
try {
|
|
3109
|
+
const area = this.mockup?.printArea;
|
|
3110
|
+
const blob = area ? await exportPrintArea(this.canvas, area, options) : await exportIsolatedPNG(this.canvas, this.canvas.getObjects(), options);
|
|
3111
|
+
this.licensing.track(`export:print:${options.format ?? "png"}`);
|
|
3112
|
+
return blob;
|
|
3113
|
+
} catch (error) {
|
|
3114
|
+
this.events.emit("error", { message: "Failed to export print file", error });
|
|
3115
|
+
throw error;
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
2508
3118
|
/** Export the current product-preview composite. Advanced warping is host-defined. */
|
|
2509
3119
|
async toMockupImage(options = {}) {
|
|
2510
3120
|
if (!this.mockup) throw new Error("No mockup is configured");
|
|
@@ -2676,7 +3286,7 @@ var CanvasEditor = class {
|
|
|
2676
3286
|
return;
|
|
2677
3287
|
}
|
|
2678
3288
|
try {
|
|
2679
|
-
const image = await
|
|
3289
|
+
const image = await import_fabric9.FabricImage.fromURL(
|
|
2680
3290
|
url,
|
|
2681
3291
|
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
2682
3292
|
{ originX: "left", originY: "top" }
|
|
@@ -2851,14 +3461,89 @@ var CanvasEditor = class {
|
|
|
2851
3461
|
clearPattern(layerId) {
|
|
2852
3462
|
return this.patterns.disable(layerId);
|
|
2853
3463
|
}
|
|
3464
|
+
// ─── Text curve ─────────────────────────────────────
|
|
3465
|
+
applyTextCurve(layerId, config) {
|
|
3466
|
+
return this.curves.apply(layerId, config);
|
|
3467
|
+
}
|
|
3468
|
+
clearTextCurve(layerId) {
|
|
3469
|
+
return this.curves.clear(layerId);
|
|
3470
|
+
}
|
|
3471
|
+
getTextCurve(layerId) {
|
|
3472
|
+
return this.curves.get(layerId);
|
|
3473
|
+
}
|
|
3474
|
+
// ─── Mask presets ───────────────────────────────────
|
|
3475
|
+
applyMaskPreset(layerId, id) {
|
|
3476
|
+
return this.maskPresets.apply(layerId, id);
|
|
3477
|
+
}
|
|
3478
|
+
clearMaskPreset(layerId) {
|
|
3479
|
+
return this.maskPresets.clear(layerId);
|
|
3480
|
+
}
|
|
3481
|
+
getMaskPreset(layerId) {
|
|
3482
|
+
return this.maskPresets.get(layerId);
|
|
3483
|
+
}
|
|
3484
|
+
// ─── Layer transform ────────────────────────────────
|
|
3485
|
+
/**
|
|
3486
|
+
* Constrain a layer to its current proportions. Persisted on the layer so a
|
|
3487
|
+
* reopened design still resizes the way it was set up to.
|
|
3488
|
+
*/
|
|
3489
|
+
setLayerAspectLock(layerId, locked) {
|
|
3490
|
+
const layer = this.layers.get(layerId);
|
|
3491
|
+
if (!layer) return false;
|
|
3492
|
+
applyAspectLock(layer.fabricObject, locked);
|
|
3493
|
+
if (locked) layer.meta.lockAspect = true;
|
|
3494
|
+
else delete layer.meta.lockAspect;
|
|
3495
|
+
this.canvas.requestRenderAll();
|
|
3496
|
+
this.events.emit("layer:modified", { layerId });
|
|
3497
|
+
this.history.save();
|
|
3498
|
+
return true;
|
|
3499
|
+
}
|
|
3500
|
+
getLayerAspectLock(layerId) {
|
|
3501
|
+
return this.layers.get(layerId)?.meta.lockAspect === true;
|
|
3502
|
+
}
|
|
3503
|
+
/** Re-apply every stored aspect lock — control visibility is not serialized. */
|
|
3504
|
+
restoreAspectLocks() {
|
|
3505
|
+
for (const layer of this.layers.getAll()) {
|
|
3506
|
+
if (layer.meta.lockAspect) applyAspectLock(layer.fabricObject, true);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
/** Drop scale, rotation, skew and flips; the layer stays where it is. */
|
|
3510
|
+
resetLayerTransform(layerId) {
|
|
3511
|
+
const layer = this.layers.get(layerId);
|
|
3512
|
+
if (!layer) return false;
|
|
3513
|
+
resetTransform(layer.fabricObject);
|
|
3514
|
+
this.canvas.requestRenderAll();
|
|
3515
|
+
this.events.emit("layer:modified", { layerId });
|
|
3516
|
+
this.history.save();
|
|
3517
|
+
return true;
|
|
3518
|
+
}
|
|
3519
|
+
// ─── Shadow ─────────────────────────────────────────
|
|
3520
|
+
setLayerShadow(layerId, config) {
|
|
3521
|
+
const layer = this.layers.get(layerId);
|
|
3522
|
+
if (!layer) return false;
|
|
3523
|
+
applyLayerShadow(layer.fabricObject, config);
|
|
3524
|
+
layer.fabricObject.dirty = true;
|
|
3525
|
+
this.canvas.requestRenderAll();
|
|
3526
|
+
this.events.emit("layer:modified", { layerId });
|
|
3527
|
+
this.history.save();
|
|
3528
|
+
return true;
|
|
3529
|
+
}
|
|
3530
|
+
getLayerShadow(layerId) {
|
|
3531
|
+
const layer = this.layers.get(layerId);
|
|
3532
|
+
return layer ? readLayerShadow(layer.fabricObject) : null;
|
|
3533
|
+
}
|
|
2854
3534
|
// ─── Mockup (preview-only) ──────────────────────────
|
|
2855
|
-
|
|
3535
|
+
/**
|
|
3536
|
+
* Show (or clear) the product preview. Pass `history: false` for preview-only
|
|
3537
|
+
* changes such as swapping a colourway — those are not design edits and
|
|
3538
|
+
* should not fill the undo stack.
|
|
3539
|
+
*/
|
|
3540
|
+
setMockup(mockup, options = {}) {
|
|
2856
3541
|
this.mockup = mockup;
|
|
2857
3542
|
this.canvas.backgroundColor = mockup ? "" : this.designBackground;
|
|
2858
3543
|
this.canvas.backgroundImage = mockup ? void 0 : this.designBackgroundImage ?? void 0;
|
|
2859
3544
|
this.canvas.requestRenderAll();
|
|
2860
3545
|
this.events.emit("mockup:changed", { mockup });
|
|
2861
|
-
this.history.save();
|
|
3546
|
+
if (options.history !== false) this.history.save();
|
|
2862
3547
|
}
|
|
2863
3548
|
clearMockup() {
|
|
2864
3549
|
this.setMockup(null);
|
|
@@ -2970,7 +3655,9 @@ var AnnotationOverlay = class {
|
|
|
2970
3655
|
CANVAS_SIZE_PRESETS,
|
|
2971
3656
|
CanvasEditor,
|
|
2972
3657
|
CropController,
|
|
3658
|
+
DEFAULT_LAYER_SHADOW,
|
|
2973
3659
|
DEFAULT_PATTERN_CONFIG,
|
|
3660
|
+
DEFAULT_TEXT_CURVE,
|
|
2974
3661
|
EventEmitter,
|
|
2975
3662
|
FontRegistry,
|
|
2976
3663
|
HistoryManager,
|
|
@@ -2978,16 +3665,26 @@ var AnnotationOverlay = class {
|
|
|
2978
3665
|
LayerManager,
|
|
2979
3666
|
LicenseManager,
|
|
2980
3667
|
MaskController,
|
|
3668
|
+
MaskPresetManager,
|
|
2981
3669
|
MaskRefinementError,
|
|
2982
3670
|
PatternManager,
|
|
2983
3671
|
ProjectManager,
|
|
3672
|
+
SHAPE_MASK_BOX,
|
|
3673
|
+
SHAPE_MASK_IDS,
|
|
2984
3674
|
SnapManager,
|
|
3675
|
+
TEXTURE_MASK_IDS,
|
|
3676
|
+
TEXTURE_MASK_SIZE,
|
|
3677
|
+
TextCurveManager,
|
|
2985
3678
|
UnitConverter,
|
|
3679
|
+
applyAspectLock,
|
|
3680
|
+
applyLayerShadow,
|
|
2986
3681
|
applyPatternLocks,
|
|
3682
|
+
buildCurvePathData,
|
|
2987
3683
|
buildPatternDataURL,
|
|
2988
3684
|
captureLocks,
|
|
2989
3685
|
clamp,
|
|
2990
3686
|
clearPatternImageCache,
|
|
3687
|
+
clearTextureMaskCache,
|
|
2991
3688
|
computeCoverPlacement,
|
|
2992
3689
|
computePrintAreaClip,
|
|
2993
3690
|
computeTilePositions,
|
|
@@ -2998,13 +3695,21 @@ var AnnotationOverlay = class {
|
|
|
2998
3695
|
exportDataURL,
|
|
2999
3696
|
exportMockup,
|
|
3000
3697
|
exportPNG,
|
|
3698
|
+
exportPrintArea,
|
|
3001
3699
|
exportSVG,
|
|
3002
3700
|
generateId,
|
|
3003
3701
|
isCssColor,
|
|
3702
|
+
isMaskPresetId,
|
|
3703
|
+
isShapeMaskId,
|
|
3704
|
+
isTextureMaskId,
|
|
3004
3705
|
loadPatternImage,
|
|
3706
|
+
readLayerShadow,
|
|
3707
|
+
renderTextureMask,
|
|
3708
|
+
resetTransform,
|
|
3005
3709
|
restoreLocks,
|
|
3006
3710
|
round2,
|
|
3007
3711
|
sanitizeSvg,
|
|
3008
|
-
serializeEditor
|
|
3712
|
+
serializeEditor,
|
|
3713
|
+
shapeMaskPathData
|
|
3009
3714
|
});
|
|
3010
3715
|
//# sourceMappingURL=index.js.map
|