test1 / index.html
farrahred's picture
Update index.html
55eeb67 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Virtual Makeup Try-On (2D Overlay)</title>
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
body { font-family: 'Inter', sans-serif; }
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
/* Styles (same as final 2D version) */
.makeup-item, .color-swatch, .foundation-swatch { transition: all 0.2s ease-in-out; cursor: pointer; }
.makeup-item:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); }
.color-swatch:hover, .foundation-swatch:hover { transform: scale(1.1); }
.selected-item { @apply ring-2 ring-offset-2 ring-pink-500; }
.selected-color { outline: 2px solid #ec4899; outline-offset: 2px; }
input[type="range"]::-webkit-slider-thumb { @apply h-5 w-5 bg-pink-500 rounded-full appearance-none cursor-pointer hover:bg-pink-600 transition-colors duration-150; }
input[type="range"]::-moz-range-thumb { @apply h-5 w-5 bg-pink-500 rounded-full cursor-pointer border-none hover:bg-pink-600 transition-colors duration-150; }
@keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.9; } }
.pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.spinner { border: 4px solid rgba(0, 0, 0, 0.1); width: 36px; height: 36px; border-radius: 50%; border-left-color: #ec4899; animation: spin 1s linear infinite; }
/* Canvas container and overlay styling */
.video-feed { position: relative; overflow: hidden; background-color: #e0e0e0; }
#output { /* Canvas for video frame + landmarks */
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
object-fit: cover; /* Ensure canvas content covers area */
}
.makeup-overlay { /* Canvas for makeup */
position: absolute;
left: 0; top: 0;
width: 100%; height: 100%;
pointer-events: none;
}
#loading-indicator { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(255, 255, 255, 0.7); display: flex; justify-content: center; align-items: center; z-index: 50; backdrop-filter: blur(4px); border-radius: 0.75rem; opacity: 1; transition: opacity 0.3s ease-out; }
#loading-indicator.hidden { opacity: 0; pointer-events: none; }
#error-message-container { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(220, 38, 38, 0.95); color: white; padding: 12px 24px; border-radius: 8px; z-index: 1000; font-size: 0.875rem; line-height: 1.25rem; box-shadow: 0 4px 10px rgba(0,0,0,0.2); display: none; text-align: center; max-width: 90%; }
</style>
</head>
<body class="bg-gradient-to-br from-pink-50 to-purple-50 min-h-screen font-sans text-gray-800">
<header class="bg-white shadow-sm sticky top-0 z-40">
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
<div class="flex items-center space-x-3">
<i class="fas fa-wand-magic-sparkles text-3xl text-pink-500"></i>
<h1 class="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-pink-500 to-purple-600">
GlamAI Try-On
</h1>
</div>
<div class="hidden md:flex items-center space-x-6"> <a href="#" class="text-gray-600 hover:text-pink-500 transition duration-150">Features</a> <a href="#" class="text-gray-600 hover:text-pink-500 transition duration-150">Looks</a> <a href="#" class="text-gray-600 hover:text-pink-500 transition duration-150">About</a> <button class="bg-pink-500 text-white px-5 py-2 rounded-full hover:bg-pink-600 transition duration-150 shadow hover:shadow-md"> Sign Up </button> </div>
<button class="md:hidden text-gray-600 hover:text-pink-500"> <i class="fas fa-bars text-2xl"></i> </button>
</div>
</header>
<main class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-3">Virtual Makeup Preview</h2>
<p class="text-gray-600 max-w-2xl mx-auto text-base md:text-lg">See how different looks appear in real-time using your camera.</p>
</div>
<div class="flex flex-col lg:flex-row gap-8">
<div class="lg:w-2/3 bg-white p-6 rounded-xl shadow-lg">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold text-gray-900">Live Camera Feed</h3>
<div class="flex space-x-3">
<button id="flip-camera" title="Flip Camera" class="bg-gray-100 p-2 rounded-full hover:bg-gray-200 text-gray-600 hover:text-pink-500 transition duration-150"> <i class="fas fa-sync-alt text-lg"></i> </button>
<button id="toggle-camera" title="Pause/Resume Camera" class="bg-pink-500 text-white w-10 h-10 flex items-center justify-center rounded-full hover:bg-pink-600 transition duration-150 shadow"> <i id="toggle-camera-icon" class="fas fa-pause text-lg"></i> </button>
</div>
</div>
<div class="relative overflow-hidden rounded-xl bg-gray-200 aspect-video flex justify-center items-center video-feed mb-6">
<video id="video" autoplay playsinline muted class="hidden"></video>
<canvas id="output"></canvas>
<canvas id="makeup-layer" class="makeup-overlay"></canvas>
<div id="start-screen" class="absolute inset-0 flex flex-col justify-center items-center bg-gray-200 rounded-xl z-20">
<div class="bg-pink-100 inline-block p-5 rounded-full mb-5 pulse">
<i class="fas fa-camera-retro text-4xl text-pink-500"></i>
</div>
<h4 class="text-xl font-semibold text-gray-800 mb-2">Ready to Try?</h4>
<p class="text-gray-600 mb-5">Allow camera access to start the virtual try-on.</p>
<button id="start-btn" class="bg-gradient-to-r from-pink-500 to-purple-600 text-white px-8 py-3 rounded-full hover:from-pink-600 hover:to-purple-700 transition duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5">
<i class="fas fa-play mr-2"></i>Start Camera
</button>
</div>
<div id="loading-indicator" class="hidden z-10"> <div class="spinner"></div> <p class="ml-3 text-gray-600">Initializing...</p> </div>
</div>
<div>
<h3 class="text-xl font-semibold text-gray-900 mb-4">Adjust Intensity</h3>
<div class="grid grid-cols-2 md:grid-cols-3 gap-x-6 gap-y-4">
<div class="slider-control"> <label for="lipstick-opacity" class="block text-sm font-medium text-gray-700 mb-1">Lipstick</label> <input type="range" id="lipstick-opacity" name="lipstick" min="0" max="100" value="70" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> </div>
<div class="slider-control"> <label for="blush-opacity" class="block text-sm font-medium text-gray-700 mb-1">Blush</label> <input type="range" id="blush-opacity" name="blush" min="0" max="100" value="50" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> </div>
<div class="slider-control"> <label for="eyeshadow-opacity" class="block text-sm font-medium text-gray-700 mb-1">Eyeshadow</label> <input type="range" id="eyeshadow-opacity" name="eyeshadow" min="0" max="100" value="60" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> </div>
<div class="slider-control"> <label for="eyeliner-opacity" class="block text-sm font-medium text-gray-700 mb-1">Eyeliner</label> <input type="range" id="eyeliner-opacity" name="eyeliner" min="0" max="100" value="80" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> </div>
<div class="slider-control"> <label for="mascara-opacity" class="block text-sm font-medium text-gray-700 mb-1">Mascara</label> <input type="range" id="mascara-opacity" name="mascara" min="0" max="100" value="65" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> </div>
<div class="slider-control"> <label for="foundation-opacity" class="block text-sm font-medium text-gray-700 mb-1">Foundation</label> <input type="range" id="foundation-opacity" name="foundation" min="0" max="100" value="40" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> </div>
</div>
</div>
</div>
<div id="controls-column" class="lg:w-1/3 space-y-6">
<div class="bg-white p-6 rounded-xl shadow-lg"> <h3 class="text-xl font-semibold text-gray-900 mb-4">Quick Looks</h3> <div class="grid grid-cols-2 gap-4"> <button data-look="romantic" class="makeup-item quick-look-btn bg-gradient-to-br from-rose-100 to-pink-200 p-4 rounded-lg text-center transition transform hover:shadow-lg"> <i class="fas fa-heart text-3xl text-rose-500 mb-2"></i> <p class="font-medium text-sm text-gray-700">Romantic</p> </button> <button data-look="night" class="makeup-item quick-look-btn bg-gradient-to-br from-indigo-200 to-purple-300 p-4 rounded-lg text-center transition transform hover:shadow-lg"> <i class="fas fa-moon text-3xl text-indigo-600 mb-2"></i> <p class="font-medium text-sm text-gray-700">Night Out</p> </button> <button data-look="day" class="makeup-item quick-look-btn bg-gradient-to-br from-amber-100 to-orange-200 p-4 rounded-lg text-center transition transform hover:shadow-lg"> <i class="fas fa-sun text-3xl text-amber-500 mb-2"></i> <p class="font-medium text-sm text-gray-700">Daytime</p> </button> <button data-look="natural" class="makeup-item quick-look-btn bg-gradient-to-br from-green-100 to-teal-200 p-4 rounded-lg text-center transition transform hover:shadow-lg"> <i class="fas fa-leaf text-3xl text-green-600 mb-2"></i> <p class="font-medium text-sm text-gray-700">Natural</p> </button> </div> </div>
<div class="bg-white p-6 rounded-xl shadow-lg"> <h3 class="text-xl font-semibold text-gray-900 mb-4">Lipstick Color</h3> <div class="flex flex-wrap gap-3"> <div title="Classic Red" class="w-8 h-8 rounded-full bg-red-600 color-swatch" data-makeup-type="lipstick" data-color="#dc2626"></div> <div title="Hot Pink" class="w-8 h-8 rounded-full bg-pink-500 color-swatch" data-makeup-type="lipstick" data-color="#ec4899"></div> <div title="Soft Rose" class="w-8 h-8 rounded-full bg-rose-400 color-swatch" data-makeup-type="lipstick" data-color="#fb7185"></div> <div title="Deep Plum" class="w-8 h-8 rounded-full bg-purple-700 color-swatch" data-makeup-type="lipstick" data-color="#7e22ce"></div> <div title="Coral Peach" class="w-8 h-8 rounded-full bg-orange-400 color-swatch" data-makeup-type="lipstick" data-color="#fb923c"></div> <div title="Nude Brown" class="w-8 h-8 rounded-full bg-yellow-800 color-swatch" data-makeup-type="lipstick" data-color="#92400e"></div> <div title="Berry Wine" class="w-8 h-8 rounded-full bg-red-800 color-swatch" data-makeup-type="lipstick" data-color="#991b1b"></div> <div title="Natural Beige" class="w-8 h-8 rounded-full bg-orange-200 color-swatch" data-makeup-type="lipstick" data-color="#fed7aa"></div> </div> </div>
<div class="bg-white p-6 rounded-xl shadow-lg"> <h3 class="text-xl font-semibold text-gray-900 mb-4">Foundation Shade</h3> <div class="flex flex-wrap gap-3"> <div title="Fair" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#F8E8DB" style="background-color: #F8E8DB;"></div> <div title="Light" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#F5D7C1" style="background-color: #F5D7C1;"></div> <div title="Medium" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#E1B99A" style="background-color: #E1B99A;"></div> <div title="Tan" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#C19A70" style="background-color: #C19A70;"></div> <div title="Deep" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#8C5A3C" style="background-color: #8C5A3C;"></div> <div title="Rich" class="w-8 h-8 rounded-full foundation-swatch border border-gray-300" data-makeup-type="foundation" data-color="#5E3B2F" style="background-color: #5E3B2F;"></div> </div> </div>
<div class="bg-white p-6 rounded-xl shadow-lg"> <h3 class="text-xl font-semibold text-gray-900 mb-4">Eyeshadow Palette</h3> <div class="grid grid-cols-2 gap-4"> <div id="sunset-glow" data-makeup-type="eyeshadow" data-colors='["#FDE68A", "#FCA5A5", "#F87171"]' title="Sunset Glow Palette" class="makeup-item eyeshadow-palette p-3 rounded-lg border border-gray-200 hover:shadow-md transition"> <div class="flex space-x-1 mb-2 h-6"> <div class="flex-1 rounded-sm" style="background-color: #FDE68A;"></div> <div class="flex-1 rounded-sm" style="background-color: #FCA5A5;"></div> <div class="flex-1 rounded-sm" style="background-color: #F87171;"></div> </div> <p class="text-sm font-medium text-gray-700">Sunset Glow</p> </div> <div id="berry-nights" data-makeup-type="eyeshadow" data-colors='["#E9D5FF", "#C084FC", "#9333EA"]' title="Berry Nights Palette" class="makeup-item eyeshadow-palette p-3 rounded-lg border border-gray-200 hover:shadow-md transition"> <div class="flex space-x-1 mb-2 h-6"> <div class="flex-1 rounded-sm" style="background-color: #E9D5FF;"></div> <div class="flex-1 rounded-sm" style="background-color: #C084FC;"></div> <div class="flex-1 rounded-sm" style="background-color: #9333EA;"></div> </div> <p class="text-sm font-medium text-gray-700">Berry Nights</p> </div> <div id="smokey-eye" data-makeup-type="eyeshadow" data-colors='["#E5E7EB", "#9CA3AF", "#4B5563"]' title="Smokey Eye Palette" class="makeup-item eyeshadow-palette p-3 rounded-lg border border-gray-200 hover:shadow-md transition"> <div class="flex space-x-1 mb-2 h-6"> <div class="flex-1 rounded-sm" style="background-color: #E5E7EB;"></div> <div class="flex-1 rounded-sm" style="background-color: #9CA3AF;"></div> <div class="flex-1 rounded-sm" style="background-color: #4B5563;"></div> </div> <p class="text-sm font-medium text-gray-700">Smokey Eye</p> </div> <div id="earth-tones" data-makeup-type="eyeshadow" data-colors='["#FEF3C7", "#FCD34D", "#D97706"]' title="Earth Tones Palette" class="makeup-item eyeshadow-palette p-3 rounded-lg border border-gray-200 hover:shadow-md transition"> <div class="flex space-x-1 mb-2 h-6"> <div class="flex-1 rounded-sm" style="background-color: #FEF3C7;"></div> <div class="flex-1 rounded-sm" style="background-color: #FCD34D;"></div> <div class="flex-1 rounded-sm" style="background-color: #D97706;"></div> </div> <p class="text-sm font-medium text-gray-700">Earth Tones</p> </div> </div> </div>
<div class="bg-white p-6 rounded-xl shadow-lg"> <h3 class="text-xl font-semibold text-gray-900 mb-4">Tools</h3> <div class="flex space-x-3"> <button id="capture-btn" title="Capture Image" class="flex-1 bg-gray-100 hover:bg-gray-200 py-3 rounded-lg transition duration-150 flex items-center justify-center space-x-2 text-sm font-medium text-gray-700 hover:text-pink-500"> <i class="fas fa-camera"></i> <span>Capture</span> </button> <button id="reset-btn" title="Reset Makeup" class="flex-1 bg-gray-100 hover:bg-gray-200 py-3 rounded-lg transition duration-150 flex items-center justify-center space-x-2 text-sm font-medium text-gray-700 hover:text-pink-500"> <i class="fas fa-undo"></i> <span>Reset All</span> </button> <button id="landmarks-toggle" title="Toggle Landmarks" class="flex-1 bg-gray-100 hover:bg-gray-200 py-3 rounded-lg transition duration-150 flex items-center justify-center space-x-2 text-sm font-medium text-gray-700 hover:text-pink-500"> <i class="fas fa-vector-square"></i> <span>Landmarks</span> </button> </div> </div>
</div>
</div>
</main>
<footer class="bg-gray-800 text-white py-10 mt-16">
<div class="container mx-auto px-4 sm:px-6 lg:px-8 text-center"> <p class="text-gray-400 text-sm">&copy; 2025 GlamAI Try-On. All rights reserved.</p> <div class="mt-4 space-x-4"> <a href="#" class="text-gray-400 hover:text-white transition text-sm">Privacy Policy</a> <a href="#" class="text-gray-400 hover:text-white transition text-sm">Terms of Service</a> </div> </div>
</footer>
<div id="error-message-container"></div>
<script type="module">
// --- DOM Element References ---
const videoElement = document.getElementById('video');
const canvasElement = document.getElementById('output'); // For video frame + landmarks
const canvasCtx = canvasElement.getContext('2d');
const makeupCanvas = document.getElementById('makeup-layer'); // For makeup overlay
const makeupCtx = makeupCanvas.getContext('2d');
const startScreen = document.getElementById('start-screen');
const startBtn = document.getElementById('start-btn');
const toggleCameraBtn = document.getElementById('toggle-camera');
const toggleCameraIcon = document.getElementById('toggle-camera-icon');
const flipCameraBtn = document.getElementById('flip-camera');
const captureBtn = document.getElementById('capture-btn');
const resetBtn = document.getElementById('reset-btn');
const landmarksToggle = document.getElementById('landmarks-toggle');
const errorMessageContainer = document.getElementById('error-message-container');
const loadingIndicator = document.getElementById('loading-indicator');
const controlsColumn = document.getElementById('controls-column'); // Parent for delegated events
// --- State Variables ---
let isCameraOn = false; // Tracks if MediaPipe processing loop is active
let isCameraStarting = false;
let showLandmarks = false;
let faceDetected = false;
let mediaPipeCamera = null; // MediaPipe Camera helper instance
let currentMakeupState = {};
let loadingTimeout = null;
let currentFacingMode = "user";
// --- Constants --- (Defaults, Landmarks, Looks - same as before)
const DEFAULT_LIP_COLOR = '#fb7185'; const DEFAULT_EYESHADOW_COLORS = ["#FEF3C7", "#FCD34D", "#D97706"]; const DEFAULT_FOUNDATION_COLOR = '#F5D7C1'; const DEFAULT_OPACITIES = { lipstick: 0.7, blush: 0.5, eyeshadow: 0.6, eyeliner: 0.8, mascara: 0.65, foundation: 0.4 };
const LANDMARKS = { /* ... */ LIPS_OUTER_UPPER: [61, 185, 40, 39, 37, 0, 267, 269, 270, 409, 291], LIPS_OUTER_LOWER: [61, 146, 91, 181, 84, 17, 314, 405, 321, 375, 291], LIPS_INNER_UPPER: [78, 191, 80, 81, 82, 13, 312, 311, 310, 415, 308], LIPS_INNER_LOWER: [78, 95, 88, 178, 87, 14, 317, 402, 318, 324, 308], LEFT_EYE_UPPER_LID0: [33, 7, 163, 144, 145, 153, 154, 155, 133], LEFT_EYE_UPPER_LID1: [246, 161, 160, 159, 158, 157, 173], LEFT_EYE_LOWER_LID: [133, 173, 157, 158, 159, 160, 161, 246, 33], LEFT_EYEBROW: [70, 63, 105, 66, 107, 55, 65], RIGHT_EYE_UPPER_LID0: [263, 249, 390, 373, 374, 380, 381, 382, 362], RIGHT_EYE_UPPER_LID1: [466, 388, 387, 386, 385, 384, 398], RIGHT_EYE_LOWER_LID: [362, 398, 384, 385, 386, 387, 388, 466, 263], RIGHT_EYEBROW: [300, 293, 334, 296, 336, 285, 295], LEFT_CHEEK_AREA: [119, 118, 117, 147, 187, 205, 50, 135, 136, 234], RIGHT_CHEEK_AREA: [348, 347, 346, 376, 411, 425, 280, 364, 365, 454], FACE_OVAL: [10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288, 397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136, 172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109, 10] };
const QUICK_LOOKS = { /* ... */ romantic: { lipstick: { color: '#ec4899', opacity: 0.9 }, blush: { opacity: 0.7 }, eyeshadow: { colors: ["#FBCFE8", "#F9A8D4", "#F472B6"], opacity: 0.8 }, eyeliner: { opacity: 0.9 }, mascara: { opacity: 0.8 }, foundation: { color: currentMakeupState.foundation?.color || DEFAULT_FOUNDATION_COLOR, opacity: 0.4 } }, night: { lipstick: { color: '#9333EA', opacity: 1.0 }, blush: { opacity: 0.4 }, eyeshadow: { colors: ["#A855F7", "#7E22CE", "#581C87"], opacity: 1.0 }, eyeliner: { opacity: 1.0 }, mascara: { opacity: 0.9 }, foundation: { color: currentMakeupState.foundation?.color || DEFAULT_FOUNDATION_COLOR, opacity: 0.5 } }, day: { lipstick: { color: '#fb7185', opacity: 0.6 }, blush: { opacity: 0.5 }, eyeshadow: { colors: DEFAULT_EYESHADOW_COLORS, opacity: 0.5 }, eyeliner: { opacity: 0.6 }, mascara: { opacity: 0.7 }, foundation: { color: currentMakeupState.foundation?.color || DEFAULT_FOUNDATION_COLOR, opacity: 0.35 } }, natural: { lipstick: { color: '#fed7aa', opacity: 0.4 }, blush: { opacity: 0.3 }, eyeshadow: { colors: ["#FEF3C7", "#FCD34D", "#D97706"], opacity: 0.3 }, eyeliner: { opacity: 0.4 }, mascara: { opacity: 0.6 }, foundation: { color: currentMakeupState.foundation?.color || DEFAULT_FOUNDATION_COLOR, opacity: 0.25 } } };
// --- Camera Constraints ---
const cameraConstraints = { video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: currentFacingMode } };
// --- MediaPipe Face Mesh Initialization ---
const faceMesh = new FaceMesh({ locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}` });
faceMesh.setOptions({ maxNumFaces: 1, refineLandmarks: true, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5 });
// --- Face Mesh Results Callback ---
faceMesh.onResults((results) => {
setLoadingIndicatorVisibility(false); clearTimeout(loadingTimeout); // Hide loader
// --- Draw video frame and landmarks on output canvas ---
canvasCtx.save();
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
// Flip horizontally for front camera before drawing video
if (currentFacingMode === "user") {
canvasCtx.scale(-1, 1);
canvasCtx.translate(-canvasElement.width, 0);
}
canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
// --- Process and Draw Makeup ---
if (results.multiFaceLandmarks && results.multiFaceLandmarks.length > 0) {
faceDetected = true;
const landmarks = results.multiFaceLandmarks[0];
// Clear previous makeup before drawing new frame
makeupCtx.clearRect(0, 0, makeupCanvas.width, makeupCanvas.height);
// Apply makeup effects to the makeup canvas
applyMakeup(landmarks);
// Draw landmarks on top if toggled
if (showLandmarks) {
// Draw landmarks on the main canvas (already transformed if needed)
drawLandmarks(results.multiFaceLandmarks);
}
} else {
faceDetected = false;
// Clear makeup canvas if no face detected
makeupCtx.clearRect(0, 0, makeupCanvas.width, makeupCanvas.height);
}
canvasCtx.restore(); // Restore context of output canvas
});
// --- Drawing Functions --- (applyMakeup, applyFoundation, etc. - Omitted for brevity, same as before)
function drawLandmarks(landmarksData) { /* ... */ for (const landmarks of landmarksData) { drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_TESSELATION, { color: '#C0C0C070', lineWidth: 1 }); drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_RIGHT_EYE, { color: '#FF3030', lineWidth: 1 }); drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_RIGHT_EYEBROW, { color: '#FF3030', lineWidth: 1 }); drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_LEFT_EYE, { color: '#30FF30', lineWidth: 1 }); drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_LEFT_EYEBROW, { color: '#30FF30', lineWidth: 1 }); drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_FACE_OVAL, { color: '#E0E0E0', lineWidth: 1 }); drawConnectors(canvasCtx, landmarks, FaceMesh.FACEMESH_LIPS, { color: '#E0E0E0', lineWidth: 1 }); } }
function applyMakeup(landmarks) { if (!faceDetected) return; makeupCtx.save(); if (currentFacingMode === "user") { makeupCtx.scale(-1, 1); makeupCtx.translate(-makeupCanvas.width, 0); } const state = currentMakeupState; if (state.foundation?.opacity > 0) applyFoundation(landmarks, state.foundation.color, state.foundation.opacity); if (state.lipstick?.opacity > 0) applyLipstick(landmarks, state.lipstick.color, state.lipstick.opacity); if (state.blush?.opacity > 0) applyBlush(landmarks, state.blush.opacity); if (state.eyeshadow?.opacity > 0) applyEyeshadow(landmarks, state.eyeshadow.colors, state.eyeshadow.opacity); if (state.eyeliner?.opacity > 0) applyEyeliner(landmarks, state.eyeliner.opacity); if (state.mascara?.opacity > 0) applyMascara(landmarks, state.mascara.opacity); makeupCtx.restore(); }
function applyFoundation(landmarks, color, opacity) { const faceOvalPoints = getLandmarksByIndices(landmarks, LANDMARKS.FACE_OVAL); if (faceOvalPoints.length < 3) return; const path = createPathFromPoints(faceOvalPoints, makeupCanvas.width, makeupCanvas.height); makeupCtx.fillStyle = hexToRgba(color, opacity * 0.85); makeupCtx.fill(path); }
function applyLipstick(landmarks, color, opacity) { const outerUpperPoints = getLandmarksByIndices(landmarks, LANDMARKS.LIPS_OUTER_UPPER); const outerLowerPoints = getLandmarksByIndices(landmarks, LANDMARKS.LIPS_OUTER_LOWER); const innerUpperPoints = getLandmarksByIndices(landmarks, LANDMARKS.LIPS_INNER_UPPER); const innerLowerPoints = getLandmarksByIndices(landmarks, LANDMARKS.LIPS_INNER_LOWER); if (outerUpperPoints.length < 2 || outerLowerPoints.length < 2 || innerUpperPoints.length < 2 || innerLowerPoints.length < 2) return; const rgbaColor = hexToRgba(color, opacity); makeupCtx.fillStyle = rgbaColor; const upperLipPath = new Path2D(); drawPointsSmoothly(upperLipPath, outerUpperPoints, true, makeupCanvas.width, makeupCanvas.height); drawPointsSmoothly(upperLipPath, innerUpperPoints.slice().reverse(), false, makeupCanvas.width, makeupCanvas.height); upperLipPath.closePath(); makeupCtx.fill(upperLipPath); const lowerLipPath = new Path2D(); drawPointsSmoothly(lowerLipPath, outerLowerPoints, true, makeupCanvas.width, makeupCanvas.height); drawPointsSmoothly(lowerLipPath, innerLowerPoints.slice().reverse(), false, makeupCanvas.width, makeupCanvas.height); lowerLipPath.closePath(); makeupCtx.fill(lowerLipPath); if (opacity > 0.3) { const upperLipCenter = landmarks[13]; const lowerLipCenter = landmarks[14]; if (upperLipCenter && lowerLipCenter && outerUpperPoints[5] && innerUpperPoints[5] && outerLowerPoints[4] && innerLowerPoints[4]) { const upperShineRadius = Math.abs(outerUpperPoints[5].y - innerUpperPoints[5].y) * makeupCanvas.height * 0.3; const lowerShineRadius = Math.abs(outerLowerPoints[4].y - innerLowerPoints[4].y) * makeupCanvas.height * 0.4; applyRadialGradient(upperLipCenter, upperShineRadius, `rgba(255, 255, 255, ${opacity * 0.3})`, makeupCanvas.width, makeupCanvas.height); applyRadialGradient(lowerLipCenter, lowerShineRadius, `rgba(255, 255, 255, ${opacity * 0.4})`, makeupCanvas.width, makeupCanvas.height); } } }
function applyBlush(landmarks, opacity) { const leftCheekPoints = getLandmarksByIndices(landmarks, LANDMARKS.LEFT_CHEEK_AREA); const rightCheekPoints = getLandmarksByIndices(landmarks, LANDMARKS.RIGHT_CHEEK_AREA); if (leftCheekPoints.length < 3 || rightCheekPoints.length < 3) return; const leftCheekCenter = calculateCenter(leftCheekPoints); const rightCheekCenter = calculateCenter(rightCheekPoints); const leftRadius = Math.hypot((leftCheekCenter.x - leftCheekPoints[0].x) * makeupCanvas.width, (leftCheekCenter.y - leftCheekPoints[0].y) * makeupCanvas.height) * 1.2; const rightRadius = Math.hypot((rightCheekCenter.x - rightCheekPoints[0].x) * makeupCanvas.width, (rightCheekCenter.y - rightCheekPoints[0].y) * makeupCanvas.height) * 1.2; const blushColor = `rgba(255, 130, 150, ${opacity * 0.6})`; const drawBlushGradient = (center, radius) => { if (radius <= 0) return; const gradient = makeupCtx.createRadialGradient(center.x * makeupCanvas.width, center.y * makeupCanvas.height, radius * 0.1, center.x * makeupCanvas.width, center.y * makeupCanvas.height, radius); gradient.addColorStop(0, blushColor); gradient.addColorStop(1, `rgba(255, 130, 150, 0)`); makeupCtx.fillStyle = gradient; makeupCtx.beginPath(); makeupCtx.arc(center.x * makeupCanvas.width, center.y * makeupCanvas.height, radius, 0, Math.PI * 2); makeupCtx.fill(); }; drawBlushGradient(leftCheekCenter, leftRadius); drawBlushGradient(rightCheekCenter, rightRadius); }
function applyEyeshadow(landmarks, colors, opacity) { applySingleEyeShadow(landmarks, true, colors, opacity); applySingleEyeShadow(landmarks, false, colors, opacity); }
function applySingleEyeShadow(landmarks, isLeftEye, colors, opacity) { const upperLidPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYE_UPPER_LID0 : LANDMARKS.RIGHT_EYE_UPPER_LID0); const eyebrowPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYEBROW : LANDMARKS.RIGHT_EYEBROW); if (upperLidPoints.length < 2 || eyebrowPoints.length < 2) return; const path = new Path2D(); drawPointsSmoothly(path, upperLidPoints, true, makeupCanvas.width, makeupCanvas.height); drawPointsSmoothly(path, eyebrowPoints.slice().reverse(), false, makeupCanvas.width, makeupCanvas.height); path.closePath(); let minY = Infinity, maxY = -Infinity; [...upperLidPoints, ...eyebrowPoints].forEach(p => { minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); }); if (minY === Infinity || maxY === -Infinity) return; const gradient = makeupCtx.createLinearGradient(0, minY * makeupCanvas.height, 0, maxY * makeupCanvas.height); const numColors = colors.length; colors.forEach((color, index) => { gradient.addColorStop(index / (numColors - 1 || 1), hexToRgba(color, opacity)); }); makeupCtx.fillStyle = gradient; makeupCtx.fill(path); }
function applyEyeliner(landmarks, opacity) { applySingleEyeLiner(landmarks, true, opacity); applySingleEyeLiner(landmarks, false, opacity); }
function applySingleEyeLiner(landmarks, isLeftEye, opacity) { const upperLidPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYE_UPPER_LID1 : LANDMARKS.RIGHT_EYE_UPPER_LID1); const lowerLidPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYE_LOWER_LID : LANDMARKS.RIGHT_EYE_LOWER_LID); if (upperLidPoints.length < 2 || lowerLidPoints.length < 2) return; makeupCtx.strokeStyle = `rgba(30, 30, 30, ${opacity})`; makeupCtx.lineWidth = 1 + (opacity * 2.5); makeupCtx.lineJoin = 'round'; makeupCtx.lineCap = 'round'; makeupCtx.beginPath(); drawPointsSmoothly(makeupCtx, upperLidPoints, true, makeupCanvas.width, makeupCanvas.height); const outerCorner = upperLidPoints[upperLidPoints.length - 1]; const controlPoint = upperLidPoints[upperLidPoints.length - 2]; if (outerCorner && controlPoint) { const wingLength = 12 + opacity * 8; const angle = Math.atan2(outerCorner.y - controlPoint.y, outerCorner.x - controlPoint.x); const wingAngleOffset = isLeftEye ? -0.35 : 0.35; const wingX = outerCorner.x * makeupCanvas.width + Math.cos(angle + wingAngleOffset) * wingLength; const wingY = outerCorner.y * makeupCanvas.height + Math.sin(angle + wingAngleOffset) * wingLength; makeupCtx.quadraticCurveTo(outerCorner.x * makeupCanvas.width + Math.cos(angle) * wingLength * 0.5, outerCorner.y * makeupCanvas.height + Math.sin(angle) * wingLength * 0.5, wingX, wingY); } makeupCtx.stroke(); makeupCtx.lineWidth = 1 + (opacity * 1.0); makeupCtx.strokeStyle = `rgba(50, 50, 50, ${opacity * 0.6})`; makeupCtx.beginPath(); const lowerMidIndex = Math.floor(lowerLidPoints.length / 2); const lowerOuterPoints = lowerLidPoints.slice(lowerMidIndex - 1); if (lowerOuterPoints.length > 1) { drawPointsSmoothly(makeupCtx, lowerOuterPoints, true, makeupCanvas.width, makeupCanvas.height); makeupCtx.stroke(); } }
function applyMascara(landmarks, opacity) { applySingleEyeMascara(landmarks, true, opacity); applySingleEyeMascara(landmarks, false, opacity); }
function applySingleEyeMascara(landmarks, isLeftEye, opacity) { const upperLashPoints = getLandmarksByIndices(landmarks, isLeftEye ? LANDMARKS.LEFT_EYE_UPPER_LID1 : LANDMARKS.RIGHT_EYE_UPPER_LID1); if (upperLashPoints.length < 2) return; makeupCtx.strokeStyle = `rgba(10, 10, 10, ${opacity * 0.9})`; const baseLashLength = 3 + opacity * 4; const lashWidth = 1 + opacity * 1.5; for (let i = 0; i < upperLashPoints.length - 1; i++) { const p1 = upperLashPoints[i]; const p2 = upperLashPoints[i + 1]; const midX = (p1.x + p2.x) / 2 * makeupCanvas.width; const midY = (p1.y + p2.y) / 2 * makeupCanvas.height; const dx = p2.x - p1.x; const dy = p2.y - p1.y; let nx = -dy; let ny = dx; if ((isLeftEye && ny > 0) || (!isLeftEye && ny > 0)) { nx *= -1; ny *= -1; } const len = Math.sqrt(nx * nx + ny * ny); if (len === 0) continue; nx /= len; ny /= len; const lashLength = baseLashLength * (0.8 + Math.random() * 0.4); makeupCtx.lineWidth = lashWidth * (0.8 + Math.random() * 0.4); makeupCtx.beginPath(); makeupCtx.moveTo(midX, midY); makeupCtx.lineTo(midX + nx * lashLength, midY + ny * lashLength); makeupCtx.stroke(); } }
// --- Helper Functions --- (getLandmarksByIndices, calculateCenter, etc. - Omitted for brevity)
function getLandmarksByIndices(landmarks, indices) { return indices.map(index => landmarks[index]).filter(p => p); }
function calculateCenter(points) { if (!points || points.length === 0) return { x: 0, y: 0 }; let sumX = 0, sumY = 0; points.forEach(p => { sumX += p.x; sumY += p.y; }); return { x: sumX / points.length, y: sumY / points.length }; }
function hexToRgba(hex, alpha = 1) { if (!hex || typeof hex !== 'string') return `rgba(0,0,0,${alpha})`; hex = hex.replace('#', ''); let r = 0, g = 0, b = 0; if (hex.length === 3) { r = parseInt(hex[0] + hex[0], 16); g = parseInt(hex[1] + hex[1], 16); b = parseInt(hex[2] + hex[2], 16); } else if (hex.length === 6) { r = parseInt(hex[0] + hex[1], 16); g = parseInt(hex[2] + hex[3], 16); b = parseInt(hex[4] + hex[5], 16); } else { return `rgba(0,0,0,${alpha})`; } alpha = Math.max(0, Math.min(1, alpha)); return `rgba(${r}, ${g}, ${b}, ${alpha})`; }
function createPathFromPoints(points, canvasWidth, canvasHeight) { const path = new Path2D(); if (points.length > 0) { path.moveTo(points[0].x * canvasWidth, points[0].y * canvasHeight); for (let i = 1; i < points.length; i++) { path.lineTo(points[i].x * canvasWidth, points[i].y * canvasHeight); } path.closePath(); } return path; }
function drawPointsSmoothly(ctxOrPath, points, moveToStart = true, canvasWidth, canvasHeight) { if (!points || points.length < 2) return; const scaledPoints = points.map(p => ({ x: p.x * canvasWidth, y: p.y * canvasHeight })); if (moveToStart) { ctxOrPath.moveTo(scaledPoints[0].x, scaledPoints[0].y); } if (scaledPoints.length === 2) { ctxOrPath.lineTo(scaledPoints[1].x, scaledPoints[1].y); return; } for (let i = 0; i < scaledPoints.length - 1; i++) { const p0 = scaledPoints[i === 0 ? i : i - 1]; const p1 = scaledPoints[i]; const p2 = scaledPoints[i + 1]; const p3 = scaledPoints[i + 2 < scaledPoints.length ? i + 2 : i + 1]; const cp1x = p1.x + (p2.x - p0.x) / 6; const cp1y = p1.y + (p2.y - p0.y) / 6; const cp2x = p2.x - (p3.x - p1.x) / 6; const cp2y = p2.y - (p3.y - p1.y) / 6; ctxOrPath.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y); } }
function applyRadialGradient(center, radius, color, canvasWidth, canvasHeight) { if (!center || radius <= 0) return; const gradient = makeupCtx.createRadialGradient(center.x * canvasWidth, center.y * canvasHeight, 0, center.x * canvasWidth, center.y * canvasHeight, radius); gradient.addColorStop(0, color); gradient.addColorStop(1, hexToRgba(color, 0)); makeupCtx.fillStyle = gradient; makeupCtx.beginPath(); makeupCtx.arc(center.x * canvasWidth, center.y * canvasHeight, radius, 0, Math.PI * 2); makeupCtx.fill(); }
// --- Camera and MediaPipe Control ---
function setLoadingIndicatorVisibility(isVisible, text = "Initializing...") { /* ... */ const textElement = loadingIndicator.querySelector('p'); if (textElement) textElement.textContent = text; if (isVisible) { loadingIndicator.classList.remove('hidden'); } else { if (!loadingIndicator.classList.contains('hidden')) { loadingIndicator.classList.add('hidden'); } } }
function showErrorMessage(message) { /* ... */ errorMessageContainer.textContent = message; errorMessageContainer.style.display = 'block'; clearTimeout(errorMessageContainer.timer); errorMessageContainer.timer = setTimeout(() => { errorMessageContainer.style.display = 'none'; }, 5000); }
async function startCamera() {
if (isCameraStarting || isCameraOn) return;
isCameraStarting = true;
setLoadingIndicatorVisibility(true);
console.log("Attempting to start camera and MediaPipe...");
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { showErrorMessage("Camera API not supported."); setLoadingIndicatorVisibility(false); isCameraStarting = false; return; }
if (!window.isSecureContext) { showErrorMessage("Camera requires HTTPS/localhost."); setLoadingIndicatorVisibility(false); isCameraStarting = false; return; }
try {
cameraConstraints.video.facingMode = currentFacingMode;
const stream = await navigator.mediaDevices.getUserMedia(cameraConstraints);
console.log("Camera stream obtained.");
videoElement.srcObject = stream;
// videoElement.classList.remove('hidden'); // Keep hidden, draw on canvas
videoElement.onloadedmetadata = () => {
console.log("Video metadata loaded. Setting up canvas and MediaPipe.");
setupCanvas(); // Setup canvas dimensions based on video
initializeMediaPipeCamera(); // Start MediaPipe processing
isCameraOn = true; // Set state only after MediaPipe starts
isCameraStarting = false;
updateToggleIcon(); // Set to pause icon
startScreen.classList.add('hidden'); // Hide start screen
// Loading hidden by onResults callback or fallback timer
clearTimeout(loadingTimeout);
loadingTimeout = setTimeout(() => { setLoadingIndicatorVisibility(false); }, 5000);
};
videoElement.onerror = (e) => { console.error("Video element error:", e); showErrorMessage("Error playing video stream."); stopCamera(); setLoadingIndicatorVisibility(false); isCameraStarting = false; };
} catch (err) {
console.error("Error starting camera:", err.name, err.message);
handleCameraError(err);
stopCamera(); // Clean up on error
setLoadingIndicatorVisibility(false);
isCameraStarting = false;
}
}
function initializeMediaPipeCamera() {
if (mediaPipeCamera) { mediaPipeCamera.close(); } // Close previous instance if any
console.log("Initializing MediaPipe Camera helper.");
mediaPipeCamera = new Camera(videoElement, {
onFrame: async () => {
if (videoElement.readyState >= 2) { // Check if video frame is ready
await faceMesh.send({ image: videoElement });
}
},
width: videoElement.videoWidth, // Use actual video dimensions
height: videoElement.videoHeight
});
mediaPipeCamera.start();
console.log("MediaPipe Camera processing started.");
}
function handleCameraError(err) { /* ... */ let message = "Could not access camera."; switch (err.name) { case "NotAllowedError": message = "Permission denied. Please allow camera access."; break; case "NotFoundError": message = "No camera found. Ensure it's connected."; break; case "NotReadableError": message = "Camera is busy or hardware error."; break; case "OverconstrainedError": message = `Camera doesn't support ${cameraConstraints.video.width.ideal}x${cameraConstraints.video.height.ideal}.`; break; case "SecurityError": message = "Camera access denied (security)."; break; case "TypeError": message = "Invalid camera constraints."; break; default: message = `Unknown camera error: ${err.name}`; break; } showErrorMessage(message); startScreen.classList.remove('hidden'); } // Don't hide video element as it's not shown
function stopCamera() {
console.log("Stopping camera and MediaPipe.");
clearTimeout(loadingTimeout);
if (mediaPipeCamera) { mediaPipeCamera.close(); mediaPipeCamera = null; }
if (videoElement.srcObject) { videoElement.srcObject.getTracks().forEach(track => track.stop()); videoElement.srcObject = null; }
isCameraOn = false; isCameraStarting = false; faceDetected = false;
makeupCtx.clearRect(0, 0, makeupCanvas.width, makeupCanvas.height);
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); // Clear video canvas too
updateToggleIcon(); setLoadingIndicatorVisibility(false);
startScreen.classList.remove('hidden'); // Show start screen again
}
function toggleCamera() {
// This button now acts as Start/Stop
if (isCameraOn) {
stopCamera();
} else {
startCamera();
}
}
function updateToggleIcon() {
// Icon represents Start/Stop state
if (isCameraOn) {
toggleCameraIcon.className = 'fas fa-stop text-lg';
toggleCameraBtn.title = "Stop Camera";
} else {
toggleCameraIcon.className = 'fas fa-play text-lg';
toggleCameraBtn.title = "Start Camera";
}
}
async function flipCamera() {
if (isCameraStarting) return;
console.log("Attempting to flip camera...");
currentFacingMode = (currentFacingMode === "user") ? "environment" : "user";
console.log("New facing mode:", currentFacingMode);
// Stop completely before restarting
const wasOn = isCameraOn; // Remember if camera was running
stopCamera();
// Only restart if it was running before flipping
if (wasOn) {
await startCamera();
}
}
function captureImage() { /* ... same capture logic ... */ if (!isCameraOn) { showErrorMessage("Camera is paused. Resume to capture."); return; } if (!faceDetected && !showLandmarks) { showErrorMessage("No face detected to capture!"); return; } const tempCanvas = document.createElement('canvas'); tempCanvas.width = canvasElement.clientWidth; tempCanvas.height = canvasElement.clientHeight; const tempCtx = tempCanvas.getContext('2d'); tempCtx.save(); if (currentFacingMode === "user") { tempCtx.scale(-1, 1); tempCtx.translate(-tempCanvas.width, 0); } tempCtx.drawImage(canvasElement, 0, 0, tempCanvas.width, tempCanvas.height); tempCtx.restore(); tempCtx.save(); if (currentFacingMode === "user") { tempCtx.scale(-1, 1); tempCtx.translate(-tempCanvas.width, 0); } tempCtx.drawImage(makeupCanvas, 0, 0, tempCanvas.width, tempCanvas.height); tempCtx.restore(); const link = document.createElement('a'); link.download = `virtual-makeup-${Date.now()}.png`; link.href = tempCanvas.toDataURL('image/png'); link.click(); console.log("Image captured."); }
function resetMakeup() { /* ... same reset logic ... */ console.log("Resetting makeup."); initializeMakeupState(); updateSlidersFromState(); updateColorSelectionUI(); updatePaletteSelectionUI(); updateLookSelectionUI(); }
function toggleLandmarks() { /* ... same landmarks toggle logic ... */ showLandmarks = !showLandmarks; landmarksToggle.classList.toggle('bg-pink-100', showLandmarks); landmarksToggle.classList.toggle('text-pink-600', showLandmarks); landmarksToggle.title = showLandmarks ? "Hide Landmarks" : "Show Landmarks"; console.log("Landmarks toggled:", showLandmarks); }
function setupCanvas() {
if (!videoElement.videoWidth || videoElement.videoWidth === 0) return;
const videoWidth = videoElement.videoWidth; const videoHeight = videoElement.videoHeight;
// Set canvas buffer size to match video resolution
canvasElement.width = makeupCanvas.width = videoWidth;
canvasElement.height = makeupCanvas.height = videoHeight;
// Set display size via CSS - already done with absolute positioning and w/h-full on parent
console.log(`Canvas buffer size set to: ${videoWidth}x${videoHeight}`);
}
// --- State Management --- (initializeMakeupState, handleSliderChange - same as before)
function initializeMakeupState() { currentMakeupState = { lipstick: { color: DEFAULT_LIP_COLOR, opacity: DEFAULT_OPACITIES.lipstick }, blush: { opacity: DEFAULT_OPACITIES.blush }, eyeshadow: { colors: [...DEFAULT_EYESHADOW_COLORS], opacity: DEFAULT_OPACITIES.eyeshadow }, eyeliner: { opacity: DEFAULT_OPACITIES.eyeliner }, mascara: { opacity: DEFAULT_OPACITIES.mascara }, foundation: { color: DEFAULT_FOUNDATION_COLOR, opacity: DEFAULT_OPACITIES.foundation } }; console.log("Makeup state initialized."); }
function handleSliderChange(event) { const makeupType = event.target.name; const opacity = parseFloat(event.target.value) / 100; if (currentMakeupState[makeupType]) { currentMakeupState[makeupType].opacity = opacity; } else { console.warn(`Makeup type ${makeupType} not found.`); } updateLookSelectionUI(); }
// --- Event Handlers --- (handleSelection, applyQuickLook - same as before)
function handleSelection(targetElement) { if (!targetElement) return; const makeupType = targetElement.dataset.makeupType; const color = targetElement.dataset.color; const colors = targetElement.dataset.colors ? JSON.parse(targetElement.dataset.colors) : null; console.log(`Handling selection for: ${makeupType}`); if (makeupType === 'lipstick' && color) { currentMakeupState.lipstick.color = color; updateColorSelectionUI('lipstick', color); } else if (makeupType === 'foundation' && color) { currentMakeupState.foundation.color = color; updateColorSelectionUI('foundation', color); } else if (makeupType === 'eyeshadow' && colors) { currentMakeupState.eyeshadow.colors = colors; updatePaletteSelectionUI(targetElement.id); } updateLookSelectionUI(); console.log(`State updated: ${makeupType} color/colors set.`); }
function applyQuickLook(targetElement) { if (!targetElement) return; const lookName = targetElement.dataset.look; const lookData = QUICK_LOOKS[lookName]; if (!lookData) { console.error(`Look "${lookName}" not found.`); return; } console.log(`Applying look: ${lookName}`); for (const makeupType in lookData) { if (currentMakeupState[makeupType]) { if (makeupType === 'foundation' && !lookData.foundation.color) { lookData.foundation.color = currentMakeupState.foundation.color || DEFAULT_FOUNDATION_COLOR; } currentMakeupState[makeupType] = { ...currentMakeupState[makeupType], ...lookData[makeupType] }; } } updateSlidersFromState(); updateColorSelectionUI('lipstick', currentMakeupState.lipstick.color); updateColorSelectionUI('foundation', currentMakeupState.foundation.color); updatePaletteSelectionUI(null, currentMakeupState.eyeshadow.colors); updateLookSelectionUI(lookName); }
// --- UI Update Functions --- (updateSlidersFromState, updateColorSelectionUI, etc. - same as before)
function updateSlidersFromState() { document.querySelectorAll('.slider-control input[type="range"]').forEach(slider => { const makeupType = slider.name; if (currentMakeupState[makeupType] && typeof currentMakeupState[makeupType].opacity === 'number') { slider.value = currentMakeupState[makeupType].opacity * 100; } }); }
function updateColorSelectionUI(type, selectedColor) { document.querySelectorAll(`.color-swatch[data-makeup-type="${type}"], .foundation-swatch[data-makeup-type="${type}"]`).forEach(swatch => { swatch.classList.toggle('selected-color', swatch.dataset.color === selectedColor); }); }
function updatePaletteSelectionUI(selectedId = null, selectedColors = null) { document.querySelectorAll('.eyeshadow-palette').forEach(palette => { let isSelected = false; if (selectedId) { isSelected = palette.id === selectedId; } else if (selectedColors) { const paletteColors = palette.dataset.colors ? JSON.parse(palette.dataset.colors) : null; isSelected = JSON.stringify(paletteColors) === JSON.stringify(selectedColors); } palette.classList.toggle('selected-item', isSelected); }); }
function updateLookSelectionUI(selectedLookName = null) { document.querySelectorAll('.quick-look-btn').forEach(btn => { btn.classList.toggle('selected-item', btn.dataset.look === selectedLookName); }); }
// --- Event Listeners ---
document.addEventListener('DOMContentLoaded', () => {
console.log("DOM ready. Initializing 2D Overlay App...");
initializeMakeupState(); updateSlidersFromState(); updateColorSelectionUI('lipstick', currentMakeupState.lipstick.color); updateColorSelectionUI('foundation', currentMakeupState.foundation.color); updatePaletteSelectionUI(null, currentMakeupState.eyeshadow.colors);
if (!window.isSecureContext && window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1") { showErrorMessage("Warning: Camera works best on HTTPS or localhost."); }
// Attach main control listeners
startBtn?.addEventListener('click', startCamera); // Use the dedicated start button
toggleCameraBtn?.addEventListener('click', toggleCamera); // This now Stops/Starts after initial start
flipCameraBtn?.addEventListener('click', flipCamera);
captureBtn?.addEventListener('click', captureImage);
resetBtn?.addEventListener('click', resetMakeup);
landmarksToggle?.addEventListener('click', toggleLandmarks);
// Attach listeners for sliders
document.querySelectorAll('.slider-control input[type="range"]').forEach(slider => {
slider.addEventListener('input', handleSliderChange);
});
// Attach delegated listener for controls column
controlsColumn?.addEventListener('click', (event) => {
const colorSwatchTarget = event.target.closest('.color-swatch, .foundation-swatch');
const paletteTarget = event.target.closest('.eyeshadow-palette');
const lookTarget = event.target.closest('.quick-look-btn');
if (colorSwatchTarget) { handleSelection(colorSwatchTarget); }
else if (paletteTarget) { handleSelection(paletteTarget); }
else if (lookTarget) { applyQuickLook(lookTarget); }
});
window.addEventListener('resize', setupCanvas); // Re-enable resize handling for canvas
console.log("Initialization complete. Ready for camera start.");
}); // End DOMContentLoaded
</script>
</body>
</html>