File size: 53,098 Bytes
ded633a
 
 
 
 
55eeb67
 
 
c27ecfd
 
 
 
ded633a
55eeb67
c27ecfd
55eeb67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ded633a
55eeb67
 
 
c27ecfd
55eeb67
c27ecfd
55eeb67
 
 
c27ecfd
 
55eeb67
 
 
ded633a
 
c27ecfd
 
 
 
 
ded633a
c27ecfd
ded633a
 
55eeb67
 
ded633a
 
 
c27ecfd
ded633a
c27ecfd
 
ded633a
 
 
c27ecfd
 
 
 
55eeb67
 
ded633a
 
c27ecfd
 
55eeb67
 
c27ecfd
55eeb67
 
c27ecfd
ded633a
c27ecfd
 
 
 
ded633a
 
55eeb67
ded633a
c27ecfd
 
 
 
55eeb67
 
 
 
 
 
ded633a
 
 
c27ecfd
55eeb67
 
 
 
 
 
ded633a
 
 
 
c27ecfd
55eeb67
ded633a
 
c27ecfd
 
 
 
ded633a
55eeb67
ded633a
55eeb67
ded633a
 
 
 
c27ecfd
ded633a
 
 
 
c27ecfd
 
55eeb67
c27ecfd
 
55eeb67
 
ded633a
 
55eeb67
 
 
 
 
 
 
 
 
 
 
 
c27ecfd
 
55eeb67
 
c27ecfd
 
ded633a
55eeb67
c27ecfd
55eeb67
ded633a
 
55eeb67
c27ecfd
 
 
 
 
 
55eeb67
ded633a
 
 
55eeb67
 
ded633a
55eeb67
6902e02
55eeb67
 
ded633a
55eeb67
ded633a
 
 
 
55eeb67
6902e02
ded633a
55eeb67
ded633a
c27ecfd
55eeb67
 
 
 
 
 
 
 
 
 
 
 
 
 
6902e02
 
 
 
 
 
c27ecfd
 
55eeb67
 
 
c27ecfd
ded633a
6902e02
c27ecfd
55eeb67
 
c27ecfd
6902e02
 
c27ecfd
ded633a
6902e02
c27ecfd
 
ded633a
55eeb67
c27ecfd
 
55eeb67
 
 
 
c27ecfd
55eeb67
 
 
 
 
c27ecfd
55eeb67
c27ecfd
ded633a
c27ecfd
55eeb67
 
 
 
ded633a
 
c27ecfd
 
55eeb67
6902e02
 
55eeb67
 
 
 
 
 
 
6902e02
 
 
c27ecfd
 
55eeb67
c27ecfd
55eeb67
 
 
 
6902e02
 
 
 
55eeb67
 
 
c27ecfd
 
ded633a
55eeb67
 
 
 
 
 
ded633a
c27ecfd
 
55eeb67
 
 
 
 
 
 
 
c27ecfd
 
 
6902e02
c27ecfd
 
 
55eeb67
 
 
 
 
 
 
c27ecfd
 
55eeb67
 
 
c27ecfd
ded633a
6902e02
55eeb67
 
 
 
 
 
c27ecfd
 
55eeb67
 
6902e02
c27ecfd
55eeb67
 
 
 
 
6902e02
 
 
 
c27ecfd
 
 
55eeb67
6902e02
 
55eeb67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c27ecfd
 
ded633a
c27ecfd
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Virtual Makeup Try-On (2D Overlay)</title>
    <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>
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js" crossorigin="anonymous"></script>
    <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" />
    <style>
        body { font-family: 'Inter', sans-serif; }
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
        /* Styles (same as final 2D version) */
        .makeup-item, .color-swatch, .foundation-swatch { transition: all 0.2s ease-in-out; cursor: pointer; }
        .makeup-item:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); }
        .color-swatch:hover, .foundation-swatch:hover { transform: scale(1.1); }
        .selected-item { @apply ring-2 ring-offset-2 ring-pink-500; }
        .selected-color { outline: 2px solid #ec4899; outline-offset: 2px; }
        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; }
        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; }
        @keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.9; } }
        .pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        .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; }

        /* Canvas container and overlay styling */
        .video-feed { position: relative; overflow: hidden; background-color: #e0e0e0; }
        #output { /* Canvas for video frame + landmarks */
            position: absolute;
            top: 0; left: 0;
            width: 100%; height: 100%;
            object-fit: cover; /* Ensure canvas content covers area */
        }
        .makeup-overlay { /* Canvas for makeup */
            position: absolute;
            left: 0; top: 0;
            width: 100%; height: 100%;
            pointer-events: none;
        }

        #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; }
        #loading-indicator.hidden { opacity: 0; pointer-events: none; }
        #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%; }
    </style>
