|
|
<!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> |