@overtone-art/canvas-editor-core 0.8.5 → 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 +174 -72
- package/dist/index.d.ts +174 -72
- package/dist/index.global.js +81 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1692 -452
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1218 -226
- 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-BeaEHTFc.d.mts → specs-B_jOmY6u.d.mts} +183 -14
- package/dist/{types-BeaEHTFc.d.ts → specs-B_jOmY6u.d.ts} +183 -14
- 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,7 +2694,18 @@ function waveLinePath(width, fontSize, config, dy) {
|
|
|
1968
2694
|
}
|
|
1969
2695
|
return { data: commands.join(" "), length };
|
|
1970
2696
|
}
|
|
1971
|
-
function
|
|
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
|
+
}
|
|
2708
|
+
function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight = 0) {
|
|
1972
2709
|
const curve = normalizeTextCurve(config);
|
|
1973
2710
|
if (!bendsBaseline(curve)) return null;
|
|
1974
2711
|
const lines = Math.max(1, Math.floor(lineCount));
|
|
@@ -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();
|
|
@@ -3387,6 +4082,123 @@ import { Group as Group6 } from "fabric";
|
|
|
3387
4082
|
|
|
3388
4083
|
// src/masks/compose.ts
|
|
3389
4084
|
import { Group as Group2, Rect as Rect2 } from "fabric";
|
|
4085
|
+
|
|
4086
|
+
// src/gradient.ts
|
|
4087
|
+
import { Color, Gradient } from "fabric";
|
|
4088
|
+
var DEFAULT_GRADIENT_CONFIG = {
|
|
4089
|
+
kind: "linear",
|
|
4090
|
+
angle: 90,
|
|
4091
|
+
center: { x: 0.5, y: 0.5 },
|
|
4092
|
+
radius: 0.5,
|
|
4093
|
+
stops: [
|
|
4094
|
+
{ color: "#ffffff", opacity: 1, position: 0 },
|
|
4095
|
+
{ color: "#000000", opacity: 1, position: 1 }
|
|
4096
|
+
]
|
|
4097
|
+
};
|
|
4098
|
+
var MIN_RADIUS = 1e-3;
|
|
4099
|
+
var MAX_GRADIENT_STOPS = 256;
|
|
4100
|
+
var FALLBACK_STOP_COLOR = DEFAULT_GRADIENT_CONFIG.stops[0].color;
|
|
4101
|
+
function linearCoords(angle, box) {
|
|
4102
|
+
const radians = angle * Math.PI / 180;
|
|
4103
|
+
const sin = Math.sin(radians);
|
|
4104
|
+
const cos = Math.cos(radians);
|
|
4105
|
+
const vx = box.width * sin;
|
|
4106
|
+
const vy = -box.height * cos;
|
|
4107
|
+
const length = Math.abs(box.width * sin) + Math.abs(box.height * cos);
|
|
4108
|
+
const scale = length / (vx * vx + vy * vy || 1);
|
|
4109
|
+
return {
|
|
4110
|
+
x1: 0.5 - scale * vx / 2,
|
|
4111
|
+
y1: 0.5 - scale * vy / 2,
|
|
4112
|
+
x2: 0.5 + scale * vx / 2,
|
|
4113
|
+
y2: 0.5 + scale * vy / 2
|
|
4114
|
+
};
|
|
4115
|
+
}
|
|
4116
|
+
function angleFromCoords(coords, box) {
|
|
4117
|
+
const dx = (coords.x2 - coords.x1) / (box.width || 1);
|
|
4118
|
+
const dy = (coords.y2 - coords.y1) / (box.height || 1);
|
|
4119
|
+
const degrees = Math.atan2(dx, -dy) * 180 / Math.PI;
|
|
4120
|
+
return (degrees + 360) % 360;
|
|
4121
|
+
}
|
|
4122
|
+
function stopColor(stop) {
|
|
4123
|
+
const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
|
|
4124
|
+
const color2 = new Color(source);
|
|
4125
|
+
const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
|
|
4126
|
+
color2.setAlpha(clamp(opacity, 0, 1));
|
|
4127
|
+
return color2.toRgba();
|
|
4128
|
+
}
|
|
4129
|
+
function toFabricGradient(config, box) {
|
|
4130
|
+
const colorStops = [...config.stops].sort((a, b) => a.position - b.position).slice(0, MAX_GRADIENT_STOPS).map((stop) => {
|
|
4131
|
+
const position = Number.isFinite(stop.position) ? stop.position : 0;
|
|
4132
|
+
return { offset: clamp(position, 0, 1), color: stopColor(stop) };
|
|
4133
|
+
});
|
|
4134
|
+
if (config.kind === "radial") {
|
|
4135
|
+
const radius = Math.max(MIN_RADIUS, config.radius);
|
|
4136
|
+
return new Gradient({
|
|
4137
|
+
type: "radial",
|
|
4138
|
+
gradientUnits: "percentage",
|
|
4139
|
+
coords: {
|
|
4140
|
+
x1: config.center.x,
|
|
4141
|
+
y1: config.center.y,
|
|
4142
|
+
r1: 0,
|
|
4143
|
+
x2: config.center.x,
|
|
4144
|
+
y2: config.center.y,
|
|
4145
|
+
r2: radius
|
|
4146
|
+
},
|
|
4147
|
+
colorStops
|
|
4148
|
+
});
|
|
4149
|
+
}
|
|
4150
|
+
return new Gradient({
|
|
4151
|
+
type: "linear",
|
|
4152
|
+
gradientUnits: "percentage",
|
|
4153
|
+
coords: linearCoords(config.angle, box),
|
|
4154
|
+
colorStops
|
|
4155
|
+
});
|
|
4156
|
+
}
|
|
4157
|
+
function toFabricMaskGradient(config, box) {
|
|
4158
|
+
const gradient = toFabricGradient(config, box);
|
|
4159
|
+
gradient.colorStops = gradient.colorStops.map((stop) => {
|
|
4160
|
+
const color2 = new Color(stop.color);
|
|
4161
|
+
const [red, green, blue] = color2.getSource();
|
|
4162
|
+
const luminance = (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255;
|
|
4163
|
+
color2.setSource([255, 255, 255, clamp(luminance * color2.getAlpha(), 0, 1)]);
|
|
4164
|
+
return { offset: stop.offset, color: color2.toRgba() };
|
|
4165
|
+
});
|
|
4166
|
+
return gradient;
|
|
4167
|
+
}
|
|
4168
|
+
function readGradientConfig(object) {
|
|
4169
|
+
const fill = object.fill;
|
|
4170
|
+
if (!fill || typeof fill === "string" || !(fill instanceof Gradient)) return null;
|
|
4171
|
+
const stops = fill.colorStops.map((stop) => {
|
|
4172
|
+
const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
|
|
4173
|
+
const color2 = new Color(source);
|
|
4174
|
+
const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
|
|
4175
|
+
return {
|
|
4176
|
+
color: `#${color2.toHex().toLowerCase()}`,
|
|
4177
|
+
opacity: color2.getAlpha(),
|
|
4178
|
+
position: clamp(offset, 0, 1)
|
|
4179
|
+
};
|
|
4180
|
+
});
|
|
4181
|
+
if (fill.type === "radial") {
|
|
4182
|
+
const { x1, y1, r2 } = fill.coords;
|
|
4183
|
+
return {
|
|
4184
|
+
kind: "radial",
|
|
4185
|
+
angle: DEFAULT_GRADIENT_CONFIG.angle,
|
|
4186
|
+
center: { x: x1, y: y1 },
|
|
4187
|
+
radius: r2,
|
|
4188
|
+
stops
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
const box = { width: object.width || 1, height: object.height || 1 };
|
|
4192
|
+
return {
|
|
4193
|
+
kind: "linear",
|
|
4194
|
+
angle: angleFromCoords(fill.coords, box),
|
|
4195
|
+
center: { ...DEFAULT_GRADIENT_CONFIG.center },
|
|
4196
|
+
radius: DEFAULT_GRADIENT_CONFIG.radius,
|
|
4197
|
+
stops
|
|
4198
|
+
};
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4201
|
+
// src/masks/compose.ts
|
|
3390
4202
|
var MODE_OPERATION = {
|
|
3391
4203
|
add: "source-over",
|
|
3392
4204
|
subtract: "destination-out",
|
|
@@ -3416,6 +4228,14 @@ function composeMaskGroup(children, entries, options) {
|
|
|
3416
4228
|
neutralize(child);
|
|
3417
4229
|
return;
|
|
3418
4230
|
}
|
|
4231
|
+
if (entry.gradient) {
|
|
4232
|
+
child.set({
|
|
4233
|
+
fill: toFabricMaskGradient(entry.gradient, {
|
|
4234
|
+
width: Math.max(1, child.width ?? 1),
|
|
4235
|
+
height: Math.max(1, child.height ?? 1)
|
|
4236
|
+
})
|
|
4237
|
+
});
|
|
4238
|
+
}
|
|
3419
4239
|
child.set({
|
|
3420
4240
|
opacity: entry.opacity,
|
|
3421
4241
|
globalCompositeOperation: MODE_OPERATION[entry.mode] ?? "source-over"
|
|
@@ -3437,11 +4257,105 @@ function needsAbsoluteSpace(entries) {
|
|
|
3437
4257
|
}
|
|
3438
4258
|
|
|
3439
4259
|
// src/masks/edit.ts
|
|
3440
|
-
import { util as
|
|
4260
|
+
import { util as util6 } from "fabric";
|
|
4261
|
+
|
|
4262
|
+
// src/masks/edit-viewport.ts
|
|
4263
|
+
var MIN_PADDING = 48;
|
|
4264
|
+
var MAX_PADDING = 512;
|
|
4265
|
+
var PADDING_FRACTION = 0.35;
|
|
4266
|
+
var OUTSIDE_SHADE = "rgba(15, 15, 15, 0.55)";
|
|
4267
|
+
var MaskEditViewport = class {
|
|
4268
|
+
constructor(canvas) {
|
|
4269
|
+
this.canvas = canvas;
|
|
4270
|
+
}
|
|
4271
|
+
canvas;
|
|
4272
|
+
state = null;
|
|
4273
|
+
open(handle) {
|
|
4274
|
+
if (this.state) return;
|
|
4275
|
+
const width = this.canvas.getWidth();
|
|
4276
|
+
const height = this.canvas.getHeight();
|
|
4277
|
+
const base = Math.min(
|
|
4278
|
+
MAX_PADDING,
|
|
4279
|
+
Math.max(MIN_PADDING, Math.max(width, height) * PADDING_FRACTION)
|
|
4280
|
+
);
|
|
4281
|
+
handle.setCoords();
|
|
4282
|
+
const bounds = handle.getBoundingRect();
|
|
4283
|
+
const left = Math.min(MAX_PADDING, Math.max(base, -bounds.left + MIN_PADDING));
|
|
4284
|
+
const top = Math.min(MAX_PADDING, Math.max(base, -bounds.top + MIN_PADDING));
|
|
4285
|
+
const right = Math.min(
|
|
4286
|
+
MAX_PADDING,
|
|
4287
|
+
Math.max(base, bounds.left + bounds.width - width + MIN_PADDING)
|
|
4288
|
+
);
|
|
4289
|
+
const bottom = Math.min(
|
|
4290
|
+
MAX_PADDING,
|
|
4291
|
+
Math.max(base, bounds.top + bounds.height - height + MIN_PADDING)
|
|
4292
|
+
);
|
|
4293
|
+
const viewport = [...this.canvas.viewportTransform];
|
|
4294
|
+
const wrapper = this.canvas.wrapperEl;
|
|
4295
|
+
this.state = {
|
|
4296
|
+
width,
|
|
4297
|
+
height,
|
|
4298
|
+
left,
|
|
4299
|
+
top,
|
|
4300
|
+
viewport,
|
|
4301
|
+
wrapperCss: wrapper.style.cssText
|
|
4302
|
+
};
|
|
4303
|
+
this.canvas.setDimensions({ width: width + left + right, height: height + top + bottom });
|
|
4304
|
+
const paddedViewport = [...viewport];
|
|
4305
|
+
paddedViewport[4] += left;
|
|
4306
|
+
paddedViewport[5] += top;
|
|
4307
|
+
this.canvas.setViewportTransform(paddedViewport);
|
|
4308
|
+
wrapper.style.position = "absolute";
|
|
4309
|
+
wrapper.style.left = `${-left}px`;
|
|
4310
|
+
wrapper.style.top = `${-top}px`;
|
|
4311
|
+
wrapper.style.zIndex = "1";
|
|
4312
|
+
this.canvas.on("after:render", this.onAfterRender);
|
|
4313
|
+
this.canvas.calcOffset();
|
|
4314
|
+
this.canvas.requestRenderAll();
|
|
4315
|
+
}
|
|
4316
|
+
close() {
|
|
4317
|
+
const state = this.state;
|
|
4318
|
+
if (!state) return;
|
|
4319
|
+
this.canvas.off("after:render", this.onAfterRender);
|
|
4320
|
+
this.canvas.setViewportTransform(state.viewport);
|
|
4321
|
+
this.canvas.setDimensions({ width: state.width, height: state.height });
|
|
4322
|
+
this.canvas.wrapperEl.style.cssText = state.wrapperCss;
|
|
4323
|
+
this.canvas.calcOffset();
|
|
4324
|
+
this.state = null;
|
|
4325
|
+
}
|
|
4326
|
+
/** Save with real document dimensions, then reopen the same editing view. */
|
|
4327
|
+
checkpoint(handle, save) {
|
|
4328
|
+
this.close();
|
|
4329
|
+
try {
|
|
4330
|
+
save();
|
|
4331
|
+
} finally {
|
|
4332
|
+
this.open(handle);
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
onAfterRender = ({ ctx }) => {
|
|
4336
|
+
const state = this.state;
|
|
4337
|
+
if (!state || ctx !== this.canvas.getContext()) return;
|
|
4338
|
+
const fullWidth = this.canvas.getWidth();
|
|
4339
|
+
const fullHeight = this.canvas.getHeight();
|
|
4340
|
+
const right = state.left + state.width;
|
|
4341
|
+
const bottom = state.top + state.height;
|
|
4342
|
+
ctx.save();
|
|
4343
|
+
ctx.fillStyle = OUTSIDE_SHADE;
|
|
4344
|
+
ctx.fillRect(0, 0, fullWidth, state.top);
|
|
4345
|
+
ctx.fillRect(0, bottom, fullWidth, fullHeight - bottom);
|
|
4346
|
+
ctx.fillRect(0, state.top, state.left, state.height);
|
|
4347
|
+
ctx.fillRect(right, state.top, fullWidth - right, state.height);
|
|
4348
|
+
ctx.restore();
|
|
4349
|
+
this.canvas.drawControls(ctx);
|
|
4350
|
+
};
|
|
4351
|
+
};
|
|
4352
|
+
|
|
4353
|
+
// src/masks/edit.ts
|
|
3441
4354
|
var MaskEditController = class {
|
|
3442
4355
|
constructor(canvas, onCommit) {
|
|
3443
4356
|
this.canvas = canvas;
|
|
3444
4357
|
this.onCommit = onCommit;
|
|
4358
|
+
this.viewport = new MaskEditViewport(canvas);
|
|
3445
4359
|
}
|
|
3446
4360
|
canvas;
|
|
3447
4361
|
onCommit;
|
|
@@ -3449,6 +4363,7 @@ var MaskEditController = class {
|
|
|
3449
4363
|
editing = null;
|
|
3450
4364
|
child = null;
|
|
3451
4365
|
group = null;
|
|
4366
|
+
viewport;
|
|
3452
4367
|
active() {
|
|
3453
4368
|
return this.editing;
|
|
3454
4369
|
}
|
|
@@ -3479,6 +4394,7 @@ var MaskEditController = class {
|
|
|
3479
4394
|
this.group = group;
|
|
3480
4395
|
this.canvas.add(handle);
|
|
3481
4396
|
this.canvas.setActiveObject(handle);
|
|
4397
|
+
this.viewport.open(handle);
|
|
3482
4398
|
this.canvas.on("object:moving", this.onTransform);
|
|
3483
4399
|
this.canvas.on("object:scaling", this.onTransform);
|
|
3484
4400
|
this.canvas.on("object:rotating", this.onTransform);
|
|
@@ -3493,6 +4409,7 @@ var MaskEditController = class {
|
|
|
3493
4409
|
this.canvas.off("object:scaling", this.onTransform);
|
|
3494
4410
|
this.canvas.off("object:rotating", this.onTransform);
|
|
3495
4411
|
this.canvas.off("object:modified", this.onModified);
|
|
4412
|
+
this.viewport.close();
|
|
3496
4413
|
if (this.canvas.getActiveObject() === this.handle) this.canvas.discardActiveObject();
|
|
3497
4414
|
this.canvas.remove(this.handle);
|
|
3498
4415
|
this.handle = null;
|
|
@@ -3511,15 +4428,15 @@ var MaskEditController = class {
|
|
|
3511
4428
|
onModified = (event) => {
|
|
3512
4429
|
if (!this.handle || event.target !== this.handle) return;
|
|
3513
4430
|
this.write();
|
|
3514
|
-
this.onCommit
|
|
4431
|
+
this.viewport.checkpoint(this.handle, this.onCommit);
|
|
3515
4432
|
};
|
|
3516
4433
|
/** Handle transform (canvas space) → clip child transform (group-relative). */
|
|
3517
4434
|
write() {
|
|
3518
4435
|
if (!this.handle || !this.child || !this.group) return;
|
|
3519
4436
|
applyMatrix(
|
|
3520
4437
|
this.child,
|
|
3521
|
-
|
|
3522
|
-
|
|
4438
|
+
util6.multiplyTransformMatrices(
|
|
4439
|
+
util6.invertTransform(matrixOf(this.group)),
|
|
3523
4440
|
matrixOf(this.handle)
|
|
3524
4441
|
)
|
|
3525
4442
|
);
|
|
@@ -3530,7 +4447,7 @@ var MaskEditController = class {
|
|
|
3530
4447
|
};
|
|
3531
4448
|
|
|
3532
4449
|
// src/masks/store.ts
|
|
3533
|
-
import { Group as Group5 } from "fabric";
|
|
4450
|
+
import { Gradient as Gradient2, Group as Group5 } from "fabric";
|
|
3534
4451
|
|
|
3535
4452
|
// src/masks/host.ts
|
|
3536
4453
|
import { Rect as Rect3 } from "fabric";
|
|
@@ -3642,6 +4559,29 @@ var MaskStackStore = class {
|
|
|
3642
4559
|
get(target, maskId) {
|
|
3643
4560
|
return this.list(target).find((entry) => entry.id === maskId);
|
|
3644
4561
|
}
|
|
4562
|
+
/** Repaint a gradient mask without replacing or re-fitting its geometry. */
|
|
4563
|
+
setGradient(target, maskId, gradient, options = {}) {
|
|
4564
|
+
const host = this.host(target);
|
|
4565
|
+
if (!host) return false;
|
|
4566
|
+
const entries = [...this.list(target)];
|
|
4567
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
4568
|
+
if (index === -1) return false;
|
|
4569
|
+
const sources = this.unwrap(target, host);
|
|
4570
|
+
const source = sources[index];
|
|
4571
|
+
if (!source) return false;
|
|
4572
|
+
source.set({
|
|
4573
|
+
fill: toFabricGradient(gradient, {
|
|
4574
|
+
width: Math.max(1, source.width ?? 1),
|
|
4575
|
+
height: Math.max(1, source.height ?? 1)
|
|
4576
|
+
})
|
|
4577
|
+
});
|
|
4578
|
+
entries[index] = {
|
|
4579
|
+
...entries[index],
|
|
4580
|
+
...options.meta ? { meta: options.meta } : {}
|
|
4581
|
+
};
|
|
4582
|
+
this.commit(target, host, entries, sources, options.save ?? true);
|
|
4583
|
+
return true;
|
|
4584
|
+
}
|
|
3645
4585
|
/** Every target that currently carries at least one mask. */
|
|
3646
4586
|
targets() {
|
|
3647
4587
|
return this.layers.getAll().filter((layer) => (layer.meta.maskStack ?? []).length > 0).map((layer) => layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
|
|
@@ -3705,11 +4645,11 @@ var MaskStackStore = class {
|
|
|
3705
4645
|
if (entries.length === 0 || !(clip instanceof Group5)) {
|
|
3706
4646
|
const source = asObject(clip);
|
|
3707
4647
|
if (meta?.overflow === "hidden") source.clipPath = void 0;
|
|
3708
|
-
return [source];
|
|
4648
|
+
return restoreGradientPaint(entries, [source]);
|
|
3709
4649
|
}
|
|
3710
4650
|
const children = unwrapGroup(clip);
|
|
3711
4651
|
const extra = children.length - entries.length;
|
|
3712
|
-
return extra > 0 ? children.slice(extra) : children;
|
|
4652
|
+
return restoreGradientPaint(entries, extra > 0 ? children.slice(extra) : children);
|
|
3713
4653
|
}
|
|
3714
4654
|
/**
|
|
3715
4655
|
* Entries for a stack, adopting a pre-stack clip as the first one. Without
|
|
@@ -3742,7 +4682,8 @@ var MaskStackStore = class {
|
|
|
3742
4682
|
if (!layer) return;
|
|
3743
4683
|
const absolute = forceAbsolute || target === CANVAS_MASK_TARGET || needsAbsoluteSpace(entries);
|
|
3744
4684
|
convertSpace(host, sources, absolute);
|
|
3745
|
-
const
|
|
4685
|
+
const painted = captureGradientPaint(entries, sources);
|
|
4686
|
+
const next = withRelativeTransforms(painted, sources, host, absolute);
|
|
3746
4687
|
installClip(host, next, sources, this.hostBox(target, absolute), absolute);
|
|
3747
4688
|
if (next.length === 0) {
|
|
3748
4689
|
delete layer.meta.maskStack;
|
|
@@ -3756,12 +4697,41 @@ var MaskStackStore = class {
|
|
|
3756
4697
|
if (save) this.history.save();
|
|
3757
4698
|
}
|
|
3758
4699
|
};
|
|
4700
|
+
function restoreGradientPaint(entries, sources) {
|
|
4701
|
+
entries.forEach((entry, index) => {
|
|
4702
|
+
const source = sources[index];
|
|
4703
|
+
if (!source || !entry.gradient) return;
|
|
4704
|
+
const box = {
|
|
4705
|
+
width: Math.max(1, source.width ?? 1),
|
|
4706
|
+
height: Math.max(1, source.height ?? 1)
|
|
4707
|
+
};
|
|
4708
|
+
const fill = source.fill;
|
|
4709
|
+
const rendered = toFabricMaskGradient(entry.gradient, box);
|
|
4710
|
+
if (fill instanceof Gradient2 && JSON.stringify(fill.toObject()) !== JSON.stringify(rendered.toObject())) {
|
|
4711
|
+
return;
|
|
4712
|
+
}
|
|
4713
|
+
source.set({
|
|
4714
|
+
fill: toFabricGradient(entry.gradient, box)
|
|
4715
|
+
});
|
|
4716
|
+
});
|
|
4717
|
+
return sources;
|
|
4718
|
+
}
|
|
4719
|
+
function captureGradientPaint(entries, sources) {
|
|
4720
|
+
return entries.map((entry, index) => {
|
|
4721
|
+
const gradient = sources[index] ? readGradientConfig(sources[index]) : null;
|
|
4722
|
+
if (gradient) return { ...entry, gradient };
|
|
4723
|
+
if (!entry.gradient) return entry;
|
|
4724
|
+
const next = { ...entry };
|
|
4725
|
+
delete next.gradient;
|
|
4726
|
+
return next;
|
|
4727
|
+
});
|
|
4728
|
+
}
|
|
3759
4729
|
|
|
3760
4730
|
// src/masks/manager.ts
|
|
3761
4731
|
var LayerMaskManager = class extends MaskStackStore {
|
|
3762
4732
|
// Committing mid-drag would recompose the group the handle writes into and
|
|
3763
4733
|
// leave it pointing at a discarded object; the geometry is settled on endEdit.
|
|
3764
|
-
edits = new MaskEditController(this.canvas, () => this.history.
|
|
4734
|
+
edits = new MaskEditController(this.canvas, () => this.history.saveImmediate());
|
|
3765
4735
|
onLayersChanged = () => this.pin();
|
|
3766
4736
|
// Selecting a layer means the user has moved on from the mask they had open;
|
|
3767
4737
|
// leaving both selected would show mask controls for an unrelated layer.
|
|
@@ -4148,110 +5118,6 @@ function layerTypeOf(object) {
|
|
|
4148
5118
|
return "shape";
|
|
4149
5119
|
}
|
|
4150
5120
|
|
|
4151
|
-
// src/gradient.ts
|
|
4152
|
-
import { Color, Gradient } from "fabric";
|
|
4153
|
-
var DEFAULT_GRADIENT_CONFIG = {
|
|
4154
|
-
kind: "linear",
|
|
4155
|
-
angle: 90,
|
|
4156
|
-
center: { x: 0.5, y: 0.5 },
|
|
4157
|
-
radius: 0.5,
|
|
4158
|
-
stops: [
|
|
4159
|
-
{ color: "#ffffff", opacity: 1, position: 0 },
|
|
4160
|
-
{ color: "#000000", opacity: 1, position: 1 }
|
|
4161
|
-
]
|
|
4162
|
-
};
|
|
4163
|
-
var MIN_RADIUS = 1e-3;
|
|
4164
|
-
var MAX_GRADIENT_STOPS = 256;
|
|
4165
|
-
var FALLBACK_STOP_COLOR = DEFAULT_GRADIENT_CONFIG.stops[0].color;
|
|
4166
|
-
function linearCoords(angle, box) {
|
|
4167
|
-
const radians = angle * Math.PI / 180;
|
|
4168
|
-
const sin = Math.sin(radians);
|
|
4169
|
-
const cos = Math.cos(radians);
|
|
4170
|
-
const vx = box.width * sin;
|
|
4171
|
-
const vy = -box.height * cos;
|
|
4172
|
-
const length = Math.abs(box.width * sin) + Math.abs(box.height * cos);
|
|
4173
|
-
const scale = length / (vx * vx + vy * vy || 1);
|
|
4174
|
-
return {
|
|
4175
|
-
x1: 0.5 - scale * vx / 2,
|
|
4176
|
-
y1: 0.5 - scale * vy / 2,
|
|
4177
|
-
x2: 0.5 + scale * vx / 2,
|
|
4178
|
-
y2: 0.5 + scale * vy / 2
|
|
4179
|
-
};
|
|
4180
|
-
}
|
|
4181
|
-
function angleFromCoords(coords, box) {
|
|
4182
|
-
const dx = (coords.x2 - coords.x1) / (box.width || 1);
|
|
4183
|
-
const dy = (coords.y2 - coords.y1) / (box.height || 1);
|
|
4184
|
-
const degrees = Math.atan2(dx, -dy) * 180 / Math.PI;
|
|
4185
|
-
return (degrees + 360) % 360;
|
|
4186
|
-
}
|
|
4187
|
-
function stopColor(stop) {
|
|
4188
|
-
const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
|
|
4189
|
-
const color = new Color(source);
|
|
4190
|
-
const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
|
|
4191
|
-
color.setAlpha(clamp(opacity, 0, 1));
|
|
4192
|
-
return color.toRgba();
|
|
4193
|
-
}
|
|
4194
|
-
function toFabricGradient(config, box) {
|
|
4195
|
-
const colorStops = [...config.stops].sort((a, b) => a.position - b.position).slice(0, MAX_GRADIENT_STOPS).map((stop) => {
|
|
4196
|
-
const position = Number.isFinite(stop.position) ? stop.position : 0;
|
|
4197
|
-
return { offset: clamp(position, 0, 1), color: stopColor(stop) };
|
|
4198
|
-
});
|
|
4199
|
-
if (config.kind === "radial") {
|
|
4200
|
-
const radius = Math.max(MIN_RADIUS, config.radius);
|
|
4201
|
-
return new Gradient({
|
|
4202
|
-
type: "radial",
|
|
4203
|
-
gradientUnits: "percentage",
|
|
4204
|
-
coords: {
|
|
4205
|
-
x1: config.center.x,
|
|
4206
|
-
y1: config.center.y,
|
|
4207
|
-
r1: 0,
|
|
4208
|
-
x2: config.center.x,
|
|
4209
|
-
y2: config.center.y,
|
|
4210
|
-
r2: radius
|
|
4211
|
-
},
|
|
4212
|
-
colorStops
|
|
4213
|
-
});
|
|
4214
|
-
}
|
|
4215
|
-
return new Gradient({
|
|
4216
|
-
type: "linear",
|
|
4217
|
-
gradientUnits: "percentage",
|
|
4218
|
-
coords: linearCoords(config.angle, box),
|
|
4219
|
-
colorStops
|
|
4220
|
-
});
|
|
4221
|
-
}
|
|
4222
|
-
function readGradientConfig(object) {
|
|
4223
|
-
const fill = object.fill;
|
|
4224
|
-
if (!fill || typeof fill === "string" || !(fill instanceof Gradient)) return null;
|
|
4225
|
-
const stops = fill.colorStops.map((stop) => {
|
|
4226
|
-
const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
|
|
4227
|
-
const color = new Color(source);
|
|
4228
|
-
const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
|
|
4229
|
-
return {
|
|
4230
|
-
color: `#${color.toHex().toLowerCase()}`,
|
|
4231
|
-
opacity: color.getAlpha(),
|
|
4232
|
-
position: clamp(offset, 0, 1)
|
|
4233
|
-
};
|
|
4234
|
-
});
|
|
4235
|
-
if (fill.type === "radial") {
|
|
4236
|
-
const { x1, y1, r2 } = fill.coords;
|
|
4237
|
-
return {
|
|
4238
|
-
kind: "radial",
|
|
4239
|
-
angle: DEFAULT_GRADIENT_CONFIG.angle,
|
|
4240
|
-
center: { x: x1, y: y1 },
|
|
4241
|
-
radius: r2,
|
|
4242
|
-
stops
|
|
4243
|
-
};
|
|
4244
|
-
}
|
|
4245
|
-
const box = { width: object.width || 1, height: object.height || 1 };
|
|
4246
|
-
return {
|
|
4247
|
-
kind: "linear",
|
|
4248
|
-
angle: angleFromCoords(fill.coords, box),
|
|
4249
|
-
center: { ...DEFAULT_GRADIENT_CONFIG.center },
|
|
4250
|
-
radius: DEFAULT_GRADIENT_CONFIG.radius,
|
|
4251
|
-
stops
|
|
4252
|
-
};
|
|
4253
|
-
}
|
|
4254
|
-
|
|
4255
5121
|
// src/editor.ts
|
|
4256
5122
|
var MIN_ZOOM = 0.01;
|
|
4257
5123
|
var MAX_ZOOM = 8;
|
|
@@ -4267,11 +5133,17 @@ var CanvasEditor = class {
|
|
|
4267
5133
|
snapping;
|
|
4268
5134
|
crop;
|
|
4269
5135
|
patterns;
|
|
5136
|
+
/** Parametric shape layers: `meta.shape` and in-place regeneration. */
|
|
5137
|
+
shapes;
|
|
4270
5138
|
curves;
|
|
4271
5139
|
wrap;
|
|
4272
5140
|
maskPresets;
|
|
5141
|
+
/** Owner of every image layer's filter array (D5). */
|
|
5142
|
+
imageEffects;
|
|
4273
5143
|
/** Stacked boolean masks, per layer and for the design as a whole. */
|
|
4274
5144
|
layerMasks;
|
|
5145
|
+
/** One effect per text or shape layer (D6). */
|
|
5146
|
+
effects;
|
|
4275
5147
|
fonts;
|
|
4276
5148
|
licensing;
|
|
4277
5149
|
pages;
|
|
@@ -4281,6 +5153,8 @@ var CanvasEditor = class {
|
|
|
4281
5153
|
zoomLevel = 1;
|
|
4282
5154
|
selectionStyle = { ...DEFAULT_SELECTION_STYLE };
|
|
4283
5155
|
mockup = null;
|
|
5156
|
+
/** Alt was held when a drag began on this layer; the drop leaves a copy behind. */
|
|
5157
|
+
altDrag = null;
|
|
4284
5158
|
// The design's configured background. The live canvas background is forced
|
|
4285
5159
|
// transparent while a mockup preview is shown, so this is the source of truth
|
|
4286
5160
|
// for serialization and export — not the (possibly transient) canvas value.
|
|
@@ -4288,6 +5162,7 @@ var CanvasEditor = class {
|
|
|
4288
5162
|
designBackgroundImage = null;
|
|
4289
5163
|
backgroundImageOptions = null;
|
|
4290
5164
|
constructor(canvasElement, config) {
|
|
5165
|
+
registerEffects(fabricModule);
|
|
4291
5166
|
this.events = new EventEmitter();
|
|
4292
5167
|
this.fonts = new FontRegistry();
|
|
4293
5168
|
config.fonts?.forEach((font) => this.fonts.register(font));
|
|
@@ -4322,6 +5197,15 @@ var CanvasEditor = class {
|
|
|
4322
5197
|
this.wrap = new TextWrapManager(this.canvas, this.layers, this.history, this.events);
|
|
4323
5198
|
this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
|
|
4324
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));
|
|
4325
5209
|
this.setupCanvasEvents();
|
|
4326
5210
|
this.refreshSelectionStyle();
|
|
4327
5211
|
this.history.saveImmediate();
|
|
@@ -4346,7 +5230,7 @@ var CanvasEditor = class {
|
|
|
4346
5230
|
}
|
|
4347
5231
|
}
|
|
4348
5232
|
/** Replace an image source without changing its layer identity or visual transform. */
|
|
4349
|
-
async replaceImageSource(layerId, url) {
|
|
5233
|
+
async replaceImageSource(layerId, url, options = {}) {
|
|
4350
5234
|
const layer = this.layers.get(layerId);
|
|
4351
5235
|
if (!layer || layer.type !== "image" && layer.type !== "mask") {
|
|
4352
5236
|
throw new Error(`Image or mask layer not found: ${layerId}`);
|
|
@@ -4357,7 +5241,11 @@ var CanvasEditor = class {
|
|
|
4357
5241
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
4358
5242
|
const previous = layer.fabricObject;
|
|
4359
5243
|
try {
|
|
4360
|
-
const replacement = await FabricImage3.fromURL(
|
|
5244
|
+
const replacement = await FabricImage3.fromURL(
|
|
5245
|
+
url,
|
|
5246
|
+
{ crossOrigin: options.crossOrigin },
|
|
5247
|
+
{ originX: "left", originY: "top" }
|
|
5248
|
+
);
|
|
4361
5249
|
replacement.set({
|
|
4362
5250
|
left: previous.left,
|
|
4363
5251
|
top: previous.top,
|
|
@@ -4379,14 +5267,28 @@ var CanvasEditor = class {
|
|
|
4379
5267
|
replacement.applyFilters();
|
|
4380
5268
|
replacement.setCoords();
|
|
4381
5269
|
this.layers.replaceObject(layerId, replacement);
|
|
5270
|
+
this.imageEffects.refresh(layerId);
|
|
4382
5271
|
return layer;
|
|
4383
5272
|
} catch (error) {
|
|
4384
5273
|
this.events.emit("error", { message: "Failed to replace image source", error });
|
|
4385
5274
|
throw error;
|
|
4386
5275
|
}
|
|
4387
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
|
+
}
|
|
4388
5289
|
addText(text, options) {
|
|
4389
|
-
const
|
|
5290
|
+
const TextboxClass = this.registered("Textbox", Textbox2);
|
|
5291
|
+
const textbox = new TextboxClass(text, {
|
|
4390
5292
|
fontSize: 32,
|
|
4391
5293
|
fontFamily: "Arial",
|
|
4392
5294
|
fill: "#000000",
|
|
@@ -4404,6 +5306,15 @@ var CanvasEditor = class {
|
|
|
4404
5306
|
addShape(plugin, options) {
|
|
4405
5307
|
const obj = plugin.create(options);
|
|
4406
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
|
+
}
|
|
4407
5318
|
this.layers.select(layer.id);
|
|
4408
5319
|
this.history.save();
|
|
4409
5320
|
return layer;
|
|
@@ -4415,7 +5326,8 @@ var CanvasEditor = class {
|
|
|
4415
5326
|
addGradient(config = DEFAULT_GRADIENT_CONFIG, box) {
|
|
4416
5327
|
const width = box?.width ?? this.canvas.getWidth();
|
|
4417
5328
|
const height = box?.height ?? this.canvas.getHeight();
|
|
4418
|
-
const
|
|
5329
|
+
const RectClass = this.registered("Rect", Rect4);
|
|
5330
|
+
const object = new RectClass({
|
|
4419
5331
|
left: box?.left ?? 0,
|
|
4420
5332
|
top: box?.top ?? 0,
|
|
4421
5333
|
originX: "left",
|
|
@@ -4544,6 +5456,49 @@ var CanvasEditor = class {
|
|
|
4544
5456
|
this.canvas.requestRenderAll();
|
|
4545
5457
|
this.history.save();
|
|
4546
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
|
+
}
|
|
4547
5502
|
/** Clone a layer (offset slightly) and select the copy. */
|
|
4548
5503
|
async duplicateLayer(id) {
|
|
4549
5504
|
const layer = this.layers.get(id);
|
|
@@ -4568,24 +5523,19 @@ var CanvasEditor = class {
|
|
|
4568
5523
|
this.history.save();
|
|
4569
5524
|
return copy;
|
|
4570
5525
|
}
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
layer.meta.imageAdjustments = next;
|
|
4585
|
-
image.applyFilters();
|
|
4586
|
-
this.canvas.requestRenderAll();
|
|
4587
|
-
this.history.save();
|
|
4588
|
-
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
|
+
});
|
|
4589
5539
|
}
|
|
4590
5540
|
/**
|
|
4591
5541
|
* Combine two or more layers into one group layer. Picking a group among them
|
|
@@ -4654,6 +5604,7 @@ var CanvasEditor = class {
|
|
|
4654
5604
|
await deserializeEditor(this, state);
|
|
4655
5605
|
await this.patterns.rehydrateAll();
|
|
4656
5606
|
this.layerMasks.refreshAll();
|
|
5607
|
+
this.imageEffects.refreshAll();
|
|
4657
5608
|
} catch (error) {
|
|
4658
5609
|
if (!managedByHistory) {
|
|
4659
5610
|
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
@@ -4730,7 +5681,9 @@ var CanvasEditor = class {
|
|
|
4730
5681
|
try {
|
|
4731
5682
|
await this.fonts.ready();
|
|
4732
5683
|
const blob = await this.withDesignBackground(
|
|
4733
|
-
() =>
|
|
5684
|
+
() => this.imageEffects.withSourceResolution(
|
|
5685
|
+
() => exportPNG(this.canvas, { ...options, format })
|
|
5686
|
+
)
|
|
4734
5687
|
);
|
|
4735
5688
|
this.events.emit("export:complete", { format });
|
|
4736
5689
|
this.licensing.track(`export:${format}`);
|
|
@@ -4768,7 +5721,9 @@ var CanvasEditor = class {
|
|
|
4768
5721
|
}
|
|
4769
5722
|
}
|
|
4770
5723
|
toDataURL(format = "png", multiplier = 1) {
|
|
4771
|
-
return this.withDesignBackground(
|
|
5724
|
+
return this.withDesignBackground(
|
|
5725
|
+
() => this.imageEffects.withSourceResolution(() => exportDataURL(this.canvas, format, multiplier))
|
|
5726
|
+
);
|
|
4772
5727
|
}
|
|
4773
5728
|
/**
|
|
4774
5729
|
* Export the print file: the design cropped to the mockup's print area, on
|
|
@@ -4792,7 +5747,9 @@ var CanvasEditor = class {
|
|
|
4792
5747
|
if (!this.mockup) throw new Error("No mockup is configured");
|
|
4793
5748
|
await this.fonts.ready();
|
|
4794
5749
|
try {
|
|
4795
|
-
const blob = await
|
|
5750
|
+
const blob = await this.imageEffects.withSourceResolution(
|
|
5751
|
+
() => exportMockup(this.canvas, this.mockup, options)
|
|
5752
|
+
);
|
|
4796
5753
|
this.licensing.track(`export:mockup:${options.format ?? "png"}`);
|
|
4797
5754
|
return blob;
|
|
4798
5755
|
} catch (error) {
|
|
@@ -4934,10 +5891,10 @@ var CanvasEditor = class {
|
|
|
4934
5891
|
this.imageProvider = provider;
|
|
4935
5892
|
}
|
|
4936
5893
|
// ─── Canvas Operations ──────────────────────────────
|
|
4937
|
-
setBackground(
|
|
4938
|
-
this.designBackground =
|
|
5894
|
+
setBackground(color2) {
|
|
5895
|
+
this.designBackground = color2;
|
|
4939
5896
|
if (!this.mockup) {
|
|
4940
|
-
this.canvas.backgroundColor =
|
|
5897
|
+
this.canvas.backgroundColor = color2;
|
|
4941
5898
|
this.canvas.requestRenderAll();
|
|
4942
5899
|
}
|
|
4943
5900
|
this.history.save();
|
|
@@ -5244,9 +6201,11 @@ var CanvasEditor = class {
|
|
|
5244
6201
|
}
|
|
5245
6202
|
// ─── Cleanup ────────────────────────────────────────
|
|
5246
6203
|
dispose() {
|
|
6204
|
+
this.imageEffects.dispose();
|
|
5247
6205
|
this.masks.dispose();
|
|
5248
6206
|
this.layerMasks.dispose();
|
|
5249
6207
|
this.snapping.dispose();
|
|
6208
|
+
this.shapes.dispose();
|
|
5250
6209
|
this.crop.dispose();
|
|
5251
6210
|
this.history.dispose();
|
|
5252
6211
|
this.patterns.dispose();
|
|
@@ -5280,12 +6239,26 @@ var CanvasEditor = class {
|
|
|
5280
6239
|
const active = this.canvas.getActiveObject();
|
|
5281
6240
|
if (active) applyObjectSelectionStyle(active, this.selectionStyle, this.zoomLevel);
|
|
5282
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
|
+
});
|
|
5283
6251
|
this.canvas.on("object:modified", (e) => {
|
|
5284
6252
|
if (!e.target) return;
|
|
5285
6253
|
const layer = this.layers.findByObject(e.target);
|
|
5286
6254
|
if (layer) {
|
|
5287
6255
|
this.events.emit("layer:modified", { layerId: layer.id });
|
|
5288
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
|
+
}
|
|
5289
6262
|
}
|
|
5290
6263
|
});
|
|
5291
6264
|
this.canvas.on("selection:created", (e) => {
|
|
@@ -5373,6 +6346,8 @@ var AnnotationOverlay = class {
|
|
|
5373
6346
|
}
|
|
5374
6347
|
};
|
|
5375
6348
|
export {
|
|
6349
|
+
ADJUST_KEYS,
|
|
6350
|
+
ADJUST_RANGES,
|
|
5376
6351
|
AnnotationOverlay,
|
|
5377
6352
|
BLEND_GROUPS,
|
|
5378
6353
|
BLEND_MODES,
|
|
@@ -5388,7 +6363,12 @@ export {
|
|
|
5388
6363
|
EventEmitter,
|
|
5389
6364
|
FontRegistry,
|
|
5390
6365
|
HistoryManager,
|
|
6366
|
+
IMAGE_FILTER_PRESETS,
|
|
6367
|
+
IMAGE_PROXY_MAX_EDGE,
|
|
6368
|
+
ImageEffectsManager,
|
|
6369
|
+
LAYER_EFFECT_DEFAULTS,
|
|
5391
6370
|
Layer,
|
|
6371
|
+
LayerEffectsManager,
|
|
5392
6372
|
LayerManager,
|
|
5393
6373
|
LayerMaskManager,
|
|
5394
6374
|
LicenseManager,
|
|
@@ -5396,10 +6376,12 @@ export {
|
|
|
5396
6376
|
MaskController,
|
|
5397
6377
|
MaskPresetManager,
|
|
5398
6378
|
MaskRefinementError,
|
|
6379
|
+
NEUTRAL_ADJUST,
|
|
5399
6380
|
PatternManager,
|
|
5400
6381
|
ProjectManager,
|
|
5401
6382
|
SHAPE_MASK_BOX,
|
|
5402
6383
|
SHAPE_MASK_IDS,
|
|
6384
|
+
ShapeManager,
|
|
5403
6385
|
SnapManager,
|
|
5404
6386
|
TEXTURE_MASK_IDS,
|
|
5405
6387
|
TEXTURE_MASK_SIZE,
|
|
@@ -5407,6 +6389,8 @@ export {
|
|
|
5407
6389
|
TextWrapManager,
|
|
5408
6390
|
TiledPatternObject,
|
|
5409
6391
|
UnitConverter,
|
|
6392
|
+
alignObjects,
|
|
6393
|
+
alphaClipMatrix,
|
|
5410
6394
|
angleFromCoords,
|
|
5411
6395
|
applyAspectLock,
|
|
5412
6396
|
applyLayerShadow,
|
|
@@ -5419,6 +6403,8 @@ export {
|
|
|
5419
6403
|
buildCurveLinePaths,
|
|
5420
6404
|
buildCurvePathData,
|
|
5421
6405
|
clamp,
|
|
6406
|
+
clampAdjust,
|
|
6407
|
+
clampAlphaClip,
|
|
5422
6408
|
clearTextureMaskCache,
|
|
5423
6409
|
composeMaskGroup,
|
|
5424
6410
|
computeCoverPlacement,
|
|
@@ -5426,6 +6412,7 @@ export {
|
|
|
5426
6412
|
computeTilePositions,
|
|
5427
6413
|
deserializeEditor,
|
|
5428
6414
|
displaceRgba,
|
|
6415
|
+
distributeObjects,
|
|
5429
6416
|
drawTiles,
|
|
5430
6417
|
escapeXml,
|
|
5431
6418
|
explodeTemplateSvg,
|
|
@@ -5437,12 +6424,15 @@ export {
|
|
|
5437
6424
|
fitTextWidth,
|
|
5438
6425
|
fitToBox,
|
|
5439
6426
|
generateId,
|
|
6427
|
+
imageEffectsFilterSpecs,
|
|
6428
|
+
imageFilterPreset,
|
|
5440
6429
|
isCssColor,
|
|
5441
6430
|
isMaskPresetId,
|
|
5442
6431
|
isShapeMaskId,
|
|
5443
6432
|
isTextureMaskId,
|
|
5444
6433
|
linearCoords,
|
|
5445
6434
|
needsAbsoluteSpace,
|
|
6435
|
+
normalizeImageEffects,
|
|
5446
6436
|
normalizeTextCurve,
|
|
5447
6437
|
preWrapWordSplit,
|
|
5448
6438
|
readGradientConfig,
|
|
@@ -5450,6 +6440,7 @@ export {
|
|
|
5450
6440
|
readTextWrap,
|
|
5451
6441
|
renderTextureMask,
|
|
5452
6442
|
resetTransform,
|
|
6443
|
+
resolveShapeParams,
|
|
5453
6444
|
restoreLocks,
|
|
5454
6445
|
round2,
|
|
5455
6446
|
sanitizeSvg,
|
|
@@ -5460,6 +6451,7 @@ export {
|
|
|
5460
6451
|
toCanvasSpace,
|
|
5461
6452
|
toFabricGradient,
|
|
5462
6453
|
toHostSpace,
|
|
6454
|
+
unionBox,
|
|
5463
6455
|
unwrapGroup
|
|
5464
6456
|
};
|
|
5465
6457
|
//# sourceMappingURL=index.mjs.map
|