embroidery-qc-image 1.0.6 → 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 +802 -628
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +803 -627
- 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
|
@@ -31,7 +31,9 @@ function styleInject(css, ref) {
|
|
|
31
31
|
var css_248z = ".render-embroidery {\r\n display: inline-block;\r\n position: relative;\r\n width: 100%;\r\n max-width: 100%;\r\n}\r\n\r\n.render-embroidery-canvas {\r\n display: block;\r\n width: 100%;\r\n height: auto;\r\n image-rendering: high-quality;\r\n background: transparent;\r\n}\r\n";
|
|
32
32
|
styleInject(css_248z);
|
|
33
33
|
|
|
34
|
-
//
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// CONSTANTS
|
|
36
|
+
// ============================================================================
|
|
35
37
|
const COLOR_MAP = {
|
|
36
38
|
"Army (1394)": "#4B5320",
|
|
37
39
|
Army: "#4B5320",
|
|
@@ -94,10 +96,332 @@ const COLOR_MAP = {
|
|
|
94
96
|
"White (9)": "#FFFFFF",
|
|
95
97
|
White: "#FFFFFF",
|
|
96
98
|
};
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
const DEFAULT_ERROR_COLOR = "#CC1F1A";
|
|
100
|
+
const BASE_URLS = {
|
|
101
|
+
FONT: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/fonts",
|
|
102
|
+
ICON: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/icons",
|
|
103
|
+
FLORAL: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/florals",
|
|
104
|
+
THREAD_COLOR: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/thread-colors",
|
|
105
|
+
};
|
|
106
|
+
const LAYOUT = {
|
|
107
|
+
// Font families
|
|
108
|
+
HEADER_FONT_FAMILY: "Times New Roman",
|
|
109
|
+
FONT_FAMILY: "Arial",
|
|
110
|
+
// Font sizes (base values, will be multiplied by scaleFactor)
|
|
111
|
+
HEADER_FONT_SIZE: 220,
|
|
112
|
+
TEXT_FONT_SIZE: 200,
|
|
113
|
+
OTHER_FONT_SIZE: 160,
|
|
114
|
+
// Colors
|
|
115
|
+
HEADER_COLOR: "#000000",
|
|
116
|
+
LABEL_COLOR: "#444444",
|
|
117
|
+
BACKGROUND_COLOR: "#FFFFFF",
|
|
118
|
+
// Text alignment
|
|
119
|
+
TEXT_ALIGN: "left",
|
|
120
|
+
TEXT_BASELINE: "top",
|
|
121
|
+
// Spacing
|
|
122
|
+
LINE_GAP: 40,
|
|
123
|
+
PADDING: 40,
|
|
124
|
+
SECTION_SPACING: 60,
|
|
125
|
+
ELEMENT_SPACING: 100,
|
|
126
|
+
SWATCH_SPACING: 25,
|
|
127
|
+
FLORAL_SPACING: 300,
|
|
128
|
+
// Visual styling
|
|
129
|
+
SWATCH_HEIGHT_RATIO: 2.025,
|
|
130
|
+
UNDERLINE_POSITION: 0.9,
|
|
131
|
+
UNDERLINE_WIDTH: 10,
|
|
132
|
+
// Swatch reserved space
|
|
133
|
+
SWATCH_RESERVED_SPACE: 1000,
|
|
134
|
+
MIN_TEXT_WIDTH: 400,
|
|
135
|
+
};
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// HELPER FUNCTIONS
|
|
138
|
+
// ============================================================================
|
|
139
|
+
const loadFont = (fontName) => {
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
const fontUrl = `${BASE_URLS.FONT}/${encodeURIComponent(fontName)}.woff2`;
|
|
142
|
+
const fontFace = new FontFace(fontName, `url(${fontUrl})`);
|
|
143
|
+
fontFace
|
|
144
|
+
.load()
|
|
145
|
+
.then((loadedFont) => {
|
|
146
|
+
document.fonts.add(loadedFont);
|
|
147
|
+
resolve();
|
|
148
|
+
})
|
|
149
|
+
.catch(() => {
|
|
150
|
+
console.warn(`Could not load font ${fontName} from CDN`);
|
|
151
|
+
resolve();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
const getImageUrl = (type, value) => {
|
|
156
|
+
if (type === "icon")
|
|
157
|
+
return `${BASE_URLS.ICON}/Icon ${value}.png`;
|
|
158
|
+
if (type === "floral")
|
|
159
|
+
return `${BASE_URLS.FLORAL}/${value}.png`;
|
|
160
|
+
return `${BASE_URLS.THREAD_COLOR}/${value}.png`;
|
|
161
|
+
};
|
|
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
|
+
}
|
|
167
|
+
const img = new Image();
|
|
168
|
+
img.crossOrigin = "anonymous";
|
|
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);
|
|
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 || !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)));
|
|
326
|
+
};
|
|
327
|
+
const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
328
|
+
const words = text.split(" ");
|
|
329
|
+
const lines = [];
|
|
330
|
+
let currentLine = words[0];
|
|
331
|
+
for (let i = 1; i < words.length; i++) {
|
|
332
|
+
const testLine = currentLine + " " + words[i];
|
|
333
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
334
|
+
lines.push(currentLine);
|
|
335
|
+
currentLine = words[i];
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
currentLine = testLine;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
lines.push(currentLine);
|
|
342
|
+
let currentY = y;
|
|
343
|
+
lines.forEach((line) => {
|
|
344
|
+
ctx.fillText(line, x, currentY);
|
|
345
|
+
currentY += lineHeight;
|
|
346
|
+
});
|
|
347
|
+
return {
|
|
348
|
+
height: lines.length * lineHeight,
|
|
349
|
+
lastLineWidth: ctx.measureText(lines[lines.length - 1]).width,
|
|
350
|
+
lastLineY: y + (lines.length - 1) * lineHeight,
|
|
351
|
+
};
|
|
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 wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
373
|
+
const words = text.split(" ");
|
|
374
|
+
const lines = [];
|
|
375
|
+
const lineStartIndices = [0];
|
|
376
|
+
let currentLine = words[0];
|
|
377
|
+
let currentCharIndex = words[0].length;
|
|
378
|
+
for (let i = 1; i < words.length; i++) {
|
|
379
|
+
const testLine = currentLine + " " + words[i];
|
|
380
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
381
|
+
lines.push(currentLine);
|
|
382
|
+
lineStartIndices.push(currentCharIndex + 1);
|
|
383
|
+
currentLine = words[i];
|
|
384
|
+
currentCharIndex += words[i].length + 1;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
currentLine = testLine;
|
|
388
|
+
currentCharIndex += words[i].length + 1;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
lines.push(currentLine);
|
|
392
|
+
let currentY = y;
|
|
393
|
+
lines.forEach((line, lineIdx) => {
|
|
394
|
+
let currentX = x;
|
|
395
|
+
const startCharIdx = lineIdx > 0 ? lineStartIndices[lineIdx] : 0;
|
|
396
|
+
for (let i = 0; i < line.length; i++) {
|
|
397
|
+
const char = line[i];
|
|
398
|
+
const globalCharIdx = startCharIdx + i;
|
|
399
|
+
const colorIndex = globalCharIdx % colors.length;
|
|
400
|
+
const color = colors[colorIndex];
|
|
401
|
+
ctx.fillStyle = COLOR_MAP[color] || "#000000";
|
|
402
|
+
ctx.fillText(char, currentX, currentY);
|
|
403
|
+
currentX += ctx.measureText(char).width;
|
|
404
|
+
}
|
|
405
|
+
currentY += lineHeight;
|
|
406
|
+
});
|
|
407
|
+
return lines.length * lineHeight;
|
|
408
|
+
};
|
|
409
|
+
const drawSwatches = (ctx, colors, startX, startY, swatchHeight, scaleFactor, imageRefs) => {
|
|
410
|
+
let swatchX = startX;
|
|
411
|
+
colors.forEach((color) => {
|
|
412
|
+
const url = getImageUrl("threadColor", color);
|
|
413
|
+
const img = imageRefs.current.get(url);
|
|
414
|
+
if (img && img.complete && img.naturalHeight > 0) {
|
|
415
|
+
const ratio = img.naturalWidth / img.naturalHeight;
|
|
416
|
+
const swatchW = Math.max(1, Math.floor(swatchHeight * ratio));
|
|
417
|
+
ctx.drawImage(img, swatchX, startY, swatchW, swatchHeight);
|
|
418
|
+
swatchX += swatchW + LAYOUT.SWATCH_SPACING * scaleFactor;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
// ============================================================================
|
|
423
|
+
// MAIN COMPONENT
|
|
424
|
+
// ============================================================================
|
|
101
425
|
const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
102
426
|
const [canvasSize] = useState({ width: 4200, height: 4800 });
|
|
103
427
|
const [loadedFonts, setLoadedFonts] = useState(new Set());
|
|
@@ -107,7 +431,7 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
107
431
|
// Load fonts
|
|
108
432
|
useEffect(() => {
|
|
109
433
|
const loadFonts = async () => {
|
|
110
|
-
if (
|
|
434
|
+
if (config.error || !config.sides?.length)
|
|
111
435
|
return;
|
|
112
436
|
const fontsToLoad = new Set();
|
|
113
437
|
config.sides.forEach((side) => {
|
|
@@ -118,14 +442,14 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
118
442
|
});
|
|
119
443
|
});
|
|
120
444
|
for (const fontName of fontsToLoad) {
|
|
121
|
-
if (loadedFonts.has(fontName))
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
445
|
+
if (!loadedFonts.has(fontName)) {
|
|
446
|
+
try {
|
|
447
|
+
await loadFont(fontName);
|
|
448
|
+
setLoadedFonts((prev) => new Set(prev).add(fontName));
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
console.warn(`Could not load font ${fontName}:`, error);
|
|
452
|
+
}
|
|
129
453
|
}
|
|
130
454
|
}
|
|
131
455
|
};
|
|
@@ -133,650 +457,500 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
|
|
|
133
457
|
}, [config.sides, loadedFonts]);
|
|
134
458
|
// Load images
|
|
135
459
|
useEffect(() => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
};
|
|
150
|
-
img.onerror = () => {
|
|
151
|
-
if (useCors) {
|
|
152
|
-
// Retry without CORS; canvas may become tainted on export
|
|
153
|
-
loadMockup(false);
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
img.src = config.image_url;
|
|
157
|
-
};
|
|
158
|
-
loadMockup(true);
|
|
159
|
-
}
|
|
160
|
-
// Load icons
|
|
161
|
-
config.sides.forEach((side) => {
|
|
162
|
-
side.positions.forEach((position) => {
|
|
163
|
-
if (position.type === "ICON" && position.icon !== 0) {
|
|
164
|
-
const iconUrl = `${ICON_BASE_URL}/Icon ${position.icon}.png`;
|
|
165
|
-
if (!imageRefs.current.has(iconUrl)) {
|
|
166
|
-
const img = new Image();
|
|
167
|
-
img.crossOrigin = "anonymous";
|
|
168
|
-
img.src = iconUrl;
|
|
169
|
-
img.onload = () => setImagesLoaded((prev) => prev + 1);
|
|
170
|
-
imageRefs.current.set(iconUrl, img);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if (position.type === "TEXT" && position.floral_pattern) {
|
|
174
|
-
const floralUrl = `${FLORAL_BASE_URL}/${position.floral_pattern}.png`;
|
|
175
|
-
if (!imageRefs.current.has(floralUrl)) {
|
|
176
|
-
const img = new Image();
|
|
177
|
-
img.crossOrigin = "anonymous";
|
|
178
|
-
img.src = floralUrl;
|
|
179
|
-
img.onload = () => setImagesLoaded((prev) => prev + 1);
|
|
180
|
-
imageRefs.current.set(floralUrl, img);
|
|
181
|
-
}
|
|
460
|
+
if (config.error || !config.sides?.length)
|
|
461
|
+
return;
|
|
462
|
+
const incrementCounter = () => setImagesLoaded((prev) => prev + 1);
|
|
463
|
+
// Load mockup
|
|
464
|
+
if (config.image_url) {
|
|
465
|
+
loadImage(config.image_url, imageRefs, incrementCounter);
|
|
466
|
+
}
|
|
467
|
+
// Load all other images
|
|
468
|
+
config.sides.forEach((side) => {
|
|
469
|
+
side.positions.forEach((position) => {
|
|
470
|
+
if (position.type === "ICON") {
|
|
471
|
+
if (position.icon !== 0) {
|
|
472
|
+
loadImage(getImageUrl("icon", position.icon), imageRefs, incrementCounter);
|
|
182
473
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
img.crossOrigin = "anonymous";
|
|
191
|
-
img.src = threadColorUrl;
|
|
192
|
-
img.onload = () => setImagesLoaded((prev) => prev + 1);
|
|
193
|
-
imageRefs.current.set(threadColorUrl, img);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// Load character color images
|
|
197
|
-
if (position.character_colors &&
|
|
198
|
-
position.character_colors.length > 0) {
|
|
199
|
-
position.character_colors.forEach((color) => {
|
|
200
|
-
const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${color}.png`;
|
|
201
|
-
if (!imageRefs.current.has(threadColorUrl)) {
|
|
202
|
-
const img = new Image();
|
|
203
|
-
img.crossOrigin = "anonymous";
|
|
204
|
-
img.src = threadColorUrl;
|
|
205
|
-
img.onload = () => setImagesLoaded((prev) => prev + 1);
|
|
206
|
-
imageRefs.current.set(threadColorUrl, img);
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
}
|
|
474
|
+
position.layer_colors?.forEach((color) => {
|
|
475
|
+
loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
if (position.type === "TEXT") {
|
|
479
|
+
if (position.floral_pattern) {
|
|
480
|
+
loadImage(getImageUrl("floral", position.floral_pattern), imageRefs, incrementCounter);
|
|
210
481
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
position.layer_colors.forEach((color) => {
|
|
214
|
-
const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${color}.png`;
|
|
215
|
-
if (!imageRefs.current.has(threadColorUrl)) {
|
|
216
|
-
const img = new Image();
|
|
217
|
-
img.crossOrigin = "anonymous";
|
|
218
|
-
img.src = threadColorUrl;
|
|
219
|
-
img.onload = () => setImagesLoaded((prev) => prev + 1);
|
|
220
|
-
imageRefs.current.set(threadColorUrl, img);
|
|
221
|
-
}
|
|
222
|
-
});
|
|
482
|
+
if (position.color) {
|
|
483
|
+
loadImage(getImageUrl("threadColor", position.color), imageRefs, incrementCounter);
|
|
223
484
|
}
|
|
224
|
-
|
|
485
|
+
position.character_colors?.forEach((color) => {
|
|
486
|
+
loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
225
489
|
});
|
|
226
|
-
};
|
|
227
|
-
loadImages();
|
|
490
|
+
});
|
|
228
491
|
}, [config]);
|
|
229
492
|
// Render canvas
|
|
230
493
|
useEffect(() => {
|
|
231
494
|
const renderCanvas = () => {
|
|
232
|
-
if (!canvasRef.current
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const canvas = canvasRef.current;
|
|
236
|
-
const ctx = canvas.getContext("2d");
|
|
237
|
-
if (!ctx)
|
|
495
|
+
if (!canvasRef.current)
|
|
238
496
|
return;
|
|
239
|
-
|
|
240
|
-
canvas.height = canvasSize.height;
|
|
241
|
-
// Clear with white background
|
|
242
|
-
ctx.fillStyle = "#FFFFFF";
|
|
243
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
244
|
-
// Collect floral images (for later drawing)
|
|
245
|
-
const floralAssets = [];
|
|
246
|
-
const seenFlorals = new Set();
|
|
247
|
-
if (config.sides) {
|
|
248
|
-
config.sides.forEach((side) => {
|
|
249
|
-
side.positions.forEach((position) => {
|
|
250
|
-
if (position.type === "TEXT" && position.floral_pattern) {
|
|
251
|
-
const floralUrl = `${FLORAL_BASE_URL}/${position.floral_pattern}.png`;
|
|
252
|
-
if (!seenFlorals.has(floralUrl)) {
|
|
253
|
-
const img = imageRefs.current.get(floralUrl);
|
|
254
|
-
if (img && img.complete && img.naturalWidth > 0) {
|
|
255
|
-
floralAssets.push(img);
|
|
256
|
-
seenFlorals.add(floralUrl);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
// Helper function to draw mockup and florals
|
|
264
|
-
const drawMockupAndFlorals = () => {
|
|
265
|
-
const mockupImg = imageRefs.current.get("mockup");
|
|
266
|
-
const margin = 40; // small padding
|
|
267
|
-
let mockupBox = null;
|
|
268
|
-
if (mockupImg && mockupImg.complete && mockupImg.naturalWidth > 0) {
|
|
269
|
-
const maxTargetWidth = Math.min(1800, canvas.width * 0.375);
|
|
270
|
-
const maxTargetHeight = canvas.height * 0.375;
|
|
271
|
-
const scale = Math.min(maxTargetWidth / mockupImg.naturalWidth, maxTargetHeight / mockupImg.naturalHeight);
|
|
272
|
-
const targetWidth = Math.max(1, Math.floor(mockupImg.naturalWidth * scale));
|
|
273
|
-
const targetHeight = Math.max(1, Math.floor(mockupImg.naturalHeight * scale));
|
|
274
|
-
const targetX = canvas.width - margin - targetWidth;
|
|
275
|
-
const targetY = canvas.height - margin - targetHeight;
|
|
276
|
-
mockupBox = {
|
|
277
|
-
x: targetX,
|
|
278
|
-
y: targetY,
|
|
279
|
-
w: targetWidth,
|
|
280
|
-
h: targetHeight,
|
|
281
|
-
};
|
|
282
|
-
ctx.drawImage(mockupImg, targetX, targetY, targetWidth, targetHeight);
|
|
283
|
-
}
|
|
284
|
-
// Draw florals to the left of mockup
|
|
285
|
-
if (mockupBox && floralAssets.length > 0) {
|
|
286
|
-
const spacing = 300;
|
|
287
|
-
const targetHeight = mockupBox.h;
|
|
288
|
-
const floralFixedH = Math.min(900, targetHeight);
|
|
289
|
-
let currentX = mockupBox.x - spacing;
|
|
290
|
-
for (let i = floralAssets.length - 1; i >= 0; i--) {
|
|
291
|
-
const img = floralAssets[i];
|
|
292
|
-
const ratio = img.naturalWidth / Math.max(1, img.naturalHeight);
|
|
293
|
-
const h = floralFixedH;
|
|
294
|
-
const w = Math.max(1, Math.floor(h * ratio));
|
|
295
|
-
currentX -= w;
|
|
296
|
-
const y = mockupBox.y + (targetHeight - h);
|
|
297
|
-
ctx.drawImage(img, currentX, y, w, h);
|
|
298
|
-
currentX -= spacing;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
// New approach: Draw images first (bottom layer), then text on top
|
|
303
|
-
// This allows text to overlay images when needed
|
|
304
|
-
// Pass 1: Measure actual height with original size (use offscreen canvas for measurement)
|
|
305
|
-
const measureCanvas = document.createElement("canvas");
|
|
306
|
-
measureCanvas.width = canvas.width;
|
|
307
|
-
measureCanvas.height = canvas.height;
|
|
308
|
-
const measureCtx = measureCanvas.getContext("2d");
|
|
309
|
-
if (!measureCtx)
|
|
310
|
-
return;
|
|
311
|
-
// Set up measurement context
|
|
312
|
-
measureCtx.font = ctx.font;
|
|
313
|
-
measureCtx.textAlign = ctx.textAlign;
|
|
314
|
-
measureCtx.textBaseline = ctx.textBaseline;
|
|
315
|
-
let measureY = 40;
|
|
316
|
-
const measureSpacing = 100;
|
|
317
|
-
config.sides.forEach((side) => {
|
|
318
|
-
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1);
|
|
319
|
-
measureY += sideHeight + measureSpacing;
|
|
320
|
-
});
|
|
321
|
-
const totalMeasuredHeight = measureY; // Total height used
|
|
322
|
-
// Calculate scale factor - only scale down when necessary
|
|
323
|
-
// Keep original font sizes (no scale up) - font size is the maximum
|
|
324
|
-
const topPadding = 40;
|
|
325
|
-
// No bottom padding - content can go to bottom, mockup will overlay
|
|
326
|
-
const targetContentHeight = canvas.height - topPadding;
|
|
327
|
-
// Only scale down if content exceeds canvas height
|
|
328
|
-
// Never scale up - preserve original font sizes
|
|
329
|
-
let scaleFactor = 1;
|
|
330
|
-
if (totalMeasuredHeight > targetContentHeight) {
|
|
331
|
-
// Scale down to fit exactly
|
|
332
|
-
scaleFactor = targetContentHeight / totalMeasuredHeight;
|
|
333
|
-
scaleFactor = Math.max(0.5, scaleFactor); // Minimum scale to prevent tiny fonts
|
|
334
|
-
}
|
|
335
|
-
// If content fits, keep scaleFactor = 1 (original font sizes)
|
|
336
|
-
// Draw mockup and florals first (bottom layer)
|
|
337
|
-
drawMockupAndFlorals();
|
|
338
|
-
// Draw content on top (top layer) - text will overlay images if needed
|
|
339
|
-
let currentY = topPadding * scaleFactor;
|
|
340
|
-
config.sides.forEach((side) => {
|
|
341
|
-
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor);
|
|
342
|
-
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
343
|
-
});
|
|
497
|
+
renderEmbroideryCanvas(canvasRef.current, config, canvasSize, imageRefs);
|
|
344
498
|
};
|
|
345
|
-
// Delay rendering to ensure fonts and images are loaded
|
|
346
499
|
const timer = setTimeout(renderCanvas, 100);
|
|
347
500
|
return () => clearTimeout(timer);
|
|
348
501
|
}, [config, canvasSize, loadedFonts, imagesLoaded]);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const lastLineY = y + (lines.length - 1) * lineHeight;
|
|
375
|
-
return {
|
|
376
|
-
height: lines.length * lineHeight,
|
|
377
|
-
lastLineWidth,
|
|
378
|
-
lastLineY,
|
|
379
|
-
};
|
|
380
|
-
};
|
|
381
|
-
// Helper to wrap and draw multi-color text (for character_colors)
|
|
382
|
-
const fillTextWrappedMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
|
|
383
|
-
const words = text.split(" ");
|
|
384
|
-
const lines = [];
|
|
385
|
-
const lineStartIndices = [0];
|
|
386
|
-
let currentLine = words[0];
|
|
387
|
-
let currentCharIndex = words[0].length;
|
|
388
|
-
for (let i = 1; i < words.length; i++) {
|
|
389
|
-
const word = words[i];
|
|
390
|
-
const testLine = currentLine + " " + word;
|
|
391
|
-
const metrics = ctx.measureText(testLine);
|
|
392
|
-
if (metrics.width > maxWidth && currentLine.length > 0) {
|
|
393
|
-
lines.push(currentLine);
|
|
394
|
-
lineStartIndices.push(currentCharIndex + 1); // +1 for space
|
|
395
|
-
currentLine = word;
|
|
396
|
-
currentCharIndex += word.length + 1;
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
currentLine = testLine;
|
|
400
|
-
currentCharIndex += word.length + 1;
|
|
401
|
-
}
|
|
502
|
+
return (jsx("div", { className: `render-embroidery${className ? ` ${className}` : ""}`, style: style, children: jsx("canvas", { ref: canvasRef, className: "render-embroidery-canvas" }) }));
|
|
503
|
+
};
|
|
504
|
+
// ============================================================================
|
|
505
|
+
// RENDERING FUNCTIONS
|
|
506
|
+
// ============================================================================
|
|
507
|
+
const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
|
|
508
|
+
const ctx = canvas.getContext("2d");
|
|
509
|
+
if (!ctx)
|
|
510
|
+
return;
|
|
511
|
+
canvas.width = canvasSize.width;
|
|
512
|
+
canvas.height = canvasSize.height;
|
|
513
|
+
ctx.fillStyle = LAYOUT.BACKGROUND_COLOR;
|
|
514
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
515
|
+
if (config.error) {
|
|
516
|
+
renderErrorState(ctx, canvas, config.error);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (!config.sides?.length)
|
|
520
|
+
return;
|
|
521
|
+
ctx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
522
|
+
ctx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
523
|
+
if (config.image_url) {
|
|
524
|
+
const mockupImage = imageRefs.current.get(config.image_url);
|
|
525
|
+
if (mockupImage) {
|
|
526
|
+
imageRefs.current.set("mockup", mockupImage);
|
|
402
527
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const startCharIdx = lineIdx > 0 ? lineStartIndices[lineIdx] : 0;
|
|
408
|
-
for (let i = 0; i < line.length; i++) {
|
|
409
|
-
const char = line[i];
|
|
410
|
-
const globalCharIdx = startCharIdx + i;
|
|
411
|
-
const colorIndex = globalCharIdx % colors.length;
|
|
412
|
-
const color = colors[colorIndex];
|
|
413
|
-
ctx.fillStyle = COLOR_MAP[color] || "#000000";
|
|
414
|
-
ctx.fillText(char, currentX, currentY);
|
|
415
|
-
currentX += ctx.measureText(char).width;
|
|
416
|
-
}
|
|
417
|
-
currentY += lineHeight;
|
|
418
|
-
});
|
|
419
|
-
return lines.length * lineHeight;
|
|
420
|
-
};
|
|
421
|
-
const renderSide = (ctx, side, startY, width, scaleFactor = 1) => {
|
|
422
|
-
let currentY = startY;
|
|
423
|
-
const padding = 40 * scaleFactor;
|
|
424
|
-
const sideWidth = width - 2 * padding;
|
|
425
|
-
const sectionHeight = 200 * scaleFactor;
|
|
426
|
-
// No background section anymore - just white background
|
|
427
|
-
// Group positions by common properties for optimization
|
|
428
|
-
const textGroups = [];
|
|
429
|
-
let currentGroup = null;
|
|
430
|
-
let currentProps = null;
|
|
528
|
+
}
|
|
529
|
+
const floralAssets = [];
|
|
530
|
+
const seenFlorals = new Set();
|
|
531
|
+
config.sides.forEach((side) => {
|
|
431
532
|
side.positions.forEach((position) => {
|
|
432
|
-
if (position.type === "TEXT") {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// Start new group
|
|
440
|
-
if (currentGroup) {
|
|
441
|
-
textGroups.push({
|
|
442
|
-
positions: currentGroup,
|
|
443
|
-
properties: currentProps,
|
|
444
|
-
});
|
|
533
|
+
if (position.type === "TEXT" && position.floral_pattern) {
|
|
534
|
+
const url = getImageUrl("floral", position.floral_pattern);
|
|
535
|
+
if (!seenFlorals.has(url)) {
|
|
536
|
+
const img = imageRefs.current.get(url);
|
|
537
|
+
if (img?.complete && img.naturalWidth > 0) {
|
|
538
|
+
floralAssets.push(img);
|
|
539
|
+
seenFlorals.add(url);
|
|
445
540
|
}
|
|
446
|
-
currentGroup = [position];
|
|
447
|
-
currentProps = {
|
|
448
|
-
font: position.font,
|
|
449
|
-
text_shape: position.text_shape,
|
|
450
|
-
color: position.color,
|
|
451
|
-
character_colors: position.character_colors,
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
else {
|
|
455
|
-
currentGroup.push(position);
|
|
456
541
|
}
|
|
457
542
|
}
|
|
458
543
|
});
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
textGroups.forEach((group, groupIndex) => {
|
|
501
|
-
group.positions.forEach((position, index) => {
|
|
502
|
-
if (index === 0 && groupIndex !== 0)
|
|
503
|
-
currentY += 50 * scaleFactor;
|
|
504
|
-
const drawnHeight = renderText(ctx, position, padding, currentY, sideWidth, sideTextCounter, {
|
|
505
|
-
font: !sideUniform.font,
|
|
506
|
-
shape: !sideUniform.shape,
|
|
507
|
-
floral: !sideUniform.floral,
|
|
508
|
-
color: !sideUniform.color,
|
|
509
|
-
}, scaleFactor);
|
|
510
|
-
sideTextCounter += 1;
|
|
511
|
-
// add padding only if something was actually drawn
|
|
512
|
-
if (drawnHeight > 0) {
|
|
513
|
-
currentY += drawnHeight + 40 * scaleFactor;
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
// Render ICON titles/values (no images here)
|
|
518
|
-
currentY += 30 * scaleFactor; // minimal spacing before icon labels
|
|
519
|
-
side.positions.forEach((position) => {
|
|
520
|
-
if (position.type === "ICON") {
|
|
521
|
-
currentY += renderIconLabels(ctx, position, padding, currentY, sideWidth, scaleFactor);
|
|
522
|
-
currentY += 10 * scaleFactor;
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
return Math.max(currentY - startY, sectionHeight);
|
|
526
|
-
};
|
|
527
|
-
const renderSideUniformLabels = (ctx, values, x, y, maxWidth, scaleFactor = 1) => {
|
|
528
|
-
const labelFontFamily = "Arial";
|
|
529
|
-
const fontSize = 180 * scaleFactor;
|
|
530
|
-
const lineGap = 20 * scaleFactor;
|
|
531
|
-
ctx.save();
|
|
532
|
-
ctx.font = `${fontSize}px ${labelFontFamily}`;
|
|
533
|
-
ctx.textAlign = "left";
|
|
534
|
-
ctx.textBaseline = "top";
|
|
535
|
-
ctx.fillStyle = "#444444";
|
|
536
|
-
let cursorY = y;
|
|
537
|
-
let rendered = 0;
|
|
538
|
-
if (values.font) {
|
|
539
|
-
const fontText = `Font: ${values.font}`;
|
|
540
|
-
const result = fillTextWrapped(ctx, fontText, x, cursorY, maxWidth, fontSize + lineGap);
|
|
541
|
-
cursorY += result.height;
|
|
542
|
-
rendered++;
|
|
543
|
-
}
|
|
544
|
-
if (values.shape && values.shape !== "None") {
|
|
545
|
-
const shapeText = `Kiểu chữ: ${values.shape}`;
|
|
546
|
-
const result = fillTextWrapped(ctx, shapeText, x, cursorY, maxWidth, fontSize + lineGap);
|
|
547
|
-
cursorY += result.height;
|
|
548
|
-
rendered++;
|
|
549
|
-
}
|
|
550
|
-
if (values.color && values.color !== "None") {
|
|
551
|
-
const colorText = `Màu chỉ: ${values.color}`;
|
|
552
|
-
// Reserve space for swatches (estimate: max 5 swatches × 200px each = 1000px)
|
|
553
|
-
const swatchReserved = 1000 * scaleFactor;
|
|
554
|
-
const textMaxWidth = Math.max(400 * scaleFactor, maxWidth - swatchReserved);
|
|
555
|
-
const result = fillTextWrapped(ctx, colorText, x, cursorY, textMaxWidth, fontSize + lineGap);
|
|
556
|
-
// Draw swatches inline for side-level color, preserving aspect ratio; 75% of previous size
|
|
557
|
-
// Position swatches after the last line of wrapped text
|
|
558
|
-
const swatchH = Math.floor(fontSize * 2.025);
|
|
559
|
-
let swatchX = x + Math.ceil(result.lastLineWidth) + 100 * scaleFactor;
|
|
560
|
-
const swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
|
|
561
|
-
const colorTokens = values.color.includes(",")
|
|
562
|
-
? values.color.split(",").map((s) => s.trim())
|
|
563
|
-
: [values.color];
|
|
564
|
-
colorTokens.forEach((color) => {
|
|
565
|
-
const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${color}.png`;
|
|
566
|
-
const img = imageRefs.current.get(threadColorUrl);
|
|
567
|
-
if (img && img.complete && img.naturalHeight > 0) {
|
|
568
|
-
const ratio = img.naturalWidth / img.naturalHeight;
|
|
569
|
-
const swatchW = Math.max(1, Math.floor(swatchH * ratio));
|
|
570
|
-
ctx.drawImage(img, swatchX, swatchY, swatchW, swatchH);
|
|
571
|
-
swatchX += swatchW + 25 * scaleFactor;
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
cursorY += result.height;
|
|
575
|
-
rendered++;
|
|
576
|
-
}
|
|
577
|
-
if (values.floral && values.floral !== "None") {
|
|
578
|
-
const floralText = `Mẫu hoa: ${values.floral}`;
|
|
579
|
-
const result = fillTextWrapped(ctx, floralText, x, cursorY, maxWidth, fontSize + lineGap);
|
|
580
|
-
cursorY += result.height;
|
|
581
|
-
rendered++;
|
|
582
|
-
}
|
|
583
|
-
if (rendered > 0)
|
|
584
|
-
cursorY += 50 * scaleFactor; // extra gap before first text line
|
|
585
|
-
ctx.restore();
|
|
586
|
-
return cursorY - y;
|
|
544
|
+
});
|
|
545
|
+
const measureCanvas = document.createElement("canvas");
|
|
546
|
+
measureCanvas.width = canvas.width;
|
|
547
|
+
measureCanvas.height = canvas.height;
|
|
548
|
+
const measureCtx = measureCanvas.getContext("2d");
|
|
549
|
+
if (!measureCtx)
|
|
550
|
+
return;
|
|
551
|
+
measureCtx.textAlign = LAYOUT.TEXT_ALIGN;
|
|
552
|
+
measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
|
|
553
|
+
let measureY = LAYOUT.PADDING;
|
|
554
|
+
const measureSpacing = LAYOUT.ELEMENT_SPACING;
|
|
555
|
+
config.sides.forEach((side) => {
|
|
556
|
+
const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
|
|
557
|
+
measureY += sideHeight + measureSpacing;
|
|
558
|
+
});
|
|
559
|
+
const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING) / measureY));
|
|
560
|
+
drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
|
|
561
|
+
let currentY = LAYOUT.PADDING * scaleFactor;
|
|
562
|
+
config.sides.forEach((side) => {
|
|
563
|
+
const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
|
|
564
|
+
currentY += sideHeight + measureSpacing * scaleFactor;
|
|
565
|
+
});
|
|
566
|
+
};
|
|
567
|
+
const renderErrorState = (ctx, canvas, message) => {
|
|
568
|
+
const sanitizedMessage = message.trim() || "Đã xảy ra lỗi";
|
|
569
|
+
const horizontalPadding = LAYOUT.PADDING * 3;
|
|
570
|
+
const maxWidth = canvas.width - horizontalPadding * 2;
|
|
571
|
+
const baseFontSize = LAYOUT.HEADER_FONT_SIZE;
|
|
572
|
+
const minFontSize = 60;
|
|
573
|
+
const centerX = canvas.width / 2;
|
|
574
|
+
ctx.save();
|
|
575
|
+
ctx.textAlign = "center";
|
|
576
|
+
ctx.textBaseline = "top";
|
|
577
|
+
ctx.fillStyle = DEFAULT_ERROR_COLOR;
|
|
578
|
+
let fontSize = baseFontSize;
|
|
579
|
+
let lineGap = LAYOUT.LINE_GAP;
|
|
580
|
+
let lineHeight = fontSize + lineGap;
|
|
581
|
+
const adjustMetrics = () => {
|
|
582
|
+
ctx.font = `bold ${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
583
|
+
lineGap = LAYOUT.LINE_GAP * (fontSize / baseFontSize);
|
|
584
|
+
lineHeight = fontSize + lineGap;
|
|
587
585
|
};
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
ctx.
|
|
628
|
-
|
|
629
|
-
currentYCursor += textResult.height;
|
|
630
|
-
drawnHeight += textResult.height;
|
|
631
|
-
}
|
|
632
|
-
// After text, print Kiểu chữ (when not uniform), then Font and Color as needed
|
|
633
|
-
currentYCursor += infoLineGap;
|
|
634
|
-
ctx.font = `${infoFontSize}px ${labelFontFamily}`;
|
|
635
|
-
ctx.fillStyle = "#444444";
|
|
636
|
-
if (showLabels.shape && position.text_shape) {
|
|
637
|
-
const shapeLabelAfter = `Kiểu chữ: ${position.text_shape}`;
|
|
638
|
-
const result = fillTextWrapped(ctx, shapeLabelAfter, x, currentYCursor, maxWidth, infoFontSize + infoLineGap);
|
|
639
|
-
currentYCursor += result.height;
|
|
640
|
-
drawnHeight += result.height;
|
|
641
|
-
}
|
|
642
|
-
if (showLabels.font && position.font) {
|
|
643
|
-
const fontLabel = `Font: ${position.font}`;
|
|
644
|
-
const result = fillTextWrapped(ctx, fontLabel, x, currentYCursor, maxWidth, infoFontSize + infoLineGap);
|
|
645
|
-
currentYCursor += result.height;
|
|
646
|
-
drawnHeight += result.height;
|
|
586
|
+
adjustMetrics();
|
|
587
|
+
let lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
588
|
+
let longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
589
|
+
while (longestLineWidth > maxWidth && fontSize > minFontSize) {
|
|
590
|
+
fontSize = Math.max(minFontSize, Math.floor(fontSize * 0.9));
|
|
591
|
+
adjustMetrics();
|
|
592
|
+
lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
|
|
593
|
+
longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
|
|
594
|
+
}
|
|
595
|
+
const totalHeight = lines.length * lineHeight;
|
|
596
|
+
const startY = Math.max(LAYOUT.PADDING * 2, canvas.height / 2 - totalHeight / 2);
|
|
597
|
+
lines.forEach((line, index) => {
|
|
598
|
+
const y = startY + index * lineHeight;
|
|
599
|
+
ctx.fillText(line, centerX, y);
|
|
600
|
+
});
|
|
601
|
+
ctx.restore();
|
|
602
|
+
};
|
|
603
|
+
const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
|
|
604
|
+
const mockupImg = imageRefs.current.get("mockup");
|
|
605
|
+
if (!mockupImg?.complete || !mockupImg.naturalWidth)
|
|
606
|
+
return;
|
|
607
|
+
const margin = LAYOUT.PADDING;
|
|
608
|
+
const maxWidth = Math.min(1800, canvas.width * 0.375);
|
|
609
|
+
const maxHeight = canvas.height * 0.375;
|
|
610
|
+
const scale = Math.min(maxWidth / mockupImg.naturalWidth, maxHeight / mockupImg.naturalHeight);
|
|
611
|
+
const width = Math.max(1, Math.floor(mockupImg.naturalWidth * scale));
|
|
612
|
+
const height = Math.max(1, Math.floor(mockupImg.naturalHeight * scale));
|
|
613
|
+
const x = canvas.width - margin - width;
|
|
614
|
+
const y = canvas.height - margin - height;
|
|
615
|
+
ctx.drawImage(mockupImg, x, y, width, height);
|
|
616
|
+
// Draw florals
|
|
617
|
+
if (floralAssets.length > 0) {
|
|
618
|
+
const floralH = Math.min(900, height);
|
|
619
|
+
let currentX = x - LAYOUT.FLORAL_SPACING;
|
|
620
|
+
for (let i = floralAssets.length - 1; i >= 0; i--) {
|
|
621
|
+
const img = floralAssets[i];
|
|
622
|
+
const ratio = img.naturalWidth / Math.max(1, img.naturalHeight);
|
|
623
|
+
const w = Math.max(1, Math.floor(floralH * ratio));
|
|
624
|
+
currentX -= w;
|
|
625
|
+
ctx.drawImage(img, currentX, y + height - floralH, w, floralH);
|
|
626
|
+
currentX -= LAYOUT.FLORAL_SPACING;
|
|
647
627
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
|
|
631
|
+
let currentY = startY;
|
|
632
|
+
const padding = LAYOUT.PADDING * scaleFactor;
|
|
633
|
+
const sideWidth = width - 2 * padding;
|
|
634
|
+
// Draw header
|
|
635
|
+
ctx.save();
|
|
636
|
+
const headerFontSize = LAYOUT.HEADER_FONT_SIZE * scaleFactor;
|
|
637
|
+
ctx.font = `bold ${headerFontSize}px ${LAYOUT.HEADER_FONT_FAMILY}`;
|
|
638
|
+
ctx.fillStyle = LAYOUT.HEADER_COLOR;
|
|
639
|
+
const headerResult = wrapText(ctx, side.print_side.toUpperCase(), padding, currentY, sideWidth, headerFontSize);
|
|
640
|
+
// Draw underline
|
|
641
|
+
const underlineY = headerResult.lastLineY + headerFontSize * LAYOUT.UNDERLINE_POSITION;
|
|
642
|
+
ctx.strokeStyle = LAYOUT.HEADER_COLOR;
|
|
643
|
+
ctx.lineWidth = LAYOUT.UNDERLINE_WIDTH * scaleFactor;
|
|
644
|
+
ctx.beginPath();
|
|
645
|
+
ctx.moveTo(padding, underlineY);
|
|
646
|
+
ctx.lineTo(padding + headerResult.lastLineWidth, underlineY);
|
|
647
|
+
ctx.stroke();
|
|
648
|
+
currentY += headerResult.height + LAYOUT.SECTION_SPACING * scaleFactor;
|
|
649
|
+
ctx.restore();
|
|
650
|
+
// Compute uniform properties
|
|
651
|
+
const textPositions = side.positions.filter((p) => p.type === "TEXT");
|
|
652
|
+
const uniformProps = computeUniformProperties(textPositions);
|
|
653
|
+
// Render uniform labels (only if more than 1 TEXT position)
|
|
654
|
+
if (textPositions.length > 1) {
|
|
655
|
+
currentY += renderUniformLabels(ctx, uniformProps, padding, currentY, sideWidth, scaleFactor, imageRefs);
|
|
656
|
+
}
|
|
657
|
+
// Group text positions by common properties
|
|
658
|
+
const textGroups = groupTextPositions(textPositions);
|
|
659
|
+
// Render text positions (with proper spacing between groups)
|
|
660
|
+
let textCounter = 1;
|
|
661
|
+
textGroups.forEach((group, groupIndex) => {
|
|
662
|
+
group.forEach((position, index) => {
|
|
663
|
+
// Add extra spacing between different groups
|
|
664
|
+
if (index === 0 && groupIndex !== 0) {
|
|
665
|
+
currentY += LAYOUT.SECTION_SPACING * scaleFactor;
|
|
652
666
|
}
|
|
653
|
-
|
|
654
|
-
|
|
667
|
+
// If only 1 TEXT position, show all labels (no uniform labels rendered)
|
|
668
|
+
const showLabels = textPositions.length === 1
|
|
669
|
+
? { font: true, shape: true, floral: true, color: true }
|
|
670
|
+
: {
|
|
671
|
+
font: !uniformProps.isUniform.font,
|
|
672
|
+
shape: !uniformProps.isUniform.shape,
|
|
673
|
+
floral: !uniformProps.isUniform.floral,
|
|
674
|
+
color: !uniformProps.isUniform.color,
|
|
675
|
+
};
|
|
676
|
+
const height = renderTextPosition(ctx, position, padding, currentY, sideWidth, textCounter, showLabels, scaleFactor, imageRefs);
|
|
677
|
+
if (height > 0) {
|
|
678
|
+
currentY += height + LAYOUT.PADDING * scaleFactor;
|
|
679
|
+
textCounter++;
|
|
655
680
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
currentYCursor += result.height;
|
|
689
|
-
drawnHeight += result.height;
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
// Render icon positions
|
|
684
|
+
currentY += LAYOUT.LINE_GAP * scaleFactor;
|
|
685
|
+
side.positions.forEach((position) => {
|
|
686
|
+
if (position.type === "ICON") {
|
|
687
|
+
currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs);
|
|
688
|
+
currentY += (LAYOUT.LINE_GAP / 3) * scaleFactor;
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
return currentY - startY;
|
|
692
|
+
};
|
|
693
|
+
const groupTextPositions = (textPositions) => {
|
|
694
|
+
const groups = [];
|
|
695
|
+
let currentGroup = null;
|
|
696
|
+
let currentProps = null;
|
|
697
|
+
textPositions.forEach((position) => {
|
|
698
|
+
const posProps = {
|
|
699
|
+
font: position.font,
|
|
700
|
+
text_shape: position.text_shape,
|
|
701
|
+
color: position.color,
|
|
702
|
+
character_colors: position.character_colors?.join(","),
|
|
703
|
+
};
|
|
704
|
+
if (!currentGroup ||
|
|
705
|
+
currentProps.font !== posProps.font ||
|
|
706
|
+
currentProps.text_shape !== posProps.text_shape ||
|
|
707
|
+
currentProps.color !== posProps.color ||
|
|
708
|
+
currentProps.character_colors !== posProps.character_colors) {
|
|
709
|
+
if (currentGroup) {
|
|
710
|
+
groups.push(currentGroup);
|
|
690
711
|
}
|
|
712
|
+
currentGroup = [position];
|
|
713
|
+
currentProps = posProps;
|
|
691
714
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const floralText = `Mẫu hoa: ${position.floral_pattern}`;
|
|
695
|
-
const result = fillTextWrapped(ctx, floralText, x, currentYCursor, maxWidth, infoFontSize + infoLineGap);
|
|
696
|
-
currentYCursor += result.height;
|
|
697
|
-
drawnHeight += result.height;
|
|
715
|
+
else {
|
|
716
|
+
currentGroup.push(position);
|
|
698
717
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
718
|
+
});
|
|
719
|
+
if (currentGroup) {
|
|
720
|
+
groups.push(currentGroup);
|
|
721
|
+
}
|
|
722
|
+
return groups;
|
|
723
|
+
};
|
|
724
|
+
const computeUniformProperties = (textPositions) => {
|
|
725
|
+
if (textPositions.length === 0) {
|
|
726
|
+
return {
|
|
727
|
+
values: { font: null, shape: null, floral: null, color: null },
|
|
728
|
+
isUniform: { font: false, shape: false, floral: false, color: false },
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
const fonts = new Set(textPositions.map((p) => p.font));
|
|
732
|
+
const shapes = new Set(textPositions.map((p) => p.text_shape));
|
|
733
|
+
const florals = new Set(textPositions.map((p) => p.floral_pattern ?? "None"));
|
|
734
|
+
const colors = new Set(textPositions.map((p) => p.character_colors?.length
|
|
735
|
+
? p.character_colors.join(",")
|
|
736
|
+
: p.color ?? "None"));
|
|
737
|
+
return {
|
|
738
|
+
values: {
|
|
739
|
+
font: fonts.size === 1 ? [...fonts][0] : null,
|
|
740
|
+
shape: shapes.size === 1 ? [...shapes][0] : null,
|
|
741
|
+
floral: florals.size === 1 ? [...florals][0] : null,
|
|
742
|
+
color: colors.size === 1 ? [...colors][0] : null,
|
|
743
|
+
},
|
|
744
|
+
isUniform: {
|
|
745
|
+
font: fonts.size === 1,
|
|
746
|
+
shape: shapes.size === 1,
|
|
747
|
+
floral: florals.size === 1,
|
|
748
|
+
color: colors.size === 1,
|
|
749
|
+
},
|
|
702
750
|
};
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
751
|
+
};
|
|
752
|
+
const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, imageRefs) => {
|
|
753
|
+
const { values } = uniformProps;
|
|
754
|
+
const fontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
755
|
+
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
756
|
+
ctx.save();
|
|
757
|
+
ctx.font = `${fontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
758
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
759
|
+
let cursorY = y;
|
|
760
|
+
let rendered = 0;
|
|
761
|
+
if (values.font) {
|
|
762
|
+
const result = wrapText(ctx, `Font: ${values.font}`, x, cursorY, maxWidth, fontSize + lineGap);
|
|
763
|
+
cursorY += result.height;
|
|
764
|
+
rendered++;
|
|
765
|
+
}
|
|
766
|
+
if (values.shape && values.shape !== "None") {
|
|
767
|
+
const result = wrapText(ctx, `Kiểu chữ: ${values.shape}`, x, cursorY, maxWidth, fontSize + lineGap);
|
|
768
|
+
cursorY += result.height;
|
|
769
|
+
rendered++;
|
|
770
|
+
}
|
|
771
|
+
if (values.color && values.color !== "None") {
|
|
772
|
+
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
773
|
+
const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, textMaxWidth, fontSize + lineGap);
|
|
774
|
+
const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
775
|
+
const swatchX = x +
|
|
776
|
+
Math.ceil(result.lastLineWidth) +
|
|
777
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
778
|
+
const swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
|
|
779
|
+
const colors = values.color.includes(",")
|
|
780
|
+
? values.color.split(",").map((s) => s.trim())
|
|
781
|
+
: [values.color];
|
|
782
|
+
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
783
|
+
cursorY += result.height;
|
|
784
|
+
rendered++;
|
|
785
|
+
}
|
|
786
|
+
if (values.floral && values.floral !== "None") {
|
|
787
|
+
const result = wrapText(ctx, `Mẫu hoa: ${values.floral}`, x, cursorY, maxWidth, fontSize + lineGap);
|
|
788
|
+
cursorY += result.height;
|
|
789
|
+
rendered++;
|
|
790
|
+
}
|
|
791
|
+
if (rendered > 0)
|
|
792
|
+
cursorY += LAYOUT.SECTION_SPACING * scaleFactor;
|
|
793
|
+
ctx.restore();
|
|
794
|
+
return cursorY - y;
|
|
795
|
+
};
|
|
796
|
+
const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLabels, scaleFactor, imageRefs) => {
|
|
797
|
+
ctx.save();
|
|
798
|
+
const textFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
|
|
799
|
+
const otherFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
800
|
+
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
801
|
+
let currentY = y;
|
|
802
|
+
let drawnHeight = 0;
|
|
803
|
+
// Draw label
|
|
804
|
+
const textLabel = `Text ${displayIndex}: `;
|
|
805
|
+
ctx.font = `bold ${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
806
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
807
|
+
const labelWidth = ctx.measureText(textLabel).width;
|
|
808
|
+
ctx.fillText(textLabel, x, currentY);
|
|
809
|
+
const textMaxWidth = maxWidth - labelWidth;
|
|
810
|
+
// Get display text (handle empty/null/undefined)
|
|
811
|
+
const isEmptyText = !position.text || position.text.trim() === "";
|
|
812
|
+
// Draw text content
|
|
813
|
+
if (isEmptyText) {
|
|
814
|
+
ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
815
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
816
|
+
const textResult = wrapText(ctx, "không thay đổi", x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
817
|
+
currentY += textResult.height;
|
|
818
|
+
drawnHeight += textResult.height;
|
|
819
|
+
}
|
|
820
|
+
else if (position.character_colors?.length) {
|
|
821
|
+
ctx.font = `${textFontSize}px ${position.font}`;
|
|
822
|
+
const textHeight = wrapTextMultiColor(ctx, position.text, position.character_colors, x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
823
|
+
currentY += textHeight;
|
|
824
|
+
drawnHeight += textHeight;
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
ctx.font = `${textFontSize}px ${position.font}`;
|
|
828
|
+
ctx.fillStyle = COLOR_MAP[position.color ?? "None"] || "#000000";
|
|
829
|
+
const textResult = wrapText(ctx, position.text, x + labelWidth, currentY, textMaxWidth, textFontSize);
|
|
830
|
+
currentY += textResult.height;
|
|
831
|
+
drawnHeight += textResult.height;
|
|
832
|
+
}
|
|
833
|
+
// Draw additional labels
|
|
834
|
+
currentY += lineGap;
|
|
835
|
+
ctx.font = `${otherFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
836
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
837
|
+
if (showLabels.shape && position.text_shape) {
|
|
838
|
+
const result = wrapText(ctx, `Kiểu chữ: ${position.text_shape}`, x, currentY, maxWidth, otherFontSize + lineGap);
|
|
839
|
+
currentY += result.height;
|
|
840
|
+
drawnHeight += result.height;
|
|
841
|
+
}
|
|
842
|
+
if (showLabels.font && position.font) {
|
|
843
|
+
const result = wrapText(ctx, `Font: ${position.font}`, x, currentY, maxWidth, otherFontSize + lineGap);
|
|
844
|
+
currentY += result.height;
|
|
845
|
+
drawnHeight += result.height;
|
|
846
|
+
}
|
|
847
|
+
if (showLabels.color) {
|
|
848
|
+
const colorValue = position.character_colors?.join(", ") || position.color;
|
|
849
|
+
if (colorValue) {
|
|
850
|
+
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
851
|
+
const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, textMaxWidth, otherFontSize + lineGap);
|
|
852
|
+
const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
853
|
+
const swatchX = x +
|
|
854
|
+
Math.ceil(result.lastLineWidth) +
|
|
855
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
856
|
+
const swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
|
|
857
|
+
const colors = position.character_colors || [position.color];
|
|
858
|
+
drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
859
|
+
currentY += result.height;
|
|
860
|
+
drawnHeight += result.height;
|
|
730
861
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
862
|
+
}
|
|
863
|
+
if (showLabels.floral && position.floral_pattern) {
|
|
864
|
+
const result = wrapText(ctx, `Mẫu hoa: ${position.floral_pattern}`, x, currentY, maxWidth, otherFontSize + lineGap);
|
|
865
|
+
currentY += result.height;
|
|
866
|
+
drawnHeight += result.height;
|
|
867
|
+
}
|
|
868
|
+
ctx.restore();
|
|
869
|
+
return drawnHeight;
|
|
870
|
+
};
|
|
871
|
+
const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs) => {
|
|
872
|
+
const iconFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
|
|
873
|
+
const lineGap = LAYOUT.LINE_GAP * scaleFactor;
|
|
874
|
+
ctx.save();
|
|
875
|
+
ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
|
|
876
|
+
ctx.fillStyle = LAYOUT.LABEL_COLOR;
|
|
877
|
+
let cursorY = y;
|
|
878
|
+
const iconText = position.icon === 0
|
|
879
|
+
? `Icon: icon mặc định theo file thêu`
|
|
880
|
+
: `Icon: ${position.icon}`;
|
|
881
|
+
const iconResult = wrapText(ctx, iconText, x, cursorY, maxWidth, iconFontSize + lineGap);
|
|
882
|
+
// Draw icon image
|
|
883
|
+
if (position.icon !== 0) {
|
|
884
|
+
const url = getImageUrl("icon", position.icon);
|
|
885
|
+
const img = imageRefs.current.get(url);
|
|
886
|
+
if (img?.complete && img.naturalHeight > 0) {
|
|
887
|
+
const ratio = img.naturalWidth / img.naturalHeight;
|
|
888
|
+
const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
|
|
889
|
+
const iconX = x +
|
|
890
|
+
Math.ceil(iconResult.lastLineWidth) +
|
|
891
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
892
|
+
const iconY = iconResult.lastLineY + Math.floor(iconFontSize / 2 - iconFontSize / 2);
|
|
893
|
+
ctx.drawImage(img, iconX, iconY, swatchW, iconFontSize);
|
|
756
894
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
895
|
+
}
|
|
896
|
+
cursorY += iconResult.height;
|
|
897
|
+
// Draw color swatches
|
|
898
|
+
if (position.layer_colors?.length) {
|
|
899
|
+
const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
|
|
900
|
+
const colorResult = wrapText(ctx, `Màu chỉ: ${position.layer_colors.join(", ")}`, x, cursorY, textMaxWidth, iconFontSize + lineGap);
|
|
901
|
+
const swatchH = Math.floor(iconFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
|
|
902
|
+
const swatchX = x +
|
|
903
|
+
Math.ceil(colorResult.lastLineWidth) +
|
|
904
|
+
LAYOUT.ELEMENT_SPACING * scaleFactor;
|
|
905
|
+
const swatchY = colorResult.lastLineY + Math.floor(iconFontSize / 2 - swatchH / 2);
|
|
906
|
+
drawSwatches(ctx, position.layer_colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
|
|
907
|
+
cursorY += colorResult.height;
|
|
908
|
+
}
|
|
909
|
+
ctx.restore();
|
|
910
|
+
return cursorY - y;
|
|
911
|
+
};
|
|
912
|
+
const prepareExportCanvas = async (config, options = {}) => {
|
|
913
|
+
const { width = 4200, height = 4800 } = options;
|
|
914
|
+
const canvas = document.createElement("canvas");
|
|
915
|
+
const imageRefs = {
|
|
916
|
+
current: new Map(),
|
|
777
917
|
};
|
|
778
|
-
|
|
918
|
+
await preloadFonts(config);
|
|
919
|
+
await preloadImages(config, imageRefs);
|
|
920
|
+
renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
|
|
921
|
+
if (!canvas.width || !canvas.height) {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
return canvas;
|
|
925
|
+
};
|
|
926
|
+
const generateEmbroideryQCImageBlob = async (config, options = {}) => {
|
|
927
|
+
if (typeof document === "undefined") {
|
|
928
|
+
throw new Error("generateEmbroideryQCImageBlob requires a browser environment.");
|
|
929
|
+
}
|
|
930
|
+
const { mimeType = "image/png", quality } = options;
|
|
931
|
+
const canvas = await prepareExportCanvas(config, options);
|
|
932
|
+
if (!canvas || typeof canvas.toBlob !== "function") {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
const blob = await new Promise((resolve) => {
|
|
936
|
+
canvas.toBlob((result) => resolve(result), mimeType, quality);
|
|
937
|
+
});
|
|
938
|
+
return blob;
|
|
939
|
+
};
|
|
940
|
+
const generateEmbroideryQCImageData = async (config, options = {}) => {
|
|
941
|
+
if (typeof document === "undefined") {
|
|
942
|
+
throw new Error("generateEmbroideryQCImageData requires a browser environment.");
|
|
943
|
+
}
|
|
944
|
+
const { mimeType = "image/png", quality } = options;
|
|
945
|
+
const canvas = await prepareExportCanvas(config, options);
|
|
946
|
+
if (!canvas) {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
if (mimeType === "image/png" || typeof quality === "undefined") {
|
|
950
|
+
return canvas.toDataURL(mimeType);
|
|
951
|
+
}
|
|
952
|
+
return canvas.toDataURL(mimeType, quality);
|
|
779
953
|
};
|
|
780
954
|
|
|
781
|
-
export { EmbroideryQCImage };
|
|
955
|
+
export { EmbroideryQCImage, generateEmbroideryQCImageBlob, generateEmbroideryQCImageData };
|
|
782
956
|
//# sourceMappingURL=index.esm.js.map
|