@wewear/virtual-try-on 1.0.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 +580 -281
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +580 -280
- 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 +12 -39
- package/package.json +2 -2
- package/README.md +0 -82
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,249 +48,498 @@
|
|
|
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;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Makes API call to virtual try-on service
|
|
61
|
-
* @param ww_access_token - Optional authentication token
|
|
62
|
-
* @param ww_user_id - User identifier
|
|
63
|
-
* @param ww_product_id - Product identifier
|
|
64
|
-
* @returns Promise with virtual try-on result or null if failed
|
|
65
|
-
*/
|
|
66
|
-
async callVirtualTryOnApi(ww_access_token, ww_user_id, ww_product_id) {
|
|
67
|
-
try {
|
|
68
|
-
const response = await fetch(`${this.config.baseUrl}/api/virtual-try-on`, {
|
|
69
|
-
method: "POST",
|
|
70
|
-
headers: {
|
|
71
|
-
"Content-Type": "application/json",
|
|
72
|
-
Accept: "application/json",
|
|
73
|
-
},
|
|
74
|
-
body: JSON.stringify({
|
|
75
|
-
ww_access_token,
|
|
76
|
-
ww_user_id,
|
|
77
|
-
ww_product_id,
|
|
78
|
-
}),
|
|
79
|
-
});
|
|
80
|
-
if (!response.ok) {
|
|
81
|
-
throw new Error(`API responded with status ${response.status}: ${response.statusText}`);
|
|
82
|
-
}
|
|
83
|
-
const result = await response.json();
|
|
84
|
-
if (!result.imageUrl) {
|
|
85
|
-
throw new Error("API response missing imageUrl");
|
|
86
|
-
}
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
console.error("[WeWear VTO] API call failed:", error);
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error("[WeWear VTO] Camera access error:", error);
|
|
80
|
+
loadingIndicator.innerHTML =
|
|
81
|
+
"Camera access denied. Please allow camera permissions.";
|
|
93
82
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
container.className = CSS_CLASSES.BUTTON_CONTAINER;
|
|
102
|
-
const positionStyles = this.getPositionStyles();
|
|
103
|
-
container.style.cssText = `
|
|
104
|
-
position: absolute;
|
|
105
|
-
${positionStyles}
|
|
106
|
-
display: flex;
|
|
107
|
-
border-radius: 50%;
|
|
108
|
-
background: #000000;
|
|
109
|
-
padding: 4px;
|
|
110
|
-
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
111
|
-
z-index: ${Z_INDEX.BUTTON};
|
|
112
|
-
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
113
|
-
`;
|
|
114
|
-
const button = document.createElement("button");
|
|
115
|
-
button.type = "button";
|
|
116
|
-
button.className = CSS_CLASSES.BUTTON;
|
|
117
|
-
button.setAttribute("aria-label", "Virtual Try-On");
|
|
118
|
-
button.setAttribute("title", "Try this product virtually");
|
|
119
|
-
button.style.cssText = `
|
|
120
|
-
display: flex;
|
|
121
|
-
width: 36px;
|
|
122
|
-
height: 36px;
|
|
123
|
-
cursor: pointer;
|
|
124
|
-
align-items: center;
|
|
125
|
-
justify-content: center;
|
|
126
|
-
border-radius: 50%;
|
|
127
|
-
padding: 10px;
|
|
128
|
-
border: none;
|
|
129
|
-
background: transparent;
|
|
130
|
-
color: #ffffff;
|
|
131
|
-
`;
|
|
132
|
-
// Add hover effects
|
|
133
|
-
container.addEventListener("mouseenter", () => {
|
|
134
|
-
container.style.transform = "scale(1.05)";
|
|
135
|
-
container.style.boxShadow = "0 30px 60px -12px rgba(0, 0, 0, 0.35)";
|
|
136
|
-
});
|
|
137
|
-
container.addEventListener("mouseleave", () => {
|
|
138
|
-
container.style.transform = "scale(1)";
|
|
139
|
-
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();
|
|
140
90
|
});
|
|
141
|
-
|
|
142
|
-
<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">
|
|
143
|
-
<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>
|
|
144
|
-
<circle cx="12" cy="13" r="3"></circle>
|
|
145
|
-
</svg>
|
|
146
|
-
`;
|
|
147
|
-
button.onclick = onClick;
|
|
148
|
-
container.appendChild(button);
|
|
149
|
-
return container;
|
|
91
|
+
video.srcObject = null;
|
|
150
92
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return "right: 20px; top: 20px;";
|
|
161
|
-
case "top-left":
|
|
162
|
-
return "left: 20px; top: 20px;";
|
|
163
|
-
default:
|
|
164
|
-
return "right: 20px; bottom: 20px;";
|
|
165
|
-
}
|
|
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");
|
|
166
102
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
align-items: center;
|
|
191
|
-
justify-content: center;
|
|
192
|
-
z-index: ${Z_INDEX.MODAL};
|
|
193
|
-
animation: fadeIn 0.3s ease;
|
|
194
|
-
`;
|
|
195
|
-
modal.innerHTML = `
|
|
196
|
-
<div style="
|
|
197
|
-
background: #ffffff;
|
|
198
|
-
padding: 20px;
|
|
199
|
-
border-radius: 12px;
|
|
200
|
-
max-width: 90vw;
|
|
201
|
-
max-height: 90vh;
|
|
202
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
|
203
|
-
animation: slideIn 0.3s ease;
|
|
204
|
-
">
|
|
205
|
-
<img
|
|
206
|
-
src="${imageUrl}"
|
|
207
|
-
alt="Virtual Try-On Result"
|
|
208
|
-
style="
|
|
209
|
-
max-width: 100%;
|
|
210
|
-
max-height: 80vh;
|
|
211
|
-
border-radius: 8px;
|
|
212
|
-
display: block;
|
|
213
|
-
"
|
|
214
|
-
/>
|
|
215
|
-
<div style="
|
|
216
|
-
text-align: right;
|
|
217
|
-
margin-top: 15px;
|
|
218
|
-
">
|
|
219
|
-
<button
|
|
220
|
-
type="button"
|
|
221
|
-
style="
|
|
222
|
-
padding: 8px 16px;
|
|
223
|
-
border: none;
|
|
224
|
-
background: #000000;
|
|
225
|
-
color: #ffffff;
|
|
226
|
-
border-radius: 6px;
|
|
227
|
-
cursor: pointer;
|
|
228
|
-
font-size: 14px;
|
|
229
|
-
font-weight: 500;
|
|
230
|
-
transition: background-color 0.2s ease;
|
|
231
|
-
"
|
|
232
|
-
onmouseover="this.style.backgroundColor='#333333'"
|
|
233
|
-
onmouseout="this.style.backgroundColor='#000000'"
|
|
234
|
-
>
|
|
235
|
-
Close
|
|
236
|
-
</button>
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
<style>
|
|
240
|
-
@keyframes fadeIn {
|
|
241
|
-
from { opacity: 0; }
|
|
242
|
-
to { opacity: 1; }
|
|
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);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
reject(new Error("Failed to create blob from canvas"));
|
|
112
|
+
}
|
|
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;
|
|
243
126
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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;";
|
|
247
139
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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) {
|
|
380
|
+
modal.remove();
|
|
255
381
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// Prevent content clicks from closing modal
|
|
263
|
-
if (modalContent) {
|
|
264
|
-
modalContent.onclick = (e) => e.stopPropagation();
|
|
382
|
+
};
|
|
383
|
+
// Close on Escape key
|
|
384
|
+
const handleEscape = (e) => {
|
|
385
|
+
if (e.key === "Escape") {
|
|
386
|
+
modal.remove();
|
|
387
|
+
document.removeEventListener("keydown", handleEscape);
|
|
265
388
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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,
|
|
272
542
|
};
|
|
273
|
-
document.addEventListener("keydown", handleEscape);
|
|
274
|
-
document.body.appendChild(modal);
|
|
275
543
|
}
|
|
276
544
|
/**
|
|
277
545
|
* Initializes the virtual try-on widget on the current page
|
|
@@ -294,7 +562,7 @@
|
|
|
294
562
|
container.style.position = "relative";
|
|
295
563
|
}
|
|
296
564
|
// Create and add the virtual try-on button
|
|
297
|
-
const button = this.
|
|
565
|
+
const button = createButton(this.config.buttonPosition, async () => {
|
|
298
566
|
await this.handleTryOnClick();
|
|
299
567
|
});
|
|
300
568
|
container.appendChild(button);
|
|
@@ -310,47 +578,95 @@
|
|
|
310
578
|
*/
|
|
311
579
|
async handleTryOnClick() {
|
|
312
580
|
var _a;
|
|
581
|
+
console.log("[WeWear VTO] Button clicked, starting try-on process...");
|
|
313
582
|
try {
|
|
314
583
|
// Get required data
|
|
315
|
-
const ww_access_token =
|
|
316
|
-
const ww_user_id =
|
|
584
|
+
const ww_access_token = getCookie("ww_access_token");
|
|
585
|
+
const ww_user_id = getCookie("ww_user_id");
|
|
317
586
|
const skuElement = document.querySelector(this.config.skuSelector);
|
|
318
587
|
const ww_product_id = (_a = skuElement === null || skuElement === void 0 ? void 0 : skuElement.textContent) === null || _a === void 0 ? void 0 : _a.trim();
|
|
588
|
+
console.log("[WeWear VTO] Retrieved data:", {
|
|
589
|
+
ww_user_id,
|
|
590
|
+
ww_product_id,
|
|
591
|
+
ww_access_token,
|
|
592
|
+
});
|
|
319
593
|
// Validate required data
|
|
320
594
|
if (!ww_user_id) {
|
|
321
595
|
console.warn("[WeWear VTO] Missing required cookie: ww_user_id");
|
|
322
|
-
this.showError("Please sign in to use virtual try-on");
|
|
323
596
|
return;
|
|
324
597
|
}
|
|
325
598
|
if (!ww_product_id) {
|
|
326
599
|
console.warn("[WeWear VTO] Product SKU not found:", this.config.skuSelector);
|
|
327
|
-
this.showError("Product information not available");
|
|
328
600
|
return;
|
|
329
601
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const result = await this.callVirtualTryOnApi(ww_access_token, ww_user_id, ww_product_id);
|
|
334
|
-
if (result === null || result === void 0 ? void 0 : result.imageUrl) {
|
|
335
|
-
this.showImageModal(result.imageUrl);
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
this.showError("Virtual try-on service is currently unavailable. Please try again later.");
|
|
602
|
+
if (!ww_access_token) {
|
|
603
|
+
console.warn("[WeWear VTO] Missing required cookie: ww_access_token");
|
|
604
|
+
return;
|
|
339
605
|
}
|
|
606
|
+
// Show camera capture modal
|
|
607
|
+
this.showCameraModalWithCallbacks(ww_access_token, ww_user_id, ww_product_id);
|
|
340
608
|
}
|
|
341
609
|
catch (error) {
|
|
342
610
|
console.error("[WeWear VTO] Try-on request failed:", error);
|
|
343
|
-
this.showError("An unexpected error occurred. Please try again.");
|
|
344
611
|
}
|
|
345
612
|
}
|
|
346
613
|
/**
|
|
347
|
-
* Shows
|
|
348
|
-
* @
|
|
614
|
+
* Shows camera modal with appropriate callbacks
|
|
615
|
+
* @private
|
|
616
|
+
*/
|
|
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);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Shows review modal with appropriate callbacks
|
|
349
637
|
* @private
|
|
350
638
|
*/
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
+
},
|
|
648
|
+
};
|
|
649
|
+
showReviewModal(imageBlob, callbacks);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Processes the accepted image by calling the API
|
|
653
|
+
* @private
|
|
654
|
+
*/
|
|
655
|
+
async processAcceptedImage(imageBlob, ww_access_token, ww_user_id, ww_product_id) {
|
|
656
|
+
try {
|
|
657
|
+
console.log("[WeWear VTO] Processing accepted image...");
|
|
658
|
+
// Call the API with the accepted image
|
|
659
|
+
const result = await callVirtualTryOnApi(this.config.baseUrl, ww_access_token, ww_user_id, ww_product_id, imageBlob);
|
|
660
|
+
if (result === null || result === void 0 ? void 0 : result.imageUrl) {
|
|
661
|
+
showImageModal(result.imageUrl);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
console.error("[WeWear VTO] Invalid API response:", result);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
console.error("[WeWear VTO] Error processing accepted image:", error);
|
|
669
|
+
}
|
|
354
670
|
}
|
|
355
671
|
/**
|
|
356
672
|
* Destroys the widget and cleans up resources
|
|
@@ -358,15 +674,9 @@
|
|
|
358
674
|
destroy() {
|
|
359
675
|
try {
|
|
360
676
|
// Remove all buttons
|
|
361
|
-
|
|
362
|
-
buttons.forEach((button) => {
|
|
363
|
-
button.remove();
|
|
364
|
-
});
|
|
677
|
+
removeElements(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
|
|
365
678
|
// Remove all modals
|
|
366
|
-
|
|
367
|
-
modals.forEach((modal) => {
|
|
368
|
-
modal.remove();
|
|
369
|
-
});
|
|
679
|
+
removeElements(`.${CSS_CLASSES.MODAL}`);
|
|
370
680
|
console.log("[WeWear VTO] Widget destroyed successfully");
|
|
371
681
|
}
|
|
372
682
|
catch (error) {
|
|
@@ -375,17 +685,7 @@
|
|
|
375
685
|
}
|
|
376
686
|
}
|
|
377
687
|
|
|
378
|
-
/**
|
|
379
|
-
* WeWear Virtual Try-On Widget Auto-Installer
|
|
380
|
-
*
|
|
381
|
-
* Provides automatic initialization and management of the virtual try-on widget.
|
|
382
|
-
* Handles DOM ready states and widget lifecycle management.
|
|
383
|
-
*/
|
|
384
688
|
let widgetInstance = null;
|
|
385
|
-
/**
|
|
386
|
-
* Initializes the virtual try-on widget with optional configuration
|
|
387
|
-
* @param config - Widget configuration options
|
|
388
|
-
*/
|
|
389
689
|
function initVirtualTryOn(config) {
|
|
390
690
|
try {
|
|
391
691
|
// Clean up any existing instance
|
|
@@ -413,9 +713,6 @@
|
|
|
413
713
|
console.error("[WeWear VTO] Initialization error:", error);
|
|
414
714
|
}
|
|
415
715
|
}
|
|
416
|
-
/**
|
|
417
|
-
* Destroys the current widget instance and cleans up resources
|
|
418
|
-
*/
|
|
419
716
|
function destroyVirtualTryOn() {
|
|
420
717
|
try {
|
|
421
718
|
if (widgetInstance) {
|
|
@@ -428,13 +725,16 @@
|
|
|
428
725
|
console.error("[WeWear VTO] Error destroying widget:", error);
|
|
429
726
|
}
|
|
430
727
|
}
|
|
431
|
-
|
|
728
|
+
function getWidgetInstance() {
|
|
729
|
+
return widgetInstance;
|
|
730
|
+
}
|
|
432
731
|
if (typeof window !== "undefined") {
|
|
433
732
|
initVirtualTryOn();
|
|
434
733
|
}
|
|
435
734
|
|
|
436
735
|
exports.VirtualTryOnWidget = VirtualTryOnWidget;
|
|
437
736
|
exports.destroyVirtualTryOn = destroyVirtualTryOn;
|
|
737
|
+
exports.getWidgetInstance = getWidgetInstance;
|
|
438
738
|
exports.initVirtualTryOn = initVirtualTryOn;
|
|
439
739
|
|
|
440
740
|
}));
|