@overtone-art/canvas-editor-core 0.8.6 → 0.9.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-TP527WVQ.mjs → chunk-QZUDGRRS.mjs} +253 -1
- package/dist/chunk-QZUDGRRS.mjs.map +1 -0
- package/dist/index.d.mts +169 -7
- package/dist/index.d.ts +169 -7
- package/dist/index.global.js +81 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1226 -157
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +946 -125
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +2 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.js +260 -1
- package/dist/node.js.map +1 -1
- package/dist/node.mjs +9 -2
- package/dist/node.mjs.map +1 -1
- package/dist/{types-F2GH3dcU.d.mts → specs-B_jOmY6u.d.mts} +111 -13
- package/dist/{types-F2GH3dcU.d.ts → specs-B_jOmY6u.d.ts} +111 -13
- package/package.json +4 -2
- package/dist/chunk-TP527WVQ.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ADJUST_KEYS,
|
|
3
|
+
ADJUST_RANGES,
|
|
4
|
+
IMAGE_FILTER_PRESETS,
|
|
5
|
+
IMAGE_PROXY_MAX_EDGE,
|
|
6
|
+
NEUTRAL_ADJUST,
|
|
2
7
|
TEXT_FIT_SLACK,
|
|
3
8
|
TextWrapManager,
|
|
9
|
+
alphaClipMatrix,
|
|
4
10
|
applyMatrix,
|
|
5
11
|
applyRelativeMatrix,
|
|
6
12
|
applyTextWrapToObject,
|
|
7
13
|
asObject,
|
|
8
14
|
asText,
|
|
15
|
+
clamp,
|
|
16
|
+
clampAdjust,
|
|
17
|
+
clampAlphaClip,
|
|
9
18
|
computeCoverPlacement,
|
|
10
19
|
computePrintAreaClip,
|
|
11
20
|
displaceRgba,
|
|
@@ -17,19 +26,34 @@ import {
|
|
|
17
26
|
exportSVG,
|
|
18
27
|
fitTextWidth,
|
|
19
28
|
fitToBox,
|
|
29
|
+
imageEffectsFilterSpecs,
|
|
30
|
+
imageFilterPreset,
|
|
20
31
|
matrixOf,
|
|
32
|
+
normalizeImageEffects,
|
|
21
33
|
preWrapWordSplit,
|
|
22
34
|
readTextWrap,
|
|
23
35
|
relativeMatrix,
|
|
36
|
+
round2,
|
|
24
37
|
textInk,
|
|
25
38
|
toCanvasSpace,
|
|
26
39
|
toHostSpace,
|
|
27
40
|
toMatrix,
|
|
28
41
|
unwrapGroup
|
|
29
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-QZUDGRRS.mjs";
|
|
30
43
|
|
|
31
44
|
// src/editor.ts
|
|
32
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
ActiveSelection as ActiveSelection3,
|
|
47
|
+
Canvas,
|
|
48
|
+
FabricImage as FabricImage3,
|
|
49
|
+
Group as Group7,
|
|
50
|
+
Rect as Rect4,
|
|
51
|
+
Textbox as Textbox2,
|
|
52
|
+
classRegistry as classRegistry2,
|
|
53
|
+
util as util7
|
|
54
|
+
} from "fabric";
|
|
55
|
+
import * as fabricModule from "fabric";
|
|
56
|
+
import { registerEffects } from "@overtone-art/canvas-editor-effects";
|
|
33
57
|
|
|
34
58
|
// src/events.ts
|
|
35
59
|
var EventEmitter = class {
|
|
@@ -67,7 +91,7 @@ var EventEmitter = class {
|
|
|
67
91
|
};
|
|
68
92
|
|
|
69
93
|
// src/layer.ts
|
|
70
|
-
import { ActiveSelection } from "fabric";
|
|
94
|
+
import { ActiveSelection, util } from "fabric";
|
|
71
95
|
|
|
72
96
|
// src/utils/id.ts
|
|
73
97
|
import { nanoid } from "nanoid";
|
|
@@ -181,9 +205,31 @@ var Layer = class {
|
|
|
181
205
|
...this.toData(),
|
|
182
206
|
// A group's own object already carries its children's geometry, so the
|
|
183
207
|
// child records in `toData` stay identity-only — nothing is duplicated.
|
|
184
|
-
fabricObject: this.
|
|
208
|
+
fabricObject: this.serializeObject()
|
|
185
209
|
};
|
|
186
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* A member of an ActiveSelection holds selection-relative coordinates and
|
|
213
|
+
* `toObject()` writes them verbatim. A snapshot taken mid-selection — which
|
|
214
|
+
* `HistoryManager.undo()` takes — then held positions no restore could use,
|
|
215
|
+
* and the redo scattered the layers. Realise the selection's transform for
|
|
216
|
+
* the write and put the object's own transform back.
|
|
217
|
+
*/
|
|
218
|
+
serializeObject() {
|
|
219
|
+
const object = this.fabricObject;
|
|
220
|
+
const selection = object.group;
|
|
221
|
+
if (!(selection instanceof ActiveSelection)) {
|
|
222
|
+
return object.toObject();
|
|
223
|
+
}
|
|
224
|
+
const saved = util.saveObjectTransform(object);
|
|
225
|
+
util.addTransformToObject(object, selection.calcOwnMatrix());
|
|
226
|
+
try {
|
|
227
|
+
return object.toObject();
|
|
228
|
+
} finally {
|
|
229
|
+
object.set(saved);
|
|
230
|
+
object.setCoords();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
187
233
|
};
|
|
188
234
|
var LayerManager = class {
|
|
189
235
|
constructor(canvas, events) {
|
|
@@ -1084,16 +1130,6 @@ function bestSnap(edges, candidates, threshold) {
|
|
|
1084
1130
|
|
|
1085
1131
|
// src/crop.ts
|
|
1086
1132
|
import { Rect } from "fabric";
|
|
1087
|
-
|
|
1088
|
-
// src/utils/clamp.ts
|
|
1089
|
-
function clamp(v, min, max) {
|
|
1090
|
-
return Math.min(max, Math.max(min, v));
|
|
1091
|
-
}
|
|
1092
|
-
function round2(v) {
|
|
1093
|
-
return Math.round(v * 100) / 100;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// src/crop.ts
|
|
1097
1133
|
var CropController = class {
|
|
1098
1134
|
constructor(canvas, history, events) {
|
|
1099
1135
|
this.canvas = canvas;
|
|
@@ -1115,7 +1151,7 @@ var CropController = class {
|
|
|
1115
1151
|
activeLayerId() {
|
|
1116
1152
|
return this.session?.layerId ?? null;
|
|
1117
1153
|
}
|
|
1118
|
-
start(layer) {
|
|
1154
|
+
start(layer, options = {}) {
|
|
1119
1155
|
if (layer.type !== "image") return;
|
|
1120
1156
|
if (this.session) this.cancel();
|
|
1121
1157
|
const image = layer.fabricObject;
|
|
@@ -1123,6 +1159,12 @@ var CropController = class {
|
|
|
1123
1159
|
if (prevAngle) image.rotate(0);
|
|
1124
1160
|
image.setCoords();
|
|
1125
1161
|
const b = image.getBoundingRect();
|
|
1162
|
+
const prevTransform = {
|
|
1163
|
+
scaleX: image.scaleX,
|
|
1164
|
+
scaleY: image.scaleY,
|
|
1165
|
+
left: image.left,
|
|
1166
|
+
top: image.top
|
|
1167
|
+
};
|
|
1126
1168
|
const rect = new Rect({
|
|
1127
1169
|
left: b.left,
|
|
1128
1170
|
top: b.top,
|
|
@@ -1147,8 +1189,95 @@ var CropController = class {
|
|
|
1147
1189
|
this.canvas.add(rect);
|
|
1148
1190
|
this.canvas.setActiveObject(rect);
|
|
1149
1191
|
this.canvas.requestRenderAll();
|
|
1150
|
-
this.session = {
|
|
1192
|
+
this.session = {
|
|
1193
|
+
layerId: layer.id,
|
|
1194
|
+
image,
|
|
1195
|
+
rect,
|
|
1196
|
+
prevSelectable,
|
|
1197
|
+
prevEvented,
|
|
1198
|
+
prevAngle,
|
|
1199
|
+
prevTransform,
|
|
1200
|
+
startRect: { left: b.left, top: b.top, width: b.width, height: b.height },
|
|
1201
|
+
aspect: null
|
|
1202
|
+
};
|
|
1151
1203
|
this.events.emit("crop:changed", { active: true, layerId: layer.id });
|
|
1204
|
+
if (options.aspect) this.setAspect(options.aspect);
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* The frame's own geometry, without its stroke.
|
|
1208
|
+
*
|
|
1209
|
+
* The rect draws a 1px dashed outline, and `getBoundingRect` /
|
|
1210
|
+
* `getScaledWidth` both count it — so reading the frame through them made the
|
|
1211
|
+
* aspect box, the zoom centre and the applied crop each about a pixel larger
|
|
1212
|
+
* than the box the user drew. The rect is axis-aligned (rotation is locked),
|
|
1213
|
+
* so its own left/top and scaled dimensions are exact.
|
|
1214
|
+
*/
|
|
1215
|
+
frameBox(rect) {
|
|
1216
|
+
return {
|
|
1217
|
+
left: rect.left,
|
|
1218
|
+
top: rect.top,
|
|
1219
|
+
width: rect.width * rect.scaleX,
|
|
1220
|
+
height: rect.height * rect.scaleY
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Lock the frame to a ratio: it becomes the largest box of that ratio inside
|
|
1225
|
+
* the current frame, centred, and only the corner handles stay — fabric's
|
|
1226
|
+
* default `uniformScaling` then keeps the ratio through a drag. `null` frees it.
|
|
1227
|
+
*/
|
|
1228
|
+
setAspect(ratio) {
|
|
1229
|
+
const s = this.session;
|
|
1230
|
+
if (!s) return;
|
|
1231
|
+
s.aspect = ratio && ratio > 0 ? ratio : null;
|
|
1232
|
+
const { rect } = s;
|
|
1233
|
+
const sides = s.aspect === null;
|
|
1234
|
+
rect.setControlsVisibility({ ml: sides, mr: sides, mt: sides, mb: sides, mtr: false });
|
|
1235
|
+
if (s.aspect !== null) {
|
|
1236
|
+
rect.setCoords();
|
|
1237
|
+
const current = this.frameBox(rect);
|
|
1238
|
+
const width = Math.min(current.width, current.height * s.aspect);
|
|
1239
|
+
const height = width / s.aspect;
|
|
1240
|
+
rect.set({
|
|
1241
|
+
left: current.left + (current.width - width) / 2,
|
|
1242
|
+
top: current.top + (current.height - height) / 2,
|
|
1243
|
+
width,
|
|
1244
|
+
height,
|
|
1245
|
+
scaleX: 1,
|
|
1246
|
+
scaleY: 1
|
|
1247
|
+
});
|
|
1248
|
+
rect.setCoords();
|
|
1249
|
+
}
|
|
1250
|
+
this.canvas.requestRenderAll();
|
|
1251
|
+
}
|
|
1252
|
+
/** Zoom the image under a fixed frame, about the frame's centre. */
|
|
1253
|
+
setScale(factor) {
|
|
1254
|
+
const s = this.session;
|
|
1255
|
+
if (!s || !Number.isFinite(factor) || factor <= 0) return;
|
|
1256
|
+
const { image, rect } = s;
|
|
1257
|
+
rect.setCoords();
|
|
1258
|
+
const frame = this.frameBox(rect);
|
|
1259
|
+
const centre = { x: frame.left + frame.width / 2, y: frame.top + frame.height / 2 };
|
|
1260
|
+
const scaleX = Math.max(0.01, image.scaleX * factor);
|
|
1261
|
+
const scaleY = Math.max(0.01, image.scaleY * factor);
|
|
1262
|
+
const applied = scaleX / image.scaleX;
|
|
1263
|
+
image.set({
|
|
1264
|
+
scaleX,
|
|
1265
|
+
scaleY,
|
|
1266
|
+
left: centre.x - (centre.x - image.left) * applied,
|
|
1267
|
+
top: centre.y - (centre.y - image.top) * applied
|
|
1268
|
+
});
|
|
1269
|
+
image.setCoords();
|
|
1270
|
+
this.canvas.requestRenderAll();
|
|
1271
|
+
}
|
|
1272
|
+
/** Back to the frame and image transform the session started with; aspect freed. */
|
|
1273
|
+
reset() {
|
|
1274
|
+
const s = this.session;
|
|
1275
|
+
if (!s) return;
|
|
1276
|
+
s.image.set(s.prevTransform);
|
|
1277
|
+
s.image.setCoords();
|
|
1278
|
+
s.rect.set({ ...s.startRect, scaleX: 1, scaleY: 1 });
|
|
1279
|
+
s.rect.setCoords();
|
|
1280
|
+
this.setAspect(null);
|
|
1152
1281
|
}
|
|
1153
1282
|
apply() {
|
|
1154
1283
|
const s = this.session;
|
|
@@ -1163,10 +1292,11 @@ var CropController = class {
|
|
|
1163
1292
|
const el = image.getElement();
|
|
1164
1293
|
const naturalW = el.naturalWidth || image.width || 0;
|
|
1165
1294
|
const naturalH = el.naturalHeight || image.height || 0;
|
|
1166
|
-
|
|
1167
|
-
let
|
|
1168
|
-
let
|
|
1169
|
-
let
|
|
1295
|
+
const frame = this.frameBox(rect);
|
|
1296
|
+
let cropX = (image.cropX ?? 0) + (frame.left - imgLeft) / scaleX;
|
|
1297
|
+
let cropY = (image.cropY ?? 0) + (frame.top - imgTop) / scaleY;
|
|
1298
|
+
let cropW = frame.width / scaleX;
|
|
1299
|
+
let cropH = frame.height / scaleY;
|
|
1170
1300
|
cropX = clamp(cropX, 0, Math.max(0, naturalW - 1));
|
|
1171
1301
|
cropY = clamp(cropY, 0, Math.max(0, naturalH - 1));
|
|
1172
1302
|
cropW = clamp(cropW, 1, naturalW - cropX);
|
|
@@ -1191,6 +1321,8 @@ var CropController = class {
|
|
|
1191
1321
|
cancel() {
|
|
1192
1322
|
const s = this.session;
|
|
1193
1323
|
if (!s) return;
|
|
1324
|
+
s.image.set(s.prevTransform);
|
|
1325
|
+
s.image.setCoords();
|
|
1194
1326
|
if (s.prevAngle) {
|
|
1195
1327
|
s.image.rotate(s.prevAngle);
|
|
1196
1328
|
s.image.setCoords();
|
|
@@ -1215,11 +1347,67 @@ var CropController = class {
|
|
|
1215
1347
|
};
|
|
1216
1348
|
var STROKE2 = "#22c55e";
|
|
1217
1349
|
|
|
1350
|
+
// src/align.ts
|
|
1351
|
+
import { ActiveSelection as ActiveSelection2, Point, util as util2 } from "fabric";
|
|
1352
|
+
function unionBox(boxes) {
|
|
1353
|
+
const left = Math.min(...boxes.map((b) => b.left));
|
|
1354
|
+
const top = Math.min(...boxes.map((b) => b.top));
|
|
1355
|
+
const right = Math.max(...boxes.map((b) => b.left + b.width));
|
|
1356
|
+
const bottom = Math.max(...boxes.map((b) => b.top + b.height));
|
|
1357
|
+
return { left, top, width: right - left, height: bottom - top };
|
|
1358
|
+
}
|
|
1359
|
+
function boxOf(object) {
|
|
1360
|
+
object.setCoords();
|
|
1361
|
+
return object.getBoundingRect();
|
|
1362
|
+
}
|
|
1363
|
+
function moveBy(object, dx, dy) {
|
|
1364
|
+
const parent = object.group;
|
|
1365
|
+
const vector = parent ? util2.sendVectorToPlane(new Point(dx, dy), void 0, parent.calcTransformMatrix()) : new Point(dx, dy);
|
|
1366
|
+
object.set({ left: (object.left ?? 0) + vector.x, top: (object.top ?? 0) + vector.y });
|
|
1367
|
+
object.setCoords();
|
|
1368
|
+
}
|
|
1369
|
+
function alignObjects(objects, target, frame) {
|
|
1370
|
+
for (const object of objects) {
|
|
1371
|
+
const b = boxOf(object);
|
|
1372
|
+
if (target === "left") moveBy(object, frame.left - b.left, 0);
|
|
1373
|
+
if (target === "center-h") moveBy(object, frame.left + (frame.width - b.width) / 2 - b.left, 0);
|
|
1374
|
+
if (target === "right") moveBy(object, frame.left + frame.width - b.width - b.left, 0);
|
|
1375
|
+
if (target === "top") moveBy(object, 0, frame.top - b.top);
|
|
1376
|
+
if (target === "middle") moveBy(object, 0, frame.top + (frame.height - b.height) / 2 - b.top);
|
|
1377
|
+
if (target === "bottom") moveBy(object, 0, frame.top + frame.height - b.height - b.top);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function distributeObjects(objects, axis) {
|
|
1381
|
+
if (objects.length < 3) return false;
|
|
1382
|
+
const measured = objects.map((object) => ({ object, box: boxOf(object) }));
|
|
1383
|
+
const pos = (b) => axis === "h" ? b.left : b.top;
|
|
1384
|
+
const size = (b) => axis === "h" ? b.width : b.height;
|
|
1385
|
+
const ordered = [...measured].sort((a, b) => pos(a.box) - pos(b.box));
|
|
1386
|
+
const span = unionBox(measured.map((m) => m.box));
|
|
1387
|
+
const total = ordered.reduce((sum, m) => sum + size(m.box), 0);
|
|
1388
|
+
const gap = (size(span) - total) / (ordered.length - 1);
|
|
1389
|
+
let cursor = pos(span);
|
|
1390
|
+
for (const { object, box } of ordered) {
|
|
1391
|
+
const delta = cursor - pos(box);
|
|
1392
|
+
moveBy(object, axis === "h" ? delta : 0, axis === "h" ? 0 : delta);
|
|
1393
|
+
cursor += size(box) + gap;
|
|
1394
|
+
}
|
|
1395
|
+
return true;
|
|
1396
|
+
}
|
|
1397
|
+
function relayoutSelection(active) {
|
|
1398
|
+
if (!(active instanceof ActiveSelection2)) return;
|
|
1399
|
+
active.triggerLayout();
|
|
1400
|
+
active.setCoords();
|
|
1401
|
+
}
|
|
1402
|
+
function canvasBox(width, height) {
|
|
1403
|
+
return { left: 0, top: 0, width, height };
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1218
1406
|
// src/pattern/pattern-manager.ts
|
|
1219
|
-
import { util } from "fabric";
|
|
1407
|
+
import { util as util3 } from "fabric";
|
|
1220
1408
|
|
|
1221
1409
|
// src/pattern/tiled-pattern-object.ts
|
|
1222
|
-
import { FabricObject, Point } from "fabric";
|
|
1410
|
+
import { FabricObject, Point as Point2 } from "fabric";
|
|
1223
1411
|
|
|
1224
1412
|
// src/pattern/tile-geometry.ts
|
|
1225
1413
|
var MAX_TILES_PER_AXIS = 200;
|
|
@@ -1376,7 +1564,7 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
|
1376
1564
|
const centre = this.getCenterPoint();
|
|
1377
1565
|
const shift = this.shiftVector(tileW, tileH, this.liveAngle());
|
|
1378
1566
|
const ink = this.inkOffset();
|
|
1379
|
-
return new
|
|
1567
|
+
return new Point2(centre.x - shift.x - ink.x, centre.y - shift.y - ink.y);
|
|
1380
1568
|
}
|
|
1381
1569
|
_render(ctx) {
|
|
1382
1570
|
const { width, height } = this.area;
|
|
@@ -1463,6 +1651,10 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
|
1463
1651
|
angle: this.angle,
|
|
1464
1652
|
scaleX: this.scaleX,
|
|
1465
1653
|
scaleY: this.scaleY,
|
|
1654
|
+
skewX: this.skewX,
|
|
1655
|
+
skewY: this.skewY,
|
|
1656
|
+
flipX: this.flipX,
|
|
1657
|
+
flipY: this.flipY,
|
|
1466
1658
|
width: this.width,
|
|
1467
1659
|
height: this.height,
|
|
1468
1660
|
visible: this.visible,
|
|
@@ -1508,12 +1700,12 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
|
1508
1700
|
*/
|
|
1509
1701
|
inkOffset() {
|
|
1510
1702
|
const text = asText(this.source);
|
|
1511
|
-
if (!text) return new
|
|
1703
|
+
if (!text) return new Point2(0, 0);
|
|
1512
1704
|
const { dx } = textInk(text);
|
|
1513
|
-
if (!dx) return new
|
|
1705
|
+
if (!dx) return new Point2(0, 0);
|
|
1514
1706
|
const scaled = dx * (this.source.scaleX ?? 1);
|
|
1515
1707
|
const radians = (this.source.angle ?? 0) * Math.PI / 180;
|
|
1516
|
-
return new
|
|
1708
|
+
return new Point2(scaled * Math.cos(radians), scaled * Math.sin(radians));
|
|
1517
1709
|
}
|
|
1518
1710
|
/** The source's on-canvas width, before the tile scale. */
|
|
1519
1711
|
baseW() {
|
|
@@ -1574,11 +1766,11 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
|
|
|
1574
1766
|
const radians = angle * Math.PI / 180;
|
|
1575
1767
|
const cos = Math.cos(radians);
|
|
1576
1768
|
const sin = Math.sin(radians);
|
|
1577
|
-
return new
|
|
1769
|
+
return new Point2(dx * cos - dy * sin, dx * sin + dy * cos);
|
|
1578
1770
|
}
|
|
1579
1771
|
anchorFromOrigin(origin, tileW, tileH, angle) {
|
|
1580
1772
|
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1581
|
-
return new
|
|
1773
|
+
return new Point2(origin.x + shift.x, origin.y + shift.y);
|
|
1582
1774
|
}
|
|
1583
1775
|
/**
|
|
1584
1776
|
* Snapshot the source at (at least) `scale`, reusing the cached one while it
|
|
@@ -1783,6 +1975,11 @@ var PatternManager = class {
|
|
|
1783
1975
|
if (wasActive) this.canvas.setActiveObject(proxy);
|
|
1784
1976
|
this.canvas.requestRenderAll();
|
|
1785
1977
|
}
|
|
1978
|
+
/** Fold a programmatic move of the proxy into the config and the source, as a drop does. */
|
|
1979
|
+
commitProxy(layerId) {
|
|
1980
|
+
const proxy = this.proxies.get(layerId);
|
|
1981
|
+
if (proxy) this.commitGesture(proxy);
|
|
1982
|
+
}
|
|
1786
1983
|
/**
|
|
1787
1984
|
* Fold a finished drag / scale / rotate on the proxy back into the pattern:
|
|
1788
1985
|
* position becomes the grid origin (carried by the source), scale becomes the
|
|
@@ -1850,7 +2047,7 @@ async function unbakeLegacyLayer(layer, state) {
|
|
|
1850
2047
|
cropY: state.original.cropY,
|
|
1851
2048
|
angle: state.original.angle
|
|
1852
2049
|
});
|
|
1853
|
-
image.clipPath = state.originalClip ? (await
|
|
2050
|
+
image.clipPath = state.originalClip ? (await util3.enlivenObjects([state.originalClip]))[0] : void 0;
|
|
1854
2051
|
restoreLocks(image, state.originalLocks);
|
|
1855
2052
|
image.setCoords();
|
|
1856
2053
|
}
|
|
@@ -1867,6 +2064,530 @@ function restoreLocks(obj, locks) {
|
|
|
1867
2064
|
);
|
|
1868
2065
|
}
|
|
1869
2066
|
|
|
2067
|
+
// src/shapes.ts
|
|
2068
|
+
function resolveShapeParams(plugin, overrides = {}) {
|
|
2069
|
+
const params = {};
|
|
2070
|
+
for (const def of plugin.params ?? []) {
|
|
2071
|
+
const value = overrides[def.key];
|
|
2072
|
+
const wanted = typeof value === "number" && Number.isFinite(value) ? value : def.default;
|
|
2073
|
+
params[def.key] = clamp(wanted, def.min, def.max);
|
|
2074
|
+
}
|
|
2075
|
+
return params;
|
|
2076
|
+
}
|
|
2077
|
+
var ShapeManager = class {
|
|
2078
|
+
constructor(canvas, layers, history, events) {
|
|
2079
|
+
this.canvas = canvas;
|
|
2080
|
+
this.layers = layers;
|
|
2081
|
+
this.history = history;
|
|
2082
|
+
this.events = events;
|
|
2083
|
+
this.canvas.on("object:modified", this.onObjectModified);
|
|
2084
|
+
}
|
|
2085
|
+
canvas;
|
|
2086
|
+
layers;
|
|
2087
|
+
history;
|
|
2088
|
+
events;
|
|
2089
|
+
plugins = /* @__PURE__ */ new Map();
|
|
2090
|
+
/**
|
|
2091
|
+
* An arrow scaled 2×1 must not stretch its head: the new box is baked into
|
|
2092
|
+
* the points and the scale returns to 1. Runs before the editor's own
|
|
2093
|
+
* `object:modified` handler (constructed earlier), so history records the
|
|
2094
|
+
* baked state.
|
|
2095
|
+
*/
|
|
2096
|
+
onObjectModified = (event) => {
|
|
2097
|
+
const target = event.target;
|
|
2098
|
+
if (!target) return;
|
|
2099
|
+
const layer = this.layers.findByObject(target);
|
|
2100
|
+
const state = layer?.meta.shape;
|
|
2101
|
+
if (!layer || !state || target !== layer.fabricObject) return;
|
|
2102
|
+
const plugin = this.plugins.get(state.id);
|
|
2103
|
+
if (!plugin?.bakesScale || !plugin.regenerate) return;
|
|
2104
|
+
const { scaleX = 1, scaleY = 1 } = target;
|
|
2105
|
+
if (Math.abs(scaleX - scaleY) < 1e-6) return;
|
|
2106
|
+
target.set({
|
|
2107
|
+
width: target.width * scaleX,
|
|
2108
|
+
height: target.height * scaleY,
|
|
2109
|
+
scaleX: 1,
|
|
2110
|
+
scaleY: 1
|
|
2111
|
+
});
|
|
2112
|
+
plugin.regenerate(target, state.params);
|
|
2113
|
+
target.setCoords();
|
|
2114
|
+
target.dirty = true;
|
|
2115
|
+
this.canvas.requestRenderAll();
|
|
2116
|
+
};
|
|
2117
|
+
dispose() {
|
|
2118
|
+
this.canvas.off("object:modified", this.onObjectModified);
|
|
2119
|
+
}
|
|
2120
|
+
/** A restored layer names its plugin by id; only a registered one can regenerate. */
|
|
2121
|
+
register(plugin) {
|
|
2122
|
+
this.plugins.set(plugin.name, plugin);
|
|
2123
|
+
}
|
|
2124
|
+
read(layerId) {
|
|
2125
|
+
return this.layers.get(layerId)?.meta.shape ?? null;
|
|
2126
|
+
}
|
|
2127
|
+
/**
|
|
2128
|
+
* `commit: false` is a slider drag: geometry and `meta.shape` are written and
|
|
2129
|
+
* `layer:modified` is emitted so the Shape panel re-reads, but nothing reaches
|
|
2130
|
+
* history. The default commits one entry.
|
|
2131
|
+
*/
|
|
2132
|
+
setParams(layerId, patch, commit = true) {
|
|
2133
|
+
const layer = this.layers.get(layerId);
|
|
2134
|
+
const state = layer?.meta.shape;
|
|
2135
|
+
if (!layer || !state) return;
|
|
2136
|
+
const plugin = this.plugins.get(state.id);
|
|
2137
|
+
if (!plugin?.regenerate) return;
|
|
2138
|
+
const params = resolveShapeParams(plugin, { ...state.params, ...patch });
|
|
2139
|
+
plugin.regenerate(layer.fabricObject, params);
|
|
2140
|
+
layer.fabricObject.setCoords();
|
|
2141
|
+
layer.fabricObject.dirty = true;
|
|
2142
|
+
const next = { ...state, params };
|
|
2143
|
+
if (commit) this.layers.setMeta(layerId, { shape: next });
|
|
2144
|
+
else layer.meta.shape = next;
|
|
2145
|
+
this.canvas.requestRenderAll();
|
|
2146
|
+
this.events.emit("layer:modified", { layerId });
|
|
2147
|
+
if (commit) this.history.save();
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
// src/image-effects/manager.ts
|
|
2152
|
+
import { classRegistry, getFilterBackend } from "fabric";
|
|
2153
|
+
|
|
2154
|
+
// src/image-effects/proxy.ts
|
|
2155
|
+
import { util as util4 } from "fabric";
|
|
2156
|
+
function sourceSize(source) {
|
|
2157
|
+
const el = source;
|
|
2158
|
+
return { width: el.naturalWidth || el.width, height: el.naturalHeight || el.height };
|
|
2159
|
+
}
|
|
2160
|
+
function proxyScale(width, height, maxEdge) {
|
|
2161
|
+
const edge = Math.max(width, height);
|
|
2162
|
+
return edge > maxEdge ? maxEdge / edge : 1;
|
|
2163
|
+
}
|
|
2164
|
+
function buildProxySource(source, maxEdge) {
|
|
2165
|
+
const { width, height } = sourceSize(source);
|
|
2166
|
+
const scale = proxyScale(width, height, maxEdge);
|
|
2167
|
+
if (scale === 1) return null;
|
|
2168
|
+
const element = util4.createCanvasElement();
|
|
2169
|
+
element.width = Math.max(1, Math.round(width * scale));
|
|
2170
|
+
element.height = Math.max(1, Math.round(height * scale));
|
|
2171
|
+
const context = element.getContext("2d");
|
|
2172
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
2173
|
+
context.drawImage(source, 0, 0, element.width, element.height);
|
|
2174
|
+
return { element, width: element.width, height: element.height };
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// src/image-effects/manager.ts
|
|
2178
|
+
var stable = (value) => JSON.stringify(
|
|
2179
|
+
value,
|
|
2180
|
+
(_k, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort()) : v
|
|
2181
|
+
);
|
|
2182
|
+
var ImageEffectsManager = class {
|
|
2183
|
+
constructor(canvas, layers, history, events) {
|
|
2184
|
+
this.canvas = canvas;
|
|
2185
|
+
this.layers = layers;
|
|
2186
|
+
this.history = history;
|
|
2187
|
+
this.events = events;
|
|
2188
|
+
this.events.on("layer:removed", this.onRemoved);
|
|
2189
|
+
}
|
|
2190
|
+
canvas;
|
|
2191
|
+
layers;
|
|
2192
|
+
history;
|
|
2193
|
+
events;
|
|
2194
|
+
proxies = /* @__PURE__ */ new Map();
|
|
2195
|
+
lastSpecs = /* @__PURE__ */ new Map();
|
|
2196
|
+
onRemoved = ({ layerId }) => this.drop(layerId);
|
|
2197
|
+
read(layerId) {
|
|
2198
|
+
const layer = this.imageLayer(layerId);
|
|
2199
|
+
return layer ? normalizeImageEffects(layer.meta) : { adjust: {} };
|
|
2200
|
+
}
|
|
2201
|
+
/** The filter instances a state derives to, in the D5 order, with no layer involved. */
|
|
2202
|
+
filtersFor(state) {
|
|
2203
|
+
return imageEffectsFilterSpecs(state).map(
|
|
2204
|
+
(spec) => new (classRegistry.getClass(spec.type))(spec)
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
preview(layerId, patch) {
|
|
2208
|
+
this.write(layerId, patch);
|
|
2209
|
+
}
|
|
2210
|
+
commit(layerId, patch) {
|
|
2211
|
+
if (this.write(layerId, patch)) this.history.save();
|
|
2212
|
+
}
|
|
2213
|
+
reset(layerId) {
|
|
2214
|
+
if (this.write(layerId, { preset: void 0, adjust: {} }, true)) this.history.save();
|
|
2215
|
+
}
|
|
2216
|
+
/** Re-derive after a source swap or a load. */
|
|
2217
|
+
refresh(layerId) {
|
|
2218
|
+
const layer = this.imageLayer(layerId);
|
|
2219
|
+
if (layer) this.apply(layer, this.read(layerId));
|
|
2220
|
+
}
|
|
2221
|
+
refreshAll() {
|
|
2222
|
+
for (const id of [...this.proxies.keys()]) if (!this.layers.get(id)) this.drop(id);
|
|
2223
|
+
for (const layer of this.layers.getAll()) {
|
|
2224
|
+
if (layer.type !== "image") continue;
|
|
2225
|
+
if ("imageAdjustments" in layer.meta) {
|
|
2226
|
+
const migrated = normalizeImageEffects(layer.meta);
|
|
2227
|
+
layer.meta.imageEffects = {
|
|
2228
|
+
...migrated.preset ? { preset: migrated.preset } : {},
|
|
2229
|
+
adjust: migrated.adjust
|
|
2230
|
+
};
|
|
2231
|
+
delete layer.meta.imageAdjustments;
|
|
2232
|
+
}
|
|
2233
|
+
this.apply(layer, this.read(layer.id));
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
hasProxy(layerId) {
|
|
2237
|
+
return this.proxies.has(layerId);
|
|
2238
|
+
}
|
|
2239
|
+
/** Run `fn` with every proxied image filtered at source resolution (export), then restore the proxies. */
|
|
2240
|
+
withSourceResolution(fn) {
|
|
2241
|
+
const swapped = [];
|
|
2242
|
+
for (const id of this.proxies.keys()) {
|
|
2243
|
+
const layer = this.imageLayer(id);
|
|
2244
|
+
if (!layer) continue;
|
|
2245
|
+
const image = layer.fabricObject;
|
|
2246
|
+
image._element = image._originalElement;
|
|
2247
|
+
image._filteredEl = void 0;
|
|
2248
|
+
image._filterScalingX = 1;
|
|
2249
|
+
image._filterScalingY = 1;
|
|
2250
|
+
image.applyFilters();
|
|
2251
|
+
swapped.push(layer);
|
|
2252
|
+
}
|
|
2253
|
+
try {
|
|
2254
|
+
return fn();
|
|
2255
|
+
} finally {
|
|
2256
|
+
for (const layer of swapped) {
|
|
2257
|
+
const image = layer.fabricObject;
|
|
2258
|
+
if (image._filteredEl) image._filteredEl.width = 0;
|
|
2259
|
+
image._filteredEl = void 0;
|
|
2260
|
+
image._element = image._originalElement;
|
|
2261
|
+
this.render(layer);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
dispose() {
|
|
2266
|
+
this.events.off("layer:removed", this.onRemoved);
|
|
2267
|
+
for (const id of [...this.proxies.keys()]) this.drop(id);
|
|
2268
|
+
this.lastSpecs.clear();
|
|
2269
|
+
}
|
|
2270
|
+
imageLayer(layerId) {
|
|
2271
|
+
const layer = this.layers.get(layerId);
|
|
2272
|
+
return layer && layer.type === "image" ? layer : void 0;
|
|
2273
|
+
}
|
|
2274
|
+
write(layerId, patch, replace = false) {
|
|
2275
|
+
const layer = this.imageLayer(layerId);
|
|
2276
|
+
if (!layer) return false;
|
|
2277
|
+
const current = replace ? { adjust: {} } : this.read(layerId);
|
|
2278
|
+
const presetChanged = "preset" in patch && stable(patch.preset) !== stable(current.preset);
|
|
2279
|
+
const seed = presetChanged && patch.preset ? imageFilterPreset(patch.preset.id)?.adjust ?? {} : current.adjust;
|
|
2280
|
+
const preset = "preset" in patch ? patch.preset : current.preset;
|
|
2281
|
+
const alphaClip = clampAlphaClip("alphaClip" in patch ? patch.alphaClip : current.alphaClip);
|
|
2282
|
+
const next = {
|
|
2283
|
+
...preset ? { preset } : {},
|
|
2284
|
+
adjust: { ...seed, ...patch.adjust ?? {} },
|
|
2285
|
+
...alphaClip ? { alphaClip } : {}
|
|
2286
|
+
};
|
|
2287
|
+
layer.meta.imageEffects = { ...preset ? { preset } : {}, adjust: next.adjust };
|
|
2288
|
+
if (alphaClip) layer.meta.imageAlphaClip = alphaClip;
|
|
2289
|
+
else delete layer.meta.imageAlphaClip;
|
|
2290
|
+
delete layer.meta.imageAdjustments;
|
|
2291
|
+
this.apply(layer, next);
|
|
2292
|
+
this.events.emit("layer:modified", { layerId });
|
|
2293
|
+
return true;
|
|
2294
|
+
}
|
|
2295
|
+
apply(layer, state) {
|
|
2296
|
+
const image = layer.fabricObject;
|
|
2297
|
+
const specs = imageEffectsFilterSpecs(state);
|
|
2298
|
+
const previous = this.lastSpecs.get(layer.id) ?? specs;
|
|
2299
|
+
const clip = clampAlphaClip(state.alphaClip);
|
|
2300
|
+
const legacyClip = clip ? stable(alphaClipMatrix(clip)) : null;
|
|
2301
|
+
const foreign = image.filters.filter((f) => {
|
|
2302
|
+
if (f.isNeutralState()) return false;
|
|
2303
|
+
const plain = f.toObject();
|
|
2304
|
+
if (legacyClip && plain.type === "ColorMatrix" && stable(plain.matrix) === legacyClip)
|
|
2305
|
+
return false;
|
|
2306
|
+
return !previous.some((s) => stable(s) === stable(plain));
|
|
2307
|
+
});
|
|
2308
|
+
const managed = specs.map((spec) => new (classRegistry.getClass(spec.type))(spec));
|
|
2309
|
+
image.filters = [...managed, ...foreign];
|
|
2310
|
+
this.lastSpecs.set(layer.id, specs);
|
|
2311
|
+
this.render(layer);
|
|
2312
|
+
}
|
|
2313
|
+
render(layer) {
|
|
2314
|
+
const image = layer.fabricObject;
|
|
2315
|
+
const record = this.proxyFor(layer.id, image);
|
|
2316
|
+
if (!record) {
|
|
2317
|
+
image.applyFilters();
|
|
2318
|
+
} else {
|
|
2319
|
+
const active = image.filters.filter((f) => !f.isNeutralState());
|
|
2320
|
+
if (active.length === 0) {
|
|
2321
|
+
image._element = image._originalElement;
|
|
2322
|
+
image._filterScalingX = 1;
|
|
2323
|
+
image._filterScalingY = 1;
|
|
2324
|
+
} else {
|
|
2325
|
+
getFilterBackend().applyFilters(
|
|
2326
|
+
active,
|
|
2327
|
+
record.source.element,
|
|
2328
|
+
record.source.width,
|
|
2329
|
+
record.source.height,
|
|
2330
|
+
record.target,
|
|
2331
|
+
`${image.cacheKey}_proxy`
|
|
2332
|
+
);
|
|
2333
|
+
const { width, height } = sourceSize(image._originalElement);
|
|
2334
|
+
image._element = record.target;
|
|
2335
|
+
image._filterScalingX = record.source.width / width;
|
|
2336
|
+
image._filterScalingY = record.source.height / height;
|
|
2337
|
+
}
|
|
2338
|
+
image.set("dirty", true);
|
|
2339
|
+
}
|
|
2340
|
+
this.canvas.requestRenderAll();
|
|
2341
|
+
}
|
|
2342
|
+
proxyFor(layerId, image) {
|
|
2343
|
+
const existing = this.proxies.get(layerId);
|
|
2344
|
+
if (existing && existing.original === image._originalElement) return existing;
|
|
2345
|
+
if (existing) this.drop(layerId);
|
|
2346
|
+
const source = buildProxySource(image._originalElement, IMAGE_PROXY_MAX_EDGE);
|
|
2347
|
+
if (!source) return null;
|
|
2348
|
+
const target = source.element.ownerDocument.createElement("canvas");
|
|
2349
|
+
target.width = source.width;
|
|
2350
|
+
target.height = source.height;
|
|
2351
|
+
const record = { original: image._originalElement, source, target };
|
|
2352
|
+
this.proxies.set(layerId, record);
|
|
2353
|
+
return record;
|
|
2354
|
+
}
|
|
2355
|
+
drop(layerId) {
|
|
2356
|
+
const record = this.proxies.get(layerId);
|
|
2357
|
+
if (!record) return;
|
|
2358
|
+
const layer = this.layers.get(layerId);
|
|
2359
|
+
if (layer)
|
|
2360
|
+
layer.fabricObject.removeTexture(
|
|
2361
|
+
`${layer.fabricObject.cacheKey}_proxy`
|
|
2362
|
+
);
|
|
2363
|
+
record.source.element.width = 0;
|
|
2364
|
+
record.target.width = 0;
|
|
2365
|
+
this.proxies.delete(layerId);
|
|
2366
|
+
this.lastSpecs.delete(layerId);
|
|
2367
|
+
}
|
|
2368
|
+
};
|
|
2369
|
+
|
|
2370
|
+
// src/layer-effects.ts
|
|
2371
|
+
import { ECHO_MAX_COUNT } from "@overtone-art/canvas-editor-effects";
|
|
2372
|
+
|
|
2373
|
+
// src/shadow.ts
|
|
2374
|
+
import { Shadow } from "fabric";
|
|
2375
|
+
|
|
2376
|
+
// src/utils/color.ts
|
|
2377
|
+
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
2378
|
+
function isCssColor(value, allowEmpty = false) {
|
|
2379
|
+
if (typeof value !== "string") return false;
|
|
2380
|
+
const trimmed = value.trim();
|
|
2381
|
+
if (!trimmed) return allowEmpty;
|
|
2382
|
+
return CSS_COLOR.test(trimmed);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// src/shadow.ts
|
|
2386
|
+
var DEFAULT_LAYER_SHADOW = {
|
|
2387
|
+
enabled: false,
|
|
2388
|
+
color: "#000000",
|
|
2389
|
+
blur: 12,
|
|
2390
|
+
offsetX: 6,
|
|
2391
|
+
offsetY: 6
|
|
2392
|
+
};
|
|
2393
|
+
function readLayerShadow(object) {
|
|
2394
|
+
const shadow = object.shadow;
|
|
2395
|
+
if (!shadow || typeof shadow === "string") return { ...DEFAULT_LAYER_SHADOW };
|
|
2396
|
+
return {
|
|
2397
|
+
enabled: true,
|
|
2398
|
+
color: typeof shadow.color === "string" ? shadow.color : DEFAULT_LAYER_SHADOW.color,
|
|
2399
|
+
blur: shadow.blur ?? DEFAULT_LAYER_SHADOW.blur,
|
|
2400
|
+
offsetX: shadow.offsetX ?? DEFAULT_LAYER_SHADOW.offsetX,
|
|
2401
|
+
offsetY: shadow.offsetY ?? DEFAULT_LAYER_SHADOW.offsetY
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
function applyLayerShadow(object, config) {
|
|
2405
|
+
const next = { ...readLayerShadow(object), ...config };
|
|
2406
|
+
if (!next.enabled) {
|
|
2407
|
+
object.set({ shadow: null });
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
const color2 = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
|
|
2411
|
+
object.set({
|
|
2412
|
+
shadow: new Shadow({
|
|
2413
|
+
color: color2,
|
|
2414
|
+
blur: Math.max(0, next.blur),
|
|
2415
|
+
offsetX: next.offsetX,
|
|
2416
|
+
offsetY: next.offsetY
|
|
2417
|
+
})
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// src/layer-effects.ts
|
|
2422
|
+
var LAYER_EFFECT_DEFAULTS = {
|
|
2423
|
+
outline: { color: "#000000", width: 4 },
|
|
2424
|
+
hollow: { color: "#000000", width: 4 },
|
|
2425
|
+
drop: { color: "#000000", blur: 0, offsetX: 6, offsetY: 6 },
|
|
2426
|
+
splice: { color: "#ff4d6d", offsetX: 8, offsetY: 8 },
|
|
2427
|
+
echo: { color: "#000000", count: 3, stepX: 6, stepY: 6, fade: 0.5 },
|
|
2428
|
+
background: { color: "#ffe600", padding: 12, radius: 8 }
|
|
2429
|
+
};
|
|
2430
|
+
var num = (p, key, fallback) => {
|
|
2431
|
+
const value = p[key];
|
|
2432
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2433
|
+
};
|
|
2434
|
+
var color = (p, key, fallback) => {
|
|
2435
|
+
const value = p[key];
|
|
2436
|
+
return typeof value === "string" && isCssColor(value) ? value : fallback;
|
|
2437
|
+
};
|
|
2438
|
+
var outlineProps = (p) => ({
|
|
2439
|
+
stroke: color(p, "color", "#000000"),
|
|
2440
|
+
strokeWidth: clamp(num(p, "width", 4), 0, 200),
|
|
2441
|
+
paintFirst: "stroke",
|
|
2442
|
+
strokeUniform: true
|
|
2443
|
+
});
|
|
2444
|
+
var LayerEffectsManager = class {
|
|
2445
|
+
constructor(canvas, layers, history, events) {
|
|
2446
|
+
this.canvas = canvas;
|
|
2447
|
+
this.layers = layers;
|
|
2448
|
+
this.history = history;
|
|
2449
|
+
this.events = events;
|
|
2450
|
+
}
|
|
2451
|
+
canvas;
|
|
2452
|
+
layers;
|
|
2453
|
+
history;
|
|
2454
|
+
events;
|
|
2455
|
+
read(layerId) {
|
|
2456
|
+
return this.layers.get(layerId)?.meta.effect ?? null;
|
|
2457
|
+
}
|
|
2458
|
+
/**
|
|
2459
|
+
* `commit: false` is a slider drag: props are written and `layer:modified`
|
|
2460
|
+
* is emitted so the panels re-read, but nothing reaches history. The default
|
|
2461
|
+
* commits ONE entry, setMeta's own save folded into the same transaction.
|
|
2462
|
+
*/
|
|
2463
|
+
apply(layerId, kind, params = {}, commit = true) {
|
|
2464
|
+
const layer = this.layers.get(layerId);
|
|
2465
|
+
if (!layer || layer.type === "group") return;
|
|
2466
|
+
const object = layer.fabricObject;
|
|
2467
|
+
const write = () => {
|
|
2468
|
+
this.clear(object, layer.meta.effect);
|
|
2469
|
+
const next = kind === "none" ? void 0 : {
|
|
2470
|
+
kind,
|
|
2471
|
+
params: this.install(object, kind, { ...LAYER_EFFECT_DEFAULTS[kind], ...params }),
|
|
2472
|
+
version: 1
|
|
2473
|
+
};
|
|
2474
|
+
if (commit) this.layers.setMeta(layerId, { effect: next });
|
|
2475
|
+
else if (next) layer.meta.effect = next;
|
|
2476
|
+
else delete layer.meta.effect;
|
|
2477
|
+
object.dirty = true;
|
|
2478
|
+
object.setCoords();
|
|
2479
|
+
this.canvas.requestRenderAll();
|
|
2480
|
+
this.events.emit("layer:modified", { layerId });
|
|
2481
|
+
};
|
|
2482
|
+
if (!commit) {
|
|
2483
|
+
write();
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
this.history.beginTransaction();
|
|
2487
|
+
try {
|
|
2488
|
+
write();
|
|
2489
|
+
this.history.save();
|
|
2490
|
+
} finally {
|
|
2491
|
+
this.history.endTransaction();
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
/** Undo what the previous kind wrote, so kinds never stack. */
|
|
2495
|
+
clear(object, previous) {
|
|
2496
|
+
if (!previous) return;
|
|
2497
|
+
switch (previous.kind) {
|
|
2498
|
+
case "outline":
|
|
2499
|
+
object.set({ strokeWidth: 0, paintFirst: "fill", strokeUniform: false });
|
|
2500
|
+
return;
|
|
2501
|
+
case "hollow": {
|
|
2502
|
+
const fill = previous.params.previousFill;
|
|
2503
|
+
object.set({
|
|
2504
|
+
strokeWidth: 0,
|
|
2505
|
+
paintFirst: "fill",
|
|
2506
|
+
strokeUniform: false,
|
|
2507
|
+
fill: typeof fill === "string" ? fill : "#000000"
|
|
2508
|
+
});
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
case "drop":
|
|
2512
|
+
applyLayerShadow(object, { enabled: false });
|
|
2513
|
+
return;
|
|
2514
|
+
case "splice":
|
|
2515
|
+
case "echo":
|
|
2516
|
+
case "background":
|
|
2517
|
+
object.set({ effect: void 0 });
|
|
2518
|
+
return;
|
|
2519
|
+
case "none":
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
install(object, kind, p) {
|
|
2524
|
+
switch (kind) {
|
|
2525
|
+
case "outline": {
|
|
2526
|
+
const props = outlineProps(p);
|
|
2527
|
+
object.set(props);
|
|
2528
|
+
return { color: props.stroke, width: props.strokeWidth };
|
|
2529
|
+
}
|
|
2530
|
+
case "hollow": {
|
|
2531
|
+
const previousFill = typeof object.fill === "string" ? object.fill : "#000000";
|
|
2532
|
+
const props = outlineProps(p);
|
|
2533
|
+
object.set({ ...props, fill: "transparent" });
|
|
2534
|
+
return { color: props.stroke, width: props.strokeWidth, previousFill };
|
|
2535
|
+
}
|
|
2536
|
+
case "drop": {
|
|
2537
|
+
const shadow = {
|
|
2538
|
+
color: color(p, "color", "#000000"),
|
|
2539
|
+
blur: clamp(num(p, "blur", 0), 0, 200),
|
|
2540
|
+
offsetX: num(p, "offsetX", 6),
|
|
2541
|
+
offsetY: num(p, "offsetY", 6)
|
|
2542
|
+
};
|
|
2543
|
+
applyLayerShadow(object, { enabled: true, ...shadow });
|
|
2544
|
+
return shadow;
|
|
2545
|
+
}
|
|
2546
|
+
case "splice": {
|
|
2547
|
+
const effect = {
|
|
2548
|
+
kind: "splice",
|
|
2549
|
+
offsetX: num(p, "offsetX", 8),
|
|
2550
|
+
offsetY: num(p, "offsetY", 8),
|
|
2551
|
+
color: color(p, "color", "#ff4d6d"),
|
|
2552
|
+
version: 1
|
|
2553
|
+
};
|
|
2554
|
+
object.set({ effect });
|
|
2555
|
+
return { offsetX: effect.offsetX, offsetY: effect.offsetY, color: effect.color };
|
|
2556
|
+
}
|
|
2557
|
+
case "echo": {
|
|
2558
|
+
const effect = {
|
|
2559
|
+
kind: "echo",
|
|
2560
|
+
count: clamp(Math.round(num(p, "count", 3)), 1, ECHO_MAX_COUNT),
|
|
2561
|
+
stepX: num(p, "stepX", 6),
|
|
2562
|
+
stepY: num(p, "stepY", 6),
|
|
2563
|
+
color: color(p, "color", "#000000"),
|
|
2564
|
+
fade: clamp(num(p, "fade", 0.5), 0, 1),
|
|
2565
|
+
version: 1
|
|
2566
|
+
};
|
|
2567
|
+
object.set({ effect });
|
|
2568
|
+
return {
|
|
2569
|
+
count: effect.count,
|
|
2570
|
+
stepX: effect.stepX,
|
|
2571
|
+
stepY: effect.stepY,
|
|
2572
|
+
color: effect.color,
|
|
2573
|
+
fade: effect.fade
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
case "background": {
|
|
2577
|
+
const effect = {
|
|
2578
|
+
kind: "background",
|
|
2579
|
+
padding: clamp(num(p, "padding", 12), 0, 400),
|
|
2580
|
+
radius: clamp(num(p, "radius", 8), 0, 400),
|
|
2581
|
+
color: color(p, "color", "#ffe600"),
|
|
2582
|
+
version: 1
|
|
2583
|
+
};
|
|
2584
|
+
object.set({ effect });
|
|
2585
|
+
return { padding: effect.padding, radius: effect.radius, color: effect.color };
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
};
|
|
2590
|
+
|
|
1870
2591
|
// src/text-curve.ts
|
|
1871
2592
|
import { Path } from "fabric";
|
|
1872
2593
|
|
|
@@ -1877,9 +2598,12 @@ var DEFAULT_TEXT_CURVE = {
|
|
|
1877
2598
|
wave: 0,
|
|
1878
2599
|
waveLength: 4.1,
|
|
1879
2600
|
offset: 0,
|
|
1880
|
-
centerOffset: 0
|
|
2601
|
+
centerOffset: 0,
|
|
2602
|
+
angle: 0
|
|
1881
2603
|
};
|
|
1882
2604
|
var MIN_ARC = 0.5;
|
|
2605
|
+
var MIN_ANGLE = 0.5;
|
|
2606
|
+
var MAX_ANGLE = 80;
|
|
1883
2607
|
var FULL_CIRCLE_ARC = 99.5;
|
|
1884
2608
|
var MIN_SWEEP = 0.12;
|
|
1885
2609
|
var WAVE_AMPLITUDE_EM = 0.9;
|
|
@@ -1904,10 +2628,12 @@ function normalizeTextCurve(config) {
|
|
|
1904
2628
|
MAX_WAVE_LENGTH
|
|
1905
2629
|
),
|
|
1906
2630
|
offset: clamp(config.offset ?? 0, -100, 100),
|
|
1907
|
-
centerOffset: clamp(config.centerOffset ?? 0, -100, 100)
|
|
2631
|
+
centerOffset: clamp(config.centerOffset ?? 0, -100, 100),
|
|
2632
|
+
angle: clamp(config.angle ?? 0, -MAX_ANGLE, MAX_ANGLE)
|
|
1908
2633
|
};
|
|
1909
2634
|
}
|
|
1910
2635
|
function bendsBaseline(config) {
|
|
2636
|
+
if (config.shape === "angle") return Math.abs(config.angle) >= MIN_ANGLE;
|
|
1911
2637
|
return config.shape === "arc" ? Math.abs(config.arc) >= MIN_ARC : config.wave > 0;
|
|
1912
2638
|
}
|
|
1913
2639
|
function arcGeometry(width, config) {
|
|
@@ -1968,6 +2694,17 @@ function waveLinePath(width, fontSize, config, dy) {
|
|
|
1968
2694
|
}
|
|
1969
2695
|
return { data: commands.join(" "), length };
|
|
1970
2696
|
}
|
|
2697
|
+
function angleLinePath(width, config, dy) {
|
|
2698
|
+
const radians = config.angle * Math.PI / 180;
|
|
2699
|
+
const half = width / 2;
|
|
2700
|
+
const dx = half * Math.cos(radians);
|
|
2701
|
+
const rise = half * Math.sin(radians);
|
|
2702
|
+
const format = ([x, y]) => `${round(x)} ${round(y)}`;
|
|
2703
|
+
return {
|
|
2704
|
+
data: `M ${format([half - dx, dy + rise])} L ${format([half + dx, dy - rise])}`,
|
|
2705
|
+
length: width
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
1971
2708
|
function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight = 0) {
|
|
1972
2709
|
const curve = normalizeTextCurve(config);
|
|
1973
2710
|
if (!bendsBaseline(curve)) return null;
|
|
@@ -1979,6 +2716,12 @@ function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight
|
|
|
1979
2716
|
}
|
|
1980
2717
|
return paths;
|
|
1981
2718
|
}
|
|
2719
|
+
if (curve.shape === "angle") {
|
|
2720
|
+
for (let index = 0; index < lines; index++) {
|
|
2721
|
+
paths.push(angleLinePath(width, curve, (index - (lines - 1) / 2) * lineHeight));
|
|
2722
|
+
}
|
|
2723
|
+
return paths;
|
|
2724
|
+
}
|
|
1982
2725
|
const geometry = arcGeometry(width, curve);
|
|
1983
2726
|
const rotation = curve.offset / 100 * geometry.sweep;
|
|
1984
2727
|
for (let index = 0; index < lines; index++) {
|
|
@@ -2559,54 +3302,6 @@ var MaskPresetManager = class {
|
|
|
2559
3302
|
}
|
|
2560
3303
|
};
|
|
2561
3304
|
|
|
2562
|
-
// src/shadow.ts
|
|
2563
|
-
import { Shadow } from "fabric";
|
|
2564
|
-
|
|
2565
|
-
// src/utils/color.ts
|
|
2566
|
-
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
2567
|
-
function isCssColor(value, allowEmpty = false) {
|
|
2568
|
-
if (typeof value !== "string") return false;
|
|
2569
|
-
const trimmed = value.trim();
|
|
2570
|
-
if (!trimmed) return allowEmpty;
|
|
2571
|
-
return CSS_COLOR.test(trimmed);
|
|
2572
|
-
}
|
|
2573
|
-
|
|
2574
|
-
// src/shadow.ts
|
|
2575
|
-
var DEFAULT_LAYER_SHADOW = {
|
|
2576
|
-
enabled: false,
|
|
2577
|
-
color: "#000000",
|
|
2578
|
-
blur: 12,
|
|
2579
|
-
offsetX: 6,
|
|
2580
|
-
offsetY: 6
|
|
2581
|
-
};
|
|
2582
|
-
function readLayerShadow(object) {
|
|
2583
|
-
const shadow = object.shadow;
|
|
2584
|
-
if (!shadow || typeof shadow === "string") return { ...DEFAULT_LAYER_SHADOW };
|
|
2585
|
-
return {
|
|
2586
|
-
enabled: true,
|
|
2587
|
-
color: typeof shadow.color === "string" ? shadow.color : DEFAULT_LAYER_SHADOW.color,
|
|
2588
|
-
blur: shadow.blur ?? DEFAULT_LAYER_SHADOW.blur,
|
|
2589
|
-
offsetX: shadow.offsetX ?? DEFAULT_LAYER_SHADOW.offsetX,
|
|
2590
|
-
offsetY: shadow.offsetY ?? DEFAULT_LAYER_SHADOW.offsetY
|
|
2591
|
-
};
|
|
2592
|
-
}
|
|
2593
|
-
function applyLayerShadow(object, config) {
|
|
2594
|
-
const next = { ...readLayerShadow(object), ...config };
|
|
2595
|
-
if (!next.enabled) {
|
|
2596
|
-
object.set({ shadow: null });
|
|
2597
|
-
return;
|
|
2598
|
-
}
|
|
2599
|
-
const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
|
|
2600
|
-
object.set({
|
|
2601
|
-
shadow: new Shadow({
|
|
2602
|
-
color,
|
|
2603
|
-
blur: Math.max(0, next.blur),
|
|
2604
|
-
offsetX: next.offsetX,
|
|
2605
|
-
offsetY: next.offsetY
|
|
2606
|
-
})
|
|
2607
|
-
});
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
3305
|
// src/selection-style.ts
|
|
2611
3306
|
var DEFAULT_SELECTION_STYLE = {
|
|
2612
3307
|
borderColor: "#c9a96e",
|
|
@@ -2712,7 +3407,7 @@ var UnitConverter = class {
|
|
|
2712
3407
|
};
|
|
2713
3408
|
|
|
2714
3409
|
// src/serialization.ts
|
|
2715
|
-
import { util as
|
|
3410
|
+
import { util as util5 } from "fabric";
|
|
2716
3411
|
var VERSION = "2.0.0";
|
|
2717
3412
|
function serializeEditor(editor) {
|
|
2718
3413
|
return {
|
|
@@ -2748,7 +3443,7 @@ async function deserializeEditor(editor, state) {
|
|
|
2748
3443
|
}
|
|
2749
3444
|
const staged = await Promise.all(
|
|
2750
3445
|
state.layers.map(async (serialized) => {
|
|
2751
|
-
const fabricObject = (await
|
|
3446
|
+
const fabricObject = (await util5.enlivenObjects([serialized.fabricObject]))[0];
|
|
2752
3447
|
if (!fabricObject) {
|
|
2753
3448
|
const source = serialized.fabricObject.src;
|
|
2754
3449
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -2759,7 +3454,7 @@ async function deserializeEditor(editor, state) {
|
|
|
2759
3454
|
return { serialized, fabricObject };
|
|
2760
3455
|
})
|
|
2761
3456
|
);
|
|
2762
|
-
const stagedBackground = state.backgroundImage ? (await
|
|
3457
|
+
const stagedBackground = state.backgroundImage ? (await util5.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
2763
3458
|
if (state.backgroundImage && !stagedBackground) {
|
|
2764
3459
|
const source = state.backgroundImage.src;
|
|
2765
3460
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -3350,8 +4045,8 @@ var MaskController = class {
|
|
|
3350
4045
|
point.y,
|
|
3351
4046
|
radius
|
|
3352
4047
|
);
|
|
3353
|
-
const
|
|
3354
|
-
gradient.addColorStop(0,
|
|
4048
|
+
const color2 = brush.mode === "subtract" ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
|
|
4049
|
+
gradient.addColorStop(0, color2);
|
|
3355
4050
|
gradient.addColorStop(1, "rgba(255,255,255,0)");
|
|
3356
4051
|
context.fillStyle = gradient;
|
|
3357
4052
|
context.beginPath();
|
|
@@ -3426,10 +4121,10 @@ function angleFromCoords(coords, box) {
|
|
|
3426
4121
|
}
|
|
3427
4122
|
function stopColor(stop) {
|
|
3428
4123
|
const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
|
|
3429
|
-
const
|
|
4124
|
+
const color2 = new Color(source);
|
|
3430
4125
|
const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
|
|
3431
|
-
|
|
3432
|
-
return
|
|
4126
|
+
color2.setAlpha(clamp(opacity, 0, 1));
|
|
4127
|
+
return color2.toRgba();
|
|
3433
4128
|
}
|
|
3434
4129
|
function toFabricGradient(config, box) {
|
|
3435
4130
|
const colorStops = [...config.stops].sort((a, b) => a.position - b.position).slice(0, MAX_GRADIENT_STOPS).map((stop) => {
|
|
@@ -3462,11 +4157,11 @@ function toFabricGradient(config, box) {
|
|
|
3462
4157
|
function toFabricMaskGradient(config, box) {
|
|
3463
4158
|
const gradient = toFabricGradient(config, box);
|
|
3464
4159
|
gradient.colorStops = gradient.colorStops.map((stop) => {
|
|
3465
|
-
const
|
|
3466
|
-
const [red, green, blue] =
|
|
4160
|
+
const color2 = new Color(stop.color);
|
|
4161
|
+
const [red, green, blue] = color2.getSource();
|
|
3467
4162
|
const luminance = (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255;
|
|
3468
|
-
|
|
3469
|
-
return { offset: stop.offset, color:
|
|
4163
|
+
color2.setSource([255, 255, 255, clamp(luminance * color2.getAlpha(), 0, 1)]);
|
|
4164
|
+
return { offset: stop.offset, color: color2.toRgba() };
|
|
3470
4165
|
});
|
|
3471
4166
|
return gradient;
|
|
3472
4167
|
}
|
|
@@ -3475,11 +4170,11 @@ function readGradientConfig(object) {
|
|
|
3475
4170
|
if (!fill || typeof fill === "string" || !(fill instanceof Gradient)) return null;
|
|
3476
4171
|
const stops = fill.colorStops.map((stop) => {
|
|
3477
4172
|
const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
|
|
3478
|
-
const
|
|
4173
|
+
const color2 = new Color(source);
|
|
3479
4174
|
const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
|
|
3480
4175
|
return {
|
|
3481
|
-
color: `#${
|
|
3482
|
-
opacity:
|
|
4176
|
+
color: `#${color2.toHex().toLowerCase()}`,
|
|
4177
|
+
opacity: color2.getAlpha(),
|
|
3483
4178
|
position: clamp(offset, 0, 1)
|
|
3484
4179
|
};
|
|
3485
4180
|
});
|
|
@@ -3562,7 +4257,7 @@ function needsAbsoluteSpace(entries) {
|
|
|
3562
4257
|
}
|
|
3563
4258
|
|
|
3564
4259
|
// src/masks/edit.ts
|
|
3565
|
-
import { util as
|
|
4260
|
+
import { util as util6 } from "fabric";
|
|
3566
4261
|
|
|
3567
4262
|
// src/masks/edit-viewport.ts
|
|
3568
4263
|
var MIN_PADDING = 48;
|
|
@@ -3740,8 +4435,8 @@ var MaskEditController = class {
|
|
|
3740
4435
|
if (!this.handle || !this.child || !this.group) return;
|
|
3741
4436
|
applyMatrix(
|
|
3742
4437
|
this.child,
|
|
3743
|
-
|
|
3744
|
-
|
|
4438
|
+
util6.multiplyTransformMatrices(
|
|
4439
|
+
util6.invertTransform(matrixOf(this.group)),
|
|
3745
4440
|
matrixOf(this.handle)
|
|
3746
4441
|
)
|
|
3747
4442
|
);
|
|
@@ -4438,11 +5133,17 @@ var CanvasEditor = class {
|
|
|
4438
5133
|
snapping;
|
|
4439
5134
|
crop;
|
|
4440
5135
|
patterns;
|
|
5136
|
+
/** Parametric shape layers: `meta.shape` and in-place regeneration. */
|
|
5137
|
+
shapes;
|
|
4441
5138
|
curves;
|
|
4442
5139
|
wrap;
|
|
4443
5140
|
maskPresets;
|
|
5141
|
+
/** Owner of every image layer's filter array (D5). */
|
|
5142
|
+
imageEffects;
|
|
4444
5143
|
/** Stacked boolean masks, per layer and for the design as a whole. */
|
|
4445
5144
|
layerMasks;
|
|
5145
|
+
/** One effect per text or shape layer (D6). */
|
|
5146
|
+
effects;
|
|
4446
5147
|
fonts;
|
|
4447
5148
|
licensing;
|
|
4448
5149
|
pages;
|
|
@@ -4452,6 +5153,8 @@ var CanvasEditor = class {
|
|
|
4452
5153
|
zoomLevel = 1;
|
|
4453
5154
|
selectionStyle = { ...DEFAULT_SELECTION_STYLE };
|
|
4454
5155
|
mockup = null;
|
|
5156
|
+
/** Alt was held when a drag began on this layer; the drop leaves a copy behind. */
|
|
5157
|
+
altDrag = null;
|
|
4455
5158
|
// The design's configured background. The live canvas background is forced
|
|
4456
5159
|
// transparent while a mockup preview is shown, so this is the source of truth
|
|
4457
5160
|
// for serialization and export — not the (possibly transient) canvas value.
|
|
@@ -4459,6 +5162,7 @@ var CanvasEditor = class {
|
|
|
4459
5162
|
designBackgroundImage = null;
|
|
4460
5163
|
backgroundImageOptions = null;
|
|
4461
5164
|
constructor(canvasElement, config) {
|
|
5165
|
+
registerEffects(fabricModule);
|
|
4462
5166
|
this.events = new EventEmitter();
|
|
4463
5167
|
this.fonts = new FontRegistry();
|
|
4464
5168
|
config.fonts?.forEach((font) => this.fonts.register(font));
|
|
@@ -4493,6 +5197,15 @@ var CanvasEditor = class {
|
|
|
4493
5197
|
this.wrap = new TextWrapManager(this.canvas, this.layers, this.history, this.events);
|
|
4494
5198
|
this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
|
|
4495
5199
|
this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
|
|
5200
|
+
this.effects = new LayerEffectsManager(this.canvas, this.layers, this.history, this.events);
|
|
5201
|
+
this.imageEffects = new ImageEffectsManager(
|
|
5202
|
+
this.canvas,
|
|
5203
|
+
this.layers,
|
|
5204
|
+
this.history,
|
|
5205
|
+
this.events
|
|
5206
|
+
);
|
|
5207
|
+
this.shapes = new ShapeManager(this.canvas, this.layers, this.history, this.events);
|
|
5208
|
+
config.shapes?.forEach((plugin) => this.shapes.register(plugin));
|
|
4496
5209
|
this.setupCanvasEvents();
|
|
4497
5210
|
this.refreshSelectionStyle();
|
|
4498
5211
|
this.history.saveImmediate();
|
|
@@ -4517,7 +5230,7 @@ var CanvasEditor = class {
|
|
|
4517
5230
|
}
|
|
4518
5231
|
}
|
|
4519
5232
|
/** Replace an image source without changing its layer identity or visual transform. */
|
|
4520
|
-
async replaceImageSource(layerId, url) {
|
|
5233
|
+
async replaceImageSource(layerId, url, options = {}) {
|
|
4521
5234
|
const layer = this.layers.get(layerId);
|
|
4522
5235
|
if (!layer || layer.type !== "image" && layer.type !== "mask") {
|
|
4523
5236
|
throw new Error(`Image or mask layer not found: ${layerId}`);
|
|
@@ -4528,7 +5241,11 @@ var CanvasEditor = class {
|
|
|
4528
5241
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
4529
5242
|
const previous = layer.fabricObject;
|
|
4530
5243
|
try {
|
|
4531
|
-
const replacement = await FabricImage3.fromURL(
|
|
5244
|
+
const replacement = await FabricImage3.fromURL(
|
|
5245
|
+
url,
|
|
5246
|
+
{ crossOrigin: options.crossOrigin },
|
|
5247
|
+
{ originX: "left", originY: "top" }
|
|
5248
|
+
);
|
|
4532
5249
|
replacement.set({
|
|
4533
5250
|
left: previous.left,
|
|
4534
5251
|
top: previous.top,
|
|
@@ -4550,14 +5267,28 @@ var CanvasEditor = class {
|
|
|
4550
5267
|
replacement.applyFilters();
|
|
4551
5268
|
replacement.setCoords();
|
|
4552
5269
|
this.layers.replaceObject(layerId, replacement);
|
|
5270
|
+
this.imageEffects.refresh(layerId);
|
|
4553
5271
|
return layer;
|
|
4554
5272
|
} catch (error) {
|
|
4555
5273
|
this.events.emit("error", { message: "Failed to replace image source", error });
|
|
4556
5274
|
throw error;
|
|
4557
5275
|
}
|
|
4558
5276
|
}
|
|
5277
|
+
/**
|
|
5278
|
+
* The class the registry holds for a stock name.
|
|
5279
|
+
*
|
|
5280
|
+
* `registerEffects` (run in the constructor) overrides the stock names, but
|
|
5281
|
+
* registration only decides what an ENLIVEN produces — a `new Textbox(...)`
|
|
5282
|
+
* here would still be the stock class, which carries no `effect` in its
|
|
5283
|
+
* `customProperties` and so could never serialize one. Authoring and
|
|
5284
|
+
* restoring have to yield the same class.
|
|
5285
|
+
*/
|
|
5286
|
+
registered(name, fallback) {
|
|
5287
|
+
return classRegistry2.getClass(name) ?? fallback;
|
|
5288
|
+
}
|
|
4559
5289
|
addText(text, options) {
|
|
4560
|
-
const
|
|
5290
|
+
const TextboxClass = this.registered("Textbox", Textbox2);
|
|
5291
|
+
const textbox = new TextboxClass(text, {
|
|
4561
5292
|
fontSize: 32,
|
|
4562
5293
|
fontFamily: "Arial",
|
|
4563
5294
|
fill: "#000000",
|
|
@@ -4575,6 +5306,15 @@ var CanvasEditor = class {
|
|
|
4575
5306
|
addShape(plugin, options) {
|
|
4576
5307
|
const obj = plugin.create(options);
|
|
4577
5308
|
const layer = this.layers.add("shape", obj, plugin.name);
|
|
5309
|
+
if (plugin.params) {
|
|
5310
|
+
this.shapes.register(plugin);
|
|
5311
|
+
const overrides = options?.params ?? {};
|
|
5312
|
+
layer.meta.shape = {
|
|
5313
|
+
id: plugin.name,
|
|
5314
|
+
params: resolveShapeParams(plugin, overrides),
|
|
5315
|
+
version: 1
|
|
5316
|
+
};
|
|
5317
|
+
}
|
|
4578
5318
|
this.layers.select(layer.id);
|
|
4579
5319
|
this.history.save();
|
|
4580
5320
|
return layer;
|
|
@@ -4586,7 +5326,8 @@ var CanvasEditor = class {
|
|
|
4586
5326
|
addGradient(config = DEFAULT_GRADIENT_CONFIG, box) {
|
|
4587
5327
|
const width = box?.width ?? this.canvas.getWidth();
|
|
4588
5328
|
const height = box?.height ?? this.canvas.getHeight();
|
|
4589
|
-
const
|
|
5329
|
+
const RectClass = this.registered("Rect", Rect4);
|
|
5330
|
+
const object = new RectClass({
|
|
4590
5331
|
left: box?.left ?? 0,
|
|
4591
5332
|
top: box?.top ?? 0,
|
|
4592
5333
|
originX: "left",
|
|
@@ -4715,6 +5456,49 @@ var CanvasEditor = class {
|
|
|
4715
5456
|
this.canvas.requestRenderAll();
|
|
4716
5457
|
this.history.save();
|
|
4717
5458
|
}
|
|
5459
|
+
// ─── Align ──────────────────────────────────────────
|
|
5460
|
+
/** What the user is acting on: the proxy where a layer has one. */
|
|
5461
|
+
selectedTargets() {
|
|
5462
|
+
return this.canvas.getActiveObjects().filter((object) => this.layers.findByObject(object) !== void 0);
|
|
5463
|
+
}
|
|
5464
|
+
afterMove(objects) {
|
|
5465
|
+
relayoutSelection(this.canvas.getActiveObject());
|
|
5466
|
+
for (const object of objects) {
|
|
5467
|
+
const layer = this.layers.findByObject(object);
|
|
5468
|
+
if (!layer) continue;
|
|
5469
|
+
if (layer.renderProxy === object) this.patterns.commitProxy(layer.id);
|
|
5470
|
+
this.events.emit("layer:modified", { layerId: layer.id });
|
|
5471
|
+
}
|
|
5472
|
+
this.canvas.requestRenderAll();
|
|
5473
|
+
this.history.save();
|
|
5474
|
+
}
|
|
5475
|
+
/** One layer aligns to the canvas; several align to their common box. One history entry. */
|
|
5476
|
+
alignLayers(target) {
|
|
5477
|
+
const objects = this.selectedTargets();
|
|
5478
|
+
if (objects.length === 0) return false;
|
|
5479
|
+
const frame = objects.length === 1 ? canvasBox(this.canvas.getWidth(), this.canvas.getHeight()) : unionBox(objects.map((o) => (o.setCoords(), o.getBoundingRect())));
|
|
5480
|
+
this.history.beginTransaction();
|
|
5481
|
+
try {
|
|
5482
|
+
alignObjects(objects, target, frame);
|
|
5483
|
+
this.afterMove(objects);
|
|
5484
|
+
} finally {
|
|
5485
|
+
this.history.endTransaction();
|
|
5486
|
+
}
|
|
5487
|
+
return true;
|
|
5488
|
+
}
|
|
5489
|
+
/** Equal gaps along an axis. Needs three selected; returns false otherwise. */
|
|
5490
|
+
distributeLayers(axis) {
|
|
5491
|
+
const objects = this.selectedTargets();
|
|
5492
|
+
if (objects.length < 3) return false;
|
|
5493
|
+
this.history.beginTransaction();
|
|
5494
|
+
try {
|
|
5495
|
+
distributeObjects(objects, axis);
|
|
5496
|
+
this.afterMove(objects);
|
|
5497
|
+
} finally {
|
|
5498
|
+
this.history.endTransaction();
|
|
5499
|
+
}
|
|
5500
|
+
return true;
|
|
5501
|
+
}
|
|
4718
5502
|
/** Clone a layer (offset slightly) and select the copy. */
|
|
4719
5503
|
async duplicateLayer(id) {
|
|
4720
5504
|
const layer = this.layers.get(id);
|
|
@@ -4739,24 +5523,19 @@ var CanvasEditor = class {
|
|
|
4739
5523
|
this.history.save();
|
|
4740
5524
|
return copy;
|
|
4741
5525
|
}
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
layer.meta.imageAdjustments = next;
|
|
4756
|
-
image.applyFilters();
|
|
4757
|
-
this.canvas.requestRenderAll();
|
|
4758
|
-
this.history.save();
|
|
4759
|
-
return true;
|
|
5526
|
+
/** The drag moved the original; a copy takes the start position, just beneath it. */
|
|
5527
|
+
async leaveCopyBehind(layer, transform) {
|
|
5528
|
+
const now = util7.saveObjectTransform(layer.fabricObject);
|
|
5529
|
+
if (now.left === transform.left && now.top === transform.top) return;
|
|
5530
|
+
await this.history.transaction(async () => {
|
|
5531
|
+
const copy = await this.duplicateLayer(layer.id);
|
|
5532
|
+
if (!copy) return;
|
|
5533
|
+
copy.fabricObject.set(transform);
|
|
5534
|
+
copy.fabricObject.setCoords();
|
|
5535
|
+
this.layers.reorder(copy.id, this.layers.getAll().indexOf(layer));
|
|
5536
|
+
this.layers.select(layer.id);
|
|
5537
|
+
this.canvas.requestRenderAll();
|
|
5538
|
+
});
|
|
4760
5539
|
}
|
|
4761
5540
|
/**
|
|
4762
5541
|
* Combine two or more layers into one group layer. Picking a group among them
|
|
@@ -4825,6 +5604,7 @@ var CanvasEditor = class {
|
|
|
4825
5604
|
await deserializeEditor(this, state);
|
|
4826
5605
|
await this.patterns.rehydrateAll();
|
|
4827
5606
|
this.layerMasks.refreshAll();
|
|
5607
|
+
this.imageEffects.refreshAll();
|
|
4828
5608
|
} catch (error) {
|
|
4829
5609
|
if (!managedByHistory) {
|
|
4830
5610
|
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
@@ -4901,7 +5681,9 @@ var CanvasEditor = class {
|
|
|
4901
5681
|
try {
|
|
4902
5682
|
await this.fonts.ready();
|
|
4903
5683
|
const blob = await this.withDesignBackground(
|
|
4904
|
-
() =>
|
|
5684
|
+
() => this.imageEffects.withSourceResolution(
|
|
5685
|
+
() => exportPNG(this.canvas, { ...options, format })
|
|
5686
|
+
)
|
|
4905
5687
|
);
|
|
4906
5688
|
this.events.emit("export:complete", { format });
|
|
4907
5689
|
this.licensing.track(`export:${format}`);
|
|
@@ -4939,7 +5721,9 @@ var CanvasEditor = class {
|
|
|
4939
5721
|
}
|
|
4940
5722
|
}
|
|
4941
5723
|
toDataURL(format = "png", multiplier = 1) {
|
|
4942
|
-
return this.withDesignBackground(
|
|
5724
|
+
return this.withDesignBackground(
|
|
5725
|
+
() => this.imageEffects.withSourceResolution(() => exportDataURL(this.canvas, format, multiplier))
|
|
5726
|
+
);
|
|
4943
5727
|
}
|
|
4944
5728
|
/**
|
|
4945
5729
|
* Export the print file: the design cropped to the mockup's print area, on
|
|
@@ -4963,7 +5747,9 @@ var CanvasEditor = class {
|
|
|
4963
5747
|
if (!this.mockup) throw new Error("No mockup is configured");
|
|
4964
5748
|
await this.fonts.ready();
|
|
4965
5749
|
try {
|
|
4966
|
-
const blob = await
|
|
5750
|
+
const blob = await this.imageEffects.withSourceResolution(
|
|
5751
|
+
() => exportMockup(this.canvas, this.mockup, options)
|
|
5752
|
+
);
|
|
4967
5753
|
this.licensing.track(`export:mockup:${options.format ?? "png"}`);
|
|
4968
5754
|
return blob;
|
|
4969
5755
|
} catch (error) {
|
|
@@ -5105,10 +5891,10 @@ var CanvasEditor = class {
|
|
|
5105
5891
|
this.imageProvider = provider;
|
|
5106
5892
|
}
|
|
5107
5893
|
// ─── Canvas Operations ──────────────────────────────
|
|
5108
|
-
setBackground(
|
|
5109
|
-
this.designBackground =
|
|
5894
|
+
setBackground(color2) {
|
|
5895
|
+
this.designBackground = color2;
|
|
5110
5896
|
if (!this.mockup) {
|
|
5111
|
-
this.canvas.backgroundColor =
|
|
5897
|
+
this.canvas.backgroundColor = color2;
|
|
5112
5898
|
this.canvas.requestRenderAll();
|
|
5113
5899
|
}
|
|
5114
5900
|
this.history.save();
|
|
@@ -5415,9 +6201,11 @@ var CanvasEditor = class {
|
|
|
5415
6201
|
}
|
|
5416
6202
|
// ─── Cleanup ────────────────────────────────────────
|
|
5417
6203
|
dispose() {
|
|
6204
|
+
this.imageEffects.dispose();
|
|
5418
6205
|
this.masks.dispose();
|
|
5419
6206
|
this.layerMasks.dispose();
|
|
5420
6207
|
this.snapping.dispose();
|
|
6208
|
+
this.shapes.dispose();
|
|
5421
6209
|
this.crop.dispose();
|
|
5422
6210
|
this.history.dispose();
|
|
5423
6211
|
this.patterns.dispose();
|
|
@@ -5451,12 +6239,26 @@ var CanvasEditor = class {
|
|
|
5451
6239
|
const active = this.canvas.getActiveObject();
|
|
5452
6240
|
if (active) applyObjectSelectionStyle(active, this.selectionStyle, this.zoomLevel);
|
|
5453
6241
|
};
|
|
6242
|
+
this.canvas.on("mouse:down", (e) => {
|
|
6243
|
+
this.altDrag = null;
|
|
6244
|
+
const target = e.target;
|
|
6245
|
+
const pointer = e.e;
|
|
6246
|
+
if (!target || !pointer?.altKey || target instanceof ActiveSelection3) return;
|
|
6247
|
+
const layer = this.layers.findByObject(target);
|
|
6248
|
+
if (!layer || layer.locked || layer.renderProxy) return;
|
|
6249
|
+
this.altDrag = { layerId: layer.id, transform: util7.saveObjectTransform(target) };
|
|
6250
|
+
});
|
|
5454
6251
|
this.canvas.on("object:modified", (e) => {
|
|
5455
6252
|
if (!e.target) return;
|
|
5456
6253
|
const layer = this.layers.findByObject(e.target);
|
|
5457
6254
|
if (layer) {
|
|
5458
6255
|
this.events.emit("layer:modified", { layerId: layer.id });
|
|
5459
6256
|
this.history.save();
|
|
6257
|
+
const pending = this.altDrag;
|
|
6258
|
+
this.altDrag = null;
|
|
6259
|
+
if (pending && pending.layerId === layer.id) {
|
|
6260
|
+
void this.leaveCopyBehind(layer, pending.transform);
|
|
6261
|
+
}
|
|
5460
6262
|
}
|
|
5461
6263
|
});
|
|
5462
6264
|
this.canvas.on("selection:created", (e) => {
|
|
@@ -5544,6 +6346,8 @@ var AnnotationOverlay = class {
|
|
|
5544
6346
|
}
|
|
5545
6347
|
};
|
|
5546
6348
|
export {
|
|
6349
|
+
ADJUST_KEYS,
|
|
6350
|
+
ADJUST_RANGES,
|
|
5547
6351
|
AnnotationOverlay,
|
|
5548
6352
|
BLEND_GROUPS,
|
|
5549
6353
|
BLEND_MODES,
|
|
@@ -5559,7 +6363,12 @@ export {
|
|
|
5559
6363
|
EventEmitter,
|
|
5560
6364
|
FontRegistry,
|
|
5561
6365
|
HistoryManager,
|
|
6366
|
+
IMAGE_FILTER_PRESETS,
|
|
6367
|
+
IMAGE_PROXY_MAX_EDGE,
|
|
6368
|
+
ImageEffectsManager,
|
|
6369
|
+
LAYER_EFFECT_DEFAULTS,
|
|
5562
6370
|
Layer,
|
|
6371
|
+
LayerEffectsManager,
|
|
5563
6372
|
LayerManager,
|
|
5564
6373
|
LayerMaskManager,
|
|
5565
6374
|
LicenseManager,
|
|
@@ -5567,10 +6376,12 @@ export {
|
|
|
5567
6376
|
MaskController,
|
|
5568
6377
|
MaskPresetManager,
|
|
5569
6378
|
MaskRefinementError,
|
|
6379
|
+
NEUTRAL_ADJUST,
|
|
5570
6380
|
PatternManager,
|
|
5571
6381
|
ProjectManager,
|
|
5572
6382
|
SHAPE_MASK_BOX,
|
|
5573
6383
|
SHAPE_MASK_IDS,
|
|
6384
|
+
ShapeManager,
|
|
5574
6385
|
SnapManager,
|
|
5575
6386
|
TEXTURE_MASK_IDS,
|
|
5576
6387
|
TEXTURE_MASK_SIZE,
|
|
@@ -5578,6 +6389,8 @@ export {
|
|
|
5578
6389
|
TextWrapManager,
|
|
5579
6390
|
TiledPatternObject,
|
|
5580
6391
|
UnitConverter,
|
|
6392
|
+
alignObjects,
|
|
6393
|
+
alphaClipMatrix,
|
|
5581
6394
|
angleFromCoords,
|
|
5582
6395
|
applyAspectLock,
|
|
5583
6396
|
applyLayerShadow,
|
|
@@ -5590,6 +6403,8 @@ export {
|
|
|
5590
6403
|
buildCurveLinePaths,
|
|
5591
6404
|
buildCurvePathData,
|
|
5592
6405
|
clamp,
|
|
6406
|
+
clampAdjust,
|
|
6407
|
+
clampAlphaClip,
|
|
5593
6408
|
clearTextureMaskCache,
|
|
5594
6409
|
composeMaskGroup,
|
|
5595
6410
|
computeCoverPlacement,
|
|
@@ -5597,6 +6412,7 @@ export {
|
|
|
5597
6412
|
computeTilePositions,
|
|
5598
6413
|
deserializeEditor,
|
|
5599
6414
|
displaceRgba,
|
|
6415
|
+
distributeObjects,
|
|
5600
6416
|
drawTiles,
|
|
5601
6417
|
escapeXml,
|
|
5602
6418
|
explodeTemplateSvg,
|
|
@@ -5608,12 +6424,15 @@ export {
|
|
|
5608
6424
|
fitTextWidth,
|
|
5609
6425
|
fitToBox,
|
|
5610
6426
|
generateId,
|
|
6427
|
+
imageEffectsFilterSpecs,
|
|
6428
|
+
imageFilterPreset,
|
|
5611
6429
|
isCssColor,
|
|
5612
6430
|
isMaskPresetId,
|
|
5613
6431
|
isShapeMaskId,
|
|
5614
6432
|
isTextureMaskId,
|
|
5615
6433
|
linearCoords,
|
|
5616
6434
|
needsAbsoluteSpace,
|
|
6435
|
+
normalizeImageEffects,
|
|
5617
6436
|
normalizeTextCurve,
|
|
5618
6437
|
preWrapWordSplit,
|
|
5619
6438
|
readGradientConfig,
|
|
@@ -5621,6 +6440,7 @@ export {
|
|
|
5621
6440
|
readTextWrap,
|
|
5622
6441
|
renderTextureMask,
|
|
5623
6442
|
resetTransform,
|
|
6443
|
+
resolveShapeParams,
|
|
5624
6444
|
restoreLocks,
|
|
5625
6445
|
round2,
|
|
5626
6446
|
sanitizeSvg,
|
|
@@ -5631,6 +6451,7 @@ export {
|
|
|
5631
6451
|
toCanvasSpace,
|
|
5632
6452
|
toFabricGradient,
|
|
5633
6453
|
toHostSpace,
|
|
6454
|
+
unionBox,
|
|
5634
6455
|
unwrapGroup
|
|
5635
6456
|
};
|
|
5636
6457
|
//# sourceMappingURL=index.mjs.map
|