@overtone-art/canvas-editor-core 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +339 -25
- package/dist/index.d.ts +339 -25
- package/dist/index.global.js +53 -53
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1053 -103
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1034 -92
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/{types-BNzE_X5M.d.mts → types-JP8MZsIs.d.mts} +42 -1
- package/dist/{types-BNzE_X5M.d.ts → types-JP8MZsIs.d.ts} +42 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AnnotationOverlay: () => AnnotationOverlay,
|
|
24
|
+
CANVAS_MASK_TARGET: () => CANVAS_MASK_TARGET,
|
|
24
25
|
CANVAS_SIZE_PRESETS: () => CANVAS_SIZE_PRESETS,
|
|
25
26
|
CanvasEditor: () => CanvasEditor,
|
|
26
27
|
CropController: () => CropController,
|
|
@@ -33,6 +34,7 @@ __export(index_exports, {
|
|
|
33
34
|
HistoryManager: () => HistoryManager,
|
|
34
35
|
Layer: () => Layer,
|
|
35
36
|
LayerManager: () => LayerManager,
|
|
37
|
+
LayerMaskManager: () => LayerMaskManager,
|
|
36
38
|
LicenseManager: () => LicenseManager,
|
|
37
39
|
MaskController: () => MaskController,
|
|
38
40
|
MaskPresetManager: () => MaskPresetManager,
|
|
@@ -54,6 +56,7 @@ __export(index_exports, {
|
|
|
54
56
|
buildCurvePathData: () => buildCurvePathData,
|
|
55
57
|
clamp: () => clamp,
|
|
56
58
|
clearTextureMaskCache: () => clearTextureMaskCache,
|
|
59
|
+
composeMaskGroup: () => composeMaskGroup,
|
|
57
60
|
computeCoverPlacement: () => computeCoverPlacement,
|
|
58
61
|
computePrintAreaClip: () => computePrintAreaClip,
|
|
59
62
|
computeTilePositions: () => computeTilePositions,
|
|
@@ -66,11 +69,13 @@ __export(index_exports, {
|
|
|
66
69
|
exportPNG: () => exportPNG,
|
|
67
70
|
exportPrintArea: () => exportPrintArea,
|
|
68
71
|
exportSVG: () => exportSVG,
|
|
72
|
+
fitToBox: () => fitToBox,
|
|
69
73
|
generateId: () => generateId,
|
|
70
74
|
isCssColor: () => isCssColor,
|
|
71
75
|
isMaskPresetId: () => isMaskPresetId,
|
|
72
76
|
isShapeMaskId: () => isShapeMaskId,
|
|
73
77
|
isTextureMaskId: () => isTextureMaskId,
|
|
78
|
+
needsAbsoluteSpace: () => needsAbsoluteSpace,
|
|
74
79
|
readLayerShadow: () => readLayerShadow,
|
|
75
80
|
renderTextureMask: () => renderTextureMask,
|
|
76
81
|
resetTransform: () => resetTransform,
|
|
@@ -78,12 +83,15 @@ __export(index_exports, {
|
|
|
78
83
|
round2: () => round2,
|
|
79
84
|
sanitizeSvg: () => sanitizeSvg,
|
|
80
85
|
serializeEditor: () => serializeEditor,
|
|
81
|
-
shapeMaskPathData: () => shapeMaskPathData
|
|
86
|
+
shapeMaskPathData: () => shapeMaskPathData,
|
|
87
|
+
toCanvasSpace: () => toCanvasSpace,
|
|
88
|
+
toHostSpace: () => toHostSpace,
|
|
89
|
+
unwrapGroup: () => unwrapGroup
|
|
82
90
|
});
|
|
83
91
|
module.exports = __toCommonJS(index_exports);
|
|
84
92
|
|
|
85
93
|
// src/editor.ts
|
|
86
|
-
var
|
|
94
|
+
var import_fabric17 = require("fabric");
|
|
87
95
|
|
|
88
96
|
// src/events.ts
|
|
89
97
|
var EventEmitter = class {
|
|
@@ -245,7 +253,13 @@ var LayerManager = class {
|
|
|
245
253
|
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
246
254
|
layer.renderProxy = proxy;
|
|
247
255
|
if (proxy) {
|
|
248
|
-
proxy.
|
|
256
|
+
proxy._layerId = id;
|
|
257
|
+
proxy.set({
|
|
258
|
+
visible: layer.visible,
|
|
259
|
+
opacity: layer.opacity,
|
|
260
|
+
selectable: !layer.locked,
|
|
261
|
+
evented: !layer.locked
|
|
262
|
+
});
|
|
249
263
|
this.canvas.add(proxy);
|
|
250
264
|
this.syncZOrder();
|
|
251
265
|
}
|
|
@@ -284,7 +298,7 @@ var LayerManager = class {
|
|
|
284
298
|
} else {
|
|
285
299
|
const layer = this.get(id);
|
|
286
300
|
if (layer) {
|
|
287
|
-
this.canvas.setActiveObject(layer.fabricObject);
|
|
301
|
+
this.canvas.setActiveObject(layer.renderProxy ?? layer.fabricObject);
|
|
288
302
|
}
|
|
289
303
|
}
|
|
290
304
|
this.canvas.requestRenderAll();
|
|
@@ -319,8 +333,9 @@ var LayerManager = class {
|
|
|
319
333
|
if (!layer) return;
|
|
320
334
|
if (layer.locked === locked) return;
|
|
321
335
|
layer.locked = locked;
|
|
322
|
-
layer.
|
|
323
|
-
|
|
336
|
+
const target = layer.renderProxy ?? layer.fabricObject;
|
|
337
|
+
target.selectable = !locked;
|
|
338
|
+
target.evented = !locked;
|
|
324
339
|
this.canvas.requestRenderAll();
|
|
325
340
|
this.emitChanged();
|
|
326
341
|
this.onPropertyChanged?.();
|
|
@@ -951,8 +966,10 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
|
|
|
951
966
|
}
|
|
952
967
|
return placements;
|
|
953
968
|
}
|
|
954
|
-
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
|
|
969
|
+
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin, draw) {
|
|
955
970
|
const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
|
|
971
|
+
const drawW = draw && draw.width > 0 ? draw.width : baseW;
|
|
972
|
+
const drawH = draw && draw.height > 0 ? draw.height : baseH;
|
|
956
973
|
ctx.save();
|
|
957
974
|
ctx.translate(anchor.x, anchor.y);
|
|
958
975
|
ctx.rotate(config.angle * Math.PI / 180);
|
|
@@ -960,7 +977,7 @@ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
|
|
|
960
977
|
ctx.save();
|
|
961
978
|
ctx.translate(tile.x, tile.y);
|
|
962
979
|
ctx.rotate(tile.rotation);
|
|
963
|
-
ctx.drawImage(img, -
|
|
980
|
+
ctx.drawImage(img, -drawW / 2, -drawH / 2, drawW, drawH);
|
|
964
981
|
ctx.restore();
|
|
965
982
|
}
|
|
966
983
|
ctx.restore();
|
|
@@ -982,46 +999,43 @@ function mod2(n) {
|
|
|
982
999
|
var MAX_SNAPSHOT_PIXELS = 16e6;
|
|
983
1000
|
var MAX_SNAPSHOT_SCALE = 8;
|
|
984
1001
|
var SNAPSHOT_SHRINK_FACTOR = 2;
|
|
1002
|
+
var TILE_BLEED_DEVICE_PX = 2;
|
|
985
1003
|
var TiledPatternObject = class _TiledPatternObject extends import_fabric2.FabricObject {
|
|
986
1004
|
static type = "TiledPattern";
|
|
987
1005
|
/** The layer's real object. Never rendered directly (it is kept at opacity 0). */
|
|
988
1006
|
source;
|
|
989
1007
|
config;
|
|
1008
|
+
/** The print area this pattern fills, in canvas units. */
|
|
1009
|
+
area;
|
|
990
1010
|
snapshotEl = null;
|
|
991
1011
|
snapshotScale = 0;
|
|
992
|
-
constructor(source, config,
|
|
1012
|
+
constructor(source, config, area) {
|
|
993
1013
|
super({
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
originX: "left",
|
|
998
|
-
originY: "top",
|
|
999
|
-
left: 0,
|
|
1000
|
-
top: 0,
|
|
1001
|
-
width,
|
|
1002
|
-
height,
|
|
1003
|
-
// Pointer events belong to the source underneath, and fabric's object
|
|
1004
|
-
// cache would freeze the tiling at screen resolution.
|
|
1005
|
-
selectable: false,
|
|
1006
|
-
evented: false,
|
|
1014
|
+
// Centre origin: the box tracks a tile, and a tile is placed by its centre.
|
|
1015
|
+
originX: "center",
|
|
1016
|
+
originY: "center",
|
|
1007
1017
|
objectCaching: false,
|
|
1008
|
-
|
|
1009
|
-
|
|
1018
|
+
// Side handles would imply a non-uniform tile scale; the config has one.
|
|
1019
|
+
lockScalingFlip: true
|
|
1010
1020
|
});
|
|
1011
1021
|
this.source = source;
|
|
1012
1022
|
this.config = config;
|
|
1023
|
+
this.area = area;
|
|
1024
|
+
this.setControlsVisibility({ ml: false, mr: false, mt: false, mb: false });
|
|
1025
|
+
this.syncBox();
|
|
1013
1026
|
}
|
|
1014
|
-
/** Swap in a new config
|
|
1027
|
+
/** Swap in a new config and re-fit the box to the tile it now describes. */
|
|
1015
1028
|
setConfig(config) {
|
|
1016
1029
|
this.config = config;
|
|
1017
|
-
this.
|
|
1030
|
+
this.syncBox();
|
|
1018
1031
|
}
|
|
1019
|
-
/**
|
|
1020
|
-
setArea(
|
|
1021
|
-
this.
|
|
1022
|
-
this.
|
|
1032
|
+
/** Re-fit to a new print area (canvas resize). */
|
|
1033
|
+
setArea(area) {
|
|
1034
|
+
this.area = area;
|
|
1035
|
+
this.invalidate();
|
|
1036
|
+
this.syncBox();
|
|
1023
1037
|
}
|
|
1024
|
-
/** Drop the cached source snapshot (
|
|
1038
|
+
/** Drop the cached source snapshot (the source was edited). */
|
|
1025
1039
|
invalidate() {
|
|
1026
1040
|
this.snapshotEl = null;
|
|
1027
1041
|
this.snapshotScale = 0;
|
|
@@ -1032,40 +1046,118 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1032
1046
|
this.snapshotEl = null;
|
|
1033
1047
|
this.snapshotScale = 0;
|
|
1034
1048
|
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Put the box back on the anchor tile: the tile's size, the pattern angle, and
|
|
1051
|
+
* the grid origin displaced by the configured phase shift. Resets any live
|
|
1052
|
+
* gesture scale, so it must run only once that gesture has been folded in.
|
|
1053
|
+
*/
|
|
1054
|
+
syncBox() {
|
|
1055
|
+
const { tileW, tileH } = this.baseTile();
|
|
1056
|
+
const origin = this.source.getCenterPoint();
|
|
1057
|
+
const anchor = this.anchorFromOrigin(origin, tileW, tileH, this.config.angle);
|
|
1058
|
+
this.set({
|
|
1059
|
+
left: anchor.x,
|
|
1060
|
+
top: anchor.y,
|
|
1061
|
+
width: tileW,
|
|
1062
|
+
height: tileH,
|
|
1063
|
+
scaleX: 1,
|
|
1064
|
+
scaleY: 1,
|
|
1065
|
+
angle: this.config.angle
|
|
1066
|
+
});
|
|
1067
|
+
this.setCoords();
|
|
1068
|
+
this.dirty = true;
|
|
1069
|
+
}
|
|
1070
|
+
/** Grid rotation in play right now — the live handle during a rotate gesture. */
|
|
1071
|
+
liveAngle() {
|
|
1072
|
+
return this.angle ?? 0;
|
|
1073
|
+
}
|
|
1074
|
+
/** Tile scale in play right now, as a config percentage. */
|
|
1075
|
+
liveScale() {
|
|
1076
|
+
return (this.config.scale ?? 100) * Math.abs(this.scaleX ?? 1);
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Where the source's centre would have to sit for the box to stay put — i.e.
|
|
1080
|
+
* the grid origin implied by a drag. `PatternManager` moves the source there.
|
|
1081
|
+
*/
|
|
1082
|
+
liveOrigin() {
|
|
1083
|
+
const { tileW, tileH } = this.liveTile();
|
|
1084
|
+
const centre = this.getCenterPoint();
|
|
1085
|
+
const shift = this.shiftVector(tileW, tileH, this.liveAngle());
|
|
1086
|
+
return new import_fabric2.Point(centre.x - shift.x, centre.y - shift.y);
|
|
1087
|
+
}
|
|
1035
1088
|
_render(ctx) {
|
|
1036
|
-
const width = this.
|
|
1037
|
-
const height = this.height ?? 0;
|
|
1089
|
+
const { width, height } = this.area;
|
|
1038
1090
|
if (width <= 0 || height <= 0) return;
|
|
1039
|
-
const
|
|
1040
|
-
const
|
|
1091
|
+
const { tileW, tileH } = this.liveTile();
|
|
1092
|
+
const scale = contextScale(ctx);
|
|
1093
|
+
const snapshot = this.ensureSnapshot(scale * (tileW / Math.max(1, this.baseW())));
|
|
1041
1094
|
if (!snapshot) return;
|
|
1042
|
-
const
|
|
1095
|
+
const centre = this.getCenterPoint();
|
|
1096
|
+
const angle = this.liveAngle();
|
|
1043
1097
|
ctx.save();
|
|
1044
|
-
ctx.
|
|
1045
|
-
|
|
1098
|
+
ctx.scale(1 / (this.scaleX || 1), 1 / (this.scaleY || 1));
|
|
1099
|
+
ctx.rotate(-angle * Math.PI / 180);
|
|
1100
|
+
ctx.translate(-centre.x, -centre.y);
|
|
1101
|
+
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1102
|
+
const origin = { x: centre.x - shift.x, y: centre.y - shift.y };
|
|
1103
|
+
const config = { ...this.config, angle, offsetX: 0, offsetY: 0 };
|
|
1104
|
+
drawTiles(
|
|
1105
|
+
ctx,
|
|
1106
|
+
snapshot,
|
|
1107
|
+
config,
|
|
1108
|
+
width,
|
|
1109
|
+
height,
|
|
1110
|
+
tileW,
|
|
1111
|
+
tileH,
|
|
1112
|
+
origin,
|
|
1113
|
+
this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX / scale)
|
|
1114
|
+
);
|
|
1046
1115
|
ctx.restore();
|
|
1047
1116
|
}
|
|
1048
1117
|
/** Raster fallback for SVG export — one `<image>` covering the print area. */
|
|
1049
1118
|
_toSVG() {
|
|
1050
|
-
const width = this.
|
|
1051
|
-
const height = this.height ?? 0;
|
|
1119
|
+
const { width, height } = this.area;
|
|
1052
1120
|
if (width <= 0 || height <= 0) return [];
|
|
1053
1121
|
const el = document.createElement("canvas");
|
|
1054
1122
|
el.width = Math.max(1, Math.round(width));
|
|
1055
1123
|
el.height = Math.max(1, Math.round(height));
|
|
1056
1124
|
const ctx = el.getContext("2d");
|
|
1057
1125
|
if (!ctx) return [];
|
|
1058
|
-
const
|
|
1059
|
-
const snapshot = this.ensureSnapshot(
|
|
1126
|
+
const { tileW, tileH } = this.liveTile();
|
|
1127
|
+
const snapshot = this.ensureSnapshot(tileW / Math.max(1, this.baseW()));
|
|
1060
1128
|
if (!snapshot) return [];
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1129
|
+
const centre = this.getCenterPoint();
|
|
1130
|
+
const angle = this.liveAngle();
|
|
1131
|
+
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1132
|
+
drawTiles(
|
|
1133
|
+
ctx,
|
|
1134
|
+
snapshot,
|
|
1135
|
+
{ ...this.config, angle, offsetX: 0, offsetY: 0 },
|
|
1136
|
+
width,
|
|
1137
|
+
height,
|
|
1138
|
+
tileW,
|
|
1139
|
+
tileH,
|
|
1140
|
+
{ x: centre.x - shift.x, y: centre.y - shift.y },
|
|
1141
|
+
// The fallback raster is built at 1:1, so a device pixel is a canvas unit.
|
|
1142
|
+
this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX)
|
|
1143
|
+
);
|
|
1063
1144
|
return [
|
|
1064
|
-
`<
|
|
1065
|
-
|
|
1145
|
+
`<g transform="rotate(${-angle}) translate(${-centre.x} ${-centre.y})">`,
|
|
1146
|
+
`<image x="0" y="0" width="${width}" height="${height}" `,
|
|
1147
|
+
`xlink:href="${el.toDataURL("image/png")}"></image></g>
|
|
1066
1148
|
`
|
|
1067
1149
|
];
|
|
1068
1150
|
}
|
|
1151
|
+
/**
|
|
1152
|
+
* The proxy holds a live reference to its source, which would make a
|
|
1153
|
+
* serialized canvas circular. Nothing persists this object (only layers are
|
|
1154
|
+
* serialized) — this keeps an accidental `canvas.toObject()` from throwing.
|
|
1155
|
+
*/
|
|
1156
|
+
toObject() {
|
|
1157
|
+
const plain = super.toObject();
|
|
1158
|
+
delete plain.source;
|
|
1159
|
+
return plain;
|
|
1160
|
+
}
|
|
1069
1161
|
/**
|
|
1070
1162
|
* Fabric's generic clone round-trips through `toObject()` + the class
|
|
1071
1163
|
* registry, which cannot carry a live source reference. Export paths clone
|
|
@@ -1073,46 +1165,110 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1073
1165
|
* tiling. The copy shares the source (it only ever reads from it).
|
|
1074
1166
|
*/
|
|
1075
1167
|
clone() {
|
|
1076
|
-
const copy = new _TiledPatternObject(
|
|
1077
|
-
this.source,
|
|
1078
|
-
this.config,
|
|
1079
|
-
this.width ?? 0,
|
|
1080
|
-
this.height ?? 0
|
|
1081
|
-
);
|
|
1168
|
+
const copy = new _TiledPatternObject(this.source, this.config, this.area);
|
|
1082
1169
|
copy.set({
|
|
1083
1170
|
left: this.left,
|
|
1084
1171
|
top: this.top,
|
|
1172
|
+
angle: this.angle,
|
|
1173
|
+
scaleX: this.scaleX,
|
|
1174
|
+
scaleY: this.scaleY,
|
|
1175
|
+
width: this.width,
|
|
1176
|
+
height: this.height,
|
|
1085
1177
|
visible: this.visible,
|
|
1086
1178
|
opacity: this.opacity
|
|
1087
1179
|
});
|
|
1088
1180
|
return Promise.resolve(copy);
|
|
1089
1181
|
}
|
|
1090
1182
|
/**
|
|
1091
|
-
* The
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1183
|
+
* The source's tile footprint: its bounding box, minus a stroke it reserves
|
|
1184
|
+
* room for but never paints.
|
|
1185
|
+
*
|
|
1186
|
+
* fabric keeps `strokeWidth` inside an object's box whether or not a `stroke`
|
|
1187
|
+
* colour paints it, and the built-in shapes arrive with `strokeWidth: 1` and no
|
|
1188
|
+
* stroke. Stepping the grid by that box puts a transparent seam between every
|
|
1189
|
+
* pair of neighbours at zero spacing: the artwork is a pixel narrower than the
|
|
1190
|
+
* box it is stepped by.
|
|
1094
1191
|
*/
|
|
1095
|
-
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1192
|
+
sourceBox() {
|
|
1193
|
+
const rect = this.source.getBoundingRect();
|
|
1194
|
+
if (paintsStroke(this.source)) return { width: rect.width, height: rect.height };
|
|
1195
|
+
const dims = this.source._getTransformedDimensions({ strokeWidth: 0 });
|
|
1196
|
+
const radians = (this.source.angle ?? 0) * Math.PI / 180;
|
|
1197
|
+
const cos = Math.abs(Math.cos(radians));
|
|
1198
|
+
const sin = Math.abs(Math.sin(radians));
|
|
1199
|
+
return {
|
|
1200
|
+
width: dims.x * cos + dims.y * sin,
|
|
1201
|
+
height: dims.x * sin + dims.y * cos
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
/** The source's on-canvas width, before the tile scale. */
|
|
1205
|
+
baseW() {
|
|
1206
|
+
return Math.max(1, this.sourceBox().width);
|
|
1207
|
+
}
|
|
1208
|
+
/** The source's on-canvas height, before the tile scale. */
|
|
1209
|
+
baseH() {
|
|
1210
|
+
return Math.max(1, this.sourceBox().height);
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Size to paint the snapshot at, for a tile of `tileW × tileH`.
|
|
1214
|
+
*
|
|
1215
|
+
* The snapshot is NOT exactly the source's box: `toCanvasElement` rounds the
|
|
1216
|
+
* raster up to whole pixels and pads it further for a shadow. Painting it into
|
|
1217
|
+
* the tile step would squeeze the artwork inside that padding — every tile
|
|
1218
|
+
* shrinks by up to a pixel and the grid shows transparent seams at zero
|
|
1219
|
+
* spacing. So the bitmap is painted at its own footprint, scaled by the same
|
|
1220
|
+
* factor the tile is, and the step stays the source's box.
|
|
1221
|
+
*/
|
|
1222
|
+
drawSize(tileW, tileH, bleed = 0) {
|
|
1223
|
+
const snapshot = this.snapshotEl;
|
|
1224
|
+
if (!snapshot || this.snapshotScale <= 0) {
|
|
1225
|
+
return { width: tileW + bleed, height: tileH + bleed };
|
|
1226
|
+
}
|
|
1227
|
+
return {
|
|
1228
|
+
width: snapshot.width / this.snapshotScale * (tileW / this.baseW()) + bleed,
|
|
1229
|
+
height: snapshot.height / this.snapshotScale * (tileH / this.baseH()) + bleed
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
/** Tile size from the config alone, ignoring any in-flight gesture. */
|
|
1233
|
+
baseTile() {
|
|
1234
|
+
const box = this.sourceBox();
|
|
1235
|
+
const scale = Math.max(1, this.config.scale ?? 100) / 100;
|
|
1236
|
+
const floor = this.tileFloor();
|
|
1237
|
+
return {
|
|
1238
|
+
tileW: Math.max(floor, box.width * scale),
|
|
1239
|
+
tileH: Math.max(floor, box.height * scale)
|
|
1240
|
+
};
|
|
1099
1241
|
}
|
|
1100
|
-
/** Tile size
|
|
1101
|
-
|
|
1102
|
-
const
|
|
1242
|
+
/** Tile size as drawn right now, including a live scale gesture. */
|
|
1243
|
+
liveTile() {
|
|
1244
|
+
const base = this.baseTile();
|
|
1245
|
+
const floor = this.tileFloor();
|
|
1103
1246
|
return {
|
|
1104
|
-
tileW: Math.max(floor,
|
|
1105
|
-
tileH: Math.max(floor,
|
|
1247
|
+
tileW: Math.max(floor, base.tileW * Math.abs(this.scaleX ?? 1)),
|
|
1248
|
+
tileH: Math.max(floor, base.tileH * Math.abs(this.scaleY ?? 1))
|
|
1106
1249
|
};
|
|
1107
1250
|
}
|
|
1108
|
-
/**
|
|
1109
|
-
|
|
1110
|
-
const
|
|
1111
|
-
return
|
|
1251
|
+
/** Smallest tile the draw loop may use, so a tiny tile can't flood the area. */
|
|
1252
|
+
tileFloor() {
|
|
1253
|
+
const { width, height } = this.area;
|
|
1254
|
+
return Math.max(2, Math.sqrt(width * width + height * height) / 200);
|
|
1255
|
+
}
|
|
1256
|
+
/** Phase shift (`offsetX`/`offsetY`) as a canvas-space vector. */
|
|
1257
|
+
shiftVector(tileW, tileH, angle) {
|
|
1258
|
+
const dx = tileW * (clampPercent(this.config.offsetX) / 100);
|
|
1259
|
+
const dy = tileH * (clampPercent(this.config.offsetY) / 100);
|
|
1260
|
+
const radians = angle * Math.PI / 180;
|
|
1261
|
+
const cos = Math.cos(radians);
|
|
1262
|
+
const sin = Math.sin(radians);
|
|
1263
|
+
return new import_fabric2.Point(dx * cos - dy * sin, dx * sin + dy * cos);
|
|
1264
|
+
}
|
|
1265
|
+
anchorFromOrigin(origin, tileW, tileH, angle) {
|
|
1266
|
+
const shift = this.shiftVector(tileW, tileH, angle);
|
|
1267
|
+
return new import_fabric2.Point(origin.x + shift.x, origin.y + shift.y);
|
|
1112
1268
|
}
|
|
1113
1269
|
/**
|
|
1114
|
-
* Snapshot the source at (at least) `scale`, reusing the cached one
|
|
1115
|
-
* still sharp enough and the source has not changed.
|
|
1270
|
+
* Snapshot the source at (at least) `scale`, reusing the cached one while it
|
|
1271
|
+
* is still sharp enough and the source has not changed.
|
|
1116
1272
|
*/
|
|
1117
1273
|
ensureSnapshot(scale) {
|
|
1118
1274
|
const wanted = this.clampScale(scale);
|
|
@@ -1143,6 +1299,16 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
|
|
|
1143
1299
|
return Math.max(0.05, Math.min(requested, budgeted));
|
|
1144
1300
|
}
|
|
1145
1301
|
};
|
|
1302
|
+
function paintsStroke(object) {
|
|
1303
|
+
if (!object.strokeWidth || object.strokeWidth <= 0) return false;
|
|
1304
|
+
const { stroke } = object;
|
|
1305
|
+
if (typeof stroke === "string") return stroke !== "" && stroke !== "transparent";
|
|
1306
|
+
return stroke !== null && stroke !== void 0;
|
|
1307
|
+
}
|
|
1308
|
+
function clampPercent(value) {
|
|
1309
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
1310
|
+
return Math.max(-100, Math.min(100, value));
|
|
1311
|
+
}
|
|
1146
1312
|
function contextScale(ctx) {
|
|
1147
1313
|
if (typeof ctx.getTransform !== "function") return 1;
|
|
1148
1314
|
try {
|
|
@@ -1154,13 +1320,15 @@ function contextScale(ctx) {
|
|
|
1154
1320
|
}
|
|
1155
1321
|
|
|
1156
1322
|
// src/pattern/pattern-manager.ts
|
|
1323
|
+
var MIN_TILE_SCALE = 10;
|
|
1324
|
+
var MAX_TILE_SCALE = 300;
|
|
1157
1325
|
var PatternManager = class {
|
|
1158
1326
|
constructor(canvas, layers, history, events) {
|
|
1159
1327
|
this.canvas = canvas;
|
|
1160
1328
|
this.layers = layers;
|
|
1161
1329
|
this.history = history;
|
|
1162
1330
|
this.events = events;
|
|
1163
|
-
this.canvas.on("object:modified", this.
|
|
1331
|
+
this.canvas.on("object:modified", this.onObjectModified);
|
|
1164
1332
|
this.canvas.on("text:changed", this.onSourceModified);
|
|
1165
1333
|
this.events.on("layer:removed", this.onLayerRemoved);
|
|
1166
1334
|
}
|
|
@@ -1169,6 +1337,11 @@ var PatternManager = class {
|
|
|
1169
1337
|
history;
|
|
1170
1338
|
events;
|
|
1171
1339
|
proxies = /* @__PURE__ */ new Map();
|
|
1340
|
+
onObjectModified = (event) => {
|
|
1341
|
+
const target = event.target;
|
|
1342
|
+
if (target instanceof TiledPatternObject) this.commitGesture(target);
|
|
1343
|
+
else this.invalidateFor(target);
|
|
1344
|
+
};
|
|
1172
1345
|
onSourceModified = (event) => {
|
|
1173
1346
|
this.invalidateFor(event.target);
|
|
1174
1347
|
};
|
|
@@ -1210,7 +1383,11 @@ var PatternManager = class {
|
|
|
1210
1383
|
try {
|
|
1211
1384
|
this.detach(layerId);
|
|
1212
1385
|
restoreLocks(layer.fabricObject, layer.meta.pattern.originalLocks);
|
|
1213
|
-
layer.fabricObject.set({
|
|
1386
|
+
layer.fabricObject.set({
|
|
1387
|
+
opacity: layer.opacity,
|
|
1388
|
+
selectable: !layer.locked,
|
|
1389
|
+
evented: !layer.locked
|
|
1390
|
+
});
|
|
1214
1391
|
this.layers.setMeta(layerId, { pattern: void 0 });
|
|
1215
1392
|
this.canvas.requestRenderAll();
|
|
1216
1393
|
this.history.save();
|
|
@@ -1242,12 +1419,8 @@ var PatternManager = class {
|
|
|
1242
1419
|
}
|
|
1243
1420
|
/** Re-fit every proxy to the print area (canvas resize). */
|
|
1244
1421
|
syncArea() {
|
|
1245
|
-
const
|
|
1246
|
-
const
|
|
1247
|
-
for (const proxy of this.proxies.values()) {
|
|
1248
|
-
proxy.setArea(width, height);
|
|
1249
|
-
proxy.invalidate();
|
|
1250
|
-
}
|
|
1422
|
+
const area = this.area();
|
|
1423
|
+
for (const proxy of this.proxies.values()) proxy.setArea(area);
|
|
1251
1424
|
if (this.proxies.size > 0) this.canvas.requestRenderAll();
|
|
1252
1425
|
}
|
|
1253
1426
|
/** Drop a layer's cached source snapshot (its content changed). */
|
|
@@ -1255,6 +1428,7 @@ var PatternManager = class {
|
|
|
1255
1428
|
const proxy = this.proxies.get(layerId);
|
|
1256
1429
|
if (!proxy) return;
|
|
1257
1430
|
proxy.invalidate();
|
|
1431
|
+
proxy.syncBox();
|
|
1258
1432
|
this.canvas.requestRenderAll();
|
|
1259
1433
|
}
|
|
1260
1434
|
/** Give a freshly cloned layer its own proxy (duplicating a pattern layer). */
|
|
@@ -1264,7 +1438,7 @@ var PatternManager = class {
|
|
|
1264
1438
|
this.attach(layer, state.config);
|
|
1265
1439
|
}
|
|
1266
1440
|
dispose() {
|
|
1267
|
-
this.canvas.off("object:modified", this.
|
|
1441
|
+
this.canvas.off("object:modified", this.onObjectModified);
|
|
1268
1442
|
this.canvas.off("text:changed", this.onSourceModified);
|
|
1269
1443
|
this.events.off("layer:removed", this.onLayerRemoved);
|
|
1270
1444
|
this.clearProxies();
|
|
@@ -1275,17 +1449,38 @@ var PatternManager = class {
|
|
|
1275
1449
|
this.proxies.clear();
|
|
1276
1450
|
}
|
|
1277
1451
|
attach(layer, config) {
|
|
1278
|
-
const proxy = new TiledPatternObject(
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
this.canvas.getWidth(),
|
|
1282
|
-
this.canvas.getHeight()
|
|
1283
|
-
);
|
|
1284
|
-
layer.fabricObject.set({ opacity: 0 });
|
|
1452
|
+
const proxy = new TiledPatternObject(layer.fabricObject, config, this.area());
|
|
1453
|
+
const wasActive = this.canvas.getActiveObject() === layer.fabricObject;
|
|
1454
|
+
layer.fabricObject.set({ opacity: 0, selectable: false, evented: false });
|
|
1285
1455
|
this.proxies.set(layer.id, proxy);
|
|
1286
1456
|
this.layers.setRenderProxy(layer.id, proxy);
|
|
1457
|
+
if (wasActive) this.canvas.setActiveObject(proxy);
|
|
1287
1458
|
this.canvas.requestRenderAll();
|
|
1288
1459
|
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Fold a finished drag / scale / rotate on the proxy back into the pattern:
|
|
1462
|
+
* position becomes the grid origin (carried by the source), scale becomes the
|
|
1463
|
+
* tile scale, rotation becomes the pattern angle. Read the live transform
|
|
1464
|
+
* first — writing the config re-fits the box and destroys it.
|
|
1465
|
+
*/
|
|
1466
|
+
commitGesture(proxy) {
|
|
1467
|
+
const layer = this.layers.findByObject(proxy);
|
|
1468
|
+
const state = layer?.meta.pattern;
|
|
1469
|
+
if (!layer || !state) return;
|
|
1470
|
+
const origin = proxy.liveOrigin();
|
|
1471
|
+
const scale = clamp2(proxy.liveScale(), MIN_TILE_SCALE, MAX_TILE_SCALE);
|
|
1472
|
+
const angle = normalizeAngle(proxy.liveAngle());
|
|
1473
|
+
layer.fabricObject.setPositionByOrigin(origin, "center", "center");
|
|
1474
|
+
layer.fabricObject.setCoords();
|
|
1475
|
+
const config = { ...state.config, scale, angle };
|
|
1476
|
+
this.layers.setMeta(layer.id, { pattern: { ...state, config } });
|
|
1477
|
+
proxy.setConfig(config);
|
|
1478
|
+
this.canvas.requestRenderAll();
|
|
1479
|
+
this.history.save();
|
|
1480
|
+
}
|
|
1481
|
+
area() {
|
|
1482
|
+
return { width: this.canvas.getWidth(), height: this.canvas.getHeight() };
|
|
1483
|
+
}
|
|
1289
1484
|
detach(layerId) {
|
|
1290
1485
|
const proxy = this.proxies.get(layerId);
|
|
1291
1486
|
if (!proxy) {
|
|
@@ -1302,6 +1497,15 @@ var PatternManager = class {
|
|
|
1302
1497
|
if (layer) this.invalidate(layer.id);
|
|
1303
1498
|
}
|
|
1304
1499
|
};
|
|
1500
|
+
function clamp2(value, min, max) {
|
|
1501
|
+
if (!Number.isFinite(value)) return min;
|
|
1502
|
+
return Math.max(min, Math.min(max, value));
|
|
1503
|
+
}
|
|
1504
|
+
function normalizeAngle(angle) {
|
|
1505
|
+
if (!Number.isFinite(angle)) return 0;
|
|
1506
|
+
const wrapped = (angle % 360 + 360) % 360;
|
|
1507
|
+
return Math.round(wrapped > 180 ? wrapped - 360 : wrapped);
|
|
1508
|
+
}
|
|
1305
1509
|
function isLegacyState(state) {
|
|
1306
1510
|
return typeof state.originalSrc === "string" && state.originalSrc.length > 0;
|
|
1307
1511
|
}
|
|
@@ -2852,6 +3056,737 @@ var MaskController = class {
|
|
|
2852
3056
|
}
|
|
2853
3057
|
};
|
|
2854
3058
|
|
|
3059
|
+
// src/masks/manager.ts
|
|
3060
|
+
var import_fabric16 = require("fabric");
|
|
3061
|
+
|
|
3062
|
+
// src/masks/compose.ts
|
|
3063
|
+
var import_fabric10 = require("fabric");
|
|
3064
|
+
var MODE_OPERATION = {
|
|
3065
|
+
add: "source-over",
|
|
3066
|
+
subtract: "destination-out",
|
|
3067
|
+
intersect: "destination-in"
|
|
3068
|
+
};
|
|
3069
|
+
function neutralize(child) {
|
|
3070
|
+
child.set({ opacity: 0, globalCompositeOperation: "source-over" });
|
|
3071
|
+
}
|
|
3072
|
+
function baseRect(box) {
|
|
3073
|
+
return new import_fabric10.Rect({
|
|
3074
|
+
left: box.left,
|
|
3075
|
+
top: box.top,
|
|
3076
|
+
width: Math.max(1, box.width),
|
|
3077
|
+
height: Math.max(1, box.height),
|
|
3078
|
+
originX: "left",
|
|
3079
|
+
originY: "top",
|
|
3080
|
+
fill: "#000000",
|
|
3081
|
+
objectCaching: false
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
function composeMaskGroup(children, entries, options) {
|
|
3085
|
+
if (children.length === 0) return void 0;
|
|
3086
|
+
children.forEach((child, index) => {
|
|
3087
|
+
const entry = entries[index];
|
|
3088
|
+
child.set({ objectCaching: false });
|
|
3089
|
+
if (!entry || !entry.visible) {
|
|
3090
|
+
neutralize(child);
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
child.set({
|
|
3094
|
+
opacity: entry.opacity,
|
|
3095
|
+
globalCompositeOperation: MODE_OPERATION[entry.mode] ?? "source-over"
|
|
3096
|
+
});
|
|
3097
|
+
});
|
|
3098
|
+
const first = entries.find((entry) => entry.visible);
|
|
3099
|
+
const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
|
|
3100
|
+
return new import_fabric10.Group(withBase, {
|
|
3101
|
+
absolutePositioned: options.absolute,
|
|
3102
|
+
// Cached, so the children's compositing operations resolve against each
|
|
3103
|
+
// other instead of against the page underneath the mask.
|
|
3104
|
+
objectCaching: true,
|
|
3105
|
+
subTargetCheck: false,
|
|
3106
|
+
interactive: false
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
function needsAbsoluteSpace(entries) {
|
|
3110
|
+
return entries.some((entry) => !entry.linked);
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
// src/masks/edit.ts
|
|
3114
|
+
var import_fabric12 = require("fabric");
|
|
3115
|
+
|
|
3116
|
+
// src/masks/space.ts
|
|
3117
|
+
var import_fabric11 = require("fabric");
|
|
3118
|
+
function matrixOf(object) {
|
|
3119
|
+
return object.calcTransformMatrix();
|
|
3120
|
+
}
|
|
3121
|
+
function applyMatrix(object, matrix) {
|
|
3122
|
+
const decomposed = import_fabric11.util.qrDecompose(matrix);
|
|
3123
|
+
object.set({
|
|
3124
|
+
flipX: false,
|
|
3125
|
+
flipY: false,
|
|
3126
|
+
originX: "center",
|
|
3127
|
+
originY: "center",
|
|
3128
|
+
left: decomposed.translateX,
|
|
3129
|
+
top: decomposed.translateY,
|
|
3130
|
+
scaleX: decomposed.scaleX,
|
|
3131
|
+
scaleY: decomposed.scaleY,
|
|
3132
|
+
angle: decomposed.angle,
|
|
3133
|
+
skewX: decomposed.skewX,
|
|
3134
|
+
skewY: 0
|
|
3135
|
+
});
|
|
3136
|
+
object.setCoords();
|
|
3137
|
+
}
|
|
3138
|
+
function toCanvasSpace(object, host) {
|
|
3139
|
+
applyMatrix(object, import_fabric11.util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));
|
|
3140
|
+
}
|
|
3141
|
+
function toHostSpace(object, host) {
|
|
3142
|
+
applyMatrix(
|
|
3143
|
+
object,
|
|
3144
|
+
import_fabric11.util.multiplyTransformMatrices(import_fabric11.util.invertTransform(matrixOf(host)), matrixOf(object))
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
function relativeMatrix(object, host) {
|
|
3148
|
+
return import_fabric11.util.multiplyTransformMatrices(import_fabric11.util.invertTransform(matrixOf(host)), matrixOf(object));
|
|
3149
|
+
}
|
|
3150
|
+
function applyRelativeMatrix(object, host, rel) {
|
|
3151
|
+
applyMatrix(object, import_fabric11.util.multiplyTransformMatrices(matrixOf(host), rel));
|
|
3152
|
+
}
|
|
3153
|
+
function asObject(clip) {
|
|
3154
|
+
return clip;
|
|
3155
|
+
}
|
|
3156
|
+
function toMatrix(values) {
|
|
3157
|
+
if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))
|
|
3158
|
+
return null;
|
|
3159
|
+
return [values[0], values[1], values[2], values[3], values[4], values[5]];
|
|
3160
|
+
}
|
|
3161
|
+
function fitToBox(object, box, zoom = 1) {
|
|
3162
|
+
const width = Math.max(1, box.width) * zoom;
|
|
3163
|
+
const height = Math.max(1, box.height) * zoom;
|
|
3164
|
+
object.set({
|
|
3165
|
+
originX: "center",
|
|
3166
|
+
originY: "center",
|
|
3167
|
+
angle: 0,
|
|
3168
|
+
skewX: 0,
|
|
3169
|
+
skewY: 0,
|
|
3170
|
+
left: box.left + box.width / 2,
|
|
3171
|
+
top: box.top + box.height / 2,
|
|
3172
|
+
scaleX: width / Math.max(1, object.width ?? 1),
|
|
3173
|
+
scaleY: height / Math.max(1, object.height ?? 1)
|
|
3174
|
+
});
|
|
3175
|
+
object.setCoords();
|
|
3176
|
+
}
|
|
3177
|
+
function unwrapGroup(group) {
|
|
3178
|
+
const children = group.removeAll();
|
|
3179
|
+
for (const child of children) child.setCoords();
|
|
3180
|
+
return children;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// src/masks/edit.ts
|
|
3184
|
+
var MaskEditController = class {
|
|
3185
|
+
constructor(canvas, onCommit) {
|
|
3186
|
+
this.canvas = canvas;
|
|
3187
|
+
this.onCommit = onCommit;
|
|
3188
|
+
}
|
|
3189
|
+
canvas;
|
|
3190
|
+
onCommit;
|
|
3191
|
+
handle = null;
|
|
3192
|
+
editing = null;
|
|
3193
|
+
child = null;
|
|
3194
|
+
group = null;
|
|
3195
|
+
active() {
|
|
3196
|
+
return this.editing;
|
|
3197
|
+
}
|
|
3198
|
+
/** Put a handle on the canvas for `child`, an object inside `group`. */
|
|
3199
|
+
async begin(edit, group, child) {
|
|
3200
|
+
this.end();
|
|
3201
|
+
const handle = await child.clone();
|
|
3202
|
+
applyMatrix(handle, matrixOf(child));
|
|
3203
|
+
handle.set({
|
|
3204
|
+
// Outline only: the mask's own fill would sit over the artwork it is
|
|
3205
|
+
// supposed to be revealing.
|
|
3206
|
+
fill: "rgba(0,0,0,0.001)",
|
|
3207
|
+
stroke: "#e0b055",
|
|
3208
|
+
strokeWidth: 2,
|
|
3209
|
+
strokeDashArray: [6, 4],
|
|
3210
|
+
strokeUniform: true,
|
|
3211
|
+
opacity: 1,
|
|
3212
|
+
selectable: true,
|
|
3213
|
+
evented: true,
|
|
3214
|
+
hasControls: true,
|
|
3215
|
+
hasBorders: true,
|
|
3216
|
+
objectCaching: false,
|
|
3217
|
+
excludeFromExport: true
|
|
3218
|
+
});
|
|
3219
|
+
this.handle = handle;
|
|
3220
|
+
this.editing = edit;
|
|
3221
|
+
this.child = child;
|
|
3222
|
+
this.group = group;
|
|
3223
|
+
this.canvas.add(handle);
|
|
3224
|
+
this.canvas.setActiveObject(handle);
|
|
3225
|
+
this.canvas.on("object:moving", this.onTransform);
|
|
3226
|
+
this.canvas.on("object:scaling", this.onTransform);
|
|
3227
|
+
this.canvas.on("object:rotating", this.onTransform);
|
|
3228
|
+
this.canvas.on("object:modified", this.onModified);
|
|
3229
|
+
this.canvas.requestRenderAll();
|
|
3230
|
+
return true;
|
|
3231
|
+
}
|
|
3232
|
+
/** Take the handle down. The geometry it wrote is already in the clip. */
|
|
3233
|
+
end() {
|
|
3234
|
+
if (!this.handle) return;
|
|
3235
|
+
this.canvas.off("object:moving", this.onTransform);
|
|
3236
|
+
this.canvas.off("object:scaling", this.onTransform);
|
|
3237
|
+
this.canvas.off("object:rotating", this.onTransform);
|
|
3238
|
+
this.canvas.off("object:modified", this.onModified);
|
|
3239
|
+
if (this.canvas.getActiveObject() === this.handle) this.canvas.discardActiveObject();
|
|
3240
|
+
this.canvas.remove(this.handle);
|
|
3241
|
+
this.handle = null;
|
|
3242
|
+
this.editing = null;
|
|
3243
|
+
this.child = null;
|
|
3244
|
+
this.group = null;
|
|
3245
|
+
this.canvas.requestRenderAll();
|
|
3246
|
+
}
|
|
3247
|
+
dispose() {
|
|
3248
|
+
this.end();
|
|
3249
|
+
}
|
|
3250
|
+
onTransform = (event) => {
|
|
3251
|
+
if (!this.handle || event.target !== this.handle) return;
|
|
3252
|
+
this.write();
|
|
3253
|
+
};
|
|
3254
|
+
onModified = (event) => {
|
|
3255
|
+
if (!this.handle || event.target !== this.handle) return;
|
|
3256
|
+
this.write();
|
|
3257
|
+
this.onCommit();
|
|
3258
|
+
};
|
|
3259
|
+
/** Handle transform (canvas space) → clip child transform (group-relative). */
|
|
3260
|
+
write() {
|
|
3261
|
+
if (!this.handle || !this.child || !this.group) return;
|
|
3262
|
+
applyMatrix(
|
|
3263
|
+
this.child,
|
|
3264
|
+
import_fabric12.util.multiplyTransformMatrices(
|
|
3265
|
+
import_fabric12.util.invertTransform(matrixOf(this.group)),
|
|
3266
|
+
matrixOf(this.handle)
|
|
3267
|
+
)
|
|
3268
|
+
);
|
|
3269
|
+
this.group.dirty = true;
|
|
3270
|
+
this.group.set({ dirty: true });
|
|
3271
|
+
this.canvas.requestRenderAll();
|
|
3272
|
+
}
|
|
3273
|
+
};
|
|
3274
|
+
|
|
3275
|
+
// src/masks/store.ts
|
|
3276
|
+
var import_fabric15 = require("fabric");
|
|
3277
|
+
|
|
3278
|
+
// src/masks/host.ts
|
|
3279
|
+
var import_fabric13 = require("fabric");
|
|
3280
|
+
function findCanvasHost(layers) {
|
|
3281
|
+
return layers.getAll().find((layer) => layer.meta.canvasMask);
|
|
3282
|
+
}
|
|
3283
|
+
function createCanvasHost(canvas, layers) {
|
|
3284
|
+
const rect = new import_fabric13.Rect({
|
|
3285
|
+
left: 0,
|
|
3286
|
+
top: 0,
|
|
3287
|
+
width: canvas.getWidth(),
|
|
3288
|
+
height: canvas.getHeight(),
|
|
3289
|
+
originX: "left",
|
|
3290
|
+
originY: "top",
|
|
3291
|
+
fill: "#000000",
|
|
3292
|
+
globalCompositeOperation: "destination-in",
|
|
3293
|
+
// It is edited from the layer list, never on the canvas: a drag box on
|
|
3294
|
+
// something that cannot be dragged only reads as broken.
|
|
3295
|
+
selectable: false,
|
|
3296
|
+
evented: false,
|
|
3297
|
+
hasControls: false,
|
|
3298
|
+
hasBorders: false,
|
|
3299
|
+
objectCaching: false
|
|
3300
|
+
});
|
|
3301
|
+
const layer = layers.add("mask", rect, "Design mask");
|
|
3302
|
+
layer.meta.canvasMask = true;
|
|
3303
|
+
layers.setLocked(layer.id, true);
|
|
3304
|
+
return layer;
|
|
3305
|
+
}
|
|
3306
|
+
function pinCanvasHost(layers) {
|
|
3307
|
+
const all = layers.getAll();
|
|
3308
|
+
const hostIndex = all.findIndex((layer) => layer.meta.canvasMask);
|
|
3309
|
+
if (hostIndex === -1) return false;
|
|
3310
|
+
let lastContent = -1;
|
|
3311
|
+
all.forEach((layer, index) => {
|
|
3312
|
+
if (layer.type !== "mask") lastContent = index;
|
|
3313
|
+
});
|
|
3314
|
+
if (hostIndex >= lastContent) return false;
|
|
3315
|
+
layers.reorder(all[hostIndex].id, all.length - 1);
|
|
3316
|
+
return true;
|
|
3317
|
+
}
|
|
3318
|
+
function hostBoxOf(canvas, host, absolute) {
|
|
3319
|
+
if (!host) {
|
|
3320
|
+
return { left: 0, top: 0, width: canvas.getWidth(), height: canvas.getHeight() };
|
|
3321
|
+
}
|
|
3322
|
+
if (absolute) {
|
|
3323
|
+
host.setCoords();
|
|
3324
|
+
const rect = host.getBoundingRect();
|
|
3325
|
+
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
|
|
3326
|
+
}
|
|
3327
|
+
const width = Math.max(1, host.width ?? 1);
|
|
3328
|
+
const height = Math.max(1, host.height ?? 1);
|
|
3329
|
+
return { left: -width / 2, top: -height / 2, width, height };
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
// src/masks/install.ts
|
|
3333
|
+
var import_fabric14 = require("fabric");
|
|
3334
|
+
function convertSpace(host, sources, absolute) {
|
|
3335
|
+
const wasAbsolute = host.clipPath instanceof import_fabric14.Group ? host.clipPath.absolutePositioned : absolute;
|
|
3336
|
+
if (absolute === wasAbsolute) return;
|
|
3337
|
+
for (const source of sources) {
|
|
3338
|
+
if (absolute) toCanvasSpace(source, host);
|
|
3339
|
+
else toHostSpace(source, host);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
function withRelativeTransforms(entries, sources, host, absolute) {
|
|
3343
|
+
return entries.map((entry, index) => {
|
|
3344
|
+
const source = sources[index];
|
|
3345
|
+
if (absolute && entry.linked && source) {
|
|
3346
|
+
return { ...entry, rel: [...relativeMatrix(source, host)] };
|
|
3347
|
+
}
|
|
3348
|
+
if (!entry.rel) return entry;
|
|
3349
|
+
const rest = { ...entry };
|
|
3350
|
+
delete rest.rel;
|
|
3351
|
+
return rest;
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
function installClip(host, entries, sources, box, absolute) {
|
|
3355
|
+
host.clipPath = composeMaskGroup(sources, entries, { box, absolute });
|
|
3356
|
+
host.dirty = true;
|
|
3357
|
+
host.setCoords();
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
// src/masks/store.ts
|
|
3361
|
+
var CANVAS_MASK_TARGET = "canvas";
|
|
3362
|
+
var DEFAULT_ENTRY = {
|
|
3363
|
+
mode: "add",
|
|
3364
|
+
linked: true,
|
|
3365
|
+
visible: true,
|
|
3366
|
+
opacity: 1
|
|
3367
|
+
};
|
|
3368
|
+
var MaskStackStore = class {
|
|
3369
|
+
constructor(canvas, layers, history, events) {
|
|
3370
|
+
this.canvas = canvas;
|
|
3371
|
+
this.layers = layers;
|
|
3372
|
+
this.history = history;
|
|
3373
|
+
this.events = events;
|
|
3374
|
+
}
|
|
3375
|
+
canvas;
|
|
3376
|
+
layers;
|
|
3377
|
+
history;
|
|
3378
|
+
events;
|
|
3379
|
+
selected = null;
|
|
3380
|
+
pinning = false;
|
|
3381
|
+
list(target) {
|
|
3382
|
+
return this.hostLayer(target)?.meta.maskStack ?? [];
|
|
3383
|
+
}
|
|
3384
|
+
get(target, maskId) {
|
|
3385
|
+
return this.list(target).find((entry) => entry.id === maskId);
|
|
3386
|
+
}
|
|
3387
|
+
/** Every target that currently carries at least one mask. */
|
|
3388
|
+
targets() {
|
|
3389
|
+
return this.layers.getAll().filter((layer) => (layer.meta.maskStack ?? []).length > 0).map((layer) => layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
|
|
3390
|
+
}
|
|
3391
|
+
/** The box a mask is fitted to, in the space the stack is composed in. */
|
|
3392
|
+
hostBox(target, absolute = target === CANVAS_MASK_TARGET || needsAbsoluteSpace(this.list(target))) {
|
|
3393
|
+
return hostBoxOf(this.canvas, this.host(target), absolute);
|
|
3394
|
+
}
|
|
3395
|
+
/** The host layer of a target, optionally creating the design overlay. */
|
|
3396
|
+
hostLayer(target, create = false) {
|
|
3397
|
+
if (target !== CANVAS_MASK_TARGET) return this.layers.get(target);
|
|
3398
|
+
const existing = findCanvasHost(this.layers);
|
|
3399
|
+
if (existing || !create) return existing;
|
|
3400
|
+
return createCanvasHost(this.canvas, this.layers);
|
|
3401
|
+
}
|
|
3402
|
+
host(target, create = false) {
|
|
3403
|
+
return this.hostLayer(target, create)?.fabricObject ?? null;
|
|
3404
|
+
}
|
|
3405
|
+
/** Re-pin the design overlay, guarding the reorder that re-triggers this. */
|
|
3406
|
+
pin() {
|
|
3407
|
+
if (this.pinning) return;
|
|
3408
|
+
this.pinning = true;
|
|
3409
|
+
try {
|
|
3410
|
+
pinCanvasHost(this.layers);
|
|
3411
|
+
} finally {
|
|
3412
|
+
this.pinning = false;
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
/**
|
|
3416
|
+
* Take the current geometry back out of the composed clip, entry-aligned.
|
|
3417
|
+
*
|
|
3418
|
+
* The entry list is the authority on how to read the clip: with no entries the
|
|
3419
|
+
* clip predates the stack (a mask preset, or a single `clipPath` an older host
|
|
3420
|
+
* installed) and is one mask whole — including when it happens to be a group,
|
|
3421
|
+
* which is why this cannot just unwrap anything group-shaped.
|
|
3422
|
+
*/
|
|
3423
|
+
unwrap(target, host) {
|
|
3424
|
+
const clip = host.clipPath;
|
|
3425
|
+
if (!clip) return [];
|
|
3426
|
+
const entries = this.list(target);
|
|
3427
|
+
if (entries.length === 0 || !(clip instanceof import_fabric15.Group)) return [asObject(clip)];
|
|
3428
|
+
const children = unwrapGroup(clip);
|
|
3429
|
+
const extra = children.length - entries.length;
|
|
3430
|
+
return extra > 0 ? children.slice(extra) : children;
|
|
3431
|
+
}
|
|
3432
|
+
/**
|
|
3433
|
+
* Entries for a stack, adopting a pre-stack clip as the first one. Without
|
|
3434
|
+
* this, the first `add()` on an already-masked layer would compose a clip it
|
|
3435
|
+
* has no entry for and silently throw that mask away.
|
|
3436
|
+
*/
|
|
3437
|
+
entriesFor(target, sources) {
|
|
3438
|
+
const existing = this.list(target);
|
|
3439
|
+
if (existing.length > 0 || sources.length !== 1) return [...existing];
|
|
3440
|
+
const layer = this.hostLayer(target);
|
|
3441
|
+
const preset = layer?.meta.maskPreset;
|
|
3442
|
+
if (layer) {
|
|
3443
|
+
delete layer.meta.maskPreset;
|
|
3444
|
+
}
|
|
3445
|
+
return [
|
|
3446
|
+
{
|
|
3447
|
+
...DEFAULT_ENTRY,
|
|
3448
|
+
id: generateId(),
|
|
3449
|
+
name: typeof preset === "string" ? preset : "Mask",
|
|
3450
|
+
linked: target === CANVAS_MASK_TARGET ? false : !sources[0].absolutePositioned
|
|
3451
|
+
}
|
|
3452
|
+
];
|
|
3453
|
+
}
|
|
3454
|
+
/**
|
|
3455
|
+
* Install a stack: convert geometry into the space the stack needs, compose the
|
|
3456
|
+
* clip, store the entries, and commit one history checkpoint for the lot.
|
|
3457
|
+
*/
|
|
3458
|
+
commit(target, host, entries, sources, save = true, forceAbsolute = false) {
|
|
3459
|
+
const layer = this.hostLayer(target);
|
|
3460
|
+
if (!layer) return;
|
|
3461
|
+
const absolute = forceAbsolute || target === CANVAS_MASK_TARGET || needsAbsoluteSpace(entries);
|
|
3462
|
+
convertSpace(host, sources, absolute);
|
|
3463
|
+
const next = withRelativeTransforms(entries, sources, host, absolute);
|
|
3464
|
+
installClip(host, next, sources, this.hostBox(target, absolute), absolute);
|
|
3465
|
+
if (next.length === 0) {
|
|
3466
|
+
delete layer.meta.maskStack;
|
|
3467
|
+
if (layer.meta.canvasMask) this.layers.remove(layer.id);
|
|
3468
|
+
} else {
|
|
3469
|
+
layer.meta.maskStack = next;
|
|
3470
|
+
}
|
|
3471
|
+
this.canvas.requestRenderAll();
|
|
3472
|
+
this.events.emit("masks:changed", { target });
|
|
3473
|
+
this.events.emit("layer:modified", { layerId: layer.id });
|
|
3474
|
+
if (save) this.history.save();
|
|
3475
|
+
}
|
|
3476
|
+
};
|
|
3477
|
+
|
|
3478
|
+
// src/masks/manager.ts
|
|
3479
|
+
var LayerMaskManager = class extends MaskStackStore {
|
|
3480
|
+
// Committing mid-drag would recompose the group the handle writes into and
|
|
3481
|
+
// leave it pointing at a discarded object; the geometry is settled on endEdit.
|
|
3482
|
+
edits = new MaskEditController(this.canvas, () => this.history.save());
|
|
3483
|
+
onLayersChanged = () => this.pin();
|
|
3484
|
+
// Selecting a layer means the user has moved on from the mask they had open;
|
|
3485
|
+
// leaving both selected would show mask controls for an unrelated layer.
|
|
3486
|
+
onLayerSelected = () => {
|
|
3487
|
+
if (this.selected) this.select(null, null);
|
|
3488
|
+
};
|
|
3489
|
+
onObjectModified = (event) => {
|
|
3490
|
+
const object = event.target;
|
|
3491
|
+
if (!object) return;
|
|
3492
|
+
const layer = this.layers.findByObject(object);
|
|
3493
|
+
if (layer) this.reflow(layer.id);
|
|
3494
|
+
};
|
|
3495
|
+
constructor(canvas, layers, history, events) {
|
|
3496
|
+
super(canvas, layers, history, events);
|
|
3497
|
+
this.events.on("layers:changed", this.onLayersChanged);
|
|
3498
|
+
this.events.on("layer:selected", this.onLayerSelected);
|
|
3499
|
+
this.canvas.on("object:modified", this.onObjectModified);
|
|
3500
|
+
}
|
|
3501
|
+
dispose() {
|
|
3502
|
+
this.edits.dispose();
|
|
3503
|
+
this.events.off("layers:changed", this.onLayersChanged);
|
|
3504
|
+
this.events.off("layer:selected", this.onLayerSelected);
|
|
3505
|
+
this.canvas.off("object:modified", this.onObjectModified);
|
|
3506
|
+
this.selected = null;
|
|
3507
|
+
}
|
|
3508
|
+
// ─── Geometry editing ────────────────────────────────
|
|
3509
|
+
/** The mask currently being dragged on the canvas, if any. */
|
|
3510
|
+
editing() {
|
|
3511
|
+
return this.edits.active();
|
|
3512
|
+
}
|
|
3513
|
+
/**
|
|
3514
|
+
* Put drag handles on one mask. The stack is composed in canvas space for the
|
|
3515
|
+
* duration — a linked mask would otherwise sit in the host's space, where the
|
|
3516
|
+
* handle's own canvas coordinates mean something else entirely. `endEdit`
|
|
3517
|
+
* returns it to whichever space its entries call for.
|
|
3518
|
+
*/
|
|
3519
|
+
async beginEdit(target, maskId) {
|
|
3520
|
+
const host = this.host(target);
|
|
3521
|
+
if (!host) return false;
|
|
3522
|
+
const entries = this.list(target);
|
|
3523
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3524
|
+
if (index === -1) return false;
|
|
3525
|
+
this.endEdit(false);
|
|
3526
|
+
this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
|
|
3527
|
+
const group = host.clipPath instanceof import_fabric16.Group ? host.clipPath : null;
|
|
3528
|
+
if (!group) return false;
|
|
3529
|
+
const children = group.getObjects();
|
|
3530
|
+
const child = children[children.length - entries.length + index];
|
|
3531
|
+
if (!child) return false;
|
|
3532
|
+
this.select(target, maskId);
|
|
3533
|
+
return this.edits.begin({ target, maskId }, group, child);
|
|
3534
|
+
}
|
|
3535
|
+
/** Take the handles down and settle the stack back into its own space. */
|
|
3536
|
+
endEdit(save = true) {
|
|
3537
|
+
const editing = this.edits.active();
|
|
3538
|
+
this.edits.end();
|
|
3539
|
+
if (editing) this.rebuild(editing.target, save);
|
|
3540
|
+
}
|
|
3541
|
+
// ─── Selection ───────────────────────────────────────
|
|
3542
|
+
getSelected() {
|
|
3543
|
+
return this.selected;
|
|
3544
|
+
}
|
|
3545
|
+
select(target, maskId) {
|
|
3546
|
+
this.selected = target && maskId ? { target, maskId } : null;
|
|
3547
|
+
this.events.emit("mask:selected", { target, maskId: this.selected ? maskId : null });
|
|
3548
|
+
}
|
|
3549
|
+
// ─── Mutations ───────────────────────────────────────
|
|
3550
|
+
add(target, object, options = {}) {
|
|
3551
|
+
const host = this.host(target, true);
|
|
3552
|
+
if (!host) return null;
|
|
3553
|
+
const sources = this.unwrap(target, host);
|
|
3554
|
+
const entries = this.entriesFor(target, sources);
|
|
3555
|
+
const entry = {
|
|
3556
|
+
...DEFAULT_ENTRY,
|
|
3557
|
+
id: generateId(),
|
|
3558
|
+
name: options.name ?? "Mask",
|
|
3559
|
+
...options.mode ? { mode: options.mode } : {},
|
|
3560
|
+
...options.linked === void 0 ? {} : { linked: options.linked },
|
|
3561
|
+
...options.meta ? { meta: options.meta } : {}
|
|
3562
|
+
};
|
|
3563
|
+
if (target === CANVAS_MASK_TARGET) entry.linked = false;
|
|
3564
|
+
if (options.fit !== false) {
|
|
3565
|
+
fitToBox(object, options.box ?? this.hostBox(target), options.fit ?? 1);
|
|
3566
|
+
}
|
|
3567
|
+
sources.push(object);
|
|
3568
|
+
entries.push(entry);
|
|
3569
|
+
this.commit(target, host, entries, sources);
|
|
3570
|
+
return entry;
|
|
3571
|
+
}
|
|
3572
|
+
/** Swap one mask's geometry, keeping its identity, mode and position in the stack. */
|
|
3573
|
+
replaceGeometry(target, maskId, object, options = {}) {
|
|
3574
|
+
const host = this.host(target);
|
|
3575
|
+
if (!host) return false;
|
|
3576
|
+
const entries = [...this.list(target)];
|
|
3577
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3578
|
+
if (index === -1) return false;
|
|
3579
|
+
const sources = this.unwrap(target, host);
|
|
3580
|
+
if (options.fit !== false) {
|
|
3581
|
+
fitToBox(object, options.box ?? this.hostBox(target), options.fit ?? 1);
|
|
3582
|
+
}
|
|
3583
|
+
sources[index] = object;
|
|
3584
|
+
entries[index] = {
|
|
3585
|
+
...entries[index],
|
|
3586
|
+
...options.name ? { name: options.name } : {},
|
|
3587
|
+
...options.meta ? { meta: options.meta } : {}
|
|
3588
|
+
};
|
|
3589
|
+
this.commit(target, host, entries, sources);
|
|
3590
|
+
return true;
|
|
3591
|
+
}
|
|
3592
|
+
/**
|
|
3593
|
+
* Re-fit one mask to its host's box (or `box`) at `fit` scale, keeping the
|
|
3594
|
+
* geometry it already has. This is what a zoom control drives: re-picking the
|
|
3595
|
+
* mask would cost another render of a generated alpha map just to resize it.
|
|
3596
|
+
*/
|
|
3597
|
+
refit(target, maskId, options = {}) {
|
|
3598
|
+
const host = this.host(target);
|
|
3599
|
+
if (!host) return false;
|
|
3600
|
+
const entries = [...this.list(target)];
|
|
3601
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3602
|
+
if (index === -1) return false;
|
|
3603
|
+
const sources = this.unwrap(target, host);
|
|
3604
|
+
const source = sources[index];
|
|
3605
|
+
if (!source) return false;
|
|
3606
|
+
fitToBox(source, options.box ?? this.hostBox(target), options.fit ?? 1);
|
|
3607
|
+
this.commit(target, host, entries, sources, options.save ?? true);
|
|
3608
|
+
return true;
|
|
3609
|
+
}
|
|
3610
|
+
remove(target, maskId) {
|
|
3611
|
+
const host = this.host(target);
|
|
3612
|
+
if (!host) return false;
|
|
3613
|
+
const entries = [...this.list(target)];
|
|
3614
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3615
|
+
if (index === -1) return false;
|
|
3616
|
+
const sources = this.unwrap(target, host);
|
|
3617
|
+
entries.splice(index, 1);
|
|
3618
|
+
sources.splice(index, 1);
|
|
3619
|
+
if (this.selected?.maskId === maskId) this.select(null, null);
|
|
3620
|
+
this.commit(target, host, entries, sources);
|
|
3621
|
+
return true;
|
|
3622
|
+
}
|
|
3623
|
+
clear(target) {
|
|
3624
|
+
const host = this.host(target);
|
|
3625
|
+
if (!host) return false;
|
|
3626
|
+
if (this.list(target).length === 0) return false;
|
|
3627
|
+
if (this.selected?.target === target) this.select(null, null);
|
|
3628
|
+
this.commit(target, host, [], []);
|
|
3629
|
+
return true;
|
|
3630
|
+
}
|
|
3631
|
+
reorder(target, maskId, index) {
|
|
3632
|
+
const host = this.host(target);
|
|
3633
|
+
if (!host) return false;
|
|
3634
|
+
const entries = [...this.list(target)];
|
|
3635
|
+
const from = entries.findIndex((entry2) => entry2.id === maskId);
|
|
3636
|
+
if (from === -1) return false;
|
|
3637
|
+
const to = Math.max(0, Math.min(entries.length - 1, Math.round(index)));
|
|
3638
|
+
if (from === to) return false;
|
|
3639
|
+
const sources = this.unwrap(target, host);
|
|
3640
|
+
const [entry] = entries.splice(from, 1);
|
|
3641
|
+
const [source] = sources.splice(from, 1);
|
|
3642
|
+
entries.splice(to, 0, entry);
|
|
3643
|
+
sources.splice(to, 0, source);
|
|
3644
|
+
this.commit(target, host, entries, sources);
|
|
3645
|
+
return true;
|
|
3646
|
+
}
|
|
3647
|
+
/** Replace a mask's host metadata (which preset or generator produced it). */
|
|
3648
|
+
setMeta(target, maskId, meta) {
|
|
3649
|
+
return this.patch(target, maskId, () => ({ meta }));
|
|
3650
|
+
}
|
|
3651
|
+
setMode(target, maskId, mode) {
|
|
3652
|
+
return this.patch(target, maskId, (entry) => entry.mode === mode ? null : { mode });
|
|
3653
|
+
}
|
|
3654
|
+
setVisible(target, maskId, visible) {
|
|
3655
|
+
return this.patch(target, maskId, (entry) => entry.visible === visible ? null : { visible });
|
|
3656
|
+
}
|
|
3657
|
+
setOpacity(target, maskId, opacity) {
|
|
3658
|
+
if (!Number.isFinite(opacity)) return false;
|
|
3659
|
+
const next = Math.max(0, Math.min(1, opacity));
|
|
3660
|
+
return this.patch(
|
|
3661
|
+
target,
|
|
3662
|
+
maskId,
|
|
3663
|
+
(entry) => entry.opacity === next ? null : { opacity: next }
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
setName(target, maskId, name) {
|
|
3667
|
+
const trimmed = name.trim();
|
|
3668
|
+
if (!trimmed) return false;
|
|
3669
|
+
return this.patch(
|
|
3670
|
+
target,
|
|
3671
|
+
maskId,
|
|
3672
|
+
(entry) => entry.name === trimmed ? null : { name: trimmed }
|
|
3673
|
+
);
|
|
3674
|
+
}
|
|
3675
|
+
/**
|
|
3676
|
+
* Link or unlink one mask. Unlinking pins it where it currently appears;
|
|
3677
|
+
* re-linking records its position relative to the host so later host moves
|
|
3678
|
+
* carry it along.
|
|
3679
|
+
*/
|
|
3680
|
+
setLinked(target, maskId, linked) {
|
|
3681
|
+
if (target === CANVAS_MASK_TARGET) return false;
|
|
3682
|
+
const host = this.host(target);
|
|
3683
|
+
if (!host) return false;
|
|
3684
|
+
const entries = [...this.list(target)];
|
|
3685
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3686
|
+
if (index === -1 || entries[index].linked === linked) return false;
|
|
3687
|
+
const sources = this.unwrap(target, host);
|
|
3688
|
+
entries[index] = { ...entries[index], linked };
|
|
3689
|
+
this.commit(target, host, entries, sources);
|
|
3690
|
+
return true;
|
|
3691
|
+
}
|
|
3692
|
+
/**
|
|
3693
|
+
* Consume a layer, turning its artwork into a mask. Without an explicit target
|
|
3694
|
+
* it masks the layer directly beneath it, and the bottom layer masks the whole
|
|
3695
|
+
* design — there is nothing under it to clip.
|
|
3696
|
+
*/
|
|
3697
|
+
convertLayer(layerId, target) {
|
|
3698
|
+
const layer = this.layers.get(layerId);
|
|
3699
|
+
if (!layer || layer.meta.canvasMask) return null;
|
|
3700
|
+
const ordered = this.layers.getAll();
|
|
3701
|
+
const index = ordered.findIndex((candidate) => candidate.id === layerId);
|
|
3702
|
+
const below = index > 0 ? ordered[index - 1] : void 0;
|
|
3703
|
+
const resolved = target ?? (below && !below.meta.canvasMask ? below.id : CANVAS_MASK_TARGET);
|
|
3704
|
+
if (resolved === layerId) return null;
|
|
3705
|
+
const object = layer.fabricObject;
|
|
3706
|
+
if (!object.fill && object.type !== "image") object.set({ fill: "#000000" });
|
|
3707
|
+
this.history.beginTransaction();
|
|
3708
|
+
try {
|
|
3709
|
+
if (this.canvas.getActiveObject() === object) this.canvas.discardActiveObject();
|
|
3710
|
+
this.canvas.remove(object);
|
|
3711
|
+
this.layers.remove(layerId);
|
|
3712
|
+
const entry = this.add(resolved, object, { name: layer.name, fit: false });
|
|
3713
|
+
return entry ? { target: resolved, entry } : null;
|
|
3714
|
+
} finally {
|
|
3715
|
+
this.history.endTransaction();
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
// ─── Rebuild / restore ───────────────────────────────
|
|
3719
|
+
/** Recompose one stack's clip from the geometry it already holds. */
|
|
3720
|
+
rebuild(target, save = false) {
|
|
3721
|
+
const host = this.host(target);
|
|
3722
|
+
if (!host) return;
|
|
3723
|
+
const sources = this.unwrap(target, host);
|
|
3724
|
+
if (sources.length === 0) return;
|
|
3725
|
+
this.commit(target, host, this.entriesFor(target, sources), sources, save);
|
|
3726
|
+
}
|
|
3727
|
+
/** Re-derive linked masks after the host moved (canvas-space stacks only). */
|
|
3728
|
+
reflow(target) {
|
|
3729
|
+
const host = this.host(target);
|
|
3730
|
+
if (!host) return;
|
|
3731
|
+
const entries = this.list(target);
|
|
3732
|
+
if (entries.length === 0 || !needsAbsoluteSpace(entries)) return;
|
|
3733
|
+
if (!entries.some((entry) => entry.linked && entry.rel)) return;
|
|
3734
|
+
const sources = this.unwrap(target, host);
|
|
3735
|
+
entries.forEach((entry, index) => {
|
|
3736
|
+
const source = sources[index];
|
|
3737
|
+
const rel = toMatrix(entry.rel);
|
|
3738
|
+
if (!source || !entry.linked || !rel) return;
|
|
3739
|
+
applyRelativeMatrix(source, host, rel);
|
|
3740
|
+
});
|
|
3741
|
+
this.commit(target, host, [...entries], sources);
|
|
3742
|
+
}
|
|
3743
|
+
/**
|
|
3744
|
+
* Adopt a clip this manager did not install — a host's own single `clipPath` —
|
|
3745
|
+
* as a one-entry stack, so it shows up in the UI as a mask row before anything
|
|
3746
|
+
* is added to it. A mask preset is left alone unless asked for by name: it is
|
|
3747
|
+
* still owned by `MaskPresetManager` until a stack operation takes it over.
|
|
3748
|
+
*/
|
|
3749
|
+
adopt(target, name) {
|
|
3750
|
+
const layer = this.hostLayer(target);
|
|
3751
|
+
const clip = layer?.fabricObject.clipPath;
|
|
3752
|
+
if (!layer || !clip || layer.meta.pattern) return null;
|
|
3753
|
+
if ((layer.meta.maskStack ?? []).length > 0) return null;
|
|
3754
|
+
const entry = {
|
|
3755
|
+
...DEFAULT_ENTRY,
|
|
3756
|
+
id: generateId(),
|
|
3757
|
+
name: name ?? "Mask",
|
|
3758
|
+
linked: target === CANVAS_MASK_TARGET ? false : !clip.absolutePositioned
|
|
3759
|
+
};
|
|
3760
|
+
layer.meta.maskStack = [entry];
|
|
3761
|
+
delete layer.meta.maskPreset;
|
|
3762
|
+
this.rebuild(target);
|
|
3763
|
+
return entry;
|
|
3764
|
+
}
|
|
3765
|
+
/** After a state restore: re-pin the design overlay and recompose every stack. */
|
|
3766
|
+
refreshAll() {
|
|
3767
|
+
this.selected = null;
|
|
3768
|
+
this.pin();
|
|
3769
|
+
for (const layer of this.layers.getAll()) {
|
|
3770
|
+
if ((layer.meta.maskStack ?? []).length === 0) continue;
|
|
3771
|
+
this.rebuild(layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
// ─── Internals ───────────────────────────────────────
|
|
3775
|
+
patch(target, maskId, change) {
|
|
3776
|
+
const host = this.host(target);
|
|
3777
|
+
if (!host) return false;
|
|
3778
|
+
const entries = [...this.list(target)];
|
|
3779
|
+
const index = entries.findIndex((entry) => entry.id === maskId);
|
|
3780
|
+
if (index === -1) return false;
|
|
3781
|
+
const patch = change(entries[index]);
|
|
3782
|
+
if (!patch) return false;
|
|
3783
|
+
const sources = this.unwrap(target, host);
|
|
3784
|
+
entries[index] = { ...entries[index], ...patch };
|
|
3785
|
+
this.commit(target, host, entries, sources);
|
|
3786
|
+
return true;
|
|
3787
|
+
}
|
|
3788
|
+
};
|
|
3789
|
+
|
|
2855
3790
|
// src/editor.ts
|
|
2856
3791
|
var MIN_ZOOM = 0.1;
|
|
2857
3792
|
var MAX_ZOOM = 8;
|
|
@@ -2869,6 +3804,8 @@ var CanvasEditor = class {
|
|
|
2869
3804
|
patterns;
|
|
2870
3805
|
curves;
|
|
2871
3806
|
maskPresets;
|
|
3807
|
+
/** Stacked boolean masks, per layer and for the design as a whole. */
|
|
3808
|
+
layerMasks;
|
|
2872
3809
|
fonts;
|
|
2873
3810
|
licensing;
|
|
2874
3811
|
pages;
|
|
@@ -2893,7 +3830,7 @@ var CanvasEditor = class {
|
|
|
2893
3830
|
const widthPx = this.units.toPixels(config.width);
|
|
2894
3831
|
const heightPx = this.units.toPixels(config.height);
|
|
2895
3832
|
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
2896
|
-
this.canvas = new
|
|
3833
|
+
this.canvas = new import_fabric17.Canvas(canvasElement, {
|
|
2897
3834
|
width: widthPx,
|
|
2898
3835
|
height: heightPx,
|
|
2899
3836
|
backgroundColor: this.designBackground,
|
|
@@ -2917,6 +3854,7 @@ var CanvasEditor = class {
|
|
|
2917
3854
|
this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
|
|
2918
3855
|
this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
|
|
2919
3856
|
this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
|
|
3857
|
+
this.layerMasks = new LayerMaskManager(this.canvas, this.layers, this.history, this.events);
|
|
2920
3858
|
this.setupCanvasEvents();
|
|
2921
3859
|
this.refreshSelectionStyle();
|
|
2922
3860
|
this.history.saveImmediate();
|
|
@@ -2926,12 +3864,13 @@ var CanvasEditor = class {
|
|
|
2926
3864
|
// ─── Layer Operations ────────────────────────────────
|
|
2927
3865
|
async addImage(url, options) {
|
|
2928
3866
|
try {
|
|
2929
|
-
const img = await
|
|
3867
|
+
const img = await import_fabric17.FabricImage.fromURL(
|
|
2930
3868
|
url,
|
|
2931
3869
|
{},
|
|
2932
3870
|
{ originX: "left", originY: "top", ...options }
|
|
2933
3871
|
);
|
|
2934
3872
|
const layer = this.layers.add("image", img);
|
|
3873
|
+
this.layers.select(layer.id);
|
|
2935
3874
|
this.history.save();
|
|
2936
3875
|
return layer;
|
|
2937
3876
|
} catch (error) {
|
|
@@ -2951,7 +3890,7 @@ var CanvasEditor = class {
|
|
|
2951
3890
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
2952
3891
|
const previous = layer.fabricObject;
|
|
2953
3892
|
try {
|
|
2954
|
-
const replacement = await
|
|
3893
|
+
const replacement = await import_fabric17.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
2955
3894
|
replacement.set({
|
|
2956
3895
|
left: previous.left,
|
|
2957
3896
|
top: previous.top,
|
|
@@ -2980,7 +3919,7 @@ var CanvasEditor = class {
|
|
|
2980
3919
|
}
|
|
2981
3920
|
}
|
|
2982
3921
|
addText(text, options) {
|
|
2983
|
-
const textbox = new
|
|
3922
|
+
const textbox = new import_fabric17.Textbox(text, {
|
|
2984
3923
|
fontSize: 32,
|
|
2985
3924
|
fontFamily: "Arial",
|
|
2986
3925
|
fill: "#000000",
|
|
@@ -2991,12 +3930,14 @@ var CanvasEditor = class {
|
|
|
2991
3930
|
...options
|
|
2992
3931
|
});
|
|
2993
3932
|
const layer = this.layers.add("text", textbox);
|
|
3933
|
+
this.layers.select(layer.id);
|
|
2994
3934
|
this.history.save();
|
|
2995
3935
|
return layer;
|
|
2996
3936
|
}
|
|
2997
3937
|
addShape(plugin, options) {
|
|
2998
3938
|
const obj = plugin.create(options);
|
|
2999
3939
|
const layer = this.layers.add("shape", obj, plugin.name);
|
|
3940
|
+
this.layers.select(layer.id);
|
|
3000
3941
|
this.history.save();
|
|
3001
3942
|
return layer;
|
|
3002
3943
|
}
|
|
@@ -3019,10 +3960,10 @@ var CanvasEditor = class {
|
|
|
3019
3960
|
return value === void 0 ? token : escapeXml(value);
|
|
3020
3961
|
})
|
|
3021
3962
|
);
|
|
3022
|
-
const { objects, options } = await (0,
|
|
3963
|
+
const { objects, options } = await (0, import_fabric17.loadSVGFromString)(resolved);
|
|
3023
3964
|
const validObjects = objects.filter((object) => object !== null);
|
|
3024
3965
|
if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
3025
|
-
const group =
|
|
3966
|
+
const group = import_fabric17.util.groupSVGElements(validObjects, options);
|
|
3026
3967
|
group.set({
|
|
3027
3968
|
left: this.canvas.getWidth() / 2,
|
|
3028
3969
|
top: this.canvas.getHeight() / 2,
|
|
@@ -3030,6 +3971,7 @@ var CanvasEditor = class {
|
|
|
3030
3971
|
originY: "center"
|
|
3031
3972
|
});
|
|
3032
3973
|
const layer = this.layers.add("template", group, template.name);
|
|
3974
|
+
this.layers.select(layer.id);
|
|
3033
3975
|
this.history.save();
|
|
3034
3976
|
return layer;
|
|
3035
3977
|
}
|
|
@@ -3081,7 +4023,7 @@ var CanvasEditor = class {
|
|
|
3081
4023
|
opacity: layer.opacity
|
|
3082
4024
|
});
|
|
3083
4025
|
this.patterns.attachTo(copy);
|
|
3084
|
-
this.
|
|
4026
|
+
this.layers.select(copy.id);
|
|
3085
4027
|
this.canvas.requestRenderAll();
|
|
3086
4028
|
this.history.save();
|
|
3087
4029
|
return copy;
|
|
@@ -3094,10 +4036,10 @@ var CanvasEditor = class {
|
|
|
3094
4036
|
const next = { ...previous, ...adjustments };
|
|
3095
4037
|
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
3096
4038
|
image.filters = [
|
|
3097
|
-
new
|
|
3098
|
-
new
|
|
3099
|
-
new
|
|
3100
|
-
new
|
|
4039
|
+
new import_fabric17.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
|
|
4040
|
+
new import_fabric17.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
|
|
4041
|
+
new import_fabric17.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
|
|
4042
|
+
new import_fabric17.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
|
|
3101
4043
|
];
|
|
3102
4044
|
layer.meta.imageAdjustments = next;
|
|
3103
4045
|
image.applyFilters();
|
|
@@ -3114,7 +4056,7 @@ var CanvasEditor = class {
|
|
|
3114
4056
|
const childData = children.map((layer) => structuredClone(layer.toData()));
|
|
3115
4057
|
const objects = children.map((layer) => layer.fabricObject);
|
|
3116
4058
|
for (const layer of children) this.layers.remove(layer.id);
|
|
3117
|
-
const group = new
|
|
4059
|
+
const group = new import_fabric17.Group(objects);
|
|
3118
4060
|
const grouped = this.layers.add("group", group, name);
|
|
3119
4061
|
grouped.meta.groupChildren = childData;
|
|
3120
4062
|
this.layers.select(grouped.id);
|
|
@@ -3129,11 +4071,9 @@ var CanvasEditor = class {
|
|
|
3129
4071
|
if (!grouped || grouped.type !== "group" || !Array.isArray(childData)) return [];
|
|
3130
4072
|
const group = grouped.fabricObject;
|
|
3131
4073
|
return this.history.transaction(() => {
|
|
3132
|
-
const transform = group.calcTransformMatrix();
|
|
3133
4074
|
const objects = group.removeAll();
|
|
3134
4075
|
this.layers.remove(id);
|
|
3135
4076
|
const restored = objects.map((object, index) => {
|
|
3136
|
-
import_fabric10.util.addTransformToObject(object, transform);
|
|
3137
4077
|
object.setCoords();
|
|
3138
4078
|
const data = childData[index];
|
|
3139
4079
|
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
@@ -3161,6 +4101,7 @@ var CanvasEditor = class {
|
|
|
3161
4101
|
try {
|
|
3162
4102
|
await deserializeEditor(this, state);
|
|
3163
4103
|
await this.patterns.rehydrateAll();
|
|
4104
|
+
this.layerMasks.refreshAll();
|
|
3164
4105
|
} catch (error) {
|
|
3165
4106
|
if (!managedByHistory) {
|
|
3166
4107
|
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
@@ -3189,7 +4130,7 @@ var CanvasEditor = class {
|
|
|
3189
4130
|
const layer = this.layers.get(id);
|
|
3190
4131
|
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
3191
4132
|
try {
|
|
3192
|
-
if (options.resolution === "source" && layer.fabricObject instanceof
|
|
4133
|
+
if (options.resolution === "source" && layer.fabricObject instanceof import_fabric17.FabricImage) {
|
|
3193
4134
|
const image = await layer.fabricObject.clone();
|
|
3194
4135
|
image.set({
|
|
3195
4136
|
left: 0,
|
|
@@ -3465,7 +4406,7 @@ var CanvasEditor = class {
|
|
|
3465
4406
|
return;
|
|
3466
4407
|
}
|
|
3467
4408
|
try {
|
|
3468
|
-
const image = await
|
|
4409
|
+
const image = await import_fabric17.FabricImage.fromURL(
|
|
3469
4410
|
url,
|
|
3470
4411
|
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
3471
4412
|
{ originX: "left", originY: "top" }
|
|
@@ -3733,6 +4674,7 @@ var CanvasEditor = class {
|
|
|
3733
4674
|
// ─── Cleanup ────────────────────────────────────────
|
|
3734
4675
|
dispose() {
|
|
3735
4676
|
this.masks.dispose();
|
|
4677
|
+
this.layerMasks.dispose();
|
|
3736
4678
|
this.snapping.dispose();
|
|
3737
4679
|
this.crop.dispose();
|
|
3738
4680
|
this.history.dispose();
|
|
@@ -3856,6 +4798,7 @@ var AnnotationOverlay = class {
|
|
|
3856
4798
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3857
4799
|
0 && (module.exports = {
|
|
3858
4800
|
AnnotationOverlay,
|
|
4801
|
+
CANVAS_MASK_TARGET,
|
|
3859
4802
|
CANVAS_SIZE_PRESETS,
|
|
3860
4803
|
CanvasEditor,
|
|
3861
4804
|
CropController,
|
|
@@ -3868,6 +4811,7 @@ var AnnotationOverlay = class {
|
|
|
3868
4811
|
HistoryManager,
|
|
3869
4812
|
Layer,
|
|
3870
4813
|
LayerManager,
|
|
4814
|
+
LayerMaskManager,
|
|
3871
4815
|
LicenseManager,
|
|
3872
4816
|
MaskController,
|
|
3873
4817
|
MaskPresetManager,
|
|
@@ -3889,6 +4833,7 @@ var AnnotationOverlay = class {
|
|
|
3889
4833
|
buildCurvePathData,
|
|
3890
4834
|
clamp,
|
|
3891
4835
|
clearTextureMaskCache,
|
|
4836
|
+
composeMaskGroup,
|
|
3892
4837
|
computeCoverPlacement,
|
|
3893
4838
|
computePrintAreaClip,
|
|
3894
4839
|
computeTilePositions,
|
|
@@ -3901,11 +4846,13 @@ var AnnotationOverlay = class {
|
|
|
3901
4846
|
exportPNG,
|
|
3902
4847
|
exportPrintArea,
|
|
3903
4848
|
exportSVG,
|
|
4849
|
+
fitToBox,
|
|
3904
4850
|
generateId,
|
|
3905
4851
|
isCssColor,
|
|
3906
4852
|
isMaskPresetId,
|
|
3907
4853
|
isShapeMaskId,
|
|
3908
4854
|
isTextureMaskId,
|
|
4855
|
+
needsAbsoluteSpace,
|
|
3909
4856
|
readLayerShadow,
|
|
3910
4857
|
renderTextureMask,
|
|
3911
4858
|
resetTransform,
|
|
@@ -3913,6 +4860,9 @@ var AnnotationOverlay = class {
|
|
|
3913
4860
|
round2,
|
|
3914
4861
|
sanitizeSvg,
|
|
3915
4862
|
serializeEditor,
|
|
3916
|
-
shapeMaskPathData
|
|
4863
|
+
shapeMaskPathData,
|
|
4864
|
+
toCanvasSpace,
|
|
4865
|
+
toHostSpace,
|
|
4866
|
+
unwrapGroup
|
|
3917
4867
|
});
|
|
3918
4868
|
//# sourceMappingURL=index.js.map
|