@overtone-art/canvas-editor-core 0.3.2 → 0.4.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/index.d.mts +127 -46
- package/dist/index.d.ts +127 -46
- package/dist/index.global.js +53 -52
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +464 -332
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +441 -305
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/{types-Dh1unrzT.d.mts → types-BNzE_X5M.d.mts} +16 -22
- package/dist/{types-Dh1unrzT.d.ts → types-BNzE_X5M.d.ts} +16 -22
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -45,17 +45,14 @@ __export(index_exports, {
|
|
|
45
45
|
TEXTURE_MASK_IDS: () => TEXTURE_MASK_IDS,
|
|
46
46
|
TEXTURE_MASK_SIZE: () => TEXTURE_MASK_SIZE,
|
|
47
47
|
TextCurveManager: () => TextCurveManager,
|
|
48
|
+
TiledPatternObject: () => TiledPatternObject,
|
|
48
49
|
UnitConverter: () => UnitConverter,
|
|
49
50
|
applyAspectLock: () => applyAspectLock,
|
|
50
51
|
applyLayerShadow: () => applyLayerShadow,
|
|
51
52
|
applyObjectSelectionStyle: () => applyObjectSelectionStyle,
|
|
52
|
-
applyPatternLocks: () => applyPatternLocks,
|
|
53
53
|
applySelectionStyle: () => applySelectionStyle,
|
|
54
54
|
buildCurvePathData: () => buildCurvePathData,
|
|
55
|
-
buildPatternDataURL: () => buildPatternDataURL,
|
|
56
|
-
captureLocks: () => captureLocks,
|
|
57
55
|
clamp: () => clamp,
|
|
58
|
-
clearPatternImageCache: () => clearPatternImageCache,
|
|
59
56
|
clearTextureMaskCache: () => clearTextureMaskCache,
|
|
60
57
|
computeCoverPlacement: () => computeCoverPlacement,
|
|
61
58
|
computePrintAreaClip: () => computePrintAreaClip,
|
|
@@ -74,7 +71,6 @@ __export(index_exports, {
|
|
|
74
71
|
isMaskPresetId: () => isMaskPresetId,
|
|
75
72
|
isShapeMaskId: () => isShapeMaskId,
|
|
76
73
|
isTextureMaskId: () => isTextureMaskId,
|
|
77
|
-
loadPatternImage: () => loadPatternImage,
|
|
78
74
|
readLayerShadow: () => readLayerShadow,
|
|
79
75
|
renderTextureMask: () => renderTextureMask,
|
|
80
76
|
resetTransform: () => resetTransform,
|
|
@@ -87,7 +83,7 @@ __export(index_exports, {
|
|
|
87
83
|
module.exports = __toCommonJS(index_exports);
|
|
88
84
|
|
|
89
85
|
// src/editor.ts
|
|
90
|
-
var
|
|
86
|
+
var import_fabric10 = require("fabric");
|
|
91
87
|
|
|
92
88
|
// src/events.ts
|
|
93
89
|
var EventEmitter = class {
|
|
@@ -139,6 +135,12 @@ var Layer = class {
|
|
|
139
135
|
/** Non-fabric data (e.g. pattern config) that must persist with the layer. */
|
|
140
136
|
meta;
|
|
141
137
|
fabricObject;
|
|
138
|
+
/**
|
|
139
|
+
* Stand-in object drawn in place of `fabricObject` (the tiled pattern). It is
|
|
140
|
+
* part of the canvas but never part of the document: it is not serialized, and
|
|
141
|
+
* the layer's real object stays the one every editor API talks to.
|
|
142
|
+
*/
|
|
143
|
+
renderProxy;
|
|
142
144
|
constructor(type, fabricObject, name, id) {
|
|
143
145
|
this.id = id ?? generateId();
|
|
144
146
|
this.type = type;
|
|
@@ -148,6 +150,7 @@ var Layer = class {
|
|
|
148
150
|
this.opacity = 1;
|
|
149
151
|
this.meta = {};
|
|
150
152
|
this.fabricObject = fabricObject;
|
|
153
|
+
this.renderProxy = null;
|
|
151
154
|
this.fabricObject._layerId = this.id;
|
|
152
155
|
}
|
|
153
156
|
hasMeta() {
|
|
@@ -196,6 +199,7 @@ var LayerManager = class {
|
|
|
196
199
|
if (index === -1) return false;
|
|
197
200
|
const layer = this.layers[index];
|
|
198
201
|
this.canvas.remove(layer.fabricObject);
|
|
202
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
199
203
|
this.layers.splice(index, 1);
|
|
200
204
|
this.events.emit("layer:removed", { layerId: id });
|
|
201
205
|
this.emitChanged();
|
|
@@ -219,6 +223,10 @@ var LayerManager = class {
|
|
|
219
223
|
evented: !layer.locked
|
|
220
224
|
});
|
|
221
225
|
this.canvas.insertAt(Math.max(0, stackIndex), fabricObject);
|
|
226
|
+
if (layer.renderProxy) {
|
|
227
|
+
fabricObject.set({ opacity: 0 });
|
|
228
|
+
this.syncZOrder();
|
|
229
|
+
}
|
|
222
230
|
if (wasActive) this.canvas.setActiveObject(fabricObject);
|
|
223
231
|
this.canvas.requestRenderAll();
|
|
224
232
|
this.events.emit("layer:modified", { layerId: id });
|
|
@@ -226,6 +234,36 @@ var LayerManager = class {
|
|
|
226
234
|
this.onPropertyChanged?.();
|
|
227
235
|
return true;
|
|
228
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Attach (or clear) the object drawn in place of a layer's own object. The
|
|
239
|
+
* proxy tracks the layer's stacking position, visibility and opacity, and is
|
|
240
|
+
* removed with the layer — it must never outlive or drift from its source.
|
|
241
|
+
*/
|
|
242
|
+
setRenderProxy(id, proxy) {
|
|
243
|
+
const layer = this.get(id);
|
|
244
|
+
if (!layer || layer.renderProxy === proxy) return;
|
|
245
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
246
|
+
layer.renderProxy = proxy;
|
|
247
|
+
if (proxy) {
|
|
248
|
+
proxy.set({ visible: layer.visible, opacity: layer.opacity });
|
|
249
|
+
this.canvas.add(proxy);
|
|
250
|
+
this.syncZOrder();
|
|
251
|
+
}
|
|
252
|
+
this.canvas.requestRenderAll();
|
|
253
|
+
this.emitChanged();
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Re-stack every canvas object to match layer order, keeping each proxy
|
|
257
|
+
* directly above the source it stands in for. Canvas indices can't be derived
|
|
258
|
+
* from layer indices once proxies are in the array, so the order is rebuilt
|
|
259
|
+
* front-to-back instead of computed.
|
|
260
|
+
*/
|
|
261
|
+
syncZOrder() {
|
|
262
|
+
for (const layer of this.layers) {
|
|
263
|
+
this.canvas.bringObjectToFront(layer.fabricObject);
|
|
264
|
+
if (layer.renderProxy) this.canvas.bringObjectToFront(layer.renderProxy);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
229
267
|
reorder(id, newIndex) {
|
|
230
268
|
const oldIndex = this.layers.findIndex((l) => l.id === id);
|
|
231
269
|
if (oldIndex === -1) return false;
|
|
@@ -234,9 +272,7 @@ var LayerManager = class {
|
|
|
234
272
|
if (oldIndex === clamped) return false;
|
|
235
273
|
const [layer] = this.layers.splice(oldIndex, 1);
|
|
236
274
|
this.layers.splice(clamped, 0, layer);
|
|
237
|
-
this.
|
|
238
|
-
this.canvas.moveObjectTo(l.fabricObject, i);
|
|
239
|
-
});
|
|
275
|
+
this.syncZOrder();
|
|
240
276
|
this.events.emit("layer:reordered", { layerIds: this.layers.map((l) => l.id) });
|
|
241
277
|
this.emitChanged();
|
|
242
278
|
this.onPropertyChanged?.();
|
|
@@ -273,6 +309,7 @@ var LayerManager = class {
|
|
|
273
309
|
if (layer.visible === visible) return;
|
|
274
310
|
layer.visible = visible;
|
|
275
311
|
layer.fabricObject.visible = visible;
|
|
312
|
+
if (layer.renderProxy) layer.renderProxy.visible = visible;
|
|
276
313
|
this.canvas.requestRenderAll();
|
|
277
314
|
this.emitChanged();
|
|
278
315
|
this.onPropertyChanged?.();
|
|
@@ -294,7 +331,8 @@ var LayerManager = class {
|
|
|
294
331
|
const next = Math.max(0, Math.min(1, opacity));
|
|
295
332
|
if (layer.opacity === next) return;
|
|
296
333
|
layer.opacity = next;
|
|
297
|
-
layer.
|
|
334
|
+
if (layer.renderProxy) layer.renderProxy.opacity = next;
|
|
335
|
+
else layer.fabricObject.opacity = next;
|
|
298
336
|
this.canvas.requestRenderAll();
|
|
299
337
|
this.emitChanged();
|
|
300
338
|
this.onPropertyChanged?.();
|
|
@@ -342,6 +380,7 @@ var LayerManager = class {
|
|
|
342
380
|
clear() {
|
|
343
381
|
for (const layer of this.layers) {
|
|
344
382
|
this.canvas.remove(layer.fabricObject);
|
|
383
|
+
if (layer.renderProxy) this.canvas.remove(layer.renderProxy);
|
|
345
384
|
}
|
|
346
385
|
this.layers = [];
|
|
347
386
|
this.emitChanged();
|
|
@@ -875,199 +914,415 @@ var CropController = class {
|
|
|
875
914
|
};
|
|
876
915
|
var STROKE2 = "#22c55e";
|
|
877
916
|
|
|
878
|
-
// src/pattern.ts
|
|
917
|
+
// src/pattern/pattern-manager.ts
|
|
918
|
+
var import_fabric3 = require("fabric");
|
|
919
|
+
|
|
920
|
+
// src/pattern/tiled-pattern-object.ts
|
|
879
921
|
var import_fabric2 = require("fabric");
|
|
922
|
+
|
|
923
|
+
// src/pattern/tile-geometry.ts
|
|
880
924
|
var MAX_TILES_PER_AXIS = 200;
|
|
925
|
+
function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
|
|
926
|
+
const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
|
|
927
|
+
const radius = cornerRadius(anchor, targetW, targetH);
|
|
928
|
+
const span = radius * 2;
|
|
929
|
+
const minTile = Math.max(1, span / MAX_TILES_PER_AXIS);
|
|
930
|
+
const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
|
|
931
|
+
const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
|
|
932
|
+
const cols = Math.ceil(span / tileW) + 2;
|
|
933
|
+
const rows = Math.ceil(span / tileH) + 2;
|
|
934
|
+
const halfCols = Math.ceil(cols / 2);
|
|
935
|
+
const halfRows = Math.ceil(rows / 2);
|
|
936
|
+
const shiftX = tileW * (clampOffset(config.offsetX) / 100);
|
|
937
|
+
const shiftY = tileH * (clampOffset(config.offsetY) / 100);
|
|
938
|
+
const placements = [];
|
|
939
|
+
for (let j = -halfRows; j <= halfRows; j++) {
|
|
940
|
+
for (let i = -halfCols; i <= halfCols; i++) {
|
|
941
|
+
let x = i * tileW + shiftX;
|
|
942
|
+
let y = j * tileH + shiftY;
|
|
943
|
+
if (config.mode === "brick-horizontal" && mod2(j) === 1) {
|
|
944
|
+
x += tileW * (config.horizontalOffset / 100);
|
|
945
|
+
} else if (config.mode === "brick-vertical" && mod2(i) === 1) {
|
|
946
|
+
y += tileH * (config.horizontalOffset / 100);
|
|
947
|
+
}
|
|
948
|
+
const rotation = (i * config.rotationStepH + j * config.rotationStepV) * Math.PI / 180;
|
|
949
|
+
placements.push({ x, y, rotation });
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return placements;
|
|
953
|
+
}
|
|
954
|
+
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
|
|
955
|
+
const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
|
|
956
|
+
ctx.save();
|
|
957
|
+
ctx.translate(anchor.x, anchor.y);
|
|
958
|
+
ctx.rotate(config.angle * Math.PI / 180);
|
|
959
|
+
for (const tile of computeTilePositions(config, targetW, targetH, baseW, baseH, anchor)) {
|
|
960
|
+
ctx.save();
|
|
961
|
+
ctx.translate(tile.x, tile.y);
|
|
962
|
+
ctx.rotate(tile.rotation);
|
|
963
|
+
ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
|
|
964
|
+
ctx.restore();
|
|
965
|
+
}
|
|
966
|
+
ctx.restore();
|
|
967
|
+
}
|
|
968
|
+
function cornerRadius(origin, w, h) {
|
|
969
|
+
const dx = Math.max(Math.abs(origin.x), Math.abs(w - origin.x));
|
|
970
|
+
const dy = Math.max(Math.abs(origin.y), Math.abs(h - origin.y));
|
|
971
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
972
|
+
}
|
|
973
|
+
function clampOffset(value) {
|
|
974
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
975
|
+
return Math.max(-100, Math.min(100, value));
|
|
976
|
+
}
|
|
977
|
+
function mod2(n) {
|
|
978
|
+
return (n % 2 + 2) % 2;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/pattern/tiled-pattern-object.ts
|
|
982
|
+
var MAX_SNAPSHOT_PIXELS = 16e6;
|
|
983
|
+
var MAX_SNAPSHOT_SCALE = 8;
|
|
984
|
+
var SNAPSHOT_SHRINK_FACTOR = 2;
|
|
985
|
+
var TiledPatternObject = class _TiledPatternObject extends import_fabric2.FabricObject {
|
|
986
|
+
static type = "TiledPattern";
|
|
987
|
+
/** The layer's real object. Never rendered directly (it is kept at opacity 0). */
|
|
988
|
+
source;
|
|
989
|
+
config;
|
|
990
|
+
snapshotEl = null;
|
|
991
|
+
snapshotScale = 0;
|
|
992
|
+
constructor(source, config, width, height) {
|
|
993
|
+
super({
|
|
994
|
+
// Explicit origin: fabric v7 defaults to `center`, and a centre-origin
|
|
995
|
+
// object placed at 0,0 covers a quarter of the canvas. That exact bug is
|
|
996
|
+
// why the old bake-into-the-layer pattern landed in the top-left corner.
|
|
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,
|
|
1007
|
+
objectCaching: false,
|
|
1008
|
+
hasControls: false,
|
|
1009
|
+
hasBorders: false
|
|
1010
|
+
});
|
|
1011
|
+
this.source = source;
|
|
1012
|
+
this.config = config;
|
|
1013
|
+
}
|
|
1014
|
+
/** Swap in a new config; the next render picks it up. */
|
|
1015
|
+
setConfig(config) {
|
|
1016
|
+
this.config = config;
|
|
1017
|
+
this.dirty = true;
|
|
1018
|
+
}
|
|
1019
|
+
/** Resize to a new print area. */
|
|
1020
|
+
setArea(width, height) {
|
|
1021
|
+
this.set({ width, height });
|
|
1022
|
+
this.setCoords();
|
|
1023
|
+
}
|
|
1024
|
+
/** Drop the cached source snapshot (e.g. the source was edited). */
|
|
1025
|
+
invalidate() {
|
|
1026
|
+
this.snapshotEl = null;
|
|
1027
|
+
this.snapshotScale = 0;
|
|
1028
|
+
this.dirty = true;
|
|
1029
|
+
}
|
|
1030
|
+
/** Free the offscreen snapshot. */
|
|
1031
|
+
dispose() {
|
|
1032
|
+
this.snapshotEl = null;
|
|
1033
|
+
this.snapshotScale = 0;
|
|
1034
|
+
}
|
|
1035
|
+
_render(ctx) {
|
|
1036
|
+
const width = this.width ?? 0;
|
|
1037
|
+
const height = this.height ?? 0;
|
|
1038
|
+
if (width <= 0 || height <= 0) return;
|
|
1039
|
+
const tileScale = Math.max(1, this.config.scale ?? 100) / 100;
|
|
1040
|
+
const snapshot = this.ensureSnapshot(contextScale(ctx) * tileScale);
|
|
1041
|
+
if (!snapshot) return;
|
|
1042
|
+
const { tileW, tileH } = this.tileSize(snapshot, tileScale, width, height);
|
|
1043
|
+
ctx.save();
|
|
1044
|
+
ctx.translate(-width / 2, -height / 2);
|
|
1045
|
+
drawTiles(ctx, snapshot, this.config, width, height, tileW, tileH, this.gridOrigin());
|
|
1046
|
+
ctx.restore();
|
|
1047
|
+
}
|
|
1048
|
+
/** Raster fallback for SVG export — one `<image>` covering the print area. */
|
|
1049
|
+
_toSVG() {
|
|
1050
|
+
const width = this.width ?? 0;
|
|
1051
|
+
const height = this.height ?? 0;
|
|
1052
|
+
if (width <= 0 || height <= 0) return [];
|
|
1053
|
+
const el = document.createElement("canvas");
|
|
1054
|
+
el.width = Math.max(1, Math.round(width));
|
|
1055
|
+
el.height = Math.max(1, Math.round(height));
|
|
1056
|
+
const ctx = el.getContext("2d");
|
|
1057
|
+
if (!ctx) return [];
|
|
1058
|
+
const tileScale = Math.max(1, this.config.scale ?? 100) / 100;
|
|
1059
|
+
const snapshot = this.ensureSnapshot(tileScale);
|
|
1060
|
+
if (!snapshot) return [];
|
|
1061
|
+
const { tileW, tileH } = this.tileSize(snapshot, tileScale, width, height);
|
|
1062
|
+
drawTiles(ctx, snapshot, this.config, width, height, tileW, tileH, this.gridOrigin());
|
|
1063
|
+
return [
|
|
1064
|
+
`<image x="${-width / 2}" y="${-height / 2}" width="${width}" height="${height}" `,
|
|
1065
|
+
`xlink:href="${el.toDataURL("image/png")}"></image>
|
|
1066
|
+
`
|
|
1067
|
+
];
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Fabric's generic clone round-trips through `toObject()` + the class
|
|
1071
|
+
* registry, which cannot carry a live source reference. Export paths clone
|
|
1072
|
+
* every canvas object, so without this a print export would silently lose the
|
|
1073
|
+
* tiling. The copy shares the source (it only ever reads from it).
|
|
1074
|
+
*/
|
|
1075
|
+
clone() {
|
|
1076
|
+
const copy = new _TiledPatternObject(
|
|
1077
|
+
this.source,
|
|
1078
|
+
this.config,
|
|
1079
|
+
this.width ?? 0,
|
|
1080
|
+
this.height ?? 0
|
|
1081
|
+
);
|
|
1082
|
+
copy.set({
|
|
1083
|
+
left: this.left,
|
|
1084
|
+
top: this.top,
|
|
1085
|
+
visible: this.visible,
|
|
1086
|
+
opacity: this.opacity
|
|
1087
|
+
});
|
|
1088
|
+
return Promise.resolve(copy);
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* The proxy holds a live reference to its source, which would make a
|
|
1092
|
+
* serialized canvas circular. Nothing persists this object (only layers are
|
|
1093
|
+
* serialized) — this keeps an accidental `canvas.toObject()` from throwing.
|
|
1094
|
+
*/
|
|
1095
|
+
toObject() {
|
|
1096
|
+
const plain = super.toObject();
|
|
1097
|
+
delete plain.source;
|
|
1098
|
+
return plain;
|
|
1099
|
+
}
|
|
1100
|
+
/** Tile size in canvas units, floored so a tiny tile can't spawn a huge loop. */
|
|
1101
|
+
tileSize(snapshot, tileScale, width, height) {
|
|
1102
|
+
const floor = Math.max(2, Math.sqrt(width * width + height * height) / 200);
|
|
1103
|
+
return {
|
|
1104
|
+
tileW: Math.max(floor, snapshot.width / this.snapshotScale * tileScale),
|
|
1105
|
+
tileH: Math.max(floor, snapshot.height / this.snapshotScale * tileScale)
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
/** Grid anchor: the source's centre, in this object's coordinates. */
|
|
1109
|
+
gridOrigin() {
|
|
1110
|
+
const centre = this.source.getCenterPoint();
|
|
1111
|
+
return { x: centre.x - (this.left ?? 0), y: centre.y - (this.top ?? 0) };
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Snapshot the source at (at least) `scale`, reusing the cached one when it is
|
|
1115
|
+
* still sharp enough and the source has not changed.
|
|
1116
|
+
*/
|
|
1117
|
+
ensureSnapshot(scale) {
|
|
1118
|
+
const wanted = this.clampScale(scale);
|
|
1119
|
+
const stale = this.source.dirty || !this.snapshotEl || this.snapshotScale < wanted || this.snapshotScale > wanted * SNAPSHOT_SHRINK_FACTOR;
|
|
1120
|
+
if (!stale) return this.snapshotEl;
|
|
1121
|
+
const source = this.source;
|
|
1122
|
+
const opacity = source.opacity;
|
|
1123
|
+
source.opacity = 1;
|
|
1124
|
+
try {
|
|
1125
|
+
const el = source.toCanvasElement({ multiplier: wanted, enableRetinaScaling: false });
|
|
1126
|
+
if (!el.width || !el.height) return null;
|
|
1127
|
+
this.snapshotEl = el;
|
|
1128
|
+
this.snapshotScale = wanted;
|
|
1129
|
+
} catch {
|
|
1130
|
+
return this.snapshotEl;
|
|
1131
|
+
} finally {
|
|
1132
|
+
source.opacity = opacity;
|
|
1133
|
+
source.dirty = false;
|
|
1134
|
+
}
|
|
1135
|
+
return this.snapshotEl;
|
|
1136
|
+
}
|
|
1137
|
+
/** Bound the snapshot by both a linear scale and a total pixel budget. */
|
|
1138
|
+
clampScale(scale) {
|
|
1139
|
+
const requested = Math.min(MAX_SNAPSHOT_SCALE, Math.max(0.05, scale));
|
|
1140
|
+
const rect = this.source.getBoundingRect();
|
|
1141
|
+
const area = Math.max(1, rect.width * rect.height);
|
|
1142
|
+
const budgeted = Math.sqrt(MAX_SNAPSHOT_PIXELS / area);
|
|
1143
|
+
return Math.max(0.05, Math.min(requested, budgeted));
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
function contextScale(ctx) {
|
|
1147
|
+
if (typeof ctx.getTransform !== "function") return 1;
|
|
1148
|
+
try {
|
|
1149
|
+
const t = ctx.getTransform();
|
|
1150
|
+
return Math.max(Math.hypot(t.a, t.b), Math.hypot(t.c, t.d), 0.05);
|
|
1151
|
+
} catch {
|
|
1152
|
+
return 1;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// src/pattern/pattern-manager.ts
|
|
881
1157
|
var PatternManager = class {
|
|
882
|
-
constructor(canvas, layers, history, events
|
|
1158
|
+
constructor(canvas, layers, history, events) {
|
|
883
1159
|
this.canvas = canvas;
|
|
884
1160
|
this.layers = layers;
|
|
885
1161
|
this.history = history;
|
|
886
1162
|
this.events = events;
|
|
887
|
-
this.
|
|
1163
|
+
this.canvas.on("object:modified", this.onSourceModified);
|
|
1164
|
+
this.canvas.on("text:changed", this.onSourceModified);
|
|
1165
|
+
this.events.on("layer:removed", this.onLayerRemoved);
|
|
888
1166
|
}
|
|
889
1167
|
canvas;
|
|
890
1168
|
layers;
|
|
891
1169
|
history;
|
|
892
1170
|
events;
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1171
|
+
proxies = /* @__PURE__ */ new Map();
|
|
1172
|
+
onSourceModified = (event) => {
|
|
1173
|
+
this.invalidateFor(event.target);
|
|
1174
|
+
};
|
|
1175
|
+
onLayerRemoved = ({ layerId }) => {
|
|
1176
|
+
const proxy = this.proxies.get(layerId);
|
|
1177
|
+
if (!proxy) return;
|
|
1178
|
+
proxy.dispose();
|
|
1179
|
+
this.proxies.delete(layerId);
|
|
1180
|
+
};
|
|
899
1181
|
isPattern(layerId) {
|
|
900
1182
|
return !!this.layers.get(layerId)?.meta.pattern;
|
|
901
1183
|
}
|
|
902
1184
|
getConfig(layerId) {
|
|
903
1185
|
return this.layers.get(layerId)?.meta.pattern?.config ?? null;
|
|
904
1186
|
}
|
|
905
|
-
/** Turn a
|
|
906
|
-
apply(layerId, config) {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const
|
|
912
|
-
if (
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
config,
|
|
916
|
-
originalSrc: elementToDataURL(image) ?? image.getSrc(),
|
|
917
|
-
originalClip: clip ? clip.toObject() : null,
|
|
918
|
-
originalLocks: captureLocks(image),
|
|
919
|
-
original: {
|
|
920
|
-
left: image.left ?? 0,
|
|
921
|
-
top: image.top ?? 0,
|
|
922
|
-
scaleX: image.scaleX ?? 1,
|
|
923
|
-
scaleY: image.scaleY ?? 1,
|
|
924
|
-
width: image.width ?? 0,
|
|
925
|
-
height: image.height ?? 0,
|
|
926
|
-
angle: image.angle ?? 0,
|
|
927
|
-
cropX: image.cropX ?? 0,
|
|
928
|
-
cropY: image.cropY ?? 0
|
|
929
|
-
}
|
|
930
|
-
};
|
|
1187
|
+
/** Turn a layer into a repeating pattern, or update an existing one. */
|
|
1188
|
+
async apply(layerId, config) {
|
|
1189
|
+
const layer = this.layers.get(layerId);
|
|
1190
|
+
if (!layer) return;
|
|
1191
|
+
try {
|
|
1192
|
+
this.layers.setMeta(layerId, { pattern: { config } });
|
|
1193
|
+
const proxy = this.proxies.get(layerId);
|
|
1194
|
+
if (proxy) {
|
|
1195
|
+
proxy.setConfig(config);
|
|
1196
|
+
this.canvas.requestRenderAll();
|
|
931
1197
|
} else {
|
|
932
|
-
layer
|
|
933
|
-
}
|
|
934
|
-
try {
|
|
935
|
-
await this.renderLayer(layer);
|
|
936
|
-
} catch (err) {
|
|
937
|
-
if (firstEnable) delete layer.meta.pattern;
|
|
938
|
-
throw err;
|
|
1198
|
+
this.attach(layer, config);
|
|
939
1199
|
}
|
|
940
1200
|
this.history.save();
|
|
941
|
-
}
|
|
942
|
-
this.events.emit("error", { message: "Failed to apply
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
this.events.emit("error", { message: "Failed to apply pattern", error });
|
|
943
1203
|
throw error;
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* Stretch every restored pattern layer back over the full print area and
|
|
948
|
-
* re-freeze it, without re-rasterising: the baked bitmap keeps whatever size
|
|
949
|
-
* it was saved at, so it is scaled (not re-tiled) to the current canvas. Used
|
|
950
|
-
* after a state restore, where the canvas may be a different display size
|
|
951
|
-
* than when the pattern was baked — and to repair states saved while a
|
|
952
|
-
* pattern could still be dragged out of the print area.
|
|
953
|
-
*/
|
|
954
|
-
repinAll() {
|
|
955
|
-
const cw = this.canvas.getWidth();
|
|
956
|
-
const ch = this.canvas.getHeight();
|
|
957
|
-
let changed = false;
|
|
958
|
-
for (const layer of this.layers.getAll()) {
|
|
959
|
-
if (!layer.meta.pattern || layer.type !== "image") continue;
|
|
960
|
-
const image = layer.fabricObject;
|
|
961
|
-
image.set({
|
|
962
|
-
left: 0,
|
|
963
|
-
top: 0,
|
|
964
|
-
angle: 0,
|
|
965
|
-
scaleX: cw / (image.width || cw),
|
|
966
|
-
scaleY: ch / (image.height || ch)
|
|
967
|
-
});
|
|
968
|
-
applyPatternLocks(image);
|
|
969
|
-
image.setCoords();
|
|
970
|
-
changed = true;
|
|
971
1204
|
}
|
|
972
|
-
if (changed) this.canvas.requestRenderAll();
|
|
973
1205
|
}
|
|
974
|
-
/**
|
|
975
|
-
disable(layerId) {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
left: state.original.left,
|
|
984
|
-
top: state.original.top,
|
|
985
|
-
scaleX: state.original.scaleX,
|
|
986
|
-
scaleY: state.original.scaleY,
|
|
987
|
-
width: state.original.width,
|
|
988
|
-
height: state.original.height,
|
|
989
|
-
cropX: state.original.cropX,
|
|
990
|
-
cropY: state.original.cropY,
|
|
991
|
-
angle: state.original.angle
|
|
992
|
-
});
|
|
993
|
-
image.clipPath = state.originalClip ? (await import_fabric2.util.enlivenObjects([state.originalClip]))[0] : void 0;
|
|
994
|
-
restoreLocks(image, state.originalLocks);
|
|
995
|
-
image.setCoords();
|
|
996
|
-
delete layer.meta.pattern;
|
|
1206
|
+
/** Drop the tiling and show the source again. */
|
|
1207
|
+
async disable(layerId) {
|
|
1208
|
+
const layer = this.layers.get(layerId);
|
|
1209
|
+
if (!layer?.meta.pattern) return;
|
|
1210
|
+
try {
|
|
1211
|
+
this.detach(layerId);
|
|
1212
|
+
restoreLocks(layer.fabricObject, layer.meta.pattern.originalLocks);
|
|
1213
|
+
layer.fabricObject.set({ opacity: layer.opacity });
|
|
1214
|
+
this.layers.setMeta(layerId, { pattern: void 0 });
|
|
997
1215
|
this.canvas.requestRenderAll();
|
|
998
1216
|
this.history.save();
|
|
999
|
-
}
|
|
1000
|
-
this.events.emit("error", { message: "Failed to clear
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
this.events.emit("error", { message: "Failed to clear pattern", error });
|
|
1001
1219
|
throw error;
|
|
1002
|
-
}
|
|
1220
|
+
}
|
|
1003
1221
|
}
|
|
1004
|
-
/**
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1222
|
+
/**
|
|
1223
|
+
* Rebuild every proxy after a state restore, migrating any layer that was
|
|
1224
|
+
* saved by the old bake-into-the-layer engine.
|
|
1225
|
+
*/
|
|
1226
|
+
async rehydrateAll() {
|
|
1227
|
+
this.clearProxies();
|
|
1228
|
+
for (const layer of this.layers.getAll()) {
|
|
1229
|
+
const state = layer.meta.pattern;
|
|
1230
|
+
if (!state) continue;
|
|
1231
|
+
try {
|
|
1232
|
+
if (isLegacyState(state)) {
|
|
1233
|
+
await unbakeLegacyLayer(layer, state);
|
|
1234
|
+
this.layers.setMeta(layer.id, { pattern: { config: state.config } });
|
|
1235
|
+
}
|
|
1236
|
+
this.attach(layer, state.config);
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
this.events.emit("error", { message: "Failed to restore pattern layer", error });
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
this.canvas.requestRenderAll();
|
|
1013
1242
|
}
|
|
1014
|
-
|
|
1243
|
+
/** Re-fit every proxy to the print area (canvas resize). */
|
|
1244
|
+
syncArea() {
|
|
1245
|
+
const width = this.canvas.getWidth();
|
|
1246
|
+
const height = this.canvas.getHeight();
|
|
1247
|
+
for (const proxy of this.proxies.values()) {
|
|
1248
|
+
proxy.setArea(width, height);
|
|
1249
|
+
proxy.invalidate();
|
|
1250
|
+
}
|
|
1251
|
+
if (this.proxies.size > 0) this.canvas.requestRenderAll();
|
|
1252
|
+
}
|
|
1253
|
+
/** Drop a layer's cached source snapshot (its content changed). */
|
|
1254
|
+
invalidate(layerId) {
|
|
1255
|
+
const proxy = this.proxies.get(layerId);
|
|
1256
|
+
if (!proxy) return;
|
|
1257
|
+
proxy.invalidate();
|
|
1258
|
+
this.canvas.requestRenderAll();
|
|
1259
|
+
}
|
|
1260
|
+
/** Give a freshly cloned layer its own proxy (duplicating a pattern layer). */
|
|
1261
|
+
attachTo(layer) {
|
|
1015
1262
|
const state = layer.meta.pattern;
|
|
1016
|
-
if (!state) return;
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1263
|
+
if (!state || this.proxies.has(layer.id)) return;
|
|
1264
|
+
this.attach(layer, state.config);
|
|
1265
|
+
}
|
|
1266
|
+
dispose() {
|
|
1267
|
+
this.canvas.off("object:modified", this.onSourceModified);
|
|
1268
|
+
this.canvas.off("text:changed", this.onSourceModified);
|
|
1269
|
+
this.events.off("layer:removed", this.onLayerRemoved);
|
|
1270
|
+
this.clearProxies();
|
|
1271
|
+
}
|
|
1272
|
+
/** Release every proxy and the offscreen snapshot it holds. */
|
|
1273
|
+
clearProxies() {
|
|
1274
|
+
for (const proxy of this.proxies.values()) proxy.dispose();
|
|
1275
|
+
this.proxies.clear();
|
|
1276
|
+
}
|
|
1277
|
+
attach(layer, config) {
|
|
1278
|
+
const proxy = new TiledPatternObject(
|
|
1279
|
+
layer.fabricObject,
|
|
1280
|
+
config,
|
|
1281
|
+
this.canvas.getWidth(),
|
|
1282
|
+
this.canvas.getHeight()
|
|
1033
1283
|
);
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
top: 0,
|
|
1038
|
-
scaleX: 1,
|
|
1039
|
-
scaleY: 1,
|
|
1040
|
-
width: cw,
|
|
1041
|
-
height: ch,
|
|
1042
|
-
cropX: 0,
|
|
1043
|
-
cropY: 0,
|
|
1044
|
-
angle: 0
|
|
1045
|
-
});
|
|
1046
|
-
image.clipPath = void 0;
|
|
1047
|
-
applyPatternLocks(image);
|
|
1048
|
-
image.setCoords();
|
|
1284
|
+
layer.fabricObject.set({ opacity: 0 });
|
|
1285
|
+
this.proxies.set(layer.id, proxy);
|
|
1286
|
+
this.layers.setRenderProxy(layer.id, proxy);
|
|
1049
1287
|
this.canvas.requestRenderAll();
|
|
1050
1288
|
}
|
|
1289
|
+
detach(layerId) {
|
|
1290
|
+
const proxy = this.proxies.get(layerId);
|
|
1291
|
+
if (!proxy) {
|
|
1292
|
+
this.layers.setRenderProxy(layerId, null);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
this.layers.setRenderProxy(layerId, null);
|
|
1296
|
+
proxy.dispose();
|
|
1297
|
+
this.proxies.delete(layerId);
|
|
1298
|
+
}
|
|
1299
|
+
invalidateFor(target) {
|
|
1300
|
+
if (!target) return;
|
|
1301
|
+
const layer = this.layers.findByObject(target);
|
|
1302
|
+
if (layer) this.invalidate(layer.id);
|
|
1303
|
+
}
|
|
1051
1304
|
};
|
|
1052
|
-
function
|
|
1053
|
-
return
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
hasControls: false
|
|
1305
|
+
function isLegacyState(state) {
|
|
1306
|
+
return typeof state.originalSrc === "string" && state.originalSrc.length > 0;
|
|
1307
|
+
}
|
|
1308
|
+
async function unbakeLegacyLayer(layer, state) {
|
|
1309
|
+
const image = layer.fabricObject;
|
|
1310
|
+
if (typeof image.setSrc !== "function" || !state.original) return;
|
|
1311
|
+
await image.setSrc(state.originalSrc);
|
|
1312
|
+
image.set({
|
|
1313
|
+
left: state.original.left,
|
|
1314
|
+
top: state.original.top,
|
|
1315
|
+
scaleX: state.original.scaleX,
|
|
1316
|
+
scaleY: state.original.scaleY,
|
|
1317
|
+
width: state.original.width,
|
|
1318
|
+
height: state.original.height,
|
|
1319
|
+
cropX: state.original.cropX,
|
|
1320
|
+
cropY: state.original.cropY,
|
|
1321
|
+
angle: state.original.angle
|
|
1070
1322
|
});
|
|
1323
|
+
image.clipPath = state.originalClip ? (await import_fabric3.util.enlivenObjects([state.originalClip]))[0] : void 0;
|
|
1324
|
+
restoreLocks(image, state.originalLocks);
|
|
1325
|
+
image.setCoords();
|
|
1071
1326
|
}
|
|
1072
1327
|
function restoreLocks(obj, locks) {
|
|
1073
1328
|
obj.set(
|
|
@@ -1081,124 +1336,9 @@ function restoreLocks(obj, locks) {
|
|
|
1081
1336
|
}
|
|
1082
1337
|
);
|
|
1083
1338
|
}
|
|
1084
|
-
function elementToDataURL(image) {
|
|
1085
|
-
try {
|
|
1086
|
-
const el = image.getElement();
|
|
1087
|
-
const w = el.naturalWidth || el.width;
|
|
1088
|
-
const h = el.naturalHeight || el.height;
|
|
1089
|
-
if (!w || !h) return null;
|
|
1090
|
-
const off = document.createElement("canvas");
|
|
1091
|
-
off.width = w;
|
|
1092
|
-
off.height = h;
|
|
1093
|
-
const ctx = off.getContext("2d");
|
|
1094
|
-
if (!ctx) return null;
|
|
1095
|
-
ctx.drawImage(el, 0, 0);
|
|
1096
|
-
return off.toDataURL("image/png");
|
|
1097
|
-
} catch {
|
|
1098
|
-
return null;
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
var IMAGE_CACHE_MAX = 16;
|
|
1102
|
-
var imageCache = /* @__PURE__ */ new Map();
|
|
1103
|
-
function loadPatternImage(src, resolver) {
|
|
1104
|
-
const cached = imageCache.get(src);
|
|
1105
|
-
if (cached) {
|
|
1106
|
-
imageCache.delete(src);
|
|
1107
|
-
imageCache.set(src, cached);
|
|
1108
|
-
return cached;
|
|
1109
|
-
}
|
|
1110
|
-
const promise = decodeImage(src).catch(async (originalError) => {
|
|
1111
|
-
if (!resolver) throw originalError;
|
|
1112
|
-
const resolved = await resolver(src);
|
|
1113
|
-
if (!resolved || resolved === src) {
|
|
1114
|
-
throw new Error("Pattern source resolver did not return a usable alternate URL", {
|
|
1115
|
-
cause: originalError
|
|
1116
|
-
});
|
|
1117
|
-
}
|
|
1118
|
-
return decodeImage(resolved);
|
|
1119
|
-
});
|
|
1120
|
-
promise.catch(() => {
|
|
1121
|
-
if (imageCache.get(src) === promise) imageCache.delete(src);
|
|
1122
|
-
});
|
|
1123
|
-
imageCache.set(src, promise);
|
|
1124
|
-
if (imageCache.size > IMAGE_CACHE_MAX) {
|
|
1125
|
-
const oldest = imageCache.keys().next().value;
|
|
1126
|
-
if (oldest !== void 0) imageCache.delete(oldest);
|
|
1127
|
-
}
|
|
1128
|
-
return promise;
|
|
1129
|
-
}
|
|
1130
|
-
function decodeImage(src) {
|
|
1131
|
-
return new Promise((resolve, reject) => {
|
|
1132
|
-
const img = new Image();
|
|
1133
|
-
img.crossOrigin = "anonymous";
|
|
1134
|
-
img.onload = () => resolve(img);
|
|
1135
|
-
img.onerror = () => reject(new Error(`Failed to load pattern source: ${src}`));
|
|
1136
|
-
img.src = src;
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
function clearPatternImageCache() {
|
|
1140
|
-
imageCache.clear();
|
|
1141
|
-
}
|
|
1142
|
-
async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH, sourceResolver) {
|
|
1143
|
-
const img = await loadPatternImage(src, sourceResolver);
|
|
1144
|
-
const off = document.createElement("canvas");
|
|
1145
|
-
off.width = Math.max(1, Math.round(targetW));
|
|
1146
|
-
off.height = Math.max(1, Math.round(targetH));
|
|
1147
|
-
const ctx = off.getContext("2d");
|
|
1148
|
-
if (!ctx) return off.toDataURL("image/png");
|
|
1149
|
-
drawTiles(ctx, img, config, targetW, targetH, baseW, baseH);
|
|
1150
|
-
return off.toDataURL("image/png");
|
|
1151
|
-
}
|
|
1152
|
-
function computeTilePositions(config, targetW, targetH, baseW, baseH) {
|
|
1153
|
-
const diag = Math.sqrt(targetW * targetW + targetH * targetH);
|
|
1154
|
-
const minTile = Math.max(1, diag / MAX_TILES_PER_AXIS);
|
|
1155
|
-
const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
|
|
1156
|
-
const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
|
|
1157
|
-
const cols = Math.ceil(diag / tileW) + 2;
|
|
1158
|
-
const rows = Math.ceil(diag / tileH) + 2;
|
|
1159
|
-
const halfCols = Math.ceil(cols / 2);
|
|
1160
|
-
const halfRows = Math.ceil(rows / 2);
|
|
1161
|
-
const shiftX = tileW * (clampOffset(config.offsetX) / 100);
|
|
1162
|
-
const shiftY = tileH * (clampOffset(config.offsetY) / 100);
|
|
1163
|
-
const placements = [];
|
|
1164
|
-
for (let j = -halfRows; j <= halfRows; j++) {
|
|
1165
|
-
for (let i = -halfCols; i <= halfCols; i++) {
|
|
1166
|
-
let x = i * tileW + shiftX;
|
|
1167
|
-
let y = j * tileH + shiftY;
|
|
1168
|
-
if (config.mode === "brick-horizontal" && mod2(j) === 1) {
|
|
1169
|
-
x += tileW * (config.horizontalOffset / 100);
|
|
1170
|
-
} else if (config.mode === "brick-vertical" && mod2(i) === 1) {
|
|
1171
|
-
y += tileH * (config.horizontalOffset / 100);
|
|
1172
|
-
}
|
|
1173
|
-
const rotation = (i * config.rotationStepH + j * config.rotationStepV) * Math.PI / 180;
|
|
1174
|
-
placements.push({ x, y, rotation });
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
return placements;
|
|
1178
|
-
}
|
|
1179
|
-
function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH) {
|
|
1180
|
-
ctx.save();
|
|
1181
|
-
ctx.translate(targetW / 2, targetH / 2);
|
|
1182
|
-
ctx.rotate(config.angle * Math.PI / 180);
|
|
1183
|
-
for (const tile of computeTilePositions(config, targetW, targetH, baseW, baseH)) {
|
|
1184
|
-
ctx.save();
|
|
1185
|
-
ctx.translate(tile.x, tile.y);
|
|
1186
|
-
ctx.rotate(tile.rotation);
|
|
1187
|
-
ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
|
|
1188
|
-
ctx.restore();
|
|
1189
|
-
}
|
|
1190
|
-
ctx.restore();
|
|
1191
|
-
}
|
|
1192
|
-
function clampOffset(value) {
|
|
1193
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
1194
|
-
return Math.max(-100, Math.min(100, value));
|
|
1195
|
-
}
|
|
1196
|
-
function mod2(n) {
|
|
1197
|
-
return (n % 2 + 2) % 2;
|
|
1198
|
-
}
|
|
1199
1339
|
|
|
1200
1340
|
// src/text-curve.ts
|
|
1201
|
-
var
|
|
1341
|
+
var import_fabric4 = require("fabric");
|
|
1202
1342
|
var DEFAULT_TEXT_CURVE = { arc: 0, wave: 0 };
|
|
1203
1343
|
var MIN_ARC = 0.5;
|
|
1204
1344
|
var FULL_CIRCLE_ARC = 99.5;
|
|
@@ -1321,7 +1461,7 @@ var TextCurveManager = class {
|
|
|
1321
1461
|
if (layer.meta.curveWidth === void 0) layer.meta.curveWidth = text.width ?? 0;
|
|
1322
1462
|
text.set({ width: Math.max(layer.meta.curveWidth, run + 2) });
|
|
1323
1463
|
text.set({
|
|
1324
|
-
path: new
|
|
1464
|
+
path: new import_fabric4.Path(curve.data, { visible: false, objectCaching: false }),
|
|
1325
1465
|
pathAlign: "center",
|
|
1326
1466
|
pathSide: "left",
|
|
1327
1467
|
pathStartOffset: Math.max(0, (curve.length - run) / 2)
|
|
@@ -1363,7 +1503,7 @@ var TextCurveManager = class {
|
|
|
1363
1503
|
};
|
|
1364
1504
|
|
|
1365
1505
|
// src/mask-presets/manager.ts
|
|
1366
|
-
var
|
|
1506
|
+
var import_fabric5 = require("fabric");
|
|
1367
1507
|
|
|
1368
1508
|
// src/mask-presets/shapes.ts
|
|
1369
1509
|
var SHAPE_MASK_IDS = [
|
|
@@ -1630,13 +1770,13 @@ var MaskPresetManager = class {
|
|
|
1630
1770
|
objectCaching: false
|
|
1631
1771
|
};
|
|
1632
1772
|
if (isShapeMaskId(id)) {
|
|
1633
|
-
return new
|
|
1773
|
+
return new import_fabric5.Path(shapeMaskPathData(id), {
|
|
1634
1774
|
...shared,
|
|
1635
1775
|
scaleX: width / SHAPE_MASK_BOX,
|
|
1636
1776
|
scaleY: height / SHAPE_MASK_BOX
|
|
1637
1777
|
});
|
|
1638
1778
|
}
|
|
1639
|
-
return new
|
|
1779
|
+
return new import_fabric5.FabricImage(renderTextureMask(id), {
|
|
1640
1780
|
...shared,
|
|
1641
1781
|
scaleX: width / TEXTURE_MASK_SIZE,
|
|
1642
1782
|
scaleY: height / TEXTURE_MASK_SIZE
|
|
@@ -1645,7 +1785,7 @@ var MaskPresetManager = class {
|
|
|
1645
1785
|
};
|
|
1646
1786
|
|
|
1647
1787
|
// src/shadow.ts
|
|
1648
|
-
var
|
|
1788
|
+
var import_fabric6 = require("fabric");
|
|
1649
1789
|
|
|
1650
1790
|
// src/utils/color.ts
|
|
1651
1791
|
var CSS_COLOR = /^(?:#[\da-f]{3,8}|(?:rgb|hsl)a?\([\d.%+\-\s,/]+\)|[a-z]+)$/i;
|
|
@@ -1683,7 +1823,7 @@ function applyLayerShadow(object, config) {
|
|
|
1683
1823
|
}
|
|
1684
1824
|
const color = isCssColor(next.color) ? next.color : DEFAULT_LAYER_SHADOW.color;
|
|
1685
1825
|
object.set({
|
|
1686
|
-
shadow: new
|
|
1826
|
+
shadow: new import_fabric6.Shadow({
|
|
1687
1827
|
color,
|
|
1688
1828
|
blur: Math.max(0, next.blur),
|
|
1689
1829
|
offsetX: next.offsetX,
|
|
@@ -1797,7 +1937,7 @@ var UnitConverter = class {
|
|
|
1797
1937
|
};
|
|
1798
1938
|
|
|
1799
1939
|
// src/serialization.ts
|
|
1800
|
-
var
|
|
1940
|
+
var import_fabric7 = require("fabric");
|
|
1801
1941
|
var VERSION = "2.0.0";
|
|
1802
1942
|
function serializeEditor(editor) {
|
|
1803
1943
|
return {
|
|
@@ -1833,7 +1973,7 @@ async function deserializeEditor(editor, state) {
|
|
|
1833
1973
|
}
|
|
1834
1974
|
const staged = await Promise.all(
|
|
1835
1975
|
state.layers.map(async (serialized) => {
|
|
1836
|
-
const fabricObject = (await
|
|
1976
|
+
const fabricObject = (await import_fabric7.util.enlivenObjects([serialized.fabricObject]))[0];
|
|
1837
1977
|
if (!fabricObject) {
|
|
1838
1978
|
const source = serialized.fabricObject.src;
|
|
1839
1979
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -1844,7 +1984,7 @@ async function deserializeEditor(editor, state) {
|
|
|
1844
1984
|
return { serialized, fabricObject };
|
|
1845
1985
|
})
|
|
1846
1986
|
);
|
|
1847
|
-
const stagedBackground = state.backgroundImage ? (await
|
|
1987
|
+
const stagedBackground = state.backgroundImage ? (await import_fabric7.util.enlivenObjects([state.backgroundImage]))[0] : null;
|
|
1848
1988
|
if (state.backgroundImage && !stagedBackground) {
|
|
1849
1989
|
const source = state.backgroundImage.src;
|
|
1850
1990
|
if (typeof source === "string" && source.startsWith("blob:")) {
|
|
@@ -1891,7 +2031,7 @@ function restoreLayer(editor, serialized, fabricObject) {
|
|
|
1891
2031
|
}
|
|
1892
2032
|
|
|
1893
2033
|
// src/export.ts
|
|
1894
|
-
var
|
|
2034
|
+
var import_fabric8 = require("fabric");
|
|
1895
2035
|
|
|
1896
2036
|
// src/displacement.ts
|
|
1897
2037
|
var CHANNEL_INDEX = {
|
|
@@ -1985,7 +2125,7 @@ async function exportPNG(canvas, options = {}) {
|
|
|
1985
2125
|
}
|
|
1986
2126
|
async function exportIsolatedPNG(source, objects, options = {}) {
|
|
1987
2127
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
1988
|
-
const canvas = new
|
|
2128
|
+
const canvas = new import_fabric8.StaticCanvas(element, {
|
|
1989
2129
|
width: options.width ?? source.getWidth(),
|
|
1990
2130
|
height: options.height ?? source.getHeight(),
|
|
1991
2131
|
backgroundColor: options.backgroundColor || void 0
|
|
@@ -2015,7 +2155,7 @@ async function exportPrintArea(source, area, options = {}) {
|
|
|
2015
2155
|
throw new Error("Print area does not overlap the canvas");
|
|
2016
2156
|
}
|
|
2017
2157
|
const element = source.lowerCanvasEl.ownerDocument.createElement("canvas");
|
|
2018
|
-
const canvas = new
|
|
2158
|
+
const canvas = new import_fabric8.StaticCanvas(element, { width, height });
|
|
2019
2159
|
try {
|
|
2020
2160
|
const clones = await Promise.all(source.getObjects().map((object) => object.clone()));
|
|
2021
2161
|
if (clones.length) canvas.add(...clones);
|
|
@@ -2455,7 +2595,7 @@ var ProjectManager = class {
|
|
|
2455
2595
|
};
|
|
2456
2596
|
|
|
2457
2597
|
// src/mask.ts
|
|
2458
|
-
var
|
|
2598
|
+
var import_fabric9 = require("fabric");
|
|
2459
2599
|
var MaskRefinementError = class extends Error {
|
|
2460
2600
|
constructor(code, message, cause) {
|
|
2461
2601
|
super(message);
|
|
@@ -2487,7 +2627,7 @@ var MaskController = class {
|
|
|
2487
2627
|
throw new Error("Mask dimensions must be positive integers");
|
|
2488
2628
|
}
|
|
2489
2629
|
const backing = this.makeCanvas(width, height);
|
|
2490
|
-
const image = new
|
|
2630
|
+
const image = new import_fabric9.FabricImage(backing, {
|
|
2491
2631
|
left: 0,
|
|
2492
2632
|
top: 0,
|
|
2493
2633
|
originX: "left",
|
|
@@ -2753,7 +2893,7 @@ var CanvasEditor = class {
|
|
|
2753
2893
|
const widthPx = this.units.toPixels(config.width);
|
|
2754
2894
|
const heightPx = this.units.toPixels(config.height);
|
|
2755
2895
|
this.designBackground = config.backgroundColor ?? "#ffffff";
|
|
2756
|
-
this.canvas = new
|
|
2896
|
+
this.canvas = new import_fabric10.Canvas(canvasElement, {
|
|
2757
2897
|
width: widthPx,
|
|
2758
2898
|
height: heightPx,
|
|
2759
2899
|
backgroundColor: this.designBackground,
|
|
@@ -2774,13 +2914,7 @@ var CanvasEditor = class {
|
|
|
2774
2914
|
this.layers.setHistoryCallback(() => this.history.save());
|
|
2775
2915
|
this.snapping = new SnapManager(this.canvas, this.events);
|
|
2776
2916
|
this.crop = new CropController(this.canvas, this.history, this.events);
|
|
2777
|
-
this.patterns = new PatternManager(
|
|
2778
|
-
this.canvas,
|
|
2779
|
-
this.layers,
|
|
2780
|
-
this.history,
|
|
2781
|
-
this.events,
|
|
2782
|
-
config.patternSourceResolver
|
|
2783
|
-
);
|
|
2917
|
+
this.patterns = new PatternManager(this.canvas, this.layers, this.history, this.events);
|
|
2784
2918
|
this.curves = new TextCurveManager(this.canvas, this.layers, this.history, this.events);
|
|
2785
2919
|
this.maskPresets = new MaskPresetManager(this.canvas, this.layers, this.history, this.events);
|
|
2786
2920
|
this.setupCanvasEvents();
|
|
@@ -2792,7 +2926,7 @@ var CanvasEditor = class {
|
|
|
2792
2926
|
// ─── Layer Operations ────────────────────────────────
|
|
2793
2927
|
async addImage(url, options) {
|
|
2794
2928
|
try {
|
|
2795
|
-
const img = await
|
|
2929
|
+
const img = await import_fabric10.FabricImage.fromURL(
|
|
2796
2930
|
url,
|
|
2797
2931
|
{},
|
|
2798
2932
|
{ originX: "left", originY: "top", ...options }
|
|
@@ -2817,7 +2951,7 @@ var CanvasEditor = class {
|
|
|
2817
2951
|
if (this.crop.activeLayerId() === layerId) this.crop.cancel();
|
|
2818
2952
|
const previous = layer.fabricObject;
|
|
2819
2953
|
try {
|
|
2820
|
-
const replacement = await
|
|
2954
|
+
const replacement = await import_fabric10.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
|
|
2821
2955
|
replacement.set({
|
|
2822
2956
|
left: previous.left,
|
|
2823
2957
|
top: previous.top,
|
|
@@ -2846,7 +2980,7 @@ var CanvasEditor = class {
|
|
|
2846
2980
|
}
|
|
2847
2981
|
}
|
|
2848
2982
|
addText(text, options) {
|
|
2849
|
-
const textbox = new
|
|
2983
|
+
const textbox = new import_fabric10.Textbox(text, {
|
|
2850
2984
|
fontSize: 32,
|
|
2851
2985
|
fontFamily: "Arial",
|
|
2852
2986
|
fill: "#000000",
|
|
@@ -2885,10 +3019,10 @@ var CanvasEditor = class {
|
|
|
2885
3019
|
return value === void 0 ? token : escapeXml(value);
|
|
2886
3020
|
})
|
|
2887
3021
|
);
|
|
2888
|
-
const { objects, options } = await (0,
|
|
3022
|
+
const { objects, options } = await (0, import_fabric10.loadSVGFromString)(resolved);
|
|
2889
3023
|
const validObjects = objects.filter((object) => object !== null);
|
|
2890
3024
|
if (validObjects.length === 0) throw new Error("Template SVG contains no renderable objects");
|
|
2891
|
-
const group =
|
|
3025
|
+
const group = import_fabric10.util.groupSVGElements(validObjects, options);
|
|
2892
3026
|
group.set({
|
|
2893
3027
|
left: this.canvas.getWidth() / 2,
|
|
2894
3028
|
top: this.canvas.getHeight() / 2,
|
|
@@ -2933,9 +3067,7 @@ var CanvasEditor = class {
|
|
|
2933
3067
|
const layer = this.layers.get(id);
|
|
2934
3068
|
if (!layer) return null;
|
|
2935
3069
|
const clone = await layer.fabricObject.clone();
|
|
2936
|
-
|
|
2937
|
-
clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
|
|
2938
|
-
}
|
|
3070
|
+
clone.set({ left: (clone.left ?? 0) + 15, top: (clone.top ?? 0) + 15 });
|
|
2939
3071
|
clone.setCoords();
|
|
2940
3072
|
const copy = this.layers.add(layer.type, clone, `${layer.name} copy`);
|
|
2941
3073
|
copy.meta = structuredClone(layer.meta);
|
|
@@ -2948,6 +3080,7 @@ var CanvasEditor = class {
|
|
|
2948
3080
|
evented: !layer.locked,
|
|
2949
3081
|
opacity: layer.opacity
|
|
2950
3082
|
});
|
|
3083
|
+
this.patterns.attachTo(copy);
|
|
2951
3084
|
this.canvas.setActiveObject(clone);
|
|
2952
3085
|
this.canvas.requestRenderAll();
|
|
2953
3086
|
this.history.save();
|
|
@@ -2961,10 +3094,10 @@ var CanvasEditor = class {
|
|
|
2961
3094
|
const next = { ...previous, ...adjustments };
|
|
2962
3095
|
const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
|
|
2963
3096
|
image.filters = [
|
|
2964
|
-
new
|
|
2965
|
-
new
|
|
2966
|
-
new
|
|
2967
|
-
new
|
|
3097
|
+
new import_fabric10.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
|
|
3098
|
+
new import_fabric10.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
|
|
3099
|
+
new import_fabric10.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
|
|
3100
|
+
new import_fabric10.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
|
|
2968
3101
|
];
|
|
2969
3102
|
layer.meta.imageAdjustments = next;
|
|
2970
3103
|
image.applyFilters();
|
|
@@ -2981,7 +3114,7 @@ var CanvasEditor = class {
|
|
|
2981
3114
|
const childData = children.map((layer) => structuredClone(layer.toData()));
|
|
2982
3115
|
const objects = children.map((layer) => layer.fabricObject);
|
|
2983
3116
|
for (const layer of children) this.layers.remove(layer.id);
|
|
2984
|
-
const group = new
|
|
3117
|
+
const group = new import_fabric10.Group(objects);
|
|
2985
3118
|
const grouped = this.layers.add("group", group, name);
|
|
2986
3119
|
grouped.meta.groupChildren = childData;
|
|
2987
3120
|
this.layers.select(grouped.id);
|
|
@@ -3000,7 +3133,7 @@ var CanvasEditor = class {
|
|
|
3000
3133
|
const objects = group.removeAll();
|
|
3001
3134
|
this.layers.remove(id);
|
|
3002
3135
|
const restored = objects.map((object, index) => {
|
|
3003
|
-
|
|
3136
|
+
import_fabric10.util.addTransformToObject(object, transform);
|
|
3004
3137
|
object.setCoords();
|
|
3005
3138
|
const data = childData[index];
|
|
3006
3139
|
const layer = this.layers.add(data?.type ?? "group", object, data?.name, data?.id);
|
|
@@ -3027,7 +3160,7 @@ var CanvasEditor = class {
|
|
|
3027
3160
|
}
|
|
3028
3161
|
try {
|
|
3029
3162
|
await deserializeEditor(this, state);
|
|
3030
|
-
this.patterns.
|
|
3163
|
+
await this.patterns.rehydrateAll();
|
|
3031
3164
|
} catch (error) {
|
|
3032
3165
|
if (!managedByHistory) {
|
|
3033
3166
|
this.events.emit("error", { message: "Failed to load editor state", error });
|
|
@@ -3056,7 +3189,7 @@ var CanvasEditor = class {
|
|
|
3056
3189
|
const layer = this.layers.get(id);
|
|
3057
3190
|
if (!layer) throw new Error(`Layer not found: ${id}`);
|
|
3058
3191
|
try {
|
|
3059
|
-
if (options.resolution === "source" && layer.fabricObject instanceof
|
|
3192
|
+
if (options.resolution === "source" && layer.fabricObject instanceof import_fabric10.FabricImage) {
|
|
3060
3193
|
const image = await layer.fabricObject.clone();
|
|
3061
3194
|
image.set({
|
|
3062
3195
|
left: 0,
|
|
@@ -3076,7 +3209,11 @@ var CanvasEditor = class {
|
|
|
3076
3209
|
cloneObjects: false
|
|
3077
3210
|
});
|
|
3078
3211
|
}
|
|
3079
|
-
return await exportIsolatedPNG(
|
|
3212
|
+
return await exportIsolatedPNG(
|
|
3213
|
+
this.canvas,
|
|
3214
|
+
[layer.renderProxy ?? layer.fabricObject],
|
|
3215
|
+
options
|
|
3216
|
+
);
|
|
3080
3217
|
} catch (error) {
|
|
3081
3218
|
this.events.emit("error", { message: `Failed to export layer: ${id}`, error });
|
|
3082
3219
|
throw error;
|
|
@@ -3328,7 +3465,7 @@ var CanvasEditor = class {
|
|
|
3328
3465
|
return;
|
|
3329
3466
|
}
|
|
3330
3467
|
try {
|
|
3331
|
-
const image = await
|
|
3468
|
+
const image = await import_fabric10.FabricImage.fromURL(
|
|
3332
3469
|
url,
|
|
3333
3470
|
{ crossOrigin: options.crossOrigin ?? null, signal: options.signal },
|
|
3334
3471
|
{ originX: "left", originY: "top" }
|
|
@@ -3381,7 +3518,6 @@ var CanvasEditor = class {
|
|
|
3381
3518
|
const sx = widthPx / oldWidth;
|
|
3382
3519
|
const sy = heightPx / oldHeight;
|
|
3383
3520
|
for (const layer of this.layers.getAll()) {
|
|
3384
|
-
if (layer.meta.pattern) continue;
|
|
3385
3521
|
const object = layer.fabricObject;
|
|
3386
3522
|
object.set({
|
|
3387
3523
|
left: (object.left ?? 0) * sx,
|
|
@@ -3402,7 +3538,7 @@ var CanvasEditor = class {
|
|
|
3402
3538
|
}
|
|
3403
3539
|
}
|
|
3404
3540
|
this.canvas.setDimensions({ width: widthPx, height: heightPx });
|
|
3405
|
-
this.patterns.
|
|
3541
|
+
this.patterns.syncArea();
|
|
3406
3542
|
this.canvas.requestRenderAll();
|
|
3407
3543
|
this.history.save();
|
|
3408
3544
|
this.events.emit("canvas:modified", {});
|
|
@@ -3600,7 +3736,7 @@ var CanvasEditor = class {
|
|
|
3600
3736
|
this.snapping.dispose();
|
|
3601
3737
|
this.crop.dispose();
|
|
3602
3738
|
this.history.dispose();
|
|
3603
|
-
|
|
3739
|
+
this.patterns.dispose();
|
|
3604
3740
|
this.events.removeAllListeners();
|
|
3605
3741
|
this.canvas.dispose();
|
|
3606
3742
|
}
|
|
@@ -3744,17 +3880,14 @@ var AnnotationOverlay = class {
|
|
|
3744
3880
|
TEXTURE_MASK_IDS,
|
|
3745
3881
|
TEXTURE_MASK_SIZE,
|
|
3746
3882
|
TextCurveManager,
|
|
3883
|
+
TiledPatternObject,
|
|
3747
3884
|
UnitConverter,
|
|
3748
3885
|
applyAspectLock,
|
|
3749
3886
|
applyLayerShadow,
|
|
3750
3887
|
applyObjectSelectionStyle,
|
|
3751
|
-
applyPatternLocks,
|
|
3752
3888
|
applySelectionStyle,
|
|
3753
3889
|
buildCurvePathData,
|
|
3754
|
-
buildPatternDataURL,
|
|
3755
|
-
captureLocks,
|
|
3756
3890
|
clamp,
|
|
3757
|
-
clearPatternImageCache,
|
|
3758
3891
|
clearTextureMaskCache,
|
|
3759
3892
|
computeCoverPlacement,
|
|
3760
3893
|
computePrintAreaClip,
|
|
@@ -3773,7 +3906,6 @@ var AnnotationOverlay = class {
|
|
|
3773
3906
|
isMaskPresetId,
|
|
3774
3907
|
isShapeMaskId,
|
|
3775
3908
|
isTextureMaskId,
|
|
3776
|
-
loadPatternImage,
|
|
3777
3909
|
readLayerShadow,
|
|
3778
3910
|
renderTextureMask,
|
|
3779
3911
|
resetTransform,
|