</head>
<body class="bg-gradient-to-br from-pink-50 to-purple-50 min-h-screen font-sans text-gray-800">
    <header class="bg-white shadow-sm sticky top-0 z-40">
        <div class="container mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
            <div class="flex items-center space-x-3">
                <i class="fas fa-wand-magic-sparkles text-3xl text-pink-500"></i>
                <h1 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-pink-500 to-purple-600">
                    GlamAI Try-On
                </h1>
            </div>
            <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>
            <button class="md:hidden text-gray-600 hover:text-pink-500"> <i class="fas fa-bars text-2xl"></i> </button>
        </div>
    </header>

    <main class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
        <div class="text-center mb-12">
            <h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-3">Virtual Makeup Preview</h2>
            <p class="text-gray-600 max-w-2xl mx-auto text-base md:text-lg">See how different looks appear in real-time using your camera.</p>
        </div>

        <div class="flex flex-col lg:flex-row gap-8">
            <div class="lg:w-2/3 bg-white p-6 rounded-xl shadow-lg">
                <div class="flex justify-between items-center mb-4">
                    <h3 class="text-xl font-semibold text-gray-900">Live Camera Feed</h3>
                    <div class="flex space-x-3">
                        <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>
                        <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>
                    </div>
                </div>

                <div class="relative overflow-hidden rounded-xl bg-gray-200 aspect-video flex justify-center items-center video-feed mb-6">
                    <video id="video" autoplay playsinline muted class="hidden"></video>
                    <canvas id="output"></canvas>
                    <canvas id="makeup-layer" class="makeup-overlay"></canvas>
                    <div id="start-screen" class="absolute inset-0 flex flex-col justify-center items-center bg-gray-200 rounded-xl z-20">
                         <div class="bg-pink-100 inline-block p-5 rounded-full mb-5 pulse">
                            <i class="fas fa-camera-retro text-4xl text-pink-500"></i>
                        </div>
                        <h4 class="text-xl font-semibold text-gray-800 mb-2">Ready to Try?</h4>
                        <p class="text-gray-600 mb-5">Allow camera access to start the virtual try-on.</p>
                        <button id="start-btn" class="bg-gradient-to-r from-pink-500 to-purple-600 text-white px-8 py-3 rounded-full hover:from-pink-600 hover:to-purple-700 transition duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5">
                            <i class="fas fa-play mr-2"></i>Start Camera
                        </button>
                    </div>
                    <div id="loading-indicator" class="hidden z-10"> <div class="spinner"></div> <p class="ml-3 text-gray-600">Initializing...</p> </div>
                </div>

                <div>
                    <h3 class="text-xl font-semibold text-gray-900 mb-4">Adjust Intensity</h3>
                    <div class="grid grid-cols-2 md:grid-cols-3 gap-x-6 gap-y-4">
                         <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>
                         <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>
                         <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>
                         <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>
                         <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>
                         <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>
                    </div>
                </div>
            </div>

            <div id="controls-column" class="lg:w-1/3 space-y-6">
                <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>
                 <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>
                 <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>
                 <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>
                 <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>
            </div>
        </div>
    </main>

    <footer class="bg-gray-800 text-white py-10 mt-16">
        <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>
    </footer>

    <div id="error-message-container"></div>

    <script type="module">
        // --- DOM Element References ---
        const videoElement = document.getElementById('video');
        const canvasElement = document.getElementById('output'); // For video frame + landmarks
        const canvasCtx = canvasElement.getContext('2d');
        const makeupCanvas = document.getElementById('makeup-layer'); // For makeup overlay
        const makeupCtx = makeupCanvas.getContext('2d');
        const startScreen = document.getElementById('start-screen');
        const startBtn = document.getElementById('start-btn');
        const toggleCameraBtn = document.getElementById('toggle-camera');
        const toggleCameraIcon = document.getElementById('toggle-camera-icon');
        const flipCameraBtn = document.getElementById('flip-camera');
        const captureBtn = document.getElementById('capture-btn');
        const resetBtn = document.getElementById('reset-btn');
        const landmarksToggle = document.getElementById('landmarks-toggle');
        const errorMessageContainer = document.getElementById('error-message-container');
        const loadingIndicator = document.getElementById('loading-indicator');
        const controlsColumn = document.getElementById('controls-column'); // Parent for delegated events

        // --- State Variables ---
        let isCameraOn = false; // Tracks if MediaPipe processing loop is active
        let isCameraStarting = false;
        let showLandmarks = false;
        let faceDetected = false;
        let mediaPipeCamera = null; // MediaPipe Camera helper instance
        let currentMakeupState = {};
        let loadingTimeout = null;
        let currentFacingMode = "user";

        // --- Constants --- (Defaults, Landmarks, Looks - same as before)
        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 };
        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] };
        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 } } };

        // --- Camera Constraints ---
        const cameraConstraints = { video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: currentFacingMode } };

        // --- MediaPipe Face Mesh Initialization ---
        const faceMesh = new FaceMesh({ locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}` });
        faceMesh.setOptions({ maxNumFaces: 1, refineLandmarks: true, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5 });

        // --- Face Mesh Results Callback ---
        faceMesh.onResults((results) => {
            setLoadingIndicatorVisibility(false); clearTimeout(loadingTimeout); // Hide loader

            // --- Draw video frame and landmarks on output canvas ---
            canvasCtx.save();
            canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
             // Flip horizontally for front camera before drawing video
            if (currentFacingMode === "user") {
                canvasCtx.scale(-1, 1);
                canvasCtx.translate(-canvasElement.width, 0);
            }
            canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);

            // --- Process and Draw Makeup ---
            if (results.multiFaceLandmarks && results.multiFaceLandmarks.length > 0) {
                faceDetected = true;
                const landmarks = results.multiFaceLandmarks[0];

                // Clear previous makeup before drawing new frame
                makeupCtx.clearRect(0, 0, makeupCanvas.width, makeupCanvas.height);
                // Apply makeup effects to the makeup canvas
                applyMakeup(landmarks);

                // Draw landmarks on top if toggled
                if (showLandmarks) {
                    // Draw landmarks on the main canvas (already transformed if needed)
                    drawLandmarks(results.multiFaceLandmarks);
                }
            } else {
                faceDetected = false;
                // Clear makeup canvas if no face detected
                makeupCtx.clearRect(0, 0, makeupCanvas.width, makeupCanvas.height);
            }
            canvasCtx.restore(); // Restore context of output canvas
        });

        // --- Drawing Functions --- (applyMakeup, applyFoundation, etc. - Omitted for brevity, same as before)
        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 }); } }
        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(); }
        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); }
        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); } } }
        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); }
        function applyEyeshadow(landmarks, colors, opacity) { applySingleEyeShadow(landmarks, true, colors, opacity); applySingleEyeShadow(landmarks, false, colors, opacity); }
        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); }
        function applyEyeliner(landmarks, opacity) { applySingleEyeLiner(landmarks, true, opacity); applySingleEyeLiner(landmarks, false, opacity); }
        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(); } }
        function applyMascara(landmarks, opacity) { applySingleEyeMascara(landmarks, true, opacity); applySingleEyeMascara(landmarks, false, opacity); }
        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(); } }

        // --- Helper Functions --- (getLandmarksByIndices, calculateCenter, etc. - Omitted for brevity)
        function getLandmarksByIndices(landmarks, indices) { return indices.map(index => landmarks[index]).filter(p => p); }
        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 }; }
        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})`; }
        function createPathFromPoints(points, canvasWidth, canvasHeight) { const path = new Path2D(); if (points.length > 0) { path.moveTo(points[0].x * canvasWidth, points[0].y * canvasHeight); for (let i = 1; i < points.length; i++) { path.lineTo(points[i].x * canvasWidth, points[i].y * canvasHeight); } path.closePath(); } return path; }
        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); } }
        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(); }


        // --- Camera and MediaPipe Control ---
        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'); } } }
        function showErrorMessage(message) { /* ... */ errorMessageContainer.textContent = message; errorMessageContainer.style.display = 'block'; clearTimeout(errorMessageContainer.timer); errorMessageContainer.timer = setTimeout(() => { errorMessageContainer.style.display = 'none'; }, 5000); }

        async function startCamera() {
            if (isCameraStarting || isCameraOn) return;
            isCameraStarting = true;
            setLoadingIndicatorVisibility(true);
            console.log("Attempting to start camera and MediaPipe...");

            if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { showErrorMessage("Camera API not supported."); setLoadingIndicatorVisibility(false); isCameraStarting = false; return; }
            if (!window.isSecureContext) { showErrorMessage("Camera requires HTTPS/localhost."); setLoadingIndicatorVisibility(false); isCameraStarting = false; return; }

            try {
                cameraConstraints.video.facingMode = currentFacingMode;
                const stream = await navigator.mediaDevices.getUserMedia(cameraConstraints);
                console.log("Camera stream obtained.");
                videoElement.srcObject = stream;
                // videoElement.classList.remove('hidden'); // Keep hidden, draw on canvas

                videoElement.onloadedmetadata = () => {
                    console.log("Video metadata loaded. Setting up canvas and MediaPipe.");
                    setupCanvas(); // Setup canvas dimensions based on video
                    initializeMediaPipeCamera(); // Start MediaPipe processing
                    isCameraOn = true; // Set state only after MediaPipe starts
                    isCameraStarting = false;
                    updateToggleIcon(); // Set to pause icon
                    startScreen.classList.add('hidden'); // Hide start screen
                    // Loading hidden by onResults callback or fallback timer
                    clearTimeout(loadingTimeout);
                    loadingTimeout = setTimeout(() => { setLoadingIndicatorVisibility(false); }, 5000);
                };
                 videoElement.onerror = (e) => { console.error("Video element error:", e); showErrorMessage("Error playing video stream."); stopCamera(); setLoadingIndicatorVisibility(false); isCameraStarting = false; };

            } catch (err) {
                console.error("Error starting camera:", err.name, err.message);
                handleCameraError(err);
                stopCamera(); // Clean up on error
                setLoadingIndicatorVisibility(false);
                isCameraStarting = false;
            }
        }

        function initializeMediaPipeCamera() {
            if (mediaPipeCamera) { mediaPipeCamera.close(); } // Close previous instance if any
            console.log("Initializing MediaPipe Camera helper.");
            mediaPipeCamera = new Camera(videoElement, {
                onFrame: async () => {
                    if (videoElement.readyState >= 2) { // Check if video frame is ready
                        await faceMesh.send({ image: videoElement });
                    }
                },
                width: videoElement.videoWidth, // Use actual video dimensions
                height: videoElement.videoHeight
            });
            mediaPipeCamera.start();
            console.log("MediaPipe Camera processing started.");
        }

        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


        function stopCamera() {
            console.log("Stopping camera and MediaPipe.");
            clearTimeout(loadingTimeout);
            if (mediaPipeCamera) { mediaPipeCamera.close(); mediaPipeCamera = null; }
            if (videoElement.srcObject) { videoElement.srcObject.getTracks().forEach(track => track.stop()); videoElement.srcObject = null; }
            isCameraOn = false; isCameraStarting = false; faceDetected = false;
            makeupCtx.clearRect(0, 0, makeupCanvas.width, makeupCanvas.height);
            canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); // Clear video canvas too
            updateToggleIcon(); setLoadingIndicatorVisibility(false);
            startScreen.classList.remove('hidden'); // Show start screen again
        }

        function toggleCamera() {
             // This button now acts as Start/Stop
            if (isCameraOn) {
                stopCamera();
            } else {
                startCamera();
            }
        }

        function updateToggleIcon() {
             // Icon represents Start/Stop state
             if (isCameraOn) {
                 toggleCameraIcon.className = 'fas fa-stop text-lg';
                 toggleCameraBtn.title = "Stop Camera";
             } else {
                 toggleCameraIcon.className = 'fas fa-play text-lg';
                 toggleCameraBtn.title = "Start Camera";
             }
        }

        async function flipCamera() {
            if (isCameraStarting) return;
            console.log("Attempting to flip camera...");
            currentFacingMode = (currentFacingMode === "user") ? "environment" : "user";
            console.log("New facing mode:", currentFacingMode);
            // Stop completely before restarting
            const wasOn = isCameraOn; // Remember if camera was running
            stopCamera();
             // Only restart if it was running before flipping
             if (wasOn) {
                 await startCamera();
             }
        }

        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."); }
        function resetMakeup() { /* ... same reset logic ... */ console.log("Resetting makeup."); initializeMakeupState(); updateSlidersFromState(); updateColorSelectionUI(); updatePaletteSelectionUI(); updateLookSelectionUI(); }
        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); }

        function setupCanvas() {
            if (!videoElement.videoWidth || videoElement.videoWidth === 0) return;
            const videoWidth = videoElement.videoWidth; const videoHeight = videoElement.videoHeight;
            // Set canvas buffer size to match video resolution
            canvasElement.width = makeupCanvas.width = videoWidth;
            canvasElement.height = makeupCanvas.height = videoHeight;
             // Set display size via CSS - already done with absolute positioning and w/h-full on parent
            console.log(`Canvas buffer size set to: ${videoWidth}x${videoHeight}`);
        }

        // --- State Management --- (initializeMakeupState, handleSliderChange - same as before)
        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."); }
        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(); }

        // --- Event Handlers --- (handleSelection, applyQuickLook - same as before)
        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.`); }
        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); }

        // --- UI Update Functions --- (updateSlidersFromState, updateColorSelectionUI, etc. - same as before)
        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; } }); }
        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); }); }
        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); }); }
        function updateLookSelectionUI(selectedLookName = null) { document.querySelectorAll('.quick-look-btn').forEach(btn => { btn.classList.toggle('selected-item', btn.dataset.look === selectedLookName); }); }

        // --- Event Listeners ---
        document.addEventListener('DOMContentLoaded', () => {
            console.log("DOM ready. Initializing 2D Overlay App...");
            initializeMakeupState(); updateSlidersFromState(); updateColorSelectionUI('lipstick', currentMakeupState.lipstick.color); updateColorSelectionUI('foundation', currentMakeupState.foundation.color); updatePaletteSelectionUI(null, currentMakeupState.eyeshadow.colors);
            if (!window.isSecureContext && window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1") { showErrorMessage("Warning: Camera works best on HTTPS or localhost."); }

            // Attach main control listeners
            startBtn?.addEventListener('click', startCamera); // Use the dedicated start button
            toggleCameraBtn?.addEventListener('click', toggleCamera); // This now Stops/Starts after initial start
            flipCameraBtn?.addEventListener('click', flipCamera);
            captureBtn?.addEventListener('click', captureImage);
            resetBtn?.addEventListener('click', resetMakeup);
            landmarksToggle?.addEventListener('click', toggleLandmarks);

            // Attach listeners for sliders
            document.querySelectorAll('.slider-control input[type="range"]').forEach(slider => {
                slider.addEventListener('input', handleSliderChange);
            });

            // Attach delegated listener for controls column
            controlsColumn?.addEventListener('click', (event) => {
                const colorSwatchTarget = event.target.closest('.color-swatch, .foundation-swatch');
                const paletteTarget = event.target.closest('.eyeshadow-palette');
                const lookTarget = event.target.closest('.quick-look-btn');

                if (colorSwatchTarget) { handleSelection(colorSwatchTarget); }
                else if (paletteTarget) { handleSelection(paletteTarget); }
                else if (lookTarget) { applyQuickLook(lookTarget); }
            });

            window.addEventListener('resize', setupCanvas); // Re-enable resize handling for canvas
            console.log("Initialization complete. Ready for camera start.");
        }); // End DOMContentLoaded

    </script>
</body>
</html>