@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/index.mjs CHANGED
@@ -6,11 +6,12 @@ import {
6
6
  exportIsolatedPNG,
7
7
  exportMockup,
8
8
  exportPNG,
9
+ exportPrintArea,
9
10
  exportSVG
10
- } from "./chunk-ORZZ6MGQ.mjs";
11
+ } from "./chunk-MCBRZQ4M.mjs";
11
12
 
12
13
  // src/editor.ts
13
- import { Canvas, FabricImage as FabricImage3, Group, Textbox, filters, loadSVGFromString, util as util3 } from "fabric";
14
+ import { Canvas, FabricImage as FabricImage4, Group, Textbox, filters, loadSVGFromString, util as util3 } from "fabric";
14
15
 
15
16
  // src/events.ts
16
17
  var EventEmitter = class {
@@ -1088,6 +1089,521 @@ function mod2(n) {
1088
1089
  return (n % 2 + 2) % 2;
1089
1090
  }
1090
1091
 
1092
+ // src/text-curve.ts
1093
+ import { Path } from "fabric";
1094
+ var DEFAULT_TEXT_CURVE = { arc: 0, wave: 0 };
1095
+ var MIN_ARC = 0.5;
1096
+ var FULL_CIRCLE_ARC = 99.5;
1097
+ var MIN_SWEEP = 0.12;
1098
+ var WAVE_PERIOD_EM = 4.1;
1099
+ var WAVE_AMPLITUDE_EM = 0.9;
1100
+ var WAVE_STEP = 6;
1101
+ var MEASURE_WIDTH = 1e5;
1102
+ var PATH_SLACK = 0.06;
1103
+ function isCurvable(object) {
1104
+ return !!object && typeof object.text === "string";
1105
+ }
1106
+ function measureText(text) {
1107
+ const authored = text.width;
1108
+ try {
1109
+ text.set({ width: MEASURE_WIDTH });
1110
+ text.initDimensions?.();
1111
+ return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
1112
+ } finally {
1113
+ if (authored !== void 0) text.set({ width: authored });
1114
+ text.initDimensions?.();
1115
+ }
1116
+ }
1117
+ function arcPathData(width, arc) {
1118
+ const magnitude = Math.min(100, Math.abs(arc));
1119
+ const direction = arc < 0 ? -1 : 1;
1120
+ const full = magnitude >= FULL_CIRCLE_ARC;
1121
+ const sweep = full ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
1122
+ const radius = width / sweep;
1123
+ const centerX = width / 2;
1124
+ const point = (angle) => [
1125
+ centerX + radius * Math.sin(angle),
1126
+ direction * radius * (1 - Math.cos(angle))
1127
+ ];
1128
+ const sweepFlag = direction > 0 ? 1 : 0;
1129
+ const format = ([x, y]) => `${round(x)} ${round(y)}`;
1130
+ if (full) {
1131
+ const start2 = point(-Math.PI);
1132
+ const top = point(0);
1133
+ return {
1134
+ data: [
1135
+ `M ${format(start2)}`,
1136
+ `A ${round(radius)} ${round(radius)} 0 0 ${sweepFlag} ${format(top)}`,
1137
+ `A ${round(radius)} ${round(radius)} 0 0 ${sweepFlag} ${format(start2)}`
1138
+ ].join(" "),
1139
+ length: width
1140
+ };
1141
+ }
1142
+ const start = point(-sweep / 2);
1143
+ const end = point(sweep / 2);
1144
+ const largeArc = sweep > Math.PI ? 1 : 0;
1145
+ return {
1146
+ data: `M ${format(start)} A ${round(radius)} ${round(radius)} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
1147
+ // radius = width / sweep, so the arc is exactly `width` long.
1148
+ length: width
1149
+ };
1150
+ }
1151
+ function wavePathData(width, fontSize, wave) {
1152
+ const amplitude = clamp(wave, 0, 100) / 100 * fontSize * WAVE_AMPLITUDE_EM;
1153
+ const period = Math.max(1, fontSize * WAVE_PERIOD_EM);
1154
+ const steps = Math.max(2, Math.ceil(width / WAVE_STEP));
1155
+ const commands = [];
1156
+ let length = 0;
1157
+ let previous = null;
1158
+ for (let index = 0; index <= steps; index++) {
1159
+ const x = width * index / steps;
1160
+ const y = amplitude * Math.sin(x / period * Math.PI * 2);
1161
+ if (previous) length += Math.hypot(x - previous[0], y - previous[1]);
1162
+ previous = [x, y];
1163
+ commands.push(`${index === 0 ? "M" : "L"} ${round(x)} ${round(y)}`);
1164
+ }
1165
+ return { data: commands.join(" "), length };
1166
+ }
1167
+ function round(value) {
1168
+ return Math.round(value * 100) / 100;
1169
+ }
1170
+ function buildCurvePathData(config, width, fontSize) {
1171
+ if (Math.abs(config.arc) >= MIN_ARC) return arcPathData(width, config.arc);
1172
+ if (config.wave > 0) return wavePathData(width, fontSize, config.wave);
1173
+ return null;
1174
+ }
1175
+ function normalize(config) {
1176
+ const arc = clamp(config.arc ?? 0, -100, 100);
1177
+ return { arc, wave: Math.abs(arc) >= MIN_ARC ? 0 : clamp(config.wave ?? 0, 0, 100) };
1178
+ }
1179
+ var TextCurveManager = class {
1180
+ constructor(canvas, layers, history, events) {
1181
+ this.canvas = canvas;
1182
+ this.layers = layers;
1183
+ this.history = history;
1184
+ this.events = events;
1185
+ }
1186
+ canvas;
1187
+ layers;
1188
+ history;
1189
+ events;
1190
+ /** Curve parameters for a layer, or null when it is not curved text. */
1191
+ get(layerId) {
1192
+ const layer = this.layers.get(layerId);
1193
+ if (!layer || !isCurvable(layer.fabricObject)) return null;
1194
+ return layer.meta.curve ?? { ...DEFAULT_TEXT_CURVE };
1195
+ }
1196
+ isCurved(layerId) {
1197
+ const curve = this.get(layerId);
1198
+ return !!curve && (Math.abs(curve.arc) >= MIN_ARC || curve.wave > 0);
1199
+ }
1200
+ /** Apply (or update) the curve on a text layer. Zeroed config clears it. */
1201
+ apply(layerId, config, save = true) {
1202
+ const layer = this.layers.get(layerId);
1203
+ if (!layer || !isCurvable(layer.fabricObject)) return false;
1204
+ const next = normalize(config);
1205
+ const text = layer.fabricObject;
1206
+ const run = measureText(text);
1207
+ const curve = buildCurvePathData(next, run * (1 + PATH_SLACK), text.fontSize);
1208
+ if (!curve) {
1209
+ this.detach(text, layer.meta.curveWidth);
1210
+ delete layer.meta.curve;
1211
+ delete layer.meta.curveWidth;
1212
+ } else {
1213
+ if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
1214
+ text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
1215
+ text.set({
1216
+ path: new Path(curve.data, { visible: false, objectCaching: false }),
1217
+ pathAlign: "center",
1218
+ pathSide: "left",
1219
+ pathStartOffset: Math.max(0, (curve.length - run) / 2)
1220
+ });
1221
+ layer.meta.curve = next;
1222
+ }
1223
+ text.initDimensions?.();
1224
+ text.setCoords();
1225
+ text.dirty = true;
1226
+ this.canvas.requestRenderAll();
1227
+ this.events.emit("layer:modified", { layerId });
1228
+ if (save) this.history.save();
1229
+ return true;
1230
+ }
1231
+ /** Remove the curve, restoring the authored text box width. */
1232
+ clear(layerId, save = true) {
1233
+ return this.apply(layerId, DEFAULT_TEXT_CURVE, save);
1234
+ }
1235
+ /**
1236
+ * Rebuild the path from the stored parameters. Text content, font family and
1237
+ * font size all change the run's width, and the path is sized to that width —
1238
+ * without this the curve keeps the geometry of the text it was created from.
1239
+ */
1240
+ refresh(layerId, save = false) {
1241
+ const curve = this.layers.get(layerId)?.meta.curve;
1242
+ if (!curve) return false;
1243
+ return this.apply(layerId, curve, save);
1244
+ }
1245
+ /** Rebuild every curved layer — used after a state restore. */
1246
+ refreshAll() {
1247
+ for (const layer of this.layers.getAll()) {
1248
+ if (layer.meta.curve) this.refresh(layer.id);
1249
+ }
1250
+ }
1251
+ detach(text, authoredWidth) {
1252
+ text.set({ path: null, pathStartOffset: 0 });
1253
+ if (authoredWidth !== void 0 && authoredWidth > 0) text.set({ width: authoredWidth });
1254
+ }
1255
+ };
1256
+
1257
+ // src/mask-presets/manager.ts
1258
+ import { FabricImage as FabricImage2, Path as Path2 } from "fabric";
1259
+
1260
+ // src/mask-presets/shapes.ts
1261
+ var SHAPE_MASK_IDS = [
1262
+ "circle",
1263
+ "square",
1264
+ "triangle",
1265
+ "star",
1266
+ "heart",
1267
+ "octagram",
1268
+ "arch",
1269
+ "zigzag"
1270
+ ];
1271
+ function isShapeMaskId(value) {
1272
+ return typeof value === "string" && SHAPE_MASK_IDS.includes(value);
1273
+ }
1274
+ function starPoints(points, outer, inner, start = -Math.PI / 2) {
1275
+ const step = Math.PI / points;
1276
+ const coordinates = [];
1277
+ for (let index = 0; index < points * 2; index++) {
1278
+ const radius = index % 2 === 0 ? outer : inner;
1279
+ const angle = start + index * step;
1280
+ const x = 50 + Math.cos(angle) * radius;
1281
+ const y = 50 + Math.sin(angle) * radius;
1282
+ coordinates.push(`${index === 0 ? "M" : "L"} ${round3(x)} ${round3(y)}`);
1283
+ }
1284
+ return `${coordinates.join(" ")} Z`;
1285
+ }
1286
+ function round3(value) {
1287
+ return Math.round(value * 100) / 100;
1288
+ }
1289
+ var SHAPE_PATHS = {
1290
+ circle: "M 96 50 A 46 46 0 1 1 4 50 A 46 46 0 1 1 96 50 Z",
1291
+ square: "M 4 4 H 96 V 96 H 4 Z",
1292
+ triangle: "M 50 4 L 96 96 L 4 96 Z",
1293
+ star: starPoints(5, 46, 22),
1294
+ heart: "M 50 96 C -8 55 12 4 50 28 C 88 4 108 55 50 96 Z",
1295
+ octagram: starPoints(8, 46, 27),
1296
+ arch: "M 4 96 V 50 A 46 46 0 0 1 96 50 V 96 Z",
1297
+ zigzag: starPoints(16, 46, 39)
1298
+ };
1299
+ function shapeMaskPathData(id) {
1300
+ return SHAPE_PATHS[id];
1301
+ }
1302
+ var SHAPE_MASK_BOX = 100;
1303
+
1304
+ // src/mask-presets/textures.ts
1305
+ var TEXTURE_MASK_IDS = [
1306
+ "vignette",
1307
+ "halftone",
1308
+ "spray",
1309
+ "grunge",
1310
+ "torn",
1311
+ "band"
1312
+ ];
1313
+ function isTextureMaskId(value) {
1314
+ return typeof value === "string" && TEXTURE_MASK_IDS.includes(value);
1315
+ }
1316
+ var TEXTURE_MASK_SIZE = 320;
1317
+ var cache = /* @__PURE__ */ new Map();
1318
+ function seeded(seed) {
1319
+ let state = seed;
1320
+ return () => {
1321
+ state = state * 16807 % 2147483647;
1322
+ return (state - 1) / 2147483646;
1323
+ };
1324
+ }
1325
+ function traceRoughEdge(context, random, jitter, inset) {
1326
+ const size = TEXTURE_MASK_SIZE;
1327
+ const corners = [
1328
+ [inset, inset],
1329
+ [size - inset, inset],
1330
+ [size - inset, size - inset],
1331
+ [inset, size - inset]
1332
+ ];
1333
+ context.beginPath();
1334
+ for (let corner = 0; corner < corners.length; corner++) {
1335
+ const [fromX, fromY] = corners[corner];
1336
+ const [toX, toY] = corners[(corner + 1) % corners.length];
1337
+ for (let step = 0; step <= 26; step++) {
1338
+ const ratio = step / 26;
1339
+ const x = fromX + (toX - fromX) * ratio + (random() - 0.5) * jitter;
1340
+ const y = fromY + (toY - fromY) * ratio + (random() - 0.5) * jitter;
1341
+ if (corner === 0 && step === 0) context.moveTo(x, y);
1342
+ else context.lineTo(x, y);
1343
+ }
1344
+ }
1345
+ context.closePath();
1346
+ context.fill();
1347
+ }
1348
+ function paint(id, context) {
1349
+ const size = TEXTURE_MASK_SIZE;
1350
+ const center = size / 2;
1351
+ context.fillStyle = "#ffffff";
1352
+ switch (id) {
1353
+ case "vignette": {
1354
+ const gradient = context.createRadialGradient(
1355
+ center,
1356
+ center,
1357
+ size * 0.18,
1358
+ center,
1359
+ center,
1360
+ size * 0.52
1361
+ );
1362
+ gradient.addColorStop(0, "rgba(255,255,255,1)");
1363
+ gradient.addColorStop(1, "rgba(255,255,255,0)");
1364
+ context.fillStyle = gradient;
1365
+ context.fillRect(0, 0, size, size);
1366
+ return;
1367
+ }
1368
+ case "band": {
1369
+ const gradient = context.createLinearGradient(0, 0, 0, size);
1370
+ gradient.addColorStop(0, "rgba(255,255,255,0)");
1371
+ gradient.addColorStop(0.25, "rgba(255,255,255,1)");
1372
+ gradient.addColorStop(0.75, "rgba(255,255,255,1)");
1373
+ gradient.addColorStop(1, "rgba(255,255,255,0)");
1374
+ context.fillStyle = gradient;
1375
+ context.fillRect(0, 0, size, size);
1376
+ return;
1377
+ }
1378
+ case "halftone": {
1379
+ const pitch = 16;
1380
+ for (let y = pitch / 2; y < size; y += pitch) {
1381
+ for (let x = pitch / 2; x < size; x += pitch) {
1382
+ const distance = Math.hypot(x - center, y - center) / (size * 0.52);
1383
+ const radius = Math.max(0, pitch / 2 * (1 - distance) * 1.15);
1384
+ if (radius < 0.4) continue;
1385
+ context.beginPath();
1386
+ context.arc(x, y, radius, 0, Math.PI * 2);
1387
+ context.fill();
1388
+ }
1389
+ }
1390
+ return;
1391
+ }
1392
+ case "spray": {
1393
+ const random = seeded(42);
1394
+ const core = context.createRadialGradient(center, center, 10, center, center, size * 0.42);
1395
+ core.addColorStop(0, "rgba(255,255,255,1)");
1396
+ core.addColorStop(1, "rgba(255,255,255,0.85)");
1397
+ context.fillStyle = core;
1398
+ context.beginPath();
1399
+ context.arc(center, center, size * 0.42, 0, Math.PI * 2);
1400
+ context.fill();
1401
+ context.fillStyle = "rgba(255,255,255,0.9)";
1402
+ for (let dot = 0; dot < 900; dot++) {
1403
+ const angle = random() * Math.PI * 2;
1404
+ const distance = size * (0.3 + random() * 0.24);
1405
+ const x = center + Math.cos(angle) * distance;
1406
+ const y = center + Math.sin(angle) * distance;
1407
+ context.globalAlpha = 1 - (distance / size - 0.3) / 0.24;
1408
+ context.beginPath();
1409
+ context.arc(x, y, random() * 2.4, 0, Math.PI * 2);
1410
+ context.fill();
1411
+ }
1412
+ context.globalAlpha = 1;
1413
+ return;
1414
+ }
1415
+ case "grunge": {
1416
+ const random = seeded(7);
1417
+ traceRoughEdge(context, random, 14, size * 0.06);
1418
+ context.globalCompositeOperation = "destination-out";
1419
+ for (let speckle = 0; speckle < 240; speckle++) {
1420
+ const x = random() * size;
1421
+ const y = random() * size;
1422
+ const nearEdge = Math.min(x, y, size - x, size - y) < size * 0.12;
1423
+ if (!nearEdge && random() >= 0.12) continue;
1424
+ context.beginPath();
1425
+ context.arc(x, y, random() * 6 + 1, 0, Math.PI * 2);
1426
+ context.fill();
1427
+ }
1428
+ context.globalCompositeOperation = "source-over";
1429
+ return;
1430
+ }
1431
+ case "torn": {
1432
+ traceRoughEdge(context, seeded(99), 10, size * 0.08);
1433
+ return;
1434
+ }
1435
+ }
1436
+ }
1437
+ function renderTextureMask(id) {
1438
+ const cached = cache.get(id);
1439
+ if (cached) return cached;
1440
+ if (typeof document === "undefined") {
1441
+ throw new Error("Texture masks require a DOM canvas");
1442
+ }
1443
+ const canvas = document.createElement("canvas");
1444
+ canvas.width = TEXTURE_MASK_SIZE;
1445
+ canvas.height = TEXTURE_MASK_SIZE;
1446
+ const context = canvas.getContext("2d");
1447
+ if (!context) throw new Error("Texture masks require a 2D canvas context");
1448
+ paint(id, context);
1449
+ cache.set(id, canvas);
1450
+ return canvas;
1451
+ }
1452
+ function clearTextureMaskCache() {
1453
+ cache.clear();
1454
+ }
1455
+
1456
+ // src/mask-presets/manager.ts
1457
+ function isMaskPresetId(value) {
1458
+ return isShapeMaskId(value) || isTextureMaskId(value);
1459
+ }
1460
+ var MaskPresetManager = class {
1461
+ constructor(canvas, layers, history, events) {
1462
+ this.canvas = canvas;
1463
+ this.layers = layers;
1464
+ this.history = history;
1465
+ this.events = events;
1466
+ }
1467
+ canvas;
1468
+ layers;
1469
+ history;
1470
+ events;
1471
+ get(layerId) {
1472
+ return this.layers.get(layerId)?.meta.maskPreset ?? null;
1473
+ }
1474
+ /** Clip the layer to `id`. Passing null (or an unknown id) clears the clip. */
1475
+ apply(layerId, id, save = true) {
1476
+ const layer = this.layers.get(layerId);
1477
+ if (!layer) return false;
1478
+ const object = layer.fabricObject;
1479
+ if (id === null) {
1480
+ if (!layer.meta.maskPreset) return false;
1481
+ object.clipPath = void 0;
1482
+ delete layer.meta.maskPreset;
1483
+ } else {
1484
+ if (!isMaskPresetId(id)) return false;
1485
+ object.clipPath = this.buildClip(id, object);
1486
+ layer.meta.maskPreset = id;
1487
+ }
1488
+ object.dirty = true;
1489
+ object.setCoords();
1490
+ this.canvas.requestRenderAll();
1491
+ this.events.emit("layer:modified", { layerId });
1492
+ if (save) this.history.save();
1493
+ return true;
1494
+ }
1495
+ clear(layerId, save = true) {
1496
+ return this.apply(layerId, null, save);
1497
+ }
1498
+ /**
1499
+ * Re-fit the clip to the layer's current size. The clip is built for the
1500
+ * object's dimensions at the time it was applied; editing text or replacing an
1501
+ * image changes them, and a stale clip would crop the wrong region.
1502
+ */
1503
+ refresh(layerId, save = false) {
1504
+ const id = this.get(layerId);
1505
+ if (!id) return false;
1506
+ return this.apply(layerId, id, save);
1507
+ }
1508
+ /** Re-fit every masked layer — used after a state restore. */
1509
+ refreshAll() {
1510
+ for (const layer of this.layers.getAll()) {
1511
+ if (layer.meta.maskPreset) this.refresh(layer.id);
1512
+ }
1513
+ }
1514
+ buildClip(id, object) {
1515
+ const width = Math.max(1, object.width ?? 1);
1516
+ const height = Math.max(1, object.height ?? 1);
1517
+ const shared = {
1518
+ originX: "center",
1519
+ originY: "center",
1520
+ left: 0,
1521
+ top: 0,
1522
+ objectCaching: false
1523
+ };
1524
+ if (isShapeMaskId(id)) {
1525
+ return new Path2(shapeMaskPathData(id), {
1526
+ ...shared,
1527
+ scaleX: width / SHAPE_MASK_BOX,
1528
+ scaleY: height / SHAPE_MASK_BOX
1529
+ });
1530
+ }
1531
+ return new FabricImage2(renderTextureMask(id), {
1532
+ ...shared,
1533
+ scaleX: width / TEXTURE_MASK_SIZE,
1534
+ scaleY: height / TEXTURE_MASK_SIZE
1535
+ });
1536
+ }
1537
+ };
1538
+
1539
+ // src/shadow.ts
1540
+ import { Shadow } from "fabric";
1541
+
1542
+ // src/utils/color.ts
1543
+ var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
1544
+ function isCssColor(value, allowEmpty = false) {
1545
+ if (typeof value !== "string") return false;
1546
+ const trimmed = value.trim();
1547
+ if (!trimmed) return allowEmpty;
1548
+ return CSS_COLOR.test(trimmed);
1549
+ }
1550
+
1551
+ // src/shadow.ts
1552
+ var DEFAULT_LAYER_SHADOW = {
1553
+ enabled: false,
1554
+ color: "#000000",
1555
+ blur: 12,
1556
+ offsetX: 6,
1557
+ offsetY: 6
1558
+ };
1559
+ function readLayerShadow(object) {
1560
+ const shadow = object.shadow;
1561
+ if (!shadow || typeof shadow === "string") return { ...DEFAULT_LAYER_SHADOW };
1562
+ return {
1563
+ enabled: true,
1564
+ color: typeof shadow.color === "string" ? shadow.color : DEFAULT_LAYER_SHADOW.color,
1565
+ blur: shadow.blur ?? DEFAULT_LAYER_SHADOW.blur,
1566
+ offsetX: shadow.offsetX ?? DEFAULT_LAYER_SHADOW.offsetX,
1567
+ offsetY: shadow.offsetY ?? DEFAULT_LAYER_SHADOW.offsetY
1568
+ };
1569
+ }
1570
+ function applyLayerShadow(object, config) {
1571
+ const next = { ...readLayerShadow(object), ...config };
1572
+ if (!next.enabled) {
1573
+ object.set({ shadow: null });
1574
+ return;
1575
+ }
1576
+ const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
1577
+ object.set({
1578
+ shadow: new Shadow({
1579
+ color,
1580
+ blur: Math.max(0, next.blur),
1581
+ offsetX: next.offsetX,
1582
+ offsetY: next.offsetY
1583
+ })
1584
+ });
1585
+ }
1586
+
1587
+ // src/transform.ts
1588
+ var SIDE_CONTROLS = ["ml", "mr", "mt", "mb"];
1589
+ function applyAspectLock(object, locked) {
1590
+ for (const control of SIDE_CONTROLS) {
1591
+ object.setControlVisible(control, !locked);
1592
+ }
1593
+ }
1594
+ function resetTransform(object) {
1595
+ object.set({
1596
+ scaleX: 1,
1597
+ scaleY: 1,
1598
+ angle: 0,
1599
+ skewX: 0,
1600
+ skewY: 0,
1601
+ flipX: false,
1602
+ flipY: false
1603
+ });
1604
+ object.setCoords();
1605
+ }
1606
+
1091
1607
  // src/utils/units.ts
1092
1608
  var MM_PER_INCH = 25.4;
1093
1609
  var UnitConverter = class {
@@ -1137,17 +1653,6 @@ var UnitConverter = class {
1137
1653
 
1138
1654
  // src/serialization.ts
1139
1655
  import { util as util2 } from "fabric";
1140
-
1141
- // src/utils/color.ts
1142
- var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
1143
- function isCssColor(value, allowEmpty = false) {
1144
- if (typeof value !== "string") return false;
1145
- const trimmed = value.trim();
1146
- if (!trimmed) return allowEmpty;
1147
- return CSS_COLOR.test(trimmed);
1148
- }
1149
-
1150
- // src/serialization.ts
1151
1656
  var VERSION = "2.0.0";
1152
1657
  function serializeEditor(editor) {
1153
1658
  return {
@@ -1226,6 +1731,7 @@ function restoreLayer(editor, serialized, fabricObject) {
1226
1731
  const layer = editor.layers.add(serialized.type, fabricObject, serialized.name, serialized.id);
1227
1732
  if (serialized.meta) {
1228
1733
  layer.meta = serialized.meta;
1734
+ if (layer.meta.lockAspect) applyAspectLock(fabricObject, true);
1229
1735
  }
1230
1736
  if (!serialized.visible) {
1231
1737
  editor.layers.setVisibility(layer.id, false);
@@ -1553,7 +2059,7 @@ var ProjectManager = class {
1553
2059
  };
1554
2060
 
1555
2061
  // src/mask.ts
1556
- import { FabricImage as FabricImage2 } from "fabric";
2062
+ import { FabricImage as FabricImage3 } from "fabric";
1557
2063
  var MaskRefinementError = class extends Error {
1558
2064
  constructor(code, message, cause) {
1559
2065
  super(message);
@@ -1585,7 +2091,7 @@ var MaskController = class {
1585
2091
  throw new Error("Mask dimensions must be positive integers");
1586
2092
  }
1587
2093
  const backing = this.makeCanvas(width, height);
1588
- const image = new FabricImage2(backing, {
2094
+ const image = new FabricImage3(backing, {
1589
2095
  left: 0,
1590
2096
  top: 0,
1591
2097
  originX: "left",
@@ -1825,6 +2331,8 @@ var CanvasEditor = class {
1825
2331
  snapping;
1826
2332
  crop;
1827
2333
  patterns;
2334
+ curves;
2335
+ maskPresets;
1828
2336
  fonts;
1829
2337
  licensing;
1830
2338
  pages;
@@ -1876,6 +2384,8 @@ var CanvasEditor = class {
1876
2384
  this.events,
1877
2385
  config.patternSourceResolver
1878
2386
  );
2387
+ this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
2388
+ this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
1879
2389
  this.setupCanvasEvents();
1880
2390
  this.history.saveImmediate();
1881
2391
  this.pages = new ProjectManager(this);
@@ -1884,7 +2394,7 @@ var CanvasEditor = class {
1884
2394
  // ─── Layer Operations ────────────────────────────────
1885
2395
  async addImage(url, options) {
1886
2396
  try {
1887
- const img = await FabricImage3.fromURL(
2397
+ const img = await FabricImage4.fromURL(
1888
2398
  url,
1889
2399
  {},
1890
2400
  { originX: "left", originY: "top", ...options }
@@ -1909,7 +2419,7 @@ var CanvasEditor = class {
1909
2419
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
1910
2420
  const previous = layer.fabricObject;
1911
2421
  try {
1912
- const replacement = await FabricImage3.fromURL(url, {}, { originX: "left", originY: "top" });
2422
+ const replacement = await FabricImage4.fromURL(url, {}, { originX: "left", originY: "top" });
1913
2423
  replacement.set({
1914
2424
  left: previous.left,
1915
2425
  top: previous.top,
@@ -2148,7 +2658,7 @@ var CanvasEditor = class {
2148
2658
  const layer = this.layers.get(id);
2149
2659
  if (!layer) throw new Error(`Layer not found: ${id}`);
2150
2660
  try {
2151
- if (options.resolution === "source" && layer.fabricObject instanceof FabricImage3) {
2661
+ if (options.resolution === "source" && layer.fabricObject instanceof FabricImage4) {
2152
2662
  const image = await layer.fabricObject.clone();
2153
2663
  image.set({
2154
2664
  left: 0,
@@ -2232,6 +2742,23 @@ var CanvasEditor = class {
2232
2742
  toDataURL(format = "png", multiplier = 1) {
2233
2743
  return this.withDesignBackground(() => exportDataURL(this.canvas, format, multiplier));
2234
2744
  }
2745
+ /**
2746
+ * Export the print file: the design cropped to the mockup's print area, on
2747
+ * transparency. Without a print area this is the whole canvas, still
2748
+ * transparent — a print file never carries the design background.
2749
+ */
2750
+ async toPrintFile(options = {}) {
2751
+ await this.fonts.ready();
2752
+ try {
2753
+ const area = this.mockup?.printArea;
2754
+ const blob = area ? await exportPrintArea(this.canvas, area, options) : await exportIsolatedPNG(this.canvas, this.canvas.getObjects(), options);
2755
+ this.licensing.track(`export:print:${options.format ?? "png"}`);
2756
+ return blob;
2757
+ } catch (error) {
2758
+ this.events.emit("error", { message: "Failed to export print file", error });
2759
+ throw error;
2760
+ }
2761
+ }
2235
2762
  /** Export the current product-preview composite. Advanced warping is host-defined. */
2236
2763
  async toMockupImage(options = {}) {
2237
2764
  if (!this.mockup) throw new Error("No mockup is configured");
@@ -2403,7 +2930,7 @@ var CanvasEditor = class {
2403
2930
  return;
2404
2931
  }
2405
2932
  try {
2406
- const image = await FabricImage3.fromURL(
2933
+ const image = await FabricImage4.fromURL(
2407
2934
  url,
2408
2935
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
2409
2936
  { originX: "left", originY: "top" }
@@ -2578,14 +3105,89 @@ var CanvasEditor = class {
2578
3105
  clearPattern(layerId) {
2579
3106
  return this.patterns.disable(layerId);
2580
3107
  }
3108
+ // ─── Text curve ─────────────────────────────────────
3109
+ applyTextCurve(layerId, config) {
3110
+ return this.curves.apply(layerId, config);
3111
+ }
3112
+ clearTextCurve(layerId) {
3113
+ return this.curves.clear(layerId);
3114
+ }
3115
+ getTextCurve(layerId) {
3116
+ return this.curves.get(layerId);
3117
+ }
3118
+ // ─── Mask presets ───────────────────────────────────
3119
+ applyMaskPreset(layerId, id) {
3120
+ return this.maskPresets.apply(layerId, id);
3121
+ }
3122
+ clearMaskPreset(layerId) {
3123
+ return this.maskPresets.clear(layerId);
3124
+ }
3125
+ getMaskPreset(layerId) {
3126
+ return this.maskPresets.get(layerId);
3127
+ }
3128
+ // ─── Layer transform ────────────────────────────────
3129
+ /**
3130
+ * Constrain a layer to its current proportions. Persisted on the layer so a
3131
+ * reopened design still resizes the way it was set up to.
3132
+ */
3133
+ setLayerAspectLock(layerId, locked) {
3134
+ const layer = this.layers.get(layerId);
3135
+ if (!layer) return false;
3136
+ applyAspectLock(layer.fabricObject, locked);
3137
+ if (locked) layer.meta.lockAspect = true;
3138
+ else delete layer.meta.lockAspect;
3139
+ this.canvas.requestRenderAll();
3140
+ this.events.emit("layer:modified", { layerId });
3141
+ this.history.save();
3142
+ return true;
3143
+ }
3144
+ getLayerAspectLock(layerId) {
3145
+ return this.layers.get(layerId)?.meta.lockAspect === true;
3146
+ }
3147
+ /** Re-apply every stored aspect lock — control visibility is not serialized. */
3148
+ restoreAspectLocks() {
3149
+ for (const layer of this.layers.getAll()) {
3150
+ if (layer.meta.lockAspect) applyAspectLock(layer.fabricObject, true);
3151
+ }
3152
+ }
3153
+ /** Drop scale, rotation, skew and flips; the layer stays where it is. */
3154
+ resetLayerTransform(layerId) {
3155
+ const layer = this.layers.get(layerId);
3156
+ if (!layer) return false;
3157
+ resetTransform(layer.fabricObject);
3158
+ this.canvas.requestRenderAll();
3159
+ this.events.emit("layer:modified", { layerId });
3160
+ this.history.save();
3161
+ return true;
3162
+ }
3163
+ // ─── Shadow ─────────────────────────────────────────
3164
+ setLayerShadow(layerId, config) {
3165
+ const layer = this.layers.get(layerId);
3166
+ if (!layer) return false;
3167
+ applyLayerShadow(layer.fabricObject, config);
3168
+ layer.fabricObject.dirty = true;
3169
+ this.canvas.requestRenderAll();
3170
+ this.events.emit("layer:modified", { layerId });
3171
+ this.history.save();
3172
+ return true;
3173
+ }
3174
+ getLayerShadow(layerId) {
3175
+ const layer = this.layers.get(layerId);
3176
+ return layer ? readLayerShadow(layer.fabricObject) : null;
3177
+ }
2581
3178
  // ─── Mockup (preview-only) ──────────────────────────
2582
- setMockup(mockup) {
3179
+ /**
3180
+ * Show (or clear) the product preview. Pass `history: false` for preview-only
3181
+ * changes such as swapping a colourway — those are not design edits and
3182
+ * should not fill the undo stack.
3183
+ */
3184
+ setMockup(mockup, options = {}) {
2583
3185
  this.mockup = mockup;
2584
3186
  this.canvas.backgroundColor = mockup ? "" : this.designBackground;
2585
3187
  this.canvas.backgroundImage = mockup ? void 0 : this.designBackgroundImage ?? void 0;
2586
3188
  this.canvas.requestRenderAll();
2587
3189
  this.events.emit("mockup:changed", { mockup });
2588
- this.history.save();
3190
+ if (options.history !== false) this.history.save();
2589
3191
  }
2590
3192
  clearMockup() {
2591
3193
  this.setMockup(null);
@@ -2696,7 +3298,9 @@ export {
2696
3298
  CANVAS_SIZE_PRESETS,
2697
3299
  CanvasEditor,
2698
3300
  CropController,
3301
+ DEFAULT_LAYER_SHADOW,
2699
3302
  DEFAULT_PATTERN_CONFIG,
3303
+ DEFAULT_TEXT_CURVE,
2700
3304
  EventEmitter,
2701
3305
  FontRegistry,
2702
3306
  HistoryManager,
@@ -2704,16 +3308,26 @@ export {
2704
3308
  LayerManager,
2705
3309
  LicenseManager,
2706
3310
  MaskController,
3311
+ MaskPresetManager,
2707
3312
  MaskRefinementError,
2708
3313
  PatternManager,
2709
3314
  ProjectManager,
3315
+ SHAPE_MASK_BOX,
3316
+ SHAPE_MASK_IDS,
2710
3317
  SnapManager,
3318
+ TEXTURE_MASK_IDS,
3319
+ TEXTURE_MASK_SIZE,
3320
+ TextCurveManager,
2711
3321
  UnitConverter,
3322
+ applyAspectLock,
3323
+ applyLayerShadow,
2712
3324
  applyPatternLocks,
3325
+ buildCurvePathData,
2713
3326
  buildPatternDataURL,
2714
3327
  captureLocks,
2715
3328
  clamp,
2716
3329
  clearPatternImageCache,
3330
+ clearTextureMaskCache,
2717
3331
  computeCoverPlacement,
2718
3332
  computePrintAreaClip,
2719
3333
  computeTilePositions,
@@ -2724,13 +3338,21 @@ export {
2724
3338
  exportDataURL,
2725
3339
  exportMockup,
2726
3340
  exportPNG,
3341
+ exportPrintArea,
2727
3342
  exportSVG,
2728
3343
  generateId,
2729
3344
  isCssColor,
3345
+ isMaskPresetId,
3346
+ isShapeMaskId,
3347
+ isTextureMaskId,
2730
3348
  loadPatternImage,
3349
+ readLayerShadow,
3350
+ renderTextureMask,
3351
+ resetTransform,
2731
3352
  restoreLocks,
2732
3353
  round2,
2733
3354
  sanitizeSvg,
2734
- serializeEditor
3355
+ serializeEditor,
3356
+ shapeMaskPathData
2735
3357
  };
2736
3358
  //# sourceMappingURL=index.mjs.map