farrahred commited on
Commit
55eeb67
·
verified ·
1 Parent(s): 6902e02

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +198 -653
index.html CHANGED
@@ -3,119 +3,47 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>AI Virtual Makeup Try-On</title>
7
- <script src="https://cdn.tailwindcss.com?plugins=forms"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
 
8
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
9
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
10
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js" crossorigin="anonymous"></script>
11
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
12
  <style>
13
- /* Custom styles for UI elements and effects */
14
- body {
15
- font-family: 'Inter', sans-serif; /* Using a common sans-serif font */
16
- }
17
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
18
-
19
- /* Enhanced item hover/selection effects */
20
- .makeup-item, .color-swatch, .foundation-swatch {
21
- transition: all 0.2s ease-in-out;
22
- cursor: pointer;
23
- }
24
- .makeup-item:hover {
25
- transform: translateY(-2px);
26
- box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
27
- }
28
- .color-swatch:hover, .foundation-swatch:hover {
29
- transform: scale(1.1);
30
- }
31
-
32
- /* Selection indicator using a ring */
33
- .selected-item {
34
- /* Applied dynamically via JS */
35
- @apply ring-2 ring-offset-2 ring-pink-500;
36
- }
37
- .selected-color {
38
- outline: 2px solid #ec4899; /* Pink outline */
39
- outline-offset: 2px;
40
- }
41
-
42
- /* Custom slider thumb style (Tailwind forms plugin helps) */
43
- input[type="range"]::-webkit-slider-thumb {
44
- @apply h-5 w-5 bg-pink-500 rounded-full appearance-none cursor-pointer hover:bg-pink-600 transition-colors duration-150;
45
- }
46
- input[type="range"]::-moz-range-thumb {
47
- @apply h-5 w-5 bg-pink-500 rounded-full cursor-pointer border-none hover:bg-pink-600 transition-colors duration-150;
48
- }
49
-
50
- /* Pulse animation for start screen icon */
51
- @keyframes pulse {
52
- 0%, 100% { transform: scale(1); opacity: 1; }
53
- 50% { transform: scale(1.1); opacity: 0.9; }
54
- }
55
- .pulse {
56
- animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
57
- }
58
-
59
- /* Makeup overlay canvas */
60
- .makeup-overlay {
61
  position: absolute;
62
- left: 0;
63
- top: 0;
64
- width: 100%;
65
- height: 100%;
66
- pointer-events: none; /* Allows interaction with elements below */
67
  }
68
-
69
- /* Loading indicator style */
70
- #loading-indicator {
71
  position: absolute;
72
- top: 0; left: 0; right: 0; bottom: 0;
73
- background-color: rgba(255, 255, 255, 0.7);
74
- display: flex; /* Hidden/shown via JS */
75
- justify-content: center;
76
- align-items: center;
77
- z-index: 50; /* Above video/canvas, below error message */
78
- backdrop-filter: blur(4px);
79
- border-radius: 0.75rem; /* Match parent border-radius */
80
- opacity: 1;
81
- transition: opacity 0.3s ease-out;
82
- }
83
- #loading-indicator.hidden {
84
- opacity: 0;
85
- pointer-events: none; /* Prevent interaction when hidden */
86
- }
87
- /* Simple spinner */
88
- .spinner {
89
- border: 4px solid rgba(0, 0, 0, 0.1);
90
- width: 36px;
91
- height: 36px;
92
- border-radius: 50%;
93
- border-left-color: #ec4899; /* Pink */
94
- animation: spin 1s linear infinite; /* Use linear for smoother spin */
95
- }
96
- @keyframes spin {
97
- 0% { transform: rotate(0deg); }
98
- 100% { transform: rotate(360deg); }
99
  }
100
 
101
- /* Error message style */
102
- #error-message-container {
103
- position: fixed;
104
- bottom: 20px;
105
- left: 50%;
106
- transform: translateX(-50%);
107
- background-color: rgba(220, 38, 38, 0.95); /* Red-600 */
108
- color: white;
109
- padding: 12px 24px;
110
- border-radius: 8px;
111
- z-index: 1000;
112
- font-size: 0.875rem; /* 14px */
113
- line-height: 1.25rem;
114
- box-shadow: 0 4px 10px rgba(0,0,0,0.2);
115
- display: none; /* Hidden by default */
116
- text-align: center;
117
- max-width: 90%;
118
- }
119
  </style>
120
  </head>
121
  <body class="bg-gradient-to-br from-pink-50 to-purple-50 min-h-screen font-sans text-gray-800">
@@ -127,17 +55,8 @@
127
  GlamAI Try-On
128
  </h1>
129
  </div>
130
- <div class="hidden md:flex items-center space-x-6">
131
- <a href="#" class="text-gray-600 hover:text-pink-500 transition duration-150">Features</a>
132
- <a href="#" class="text-gray-600 hover:text-pink-500 transition duration-150">Looks</a>
133
- <a href="#" class="text-gray-600 hover:text-pink-500 transition duration-150">About</a>
134
- <button class="bg-pink-500 text-white px-5 py-2 rounded-full hover:bg-pink-600 transition duration-150 shadow hover:shadow-md">
135
- Sign Up
136
- </button>
137
- </div>
138
- <button class="md:hidden text-gray-600 hover:text-pink-500">
139
- <i class="fas fa-bars text-2xl"></i>
140
- </button>
141
  </div>
142
  </header>
143
 
@@ -152,20 +71,17 @@
152
  <div class="flex justify-between items-center mb-4">
153
  <h3 class="text-xl font-semibold text-gray-900">Live Camera Feed</h3>
154
  <div class="flex space-x-3">
155
- <button id="flip-camera" title="Flip Camera" class="bg-gray-100 p-2 rounded-full hover:bg-gray-200 text-gray-600 hover:text-pink-500 transition duration-150">
156
- <i class="fas fa-sync-alt text-lg"></i>
157
- </button>
158
- <button id="toggle-camera" title="Pause/Resume Camera" class="bg-pink-500 text-white w-10 h-10 flex items-center justify-center rounded-full hover:bg-pink-600 transition duration-150 shadow">
159
- <i id="toggle-camera-icon" class="fas fa-pause text-lg"></i> </button>
160
  </div>
161
  </div>
162
 
163
  <div class="relative overflow-hidden rounded-xl bg-gray-200 aspect-video flex justify-center items-center video-feed mb-6">
164
- <video id="video" autoplay playsinline muted class="block w-full h-full object-cover hidden"></video>
165
- <canvas id="output" class="absolute top-0 left-0 w-full h-full"></canvas>
166
  <canvas id="makeup-layer" class="makeup-overlay"></canvas>
167
- <div id="start-screen" class="text-center p-6 z-10">
168
- <div class="bg-pink-100 inline-block p-5 rounded-full mb-5 pulse">
169
  <i class="fas fa-camera-retro text-4xl text-pink-500"></i>
170
  </div>
171
  <h4 class="text-xl font-semibold text-gray-800 mb-2">Ready to Try?</h4>
@@ -174,169 +90,44 @@
174
  <i class="fas fa-play mr-2"></i>Start Camera
175
  </button>
176
  </div>
177
- <div id="loading-indicator" class="hidden"> <div class="spinner"></div>
178
- </div>
179
  </div>
180
 
181
  <div>
182
  <h3 class="text-xl font-semibold text-gray-900 mb-4">Adjust Intensity</h3>
183
  <div class="grid grid-cols-2 md:grid-cols-3 gap-x-6 gap-y-4">
184
- <div class="slider-control">
185
- <label for="lipstick-opacity" class="block text-sm font-medium text-gray-700 mb-1">Lipstick</label>
186
- <input type="range" id="lipstick-opacity" name="lipstick" min="0" max="100" value="70" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb">
187
- </div>
188
- <div class="slider-control">
189
- <label for="blush-opacity" class="block text-sm font-medium text-gray-700 mb-1">Blush</label>
190
- <input type="range" id="blush-opacity" name="blush" min="0" max="100" value="50" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb">
191
- </div>
192
- <div class="slider-control">
193
- <label for="eyeshadow-opacity" class="block text-sm font-medium text-gray-700 mb-1">Eyeshadow</label>
194
- <input type="range" id="eyeshadow-opacity" name="eyeshadow" min="0" max="100" value="60" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb">
195
- </div>
196
- <div class="slider-control">
197
- <label for="eyeliner-opacity" class="block text-sm font-medium text-gray-700 mb-1">Eyeliner</label>
198
- <input type="range" id="eyeliner-opacity" name="eyeliner" min="0" max="100" value="80" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb">
199
- </div>
200
- <div class="slider-control">
201
- <label for="mascara-opacity" class="block text-sm font-medium text-gray-700 mb-1">Mascara</label>
202
- <input type="range" id="mascara-opacity" name="mascara" min="0" max="100" value="65" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb">
203
- </div>
204
- <div class="slider-control">
205
- <label for="foundation-opacity" class="block text-sm font-medium text-gray-700 mb-1">Foundation</label>
206
- <input type="range" id="foundation-opacity" name="foundation" min="0" max="100" value="40" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb">
207
- </div>
208
  </div>
209
  </div>
210
  </div>
211
 
212
- <div class="lg:w-1/3 space-y-6">
213
- <div class="bg-white p-6 rounded-xl shadow-lg">
214
- <h3 class="text-xl font-semibold text-gray-900 mb-4">Quick Looks</h3>
215
- <div class="grid grid-cols-2 gap-4">
216
- <button data-look="romantic" class="makeup-item quick-look-btn bg-gradient-to-br from-rose-100 to-pink-200 p-4 rounded-lg text-center transition transform hover:shadow-lg">
217
- <i class="fas fa-heart text-3xl text-rose-500 mb-2"></i>
218
- <p class="font-medium text-sm text-gray-700">Romantic</p>
219
- </button>
220
- <button data-look="night" class="makeup-item quick-look-btn bg-gradient-to-br from-indigo-200 to-purple-300 p-4 rounded-lg text-center transition transform hover:shadow-lg">
221
- <i class="fas fa-moon text-3xl text-indigo-600 mb-2"></i>
222
- <p class="font-medium text-sm text-gray-700">Night Out</p>
223
- </button>
224
- <button data-look="day" class="makeup-item quick-look-btn bg-gradient-to-br from-amber-100 to-orange-200 p-4 rounded-lg text-center transition transform hover:shadow-lg">
225
- <i class="fas fa-sun text-3xl text-amber-500 mb-2"></i>
226
- <p class="font-medium text-sm text-gray-700">Daytime</p>
227
- </button>
228
- <button data-look="natural" class="makeup-item quick-look-btn bg-gradient-to-br from-green-100 to-teal-200 p-4 rounded-lg text-center transition transform hover:shadow-lg">
229
- <i class="fas fa-leaf text-3xl text-green-600 mb-2"></i>
230
- <p class="font-medium text-sm text-gray-700">Natural</p>
231
- </button>
232
- </div>
233
- </div>
234
-
235
- <div class="bg-white p-6 rounded-xl shadow-lg">
236
- <h3 class="text-xl font-semibold text-gray-900 mb-4">Lipstick Color</h3>
237
- <div class="flex flex-wrap gap-3">
238
- <div title="Classic Red" class="w-8 h-8 rounded-full bg-red-600 color-swatch" data-makeup-type="lipstick" data-color="#dc2626"></div>
239
- <div title="Hot Pink" class="w-8 h-8 rounded-full bg-pink-500 color-swatch" data-makeup-type="lipstick" data-color="#ec4899"></div>
240
- <div title="Soft Rose" class="w-8 h-8 rounded-full bg-rose-400 color-swatch" data-makeup-type="lipstick" data-color="#fb7185"></div>
241
- <div title="Deep Plum" class="w-8 h-8 rounded-full bg-purple-700 color-swatch" data-makeup-type="lipstick" data-color="#7e22ce"></div>
242
- <div title="Coral Peach" class="w-8 h-8 rounded-full bg-orange-400 color-swatch" data-makeup-type="lipstick" data-color="#fb923c"></div>
243
- <div title="Nude Brown" class="w-8 h-8 rounded-full bg-yellow-800 color-swatch" data-makeup-type="lipstick" data-color="#92400e"></div>
244
- <div title="Berry Wine" class="w-8 h-8 rounded-full bg-red-800 color-swatch" data-makeup-type="lipstick" data-color="#991b1b"></div>
245
- <div title="Natural Beige" class="w-8 h-8 rounded-full bg-orange-200 color-swatch" data-makeup-type="lipstick" data-color="#fed7aa"></div>
246
- </div>
247
- </div>
248
-
249
- <div class="bg-white p-6 rounded-xl shadow-lg">
250
- <h3 class="text-xl font-semibold text-gray-900 mb-4">Foundation Shade</h3>
251
- <div class="flex flex-wrap gap-3">
252
- <div title="Fair" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#F8E8DB" style="background-color: #F8E8DB;"></div>
253
- <div title="Light" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#F5D7C1" style="background-color: #F5D7C1;"></div>
254
- <div title="Medium" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#E1B99A" style="background-color: #E1B99A;"></div>
255
- <div title="Tan" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#C19A70" style="background-color: #C19A70;"></div>
256
- <div title="Deep" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#8C5A3C" style="background-color: #8C5A3C;"></div>
257
- <div title="Rich" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#5E3B2F" style="background-color: #5E3B2F;"></div>
258
- </div>
259
- </div>
260
-
261
- <div class="bg-white p-6 rounded-xl shadow-lg">
262
- <h3 class="text-xl font-semibold text-gray-900 mb-4">Eyeshadow Palette</h3>
263
- <div class="grid grid-cols-2 gap-4">
264
- <div id="sunset-glow" data-makeup-type="eyeshadow" data-colors='["#FDE68A", "#FCA5A5", "#F87171"]' title="Sunset Glow Palette" class="makeup-item eyeshadow-palette p-3 rounded-lg border border-gray-200 hover:shadow-md transition">
265
- <div class="flex space-x-1 mb-2 h-6">
266
- <div class="flex-1 rounded-sm" style="background-color: #FDE68A;"></div>
267
- <div class="flex-1 rounded-sm" style="background-color: #FCA5A5;"></div>
268
- <div class="flex-1 rounded-sm" style="background-color: #F87171;"></div>
269
- </div>
270
- <p class="text-sm font-medium text-gray-700">Sunset Glow</p>
271
- </div>
272
- <div id="berry-nights" data-makeup-type="eyeshadow" data-colors='["#E9D5FF", "#C084FC", "#9333EA"]' title="Berry Nights Palette" class="makeup-item eyeshadow-palette p-3 rounded-lg border border-gray-200 hover:shadow-md transition">
273
- <div class="flex space-x-1 mb-2 h-6">
274
- <div class="flex-1 rounded-sm" style="background-color: #E9D5FF;"></div>
275
- <div class="flex-1 rounded-sm" style="background-color: #C084FC;"></div>
276
- <div class="flex-1 rounded-sm" style="background-color: #9333EA;"></div>
277
- </div>
278
- <p class="text-sm font-medium text-gray-700">Berry Nights</p>
279
- </div>
280
- <div id="smokey-eye" data-makeup-type="eyeshadow" data-colors='["#E5E7EB", "#9CA3AF", "#4B5563"]' title="Smokey Eye Palette" class="makeup-item eyeshadow-palette p-3 rounded-lg border border-gray-200 hover:shadow-md transition">
281
- <div class="flex space-x-1 mb-2 h-6">
282
- <div class="flex-1 rounded-sm" style="background-color: #E5E7EB;"></div>
283
- <div class="flex-1 rounded-sm" style="background-color: #9CA3AF;"></div>
284
- <div class="flex-1 rounded-sm" style="background-color: #4B5563;"></div>
285
- </div>
286
- <p class="text-sm font-medium text-gray-700">Smokey Eye</p>
287
- </div>
288
- <div id="earth-tones" data-makeup-type="eyeshadow" data-colors='["#FEF3C7", "#FCD34D", "#D97706"]' title="Earth Tones Palette" class="makeup-item eyeshadow-palette p-3 rounded-lg border border-gray-200 hover:shadow-md transition">
289
- <div class="flex space-x-1 mb-2 h-6">
290
- <div class="flex-1 rounded-sm" style="background-color: #FEF3C7;"></div>
291
- <div class="flex-1 rounded-sm" style="background-color: #FCD34D;"></div>
292
- <div class="flex-1 rounded-sm" style="background-color: #D97706;"></div>
293
- </div>
294
- <p class="text-sm font-medium text-gray-700">Earth Tones</p>
295
- </div>
296
- </div>
297
- </div>
298
-
299
- <div class="bg-white p-6 rounded-xl shadow-lg">
300
- <h3 class="text-xl font-semibold text-gray-900 mb-4">Tools</h3>
301
- <div class="flex space-x-3">
302
- <button id="capture-btn" title="Capture Image" class="flex-1 bg-gray-100 hover:bg-gray-200 py-3 rounded-lg transition duration-150 flex items-center justify-center space-x-2 text-sm font-medium text-gray-700 hover:text-pink-500">
303
- <i class="fas fa-camera"></i>
304
- <span>Capture</span>
305
- </button>
306
- <button id="reset-btn" title="Reset Makeup" class="flex-1 bg-gray-100 hover:bg-gray-200 py-3 rounded-lg transition duration-150 flex items-center justify-center space-x-2 text-sm font-medium text-gray-700 hover:text-pink-500">
307
- <i class="fas fa-undo"></i>
308
- <span>Reset All</span>
309
- </button>
310
- <button id="landmarks-toggle" title="Toggle Landmarks" class="flex-1 bg-gray-100 hover:bg-gray-200 py-3 rounded-lg transition duration-150 flex items-center justify-center space-x-2 text-sm font-medium text-gray-700 hover:text-pink-500">
311
- <i class="fas fa-vector-square"></i>
312
- <span>Landmarks</span>
313
- </button>
314
- </div>
315
- </div>
316
  </div>
