@pooder/kit 4.0.0 → 4.1.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/CHANGELOG.md +6 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +147 -0
- package/dist/index.mjs +147 -0
- package/package.json +1 -1
- package/src/background.ts +230 -230
- package/src/constraints.ts +158 -0
- package/src/coordinate.ts +106 -106
- package/src/dieline.ts +35 -0
- package/src/feature.ts +25 -0
- package/src/film.ts +194 -194
- package/src/geometry.ts +4 -0
- package/src/image.ts +512 -512
- package/src/mirror.ts +128 -128
- package/src/ruler.ts +500 -500
- package/src/tracer.ts +570 -570
- package/src/white-ink.ts +373 -373
package/src/white-ink.ts
CHANGED
|
@@ -1,373 +1,373 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Extension,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
ContributionPointIds,
|
|
5
|
-
CommandContribution,
|
|
6
|
-
ConfigurationContribution,
|
|
7
|
-
} from "@pooder/core";
|
|
8
|
-
import { FabricImage as Image, filters } from "fabric";
|
|
9
|
-
import CanvasService from "./CanvasService";
|
|
10
|
-
|
|
11
|
-
export class WhiteInkTool implements Extension {
|
|
12
|
-
id = "pooder.kit.white-ink";
|
|
13
|
-
|
|
14
|
-
public metadata = {
|
|
15
|
-
name: "WhiteInkTool",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
private customMask: string = "";
|
|
19
|
-
private opacity: number = 1;
|
|
20
|
-
private enableClip: boolean = false;
|
|
21
|
-
|
|
22
|
-
private canvasService?: CanvasService;
|
|
23
|
-
private syncHandler: ((e: any) => void) | undefined;
|
|
24
|
-
private _loadingUrl: string | null = null;
|
|
25
|
-
|
|
26
|
-
constructor(
|
|
27
|
-
options?: Partial<{
|
|
28
|
-
customMask: string;
|
|
29
|
-
opacity: number;
|
|
30
|
-
enableClip: boolean;
|
|
31
|
-
}>,
|
|
32
|
-
) {
|
|
33
|
-
if (options) {
|
|
34
|
-
Object.assign(this, options);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
activate(context: ExtensionContext) {
|
|
39
|
-
this.canvasService = context.services.get<CanvasService>("CanvasService");
|
|
40
|
-
if (!this.canvasService) {
|
|
41
|
-
console.warn("CanvasService not found for WhiteInkTool");
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const configService = context.services.get<any>("ConfigurationService");
|
|
46
|
-
if (configService) {
|
|
47
|
-
// Load initial config
|
|
48
|
-
this.customMask = configService.get(
|
|
49
|
-
"whiteInk.customMask",
|
|
50
|
-
this.customMask,
|
|
51
|
-
);
|
|
52
|
-
this.opacity = configService.get("whiteInk.opacity", this.opacity);
|
|
53
|
-
this.enableClip = configService.get(
|
|
54
|
-
"whiteInk.enableClip",
|
|
55
|
-
this.enableClip,
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
// Listen for changes
|
|
59
|
-
configService.onAnyChange((e: { key: string; value: any }) => {
|
|
60
|
-
if (e.key.startsWith("whiteInk.")) {
|
|
61
|
-
const prop = e.key.split(".")[1];
|
|
62
|
-
console.log(
|
|
63
|
-
`[WhiteInkTool] Config change detected: ${e.key} -> ${e.value}`,
|
|
64
|
-
);
|
|
65
|
-
if (prop && prop in this) {
|
|
66
|
-
(this as any)[prop] = e.value;
|
|
67
|
-
this.updateWhiteInk();
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
this.setup();
|
|
74
|
-
this.updateWhiteInk();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
deactivate(context: ExtensionContext) {
|
|
78
|
-
this.teardown();
|
|
79
|
-
this.canvasService = undefined;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
contribute() {
|
|
83
|
-
return {
|
|
84
|
-
[ContributionPointIds.CONFIGURATIONS]: [
|
|
85
|
-
{
|
|
86
|
-
id: "whiteInk.customMask",
|
|
87
|
-
type: "string",
|
|
88
|
-
label: "Custom Mask URL",
|
|
89
|
-
default: "",
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
id: "whiteInk.opacity",
|
|
93
|
-
type: "number",
|
|
94
|
-
label: "Opacity",
|
|
95
|
-
min: 0,
|
|
96
|
-
max: 1,
|
|
97
|
-
step: 0.01,
|
|
98
|
-
default: 1,
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: "whiteInk.enableClip",
|
|
102
|
-
type: "boolean",
|
|
103
|
-
label: "Enable Clip",
|
|
104
|
-
default: false,
|
|
105
|
-
},
|
|
106
|
-
] as ConfigurationContribution[],
|
|
107
|
-
[ContributionPointIds.COMMANDS]: [
|
|
108
|
-
{
|
|
109
|
-
command: "setWhiteInkImage",
|
|
110
|
-
title: "Set White Ink Image",
|
|
111
|
-
handler: (
|
|
112
|
-
customMask: string,
|
|
113
|
-
opacity: number,
|
|
114
|
-
enableClip: boolean = true,
|
|
115
|
-
) => {
|
|
116
|
-
if (
|
|
117
|
-
this.customMask === customMask &&
|
|
118
|
-
this.opacity === opacity &&
|
|
119
|
-
this.enableClip === enableClip
|
|
120
|
-
)
|
|
121
|
-
return true;
|
|
122
|
-
|
|
123
|
-
this.customMask = customMask;
|
|
124
|
-
this.opacity = opacity;
|
|
125
|
-
this.enableClip = enableClip;
|
|
126
|
-
|
|
127
|
-
this.updateWhiteInk();
|
|
128
|
-
return true;
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
] as CommandContribution[],
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private setup() {
|
|
136
|
-
if (!this.canvasService) return;
|
|
137
|
-
const canvas = this.canvasService.canvas;
|
|
138
|
-
|
|
139
|
-
let userLayer = this.canvasService.getLayer("user");
|
|
140
|
-
if (!userLayer) {
|
|
141
|
-
userLayer = this.canvasService.createLayer("user", {
|
|
142
|
-
width: canvas.width,
|
|
143
|
-
height: canvas.height,
|
|
144
|
-
left: 0,
|
|
145
|
-
top: 0,
|
|
146
|
-
originX: "left",
|
|
147
|
-
originY: "top",
|
|
148
|
-
selectable: false,
|
|
149
|
-
evented: true,
|
|
150
|
-
subTargetCheck: true,
|
|
151
|
-
interactive: true,
|
|
152
|
-
});
|
|
153
|
-
canvas.add(userLayer);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!this.syncHandler) {
|
|
157
|
-
this.syncHandler = (e: any) => {
|
|
158
|
-
const target = e.target;
|
|
159
|
-
if (target && target.data?.id === "user-image") {
|
|
160
|
-
this.syncWithUserImage();
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
canvas.on("object:moving", this.syncHandler);
|
|
165
|
-
canvas.on("object:scaling", this.syncHandler);
|
|
166
|
-
canvas.on("object:rotating", this.syncHandler);
|
|
167
|
-
canvas.on("object:modified", this.syncHandler);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private teardown() {
|
|
172
|
-
if (!this.canvasService) return;
|
|
173
|
-
const canvas = this.canvasService.canvas;
|
|
174
|
-
|
|
175
|
-
if (this.syncHandler) {
|
|
176
|
-
canvas.off("object:moving", this.syncHandler);
|
|
177
|
-
canvas.off("object:scaling", this.syncHandler);
|
|
178
|
-
canvas.off("object:rotating", this.syncHandler);
|
|
179
|
-
canvas.off("object:modified", this.syncHandler);
|
|
180
|
-
this.syncHandler = undefined;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const layer = this.canvasService.getLayer("user");
|
|
184
|
-
if (layer) {
|
|
185
|
-
const whiteInk = this.canvasService.getObject("white-ink", "user");
|
|
186
|
-
if (whiteInk) {
|
|
187
|
-
layer.remove(whiteInk);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const userImage = this.canvasService.getObject("user-image", "user") as any;
|
|
192
|
-
if (userImage && userImage.clipPath) {
|
|
193
|
-
userImage.set({ clipPath: undefined });
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
this.canvasService.requestRenderAll();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private updateWhiteInk() {
|
|
200
|
-
if (!this.canvasService) return;
|
|
201
|
-
const { customMask, opacity, enableClip } = this;
|
|
202
|
-
|
|
203
|
-
const layer = this.canvasService.getLayer("user");
|
|
204
|
-
if (!layer) {
|
|
205
|
-
console.warn("[WhiteInkTool] User layer not found");
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const whiteInk = this.canvasService.getObject("white-ink", "user") as any;
|
|
210
|
-
const userImage = this.canvasService.getObject("user-image", "user") as any;
|
|
211
|
-
|
|
212
|
-
if (!customMask) {
|
|
213
|
-
if (whiteInk) {
|
|
214
|
-
layer.remove(whiteInk);
|
|
215
|
-
}
|
|
216
|
-
if (userImage && userImage.clipPath) {
|
|
217
|
-
userImage.set({ clipPath: undefined });
|
|
218
|
-
}
|
|
219
|
-
layer.dirty = true;
|
|
220
|
-
this.canvasService.requestRenderAll();
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Check if we need to load/reload white ink backing
|
|
225
|
-
if (whiteInk) {
|
|
226
|
-
const currentSrc = whiteInk.getSrc?.() || whiteInk._element?.src;
|
|
227
|
-
if (currentSrc !== customMask) {
|
|
228
|
-
this.loadWhiteInk(layer, customMask, opacity, enableClip, whiteInk);
|
|
229
|
-
} else {
|
|
230
|
-
if (whiteInk.opacity !== opacity) {
|
|
231
|
-
whiteInk.set({ opacity });
|
|
232
|
-
layer.dirty = true;
|
|
233
|
-
this.canvasService.requestRenderAll();
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
this.loadWhiteInk(layer, customMask, opacity, enableClip);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Handle Clip Path Toggle
|
|
241
|
-
if (userImage) {
|
|
242
|
-
if (enableClip) {
|
|
243
|
-
if (!userImage.clipPath) {
|
|
244
|
-
this.applyClipPath(customMask);
|
|
245
|
-
}
|
|
246
|
-
} else {
|
|
247
|
-
if (userImage.clipPath) {
|
|
248
|
-
userImage.set({ clipPath: undefined });
|
|
249
|
-
layer.dirty = true;
|
|
250
|
-
this.canvasService.requestRenderAll();
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private loadWhiteInk(
|
|
257
|
-
layer: any,
|
|
258
|
-
url: string,
|
|
259
|
-
opacity: number,
|
|
260
|
-
enableClip: boolean,
|
|
261
|
-
oldImage?: any,
|
|
262
|
-
) {
|
|
263
|
-
if (!this.canvasService) return;
|
|
264
|
-
|
|
265
|
-
if (this._loadingUrl === url) return;
|
|
266
|
-
this._loadingUrl = url;
|
|
267
|
-
|
|
268
|
-
Image.fromURL(url, { crossOrigin: "anonymous" })
|
|
269
|
-
.then((image) => {
|
|
270
|
-
if (this._loadingUrl !== url) return;
|
|
271
|
-
this._loadingUrl = null;
|
|
272
|
-
|
|
273
|
-
if (oldImage) {
|
|
274
|
-
layer.remove(oldImage);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
image.filters?.push(
|
|
278
|
-
new filters.BlendColor({
|
|
279
|
-
color: "#FFFFFF",
|
|
280
|
-
mode: "add",
|
|
281
|
-
}),
|
|
282
|
-
);
|
|
283
|
-
image.applyFilters();
|
|
284
|
-
|
|
285
|
-
image.set({
|
|
286
|
-
opacity,
|
|
287
|
-
selectable: false,
|
|
288
|
-
evented: false,
|
|
289
|
-
data: {
|
|
290
|
-
id: "white-ink",
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// Add to layer
|
|
295
|
-
layer.add(image);
|
|
296
|
-
|
|
297
|
-
// Ensure white-ink is behind user-image
|
|
298
|
-
const userImage = this.canvasService!.getObject("user-image", "user");
|
|
299
|
-
if (userImage) {
|
|
300
|
-
// Re-adding moves it to the top of the stack
|
|
301
|
-
layer.remove(userImage);
|
|
302
|
-
layer.add(userImage);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Apply clip path to user-image if enabled
|
|
306
|
-
if (enableClip) {
|
|
307
|
-
this.applyClipPath(url);
|
|
308
|
-
} else if (userImage) {
|
|
309
|
-
userImage.set({ clipPath: undefined });
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Sync position immediately
|
|
313
|
-
this.syncWithUserImage();
|
|
314
|
-
|
|
315
|
-
layer.dirty = true;
|
|
316
|
-
this.canvasService!.requestRenderAll();
|
|
317
|
-
})
|
|
318
|
-
.catch((err) => {
|
|
319
|
-
console.error("Failed to load white ink mask", url, err);
|
|
320
|
-
this._loadingUrl = null;
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
private applyClipPath(url: string) {
|
|
325
|
-
if (!this.canvasService) return;
|
|
326
|
-
const userImage = this.canvasService.getObject("user-image", "user") as any;
|
|
327
|
-
if (!userImage) return;
|
|
328
|
-
|
|
329
|
-
Image.fromURL(url, { crossOrigin: "anonymous" })
|
|
330
|
-
.then((maskImage) => {
|
|
331
|
-
// Configure clipPath
|
|
332
|
-
maskImage.set({
|
|
333
|
-
originX: "center",
|
|
334
|
-
originY: "center",
|
|
335
|
-
left: 0,
|
|
336
|
-
top: 0,
|
|
337
|
-
// Scale to fit userImage if dimensions differ
|
|
338
|
-
scaleX: userImage.width / maskImage.width,
|
|
339
|
-
scaleY: userImage.height / maskImage.height,
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
userImage.set({ clipPath: maskImage });
|
|
343
|
-
const layer = this.canvasService!.getLayer("user");
|
|
344
|
-
if (layer) layer.dirty = true;
|
|
345
|
-
this.canvasService!.requestRenderAll();
|
|
346
|
-
})
|
|
347
|
-
.catch((err) => {
|
|
348
|
-
console.error("Failed to load clip path", url, err);
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
private syncWithUserImage() {
|
|
353
|
-
if (!this.canvasService) return;
|
|
354
|
-
const userImage = this.canvasService.getObject("user-image", "user");
|
|
355
|
-
const whiteInk = this.canvasService.getObject("white-ink", "user");
|
|
356
|
-
|
|
357
|
-
if (userImage && whiteInk) {
|
|
358
|
-
whiteInk.set({
|
|
359
|
-
left: userImage.left,
|
|
360
|
-
top: userImage.top,
|
|
361
|
-
scaleX: userImage.scaleX,
|
|
362
|
-
scaleY: userImage.scaleY,
|
|
363
|
-
angle: userImage.angle,
|
|
364
|
-
skewX: userImage.skewX,
|
|
365
|
-
skewY: userImage.skewY,
|
|
366
|
-
flipX: userImage.flipX,
|
|
367
|
-
flipY: userImage.flipY,
|
|
368
|
-
originX: userImage.originX,
|
|
369
|
-
originY: userImage.originY,
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
Extension,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ContributionPointIds,
|
|
5
|
+
CommandContribution,
|
|
6
|
+
ConfigurationContribution,
|
|
7
|
+
} from "@pooder/core";
|
|
8
|
+
import { FabricImage as Image, filters } from "fabric";
|
|
9
|
+
import CanvasService from "./CanvasService";
|
|
10
|
+
|
|
11
|
+
export class WhiteInkTool implements Extension {
|
|
12
|
+
id = "pooder.kit.white-ink";
|
|
13
|
+
|
|
14
|
+
public metadata = {
|
|
15
|
+
name: "WhiteInkTool",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
private customMask: string = "";
|
|
19
|
+
private opacity: number = 1;
|
|
20
|
+
private enableClip: boolean = false;
|
|
21
|
+
|
|
22
|
+
private canvasService?: CanvasService;
|
|
23
|
+
private syncHandler: ((e: any) => void) | undefined;
|
|
24
|
+
private _loadingUrl: string | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
options?: Partial<{
|
|
28
|
+
customMask: string;
|
|
29
|
+
opacity: number;
|
|
30
|
+
enableClip: boolean;
|
|
31
|
+
}>,
|
|
32
|
+
) {
|
|
33
|
+
if (options) {
|
|
34
|
+
Object.assign(this, options);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
activate(context: ExtensionContext) {
|
|
39
|
+
this.canvasService = context.services.get<CanvasService>("CanvasService");
|
|
40
|
+
if (!this.canvasService) {
|
|
41
|
+
console.warn("CanvasService not found for WhiteInkTool");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const configService = context.services.get<any>("ConfigurationService");
|
|
46
|
+
if (configService) {
|
|
47
|
+
// Load initial config
|
|
48
|
+
this.customMask = configService.get(
|
|
49
|
+
"whiteInk.customMask",
|
|
50
|
+
this.customMask,
|
|
51
|
+
);
|
|
52
|
+
this.opacity = configService.get("whiteInk.opacity", this.opacity);
|
|
53
|
+
this.enableClip = configService.get(
|
|
54
|
+
"whiteInk.enableClip",
|
|
55
|
+
this.enableClip,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Listen for changes
|
|
59
|
+
configService.onAnyChange((e: { key: string; value: any }) => {
|
|
60
|
+
if (e.key.startsWith("whiteInk.")) {
|
|
61
|
+
const prop = e.key.split(".")[1];
|
|
62
|
+
console.log(
|
|
63
|
+
`[WhiteInkTool] Config change detected: ${e.key} -> ${e.value}`,
|
|
64
|
+
);
|
|
65
|
+
if (prop && prop in this) {
|
|
66
|
+
(this as any)[prop] = e.value;
|
|
67
|
+
this.updateWhiteInk();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.setup();
|
|
74
|
+
this.updateWhiteInk();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
deactivate(context: ExtensionContext) {
|
|
78
|
+
this.teardown();
|
|
79
|
+
this.canvasService = undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
contribute() {
|
|
83
|
+
return {
|
|
84
|
+
[ContributionPointIds.CONFIGURATIONS]: [
|
|
85
|
+
{
|
|
86
|
+
id: "whiteInk.customMask",
|
|
87
|
+
type: "string",
|
|
88
|
+
label: "Custom Mask URL",
|
|
89
|
+
default: "",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "whiteInk.opacity",
|
|
93
|
+
type: "number",
|
|
94
|
+
label: "Opacity",
|
|
95
|
+
min: 0,
|
|
96
|
+
max: 1,
|
|
97
|
+
step: 0.01,
|
|
98
|
+
default: 1,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "whiteInk.enableClip",
|
|
102
|
+
type: "boolean",
|
|
103
|
+
label: "Enable Clip",
|
|
104
|
+
default: false,
|
|
105
|
+
},
|
|
106
|
+
] as ConfigurationContribution[],
|
|
107
|
+
[ContributionPointIds.COMMANDS]: [
|
|
108
|
+
{
|
|
109
|
+
command: "setWhiteInkImage",
|
|
110
|
+
title: "Set White Ink Image",
|
|
111
|
+
handler: (
|
|
112
|
+
customMask: string,
|
|
113
|
+
opacity: number,
|
|
114
|
+
enableClip: boolean = true,
|
|
115
|
+
) => {
|
|
116
|
+
if (
|
|
117
|
+
this.customMask === customMask &&
|
|
118
|
+
this.opacity === opacity &&
|
|
119
|
+
this.enableClip === enableClip
|
|
120
|
+
)
|
|
121
|
+
return true;
|
|
122
|
+
|
|
123
|
+
this.customMask = customMask;
|
|
124
|
+
this.opacity = opacity;
|
|
125
|
+
this.enableClip = enableClip;
|
|
126
|
+
|
|
127
|
+
this.updateWhiteInk();
|
|
128
|
+
return true;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
] as CommandContribution[],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private setup() {
|
|
136
|
+
if (!this.canvasService) return;
|
|
137
|
+
const canvas = this.canvasService.canvas;
|
|
138
|
+
|
|
139
|
+
let userLayer = this.canvasService.getLayer("user");
|
|
140
|
+
if (!userLayer) {
|
|
141
|
+
userLayer = this.canvasService.createLayer("user", {
|
|
142
|
+
width: canvas.width,
|
|
143
|
+
height: canvas.height,
|
|
144
|
+
left: 0,
|
|
145
|
+
top: 0,
|
|
146
|
+
originX: "left",
|
|
147
|
+
originY: "top",
|
|
148
|
+
selectable: false,
|
|
149
|
+
evented: true,
|
|
150
|
+
subTargetCheck: true,
|
|
151
|
+
interactive: true,
|
|
152
|
+
});
|
|
153
|
+
canvas.add(userLayer);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!this.syncHandler) {
|
|
157
|
+
this.syncHandler = (e: any) => {
|
|
158
|
+
const target = e.target;
|
|
159
|
+
if (target && target.data?.id === "user-image") {
|
|
160
|
+
this.syncWithUserImage();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
canvas.on("object:moving", this.syncHandler);
|
|
165
|
+
canvas.on("object:scaling", this.syncHandler);
|
|
166
|
+
canvas.on("object:rotating", this.syncHandler);
|
|
167
|
+
canvas.on("object:modified", this.syncHandler);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private teardown() {
|
|
172
|
+
if (!this.canvasService) return;
|
|
173
|
+
const canvas = this.canvasService.canvas;
|
|
174
|
+
|
|
175
|
+
if (this.syncHandler) {
|
|
176
|
+
canvas.off("object:moving", this.syncHandler);
|
|
177
|
+
canvas.off("object:scaling", this.syncHandler);
|
|
178
|
+
canvas.off("object:rotating", this.syncHandler);
|
|
179
|
+
canvas.off("object:modified", this.syncHandler);
|
|
180
|
+
this.syncHandler = undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const layer = this.canvasService.getLayer("user");
|
|
184
|
+
if (layer) {
|
|
185
|
+
const whiteInk = this.canvasService.getObject("white-ink", "user");
|
|
186
|
+
if (whiteInk) {
|
|
187
|
+
layer.remove(whiteInk);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const userImage = this.canvasService.getObject("user-image", "user") as any;
|
|
192
|
+
if (userImage && userImage.clipPath) {
|
|
193
|
+
userImage.set({ clipPath: undefined });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.canvasService.requestRenderAll();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private updateWhiteInk() {
|
|
200
|
+
if (!this.canvasService) return;
|
|
201
|
+
const { customMask, opacity, enableClip } = this;
|
|
202
|
+
|
|
203
|
+
const layer = this.canvasService.getLayer("user");
|
|
204
|
+
if (!layer) {
|
|
205
|
+
console.warn("[WhiteInkTool] User layer not found");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const whiteInk = this.canvasService.getObject("white-ink", "user") as any;
|
|
210
|
+
const userImage = this.canvasService.getObject("user-image", "user") as any;
|
|
211
|
+
|
|
212
|
+
if (!customMask) {
|
|
213
|
+
if (whiteInk) {
|
|
214
|
+
layer.remove(whiteInk);
|
|
215
|
+
}
|
|
216
|
+
if (userImage && userImage.clipPath) {
|
|
217
|
+
userImage.set({ clipPath: undefined });
|
|
218
|
+
}
|
|
219
|
+
layer.dirty = true;
|
|
220
|
+
this.canvasService.requestRenderAll();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check if we need to load/reload white ink backing
|
|
225
|
+
if (whiteInk) {
|
|
226
|
+
const currentSrc = whiteInk.getSrc?.() || whiteInk._element?.src;
|
|
227
|
+
if (currentSrc !== customMask) {
|
|
228
|
+
this.loadWhiteInk(layer, customMask, opacity, enableClip, whiteInk);
|
|
229
|
+
} else {
|
|
230
|
+
if (whiteInk.opacity !== opacity) {
|
|
231
|
+
whiteInk.set({ opacity });
|
|
232
|
+
layer.dirty = true;
|
|
233
|
+
this.canvasService.requestRenderAll();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
this.loadWhiteInk(layer, customMask, opacity, enableClip);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Handle Clip Path Toggle
|
|
241
|
+
if (userImage) {
|
|
242
|
+
if (enableClip) {
|
|
243
|
+
if (!userImage.clipPath) {
|
|
244
|
+
this.applyClipPath(customMask);
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
if (userImage.clipPath) {
|
|
248
|
+
userImage.set({ clipPath: undefined });
|
|
249
|
+
layer.dirty = true;
|
|
250
|
+
this.canvasService.requestRenderAll();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private loadWhiteInk(
|
|
257
|
+
layer: any,
|
|
258
|
+
url: string,
|
|
259
|
+
opacity: number,
|
|
260
|
+
enableClip: boolean,
|
|
261
|
+
oldImage?: any,
|
|
262
|
+
) {
|
|
263
|
+
if (!this.canvasService) return;
|
|
264
|
+
|
|
265
|
+
if (this._loadingUrl === url) return;
|
|
266
|
+
this._loadingUrl = url;
|
|
267
|
+
|
|
268
|
+
Image.fromURL(url, { crossOrigin: "anonymous" })
|
|
269
|
+
.then((image) => {
|
|
270
|
+
if (this._loadingUrl !== url) return;
|
|
271
|
+
this._loadingUrl = null;
|
|
272
|
+
|
|
273
|
+
if (oldImage) {
|
|
274
|
+
layer.remove(oldImage);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
image.filters?.push(
|
|
278
|
+
new filters.BlendColor({
|
|
279
|
+
color: "#FFFFFF",
|
|
280
|
+
mode: "add",
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
image.applyFilters();
|
|
284
|
+
|
|
285
|
+
image.set({
|
|
286
|
+
opacity,
|
|
287
|
+
selectable: false,
|
|
288
|
+
evented: false,
|
|
289
|
+
data: {
|
|
290
|
+
id: "white-ink",
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Add to layer
|
|
295
|
+
layer.add(image);
|
|
296
|
+
|
|
297
|
+
// Ensure white-ink is behind user-image
|
|
298
|
+
const userImage = this.canvasService!.getObject("user-image", "user");
|
|
299
|
+
if (userImage) {
|
|
300
|
+
// Re-adding moves it to the top of the stack
|
|
301
|
+
layer.remove(userImage);
|
|
302
|
+
layer.add(userImage);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Apply clip path to user-image if enabled
|
|
306
|
+
if (enableClip) {
|
|
307
|
+
this.applyClipPath(url);
|
|
308
|
+
} else if (userImage) {
|
|
309
|
+
userImage.set({ clipPath: undefined });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Sync position immediately
|
|
313
|
+
this.syncWithUserImage();
|
|
314
|
+
|
|
315
|
+
layer.dirty = true;
|
|
316
|
+
this.canvasService!.requestRenderAll();
|
|
317
|
+
})
|
|
318
|
+
.catch((err) => {
|
|
319
|
+
console.error("Failed to load white ink mask", url, err);
|
|
320
|
+
this._loadingUrl = null;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private applyClipPath(url: string) {
|
|
325
|
+
if (!this.canvasService) return;
|
|
326
|
+
const userImage = this.canvasService.getObject("user-image", "user") as any;
|
|
327
|
+
if (!userImage) return;
|
|
328
|
+
|
|
329
|
+
Image.fromURL(url, { crossOrigin: "anonymous" })
|
|
330
|
+
.then((maskImage) => {
|
|
331
|
+
// Configure clipPath
|
|
332
|
+
maskImage.set({
|
|
333
|
+
originX: "center",
|
|
334
|
+
originY: "center",
|
|
335
|
+
left: 0,
|
|
336
|
+
top: 0,
|
|
337
|
+
// Scale to fit userImage if dimensions differ
|
|
338
|
+
scaleX: userImage.width / maskImage.width,
|
|
339
|
+
scaleY: userImage.height / maskImage.height,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
userImage.set({ clipPath: maskImage });
|
|
343
|
+
const layer = this.canvasService!.getLayer("user");
|
|
344
|
+
if (layer) layer.dirty = true;
|
|
345
|
+
this.canvasService!.requestRenderAll();
|
|
346
|
+
})
|
|
347
|
+
.catch((err) => {
|
|
348
|
+
console.error("Failed to load clip path", url, err);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private syncWithUserImage() {
|
|
353
|
+
if (!this.canvasService) return;
|
|
354
|
+
const userImage = this.canvasService.getObject("user-image", "user");
|
|
355
|
+
const whiteInk = this.canvasService.getObject("white-ink", "user");
|
|
356
|
+
|
|
357
|
+
if (userImage && whiteInk) {
|
|
358
|
+
whiteInk.set({
|
|
359
|
+
left: userImage.left,
|
|
360
|
+
top: userImage.top,
|
|
361
|
+
scaleX: userImage.scaleX,
|
|
362
|
+
scaleY: userImage.scaleY,
|
|
363
|
+
angle: userImage.angle,
|
|
364
|
+
skewX: userImage.skewX,
|
|
365
|
+
skewY: userImage.skewY,
|
|
366
|
+
flipX: userImage.flipX,
|
|
367
|
+
flipY: userImage.flipY,
|
|
368
|
+
originX: userImage.originX,
|
|
369
|
+
originY: userImage.originY,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|