watch-try-on / index.html
farrahred's picture
Add 2 files
951a87b verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advanced Virtual Watch Try-On</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- MediaPipe CDN -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script>
<style>
.watch-container {
position: relative;
width: 100%;
height: 400px;
margin: 0 auto;
overflow: hidden;
}
.camera-feed {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 15px;
display: none;
}
.watch-overlay {
position: absolute;
pointer-events: none;
transition: all 0.3s ease;
}
.color-option {
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
transition: transform 0.2s;
border: 2px solid transparent;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.selected {
border: 2px solid #000;
transform: scale(1.1);
}
.watch-preview {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
position: relative;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.8);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
border-radius: 20px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-left-color: #3b82f6;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.mirror {
transform: scaleX(-1);
}
.model-option {
transition: all 0.2s ease;
}
.model-option:hover {
transform: translateY(-5px);
}
.model-option.selected {
border: 2px solid #3b82f6;
box-shadow: 0 5px 15px rgba(59, 130, 246, 0.3);
}
.band-option {
transition: all 0.2s ease;
}
.band-option:hover {
transform: translateY(-3px);
}
.band-option.selected {
border: 2px solid #3b82f6;
box-shadow: 0 3px 10px rgba(59, 130, 246, 0.2);
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="text-center mb-8">
<h1 class="text-4xl font-bold text-gray-800 mb-2">Advanced Virtual Watch Try-On</h1>
<p class="text-gray-600">Experience precise watch placement with AI-powered hand tracking</p>
</header>
<div class="flex flex-col lg:flex-row gap-8 items-center justify-center">
<!-- Watch Preview Section -->
<div class="watch-preview p-8 w-full lg:w-1/2 max-w-lg">
<div class="loading-overlay" id="loading-overlay">
<div class="spinner"></div>
<p class="text-gray-700 font-medium">Loading hand tracking model...</p>
</div>
<div class="watch-container relative">
<!-- Camera feed will be shown here when active -->
<video id="camera-feed" class="camera-feed mirror" autoplay playsinline></video>
<!-- Canvas for MediaPipe hand tracking -->
<canvas id="output-canvas" class="absolute top-0 left-0 w-full h-full" style="display: none;"></canvas>
<!-- Watch overlay that will be positioned on the wrist -->
<img id="watch-overlay" src="https://i.imgur.com/JQZ1lzE.png" alt="Watch" class="watch-overlay" style="display: none;">
</div>
<div class="flex justify-center mt-6 gap-2">
<button id="try-on-btn" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-full font-medium transition flex items-center">
<i class="fas fa-camera mr-2"></i> Live Try-On
</button>
<button id="static-btn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-6 py-2 rounded-full font-medium transition flex items-center">
<i class="fas fa-image mr-2"></i> Sample Wrist
</button>
</div>
</div>
<!-- Watch Customization Section -->
<div class="w-full lg:w-1/2 max-w-lg">
<div class="bg-white rounded-xl shadow-md p-6">
<h2 class="text-2xl font-bold text-gray-800 mb-4">Customize Your Watch</h2>
<!-- Model Selection -->
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-700 mb-3">Select Model</h3>
<div class="grid grid-cols-3 gap-3">
<div class="model-option border rounded-lg p-2 cursor-pointer hover:bg-gray-50 transition selected" data-model="1">
<img src="https://i.imgur.com/JQZ1lzE.png" alt="Classic" class="w-full h-20 object-contain">
<p class="text-center mt-1 text-sm">Classic</p>
</div>
<div class="model-option border rounded-lg p-2 cursor-pointer hover:bg-gray-50 transition" data-model="2">
<img src="https://i.imgur.com/mXJQZ9L.png" alt="Sport" class="w-full h-20 object-contain">
<p class="text-center mt-1 text-sm">Sport</p>
</div>
<div class="model-option border rounded-lg p-2 cursor-pointer hover:bg-gray-50 transition" data-model="3">
<img src="https://i.imgur.com/nXJQZ9L.png" alt="Luxury" class="w-full h-20 object-contain">
<p class="text-center mt-1 text-sm">Luxury</p>
</div>
</div>
</div>
<!-- Color Selection -->
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-700 mb-3">Select Color</h3>
<div class="flex gap-3">
<div class="color-option bg-black selected" data-color="black" title="Black"></div>
<div class="color-option bg-slate-500" data-color="silver" title="Silver"></div>
<div class="color-option bg-amber-700" data-color="gold" title="Gold"></div>
<div class="color-option bg-blue-600" data-color="blue" title="Blue"></div>
<div class="color-option bg-red-600" data-color="red" title="Red"></div>
</div>
</div>
<!-- Band Selection -->
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-700 mb-3">Select Band</h3>
<div class="flex gap-3">
<div class="band-option border rounded-lg p-2 cursor-pointer hover:bg-gray-50 transition selected" data-band="leather">
<img src="https://i.imgur.com/leather-band.png" alt="Leather" class="w-12 h-12 object-contain">
<p class="text-center mt-1 text-sm">Leather</p>
</div>
<div class="band-option border rounded-lg p-2 cursor-pointer hover:bg-gray-50 transition" data-band="metal">
<img src="https://i.imgur.com/metal-band.png" alt="Metal" class="w-12 h-12 object-contain">
<p class="text-center mt-1 text-sm">Metal</p>
</div>
<div class="band-option border rounded-lg p-2 cursor-pointer hover:bg-gray-50 transition" data-band="silicone">
<img src="https://i.imgur.com/silicone-band.png" alt="Silicone" class="w-12 h-12 object-contain">
<p class="text-center mt-1 text-sm">Silicone</p>
</div>
</div>
</div>
<!-- Size Adjustment -->
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-700 mb-3">Adjust Size</h3>
<div class="flex items-center gap-4">
<button id="size-down" class="bg-gray-200 hover:bg-gray-300 text-gray-800 w-8 h-8 rounded-full flex items-center justify-center transition">
<i class="fas fa-minus"></i>
</button>
<div class="flex-1">
<input type="range" id="size-slider" min="70" max="130" value="100" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<button id="size-up" class="bg-gray-200 hover:bg-gray-300 text-gray-800 w-8 h-8 rounded-full flex items-center justify-center transition">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<button class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-medium flex-1 transition flex items-center justify-center">
<i class="fas fa-shopping-cart mr-2"></i> Add to Cart
</button>
<button class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-6 py-3 rounded-lg font-medium transition flex items-center justify-center">
<i class="fas fa-heart mr-2"></i> Save
</button>
</div>
</div>
</div>
</div>
<!-- Instructions Modal -->
<div id="instructions-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-xl p-6 max-w-md mx-4">
<h3 class="text-xl font-bold text-gray-800 mb-4">How to Use Virtual Try-On</h3>
<ul class="space-y-3 text-gray-700 mb-6">
<li class="flex items-start">
<i class="fas fa-hand-paper text-blue-500 mt-1 mr-2"></i>
<span>Hold your hand in front of your camera with your palm facing the camera</span>
</li>
<li class="flex items-start">
<i class="fas fa-lightbulb text-blue-500 mt-1 mr-2"></i>
<span>Make sure you're in a well-lit environment</span>
</li>
<li class="flex items-start">
<i class="fas fa-expand text-blue-500 mt-1 mr-2"></i>
<span>Keep your hand at a reasonable distance (not too close or far)</span>
</li>
<li class="flex items-start">
<i class="fas fa-stopwatch text-blue-500 mt-1 mr-2"></i>
<span>It may take a moment to detect your hand initially</span>
</li>
</ul>
<button id="close-instructions" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded-lg font-medium transition">
Got it!
</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Elements
const cameraFeed = document.getElementById('camera-feed');
const outputCanvas = document.getElementById('output-canvas');
const watchOverlay = document.getElementById('watch-overlay');
const tryOnBtn = document.getElementById('try-on-btn');
const staticBtn = document.getElementById('static-btn');
const colorOptions = document.querySelectorAll('.color-option');
const modelOptions = document.querySelectorAll('.model-option');
const bandOptions = document.querySelectorAll('.band-option');
const sizeSlider = document.getElementById('size-slider');
const sizeDown = document.getElementById('size-down');
const sizeUp = document.getElementById('size-up');
const loadingOverlay = document.getElementById('loading-overlay');
const instructionsModal = document.getElementById('instructions-modal');
const closeInstructions = document.getElementById('close-instructions');
// Model images (placeholder URLs - replace with actual images)
const models = {
1: {
image: 'https://i.imgur.com/JQZ1lzE.png',
name: 'Classic',
width: 150,
height: 150,
wristRatio: 0.6 // Ratio of watch width to wrist width
},
2: {
image: 'https://i.imgur.com/mXJQZ9L.png',
name: 'Sport',
width: 140,
height: 140,
wristRatio: 0.55
},
3: {
image: 'https://i.imgur.com/nXJQZ9L.png',
name: 'Luxury',
width: 160,
height: 160,
wristRatio: 0.65
}
};
// Color filters (simplified - in a real app you'd use different images)
const colorFilters = {
black: 'brightness(0) saturate(100%)',
silver: 'brightness(1.5) saturate(0.8) hue-rotate(-10deg)',
gold: 'brightness(1.2) saturate(1.5) hue-rotate(10deg)',
blue: 'brightness(0.8) saturate(1.5) hue-rotate(200deg)',
red: 'brightness(0.9) saturate(1.8) hue-rotate(350deg)'
};
// Current state
let currentMode = 'static'; // 'camera' or 'static'
let currentColor = 'black';
let currentModel = '1';
let currentBand = 'leather';
let currentSize = 100;
let handTrackingActive = false;
let lastWristPosition = null;
// Initialize MediaPipe Hands
const hands = new Hands({
locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
}
});
hands.setOptions({
maxNumHands: 1,
modelComplexity: 1,
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5
});
hands.onResults(onHandResults);
// Initialize
updateWatchDisplay();
showInstructions();
// Event Listeners
tryOnBtn.addEventListener('click', activateCameraMode);
staticBtn.addEventListener('click', activateStaticMode);
colorOptions.forEach(option => {
option.addEventListener('click', function() {
colorOptions.forEach(opt => opt.classList.remove('selected'));
this.classList.add('selected');
currentColor = this.dataset.color;
updateWatchDisplay();
});
});
modelOptions.forEach(option => {
option.addEventListener('click', function() {
modelOptions.forEach(opt => opt.classList.remove('selected'));
this.classList.add('selected');
currentModel = this.dataset.model;
updateWatchDisplay();
});
});
bandOptions.forEach(option => {
option.addEventListener('click', function() {
bandOptions.forEach(opt => opt.classList.remove('selected'));
this.classList.add('selected');
currentBand = this.dataset.band;
updateWatchDisplay();
});
});
sizeSlider.addEventListener('input', function() {
currentSize = parseInt(this.value);
updateWatchDisplay();
});
sizeDown.addEventListener('click', function() {
currentSize = Math.max(70, currentSize - 5);
sizeSlider.value = currentSize;
updateWatchDisplay();
});
sizeUp.addEventListener('click', function() {
currentSize = Math.min(130, currentSize + 5);
sizeSlider.value = currentSize;
updateWatchDisplay();
});
closeInstructions.addEventListener('click', function() {
instructionsModal.classList.add('hidden');
});
// Functions
function updateWatchDisplay() {
// Apply color filter
watchOverlay.style.filter = colorFilters[currentColor];
// Apply size
const sizePercent = currentSize / 100;
const model = models[currentModel];
watchOverlay.style.width = `${model.width * sizePercent}px`;
watchOverlay.style.height = `${model.height * sizePercent}px`;
// Update watch image
watchOverlay.src = model.image;
// If we have a wrist position from hand tracking, update watch position
if (lastWristPosition) {
positionWatchOnWrist(lastWristPosition);
}
}
function activateCameraMode() {
currentMode = 'camera';
tryOnBtn.classList.remove('bg-gray-200', 'text-gray-800');
tryOnBtn.classList.add('bg-blue-600', 'text-white');
staticBtn.classList.remove('bg-blue-600', 'text-white');
staticBtn.classList.add('bg-gray-200', 'text-gray-800');
watchOverlay.style.display = 'block';
outputCanvas.style.display = 'none'; // We don't need to show the hand landmarks
cameraFeed.style.display = 'block';
// Access camera
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'user',
width: { ideal: 1280 },
height: { ideal: 720 }
}
})
.then(function(stream) {
cameraFeed.srcObject = stream;
handTrackingActive = true;
// Process each frame with MediaPipe
const cameraLoop = () => {
if (!handTrackingActive) return;
// Skip if video isn't ready yet
if (cameraFeed.readyState < 2) {
requestAnimationFrame(cameraLoop);
return;
}
try {
hands.send({image: cameraFeed});
} catch (e) {
console.error("Error processing frame:", e);
}
requestAnimationFrame(cameraLoop);
};
cameraLoop();
})
.catch(function(error) {
console.error("Camera access error:", error);
alert("Could not access the camera. Please check permissions.");
activateStaticMode();
});
} else {
alert("Camera access is not supported by your browser.");
activateStaticMode();
}
}
function activateStaticMode() {
currentMode = 'static';
staticBtn.classList.remove('bg-gray-200', 'text-gray-800');
staticBtn.classList.add('bg-blue-600', 'text-white');
tryOnBtn.classList.remove('bg-blue-600', 'text-white');
tryOnBtn.classList.add('bg-gray-200', 'text-gray-800');
watchOverlay.style.display = 'block';
outputCanvas.style.display = 'none';
cameraFeed.style.display = 'none';
// Position watch in the center for static mode
const container = document.querySelector('.watch-container');
const containerRect = container.getBoundingClientRect();
watchOverlay.style.left = `${(containerRect.width - watchOverlay.width) / 2}px`;
watchOverlay.style.top = `${(containerRect.height - watchOverlay.height) / 2}px`;
// Stop camera stream if active
if (cameraFeed.srcObject) {
handTrackingActive = false;
cameraFeed.srcObject.getTracks().forEach(track => track.stop());
cameraFeed.srcObject = null;
}
}
function onHandResults(results) {
// Hide loading overlay once we get first results
if (loadingOverlay.style.display !== 'none') {
loadingOverlay.style.display = 'none';
}
if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
const landmarks = results.multiHandLandmarks[0];
// Calculate wrist position (landmarks 0 is wrist)
const wrist = landmarks[0];
// Calculate wrist width using landmarks 1 and 5 (base of index and pinky fingers)
const baseIndex = landmarks[5];
const basePinky = landmarks[17];
// Convert normalized coordinates to pixel coordinates
const videoWidth = cameraFeed.videoWidth;
const videoHeight = cameraFeed.videoHeight;
const container = document.querySelector('.watch-container');
const containerRect = container.getBoundingClientRect();
const scaleX = containerRect.width / videoWidth;
const scaleY = containerRect.height / videoHeight;
const wristX = wrist.x * videoWidth * scaleX;
const wristY = wrist.y * videoHeight * scaleY;
const baseIndexX = baseIndex.x * videoWidth * scaleX;
const baseIndexY = baseIndex.y * videoHeight * scaleY;
const basePinkyX = basePinky.x * videoWidth * scaleX;
const basePinkyY = basePinky.y * videoHeight * scaleY;
// Calculate wrist width (distance between base of index and pinky)
const wristWidth = Math.sqrt(
Math.pow(basePinkyX - baseIndexX, 2) +
Math.pow(basePinkyY - baseIndexY, 2)
);
// Calculate wrist angle (for proper watch orientation)
const wristAngle = Math.atan2(
basePinkyY - baseIndexY,
basePinkyX - baseIndexX
) * 180 / Math.PI;
// Save wrist position data
lastWristPosition = {
x: wristX,
y: wristY,
width: wristWidth,
angle: wristAngle
};
// Position the watch on the wrist
positionWatchOnWrist(lastWristPosition);
} else {
// No hand detected
watchOverlay.style.display = 'none';
}
}
function positionWatchOnWrist(wristData) {
const model = models[currentModel];
const sizePercent = currentSize / 100;
// Calculate watch width based on wrist width and model ratio
const watchWidth = wristData.width * model.wristRatio * sizePercent;
const watchHeight = watchWidth * (model.height / model.width);
// Position watch in the middle of the wrist
const watchX = wristData.x - watchWidth / 2;
const watchY = wristData.y - watchHeight / 2;
// Apply styles to watch overlay
watchOverlay.style.width = `${watchWidth}px`;
watchOverlay.style.height = `${watchHeight}px`;
watchOverlay.style.left = `${watchX}px`;
watchOverlay.style.top = `${watchY}px`;
watchOverlay.style.transform = `rotate(${wristData.angle}deg)`;
watchOverlay.style.display = 'block';
}
function showInstructions() {
// Only show instructions if this is the first time
if (!localStorage.getItem('watchesInstructionsShown')) {
instructionsModal.classList.remove('hidden');
localStorage.setItem('watchesInstructionsShown', 'true');
}
}
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=farrahred/watch-try-on" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body>
</html>