317
  </div>
318
  </main>
319
 
320
  <footer class="bg-gray-800 text-white py-10 mt-16">
321
- <div class="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
322
- <p class="text-gray-400 text-sm">&copy; 2025 GlamAI Try-On. All rights reserved.</p>
323
- <div class="mt-4 space-x-4">
324
- <a href="#" class="text-gray-400 hover:text-white transition text-sm">Privacy Policy</a>
325
- <a href="#" class="text-gray-400 hover:text-white transition text-sm">Terms of Service</a>
326
- </div>
327
- </div>
328
  </footer>
329
 
330
  <div id="error-message-container"></div>
331
 
332
  <script type="module">
333
- // Ensure using ES module type for potential future imports if needed
334
-
335
  // --- DOM Element References ---
336
  const videoElement = document.getElementById('video');
337
- const canvasElement = document.getElementById('output');
338
  const canvasCtx = canvasElement.getContext('2d');
339
- const makeupCanvas = document.getElementById('makeup-layer');
340
  const makeupCtx = makeupCanvas.getContext('2d');
341
  const startScreen = document.getElementById('start-screen');
342
  const startBtn = document.getElementById('start-btn');
@@ -348,296 +139,81 @@
348
  const landmarksToggle = document.getElementById('landmarks-toggle');
349
  const errorMessageContainer = document.getElementById('error-message-container');
350
  const loadingIndicator = document.getElementById('loading-indicator');
 
351
 
352
  // --- State Variables ---
353
- let isCameraOn = false;
354
- let isCameraStarting = false; // Prevent double-clicks
355
  let showLandmarks = false;
356
  let faceDetected = false;
357
- let mediaPipeCamera = null; // Renamed from 'camera' to avoid conflict
358
- let currentMakeupState = {}; // Store current colors and opacities
359
- let loadingTimeout = null; // Timeout ID for fallback loading hide
360
-
361
- // --- Constants ---
362
- const DEFAULT_LIP_COLOR = '#fb7185'; // Soft Rose
363
- const DEFAULT_EYESHADOW_COLORS = ["#FEF3C7", "#FCD34D", "#D97706"]; // Earth Tones
364
- const DEFAULT_FOUNDATION_COLOR = '#F5D7C1'; // Light
365
-
366
- // Default opacities (can be overridden by Quick Looks)
367
- const DEFAULT_OPACITIES = {
368
- lipstick: 0.7, blush: 0.5, eyeshadow: 0.6, eyeliner: 0.8, mascara: 0.65, foundation: 0.4
369
- };
370
-
371
- // Landmark indices constants (using MediaPipe names where possible)
372
- const LANDMARKS = {
373
- LIPS_OUTER_UPPER: [61, 185, 40, 39, 37, 0, 267, 269, 270, 409, 291],
374
- LIPS_OUTER_LOWER: [61, 146, 91, 181, 84, 17, 314, 405, 321, 375, 291],
375
- LIPS_INNER_UPPER: [78, 191, 80, 81, 82, 13, 312, 311, 310, 415, 308],
376
- LIPS_INNER_LOWER: [78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308],
377
- LEFT_EYE_UPPER_LID0: [33, 7, 163, 144, 145, 153, 154, 155, 133],
378
- LEFT_EYE_UPPER_LID1: [246, 161, 160, 159, 158, 157, 173],
379
- LEFT_EYE_LOWER_LID: [133, 173, 157, 158, 159, 160, 161, 246, 33],
380
- LEFT_EYEBROW: [70, 63, 105, 66, 107, 55, 65],
381
- RIGHT_EYE_UPPER_LID0: [263, 249, 390, 373, 374, 380, 381, 382, 362],
382
- RIGHT_EYE_UPPER_LID1: [466, 388, 387, 386, 385, 384, 398],
383
- RIGHT_EYE_LOWER_LID: [362, 398, 384, 385, 386, 387, 388, 466, 263],
384
- RIGHT_EYEBROW: [300, 293, 334, 296, 336, 285, 295],
385
- LEFT_CHEEK_AREA: [119, 118, 117, 147, 187, 205, 50, 135, 136, 234],
386
- RIGHT_CHEEK_AREA: [348, 347, 346, 376, 411, 425, 280, 364, 365, 454],
387
- FACE_OVAL: [
388
- 10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288,
389
- 397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136,
390
- 172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109, 10
391
- ]
392
- };
393
-
394
- // Quick Look Definitions
395
- const QUICK_LOOKS = {
396
- romantic: {
397
- lipstick: { color: '#ec4899', opacity: 0.9 }, blush: { opacity: 0.7 },
398
- eyeshadow: { colors: ["#FBCFE8", "#F9A8D4", "#F472B6"], opacity: 0.8 },
399
- eyeliner: { opacity: 0.9 }, mascara: { opacity: 0.8 },
400
- foundation: { color: currentMakeupState.foundation?.color || DEFAULT_FOUNDATION_COLOR, opacity: 0.4 }
401
- },
402
- night: {
403
- lipstick: { color: '#9333EA', opacity: 1.0 }, blush: { opacity: 0.4 },
404
- eyeshadow: { colors: ["#A855F7", "#7E22CE", "#581C87"], opacity: 1.0 },
405
- eyeliner: { opacity: 1.0 }, mascara: { opacity: 0.9 },
406
- foundation: { color: currentMakeupState.foundation?.color || DEFAULT_FOUNDATION_COLOR, opacity: 0.5 }
407
- },
408
- day: {
409
- lipstick: { color: '#fb7185', opacity: 0.6 }, blush: { opacity: 0.5 },
410
- eyeshadow: { colors: DEFAULT_EYESHADOW_COLORS, opacity: 0.5 },
411
- eyeliner: { opacity: 0.6 }, mascara: { opacity: 0.7 },
412
- foundation: { color: currentMakeupState.foundation?.color || DEFAULT_FOUNDATION_COLOR, opacity: 0.35 }
413
- },
414
- natural: {
415
- lipstick: { color: '#fed7aa', opacity: 0.4 }, blush: { opacity: 0.3 },
416
- eyeshadow: { colors: ["#FEF3C7", "#FCD34D", "#D97706"], opacity: 0.3 },
417
- eyeliner: { opacity: 0.4 }, mascara: { opacity: 0.6 },
418
- foundation: { color: currentMakeupState.foundation?.color || DEFAULT_FOUNDATION_COLOR, opacity: 0.25 }
419
- }
420
- };
421
-
422
- // --- Camera Configuration ---
423
- let currentFacingMode = "user"; // Start with front camera
424
- const cameraConstraints = {
425
- video: {
426
- // Request higher resolution - may improve sharpness if supported
427
- // If camera fails, might need to revert to 640x480
428
- width: { ideal: 1280 },
429
- height: { ideal: 720 },
430
- facingMode: currentFacingMode
431
- }
432
- };
433
 
434
  // --- MediaPipe Face Mesh Initialization ---
