embroidery-qc-image 1.0.7 → 1.0.8
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/README.md +61 -84
- package/dist/components/EmbroideryQCImage.d.ts +9 -1
- package/dist/components/EmbroideryQCImage.d.ts.map +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +407 -112
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +408 -111
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -37,37 +37,68 @@ styleInject(css_248z);
|
|
|
37
37
|
// CONSTANTS
|
|
38
38
|
// ============================================================================
|
|
39
39
|
const COLOR_MAP = {
|
|
40
|
-
"Army (1394)": "#4B5320",
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
|
|
40
|
+
"Army (1394)": "#4B5320",
|
|
41
|
+
Army: "#4B5320",
|
|
42
|
+
"Black (8)": "#000000",
|
|
43
|
+
Black: "#000000",
|
|
44
|
+
"Bubblegum (1309)": "#FFC1CC",
|
|
45
|
+
Bubblegum: "#FFC1CC",
|
|
46
|
+
"Carolina Blue (1274)": "#7BAFD4",
|
|
47
|
+
"Carolina Blue": "#7BAFD4",
|
|
48
|
+
"Celadon (1098)": "#ACE1AF",
|
|
49
|
+
Celadon: "#ACE1AF",
|
|
50
|
+
"Coffee Bean (1145)": "#6F4E37",
|
|
51
|
+
"Coffee Bean": "#6F4E37",
|
|
52
|
+
"Daffodil (1180)": "#FFFF31",
|
|
53
|
+
Daffodil: "#FFFF31",
|
|
54
|
+
"Dark Gray (1131)": "#A9A9A9",
|
|
55
|
+
"Dark Gray": "#A9A9A9",
|
|
56
|
+
"Doe Skin Beige (1344)": "#F5E6D3",
|
|
57
|
+
"Doe Skin Beige": "#F5E6D3",
|
|
58
|
+
"Dusty Blue (1373)": "#6699CC",
|
|
59
|
+
"Dusty Blue": "#6699CC",
|
|
60
|
+
"Forest Green (1397)": "#228B22",
|
|
61
|
+
"Forest Green": "#228B22",
|
|
62
|
+
"Gold (1425)": "#FFD700",
|
|
63
|
+
Gold: "#FFD700",
|
|
64
|
+
"Gray (1118)": "#808080",
|
|
65
|
+
Gray: "#808080",
|
|
66
|
+
"Ivory (1072)": "#FFFFF0",
|
|
67
|
+
Ivory: "#FFFFF0",
|
|
68
|
+
"Lavender (1032)": "#E6E6FA",
|
|
69
|
+
Lavender: "#E6E6FA",
|
|
70
|
+
"Light Denim (1133)": "#B0C4DE",
|
|
71
|
+
"Light Denim": "#B0C4DE",
|
|
72
|
+
"Light Salmon (1018)": "#FFA07A",
|
|
73
|
+
"Light Salmon": "#FFA07A",
|
|
74
|
+
"Maroon (1374)": "#800000",
|
|
75
|
+
Maroon: "#800000",
|
|
76
|
+
"Navy Blue (1044)": "#000080",
|
|
77
|
+
"Navy Blue": "#000080",
|
|
78
|
+
"Olive Green (1157)": "#556B2F",
|
|
79
|
+
"Olive Green": "#556B2F",
|
|
80
|
+
"Orange (1278)": "#FFA500",
|
|
81
|
+
Orange: "#FFA500",
|
|
82
|
+
"Peach Blush (1053)": "#FFCCCB",
|
|
83
|
+
"Peach Blush": "#FFCCCB",
|
|
84
|
+
"Pink (1148)": "#FFC0CB",
|
|
85
|
+
Pink: "#FFC0CB",
|
|
86
|
+
"Purple (1412)": "#800080",
|
|
87
|
+
Purple: "#800080",
|
|
88
|
+
"Red (1037)": "#FF0000",
|
|
89
|
+
Red: "#FF0000",
|
|
90
|
+
"Silver Sage (1396)": "#A8A8A8",
|
|
91
|
+
"Silver Sage": "#A8A8A8",
|
|
92
|
+
"Summer Sky (1432)": "#87CEEB",
|
|
93
|
+
"Summer Sky": "#87CEEB",
|
|
94
|
+
"Terra Cotta (1477)": "#E2725B",
|
|
95
|
+
"Terra Cotta": "#E2725B",
|
|
96
|
+
"Sand (1055)": "#F4A460",
|
|
97
|
+
Sand: "#F4A460",
|
|
98
|
+
"White (9)": "#FFFFFF",
|
|
99
|
+
White: "#FFFFFF",
|
|
70
100
|
};
|
|
101
|
+
const DEFAULT_ERROR_COLOR = "#CC1F1A";
|
|
71
102
|
const BASE_URLS = {
|
|
72
103
|
FONT: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/fonts",
|
|
73
104
|
ICON: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/icons",
|
|
@@ -130,14 +161,170 @@ const getImageUrl = (type, value) => {
|
|
|
130
161
|
return `${BASE_URLS.FLORAL}/${value}.png`;
|
|
131
162
|
return `${BASE_URLS.THREAD_COLOR}/${value}.png`;
|
|
132
163
|
};
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
164
|
+
const getProxyUrl = (url) => `https://proxy-img.c8p.workers.dev?url=${encodeURIComponent(url)}`;
|
|
165
|
+
const ensureImage = (existing) => {
|
|
166
|
+
if (existing && existing.crossOrigin === "anonymous") {
|
|
167
|
+
return existing;
|
|
168
|
+
}
|
|
136
169
|
const img = new Image();
|
|
137
170
|
img.crossOrigin = "anonymous";
|
|
138
|
-
img.
|
|
139
|
-
img
|
|
171
|
+
img.decoding = "async";
|
|
172
|
+
return img;
|
|
173
|
+
};
|
|
174
|
+
const loadImage = (url, imageRefs, onLoad) => {
|
|
175
|
+
const existing = imageRefs.current.get(url);
|
|
176
|
+
if (existing?.complete &&
|
|
177
|
+
existing.naturalWidth > 0 &&
|
|
178
|
+
existing.crossOrigin === "anonymous") {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const img = ensureImage(existing);
|
|
140
182
|
imageRefs.current.set(url, img);
|
|
183
|
+
let attemptedProxy = existing?.dataset?.proxyUsed === "true";
|
|
184
|
+
const cleanup = () => {
|
|
185
|
+
img.onload = null;
|
|
186
|
+
img.onerror = null;
|
|
187
|
+
};
|
|
188
|
+
img.onload = () => {
|
|
189
|
+
img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
190
|
+
cleanup();
|
|
191
|
+
onLoad();
|
|
192
|
+
};
|
|
193
|
+
img.onerror = () => {
|
|
194
|
+
if (!attemptedProxy) {
|
|
195
|
+
attemptedProxy = true;
|
|
196
|
+
img.src = getProxyUrl(url);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
200
|
+
cleanup();
|
|
201
|
+
onLoad();
|
|
202
|
+
};
|
|
203
|
+
img.src = attemptedProxy ? getProxyUrl(url) : url;
|
|
204
|
+
};
|
|
205
|
+
const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
206
|
+
const key = cacheKey ?? url;
|
|
207
|
+
const existing = imageRefs.current.get(key) ?? imageRefs.current.get(url);
|
|
208
|
+
if (existing?.complete &&
|
|
209
|
+
existing.naturalWidth > 0 &&
|
|
210
|
+
existing.crossOrigin === "anonymous" &&
|
|
211
|
+
existing.dataset?.proxyUsed !== undefined) {
|
|
212
|
+
if (existing !== imageRefs.current.get(key)) {
|
|
213
|
+
imageRefs.current.set(key, existing);
|
|
214
|
+
}
|
|
215
|
+
if (existing !== imageRefs.current.get(url)) {
|
|
216
|
+
imageRefs.current.set(url, existing);
|
|
217
|
+
}
|
|
218
|
+
return Promise.resolve(existing);
|
|
219
|
+
}
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
const target = ensureImage(existing);
|
|
222
|
+
if (target !== existing) {
|
|
223
|
+
imageRefs.current.set(key, target);
|
|
224
|
+
imageRefs.current.set(url, target);
|
|
225
|
+
}
|
|
226
|
+
let attemptedProxy = target.dataset.proxyUsed === "true";
|
|
227
|
+
const finalize = () => {
|
|
228
|
+
target.onload = null;
|
|
229
|
+
target.onerror = null;
|
|
230
|
+
if (target.complete && target.naturalWidth > 0) {
|
|
231
|
+
imageRefs.current.set(key, target);
|
|
232
|
+
imageRefs.current.set(url, target);
|
|
233
|
+
}
|
|
234
|
+
target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
235
|
+
resolve(target);
|
|
236
|
+
};
|
|
237
|
+
target.onload = finalize;
|
|
238
|
+
target.onerror = () => {
|
|
239
|
+
if (!attemptedProxy) {
|
|
240
|
+
attemptedProxy = true;
|
|
241
|
+
target.src = getProxyUrl(url);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
245
|
+
finalize();
|
|
246
|
+
};
|
|
247
|
+
const desiredSrc = attemptedProxy ? getProxyUrl(url) : url;
|
|
248
|
+
if (target.src !== desiredSrc) {
|
|
249
|
+
target.src = desiredSrc;
|
|
250
|
+
}
|
|
251
|
+
else if (target.complete && target.naturalWidth > 0) {
|
|
252
|
+
finalize();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
const preloadFonts = async (config) => {
|
|
257
|
+
if (config.error || !config.sides?.length)
|
|
258
|
+
return;
|
|
259
|
+
const fonts = new Set();
|
|
260
|
+
config.sides.forEach((side) => {
|
|
261
|
+
side.positions.forEach((position) => {
|
|
262
|
+
if (position.type === "TEXT" && position.font) {
|
|
263
|
+
fonts.add(position.font);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
if (fonts.size === 0)
|
|
268
|
+
return;
|
|
269
|
+
await Promise.all([...fonts].map((font) => loadFont(font)));
|
|
270
|
+
};
|
|
271
|
+
const preloadImages = async (config, imageRefs) => {
|
|
272
|
+
const entries = [];
|
|
273
|
+
const seen = new Set();
|
|
274
|
+
if (config.image_url) {
|
|
275
|
+
entries.push({ url: config.image_url, cacheKey: "mockup" });
|
|
276
|
+
seen.add(config.image_url);
|
|
277
|
+
}
|
|
278
|
+
if (!config.sides?.length) {
|
|
279
|
+
await Promise.all(entries.map(({ url, cacheKey }) => loadImageAsync(url, imageRefs, cacheKey)));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
config.sides.forEach((side) => {
|
|
283
|
+
side.positions.forEach((position) => {
|
|
284
|
+
if (position.type === "ICON") {
|
|
285
|
+
if (position.icon !== 0) {
|
|
286
|
+
const iconUrl = getImageUrl("icon", position.icon);
|
|
287
|
+
if (!seen.has(iconUrl)) {
|
|
288
|
+
entries.push({ url: iconUrl });
|
|
289
|
+
seen.add(iconUrl);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
position.layer_colors?.forEach((color) => {
|
|
293
|
+
const colorUrl = getImageUrl("threadColor", color);
|
|
294
|
+
if (!seen.has(colorUrl)) {
|
|
295
|
+
entries.push({ url: colorUrl });
|
|
296
|
+
seen.add(colorUrl);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (position.type === "TEXT") {
|
|
301
|
+
if (position.floral_pattern) {
|
|
302
|
+
const floralUrl = getImageUrl("floral", position.floral_pattern);
|
|
303
|
+
if (!seen.has(floralUrl)) {
|
|
304
|
+
entries.push({ url: floralUrl });
|
|
305
|
+
seen.add(floralUrl);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (position.color) {
|
|
309
|
+
const threadUrl = getImageUrl("threadColor", position.color);
|
|
310
|
+
if (!seen.has(threadUrl)) {
|
|
311
|
+
entries.push({ url: threadUrl });
|
|
312
|
+
seen.add(threadUrl);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
position.character_colors?.forEach((color) => {
|
|
316
|
+
const characterColorUrl = getImageUrl("threadColor", color);
|
|
317
|
+
if (!seen.has(characterColorUrl)) {
|
|
318
|
+
entries.push({ url: characterColorUrl });
|
|
319
|
+
seen.add(characterColorUrl);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
if (entries.length === 0)
|
|
326
|
+
return;
|
|
327
|
+
await Promise.all(entries.map(({ url, cacheKey }) => loadImageAsync(url, imageRefs, cacheKey)));
|
|
141
328
|
};
|
|
142
329
|
const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
143
330
|
const words = text.split(" ");
|
|
@@ -165,6 +352,25 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
|
165
352
|
lastLineY: y + (lines.length - 1) * lineHeight,
|
|
166
353
|
};
|
|
167
354
|
};
|
|
355
|
+
const buildWrappedLines = (ctx, text, maxWidth) => {
|
|
356
|
+
const words = text.split(" ").filter((word) => word.length > 0);
|
|
357
|
+
if (words.length === 0)
|
|
358
|
+
return [""];
|
|
359
|
+
const lines = [];
|
|
360
|
+
let currentLine = words[0];
|
|
361
|
+
for (let i = 1; i < words.length; i++) {
|
|
362
|
+
const testLine = `${currentLine} ${words[i]}`;
|
|
363
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
364
|
+
lines.push(currentLine);
|
|
365
|
+
currentLine = words[i];
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
currentLine = testLine;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
lines.push(currentLine);
|
|
372
|
+
return lines;
|
|
373
|
+
};
|
|
168
374
|
const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
169
375
|
const words = text.split(" ");
|
|
170
376
|
const lines = [];
|
|
@@ -227,7 +433,7 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
227
433
|
// Load fonts
|
|
228
434
|
react.useEffect(() => {
|
|
229
435
|
const loadFonts = async () => {
|
|
230
|
-
if (!config.sides?.length)
|
|
436
|
+
if (config.error || !config.sides?.length)
|
|
231
437
|
return;
|
|
232
438
|
const fontsToLoad = new Set();
|
|
233
439
|
config.sides.forEach((side) => {
|
|
@@ -253,23 +459,12 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
253
459
|
}, [config.sides, loadedFonts]);
|
|
254
460
|
// Load images
|
|
255
461
|
react.useEffect(() => {
|
|
256
|
-
if (!config.sides?.length)
|
|
462
|
+
if (config.error || !config.sides?.length)
|
|
257
463
|
return;
|
|
258
464
|
const incrementCounter = () => setImagesLoaded((prev) => prev + 1);
|
|
259
465
|
// Load mockup
|
|
260
466
|
if (config.image_url) {
|
|
261
|
-
|
|
262
|
-
const img = new Image();
|
|
263
|
-
if (useCors)
|
|
264
|
-
img.crossOrigin = "anonymous";
|
|
265
|
-
img.onload = () => {
|
|
266
|
-
imageRefs.current.set("mockup", img);
|
|
267
|
-
incrementCounter();
|
|
268
|
-
};
|
|
269
|
-
img.onerror = () => useCors && loadMockup(false);
|
|
270
|
-
img.src = config.image_url;
|
|
271
|
-
};
|
|
272
|
-
loadMockup(true);
|
|
467
|
+
loadImage(config.image_url, imageRefs, incrementCounter);
|
|
273
468
|
}
|
|
274
469
|
// Load all other images
|
|
275
470
|
config.sides.forEach((side) => {
|
|
@@ -299,61 +494,9 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
299
494
|
// Render canvas
|
|
300
495
|
react.useEffect(() => {
|
|
301
496
|
const renderCanvas = () => {
|
|
302
|
-
if (!canvasRef.current
|
|
497
|
+
if (!canvasRef.current)
|
|
303
498
|
return;
|
|
304
|
-
|
|
305
|
-
const ctx = canvas.getContext("2d");
|
|
306
|
-
if (!ctx)
|
|
307
|
-
return;
|
|
308
|
-
canvas.width = canvasSize.width;
|
|
309
|
-
canvas.height = canvasSize.height;
|
|
310
|
-
// Set text alignment once
|
|
311
|
-
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
312
|
-
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
313
|
-
// Clear background
|
|
314
|
-
ctx.fillStyle = LAYOUT.BACKGROUND_COLOR;
|
|
315
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
316
|
-
// Collect floral assets
|
|
317
|
-
const floralAssets = [];
|
|
318
|
-
const seenFlorals = new Set();
|
|
319
|
-
config.sides.forEach((side) => {
|
|
320
|
-
side.positions.forEach((position) => {
|
|
321
|
-
if (position.type === "TEXT" && position.floral_pattern) {
|
|
322
|
-
const url = getImageUrl("floral", position.floral_pattern);
|
|
323
|
-
if (!seenFlorals.has(url)) {
|
|
324
|
-
const img = imageRefs.current.get(url);
|
|
325
|
-
if (img?.complete && img.naturalWidth > 0) {
|
|
326
|
-
floralAssets.push(img);
|
|
327
|
-
seenFlorals.add(url);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
// Calculate scale factor
|
|
334
|
-
const measureCanvas = document.createElement("canvas");
|
|
335
|
-
measureCanvas.width = canvas.width;
|
|
336
|
-
measureCanvas.height = canvas.height;
|
|
337
|
-
const measureCtx = measureCanvas.getContext("2d");
|
|
338
|
-
if (!measureCtx)
|
|
339
|
-
return;
|
|
340
|
-
measureCtx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
341
|
-
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
342
|
-
let measureY = LAYOUT.PADDING;
|
|
343
|
-
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
344
|
-
config.sides.forEach((side) => {
|
|
345
|
-
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
|
|
346
|
-
measureY += sideHeight + measureSpacing;
|
|
347
|
-
});
|
|
348
|
-
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING) / measureY));
|
|
349
|
-
// Draw mockup and florals
|
|
350
|
-
drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
|
|
351
|
-
// Draw content
|
|
352
|
-
let currentY = LAYOUT.PADDING * scaleFactor;
|
|
353
|
-
config.sides.forEach((side) => {
|
|
354
|
-
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
355
|
-
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
356
|
-
});
|
|
499
|
+
renderEmbroideryCanvas(canvasRef.current, config, canvasSize, imageRefs);
|
|
357
500
|
};
|
|
358
501
|
const timer = setTimeout(renderCanvas, 100);
|
|
359
502
|
return () => clearTimeout(timer);
|
|
@@ -363,6 +506,102 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
363
506
|
// ============================================================================
|
|
364
507
|
// RENDERING FUNCTIONS
|
|
365
508
|
// ============================================================================
|
|
509
|
+
const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
510
|
+
const ctx = canvas.getContext("2d");
|
|
511
|
+
if (!ctx)
|
|
512
|
+
return;
|
|
513
|
+
canvas.width = canvasSize.width;
|
|
514
|
+
canvas.height = canvasSize.height;
|
|
515
|
+
ctx.fillStyle = LAYOUT.BACKGROUND_COLOR;
|
|
516
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
517
|
+
if (config.error) {
|
|
518
|
+
renderErrorState(ctx, canvas, config.error);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (!config.sides?.length)
|
|
522
|
+
return;
|
|
523
|
+
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
524
|
+
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
525
|
+
if (config.image_url) {
|
|
526
|
+
const mockupImage = imageRefs.current.get(config.image_url);
|
|
527
|
+
if (mockupImage) {
|
|
528
|
+
imageRefs.current.set("mockup", mockupImage);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const floralAssets = [];
|
|
532
|
+
const seenFlorals = new Set();
|
|
533
|
+
config.sides.forEach((side) => {
|
|
534
|
+
side.positions.forEach((position) => {
|
|
535
|
+
if (position.type === "TEXT" && position.floral_pattern) {
|
|
536
|
+
const url = getImageUrl("floral", position.floral_pattern);
|
|
537
|
+
if (!seenFlorals.has(url)) {
|
|
538
|
+
const img = imageRefs.current.get(url);
|
|
539
|
+
if (img?.complete && img.naturalWidth > 0) {
|
|
540
|
+
floralAssets.push(img);
|
|
541
|
+
seenFlorals.add(url);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
const measureCanvas = document.createElement("canvas");
|
|
548
|
+
measureCanvas.width = canvas.width;
|
|
549
|
+
measureCanvas.height = canvas.height;
|
|
550
|
+
const measureCtx = measureCanvas.getContext("2d");
|
|
551
|
+
if (!measureCtx)
|
|
552
|
+
return;
|
|
553
|
+
measureCtx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
554
|
+
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
555
|
+
let measureY = LAYOUT.PADDING;
|
|
556
|
+
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
557
|
+
config.sides.forEach((side) => {
|
|
558
|
+
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
|
|
559
|
+
measureY += sideHeight + measureSpacing;
|
|
560
|
+
});
|
|
561
|
+
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING) / measureY));
|
|
562
|
+
drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
|
|
563
|
+
let currentY = LAYOUT.PADDING * scaleFactor;
|
|
564
|
+
config.sides.forEach((side) => {
|
|
565
|
+
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
566
|
+
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
const renderErrorState = (ctx, canvas, message) => {
|
|
570
|
+
const sanitizedMessage = message.trim() || "Đã xảy ra lỗi";
|
|
571
|
+
const horizontalPadding = LAYOUT.PADDING * 3;
|
|
572
|
+
const maxWidth = canvas.width - horizontalPadding * 2;
|
|
573
|
+
const baseFontSize = LAYOUT.HEADER_FONT_SIZE;
|
|
574
|
+
const minFontSize = 60;
|
|
575
|
+
const centerX = canvas.width / 2;
|
|
576
|
+
ctx.save();
|
|
577
|
+
ctx.textAlign = "center";
|
|
578
|
+
ctx.textBaseline = "top";
|
|
579
|
+
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
580
|
+
let fontSize = baseFontSize;
|
|
581
|
+
let lineGap = LAYOUT.LINE_GAP;
|
|
582
|
+
let lineHeight = fontSize + lineGap;
|
|
583
|
+
const adjustMetrics = () => {
|
|
584
|
+
ctx.font = `bold ${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
585
|
+
lineGap = LAYOUT.LINE_GAP * (fontSize / baseFontSize);
|
|
586
|
+
lineHeight = fontSize + lineGap;
|
|
587
|
+
};
|
|
588
|
+
adjustMetrics();
|
|
589
|
+
let lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
590
|
+
let longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
591
|
+
while (longestLineWidth > maxWidth && fontSize > minFontSize) {
|
|
592
|
+
fontSize = Math.max(minFontSize, Math.floor(fontSize * 0.9));
|
|
593
|
+
adjustMetrics();
|
|
594
|
+
lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
595
|
+
longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
596
|
+
}
|
|
597
|
+
const totalHeight = lines.length * lineHeight;
|
|
598
|
+
const startY = Math.max(LAYOUT.PADDING * 2, canvas.height / 2 - totalHeight / 2);
|
|
599
|
+
lines.forEach((line, index) => {
|
|
600
|
+
const y = startY + index * lineHeight;
|
|
601
|
+
ctx.fillText(line, centerX, y);
|
|
602
|
+
});
|
|
603
|
+
ctx.restore();
|
|
604
|
+
};
|
|
366
605
|
const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
|
|
367
606
|
const mockupImg = imageRefs.current.get("mockup");
|
|
368
607
|
if (!mockupImg?.complete || !mockupImg.naturalWidth)
|
|
@@ -448,7 +687,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
448
687
|
side.positions.forEach((position) => {
|
|
449
688
|
if (position.type === "ICON") {
|
|
450
689
|
currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs);
|
|
451
|
-
currentY += LAYOUT.LINE_GAP / 3 * scaleFactor;
|
|
690
|
+
currentY += (LAYOUT.LINE_GAP / 3) * scaleFactor;
|
|
452
691
|
}
|
|
453
692
|
});
|
|
454
693
|
return currentY - startY;
|
|
@@ -494,7 +733,9 @@ const computeUniformProperties = (textPositions) => {
|
|
|
494
733
|
const fonts = new Set(textPositions.map((p) => p.font));
|
|
495
734
|
const shapes = new Set(textPositions.map((p) => p.text_shape));
|
|
496
735
|
const florals = new Set(textPositions.map((p) => p.floral_pattern ?? "None"));
|
|
497
|
-
const colors = new Set(textPositions.map((p) => p.character_colors?.length
|
|
736
|
+
const colors = new Set(textPositions.map((p) => p.character_colors?.length
|
|
737
|
+
? p.character_colors.join(",")
|
|
738
|
+
: p.color ?? "None"));
|
|
498
739
|
return {
|
|
499
740
|
values: {
|
|
500
741
|
font: fonts.size === 1 ? [...fonts][0] : null,
|
|
@@ -533,9 +774,13 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
533
774
|
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
534
775
|
const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, textMaxWidth, fontSize + lineGap);
|
|
535
776
|
const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
536
|
-
const swatchX = x +
|
|
777
|
+
const swatchX = x +
|
|
778
|
+
Math.ceil(result.lastLineWidth) +
|
|
779
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
537
780
|
const swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
|
|
538
|
-
const colors = values.color.includes(",")
|
|
781
|
+
const colors = values.color.includes(",")
|
|
782
|
+
? values.color.split(",").map((s) => s.trim())
|
|
783
|
+
: [values.color];
|
|
539
784
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
540
785
|
cursorY += result.height;
|
|
541
786
|
rendered++;
|
|
@@ -607,7 +852,9 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
607
852
|
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
608
853
|
const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, textMaxWidth, otherFontSize + lineGap);
|
|
609
854
|
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
610
|
-
const swatchX = x +
|
|
855
|
+
const swatchX = x +
|
|
856
|
+
Math.ceil(result.lastLineWidth) +
|
|
857
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
611
858
|
const swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
612
859
|
const colors = position.character_colors || [position.color];
|
|
613
860
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
@@ -630,7 +877,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
630
877
|
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
631
878
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
632
879
|
let cursorY = y;
|
|
633
|
-
const iconText = position.icon === 0
|
|
880
|
+
const iconText = position.icon === 0
|
|
881
|
+
? `Icon: icon mặc định theo file thêu`
|
|
882
|
+
: `Icon: ${position.icon}`;
|
|
634
883
|
const iconResult = wrapText(ctx, iconText, x, cursorY, maxWidth, iconFontSize + lineGap);
|
|
635
884
|
// Draw icon image
|
|
636
885
|
if (position.icon !== 0) {
|
|
@@ -639,7 +888,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
639
888
|
if (img?.complete && img.naturalHeight > 0) {
|
|
640
889
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
641
890
|
const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
|
|
642
|
-
const iconX = x +
|
|
891
|
+
const iconX = x +
|
|
892
|
+
Math.ceil(iconResult.lastLineWidth) +
|
|
893
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
643
894
|
const iconY = iconResult.lastLineY + Math.floor(iconFontSize / 2 - iconFontSize / 2);
|
|
644
895
|
ctx.drawImage(img, iconX, iconY, swatchW, iconFontSize);
|
|
645
896
|
}
|
|
@@ -650,7 +901,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
650
901
|
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
651
902
|
const colorResult = wrapText(ctx, `Màu chỉ: ${position.layer_colors.join(", ")}`, x, cursorY, textMaxWidth, iconFontSize + lineGap);
|
|
652
903
|
const swatchH = Math.floor(iconFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
653
|
-
const swatchX = x +
|
|
904
|
+
const swatchX = x +
|
|
905
|
+
Math.ceil(colorResult.lastLineWidth) +
|
|
906
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
654
907
|
const swatchY = colorResult.lastLineY + Math.floor(iconFontSize / 2 - swatchH / 2);
|
|
655
908
|
drawSwatches(ctx, position.layer_colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
656
909
|
cursorY += colorResult.height;
|
|
@@ -658,6 +911,50 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
658
911
|
ctx.restore();
|
|
659
912
|
return cursorY - y;
|
|
660
913
|
};
|
|
914
|
+
const prepareExportCanvas = async (config, options = {}) => {
|
|
915
|
+
const { width = 4200, height = 4800 } = options;
|
|
916
|
+
const canvas = document.createElement("canvas");
|
|
917
|
+
const imageRefs = {
|
|
918
|
+
current: new Map(),
|
|
919
|
+
};
|
|
920
|
+
await preloadFonts(config);
|
|
921
|
+
await preloadImages(config, imageRefs);
|
|
922
|
+
renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
|
|
923
|
+
if (!canvas.width || !canvas.height) {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
return canvas;
|
|
927
|
+
};
|
|
928
|
+
const generateEmbroideryQCImageBlob = async (config, options = {}) => {
|
|
929
|
+
if (typeof document === "undefined") {
|
|
930
|
+
throw new Error("generateEmbroideryQCImageBlob requires a browser environment.");
|
|
931
|
+
}
|
|
932
|
+
const { mimeType = "image/png", quality } = options;
|
|
933
|
+
const canvas = await prepareExportCanvas(config, options);
|
|
934
|
+
if (!canvas || typeof canvas.toBlob !== "function") {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
const blob = await new Promise((resolve) => {
|
|
938
|
+
canvas.toBlob((result) => resolve(result), mimeType, quality);
|
|
939
|
+
});
|
|
940
|
+
return blob;
|
|
941
|
+
};
|
|
942
|
+
const generateEmbroideryQCImageData = async (config, options = {}) => {
|
|
943
|
+
if (typeof document === "undefined") {
|
|
944
|
+
throw new Error("generateEmbroideryQCImageData requires a browser environment.");
|
|
945
|
+
}
|
|
946
|
+
const { mimeType = "image/png", quality } = options;
|
|
947
|
+
const canvas = await prepareExportCanvas(config, options);
|
|
948
|
+
if (!canvas) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
if (mimeType === "image/png" || typeof quality === "undefined") {
|
|
952
|
+
return canvas.toDataURL(mimeType);
|
|
953
|
+
}
|
|
954
|
+
return canvas.toDataURL(mimeType, quality);
|
|
955
|
+
};
|
|
661
956
|
|
|
662
957
|
exports.EmbroideryQCImage = EmbroideryQCImage;
|
|
958
|
+
exports.generateEmbroideryQCImageBlob = generateEmbroideryQCImageBlob;
|
|
959
|
+
exports.generateEmbroideryQCImageData = generateEmbroideryQCImageData;
|
|
663
960
|
//# sourceMappingURL=index.js.map
|