@wewear/virtual-try-on 1.4.0 → 1.4.1
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/dist/api.d.ts +7 -1
- package/dist/components/alert-overlay.d.ts +1 -1
- package/dist/components/badge.d.ts +1 -0
- package/dist/components/index.d.ts +2 -3
- package/dist/components/loading-overlay.d.ts +0 -8
- package/dist/constants.d.ts +1 -18
- package/dist/index.d.ts +2 -8
- package/dist/index.esm.js +321 -636
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +320 -636
- package/dist/index.js.map +1 -1
- package/dist/installer.d.ts +0 -1
- package/dist/widget.d.ts +12 -14
- package/package.json +1 -1
- package/dist/components/dom-alert.d.ts +0 -2
- package/dist/components/image-modal.d.ts +0 -1
package/dist/index.esm.js
CHANGED
|
@@ -1,117 +1,16 @@
|
|
|
1
|
-
async function callVirtualTryOnApi(baseUrl, ww_access_token, ww_user_id, ww_product_image, ww_image) {
|
|
2
|
-
const formData = new FormData();
|
|
3
|
-
formData.append("ww_user_id", ww_user_id);
|
|
4
|
-
formData.append("ww_product_image", ww_product_image);
|
|
5
|
-
formData.append("ww_image", ww_image, "captured-image.jpg");
|
|
6
|
-
formData.append("ww_access_token", ww_access_token);
|
|
7
|
-
const response = await fetch(`${baseUrl}/api/virtual-try-on`, {
|
|
8
|
-
method: "POST",
|
|
9
|
-
body: formData,
|
|
10
|
-
});
|
|
11
|
-
if (!response.ok) {
|
|
12
|
-
console.error("[WeWear VTO] API request failed:", response.status, response.statusText);
|
|
13
|
-
if (response.status === 500) {
|
|
14
|
-
return { "imageUrl": null, "error": { message: `Service unavailable. Please try again later.`, type: 'error' } };
|
|
15
|
-
}
|
|
16
|
-
else if (response.status === 503) {
|
|
17
|
-
return { "imageUrl": null, "error": { message: `Service overloaded. Please try again later.`, type: 'warning' } };
|
|
18
|
-
}
|
|
19
|
-
return { "imageUrl": null, "error": { message: `${response.statusText}`, type: 'warning' } };
|
|
20
|
-
}
|
|
21
|
-
const result = await response.json();
|
|
22
|
-
console.log("[WeWear VTO] API response:", result);
|
|
23
|
-
return result;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
1
|
/** CSS class names for consistent styling */
|
|
27
2
|
const CSS_CLASSES = {
|
|
28
3
|
BUTTON_CONTAINER: "wewear-vto-button-container",
|
|
29
4
|
BUTTON: "wewear-vto-button",
|
|
30
5
|
MODAL: "wewear-vto-modal",
|
|
6
|
+
PREVIEW_BADGE: "ww-vto-preview-badge",
|
|
31
7
|
};
|
|
32
8
|
/** Z-index values for proper layering */
|
|
33
9
|
const Z_INDEX = {
|
|
34
10
|
BUTTON: 10,
|
|
35
11
|
MODAL: 99999,
|
|
36
12
|
};
|
|
37
|
-
/** Camera constraints for optimal capture */
|
|
38
|
-
const CAMERA_CONSTRAINTS = {
|
|
39
|
-
video: {
|
|
40
|
-
width: { ideal: 1280 },
|
|
41
|
-
height: { ideal: 720 },
|
|
42
|
-
facingMode: "user", // Front-facing camera
|
|
43
|
-
},
|
|
44
|
-
audio: false,
|
|
45
|
-
};
|
|
46
|
-
/** Image capture settings */
|
|
47
|
-
const IMAGE_SETTINGS = {
|
|
48
|
-
FORMAT: "image/jpeg",
|
|
49
|
-
QUALITY: 0.8,
|
|
50
|
-
};
|
|
51
13
|
|
|
52
|
-
async function startCamera(video, loadingIndicator, captureButton) {
|
|
53
|
-
console.log("[WeWear VTO] Starting camera...");
|
|
54
|
-
try {
|
|
55
|
-
console.log("[WeWear VTO] Requesting camera access...");
|
|
56
|
-
const stream = await navigator.mediaDevices.getUserMedia(CAMERA_CONSTRAINTS);
|
|
57
|
-
video.srcObject = stream;
|
|
58
|
-
video.onloadedmetadata = () => {
|
|
59
|
-
console.log("[WeWear VTO] Camera stream loaded successfully");
|
|
60
|
-
loadingIndicator.style.display = "none";
|
|
61
|
-
captureButton.style.display = "flex";
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
console.error("[WeWear VTO] Camera access error:", error);
|
|
66
|
-
loadingIndicator.innerHTML =
|
|
67
|
-
"Camera access denied. Please allow camera permissions.";
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function stopCamera(video) {
|
|
71
|
-
const stream = video.srcObject;
|
|
72
|
-
if (stream) {
|
|
73
|
-
const tracks = stream.getTracks();
|
|
74
|
-
tracks.forEach((track) => {
|
|
75
|
-
track.stop();
|
|
76
|
-
});
|
|
77
|
-
video.srcObject = null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async function captureImageFromVideo(video, canvas) {
|
|
81
|
-
// Set canvas dimensions to match video
|
|
82
|
-
canvas.width = video.videoWidth;
|
|
83
|
-
canvas.height = video.videoHeight;
|
|
84
|
-
// Draw video frame to canvas
|
|
85
|
-
const ctx = canvas.getContext("2d");
|
|
86
|
-
if (!ctx) {
|
|
87
|
-
throw new Error("Could not get canvas context");
|
|
88
|
-
}
|
|
89
|
-
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
90
|
-
// Convert canvas to blob
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
canvas.toBlob((blob) => {
|
|
93
|
-
if (blob) {
|
|
94
|
-
resolve(blob);
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
reject(new Error("Failed to create blob from canvas"));
|
|
98
|
-
}
|
|
99
|
-
}, IMAGE_SETTINGS.FORMAT, IMAGE_SETTINGS.QUALITY);
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function getCookie(name) {
|
|
104
|
-
var _a;
|
|
105
|
-
if (typeof document === "undefined")
|
|
106
|
-
return null;
|
|
107
|
-
const value = `; ${document.cookie}`;
|
|
108
|
-
const parts = value.split(`; ${name}=`);
|
|
109
|
-
if (parts.length === 2) {
|
|
110
|
-
const part = parts.pop();
|
|
111
|
-
return part ? ((_a = part.split(";").shift()) === null || _a === void 0 ? void 0 : _a.trim()) || null : null;
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
14
|
function getPositionStyles(position) {
|
|
116
15
|
switch (position) {
|
|
117
16
|
case "bottom-left":
|
|
@@ -241,174 +140,18 @@ function createButtonContainer(buttonPosition, hasVirtualTryOn = false, onCamera
|
|
|
241
140
|
}
|
|
242
141
|
function createButton(buttonPosition, onClick, disabled = false) {
|
|
243
142
|
const container = createButtonContainer(buttonPosition, false, onClick);
|
|
244
|
-
const cameraButton = container.querySelector(
|
|
143
|
+
const cameraButton = container.querySelector(".ww-camera-btn");
|
|
245
144
|
if (cameraButton) {
|
|
246
145
|
cameraButton.disabled = disabled;
|
|
247
146
|
if (disabled) {
|
|
248
|
-
cameraButton.style.opacity =
|
|
249
|
-
cameraButton.style.cursor =
|
|
250
|
-
cameraButton.title =
|
|
147
|
+
cameraButton.style.opacity = "0.5";
|
|
148
|
+
cameraButton.style.cursor = "not-allowed";
|
|
149
|
+
cameraButton.title = "Find your ideal size first";
|
|
251
150
|
}
|
|
252
151
|
}
|
|
253
152
|
return container;
|
|
254
153
|
}
|
|
255
154
|
|
|
256
|
-
function showCameraModal(callbacks) {
|
|
257
|
-
console.log("[WeWear VTO] Opening camera modal...");
|
|
258
|
-
// Remove any existing modals first
|
|
259
|
-
removeElements(`.${CSS_CLASSES.MODAL}`);
|
|
260
|
-
// Create modal container
|
|
261
|
-
const modal = document.createElement("div");
|
|
262
|
-
modal.className = CSS_CLASSES.MODAL;
|
|
263
|
-
modal.style.cssText = `
|
|
264
|
-
position: fixed;
|
|
265
|
-
top: 0;
|
|
266
|
-
left: 0;
|
|
267
|
-
width: 100%;
|
|
268
|
-
height: 100%;
|
|
269
|
-
background-color: rgba(0, 0, 0, 0.95);
|
|
270
|
-
display: flex;
|
|
271
|
-
flex-direction: column;
|
|
272
|
-
align-items: center;
|
|
273
|
-
justify-content: center;
|
|
274
|
-
z-index: ${Z_INDEX.MODAL};
|
|
275
|
-
padding: 20px;
|
|
276
|
-
box-sizing: border-box;
|
|
277
|
-
`;
|
|
278
|
-
// Create camera container
|
|
279
|
-
const cameraContainer = document.createElement("div");
|
|
280
|
-
cameraContainer.style.cssText = `
|
|
281
|
-
position: relative;
|
|
282
|
-
width: 100%;
|
|
283
|
-
max-width: 500px;
|
|
284
|
-
height: 70vh;
|
|
285
|
-
background-color: #000;
|
|
286
|
-
border-radius: 12px;
|
|
287
|
-
overflow: hidden;
|
|
288
|
-
display: flex;
|
|
289
|
-
flex-direction: column;
|
|
290
|
-
align-items: center;
|
|
291
|
-
justify-content: center;
|
|
292
|
-
`;
|
|
293
|
-
// Create video element
|
|
294
|
-
const video = document.createElement("video");
|
|
295
|
-
video.autoplay = true;
|
|
296
|
-
video.playsInline = true;
|
|
297
|
-
video.muted = true;
|
|
298
|
-
video.style.cssText = `
|
|
299
|
-
width: 100%;
|
|
300
|
-
height: 100%;
|
|
301
|
-
object-fit: cover;
|
|
302
|
-
border-radius: 12px;
|
|
303
|
-
`;
|
|
304
|
-
// Create canvas for capturing
|
|
305
|
-
const canvas = document.createElement("canvas");
|
|
306
|
-
canvas.style.display = "none";
|
|
307
|
-
// Create face overlay
|
|
308
|
-
const faceOverlay = document.createElement("div");
|
|
309
|
-
faceOverlay.style.cssText = `
|
|
310
|
-
position: absolute;
|
|
311
|
-
top: 0;
|
|
312
|
-
left: 0;
|
|
313
|
-
width: 100%;
|
|
314
|
-
height: 100%;
|
|
315
|
-
pointer-events: none;
|
|
316
|
-
display: flex;
|
|
317
|
-
align-items: center;
|
|
318
|
-
justify-content: center;
|
|
319
|
-
z-index: 2;
|
|
320
|
-
`;
|
|
321
|
-
// SVG face outline (simple oval)
|
|
322
|
-
faceOverlay.innerHTML = `
|
|
323
|
-
<svg width="380" height="520" viewBox="0 0 380 520" fill="none" xmlns="http://www.w3.org/2000/svg" style="opacity:0.7;">
|
|
324
|
-
<ellipse cx="190" cy="240" rx="160" ry="220" stroke="rgba(0, 0, 0, 0.7)" stroke-width="6" fill="none" />
|
|
325
|
-
</svg>
|
|
326
|
-
`;
|
|
327
|
-
// Create capture button
|
|
328
|
-
const captureButton = document.createElement("button");
|
|
329
|
-
captureButton.innerHTML = "";
|
|
330
|
-
captureButton.style.cssText = `
|
|
331
|
-
position: absolute;
|
|
332
|
-
bottom: 20px;
|
|
333
|
-
left: 50%;
|
|
334
|
-
transform: translateX(-50%);
|
|
335
|
-
width: 60px;
|
|
336
|
-
height: 60px;
|
|
337
|
-
border: 3px solid white;
|
|
338
|
-
background-color: rgba(0, 0, 0, 0.7);
|
|
339
|
-
border-radius: 50%;
|
|
340
|
-
cursor: pointer;
|
|
341
|
-
display: flex;
|
|
342
|
-
align-items: center;
|
|
343
|
-
justify-content: center;
|
|
344
|
-
transition: all 0.2s ease;
|
|
345
|
-
`;
|
|
346
|
-
// Create close button
|
|
347
|
-
const closeButton = document.createElement("button");
|
|
348
|
-
closeButton.innerHTML = "×";
|
|
349
|
-
closeButton.style.cssText = `
|
|
350
|
-
position: absolute;
|
|
351
|
-
top: 15px;
|
|
352
|
-
right: 15px;
|
|
353
|
-
width: 40px;
|
|
354
|
-
height: 40px;
|
|
355
|
-
border: none;
|
|
356
|
-
background-color: rgba(0, 0, 0, 0.7);
|
|
357
|
-
color: white;
|
|
358
|
-
font-size: 24px;
|
|
359
|
-
font-weight: bold;
|
|
360
|
-
cursor: pointer;
|
|
361
|
-
border-radius: 50%;
|
|
362
|
-
display: flex;
|
|
363
|
-
align-items: center;
|
|
364
|
-
justify-content: center;
|
|
365
|
-
`;
|
|
366
|
-
// Create loading indicator
|
|
367
|
-
const loadingIndicator = document.createElement("div");
|
|
368
|
-
loadingIndicator.innerHTML = "Initializing camera...";
|
|
369
|
-
loadingIndicator.style.cssText = `
|
|
370
|
-
color: white;
|
|
371
|
-
font-size: 16px;
|
|
372
|
-
position: absolute;
|
|
373
|
-
top: 50%;
|
|
374
|
-
left: 50%;
|
|
375
|
-
transform: translate(-50%, -50%);
|
|
376
|
-
`;
|
|
377
|
-
// Add event listeners
|
|
378
|
-
closeButton.onclick = () => {
|
|
379
|
-
stopCamera(video);
|
|
380
|
-
modal.remove();
|
|
381
|
-
};
|
|
382
|
-
captureButton.onclick = async () => {
|
|
383
|
-
await callbacks.onCapture(video, canvas);
|
|
384
|
-
};
|
|
385
|
-
// Assemble the camera modal
|
|
386
|
-
cameraContainer.appendChild(video);
|
|
387
|
-
cameraContainer.appendChild(faceOverlay);
|
|
388
|
-
cameraContainer.appendChild(canvas);
|
|
389
|
-
cameraContainer.appendChild(captureButton);
|
|
390
|
-
cameraContainer.appendChild(closeButton);
|
|
391
|
-
modal.appendChild(cameraContainer);
|
|
392
|
-
// Add instruction text outside the camera box
|
|
393
|
-
const instructionText = document.createElement("div");
|
|
394
|
-
instructionText.style.cssText = `
|
|
395
|
-
width: 100%;
|
|
396
|
-
max-width: 500px;
|
|
397
|
-
margin: 18px auto 0 auto;
|
|
398
|
-
text-align: center;
|
|
399
|
-
color: #fff;
|
|
400
|
-
font-size: 18px;
|
|
401
|
-
font-weight: bold;
|
|
402
|
-
text-shadow: 0 2px 8px #000;
|
|
403
|
-
`;
|
|
404
|
-
instructionText.innerText = "Please align your face inside the outline for better results.";
|
|
405
|
-
modal.appendChild(instructionText);
|
|
406
|
-
document.body.appendChild(modal);
|
|
407
|
-
console.log("[WeWear VTO] Camera modal added to DOM");
|
|
408
|
-
// Start camera
|
|
409
|
-
startCamera(video, loadingIndicator, captureButton);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
155
|
/**
|
|
413
156
|
* Creates a loading overlay that can be shown in different containers
|
|
414
157
|
*/
|
|
@@ -461,24 +204,6 @@ function createLoadingOverlay(text = "Processing...") {
|
|
|
461
204
|
`;
|
|
462
205
|
return overlay;
|
|
463
206
|
}
|
|
464
|
-
/**
|
|
465
|
-
* Shows a loading overlay in a modal container
|
|
466
|
-
*/
|
|
467
|
-
function showModalLoading(container) {
|
|
468
|
-
// Remove any existing loading overlays
|
|
469
|
-
removeModalLoading();
|
|
470
|
-
// Show loading in the product area
|
|
471
|
-
showProductLoading(container);
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* Removes loading overlay from modal
|
|
475
|
-
*/
|
|
476
|
-
function removeModalLoading() {
|
|
477
|
-
const existingOverlay = document.body.querySelector(".ww-loading-overlay");
|
|
478
|
-
if (existingOverlay) {
|
|
479
|
-
existingOverlay.remove();
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
207
|
/**
|
|
483
208
|
* Shows a loading overlay in the product gallery container
|
|
484
209
|
*/
|
|
@@ -503,202 +228,115 @@ function removeProductLoading(container) {
|
|
|
503
228
|
}
|
|
504
229
|
}
|
|
505
230
|
|
|
506
|
-
function
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
image.alt = "Review your photo";
|
|
550
|
-
image.style.cssText = `
|
|
551
|
-
width: 100%;
|
|
552
|
-
height: 100%;
|
|
553
|
-
object-fit: cover;
|
|
554
|
-
border-radius: 12px;
|
|
555
|
-
`;
|
|
556
|
-
// Create close button
|
|
557
|
-
const closeButton = document.createElement("button");
|
|
558
|
-
closeButton.innerHTML = "×";
|
|
559
|
-
closeButton.style.cssText = `
|
|
560
|
-
position: absolute;
|
|
561
|
-
top: 15px;
|
|
562
|
-
right: 15px;
|
|
563
|
-
width: 40px;
|
|
564
|
-
height: 40px;
|
|
565
|
-
border: none;
|
|
566
|
-
background-color: rgba(0, 0, 0, 0.7);
|
|
567
|
-
color: white;
|
|
568
|
-
font-size: 24px;
|
|
569
|
-
font-weight: bold;
|
|
570
|
-
cursor: pointer;
|
|
571
|
-
border-radius: 50%;
|
|
572
|
-
display: flex;
|
|
573
|
-
align-items: center;
|
|
574
|
-
justify-content: center;
|
|
575
|
-
`;
|
|
576
|
-
// Create button container
|
|
577
|
-
const buttonContainer = document.createElement("div");
|
|
578
|
-
buttonContainer.style.cssText = `
|
|
579
|
-
display: flex;
|
|
580
|
-
gap: 15px;
|
|
581
|
-
width: 100%;
|
|
582
|
-
max-width: 500px;
|
|
583
|
-
`;
|
|
584
|
-
// Create retake button
|
|
585
|
-
const retakeButton = document.createElement("button");
|
|
586
|
-
retakeButton.textContent = "Retake";
|
|
587
|
-
retakeButton.style.cssText = `
|
|
588
|
-
flex: 1;
|
|
589
|
-
padding: 12px 24px;
|
|
590
|
-
background-color: rgba(255, 255, 255, 0.9);
|
|
591
|
-
color: black;
|
|
592
|
-
border-radius: 8px;
|
|
593
|
-
border: none;
|
|
594
|
-
font-size: 16px;
|
|
595
|
-
font-weight: normal;
|
|
596
|
-
cursor: pointer;
|
|
597
|
-
`;
|
|
598
|
-
// Create use photo button
|
|
599
|
-
const usePhotoButton = document.createElement("button");
|
|
600
|
-
usePhotoButton.textContent = "Use Photo";
|
|
601
|
-
usePhotoButton.style.cssText = `
|
|
602
|
-
flex: 1;
|
|
603
|
-
padding: 12px 24px;
|
|
604
|
-
background-color: rgba(0, 0, 0, 0.7);
|
|
605
|
-
color: white;
|
|
606
|
-
border-radius: 8px;
|
|
607
|
-
border: none;
|
|
608
|
-
font-size: 16px;
|
|
609
|
-
font-weight: normal;
|
|
610
|
-
cursor: pointer;
|
|
611
|
-
`;
|
|
612
|
-
// Add event listeners
|
|
613
|
-
closeButton.onclick = () => {
|
|
614
|
-
URL.revokeObjectURL(imageUrl); // Clean up
|
|
615
|
-
modal.remove();
|
|
616
|
-
};
|
|
617
|
-
retakeButton.onclick = () => {
|
|
618
|
-
URL.revokeObjectURL(imageUrl); // Clean up
|
|
619
|
-
modal.remove();
|
|
620
|
-
callbacks.onRetake();
|
|
621
|
-
};
|
|
622
|
-
usePhotoButton.onclick = async () => {
|
|
623
|
-
URL.revokeObjectURL(imageUrl); // Clean up
|
|
624
|
-
modal.remove();
|
|
625
|
-
await callbacks.onAccept(imageBlob);
|
|
626
|
-
};
|
|
627
|
-
// Assemble the review modal
|
|
628
|
-
buttonContainer.appendChild(retakeButton);
|
|
629
|
-
buttonContainer.appendChild(usePhotoButton);
|
|
630
|
-
reviewContainer.appendChild(image);
|
|
631
|
-
reviewContainer.appendChild(closeButton);
|
|
632
|
-
modal.appendChild(reviewContainer);
|
|
633
|
-
modal.appendChild(buttonContainer);
|
|
634
|
-
document.body.appendChild(modal);
|
|
635
|
-
console.log("[WeWear VTO] Review modal added to DOM");
|
|
231
|
+
function createPreviewBadge() {
|
|
232
|
+
const badge = document.createElement("div");
|
|
233
|
+
badge.className = CSS_CLASSES.PREVIEW_BADGE;
|
|
234
|
+
badge.innerText = "PREVIEW";
|
|
235
|
+
const styleId = "ww-vto-badge-styles";
|
|
236
|
+
if (!document.getElementById(styleId)) {
|
|
237
|
+
const styles = document.createElement("style");
|
|
238
|
+
styles.id = styleId;
|
|
239
|
+
styles.innerHTML = `
|
|
240
|
+
.${CSS_CLASSES.PREVIEW_BADGE} {
|
|
241
|
+
position: absolute;
|
|
242
|
+
top: 16px;
|
|
243
|
+
right: 16px;
|
|
244
|
+
background-color: rgba(0, 0, 0, 0.6);
|
|
245
|
+
color: white;
|
|
246
|
+
padding: 4px 8px;
|
|
247
|
+
border-radius: 4px;
|
|
248
|
+
font-size: 12px;
|
|
249
|
+
font-weight: 600;
|
|
250
|
+
z-index: 10;
|
|
251
|
+
animation: ww-vto-pulse 2s infinite;
|
|
252
|
+
pointer-events: none;
|
|
253
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@keyframes ww-vto-pulse {
|
|
257
|
+
0% {
|
|
258
|
+
transform: scale(1);
|
|
259
|
+
opacity: 0.9;
|
|
260
|
+
}
|
|
261
|
+
50% {
|
|
262
|
+
transform: scale(1.05);
|
|
263
|
+
opacity: 1;
|
|
264
|
+
}
|
|
265
|
+
100% {
|
|
266
|
+
transform: scale(1);
|
|
267
|
+
opacity: 0.9;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
`;
|
|
271
|
+
document.head.appendChild(styles);
|
|
272
|
+
}
|
|
273
|
+
return badge;
|
|
636
274
|
}
|
|
637
275
|
|
|
638
276
|
function showAlert(container, message, type) {
|
|
639
277
|
var _a;
|
|
640
278
|
removeAlert(container); // Remove any existing alert
|
|
641
|
-
const alertDiv = document.createElement(
|
|
279
|
+
const alertDiv = document.createElement("div");
|
|
642
280
|
alertDiv.className = `ww-alert ww-alert-${type}`;
|
|
643
|
-
alertDiv.setAttribute(
|
|
644
|
-
alertDiv.setAttribute(
|
|
645
|
-
alertDiv.style.cssText = `
|
|
646
|
-
position: absolute;
|
|
647
|
-
top: 16px;
|
|
648
|
-
left: 50%;
|
|
649
|
-
transform: translateX(-50%) translateY(-10px);
|
|
650
|
-
background: ${
|
|
651
|
-
color: ${
|
|
652
|
-
border: 1px solid ${
|
|
653
|
-
border-radius: 8px;
|
|
654
|
-
padding: 12px 16px;
|
|
655
|
-
z-index: 9999;
|
|
656
|
-
font-size: 15px;
|
|
657
|
-
font-weight: 500;
|
|
658
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
659
|
-
display: flex;
|
|
660
|
-
align-items: center;
|
|
661
|
-
min-width: 220px;
|
|
662
|
-
max-width: 90%;
|
|
663
|
-
opacity: 0;
|
|
664
|
-
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
281
|
+
alertDiv.setAttribute("role", "alert" );
|
|
282
|
+
alertDiv.setAttribute("aria-live", "assertive");
|
|
283
|
+
alertDiv.style.cssText = `
|
|
284
|
+
position: absolute;
|
|
285
|
+
top: 16px;
|
|
286
|
+
left: 50%;
|
|
287
|
+
transform: translateX(-50%) translateY(-10px);
|
|
288
|
+
background: ${"#FEE2E2" };
|
|
289
|
+
color: ${"#B91C1C" };
|
|
290
|
+
border: 1px solid ${"#F87171" };
|
|
291
|
+
border-radius: 8px;
|
|
292
|
+
padding: 12px 16px;
|
|
293
|
+
z-index: 9999;
|
|
294
|
+
font-size: 15px;
|
|
295
|
+
font-weight: 500;
|
|
296
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
297
|
+
display: flex;
|
|
298
|
+
align-items: center;
|
|
299
|
+
min-width: 220px;
|
|
300
|
+
max-width: 90%;
|
|
301
|
+
opacity: 0;
|
|
302
|
+
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
665
303
|
`;
|
|
666
|
-
alertDiv.innerHTML = `
|
|
667
|
-
<span style="flex: 1; line-height: 1.4;">${message}</span>
|
|
668
|
-
<button type="button" aria-label="Close alert"
|
|
669
|
-
style="
|
|
670
|
-
background: none;
|
|
671
|
-
border: none;
|
|
672
|
-
font-size: 20px;
|
|
673
|
-
color: inherit;
|
|
674
|
-
cursor: pointer;
|
|
675
|
-
margin-left: 12px;
|
|
676
|
-
padding: 0;
|
|
677
|
-
line-height: 1;
|
|
678
|
-
">
|
|
679
|
-
×
|
|
680
|
-
</button>
|
|
304
|
+
alertDiv.innerHTML = `
|
|
305
|
+
<span style="flex: 1; line-height: 1.4;">${message}</span>
|
|
306
|
+
<button type="button" aria-label="Close alert"
|
|
307
|
+
style="
|
|
308
|
+
background: none;
|
|
309
|
+
border: none;
|
|
310
|
+
font-size: 20px;
|
|
311
|
+
color: inherit;
|
|
312
|
+
cursor: pointer;
|
|
313
|
+
margin-left: 12px;
|
|
314
|
+
padding: 0;
|
|
315
|
+
line-height: 1;
|
|
316
|
+
">
|
|
317
|
+
×
|
|
318
|
+
</button>
|
|
681
319
|
`;
|
|
682
320
|
// Close on button click
|
|
683
|
-
(_a = alertDiv.querySelector(
|
|
321
|
+
(_a = alertDiv.querySelector("button")) === null || _a === void 0 ? void 0 : _a.addEventListener("click", () => {
|
|
684
322
|
fadeOutAndRemove(alertDiv);
|
|
685
323
|
});
|
|
686
324
|
container.appendChild(alertDiv);
|
|
687
325
|
// Animate in
|
|
688
326
|
requestAnimationFrame(() => {
|
|
689
|
-
alertDiv.style.opacity =
|
|
690
|
-
alertDiv.style.transform =
|
|
327
|
+
alertDiv.style.opacity = "1";
|
|
328
|
+
alertDiv.style.transform = "translateX(-50%) translateY(0)";
|
|
691
329
|
});
|
|
692
330
|
}
|
|
693
331
|
function fadeOutAndRemove(element) {
|
|
694
|
-
element.style.opacity =
|
|
695
|
-
element.style.transform =
|
|
332
|
+
element.style.opacity = "0";
|
|
333
|
+
element.style.transform = "translateX(-50%) translateY(-10px)";
|
|
696
334
|
setTimeout(() => {
|
|
697
335
|
element.remove();
|
|
698
336
|
}, 300);
|
|
699
337
|
}
|
|
700
338
|
function removeAlert(container) {
|
|
701
|
-
const alert = container.querySelector(
|
|
339
|
+
const alert = container.querySelector(".ww-alert");
|
|
702
340
|
if (alert)
|
|
703
341
|
fadeOutAndRemove(alert);
|
|
704
342
|
}
|
|
@@ -706,10 +344,13 @@ function removeAlert(container) {
|
|
|
706
344
|
class VirtualTryOnWidget {
|
|
707
345
|
constructor(config) {
|
|
708
346
|
this.virtualTryOnImageUrl = null;
|
|
347
|
+
this.originalProductImageUrl = null;
|
|
709
348
|
this.isShowingVirtualTryOn = false;
|
|
710
|
-
this.
|
|
349
|
+
this.lastModelImage = null;
|
|
711
350
|
this.cookieCheckInterval = null;
|
|
712
351
|
this.cameraButton = null;
|
|
352
|
+
this.iframeMessageListener = null;
|
|
353
|
+
this.previewBadge = null;
|
|
713
354
|
this.config = {
|
|
714
355
|
baseUrl: config.baseUrl,
|
|
715
356
|
productPageSelector: config.productPageSelector,
|
|
@@ -738,40 +379,18 @@ class VirtualTryOnWidget {
|
|
|
738
379
|
if (getComputedStyle(container).position === "static") {
|
|
739
380
|
container.style.position = "relative";
|
|
740
381
|
}
|
|
741
|
-
// Check required cookies
|
|
742
|
-
const ww_access_token = getCookie("ww_access_token");
|
|
743
|
-
const ww_user_id = getCookie("ww_user_id");
|
|
744
|
-
const cookiesPresent = !!ww_access_token && !!ww_user_id;
|
|
745
382
|
// Create and add the virtual try-on button
|
|
746
383
|
const button = createButton(this.config.buttonPosition, async () => {
|
|
747
384
|
if (this.cameraButton && !this.cameraButton.disabled) {
|
|
748
385
|
await this.handleTryOnClick();
|
|
749
386
|
}
|
|
750
|
-
}
|
|
387
|
+
});
|
|
751
388
|
// Store reference to camera button for dynamic enable/disable
|
|
752
|
-
this.cameraButton = button.querySelector(
|
|
389
|
+
this.cameraButton = button.querySelector(".ww-camera-btn");
|
|
753
390
|
container.appendChild(button);
|
|
754
391
|
console.log("[WeWear VTO] Widget initialized successfully");
|
|
755
|
-
//
|
|
756
|
-
this.
|
|
757
|
-
const accessToken = getCookie("ww_access_token");
|
|
758
|
-
const userId = getCookie("ww_user_id");
|
|
759
|
-
const present = !!accessToken && !!userId;
|
|
760
|
-
if (this.cameraButton) {
|
|
761
|
-
if (present && this.cameraButton.disabled) {
|
|
762
|
-
this.cameraButton.disabled = false;
|
|
763
|
-
this.cameraButton.style.opacity = '1';
|
|
764
|
-
this.cameraButton.style.cursor = 'pointer';
|
|
765
|
-
this.cameraButton.title = '';
|
|
766
|
-
}
|
|
767
|
-
else if (!present && !this.cameraButton.disabled) {
|
|
768
|
-
this.cameraButton.disabled = true;
|
|
769
|
-
this.cameraButton.style.opacity = '0.5';
|
|
770
|
-
this.cameraButton.style.cursor = 'not-allowed';
|
|
771
|
-
this.cameraButton.title = 'Required cookies missing';
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}, 2000); // check every 2 seconds
|
|
392
|
+
// Listen for messages from the photo upload page
|
|
393
|
+
this.setupIframeListener();
|
|
775
394
|
}
|
|
776
395
|
catch (error) {
|
|
777
396
|
console.error("[WeWear VTO] Initialization failed:", error);
|
|
@@ -785,152 +404,214 @@ class VirtualTryOnWidget {
|
|
|
785
404
|
console.log("[WeWear VTO] Button clicked, starting try-on process...");
|
|
786
405
|
try {
|
|
787
406
|
// Get required data
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
407
|
+
if (!this.originalProductImageUrl) {
|
|
408
|
+
const productImageElement = document.querySelector(this.config.productImageSelector);
|
|
409
|
+
this.originalProductImageUrl =
|
|
410
|
+
(productImageElement === null || productImageElement === void 0 ? void 0 : productImageElement.src) ||
|
|
411
|
+
(productImageElement === null || productImageElement === void 0 ? void 0 : productImageElement.getAttribute("data-src")) ||
|
|
412
|
+
"";
|
|
413
|
+
}
|
|
414
|
+
const ww_product_image = this.originalProductImageUrl;
|
|
792
415
|
console.log("[WeWear VTO] Retrieved data:", {
|
|
793
|
-
ww_user_id,
|
|
794
416
|
ww_product_image,
|
|
795
|
-
ww_access_token,
|
|
796
417
|
});
|
|
797
|
-
// Validate required data
|
|
798
|
-
if (!ww_user_id) {
|
|
799
|
-
console.warn("[WeWear VTO] Missing required cookie: ww_user_id");
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
418
|
if (!ww_product_image) {
|
|
803
419
|
console.warn("[WeWear VTO] Product image not found:", this.config.productImageSelector);
|
|
804
420
|
return;
|
|
805
421
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
// Show camera capture modal
|
|
811
|
-
this.showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_image);
|
|
422
|
+
// Open the photo upload page in a modal
|
|
423
|
+
const photoUploadUrl = new URL(`${this.config.baseUrl}`);
|
|
424
|
+
photoUploadUrl.searchParams.append("ww_product_image", ww_product_image);
|
|
425
|
+
this.showPhotoUploadModal(photoUploadUrl.toString());
|
|
812
426
|
}
|
|
813
427
|
catch (error) {
|
|
814
428
|
console.error("[WeWear VTO] Try-on request failed:", error);
|
|
815
429
|
}
|
|
816
430
|
}
|
|
817
431
|
/**
|
|
818
|
-
* Shows
|
|
432
|
+
* Shows a modal with an iframe for the photo upload page
|
|
819
433
|
* @private
|
|
820
434
|
*/
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
435
|
+
showPhotoUploadModal(url) {
|
|
436
|
+
// Remove existing modals
|
|
437
|
+
removeElements(`.${CSS_CLASSES.MODAL}`);
|
|
438
|
+
const modal = document.createElement("div");
|
|
439
|
+
modal.className = CSS_CLASSES.MODAL;
|
|
440
|
+
modal.style.cssText = `
|
|
441
|
+
position: fixed;
|
|
442
|
+
top: 0;
|
|
443
|
+
left: 0;
|
|
444
|
+
width: 100%;
|
|
445
|
+
height: 100%;
|
|
446
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
447
|
+
display: flex;
|
|
448
|
+
justify-content: center;
|
|
449
|
+
align-items: center;
|
|
450
|
+
z-index: 1000;
|
|
451
|
+
`;
|
|
452
|
+
const iframe = document.createElement("iframe");
|
|
453
|
+
iframe.src = url;
|
|
454
|
+
iframe.style.cssText = `
|
|
455
|
+
width: 90%;
|
|
456
|
+
height: 90%;
|
|
457
|
+
max-width: 800px;
|
|
458
|
+
max-height: 600px;
|
|
459
|
+
border: none;
|
|
460
|
+
border-radius: 8px;
|
|
461
|
+
`;
|
|
462
|
+
const closeButton = document.createElement("button");
|
|
463
|
+
closeButton.innerText = "X";
|
|
464
|
+
closeButton.style.cssText = `
|
|
465
|
+
position: absolute;
|
|
466
|
+
top: 10px;
|
|
467
|
+
right: 10px;
|
|
468
|
+
background: white;
|
|
469
|
+
border: none;
|
|
470
|
+
border-radius: 50%;
|
|
471
|
+
width: 30px;
|
|
472
|
+
height: 30px;
|
|
473
|
+
cursor: pointer;
|
|
474
|
+
`;
|
|
475
|
+
closeButton.onclick = () => {
|
|
476
|
+
modal.remove();
|
|
852
477
|
};
|
|
853
|
-
|
|
478
|
+
modal.appendChild(iframe);
|
|
479
|
+
modal.appendChild(closeButton);
|
|
480
|
+
document.body.appendChild(modal);
|
|
854
481
|
}
|
|
855
482
|
/**
|
|
856
|
-
*
|
|
483
|
+
* Sets up listener for messages from the iframe
|
|
857
484
|
* @private
|
|
858
485
|
*/
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
this.
|
|
866
|
-
|
|
867
|
-
ww_access_token,
|
|
868
|
-
ww_user_id,
|
|
869
|
-
ww_product_image,
|
|
870
|
-
};
|
|
871
|
-
// Call the API with the accepted image
|
|
872
|
-
const result = await callVirtualTryOnApi(this.config.baseUrl, ww_access_token, ww_user_id, ww_product_image, imageBlob);
|
|
873
|
-
// Remove modal loading and close modal
|
|
874
|
-
removeModalLoading();
|
|
875
|
-
removeElements(`.${CSS_CLASSES.MODAL}`);
|
|
876
|
-
if (result === null || result === void 0 ? void 0 : result.imageUrl) {
|
|
877
|
-
this.replaceProductImage(result.imageUrl);
|
|
486
|
+
setupIframeListener() {
|
|
487
|
+
// Remove existing listener if any
|
|
488
|
+
if (this.iframeMessageListener) {
|
|
489
|
+
window.removeEventListener("message", this.iframeMessageListener);
|
|
490
|
+
}
|
|
491
|
+
this.iframeMessageListener = (event) => {
|
|
492
|
+
if (event.origin !== new URL(this.config.baseUrl).origin) {
|
|
493
|
+
return;
|
|
878
494
|
}
|
|
879
|
-
|
|
880
|
-
|
|
495
|
+
switch (event.data.type) {
|
|
496
|
+
case "VTO_IMAGE_SELECTED":
|
|
497
|
+
if (event.data.image) {
|
|
498
|
+
this.lastModelImage = event.data.image;
|
|
499
|
+
this.startVirtualTryOn();
|
|
500
|
+
}
|
|
501
|
+
break;
|
|
502
|
+
case "CLOSE_MODAL":
|
|
503
|
+
removeElements(`.${CSS_CLASSES.MODAL}`);
|
|
504
|
+
break;
|
|
881
505
|
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
console.error("[WeWear VTO] Error processing accepted image:", error);
|
|
885
|
-
// Remove loading on error
|
|
886
|
-
removeModalLoading();
|
|
887
|
-
}
|
|
506
|
+
};
|
|
507
|
+
window.addEventListener("message", this.iframeMessageListener);
|
|
888
508
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
console.warn("[WeWear VTO] No previous API parameters available for refresh");
|
|
509
|
+
async startVirtualTryOn(isRefresh = false) {
|
|
510
|
+
if (isRefresh) {
|
|
511
|
+
console.log("[WeWear VTO] Refreshing virtual try-on with previous parameters...");
|
|
512
|
+
}
|
|
513
|
+
if (!this.lastModelImage || !this.originalProductImageUrl) {
|
|
514
|
+
console.warn("[WeWear VTO] Missing required data to start virtual try-on.");
|
|
896
515
|
return;
|
|
897
516
|
}
|
|
517
|
+
// Hide the modal and show loading indicator
|
|
518
|
+
const modal = document.querySelector(`.${CSS_CLASSES.MODAL}`);
|
|
519
|
+
if (modal && modal instanceof HTMLElement) {
|
|
520
|
+
modal.style.display = "none";
|
|
521
|
+
}
|
|
522
|
+
const container = document.querySelector(this.config.gallerySelector);
|
|
523
|
+
if (container instanceof HTMLElement) {
|
|
524
|
+
showProductLoading(container, "Generating your virtual try-on...");
|
|
525
|
+
}
|
|
898
526
|
try {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
527
|
+
const submitResponse = await this.callVtoApi(this.lastModelImage, this.originalProductImageUrl);
|
|
528
|
+
let status = await this.fetchJobStatus(submitResponse.job_id);
|
|
529
|
+
const previews = [];
|
|
530
|
+
while (status.status !== "COMPLETED" && status.status !== "FAILED") {
|
|
531
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
532
|
+
status = await this.fetchJobStatus(submitResponse.job_id);
|
|
533
|
+
for (let i = previews.length; i < status.previews_available; i++) {
|
|
534
|
+
try {
|
|
535
|
+
const previewUrl = await this.fetchJobImage(submitResponse.job_id, i);
|
|
536
|
+
previews.push(previewUrl);
|
|
537
|
+
this.replaceProductImage(previewUrl);
|
|
538
|
+
if (!this.previewBadge && container instanceof HTMLElement) {
|
|
539
|
+
this.previewBadge = createPreviewBadge();
|
|
540
|
+
container.appendChild(this.previewBadge);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch (e) {
|
|
544
|
+
if (!(e instanceof Error && e.message === "202_PROCESSING"))
|
|
545
|
+
throw e;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
904
548
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
this.showVirtualTryOnImage(container);
|
|
914
|
-
this.updateButtonContainer(container);
|
|
549
|
+
if (status.status === "COMPLETED") {
|
|
550
|
+
const finalImage = await this.fetchJobImage(submitResponse.job_id);
|
|
551
|
+
if (finalImage) {
|
|
552
|
+
this.replaceProductImage(finalImage);
|
|
553
|
+
}
|
|
554
|
+
if (this.previewBadge) {
|
|
555
|
+
this.previewBadge.remove();
|
|
556
|
+
this.previewBadge = null;
|
|
915
557
|
}
|
|
916
558
|
}
|
|
917
|
-
|
|
918
|
-
|
|
559
|
+
if (status.status === "FAILED") {
|
|
560
|
+
console.error("[WeWear VTO] VTO process failed:", status.message);
|
|
919
561
|
if (container instanceof HTMLElement) {
|
|
920
|
-
|
|
921
|
-
showAlert(container, result.error.message, result.error.type);
|
|
562
|
+
showAlert(container, status.message, "error");
|
|
922
563
|
}
|
|
923
564
|
}
|
|
924
565
|
}
|
|
925
566
|
catch (error) {
|
|
926
|
-
console.error("[WeWear VTO] Error
|
|
927
|
-
|
|
928
|
-
|
|
567
|
+
console.error("[WeWear VTO] Error during virtual try-on process:", error);
|
|
568
|
+
if (container instanceof HTMLElement) {
|
|
569
|
+
showAlert(container, "An unexpected error occurred.", "error");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
929
573
|
if (container instanceof HTMLElement) {
|
|
930
574
|
removeProductLoading(container);
|
|
931
575
|
}
|
|
576
|
+
removeElements(`.${CSS_CLASSES.MODAL}`);
|
|
932
577
|
}
|
|
933
578
|
}
|
|
579
|
+
async callVtoApi(modelImage, productImage) {
|
|
580
|
+
const formData = new FormData();
|
|
581
|
+
formData.append("model_image", modelImage, "model_image.png");
|
|
582
|
+
formData.append("ww_product_image", productImage);
|
|
583
|
+
const res = await fetch(`${this.config.baseUrl}/api/vto`, {
|
|
584
|
+
method: "POST",
|
|
585
|
+
body: formData,
|
|
586
|
+
});
|
|
587
|
+
if (!res.ok) {
|
|
588
|
+
const errorText = await res.text();
|
|
589
|
+
console.error("[WeWear VTO] API submission failed:", res.status, errorText);
|
|
590
|
+
throw new Error(`API submission failed: ${res.status}`);
|
|
591
|
+
}
|
|
592
|
+
return res.json();
|
|
593
|
+
}
|
|
594
|
+
async fetchJobStatus(jobId) {
|
|
595
|
+
const res = await fetch(`${this.config.baseUrl}/api/vto/status?job_id=${jobId}`);
|
|
596
|
+
if (!res.ok)
|
|
597
|
+
throw new Error(`Status check failed: ${res.status}`);
|
|
598
|
+
return res.json();
|
|
599
|
+
}
|
|
600
|
+
async fetchJobImage(jobId, previewIndex) {
|
|
601
|
+
let url = `${this.config.baseUrl}/api/vto/image?job_id=${jobId}`;
|
|
602
|
+
if (previewIndex !== undefined)
|
|
603
|
+
url += `&preview_index=${previewIndex}`;
|
|
604
|
+
const res = await fetch(url);
|
|
605
|
+
if (res.status === 202)
|
|
606
|
+
throw new Error("202_PROCESSING");
|
|
607
|
+
if (!res.ok)
|
|
608
|
+
throw new Error(`Image fetch failed: ${res.status}`);
|
|
609
|
+
const blob = await res.blob();
|
|
610
|
+
return URL.createObjectURL(blob);
|
|
611
|
+
}
|
|
612
|
+
async refreshVirtualTryOn() {
|
|
613
|
+
await this.startVirtualTryOn(true);
|
|
614
|
+
}
|
|
934
615
|
/**
|
|
935
616
|
* Replaces the product image in the gallery with the virtual try-on result
|
|
936
617
|
* @private
|
|
@@ -950,8 +631,9 @@ class VirtualTryOnWidget {
|
|
|
950
631
|
container.setAttribute("data-ww-original-content", container.innerHTML);
|
|
951
632
|
}
|
|
952
633
|
// Capture original image dimensions before replacement to prevent layout shift
|
|
953
|
-
const originalImg = container.querySelector(
|
|
954
|
-
if (originalImg &&
|
|
634
|
+
const originalImg = container.querySelector("img");
|
|
635
|
+
if (originalImg &&
|
|
636
|
+
!container.hasAttribute("data-ww-original-dimensions")) {
|
|
955
637
|
const computedStyle = window.getComputedStyle(originalImg);
|
|
956
638
|
const rect = originalImg.getBoundingClientRect();
|
|
957
639
|
container.setAttribute("data-ww-original-width", computedStyle.width);
|
|
@@ -992,6 +674,7 @@ class VirtualTryOnWidget {
|
|
|
992
674
|
else {
|
|
993
675
|
this.showVirtualTryOnImage(container);
|
|
994
676
|
}
|
|
677
|
+
removeProductLoading(container);
|
|
995
678
|
this.updateButtonContainer(container);
|
|
996
679
|
}, this.isShowingVirtualTryOn);
|
|
997
680
|
container.appendChild(buttonContainer);
|
|
@@ -1005,44 +688,46 @@ class VirtualTryOnWidget {
|
|
|
1005
688
|
console.warn("[WeWear VTO] No virtual try-on image URL available");
|
|
1006
689
|
return;
|
|
1007
690
|
}
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1010
|
-
|
|
691
|
+
// Remove all direct children except for the button container
|
|
692
|
+
Array.from(container.children).forEach((child) => {
|
|
693
|
+
if (!child.classList.contains(CSS_CLASSES.BUTTON_CONTAINER)) {
|
|
694
|
+
child.remove();
|
|
695
|
+
}
|
|
696
|
+
});
|
|
1011
697
|
// Get stored original dimensions to prevent layout shift
|
|
1012
|
-
const originalWidth = container.getAttribute("data-ww-original-width") ||
|
|
1013
|
-
const originalHeight = container.getAttribute("data-ww-original-height") ||
|
|
1014
|
-
const originalRectWidth = container.getAttribute("data-ww-original-rect-width") ||
|
|
1015
|
-
const originalRectHeight = container.getAttribute("data-ww-original-rect-height") ||
|
|
698
|
+
const originalWidth = container.getAttribute("data-ww-original-width") || "";
|
|
699
|
+
const originalHeight = container.getAttribute("data-ww-original-height") || "";
|
|
700
|
+
const originalRectWidth = container.getAttribute("data-ww-original-rect-width") || "";
|
|
701
|
+
const originalRectHeight = container.getAttribute("data-ww-original-rect-height") || "";
|
|
1016
702
|
const image = document.createElement("img");
|
|
1017
703
|
image.src = this.virtualTryOnImageUrl;
|
|
1018
704
|
image.alt = "Virtual Try-On Result";
|
|
1019
705
|
// Use original dimensions to prevent layout shift
|
|
1020
|
-
let widthStyle =
|
|
1021
|
-
let heightStyle =
|
|
706
|
+
let widthStyle = "100%";
|
|
707
|
+
let heightStyle = "100%";
|
|
1022
708
|
// Prefer computed style dimensions, fallback to bounding rect, then container fill
|
|
1023
|
-
if (originalWidth && originalWidth !==
|
|
709
|
+
if (originalWidth && originalWidth !== "auto" && originalWidth !== "0px") {
|
|
1024
710
|
widthStyle = originalWidth;
|
|
1025
711
|
}
|
|
1026
|
-
else if (originalRectWidth && originalRectWidth !==
|
|
712
|
+
else if (originalRectWidth && originalRectWidth !== "0") {
|
|
1027
713
|
widthStyle = `${originalRectWidth}px`;
|
|
1028
714
|
}
|
|
1029
|
-
if (originalHeight &&
|
|
715
|
+
if (originalHeight &&
|
|
716
|
+
originalHeight !== "auto" &&
|
|
717
|
+
originalHeight !== "0px") {
|
|
1030
718
|
heightStyle = originalHeight;
|
|
1031
719
|
}
|
|
1032
|
-
else if (originalRectHeight && originalRectHeight !==
|
|
720
|
+
else if (originalRectHeight && originalRectHeight !== "0") {
|
|
1033
721
|
heightStyle = `${originalRectHeight}px`;
|
|
1034
722
|
}
|
|
1035
|
-
image.style.cssText = `
|
|
1036
|
-
width: ${widthStyle};
|
|
1037
|
-
height: ${heightStyle};
|
|
1038
|
-
object-fit: cover;
|
|
1039
|
-
border-radius: 8px;
|
|
723
|
+
image.style.cssText = `
|
|
724
|
+
width: ${widthStyle};
|
|
725
|
+
height: ${heightStyle};
|
|
726
|
+
object-fit: cover;
|
|
727
|
+
border-radius: 8px;
|
|
1040
728
|
`;
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
existingButtons.forEach((btn) => {
|
|
1044
|
-
container.appendChild(btn);
|
|
1045
|
-
});
|
|
729
|
+
// Prepend the image to ensure buttons are rendered on top
|
|
730
|
+
container.prepend(image);
|
|
1046
731
|
this.isShowingVirtualTryOn = true;
|
|
1047
732
|
}
|
|
1048
733
|
/**
|
|
@@ -1050,16 +735,18 @@ class VirtualTryOnWidget {
|
|
|
1050
735
|
* @private
|
|
1051
736
|
*/
|
|
1052
737
|
showOriginalImage(container) {
|
|
1053
|
-
const
|
|
1054
|
-
if (
|
|
1055
|
-
//
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
existingButtons.forEach((btn) => {
|
|
1061
|
-
container.appendChild(btn);
|
|
738
|
+
const originalContentHTML = container.getAttribute("data-ww-original-content");
|
|
739
|
+
if (originalContentHTML) {
|
|
740
|
+
// Remove all direct children except for the button container
|
|
741
|
+
Array.from(container.children).forEach((child) => {
|
|
742
|
+
if (!child.classList.contains(CSS_CLASSES.BUTTON_CONTAINER)) {
|
|
743
|
+
child.remove();
|
|
744
|
+
}
|
|
1062
745
|
});
|
|
746
|
+
// Parse the original content and prepend it
|
|
747
|
+
const tempDiv = document.createElement("div");
|
|
748
|
+
tempDiv.innerHTML = originalContentHTML;
|
|
749
|
+
container.prepend(...Array.from(tempDiv.children));
|
|
1063
750
|
}
|
|
1064
751
|
this.isShowingVirtualTryOn = false;
|
|
1065
752
|
}
|
|
@@ -1078,10 +765,20 @@ class VirtualTryOnWidget {
|
|
|
1078
765
|
this.cookieCheckInterval = null;
|
|
1079
766
|
}
|
|
1080
767
|
this.cameraButton = null;
|
|
768
|
+
// Remove message listener
|
|
769
|
+
if (this.iframeMessageListener) {
|
|
770
|
+
window.removeEventListener("message", this.iframeMessageListener);
|
|
771
|
+
this.iframeMessageListener = null;
|
|
772
|
+
}
|
|
1081
773
|
// Reset state
|
|
1082
774
|
this.virtualTryOnImageUrl = null;
|
|
1083
775
|
this.isShowingVirtualTryOn = false;
|
|
1084
|
-
this.
|
|
776
|
+
this.lastModelImage = null;
|
|
777
|
+
this.originalProductImageUrl = null;
|
|
778
|
+
if (this.previewBadge) {
|
|
779
|
+
this.previewBadge.remove();
|
|
780
|
+
this.previewBadge = null;
|
|
781
|
+
}
|
|
1085
782
|
console.log("[WeWear VTO] Widget destroyed successfully");
|
|
1086
783
|
}
|
|
1087
784
|
catch (error) {
|
|
@@ -1123,18 +820,6 @@ function initVirtualTryOn(config) {
|
|
|
1123
820
|
console.error("[WeWear VTO] Initialization error:", error);
|
|
1124
821
|
}
|
|
1125
822
|
}
|
|
1126
|
-
function destroyVirtualTryOn() {
|
|
1127
|
-
try {
|
|
1128
|
-
if (widgetInstance) {
|
|
1129
|
-
widgetInstance.destroy();
|
|
1130
|
-
widgetInstance = null;
|
|
1131
|
-
console.log("[WeWear VTO] Widget instance destroyed");
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
catch (error) {
|
|
1135
|
-
console.error("[WeWear VTO] Error destroying widget:", error);
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
823
|
function getWidgetInstance() {
|
|
1139
824
|
return widgetInstance;
|
|
1140
825
|
}
|
|
@@ -1142,5 +827,5 @@ if (typeof window !== "undefined") {
|
|
|
1142
827
|
initVirtualTryOn();
|
|
1143
828
|
}
|
|
1144
829
|
|
|
1145
|
-
export { VirtualTryOnWidget,
|
|
830
|
+
export { VirtualTryOnWidget, getWidgetInstance, initVirtualTryOn };
|
|
1146
831
|
//# sourceMappingURL=index.esm.js.map
|