435
- const faceMesh = new FaceMesh({
436
- locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`
437
- });
438
- faceMesh.setOptions({
439
- maxNumFaces: 1,
440
- refineLandmarks: true,
441
- minDetectionConfidence: 0.5,
442
- minTrackingConfidence: 0.5
443
- });
444
 
445
  // --- Face Mesh Results Callback ---
446
  faceMesh.onResults((results) => {
447
- // Hide loading indicator (primary method)
448
- setLoadingIndicatorVisibility(false); // Make it hidden
449
- clearTimeout(loadingTimeout); // Clear fallback timer if it exists
450
 
 
451
  canvasCtx.save();
452
  canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
 
453
  if (currentFacingMode === "user") {
454
  canvasCtx.scale(-1, 1);
455
  canvasCtx.translate(-canvasElement.width, 0);
456
  }
457
  canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
458
 
 
459
  if (results.multiFaceLandmarks && results.multiFaceLandmarks.length > 0) {
460
  faceDetected = true;
461
  const landmarks = results.multiFaceLandmarks[0];
 
 
462
  makeupCtx.clearRect(0, 0, makeupCanvas.width, makeupCanvas.height);
 
463
  applyMakeup(landmarks);
 
 
464
  if (showLandmarks) {
 
465
  drawLandmarks(results.multiFaceLandmarks);
466
  }
467
  } else {
468
  faceDetected = false;
 
469
  makeupCtx.clearRect(0, 0, makeupCanvas.width, makeupCanvas.height);
470
  }
471
- canvasCtx.restore();
472
  });
473
 
474
- // --- Drawing Functions ---
475
-
476
- /** Draws landmarks using MediaPipe's utility */
477
- function drawLandmarks(landmarksData) {
478
- for (const landmarks of landmarksData) {
479
- drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_TESSELATION, { color: '#C0C0C070', lineWidth: 1 });
480
- drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_RIGHT_EYE, { color: '#FF3030', lineWidth: 1 });
481
- drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_RIGHT_EYEBROW, { color: '#FF3030', lineWidth: 1 });
482
- drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_LEFT_EYE, { color: '#30FF30', lineWidth: 1 });
483
- drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_LEFT_EYEBROW, { color: '#30FF30', lineWidth: 1 });
484
- drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_FACE_OVAL, { color: '#E0E0E0', lineWidth: 1 });
485
- drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_LIPS, { color: '#E0E0E0', lineWidth: 1 });
486
- }
487
- }
488
-
489
- /** Main function to apply all makeup types based on currentMakeupState */
490
- function applyMakeup(landmarks) {
491
- if (!faceDetected) return;
492
- makeupCtx.save();
493
- if (currentFacingMode === "user") {
494
- makeupCtx.scale(-1, 1);
495
- makeupCtx.translate(-makeupCanvas.width, 0);
496
- }
497
- const state = currentMakeupState;
498
- if (state.foundation?.opacity > 0) applyFoundation(landmarks, state.foundation.color, state.foundation.opacity);
499
- if (state.lipstick?.opacity > 0) applyLipstick(landmarks, state.lipstick.color, state.lipstick.opacity);
500
- if (state.blush?.opacity > 0) applyBlush(landmarks, state.blush.opacity);
501
- if (state.eyeshadow?.opacity > 0) applyEyeshadow(landmarks, state.eyeshadow.colors, state.eyeshadow.opacity);
502
- if (state.eyeliner?.opacity > 0) applyEyeliner(landmarks, state.eyeliner.opacity);
503
- if (state.mascara?.opacity > 0) applyMascara(landmarks, state.mascara.opacity);
504
- makeupCtx.restore();
505
- }
506
-
507
- /** Applies foundation */
508
- function applyFoundation(landmarks, color, opacity) {
509
- const faceOvalPoints = getLandmarksByIndices(landmarks, LANDMARKS.FACE_OVAL);
510
- if (faceOvalPoints.length < 3) return;
511
- const path = createPathFromPoints(faceOvalPoints, makeupCanvas.width, makeupCanvas.height);
512
- makeupCtx.fillStyle = hexToRgba(color, opacity * 0.85);
513
- makeupCtx.fill(path);
514
- }
515
-
516
- /** Applies lipstick */
517
- function applyLipstick(landmarks, color, opacity) {
518
- const outerUpperPoints = getLandmarksByIndices(landmarks, LANDMARKS.LIPS_OUTER_UPPER);
519
- const outerLowerPoints = getLandmarksByIndices(landmarks, LANDMARKS.LIPS_OUTER_LOWER);
520
- const innerUpperPoints = getLandmarksByIndices(landmarks, LANDMARKS.LIPS_INNER_UPPER);
521
- const innerLowerPoints = getLandmarksByIndices(landmarks, LANDMARKS.LIPS_INNER_LOWER);
522
- if (outerUpperPoints.length < 2 || outerLowerPoints.length < 2 || innerUpperPoints.length < 2 || innerLowerPoints.length < 2) return;
523
- const rgbaColor = hexToRgba(color, opacity);
524
- makeupCtx.fillStyle = rgbaColor;
525
- const upperLipPath = new Path2D();
526
- drawPointsSmoothly(upperLipPath, outerUpperPoints, true, makeupCanvas.width, makeupCanvas.height);
527
- drawPointsSmoothly(upperLipPath, innerUpperPoints.slice().reverse(), false, makeupCanvas.width, makeupCanvas.height);
528
- upperLipPath.closePath();
529
- makeupCtx.fill(upperLipPath);
530
- const lowerLipPath = new Path2D();
531
- drawPointsSmoothly(lowerLipPath, outerLowerPoints, true, makeupCanvas.width, makeupCanvas.height);
532
- drawPointsSmoothly(lowerLipPath, innerLowerPoints.slice().reverse(), false, makeupCanvas.width, makeupCanvas.height);
533
- lowerLipPath.closePath();
534
- makeupCtx.fill(lowerLipPath);
535
- if (opacity > 0.3) {
536
- const upperLipCenter = landmarks[13]; const lowerLipCenter = landmarks[14];
537
- if (upperLipCenter && lowerLipCenter && outerUpperPoints[5] && innerUpperPoints[5] && outerLowerPoints[4] && innerLowerPoints[4]) {
538
- const upperShineRadius = Math.abs(outerUpperPoints[5].y - innerUpperPoints[5].y) * makeupCanvas.height * 0.3;
539
- const lowerShineRadius = Math.abs(outerLowerPoints[4].y - innerLowerPoints[4].y) * makeupCanvas.height * 0.4;
540
- applyRadialGradient(upperLipCenter, upperShineRadius, `rgba(255, 255, 255, ${opacity * 0.3})`, makeupCanvas.width, makeupCanvas.height);
541
- applyRadialGradient(lowerLipCenter, lowerShineRadius, `rgba(255, 255, 255, ${opacity * 0.4})`, makeupCanvas.width, makeupCanvas.height);
542
- }
543
- }
544
- }
545
-
546
- /** Applies blush */
547
- function applyBlush(landmarks, opacity) {
548
- const leftCheekPoints = getLandmarksByIndices(landmarks, LANDMARKS.LEFT_CHEEK_AREA);
549
- const rightCheekPoints = getLandmarksByIndices(landmarks, LANDMARKS.RIGHT_CHEEK_AREA);
550
- if (leftCheekPoints.length < 3 || rightCheekPoints.length < 3) return;
551
- const leftCheekCenter = calculateCenter(leftCheekPoints);
552
- const rightCheekCenter = calculateCenter(rightCheekPoints);
553
- const leftRadius = Math.hypot((leftCheekCenter.x - leftCheekPoints[0].x) * makeupCanvas.width, (leftCheekCenter.y - leftCheekPoints[0].y) * makeupCanvas.height) * 1.2;
554
- const rightRadius = Math.hypot((rightCheekCenter.x - rightCheekPoints[0].x) * makeupCanvas.width, (rightCheekCenter.y - rightCheekPoints[0].y) * makeupCanvas.height) * 1.2;
555
- const blushColor = `rgba(255, 130, 150, ${opacity * 0.6})`;
556
- const drawBlushGradient = (center, radius) => {
557
- if (radius <= 0) return;
558
- const gradient = makeupCtx.createRadialGradient(center.x * makeupCanvas.width, center.y * makeupCanvas.height, radius * 0.1, center.x * makeupCanvas.width, center.y * makeupCanvas.height, radius);
559
- gradient.addColorStop(0, blushColor); gradient.addColorStop(1, `rgba(255, 130, 150, 0)`);
560
- makeupCtx.fillStyle = gradient; makeupCtx.beginPath(); makeupCtx.arc(center.x * makeupCanvas.width, center.y * makeupCanvas.height, radius, 0, Math.PI * 2); makeupCtx.fill();
561
- };
562
- drawBlushGradient(leftCheekCenter, leftRadius); drawBlushGradient(rightCheekCenter, rightRadius);
563
- }
564
-
565
- /** Applies eyeshadow */
566
- function applyEyeshadow(landmarks, colors, opacity) {
567
- applySingleEyeShadow(landmarks, true, colors, opacity); applySingleEyeShadow(landmarks, false, colors, opacity);
568
- }
569
-
570
- /** Applies eyeshadow to one eye */
571
- function applySingleEyeShadow(landmarks, isLeftEye, colors, opacity) {
572
- const upperLidPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYE_UPPER_LID0 : LANDMARKS.RIGHT_EYE_UPPER_LID0);
573
- const eyebrowPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYEBROW : LANDMARKS.RIGHT_EYEBROW);
574
- if (upperLidPoints.length < 2 || eyebrowPoints.length < 2) return;
575
- const path = new Path2D();
576
- drawPointsSmoothly(path, upperLidPoints, true, makeupCanvas.width, makeupCanvas.height);
577
- drawPointsSmoothly(path, eyebrowPoints.slice().reverse(), false, makeupCanvas.width, makeupCanvas.height);
578
- path.closePath();
579
- let minY = Infinity, maxY = -Infinity;
580
- [...upperLidPoints, ...eyebrowPoints].forEach(p => { minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); });
581
- if (minY === Infinity || maxY === -Infinity) return;
582
- const gradient = makeupCtx.createLinearGradient(0, minY * makeupCanvas.height, 0, maxY * makeupCanvas.height);
583
- const numColors = colors.length;
584
- colors.forEach((color, index) => { gradient.addColorStop(index / (numColors - 1 || 1), hexToRgba(color, opacity)); });
585
- makeupCtx.fillStyle = gradient; makeupCtx.fill(path);
586
- }
587
-
588
- /** Applies eyeliner */
589
- function applyEyeliner(landmarks, opacity) {
590
- applySingleEyeLiner(landmarks, true, opacity); applySingleEyeLiner(landmarks, false, opacity);
591
- }
592
-
593
- /** Applies eyeliner to one eye */
594
- function applySingleEyeLiner(landmarks, isLeftEye, opacity) {
595
- const upperLidPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYE_UPPER_LID1 : LANDMARKS.RIGHT_EYE_UPPER_LID1);
596
- const lowerLidPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYE_LOWER_LID : LANDMARKS.RIGHT_EYE_LOWER_LID);
597
- if (upperLidPoints.length < 2 || lowerLidPoints.length < 2) return;
598
- makeupCtx.strokeStyle = `rgba(30, 30, 30, ${opacity})`; makeupCtx.lineWidth = 1 + (opacity * 2.5);
599
- makeupCtx.lineJoin = 'round'; makeupCtx.lineCap = 'round';
600
- makeupCtx.beginPath(); drawPointsSmoothly(makeupCtx, upperLidPoints, true, makeupCanvas.width, makeupCanvas.height);
601
- const outerCorner = upperLidPoints[upperLidPoints.length - 1]; const controlPoint = upperLidPoints[upperLidPoints.length - 2];
602
- if (outerCorner && controlPoint) {
603
- const wingLength = 12 + opacity * 8; const angle = Math.atan2(outerCorner.y - controlPoint.y, outerCorner.x - controlPoint.x);
604
- const wingAngleOffset = isLeftEye ? -0.35 : 0.35;
605
- const wingX = outerCorner.x * makeupCanvas.width + Math.cos(angle + wingAngleOffset) * wingLength;
606
- const wingY = outerCorner.y * makeupCanvas.height + Math.sin(angle + wingAngleOffset) * wingLength;
607
- makeupCtx.quadraticCurveTo(outerCorner.x * makeupCanvas.width + Math.cos(angle) * wingLength * 0.5, outerCorner.y * makeupCanvas.height + Math.sin(angle) * wingLength * 0.5, wingX, wingY);
608
- }
609
- makeupCtx.stroke();
610
- makeupCtx.lineWidth = 1 + (opacity * 1.0); makeupCtx.strokeStyle = `rgba(50, 50, 50, ${opacity * 0.6})`;
611
- makeupCtx.beginPath();
612
- const lowerMidIndex = Math.floor(lowerLidPoints.length / 2); const lowerOuterPoints = lowerLidPoints.slice(lowerMidIndex - 1);
613
- if (lowerOuterPoints.length > 1) { drawPointsSmoothly(makeupCtx, lowerOuterPoints, true, makeupCanvas.width, makeupCanvas.height); makeupCtx.stroke(); }
614
- }
615
-
616
- /** Applies mascara effect */
617
- function applyMascara(landmarks, opacity) {
618
- applySingleEyeMascara(landmarks, true, opacity); applySingleEyeMascara(landmarks, false, opacity);
619
- }
620
-
621
- /** Applies mascara to one eye */
622
- function applySingleEyeMascara(landmarks, isLeftEye, opacity) {
623
- const upperLashPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYE_UPPER_LID1 : LANDMARKS.RIGHT_EYE_UPPER_LID1);
624
- if (upperLashPoints.length < 2) return;
625
- makeupCtx.strokeStyle = `rgba(10, 10, 10, ${opacity * 0.9})`;
626
- const baseLashLength = 3 + opacity * 4; const lashWidth = 1 + opacity * 1.5;
627
- for (let i = 0; i < upperLashPoints.length - 1; i++) {
628
- const p1 = upperLashPoints[i]; const p2 = upperLashPoints[i + 1];
629
- const midX = (p1.x + p2.x) / 2 * makeupCanvas.width; const midY = (p1.y + p2.y) / 2 * makeupCanvas.height;
630
- const dx = p2.x - p1.x; const dy = p2.y - p1.y; let nx = -dy; let ny = dx;
631
- if ((isLeftEye && ny > 0) || (!isLeftEye && ny > 0)) { nx *= -1; ny *= -1; }
632
- const len = Math.sqrt(nx * nx + ny * ny); if (len === 0) continue;
633
- nx /= len; ny /= len;
634
- const lashLength = baseLashLength * (0.8 + Math.random() * 0.4);
635
- makeupCtx.lineWidth = lashWidth * (0.8 + Math.random() * 0.4);
636
- makeupCtx.beginPath(); makeupCtx.moveTo(midX, midY); makeupCtx.lineTo(midX + nx * lashLength, midY + ny * lashLength); makeupCtx.stroke();
637
- }
638
- }
639
-
640
- // --- Helper Functions ---
641
  function getLandmarksByIndices(landmarks, indices) { return indices.map(index => landmarks[index]).filter(p => p); }
642
  function calculateCenter(points) { if (!points || points.length === 0) return { x: 0, y: 0 }; let sumX = 0, sumY = 0; points.forEach(p => { sumX += p.x; sumY += p.y; }); return { x: sumX / points.length, y: sumY / points.length }; }
643
  function hexToRgba(hex, alpha = 1) { if (!hex || typeof hex !== 'string') return `rgba(0,0,0,${alpha})`; hex = hex.replace('#', ''); let r = 0, g = 0, b = 0; if (hex.length === 3) { r = parseInt(hex[0] + hex[0], 16); g = parseInt(hex[1] + hex[1], 16); b = parseInt(hex[2] + hex[2], 16); } else if (hex.length === 6) { r = parseInt(hex[0] + hex[1], 16); g = parseInt(hex[2] + hex[3], 16); b = parseInt(hex[4] + hex[5], 16); } else { return `rgba(0,0,0,${alpha})`; } alpha = Math.max(0, Math.min(1, alpha)); return `rgba(${r}, ${g}, ${b}, ${alpha})`; }
@@ -645,27 +221,16 @@
645
  function drawPointsSmoothly(ctxOrPath, points, moveToStart = true, canvasWidth, canvasHeight) { if (!points || points.length < 2) return; const scaledPoints = points.map(p => ({ x: p.x * canvasWidth, y: p.y * canvasHeight })); if (moveToStart) { ctxOrPath.moveTo(scaledPoints[0].x, scaledPoints[0].y); } if (scaledPoints.length === 2) { ctxOrPath.lineTo(scaledPoints[1].x, scaledPoints[1].y); return; } for (let i = 0; i < scaledPoints.length - 1; i++) { const p0 = scaledPoints[i === 0 ? i : i - 1]; const p1 = scaledPoints[i]; const p2 = scaledPoints[i + 1]; const p3 = scaledPoints[i + 2 < scaledPoints.length ? i + 2 : i + 1]; const cp1x = p1.x + (p2.x - p0.x) / 6; const cp1y = p1.y + (p2.y - p0.y) / 6; const cp2x = p2.x - (p3.x - p1.x) / 6; const cp2y = p2.y - (p3.y - p1.y) / 6; ctxOrPath.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y); } }
646
  function applyRadialGradient(center, radius, color, canvasWidth, canvasHeight) { if (!center || radius <= 0) return; const gradient = makeupCtx.createRadialGradient(center.x * canvasWidth, center.y * canvasHeight, 0, center.x * canvasWidth, center.y * canvasHeight, radius); gradient.addColorStop(0, color); gradient.addColorStop(1, hexToRgba(color, 0)); makeupCtx.fillStyle = gradient; makeupCtx.beginPath(); makeupCtx.arc(center.x * canvasWidth, center.y * canvasHeight, radius, 0, Math.PI * 2); makeupCtx.fill(); }
647
 
648
- // --- Camera and UI Control Functions ---
649
 
650
- /** Sets the visibility of the loading indicator */
651
- function setLoadingIndicatorVisibility(isVisible) {
652
- if (isVisible) {
653
- loadingIndicator.classList.remove('hidden');
654
- } else {
655
- // Only hide if not already hidden to avoid redundant logging
656
- if (!loadingIndicator.classList.contains('hidden')) {
657
- console.log("Hiding loading indicator.");
658
- loadingIndicator.classList.add('hidden');
659
- }
660
- }
661
- }
662
 
663
- /** Initializes and starts the camera */
664
  async function startCamera() {
665
  if (isCameraStarting || isCameraOn) return;
666
  isCameraStarting = true;
667
- setLoadingIndicatorVisibility(true); // Make it visible
668
- console.log("Attempting to start camera...");
669
 
670
  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { showErrorMessage("Camera API not supported."); setLoadingIndicatorVisibility(false); isCameraStarting = false; return; }
671
  if (!window.isSecureContext) { showErrorMessage("Camera requires HTTPS/localhost."); setLoadingIndicatorVisibility(false); isCameraStarting = false; return; }
@@ -675,161 +240,119 @@
675
  const stream = await navigator.mediaDevices.getUserMedia(cameraConstraints);
676
  console.log("Camera stream obtained.");
677
  videoElement.srcObject = stream;
678
- videoElement.classList.remove('hidden');
679
- startScreen.classList.add('hidden');
680
- isCameraOn = true;
681
- updateToggleIcon();
682
 
683
  videoElement.onloadedmetadata = () => {
684
- console.log("Video metadata loaded.");
685
- setupCanvas();
686
- initializeMediaPipeCamera();
 
687
  isCameraStarting = false;
688
- // Fallback to hide loader after 5s if onResults doesn't fire quickly
689
- clearTimeout(loadingTimeout); // Clear previous timer
690
- loadingTimeout = setTimeout(() => {
691
- console.log("Fallback: Hiding loading indicator via timeout.");
692
- setLoadingIndicatorVisibility(false);
693
- }, 5000);
694
  };
695
- videoElement.onerror = (e) => { console.error("Video element error:", e); showErrorMessage("Error playing video stream."); stopCameraStream(); setLoadingIndicatorVisibility(false); isCameraStarting = false; };
696
 
697
  } catch (err) {
698
  console.error("Error starting camera:", err.name, err.message);
699
- handleCameraError(err); stopCameraStream(); setLoadingIndicatorVisibility(false); isCameraStarting = false;
 
 
 
700
  }
701
  }
702
 
703
- /** Initializes or re-initializes the MediaPipe Camera helper */
704
  function initializeMediaPipeCamera() {
705
- if (mediaPipeCamera) { mediaPipeCamera.close(); }
706
  console.log("Initializing MediaPipe Camera helper.");
707
  mediaPipeCamera = new Camera(videoElement, {
708
- onFrame: async () => { if (videoElement.readyState >= 2 && isCameraOn) { await faceMesh.send({ image: videoElement }); } },
709
- width: cameraConstraints.video.width.ideal, height: cameraConstraints.video.height.ideal
 
 
 
 
 
710
  });
711
  mediaPipeCamera.start();
712
  console.log("MediaPipe Camera processing started.");
713
  }
714
 
715
- /** Handles different camera start errors */
716
- function handleCameraError(err) {
717
- let message = "Could not access camera.";
718
- switch (err.name) {
719
- case "NotAllowedError": message = "Permission denied. Please allow camera access."; break;
720
- case "NotFoundError": message = "No camera found. Ensure it's connected."; break;
721
- case "NotReadableError": message = "Camera is busy or hardware error."; break;
722
- case "OverconstrainedError": message = `Camera doesn't support ${cameraConstraints.video.width.ideal}x${cameraConstraints.video.height.ideal}.`; break;
723
- case "SecurityError": message = "Camera access denied (security)."; break;
724
- case "TypeError": message = "Invalid camera constraints."; break;
725
- default: message = `Unknown camera error: ${err.name}`; break;
726
- }
727
- showErrorMessage(message);
728
- startScreen.classList.remove('hidden'); videoElement.classList.add('hidden');
729
- }
730
 
