| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Virtual Fitting Room</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script> |
| <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/face-landmarks-detection@0.0.1/dist/face-landmarks-detection.js"></script> |
| <style> |
| .loading-spinner { |
| border: 4px solid rgba(255, 255, 255, 0.3); |
| border-radius: 50%; |
| border-top: 4px solid #6366f1; |
| width: 30px; |
| height: 30px; |
| animation: spin 1s linear infinite; |
| } |
| |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| |
| .measurement-line { |
| position: absolute; |
| background-color: rgba(99, 102, 241, 0.7); |
| height: 2px; |
| transform-origin: left center; |
| z-index: 10; |
| } |
| |
| .measurement-label { |
| position: absolute; |
| background-color: rgba(99, 102, 241, 0.9); |
| color: white; |
| padding: 2px 5px; |
| border-radius: 4px; |
| font-size: 12px; |
| z-index: 11; |
| transform: translateY(-50%); |
| } |
| |
| .clothing-item { |
| position: absolute; |
| transition: all 0.3s ease; |
| cursor: move; |
| z-index: 5; |
| } |
| |
| .clothing-item.selected { |
| outline: 2px solid #6366f1; |
| } |
| |
| .posture-indicator { |
| position: absolute; |
| width: 10px; |
| height: 10px; |
| border-radius: 50%; |
| background-color: #10b981; |
| z-index: 10; |
| } |
| |
| .posture-indicator.bad { |
| background-color: #ef4444; |
| } |
| |
| #previewCanvas { |
| transform: scaleX(-1); |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| <header class="mb-8"> |
| <h1 class="text-3xl font-bold text-indigo-700 text-center">Virtual Fitting Room</h1> |
| <p class="text-gray-600 text-center mt-2">Try on clothes virtually with real-time measurements</p> |
| </header> |
| |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
| |
| <div class="lg:col-span-2 bg-white rounded-lg shadow-md p-4"> |
| <div class="relative"> |
| <video id="video" width="100%" height="auto" autoplay muted playsinline class="rounded-lg hidden"></video> |
| <canvas id="previewCanvas" width="640" height="480" class="rounded-lg w-full"></canvas> |
| <div id="loadingIndicator" class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 rounded-lg"> |
| <div class="loading-spinner"></div> |
| <span class="ml-2 text-white">Loading models...</span> |
| </div> |
| <div id="errorMessage" class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 rounded-lg hidden"> |
| <div class="bg-red-500 text-white p-4 rounded-lg"> |
| <p id="errorText">Error message</p> |
| <button id="retryButton" class="mt-2 bg-white text-red-500 px-4 py-1 rounded">Retry</button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="mt-4 flex flex-wrap gap-2"> |
| <button id="startCamera" class="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700 transition"> |
| Start Camera |
| </button> |
| <button id="takePhoto" class="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 transition hidden"> |
| Take Photo |
| </button> |
| <button id="toggleMeasurements" class="bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700 transition"> |
| Show Measurements |
| </button> |
| <button id="togglePosture" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition"> |
| Show Posture |
| </button> |
| <button id="resetClothing" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition"> |
| Reset Clothing |
| </button> |
| </div> |
| |
| <div class="mt-4 p-4 bg-gray-50 rounded-lg"> |
| <h3 class="font-semibold text-lg mb-2">Posture Analysis</h3> |
| <div id="postureFeedback" class="text-sm"> |
| <p>Stand in front of the camera to analyze your posture.</p> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="bg-white rounded-lg shadow-md p-4"> |
| <h2 class="text-xl font-semibold mb-4">Select Clothing</h2> |
| |
| <div class="mb-4"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Clothing Type</label> |
| <select id="clothingType" class="w-full p-2 border border-gray-300 rounded-md"> |
| <option value="t-shirt">T-Shirt</option> |
| <option value="shirt">Shirt</option> |
| <option value="dress">Dress</option> |
| <option value="pants">Pants</option> |
| <option value="jacket">Jacket</option> |
| </select> |
| </div> |
| |
| <div class="mb-4"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Color</label> |
| <div class="flex flex-wrap gap-2"> |
| <div class="w-8 h-8 rounded-full bg-red-500 cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="red"></div> |
| <div class="w-8 h-8 rounded-full bg-blue-500 cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="blue"></div> |
| <div class="w-8 h-8 rounded-full bg-green-500 cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="green"></div> |
| <div class="w-8 h-8 rounded-full bg-black cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="black"></div> |
| <div class="w-8 h-8 rounded-full bg-white cursor-pointer border-2 border-gray-300 hover:border-gray-500" data-color="white"></div> |
| <div class="w-8 h-8 rounded-full bg-yellow-400 cursor-pointer border-2 border-transparent hover:border-gray-300" data-color="yellow"></div> |
| </div> |
| </div> |
| |
| <div class="mb-4"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Pattern</label> |
| <div class="flex flex-wrap gap-2"> |
| <div class="w-12 h-12 bg-gray-200 cursor-pointer border-2 border-transparent hover:border-gray-300 flex items-center justify-center" data-pattern="solid"> |
| <span class="text-xs">Solid</span> |
| </div> |
| <div class="w-12 h-12 bg-gray-200 cursor-pointer border-2 border-transparent hover:border-gray-300" style="background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc), linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc); background-size: 10px 10px;" data-pattern="checkered"></div> |
| <div class="w-12 h-12 bg-gray-200 cursor-pointer border-2 border-transparent hover:border-gray-300" style="background-image: radial-gradient(circle, #999 1px, transparent 1px); background-size: 10px 10px;" data-pattern="polka"></div> |
| <div class="w-12 h-12 bg-gray-200 cursor-pointer border-2 border-transparent hover:border-gray-300" style="background-image: linear-gradient(0deg, #999, #999 50%, transparent 50%, transparent 100%); background-size: 10px 10px;" data-pattern="striped"></div> |
| </div> |
| </div> |
| |
| <div class="mb-4"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Size Adjustment</label> |
| <div class="flex items-center gap-2"> |
| <button id="decreaseSize" class="bg-gray-200 p-1 rounded-full w-8 h-8 flex items-center justify-center hover:bg-gray-300">-</button> |
| <span id="sizeValue" class="font-medium">100%</span> |
| <button id="increaseSize" class="bg-gray-200 p-1 rounded-full w-8 h-8 flex items-center justify-center hover:bg-gray-300">+</button> |
| </div> |
| </div> |
| |
| <button id="addClothing" class="w-full bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700 transition"> |
| Add to Fitting Room |
| </button> |
| |
| <div class="mt-6"> |
| <h3 class="font-semibold text-lg mb-2">Your Measurements</h3> |
| <div id="measurementsDisplay" class="text-sm space-y-1"> |
| <div class="flex justify-between"> |
| <span>Shoulder Width:</span> |
| <span id="shoulderWidth">-- cm</span> |
| </div> |
| <div class="flex justify-between"> |
| <span>Chest Width:</span> |
| <span id="chestWidth">-- cm</span> |
| </div> |
| <div class="flex justify-between"> |
| <span>Waist Width:</span> |
| <span id="waistWidth">-- cm</span> |
| </div> |
| <div class="flex justify-between"> |
| <span>Hip Width:</span> |
| <span id="hipWidth">-- cm</span> |
| </div> |
| <div class="flex justify-between"> |
| <span>Torso Length:</span> |
| <span id="torsoLength">-- cm</span> |
| </div> |
| </div> |
| </div> |
| |
| <div class="mt-6 p-4 bg-indigo-50 rounded-lg"> |
| <h3 class="font-semibold text-lg mb-2 text-indigo-800">Fit Recommendation</h3> |
| <div id="fitRecommendation" class="text-sm text-indigo-700"> |
| <p>Add clothing to see fit recommendations based on your measurements.</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let model = null; |
| let video = document.getElementById('video'); |
| let canvas = document.getElementById('previewCanvas'); |
| let ctx = canvas.getContext('2d'); |
| let loadingIndicator = document.getElementById('loadingIndicator'); |
| let errorMessage = document.getElementById('errorMessage'); |
| let errorText = document.getElementById('errorText'); |
| let retryButton = document.getElementById('retryButton'); |
| let startCameraButton = document.getElementById('startCamera'); |
| let takePhotoButton = document.getElementById('takePhoto'); |
| let toggleMeasurementsButton = document.getElementById('toggleMeasurements'); |
| let togglePostureButton = document.getElementById('togglePosture'); |
| let resetClothingButton = document.getElementById('resetClothing'); |
| let addClothingButton = document.getElementById('addClothing'); |
| let clothingTypeSelect = document.getElementById('clothingType'); |
| let decreaseSizeButton = document.getElementById('decreaseSize'); |
| let increaseSizeButton = document.getElementById('increaseSize'); |
| let sizeValueSpan = document.getElementById('sizeValue'); |
| let postureFeedback = document.getElementById('postureFeedback'); |
| let fitRecommendation = document.getElementById('fitRecommendation'); |
| |
| |
| let shoulderWidthSpan = document.getElementById('shoulderWidth'); |
| let chestWidthSpan = document.getElementById('chestWidth'); |
| let waistWidthSpan = document.getElementById('waistWidth'); |
| let hipWidthSpan = document.getElementById('hipWidth'); |
| let torsoLengthSpan = document.getElementById('torsoLength'); |
| |
| |
| let isCameraOn = false; |
| let showMeasurements = false; |
| let showPosture = false; |
| let currentSize = 100; |
| let selectedColor = 'blue'; |
| let selectedPattern = 'solid'; |
| let clothingItems = []; |
| let selectedClothingItem = null; |
| let isDragging = false; |
| let dragOffsetX = 0; |
| let dragOffsetY = 0; |
| let measurements = { |
| shoulderWidth: 0, |
| chestWidth: 0, |
| waistWidth: 0, |
| hipWidth: 0, |
| torsoLength: 0 |
| }; |
| |
| |
| const PIXELS_PER_CM = 10; |
| const GOOD_POSTURE_THRESHOLD = 5; |
| |
| |
| async function init() { |
| try { |
| |
| model = await faceLandmarksDetection.load( |
| faceLandmarksDetection.SupportedPackages.mediapipeFacemesh, |
| { maxFaces: 1 } |
| ); |
| |
| loadingIndicator.classList.add('hidden'); |
| startCameraButton.classList.remove('hidden'); |
| |
| |
| setupEventListeners(); |
| |
| } catch (error) { |
| console.error('Error loading model:', error); |
| showError('Failed to load the detection model. Please try again.'); |
| } |
| } |
| |
| |
| function setupEventListeners() { |
| |
| startCameraButton.addEventListener('click', startCamera); |
| retryButton.addEventListener('click', init); |
| takePhotoButton.addEventListener('click', takePhoto); |
| |
| |
| toggleMeasurementsButton.addEventListener('click', () => { |
| showMeasurements = !showMeasurements; |
| toggleMeasurementsButton.classList.toggle('bg-purple-600'); |
| toggleMeasurementsButton.classList.toggle('bg-gray-600'); |
| }); |
| |
| togglePostureButton.addEventListener('click', () => { |
| showPosture = !showPosture; |
| togglePostureButton.classList.toggle('bg-blue-600'); |
| togglePostureButton.classList.toggle('bg-gray-600'); |
| }); |
| |
| resetClothingButton.addEventListener('click', resetClothing); |
| |
| |
| document.querySelectorAll('[data-color]').forEach(el => { |
| el.addEventListener('click', () => { |
| document.querySelectorAll('[data-color]').forEach(e => e.classList.remove('border-indigo-500')); |
| el.classList.add('border-indigo-500'); |
| selectedColor = el.getAttribute('data-color'); |
| }); |
| }); |
| |
| document.querySelectorAll('[data-pattern]').forEach(el => { |
| el.addEventListener('click', () => { |
| document.querySelectorAll('[data-pattern]').forEach(e => e.classList.remove('border-indigo-500')); |
| el.classList.add('border-indigo-500'); |
| selectedPattern = el.getAttribute('data-pattern'); |
| }); |
| }); |
| |
| |
| decreaseSizeButton.addEventListener('click', () => { |
| if (currentSize > 50) { |
| currentSize -= 5; |
| sizeValueSpan.textContent = `${currentSize}%`; |
| if (selectedClothingItem) { |
| resizeClothing(selectedClothingItem, currentSize); |
| } |
| } |
| }); |
| |
| increaseSizeButton.addEventListener('click', () => { |
| if (currentSize < 150) { |
| currentSize += 5; |
| sizeValueSpan.textContent = `${currentSize}%`; |
| if (selectedClothingItem) { |
| resizeClothing(selectedClothingItem, currentSize); |
| } |
| } |
| }); |
| |
| |
| addClothingButton.addEventListener('click', addClothingItem); |
| |
| |
| document.querySelector('[data-color="blue"]').classList.add('border-indigo-500'); |
| document.querySelector('[data-pattern="solid"]').classList.add('border-indigo-500'); |
| } |
| |
| |
| async function startCamera() { |
| try { |
| const stream = await navigator.mediaDevices.getUserMedia({ |
| video: { width: 640, height: 480, facingMode: 'user' }, |
| audio: false |
| }); |
| |
| video.srcObject = stream; |
| video.classList.remove('hidden'); |
| startCameraButton.classList.add('hidden'); |
| takePhotoButton.classList.remove('hidden'); |
| isCameraOn = true; |
| |
| |
| detect(); |
| |
| } catch (error) { |
| console.error('Error accessing camera:', error); |
| showError('Could not access the camera. Please ensure you have granted camera permissions.'); |
| } |
| } |
| |
| |
| async function detect() { |
| if (!isCameraOn || !model) return; |
| |
| try { |
| |
| const faces = await model.estimateFaces({ |
| input: video, |
| returnTensors: false, |
| flipHorizontal: false, |
| predictIrises: true |
| }); |
| |
| |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| |
| |
| ctx.save(); |
| ctx.scale(-1, 1); |
| ctx.drawImage(video, 0, 0, canvas.width * -1, canvas.height); |
| ctx.restore(); |
| |
| if (faces.length > 0) { |
| const face = faces[0]; |
| |
| |
| calculateMeasurements(face); |
| |
| |
| analyzePosture(face); |
| |
| |
| updateFitRecommendations(); |
| } |
| |
| |
| drawClothingItems(); |
| |
| |
| if (showMeasurements) { |
| drawMeasurements(); |
| } |
| |
| |
| if (showPosture) { |
| drawPostureIndicators(); |
| } |
| |
| } catch (error) { |
| console.error('Error during detection:', error); |
| } |
| |
| |
| requestAnimationFrame(detect); |
| } |
| |
| |
| function calculateMeasurements(face) { |
| |
| const landmarks = face.scaledMesh; |
| |
| |
| const leftEar = landmarks[234]; |
| const rightEar = landmarks[454]; |
| const earDistance = Math.sqrt( |
| Math.pow(rightEar[0] - leftEar[0], 2) + |
| Math.pow(rightEar[1] - leftEar[1], 2) |
| ); |
| |
| measurements.shoulderWidth = earDistance * 2.5; |
| measurements.chestWidth = earDistance * 2.8; |
| measurements.waistWidth = earDistance * 2.6; |
| measurements.hipWidth = earDistance * 2.9; |
| |
| |
| const chin = landmarks[152]; |
| const waistY = chin[1] + earDistance * 4; |
| measurements.torsoLength = waistY - chin[1]; |
| |
| |
| shoulderWidthSpan.textContent = `${Math.round(measurements.shoulderWidth / PIXELS_PER_CM)} cm`; |
| chestWidthSpan.textContent = `${Math.round(measurements.chestWidth / PIXELS_PER_CM)} cm`; |
| waistWidthSpan.textContent = `${Math.round(measurements.waistWidth / PIXELS_PER_CM)} cm`; |
| hipWidthSpan.textContent = `${Math.round(measurements.hipWidth / PIXELS_PER_CM)} cm`; |
| torsoLengthSpan.textContent = `${Math.round(measurements.torsoLength / PIXELS_PER_CM)} cm`; |
| } |
| |
| |
| function analyzePosture(face) { |
| const landmarks = face.scaledMesh; |
| |
| |
| const noseTip = landmarks[1]; |
| const chin = landmarks[152]; |
| |
| |
| const dx = chin[0] - noseTip[0]; |
| const dy = chin[1] - noseTip[1]; |
| const angle = Math.atan2(dy, dx) * 180 / Math.PI; |
| const deviationFromVertical = Math.abs(90 - angle); |
| |
| |
| if (deviationFromVertical > GOOD_POSTURE_THRESHOLD) { |
| postureFeedback.innerHTML = ` |
| <p class="text-red-600 font-medium">Poor Posture Detected</p> |
| <p class="mt-1">Your head is tilted by ${Math.round(deviationFromVertical)}° from vertical.</p> |
| <p class="mt-1">Try to straighten your back and align your head with your spine.</p> |
| `; |
| } else { |
| postureFeedback.innerHTML = ` |
| <p class="text-green-600 font-medium">Good Posture</p> |
| <p class="mt-1">Your head alignment looks good!</p> |
| `; |
| } |
| |
| |
| postureData = { |
| noseTip, |
| chin, |
| deviation: deviationFromVertical |
| }; |
| } |
| |
| |
| function updateFitRecommendations() { |
| if (clothingItems.length === 0) return; |
| |
| let recommendations = []; |
| |
| clothingItems.forEach(item => { |
| |
| const shoulderFit = item.width / measurements.shoulderWidth; |
| const lengthFit = item.height / measurements.torsoLength; |
| |
| if (shoulderFit < 0.9 || lengthFit < 0.9) { |
| recommendations.push(`The ${item.type} might be too small. Consider sizing up.`); |
| } else if (shoulderFit > 1.1 || lengthFit > 1.1) { |
| recommendations.push(`The ${item.type} might be too large. Consider sizing down.`); |
| } else { |
| recommendations.push(`The ${item.type} appears to be a good fit!`); |
| } |
| }); |
| |
| fitRecommendation.innerHTML = recommendations.map(r => `<p class="mt-1">${r}</p>`).join(''); |
| } |
| |
| |
| function drawMeasurements() { |
| |
| document.querySelectorAll('.measurement-line, .measurement-label').forEach(el => el.remove()); |
| |
| |
| const shoulderY = canvas.height / 3; |
| const shoulderLine = document.createElement('div'); |
| shoulderLine.className = 'measurement-line'; |
| shoulderLine.style.width = `${measurements.shoulderWidth}px`; |
| shoulderLine.style.left = `${(canvas.width - measurements.shoulderWidth) / 2}px`; |
| shoulderLine.style.top = `${shoulderY}px`; |
| document.body.appendChild(shoulderLine); |
| |
| const shoulderLabel = document.createElement('div'); |
| shoulderLabel.className = 'measurement-label'; |
| shoulderLabel.textContent = `Shoulder: ${Math.round(measurements.shoulderWidth / PIXELS_PER_CM)} cm`; |
| shoulderLabel.style.left = `${(canvas.width - measurements.shoulderWidth) / 2 + measurements.shoulderWidth / 2 - 50}px`; |
| shoulderLabel.style.top = `${shoulderY - 20}px`; |
| document.body.appendChild(shoulderLabel); |
| |
| |
| const chestY = shoulderY + measurements.torsoLength * 0.2; |
| const chestLine = document.createElement('div'); |
| chestLine.className = 'measurement-line'; |
| chestLine.style.width = `${measurements.chestWidth}px`; |
| chestLine.style.left = `${(canvas.width - measurements.chestWidth) / 2}px`; |
| chestLine.style.top = `${chestY}px`; |
| document.body.appendChild(chestLine); |
| |
| const chestLabel = document.createElement('div'); |
| chestLabel.className = 'measurement-label'; |
| chestLabel.textContent = `Chest: ${Math.round(measurements.chestWidth / PIXELS_PER_CM)} cm`; |
| chestLabel.style.left = `${(canvas.width - measurements.chestWidth) / 2 + measurements.chestWidth / 2 - 50}px`; |
| chestLabel.style.top = `${chestY - 20}px`; |
| document.body.appendChild(chestLabel); |
| |
| |
| const waistY = shoulderY + measurements.torsoLength * 0.5; |
| const waistLine = document.createElement('div'); |
| waistLine.className = 'measurement-line'; |
| waistLine.style.width = `${measurements.waistWidth}px`; |
| waistLine.style.left = `${(canvas.width - measurements.waistWidth) / 2}px`; |
| waistLine.style.top = `${waistY}px`; |
| document.body.appendChild(waistLine); |
| |
| const waistLabel = document.createElement('div'); |
| waistLabel.className = 'measurement-label'; |
| waistLabel.textContent = `Waist: ${Math.round(measurements.waistWidth / PIXELS_PER_CM)} cm`; |
| waistLabel.style.left = `${(canvas.width - measurements.waistWidth) / 2 + measurements.waistWidth / 2 - 50}px`; |
| waistLabel.style.top = `${waistY - 20}px`; |
| document.body.appendChild(waistLabel); |
| |
| |
| const hipY = shoulderY + measurements.torsoLength * 0.8; |
| const hipLine = document.createElement('div'); |
| hipLine.className = 'measurement-line'; |
| hipLine.style.width = `${measurements.hipWidth}px`; |
| hipLine.style.left = `${(canvas.width - measurements.hipWidth) / 2}px`; |
| hipLine.style.top = `${hipY}px`; |
| document.body.appendChild(hipLine); |
| |
| const hipLabel = document.createElement('div'); |
| hipLabel.className = 'measurement-label'; |
| hipLabel.textContent = `Hip: ${Math.round(measurements.hipWidth / PIXELS_PER_CM)} cm`; |
| hipLabel.style.left = `${(canvas.width - measurements.hipWidth) / 2 + measurements.hipWidth / 2 - 50}px`; |
| hipLabel.style.top = `${hipY - 20}px`; |
| document.body.appendChild(hipLabel); |
| |
| |
| const torsoLine = document.createElement('div'); |
| torsoLine.className = 'measurement-line'; |
| torsoLine.style.width = `${measurements.torsoLength}px`; |
| torsoLine.style.left = `${canvas.width / 2}px`; |
| torsoLine.style.top = `${shoulderY}px`; |
| torsoLine.style.transform = `rotate(90deg)`; |
| document.body.appendChild(torsoLine); |
| |
| const torsoLabel = document.createElement('div'); |
| torsoLabel.className = 'measurement-label'; |
| torsoLabel.textContent = `Torso: ${Math.round(measurements.torsoLength / PIXELS_PER_CM)} cm`; |
| torsoLabel.style.left = `${canvas.width / 2 + 20}px`; |
| torsoLabel.style.top = `${shoulderY + measurements.torsoLength / 2}px`; |
| document.body.appendChild(torsoLabel); |
| } |
| |
| |
| function drawPostureIndicators() { |
| |
| document.querySelectorAll('.posture-indicator').forEach(el => el.remove()); |
| |
| if (!postureData) return; |
| |
| |
| const headIndicator = document.createElement('div'); |
| headIndicator.className = `posture-indicator ${postureData.deviation > GOOD_POSTURE_THRESHOLD ? 'bad' : ''}`; |
| headIndicator.style.left = `${postureData.noseTip[0]}px`; |
| headIndicator.style.top = `${postureData.noseTip[1]}px`; |
| document.body.appendChild(headIndicator); |
| |
| |
| const chinIndicator = document.createElement('div'); |
| chinIndicator.className = `posture-indicator ${postureData.deviation > GOOD_POSTURE_THRESHOLD ? 'bad' : ''}`; |
| chinIndicator.style.left = `${postureData.chin[0]}px`; |
| chinIndicator.style.top = `${postureData.chin[1]}px`; |
| document.body.appendChild(chinIndicator); |
| } |
| |
| |
| function addClothingItem() { |
| const type = clothingTypeSelect.value; |
| const baseWidth = measurements.shoulderWidth * 1.2; |
| const baseHeight = measurements.torsoLength * 1.1; |
| |
| |
| let width, height; |
| switch (type) { |
| case 't-shirt': |
| case 'shirt': |
| width = baseWidth; |
| height = baseHeight * 0.8; |
| break; |
| case 'dress': |
| width = baseWidth * 0.9; |
| height = baseHeight * 1.5; |
| break; |
| case 'pants': |
| width = baseWidth * 0.8; |
| height = baseHeight * 0.9; |
| break; |
| case 'jacket': |
| width = baseWidth * 1.1; |
| height = baseHeight * 0.9; |
| break; |
| default: |
| width = baseWidth; |
| height = baseHeight; |
| } |
| |
| const clothingItem = { |
| id: Date.now(), |
| type, |
| color: selectedColor, |
| pattern: selectedPattern, |
| x: (canvas.width - width) / 2, |
| y: canvas.height / 3 - height * 0.2, |
| width, |
| height, |
| originalWidth: width, |
| originalHeight: height, |
| size: 100 |
| }; |
| |
| clothingItems.push(clothingItem); |
| selectClothingItem(clothingItem); |
| |
| |
| updateFitRecommendations(); |
| } |
| |
| |
| function drawClothingItems() { |
| |
| document.querySelectorAll('.clothing-item').forEach(el => el.remove()); |
| |
| clothingItems.forEach(item => { |
| const clothingElement = document.createElement('div'); |
| clothingElement.className = 'clothing-item'; |
| clothingElement.dataset.id = item.id; |
| |
| |
| clothingElement.style.left = `${item.x}px`; |
| clothingElement.style.top = `${item.y}px`; |
| clothingElement.style.width = `${item.width}px`; |
| clothingElement.style.height = `${item.height}px`; |
| |
| |
| let bgStyle = ''; |
| if (item.pattern === 'solid') { |
| bgStyle = `background-color: ${getColorValue(item.color)};`; |
| } else if (item.pattern === 'checkered') { |
| bgStyle = `background-image: linear-gradient(45deg, #999 25%, transparent 25%, transparent 75%, #999 75%, #999), |
| linear-gradient(45deg, #999 25%, transparent 25%, transparent 75%, #999 75%, #999); |
| background-size: 10px 10px; |
| background-color: ${getColorValue(item.color)};`; |
| } else if (item.pattern === 'polka') { |
| bgStyle = `background-image: radial-gradient(circle, #999 1px, transparent 1px); |
| background-size: 10px 10px; |
| background-color: ${getColorValue(item.color)};`; |
| } else if (item.pattern === 'striped') { |
| bgStyle = `background-image: linear-gradient(0deg, #999, #999 50%, transparent 50%, transparent 100%); |
| background-size: 10px 10px; |
| background-color: ${getColorValue(item.color)};`; |
| } |
| |
| clothingElement.style.cssText += bgStyle; |
| |
| |
| if (selectedClothingItem && selectedClothingItem.id === item.id) { |
| clothingElement.classList.add('selected'); |
| } |
| |
| |
| clothingElement.addEventListener('mousedown', (e) => { |
| selectClothingItem(item); |
| |
| |
| isDragging = true; |
| dragOffsetX = e.clientX - item.x; |
| dragOffsetY = e.clientY - item.y; |
| }); |
| |
| document.body.appendChild(clothingElement); |
| }); |
| } |
| |
| |
| document.addEventListener('mousemove', (e) => { |
| if (isDragging && selectedClothingItem) { |
| selectedClothingItem.x = e.clientX - dragOffsetX; |
| selectedClothingItem.y = e.clientY - dragOffsetY; |
| |
| |
| selectedClothingItem.x = Math.max(0, Math.min(canvas.width - selectedClothingItem.width, selectedClothingItem.x)); |
| selectedClothingItem.y = Math.max(0, Math.min(canvas.height - selectedClothingItem.height, selectedClothingItem.y)); |
| } |
| }); |
| |
| |
| document.addEventListener('mouseup', () => { |
| isDragging = false; |
| }); |
| |
| |
| function selectClothingItem(item) { |
| selectedClothingItem = item; |
| currentSize = item.size; |
| sizeValueSpan.textContent = `${currentSize}%`; |
| |
| |
| drawClothingItems(); |
| } |
| |
| |
| function resizeClothing(item, newSize) { |
| const scaleFactor = newSize / 100; |
| item.width = item.originalWidth * scaleFactor; |
| item.height = item.originalHeight * scaleFactor; |
| item.size = newSize; |
| |
| |
| const centerX = item.x + item.width / 2; |
| const centerY = item.y + item.height / 2; |
| |
| item.x = centerX - item.width / 2; |
| item.y = centerY - item.height / 2; |
| } |
| |
| |
| function resetClothing() { |
| clothingItems = []; |
| selectedClothingItem = null; |
| drawClothingItems(); |
| fitRecommendation.innerHTML = '<p>Add clothing to see fit recommendations based on your measurements.</p>'; |
| } |
| |
| |
| function takePhoto() { |
| |
| const tempCanvas = document.createElement('canvas'); |
| tempCanvas.width = canvas.width; |
| tempCanvas.height = canvas.height; |
| const tempCtx = tempCanvas.getContext('2d'); |
| |
| |
| tempCtx.save(); |
| tempCtx.scale(-1, 1); |
| tempCtx.drawImage(video, 0, 0, canvas.width * -1, canvas.height); |
| tempCtx.restore(); |
| |
| |
| clothingItems.forEach(item => { |
| tempCtx.fillStyle = getColorValue(item.color); |
| tempCtx.fillRect(item.x, item.y, item.width, item.height); |
| }); |
| |
| |
| const dataUrl = tempCanvas.toDataURL('image/png'); |
| const link = document.createElement('a'); |
| link.download = 'virtual-fitting-room.png'; |
| link.href = dataUrl; |
| link.click(); |
| } |
| |
| |
| function getColorValue(colorName) { |
| const colors = { |
| 'red': '#ef4444', |
| 'blue': '#3b82f6', |
| 'green': '#10b981', |
| 'black': '#000000', |
| 'white': '#ffffff', |
| 'yellow': '#f59e0b' |
| }; |
| return colors[colorName] || '#3b82f6'; |
| } |
| |
| |
| function showError(message) { |
| errorText.textContent = message; |
| errorMessage.classList.remove('hidden'); |
| loadingIndicator.classList.add('hidden'); |
| } |
| |
| |
| window.addEventListener('DOMContentLoaded', init); |
| </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=viswanani/clone-app" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |