@wewear/virtual-try-on 1.1.0 → 1.2.0
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 +2 -0
- package/dist/camera.d.ts +3 -0
- package/dist/components/button.d.ts +1 -0
- package/dist/components/camera-modal.d.ts +4 -0
- package/dist/components/image-modal.d.ts +1 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/review-modal.d.ts +5 -0
- package/dist/constants.d.ts +37 -0
- package/dist/index.d.ts +4 -23
- package/dist/index.esm.js +555 -648
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +555 -647
- package/dist/index.js.map +1 -1
- package/dist/installer.d.ts +0 -17
- package/dist/utils.d.ts +3 -0
- package/dist/widget.d.ts +6 -82
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,12 +4,31 @@
|
|
|
4
4
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.WeWearVirtualTryOn = {}));
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
async function callVirtualTryOnApi(baseUrl, ww_access_token, ww_user_id, ww_product_id, ww_image) {
|
|
8
|
+
try {
|
|
9
|
+
const formData = new FormData();
|
|
10
|
+
formData.append("ww_user_id", ww_user_id);
|
|
11
|
+
formData.append("ww_product_id", ww_product_id);
|
|
12
|
+
formData.append("ww_image", ww_image, "captured-image.jpg");
|
|
13
|
+
formData.append("ww_access_token", ww_access_token);
|
|
14
|
+
const response = await fetch(`${baseUrl}/api/virtual-try-on`, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
body: formData,
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
console.error("[WeWear VTO] API request failed:", response.status, response.statusText);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const result = await response.json();
|
|
23
|
+
console.log("[WeWear VTO] API response:", result);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error("[WeWear VTO] API call failed:", error);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
/** Default configuration constants */
|
|
14
33
|
const DEFAULT_CONFIG = {
|
|
15
34
|
BASE_URL: "https://virtual-try-on-widget.vercel.app",
|
|
@@ -29,335 +48,500 @@
|
|
|
29
48
|
BUTTON: 10,
|
|
30
49
|
MODAL: 99999,
|
|
31
50
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
/** Camera constraints for optimal capture */
|
|
52
|
+
const CAMERA_CONSTRAINTS = {
|
|
53
|
+
video: {
|
|
54
|
+
width: { ideal: 1280 },
|
|
55
|
+
height: { ideal: 720 },
|
|
56
|
+
facingMode: "user", // Front-facing camera
|
|
57
|
+
},
|
|
58
|
+
audio: false,
|
|
59
|
+
};
|
|
60
|
+
/** Image capture settings */
|
|
61
|
+
const IMAGE_SETTINGS = {
|
|
62
|
+
FORMAT: "image/jpeg",
|
|
63
|
+
QUALITY: 0.8,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
async function startCamera(video, loadingIndicator, captureButton) {
|
|
67
|
+
console.log("[WeWear VTO] Starting camera...");
|
|
68
|
+
try {
|
|
69
|
+
console.log("[WeWear VTO] Requesting camera access...");
|
|
70
|
+
const stream = await navigator.mediaDevices.getUserMedia(CAMERA_CONSTRAINTS);
|
|
71
|
+
video.srcObject = stream;
|
|
72
|
+
video.onloadedmetadata = () => {
|
|
73
|
+
console.log("[WeWear VTO] Camera stream loaded successfully");
|
|
74
|
+
loadingIndicator.style.display = "none";
|
|
75
|
+
captureButton.style.display = "flex";
|
|
40
76
|
};
|
|
41
77
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*/
|
|
47
|
-
getCookie(name) {
|
|
48
|
-
var _a;
|
|
49
|
-
if (typeof document === "undefined")
|
|
50
|
-
return null;
|
|
51
|
-
const value = `; ${document.cookie}`;
|
|
52
|
-
const parts = value.split(`; ${name}=`);
|
|
53
|
-
if (parts.length === 2) {
|
|
54
|
-
const part = parts.pop();
|
|
55
|
-
return part ? ((_a = part.split(";").shift()) === null || _a === void 0 ? void 0 : _a.trim()) || null : null;
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error("[WeWear VTO] Camera access error:", error);
|
|
80
|
+
loadingIndicator.innerHTML =
|
|
81
|
+
"Camera access denied. Please allow camera permissions.";
|
|
58
82
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
* @returns Button container element
|
|
67
|
-
*/
|
|
68
|
-
createButton(onClick) {
|
|
69
|
-
const container = document.createElement("div");
|
|
70
|
-
container.className = CSS_CLASSES.BUTTON_CONTAINER;
|
|
71
|
-
const positionStyles = this.getPositionStyles();
|
|
72
|
-
container.style.cssText = `
|
|
73
|
-
position: absolute;
|
|
74
|
-
${positionStyles}
|
|
75
|
-
display: flex;
|
|
76
|
-
border-radius: 50%;
|
|
77
|
-
background: #000000;
|
|
78
|
-
padding: 4px;
|
|
79
|
-
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
80
|
-
z-index: ${Z_INDEX.BUTTON};
|
|
81
|
-
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
82
|
-
`;
|
|
83
|
-
const button = document.createElement("button");
|
|
84
|
-
button.type = "button";
|
|
85
|
-
button.className = CSS_CLASSES.BUTTON;
|
|
86
|
-
button.setAttribute("aria-label", "Virtual Try-On");
|
|
87
|
-
button.setAttribute("title", "Try this product virtually");
|
|
88
|
-
button.style.cssText = `
|
|
89
|
-
display: flex;
|
|
90
|
-
width: 36px;
|
|
91
|
-
height: 36px;
|
|
92
|
-
cursor: pointer;
|
|
93
|
-
align-items: center;
|
|
94
|
-
justify-content: center;
|
|
95
|
-
border-radius: 50%;
|
|
96
|
-
padding: 10px;
|
|
97
|
-
border: none;
|
|
98
|
-
background: transparent;
|
|
99
|
-
color: #ffffff;
|
|
100
|
-
`;
|
|
101
|
-
// Add hover effects
|
|
102
|
-
container.addEventListener("mouseenter", () => {
|
|
103
|
-
container.style.transform = "scale(1.05)";
|
|
104
|
-
container.style.boxShadow = "0 30px 60px -12px rgba(0, 0, 0, 0.35)";
|
|
105
|
-
});
|
|
106
|
-
container.addEventListener("mouseleave", () => {
|
|
107
|
-
container.style.transform = "scale(1)";
|
|
108
|
-
container.style.boxShadow = "0 25px 50px -12px rgba(0, 0, 0, 0.25)";
|
|
83
|
+
}
|
|
84
|
+
function stopCamera(video) {
|
|
85
|
+
const stream = video.srcObject;
|
|
86
|
+
if (stream) {
|
|
87
|
+
const tracks = stream.getTracks();
|
|
88
|
+
tracks.forEach((track) => {
|
|
89
|
+
track.stop();
|
|
109
90
|
});
|
|
110
|
-
|
|
111
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
112
|
-
<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"></path>
|
|
113
|
-
<circle cx="12" cy="13" r="3"></circle>
|
|
114
|
-
</svg>
|
|
115
|
-
`;
|
|
116
|
-
button.onclick = onClick;
|
|
117
|
-
container.appendChild(button);
|
|
118
|
-
return container;
|
|
91
|
+
video.srcObject = null;
|
|
119
92
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return "right: 20px; top: 20px;";
|
|
130
|
-
case "top-left":
|
|
131
|
-
return "left: 20px; top: 20px;";
|
|
132
|
-
default:
|
|
133
|
-
return "right: 20px; bottom: 20px;";
|
|
134
|
-
}
|
|
93
|
+
}
|
|
94
|
+
async function captureImageFromVideo(video, canvas) {
|
|
95
|
+
// Set canvas dimensions to match video
|
|
96
|
+
canvas.width = video.videoWidth;
|
|
97
|
+
canvas.height = video.videoHeight;
|
|
98
|
+
// Draw video frame to canvas
|
|
99
|
+
const ctx = canvas.getContext("2d");
|
|
100
|
+
if (!ctx) {
|
|
101
|
+
throw new Error("Could not get canvas context");
|
|
135
102
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const existingModals = document.querySelectorAll(`.${CSS_CLASSES.MODAL}`);
|
|
143
|
-
existingModals.forEach((modal) => {
|
|
144
|
-
modal.remove();
|
|
145
|
-
});
|
|
146
|
-
// Create modal container
|
|
147
|
-
const modal = document.createElement("div");
|
|
148
|
-
modal.className = CSS_CLASSES.MODAL;
|
|
149
|
-
modal.style.cssText = `
|
|
150
|
-
position: fixed;
|
|
151
|
-
top: 0;
|
|
152
|
-
left: 0;
|
|
153
|
-
width: 100%;
|
|
154
|
-
height: 100%;
|
|
155
|
-
background-color: rgba(0, 0, 0, 0.9);
|
|
156
|
-
display: flex;
|
|
157
|
-
align-items: center;
|
|
158
|
-
justify-content: center;
|
|
159
|
-
z-index: ${Z_INDEX.MODAL};
|
|
160
|
-
padding: 20px;
|
|
161
|
-
box-sizing: border-box;
|
|
162
|
-
`;
|
|
163
|
-
// Create image container
|
|
164
|
-
const imageContainer = document.createElement("div");
|
|
165
|
-
imageContainer.style.cssText = `
|
|
166
|
-
position: relative;
|
|
167
|
-
max-width: 90%;
|
|
168
|
-
max-height: 90%;
|
|
169
|
-
background-color: white;
|
|
170
|
-
border-radius: 8px;
|
|
171
|
-
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
172
|
-
overflow: hidden;
|
|
173
|
-
`;
|
|
174
|
-
// Create image element
|
|
175
|
-
const image = document.createElement("img");
|
|
176
|
-
image.src = imageUrl;
|
|
177
|
-
image.alt = "Virtual Try-On Result";
|
|
178
|
-
image.style.cssText = `
|
|
179
|
-
width: 100%;
|
|
180
|
-
height: 100%;
|
|
181
|
-
object-fit: contain;
|
|
182
|
-
display: block;
|
|
183
|
-
`;
|
|
184
|
-
// Create close button
|
|
185
|
-
const closeButton = document.createElement("button");
|
|
186
|
-
closeButton.innerHTML = "×";
|
|
187
|
-
closeButton.style.cssText = `
|
|
188
|
-
position: absolute;
|
|
189
|
-
top: 10px;
|
|
190
|
-
right: 10px;
|
|
191
|
-
width: 30px;
|
|
192
|
-
height: 30px;
|
|
193
|
-
border: none;
|
|
194
|
-
background-color: rgba(0, 0, 0, 0.7);
|
|
195
|
-
color: white;
|
|
196
|
-
font-size: 20px;
|
|
197
|
-
font-weight: bold;
|
|
198
|
-
cursor: pointer;
|
|
199
|
-
border-radius: 50%;
|
|
200
|
-
display: flex;
|
|
201
|
-
align-items: center;
|
|
202
|
-
justify-content: center;
|
|
203
|
-
`;
|
|
204
|
-
closeButton.onclick = () => {
|
|
205
|
-
modal.remove();
|
|
206
|
-
};
|
|
207
|
-
// Close on backdrop click
|
|
208
|
-
modal.onclick = (e) => {
|
|
209
|
-
if (e.target === modal) {
|
|
210
|
-
modal.remove();
|
|
103
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
104
|
+
// Convert canvas to blob
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
canvas.toBlob((blob) => {
|
|
107
|
+
if (blob) {
|
|
108
|
+
resolve(blob);
|
|
211
109
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const handleEscape = (e) => {
|
|
215
|
-
if (e.key === "Escape") {
|
|
216
|
-
modal.remove();
|
|
217
|
-
document.removeEventListener("keydown", handleEscape);
|
|
110
|
+
else {
|
|
111
|
+
reject(new Error("Failed to create blob from canvas"));
|
|
218
112
|
}
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
113
|
+
}, IMAGE_SETTINGS.FORMAT, IMAGE_SETTINGS.QUALITY);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getCookie(name) {
|
|
118
|
+
var _a;
|
|
119
|
+
if (typeof document === "undefined")
|
|
120
|
+
return null;
|
|
121
|
+
const value = `; ${document.cookie}`;
|
|
122
|
+
const parts = value.split(`; ${name}=`);
|
|
123
|
+
if (parts.length === 2) {
|
|
124
|
+
const part = parts.pop();
|
|
125
|
+
return part ? ((_a = part.split(";").shift()) === null || _a === void 0 ? void 0 : _a.trim()) || null : null;
|
|
225
126
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
function getPositionStyles(position) {
|
|
130
|
+
switch (position) {
|
|
131
|
+
case "bottom-left":
|
|
132
|
+
return "left: 20px; bottom: 20px;";
|
|
133
|
+
case "top-right":
|
|
134
|
+
return "right: 20px; top: 20px;";
|
|
135
|
+
case "top-left":
|
|
136
|
+
return "left: 20px; top: 20px;";
|
|
137
|
+
default:
|
|
138
|
+
return "right: 20px; bottom: 20px;";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function removeElements(selector) {
|
|
142
|
+
const elements = document.querySelectorAll(selector);
|
|
143
|
+
elements.forEach((element) => {
|
|
144
|
+
element.remove();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createButton(buttonPosition, onClick) {
|
|
149
|
+
const container = document.createElement("div");
|
|
150
|
+
container.className = CSS_CLASSES.BUTTON_CONTAINER;
|
|
151
|
+
const positionStyles = getPositionStyles(buttonPosition);
|
|
152
|
+
container.style.cssText = `
|
|
153
|
+
position: absolute;
|
|
154
|
+
${positionStyles}
|
|
155
|
+
display: flex;
|
|
156
|
+
border-radius: 50%;
|
|
157
|
+
background: #000000;
|
|
158
|
+
padding: 4px;
|
|
159
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
160
|
+
z-index: ${Z_INDEX.BUTTON};
|
|
161
|
+
`;
|
|
162
|
+
const button = document.createElement("button");
|
|
163
|
+
button.type = "button";
|
|
164
|
+
button.className = CSS_CLASSES.BUTTON;
|
|
165
|
+
button.setAttribute("aria-label", "Virtual Try-On");
|
|
166
|
+
button.style.cssText = `
|
|
167
|
+
display: flex;
|
|
168
|
+
width: 36px;
|
|
169
|
+
height: 36px;
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
align-items: center;
|
|
172
|
+
justify-content: center;
|
|
173
|
+
border-radius: 50%;
|
|
174
|
+
padding: 10px;
|
|
175
|
+
border: none;
|
|
176
|
+
background: transparent;
|
|
177
|
+
color: #ffffff;
|
|
178
|
+
`;
|
|
179
|
+
button.innerHTML = `
|
|
180
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-camera-icon lucide-camera"><path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"/><circle cx="12" cy="13" r="3"/></svg>
|
|
181
|
+
`;
|
|
182
|
+
button.onclick = onClick;
|
|
183
|
+
container.appendChild(button);
|
|
184
|
+
return container;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function showCameraModal(callbacks) {
|
|
188
|
+
console.log("[WeWear VTO] Opening camera modal...");
|
|
189
|
+
// Remove any existing modals first
|
|
190
|
+
removeElements(`.${CSS_CLASSES.MODAL}`);
|
|
191
|
+
// Create modal container
|
|
192
|
+
const modal = document.createElement("div");
|
|
193
|
+
modal.className = CSS_CLASSES.MODAL;
|
|
194
|
+
modal.style.cssText = `
|
|
195
|
+
position: fixed;
|
|
196
|
+
top: 0;
|
|
197
|
+
left: 0;
|
|
198
|
+
width: 100%;
|
|
199
|
+
height: 100%;
|
|
200
|
+
background-color: rgba(0, 0, 0, 0.95);
|
|
201
|
+
display: flex;
|
|
202
|
+
flex-direction: column;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: center;
|
|
205
|
+
z-index: ${Z_INDEX.MODAL};
|
|
206
|
+
padding: 20px;
|
|
207
|
+
box-sizing: border-box;
|
|
208
|
+
`;
|
|
209
|
+
// Create camera container
|
|
210
|
+
const cameraContainer = document.createElement("div");
|
|
211
|
+
cameraContainer.style.cssText = `
|
|
212
|
+
position: relative;
|
|
213
|
+
width: 100%;
|
|
214
|
+
max-width: 500px;
|
|
215
|
+
height: 70vh;
|
|
216
|
+
background-color: #000;
|
|
217
|
+
border-radius: 12px;
|
|
218
|
+
overflow: hidden;
|
|
219
|
+
display: flex;
|
|
220
|
+
flex-direction: column;
|
|
221
|
+
align-items: center;
|
|
222
|
+
justify-content: center;
|
|
223
|
+
`;
|
|
224
|
+
// Create video element
|
|
225
|
+
const video = document.createElement("video");
|
|
226
|
+
video.autoplay = true;
|
|
227
|
+
video.playsInline = true;
|
|
228
|
+
video.muted = true;
|
|
229
|
+
video.style.cssText = `
|
|
230
|
+
width: 100%;
|
|
231
|
+
height: 100%;
|
|
232
|
+
object-fit: cover;
|
|
233
|
+
border-radius: 12px;
|
|
234
|
+
`;
|
|
235
|
+
// Create canvas for capturing
|
|
236
|
+
const canvas = document.createElement("canvas");
|
|
237
|
+
canvas.style.display = "none";
|
|
238
|
+
// Create capture button
|
|
239
|
+
const captureButton = document.createElement("button");
|
|
240
|
+
captureButton.innerHTML = "";
|
|
241
|
+
captureButton.style.cssText = `
|
|
242
|
+
position: absolute;
|
|
243
|
+
bottom: 20px;
|
|
244
|
+
left: 50%;
|
|
245
|
+
transform: translateX(-50%);
|
|
246
|
+
width: 60px;
|
|
247
|
+
height: 60px;
|
|
248
|
+
border: 3px solid white;
|
|
249
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
250
|
+
border-radius: 50%;
|
|
251
|
+
cursor: pointer;
|
|
252
|
+
display: none;
|
|
253
|
+
align-items: center;
|
|
254
|
+
justify-content: center;
|
|
255
|
+
transition: all 0.2s ease;
|
|
256
|
+
`;
|
|
257
|
+
// Create close button
|
|
258
|
+
const closeButton = document.createElement("button");
|
|
259
|
+
closeButton.innerHTML = "×";
|
|
260
|
+
closeButton.style.cssText = `
|
|
261
|
+
position: absolute;
|
|
262
|
+
top: 15px;
|
|
263
|
+
right: 15px;
|
|
264
|
+
width: 40px;
|
|
265
|
+
height: 40px;
|
|
266
|
+
border: none;
|
|
267
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
268
|
+
color: white;
|
|
269
|
+
font-size: 24px;
|
|
270
|
+
font-weight: bold;
|
|
271
|
+
cursor: pointer;
|
|
272
|
+
border-radius: 50%;
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
justify-content: center;
|
|
276
|
+
`;
|
|
277
|
+
// Create loading indicator
|
|
278
|
+
const loadingIndicator = document.createElement("div");
|
|
279
|
+
loadingIndicator.innerHTML = "Initializing camera...";
|
|
280
|
+
loadingIndicator.style.cssText = `
|
|
281
|
+
color: white;
|
|
282
|
+
font-size: 16px;
|
|
283
|
+
position: absolute;
|
|
284
|
+
top: 50%;
|
|
285
|
+
left: 50%;
|
|
286
|
+
transform: translate(-50%, -50%);
|
|
287
|
+
`;
|
|
288
|
+
// Add event listeners
|
|
289
|
+
closeButton.onclick = () => {
|
|
290
|
+
stopCamera(video);
|
|
291
|
+
modal.remove();
|
|
292
|
+
};
|
|
293
|
+
captureButton.onclick = async () => {
|
|
294
|
+
await callbacks.onCapture(video, canvas);
|
|
295
|
+
};
|
|
296
|
+
// Assemble the camera modal
|
|
297
|
+
cameraContainer.appendChild(video);
|
|
298
|
+
cameraContainer.appendChild(canvas);
|
|
299
|
+
cameraContainer.appendChild(captureButton);
|
|
300
|
+
cameraContainer.appendChild(closeButton);
|
|
301
|
+
cameraContainer.appendChild(loadingIndicator);
|
|
302
|
+
modal.appendChild(cameraContainer);
|
|
303
|
+
document.body.appendChild(modal);
|
|
304
|
+
console.log("[WeWear VTO] Camera modal added to DOM");
|
|
305
|
+
// Start camera
|
|
306
|
+
startCamera(video, loadingIndicator, captureButton);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function showImageModal(imageUrl) {
|
|
310
|
+
// Remove any existing modals first
|
|
311
|
+
removeElements(`.${CSS_CLASSES.MODAL}`);
|
|
312
|
+
// Create modal container
|
|
313
|
+
const modal = document.createElement("div");
|
|
314
|
+
modal.className = CSS_CLASSES.MODAL;
|
|
315
|
+
modal.style.cssText = `
|
|
316
|
+
position: fixed;
|
|
317
|
+
top: 0;
|
|
318
|
+
left: 0;
|
|
319
|
+
width: 100%;
|
|
320
|
+
height: 100%;
|
|
321
|
+
background-color: rgba(0, 0, 0, 0.9);
|
|
322
|
+
display: flex;
|
|
323
|
+
align-items: center;
|
|
324
|
+
justify-content: center;
|
|
325
|
+
z-index: ${Z_INDEX.MODAL};
|
|
326
|
+
padding: 20px;
|
|
327
|
+
box-sizing: border-box;
|
|
328
|
+
`;
|
|
329
|
+
// Create image container
|
|
330
|
+
const imageContainer = document.createElement("div");
|
|
331
|
+
imageContainer.style.cssText = `
|
|
332
|
+
position: relative;
|
|
333
|
+
width: 100%;
|
|
334
|
+
max-width: 500px;
|
|
335
|
+
height: 70vh;
|
|
336
|
+
background-color: #000;
|
|
337
|
+
border-radius: 12px;
|
|
338
|
+
overflow: hidden;
|
|
339
|
+
display: flex;
|
|
340
|
+
flex-direction: column;
|
|
341
|
+
align-items: center;
|
|
342
|
+
justify-content: center;
|
|
343
|
+
`;
|
|
344
|
+
// Create image element
|
|
345
|
+
const image = document.createElement("img");
|
|
346
|
+
image.src = imageUrl;
|
|
347
|
+
image.alt = "Virtual Try-On Result";
|
|
348
|
+
image.style.cssText = `
|
|
349
|
+
width: 100%;
|
|
350
|
+
height: 100%;
|
|
351
|
+
object-fit: cover;
|
|
352
|
+
border-radius: 12px;
|
|
353
|
+
`;
|
|
354
|
+
// Create close button
|
|
355
|
+
const closeButton = document.createElement("button");
|
|
356
|
+
closeButton.innerHTML = "×";
|
|
357
|
+
closeButton.style.cssText = `
|
|
358
|
+
position: absolute;
|
|
359
|
+
top: 15px;
|
|
360
|
+
right: 15px;
|
|
361
|
+
width: 40px;
|
|
362
|
+
height: 40px;
|
|
363
|
+
border: none;
|
|
364
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
365
|
+
color: white;
|
|
366
|
+
font-size: 24px;
|
|
367
|
+
font-weight: bold;
|
|
368
|
+
cursor: pointer;
|
|
369
|
+
border-radius: 50%;
|
|
370
|
+
display: flex;
|
|
371
|
+
align-items: center;
|
|
372
|
+
justify-content: center;
|
|
373
|
+
`;
|
|
374
|
+
closeButton.onclick = () => {
|
|
375
|
+
modal.remove();
|
|
376
|
+
};
|
|
377
|
+
// Close on backdrop click
|
|
378
|
+
modal.onclick = (e) => {
|
|
379
|
+
if (e.target === modal) {
|
|
237
380
|
modal.remove();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
position: fixed;
|
|
244
|
-
top: 0;
|
|
245
|
-
left: 0;
|
|
246
|
-
width: 100%;
|
|
247
|
-
height: 100%;
|
|
248
|
-
background-color: rgba(0, 0, 0, 0.95);
|
|
249
|
-
display: flex;
|
|
250
|
-
flex-direction: column;
|
|
251
|
-
align-items: center;
|
|
252
|
-
justify-content: center;
|
|
253
|
-
z-index: ${Z_INDEX.MODAL};
|
|
254
|
-
padding: 20px;
|
|
255
|
-
box-sizing: border-box;
|
|
256
|
-
`;
|
|
257
|
-
// Create camera container
|
|
258
|
-
const cameraContainer = document.createElement("div");
|
|
259
|
-
cameraContainer.style.cssText = `
|
|
260
|
-
position: relative;
|
|
261
|
-
width: 100%;
|
|
262
|
-
max-width: 500px;
|
|
263
|
-
height: 70vh;
|
|
264
|
-
background-color: #000;
|
|
265
|
-
border-radius: 12px;
|
|
266
|
-
overflow: hidden;
|
|
267
|
-
display: flex;
|
|
268
|
-
flex-direction: column;
|
|
269
|
-
align-items: center;
|
|
270
|
-
justify-content: center;
|
|
271
|
-
`;
|
|
272
|
-
// Create video element
|
|
273
|
-
const video = document.createElement("video");
|
|
274
|
-
video.autoplay = true;
|
|
275
|
-
video.playsInline = true;
|
|
276
|
-
video.muted = true;
|
|
277
|
-
video.style.cssText = `
|
|
278
|
-
width: 100%;
|
|
279
|
-
height: 100%;
|
|
280
|
-
object-fit: cover;
|
|
281
|
-
border-radius: 12px;
|
|
282
|
-
`;
|
|
283
|
-
// Create canvas for capturing
|
|
284
|
-
const canvas = document.createElement("canvas");
|
|
285
|
-
canvas.style.display = "none";
|
|
286
|
-
// Create capture button
|
|
287
|
-
const captureButton = document.createElement("button");
|
|
288
|
-
captureButton.innerHTML = `
|
|
289
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
|
|
290
|
-
<circle cx="12" cy="12" r="10" stroke="white" stroke-width="2" fill="none"/>
|
|
291
|
-
<circle cx="12" cy="12" r="6" fill="white"/>
|
|
292
|
-
</svg>
|
|
293
|
-
`;
|
|
294
|
-
captureButton.style.cssText = `
|
|
295
|
-
position: absolute;
|
|
296
|
-
bottom: 20px;
|
|
297
|
-
left: 50%;
|
|
298
|
-
transform: translateX(-50%);
|
|
299
|
-
width: 60px;
|
|
300
|
-
height: 60px;
|
|
301
|
-
border: 3px solid white;
|
|
302
|
-
background-color: transparent;
|
|
303
|
-
border-radius: 50%;
|
|
304
|
-
cursor: pointer;
|
|
305
|
-
display: none;
|
|
306
|
-
align-items: center;
|
|
307
|
-
justify-content: center;
|
|
308
|
-
transition: all 0.2s ease;
|
|
309
|
-
`;
|
|
310
|
-
// Create close button
|
|
311
|
-
const closeButton = document.createElement("button");
|
|
312
|
-
closeButton.innerHTML = "×";
|
|
313
|
-
closeButton.style.cssText = `
|
|
314
|
-
position: absolute;
|
|
315
|
-
top: 15px;
|
|
316
|
-
right: 15px;
|
|
317
|
-
width: 40px;
|
|
318
|
-
height: 40px;
|
|
319
|
-
border: none;
|
|
320
|
-
background-color: rgba(0, 0, 0, 0.7);
|
|
321
|
-
color: white;
|
|
322
|
-
font-size: 24px;
|
|
323
|
-
font-weight: bold;
|
|
324
|
-
cursor: pointer;
|
|
325
|
-
border-radius: 50%;
|
|
326
|
-
display: flex;
|
|
327
|
-
align-items: center;
|
|
328
|
-
justify-content: center;
|
|
329
|
-
`;
|
|
330
|
-
// Create loading indicator
|
|
331
|
-
const loadingIndicator = document.createElement("div");
|
|
332
|
-
loadingIndicator.innerHTML = "Initializing camera...";
|
|
333
|
-
loadingIndicator.style.cssText = `
|
|
334
|
-
color: white;
|
|
335
|
-
font-size: 16px;
|
|
336
|
-
position: absolute;
|
|
337
|
-
top: 50%;
|
|
338
|
-
left: 50%;
|
|
339
|
-
transform: translate(-50%, -50%);
|
|
340
|
-
`;
|
|
341
|
-
// Add event listeners
|
|
342
|
-
closeButton.onclick = () => {
|
|
343
|
-
this.stopCamera(video);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
// Close on Escape key
|
|
384
|
+
const handleEscape = (e) => {
|
|
385
|
+
if (e.key === "Escape") {
|
|
344
386
|
modal.remove();
|
|
387
|
+
document.removeEventListener("keydown", handleEscape);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
document.addEventListener("keydown", handleEscape);
|
|
391
|
+
imageContainer.appendChild(image);
|
|
392
|
+
imageContainer.appendChild(closeButton);
|
|
393
|
+
modal.appendChild(imageContainer);
|
|
394
|
+
document.body.appendChild(modal);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function showReviewModal(imageBlob, callbacks) {
|
|
398
|
+
console.log("[WeWear VTO] Opening review modal...");
|
|
399
|
+
// Remove any existing modals first
|
|
400
|
+
removeElements(`.${CSS_CLASSES.MODAL}`);
|
|
401
|
+
// Create image URL for preview
|
|
402
|
+
const imageUrl = URL.createObjectURL(imageBlob);
|
|
403
|
+
// Create modal container
|
|
404
|
+
const modal = document.createElement("div");
|
|
405
|
+
modal.className = CSS_CLASSES.MODAL;
|
|
406
|
+
modal.style.cssText = `
|
|
407
|
+
position: fixed;
|
|
408
|
+
top: 0;
|
|
409
|
+
left: 0;
|
|
410
|
+
width: 100%;
|
|
411
|
+
height: 100%;
|
|
412
|
+
background-color: rgba(0, 0, 0, 0.95);
|
|
413
|
+
display: flex;
|
|
414
|
+
flex-direction: column;
|
|
415
|
+
align-items: center;
|
|
416
|
+
justify-content: center;
|
|
417
|
+
z-index: ${Z_INDEX.MODAL};
|
|
418
|
+
padding: 20px;
|
|
419
|
+
box-sizing: border-box;
|
|
420
|
+
`;
|
|
421
|
+
// Create review container
|
|
422
|
+
const reviewContainer = document.createElement("div");
|
|
423
|
+
reviewContainer.style.cssText = `
|
|
424
|
+
position: relative;
|
|
425
|
+
width: 100%;
|
|
426
|
+
max-width: 500px;
|
|
427
|
+
height: 70vh;
|
|
428
|
+
background-color: #000;
|
|
429
|
+
border-radius: 12px;
|
|
430
|
+
overflow: hidden;
|
|
431
|
+
display: flex;
|
|
432
|
+
flex-direction: column;
|
|
433
|
+
align-items: center;
|
|
434
|
+
justify-content: center;
|
|
435
|
+
`;
|
|
436
|
+
// Create image element
|
|
437
|
+
const image = document.createElement("img");
|
|
438
|
+
image.src = imageUrl;
|
|
439
|
+
image.alt = "Review your photo";
|
|
440
|
+
image.style.cssText = `
|
|
441
|
+
width: 100%;
|
|
442
|
+
height: 100%;
|
|
443
|
+
object-fit: cover;
|
|
444
|
+
border-radius: 12px;
|
|
445
|
+
`;
|
|
446
|
+
// Create close button
|
|
447
|
+
const closeButton = document.createElement("button");
|
|
448
|
+
closeButton.innerHTML = "×";
|
|
449
|
+
closeButton.style.cssText = `
|
|
450
|
+
position: absolute;
|
|
451
|
+
top: 15px;
|
|
452
|
+
right: 15px;
|
|
453
|
+
width: 40px;
|
|
454
|
+
height: 40px;
|
|
455
|
+
border: none;
|
|
456
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
457
|
+
color: white;
|
|
458
|
+
font-size: 24px;
|
|
459
|
+
font-weight: bold;
|
|
460
|
+
cursor: pointer;
|
|
461
|
+
border-radius: 50%;
|
|
462
|
+
display: flex;
|
|
463
|
+
align-items: center;
|
|
464
|
+
justify-content: center;
|
|
465
|
+
`;
|
|
466
|
+
// Create button container
|
|
467
|
+
const buttonContainer = document.createElement("div");
|
|
468
|
+
buttonContainer.style.cssText = `
|
|
469
|
+
position: absolute;
|
|
470
|
+
bottom: 20px;
|
|
471
|
+
left: 50%;
|
|
472
|
+
transform: translateX(-50%);
|
|
473
|
+
display: flex;
|
|
474
|
+
gap: 15px;
|
|
475
|
+
width: calc(100% - 40px);
|
|
476
|
+
max-width: 300px;
|
|
477
|
+
`;
|
|
478
|
+
// Create retake button
|
|
479
|
+
const retakeButton = document.createElement("button");
|
|
480
|
+
retakeButton.textContent = "Retake";
|
|
481
|
+
retakeButton.style.cssText = `
|
|
482
|
+
flex: 1;
|
|
483
|
+
padding: 12px 24px;
|
|
484
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
485
|
+
color: white;
|
|
486
|
+
border-radius: 8px;
|
|
487
|
+
border: none;
|
|
488
|
+
font-size: 16px;
|
|
489
|
+
font-weight: 600;
|
|
490
|
+
cursor: pointer;
|
|
491
|
+
transition: all 0.2s ease;
|
|
492
|
+
`;
|
|
493
|
+
// Create use photo button
|
|
494
|
+
const usePhotoButton = document.createElement("button");
|
|
495
|
+
usePhotoButton.textContent = "Use Photo";
|
|
496
|
+
usePhotoButton.style.cssText = `
|
|
497
|
+
flex: 1;
|
|
498
|
+
padding: 12px 24px;
|
|
499
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
500
|
+
color: white;
|
|
501
|
+
border-radius: 8px;
|
|
502
|
+
border: none;
|
|
503
|
+
font-size: 16px;
|
|
504
|
+
font-weight: 600;
|
|
505
|
+
cursor: pointer;
|
|
506
|
+
transition: all 0.2s ease;
|
|
507
|
+
`;
|
|
508
|
+
// Add event listeners
|
|
509
|
+
closeButton.onclick = () => {
|
|
510
|
+
URL.revokeObjectURL(imageUrl); // Clean up
|
|
511
|
+
modal.remove();
|
|
512
|
+
};
|
|
513
|
+
retakeButton.onclick = () => {
|
|
514
|
+
URL.revokeObjectURL(imageUrl); // Clean up
|
|
515
|
+
modal.remove();
|
|
516
|
+
callbacks.onRetake();
|
|
517
|
+
};
|
|
518
|
+
usePhotoButton.onclick = async () => {
|
|
519
|
+
URL.revokeObjectURL(imageUrl); // Clean up
|
|
520
|
+
modal.remove();
|
|
521
|
+
await callbacks.onAccept(imageBlob);
|
|
522
|
+
};
|
|
523
|
+
// Assemble the review modal
|
|
524
|
+
buttonContainer.appendChild(retakeButton);
|
|
525
|
+
buttonContainer.appendChild(usePhotoButton);
|
|
526
|
+
reviewContainer.appendChild(image);
|
|
527
|
+
reviewContainer.appendChild(closeButton);
|
|
528
|
+
reviewContainer.appendChild(buttonContainer);
|
|
529
|
+
modal.appendChild(reviewContainer);
|
|
530
|
+
document.body.appendChild(modal);
|
|
531
|
+
console.log("[WeWear VTO] Review modal added to DOM");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
class VirtualTryOnWidget {
|
|
535
|
+
constructor(config = {}) {
|
|
536
|
+
this.config = {
|
|
537
|
+
baseUrl: config.baseUrl || DEFAULT_CONFIG.BASE_URL,
|
|
538
|
+
productPageSelector: config.productPageSelector || DEFAULT_CONFIG.PRODUCT_PAGE_SELECTOR,
|
|
539
|
+
gallerySelector: config.gallerySelector || DEFAULT_CONFIG.GALLERY_SELECTOR,
|
|
540
|
+
skuSelector: config.skuSelector || DEFAULT_CONFIG.SKU_SELECTOR,
|
|
541
|
+
buttonPosition: config.buttonPosition || DEFAULT_CONFIG.BUTTON_POSITION,
|
|
345
542
|
};
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
};
|
|
349
|
-
// Assemble the camera modal
|
|
350
|
-
cameraContainer.appendChild(video);
|
|
351
|
-
cameraContainer.appendChild(canvas);
|
|
352
|
-
cameraContainer.appendChild(captureButton);
|
|
353
|
-
cameraContainer.appendChild(closeButton);
|
|
354
|
-
cameraContainer.appendChild(loadingIndicator);
|
|
355
|
-
modal.appendChild(cameraContainer);
|
|
356
|
-
document.body.appendChild(modal);
|
|
357
|
-
console.log("[WeWear VTO] Camera modal added to DOM");
|
|
358
|
-
// Start camera
|
|
359
|
-
this.startCamera(video, loadingIndicator, captureButton);
|
|
360
|
-
} /**
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
361
545
|
* Initializes the virtual try-on widget on the current page
|
|
362
546
|
* @returns Promise that resolves when initialization is complete
|
|
363
547
|
*/
|
|
@@ -378,7 +562,7 @@
|
|
|
378
562
|
container.style.position = "relative";
|
|
379
563
|
}
|
|
380
564
|
// Create and add the virtual try-on button
|
|
381
|
-
const button = this.
|
|
565
|
+
const button = createButton(this.config.buttonPosition, async () => {
|
|
382
566
|
await this.handleTryOnClick();
|
|
383
567
|
});
|
|
384
568
|
container.appendChild(button);
|
|
@@ -397,308 +581,84 @@
|
|
|
397
581
|
console.log("[WeWear VTO] Button clicked, starting try-on process...");
|
|
398
582
|
try {
|
|
399
583
|
// Get required data
|
|
400
|
-
const ww_access_token =
|
|
401
|
-
const ww_user_id =
|
|
584
|
+
const ww_access_token = getCookie("ww_access_token");
|
|
585
|
+
const ww_user_id = getCookie("ww_user_id");
|
|
402
586
|
const skuElement = document.querySelector(this.config.skuSelector);
|
|
403
587
|
const ww_product_id = (_a = skuElement === null || skuElement === void 0 ? void 0 : skuElement.textContent) === null || _a === void 0 ? void 0 : _a.trim();
|
|
404
588
|
console.log("[WeWear VTO] Retrieved data:", {
|
|
405
589
|
ww_user_id,
|
|
406
590
|
ww_product_id,
|
|
407
|
-
ww_access_token
|
|
591
|
+
ww_access_token,
|
|
408
592
|
});
|
|
409
593
|
// Validate required data
|
|
410
594
|
if (!ww_user_id) {
|
|
411
595
|
console.warn("[WeWear VTO] Missing required cookie: ww_user_id");
|
|
412
|
-
this.showError("Please sign in to use virtual try-on");
|
|
413
596
|
return;
|
|
414
597
|
}
|
|
415
598
|
if (!ww_product_id) {
|
|
416
599
|
console.warn("[WeWear VTO] Product SKU not found:", this.config.skuSelector);
|
|
417
|
-
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (!ww_access_token) {
|
|
603
|
+
console.warn("[WeWear VTO] Missing required cookie: ww_access_token");
|
|
418
604
|
return;
|
|
419
605
|
}
|
|
420
606
|
// Show camera capture modal
|
|
421
|
-
this.
|
|
607
|
+
this.showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_id);
|
|
422
608
|
}
|
|
423
609
|
catch (error) {
|
|
424
610
|
console.error("[WeWear VTO] Try-on request failed:", error);
|
|
425
|
-
this.showError("An unexpected error occurred. Please try again.");
|
|
426
611
|
}
|
|
427
612
|
}
|
|
428
613
|
/**
|
|
429
|
-
* Shows
|
|
430
|
-
* @param message - Error message to display
|
|
614
|
+
* Shows camera modal with appropriate callbacks
|
|
431
615
|
* @private
|
|
432
616
|
*/
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
facingMode: "user", // Front-facing camera
|
|
451
|
-
},
|
|
452
|
-
audio: false,
|
|
453
|
-
};
|
|
454
|
-
console.log("[WeWear VTO] Requesting camera access...");
|
|
455
|
-
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
456
|
-
video.srcObject = stream;
|
|
457
|
-
video.onloadedmetadata = () => {
|
|
458
|
-
console.log("[WeWear VTO] Camera stream loaded successfully");
|
|
459
|
-
loadingIndicator.style.display = "none";
|
|
460
|
-
captureButton.style.display = "flex";
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
catch (error) {
|
|
464
|
-
console.error("[WeWear VTO] Camera access error:", error);
|
|
465
|
-
loadingIndicator.innerHTML =
|
|
466
|
-
"Camera access denied. Please allow camera permissions.";
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Stops the camera stream
|
|
471
|
-
* @param video - Video element with camera stream
|
|
472
|
-
*/
|
|
473
|
-
stopCamera(video) {
|
|
474
|
-
const stream = video.srcObject;
|
|
475
|
-
if (stream) {
|
|
476
|
-
const tracks = stream.getTracks();
|
|
477
|
-
tracks.forEach((track) => {
|
|
478
|
-
track.stop();
|
|
479
|
-
});
|
|
480
|
-
video.srcObject = null;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Captures an image from the video stream
|
|
485
|
-
* @param video - Video element with camera stream
|
|
486
|
-
* @param canvas - Canvas element for capturing
|
|
487
|
-
* @param ww_access_token - Access token for API
|
|
488
|
-
* @param ww_user_id - User identifier
|
|
489
|
-
* @param ww_product_id - Product identifier
|
|
490
|
-
* @param modal - Modal element to close after processing
|
|
491
|
-
*/
|
|
492
|
-
async captureImage(video, canvas, ww_access_token, ww_user_id, ww_product_id, modal) {
|
|
493
|
-
try {
|
|
494
|
-
// Set canvas dimensions to match video
|
|
495
|
-
canvas.width = video.videoWidth;
|
|
496
|
-
canvas.height = video.videoHeight;
|
|
497
|
-
// Draw video frame to canvas
|
|
498
|
-
const ctx = canvas.getContext("2d");
|
|
499
|
-
if (!ctx) {
|
|
500
|
-
throw new Error("Could not get canvas context");
|
|
501
|
-
}
|
|
502
|
-
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
503
|
-
// Convert canvas to blob
|
|
504
|
-
const blob = await new Promise((resolve, reject) => {
|
|
505
|
-
canvas.toBlob((blob) => {
|
|
506
|
-
if (blob) {
|
|
507
|
-
resolve(blob);
|
|
508
|
-
}
|
|
509
|
-
else {
|
|
510
|
-
reject(new Error("Failed to create blob from canvas"));
|
|
511
|
-
}
|
|
512
|
-
}, "image/jpeg", 0.8);
|
|
513
|
-
});
|
|
514
|
-
// Stop camera and close camera modal
|
|
515
|
-
this.stopCamera(video);
|
|
516
|
-
modal.remove();
|
|
517
|
-
// Show review modal instead of immediately processing
|
|
518
|
-
this.showReviewModal(blob, ww_access_token, ww_user_id, ww_product_id);
|
|
519
|
-
}
|
|
520
|
-
catch (error) {
|
|
521
|
-
console.error("[WeWear VTO] Image capture error:", error);
|
|
522
|
-
this.showError("Failed to capture image. Please try again.");
|
|
523
|
-
}
|
|
617
|
+
showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_id) {
|
|
618
|
+
const callbacks = {
|
|
619
|
+
onCapture: async (video, canvas) => {
|
|
620
|
+
try {
|
|
621
|
+
// Capture image from video
|
|
622
|
+
const imageBlob = await captureImageFromVideo(video, canvas);
|
|
623
|
+
// Stop camera and show review modal
|
|
624
|
+
stopCamera(video);
|
|
625
|
+
// Show review modal instead of immediately processing
|
|
626
|
+
this.showReviewModalWithCallbacks(imageBlob, ww_access_token, ww_user_id, ww_product_id);
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
console.error("[WeWear VTO] Image capture error:", error);
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
showCameraModal(callbacks);
|
|
524
634
|
}
|
|
525
635
|
/**
|
|
526
|
-
* Shows
|
|
527
|
-
* @
|
|
528
|
-
* @param ww_access_token - Access token for API
|
|
529
|
-
* @param ww_user_id - User identifier
|
|
530
|
-
* @param ww_product_id - Product identifier
|
|
636
|
+
* Shows review modal with appropriate callbacks
|
|
637
|
+
* @private
|
|
531
638
|
*/
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
// Create modal container (same as camera modal)
|
|
542
|
-
const modal = document.createElement("div");
|
|
543
|
-
modal.className = CSS_CLASSES.MODAL;
|
|
544
|
-
modal.style.cssText = `
|
|
545
|
-
position: fixed;
|
|
546
|
-
top: 0;
|
|
547
|
-
left: 0;
|
|
548
|
-
width: 100%;
|
|
549
|
-
height: 100%;
|
|
550
|
-
background-color: rgba(0, 0, 0, 0.95);
|
|
551
|
-
display: flex;
|
|
552
|
-
flex-direction: column;
|
|
553
|
-
align-items: center;
|
|
554
|
-
justify-content: center;
|
|
555
|
-
z-index: ${Z_INDEX.MODAL};
|
|
556
|
-
padding: 20px;
|
|
557
|
-
box-sizing: border-box;
|
|
558
|
-
`;
|
|
559
|
-
// Create review container (similar to camera container)
|
|
560
|
-
const reviewContainer = document.createElement("div");
|
|
561
|
-
reviewContainer.style.cssText = `
|
|
562
|
-
position: relative;
|
|
563
|
-
width: 100%;
|
|
564
|
-
max-width: 500px;
|
|
565
|
-
height: 70vh;
|
|
566
|
-
background-color: #000;
|
|
567
|
-
border-radius: 12px;
|
|
568
|
-
overflow: hidden;
|
|
569
|
-
display: flex;
|
|
570
|
-
flex-direction: column;
|
|
571
|
-
align-items: center;
|
|
572
|
-
justify-content: center;
|
|
573
|
-
`;
|
|
574
|
-
// Create image element (replaces video element)
|
|
575
|
-
const image = document.createElement("img");
|
|
576
|
-
image.src = imageUrl;
|
|
577
|
-
image.alt = "Review your photo";
|
|
578
|
-
image.style.cssText = `
|
|
579
|
-
width: 100%;
|
|
580
|
-
height: 100%;
|
|
581
|
-
object-fit: cover;
|
|
582
|
-
border-radius: 12px;
|
|
583
|
-
`;
|
|
584
|
-
// Create close button (same as camera modal)
|
|
585
|
-
const closeButton = document.createElement("button");
|
|
586
|
-
closeButton.innerHTML = "×";
|
|
587
|
-
closeButton.style.cssText = `
|
|
588
|
-
position: absolute;
|
|
589
|
-
top: 15px;
|
|
590
|
-
right: 15px;
|
|
591
|
-
width: 40px;
|
|
592
|
-
height: 40px;
|
|
593
|
-
border: none;
|
|
594
|
-
background-color: rgba(0, 0, 0, 0.7);
|
|
595
|
-
color: white;
|
|
596
|
-
font-size: 24px;
|
|
597
|
-
font-weight: bold;
|
|
598
|
-
cursor: pointer;
|
|
599
|
-
border-radius: 50%;
|
|
600
|
-
display: flex;
|
|
601
|
-
align-items: center;
|
|
602
|
-
justify-content: center;
|
|
603
|
-
`;
|
|
604
|
-
// Create button container positioned at bottom (similar to capture button position)
|
|
605
|
-
const buttonContainer = document.createElement("div");
|
|
606
|
-
buttonContainer.style.cssText = `
|
|
607
|
-
position: absolute;
|
|
608
|
-
bottom: 20px;
|
|
609
|
-
left: 50%;
|
|
610
|
-
transform: translateX(-50%);
|
|
611
|
-
display: flex;
|
|
612
|
-
gap: 15px;
|
|
613
|
-
width: calc(100% - 40px);
|
|
614
|
-
max-width: 300px;
|
|
615
|
-
`;
|
|
616
|
-
// Create retake button
|
|
617
|
-
const retakeButton = document.createElement("button");
|
|
618
|
-
retakeButton.textContent = "Retake";
|
|
619
|
-
retakeButton.style.cssText = `
|
|
620
|
-
flex: 1;
|
|
621
|
-
padding: 12px 24px;
|
|
622
|
-
background-color: transparent;
|
|
623
|
-
color: white;
|
|
624
|
-
border: 2px solid white;
|
|
625
|
-
border-radius: 8px;
|
|
626
|
-
font-size: 16px;
|
|
627
|
-
font-weight: 600;
|
|
628
|
-
cursor: pointer;
|
|
629
|
-
transition: all 0.2s ease;
|
|
630
|
-
`;
|
|
631
|
-
// Create use photo button
|
|
632
|
-
const usePhotoButton = document.createElement("button");
|
|
633
|
-
usePhotoButton.textContent = "Use Photo";
|
|
634
|
-
usePhotoButton.style.cssText = `
|
|
635
|
-
flex: 1;
|
|
636
|
-
padding: 12px 24px;
|
|
637
|
-
background-color: white;
|
|
638
|
-
color: black;
|
|
639
|
-
border: 2px solid white;
|
|
640
|
-
border-radius: 8px;
|
|
641
|
-
font-size: 16px;
|
|
642
|
-
font-weight: 600;
|
|
643
|
-
cursor: pointer;
|
|
644
|
-
transition: all 0.2s ease;
|
|
645
|
-
`;
|
|
646
|
-
// Add hover effects
|
|
647
|
-
retakeButton.addEventListener("mouseenter", () => {
|
|
648
|
-
retakeButton.style.backgroundColor = "white";
|
|
649
|
-
retakeButton.style.color = "black";
|
|
650
|
-
});
|
|
651
|
-
retakeButton.addEventListener("mouseleave", () => {
|
|
652
|
-
retakeButton.style.backgroundColor = "transparent";
|
|
653
|
-
retakeButton.style.color = "white";
|
|
654
|
-
});
|
|
655
|
-
usePhotoButton.addEventListener("mouseenter", () => {
|
|
656
|
-
usePhotoButton.style.backgroundColor = "rgba(255, 255, 255, 0.9)";
|
|
657
|
-
});
|
|
658
|
-
usePhotoButton.addEventListener("mouseleave", () => {
|
|
659
|
-
usePhotoButton.style.backgroundColor = "white";
|
|
660
|
-
});
|
|
661
|
-
// Add event listeners
|
|
662
|
-
closeButton.onclick = () => {
|
|
663
|
-
URL.revokeObjectURL(imageUrl); // Clean up
|
|
664
|
-
modal.remove();
|
|
665
|
-
};
|
|
666
|
-
retakeButton.onclick = () => {
|
|
667
|
-
URL.revokeObjectURL(imageUrl); // Clean up
|
|
668
|
-
modal.remove();
|
|
669
|
-
// Reopen camera modal
|
|
670
|
-
this.showCameraModal(ww_access_token, ww_user_id, ww_product_id);
|
|
671
|
-
};
|
|
672
|
-
usePhotoButton.onclick = async () => {
|
|
673
|
-
URL.revokeObjectURL(imageUrl); // Clean up
|
|
674
|
-
modal.remove();
|
|
675
|
-
// Process the image
|
|
676
|
-
await this.processAcceptedImage(imageBlob, ww_access_token, ww_user_id, ww_product_id);
|
|
639
|
+
showReviewModalWithCallbacks(imageBlob, ww_access_token, ww_user_id, ww_product_id) {
|
|
640
|
+
const callbacks = {
|
|
641
|
+
onRetake: () => {
|
|
642
|
+
// Reopen camera modal
|
|
643
|
+
this.showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_id);
|
|
644
|
+
},
|
|
645
|
+
onAccept: async (acceptedImageBlob) => {
|
|
646
|
+
await this.processAcceptedImage(acceptedImageBlob, ww_access_token, ww_user_id, ww_product_id);
|
|
647
|
+
},
|
|
677
648
|
};
|
|
678
|
-
|
|
679
|
-
buttonContainer.appendChild(retakeButton);
|
|
680
|
-
buttonContainer.appendChild(usePhotoButton);
|
|
681
|
-
reviewContainer.appendChild(image);
|
|
682
|
-
reviewContainer.appendChild(closeButton);
|
|
683
|
-
reviewContainer.appendChild(buttonContainer);
|
|
684
|
-
modal.appendChild(reviewContainer);
|
|
685
|
-
document.body.appendChild(modal);
|
|
686
|
-
console.log("[WeWear VTO] Review modal added to DOM");
|
|
649
|
+
showReviewModal(imageBlob, callbacks);
|
|
687
650
|
}
|
|
688
651
|
/**
|
|
689
652
|
* Processes the accepted image by calling the API
|
|
690
|
-
* @
|
|
691
|
-
* @param ww_access_token - Access token for API
|
|
692
|
-
* @param ww_user_id - User identifier
|
|
693
|
-
* @param ww_product_id - Product identifier
|
|
653
|
+
* @private
|
|
694
654
|
*/
|
|
695
655
|
async processAcceptedImage(imageBlob, ww_access_token, ww_user_id, ww_product_id) {
|
|
696
656
|
try {
|
|
697
657
|
console.log("[WeWear VTO] Processing accepted image...");
|
|
698
658
|
// Call the API with the accepted image
|
|
699
|
-
const result = await this.
|
|
659
|
+
const result = await callVirtualTryOnApi(this.config.baseUrl, ww_access_token, ww_user_id, ww_product_id, imageBlob);
|
|
700
660
|
if (result === null || result === void 0 ? void 0 : result.imageUrl) {
|
|
701
|
-
|
|
661
|
+
showImageModal(result.imageUrl);
|
|
702
662
|
}
|
|
703
663
|
else {
|
|
704
664
|
console.error("[WeWear VTO] Invalid API response:", result);
|
|
@@ -706,42 +666,6 @@
|
|
|
706
666
|
}
|
|
707
667
|
catch (error) {
|
|
708
668
|
console.error("[WeWear VTO] Error processing accepted image:", error);
|
|
709
|
-
this.showError("Failed to process image. Please try again.");
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Makes API call to virtual try-on service with captured image
|
|
714
|
-
* @param ww_access_token - Optional authentication token
|
|
715
|
-
* @param ww_user_id - User identifier
|
|
716
|
-
* @param ww_product_id - Product identifier
|
|
717
|
-
* @param imageBlob - Captured image blob
|
|
718
|
-
* @returns Promise with virtual try-on result or null if failed
|
|
719
|
-
*/
|
|
720
|
-
async callVirtualTryOnApiWithImage(ww_access_token, ww_user_id, ww_product_id, imageBlob) {
|
|
721
|
-
try {
|
|
722
|
-
// Create form data for multipart upload
|
|
723
|
-
const formData = new FormData();
|
|
724
|
-
formData.append("ww_user_id", ww_user_id);
|
|
725
|
-
formData.append("ww_product_id", ww_product_id);
|
|
726
|
-
formData.append("image", imageBlob, "captured-image.jpg");
|
|
727
|
-
if (ww_access_token) {
|
|
728
|
-
formData.append("ww_access_token", ww_access_token);
|
|
729
|
-
}
|
|
730
|
-
const response = await fetch(`${this.config.baseUrl}/api/virtual-try-on`, {
|
|
731
|
-
method: "POST",
|
|
732
|
-
body: formData,
|
|
733
|
-
});
|
|
734
|
-
if (!response.ok) {
|
|
735
|
-
console.error("[WeWear VTO] API request failed:", response.status, response.statusText);
|
|
736
|
-
return null;
|
|
737
|
-
}
|
|
738
|
-
const result = await response.json();
|
|
739
|
-
console.log("[WeWear VTO] API response:", result);
|
|
740
|
-
return result;
|
|
741
|
-
}
|
|
742
|
-
catch (error) {
|
|
743
|
-
console.error("[WeWear VTO] API call failed:", error);
|
|
744
|
-
return null;
|
|
745
669
|
}
|
|
746
670
|
}
|
|
747
671
|
/**
|
|
@@ -750,15 +674,9 @@
|
|
|
750
674
|
destroy() {
|
|
751
675
|
try {
|
|
752
676
|
// Remove all buttons
|
|
753
|
-
|
|
754
|
-
buttons.forEach((button) => {
|
|
755
|
-
button.remove();
|
|
756
|
-
});
|
|
677
|
+
removeElements(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
|
|
757
678
|
// Remove all modals
|
|
758
|
-
|
|
759
|
-
modals.forEach((modal) => {
|
|
760
|
-
modal.remove();
|
|
761
|
-
});
|
|
679
|
+
removeElements(`.${CSS_CLASSES.MODAL}`);
|
|
762
680
|
console.log("[WeWear VTO] Widget destroyed successfully");
|
|
763
681
|
}
|
|
764
682
|
catch (error) {
|
|
@@ -767,17 +685,7 @@
|
|
|
767
685
|
}
|
|
768
686
|
}
|
|
769
687
|
|
|
770
|
-
/**
|
|
771
|
-
* WeWear Virtual Try-On Widget Auto-Installer
|
|
772
|
-
*
|
|
773
|
-
* Provides automatic initialization and management of the virtual try-on widget.
|
|
774
|
-
* Handles DOM ready states and widget lifecycle management.
|
|
775
|
-
*/
|
|
776
688
|
let widgetInstance = null;
|
|
777
|
-
/**
|
|
778
|
-
* Initializes the virtual try-on widget with optional configuration
|
|
779
|
-
* @param config - Widget configuration options
|
|
780
|
-
*/
|
|
781
689
|
function initVirtualTryOn(config) {
|
|
782
690
|
try {
|
|
783
691
|
// Clean up any existing instance
|
|
@@ -805,9 +713,6 @@
|
|
|
805
713
|
console.error("[WeWear VTO] Initialization error:", error);
|
|
806
714
|
}
|
|
807
715
|
}
|
|
808
|
-
/**
|
|
809
|
-
* Destroys the current widget instance and cleans up resources
|
|
810
|
-
*/
|
|
811
716
|
function destroyVirtualTryOn() {
|
|
812
717
|
try {
|
|
813
718
|
if (widgetInstance) {
|
|
@@ -820,13 +725,16 @@
|
|
|
820
725
|
console.error("[WeWear VTO] Error destroying widget:", error);
|
|
821
726
|
}
|
|
822
727
|
}
|
|
823
|
-
|
|
728
|
+
function getWidgetInstance() {
|
|
729
|
+
return widgetInstance;
|
|
730
|
+
}
|
|
824
731
|
if (typeof window !== "undefined") {
|
|
825
732
|
initVirtualTryOn();
|
|
826
733
|
}
|
|
827
734
|
|
|
828
735
|
exports.VirtualTryOnWidget = VirtualTryOnWidget;
|
|
829
736
|
exports.destroyVirtualTryOn = destroyVirtualTryOn;
|
|
737
|
+
exports.getWidgetInstance = getWidgetInstance;
|
|
830
738
|
exports.initVirtualTryOn = initVirtualTryOn;
|
|
831
739
|
|
|
832
740
|
}));
|