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