@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.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,11 +17,21 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
33
|
+
ADJUST_KEYS: () => ADJUST_KEYS,
|
|
34
|
+
ADJUST_RANGES: () => ADJUST_RANGES,
|
|
23
35
|
AnnotationOverlay: () => AnnotationOverlay,
|
|
24
36
|
BLEND_GROUPS: () => BLEND_GROUPS,
|
|
25
37
|
BLEND_MODES: () => BLEND_MODES,
|
|
@@ -35,7 +47,12 @@ __export(index_exports, {
|
|
|
35
47
|
EventEmitter: () => EventEmitter,
|
|
36
48
|
FontRegistry: () => FontRegistry,
|
|
37
49
|
HistoryManager: () => HistoryManager,
|
|
50
|
+
IMAGE_FILTER_PRESETS: () => IMAGE_FILTER_PRESETS,
|
|
51
|
+
IMAGE_PROXY_MAX_EDGE: () => IMAGE_PROXY_MAX_EDGE,
|
|
52
|
+
ImageEffectsManager: () => ImageEffectsManager,
|
|
53
|
+
LAYER_EFFECT_DEFAULTS: () => LAYER_EFFECT_DEFAULTS,
|
|
38
54
|
Layer: () => Layer,
|
|
55
|
+
LayerEffectsManager: () => LayerEffectsManager,
|
|
39
56
|
LayerManager: () => LayerManager,
|
|
40
57
|
LayerMaskManager: () => LayerMaskManager,
|
|
41
58
|
LicenseManager: () => LicenseManager,
|
|
@@ -43,10 +60,12 @@ __export(index_exports, {
|
|
|
43
60
|
MaskController: () => MaskController,
|
|
44
61
|
MaskPresetManager: () => MaskPresetManager,
|
|
45
62
|
MaskRefinementError: () => MaskRefinementError,
|
|
63
|
+
NEUTRAL_ADJUST: () => NEUTRAL_ADJUST,
|
|
46
64
|
PatternManager: () => PatternManager,
|
|
47
65
|
ProjectManager: () => ProjectManager,
|
|
48
66
|
SHAPE_MASK_BOX: () => SHAPE_MASK_BOX,
|
|
49
67
|
SHAPE_MASK_IDS: () => SHAPE_MASK_IDS,
|
|
68
|
+
ShapeManager: () => ShapeManager,
|
|
50
69
|
SnapManager: () => SnapManager,
|
|
51
70
|
TEXTURE_MASK_IDS: () => TEXTURE_MASK_IDS,
|
|
52
71
|
TEXTURE_MASK_SIZE: () => TEXTURE_MASK_SIZE,
|
|
@@ -54,6 +73,8 @@ __export(index_exports, {
|
|
|
54
73
|
TextWrapManager: () => TextWrapManager,
|
|
55
74
|
TiledPatternObject: () => TiledPatternObject,
|
|
56
75
|
UnitConverter: () => UnitConverter,
|
|
76
|
+
alignObjects: () => alignObjects,
|
|
77
|
+
alphaClipMatrix: () => alphaClipMatrix,
|
|
57
78
|
angleFromCoords: () => angleFromCoords,
|
|
58
79
|
applyAspectLock: () => applyAspectLock,
|
|
59
80
|
applyLayerShadow: () => applyLayerShadow,
|
|
@@ -66,6 +87,8 @@ __export(index_exports, {
|
|
|
66
87
|
buildCurveLinePaths: () => buildCurveLinePaths,
|
|
67
88
|
buildCurvePathData: () => buildCurvePathData,
|
|
68
89
|
clamp: () => clamp,
|
|
90
|
+
clampAdjust: () => clampAdjust,
|
|
91
|
+
clampAlphaClip: () => clampAlphaClip,
|
|
69
92
|
clearTextureMaskCache: () => clearTextureMaskCache,
|
|
70
93
|
composeMaskGroup: () => composeMaskGroup,
|
|
71
94
|
computeCoverPlacement: () => computeCoverPlacement,
|
|
@@ -73,6 +96,7 @@ __export(index_exports, {
|
|
|
73
96
|
computeTilePositions: () => computeTilePositions,
|
|
74
97
|
deserializeEditor: () => deserializeEditor,
|
|
75
98
|
displaceRgba: () => displaceRgba,
|
|
99
|
+
distributeObjects: () => distributeObjects,
|
|
76
100
|
drawTiles: () => drawTiles,
|
|
77
101
|
escapeXml: () => escapeXml,
|
|
78
102
|
explodeTemplateSvg: () => explodeTemplateSvg,
|
|
@@ -84,12 +108,15 @@ __export(index_exports, {
|
|
|
84
108
|
fitTextWidth: () => fitTextWidth,
|
|
85
109
|
fitToBox: () => fitToBox,
|
|
86
110
|
generateId: () => generateId,
|
|
111
|
+
imageEffectsFilterSpecs: () => imageEffectsFilterSpecs,
|
|
112
|
+
imageFilterPreset: () => imageFilterPreset,
|
|
87
113
|
isCssColor: () => isCssColor,
|
|
88
114
|
isMaskPresetId: () => isMaskPresetId,
|
|
89
115
|
isShapeMaskId: () => isShapeMaskId,
|
|
90
116
|
isTextureMaskId: () => isTextureMaskId,
|
|
91
117
|
linearCoords: () => linearCoords,
|
|
92
118
|
needsAbsoluteSpace: () => needsAbsoluteSpace,
|
|
119
|
+
normalizeImageEffects: () => normalizeImageEffects,
|
|
93
120
|
normalizeTextCurve: () => normalizeTextCurve,
|
|
94
121
|
preWrapWordSplit: () => preWrapWordSplit,
|
|
95
122
|
readGradientConfig: () => readGradientConfig,
|
|
@@ -97,6 +124,7 @@ __export(index_exports, {
|
|
|
97
124
|
readTextWrap: () => readTextWrap,
|
|
98
125
|
renderTextureMask: () => renderTextureMask,
|
|
99
126
|
resetTransform: () => resetTransform,
|
|
127
|
+
resolveShapeParams: () => resolveShapeParams,
|
|
100
128
|
restoreLocks: () => restoreLocks,
|
|
101
129
|
round2: () => round2,
|
|
102
130
|
sanitizeSvg: () => sanitizeSvg,
|
|
@@ -107,12 +135,15 @@ __export(index_exports, {
|
|
|
107
135
|
toCanvasSpace: () => toCanvasSpace,
|
|
108
136
|
toFabricGradient: () => toFabricGradient,
|
|
109
137
|
toHostSpace: () => toHostSpace,
|
|
138
|
+
unionBox: () => unionBox,
|
|
110
139
|
unwrapGroup: () => unwrapGroup
|
|
111
140
|
});
|
|
112
141
|
module.exports = __toCommonJS(index_exports);
|
|
113
142
|
|
|
114
143
|
// src/editor.ts
|
|
115
|
-
var
|
|
144
|
+
var import_fabric25 = require("fabric");
|
|
145
|
+
var fabricModule = __toESM(require("fabric"));
|
|
146
|
+
var import_canvas_editor_effects2 = require("@overtone-art/canvas-editor-effects");
|
|
116
147
|
|
|
117
148
|
// src/events.ts
|
|
118
149
|
var EventEmitter = class {
|
|
@@ -264,9 +295,31 @@ var Layer = class {
|
|
|
264
295
|
...this.toData(),
|
|
265
296
|
// A group's own object already carries its children's geometry, so the
|
|
266
297
|
// child records in `toData` stay identity-only — nothing is duplicated.
|
|
267
|
-
fabricObject: this.
|
|
298
|
+
fabricObject: this.serializeObject()
|
|
268
299
|
};
|
|
269
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* A member of an ActiveSelection holds selection-relative coordinates and
|
|
303
|
+
* `toObject()` writes them verbatim. A snapshot taken mid-selection — which
|
|
304
|
+
* `HistoryManager.undo()` takes — then held positions no restore could use,
|
|
305
|
+
* and the redo scattered the layers. Realise the selection's transform for
|
|
306
|
+
* the write and put the object's own transform back.
|
|
307
|
+
*/
|
|
308
|
+
serializeObject() {
|
|
309
|
+
const object = this.fabricObject;
|
|
310
|
+
const selection = object.group;
|
|
311
|
+
if (!(selection instanceof import_fabric.ActiveSelection)) {
|
|
312
|
+
return object.toObject();
|
|
313
|
+
}
|
|
314
|
+
const saved = import_fabric.util.saveObjectTransform(object);
|
|
315
|
+
import_fabric.util.addTransformToObject(object, selection.calcOwnMatrix());
|
|
316
|
+
try {
|
|
317
|
+
return object.toObject();
|
|
318
|
+
} finally {
|
|
319
|
+
object.set(saved);
|
|
320
|
+
object.setCoords();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
270
323
|
};
|
|
271
324
|
var LayerManager = class {
|
|
272
325
|
constructor(canvas, events) {
|
|
@@ -1198,7 +1251,7 @@ var CropController = class {
|
|
|
1198
1251
|
activeLayerId() {
|
|
1199
1252
|
return this.session?.layerId ?? null;
|
|
1200
1253
|
}
|
|
1201
|
-
start(layer) {
|
|
1254
|
+
start(layer, options = {}) {
|
|
1202
1255
|
if (layer.type !== "image") return;
|
|
1203
1256
|
if (this.session) this.cancel();
|
|
1204
1257
|
const image = layer.fabricObject;
|
|
@@ -1206,6 +1259,12 @@ var CropController = class {
|
|
|
1206
1259
|
if (prevAngle) image.rotate(0);
|
|
1207
1260
|
image.setCoords();
|
|
1208
1261
|
const b = image.getBoundingRect();
|
|
1262
|
+
const prevTransform = {
|
|
1263
|
+
scaleX: image.scaleX,
|
|
1264
|
+
scaleY: image.scaleY,
|
|
1265
|
+
left: image.left,
|
|
1266
|
+
top: image.top
|
|
1267
|
+
};
|
|
1209
1268
|
const rect = new import_fabric3.Rect({
|
|
1210
1269
|
left: b.left,
|
|
1211
1270
|
top: b.top,
|
|
@@ -1230,8 +1289,95 @@ var CropController = class {
|
|
|
1230
1289
|
this.canvas.add(rect);
|
|
1231
1290
|
this.canvas.setActiveObject(rect);
|
|
1232
1291
|
this.canvas.requestRenderAll();
|
|
1233
|
-
this.session = {
|
|
1292
|
+
this.session = {
|
|
1293
|
+
layerId: layer.id,
|
|
1294
|
+
image,
|
|
1295
|
+
rect,
|
|
1296
|
+
prevSelectable,
|
|
1297
|
+
prevEvented,
|
|
1298
|
+
prevAngle,
|
|
1299
|
+
prevTransform,
|
|
1300
|
+
startRect: { left: b.left, top: b.top, width: b.width, height: b.height },
|
|
1301
|
+
aspect: null
|
|
1302
|
+
};
|
|
1234
1303
|
this.events.emit("crop:changed", { active: true, layerId: layer.id });
|
|
1304
|
+
if (options.aspect) this.setAspect(options.aspect);
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* The frame's own geometry, without its stroke.
|
|
1308
|
+
*
|
|
1309
|
+
* The rect draws a 1px dashed outline, and `getBoundingRect` /
|
|
1310
|
+
* `getScaledWidth` both count it — so reading the frame through them made the
|
|
1311
|
+
* aspect box, the zoom centre and the applied crop each about a pixel larger
|
|
1312
|
+
* than the box the user drew. The rect is axis-aligned (rotation is locked),
|
|
1313
|
+
* so its own left/top and scaled dimensions are exact.
|
|
1314
|
+
*/
|
|
1315
|
+
frameBox(rect) {
|
|
1316
|
+
return {
|
|
1317
|
+
left: rect.left,
|
|
1318
|
+
top: rect.top,
|
|
1319
|
+
width: rect.width * rect.scaleX,
|
|
1320
|
+
height: rect.height * rect.scaleY
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Lock the frame to a ratio: it becomes the largest box of that ratio inside
|
|
1325
|
+
* the current frame, centred, and only the corner handles stay — fabric's
|
|
1326
|
+
* default `uniformScaling` then keeps the ratio through a drag. `null` frees it.
|
|
1327
|
+
*/
|
|
1328
|
+
setAspect(ratio) {
|
|
1329
|
+
const s = this.session;
|
|
1330
|
+
if (!s) return;
|
|
1331
|
+
s.aspect = ratio && ratio > 0 ? ratio : null;
|
|
1332
|
+
const { rect } = s;
|
|
1333
|
+
const sides = s.aspect === null;
|
|
1334
|
+
rect.setControlsVisibility({ ml: sides, mr: sides, mt: sides, mb: sides, mtr: false });
|
|
1335
|
+
if (s.aspect !== null) {
|
|
1336
|
+
rect.setCoords();
|
|
1337
|
+
const current = this.frameBox(rect);
|
|
1338
|
+
const width = Math.min(current.width, current.height * s.aspect);
|
|
1339
|
+
const height = width / s.aspect;
|
|
1340
|
+
rect.set({
|
|
1341
|
+
left: current.left + (current.width - width) / 2,
|
|
1342
|
+
top: current.top + (current.height - height) / 2,
|
|
1343
|
+
width,
|
|
1344
|
+
height,
|
|
1345
|
+
scaleX: 1,
|
|
1346
|
+
scaleY: 1
|
|
1347
|
+
});
|
|
1348
|
+
rect.setCoords();
|
|
1349
|
+
}
|
|
1350
|
+
this.canvas.requestRenderAll();
|
|
1351
|
+
}
|
|
1352
|
+
/** Zoom the image under a fixed frame, about the frame's centre. */
|
|
1353
|
+
setScale(factor) {
|
|
1354
|
+
const s = this.session;
|
|
1355
|
+
if (!s || !Number.isFinite(factor) || factor <= 0) return;
|
|
1356
|
+
const { image, rect } = s;
|
|
1357
|
+
rect.setCoords();
|
|
1358
|
+
const frame = this.frameBox(rect);
|
|
1359
|
+
const centre = { x: frame.left + frame.width / 2, y: frame.top + frame.height / 2 };
|
|
1360
|
+
const scaleX = Math.max(0.01, image.scaleX * factor);
|
|
1361
|
+
const scaleY = Math.max(0.01, image.scaleY * factor);
|
|
1362
|
+
const applied = scaleX / image.scaleX;
|
|
1363
|
+
image.set({
|
|
1364
|
+
scaleX,
|
|
1365
|
+
scaleY,
|
|
1366
|
+
left: centre.x - (centre.x - image.left) * applied,
|
|
1367
|
+
top: centre.y - (centre.y - image.top) * applied
|
|
1368
|
+
});
|
|
1369
|
+
image.setCoords();
|
|
1370
|
+
this.canvas.requestRenderAll();
|
|
1371
|
+
}
|
|
1372
|
+
/** Back to the frame and image transform the session started with; aspect freed. */
|
|
1373
|
+
reset() {
|
|
1374
|
+
const s = this.session;
|
|
1375
|
+
if (!s) return;
|
|
1376
|
+
s.image.set(s.prevTransform);
|
|
1377
|
+
s.image.setCoords();
|
|
1378
|
+
s.rect.set({ ...s.startRect, scaleX: 1, scaleY: 1 });
|
|
1379
|
+
s.rect.setCoords();
|
|
1380
|
+
this.setAspect(null);
|
|
1235
1381
|
}
|
|
1236
1382
|
apply() {
|
|
1237
1383
|
const s = this.session;
|
|
@@ -1246,10 +1392,11 @@ var CropController = class {
|
|
|
1246
1392
|
const el = image.getElement();
|
|
1247
1393
|
const naturalW = el.naturalWidth || image.width || 0;
|
|
1248
1394
|
const naturalH = el.naturalHeight || image.height || 0;
|
|
1249
|
-
|
|
1250
|
-
let
|
|
1251
|
-
let
|
|
1252
|
-
let
|
|
1395
|
+
const frame = this.frameBox(rect);
|
|
1396
|
+
let cropX = (image.cropX ?? 0) + (frame.left - imgLeft) / scaleX;
|
|
1397
|
+
let cropY = (image.cropY ?? 0) + (frame.top - imgTop) / scaleY;
|
|
1398
|
+
let cropW = frame.width / scaleX;
|
|
1399
|
+
let cropH = frame.height / scaleY;
|
|
1253
1400
|
cropX = clamp(cropX, 0, Math.max(0, naturalW - 1));
|
|
1254
1401
|
cropY = clamp(cropY, 0, Math.max(0, naturalH - 1));
|
|
1255
1402
|
cropW = clamp(cropW, 1, naturalW - cropX);
|
|
@@ -1274,6 +1421,8 @@ var CropController = class {
|
|
|
1274
1421
|
cancel() {
|
|
1275
1422
|
const s = this.session;
|
|
1276
1423
|
if (!s) return;
|
|
1424
|
+
s.image.set(s.prevTransform);
|
|
1425
|
+
s.image.setCoords();
|
|
1277
1426
|
if (s.prevAngle) {
|
|
1278
1427
|
s.image.rotate(s.prevAngle);
|
|
1279
1428
|
s.image.setCoords();
|
|
@@ -1298,14 +1447,70 @@ var CropController = class {
|
|
|
1298
1447
|
};
|
|
1299
1448
|
var STROKE2 = "#22c55e";
|
|
1300
1449
|
|
|
1450
|
+
// src/align.ts
|
|
1451
|
+
var import_fabric4 = require("fabric");
|
|
1452
|
+
function unionBox(boxes) {
|
|
1453
|
+
const left = Math.min(...boxes.map((b) => b.left));
|
|
1454
|
+
const top = Math.min(...boxes.map((b) => b.top));
|
|
1455
|
+
const right = Math.max(...boxes.map((b) => b.left + b.width));
|
|
1456
|
+
const bottom = Math.max(...boxes.map((b) => b.top + b.height));
|
|
1457
|
+
return { left, top, width: right - left, height: bottom - top };
|
|
1458
|
+
}
|
|
1459
|
+
function boxOf(object) {
|
|
1460
|
+
object.setCoords();
|
|
1461
|
+
return object.getBoundingRect();
|
|
1462
|
+
}
|
|
1463
|
+
function moveBy(object, dx, dy) {
|
|
1464
|
+
const parent = object.group;
|
|
1465
|
+
const vector = parent ? import_fabric4.util.sendVectorToPlane(new import_fabric4.Point(dx, dy), void 0, parent.calcTransformMatrix()) : new import_fabric4.Point(dx, dy);
|
|
1466
|
+
object.set({ left: (object.left ?? 0) + vector.x, top: (object.top ?? 0) + vector.y });
|
|
1467
|
+
object.setCoords();
|
|
1468
|
+
}
|
|
1469
|
+
function alignObjects(objects, target, frame) {
|
|
1470
|
+
for (const object of objects) {
|
|
1471
|
+
const b = boxOf(object);
|
|
1472
|
+
if (target === "left") moveBy(object, frame.left - b.left, 0);
|
|
1473
|
+
if (target === "center-h") moveBy(object, frame.left + (frame.width - b.width) / 2 - b.left, 0);
|
|
1474
|
+
if (target === "right") moveBy(object, frame.left + frame.width - b.width - b.left, 0);
|
|
1475
|
+
if (target === "top") moveBy(object, 0, frame.top - b.top);
|
|
1476
|
+
if (target === "middle") moveBy(object, 0, frame.top + (frame.height - b.height) / 2 - b.top);
|
|
1477
|
+
if (target === "bottom") moveBy(object, 0, frame.top + frame.height - b.height - b.top);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
function distributeObjects(objects, axis) {
|
|
1481
|
+
if (objects.length < 3) return false;
|
|
1482
|
+
const measured = objects.map((object) => ({ object, box: boxOf(object) }));
|
|
1483
|
+
const pos = (b) => axis === "h" ? b.left : b.top;
|
|
1484
|
+
const size = (b) => axis === "h" ? b.width : b.height;
|
|
1485
|
+
const ordered = [...measured].sort((a, b) => pos(a.box) - pos(b.box));
|
|
1486
|
+
const span = unionBox(measured.map((m) => m.box));
|
|
1487
|
+
const total = ordered.reduce((sum, m) => sum + size(m.box), 0);
|
|
1488
|
+
const gap = (size(span) - total) / (ordered.length - 1);
|
|
1489
|
+
let cursor = pos(span);
|
|
1490
|
+
for (const { object, box } of ordered) {
|
|
1491
|
+
const delta = cursor - pos(box);
|
|
1492
|
+
moveBy(object, axis === "h" ? delta : 0, axis === "h" ? 0 : delta);
|
|
1493
|
+
cursor += size(box) + gap;
|
|
1494
|
+
}
|
|
1495
|
+
return true;
|
|
1496
|
+
}
|
|
1497
|
+
function relayoutSelection(active) {
|
|
1498
|
+
if (!(active instanceof import_fabric4.ActiveSelection)) return;
|
|
1499
|
+
active.triggerLayout();
|
|
1500
|
+
active.setCoords();
|
|
1501
|
+
}
|
|
1502
|
+
function canvasBox(width, height) {
|
|
1503
|
+
return { left: 0, top: 0, width, height };
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1301
1506
|
// src/pattern/pattern-manager.ts
|
|
1302
|
-
var
|
|
1507
|
+
var import_fabric7 = require("fabric");
|
|
1303
1508
|
|
|
1304
1509
|
// src/pattern/tiled-pattern-object.ts
|
|
1305
|
-
var
|
|
1510
|
+
var import_fabric6 = require("fabric");
|
|
1306
1511
|
|
|
1307
1512
|
// src/text-fit.ts
|
|
1308
|
-
var
|
|
1513
|
+
var import_fabric5 = require("fabric");
|
|
1309
1514
|
var TEXT_FIT_SLACK = 0.5;
|
|
1310
1515
|
var MEASURE_WIDTH = 1e5;
|
|
1311
1516
|
var GROWTH_FACTOR = 1.1;
|
|
@@ -1394,7 +1599,7 @@ function fitTextWidth(object) {
|
|
|
1394
1599
|
const after = textInk(text);
|
|
1395
1600
|
const shift = (before.dx - after.dx) * (text.scaleX ?? 1);
|
|
1396
1601
|
const radians = (text.angle ?? 0) * Math.PI / 180;
|
|
1397
|
-
const moved = new
|
|
1602
|
+
const moved = new import_fabric5.Point(
|
|
1398
1603
|
centre.x + shift * Math.cos(radians),
|
|
1399
1604
|
centre.y + shift * Math.sin(radians)
|
|
1400
1605
|
);
|
|
@@ -1418,9 +1623,9 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
|
|
|
1418
1623
|
const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
|
|
1419
1624
|
const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
|
|
1420
1625
|
const cols = Math.ceil(span / tileW) + 2;
|
|
1421
|
-
const
|
|
1626
|
+
const rows2 = Math.ceil(span / tileH) + 2;
|
|
1422
1627
|
const [colFrom, colTo] = axisRange(config.horizontalLimit, Math.ceil(cols / 2));
|
|
1423
|
-
const [rowFrom, rowTo] = axisRange(config.verticalLimit, Math.ceil(
|
|
1628
|
+
const [rowFrom, rowTo] = axisRange(config.verticalLimit, Math.ceil(rows2 / 2));
|
|
1424
1629
|
const shiftX = tileW * (clampOffset(config.offsetX) / 100);
|
|
1425
1630
|
const shiftY = tileH * (clampOffset(config.offsetY) / 100);
|
|
1426
1631
|
const placements = [];
|
|
@@ -1479,7 +1684,7 @@ var MAX_SNAPSHOT_PIXELS = 16e6;
|
|
|
1479
1684
|
var MAX_SNAPSHOT_SCALE = 8;
|
|
1480
1685
|
var SNAPSHOT_SHRINK_FACTOR = 2;
|
|
1481
1686
|
var TILE_BLEED_DEVICE_PX = 2;
|
|
1482
|
-
var TiledPatternObject = class _TiledPatternObject extends
|
|
1687
|
+
var TiledPatternObject = class _TiledPatternObject extends import_fabric6.FabricObject {
|
|
1483
1688
|
static type = "TiledPattern";
|
|
1484
1689
|
/** The layer's real object. Never rendered directly (it is kept at opacity 0). */
|
|
1485
1690
|
source;
|
|
@@ -1563,7 +1768,7 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric5.Fabric
|
|
|
1563
1768
|
const centre = this.getCenterPoint();
|
|
1564
1769
|
const shift = this.shiftVector(tileW, tileH, this.liveAngle());
|
|
1565
1770
|
const ink = this.inkOffset();
|
|
1566
|
-
return new
|
|
1771
|
+
return new import_fabric6.Point(centre.x - shift.x - ink.x, centre.y - shift.y - ink.y);
|
|
1567
1772
|
}
|
|
1568
1773
|
_render(ctx) {
|
|
1569
1774
|
const { width, height } = this.area;
|
|
@@ -1650,6 +1855,10 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric5.Fabric
|
|
|
1650
1855
|
angle: this.angle,
|
|
1651
1856
|
scaleX: this.scaleX,
|
|
1652
1857
|
scaleY: this.scaleY,
|
|
1858
|
+
skewX: this.skewX,
|
|
1859
|
+
skewY: this.skewY,
|
|
1860
|
+
flipX: this.flipX,
|
|
1861
|
+
flipY: this.flipY,
|
|
1653
1862
|
width: this.width,
|
|
1654
1863
|
height: this.height,
|
|
1655
1864
|
visible: this.visible,
|
|
@@ -1695,12 +1904,12 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric5.Fabric
|
|
|
1695
1904
|
*/
|
|
1696
1905
|
inkOffset() {
|
|
1697
1906
|
const text = asText(this.source);
|
|
1698
|
-
if (!text) return new
|
|
1907
|
+
if (!text) return new import_fabric6.Point(0, 0);
|
|
1699
1908
|
const { dx } = textInk(text);
|
|
1700
|
-
if (!dx) return new
|
|
1909
|
+
if (!dx) return new import_fabric6.Point(0, 0);
|
|
1701
1910
|
const scaled = dx * (this.source.scaleX ?? 1);
|
|
1702
1911
|
const radians = (this.source.angle ?? 0) * Math.PI / 180;
|
|
1703
|
-
return new
|
|
1912
|
+
return new import_fabric6.Point(scaled * Math.cos(radians), scaled * Math.sin(radians));
|
|
1704
1913
|
}
|
|
1705
1914
|
/** The source's on-canvas width, before the tile scale. */
|
|
1706
1915
|
baseW() {
|
|
@@ -1761,11 +1970,11 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric5.Fabric
|
|
|
1761
1970
|
const radians = angle * Math.PI / 180;
|
|
1762
1971
|
const cos = Math.cos(radians);
|
|
1763
1972
|
const sin = Math.sin(radians);
|
|
1764
|
-
return new
|
|
1973
|
+
return new import_fabric6.Point(dx * cos - dy * sin, dx * sin + dy * cos);
|
|
1765
1974
|
}
|
|
1766
1975
|
anchorFromOrigin(origin, tileW, tileH, angle) {
|
|
1767
1976
|
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1768
|
-
return new
|
|
1977
|
+
return new import_fabric6.Point(origin.x + shift.x, origin.y + shift.y);
|
|
1769
1978
|
}
|
|
1770
1979
|
/**
|
|
1771
1980
|
* Snapshot the source at (at least) `scale`, reusing the cached one while it
|
|
@@ -1970,6 +2179,11 @@ var PatternManager = class {
|
|
|
1970
2179
|
if (wasActive) this.canvas.setActiveObject(proxy);
|
|
1971
2180
|
this.canvas.requestRenderAll();
|
|
1972
2181
|
}
|
|
2182
|
+
/** Fold a programmatic move of the proxy into the config and the source, as a drop does. */
|
|
2183
|
+
commitProxy(layerId) {
|
|
2184
|
+
const proxy = this.proxies.get(layerId);
|
|
2185
|
+
if (proxy) this.commitGesture(proxy);
|
|
2186
|
+
}
|
|
1973
2187
|
/**
|
|
1974
2188
|
* Fold a finished drag / scale / rotate on the proxy back into the pattern:
|
|
1975
2189
|
* position becomes the grid origin (carried by the source), scale becomes the
|
|
@@ -2037,7 +2251,7 @@ async function unbakeLegacyLayer(layer, state) {
|
|
|
2037
2251
|
cropY: state.original.cropY,
|
|
2038
2252
|
angle: state.original.angle
|
|
2039
2253
|
});
|
|
2040
|
-
image.clipPath = state.originalClip ? (await
|
|
2254
|
+
image.clipPath = state.originalClip ? (await import_fabric7.util.enlivenObjects([state.originalClip]))[0] : void 0;
|
|
2041
2255
|
restoreLocks(image, state.originalLocks);
|
|
2042
2256
|
image.setCoords();
|
|
2043
2257
|
}
|
|
@@ -2054,220 +2268,997 @@ function restoreLocks(obj, locks) {
|
|
|
2054
2268
|
);
|
|
2055
2269
|
}
|
|
2056
2270
|
|
|
2057
|
-
// src/
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
wave: 0,
|
|
2065
|
-
waveLength: 4.1,
|
|
2066
|
-
offset: 0,
|
|
2067
|
-
centerOffset: 0
|
|
2068
|
-
};
|
|
2069
|
-
var MIN_ARC = 0.5;
|
|
2070
|
-
var FULL_CIRCLE_ARC = 99.5;
|
|
2071
|
-
var MIN_SWEEP = 0.12;
|
|
2072
|
-
var WAVE_AMPLITUDE_EM = 0.9;
|
|
2073
|
-
var MIN_WAVE_LENGTH = 1;
|
|
2074
|
-
var MAX_WAVE_LENGTH = 12;
|
|
2075
|
-
var WAVE_STEP = 6;
|
|
2076
|
-
var MAX_WAVE_STEPS = 4e3;
|
|
2077
|
-
function round(value) {
|
|
2078
|
-
return Math.round(value * 100) / 100;
|
|
2079
|
-
}
|
|
2080
|
-
function normalizeTextCurve(config) {
|
|
2081
|
-
const arc = clamp(config.arc ?? 0, -100, 100);
|
|
2082
|
-
const wave = clamp(config.wave ?? 0, 0, 100);
|
|
2083
|
-
const inferred = Math.abs(arc) < MIN_ARC && wave > 0 ? "wave" : "arc";
|
|
2084
|
-
return {
|
|
2085
|
-
shape: config.shape ?? inferred,
|
|
2086
|
-
arc,
|
|
2087
|
-
wave,
|
|
2088
|
-
waveLength: clamp(
|
|
2089
|
-
config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength,
|
|
2090
|
-
MIN_WAVE_LENGTH,
|
|
2091
|
-
MAX_WAVE_LENGTH
|
|
2092
|
-
),
|
|
2093
|
-
offset: clamp(config.offset ?? 0, -100, 100),
|
|
2094
|
-
centerOffset: clamp(config.centerOffset ?? 0, -100, 100)
|
|
2095
|
-
};
|
|
2096
|
-
}
|
|
2097
|
-
function bendsBaseline(config) {
|
|
2098
|
-
return config.shape === "arc" ? Math.abs(config.arc) >= MIN_ARC : config.wave > 0;
|
|
2099
|
-
}
|
|
2100
|
-
function arcGeometry(width, config) {
|
|
2101
|
-
const magnitude = Math.min(100, Math.abs(config.arc));
|
|
2102
|
-
const direction = config.arc < 0 ? -1 : 1;
|
|
2103
|
-
const baseSweep = magnitude >= FULL_CIRCLE_ARC ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
|
|
2104
|
-
const baseRadius = width / baseSweep;
|
|
2105
|
-
const radius = baseRadius * Math.pow(2, config.centerOffset / 100);
|
|
2106
|
-
const sweep = radius >= baseRadius ? baseSweep : Math.min(Math.PI * 2, width / Math.max(radius, 1e-3));
|
|
2107
|
-
return { radius, sweep, direction, baseRadius };
|
|
2108
|
-
}
|
|
2109
|
-
function arcLinePath(width, geometry, radius, rotation) {
|
|
2110
|
-
const { sweep, direction, baseRadius } = geometry;
|
|
2111
|
-
const full = sweep >= Math.PI * 2 - 1e-6;
|
|
2112
|
-
const centerX = width / 2;
|
|
2113
|
-
const point = (angle) => [
|
|
2114
|
-
centerX + radius * Math.sin(angle),
|
|
2115
|
-
direction * (baseRadius - radius * Math.cos(angle))
|
|
2116
|
-
];
|
|
2117
|
-
const sweepFlag = direction > 0 ? 1 : 0;
|
|
2118
|
-
const format = ([x, y]) => `${round(x)} ${round(y)}`;
|
|
2119
|
-
const r = round(radius);
|
|
2120
|
-
if (full) {
|
|
2121
|
-
const start2 = point(rotation - Math.PI);
|
|
2122
|
-
const opposite = point(rotation);
|
|
2123
|
-
return {
|
|
2124
|
-
data: [
|
|
2125
|
-
`M ${format(start2)}`,
|
|
2126
|
-
`A ${r} ${r} 0 0 ${sweepFlag} ${format(opposite)}`,
|
|
2127
|
-
`A ${r} ${r} 0 0 ${sweepFlag} ${format(start2)}`
|
|
2128
|
-
].join(" "),
|
|
2129
|
-
length: radius * Math.PI * 2
|
|
2130
|
-
};
|
|
2131
|
-
}
|
|
2132
|
-
const start = point(rotation - sweep / 2);
|
|
2133
|
-
const end = point(rotation + sweep / 2);
|
|
2134
|
-
const largeArc = sweep > Math.PI ? 1 : 0;
|
|
2135
|
-
return {
|
|
2136
|
-
data: `M ${format(start)} A ${r} ${r} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
|
|
2137
|
-
length: radius * sweep
|
|
2138
|
-
};
|
|
2139
|
-
}
|
|
2140
|
-
function waveLinePath(width, fontSize, config, dy) {
|
|
2141
|
-
const amplitude = config.wave / 100 * fontSize * WAVE_AMPLITUDE_EM;
|
|
2142
|
-
const period = Math.max(1, fontSize * config.waveLength);
|
|
2143
|
-
const phase = config.offset / 100 * Math.PI * 2;
|
|
2144
|
-
const step = Math.min(WAVE_STEP, period / 8);
|
|
2145
|
-
const steps = Math.min(MAX_WAVE_STEPS, Math.max(2, Math.ceil(width / step)));
|
|
2146
|
-
const commands = [];
|
|
2147
|
-
let length = 0;
|
|
2148
|
-
let previous = null;
|
|
2149
|
-
for (let index = 0; index <= steps; index++) {
|
|
2150
|
-
const x = width * index / steps;
|
|
2151
|
-
const y = dy + amplitude * Math.sin(x / period * Math.PI * 2 + phase);
|
|
2152
|
-
if (previous) length += Math.hypot(x - previous[0], y - previous[1]);
|
|
2153
|
-
previous = [x, y];
|
|
2154
|
-
commands.push(`${index === 0 ? "M" : "L"} ${round(x)} ${round(y)}`);
|
|
2155
|
-
}
|
|
2156
|
-
return { data: commands.join(" "), length };
|
|
2157
|
-
}
|
|
2158
|
-
function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight = 0) {
|
|
2159
|
-
const curve = normalizeTextCurve(config);
|
|
2160
|
-
if (!bendsBaseline(curve)) return null;
|
|
2161
|
-
const lines = Math.max(1, Math.floor(lineCount));
|
|
2162
|
-
const paths = [];
|
|
2163
|
-
if (curve.shape === "wave") {
|
|
2164
|
-
for (let index = 0; index < lines; index++) {
|
|
2165
|
-
paths.push(waveLinePath(width, fontSize, curve, (index - (lines - 1) / 2) * lineHeight));
|
|
2166
|
-
}
|
|
2167
|
-
return paths;
|
|
2168
|
-
}
|
|
2169
|
-
const geometry = arcGeometry(width, curve);
|
|
2170
|
-
const rotation = curve.offset / 100 * geometry.sweep;
|
|
2171
|
-
for (let index = 0; index < lines; index++) {
|
|
2172
|
-
const radius = Math.max(1, geometry.radius + (index - (lines - 1) / 2) * lineHeight);
|
|
2173
|
-
paths.push(arcLinePath(width, geometry, radius, rotation));
|
|
2174
|
-
}
|
|
2175
|
-
return paths;
|
|
2176
|
-
}
|
|
2177
|
-
function buildCurvePathData(config, width, fontSize) {
|
|
2178
|
-
return buildCurveLinePaths(config, width, fontSize)?.[0] ?? null;
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
// src/text-curve.ts
|
|
2182
|
-
var MEASURE_WIDTH2 = 1e5;
|
|
2183
|
-
var PATH_SLACK = 0.06;
|
|
2184
|
-
var FALLBACK_LINE_HEIGHT = 1.16;
|
|
2185
|
-
var patches = /* @__PURE__ */ new WeakMap();
|
|
2186
|
-
var dimensionPatches = /* @__PURE__ */ new WeakMap();
|
|
2187
|
-
var authoredCaching = /* @__PURE__ */ new WeakMap();
|
|
2188
|
-
var GLYPH_HALF_HEIGHT = 0.7;
|
|
2189
|
-
function curvedInkSize(text) {
|
|
2190
|
-
const lineCount = text._textLines?.length ?? 0;
|
|
2191
|
-
for (let index = 0; index < lineCount; index += 1) text.getLineWidth?.(index);
|
|
2192
|
-
const lines = text.__charBounds;
|
|
2193
|
-
if (!lines?.length) return null;
|
|
2194
|
-
let halfWidth = 0;
|
|
2195
|
-
let halfHeight = 0;
|
|
2196
|
-
for (const line of lines) {
|
|
2197
|
-
for (const box of line ?? []) {
|
|
2198
|
-
const { renderLeft, renderTop } = box;
|
|
2199
|
-
if (typeof renderLeft !== "number" || typeof renderTop !== "number") continue;
|
|
2200
|
-
const halfGlyphWidth = (box.kernedWidth ?? box.width ?? 0) / 2;
|
|
2201
|
-
const halfGlyphHeight = (box.height ?? text.fontSize) * GLYPH_HALF_HEIGHT;
|
|
2202
|
-
const angle = box.angle ?? 0;
|
|
2203
|
-
const cos = Math.abs(Math.cos(angle));
|
|
2204
|
-
const sin = Math.abs(Math.sin(angle));
|
|
2205
|
-
halfWidth = Math.max(
|
|
2206
|
-
halfWidth,
|
|
2207
|
-
Math.abs(renderLeft) + halfGlyphWidth * cos + halfGlyphHeight * sin
|
|
2208
|
-
);
|
|
2209
|
-
halfHeight = Math.max(
|
|
2210
|
-
halfHeight,
|
|
2211
|
-
Math.abs(renderTop) + halfGlyphWidth * sin + halfGlyphHeight * cos
|
|
2212
|
-
);
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
if (halfWidth === 0 && halfHeight === 0) return null;
|
|
2216
|
-
return { width: halfWidth * 2, height: halfHeight * 2 };
|
|
2217
|
-
}
|
|
2218
|
-
function growBoxToInk(text) {
|
|
2219
|
-
const ink = curvedInkSize(text);
|
|
2220
|
-
if (!ink) return;
|
|
2221
|
-
if (ink.width > (text.width ?? 0)) text.width = ink.width;
|
|
2222
|
-
if (ink.height > (text.height ?? 0)) text.height = ink.height;
|
|
2223
|
-
}
|
|
2224
|
-
function isCurvable(object) {
|
|
2225
|
-
return !!object && typeof object.text === "string";
|
|
2226
|
-
}
|
|
2227
|
-
function measureText(text) {
|
|
2228
|
-
const authored = text.width;
|
|
2229
|
-
try {
|
|
2230
|
-
text.set({ width: MEASURE_WIDTH2 });
|
|
2231
|
-
text.initDimensions?.();
|
|
2232
|
-
return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
|
|
2233
|
-
} finally {
|
|
2234
|
-
if (authored !== void 0) text.set({ width: authored });
|
|
2235
|
-
text.initDimensions?.();
|
|
2271
|
+
// src/shapes.ts
|
|
2272
|
+
function resolveShapeParams(plugin, overrides = {}) {
|
|
2273
|
+
const params = {};
|
|
2274
|
+
for (const def of plugin.params ?? []) {
|
|
2275
|
+
const value = overrides[def.key];
|
|
2276
|
+
const wanted = typeof value === "number" && Number.isFinite(value) ? value : def.default;
|
|
2277
|
+
params[def.key] = clamp(wanted, def.min, def.max);
|
|
2236
2278
|
}
|
|
2279
|
+
return params;
|
|
2237
2280
|
}
|
|
2238
|
-
|
|
2239
|
-
return Math.max(1, text._textLines?.length ?? 1);
|
|
2240
|
-
}
|
|
2241
|
-
function lineHeightOf(text) {
|
|
2242
|
-
const measured = text.getHeightOfLine?.(0);
|
|
2243
|
-
if (typeof measured === "number" && Number.isFinite(measured) && measured > 0) return measured;
|
|
2244
|
-
return text.fontSize * (text.lineHeight ?? 1) * FALLBACK_LINE_HEIGHT;
|
|
2245
|
-
}
|
|
2246
|
-
function toFabricPath(curve) {
|
|
2247
|
-
return new import_fabric7.Path(curve.data, { visible: false, objectCaching: false });
|
|
2248
|
-
}
|
|
2249
|
-
var TextCurveManager = class {
|
|
2281
|
+
var ShapeManager = class {
|
|
2250
2282
|
constructor(canvas, layers, history, events) {
|
|
2251
2283
|
this.canvas = canvas;
|
|
2252
2284
|
this.layers = layers;
|
|
2253
2285
|
this.history = history;
|
|
2254
2286
|
this.events = events;
|
|
2255
|
-
this.canvas.on("
|
|
2287
|
+
this.canvas.on("object:modified", this.onObjectModified);
|
|
2256
2288
|
}
|
|
2257
2289
|
canvas;
|
|
2258
2290
|
layers;
|
|
2259
2291
|
history;
|
|
2260
2292
|
events;
|
|
2293
|
+
plugins = /* @__PURE__ */ new Map();
|
|
2261
2294
|
/**
|
|
2262
|
-
*
|
|
2263
|
-
*
|
|
2264
|
-
*
|
|
2265
|
-
*
|
|
2266
|
-
*
|
|
2267
|
-
* No history entry: fabric records the edit when editing exits, and one save
|
|
2268
|
-
* per keystroke would bury every earlier step under the typing.
|
|
2295
|
+
* An arrow scaled 2×1 must not stretch its head: the new box is baked into
|
|
2296
|
+
* the points and the scale returns to 1. Runs before the editor's own
|
|
2297
|
+
* `object:modified` handler (constructed earlier), so history records the
|
|
2298
|
+
* baked state.
|
|
2269
2299
|
*/
|
|
2270
|
-
|
|
2300
|
+
onObjectModified = (event) => {
|
|
2301
|
+
const target = event.target;
|
|
2302
|
+
if (!target) return;
|
|
2303
|
+
const layer = this.layers.findByObject(target);
|
|
2304
|
+
const state = layer?.meta.shape;
|
|
2305
|
+
if (!layer || !state || target !== layer.fabricObject) return;
|
|
2306
|
+
const plugin = this.plugins.get(state.id);
|
|
2307
|
+
if (!plugin?.bakesScale || !plugin.regenerate) return;
|
|
2308
|
+
const { scaleX = 1, scaleY = 1 } = target;
|
|
2309
|
+
if (Math.abs(scaleX - scaleY) < 1e-6) return;
|
|
2310
|
+
target.set({
|
|
2311
|
+
width: target.width * scaleX,
|
|
2312
|
+
height: target.height * scaleY,
|
|
2313
|
+
scaleX: 1,
|
|
2314
|
+
scaleY: 1
|
|
2315
|
+
});
|
|
2316
|
+
plugin.regenerate(target, state.params);
|
|
2317
|
+
target.setCoords();
|
|
2318
|
+
target.dirty = true;
|
|
2319
|
+
this.canvas.requestRenderAll();
|
|
2320
|
+
};
|
|
2321
|
+
dispose() {
|
|
2322
|
+
this.canvas.off("object:modified", this.onObjectModified);
|
|
2323
|
+
}
|
|
2324
|
+
/** A restored layer names its plugin by id; only a registered one can regenerate. */
|
|
2325
|
+
register(plugin) {
|
|
2326
|
+
this.plugins.set(plugin.name, plugin);
|
|
2327
|
+
}
|
|
2328
|
+
read(layerId) {
|
|
2329
|
+
return this.layers.get(layerId)?.meta.shape ?? null;
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* `commit: false` is a slider drag: geometry and `meta.shape` are written and
|
|
2333
|
+
* `layer:modified` is emitted so the Shape panel re-reads, but nothing reaches
|
|
2334
|
+
* history. The default commits one entry.
|
|
2335
|
+
*/
|
|
2336
|
+
setParams(layerId, patch, commit = true) {
|
|
2337
|
+
const layer = this.layers.get(layerId);
|
|
2338
|
+
const state = layer?.meta.shape;
|
|
2339
|
+
if (!layer || !state) return;
|
|
2340
|
+
const plugin = this.plugins.get(state.id);
|
|
2341
|
+
if (!plugin?.regenerate) return;
|
|
2342
|
+
const params = resolveShapeParams(plugin, { ...state.params, ...patch });
|
|
2343
|
+
plugin.regenerate(layer.fabricObject, params);
|
|
2344
|
+
layer.fabricObject.setCoords();
|
|
2345
|
+
layer.fabricObject.dirty = true;
|
|
2346
|
+
const next = { ...state, params };
|
|
2347
|
+
if (commit) this.layers.setMeta(layerId, { shape: next });
|
|
2348
|
+
else layer.meta.shape = next;
|
|
2349
|
+
this.canvas.requestRenderAll();
|
|
2350
|
+
this.events.emit("layer:modified", { layerId });
|
|
2351
|
+
if (commit) this.history.save();
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
|
|
2355
|
+
// src/image-effects/manager.ts
|
|
2356
|
+
var import_fabric9 = require("fabric");
|
|
2357
|
+
|
|
2358
|
+
// src/image-effects/presets.ts
|
|
2359
|
+
var LUMA = [0.299, 0.587, 0.114];
|
|
2360
|
+
var rows = (r, g, b) => [...r, ...g, ...b, 0, 0, 0, 1, 0];
|
|
2361
|
+
var IMAGE_FILTER_PRESETS = [
|
|
2362
|
+
{ id: "original", name: "Original", version: 1, adjust: {} },
|
|
2363
|
+
{
|
|
2364
|
+
id: "black-white",
|
|
2365
|
+
name: "Black & White",
|
|
2366
|
+
version: 1,
|
|
2367
|
+
adjust: {},
|
|
2368
|
+
matrix: rows([...LUMA, 0, 0], [...LUMA, 0, 0], [...LUMA, 0, 0])
|
|
2369
|
+
},
|
|
2370
|
+
{
|
|
2371
|
+
id: "sepia",
|
|
2372
|
+
name: "Sepia",
|
|
2373
|
+
version: 1,
|
|
2374
|
+
adjust: {},
|
|
2375
|
+
matrix: rows(
|
|
2376
|
+
[0.393, 0.769, 0.189, 0, 0],
|
|
2377
|
+
[0.349, 0.686, 0.168, 0, 0],
|
|
2378
|
+
[0.272, 0.534, 0.131, 0, 0]
|
|
2379
|
+
)
|
|
2380
|
+
},
|
|
2381
|
+
{
|
|
2382
|
+
id: "fade",
|
|
2383
|
+
name: "Fade",
|
|
2384
|
+
version: 1,
|
|
2385
|
+
adjust: { contrast: -0.2, saturation: -0.25, brightness: 0.08 }
|
|
2386
|
+
},
|
|
2387
|
+
{
|
|
2388
|
+
id: "vivid",
|
|
2389
|
+
name: "Vivid",
|
|
2390
|
+
version: 1,
|
|
2391
|
+
adjust: { vibrance: 0.5, contrast: 0.15, saturation: 0.15 }
|
|
2392
|
+
},
|
|
2393
|
+
{ id: "fuchsia", name: "Fuchsia", version: 1, adjust: { tint: 0.5, saturation: 0.2 } },
|
|
2394
|
+
{ id: "cool", name: "Cool", version: 1, adjust: { temperature: -0.45 } },
|
|
2395
|
+
{ id: "warm", name: "Warm", version: 1, adjust: { temperature: 0.45 } },
|
|
2396
|
+
{
|
|
2397
|
+
id: "duotone",
|
|
2398
|
+
name: "Duotone",
|
|
2399
|
+
version: 1,
|
|
2400
|
+
adjust: {},
|
|
2401
|
+
matrix: rows(
|
|
2402
|
+
[0.2544, 0.4995, 0.097, 0, 0.1059],
|
|
2403
|
+
[0.2216, 0.435, 0.0845, 0, 0.1647],
|
|
2404
|
+
[0.1453, 0.2853, 0.0554, 0, 0.2863]
|
|
2405
|
+
)
|
|
2406
|
+
},
|
|
2407
|
+
{
|
|
2408
|
+
id: "nashville",
|
|
2409
|
+
name: "Nashville",
|
|
2410
|
+
version: 1,
|
|
2411
|
+
adjust: { temperature: 0.25, tint: -0.1, contrast: 0.2, saturation: 0.15, brightness: 0.05 }
|
|
2412
|
+
},
|
|
2413
|
+
{
|
|
2414
|
+
id: "valencia",
|
|
2415
|
+
name: "Valencia",
|
|
2416
|
+
version: 1,
|
|
2417
|
+
adjust: { temperature: 0.2, contrast: 0.08, brightness: 0.08, saturation: -0.05 }
|
|
2418
|
+
},
|
|
2419
|
+
{
|
|
2420
|
+
id: "clarendon",
|
|
2421
|
+
name: "Clarendon",
|
|
2422
|
+
version: 1,
|
|
2423
|
+
adjust: { contrast: 0.2, saturation: 0.35, temperature: -0.1 }
|
|
2424
|
+
},
|
|
2425
|
+
{
|
|
2426
|
+
id: "reyes",
|
|
2427
|
+
name: "Reyes",
|
|
2428
|
+
version: 1,
|
|
2429
|
+
adjust: { brightness: 0.1, contrast: -0.15, saturation: -0.35, temperature: 0.15 }
|
|
2430
|
+
},
|
|
2431
|
+
{
|
|
2432
|
+
id: "lark",
|
|
2433
|
+
name: "Lark",
|
|
2434
|
+
version: 1,
|
|
2435
|
+
adjust: { brightness: 0.08, contrast: -0.05, saturation: 0.1, temperature: -0.15 }
|
|
2436
|
+
},
|
|
2437
|
+
{
|
|
2438
|
+
id: "juno",
|
|
2439
|
+
name: "Juno",
|
|
2440
|
+
version: 1,
|
|
2441
|
+
adjust: { saturation: 0.3, contrast: 0.1, temperature: 0.15, tint: 0.1 }
|
|
2442
|
+
}
|
|
2443
|
+
];
|
|
2444
|
+
function imageFilterPreset(id) {
|
|
2445
|
+
return IMAGE_FILTER_PRESETS.find((preset) => preset.id === id);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// src/image-effects/proxy.ts
|
|
2449
|
+
var import_fabric8 = require("fabric");
|
|
2450
|
+
function sourceSize(source) {
|
|
2451
|
+
const el = source;
|
|
2452
|
+
return { width: el.naturalWidth || el.width, height: el.naturalHeight || el.height };
|
|
2453
|
+
}
|
|
2454
|
+
function proxyScale(width, height, maxEdge) {
|
|
2455
|
+
const edge = Math.max(width, height);
|
|
2456
|
+
return edge > maxEdge ? maxEdge / edge : 1;
|
|
2457
|
+
}
|
|
2458
|
+
function buildProxySource(source, maxEdge) {
|
|
2459
|
+
const { width, height } = sourceSize(source);
|
|
2460
|
+
const scale = proxyScale(width, height, maxEdge);
|
|
2461
|
+
if (scale === 1) return null;
|
|
2462
|
+
const element = import_fabric8.util.createCanvasElement();
|
|
2463
|
+
element.width = Math.max(1, Math.round(width * scale));
|
|
2464
|
+
element.height = Math.max(1, Math.round(height * scale));
|
|
2465
|
+
const context = element.getContext("2d");
|
|
2466
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
2467
|
+
context.drawImage(source, 0, 0, element.width, element.height);
|
|
2468
|
+
return { element, width: element.width, height: element.height };
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// src/image-effects/types.ts
|
|
2472
|
+
var ADJUST_KEYS = [
|
|
2473
|
+
"temperature",
|
|
2474
|
+
"tint",
|
|
2475
|
+
"brightness",
|
|
2476
|
+
"contrast",
|
|
2477
|
+
"highlights",
|
|
2478
|
+
"shadows",
|
|
2479
|
+
"whites",
|
|
2480
|
+
"blacks",
|
|
2481
|
+
"invert",
|
|
2482
|
+
"vibrance",
|
|
2483
|
+
"saturation",
|
|
2484
|
+
"hue",
|
|
2485
|
+
"sharpness",
|
|
2486
|
+
"clarity",
|
|
2487
|
+
"vignette",
|
|
2488
|
+
"blur",
|
|
2489
|
+
"noise",
|
|
2490
|
+
"pixelate"
|
|
2491
|
+
];
|
|
2492
|
+
var NEUTRAL_ADJUST = Object.fromEntries(
|
|
2493
|
+
ADJUST_KEYS.map((k) => [k, 0])
|
|
2494
|
+
);
|
|
2495
|
+
var ADJUST_RANGES = {
|
|
2496
|
+
invert: [0, 1],
|
|
2497
|
+
vignette: [0, 1],
|
|
2498
|
+
blur: [0, 1],
|
|
2499
|
+
noise: [0, 1],
|
|
2500
|
+
pixelate: [0, 1]
|
|
2501
|
+
};
|
|
2502
|
+
var IMAGE_PROXY_MAX_EDGE = 2048;
|
|
2503
|
+
|
|
2504
|
+
// src/image-effects/specs.ts
|
|
2505
|
+
var ORDER = [
|
|
2506
|
+
"highlights",
|
|
2507
|
+
"shadows",
|
|
2508
|
+
"whites",
|
|
2509
|
+
"blacks",
|
|
2510
|
+
"brightness",
|
|
2511
|
+
"contrast",
|
|
2512
|
+
"temperature",
|
|
2513
|
+
"tint",
|
|
2514
|
+
"vibrance",
|
|
2515
|
+
"saturation",
|
|
2516
|
+
"hue",
|
|
2517
|
+
"invert",
|
|
2518
|
+
"clarity",
|
|
2519
|
+
"sharpness",
|
|
2520
|
+
"noise",
|
|
2521
|
+
"pixelate",
|
|
2522
|
+
"blur",
|
|
2523
|
+
"vignette"
|
|
2524
|
+
];
|
|
2525
|
+
function clampAdjust(adjust) {
|
|
2526
|
+
const out = {};
|
|
2527
|
+
for (const key of ADJUST_KEYS) {
|
|
2528
|
+
const value = adjust[key];
|
|
2529
|
+
if (typeof value !== "number" || !Number.isFinite(value)) continue;
|
|
2530
|
+
const [min, max] = ADJUST_RANGES[key] ?? [-1, 1];
|
|
2531
|
+
out[key] = key === "invert" ? value >= 0.5 ? 1 : 0 : clamp(value, min, max);
|
|
2532
|
+
}
|
|
2533
|
+
return out;
|
|
2534
|
+
}
|
|
2535
|
+
function clampAlphaClip(clip) {
|
|
2536
|
+
if (!clip || typeof clip !== "object") return void 0;
|
|
2537
|
+
const high = clamp(Number(clip.high) || 0, 1, 255);
|
|
2538
|
+
const low = clamp(Math.min(Number(clip.low) || 0, high - 1), 0, 254);
|
|
2539
|
+
return low === 0 && high === 255 ? void 0 : { low, high };
|
|
2540
|
+
}
|
|
2541
|
+
function alphaClipMatrix({ low, high }) {
|
|
2542
|
+
const span = Math.max(1, high - low);
|
|
2543
|
+
return [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 255 / span, -low / span];
|
|
2544
|
+
}
|
|
2545
|
+
function specFor(key, v) {
|
|
2546
|
+
if (v === 0) return null;
|
|
2547
|
+
switch (key) {
|
|
2548
|
+
case "highlights":
|
|
2549
|
+
return { type: "Highlights", amount: v };
|
|
2550
|
+
case "shadows":
|
|
2551
|
+
return { type: "Shadows", amount: v };
|
|
2552
|
+
case "whites":
|
|
2553
|
+
return { type: "Whites", amount: v };
|
|
2554
|
+
case "blacks":
|
|
2555
|
+
return { type: "Blacks", amount: v };
|
|
2556
|
+
case "brightness":
|
|
2557
|
+
return { type: "Brightness", brightness: v };
|
|
2558
|
+
case "contrast":
|
|
2559
|
+
return { type: "Contrast", contrast: v };
|
|
2560
|
+
case "temperature":
|
|
2561
|
+
return { type: "Temperature", amount: v };
|
|
2562
|
+
case "tint":
|
|
2563
|
+
return { type: "Tint", amount: v };
|
|
2564
|
+
case "vibrance":
|
|
2565
|
+
return { type: "Vibrance", vibrance: v };
|
|
2566
|
+
case "saturation":
|
|
2567
|
+
return { type: "Saturation", saturation: v };
|
|
2568
|
+
case "hue":
|
|
2569
|
+
return { type: "HueRotation", rotation: v };
|
|
2570
|
+
case "invert":
|
|
2571
|
+
return { type: "Invert", alpha: false, invert: true };
|
|
2572
|
+
case "clarity":
|
|
2573
|
+
return { type: "Clarity", amount: v };
|
|
2574
|
+
case "sharpness":
|
|
2575
|
+
return { type: "Convolute", opaque: false, matrix: [0, -v, 0, -v, 1 + 4 * v, -v, 0, -v, 0] };
|
|
2576
|
+
case "noise":
|
|
2577
|
+
return { type: "Noise", noise: Math.round(v * 300) };
|
|
2578
|
+
case "pixelate":
|
|
2579
|
+
return { type: "Pixelate", blocksize: Math.max(1, Math.round(1 + v * 40)) };
|
|
2580
|
+
case "blur":
|
|
2581
|
+
return { type: "Blur", blur: v };
|
|
2582
|
+
case "vignette":
|
|
2583
|
+
return { type: "Vignette", amount: v };
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
function imageEffectsFilterSpecs(state) {
|
|
2587
|
+
const specs = [];
|
|
2588
|
+
const preset = state.preset ? imageFilterPreset(state.preset.id) : void 0;
|
|
2589
|
+
if (preset?.matrix)
|
|
2590
|
+
specs.push({ type: "ColorMatrix", matrix: [...preset.matrix], colorsOnly: true });
|
|
2591
|
+
const adjust = clampAdjust(state.adjust);
|
|
2592
|
+
for (const key of ORDER) {
|
|
2593
|
+
const spec = specFor(key, adjust[key] ?? 0);
|
|
2594
|
+
if (spec) specs.push(spec);
|
|
2595
|
+
}
|
|
2596
|
+
const clip = clampAlphaClip(state.alphaClip);
|
|
2597
|
+
if (clip) specs.push({ type: "ColorMatrix", matrix: alphaClipMatrix(clip), colorsOnly: false });
|
|
2598
|
+
return specs;
|
|
2599
|
+
}
|
|
2600
|
+
function normalizeImageEffects(meta) {
|
|
2601
|
+
const alphaClip = clampAlphaClip(meta.imageAlphaClip);
|
|
2602
|
+
const withClip = (state) => alphaClip ? { ...state, alphaClip } : state;
|
|
2603
|
+
const current = meta.imageEffects;
|
|
2604
|
+
if (current && typeof current === "object") {
|
|
2605
|
+
const preset = current.preset && imageFilterPreset(current.preset.id) ? { id: current.preset.id, version: current.preset.version } : void 0;
|
|
2606
|
+
return withClip({ ...preset ? { preset } : {}, adjust: clampAdjust(current.adjust ?? {}) });
|
|
2607
|
+
}
|
|
2608
|
+
const legacy = meta.imageAdjustments;
|
|
2609
|
+
return withClip({ adjust: clampAdjust(legacy ?? {}) });
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
// src/image-effects/manager.ts
|
|
2613
|
+
var stable = (value) => JSON.stringify(
|
|
2614
|
+
value,
|
|
2615
|
+
(_k, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort()) : v
|
|
2616
|
+
);
|
|
2617
|
+
var ImageEffectsManager = class {
|
|
2618
|
+
constructor(canvas, layers, history, events) {
|
|
2619
|
+
this.canvas = canvas;
|
|
2620
|
+
this.layers = layers;
|
|
2621
|
+
this.history = history;
|
|
2622
|
+
this.events = events;
|
|
2623
|
+
this.events.on("layer:removed", this.onRemoved);
|
|
2624
|
+
}
|
|
2625
|
+
canvas;
|
|
2626
|
+
layers;
|
|
2627
|
+
history;
|
|
2628
|
+
events;
|
|
2629
|
+
proxies = /* @__PURE__ */ new Map();
|
|
2630
|
+
lastSpecs = /* @__PURE__ */ new Map();
|
|
2631
|
+
onRemoved = ({ layerId }) => this.drop(layerId);
|
|
2632
|
+
read(layerId) {
|
|
2633
|
+
const layer = this.imageLayer(layerId);
|
|
2634
|
+
return layer ? normalizeImageEffects(layer.meta) : { adjust: {} };
|
|
2635
|
+
}
|
|
2636
|
+
/** The filter instances a state derives to, in the D5 order, with no layer involved. */
|
|
2637
|
+
filtersFor(state) {
|
|
2638
|
+
return imageEffectsFilterSpecs(state).map(
|
|
2639
|
+
(spec) => new (import_fabric9.classRegistry.getClass(spec.type))(spec)
|
|
2640
|
+
);
|
|
2641
|
+
}
|
|
2642
|
+
preview(layerId, patch) {
|
|
2643
|
+
this.write(layerId, patch);
|
|
2644
|
+
}
|
|
2645
|
+
commit(layerId, patch) {
|
|
2646
|
+
if (this.write(layerId, patch)) this.history.save();
|
|
2647
|
+
}
|
|
2648
|
+
reset(layerId) {
|
|
2649
|
+
if (this.write(layerId, { preset: void 0, adjust: {} }, true)) this.history.save();
|
|
2650
|
+
}
|
|
2651
|
+
/** Re-derive after a source swap or a load. */
|
|
2652
|
+
refresh(layerId) {
|
|
2653
|
+
const layer = this.imageLayer(layerId);
|
|
2654
|
+
if (layer) this.apply(layer, this.read(layerId));
|
|
2655
|
+
}
|
|
2656
|
+
refreshAll() {
|
|
2657
|
+
for (const id of [...this.proxies.keys()]) if (!this.layers.get(id)) this.drop(id);
|
|
2658
|
+
for (const layer of this.layers.getAll()) {
|
|
2659
|
+
if (layer.type !== "image") continue;
|
|
2660
|
+
if ("imageAdjustments" in layer.meta) {
|
|
2661
|
+
const migrated = normalizeImageEffects(layer.meta);
|
|
2662
|
+
layer.meta.imageEffects = {
|
|
2663
|
+
...migrated.preset ? { preset: migrated.preset } : {},
|
|
2664
|
+
adjust: migrated.adjust
|
|
2665
|
+
};
|
|
2666
|
+
delete layer.meta.imageAdjustments;
|
|
2667
|
+
}
|
|
2668
|
+
this.apply(layer, this.read(layer.id));
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
hasProxy(layerId) {
|
|
2672
|
+
return this.proxies.has(layerId);
|
|
2673
|
+
}
|
|
2674
|
+
/** Run `fn` with every proxied image filtered at source resolution (export), then restore the proxies. */
|
|
2675
|
+
withSourceResolution(fn) {
|
|
2676
|
+
const swapped = [];
|
|
2677
|
+
for (const id of this.proxies.keys()) {
|
|
2678
|
+
const layer = this.imageLayer(id);
|
|
2679
|
+
if (!layer) continue;
|
|
2680
|
+
const image = layer.fabricObject;
|
|
2681
|
+
image._element = image._originalElement;
|
|
2682
|
+
image._filteredEl = void 0;
|
|
2683
|
+
image._filterScalingX = 1;
|
|
2684
|
+
image._filterScalingY = 1;
|
|
2685
|
+
image.applyFilters();
|
|
2686
|
+
swapped.push(layer);
|
|
2687
|
+
}
|
|
2688
|
+
try {
|
|
2689
|
+
return fn();
|
|
2690
|
+
} finally {
|
|
2691
|
+
for (const layer of swapped) {
|
|
2692
|
+
const image = layer.fabricObject;
|
|
2693
|
+
if (image._filteredEl) image._filteredEl.width = 0;
|
|
2694
|
+
image._filteredEl = void 0;
|
|
2695
|
+
image._element = image._originalElement;
|
|
2696
|
+
this.render(layer);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
dispose() {
|
|
2701
|
+
this.events.off("layer:removed", this.onRemoved);
|
|
2702
|
+
for (const id of [...this.proxies.keys()]) this.drop(id);
|
|
2703
|
+
this.lastSpecs.clear();
|
|
2704
|
+
}
|
|
2705
|
+
imageLayer(layerId) {
|
|
2706
|
+
const layer = this.layers.get(layerId);
|
|
2707
|
+
return layer && layer.type === "image" ? layer : void 0;
|
|
2708
|
+
}
|
|
2709
|
+
write(layerId, patch, replace = false) {
|
|
2710
|
+
const layer = this.imageLayer(layerId);
|
|
2711
|
+
if (!layer) return false;
|
|
2712
|
+
const current = replace ? { adjust: {} } : this.read(layerId);
|
|
2713
|
+
const presetChanged = "preset" in patch && stable(patch.preset) !== stable(current.preset);
|
|
2714
|
+
const seed = presetChanged && patch.preset ? imageFilterPreset(patch.preset.id)?.adjust ?? {} : current.adjust;
|
|
2715
|
+
const preset = "preset" in patch ? patch.preset : current.preset;
|
|
2716
|
+
const alphaClip = clampAlphaClip("alphaClip" in patch ? patch.alphaClip : current.alphaClip);
|
|
2717
|
+
const next = {
|
|
2718
|
+
...preset ? { preset } : {},
|
|
2719
|
+
adjust: { ...seed, ...patch.adjust ?? {} },
|
|
2720
|
+
...alphaClip ? { alphaClip } : {}
|
|
2721
|
+
};
|
|
2722
|
+
layer.meta.imageEffects = { ...preset ? { preset } : {}, adjust: next.adjust };
|
|
2723
|
+
if (alphaClip) layer.meta.imageAlphaClip = alphaClip;
|
|
2724
|
+
else delete layer.meta.imageAlphaClip;
|
|
2725
|
+
delete layer.meta.imageAdjustments;
|
|
2726
|
+
this.apply(layer, next);
|
|
2727
|
+
this.events.emit("layer:modified", { layerId });
|
|
2728
|
+
return true;
|
|
2729
|
+
}
|
|
2730
|
+
apply(layer, state) {
|
|
2731
|
+
const image = layer.fabricObject;
|
|
2732
|
+
const specs = imageEffectsFilterSpecs(state);
|
|
2733
|
+
const previous = this.lastSpecs.get(layer.id) ?? specs;
|
|
2734
|
+
const clip = clampAlphaClip(state.alphaClip);
|
|
2735
|
+
const legacyClip = clip ? stable(alphaClipMatrix(clip)) : null;
|
|
2736
|
+
const foreign = image.filters.filter((f) => {
|
|
2737
|
+
if (f.isNeutralState()) return false;
|
|
2738
|
+
const plain = f.toObject();
|
|
2739
|
+
if (legacyClip && plain.type === "ColorMatrix" && stable(plain.matrix) === legacyClip)
|
|
2740
|
+
return false;
|
|
2741
|
+
return !previous.some((s) => stable(s) === stable(plain));
|
|
2742
|
+
});
|
|
2743
|
+
const managed = specs.map((spec) => new (import_fabric9.classRegistry.getClass(spec.type))(spec));
|
|
2744
|
+
image.filters = [...managed, ...foreign];
|
|
2745
|
+
this.lastSpecs.set(layer.id, specs);
|
|
2746
|
+
this.render(layer);
|
|
2747
|
+
}
|
|
2748
|
+
render(layer) {
|
|
2749
|
+
const image = layer.fabricObject;
|
|
2750
|
+
const record = this.proxyFor(layer.id, image);
|
|
2751
|
+
if (!record) {
|
|
2752
|
+
image.applyFilters();
|
|
2753
|
+
} else {
|
|
2754
|
+
const active = image.filters.filter((f) => !f.isNeutralState());
|
|
2755
|
+
if (active.length === 0) {
|
|
2756
|
+
image._element = image._originalElement;
|
|
2757
|
+
image._filterScalingX = 1;
|
|
2758
|
+
image._filterScalingY = 1;
|
|
2759
|
+
} else {
|
|
2760
|
+
(0, import_fabric9.getFilterBackend)().applyFilters(
|
|
2761
|
+
active,
|
|
2762
|
+
record.source.element,
|
|
2763
|
+
record.source.width,
|
|
2764
|
+
record.source.height,
|
|
2765
|
+
record.target,
|
|
2766
|
+
`${image.cacheKey}_proxy`
|
|
2767
|
+
);
|
|
2768
|
+
const { width, height } = sourceSize(image._originalElement);
|
|
2769
|
+
image._element = record.target;
|
|
2770
|
+
image._filterScalingX = record.source.width / width;
|
|
2771
|
+
image._filterScalingY = record.source.height / height;
|
|
2772
|
+
}
|
|
2773
|
+
image.set("dirty", true);
|
|
2774
|
+
}
|
|
2775
|
+
this.canvas.requestRenderAll();
|
|
2776
|
+
}
|
|
2777
|
+
proxyFor(layerId, image) {
|
|
2778
|
+
const existing = this.proxies.get(layerId);
|
|
2779
|
+
if (existing && existing.original === image._originalElement) return existing;
|
|
2780
|
+
if (existing) this.drop(layerId);
|
|
2781
|
+
const source = buildProxySource(image._originalElement, IMAGE_PROXY_MAX_EDGE);
|
|
2782
|
+
if (!source) return null;
|
|
2783
|
+
const target = source.element.ownerDocument.createElement("canvas");
|
|
2784
|
+
target.width = source.width;
|
|
2785
|
+
target.height = source.height;
|
|
2786
|
+
const record = { original: image._originalElement, source, target };
|
|
2787
|
+
this.proxies.set(layerId, record);
|
|
2788
|
+
return record;
|
|
2789
|
+
}
|
|
2790
|
+
drop(layerId) {
|
|
2791
|
+
const record = this.proxies.get(layerId);
|
|
2792
|
+
if (!record) return;
|
|
2793
|
+
const layer = this.layers.get(layerId);
|
|
2794
|
+
if (layer)
|
|
2795
|
+
layer.fabricObject.removeTexture(
|
|
2796
|
+
`${layer.fabricObject.cacheKey}_proxy`
|
|
2797
|
+
);
|
|
2798
|
+
record.source.element.width = 0;
|
|
2799
|
+
record.target.width = 0;
|
|
2800
|
+
this.proxies.delete(layerId);
|
|
2801
|
+
this.lastSpecs.delete(layerId);
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
|
|
2805
|
+
// src/layer-effects.ts
|
|
2806
|
+
var import_canvas_editor_effects = require("@overtone-art/canvas-editor-effects");
|
|
2807
|
+
|
|
2808
|
+
// src/shadow.ts
|
|
2809
|
+
var import_fabric10 = require("fabric");
|
|
2810
|
+
|
|
2811
|
+
// src/utils/color.ts
|
|
2812
|
+
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
2813
|
+
function isCssColor(value, allowEmpty = false) {
|
|
2814
|
+
if (typeof value !== "string") return false;
|
|
2815
|
+
const trimmed = value.trim();
|
|
2816
|
+
if (!trimmed) return allowEmpty;
|
|
2817
|
+
return CSS_COLOR.test(trimmed);
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
// src/shadow.ts
|
|
2821
|
+
var DEFAULT_LAYER_SHADOW = {
|
|
2822
|
+
enabled: false,
|
|
2823
|
+
color: "#000000",
|
|
2824
|
+
blur: 12,
|
|
2825
|
+
offsetX: 6,
|
|
2826
|
+
offsetY: 6
|
|
2827
|
+
};
|
|
2828
|
+
function readLayerShadow(object) {
|
|
2829
|
+
const shadow = object.shadow;
|
|
2830
|
+
if (!shadow || typeof shadow === "string") return { ...DEFAULT_LAYER_SHADOW };
|
|
2831
|
+
return {
|
|
2832
|
+
enabled: true,
|
|
2833
|
+
color: typeof shadow.color === "string" ? shadow.color : DEFAULT_LAYER_SHADOW.color,
|
|
2834
|
+
blur: shadow.blur ?? DEFAULT_LAYER_SHADOW.blur,
|
|
2835
|
+
offsetX: shadow.offsetX ?? DEFAULT_LAYER_SHADOW.offsetX,
|
|
2836
|
+
offsetY: shadow.offsetY ?? DEFAULT_LAYER_SHADOW.offsetY
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
function applyLayerShadow(object, config) {
|
|
2840
|
+
const next = { ...readLayerShadow(object), ...config };
|
|
2841
|
+
if (!next.enabled) {
|
|
2842
|
+
object.set({ shadow: null });
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
const color2 = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
|
|
2846
|
+
object.set({
|
|
2847
|
+
shadow: new import_fabric10.Shadow({
|
|
2848
|
+
color: color2,
|
|
2849
|
+
blur: Math.max(0, next.blur),
|
|
2850
|
+
offsetX: next.offsetX,
|
|
2851
|
+
offsetY: next.offsetY
|
|
2852
|
+
})
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
// src/layer-effects.ts
|
|
2857
|
+
var LAYER_EFFECT_DEFAULTS = {
|
|
2858
|
+
outline: { color: "#000000", width: 4 },
|
|
2859
|
+
hollow: { color: "#000000", width: 4 },
|
|
2860
|
+
drop: { color: "#000000", blur: 0, offsetX: 6, offsetY: 6 },
|
|
2861
|
+
splice: { color: "#ff4d6d", offsetX: 8, offsetY: 8 },
|
|
2862
|
+
echo: { color: "#000000", count: 3, stepX: 6, stepY: 6, fade: 0.5 },
|
|
2863
|
+
background: { color: "#ffe600", padding: 12, radius: 8 }
|
|
2864
|
+
};
|
|
2865
|
+
var num = (p, key, fallback) => {
|
|
2866
|
+
const value = p[key];
|
|
2867
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2868
|
+
};
|
|
2869
|
+
var color = (p, key, fallback) => {
|
|
2870
|
+
const value = p[key];
|
|
2871
|
+
return typeof value === "string" && isCssColor(value) ? value : fallback;
|
|
2872
|
+
};
|
|
2873
|
+
var outlineProps = (p) => ({
|
|
2874
|
+
stroke: color(p, "color", "#000000"),
|
|
2875
|
+
strokeWidth: clamp(num(p, "width", 4), 0, 200),
|
|
2876
|
+
paintFirst: "stroke",
|
|
2877
|
+
strokeUniform: true
|
|
2878
|
+
});
|
|
2879
|
+
var LayerEffectsManager = class {
|
|
2880
|
+
constructor(canvas, layers, history, events) {
|
|
2881
|
+
this.canvas = canvas;
|
|
2882
|
+
this.layers = layers;
|
|
2883
|
+
this.history = history;
|
|
2884
|
+
this.events = events;
|
|
2885
|
+
}
|
|
2886
|
+
canvas;
|
|
2887
|
+
layers;
|
|
2888
|
+
history;
|
|
2889
|
+
events;
|
|
2890
|
+
read(layerId) {
|
|
2891
|
+
return this.layers.get(layerId)?.meta.effect ?? null;
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* `commit: false` is a slider drag: props are written and `layer:modified`
|
|
2895
|
+
* is emitted so the panels re-read, but nothing reaches history. The default
|
|
2896
|
+
* commits ONE entry, setMeta's own save folded into the same transaction.
|
|
2897
|
+
*/
|
|
2898
|
+
apply(layerId, kind, params = {}, commit = true) {
|
|
2899
|
+
const layer = this.layers.get(layerId);
|
|
2900
|
+
if (!layer || layer.type === "group") return;
|
|
2901
|
+
const object = layer.fabricObject;
|
|
2902
|
+
const write = () => {
|
|
2903
|
+
this.clear(object, layer.meta.effect);
|
|
2904
|
+
const next = kind === "none" ? void 0 : {
|
|
2905
|
+
kind,
|
|
2906
|
+
params: this.install(object, kind, { ...LAYER_EFFECT_DEFAULTS[kind], ...params }),
|
|
2907
|
+
version: 1
|
|
2908
|
+
};
|
|
2909
|
+
if (commit) this.layers.setMeta(layerId, { effect: next });
|
|
2910
|
+
else if (next) layer.meta.effect = next;
|
|
2911
|
+
else delete layer.meta.effect;
|
|
2912
|
+
object.dirty = true;
|
|
2913
|
+
object.setCoords();
|
|
2914
|
+
this.canvas.requestRenderAll();
|
|
2915
|
+
this.events.emit("layer:modified", { layerId });
|
|
2916
|
+
};
|
|
2917
|
+
if (!commit) {
|
|
2918
|
+
write();
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
this.history.beginTransaction();
|
|
2922
|
+
try {
|
|
2923
|
+
write();
|
|
2924
|
+
this.history.save();
|
|
2925
|
+
} finally {
|
|
2926
|
+
this.history.endTransaction();
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
/** Undo what the previous kind wrote, so kinds never stack. */
|
|
2930
|
+
clear(object, previous) {
|
|
2931
|
+
if (!previous) return;
|
|
2932
|
+
switch (previous.kind) {
|
|
2933
|
+
case "outline":
|
|
2934
|
+
object.set({ strokeWidth: 0, paintFirst: "fill", strokeUniform: false });
|
|
2935
|
+
return;
|
|
2936
|
+
case "hollow": {
|
|
2937
|
+
const fill = previous.params.previousFill;
|
|
2938
|
+
object.set({
|
|
2939
|
+
strokeWidth: 0,
|
|
2940
|
+
paintFirst: "fill",
|
|
2941
|
+
strokeUniform: false,
|
|
2942
|
+
fill: typeof fill === "string" ? fill : "#000000"
|
|
2943
|
+
});
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
case "drop":
|
|
2947
|
+
applyLayerShadow(object, { enabled: false });
|
|
2948
|
+
return;
|
|
2949
|
+
case "splice":
|
|
2950
|
+
case "echo":
|
|
2951
|
+
case "background":
|
|
2952
|
+
object.set({ effect: void 0 });
|
|
2953
|
+
return;
|
|
2954
|
+
case "none":
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
install(object, kind, p) {
|
|
2959
|
+
switch (kind) {
|
|
2960
|
+
case "outline": {
|
|
2961
|
+
const props = outlineProps(p);
|
|
2962
|
+
object.set(props);
|
|
2963
|
+
return { color: props.stroke, width: props.strokeWidth };
|
|
2964
|
+
}
|
|
2965
|
+
case "hollow": {
|
|
2966
|
+
const previousFill = typeof object.fill === "string" ? object.fill : "#000000";
|
|
2967
|
+
const props = outlineProps(p);
|
|
2968
|
+
object.set({ ...props, fill: "transparent" });
|
|
2969
|
+
return { color: props.stroke, width: props.strokeWidth, previousFill };
|
|
2970
|
+
}
|
|
2971
|
+
case "drop": {
|
|
2972
|
+
const shadow = {
|
|
2973
|
+
color: color(p, "color", "#000000"),
|
|
2974
|
+
blur: clamp(num(p, "blur", 0), 0, 200),
|
|
2975
|
+
offsetX: num(p, "offsetX", 6),
|
|
2976
|
+
offsetY: num(p, "offsetY", 6)
|
|
2977
|
+
};
|
|
2978
|
+
applyLayerShadow(object, { enabled: true, ...shadow });
|
|
2979
|
+
return shadow;
|
|
2980
|
+
}
|
|
2981
|
+
case "splice": {
|
|
2982
|
+
const effect = {
|
|
2983
|
+
kind: "splice",
|
|
2984
|
+
offsetX: num(p, "offsetX", 8),
|
|
2985
|
+
offsetY: num(p, "offsetY", 8),
|
|
2986
|
+
color: color(p, "color", "#ff4d6d"),
|
|
2987
|
+
version: 1
|
|
2988
|
+
};
|
|
2989
|
+
object.set({ effect });
|
|
2990
|
+
return { offsetX: effect.offsetX, offsetY: effect.offsetY, color: effect.color };
|
|
2991
|
+
}
|
|
2992
|
+
case "echo": {
|
|
2993
|
+
const effect = {
|
|
2994
|
+
kind: "echo",
|
|
2995
|
+
count: clamp(Math.round(num(p, "count", 3)), 1, import_canvas_editor_effects.ECHO_MAX_COUNT),
|
|
2996
|
+
stepX: num(p, "stepX", 6),
|
|
2997
|
+
stepY: num(p, "stepY", 6),
|
|
2998
|
+
color: color(p, "color", "#000000"),
|
|
2999
|
+
fade: clamp(num(p, "fade", 0.5), 0, 1),
|
|
3000
|
+
version: 1
|
|
3001
|
+
};
|
|
3002
|
+
object.set({ effect });
|
|
3003
|
+
return {
|
|
3004
|
+
count: effect.count,
|
|
3005
|
+
stepX: effect.stepX,
|
|
3006
|
+
stepY: effect.stepY,
|
|
3007
|
+
color: effect.color,
|
|
3008
|
+
fade: effect.fade
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
case "background": {
|
|
3012
|
+
const effect = {
|
|
3013
|
+
kind: "background",
|
|
3014
|
+
padding: clamp(num(p, "padding", 12), 0, 400),
|
|
3015
|
+
radius: clamp(num(p, "radius", 8), 0, 400),
|
|
3016
|
+
color: color(p, "color", "#ffe600"),
|
|
3017
|
+
version: 1
|
|
3018
|
+
};
|
|
3019
|
+
object.set({ effect });
|
|
3020
|
+
return { padding: effect.padding, radius: effect.radius, color: effect.color };
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
};
|
|
3025
|
+
|
|
3026
|
+
// src/text-curve.ts
|
|
3027
|
+
var import_fabric11 = require("fabric");
|
|
3028
|
+
|
|
3029
|
+
// src/text-curve-geometry.ts
|
|
3030
|
+
var DEFAULT_TEXT_CURVE = {
|
|
3031
|
+
shape: "arc",
|
|
3032
|
+
arc: 0,
|
|
3033
|
+
wave: 0,
|
|
3034
|
+
waveLength: 4.1,
|
|
3035
|
+
offset: 0,
|
|
3036
|
+
centerOffset: 0,
|
|
3037
|
+
angle: 0
|
|
3038
|
+
};
|
|
3039
|
+
var MIN_ARC = 0.5;
|
|
3040
|
+
var MIN_ANGLE = 0.5;
|
|
3041
|
+
var MAX_ANGLE = 80;
|
|
3042
|
+
var FULL_CIRCLE_ARC = 99.5;
|
|
3043
|
+
var MIN_SWEEP = 0.12;
|
|
3044
|
+
var WAVE_AMPLITUDE_EM = 0.9;
|
|
3045
|
+
var MIN_WAVE_LENGTH = 1;
|
|
3046
|
+
var MAX_WAVE_LENGTH = 12;
|
|
3047
|
+
var WAVE_STEP = 6;
|
|
3048
|
+
var MAX_WAVE_STEPS = 4e3;
|
|
3049
|
+
function round(value) {
|
|
3050
|
+
return Math.round(value * 100) / 100;
|
|
3051
|
+
}
|
|
3052
|
+
function normalizeTextCurve(config) {
|
|
3053
|
+
const arc = clamp(config.arc ?? 0, -100, 100);
|
|
3054
|
+
const wave = clamp(config.wave ?? 0, 0, 100);
|
|
3055
|
+
const inferred = Math.abs(arc) < MIN_ARC && wave > 0 ? "wave" : "arc";
|
|
3056
|
+
return {
|
|
3057
|
+
shape: config.shape ?? inferred,
|
|
3058
|
+
arc,
|
|
3059
|
+
wave,
|
|
3060
|
+
waveLength: clamp(
|
|
3061
|
+
config.waveLength ?? DEFAULT_TEXT_CURVE.waveLength,
|
|
3062
|
+
MIN_WAVE_LENGTH,
|
|
3063
|
+
MAX_WAVE_LENGTH
|
|
3064
|
+
),
|
|
3065
|
+
offset: clamp(config.offset ?? 0, -100, 100),
|
|
3066
|
+
centerOffset: clamp(config.centerOffset ?? 0, -100, 100),
|
|
3067
|
+
angle: clamp(config.angle ?? 0, -MAX_ANGLE, MAX_ANGLE)
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
function bendsBaseline(config) {
|
|
3071
|
+
if (config.shape === "angle") return Math.abs(config.angle) >= MIN_ANGLE;
|
|
3072
|
+
return config.shape === "arc" ? Math.abs(config.arc) >= MIN_ARC : config.wave > 0;
|
|
3073
|
+
}
|
|
3074
|
+
function arcGeometry(width, config) {
|
|
3075
|
+
const magnitude = Math.min(100, Math.abs(config.arc));
|
|
3076
|
+
const direction = config.arc < 0 ? -1 : 1;
|
|
3077
|
+
const baseSweep = magnitude >= FULL_CIRCLE_ARC ? Math.PI * 2 : Math.max(MIN_SWEEP, magnitude / 100 * Math.PI);
|
|
3078
|
+
const baseRadius = width / baseSweep;
|
|
3079
|
+
const radius = baseRadius * Math.pow(2, config.centerOffset / 100);
|
|
3080
|
+
const sweep = radius >= baseRadius ? baseSweep : Math.min(Math.PI * 2, width / Math.max(radius, 1e-3));
|
|
3081
|
+
return { radius, sweep, direction, baseRadius };
|
|
3082
|
+
}
|
|
3083
|
+
function arcLinePath(width, geometry, radius, rotation) {
|
|
3084
|
+
const { sweep, direction, baseRadius } = geometry;
|
|
3085
|
+
const full = sweep >= Math.PI * 2 - 1e-6;
|
|
3086
|
+
const centerX = width / 2;
|
|
3087
|
+
const point = (angle) => [
|
|
3088
|
+
centerX + radius * Math.sin(angle),
|
|
3089
|
+
direction * (baseRadius - radius * Math.cos(angle))
|
|
3090
|
+
];
|
|
3091
|
+
const sweepFlag = direction > 0 ? 1 : 0;
|
|
3092
|
+
const format = ([x, y]) => `${round(x)} ${round(y)}`;
|
|
3093
|
+
const r = round(radius);
|
|
3094
|
+
if (full) {
|
|
3095
|
+
const start2 = point(rotation - Math.PI);
|
|
3096
|
+
const opposite = point(rotation);
|
|
3097
|
+
return {
|
|
3098
|
+
data: [
|
|
3099
|
+
`M ${format(start2)}`,
|
|
3100
|
+
`A ${r} ${r} 0 0 ${sweepFlag} ${format(opposite)}`,
|
|
3101
|
+
`A ${r} ${r} 0 0 ${sweepFlag} ${format(start2)}`
|
|
3102
|
+
].join(" "),
|
|
3103
|
+
length: radius * Math.PI * 2
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
const start = point(rotation - sweep / 2);
|
|
3107
|
+
const end = point(rotation + sweep / 2);
|
|
3108
|
+
const largeArc = sweep > Math.PI ? 1 : 0;
|
|
3109
|
+
return {
|
|
3110
|
+
data: `M ${format(start)} A ${r} ${r} 0 ${largeArc} ${sweepFlag} ${format(end)}`,
|
|
3111
|
+
length: radius * sweep
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
function waveLinePath(width, fontSize, config, dy) {
|
|
3115
|
+
const amplitude = config.wave / 100 * fontSize * WAVE_AMPLITUDE_EM;
|
|
3116
|
+
const period = Math.max(1, fontSize * config.waveLength);
|
|
3117
|
+
const phase = config.offset / 100 * Math.PI * 2;
|
|
3118
|
+
const step = Math.min(WAVE_STEP, period / 8);
|
|
3119
|
+
const steps = Math.min(MAX_WAVE_STEPS, Math.max(2, Math.ceil(width / step)));
|
|
3120
|
+
const commands = [];
|
|
3121
|
+
let length = 0;
|
|
3122
|
+
let previous = null;
|
|
3123
|
+
for (let index = 0; index <= steps; index++) {
|
|
3124
|
+
const x = width * index / steps;
|
|
3125
|
+
const y = dy + amplitude * Math.sin(x / period * Math.PI * 2 + phase);
|
|
3126
|
+
if (previous) length += Math.hypot(x - previous[0], y - previous[1]);
|
|
3127
|
+
previous = [x, y];
|
|
3128
|
+
commands.push(`${index === 0 ? "M" : "L"} ${round(x)} ${round(y)}`);
|
|
3129
|
+
}
|
|
3130
|
+
return { data: commands.join(" "), length };
|
|
3131
|
+
}
|
|
3132
|
+
function angleLinePath(width, config, dy) {
|
|
3133
|
+
const radians = config.angle * Math.PI / 180;
|
|
3134
|
+
const half = width / 2;
|
|
3135
|
+
const dx = half * Math.cos(radians);
|
|
3136
|
+
const rise = half * Math.sin(radians);
|
|
3137
|
+
const format = ([x, y]) => `${round(x)} ${round(y)}`;
|
|
3138
|
+
return {
|
|
3139
|
+
data: `M ${format([half - dx, dy + rise])} L ${format([half + dx, dy - rise])}`,
|
|
3140
|
+
length: width
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
function buildCurveLinePaths(config, width, fontSize, lineCount = 1, lineHeight = 0) {
|
|
3144
|
+
const curve = normalizeTextCurve(config);
|
|
3145
|
+
if (!bendsBaseline(curve)) return null;
|
|
3146
|
+
const lines = Math.max(1, Math.floor(lineCount));
|
|
3147
|
+
const paths = [];
|
|
3148
|
+
if (curve.shape === "wave") {
|
|
3149
|
+
for (let index = 0; index < lines; index++) {
|
|
3150
|
+
paths.push(waveLinePath(width, fontSize, curve, (index - (lines - 1) / 2) * lineHeight));
|
|
3151
|
+
}
|
|
3152
|
+
return paths;
|
|
3153
|
+
}
|
|
3154
|
+
if (curve.shape === "angle") {
|
|
3155
|
+
for (let index = 0; index < lines; index++) {
|
|
3156
|
+
paths.push(angleLinePath(width, curve, (index - (lines - 1) / 2) * lineHeight));
|
|
3157
|
+
}
|
|
3158
|
+
return paths;
|
|
3159
|
+
}
|
|
3160
|
+
const geometry = arcGeometry(width, curve);
|
|
3161
|
+
const rotation = curve.offset / 100 * geometry.sweep;
|
|
3162
|
+
for (let index = 0; index < lines; index++) {
|
|
3163
|
+
const radius = Math.max(1, geometry.radius + (index - (lines - 1) / 2) * lineHeight);
|
|
3164
|
+
paths.push(arcLinePath(width, geometry, radius, rotation));
|
|
3165
|
+
}
|
|
3166
|
+
return paths;
|
|
3167
|
+
}
|
|
3168
|
+
function buildCurvePathData(config, width, fontSize) {
|
|
3169
|
+
return buildCurveLinePaths(config, width, fontSize)?.[0] ?? null;
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// src/text-curve.ts
|
|
3173
|
+
var MEASURE_WIDTH2 = 1e5;
|
|
3174
|
+
var PATH_SLACK = 0.06;
|
|
3175
|
+
var FALLBACK_LINE_HEIGHT = 1.16;
|
|
3176
|
+
var patches = /* @__PURE__ */ new WeakMap();
|
|
3177
|
+
var dimensionPatches = /* @__PURE__ */ new WeakMap();
|
|
3178
|
+
var authoredCaching = /* @__PURE__ */ new WeakMap();
|
|
3179
|
+
var GLYPH_HALF_HEIGHT = 0.7;
|
|
3180
|
+
function curvedInkSize(text) {
|
|
3181
|
+
const lineCount = text._textLines?.length ?? 0;
|
|
3182
|
+
for (let index = 0; index < lineCount; index += 1) text.getLineWidth?.(index);
|
|
3183
|
+
const lines = text.__charBounds;
|
|
3184
|
+
if (!lines?.length) return null;
|
|
3185
|
+
let halfWidth = 0;
|
|
3186
|
+
let halfHeight = 0;
|
|
3187
|
+
for (const line of lines) {
|
|
3188
|
+
for (const box of line ?? []) {
|
|
3189
|
+
const { renderLeft, renderTop } = box;
|
|
3190
|
+
if (typeof renderLeft !== "number" || typeof renderTop !== "number") continue;
|
|
3191
|
+
const halfGlyphWidth = (box.kernedWidth ?? box.width ?? 0) / 2;
|
|
3192
|
+
const halfGlyphHeight = (box.height ?? text.fontSize) * GLYPH_HALF_HEIGHT;
|
|
3193
|
+
const angle = box.angle ?? 0;
|
|
3194
|
+
const cos = Math.abs(Math.cos(angle));
|
|
3195
|
+
const sin = Math.abs(Math.sin(angle));
|
|
3196
|
+
halfWidth = Math.max(
|
|
3197
|
+
halfWidth,
|
|
3198
|
+
Math.abs(renderLeft) + halfGlyphWidth * cos + halfGlyphHeight * sin
|
|
3199
|
+
);
|
|
3200
|
+
halfHeight = Math.max(
|
|
3201
|
+
halfHeight,
|
|
3202
|
+
Math.abs(renderTop) + halfGlyphWidth * sin + halfGlyphHeight * cos
|
|
3203
|
+
);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
if (halfWidth === 0 && halfHeight === 0) return null;
|
|
3207
|
+
return { width: halfWidth * 2, height: halfHeight * 2 };
|
|
3208
|
+
}
|
|
3209
|
+
function growBoxToInk(text) {
|
|
3210
|
+
const ink = curvedInkSize(text);
|
|
3211
|
+
if (!ink) return;
|
|
3212
|
+
if (ink.width > (text.width ?? 0)) text.width = ink.width;
|
|
3213
|
+
if (ink.height > (text.height ?? 0)) text.height = ink.height;
|
|
3214
|
+
}
|
|
3215
|
+
function isCurvable(object) {
|
|
3216
|
+
return !!object && typeof object.text === "string";
|
|
3217
|
+
}
|
|
3218
|
+
function measureText(text) {
|
|
3219
|
+
const authored = text.width;
|
|
3220
|
+
try {
|
|
3221
|
+
text.set({ width: MEASURE_WIDTH2 });
|
|
3222
|
+
text.initDimensions?.();
|
|
3223
|
+
return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
|
|
3224
|
+
} finally {
|
|
3225
|
+
if (authored !== void 0) text.set({ width: authored });
|
|
3226
|
+
text.initDimensions?.();
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
function lineCountOf(text) {
|
|
3230
|
+
return Math.max(1, text._textLines?.length ?? 1);
|
|
3231
|
+
}
|
|
3232
|
+
function lineHeightOf(text) {
|
|
3233
|
+
const measured = text.getHeightOfLine?.(0);
|
|
3234
|
+
if (typeof measured === "number" && Number.isFinite(measured) && measured > 0) return measured;
|
|
3235
|
+
return text.fontSize * (text.lineHeight ?? 1) * FALLBACK_LINE_HEIGHT;
|
|
3236
|
+
}
|
|
3237
|
+
function toFabricPath(curve) {
|
|
3238
|
+
return new import_fabric11.Path(curve.data, { visible: false, objectCaching: false });
|
|
3239
|
+
}
|
|
3240
|
+
var TextCurveManager = class {
|
|
3241
|
+
constructor(canvas, layers, history, events) {
|
|
3242
|
+
this.canvas = canvas;
|
|
3243
|
+
this.layers = layers;
|
|
3244
|
+
this.history = history;
|
|
3245
|
+
this.events = events;
|
|
3246
|
+
this.canvas.on("text:changed", this.onTextChanged);
|
|
3247
|
+
}
|
|
3248
|
+
canvas;
|
|
3249
|
+
layers;
|
|
3250
|
+
history;
|
|
3251
|
+
events;
|
|
3252
|
+
/**
|
|
3253
|
+
* Typing on the canvas changes the run the paths were built for. Fabric drops
|
|
3254
|
+
* a glyph whose centre falls past the end of the path, so without rebuilding
|
|
3255
|
+
* here a longer replacement simply vanishes — select-all-and-retype rendered
|
|
3256
|
+
* an empty box — and a shorter one piles up on a path built for the old text.
|
|
3257
|
+
*
|
|
3258
|
+
* No history entry: fabric records the edit when editing exits, and one save
|
|
3259
|
+
* per keystroke would bury every earlier step under the typing.
|
|
3260
|
+
*/
|
|
3261
|
+
onTextChanged = (event) => {
|
|
2271
3262
|
const layer = event.target ? this.layers.findByObject(event.target) : void 0;
|
|
2272
3263
|
if (!layer?.meta.curve) return;
|
|
2273
3264
|
this.refresh(layer.id, false);
|
|
@@ -2464,7 +3455,7 @@ var TextCurveManager = class {
|
|
|
2464
3455
|
};
|
|
2465
3456
|
|
|
2466
3457
|
// src/text-wrap.ts
|
|
2467
|
-
var
|
|
3458
|
+
var import_fabric13 = require("fabric");
|
|
2468
3459
|
|
|
2469
3460
|
// src/text-wrap-split.ts
|
|
2470
3461
|
var WHITESPACE = /[ \t\r]/;
|
|
@@ -2490,12 +3481,12 @@ function preWrapWordSplit(value) {
|
|
|
2490
3481
|
}
|
|
2491
3482
|
|
|
2492
3483
|
// src/masks/space.ts
|
|
2493
|
-
var
|
|
3484
|
+
var import_fabric12 = require("fabric");
|
|
2494
3485
|
function matrixOf(object) {
|
|
2495
3486
|
return object.calcTransformMatrix();
|
|
2496
3487
|
}
|
|
2497
3488
|
function applyMatrix(object, matrix) {
|
|
2498
|
-
const decomposed =
|
|
3489
|
+
const decomposed = import_fabric12.util.qrDecompose(matrix);
|
|
2499
3490
|
object.set({
|
|
2500
3491
|
flipX: false,
|
|
2501
3492
|
flipY: false,
|
|
@@ -2512,19 +3503,19 @@ function applyMatrix(object, matrix) {
|
|
|
2512
3503
|
object.setCoords();
|
|
2513
3504
|
}
|
|
2514
3505
|
function toCanvasSpace(object, host) {
|
|
2515
|
-
applyMatrix(object,
|
|
3506
|
+
applyMatrix(object, import_fabric12.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
|
|
2516
3507
|
}
|
|
2517
3508
|
function toHostSpace(object, host) {
|
|
2518
3509
|
applyMatrix(
|
|
2519
3510
|
object,
|
|
2520
|
-
|
|
3511
|
+
import_fabric12.util.multiplyTransformMatrices(import_fabric12.util.invertTransform(matrixOf(host)), matrixOf(object))
|
|
2521
3512
|
);
|
|
2522
3513
|
}
|
|
2523
3514
|
function relativeMatrix(object, host) {
|
|
2524
|
-
return
|
|
3515
|
+
return import_fabric12.util.multiplyTransformMatrices(import_fabric12.util.invertTransform(matrixOf(host)), matrixOf(object));
|
|
2525
3516
|
}
|
|
2526
3517
|
function applyRelativeMatrix(object, host, rel) {
|
|
2527
|
-
applyMatrix(object,
|
|
3518
|
+
applyMatrix(object, import_fabric12.util.multiplyTransformMatrices(matrixOf(host), rel));
|
|
2528
3519
|
}
|
|
2529
3520
|
function asObject(clip) {
|
|
2530
3521
|
return clip;
|
|
@@ -2587,7 +3578,7 @@ function boxClip(text, makeRect) {
|
|
|
2587
3578
|
function hasMask(meta) {
|
|
2588
3579
|
return !!meta.maskPreset || (meta.maskStack?.length ?? 0) > 0;
|
|
2589
3580
|
}
|
|
2590
|
-
function applyTextWrapToObject(object, meta, makeRect = (options) => new
|
|
3581
|
+
function applyTextWrapToObject(object, meta, makeRect = (options) => new import_fabric13.Rect(options)) {
|
|
2591
3582
|
const text = asText(object);
|
|
2592
3583
|
if (!text) return;
|
|
2593
3584
|
const state = readTextWrap(meta);
|
|
@@ -2726,7 +3717,7 @@ var TextWrapManager = class {
|
|
|
2726
3717
|
};
|
|
2727
3718
|
|
|
2728
3719
|
// src/mask-presets/manager.ts
|
|
2729
|
-
var
|
|
3720
|
+
var import_fabric14 = require("fabric");
|
|
2730
3721
|
|
|
2731
3722
|
// src/mask-presets/shapes.ts
|
|
2732
3723
|
var SHAPE_MASK_IDS = [
|
|
@@ -2994,13 +3985,13 @@ var MaskPresetManager = class {
|
|
|
2994
3985
|
objectCaching: false
|
|
2995
3986
|
};
|
|
2996
3987
|
if (isShapeMaskId(id)) {
|
|
2997
|
-
return new
|
|
3988
|
+
return new import_fabric14.Path(shapeMaskPathData(id), {
|
|
2998
3989
|
...shared,
|
|
2999
3990
|
scaleX: width / SHAPE_MASK_BOX,
|
|
3000
3991
|
scaleY: height / SHAPE_MASK_BOX
|
|
3001
3992
|
});
|
|
3002
3993
|
}
|
|
3003
|
-
return new
|
|
3994
|
+
return new import_fabric14.FabricImage(renderTextureMask(id), {
|
|
3004
3995
|
...shared,
|
|
3005
3996
|
scaleX: width / TEXTURE_MASK_SIZE,
|
|
3006
3997
|
scaleY: height / TEXTURE_MASK_SIZE
|
|
@@ -3008,54 +3999,6 @@ var MaskPresetManager = class {
|
|
|
3008
3999
|
}
|
|
3009
4000
|
};
|
|
3010
4001
|
|
|
3011
|
-
// src/shadow.ts
|
|
3012
|
-
var import_fabric11 = require("fabric");
|
|
3013
|
-
|
|
3014
|
-
// src/utils/color.ts
|
|
3015
|
-
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
3016
|
-
function isCssColor(value, allowEmpty = false) {
|
|
3017
|
-
if (typeof value !== "string") return false;
|
|
3018
|
-
const trimmed = value.trim();
|
|
3019
|
-
if (!trimmed) return allowEmpty;
|
|
3020
|
-
return CSS_COLOR.test(trimmed);
|
|
3021
|
-
}
|
|
3022
|
-
|
|
3023
|
-
// src/shadow.ts
|
|
3024
|
-
var DEFAULT_LAYER_SHADOW = {
|
|
3025
|
-
enabled: false,
|
|
3026
|
-
color: "#000000",
|
|
3027
|
-
blur: 12,
|
|
3028
|
-
offsetX: 6,
|
|
3029
|
-
offsetY: 6
|
|
3030
|
-
};
|
|
3031
|
-
function readLayerShadow(object) {
|
|
3032
|
-
const shadow = object.shadow;
|
|
3033
|
-
if (!shadow || typeof shadow === "string") return { ...DEFAULT_LAYER_SHADOW };
|
|
3034
|
-
return {
|
|
3035
|
-
enabled: true,
|
|
3036
|
-
color: typeof shadow.color === "string" ? shadow.color : DEFAULT_LAYER_SHADOW.color,
|
|
3037
|
-
blur: shadow.blur ?? DEFAULT_LAYER_SHADOW.blur,
|
|
3038
|
-
offsetX: shadow.offsetX ?? DEFAULT_LAYER_SHADOW.offsetX,
|
|
3039
|
-
offsetY: shadow.offsetY ?? DEFAULT_LAYER_SHADOW.offsetY
|
|
3040
|
-
};
|
|
3041
|
-
}
|
|
3042
|
-
function applyLayerShadow(object, config) {
|
|
3043
|
-
const next = { ...readLayerShadow(object), ...config };
|
|
3044
|
-
if (!next.enabled) {
|
|
3045
|
-
object.set({ shadow: null });
|
|
3046
|
-
return;
|
|
3047
|
-
}
|
|
3048
|
-
const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
|
|
3049
|
-
object.set({
|
|
3050
|
-
shadow: new import_fabric11.Shadow({
|
|
3051
|
-
color,
|
|
3052
|
-
blur: Math.max(0, next.blur),
|
|
3053
|
-
offsetX: next.offsetX,
|
|
3054
|
-
offsetY: next.offsetY
|
|
3055
|
-
})
|
|
3056
|
-
});
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
4002
|
// src/selection-style.ts
|
|
3060
4003
|
var DEFAULT_SELECTION_STYLE = {
|
|
3061
4004
|
borderColor: "#c9a96e",
|
|
@@ -3161,7 +4104,7 @@ var UnitConverter = class {
|
|
|
3161
4104
|
};
|
|
3162
4105
|
|
|
3163
4106
|
// src/serialization.ts
|
|
3164
|
-
var
|
|
4107
|
+
var import_fabric15 = require("fabric");
|
|
3165
4108
|
var VERSION = "2.0.0";
|
|
3166
4109
|
function serializeEditor(editor) {
|
|
3167
4110
|
return {
|
|
@@ -3197,7 +4140,7 @@ async function deserializeEditor(editor, state) {
|
|
|
3197
4140
|
}
|
|
3198
4141
|
const staged = await Promise.all(
|
|
3199
4142
|
state.layers.map(async (serialized) => {
|
|
3200
|
-
const fabricObject = (await
|
|
4143
|
+
const fabricObject = (await import_fabric15.util.enlivenObjects([serialized.fabricObject]))[0];
|
|
3201
4144
|
if (!fabricObject) {
|
|
3202
4145
|
const source = serialized.fabricObject.src;
|
|
3203
4146
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -3208,7 +4151,7 @@ async function deserializeEditor(editor, state) {
|
|
|
3208
4151
|
return { serialized, fabricObject };
|
|
3209
4152
|
})
|
|
3210
4153
|
);
|
|
3211
|
-
const stagedBackground = state.backgroundImage ? (await
|
|
4154
|
+
const stagedBackground = state.backgroundImage ? (await import_fabric15.util.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
3212
4155
|
if (state.backgroundImage && !stagedBackground) {
|
|
3213
4156
|
const source = state.backgroundImage.src;
|
|
3214
4157
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -3261,7 +4204,7 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
3261
4204
|
}
|
|
3262
4205
|
|
|
3263
4206
|
// src/export.ts
|
|
3264
|
-
var
|
|
4207
|
+
var import_fabric16 = require("fabric");
|
|
3265
4208
|
|
|
3266
4209
|
// src/displacement.ts
|
|
3267
4210
|
var CHANNEL_INDEX = {
|
|
@@ -3355,7 +4298,7 @@ async function exportPNG(canvas, options = {}) {
|
|
|
3355
4298
|
}
|
|
3356
4299
|
async function exportIsolatedPNG(source, objects, options = {}) {
|
|
3357
4300
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
3358
|
-
const canvas = new
|
|
4301
|
+
const canvas = new import_fabric16.StaticCanvas(element, {
|
|
3359
4302
|
width: options.width ?? source.getWidth(),
|
|
3360
4303
|
height: options.height ?? source.getHeight(),
|
|
3361
4304
|
backgroundColor: options.backgroundColor || void 0
|
|
@@ -3385,7 +4328,7 @@ async function exportPrintArea(source, area, options = {}) {
|
|
|
3385
4328
|
throw new Error("Print area does not overlap the canvas");
|
|
3386
4329
|
}
|
|
3387
4330
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
3388
|
-
const canvas = new
|
|
4331
|
+
const canvas = new import_fabric16.StaticCanvas(element, { width, height });
|
|
3389
4332
|
try {
|
|
3390
4333
|
const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
|
|
3391
4334
|
if (clones.length) canvas.add(...clones);
|
|
@@ -3825,7 +4768,7 @@ var ProjectManager = class {
|
|
|
3825
4768
|
};
|
|
3826
4769
|
|
|
3827
4770
|
// src/mask.ts
|
|
3828
|
-
var
|
|
4771
|
+
var import_fabric17 = require("fabric");
|
|
3829
4772
|
var MaskRefinementError = class extends Error {
|
|
3830
4773
|
constructor(code, message, cause) {
|
|
3831
4774
|
super(message);
|
|
@@ -3857,7 +4800,7 @@ var MaskController = class {
|
|
|
3857
4800
|
throw new Error("Mask dimensions must be positive integers");
|
|
3858
4801
|
}
|
|
3859
4802
|
const backing = this.makeCanvas(width, height);
|
|
3860
|
-
const image = new
|
|
4803
|
+
const image = new import_fabric17.FabricImage(backing, {
|
|
3861
4804
|
left: 0,
|
|
3862
4805
|
top: 0,
|
|
3863
4806
|
originX: "left",
|
|
@@ -4050,8 +4993,8 @@ var MaskController = class {
|
|
|
4050
4993
|
point.y,
|
|
4051
4994
|
radius
|
|
4052
4995
|
);
|
|
4053
|
-
const
|
|
4054
|
-
gradient.addColorStop(0,
|
|
4996
|
+
const color2 = brush.mode === "subtract" ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)";
|
|
4997
|
+
gradient.addColorStop(0, color2);
|
|
4055
4998
|
gradient.addColorStop(1, "rgba(255,255,255,0)");
|
|
4056
4999
|
context.fillStyle = gradient;
|
|
4057
5000
|
context.beginPath();
|
|
@@ -4077,16 +5020,133 @@ var MaskController = class {
|
|
|
4077
5020
|
if (!layer || layer.type !== "mask") throw new Error(`Mask layer not found: ${layerId}`);
|
|
4078
5021
|
return layer;
|
|
4079
5022
|
}
|
|
4080
|
-
assertActive() {
|
|
4081
|
-
if (this.disposed) throw new Error("Mask controller has been disposed");
|
|
5023
|
+
assertActive() {
|
|
5024
|
+
if (this.disposed) throw new Error("Mask controller has been disposed");
|
|
5025
|
+
}
|
|
5026
|
+
};
|
|
5027
|
+
|
|
5028
|
+
// src/masks/manager.ts
|
|
5029
|
+
var import_fabric24 = require("fabric");
|
|
5030
|
+
|
|
5031
|
+
// src/masks/compose.ts
|
|
5032
|
+
var import_fabric19 = require("fabric");
|
|
5033
|
+
|
|
5034
|
+
// src/gradient.ts
|
|
5035
|
+
var import_fabric18 = require("fabric");
|
|
5036
|
+
var DEFAULT_GRADIENT_CONFIG = {
|
|
5037
|
+
kind: "linear",
|
|
5038
|
+
angle: 90,
|
|
5039
|
+
center: { x: 0.5, y: 0.5 },
|
|
5040
|
+
radius: 0.5,
|
|
5041
|
+
stops: [
|
|
5042
|
+
{ color: "#ffffff", opacity: 1, position: 0 },
|
|
5043
|
+
{ color: "#000000", opacity: 1, position: 1 }
|
|
5044
|
+
]
|
|
5045
|
+
};
|
|
5046
|
+
var MIN_RADIUS = 1e-3;
|
|
5047
|
+
var MAX_GRADIENT_STOPS = 256;
|
|
5048
|
+
var FALLBACK_STOP_COLOR = DEFAULT_GRADIENT_CONFIG.stops[0].color;
|
|
5049
|
+
function linearCoords(angle, box) {
|
|
5050
|
+
const radians = angle * Math.PI / 180;
|
|
5051
|
+
const sin = Math.sin(radians);
|
|
5052
|
+
const cos = Math.cos(radians);
|
|
5053
|
+
const vx = box.width * sin;
|
|
5054
|
+
const vy = -box.height * cos;
|
|
5055
|
+
const length = Math.abs(box.width * sin) + Math.abs(box.height * cos);
|
|
5056
|
+
const scale = length / (vx * vx + vy * vy || 1);
|
|
5057
|
+
return {
|
|
5058
|
+
x1: 0.5 - scale * vx / 2,
|
|
5059
|
+
y1: 0.5 - scale * vy / 2,
|
|
5060
|
+
x2: 0.5 + scale * vx / 2,
|
|
5061
|
+
y2: 0.5 + scale * vy / 2
|
|
5062
|
+
};
|
|
5063
|
+
}
|
|
5064
|
+
function angleFromCoords(coords, box) {
|
|
5065
|
+
const dx = (coords.x2 - coords.x1) / (box.width || 1);
|
|
5066
|
+
const dy = (coords.y2 - coords.y1) / (box.height || 1);
|
|
5067
|
+
const degrees = Math.atan2(dx, -dy) * 180 / Math.PI;
|
|
5068
|
+
return (degrees + 360) % 360;
|
|
5069
|
+
}
|
|
5070
|
+
function stopColor(stop) {
|
|
5071
|
+
const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
|
|
5072
|
+
const color2 = new import_fabric18.Color(source);
|
|
5073
|
+
const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
|
|
5074
|
+
color2.setAlpha(clamp(opacity, 0, 1));
|
|
5075
|
+
return color2.toRgba();
|
|
5076
|
+
}
|
|
5077
|
+
function toFabricGradient(config, box) {
|
|
5078
|
+
const colorStops = [...config.stops].sort((a, b) => a.position - b.position).slice(0, MAX_GRADIENT_STOPS).map((stop) => {
|
|
5079
|
+
const position = Number.isFinite(stop.position) ? stop.position : 0;
|
|
5080
|
+
return { offset: clamp(position, 0, 1), color: stopColor(stop) };
|
|
5081
|
+
});
|
|
5082
|
+
if (config.kind === "radial") {
|
|
5083
|
+
const radius = Math.max(MIN_RADIUS, config.radius);
|
|
5084
|
+
return new import_fabric18.Gradient({
|
|
5085
|
+
type: "radial",
|
|
5086
|
+
gradientUnits: "percentage",
|
|
5087
|
+
coords: {
|
|
5088
|
+
x1: config.center.x,
|
|
5089
|
+
y1: config.center.y,
|
|
5090
|
+
r1: 0,
|
|
5091
|
+
x2: config.center.x,
|
|
5092
|
+
y2: config.center.y,
|
|
5093
|
+
r2: radius
|
|
5094
|
+
},
|
|
5095
|
+
colorStops
|
|
5096
|
+
});
|
|
5097
|
+
}
|
|
5098
|
+
return new import_fabric18.Gradient({
|
|
5099
|
+
type: "linear",
|
|
5100
|
+
gradientUnits: "percentage",
|
|
5101
|
+
coords: linearCoords(config.angle, box),
|
|
5102
|
+
colorStops
|
|
5103
|
+
});
|
|
5104
|
+
}
|
|
5105
|
+
function toFabricMaskGradient(config, box) {
|
|
5106
|
+
const gradient = toFabricGradient(config, box);
|
|
5107
|
+
gradient.colorStops = gradient.colorStops.map((stop) => {
|
|
5108
|
+
const color2 = new import_fabric18.Color(stop.color);
|
|
5109
|
+
const [red, green, blue] = color2.getSource();
|
|
5110
|
+
const luminance = (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255;
|
|
5111
|
+
color2.setSource([255, 255, 255, clamp(luminance * color2.getAlpha(), 0, 1)]);
|
|
5112
|
+
return { offset: stop.offset, color: color2.toRgba() };
|
|
5113
|
+
});
|
|
5114
|
+
return gradient;
|
|
5115
|
+
}
|
|
5116
|
+
function readGradientConfig(object) {
|
|
5117
|
+
const fill = object.fill;
|
|
5118
|
+
if (!fill || typeof fill === "string" || !(fill instanceof import_fabric18.Gradient)) return null;
|
|
5119
|
+
const stops = fill.colorStops.map((stop) => {
|
|
5120
|
+
const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
|
|
5121
|
+
const color2 = new import_fabric18.Color(source);
|
|
5122
|
+
const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
|
|
5123
|
+
return {
|
|
5124
|
+
color: `#${color2.toHex().toLowerCase()}`,
|
|
5125
|
+
opacity: color2.getAlpha(),
|
|
5126
|
+
position: clamp(offset, 0, 1)
|
|
5127
|
+
};
|
|
5128
|
+
});
|
|
5129
|
+
if (fill.type === "radial") {
|
|
5130
|
+
const { x1, y1, r2 } = fill.coords;
|
|
5131
|
+
return {
|
|
5132
|
+
kind: "radial",
|
|
5133
|
+
angle: DEFAULT_GRADIENT_CONFIG.angle,
|
|
5134
|
+
center: { x: x1, y: y1 },
|
|
5135
|
+
radius: r2,
|
|
5136
|
+
stops
|
|
5137
|
+
};
|
|
4082
5138
|
}
|
|
4083
|
-
};
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
5139
|
+
const box = { width: object.width || 1, height: object.height || 1 };
|
|
5140
|
+
return {
|
|
5141
|
+
kind: "linear",
|
|
5142
|
+
angle: angleFromCoords(fill.coords, box),
|
|
5143
|
+
center: { ...DEFAULT_GRADIENT_CONFIG.center },
|
|
5144
|
+
radius: DEFAULT_GRADIENT_CONFIG.radius,
|
|
5145
|
+
stops
|
|
5146
|
+
};
|
|
5147
|
+
}
|
|
4087
5148
|
|
|
4088
5149
|
// src/masks/compose.ts
|
|
4089
|
-
var import_fabric15 = require("fabric");
|
|
4090
5150
|
var MODE_OPERATION = {
|
|
4091
5151
|
add: "source-over",
|
|
4092
5152
|
subtract: "destination-out",
|
|
@@ -4096,7 +5156,7 @@ function neutralize(child) {
|
|
|
4096
5156
|
child.set({ opacity: 0, globalCompositeOperation: "source-over" });
|
|
4097
5157
|
}
|
|
4098
5158
|
function baseRect(box) {
|
|
4099
|
-
return new
|
|
5159
|
+
return new import_fabric19.Rect({
|
|
4100
5160
|
left: box.left,
|
|
4101
5161
|
top: box.top,
|
|
4102
5162
|
width: Math.max(1, box.width),
|
|
@@ -4116,6 +5176,14 @@ function composeMaskGroup(children, entries, options) {
|
|
|
4116
5176
|
neutralize(child);
|
|
4117
5177
|
return;
|
|
4118
5178
|
}
|
|
5179
|
+
if (entry.gradient) {
|
|
5180
|
+
child.set({
|
|
5181
|
+
fill: toFabricMaskGradient(entry.gradient, {
|
|
5182
|
+
width: Math.max(1, child.width ?? 1),
|
|
5183
|
+
height: Math.max(1, child.height ?? 1)
|
|
5184
|
+
})
|
|
5185
|
+
});
|
|
5186
|
+
}
|
|
4119
5187
|
child.set({
|
|
4120
5188
|
opacity: entry.opacity,
|
|
4121
5189
|
globalCompositeOperation: MODE_OPERATION[entry.mode] ?? "source-over"
|
|
@@ -4123,7 +5191,7 @@ function composeMaskGroup(children, entries, options) {
|
|
|
4123
5191
|
});
|
|
4124
5192
|
const first = entries.find((entry) => entry.visible);
|
|
4125
5193
|
const withBase = !first || first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
|
|
4126
|
-
return new
|
|
5194
|
+
return new import_fabric19.Group(withBase, {
|
|
4127
5195
|
absolutePositioned: options.absolute,
|
|
4128
5196
|
// Cached, so the children's compositing operations resolve against each
|
|
4129
5197
|
// other instead of against the page underneath the mask.
|
|
@@ -4137,11 +5205,105 @@ function needsAbsoluteSpace(entries) {
|
|
|
4137
5205
|
}
|
|
4138
5206
|
|
|
4139
5207
|
// src/masks/edit.ts
|
|
4140
|
-
var
|
|
5208
|
+
var import_fabric20 = require("fabric");
|
|
5209
|
+
|
|
5210
|
+
// src/masks/edit-viewport.ts
|
|
5211
|
+
var MIN_PADDING = 48;
|
|
5212
|
+
var MAX_PADDING = 512;
|
|
5213
|
+
var PADDING_FRACTION = 0.35;
|
|
5214
|
+
var OUTSIDE_SHADE = "rgba(15, 15, 15, 0.55)";
|
|
5215
|
+
var MaskEditViewport = class {
|
|
5216
|
+
constructor(canvas) {
|
|
5217
|
+
this.canvas = canvas;
|
|
5218
|
+
}
|
|
5219
|
+
canvas;
|
|
5220
|
+
state = null;
|
|
5221
|
+
open(handle) {
|
|
5222
|
+
if (this.state) return;
|
|
5223
|
+
const width = this.canvas.getWidth();
|
|
5224
|
+
const height = this.canvas.getHeight();
|
|
5225
|
+
const base = Math.min(
|
|
5226
|
+
MAX_PADDING,
|
|
5227
|
+
Math.max(MIN_PADDING, Math.max(width, height) * PADDING_FRACTION)
|
|
5228
|
+
);
|
|
5229
|
+
handle.setCoords();
|
|
5230
|
+
const bounds = handle.getBoundingRect();
|
|
5231
|
+
const left = Math.min(MAX_PADDING, Math.max(base, -bounds.left + MIN_PADDING));
|
|
5232
|
+
const top = Math.min(MAX_PADDING, Math.max(base, -bounds.top + MIN_PADDING));
|
|
5233
|
+
const right = Math.min(
|
|
5234
|
+
MAX_PADDING,
|
|
5235
|
+
Math.max(base, bounds.left + bounds.width - width + MIN_PADDING)
|
|
5236
|
+
);
|
|
5237
|
+
const bottom = Math.min(
|
|
5238
|
+
MAX_PADDING,
|
|
5239
|
+
Math.max(base, bounds.top + bounds.height - height + MIN_PADDING)
|
|
5240
|
+
);
|
|
5241
|
+
const viewport = [...this.canvas.viewportTransform];
|
|
5242
|
+
const wrapper = this.canvas.wrapperEl;
|
|
5243
|
+
this.state = {
|
|
5244
|
+
width,
|
|
5245
|
+
height,
|
|
5246
|
+
left,
|
|
5247
|
+
top,
|
|
5248
|
+
viewport,
|
|
5249
|
+
wrapperCss: wrapper.style.cssText
|
|
5250
|
+
};
|
|
5251
|
+
this.canvas.setDimensions({ width: width + left + right, height: height + top + bottom });
|
|
5252
|
+
const paddedViewport = [...viewport];
|
|
5253
|
+
paddedViewport[4] += left;
|
|
5254
|
+
paddedViewport[5] += top;
|
|
5255
|
+
this.canvas.setViewportTransform(paddedViewport);
|
|
5256
|
+
wrapper.style.position = "absolute";
|
|
5257
|
+
wrapper.style.left = `${-left}px`;
|
|
5258
|
+
wrapper.style.top = `${-top}px`;
|
|
5259
|
+
wrapper.style.zIndex = "1";
|
|
5260
|
+
this.canvas.on("after:render", this.onAfterRender);
|
|
5261
|
+
this.canvas.calcOffset();
|
|
5262
|
+
this.canvas.requestRenderAll();
|
|
5263
|
+
}
|
|
5264
|
+
close() {
|
|
5265
|
+
const state = this.state;
|
|
5266
|
+
if (!state) return;
|
|
5267
|
+
this.canvas.off("after:render", this.onAfterRender);
|
|
5268
|
+
this.canvas.setViewportTransform(state.viewport);
|
|
5269
|
+
this.canvas.setDimensions({ width: state.width, height: state.height });
|
|
5270
|
+
this.canvas.wrapperEl.style.cssText = state.wrapperCss;
|
|
5271
|
+
this.canvas.calcOffset();
|
|
5272
|
+
this.state = null;
|
|
5273
|
+
}
|
|
5274
|
+
/** Save with real document dimensions, then reopen the same editing view. */
|
|
5275
|
+
checkpoint(handle, save) {
|
|
5276
|
+
this.close();
|
|
5277
|
+
try {
|
|
5278
|
+
save();
|
|
5279
|
+
} finally {
|
|
5280
|
+
this.open(handle);
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
onAfterRender = ({ ctx }) => {
|
|
5284
|
+
const state = this.state;
|
|
5285
|
+
if (!state || ctx !== this.canvas.getContext()) return;
|
|
5286
|
+
const fullWidth = this.canvas.getWidth();
|
|
5287
|
+
const fullHeight = this.canvas.getHeight();
|
|
5288
|
+
const right = state.left + state.width;
|
|
5289
|
+
const bottom = state.top + state.height;
|
|
5290
|
+
ctx.save();
|
|
5291
|
+
ctx.fillStyle = OUTSIDE_SHADE;
|
|
5292
|
+
ctx.fillRect(0, 0, fullWidth, state.top);
|
|
5293
|
+
ctx.fillRect(0, bottom, fullWidth, fullHeight - bottom);
|
|
5294
|
+
ctx.fillRect(0, state.top, state.left, state.height);
|
|
5295
|
+
ctx.fillRect(right, state.top, fullWidth - right, state.height);
|
|
5296
|
+
ctx.restore();
|
|
5297
|
+
this.canvas.drawControls(ctx);
|
|
5298
|
+
};
|
|
5299
|
+
};
|
|
5300
|
+
|
|
5301
|
+
// src/masks/edit.ts
|
|
4141
5302
|
var MaskEditController = class {
|
|
4142
5303
|
constructor(canvas, onCommit) {
|
|
4143
5304
|
this.canvas = canvas;
|
|
4144
5305
|
this.onCommit = onCommit;
|
|
5306
|
+
this.viewport = new MaskEditViewport(canvas);
|
|
4145
5307
|
}
|
|
4146
5308
|
canvas;
|
|
4147
5309
|
onCommit;
|
|
@@ -4149,6 +5311,7 @@ var MaskEditController = class {
|
|
|
4149
5311
|
editing = null;
|
|
4150
5312
|
child = null;
|
|
4151
5313
|
group = null;
|
|
5314
|
+
viewport;
|
|
4152
5315
|
active() {
|
|
4153
5316
|
return this.editing;
|
|
4154
5317
|
}
|
|
@@ -4179,6 +5342,7 @@ var MaskEditController = class {
|
|
|
4179
5342
|
this.group = group;
|
|
4180
5343
|
this.canvas.add(handle);
|
|
4181
5344
|
this.canvas.setActiveObject(handle);
|
|
5345
|
+
this.viewport.open(handle);
|
|
4182
5346
|
this.canvas.on("object:moving", this.onTransform);
|
|
4183
5347
|
this.canvas.on("object:scaling", this.onTransform);
|
|
4184
5348
|
this.canvas.on("object:rotating", this.onTransform);
|
|
@@ -4193,6 +5357,7 @@ var MaskEditController = class {
|
|
|
4193
5357
|
this.canvas.off("object:scaling", this.onTransform);
|
|
4194
5358
|
this.canvas.off("object:rotating", this.onTransform);
|
|
4195
5359
|
this.canvas.off("object:modified", this.onModified);
|
|
5360
|
+
this.viewport.close();
|
|
4196
5361
|
if (this.canvas.getActiveObject() === this.handle) this.canvas.discardActiveObject();
|
|
4197
5362
|
this.canvas.remove(this.handle);
|
|
4198
5363
|
this.handle = null;
|
|
@@ -4211,15 +5376,15 @@ var MaskEditController = class {
|
|
|
4211
5376
|
onModified = (event) => {
|
|
4212
5377
|
if (!this.handle || event.target !== this.handle) return;
|
|
4213
5378
|
this.write();
|
|
4214
|
-
this.onCommit
|
|
5379
|
+
this.viewport.checkpoint(this.handle, this.onCommit);
|
|
4215
5380
|
};
|
|
4216
5381
|
/** Handle transform (canvas space) → clip child transform (group-relative). */
|
|
4217
5382
|
write() {
|
|
4218
5383
|
if (!this.handle || !this.child || !this.group) return;
|
|
4219
5384
|
applyMatrix(
|
|
4220
5385
|
this.child,
|
|
4221
|
-
|
|
4222
|
-
|
|
5386
|
+
import_fabric20.util.multiplyTransformMatrices(
|
|
5387
|
+
import_fabric20.util.invertTransform(matrixOf(this.group)),
|
|
4223
5388
|
matrixOf(this.handle)
|
|
4224
5389
|
)
|
|
4225
5390
|
);
|
|
@@ -4230,15 +5395,15 @@ var MaskEditController = class {
|
|
|
4230
5395
|
};
|
|
4231
5396
|
|
|
4232
5397
|
// src/masks/store.ts
|
|
4233
|
-
var
|
|
5398
|
+
var import_fabric23 = require("fabric");
|
|
4234
5399
|
|
|
4235
5400
|
// src/masks/host.ts
|
|
4236
|
-
var
|
|
5401
|
+
var import_fabric21 = require("fabric");
|
|
4237
5402
|
function findCanvasHost(layers) {
|
|
4238
5403
|
return layers.getAll().find((layer) => layer.meta.canvasMask);
|
|
4239
5404
|
}
|
|
4240
5405
|
function createCanvasHost(canvas, layers) {
|
|
4241
|
-
const rect = new
|
|
5406
|
+
const rect = new import_fabric21.Rect({
|
|
4242
5407
|
left: 0,
|
|
4243
5408
|
top: 0,
|
|
4244
5409
|
width: canvas.getWidth(),
|
|
@@ -4287,9 +5452,9 @@ function hostBoxOf(canvas, host, absolute) {
|
|
|
4287
5452
|
}
|
|
4288
5453
|
|
|
4289
5454
|
// src/masks/install.ts
|
|
4290
|
-
var
|
|
5455
|
+
var import_fabric22 = require("fabric");
|
|
4291
5456
|
function convertSpace(host, sources, absolute) {
|
|
4292
|
-
const wasAbsolute = host.clipPath instanceof
|
|
5457
|
+
const wasAbsolute = host.clipPath instanceof import_fabric22.Group ? host.clipPath.absolutePositioned : absolute;
|
|
4293
5458
|
if (absolute === wasAbsolute) return;
|
|
4294
5459
|
for (const source of sources) {
|
|
4295
5460
|
if (absolute) toCanvasSpace(source, host);
|
|
@@ -4342,6 +5507,29 @@ var MaskStackStore = class {
|
|
|
4342
5507
|
get(target, maskId) {
|
|
4343
5508
|
return this.list(target).find((entry) => entry.id === maskId);
|
|
4344
5509
|
}
|
|
5510
|
+
/** Repaint a gradient mask without replacing or re-fitting its geometry. */
|
|
5511
|
+
setGradient(target, maskId, gradient, options = {}) {
|
|
5512
|
+
const host = this.host(target);
|
|
5513
|
+
if (!host) return false;
|
|
5514
|
+
const entries = [...this.list(target)];
|
|
5515
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
5516
|
+
if (index === -1) return false;
|
|
5517
|
+
const sources = this.unwrap(target, host);
|
|
5518
|
+
const source = sources[index];
|
|
5519
|
+
if (!source) return false;
|
|
5520
|
+
source.set({
|
|
5521
|
+
fill: toFabricGradient(gradient, {
|
|
5522
|
+
width: Math.max(1, source.width ?? 1),
|
|
5523
|
+
height: Math.max(1, source.height ?? 1)
|
|
5524
|
+
})
|
|
5525
|
+
});
|
|
5526
|
+
entries[index] = {
|
|
5527
|
+
...entries[index],
|
|
5528
|
+
...options.meta ? { meta: options.meta } : {}
|
|
5529
|
+
};
|
|
5530
|
+
this.commit(target, host, entries, sources, options.save ?? true);
|
|
5531
|
+
return true;
|
|
5532
|
+
}
|
|
4345
5533
|
/** Every target that currently carries at least one mask. */
|
|
4346
5534
|
targets() {
|
|
4347
5535
|
return this.layers.getAll().filter((layer) => (layer.meta.maskStack ?? []).length > 0).map((layer) => layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
|
|
@@ -4402,14 +5590,14 @@ var MaskStackStore = class {
|
|
|
4402
5590
|
const entries = this.list(target);
|
|
4403
5591
|
const meta = this.hostLayer(target)?.meta;
|
|
4404
5592
|
if (entries.length === 0 && this.holdsBoxClip(meta)) return [];
|
|
4405
|
-
if (entries.length === 0 || !(clip instanceof
|
|
5593
|
+
if (entries.length === 0 || !(clip instanceof import_fabric23.Group)) {
|
|
4406
5594
|
const source = asObject(clip);
|
|
4407
5595
|
if (meta?.overflow === "hidden") source.clipPath = void 0;
|
|
4408
|
-
return [source];
|
|
5596
|
+
return restoreGradientPaint(entries, [source]);
|
|
4409
5597
|
}
|
|
4410
5598
|
const children = unwrapGroup(clip);
|
|
4411
5599
|
const extra = children.length - entries.length;
|
|
4412
|
-
return extra > 0 ? children.slice(extra) : children;
|
|
5600
|
+
return restoreGradientPaint(entries, extra > 0 ? children.slice(extra) : children);
|
|
4413
5601
|
}
|
|
4414
5602
|
/**
|
|
4415
5603
|
* Entries for a stack, adopting a pre-stack clip as the first one. Without
|
|
@@ -4442,7 +5630,8 @@ var MaskStackStore = class {
|
|
|
4442
5630
|
if (!layer) return;
|
|
4443
5631
|
const absolute = forceAbsolute || target === CANVAS_MASK_TARGET || needsAbsoluteSpace(entries);
|
|
4444
5632
|
convertSpace(host, sources, absolute);
|
|
4445
|
-
const
|
|
5633
|
+
const painted = captureGradientPaint(entries, sources);
|
|
5634
|
+
const next = withRelativeTransforms(painted, sources, host, absolute);
|
|
4446
5635
|
installClip(host, next, sources, this.hostBox(target, absolute), absolute);
|
|
4447
5636
|
if (next.length === 0) {
|
|
4448
5637
|
delete layer.meta.maskStack;
|
|
@@ -4456,12 +5645,41 @@ var MaskStackStore = class {
|
|
|
4456
5645
|
if (save) this.history.save();
|
|
4457
5646
|
}
|
|
4458
5647
|
};
|
|
5648
|
+
function restoreGradientPaint(entries, sources) {
|
|
5649
|
+
entries.forEach((entry, index) => {
|
|
5650
|
+
const source = sources[index];
|
|
5651
|
+
if (!source || !entry.gradient) return;
|
|
5652
|
+
const box = {
|
|
5653
|
+
width: Math.max(1, source.width ?? 1),
|
|
5654
|
+
height: Math.max(1, source.height ?? 1)
|
|
5655
|
+
};
|
|
5656
|
+
const fill = source.fill;
|
|
5657
|
+
const rendered = toFabricMaskGradient(entry.gradient, box);
|
|
5658
|
+
if (fill instanceof import_fabric23.Gradient && JSON.stringify(fill.toObject()) !== JSON.stringify(rendered.toObject())) {
|
|
5659
|
+
return;
|
|
5660
|
+
}
|
|
5661
|
+
source.set({
|
|
5662
|
+
fill: toFabricGradient(entry.gradient, box)
|
|
5663
|
+
});
|
|
5664
|
+
});
|
|
5665
|
+
return sources;
|
|
5666
|
+
}
|
|
5667
|
+
function captureGradientPaint(entries, sources) {
|
|
5668
|
+
return entries.map((entry, index) => {
|
|
5669
|
+
const gradient = sources[index] ? readGradientConfig(sources[index]) : null;
|
|
5670
|
+
if (gradient) return { ...entry, gradient };
|
|
5671
|
+
if (!entry.gradient) return entry;
|
|
5672
|
+
const next = { ...entry };
|
|
5673
|
+
delete next.gradient;
|
|
5674
|
+
return next;
|
|
5675
|
+
});
|
|
5676
|
+
}
|
|
4459
5677
|
|
|
4460
5678
|
// src/masks/manager.ts
|
|
4461
5679
|
var LayerMaskManager = class extends MaskStackStore {
|
|
4462
5680
|
// Committing mid-drag would recompose the group the handle writes into and
|
|
4463
5681
|
// leave it pointing at a discarded object; the geometry is settled on endEdit.
|
|
4464
|
-
edits = new MaskEditController(this.canvas, () => this.history.
|
|
5682
|
+
edits = new MaskEditController(this.canvas, () => this.history.saveImmediate());
|
|
4465
5683
|
onLayersChanged = () => this.pin();
|
|
4466
5684
|
// Selecting a layer means the user has moved on from the mask they had open;
|
|
4467
5685
|
// leaving both selected would show mask controls for an unrelated layer.
|
|
@@ -4506,7 +5724,7 @@ var LayerMaskManager = class extends MaskStackStore {
|
|
|
4506
5724
|
if (index === -1) return false;
|
|
4507
5725
|
this.endEdit(false);
|
|
4508
5726
|
this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
|
|
4509
|
-
const group = host.clipPath instanceof
|
|
5727
|
+
const group = host.clipPath instanceof import_fabric24.Group ? host.clipPath : null;
|
|
4510
5728
|
if (!group) return false;
|
|
4511
5729
|
const children = group.getObjects();
|
|
4512
5730
|
const child = children[children.length - entries.length + index];
|
|
@@ -4848,110 +6066,6 @@ function layerTypeOf(object) {
|
|
|
4848
6066
|
return "shape";
|
|
4849
6067
|
}
|
|
4850
6068
|
|
|
4851
|
-
// src/gradient.ts
|
|
4852
|
-
var import_fabric21 = require("fabric");
|
|
4853
|
-
var DEFAULT_GRADIENT_CONFIG = {
|
|
4854
|
-
kind: "linear",
|
|
4855
|
-
angle: 90,
|
|
4856
|
-
center: { x: 0.5, y: 0.5 },
|
|
4857
|
-
radius: 0.5,
|
|
4858
|
-
stops: [
|
|
4859
|
-
{ color: "#ffffff", opacity: 1, position: 0 },
|
|
4860
|
-
{ color: "#000000", opacity: 1, position: 1 }
|
|
4861
|
-
]
|
|
4862
|
-
};
|
|
4863
|
-
var MIN_RADIUS = 1e-3;
|
|
4864
|
-
var MAX_GRADIENT_STOPS = 256;
|
|
4865
|
-
var FALLBACK_STOP_COLOR = DEFAULT_GRADIENT_CONFIG.stops[0].color;
|
|
4866
|
-
function linearCoords(angle, box) {
|
|
4867
|
-
const radians = angle * Math.PI / 180;
|
|
4868
|
-
const sin = Math.sin(radians);
|
|
4869
|
-
const cos = Math.cos(radians);
|
|
4870
|
-
const vx = box.width * sin;
|
|
4871
|
-
const vy = -box.height * cos;
|
|
4872
|
-
const length = Math.abs(box.width * sin) + Math.abs(box.height * cos);
|
|
4873
|
-
const scale = length / (vx * vx + vy * vy || 1);
|
|
4874
|
-
return {
|
|
4875
|
-
x1: 0.5 - scale * vx / 2,
|
|
4876
|
-
y1: 0.5 - scale * vy / 2,
|
|
4877
|
-
x2: 0.5 + scale * vx / 2,
|
|
4878
|
-
y2: 0.5 + scale * vy / 2
|
|
4879
|
-
};
|
|
4880
|
-
}
|
|
4881
|
-
function angleFromCoords(coords, box) {
|
|
4882
|
-
const dx = (coords.x2 - coords.x1) / (box.width || 1);
|
|
4883
|
-
const dy = (coords.y2 - coords.y1) / (box.height || 1);
|
|
4884
|
-
const degrees = Math.atan2(dx, -dy) * 180 / Math.PI;
|
|
4885
|
-
return (degrees + 360) % 360;
|
|
4886
|
-
}
|
|
4887
|
-
function stopColor(stop) {
|
|
4888
|
-
const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
|
|
4889
|
-
const color = new import_fabric21.Color(source);
|
|
4890
|
-
const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
|
|
4891
|
-
color.setAlpha(clamp(opacity, 0, 1));
|
|
4892
|
-
return color.toRgba();
|
|
4893
|
-
}
|
|
4894
|
-
function toFabricGradient(config, box) {
|
|
4895
|
-
const colorStops = [...config.stops].sort((a, b) => a.position - b.position).slice(0, MAX_GRADIENT_STOPS).map((stop) => {
|
|
4896
|
-
const position = Number.isFinite(stop.position) ? stop.position : 0;
|
|
4897
|
-
return { offset: clamp(position, 0, 1), color: stopColor(stop) };
|
|
4898
|
-
});
|
|
4899
|
-
if (config.kind === "radial") {
|
|
4900
|
-
const radius = Math.max(MIN_RADIUS, config.radius);
|
|
4901
|
-
return new import_fabric21.Gradient({
|
|
4902
|
-
type: "radial",
|
|
4903
|
-
gradientUnits: "percentage",
|
|
4904
|
-
coords: {
|
|
4905
|
-
x1: config.center.x,
|
|
4906
|
-
y1: config.center.y,
|
|
4907
|
-
r1: 0,
|
|
4908
|
-
x2: config.center.x,
|
|
4909
|
-
y2: config.center.y,
|
|
4910
|
-
r2: radius
|
|
4911
|
-
},
|
|
4912
|
-
colorStops
|
|
4913
|
-
});
|
|
4914
|
-
}
|
|
4915
|
-
return new import_fabric21.Gradient({
|
|
4916
|
-
type: "linear",
|
|
4917
|
-
gradientUnits: "percentage",
|
|
4918
|
-
coords: linearCoords(config.angle, box),
|
|
4919
|
-
colorStops
|
|
4920
|
-
});
|
|
4921
|
-
}
|
|
4922
|
-
function readGradientConfig(object) {
|
|
4923
|
-
const fill = object.fill;
|
|
4924
|
-
if (!fill || typeof fill === "string" || !(fill instanceof import_fabric21.Gradient)) return null;
|
|
4925
|
-
const stops = fill.colorStops.map((stop) => {
|
|
4926
|
-
const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
|
|
4927
|
-
const color = new import_fabric21.Color(source);
|
|
4928
|
-
const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
|
|
4929
|
-
return {
|
|
4930
|
-
color: `#${color.toHex().toLowerCase()}`,
|
|
4931
|
-
opacity: color.getAlpha(),
|
|
4932
|
-
position: clamp(offset, 0, 1)
|
|
4933
|
-
};
|
|
4934
|
-
});
|
|
4935
|
-
if (fill.type === "radial") {
|
|
4936
|
-
const { x1, y1, r2 } = fill.coords;
|
|
4937
|
-
return {
|
|
4938
|
-
kind: "radial",
|
|
4939
|
-
angle: DEFAULT_GRADIENT_CONFIG.angle,
|
|
4940
|
-
center: { x: x1, y: y1 },
|
|
4941
|
-
radius: r2,
|
|
4942
|
-
stops
|
|
4943
|
-
};
|
|
4944
|
-
}
|
|
4945
|
-
const box = { width: object.width || 1, height: object.height || 1 };
|
|
4946
|
-
return {
|
|
4947
|
-
kind: "linear",
|
|
4948
|
-
angle: angleFromCoords(fill.coords, box),
|
|
4949
|
-
center: { ...DEFAULT_GRADIENT_CONFIG.center },
|
|
4950
|
-
radius: DEFAULT_GRADIENT_CONFIG.radius,
|
|
4951
|
-
stops
|
|
4952
|
-
};
|
|
4953
|
-
}
|
|
4954
|
-
|
|
4955
6069
|
// src/editor.ts
|
|
4956
6070
|
var MIN_ZOOM = 0.01;
|
|
4957
6071
|
var MAX_ZOOM = 8;
|
|
@@ -4967,11 +6081,17 @@ var CanvasEditor = class {
|
|
|
4967
6081
|
snapping;
|
|
4968
6082
|
crop;
|
|
4969
6083
|
patterns;
|
|
6084
|
+
/** Parametric shape layers: `meta.shape` and in-place regeneration. */
|
|
6085
|
+
shapes;
|
|
4970
6086
|
curves;
|
|
4971
6087
|
wrap;
|
|
4972
6088
|
maskPresets;
|
|
6089
|
+
/** Owner of every image layer's filter array (D5). */
|
|
6090
|
+
imageEffects;
|
|
4973
6091
|
/** Stacked boolean masks, per layer and for the design as a whole. */
|
|
4974
6092
|
layerMasks;
|
|
6093
|
+
/** One effect per text or shape layer (D6). */
|
|
6094
|
+
effects;
|
|
4975
6095
|
fonts;
|
|
4976
6096
|
licensing;
|
|
4977
6097
|
pages;
|
|
@@ -4981,6 +6101,8 @@ var CanvasEditor = class {
|
|
|
4981
6101
|
zoomLevel = 1;
|
|
4982
6102
|
selectionStyle = { ...DEFAULT_SELECTION_STYLE };
|
|
4983
6103
|
mockup = null;
|
|
6104
|
+
/** Alt was held when a drag began on this layer; the drop leaves a copy behind. */
|
|
6105
|
+
altDrag = null;
|
|
4984
6106
|
// The design's configured background. The live canvas background is forced
|
|
4985
6107
|
// transparent while a mockup preview is shown, so this is the source of truth
|
|
4986
6108
|
// for serialization and export — not the (possibly transient) canvas value.
|
|
@@ -4988,6 +6110,7 @@ var CanvasEditor = class {
|
|
|
4988
6110
|
designBackgroundImage = null;
|
|
4989
6111
|
backgroundImageOptions = null;
|
|
4990
6112
|
constructor(canvasElement, config) {
|
|
6113
|
+
(0, import_canvas_editor_effects2.registerEffects)(fabricModule);
|
|
4991
6114
|
this.events = new EventEmitter();
|
|
4992
6115
|
this.fonts = new FontRegistry();
|
|
4993
6116
|
config.fonts?.forEach((font) => this.fonts.register(font));
|
|
@@ -4996,7 +6119,7 @@ var CanvasEditor = class {
|
|
|
4996
6119
|
const widthPx = this.units.toPixels(config.width);
|
|
4997
6120
|
const heightPx = this.units.toPixels(config.height);
|
|
4998
6121
|
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
4999
|
-
this.canvas = new
|
|
6122
|
+
this.canvas = new import_fabric25.Canvas(canvasElement, {
|
|
5000
6123
|
width: widthPx,
|
|
5001
6124
|
height: heightPx,
|
|
5002
6125
|
backgroundColor: this.designBackground,
|
|
@@ -5022,6 +6145,15 @@ var CanvasEditor = class {
|
|
|
5022
6145
|
this.wrap = new TextWrapManager(this.canvas, this.layers, this.history, this.events);
|
|
5023
6146
|
this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
|
|
5024
6147
|
this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
|
|
6148
|
+
this.effects = new LayerEffectsManager(this.canvas, this.layers, this.history, this.events);
|
|
6149
|
+
this.imageEffects = new ImageEffectsManager(
|
|
6150
|
+
this.canvas,
|
|
6151
|
+
this.layers,
|
|
6152
|
+
this.history,
|
|
6153
|
+
this.events
|
|
6154
|
+
);
|
|
6155
|
+
this.shapes = new ShapeManager(this.canvas, this.layers, this.history, this.events);
|
|
6156
|
+
config.shapes?.forEach((plugin) => this.shapes.register(plugin));
|
|
5025
6157
|
this.setupCanvasEvents();
|
|
5026
6158
|
this.refreshSelectionStyle();
|
|
5027
6159
|
this.history.saveImmediate();
|
|
@@ -5031,7 +6163,7 @@ var CanvasEditor = class {
|
|
|
5031
6163
|
// ─── Layer Operations ────────────────────────────────
|
|
5032
6164
|
async addImage(url, options) {
|
|
5033
6165
|
try {
|
|
5034
|
-
const img = await
|
|
6166
|
+
const img = await import_fabric25.FabricImage.fromURL(
|
|
5035
6167
|
url,
|
|
5036
6168
|
{},
|
|
5037
6169
|
{ originX: "left", originY: "top", ...options }
|
|
@@ -5046,7 +6178,7 @@ var CanvasEditor = class {
|
|
|
5046
6178
|
}
|
|
5047
6179
|
}
|
|
5048
6180
|
/** Replace an image source without changing its layer identity or visual transform. */
|
|
5049
|
-
async replaceImageSource(layerId, url) {
|
|
6181
|
+
async replaceImageSource(layerId, url, options = {}) {
|
|
5050
6182
|
const layer = this.layers.get(layerId);
|
|
5051
6183
|
if (!layer || layer.type !== "image" && layer.type !== "mask") {
|
|
5052
6184
|
throw new Error(`Image or mask layer not found: ${layerId}`);
|
|
@@ -5057,7 +6189,11 @@ var CanvasEditor = class {
|
|
|
5057
6189
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
5058
6190
|
const previous = layer.fabricObject;
|
|
5059
6191
|
try {
|
|
5060
|
-
const replacement = await
|
|
6192
|
+
const replacement = await import_fabric25.FabricImage.fromURL(
|
|
6193
|
+
url,
|
|
6194
|
+
{ crossOrigin: options.crossOrigin },
|
|
6195
|
+
{ originX: "left", originY: "top" }
|
|
6196
|
+
);
|
|
5061
6197
|
replacement.set({
|
|
5062
6198
|
left: previous.left,
|
|
5063
6199
|
top: previous.top,
|
|
@@ -5079,14 +6215,28 @@ var CanvasEditor = class {
|
|
|
5079
6215
|
replacement.applyFilters();
|
|
5080
6216
|
replacement.setCoords();
|
|
5081
6217
|
this.layers.replaceObject(layerId, replacement);
|
|
6218
|
+
this.imageEffects.refresh(layerId);
|
|
5082
6219
|
return layer;
|
|
5083
6220
|
} catch (error) {
|
|
5084
6221
|
this.events.emit("error", { message: "Failed to replace image source", error });
|
|
5085
6222
|
throw error;
|
|
5086
6223
|
}
|
|
5087
6224
|
}
|
|
6225
|
+
/**
|
|
6226
|
+
* The class the registry holds for a stock name.
|
|
6227
|
+
*
|
|
6228
|
+
* `registerEffects` (run in the constructor) overrides the stock names, but
|
|
6229
|
+
* registration only decides what an ENLIVEN produces — a `new Textbox(...)`
|
|
6230
|
+
* here would still be the stock class, which carries no `effect` in its
|
|
6231
|
+
* `customProperties` and so could never serialize one. Authoring and
|
|
6232
|
+
* restoring have to yield the same class.
|
|
6233
|
+
*/
|
|
6234
|
+
registered(name, fallback) {
|
|
6235
|
+
return import_fabric25.classRegistry.getClass(name) ?? fallback;
|
|
6236
|
+
}
|
|
5088
6237
|
addText(text, options) {
|
|
5089
|
-
const
|
|
6238
|
+
const TextboxClass = this.registered("Textbox", import_fabric25.Textbox);
|
|
6239
|
+
const textbox = new TextboxClass(text, {
|
|
5090
6240
|
fontSize: 32,
|
|
5091
6241
|
fontFamily: "Arial",
|
|
5092
6242
|
fill: "#000000",
|
|
@@ -5104,6 +6254,15 @@ var CanvasEditor = class {
|
|
|
5104
6254
|
addShape(plugin, options) {
|
|
5105
6255
|
const obj = plugin.create(options);
|
|
5106
6256
|
const layer = this.layers.add("shape", obj, plugin.name);
|
|
6257
|
+
if (plugin.params) {
|
|
6258
|
+
this.shapes.register(plugin);
|
|
6259
|
+
const overrides = options?.params ?? {};
|
|
6260
|
+
layer.meta.shape = {
|
|
6261
|
+
id: plugin.name,
|
|
6262
|
+
params: resolveShapeParams(plugin, overrides),
|
|
6263
|
+
version: 1
|
|
6264
|
+
};
|
|
6265
|
+
}
|
|
5107
6266
|
this.layers.select(layer.id);
|
|
5108
6267
|
this.history.save();
|
|
5109
6268
|
return layer;
|
|
@@ -5115,7 +6274,8 @@ var CanvasEditor = class {
|
|
|
5115
6274
|
addGradient(config = DEFAULT_GRADIENT_CONFIG, box) {
|
|
5116
6275
|
const width = box?.width ?? this.canvas.getWidth();
|
|
5117
6276
|
const height = box?.height ?? this.canvas.getHeight();
|
|
5118
|
-
const
|
|
6277
|
+
const RectClass = this.registered("Rect", import_fabric25.Rect);
|
|
6278
|
+
const object = new RectClass({
|
|
5119
6279
|
left: box?.left ?? 0,
|
|
5120
6280
|
top: box?.top ?? 0,
|
|
5121
6281
|
originX: "left",
|
|
@@ -5244,6 +6404,49 @@ var CanvasEditor = class {
|
|
|
5244
6404
|
this.canvas.requestRenderAll();
|
|
5245
6405
|
this.history.save();
|
|
5246
6406
|
}
|
|
6407
|
+
// ─── Align ──────────────────────────────────────────
|
|
6408
|
+
/** What the user is acting on: the proxy where a layer has one. */
|
|
6409
|
+
selectedTargets() {
|
|
6410
|
+
return this.canvas.getActiveObjects().filter((object) => this.layers.findByObject(object) !== void 0);
|
|
6411
|
+
}
|
|
6412
|
+
afterMove(objects) {
|
|
6413
|
+
relayoutSelection(this.canvas.getActiveObject());
|
|
6414
|
+
for (const object of objects) {
|
|
6415
|
+
const layer = this.layers.findByObject(object);
|
|
6416
|
+
if (!layer) continue;
|
|
6417
|
+
if (layer.renderProxy === object) this.patterns.commitProxy(layer.id);
|
|
6418
|
+
this.events.emit("layer:modified", { layerId: layer.id });
|
|
6419
|
+
}
|
|
6420
|
+
this.canvas.requestRenderAll();
|
|
6421
|
+
this.history.save();
|
|
6422
|
+
}
|
|
6423
|
+
/** One layer aligns to the canvas; several align to their common box. One history entry. */
|
|
6424
|
+
alignLayers(target) {
|
|
6425
|
+
const objects = this.selectedTargets();
|
|
6426
|
+
if (objects.length === 0) return false;
|
|
6427
|
+
const frame = objects.length === 1 ? canvasBox(this.canvas.getWidth(), this.canvas.getHeight()) : unionBox(objects.map((o) => (o.setCoords(), o.getBoundingRect())));
|
|
6428
|
+
this.history.beginTransaction();
|
|
6429
|
+
try {
|
|
6430
|
+
alignObjects(objects, target, frame);
|
|
6431
|
+
this.afterMove(objects);
|
|
6432
|
+
} finally {
|
|
6433
|
+
this.history.endTransaction();
|
|
6434
|
+
}
|
|
6435
|
+
return true;
|
|
6436
|
+
}
|
|
6437
|
+
/** Equal gaps along an axis. Needs three selected; returns false otherwise. */
|
|
6438
|
+
distributeLayers(axis) {
|
|
6439
|
+
const objects = this.selectedTargets();
|
|
6440
|
+
if (objects.length < 3) return false;
|
|
6441
|
+
this.history.beginTransaction();
|
|
6442
|
+
try {
|
|
6443
|
+
distributeObjects(objects, axis);
|
|
6444
|
+
this.afterMove(objects);
|
|
6445
|
+
} finally {
|
|
6446
|
+
this.history.endTransaction();
|
|
6447
|
+
}
|
|
6448
|
+
return true;
|
|
6449
|
+
}
|
|
5247
6450
|
/** Clone a layer (offset slightly) and select the copy. */
|
|
5248
6451
|
async duplicateLayer(id) {
|
|
5249
6452
|
const layer = this.layers.get(id);
|
|
@@ -5268,24 +6471,19 @@ var CanvasEditor = class {
|
|
|
5268
6471
|
this.history.save();
|
|
5269
6472
|
return copy;
|
|
5270
6473
|
}
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
layer.meta.imageAdjustments = next;
|
|
5285
|
-
image.applyFilters();
|
|
5286
|
-
this.canvas.requestRenderAll();
|
|
5287
|
-
this.history.save();
|
|
5288
|
-
return true;
|
|
6474
|
+
/** The drag moved the original; a copy takes the start position, just beneath it. */
|
|
6475
|
+
async leaveCopyBehind(layer, transform) {
|
|
6476
|
+
const now = import_fabric25.util.saveObjectTransform(layer.fabricObject);
|
|
6477
|
+
if (now.left === transform.left && now.top === transform.top) return;
|
|
6478
|
+
await this.history.transaction(async () => {
|
|
6479
|
+
const copy = await this.duplicateLayer(layer.id);
|
|
6480
|
+
if (!copy) return;
|
|
6481
|
+
copy.fabricObject.set(transform);
|
|
6482
|
+
copy.fabricObject.setCoords();
|
|
6483
|
+
this.layers.reorder(copy.id, this.layers.getAll().indexOf(layer));
|
|
6484
|
+
this.layers.select(layer.id);
|
|
6485
|
+
this.canvas.requestRenderAll();
|
|
6486
|
+
});
|
|
5289
6487
|
}
|
|
5290
6488
|
/**
|
|
5291
6489
|
* Combine two or more layers into one group layer. Picking a group among them
|
|
@@ -5320,7 +6518,7 @@ var CanvasEditor = class {
|
|
|
5320
6518
|
this.layers.detach(layer.id);
|
|
5321
6519
|
}
|
|
5322
6520
|
}
|
|
5323
|
-
const group = new
|
|
6521
|
+
const group = new import_fabric25.Group(objects);
|
|
5324
6522
|
const grouped = this.layers.add("group", group, name);
|
|
5325
6523
|
this.layers.adoptChildren(grouped, children);
|
|
5326
6524
|
this.layers.reorder(grouped.id, insertIndex);
|
|
@@ -5354,6 +6552,7 @@ var CanvasEditor = class {
|
|
|
5354
6552
|
await deserializeEditor(this, state);
|
|
5355
6553
|
await this.patterns.rehydrateAll();
|
|
5356
6554
|
this.layerMasks.refreshAll();
|
|
6555
|
+
this.imageEffects.refreshAll();
|
|
5357
6556
|
} catch (error) {
|
|
5358
6557
|
if (!managedByHistory) {
|
|
5359
6558
|
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
@@ -5382,7 +6581,7 @@ var CanvasEditor = class {
|
|
|
5382
6581
|
const layer = this.layers.get(id);
|
|
5383
6582
|
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
5384
6583
|
try {
|
|
5385
|
-
if (options.resolution === "source" && layer.fabricObject instanceof
|
|
6584
|
+
if (options.resolution === "source" && layer.fabricObject instanceof import_fabric25.FabricImage) {
|
|
5386
6585
|
const image = await layer.fabricObject.clone();
|
|
5387
6586
|
image.set({
|
|
5388
6587
|
left: 0,
|
|
@@ -5430,7 +6629,9 @@ var CanvasEditor = class {
|
|
|
5430
6629
|
try {
|
|
5431
6630
|
await this.fonts.ready();
|
|
5432
6631
|
const blob = await this.withDesignBackground(
|
|
5433
|
-
() =>
|
|
6632
|
+
() => this.imageEffects.withSourceResolution(
|
|
6633
|
+
() => exportPNG(this.canvas, { ...options, format })
|
|
6634
|
+
)
|
|
5434
6635
|
);
|
|
5435
6636
|
this.events.emit("export:complete", { format });
|
|
5436
6637
|
this.licensing.track(`export:${format}`);
|
|
@@ -5468,7 +6669,9 @@ var CanvasEditor = class {
|
|
|
5468
6669
|
}
|
|
5469
6670
|
}
|
|
5470
6671
|
toDataURL(format = "png", multiplier = 1) {
|
|
5471
|
-
return this.withDesignBackground(
|
|
6672
|
+
return this.withDesignBackground(
|
|
6673
|
+
() => this.imageEffects.withSourceResolution(() => exportDataURL(this.canvas, format, multiplier))
|
|
6674
|
+
);
|
|
5472
6675
|
}
|
|
5473
6676
|
/**
|
|
5474
6677
|
* Export the print file: the design cropped to the mockup's print area, on
|
|
@@ -5492,7 +6695,9 @@ var CanvasEditor = class {
|
|
|
5492
6695
|
if (!this.mockup) throw new Error("No mockup is configured");
|
|
5493
6696
|
await this.fonts.ready();
|
|
5494
6697
|
try {
|
|
5495
|
-
const blob = await
|
|
6698
|
+
const blob = await this.imageEffects.withSourceResolution(
|
|
6699
|
+
() => exportMockup(this.canvas, this.mockup, options)
|
|
6700
|
+
);
|
|
5496
6701
|
this.licensing.track(`export:mockup:${options.format ?? "png"}`);
|
|
5497
6702
|
return blob;
|
|
5498
6703
|
} catch (error) {
|
|
@@ -5634,10 +6839,10 @@ var CanvasEditor = class {
|
|
|
5634
6839
|
this.imageProvider = provider;
|
|
5635
6840
|
}
|
|
5636
6841
|
// ─── Canvas Operations ──────────────────────────────
|
|
5637
|
-
setBackground(
|
|
5638
|
-
this.designBackground =
|
|
6842
|
+
setBackground(color2) {
|
|
6843
|
+
this.designBackground = color2;
|
|
5639
6844
|
if (!this.mockup) {
|
|
5640
|
-
this.canvas.backgroundColor =
|
|
6845
|
+
this.canvas.backgroundColor = color2;
|
|
5641
6846
|
this.canvas.requestRenderAll();
|
|
5642
6847
|
}
|
|
5643
6848
|
this.history.save();
|
|
@@ -5658,7 +6863,7 @@ var CanvasEditor = class {
|
|
|
5658
6863
|
return;
|
|
5659
6864
|
}
|
|
5660
6865
|
try {
|
|
5661
|
-
const image = await
|
|
6866
|
+
const image = await import_fabric25.FabricImage.fromURL(
|
|
5662
6867
|
url,
|
|
5663
6868
|
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
5664
6869
|
{ originX: "left", originY: "top" }
|
|
@@ -5944,9 +7149,11 @@ var CanvasEditor = class {
|
|
|
5944
7149
|
}
|
|
5945
7150
|
// ─── Cleanup ────────────────────────────────────────
|
|
5946
7151
|
dispose() {
|
|
7152
|
+
this.imageEffects.dispose();
|
|
5947
7153
|
this.masks.dispose();
|
|
5948
7154
|
this.layerMasks.dispose();
|
|
5949
7155
|
this.snapping.dispose();
|
|
7156
|
+
this.shapes.dispose();
|
|
5950
7157
|
this.crop.dispose();
|
|
5951
7158
|
this.history.dispose();
|
|
5952
7159
|
this.patterns.dispose();
|
|
@@ -5980,12 +7187,26 @@ var CanvasEditor = class {
|
|
|
5980
7187
|
const active = this.canvas.getActiveObject();
|
|
5981
7188
|
if (active) applyObjectSelectionStyle(active, this.selectionStyle, this.zoomLevel);
|
|
5982
7189
|
};
|
|
7190
|
+
this.canvas.on("mouse:down", (e) => {
|
|
7191
|
+
this.altDrag = null;
|
|
7192
|
+
const target = e.target;
|
|
7193
|
+
const pointer = e.e;
|
|
7194
|
+
if (!target || !pointer?.altKey || target instanceof import_fabric25.ActiveSelection) return;
|
|
7195
|
+
const layer = this.layers.findByObject(target);
|
|
7196
|
+
if (!layer || layer.locked || layer.renderProxy) return;
|
|
7197
|
+
this.altDrag = { layerId: layer.id, transform: import_fabric25.util.saveObjectTransform(target) };
|
|
7198
|
+
});
|
|
5983
7199
|
this.canvas.on("object:modified", (e) => {
|
|
5984
7200
|
if (!e.target) return;
|
|
5985
7201
|
const layer = this.layers.findByObject(e.target);
|
|
5986
7202
|
if (layer) {
|
|
5987
7203
|
this.events.emit("layer:modified", { layerId: layer.id });
|
|
5988
7204
|
this.history.save();
|
|
7205
|
+
const pending = this.altDrag;
|
|
7206
|
+
this.altDrag = null;
|
|
7207
|
+
if (pending && pending.layerId === layer.id) {
|
|
7208
|
+
void this.leaveCopyBehind(layer, pending.transform);
|
|
7209
|
+
}
|
|
5989
7210
|
}
|
|
5990
7211
|
});
|
|
5991
7212
|
this.canvas.on("selection:created", (e) => {
|
|
@@ -6074,6 +7295,8 @@ var AnnotationOverlay = class {
|
|
|
6074
7295
|
};
|
|
6075
7296
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6076
7297
|
0 && (module.exports = {
|
|
7298
|
+
ADJUST_KEYS,
|
|
7299
|
+
ADJUST_RANGES,
|
|
6077
7300
|
AnnotationOverlay,
|
|
6078
7301
|
BLEND_GROUPS,
|
|
6079
7302
|
BLEND_MODES,
|
|
@@ -6089,7 +7312,12 @@ var AnnotationOverlay = class {
|
|
|
6089
7312
|
EventEmitter,
|
|
6090
7313
|
FontRegistry,
|
|
6091
7314
|
HistoryManager,
|
|
7315
|
+
IMAGE_FILTER_PRESETS,
|
|
7316
|
+
IMAGE_PROXY_MAX_EDGE,
|
|
7317
|
+
ImageEffectsManager,
|
|
7318
|
+
LAYER_EFFECT_DEFAULTS,
|
|
6092
7319
|
Layer,
|
|
7320
|
+
LayerEffectsManager,
|
|
6093
7321
|
LayerManager,
|
|
6094
7322
|
LayerMaskManager,
|
|
6095
7323
|
LicenseManager,
|
|
@@ -6097,10 +7325,12 @@ var AnnotationOverlay = class {
|
|
|
6097
7325
|
MaskController,
|
|
6098
7326
|
MaskPresetManager,
|
|
6099
7327
|
MaskRefinementError,
|
|
7328
|
+
NEUTRAL_ADJUST,
|
|
6100
7329
|
PatternManager,
|
|
6101
7330
|
ProjectManager,
|
|
6102
7331
|
SHAPE_MASK_BOX,
|
|
6103
7332
|
SHAPE_MASK_IDS,
|
|
7333
|
+
ShapeManager,
|
|
6104
7334
|
SnapManager,
|
|
6105
7335
|
TEXTURE_MASK_IDS,
|
|
6106
7336
|
TEXTURE_MASK_SIZE,
|
|
@@ -6108,6 +7338,8 @@ var AnnotationOverlay = class {
|
|
|
6108
7338
|
TextWrapManager,
|
|
6109
7339
|
TiledPatternObject,
|
|
6110
7340
|
UnitConverter,
|
|
7341
|
+
alignObjects,
|
|
7342
|
+
alphaClipMatrix,
|
|
6111
7343
|
angleFromCoords,
|
|
6112
7344
|
applyAspectLock,
|
|
6113
7345
|
applyLayerShadow,
|
|
@@ -6120,6 +7352,8 @@ var AnnotationOverlay = class {
|
|
|
6120
7352
|
buildCurveLinePaths,
|
|
6121
7353
|
buildCurvePathData,
|
|
6122
7354
|
clamp,
|
|
7355
|
+
clampAdjust,
|
|
7356
|
+
clampAlphaClip,
|
|
6123
7357
|
clearTextureMaskCache,
|
|
6124
7358
|
composeMaskGroup,
|
|
6125
7359
|
computeCoverPlacement,
|
|
@@ -6127,6 +7361,7 @@ var AnnotationOverlay = class {
|
|
|
6127
7361
|
computeTilePositions,
|
|
6128
7362
|
deserializeEditor,
|
|
6129
7363
|
displaceRgba,
|
|
7364
|
+
distributeObjects,
|
|
6130
7365
|
drawTiles,
|
|
6131
7366
|
escapeXml,
|
|
6132
7367
|
explodeTemplateSvg,
|
|
@@ -6138,12 +7373,15 @@ var AnnotationOverlay = class {
|
|
|
6138
7373
|
fitTextWidth,
|
|
6139
7374
|
fitToBox,
|
|
6140
7375
|
generateId,
|
|
7376
|
+
imageEffectsFilterSpecs,
|
|
7377
|
+
imageFilterPreset,
|
|
6141
7378
|
isCssColor,
|
|
6142
7379
|
isMaskPresetId,
|
|
6143
7380
|
isShapeMaskId,
|
|
6144
7381
|
isTextureMaskId,
|
|
6145
7382
|
linearCoords,
|
|
6146
7383
|
needsAbsoluteSpace,
|
|
7384
|
+
normalizeImageEffects,
|
|
6147
7385
|
normalizeTextCurve,
|
|
6148
7386
|
preWrapWordSplit,
|
|
6149
7387
|
readGradientConfig,
|
|
@@ -6151,6 +7389,7 @@ var AnnotationOverlay = class {
|
|
|
6151
7389
|
readTextWrap,
|
|
6152
7390
|
renderTextureMask,
|
|
6153
7391
|
resetTransform,
|
|
7392
|
+
resolveShapeParams,
|
|
6154
7393
|
restoreLocks,
|
|
6155
7394
|
round2,
|
|
6156
7395
|
sanitizeSvg,
|
|
@@ -6161,6 +7400,7 @@ var AnnotationOverlay = class {
|
|
|
6161
7400
|
toCanvasSpace,
|
|
6162
7401
|
toFabricGradient,
|
|
6163
7402
|
toHostSpace,
|
|
7403
|
+
unionBox,
|
|
6164
7404
|
unwrapGroup
|
|
6165
7405
|
});
|
|
6166
7406
|
//# sourceMappingURL=index.js.map
|