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">© 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>
|