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.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_message || !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,31 @@ 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
|
+
};
|
|
374
|
+
const isLightColor = (colorName) => {
|
|
375
|
+
return (colorName === "White" ||
|
|
376
|
+
colorName === "White (9)" ||
|
|
377
|
+
colorName === "Ivory" ||
|
|
378
|
+
colorName === "Ivory (1072)");
|
|
379
|
+
};
|
|
168
380
|
const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
169
381
|
const words = text.split(" ");
|
|
170
382
|
const lines = [];
|
|
@@ -185,6 +397,7 @@ const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
|
185
397
|
}
|
|
186
398
|
}
|
|
187
399
|
lines.push(currentLine);
|
|
400
|
+
const hasLightColor = colors.some(isLightColor);
|
|
188
401
|
let currentY = y;
|
|
189
402
|
lines.forEach((line, lineIdx) => {
|
|
190
403
|
let currentX = x;
|
|
@@ -194,7 +407,12 @@ const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
|
194
407
|
const globalCharIdx = startCharIdx + i;
|
|
195
408
|
const colorIndex = globalCharIdx % colors.length;
|
|
196
409
|
const color = colors[colorIndex];
|
|
197
|
-
|
|
410
|
+
if (hasLightColor) {
|
|
411
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
ctx.fillStyle = COLOR_MAP[color] || LAYOUT.LABEL_COLOR;
|
|
415
|
+
}
|
|
198
416
|
ctx.fillText(char, currentX, currentY);
|
|
199
417
|
currentX += ctx.measureText(char).width;
|
|
200
418
|
}
|
|
@@ -227,7 +445,7 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
227
445
|
// Load fonts
|
|
228
446
|
react.useEffect(() => {
|
|
229
447
|
const loadFonts = async () => {
|
|
230
|
-
if (!config.sides?.length)
|
|
448
|
+
if (config.error_message || !config.sides?.length)
|
|
231
449
|
return;
|
|
232
450
|
const fontsToLoad = new Set();
|
|
233
451
|
config.sides.forEach((side) => {
|
|
@@ -253,23 +471,12 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
253
471
|
}, [config.sides, loadedFonts]);
|
|
254
472
|
// Load images
|
|
255
473
|
react.useEffect(() => {
|
|
256
|
-
if (!config.sides?.length)
|
|
474
|
+
if (config.error_message || !config.sides?.length)
|
|
257
475
|
return;
|
|
258
476
|
const incrementCounter = () => setImagesLoaded((prev) => prev + 1);
|
|
259
477
|
// Load mockup
|
|
260
478
|
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);
|
|
479
|
+
loadImage(config.image_url, imageRefs, incrementCounter);
|
|
273
480
|
}
|
|
274
481
|
// Load all other images
|
|
275
482
|
config.sides.forEach((side) => {
|
|
@@ -299,61 +506,9 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
299
506
|
// Render canvas
|
|
300
507
|
react.useEffect(() => {
|
|
301
508
|
const renderCanvas = () => {
|
|
302
|
-
if (!canvasRef.current
|
|
303
|
-
return;
|
|
304
|
-
const canvas = canvasRef.current;
|
|
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)
|
|
509
|
+
if (!canvasRef.current)
|
|
339
510
|
return;
|
|
340
|
-
|
|
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
|
-
});
|
|
511
|
+
renderEmbroideryCanvas(canvasRef.current, config, canvasSize, imageRefs);
|
|
357
512
|
};
|
|
358
513
|
const timer = setTimeout(renderCanvas, 100);
|
|
359
514
|
return () => clearTimeout(timer);
|
|
@@ -363,6 +518,102 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
363
518
|
// ============================================================================
|
|
364
519
|
// RENDERING FUNCTIONS
|
|
365
520
|
// ============================================================================
|
|
521
|
+
const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
522
|
+
const ctx = canvas.getContext("2d");
|
|
523
|
+
if (!ctx)
|
|
524
|
+
return;
|
|
525
|
+
canvas.width = canvasSize.width;
|
|
526
|
+
canvas.height = canvasSize.height;
|
|
527
|
+
ctx.fillStyle = LAYOUT.BACKGROUND_COLOR;
|
|
528
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
529
|
+
if (config.error_message) {
|
|
530
|
+
renderErrorState(ctx, canvas, config.error_message);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (!config.sides?.length)
|
|
534
|
+
return;
|
|
535
|
+
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
536
|
+
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
537
|
+
if (config.image_url) {
|
|
538
|
+
const mockupImage = imageRefs.current.get(config.image_url);
|
|
539
|
+
if (mockupImage) {
|
|
540
|
+
imageRefs.current.set("mockup", mockupImage);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const floralAssets = [];
|
|
544
|
+
const seenFlorals = new Set();
|
|
545
|
+
config.sides.forEach((side) => {
|
|
546
|
+
side.positions.forEach((position) => {
|
|
547
|
+
if (position.type === "TEXT" && position.floral_pattern) {
|
|
548
|
+
const url = getImageUrl("floral", position.floral_pattern);
|
|
549
|
+
if (!seenFlorals.has(url)) {
|
|
550
|
+
const img = imageRefs.current.get(url);
|
|
551
|
+
if (img?.complete && img.naturalWidth > 0) {
|
|
552
|
+
floralAssets.push(img);
|
|
553
|
+
seenFlorals.add(url);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
const measureCanvas = document.createElement("canvas");
|
|
560
|
+
measureCanvas.width = canvas.width;
|
|
561
|
+
measureCanvas.height = canvas.height;
|
|
562
|
+
const measureCtx = measureCanvas.getContext("2d");
|
|
563
|
+
if (!measureCtx)
|
|
564
|
+
return;
|
|
565
|
+
measureCtx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
566
|
+
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
567
|
+
let measureY = LAYOUT.PADDING;
|
|
568
|
+
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
569
|
+
config.sides.forEach((side) => {
|
|
570
|
+
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
|
|
571
|
+
measureY += sideHeight + measureSpacing;
|
|
572
|
+
});
|
|
573
|
+
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING) / measureY));
|
|
574
|
+
drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
|
|
575
|
+
let currentY = LAYOUT.PADDING * scaleFactor;
|
|
576
|
+
config.sides.forEach((side) => {
|
|
577
|
+
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
578
|
+
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
579
|
+
});
|
|
580
|
+
};
|
|
581
|
+
const renderErrorState = (ctx, canvas, message) => {
|
|
582
|
+
const sanitizedMessage = message.trim() || "Đã xảy ra lỗi";
|
|
583
|
+
const horizontalPadding = LAYOUT.PADDING * 3;
|
|
584
|
+
const maxWidth = canvas.width - horizontalPadding * 2;
|
|
585
|
+
const baseFontSize = LAYOUT.HEADER_FONT_SIZE;
|
|
586
|
+
const minFontSize = 60;
|
|
587
|
+
const centerX = canvas.width / 2;
|
|
588
|
+
ctx.save();
|
|
589
|
+
ctx.textAlign = "center";
|
|
590
|
+
ctx.textBaseline = "top";
|
|
591
|
+
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
592
|
+
let fontSize = baseFontSize;
|
|
593
|
+
let lineGap = LAYOUT.LINE_GAP;
|
|
594
|
+
let lineHeight = fontSize + lineGap;
|
|
595
|
+
const adjustMetrics = () => {
|
|
596
|
+
ctx.font = `bold ${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
597
|
+
lineGap = LAYOUT.LINE_GAP * (fontSize / baseFontSize);
|
|
598
|
+
lineHeight = fontSize + lineGap;
|
|
599
|
+
};
|
|
600
|
+
adjustMetrics();
|
|
601
|
+
let lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
602
|
+
let longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
603
|
+
while (longestLineWidth > maxWidth && fontSize > minFontSize) {
|
|
604
|
+
fontSize = Math.max(minFontSize, Math.floor(fontSize * 0.9));
|
|
605
|
+
adjustMetrics();
|
|
606
|
+
lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
607
|
+
longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
608
|
+
}
|
|
609
|
+
const totalHeight = lines.length * lineHeight;
|
|
610
|
+
const startY = Math.max(LAYOUT.PADDING * 2, canvas.height / 2 - totalHeight / 2);
|
|
611
|
+
lines.forEach((line, index) => {
|
|
612
|
+
const y = startY + index * lineHeight;
|
|
613
|
+
ctx.fillText(line, centerX, y);
|
|
614
|
+
});
|
|
615
|
+
ctx.restore();
|
|
616
|
+
};
|
|
366
617
|
const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
|
|
367
618
|
const mockupImg = imageRefs.current.get("mockup");
|
|
368
619
|
if (!mockupImg?.complete || !mockupImg.naturalWidth)
|
|
@@ -448,7 +699,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
|
448
699
|
side.positions.forEach((position) => {
|
|
449
700
|
if (position.type === "ICON") {
|
|
450
701
|
currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs);
|
|
451
|
-
currentY += LAYOUT.LINE_GAP / 3 * scaleFactor;
|
|
702
|
+
currentY += (LAYOUT.LINE_GAP / 3) * scaleFactor;
|
|
452
703
|
}
|
|
453
704
|
});
|
|
454
705
|
return currentY - startY;
|
|
@@ -494,7 +745,9 @@ const computeUniformProperties = (textPositions) => {
|
|
|
494
745
|
const fonts = new Set(textPositions.map((p) => p.font));
|
|
495
746
|
const shapes = new Set(textPositions.map((p) => p.text_shape));
|
|
496
747
|
const florals = new Set(textPositions.map((p) => p.floral_pattern ?? "None"));
|
|
497
|
-
const colors = new Set(textPositions.map((p) => p.character_colors?.length
|
|
748
|
+
const colors = new Set(textPositions.map((p) => p.character_colors?.length
|
|
749
|
+
? p.character_colors.join(",")
|
|
750
|
+
: p.color ?? "None"));
|
|
498
751
|
return {
|
|
499
752
|
values: {
|
|
500
753
|
font: fonts.size === 1 ? [...fonts][0] : null,
|
|
@@ -533,9 +786,13 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
|
|
|
533
786
|
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
534
787
|
const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, textMaxWidth, fontSize + lineGap);
|
|
535
788
|
const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
536
|
-
const swatchX = x +
|
|
789
|
+
const swatchX = x +
|
|
790
|
+
Math.ceil(result.lastLineWidth) +
|
|
791
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
537
792
|
const swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
|
|
538
|
-
const colors = values.color.includes(",")
|
|
793
|
+
const colors = values.color.includes(",")
|
|
794
|
+
? values.color.split(",").map((s) => s.trim())
|
|
795
|
+
: [values.color];
|
|
539
796
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
540
797
|
cursorY += result.height;
|
|
541
798
|
rendered++;
|
|
@@ -570,7 +827,7 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
570
827
|
if (isEmptyText) {
|
|
571
828
|
ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
572
829
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
573
|
-
const textResult = wrapText(ctx, "không
|
|
830
|
+
const textResult = wrapText(ctx, "(không có text)", x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
574
831
|
currentY += textResult.height;
|
|
575
832
|
drawnHeight += textResult.height;
|
|
576
833
|
}
|
|
@@ -582,7 +839,8 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
582
839
|
}
|
|
583
840
|
else {
|
|
584
841
|
ctx.font = `${textFontSize}px ${position.font}`;
|
|
585
|
-
|
|
842
|
+
const isLight = isLightColor(position.color ?? "");
|
|
843
|
+
ctx.fillStyle = isLight ? LAYOUT.LABEL_COLOR : (COLOR_MAP[position.color ?? "None"] || "#000000");
|
|
586
844
|
const textResult = wrapText(ctx, position.text, x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
587
845
|
currentY += textResult.height;
|
|
588
846
|
drawnHeight += textResult.height;
|
|
@@ -607,7 +865,9 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
|
|
|
607
865
|
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
608
866
|
const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, textMaxWidth, otherFontSize + lineGap);
|
|
609
867
|
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
610
|
-
const swatchX = x +
|
|
868
|
+
const swatchX = x +
|
|
869
|
+
Math.ceil(result.lastLineWidth) +
|
|
870
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
611
871
|
const swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
612
872
|
const colors = position.character_colors || [position.color];
|
|
613
873
|
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
@@ -630,7 +890,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
630
890
|
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
631
891
|
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
632
892
|
let cursorY = y;
|
|
633
|
-
const iconText = position.icon === 0
|
|
893
|
+
const iconText = position.icon === 0
|
|
894
|
+
? `Icon: (icon mặc định theo file thêu)`
|
|
895
|
+
: `Icon: ${position.icon}`;
|
|
634
896
|
const iconResult = wrapText(ctx, iconText, x, cursorY, maxWidth, iconFontSize + lineGap);
|
|
635
897
|
// Draw icon image
|
|
636
898
|
if (position.icon !== 0) {
|
|
@@ -639,7 +901,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
639
901
|
if (img?.complete && img.naturalHeight > 0) {
|
|
640
902
|
const ratio = img.naturalWidth / img.naturalHeight;
|
|
641
903
|
const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
|
|
642
|
-
const iconX = x +
|
|
904
|
+
const iconX = x +
|
|
905
|
+
Math.ceil(iconResult.lastLineWidth) +
|
|
906
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
643
907
|
const iconY = iconResult.lastLineY + Math.floor(iconFontSize / 2 - iconFontSize / 2);
|
|
644
908
|
ctx.drawImage(img, iconX, iconY, swatchW, iconFontSize);
|
|
645
909
|
}
|
|
@@ -650,7 +914,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
650
914
|
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
651
915
|
const colorResult = wrapText(ctx, `Màu chỉ: ${position.layer_colors.join(", ")}`, x, cursorY, textMaxWidth, iconFontSize + lineGap);
|
|
652
916
|
const swatchH = Math.floor(iconFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
653
|
-
const swatchX = x +
|
|
917
|
+
const swatchX = x +
|
|
918
|
+
Math.ceil(colorResult.lastLineWidth) +
|
|
919
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
654
920
|
const swatchY = colorResult.lastLineY + Math.floor(iconFontSize / 2 - swatchH / 2);
|
|
655
921
|
drawSwatches(ctx, position.layer_colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
656
922
|
cursorY += colorResult.height;
|
|
@@ -658,6 +924,50 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
|
|
|
658
924
|
ctx.restore();
|
|
659
925
|
return cursorY - y;
|
|
660
926
|
};
|
|
927
|
+
const prepareExportCanvas = async (config, options = {}) => {
|
|
928
|
+
const { width = 4200, height = 4800 } = options;
|
|
929
|
+
const canvas = document.createElement("canvas");
|
|
930
|
+
const imageRefs = {
|
|
931
|
+
current: new Map(),
|
|
932
|
+
};
|
|
933
|
+
await preloadFonts(config);
|
|
934
|
+
await preloadImages(config, imageRefs);
|
|
935
|
+
renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
|
|
936
|
+
if (!canvas.width || !canvas.height) {
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
return canvas;
|
|
940
|
+
};
|
|
941
|
+
const generateEmbroideryQCImageBlob = async (config, options = {}) => {
|
|
942
|
+
if (typeof document === "undefined") {
|
|
943
|
+
throw new Error("generateEmbroideryQCImageBlob requires a browser environment.");
|
|
944
|
+
}
|
|
945
|
+
const { mimeType = "image/png", quality } = options;
|
|
946
|
+
const canvas = await prepareExportCanvas(config, options);
|
|
947
|
+
if (!canvas || typeof canvas.toBlob !== "function") {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
const blob = await new Promise((resolve) => {
|
|
951
|
+
canvas.toBlob((result) => resolve(result), mimeType, quality);
|
|
952
|
+
});
|
|
953
|
+
return blob;
|
|
954
|
+
};
|
|
955
|
+
const generateEmbroideryQCImageData = async (config, options = {}) => {
|
|
956
|
+
if (typeof document === "undefined") {
|
|
957
|
+
throw new Error("generateEmbroideryQCImageData requires a browser environment.");
|
|
958
|
+
}
|
|
959
|
+
const { mimeType = "image/png", quality } = options;
|
|
960
|
+
const canvas = await prepareExportCanvas(config, options);
|
|
961
|
+
if (!canvas) {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
if (mimeType === "image/png" || typeof quality === "undefined") {
|
|
965
|
+
return canvas.toDataURL(mimeType);
|
|
966
|
+
}
|
|
967
|
+
return canvas.toDataURL(mimeType, quality);
|
|
968
|
+
};
|
|
661
969
|
|
|
662
970
|
exports.EmbroideryQCImage = EmbroideryQCImage;
|
|
971
|
+
exports.generateEmbroideryQCImageBlob = generateEmbroideryQCImageBlob;
|
|
972
|
+
exports.generateEmbroideryQCImageData = generateEmbroideryQCImageData;
|
|
663
973
|
//# sourceMappingURL=index.js.map
|