@overtone-art/canvas-editor-core 0.2.8 → 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-ORZZ6MGQ.mjs → chunk-MCBRZQ4M.mjs} +34 -1
- package/dist/chunk-MCBRZQ4M.mjs.map +1 -0
- package/dist/index.d.mts +172 -4
- package/dist/index.d.ts +172 -4
- package/dist/index.global.js +67 -67
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +710 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +644 -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 {
|
|
@@ -1142,6 +1162,521 @@ function mod2(n) {
|
|
|
1142
1162
|
return (n % 2 + 2) % 2;
|
|
1143
1163
|
}
|
|
1144
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
|
+
|
|
1145
1680
|
// src/utils/units.ts
|
|
1146
1681
|
var MM_PER_INCH = 25.4;
|
|
1147
1682
|
var UnitConverter = class {
|
|
@@ -1190,18 +1725,7 @@ var UnitConverter = class {
|
|
|
1190
1725
|
};
|
|
1191
1726
|
|
|
1192
1727
|
// 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
|
|
1728
|
+
var import_fabric6 = require("fabric");
|
|
1205
1729
|
var VERSION = "2.0.0";
|
|
1206
1730
|
function serializeEditor(editor) {
|
|
1207
1731
|
return {
|
|
@@ -1237,7 +1761,7 @@ async function deserializeEditor(editor, state) {
|
|
|
1237
1761
|
}
|
|
1238
1762
|
const staged = await Promise.all(
|
|
1239
1763
|
state.layers.map(async (serialized) => {
|
|
1240
|
-
const fabricObject = (await
|
|
1764
|
+
const fabricObject = (await import_fabric6.util.enlivenObjects([serialized.fabricObject]))[0];
|
|
1241
1765
|
if (!fabricObject) {
|
|
1242
1766
|
const source = serialized.fabricObject.src;
|
|
1243
1767
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -1248,7 +1772,7 @@ async function deserializeEditor(editor, state) {
|
|
|
1248
1772
|
return { serialized, fabricObject };
|
|
1249
1773
|
})
|
|
1250
1774
|
);
|
|
1251
|
-
const stagedBackground = state.backgroundImage ? (await
|
|
1775
|
+
const stagedBackground = state.backgroundImage ? (await import_fabric6.util.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
1252
1776
|
if (state.backgroundImage && !stagedBackground) {
|
|
1253
1777
|
const source = state.backgroundImage.src;
|
|
1254
1778
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -1280,6 +1804,7 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
1280
1804
|
const layer = editor.layers.add(serialized.type, fabricObject, serialized.name, serialized.id);
|
|
1281
1805
|
if (serialized.meta) {
|
|
1282
1806
|
layer.meta = serialized.meta;
|
|
1807
|
+
if (layer.meta.lockAspect) applyAspectLock(fabricObject, true);
|
|
1283
1808
|
}
|
|
1284
1809
|
if (!serialized.visible) {
|
|
1285
1810
|
editor.layers.setVisibility(layer.id, false);
|
|
@@ -1294,7 +1819,7 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
1294
1819
|
}
|
|
1295
1820
|
|
|
1296
1821
|
// src/export.ts
|
|
1297
|
-
var
|
|
1822
|
+
var import_fabric7 = require("fabric");
|
|
1298
1823
|
|
|
1299
1824
|
// src/displacement.ts
|
|
1300
1825
|
var CHANNEL_INDEX = {
|
|
@@ -1388,7 +1913,7 @@ async function exportPNG(canvas, options = {}) {
|
|
|
1388
1913
|
}
|
|
1389
1914
|
async function exportIsolatedPNG(source, objects, options = {}) {
|
|
1390
1915
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
1391
|
-
const canvas = new
|
|
1916
|
+
const canvas = new import_fabric7.StaticCanvas(element, {
|
|
1392
1917
|
width: options.width ?? source.getWidth(),
|
|
1393
1918
|
height: options.height ?? source.getHeight(),
|
|
1394
1919
|
backgroundColor: options.backgroundColor || void 0
|
|
@@ -1403,6 +1928,38 @@ async function exportIsolatedPNG(source, objects, options = {}) {
|
|
|
1403
1928
|
canvas.dispose();
|
|
1404
1929
|
}
|
|
1405
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
|
+
}
|
|
1406
1963
|
async function exportMockup(canvas, mockup, options = {}) {
|
|
1407
1964
|
const { multiplier = 1, format = "png", quality = 1 } = options;
|
|
1408
1965
|
const design = canvas.toCanvasElement(multiplier);
|
|
@@ -1826,7 +2383,7 @@ var ProjectManager = class {
|
|
|
1826
2383
|
};
|
|
1827
2384
|
|
|
1828
2385
|
// src/mask.ts
|
|
1829
|
-
var
|
|
2386
|
+
var import_fabric8 = require("fabric");
|
|
1830
2387
|
var MaskRefinementError = class extends Error {
|
|
1831
2388
|
constructor(code, message, cause) {
|
|
1832
2389
|
super(message);
|
|
@@ -1858,7 +2415,7 @@ var MaskController = class {
|
|
|
1858
2415
|
throw new Error("Mask dimensions must be positive integers");
|
|
1859
2416
|
}
|
|
1860
2417
|
const backing = this.makeCanvas(width, height);
|
|
1861
|
-
const image = new
|
|
2418
|
+
const image = new import_fabric8.FabricImage(backing, {
|
|
1862
2419
|
left: 0,
|
|
1863
2420
|
top: 0,
|
|
1864
2421
|
originX: "left",
|
|
@@ -2098,6 +2655,8 @@ var CanvasEditor = class {
|
|
|
2098
2655
|
snapping;
|
|
2099
2656
|
crop;
|
|
2100
2657
|
patterns;
|
|
2658
|
+
curves;
|
|
2659
|
+
maskPresets;
|
|
2101
2660
|
fonts;
|
|
2102
2661
|
licensing;
|
|
2103
2662
|
pages;
|
|
@@ -2121,7 +2680,7 @@ var CanvasEditor = class {
|
|
|
2121
2680
|
const widthPx = this.units.toPixels(config.width);
|
|
2122
2681
|
const heightPx = this.units.toPixels(config.height);
|
|
2123
2682
|
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
2124
|
-
this.canvas = new
|
|
2683
|
+
this.canvas = new import_fabric9.Canvas(canvasElement, {
|
|
2125
2684
|
width: widthPx,
|
|
2126
2685
|
height: heightPx,
|
|
2127
2686
|
backgroundColor: this.designBackground,
|
|
@@ -2149,6 +2708,8 @@ var CanvasEditor = class {
|
|
|
2149
2708
|
this.events,
|
|
2150
2709
|
config.patternSourceResolver
|
|
2151
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);
|
|
2152
2713
|
this.setupCanvasEvents();
|
|
2153
2714
|
this.history.saveImmediate();
|
|
2154
2715
|
this.pages = new ProjectManager(this);
|
|
@@ -2157,7 +2718,7 @@ var CanvasEditor = class {
|
|
|
2157
2718
|
// ─── Layer Operations ────────────────────────────────
|
|
2158
2719
|
async addImage(url, options) {
|
|
2159
2720
|
try {
|
|
2160
|
-
const img = await
|
|
2721
|
+
const img = await import_fabric9.FabricImage.fromURL(
|
|
2161
2722
|
url,
|
|
2162
2723
|
{},
|
|
2163
2724
|
{ originX: "left", originY: "top", ...options }
|
|
@@ -2182,7 +2743,7 @@ var CanvasEditor = class {
|
|
|
2182
2743
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
2183
2744
|
const previous = layer.fabricObject;
|
|
2184
2745
|
try {
|
|
2185
|
-
const replacement = await
|
|
2746
|
+
const replacement = await import_fabric9.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
2186
2747
|
replacement.set({
|
|
2187
2748
|
left: previous.left,
|
|
2188
2749
|
top: previous.top,
|
|
@@ -2211,7 +2772,7 @@ var CanvasEditor = class {
|
|
|
2211
2772
|
}
|
|
2212
2773
|
}
|
|
2213
2774
|
addText(text, options) {
|
|
2214
|
-
const textbox = new
|
|
2775
|
+
const textbox = new import_fabric9.Textbox(text, {
|
|
2215
2776
|
fontSize: 32,
|
|
2216
2777
|
fontFamily: "Arial",
|
|
2217
2778
|
fill: "#000000",
|
|
@@ -2250,10 +2811,10 @@ var CanvasEditor = class {
|
|
|
2250
2811
|
return value === void 0 ? token : escapeXml(value);
|
|
2251
2812
|
})
|
|
2252
2813
|
);
|
|
2253
|
-
const { objects, options } = await (0,
|
|
2814
|
+
const { objects, options } = await (0, import_fabric9.loadSVGFromString)(resolved);
|
|
2254
2815
|
const validObjects = objects.filter((object) => object !== null);
|
|
2255
2816
|
if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
2256
|
-
const group =
|
|
2817
|
+
const group = import_fabric9.util.groupSVGElements(validObjects, options);
|
|
2257
2818
|
group.set({
|
|
2258
2819
|
left: this.canvas.getWidth() / 2,
|
|
2259
2820
|
top: this.canvas.getHeight() / 2,
|
|
@@ -2326,10 +2887,10 @@ var CanvasEditor = class {
|
|
|
2326
2887
|
const next = { ...previous, ...adjustments };
|
|
2327
2888
|
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
2328
2889
|
image.filters = [
|
|
2329
|
-
new
|
|
2330
|
-
new
|
|
2331
|
-
new
|
|
2332
|
-
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) })
|
|
2333
2894
|
];
|
|
2334
2895
|
layer.meta.imageAdjustments = next;
|
|
2335
2896
|
image.applyFilters();
|
|
@@ -2346,7 +2907,7 @@ var CanvasEditor = class {
|
|
|
2346
2907
|
const childData = children.map((layer) => structuredClone(layer.toData()));
|
|
2347
2908
|
const objects = children.map((layer) => layer.fabricObject);
|
|
2348
2909
|
for (const layer of children) this.layers.remove(layer.id);
|
|
2349
|
-
const group = new
|
|
2910
|
+
const group = new import_fabric9.Group(objects);
|
|
2350
2911
|
const grouped = this.layers.add("group", group, name);
|
|
2351
2912
|
grouped.meta.groupChildren = childData;
|
|
2352
2913
|
this.layers.select(grouped.id);
|
|
@@ -2365,7 +2926,7 @@ var CanvasEditor = class {
|
|
|
2365
2926
|
const objects = group.removeAll();
|
|
2366
2927
|
this.layers.remove(id);
|
|
2367
2928
|
const restored = objects.map((object, index) => {
|
|
2368
|
-
|
|
2929
|
+
import_fabric9.util.addTransformToObject(object, transform);
|
|
2369
2930
|
object.setCoords();
|
|
2370
2931
|
const data = childData[index];
|
|
2371
2932
|
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
@@ -2421,7 +2982,7 @@ var CanvasEditor = class {
|
|
|
2421
2982
|
const layer = this.layers.get(id);
|
|
2422
2983
|
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
2423
2984
|
try {
|
|
2424
|
-
if (options.resolution === "source" && layer.fabricObject instanceof
|
|
2985
|
+
if (options.resolution === "source" && layer.fabricObject instanceof import_fabric9.FabricImage) {
|
|
2425
2986
|
const image = await layer.fabricObject.clone();
|
|
2426
2987
|
image.set({
|
|
2427
2988
|
left: 0,
|
|
@@ -2505,6 +3066,23 @@ var CanvasEditor = class {
|
|
|
2505
3066
|
toDataURL(format = "png", multiplier = 1) {
|
|
2506
3067
|
return this.withDesignBackground(() => exportDataURL(this.canvas, format, multiplier));
|
|
2507
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
|
+
}
|
|
2508
3086
|
/** Export the current product-preview composite. Advanced warping is host-defined. */
|
|
2509
3087
|
async toMockupImage(options = {}) {
|
|
2510
3088
|
if (!this.mockup) throw new Error("No mockup is configured");
|
|
@@ -2676,7 +3254,7 @@ var CanvasEditor = class {
|
|
|
2676
3254
|
return;
|
|
2677
3255
|
}
|
|
2678
3256
|
try {
|
|
2679
|
-
const image = await
|
|
3257
|
+
const image = await import_fabric9.FabricImage.fromURL(
|
|
2680
3258
|
url,
|
|
2681
3259
|
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
2682
3260
|
{ originX: "left", originY: "top" }
|
|
@@ -2851,14 +3429,89 @@ var CanvasEditor = class {
|
|
|
2851
3429
|
clearPattern(layerId) {
|
|
2852
3430
|
return this.patterns.disable(layerId);
|
|
2853
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
|
+
}
|
|
2854
3502
|
// ─── Mockup (preview-only) ──────────────────────────
|
|
2855
|
-
|
|
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 = {}) {
|
|
2856
3509
|
this.mockup = mockup;
|
|
2857
3510
|
this.canvas.backgroundColor = mockup ? "" : this.designBackground;
|
|
2858
3511
|
this.canvas.backgroundImage = mockup ? void 0 : this.designBackgroundImage ?? void 0;
|
|
2859
3512
|
this.canvas.requestRenderAll();
|
|
2860
3513
|
this.events.emit("mockup:changed", { mockup });
|
|
2861
|
-
this.history.save();
|
|
3514
|
+
if (options.history !== false) this.history.save();
|
|
2862
3515
|
}
|
|
2863
3516
|
clearMockup() {
|
|
2864
3517
|
this.setMockup(null);
|
|
@@ -2970,7 +3623,9 @@ var AnnotationOverlay = class {
|
|
|
2970
3623
|
CANVAS_SIZE_PRESETS,
|
|
2971
3624
|
CanvasEditor,
|
|
2972
3625
|
CropController,
|
|
3626
|
+
DEFAULT_LAYER_SHADOW,
|
|
2973
3627
|
DEFAULT_PATTERN_CONFIG,
|
|
3628
|
+
DEFAULT_TEXT_CURVE,
|
|
2974
3629
|
EventEmitter,
|
|
2975
3630
|
FontRegistry,
|
|
2976
3631
|
HistoryManager,
|
|
@@ -2978,16 +3633,26 @@ var AnnotationOverlay = class {
|
|
|
2978
3633
|
LayerManager,
|
|
2979
3634
|
LicenseManager,
|
|
2980
3635
|
MaskController,
|
|
3636
|
+
MaskPresetManager,
|
|
2981
3637
|
MaskRefinementError,
|
|
2982
3638
|
PatternManager,
|
|
2983
3639
|
ProjectManager,
|
|
3640
|
+
SHAPE_MASK_BOX,
|
|
3641
|
+
SHAPE_MASK_IDS,
|
|
2984
3642
|
SnapManager,
|
|
3643
|
+
TEXTURE_MASK_IDS,
|
|
3644
|
+
TEXTURE_MASK_SIZE,
|
|
3645
|
+
TextCurveManager,
|
|
2985
3646
|
UnitConverter,
|
|
3647
|
+
applyAspectLock,
|
|
3648
|
+
applyLayerShadow,
|
|
2986
3649
|
applyPatternLocks,
|
|
3650
|
+
buildCurvePathData,
|
|
2987
3651
|
buildPatternDataURL,
|
|
2988
3652
|
captureLocks,
|
|
2989
3653
|
clamp,
|
|
2990
3654
|
clearPatternImageCache,
|
|
3655
|
+
clearTextureMaskCache,
|
|
2991
3656
|
computeCoverPlacement,
|
|
2992
3657
|
computePrintAreaClip,
|
|
2993
3658
|
computeTilePositions,
|
|
@@ -2998,13 +3663,21 @@ var AnnotationOverlay = class {
|
|
|
2998
3663
|
exportDataURL,
|
|
2999
3664
|
exportMockup,
|
|
3000
3665
|
exportPNG,
|
|
3666
|
+
exportPrintArea,
|
|
3001
3667
|
exportSVG,
|
|
3002
3668
|
generateId,
|
|
3003
3669
|
isCssColor,
|
|
3670
|
+
isMaskPresetId,
|
|
3671
|
+
isShapeMaskId,
|
|
3672
|
+
isTextureMaskId,
|
|
3004
3673
|
loadPatternImage,
|
|
3674
|
+
readLayerShadow,
|
|
3675
|
+
renderTextureMask,
|
|
3676
|
+
resetTransform,
|
|
3005
3677
|
restoreLocks,
|
|
3006
3678
|
round2,
|
|
3007
3679
|
sanitizeSvg,
|
|
3008
|
-
serializeEditor
|
|
3680
|
+
serializeEditor,
|
|
3681
|
+
shapeMaskPathData
|
|
3009
3682
|
});
|
|
3010
3683
|
//# sourceMappingURL=index.js.map
|