731
- /** Stops the camera stream and cleans up */
732
- function stopCameraStream() {
733
- console.log("Stopping camera stream and MediaPipe.");
734
- clearTimeout(loadingTimeout); // Clear fallback timer
735
  if (mediaPipeCamera) { mediaPipeCamera.close(); mediaPipeCamera = null; }
736
  if (videoElement.srcObject) { videoElement.srcObject.getTracks().forEach(track => track.stop()); videoElement.srcObject = null; }
737
- videoElement.classList.add('hidden'); startScreen.classList.remove('hidden');
738
  isCameraOn = false; isCameraStarting = false; faceDetected = false;
739
  makeupCtx.clearRect(0, 0, makeupCanvas.width, makeupCanvas.height);
740
- canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
741
- updateToggleIcon(); setLoadingIndicatorVisibility(false); // Ensure loader is hidden
 
742
  }
743
 
744
- /** Toggles camera pause/play */
745
  function toggleCamera() {
746
- if (isCameraStarting || !videoElement.srcObject) return;
747
- isCameraOn = !isCameraOn;
748
- if (isCameraOn) { videoElement.play(); mediaPipeCamera?.start(); console.log("Camera resumed."); }
749
- else { videoElement.pause(); mediaPipeCamera?.stop(); console.log("Camera paused."); }
750
- updateToggleIcon();
 
751
  }
752
 
753
- /** Updates the toggle button icon */
754
  function updateToggleIcon() {
755
- if (isCameraOn) { toggleCameraIcon.className = 'fas fa-pause text-lg'; toggleCameraBtn.title = "Pause Camera"; }
756
- else { toggleCameraIcon.className = 'fas fa-play text-lg'; toggleCameraBtn.title = "Resume Camera"; }
 
 
 
 
 
 
757
  }
758
 
759
- /** Flips camera */
760
  async function flipCamera() {
761
  if (isCameraStarting) return;
762
  console.log("Attempting to flip camera...");
763
  currentFacingMode = (currentFacingMode === "user") ? "environment" : "user";
764
  console.log("New facing mode:", currentFacingMode);
765
- stopCameraStream(); await startCamera();
 
 
 
 
 
 
766
  }
767
 
768
- /** Captures image */
769
- function captureImage() {
770
- if (!isCameraOn) { showErrorMessage("Camera is paused. Resume to capture."); return; }
771
- if (!faceDetected && !showLandmarks) { showErrorMessage("No face detected to capture!"); return; }
772
- const tempCanvas = document.createElement('canvas');
773
- tempCanvas.width = canvasElement.clientWidth; tempCanvas.height = canvasElement.clientHeight;
774
- const tempCtx = tempCanvas.getContext('2d');
775
- tempCtx.save();
776
- if (currentFacingMode === "user") { tempCtx.scale(-1, 1); tempCtx.translate(-tempCanvas.width, 0); }
777
- tempCtx.drawImage(canvasElement, 0, 0, tempCanvas.width, tempCanvas.height);
778
- tempCtx.restore();
779
- tempCtx.save();
780
- if (currentFacingMode === "user") { tempCtx.scale(-1, 1); tempCtx.translate(-tempCanvas.width, 0); }
781
- tempCtx.drawImage(makeupCanvas, 0, 0, tempCanvas.width, tempCanvas.height);
782
- tempCtx.restore();
783
- const link = document.createElement('a'); link.download = `virtual-makeup-${Date.now()}.png`; link.href = tempCanvas.toDataURL('image/png'); link.click();
784
- console.log("Image captured.");
785
- }
786
 
787
- /** Resets makeup state and UI */
788
- function resetMakeup() {
789
- console.log("Resetting makeup.");
790
- initializeMakeupState(); updateSlidersFromState(); updateColorSelectionUI();
791
- updatePaletteSelectionUI(); updateLookSelectionUI();
792
- }
793
-
794
- /** Toggles landmarks */
795
- function toggleLandmarks() {
796
- showLandmarks = !showLandmarks;
797
- landmarksToggle.classList.toggle('bg-pink-100', showLandmarks);
798
- landmarksToggle.classList.toggle('text-pink-600', showLandmarks);
799
- landmarksToggle.title = showLandmarks ? "Hide Landmarks" : "Show Landmarks";
800
- console.log("Landmarks toggled:", showLandmarks);
801
- }
802
-
803
- /** Sets canvas dimensions */
804
  function setupCanvas() {
805
  if (!videoElement.videoWidth || videoElement.videoWidth === 0) return;
806
- const container = videoElement.parentElement; const videoWidth = videoElement.videoWidth; const videoHeight = videoElement.videoHeight;
807
- canvasElement.width = makeupCanvas.width = videoWidth; canvasElement.height = makeupCanvas.height = videoHeight;
808
- canvasElement.style.width = makeupCanvas.style.width = `100%`; canvasElement.style.height = makeupCanvas.style.height = `100%`;
809
- videoElement.style.width = `100%`; videoElement.style.height = `100%`;
810
- console.log(`Canvas buffer size: ${videoWidth}x${videoHeight}, Display size fills container.`);
 
811
  }
812
 
813
- /** Displays error message */
814
- function showErrorMessage(message) {
815
- errorMessageContainer.textContent = message; errorMessageContainer.style.display = 'block';
816
- clearTimeout(errorMessageContainer.timer); errorMessageContainer.timer = setTimeout(() => { errorMessageContainer.style.display = 'none'; }, 5000);
817
- }
818
-
819
- // --- State Management ---
820
- function initializeMakeupState() {
821
- currentMakeupState = {
822
- lipstick: { color: DEFAULT_LIP_COLOR, opacity: DEFAULT_OPACITIES.lipstick }, blush: { opacity: DEFAULT_OPACITIES.blush },
823
- eyeshadow: { colors: [...DEFAULT_EYESHADOW_COLORS], opacity: DEFAULT_OPACITIES.eyeshadow }, eyeliner: { opacity: DEFAULT_OPACITIES.eyeliner },
824
- mascara: { opacity: DEFAULT_OPACITIES.mascara }, foundation: { color: DEFAULT_FOUNDATION_COLOR, opacity: DEFAULT_OPACITIES.foundation }
825
- };
826
- console.log("Makeup state initialized.");
827
- }
828
  function handleSliderChange(event) { const makeupType = event.target.name; const opacity = parseFloat(event.target.value) / 100; if (currentMakeupState[makeupType]) { currentMakeupState[makeupType].opacity = opacity; } else { console.warn(`Makeup type ${makeupType} not found.`); } updateLookSelectionUI(); }
829
- function handleColorSelection(event) { const target = event.currentTarget; const makeupType = target.dataset.makeupType; const color = target.dataset.color; const colors = target.dataset.colors ? JSON.parse(target.dataset.colors) : null; if (makeupType === 'lipstick' && color) { currentMakeupState.lipstick.color = color; updateColorSelectionUI('lipstick', color); } else if (makeupType === 'foundation' && color) { currentMakeupState.foundation.color = color; updateColorSelectionUI('foundation', color); } else if (makeupType === 'eyeshadow' && colors) { currentMakeupState.eyeshadow.colors = colors; updatePaletteSelectionUI(target.id); } updateLookSelectionUI(); console.log(`State updated: ${makeupType} color/colors set.`); }
830
- function applyQuickLook(event) { const lookName = event.currentTarget.dataset.look; const lookData = QUICK_LOOKS[lookName]; if (!lookData) { console.error(`Look "${lookName}" not found.`); return; } console.log(`Applying look: ${lookName}`); for (const makeupType in lookData) { if (currentMakeupState[makeupType]) { if (makeupType === 'foundation' && !lookData.foundation.color) { lookData.foundation.color = currentMakeupState.foundation.color || DEFAULT_FOUNDATION_COLOR; } currentMakeupState[makeupType] = { ...currentMakeupState[makeupType], ...lookData[makeupType] }; } } updateSlidersFromState(); updateColorSelectionUI('lipstick', currentMakeupState.lipstick.color); updateColorSelectionUI('foundation', currentMakeupState.foundation.color); updatePaletteSelectionUI(null, currentMakeupState.eyeshadow.colors); updateLookSelectionUI(lookName); }
831
 
832
- // --- UI Update Functions ---
 
 
 
 
833
  function updateSlidersFromState() { document.querySelectorAll('.slider-control input[type="range"]').forEach(slider => { const makeupType = slider.name; if (currentMakeupState[makeupType] && typeof currentMakeupState[makeupType].opacity === 'number') { slider.value = currentMakeupState[makeupType].opacity * 100; } }); }
