@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.mjs
CHANGED
|
@@ -6,11 +6,12 @@ import {
|
|
|
6
6
|
exportIsolatedPNG,
|
|
7
7
|
exportMockup,
|
|
8
8
|
exportPNG,
|
|
9
|
+
exportPrintArea,
|
|
9
10
|
exportSVG
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-MCBRZQ4M.mjs";
|
|
11
12
|
|
|
12
13
|
// src/editor.ts
|
|
13
|
-
import { Canvas, FabricImage as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|