embroidery-qc-image 1.0.7 → 1.0.9
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 +423 -115
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +424 -114
- 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.esm.js
CHANGED
|
@@ -35,37 +35,68 @@ styleInject(css_248z);
|
|
|
35
35
|
// CONSTANTS
|
|
36
36
|
// ============================================================================
|
|
37
37
|
const COLOR_MAP = {
|
|
38
|
-
"Army (1394)": "#4B5320",
|
|
39
|
-
|
|
40
|
-
"
|
|
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
|
-
|
|
38
|
+
"Army (1394)": "#4B5320",
|
|
39
|
+
Army: "#4B5320",
|
|
40
|
+
"Black (8)": "#000000",
|
|
41
|
+
Black: "#000000",
|
|
42
|
+
"Bubblegum (1309)": "#FFC1CC",
|
|
43
|
+
Bubblegum: "#FFC1CC",
|
|
44
|
+
"Carolina Blue (1274)": "#7BAFD4",
|
|
45
|
+
"Carolina Blue": "#7BAFD4",
|
|
46
|
+
"Celadon (1098)": "#ACE1AF",
|
|
47
|
+
Celadon: "#ACE1AF",
|
|
48
|
+
"Coffee Bean (1145)": "#6F4E37",
|
|
49
|
+
"Coffee Bean": "#6F4E37",
|
|
50
|
+
"Daffodil (1180)": "#FFFF31",
|
|
51
|
+
Daffodil: "#FFFF31",
|
|
52
|
+
"Dark Gray (1131)": "#A9A9A9",
|
|
53
|
+
"Dark Gray": "#A9A9A9",
|
|
54
|
+
"Doe Skin Beige (1344)": "#F5E6D3",
|
|
55
|
+
"Doe Skin Beige": "#F5E6D3",
|
|
56
|
+
"Dusty Blue (1373)": "#6699CC",
|
|
57
|
+
"Dusty Blue": "#6699CC",
|
|
58
|
+
"Forest Green (1397)": "#228B22",
|
|
59
|
+
"Forest Green": "#228B22",
|
|
60
|
+
"Gold (1425)": "#FFD700",
|
|
61
|
+
Gold: "#FFD700",
|
|
62
|
+
"Gray (1118)": "#808080",
|
|
63
|
+
Gray: "#808080",
|
|
64
|
+
"Ivory (1072)": "#FFFFF0",
|
|
65
|
+
Ivory: "#FFFFF0",
|
|
66
|
+
"Lavender (1032)": "#E6E6FA",
|
|
67
|
+
Lavender: "#E6E6FA",
|
|
68
|
+
"Light Denim (1133)": "#B0C4DE",
|
|
69
|
+
"Light Denim": "#B0C4DE",
|
|
70
|
+
"Light Salmon (1018)": "#FFA07A",
|
|
71
|
+
"Light Salmon": "#FFA07A",
|
|
72
|
+
"Maroon (1374)": "#800000",
|
|
73
|
+
Maroon: "#800000",
|
|
74
|
+
"Navy Blue (1044)": "#000080",
|
|
75
|
+
"Navy Blue": "#000080",
|
|
76
|
+
"Olive Green (1157)": "#556B2F",
|
|
77
|
+
"Olive Green": "#556B2F",
|
|
78
|
+
"Orange (1278)": "#FFA500",
|
|
79
|
+
Orange: "#FFA500",
|
|
80
|
+
"Peach Blush (1053)": "#FFCCCB",
|
|
81
|
+
"Peach Blush": "#FFCCCB",
|
|
82
|
+
"Pink (1148)": "#FFC0CB",
|
|
83
|
+
Pink: "#FFC0CB",
|
|
84
|
+
"Purple (1412)": "#800080",
|
|
85
|
+
Purple: "#800080",
|
|
86
|
+
"Red (1037)": "#FF0000",
|
|
87
|
+
Red: "#FF0000",
|
|
88
|
+
"Silver Sage (1396)": "#A8A8A8",
|
|
89
|
+
"Silver Sage": "#A8A8A8",
|
|
90
|
+
"Summer Sky (1432)": "#87CEEB",
|
|
91
|
+
"Summer Sky": "#87CEEB",
|
|
92
|
+
"Terra Cotta (1477)": "#E2725B",
|
|
93
|
+
"Terra Cotta": "#E2725B",
|
|
94
|
+
"Sand (1055)": "#F4A460",
|
|
95
|
+
Sand: "#F4A460",
|
|
96
|
+
"White (9)": "#FFFFFF",
|
|
97
|
+
White: "#FFFFFF",
|
|
68
98
|
};
|
|
99
|
+
const DEFAULT_ERROR_COLOR = "#CC1F1A";
|
|
69
100
|
const BASE_URLS = {
|
|
70
101
|
FONT: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/fonts",
|
|
71
102
|
ICON: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/icons",
|
|
@@ -128,14 +159,170 @@ const getImageUrl = (type, value) => {
|
|
|
128
159
|
return `${BASE_URLS.FLORAL}/${value}.png`;
|
|
129
160
|
return `${BASE_URLS.THREAD_COLOR}/${value}.png`;
|
|
130
161
|
};
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
162
|
+
const getProxyUrl = (url) => `https://proxy-img.c8p.workers.dev?url=${encodeURIComponent(url)}`;
|
|
163
|
+
const ensureImage = (existing) => {
|
|
164
|
+
if (existing && existing.crossOrigin === "anonymous") {
|
|
165
|
+
return existing;
|
|
166
|
+
}
|
|
134
167
|
const img = new Image();
|
|
135
168
|
img.crossOrigin = "anonymous";
|
|
136
|
-
img.
|
|
137
|
-
img
|
|
169
|
+
img.decoding = "async";
|
|
170
|
+
return img;
|
|
171
|
+
};
|
|
172
|
+
const loadImage = (url, imageRefs, onLoad) => {
|
|
173
|
+
const existing = imageRefs.current.get(url);
|
|
174
|
+
if (existing?.complete &&
|
|
175
|
+
existing.naturalWidth > 0 &&
|
|
176
|
+
existing.crossOrigin === "anonymous") {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const img = ensureImage(existing);
|
|
138
180
|
imageRefs.current.set(url, img);
|
|
181
|
+
let attemptedProxy = existing?.dataset?.proxyUsed === "true";
|
|
182
|
+
const cleanup = () => {
|
|
183
|
+
img.onload = null;
|
|
184
|
+
img.onerror = null;
|
|
185
|
+
};
|
|
186
|
+
img.onload = () => {
|
|
187
|
+
img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
188
|
+
cleanup();
|
|
189
|
+
onLoad();
|
|
190
|
+
};
|
|
191
|
+
img.onerror = () => {
|
|
192
|
+
if (!attemptedProxy) {
|
|
193
|
+
attemptedProxy = true;
|
|
194
|
+
img.src = getProxyUrl(url);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
198
|
+
cleanup();
|
|
199
|
+
onLoad();
|
|
200
|
+
};
|
|
201
|
+
img.src = attemptedProxy ? getProxyUrl(url) : url;
|
|
202
|
+
};
|
|
203
|
+
const loadImageAsync = (url, imageRefs, cacheKey) => {
|
|
204
|
+
const key = cacheKey ?? url;
|
|
205
|
+
const existing = imageRefs.current.get(key) ?? imageRefs.current.get(url);
|
|
206
|
+
if (existing?.complete &&
|
|
207
|
+
existing.naturalWidth > 0 &&
|
|
208
|
+
existing.crossOrigin === "anonymous" &&
|
|
209
|
+
existing.dataset?.proxyUsed !== undefined) {
|
|
210
|
+
if (existing !== imageRefs.current.get(key)) {
|
|
211
|
+
imageRefs.current.set(key, existing);
|
|
212
|
+
}
|
|
213
|
+
if (existing !== imageRefs.current.get(url)) {
|
|
214
|
+
imageRefs.current.set(url, existing);
|
|
215
|
+
}
|
|
216
|
+
return Promise.resolve(existing);
|
|
217
|
+
}
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
const target = ensureImage(existing);
|
|
220
|
+
if (target !== existing) {
|
|
221
|
+
imageRefs.current.set(key, target);
|
|
222
|
+
imageRefs.current.set(url, target);
|
|
223
|
+
}
|
|
224
|
+
let attemptedProxy = target.dataset.proxyUsed === "true";
|
|
225
|
+
const finalize = () => {
|
|
226
|
+
target.onload = null;
|
|
227
|
+
target.onerror = null;
|
|
228
|
+
if (target.complete && target.naturalWidth > 0) {
|
|
229
|
+
imageRefs.current.set(key, target);
|
|
230
|
+
imageRefs.current.set(url, target);
|
|
231
|
+
}
|
|
232
|
+
target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
233
|
+
resolve(target);
|
|
234
|
+
};
|
|
235
|
+
target.onload = finalize;
|
|
236
|
+
target.onerror = () => {
|
|
237
|
+
if (!attemptedProxy) {
|
|
238
|
+
attemptedProxy = true;
|
|
239
|
+
target.src = getProxyUrl(url);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
|
|
243
|
+
finalize();
|
|
244
|
+
};
|
|
245
|
+
const desiredSrc = attemptedProxy ? getProxyUrl(url) : url;
|
|
246
|
+
if (target.src !== desiredSrc) {
|
|
247
|
+
target.src = desiredSrc;
|
|
248
|
+
}
|
|
249
|
+
else if (target.complete && target.naturalWidth > 0) {
|
|
250
|
+
finalize();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
const preloadFonts = async (config) => {
|
|
255
|
+
if (config.error_message || !config.sides?.length)
|
|
256
|
+
return;
|
|
257
|
+
const fonts = new Set();
|
|
258
|
+
config.sides.forEach((side) => {
|
|
259
|
+
side.positions.forEach((position) => {
|
|
260
|
+
if (position.type === "TEXT" && position.font) {
|
|
261
|
+
fonts.add(position.font);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
if (fonts.size === 0)
|
|
266
|
+
return;
|
|
267
|
+
await Promise.all([...fonts].map((font) => loadFont(font)));
|
|
268
|
+
};
|
|
269
|
+
const preloadImages = async (config, imageRefs) => {
|
|
270
|
+
const entries = [];
|
|
271
|
+
const seen = new Set();
|
|
272
|
+
if (config.image_url) {
|
|
273
|
+
entries.push({ url: config.image_url, cacheKey: "mockup" });
|
|
274
|
+
seen.add(config.image_url);
|
|
275
|
+
}
|
|
276
|
+
if (!config.sides?.length) {
|
|
277
|
+
await Promise.all(entries.map(({ url, cacheKey }) => loadImageAsync(url, imageRefs, cacheKey)));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
config.sides.forEach((side) => {
|
|
281
|
+
side.positions.forEach((position) => {
|
|
282
|
+
if (position.type === "ICON") {
|
|
283
|
+
if (position.icon !== 0) {
|
|
284
|
+
const iconUrl = getImageUrl("icon", position.icon);
|
|
285
|
+
if (!seen.has(iconUrl)) {
|
|
286
|
+
entries.push({ url: iconUrl });
|
|
287
|
+
seen.add(iconUrl);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
position.layer_colors?.forEach((color) => {
|
|
291
|
+
const colorUrl = getImageUrl("threadColor", color);
|
|
292
|
+
if (!seen.has(colorUrl)) {
|
|
293
|
+
entries.push({ url: colorUrl });
|
|
294
|
+
seen.add(colorUrl);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (position.type === "TEXT") {
|
|
299
|
+
if (position.floral_pattern) {
|
|
300
|
+
const floralUrl = getImageUrl("floral", position.floral_pattern);
|
|
301
|
+
if (!seen.has(floralUrl)) {
|
|
302
|
+
entries.push({ url: floralUrl });
|
|
303
|
+
seen.add(floralUrl);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (position.color) {
|
|
307
|
+
const threadUrl = getImageUrl("threadColor", position.color);
|
|
308
|
+
if (!seen.has(threadUrl)) {
|
|
309
|
+
entries.push({ url: threadUrl });
|
|
310
|
+
seen.add(threadUrl);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
position.character_colors?.forEach((color) => {
|
|
314
|
+
const characterColorUrl = getImageUrl("threadColor", color);
|
|
315
|
+
if (!seen.has(characterColorUrl)) {
|
|
316
|
+
entries.push({ url: characterColorUrl });
|
|
317
|
+
seen.add(characterColorUrl);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
if (entries.length === 0)
|
|
324
|
+
return;
|
|
325
|
+
await Promise.all(entries.map(({ url, cacheKey }) => loadImageAsync(url, imageRefs, cacheKey)));
|
|
139
326
|
};
|
|
140
327
|
const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
141
328
|
const words = text.split(" ");
|
|
@@ -163,6 +350,31 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
|
163
350
|
lastLineY: y + (lines.length - 1) * lineHeight,
|
|
164
351
|
};
|
|
165
352
|
};
|
|
353
|
+
const buildWrappedLines = (ctx, text, maxWidth) => {
|
|
354
|
+
const words = text.split(" ").filter((word) => word.length > 0);
|
|
355
|
+
if (words.length === 0)
|
|
356
|
+
return [""];
|
|
357
|
+
const lines = [];
|
|
358
|
+
let currentLine = words[0];
|
|
359
|
+
for (let i = 1; i < words.length; i++) {
|
|
360
|
+
const testLine = `${currentLine} ${words[i]}`;
|
|
361
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
362
|
+
lines.push(currentLine);
|
|
363
|
+
currentLine = words[i];
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
currentLine = testLine;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
lines.push(currentLine);
|
|
370
|
+
return lines;
|
|
371
|
+
};
|
|
372
|
+
const isLightColor = (colorName) => {
|
|
373
|
+
return (colorName === "White" ||
|
|
374
|
+
colorName === "White (9)" ||
|
|
375
|
+
colorName === "Ivory" ||
|
|
376
|
+
colorName === "Ivory (1072)");
|
|
377
|
+
};
|
|
166
378
|
const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
167
379
|
const words = text.split(" ");
|
|
168
380
|
const lines = [];
|
|
@@ -183,6 +395,7 @@ const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
|
183
395
|
}
|
|
184
396
|
}
|
|
185
397
|
lines.push(currentLine);
|
|
398
|
+
const hasLightColor = colors.some(isLightColor);
|
|
186
399
|
let currentY = y;
|
|
187
400
|
lines.forEach((line, lineIdx) => {
|
|
188
401
|
let currentX = x;
|
|
@@ -192,7 +405,12 @@ const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
|
192
405
|
const globalCharIdx = startCharIdx + i;
|
|
193
406
|
const colorIndex = globalCharIdx % colors.length;
|
|
194
407
|
const color = colors[colorIndex];
|
|
195
|
-
|
|
408
|
+
if (hasLightColor) {
|
|
409
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
ctx.fillStyle = COLOR_MAP[color] || LAYOUT.LABEL_COLOR;
|
|
413
|
+
}
|
|
196
414
|
ctx.fillText(char, currentX, currentY);
|
|
197
415
|
currentX += ctx.measureText(char).width;
|
|
198
416
|
}
|
|
@@ -225,7 +443,7 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
225
443
|
// Load fonts
|
|
226
444
|
useEffect(() => {
|
|
227
445
|
const loadFonts = async () => {
|
|
228
|
-
if (!config.sides?.length)
|
|
446
|
+
if (config.error_message || !config.sides?.length)
|
|
229
447
|
return;
|
|
230
448
|
const fontsToLoad = new Set();
|
|
231
449
|
config.sides.forEach((side) => {
|
|
@@ -251,23 +469,12 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
251
469
|
}, [config.sides, loadedFonts]);
|
|
252
470
|
// Load images
|
|
253
471
|
useEffect(() => {
|
|
254
|
-
if (!config.sides?.length)
|
|
472
|
+
if (config.error_message || !config.sides?.length)
|
|
255
473
|
return;
|
|
256
474
|
const incrementCounter = () => setImagesLoaded((prev) => prev + 1);
|
|
257
475
|
// Load mockup
|
|
258
476
|
if (config.image_url) {
|
|
259
|
-
|
|
260
|
-
const img = new Image();
|
|
261
|
-
if (useCors)
|
|
262
|
-
img.crossOrigin = "anonymous";
|
|
263
|
-
img.onload = () => {
|
|
264
|
-
imageRefs.current.set("mockup", img);
|
|
265
|
-
incrementCounter();
|
|
266
|
-
};
|
|
267
|
-
img.onerror = () => useCors && loadMockup(false);
|
|
268
|
-
img.src = config.image_url;
|
|
269
|
-
};
|
|
270
|
-
loadMockup(true);
|
|
477
|
+
loadImage(config.image_url, imageRefs, incrementCounter);
|
|
271
478
|
}
|
|
272
479
|
// Load all other images
|
|
273
480
|
config.sides.forEach((side) => {
|
|
@@ -297,61 +504,9 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
297
504
|
// Render canvas
|
|
298
505
|
useEffect(() => {
|
|
299
506
|
const renderCanvas = () => {
|
|
300
|
-
if (!canvasRef.current
|
|
301
|
-
return;
|
|
302
|
-
const canvas = canvasRef.current;
|
|
303
|
-
const ctx = canvas.getContext("2d");
|
|
304
|
-
if (!ctx)
|
|
305
|
-
return;
|
|
306
|
-
canvas.width = canvasSize.width;
|
|
307
|
-
canvas.height = canvasSize.height;
|
|
308
|
-
// Set text alignment once
|
|
309
|
-
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
310
|
-
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
311
|
-
// Clear background
|
|
312
|
-
ctx.fillStyle = LAYOUT.BACKGROUND_COLOR;
|
|
313
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
314
|
-
// Collect floral assets
|
|
315
|
-
const floralAssets = [];
|
|
316
|
-
const seenFlorals = new Set();
|
|
317
|
-
config.sides.forEach((side) => {
|
|
318
|
-
side.positions.forEach((position) => {
|
|
319
|
-
if (position.type === "TEXT" && position.floral_pattern) {
|
|
320
|
-
const url = getImageUrl("floral", position.floral_pattern);
|
|
321
|
-
if (!seenFlorals.has(url)) {
|
|
322
|
-
const img = imageRefs.current.get(url);
|
|
323
|
-
if (img?.complete && img.naturalWidth > 0) {
|
|
324
|
-
floralAssets.push(img);
|
|
325
|
-
seenFlorals.add(url);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
// Calculate scale factor
|
|
332
|
-
const measureCanvas = document.createElement("canvas");
|
|
333
|
-
measureCanvas.width = canvas.width;
|
|
334
|
-
measureCanvas.height = canvas.height;
|
|
335
|
-
const measureCtx = measureCanvas.getContext("2d");
|
|
336
|
-
if (!measureCtx)
|
|
507
|
+
if (!canvasRef.current)
|
|
337
508
|
return;
|
|
338
|
-
|
|
339
|
-
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
340
|
-
let measureY = LAYOUT.PADDING;
|
|
341
|
-
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
342
|
-
config.sides.forEach((side) => {
|
|
343
|
-
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
|
|
344
|
-
measureY += sideHeight + measureSpacing;
|
|
345
|
-
});
|
|
346
|
-
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING) / measureY));
|
|
347
|
-
// Draw mockup and florals
|
|
348
|
-
drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
|
|
349
|
-
// Draw content
|
|
350
|
-
let currentY = LAYOUT.PADDING * scaleFactor;
|
|
351
|
-
config.sides.forEach((side) => {
|
|
352
|
-
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
353
|
-
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
354
|
-
});
|
|
509
|
+
renderEmbroideryCanvas(canvasRef.current, config, canvasSize, imageRefs);
|
|
355
510
|
};
|
|
356
511
|
const timer = setTimeout(renderCanvas, 100);
|
|
357
512
|
return () => clearTimeout(timer);
|
|
@@ -361,6 +516,102 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
361
516
|
// ============================================================================
|
|
362
517
|
// RENDERING FUNCTIONS
|
|
363
518
|
// ============================================================================
|
|
519
|
+
const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
520
|
+
const ctx = canvas.getContext("2d");
|
|
521
|
+
if (!ctx)
|
|
522
|
+
return;
|
|
523
|
+
canvas.width = canvasSize.width;
|
|
524
|
+
canvas.height = canvasSize.height;
|
|
525
|
+
ctx.fillStyle = LAYOUT.BACKGROUND_COLOR;
|
|
526
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
527
|
+
if (config.error_message) {
|
|
528
|
+
renderErrorState(ctx, canvas, config.error_message);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (!config.sides?.length)
|
|
532
|
+
return;
|
|
533
|
+
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
534
|
+
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
535
|
+
if (config.image_url) {
|
|
536
|
+
const mockupImage = imageRefs.current.get(config.image_url);
|
|
537
|
+
if (mockupImage) {
|
|
538
|
+
imageRefs.current.set("mockup", mockupImage);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const floralAssets = [];
|
|
542
|
+
const seenFlorals = new Set();
|
|
543
|
+
config.sides.forEach((side) => {
|
|
544
|
+
side.positions.forEach((position) => {
|
|
545
|
+
if (position.type === "TEXT" && position.floral_pattern) {
|
|
546
|
+
const url = getImageUrl("floral", position.floral_pattern);
|
|
547
|
+
if (!seenFlorals.has(url)) {
|
|
548
|
+
const img = imageRefs.current.get(url);
|
|
549
|
+
if (img?.complete && img.naturalWidth > 0) {
|
|
550
|
+
floralAssets.push(img);
|
|
551
|
+
seenFlorals.add(url);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
const measureCanvas = document.createElement("canvas");
|
|
558
|
+
measureCanvas.width = canvas.width;
|
|
559
|
+
measureCanvas.height = canvas.height;
|
|
560
|
+
const measureCtx = measureCanvas.getContext("2d");
|
|
561
|
+
if (!measureCtx)
|
|
562
|
+
return;
|
|
563
|
+
measureCtx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
564
|
+
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
565
|
+
let measureY = LAYOUT.PADDING;
|
|
566
|
+
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
567
|
+
config.sides.forEach((side) => {
|
|
568
|
+
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
|
|
569
|
+
measureY += sideHeight + measureSpacing;
|
|
570
|
+
});
|
|
571
|
+
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING) / measureY));
|
|
572
|
+
drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
|
|
573
|
+
let currentY = LAYOUT.PADDING * scaleFactor;
|
|
574
|
+
config.sides.forEach((side) => {
|
|
575
|
+
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
576
|
+
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
577
|
+
});
|
|
578
|
+
};
|
|
579
|
+
const renderErrorState = (ctx, canvas, message) => {
|
|
580
|
+
const sanitizedMessage = message.trim() || "Đã xảy ra lỗi";
|
|
581
|
+
const horizontalPadding = LAYOUT.PADDING * 3;
|
|
582
|
+
const maxWidth = canvas.width - horizontalPadding * 2;
|
|
583
|
+
const baseFontSize = LAYOUT.HEADER_FONT_SIZE;
|
|
584
|
+
const minFontSize = 60;
|
|
585
|
+
const centerX = canvas.width / 2;
|
|
586
|
+
ctx.save();
|
|
587
|
+
ctx.textAlign = "center";
|
|
588
|
+
ctx.textBaseline = "top";
|
|
589
|
+
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
590
|
+
let fontSize = baseFontSize;
|
|
591
|
+
let lineGap = LAYOUT.LINE_GAP;
|
|
592
|
+
let lineHeight = fontSize + lineGap;
|
|
593
|
+
const adjustMetrics = () => {
|
|
594
|
+
ctx.font = `bold ${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
595
|
+
lineGap = LAYOUT.LINE_GAP * (fontSize / baseFontSize);
|
|
596
|
+
lineHeight = fontSize + lineGap;
|
|
597
|
+
};
|
|
598
|
+
adjustMetrics();
|
|
599
|
+
let lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
600
|
+
let longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
601
|
+
while (longestLineWidth > maxWidth && fontSize > minFontSize) {
|
|
602
|
+
fontSize = Math.max(minFontSize, Math.floor(fontSize * 0.9));
|
|
603
|
+
adjustMetrics();
|
|
604
|
+
lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
605
|
+
longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
606
|
+
}
|
|
607
|
+
const totalHeight = lines.length * lineHeight;
|
|
608
|
+
const startY = Math.max(LAYOUT.PADDING * 2, canvas.height / 2 - totalHeight / 2);
|
|
609
|
+
lines.forEach((line, index) => {
|
|
610
|
+
const y = startY + index * lineHeight;
|
|
611
|
+
ctx.fillText(line, centerX, y);
|
|
612
|
+
});
|
|
613
|
+
ctx.restore();
|
|
614
|
+
};
|
|
364
615
|
const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
|
|
365
616
|
const mockupImg = imageRefs.current.get("mockup");
|
|
366
617
|
if (!mockupImg?.complete || !mockupImg.naturalWidth)
|
|
@@ -446,7 +697,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
446
697
|
side.positions.forEach((position) => {
|
|
447
698
|
if (position.type === "ICON") {
|
|
448
699
|
currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs);
|
|
449
|
-
currentY += LAYOUT.LINE_GAP / 3 * scaleFactor;
|
|
700
|
+
currentY += (LAYOUT.LINE_GAP / 3) * scaleFactor;
|
|
450
701
|
}
|
|
451
702
|
});
|
|
452
703
|
return currentY - startY;
|
|
@@ -492,7 +743,9 @@ const computeUniformProperties = (textPositions) => {
|
|
|
492
743
|
const fonts = new Set(textPositions.map((p) => p.font));
|
|
493
744
|
const shapes = new Set(textPositions.map((p) => p.text_shape));
|
|
494
745
|
const florals = new Set(textPositions.map((p) => p.floral_pattern ?? "None"));
|
|
495
|
-
const colors = new Set(textPositions.map((p) => p.character_colors?.length
|
|
746
|
+
const colors = new Set(textPositions.map((p) => p.character_colors?.length
|
|
747
|
+
? p.character_colors.join(",")
|
|
748
|
+
: p.color ?? "None"));
|
|
496
749
|
return {
|
|
497
750
|
values: {
|
|
498
751
|
font: fonts.size === 1 ? [...fonts][0] : null,
|
|
@@ -531,9 +784,13 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
531
784
|
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
532
785
|
const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, textMaxWidth, fontSize + lineGap);
|
|
533
786
|
const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
534
|
-
const swatchX = x +
|
|
787
|
+
const swatchX = x +
|
|
788
|
+
Math.ceil(result.lastLineWidth) +
|
|
789
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
535
790
|
const swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
|
|
536
|
-
const colors = values.color.includes(",")
|
|
791
|
+
const colors = values.color.includes(",")
|
|
792
|
+
? values.color.split(",").map((s) => s.trim())
|
|
793
|
+
: [values.color];
|
|
537
794
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
538
795
|
cursorY += result.height;
|
|
539
796
|
rendered++;
|
|
@@ -568,7 +825,7 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
568
825
|
if (isEmptyText) {
|
|
569
826
|
ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
570
827
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
571
|
-
const textResult = wrapText(ctx, "không
|
|
828
|
+
const textResult = wrapText(ctx, "(không có text)", x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
572
829
|
currentY += textResult.height;
|
|
573
830
|
drawnHeight += textResult.height;
|
|
574
831
|
}
|
|
@@ -580,7 +837,8 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
580
837
|
}
|
|
581
838
|
else {
|
|
582
839
|
ctx.font = `${textFontSize}px ${position.font}`;
|
|
583
|
-
|
|
840
|
+
const isLight = isLightColor(position.color ?? "");
|
|
841
|
+
ctx.fillStyle = isLight ? LAYOUT.LABEL_COLOR : (COLOR_MAP[position.color ?? "None"] || "#000000");
|
|
584
842
|
const textResult = wrapText(ctx, position.text, x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
585
843
|
currentY += textResult.height;
|
|
586
844
|
drawnHeight += textResult.height;
|
|
@@ -605,7 +863,9 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
605
863
|
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
606
864
|
const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, textMaxWidth, otherFontSize + lineGap);
|
|
607
865
|
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
608
|
-
const swatchX = x +
|
|
866
|
+
const swatchX = x +
|
|
867
|
+
Math.ceil(result.lastLineWidth) +
|
|
868
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
609
869
|
const swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
610
870
|
const colors = position.character_colors || [position.color];
|
|
611
871
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
@@ -628,7 +888,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
628
888
|
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
629
889
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
630
890
|
let cursorY = y;
|
|
631
|
-
const iconText = position.icon === 0
|
|
891
|
+
const iconText = position.icon === 0
|
|
892
|
+
? `Icon: (icon mặc định theo file thêu)`
|
|
893
|
+
: `Icon: ${position.icon}`;
|
|
632
894
|
const iconResult = wrapText(ctx, iconText, x, cursorY, maxWidth, iconFontSize + lineGap);
|
|
633
895
|
// Draw icon image
|
|
634
896
|
if (position.icon !== 0) {
|
|
@@ -637,7 +899,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
637
899
|
if (img?.complete && img.naturalHeight > 0) {
|
|
638
900
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
639
901
|
const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
|
|
640
|
-
const iconX = x +
|
|
902
|
+
const iconX = x +
|
|
903
|
+
Math.ceil(iconResult.lastLineWidth) +
|
|
904
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
641
905
|
const iconY = iconResult.lastLineY + Math.floor(iconFontSize / 2 - iconFontSize / 2);
|
|
642
906
|
ctx.drawImage(img, iconX, iconY, swatchW, iconFontSize);
|
|
643
907
|
}
|
|
@@ -648,7 +912,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
648
912
|
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
649
913
|
const colorResult = wrapText(ctx, `Màu chỉ: ${position.layer_colors.join(", ")}`, x, cursorY, textMaxWidth, iconFontSize + lineGap);
|
|
650
914
|
const swatchH = Math.floor(iconFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
651
|
-
const swatchX = x +
|
|
915
|
+
const swatchX = x +
|
|
916
|
+
Math.ceil(colorResult.lastLineWidth) +
|
|
917
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
652
918
|
const swatchY = colorResult.lastLineY + Math.floor(iconFontSize / 2 - swatchH / 2);
|
|
653
919
|
drawSwatches(ctx, position.layer_colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
654
920
|
cursorY += colorResult.height;
|
|
@@ -656,6 +922,48 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
656
922
|
ctx.restore();
|
|
657
923
|
return cursorY - y;
|
|
658
924
|
};
|
|
925
|
+
const prepareExportCanvas = async (config, options = {}) => {
|
|
926
|
+
const { width = 4200, height = 4800 } = options;
|
|
927
|
+
const canvas = document.createElement("canvas");
|
|
928
|
+
const imageRefs = {
|
|
929
|
+
current: new Map(),
|
|
930
|
+
};
|
|
931
|
+
await preloadFonts(config);
|
|
932
|
+
await preloadImages(config, imageRefs);
|
|
933
|
+
renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
|
|
934
|
+
if (!canvas.width || !canvas.height) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
return canvas;
|
|
938
|
+
};
|
|
939
|
+
const generateEmbroideryQCImageBlob = async (config, options = {}) => {
|
|
940
|
+
if (typeof document === "undefined") {
|
|
941
|
+
throw new Error("generateEmbroideryQCImageBlob requires a browser environment.");
|
|
942
|
+
}
|
|
943
|
+
const { mimeType = "image/png", quality } = options;
|
|
944
|
+
const canvas = await prepareExportCanvas(config, options);
|
|
945
|
+
if (!canvas || typeof canvas.toBlob !== "function") {
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
const blob = await new Promise((resolve) => {
|
|
949
|
+
canvas.toBlob((result) => resolve(result), mimeType, quality);
|
|
950
|
+
});
|
|
951
|
+
return blob;
|
|
952
|
+
};
|
|
953
|
+
const generateEmbroideryQCImageData = async (config, options = {}) => {
|
|
954
|
+
if (typeof document === "undefined") {
|
|
955
|
+
throw new Error("generateEmbroideryQCImageData requires a browser environment.");
|
|
956
|
+
}
|
|
957
|
+
const { mimeType = "image/png", quality } = options;
|
|
958
|
+
const canvas = await prepareExportCanvas(config, options);
|
|
959
|
+
if (!canvas) {
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
if (mimeType === "image/png" || typeof quality === "undefined") {
|
|
963
|
+
return canvas.toDataURL(mimeType);
|
|
964
|
+
}
|
|
965
|
+
return canvas.toDataURL(mimeType, quality);
|
|
966
|
+
};
|
|
659
967
|
|
|
660
|
-
export { EmbroideryQCImage };
|
|
968
|
+
export { EmbroideryQCImage, generateEmbroideryQCImageBlob, generateEmbroideryQCImageData };
|
|
661
969
|
//# sourceMappingURL=index.esm.js.map
|