834
  function updateColorSelectionUI(type, selectedColor) { document.querySelectorAll(`.color-swatch[data-makeup-type="${type}"], .foundation-swatch[data-makeup-type="${type}"]`).forEach(swatch => { swatch.classList.toggle('selected-color', swatch.dataset.color === selectedColor); }); }
835
  function updatePaletteSelectionUI(selectedId = null, selectedColors = null) { document.querySelectorAll('.eyeshadow-palette').forEach(palette => { let isSelected = false; if (selectedId) { isSelected = palette.id === selectedId; } else if (selectedColors) { const paletteColors = palette.dataset.colors ? JSON.parse(palette.dataset.colors) : null; isSelected = JSON.stringify(paletteColors) === JSON.stringify(selectedColors); } palette.classList.toggle('selected-item', isSelected); }); }
@@ -837,14 +360,36 @@
837
 
838
  // --- Event Listeners ---
839
  document.addEventListener('DOMContentLoaded', () => {
840
- console.log("DOM ready. Initializing...");
841
  initializeMakeupState(); updateSlidersFromState(); updateColorSelectionUI('lipstick', currentMakeupState.lipstick.color); updateColorSelectionUI('foundation', currentMakeupState.foundation.color); updatePaletteSelectionUI(null, currentMakeupState.eyeshadow.colors);
842
  if (!window.isSecureContext && window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1") { showErrorMessage("Warning: Camera works best on HTTPS or localhost."); }
843
- startBtn?.addEventListener('click', startCamera); toggleCameraBtn?.addEventListener('click', toggleCamera); flipCameraBtn?.addEventListener('click', flipCamera); captureBtn?.addEventListener('click', captureImage); resetBtn?.addEventListener('click', resetMakeup); landmarksToggle?.addEventListener('click', toggleLandmarks);
844
- document.querySelectorAll('.slider-control input[type="range"]').forEach(slider => { slider.addEventListener('input', handleSliderChange); });
845
- document.querySelector('.lg\\:w-1\\/3')?.addEventListener('click', (event) => { if (event.target.matches('.color-swatch, .foundation-swatch')) { handleColorSelection(event); } else if (event.target.closest('.eyeshadow-palette')) { handleColorSelection({ currentTarget: event.target.closest('.eyeshadow-palette') }); } else if (event.target.closest('.quick-look-btn')) { applyQuickLook({ currentTarget: event.target.closest('.quick-look-btn') }); } });
846
- window.addEventListener('resize', setupCanvas);
847
- console.log("Initialization complete. Ready.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
  }); // End DOMContentLoaded
849
 
850
  </script>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Virtual Makeup Try-On (2D Overlay)</title>
7
+ <script src="https://cdn.tailwindcss.com?plugins=forms"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
9
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
10
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
11
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js" crossorigin="anonymous"></script>
12
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
13
  <style>
14
+ body { font-family: 'Inter', sans-serif; }
 
 
 
15
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
16
+ /* Styles (same as final 2D version) */
17
+ .makeup-item, .color-swatch, .foundation-swatch { transition: all 0.2s ease-in-out; cursor: pointer; }
18
+ .makeup-item:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); }
19
+ .color-swatch:hover, .foundation-swatch:hover { transform: scale(1.1); }
20
+ .selected-item { @apply ring-2 ring-offset-2 ring-pink-500; }
21
+ .selected-color { outline: 2px solid #ec4899; outline-offset: 2px; }
22
+ input[type="range"]::-webkit-slider-thumb { @apply h-5 w-5 bg-pink-500 rounded-full appearance-none cursor-pointer hover:bg-pink-600 transition-colors duration-150; }
23
+ input[type="range"]::-moz-range-thumb { @apply h-5 w-5 bg-pink-500 rounded-full cursor-pointer border-none hover:bg-pink-600 transition-colors duration-150; }
24
+ @keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.9; } }
25
+ .pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
26
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
27
+ .spinner { border: 4px solid rgba(0, 0, 0, 0.1); width: 36px; height: 36px; border-radius: 50%; border-left-color: #ec4899; animation: spin 1s linear infinite; }
28
+
29
+ /* Canvas container and overlay styling */
30
+ .video-feed { position: relative; overflow: hidden; background-color: #e0e0e0; }
31
+ #output { /* Canvas for video frame + landmarks */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  position: absolute;
33
+ top: 0; left: 0;
34
+ width: 100%; height: 100%;
35
+ object-fit: cover; /* Ensure canvas content covers area */
 
 
36
  }
37
+ .makeup-overlay { /* Canvas for makeup */
 
 
38
  position: absolute;
39
+ left: 0; top: 0;
40
+ width: 100%; height: 100%;
41
+ pointer-events: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
 
44
+ #loading-indicator { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(255, 255, 255, 0.7); display: flex; justify-content: center; align-items: center; z-index: 50; backdrop-filter: blur(4px); border-radius: 0.75rem; opacity: 1; transition: opacity 0.3s ease-out; }
45
+ #loading-indicator.hidden { opacity: 0; pointer-events: none; }
46
+ #error-message-container { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(220, 38, 38, 0.95); color: white; padding: 12px 24px; border-radius: 8px; z-index: 1000; font-size: 0.875rem; line-height: 1.25rem; box-shadow: 0 4px 10px rgba(0,0,0,0.2); display: none; text-align: center; max-width: 90%; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  </style>
48
  </head>
49
  <body class="bg-gradient-to-br from-pink-50 to-purple-50 min-h-screen font-sans text-gray-800">
 
55
  GlamAI Try-On
56
  </h1>
57
  </div>
58
+ <div class="hidden md:flex items-center space-x-6"> <a href="#" class="text-gray-600 hover:text-pink-500 transition duration-150">Features</a> <a href="#" class="text-gray-600 hover:text-pink-500 transition duration-150">Looks</a> <a href="#" class="text-gray-600 hover:text-pink-500 transition duration-150">About</a> <button class="bg-pink-500 text-white px-5 py-2 rounded-full hover:bg-pink-600 transition duration-150 shadow hover:shadow-md"> Sign Up </button> </div>
59
+ <button class="md:hidden text-gray-600 hover:text-pink-500"> <i class="fas fa-bars text-2xl"></i> </button>
 
 
 
 
 
 
 
 
 
60
  </div>
61
  </header>
62
 
 
71
  <div class="flex justify-between items-center mb-4">
72
  <h3 class="text-xl font-semibold text-gray-900">Live Camera Feed</h3>
73
  <div class="flex space-x-3">
74
+ <button id="flip-camera" title="Flip Camera" class="bg-gray-100 p-2 rounded-full hover:bg-gray-200 text-gray-600 hover:text-pink-500 transition duration-150"> <i class="fas fa-sync-alt text-lg"></i> </button>
75
+ <button id="toggle-camera" title="Pause/Resume Camera" class="bg-pink-500 text-white w-10 h-10 flex items-center justify-center rounded-full hover:bg-pink-600 transition duration-150 shadow"> <i id="toggle-camera-icon" class="fas fa-pause text-lg"></i> </button>
 
 
 
76
  </div>
77
  </div>
78
 
79
  <div class="relative overflow-hidden rounded-xl bg-gray-200 aspect-video flex justify-center items-center video-feed mb-6">
80
+ <video id="video" autoplay playsinline muted class="hidden"></video>
81
+ <canvas id="output"></canvas>
82
  <canvas id="makeup-layer" class="makeup-overlay"></canvas>
83
+ <div id="start-screen" class="absolute inset-0 flex flex-col justify-center items-center bg-gray-200 rounded-xl z-20">
84
+ <div class="bg-pink-100 inline-block p-5 rounded-full mb-5 pulse">
85
  <i class="fas fa-camera-retro text-4xl text-pink-500"></i>
86
  </div>
87
  <h4 class="text-xl font-semibold text-gray-800 mb-2">Ready to Try?</h4>
 
90
  <i class="fas fa-play mr-2"></i>Start Camera
91
  </button>
92
  </div>
93
+ <div id="loading-indicator" class="hidden z-10"> <div class="spinner"></div> <p class="ml-3 text-gray-600">Initializing...</p> </div>
 
94
  </div>
95
 
96
  <div>
97
  <h3 class="text-xl font-semibold text-gray-900 mb-4">Adjust Intensity</h3>
98
  <div class="grid grid-cols-2 md:grid-cols-3 gap-x-6 gap-y-4">
99
+ <div class="slider-control"> <label for="lipstick-opacity" class="block text-sm font-medium text-gray-700 mb-1">Lipstick</label> <input type="range" id="lipstick-opacity" name="lipstick" min="0" max="100" value="70" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> </div>
100
+ <div class="slider-control"> <label for="blush-opacity" class="block text-sm font-medium text-gray-700 mb-1">Blush</label> <input type="range" id="blush-opacity" name="blush" min="0" max="100" value="50" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> </div>
101
+ <div class="slider-control"> <label for="eyeshadow-opacity" class="block text-sm font-medium text-gray-700 mb-1">Eyeshadow</label> <input type="range" id="eyeshadow-opacity" name="eyeshadow" min="0" max="100" value="60" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> </div>
102
+ <div class="slider-control"> <label for="eyeliner-opacity" class="block text-sm font-medium text-gray-700 mb-1">Eyeliner</label> <input type="range" id="eyeliner-opacity" name="eyeliner" min="0" max="100" value="80" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> </div>
103
+ <div class="slider-control"> <label for="mascara-opacity" class="block text-sm font-medium text-gray-700 mb-1">Mascara</label> <input type="range" id="mascara-opacity" name="mascara" min="0" max="100" value="65" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> </div>
104
+ <div class="slider-control"> <label for="foundation-opacity" class="block text-sm font-medium text-gray-700 mb-1">Foundation</label> <input type="range" id="foundation-opacity" name="foundation" min="0" max="100" value="40" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  </div>
106
  </div>
107
  </div>
108
 
109
+ <div id="controls-column" class="lg:w-1/3 space-y-6">
110
+ <div class="bg-white p-6 rounded-xl shadow-lg"> <h3 class="text-xl font-semibold text-gray-900 mb-4">Quick Looks</h3> <div class="grid grid-cols-2 gap-4"> <button data-look="romantic" class="makeup-item quick-look-btn bg-gradient-to-br from-rose-100 to-pink-200 p-4 rounded-lg text-center transition transform hover:shadow-lg"> <i class="fas fa-heart text-3xl text-rose-500 mb-2"></i> <p class="font-medium text-sm text-gray-700">Romantic</p> </button> <button data-look="night" class="makeup-item quick-look-btn bg-gradient-to-br from-indigo-200 to-purple-300 p-4 rounded-lg text-center transition transform hover:shadow-lg"> <i class="fas fa-moon text-3xl text-indigo-600 mb-2"></i> <p class="font-medium text-sm text-gray-700">Night Out</p> </button> <button data-look="day" class="makeup-item quick-look-btn bg-gradient-to-br from-amber-100 to-orange-200 p-4 rounded-lg text-center transition transform hover:shadow-lg"> <i class="fas fa-sun text-3xl text-amber-500 mb-2"></i> <p class="font-medium text-sm text-gray-700">Daytime</p> </button> <button data-look="natural" class="makeup-item quick-look-btn bg-gradient-to-br from-green-100 to-teal-200 p-4 rounded-lg text-center transition transform hover:shadow-lg"> <i class="fas fa-leaf text-3xl text-green-600 mb-2"></i> <p class="font-medium text-sm text-gray-700">Natural</p> </button> </div> </div>
111
+ <div class="bg-white p-6 rounded-xl shadow-lg"> <h3 class="text-xl font-semibold text-gray-900 mb-4">Lipstick Color</h3> <div class="flex flex-wrap gap-3"> <div title="Classic Red" class="w-8 h-8 rounded-full bg-red-600 color-swatch" data-makeup-type="lipstick" data-color="#dc2626"></div> <div title="Hot Pink" class="w-8 h-8 rounded-full bg-pink-500 color-swatch" data-makeup-type="lipstick" data-color="#ec4899"></div> <div title="Soft Rose" class="w-8 h-8 rounded-full bg-rose-400 color-swatch" data-makeup-type="lipstick" data-color="#fb7185"></div> <div title="Deep Plum" class="w-8 h-8 rounded-full bg-purple-700 color-swatch" data-makeup-type="lipstick" data-color="#7e22ce"></div> <div title="Coral Peach" class="w-8 h-8 rounded-full bg-orange-400 color-swatch" data-makeup-type="lipstick" data-color="#fb923c"></div> <div title="Nude Brown" class="w-8 h-8 rounded-full bg-yellow-800 color-swatch" data-makeup-type="lipstick" data-color="#92400e"></div> <div title="Berry Wine" class="w-8 h-8 rounded-full bg-red-800 color-swatch" data-makeup-type="lipstick" data-color="#991b1b"></div> <div title="Natural Beige" class="w-8 h-8 rounded-full bg-orange-200 color-swatch" data-makeup-type="lipstick" data-color="#fed7aa"></div> </div> </div>
112
+ <div class="bg-white p-6 rounded-xl shadow-lg"> <h3 class="text-xl font-semibold text-gray-900 mb-4">Foundation Shade</h3> <div class="flex flex-wrap gap-3"> <div title="Fair" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#F8E8DB" style="background-color: #F8E8DB;"></div> <div title="Light" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#F5D7C1" style="background-color: #F5D7C1;"></div> <div title="Medium" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#E1B99A" style="background-color: #E1B99A;"></div> <div title="Tan" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#C19A70" style="background-color: #C19A70;"></div> <div title="Deep" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#8C5A3C" style="background-color: #8C5A3C;"></div> <div title="Rich" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#5E3B2F" style="background-color: #5E3B2F;"></div> </div> </div>
113
+ <div class="bg-white p-6 rounded-xl shadow-lg"> <h3 class="text-xl font-semibold text-gray-900 mb-4">Eyeshadow Palette</h3> <div class="grid grid-cols-2 gap-4"> <div id="sunset-glow" data-makeup-type="eyeshadow" data-colors='["#FDE68A", "#FCA5A5", "#F87171"]' title="Sunset Glow Palette" class="makeup-item eyeshadow-palette p-3 rounded-lg border border-gray-200 hover:shadow-md transition"> <div class="flex space-x-1 mb-2 h-6"> <div class="flex-1 rounded-sm" style="background-color: #FDE68A;"></div> <div class="flex-1 rounded-sm" style="background-color: #FCA5A5;"></div> <div class="flex-1 rounded-sm" style="background-color: #F87171;"></div> </div> <p class="text-sm font-medium text-gray-700">Sunset Glow</p> </div> <div id="berry-nights" data-makeup-type="eyeshadow" data-colors='["#E9D5FF", "#C084FC", "#9333EA"]' title="Berry Nights Palette" class="makeup-item eyeshadow-palette p-3 rounded-lg border border-gray-200 hover:shadow-md transition"> <div class="flex space-x-1 mb-2 h-6"> <div class="flex-1 rounded-sm" style="background-color: #E9D5FF;"></div> <div class="flex-1 rounded-sm" style="background-color: #C084FC;"></div> <div class="flex-1 rounded-sm" style="background-color: #9333EA;"></div> </div> <p class="text-sm font-medium text-gray-700">Berry Nights</p> </div> <div id="smokey-eye" data-makeup-type="eyeshadow" data-colors='["#E5E7EB", "#9CA3AF", "#4B5563"]' title="Smokey Eye Palette" class="makeup-item eyeshadow-palette p-3 rounded-lg border border-gray-200 hover:shadow-md transition"> <div class="flex space-x-1 mb-2 h-6"> <div class="flex-1 rounded-sm" style="background-color: #E5E7EB;"></div> <div class="flex-1 rounded-sm" style="background-color: #9CA3AF;"></div> <div class="flex-1 rounded-sm" style="background-color: #4B5563;"></div> </div> <p class="text-sm font-medium text-gray-700">Smokey Eye</p> </div> <div id="earth-tones" data-makeup-type="eyeshadow" data-colors='["#FEF3C7", "#FCD34D", "#D97706"]' title="Earth Tones Palette" class="makeup-item eyeshadow-palette p-3 rounded-lg border border-gray-200 hover:shadow-md transition"> <div class="flex space-x-1 mb-2 h-6"> <div class="flex-1 rounded-sm" style="background-color: #FEF3C7;"></div> <div class="flex-1 rounded-sm" style="background-color: #FCD34D;"></div> <div class="flex-1 rounded-sm" style="background-color: #D97706;"></div> </div> <p class="text-sm font-medium text-gray-700">Earth Tones</p> </div> </div> </div>
114
+ <div class="bg-white p-6 rounded-xl shadow-lg"> <h3 class="text-xl font-semibold text-gray-900 mb-4">Tools</h3> <div class="flex space-x-3"> <button id="capture-btn" title="Capture Image" class="flex-1 bg-gray-100 hover:bg-gray-200 py-3 rounded-lg transition duration-150 flex items-center justify-center space-x-2 text-sm font-medium text-gray-700 hover:text-pink-500"> <i class="fas fa-camera"></i> <span>Capture</span> </button> <button id="reset-btn" title="Reset Makeup" class="flex-1 bg-gray-100 hover:bg-gray-200 py-3 rounded-lg transition duration-150 flex items-center justify-center space-x-2 text-sm font-medium text-gray-700 hover:text-pink-500"> <i class="fas fa-undo"></i> <span>Reset All</span> </button> <button id="landmarks-toggle" title="Toggle Landmarks" class="flex-1 bg-gray-100 hover:bg-gray-200 py-3 rounded-lg transition duration-150 flex items-center justify-center space-x-2 text-sm font-medium text-gray-700 hover:text-pink-500"> <i class="fas fa-vector-square"></i> <span>Landmarks</span> </button> </div> </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  </div>
116
  </div>
117
  </main>
118
 
119
  <footer class="bg-gray-800 text-white py-10 mt-16">
120
+ <div class="container mx-auto px-4 sm:px-6 lg:px-8 text-center"> <p class="text-gray-400 text-sm">&copy; 2025 GlamAI Try-On. All rights reserved.</p> <div class="mt-4 space-x-4"> <a href="#" class="text-gray-400 hover:text-white transition text-sm">Privacy Policy</a> <a href="#" class="text-gray-400 hover:text-white transition text-sm">Terms of Service</a> </div> </div>
 
 
 
 
 
 
121
  </footer>
122
 
123
  <div id="error-message-container"></div>
124
 
125
  <script type="module">
 
 
126
  // --- DOM Element References ---
127
  const videoElement = document.getElementById('video');
128
+ const canvasElement = document.getElementById('output'); // For video frame + landmarks
129
  const canvasCtx = canvasElement.getContext('2d');
130
+ const makeupCanvas = document.getElementById('makeup-layer'); // For makeup overlay
131
  const makeupCtx = makeupCanvas.getContext('2d');
132
  const startScreen = document.getElementById('start-screen');
133
  const startBtn = document.getElementById('start-btn');
 
139
  const landmarksToggle = document.getElementById('landmarks-toggle');
140
  const errorMessageContainer = document.getElementById('error-message-container');
141
  const loadingIndicator = document.getElementById('loading-indicator');
142
+ const controlsColumn = document.getElementById('controls-column'); // Parent for delegated events
143
 
144
  // --- State Variables ---
145
+ let isCameraOn = false; // Tracks if MediaPipe processing loop is active
146
+ let isCameraStarting = false;
147
  let showLandmarks = false;
148
  let faceDetected = false;
149
+ let mediaPipeCamera = null; // MediaPipe Camera helper instance
150
+ let currentMakeupState = {};
151
+ let loadingTimeout = null;
152
+ let currentFacingMode = "user";
153
+
154
+ // --- Constants --- (Defaults, Landmarks, Looks - same as before)
155
+ const DEFAULT_LIP_COLOR = '#fb7185'; const DEFAULT_EYESHADOW_COLORS = ["#FEF3C7", "#FCD34D", "#D97706"]; const DEFAULT_FOUNDATION_COLOR = '#F5D7C1'; const DEFAULT_OPACITIES = { lipstick: 0.7, blush: 0.5, eyeshadow: 0.6, eyeliner: 0.8, mascara: 0.65, foundation: 0.4 };
156
+ const LANDMARKS = { /* ... */ LIPS_OUTER_UPPER: [61, 185, 40, 39, 37, 0, 267, 269, 270, 409, 291], LIPS_OUTER_LOWER: [61, 146, 91, 181, 84, 17, 314, 405, 321, 375, 291], LIPS_INNER_UPPER: [78, 191, 80, 81, 82, 13, 312, 311, 310, 415, 308], LIPS_INNER_LOWER: [78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308], LEFT_EYE_UPPER_LID0: [33, 7, 163, 144, 145, 153, 154, 155, 133], LEFT_EYE_UPPER_LID1: [246, 161, 160, 159, 158, 157, 173], LEFT_EYE_LOWER_LID: [133, 173, 157, 158, 159, 160, 161, 246, 33], LEFT_EYEBROW: [70, 63, 105, 66, 107, 55, 65], RIGHT_EYE_UPPER_LID0: [263, 249, 390, 373, 374, 380, 381, 382, 362], RIGHT_EYE_UPPER_LID1: [466, 388, 387, 386, 385, 384, 398], RIGHT_EYE_LOWER_LID: [362, 398, 384, 385, 386, 387, 388, 466, 263], RIGHT_EYEBROW: [300, 293, 334, 296, 336, 285, 295], LEFT_CHEEK_AREA: [119, 118, 117, 147, 187, 205, 50, 135, 136, 234], RIGHT_CHEEK_AREA: [348, 347, 346, 376, 411, 425, 280, 364, 365, 454], FACE_OVAL: [10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288, 397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136, 172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109, 10] };
157
+ const QUICK_LOOKS = { /* ... */ romantic: { lipstick: { color: '#ec4899', opacity: 0.9 }, blush: { opacity: 0.7 }, eyeshadow: { colors: ["#FBCFE8", "#F9A8D4", "#F472B6"], opacity: 0.8 }, eyeliner: { opacity: 0.9 }, mascara: { opacity: 0.8 }, foundation: { color: currentMakeupState.foundation?.color || DEFAULT_FOUNDATION_COLOR, opacity: 0.4 } }, night: { lipstick: { color: '#9333EA', opacity: 1.0 }, blush: { opacity: 0.4 }, eyeshadow: { colors: ["#A855F7", "#7E22CE", "#581C87"], opacity: 1.0 }, eyeliner: { opacity: 1.0 }, mascara: { opacity: 0.9 }, foundation: { color: currentMakeupState.foundation?.color || DEFAULT_FOUNDATION_COLOR, opacity: 0.5 } }, day: { lipstick: { color: '#fb7185', opacity: 0.6 }, blush: { opacity: 0.5 }, eyeshadow: { colors: DEFAULT_EYESHADOW_COLORS, opacity: 0.5 }, eyeliner: { opacity: 0.6 }, mascara: { opacity: 0.7 }, foundation: { color: currentMakeupState.foundation?.color || DEFAULT_FOUNDATION_COLOR, opacity: 0.35 } }, natural: { lipstick: { color: '#fed7aa', opacity: 0.4 }, blush: { opacity: 0.3 }, eyeshadow: { colors: ["#FEF3C7", "#FCD34D", "#D97706"], opacity: 0.3 }, eyeliner: { opacity: 0.4 }, mascara: { opacity: 0.6 }, foundation: { color: currentMakeupState.foundation?.color || DEFAULT_FOUNDATION_COLOR, opacity: 0.25 } } };
158
+
159
+ // --- Camera Constraints ---
160
+ const cameraConstraints = { video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: currentFacingMode } };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
  // --- MediaPipe Face Mesh Initialization ---
163
+ const faceMesh = new FaceMesh({ locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}` });
164
+ faceMesh.setOptions({ maxNumFaces: 1, refineLandmarks: true, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5 });
 
 
 
 
 
 
 
165
 
166
  // --- Face Mesh Results Callback ---
167
  faceMesh.onResults((results) => {
168
+ setLoadingIndicatorVisibility(false); clearTimeout(loadingTimeout); // Hide loader
 
 
169
 
170
+ // --- Draw video frame and landmarks on output canvas ---
171
  canvasCtx.save();
172
  canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
173
+ // Flip horizontally for front camera before drawing video
174
  if (currentFacingMode === "user") {
175
  canvasCtx.scale(-1, 1);
176
  canvasCtx.translate(-canvasElement.width, 0);
177
  }
178
  canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
179
 
180
+ // --- Process and Draw Makeup ---
181
  if (results.multiFaceLandmarks && results.multiFaceLandmarks.length > 0) {
182
  faceDetected = true;
183
  const landmarks = results.multiFaceLandmarks[0];
184
+
185
+ // Clear previous makeup before drawing new frame
186
  makeupCtx.clearRect(0, 0, makeupCanvas.width, makeupCanvas.height);
187
+ // Apply makeup effects to the makeup canvas
188
  applyMakeup(landmarks);
189
+
190
+ // Draw landmarks on top if toggled
191
  if (showLandmarks) {
192
+ // Draw landmarks on the main canvas (already transformed if needed)
193
  drawLandmarks(results.multiFaceLandmarks);
194
  }
195
  } else {
196
  faceDetected = false;
197
+ // Clear makeup canvas if no face detected
198
  makeupCtx.clearRect(0, 0, makeupCanvas.width, makeupCanvas.height);
199
  }
200
+ canvasCtx.restore(); // Restore context of output canvas
201
  });
202
 
203
+ // --- Drawing Functions --- (applyMakeup, applyFoundation, etc. - Omitted for brevity, same as before)
204
+ function drawLandmarks(landmarksData) { /* ... */ for (const landmarks of landmarksData) { drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_TESSELATION, { color: '#C0C0C070', lineWidth: 1 }); drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_RIGHT_EYE, { color: '#FF3030', lineWidth: 1 }); drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_RIGHT_EYEBROW, { color: '#FF3030', lineWidth: 1 }); drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_LEFT_EYE, { color: '#30FF30', lineWidth: 1 }); drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_LEFT_EYEBROW, { color: '#30FF30', lineWidth: 1 }); drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_FACE_OVAL, { color: '#E0E0E0', lineWidth: 1 }); drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_LIPS, { color: '#E0E0E0', lineWidth: 1 }); } }
205
+ function applyMakeup(landmarks) { if (!faceDetected) return; makeupCtx.save(); if (currentFacingMode === "user") { makeupCtx.scale(-1, 1); makeupCtx.translate(-makeupCanvas.width, 0); } const state = currentMakeupState; if (state.foundation?.opacity > 0) applyFoundation(landmarks, state.foundation.color, state.foundation.opacity); if (state.lipstick?.opacity > 0) applyLipstick(landmarks, state.lipstick.color, state.lipstick.opacity); if (state.blush?.opacity > 0) applyBlush(landmarks, state.blush.opacity); if (state.eyeshadow?.opacity > 0) applyEyeshadow(landmarks, state.eyeshadow.colors, state.eyeshadow.opacity); if (state.eyeliner?.opacity > 0) applyEyeliner(landmarks, state.eyeliner.opacity); if (state.mascara?.opacity > 0) applyMascara(landmarks, state.mascara.opacity); makeupCtx.restore(); }
206
+ function applyFoundation(landmarks, color, opacity) { const faceOvalPoints = getLandmarksByIndices(landmarks, LANDMARKS.FACE_OVAL); if (faceOvalPoints.length < 3) return; const path = createPathFromPoints(faceOvalPoints, makeupCanvas.width, makeupCanvas.height); makeupCtx.fillStyle = hexToRgba(color, opacity * 0.85); makeupCtx.fill(path); }
207
+ function applyLipstick(landmarks, color, opacity) { const outerUpperPoints = getLandmarksByIndices(landmarks, LANDMARKS.LIPS_OUTER_UPPER); const outerLowerPoints = getLandmarksByIndices(landmarks, LANDMARKS.LIPS_OUTER_LOWER); const innerUpperPoints = getLandmarksByIndices(landmarks, LANDMARKS.LIPS_INNER_UPPER); const innerLowerPoints = getLandmarksByIndices(landmarks, LANDMARKS.LIPS_INNER_LOWER); if (outerUpperPoints.length < 2 || outerLowerPoints.length < 2 || innerUpperPoints.length < 2 || innerLowerPoints.length < 2) return; const rgbaColor = hexToRgba(color, opacity); makeupCtx.fillStyle = rgbaColor; const upperLipPath = new Path2D(); drawPointsSmoothly(upperLipPath, outerUpperPoints, true, makeupCanvas.width, makeupCanvas.height); drawPointsSmoothly(upperLipPath, innerUpperPoints.slice().reverse(), false, makeupCanvas.width, makeupCanvas.height); upperLipPath.closePath(); makeupCtx.fill(upperLipPath); const lowerLipPath = new Path2D(); drawPointsSmoothly(lowerLipPath, outerLowerPoints, true, makeupCanvas.width, makeupCanvas.height); drawPointsSmoothly(lowerLipPath, innerLowerPoints.slice().reverse(), false, makeupCanvas.width, makeupCanvas.height); lowerLipPath.closePath(); makeupCtx.fill(lowerLipPath); if (opacity > 0.3) { const upperLipCenter = landmarks[13]; const lowerLipCenter = landmarks[14]; if (upperLipCenter && lowerLipCenter && outerUpperPoints[5] && innerUpperPoints[5] && outerLowerPoints[4] && innerLowerPoints[4]) { const upperShineRadius = Math.abs(outerUpperPoints[5].y - innerUpperPoints[5].y) * makeupCanvas.height * 0.3; const lowerShineRadius = Math.abs(outerLowerPoints[4].y - innerLowerPoints[4].y) * makeupCanvas.height * 0.4; applyRadialGradient(upperLipCenter, upperShineRadius, `rgba(255, 255, 255, ${opacity * 0.3})`, makeupCanvas.width, makeupCanvas.height); applyRadialGradient(lowerLipCenter, lowerShineRadius, `rgba(255, 255, 255, ${opacity * 0.4})`, makeupCanvas.width, makeupCanvas.height); } } }
208
+ function applyBlush(landmarks, opacity) { const leftCheekPoints = getLandmarksByIndices(landmarks, LANDMARKS.LEFT_CHEEK_AREA); const rightCheekPoints = getLandmarksByIndices(landmarks, LANDMARKS.RIGHT_CHEEK_AREA); if (leftCheekPoints.length < 3 || rightCheekPoints.length < 3) return; const leftCheekCenter = calculateCenter(leftCheekPoints); const rightCheekCenter = calculateCenter(rightCheekPoints); const leftRadius = Math.hypot((leftCheekCenter.x - leftCheekPoints[0].x) * makeupCanvas.width, (leftCheekCenter.y - leftCheekPoints[0].y) * makeupCanvas.height) * 1.2; const rightRadius = Math.hypot((rightCheekCenter.x - rightCheekPoints[0].x) * makeupCanvas.width, (rightCheekCenter.y - rightCheekPoints[0].y) * makeupCanvas.height) * 1.2; const blushColor = `rgba(255, 130, 150, ${opacity * 0.6})`; const drawBlushGradient = (center, radius) => { if (radius <= 0) return; const gradient = makeupCtx.createRadialGradient(center.x * makeupCanvas.width, center.y * makeupCanvas.height, radius * 0.1, center.x * makeupCanvas.width, center.y * makeupCanvas.height, radius); gradient.addColorStop(0, blushColor); gradient.addColorStop(1, `rgba(255, 130, 150, 0)`); makeupCtx.fillStyle = gradient; makeupCtx.beginPath(); makeupCtx.arc(center.x * makeupCanvas.width, center.y * makeupCanvas.height, radius, 0, Math.PI * 2); makeupCtx.fill(); }; drawBlushGradient(leftCheekCenter, leftRadius); drawBlushGradient(rightCheekCenter, rightRadius); }
209
+ function applyEyeshadow(landmarks, colors, opacity) { applySingleEyeShadow(landmarks, true, colors, opacity); applySingleEyeShadow(landmarks, false, colors, opacity); }
210
+ function applySingleEyeShadow(landmarks, isLeftEye, colors, opacity) { const upperLidPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYE_UPPER_LID0 : LANDMARKS.RIGHT_EYE_UPPER_LID0); const eyebrowPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYEBROW : LANDMARKS.RIGHT_EYEBROW); if (upperLidPoints.length < 2 || eyebrowPoints.length < 2) return; const path = new Path2D(); drawPointsSmoothly(path, upperLidPoints, true, makeupCanvas.width, makeupCanvas.height); drawPointsSmoothly(path, eyebrowPoints.slice().reverse(), false, makeupCanvas.width, makeupCanvas.height); path.closePath(); let minY = Infinity, maxY = -Infinity; [...upperLidPoints, ...eyebrowPoints].forEach(p => { minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); }); if (minY === Infinity || maxY === -Infinity) return; const gradient = makeupCtx.createLinearGradient(0, minY * makeupCanvas.height, 0, maxY * makeupCanvas.height); const numColors = colors.length; colors.forEach((color, index) => { gradient.addColorStop(index / (numColors - 1 || 1), hexToRgba(color, opacity)); }); makeupCtx.fillStyle = gradient; makeupCtx.fill(path); }
211
+ function applyEyeliner(landmarks, opacity) { applySingleEyeLiner(landmarks, true, opacity); applySingleEyeLiner(landmarks, false, opacity); }
212
+ function applySingleEyeLiner(landmarks, isLeftEye, opacity) { const upperLidPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYE_UPPER_LID1 : LANDMARKS.RIGHT_EYE_UPPER_LID1); const lowerLidPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYE_LOWER_LID : LANDMARKS.RIGHT_EYE_LOWER_LID); if (upperLidPoints.length < 2 || lowerLidPoints.length < 2) return; makeupCtx.strokeStyle = `rgba(30, 30, 30, ${opacity})`; makeupCtx.lineWidth = 1 + (opacity * 2.5); makeupCtx.lineJoin = 'round'; makeupCtx.lineCap = 'round'; makeupCtx.beginPath(); drawPointsSmoothly(makeupCtx, upperLidPoints, true, makeupCanvas.width, makeupCanvas.height); const outerCorner = upperLidPoints[upperLidPoints.length - 1]; const controlPoint = upperLidPoints[upperLidPoints.length - 2]; if (outerCorner && controlPoint) { const wingLength = 12 + opacity * 8; const angle = Math.atan2(outerCorner.y - controlPoint.y, outerCorner.x - controlPoint.x); const wingAngleOffset = isLeftEye ? -0.35 : 0.35; const wingX = outerCorner.x * makeupCanvas.width + Math.cos(angle + wingAngleOffset) * wingLength; const wingY = outerCorner.y * makeupCanvas.height + Math.sin(angle + wingAngleOffset) * wingLength; makeupCtx.quadraticCurveTo(outerCorner.x * makeupCanvas.width + Math.cos(angle) * wingLength * 0.5, outerCorner.y * makeupCanvas.height + Math.sin(angle) * wingLength * 0.5, wingX, wingY); } makeupCtx.stroke(); makeupCtx.lineWidth = 1 + (opacity * 1.0); makeupCtx.strokeStyle = `rgba(50, 50, 50, ${opacity * 0.6})`; makeupCtx.beginPath(); const lowerMidIndex = Math.floor(lowerLidPoints.length / 2); const lowerOuterPoints = lowerLidPoints.slice(lowerMidIndex - 1); if (lowerOuterPoints.length > 1) { drawPointsSmoothly(makeupCtx, lowerOuterPoints, true, makeupCanvas.width, makeupCanvas.height); makeupCtx.stroke(); } }
213
+ function applyMascara(landmarks, opacity) { applySingleEyeMascara(landmarks, true, opacity); applySingleEyeMascara(landmarks, false, opacity); }
214
+ function applySingleEyeMascara(landmarks, isLeftEye, opacity) { const upperLashPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYE_UPPER_LID1 : LANDMARKS.RIGHT_EYE_UPPER_LID1); if (upperLashPoints.length < 2) return; makeupCtx.strokeStyle = `rgba(10, 10, 10, ${opacity * 0.9})`; const baseLashLength = 3 + opacity * 4; const lashWidth = 1 + opacity * 1.5; for (let i = 0; i < upperLashPoints.length - 1; i++) { const p1 = upperLashPoints[i]; const p2 = upperLashPoints[i + 1]; const midX = (p1.x + p2.x) / 2 * makeupCanvas.width; const midY = (p1.y + p2.y) / 2 * makeupCanvas.height; const dx = p2.x - p1.x; const dy = p2.y - p1.y; let nx = -dy; let ny = dx; if ((isLeftEye && ny > 0) || (!isLeftEye && ny > 0)) { nx *= -1; ny *= -1; } const len = Math.sqrt(nx * nx + ny * ny); if (len === 0) continue; nx /= len; ny /= len; const lashLength = baseLashLength * (0.8 + Math.random() * 0.4); makeupCtx.lineWidth = lashWidth * (0.8 + Math.random() * 0.4); makeupCtx.beginPath(); makeupCtx.moveTo(midX, midY); makeupCtx.lineTo(midX + nx * lashLength, midY + ny * lashLength); makeupCtx.stroke(); } }
215
+
216
+ // --- Helper Functions --- (getLandmarksByIndices, calculateCenter, etc. - Omitted for brevity)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  function getLandmarksByIndices(landmarks, indices) { return indices.map(index => landmarks[index]).filter(p => p); }
218
  function calculateCenter(points) { if (!points || points.length === 0) return { x: 0, y: 0 }; let sumX = 0, sumY = 0; points.forEach(p => { sumX += p.x; sumY += p.y; }); return { x: sumX / points.length, y: sumY / points.length }; }
219
  function hexToRgba(hex, alpha = 1) { if (!hex || typeof hex !== 'string') return `rgba(0,0,0,${alpha})`; hex = hex.replace('#', ''); let r = 0, g = 0, b = 0; if (hex.length === 3) { r = parseInt(hex[0] + hex[0], 16); g = parseInt(hex[1] + hex[1], 16); b = parseInt(hex[2] + hex[2], 16); } else if (hex.length === 6) { r = parseInt(hex[0] + hex[1], 16); g = parseInt(hex[2] + hex[3], 16); b = parseInt(hex[4] + hex[5], 16); } else { return `rgba(0,0,0,${alpha})`; } alpha = Math.max(0, Math.min(1, alpha)); return `rgba(${r}, ${g}, ${b}, ${alpha})`; }
 
221
  function drawPointsSmoothly(ctxOrPath, points, moveToStart = true, canvasWidth, canvasHeight) { if (!points || points.length < 2) return; const scaledPoints = points.map(p => ({ x: p.x * canvasWidth, y: p.y * canvasHeight })); if (moveToStart) { ctxOrPath.moveTo(scaledPoints[0].x, scaledPoints[0].y); } if (scaledPoints.length === 2) { ctxOrPath.lineTo(scaledPoints[1].x, scaledPoints[1].y); return; } for (let i = 0; i < scaledPoints.length - 1; i++) { const p0 = scaledPoints[i === 0 ? i : i - 1]; const p1 = scaledPoints[i]; const p2 = scaledPoints[i + 1]; const p3 = scaledPoints[i + 2 < scaledPoints.length ? i + 2 : i + 1]; const cp1x = p1.x + (p2.x - p0.x) / 6; const cp1y = p1.y + (p2.y - p0.y) / 6; const cp2x = p2.x - (p3.x - p1.x) / 6; const cp2y = p2.y - (p3.y - p1.y) / 6; ctxOrPath.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y); } }
222
  function applyRadialGradient(center, radius, color, canvasWidth, canvasHeight) { if (!center || radius <= 0) return; const gradient = makeupCtx.createRadialGradient(center.x * canvasWidth, center.y * canvasHeight, 0, center.x * canvasWidth, center.y * canvasHeight, radius); gradient.addColorStop(0, color); gradient.addColorStop(1, hexToRgba(color, 0)); makeupCtx.fillStyle = gradient; makeupCtx.beginPath(); makeupCtx.arc(center.x * canvasWidth, center.y * canvasHeight, radius, 0, Math.PI * 2); makeupCtx.fill(); }
223
 
 
224
 
225
+ // --- Camera and MediaPipe Control ---
226
+ function setLoadingIndicatorVisibility(isVisible, text = "Initializing...") { /* ... */ const textElement = loadingIndicator.querySelector('p'); if (textElement) textElement.textContent = text; if (isVisible) { loadingIndicator.classList.remove('hidden'); } else { if (!loadingIndicator.classList.contains('hidden')) { loadingIndicator.classList.add('hidden'); } } }
227
+ function showErrorMessage(message) { /* ... */ errorMessageContainer.textContent = message; errorMessageContainer.style.display = 'block'; clearTimeout(errorMessageContainer.timer); errorMessageContainer.timer = setTimeout(() => { errorMessageContainer.style.display = 'none'; }, 5000); }
 
 
 
 
 
 
 
 
 
228
 
 
229
  async function startCamera() {
230
  if (isCameraStarting || isCameraOn) return;
231
  isCameraStarting = true;
232
+ setLoadingIndicatorVisibility(true);
233
+ console.log("Attempting to start camera and MediaPipe...");
234
 
235
  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { showErrorMessage("Camera API not supported."); setLoadingIndicatorVisibility(false); isCameraStarting = false; return; }
236
  if (!window.isSecureContext) { showErrorMessage("Camera requires HTTPS/localhost."); setLoadingIndicatorVisibility(false); isCameraStarting = false; return; }
 
240
  const stream = await navigator.mediaDevices.getUserMedia(cameraConstraints);
241
  console.log("Camera stream obtained.");
242
  videoElement.srcObject = stream;
243
+ // videoElement.classList.remove('hidden'); // Keep hidden, draw on canvas
 
 
 
244
 
245
  videoElement.onloadedmetadata = () => {
246
+ console.log("Video metadata loaded. Setting up canvas and MediaPipe.");
247
+ setupCanvas(); // Setup canvas dimensions based on video
248
+ initializeMediaPipeCamera(); // Start MediaPipe processing
249
+ isCameraOn = true; // Set state only after MediaPipe starts
250
  isCameraStarting = false;
251
+ updateToggleIcon(); // Set to pause icon
252
+ startScreen.classList.add('hidden'); // Hide start screen
253
+ // Loading hidden by onResults callback or fallback timer
254
+ clearTimeout(loadingTimeout);
255
+ loadingTimeout = setTimeout(() => { setLoadingIndicatorVisibility(false); }, 5000);
 
256
  };
257
+ videoElement.onerror = (e) => { console.error("Video element error:", e); showErrorMessage("Error playing video stream."); stopCamera(); setLoadingIndicatorVisibility(false); isCameraStarting = false; };
258
 
259
  } catch (err) {
260
  console.error("Error starting camera:", err.name, err.message);
261
+ handleCameraError(err);
262
+ stopCamera(); // Clean up on error
263
+ setLoadingIndicatorVisibility(false);
264
+ isCameraStarting = false;
265
  }
266
  }
267
 
 
268
  function initializeMediaPipeCamera() {
269
+ if (mediaPipeCamera) { mediaPipeCamera.close(); } // Close previous instance if any
270
  console.log("Initializing MediaPipe Camera helper.");
271
  mediaPipeCamera = new Camera(videoElement, {
272
+ onFrame: async () => {
273
+ if (videoElement.readyState >= 2) { // Check if video frame is ready
274
+ await faceMesh.send({ image: videoElement });
275
+ }
276
+ },
277
+ width: videoElement.videoWidth, // Use actual video dimensions
278
+ height: videoElement.videoHeight
279
  });
280
  mediaPipeCamera.start();
281
  console.log("MediaPipe Camera processing started.");
282
  }
283
 
284
+ function handleCameraError(err) { /* ... */ let message = "Could not access camera."; switch (err.name) { case "NotAllowedError": message = "Permission denied. Please allow camera access."; break; case "NotFoundError": message = "No camera found. Ensure it's connected."; break; case "NotReadableError": message = "Camera is busy or hardware error."; break; case "OverconstrainedError": message = `Camera doesn't support ${cameraConstraints.video.width.ideal}x${cameraConstraints.video.height.ideal}.`; break; case "SecurityError": message = "Camera access denied (security)."; break; case "TypeError": message = "Invalid camera constraints."; break; default: message = `Unknown camera error: ${err.name}`; break; } showErrorMessage(message); startScreen.classList.remove('hidden'); } // Don't hide video element as it's not shown
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+
287
+ function stopCamera() {
288
+ console.log("Stopping camera and MediaPipe.");
289
+ clearTimeout(loadingTimeout);
290
  if (mediaPipeCamera) { mediaPipeCamera.close(); mediaPipeCamera = null; }
291
  if (videoElement.srcObject) { videoElement.srcObject.getTracks().forEach(track => track.stop()); videoElement.srcObject = null; }
 
292
  isCameraOn = false; isCameraStarting = false; faceDetected = false;
293
  makeupCtx.clearRect(0, 0, makeupCanvas.width, makeupCanvas.height);
294
+ canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); // Clear video canvas too
295
+ updateToggleIcon(); setLoadingIndicatorVisibility(false);
296
+ startScreen.classList.remove('hidden'); // Show start screen again
297
  }
298
 
 
299
  function toggleCamera() {
300
+ // This button now acts as Start/Stop
301
+ if (isCameraOn) {
302
+ stopCamera();
303
+ } else {
304
+ startCamera();
305
+ }
306
  }
307
 
 
308
  function updateToggleIcon() {
309
+ // Icon represents Start/Stop state
310
+ if (isCameraOn) {
311
+ toggleCameraIcon.className = 'fas fa-stop text-lg';
312
+ toggleCameraBtn.title = "Stop Camera";
313
+ } else {
314
+ toggleCameraIcon.className = 'fas fa-play text-lg';
315
+ toggleCameraBtn.title = "Start Camera";
316
+ }
317
  }
318
 
 
319
  async function flipCamera() {
320
  if (isCameraStarting) return;
321
  console.log("Attempting to flip camera...");
322
  currentFacingMode = (currentFacingMode === "user") ? "environment" : "user";
323
  console.log("New facing mode:", currentFacingMode);
324
+ // Stop completely before restarting
325
+ const wasOn = isCameraOn; // Remember if camera was running
326
+ stopCamera();
327
+ // Only restart if it was running before flipping
328
+ if (wasOn) {
329
+ await startCamera();
330
+ }
331
  }
332
 
333
+ function captureImage() { /* ... same capture logic ... */ if (!isCameraOn) { showErrorMessage("Camera is paused. Resume to capture."); return; } if (!faceDetected && !showLandmarks) { showErrorMessage("No face detected to capture!"); return; } const tempCanvas = document.createElement('canvas'); tempCanvas.width = canvasElement.clientWidth; tempCanvas.height = canvasElement.clientHeight; const tempCtx = tempCanvas.getContext('2d'); tempCtx.save(); if (currentFacingMode === "user") { tempCtx.scale(-1, 1); tempCtx.translate(-tempCanvas.width, 0); } tempCtx.drawImage(canvasElement, 0, 0, tempCanvas.width, tempCanvas.height); tempCtx.restore(); tempCtx.save(); if (currentFacingMode === "user") { tempCtx.scale(-1, 1); tempCtx.translate(-tempCanvas.width, 0); } tempCtx.drawImage(makeupCanvas, 0, 0, tempCanvas.width, tempCanvas.height); tempCtx.restore(); const link = document.createElement('a'); link.download = `virtual-makeup-${Date.now()}.png`; link.href = tempCanvas.toDataURL('image/png'); link.click(); console.log("Image captured."); }
334
+ function resetMakeup() { /* ... same reset logic ... */ console.log("Resetting makeup."); initializeMakeupState(); updateSlidersFromState(); updateColorSelectionUI(); updatePaletteSelectionUI(); updateLookSelectionUI(); }
335
+ function toggleLandmarks() { /* ... same landmarks toggle logic ... */ showLandmarks = !showLandmarks; landmarksToggle.classList.toggle('bg-pink-100', showLandmarks); landmarksToggle.classList.toggle('text-pink-600', showLandmarks); landmarksToggle.title = showLandmarks ? "Hide Landmarks" : "Show Landmarks"; console.log("Landmarks toggled:", showLandmarks); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  function setupCanvas() {
338
  if (!videoElement.videoWidth || videoElement.videoWidth === 0) return;
339
+ const videoWidth = videoElement.videoWidth; const videoHeight = videoElement.videoHeight;
340
+ // Set canvas buffer size to match video resolution
341
+ canvasElement.width = makeupCanvas.width = videoWidth;
342
+ canvasElement.height = makeupCanvas.height = videoHeight;
343
+ // Set display size via CSS - already done with absolute positioning and w/h-full on parent
344
+ console.log(`Canvas buffer size set to: ${videoWidth}x${videoHeight}`);
345
  }
346
 
347
+ // --- State Management --- (initializeMakeupState, handleSliderChange - same as before)
348
+ function initializeMakeupState() { currentMakeupState = { lipstick: { color: DEFAULT_LIP_COLOR, opacity: DEFAULT_OPACITIES.lipstick }, blush: { opacity: DEFAULT_OPACITIES.blush }, eyeshadow: { colors: [...DEFAULT_EYESHADOW_COLORS], opacity: DEFAULT_OPACITIES.eyeshadow }, eyeliner: { opacity: DEFAULT_OPACITIES.eyeliner }, mascara: { opacity: DEFAULT_OPACITIES.mascara }, foundation: { color: DEFAULT_FOUNDATION_COLOR, opacity: DEFAULT_OPACITIES.foundation } }; console.log("Makeup state initialized."); }
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  function handleSliderChange(event) { const makeupType = event.target.name; const opacity = parseFloat(event.target.value) / 100; if (currentMakeupState[makeupType]) { currentMakeupState[makeupType].opacity = opacity; } else { console.warn(`Makeup type ${makeupType} not found.`); } updateLookSelectionUI(); }
 
 
350
 
351
+ // --- Event Handlers --- (handleSelection, applyQuickLook - same as before)
352
+ function handleSelection(targetElement) { if (!targetElement) return; const makeupType = targetElement.dataset.makeupType; const color = targetElement.dataset.color; const colors = targetElement.dataset.colors ? JSON.parse(targetElement.dataset.colors) : null; console.log(`Handling selection for: ${makeupType}`); if (makeupType === 'lipstick' && color) { currentMakeupState.lipstick.color = color; updateColorSelectionUI('lipstick', color); } else if (makeupType === 'foundation' && color) { currentMakeupState.foundation.color = color; updateColorSelectionUI('foundation', color); } else if (makeupType === 'eyeshadow' && colors) { currentMakeupState.eyeshadow.colors = colors; updatePaletteSelectionUI(targetElement.id); } updateLookSelectionUI(); console.log(`State updated: ${makeupType} color/colors set.`); }
353
+ function applyQuickLook(targetElement) { if (!targetElement) return; const lookName = targetElement.dataset.look; const lookData = QUICK_LOOKS[lookName]; if (!lookData) { console.error(`Look "${lookName}" not found.`); return; } console.log(`Applying look: ${lookName}`); for (const makeupType in lookData) { if (currentMakeupState[makeupType]) { if (makeupType === 'foundation' && !lookData.foundation.color) { lookData.foundation.color = currentMakeupState.foundation.color || DEFAULT_FOUNDATION_COLOR; } currentMakeupState[makeupType] = { ...currentMakeupState[makeupType], ...lookData[makeupType] }; } } updateSlidersFromState(); updateColorSelectionUI('lipstick', currentMakeupState.lipstick.color); updateColorSelectionUI('foundation', currentMakeupState.foundation.color); updatePaletteSelectionUI(null, currentMakeupState.eyeshadow.colors); updateLookSelectionUI(lookName); }
354
+
355
+ // --- UI Update Functions --- (updateSlidersFromState, updateColorSelectionUI, etc. - same as before)
356
  function updateSlidersFromState() { document.querySelectorAll('.slider-control input[type="range"]').forEach(slider => { const makeupType = slider.name; if (currentMakeupState[makeupType] && typeof currentMakeupState[makeupType].opacity === 'number') { slider.value = currentMakeupState[makeupType].opacity * 100; } }); }
357
  function updateColorSelectionUI(type, selectedColor) { document.querySelectorAll(`.color-swatch[data-makeup-type="${type}"], .foundation-swatch[data-makeup-type="${type}"]`).forEach(swatch => { swatch.classList.toggle('selected-color', swatch.dataset.color === selectedColor); }); }
358
  function updatePaletteSelectionUI(selectedId = null, selectedColors = null) { document.querySelectorAll('.eyeshadow-palette').forEach(palette => { let isSelected = false; if (selectedId) { isSelected = palette.id === selectedId; } else if (selectedColors) { const paletteColors = palette.dataset.colors ? JSON.parse(palette.dataset.colors) : null; isSelected = JSON.stringify(paletteColors) === JSON.stringify(selectedColors); } palette.classList.toggle('selected-item', isSelected); }); }
 
360
 
361
  // --- Event Listeners ---
362
  document.addEventListener('DOMContentLoaded', () => {
363
+ console.log("DOM ready. Initializing 2D Overlay App...");
364
  initializeMakeupState(); updateSlidersFromState(); updateColorSelectionUI('lipstick', currentMakeupState.lipstick.color); updateColorSelectionUI('foundation', currentMakeupState.foundation.color); updatePaletteSelectionUI(null, currentMakeupState.eyeshadow.colors);
365
  if (!window.isSecureContext && window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1") { showErrorMessage("Warning: Camera works best on HTTPS or localhost."); }
366
+
367
+ // Attach main control listeners
368
+ startBtn?.addEventListener('click', startCamera); // Use the dedicated start button
369
+ toggleCameraBtn?.addEventListener('click', toggleCamera); // This now Stops/Starts after initial start
370
+ flipCameraBtn?.addEventListener('click', flipCamera);
371
+ captureBtn?.addEventListener('click', captureImage);
372
+ resetBtn?.addEventListener('click', resetMakeup);
373
+ landmarksToggle?.addEventListener('click', toggleLandmarks);
374
+
375
+ // Attach listeners for sliders
376
+ document.querySelectorAll('.slider-control input[type="range"]').forEach(slider => {
377
+ slider.addEventListener('input', handleSliderChange);
378
+ });
379
+
380
+ // Attach delegated listener for controls column
381
+ controlsColumn?.addEventListener('click', (event) => {
382
+ const colorSwatchTarget = event.target.closest('.color-swatch, .foundation-swatch');
383
+ const paletteTarget = event.target.closest('.eyeshadow-palette');
384
+ const lookTarget = event.target.closest('.quick-look-btn');
385
+
386
+ if (colorSwatchTarget) { handleSelection(colorSwatchTarget); }
387
+ else if (paletteTarget) { handleSelection(paletteTarget); }
388
+ else if (lookTarget) { applyQuickLook(lookTarget); }
389
+ });
390
+
391
+ window.addEventListener('resize', setupCanvas); // Re-enable resize handling for canvas
392
+ console.log("Initialization complete. Ready for camera start.");
393
  }); // End DOMContentLoaded
394
 
395
  </script>