fitting-mirror / index.html
TDN-M's picture
Các phần triển khai tạo ra hình ảnh trên gương đã được hoàn thiện chưa? Tôi cần tất cả đều phải hoàn thiện - Follow Up Deployment
17f4892 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Virtual Dressing Mirror</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/body-pix@latest"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.mirror-container {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
background-color: #000;
}
#mirror-video {
width: 100%;
height: 100%;
object-fit: cover;
transform: scaleX(-1);
}
#mirror-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
}
.product-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 20;
pointer-events: none;
}
.product-item {
position: absolute;
transition: all 0.3s ease;
pointer-events: none;
}
.product-item img {
width: 100%;
height: 100%;
object-fit: contain;
}
.controls {
position: absolute;
bottom: 20px;
left: 0;
width: 100%;
z-index: 30;
display: flex;
justify-content: center;
gap: 15px;
}
.control-btn {
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
cursor: pointer;
transition: all 0.2s ease;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.product-selector {
position: absolute;
bottom: 100px;
left: 0;
width: 100%;
height: 120px;
z-index: 30;
display: flex;
justify-content: center;
gap: 10px;
padding: 0 20px;
overflow-x: auto;
scrollbar-width: none;
}
.product-selector::-webkit-scrollbar {
display: none;
}
.product-thumbnail {
width: 80px;
height: 80px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
overflow: hidden;
border: 2px solid rgba(255, 255, 255, 0.2);
}
.product-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-thumbnail.active {
border-color: white;
transform: scale(1.1);
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
margin-bottom: 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.category-selector {
position: absolute;
top: 20px;
left: 0;
width: 100%;
z-index: 30;
display: flex;
justify-content: center;
gap: 10px;
padding: 10px;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
}
.category-btn {
padding: 8px 15px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.1);
color: white;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
border: none;
outline: none;
}
.category-btn.active {
background: white;
color: black;
}
.product-info {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
color: white;
padding: 15px;
border-radius: 10px;
max-width: 200px;
z-index: 30;
display: none;
}
.product-info h3 {
margin: 0 0 5px 0;
font-size: 16px;
}
.product-info p {
margin: 0;
font-size: 14px;
opacity: 0.8;
}
.product-info .price {
font-weight: bold;
margin-top: 10px;
font-size: 18px;
}
</style>
</head>
<body class="bg-black text-white">
<div class="mirror-container">
<!-- Loading overlay -->
<div class="loading-overlay">
<div class="spinner"></div>
<div>Initializing virtual mirror...</div>
</div>
<!-- Video feed -->
<video id="mirror-video" autoplay muted></video>
<!-- Canvas for segmentation -->
<canvas id="mirror-canvas"></canvas>
<!-- Product overlays -->
<div class="product-overlay" id="product-overlay"></div>
<!-- Category selector -->
<div class="category-selector">
<button class="category-btn active" data-category="tops">Tops</button>
<button class="category-btn" data-category="bottoms">Bottoms</button>
<button class="category-btn" data-category="dresses">Dresses</button>
<button class="category-btn" data-category="outerwear">Outerwear</button>
<button class="category-btn" data-category="accessories">Accessories</button>
</div>
<!-- Product info panel -->
<div class="product-info" id="product-info">
<h3 id="product-name">Product Name</h3>
<p id="product-description">Product description goes here</p>
<div class="price" id="product-price">$99.99</div>
</div>
<!-- Product selector -->
<div class="product-selector" id="product-selector">
<!-- Products will be loaded here -->
</div>
<!-- Controls -->
<div class="controls">
<div class="control-btn" id="capture-btn">
<i class="fas fa-camera"></i>
</div>
<div class="control-btn" id="toggle-mirror-btn">
<i class="fas fa-eye"></i>
</div>
<div class="control-btn" id="reset-btn">
<i class="fas fa-redo"></i>
</div>
</div>
</div>
<script>
// DOM elements
const video = document.getElementById('mirror-video');
const canvas = document.getElementById('mirror-canvas');
const ctx = canvas.getContext('2d');
const productOverlay = document.getElementById('product-overlay');
const productSelector = document.getElementById('product-selector');
const categoryButtons = document.querySelectorAll('.category-btn');
const captureBtn = document.getElementById('capture-btn');
const toggleMirrorBtn = document.getElementById('toggle-mirror-btn');
const resetBtn = document.getElementById('reset-btn');
const productInfo = document.getElementById('product-info');
const loadingOverlay = document.querySelector('.loading-overlay');
// App state
let net;
let currentCategory = 'tops';
let selectedProduct = null;
let products = {
tops: [
{ id: 't1', name: 'Classic White Tee', price: '$29.99', description: '100% cotton crew neck t-shirt', image: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/LzVsz08/tshirt.png', position: { x: 50, y: 30, width: 40, rotation: 0 } },
{ id: 't2', name: 'Striped Blouse', price: '$49.99', description: 'Silk blend striped blouse', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/LzVsz08/tshirt.png', position: { x: 50, y: 30, width: 40, rotation: 0 } },
{ id: 't3', name: 'Black Crop Top', price: '$34.99', description: 'Stretchy black crop top', image: 'https://images.unsplash.com/photo-1576566588028-4147f3842f27?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/LzVsz08/tshirt.png', position: { x: 50, y: 30, width: 40, rotation: 0 } },
{ id: 't4', name: 'Denim Shirt', price: '$59.99', description: 'Classic denim button-up', image: 'https://images.unsplash.com/photo-1598033129183-c4f50c736f10?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/LzVsz08/tshirt.png', position: { x: 50, y: 30, width: 40, rotation: 0 } },
{ id: 't5', name: 'Pink Sweater', price: '$65.99', description: 'Cozy knit sweater', image: 'https://images.unsplash.com/photo-1520367445093-50dc08a59d9d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/LzVsz08/tshirt.png', position: { x: 50, y: 30, width: 40, rotation: 0 } }
],
bottoms: [
{ id: 'b1', name: 'Skinny Jeans', price: '$79.99', description: 'Stretch denim skinny jeans', image: 'https://images.unsplash.com/photo-1541099649105-f69ad21a3246?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/jeans.png', position: { x: 50, y: 60, width: 40, rotation: 0 } },
{ id: 'b2', name: 'Black Slacks', price: '$89.99', description: 'Tailored work pants', image: 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/jeans.png', position: { x: 50, y: 60, width: 40, rotation: 0 } },
{ id: 'b3', name: 'Pleated Skirt', price: '$59.99', description: 'Mid-length pleated skirt', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/jeans.png', position: { x: 50, y: 60, width: 40, rotation: 0 } },
{ id: 'b4', name: 'Denim Shorts', price: '$49.99', description: 'High-waisted denim shorts', image: 'https://images.unsplash.com/photo-1602810318383-e386cc2a3ccf?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/jeans.png', position: { x: 50, y: 60, width: 40, rotation: 0 } },
{ id: 'b5', name: 'Yoga Pants', price: '$54.99', description: 'High-performance leggings', image: 'https://images.unsplash.com/photo-1583744949095-cf0f6a4e6c5a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/jeans.png', position: { x: 50, y: 60, width: 40, rotation: 0 } }
],
dresses: [
{ id: 'd1', name: 'Little Black Dress', price: '$99.99', description: 'Classic A-line dress', image: 'https://images.unsplash.com/photo-1539533018447-63fcce2678e5?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/dress.png', position: { x: 50, y: 40, width: 45, rotation: 0 } },
{ id: 'd2', name: 'Floral Sundress', price: '$79.99', description: 'Lightweight summer dress', image: 'https://images.unsplash.com/photo-1591047139829-d91aecb6caea?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/dress.png', position: { x: 50, y: 40, width: 45, rotation: 0 } },
{ id: 'd3', name: 'Wrap Dress', price: '$89.99', description: 'Flattering wrap silhouette', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/dress.png', position: { x: 50, y: 40, width: 45, rotation: 0 } },
{ id: 'd4', name: 'Maxi Dress', price: '$109.99', description: 'Floor-length bohemian dress', image: 'https://images.unsplash.com/photo-1572804013309-59a88b7e92f1?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/dress.png', position: { x: 50, y: 40, width: 45, rotation: 0 } },
{ id: 'd5', name: 'Cocktail Dress', price: '$119.99', description: 'Sequin embellished party dress', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/dress.png', position: { x: 50, y: 40, width: 45, rotation: 0 } }
],
outerwear: [
{ id: 'o1', name: 'Denim Jacket', price: '$89.99', description: 'Classic blue denim jacket', image: 'https://images.unsplash.com/photo-1591047139829-d91aecb6caea?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/LzVsz08/jacket.png', position: { x: 50, y: 30, width: 45, rotation: 0 } },
{ id: 'o2', name: 'Trench Coat', price: '$149.99', description: 'Water-resistant classic trench', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/LzVsz08/jacket.png', position: { x: 50, y: 30, width: 45, rotation: 0 } },
{ id: 'o3', name: 'Leather Jacket', price: '$199.99', description: 'Genuine leather biker jacket', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/LzVsz08/jacket.png', position: { x: 50, y: 30, width: 45, rotation: 0 } },
{ id: 'o4', name: 'Puffer Jacket', price: '$129.99', description: 'Warm winter puffer', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/LzVsz08/jacket.png', position: { x: 50, y: 30, width: 45, rotation: 0 } },
{ id: 'o5', name: 'Blazer', price: '$119.99', description: 'Tailored work blazer', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/LzVsz08/jacket.png', position: { x: 50, y: 30, width: 45, rotation: 0 } }
],
accessories: [
{ id: 'a1', name: 'Silk Scarf', price: '$39.99', description: 'Printed silk scarf', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/scarf.png', position: { x: 50, y: 20, width: 30, rotation: 0 } },
{ id: 'a2', name: 'Statement Necklace', price: '$59.99', description: 'Chunky gold-tone necklace', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/necklace.png', position: { x: 50, y: 20, width: 30, rotation: 0 } },
{ id: 'a3', name: 'Wide Brim Hat', price: '$49.99', description: 'Straw sun hat', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/LzVsz08/hat.png', position: { x: 50, y: 10, width: 40, rotation: 0 } },
{ id: 'a4', name: 'Designer Handbag', price: '$199.99', description: 'Leather crossbody bag', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/0Xz9Fk1/bag.png', position: { x: 20, y: 60, width: 20, rotation: 0 } },
{ id: 'a5', name: 'Oversized Sunglasses', price: '$79.99', description: 'UV-protective lenses', image: 'https://images.unsplash.com/photo-1551232864-3f0890e580d9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200&q=80', overlay: 'https://i.ibb.co/LzVsz08/sunglasses.png', position: { x: 50, y: 25, width: 30, rotation: 0 } }
]
};
// Initialize the app
async function init() {
try {
// Load body segmentation model
net = await bodyPix.load({
architecture: 'MobileNetV1',
outputStride: 16,
multiplier: 0.75,
quantBytes: 2
});
// Start camera
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'user',
width: { ideal: 1080 },
height: { ideal: 1920 }
},
audio: false
});
video.srcObject = stream;
// Wait for video to be ready
video.onloadedmetadata = () => {
// Set canvas dimensions to match video
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// Hide loading overlay
setTimeout(() => {
loadingOverlay.style.display = 'none';
}, 1000);
// Start segmentation loop
segmentationLoop();
};
} else {
alert('Camera access is not supported by your browser');
}
// Load initial products
loadProducts(currentCategory);
// Set up event listeners
setupEventListeners();
} catch (error) {
console.error('Error initializing app:', error);
loadingOverlay.querySelector('div').textContent = 'Error initializing. Please refresh the page.';
}
}
// Set up event listeners
function setupEventListeners() {
// Category buttons
categoryButtons.forEach(btn => {
btn.addEventListener('click', () => {
// Update active button
categoryButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Load new category
currentCategory = btn.dataset.category;
loadProducts(currentCategory);
});
});
// Capture button
captureBtn.addEventListener('click', captureOutfit);
// Toggle mirror button
toggleMirrorBtn.addEventListener('click', toggleMirrorEffect);
// Reset button
resetBtn.addEventListener('click', resetProducts);
}
// Load products for a category
function loadProducts(category) {
productSelector.innerHTML = '';
products[category].forEach(product => {
const productEl = document.createElement('div');
productEl.className = 'product-thumbnail';
productEl.dataset.id = product.id;
const img = document.createElement('img');
img.src = product.image;
img.alt = product.name;
productEl.appendChild(img);
productEl.addEventListener('click', () => {
// Highlight selected product
document.querySelectorAll('.product-thumbnail').forEach(el => {
el.classList.remove('active');
});
productEl.classList.add('active');
// Show product on mirror
showProduct(product);
});
productSelector.appendChild(productEl);
});
}
// Show product on mirror
function showProduct(product) {
selectedProduct = product;
// Clear previous products of the same category
const existingProducts = document.querySelectorAll(`.product-item[data-category="${currentCategory}"]`);
existingProducts.forEach(el => el.remove());
// Create product element
const productEl = document.createElement('div');
productEl.className = 'product-item';
productEl.dataset.id = product.id;
productEl.dataset.category = currentCategory;
const img = document.createElement('img');
img.src = product.overlay;
img.alt = product.name;
productEl.appendChild(img);
productEl.style.width = `${product.position.width}%`;
productEl.style.left = `${product.position.x}%`;
productEl.style.top = `${product.position.y}%`;
productEl.style.transform = `translate(-50%, -50%) rotate(${product.position.rotation}deg)`;
productOverlay.appendChild(productEl);
// Show product info
document.getElementById('product-name').textContent = product.name;
document.getElementById('product-description').textContent = product.description;
document.getElementById('product-price').textContent = product.price;
productInfo.style.display = 'block';
}
// Segmentation loop
async function segmentationLoop() {
// Perform segmentation
const segmentation = await net.segmentPerson(video, {
flipHorizontal: true,
internalResolution: 'medium',
segmentationThreshold: 0.7
});
// Draw segmentation mask
const mask = bodyPix.toMask(segmentation);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.putImageData(mask, 0, 0);
// Continue loop
requestAnimationFrame(segmentationLoop);
}
// Capture outfit
function captureOutfit() {
// Create temporary canvas
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
const tempCtx = tempCanvas.getContext('2d');
// Draw video frame
tempCtx.save();
tempCtx.scale(-1, 1);
tempCtx.drawImage(video, -canvas.width, 0, canvas.width, canvas.height);
tempCtx.restore();
// Draw products
const productElements = document.querySelectorAll('.product-item');
productElements.forEach(el => {
const img = el.querySelector('img');
const rect = el.getBoundingClientRect();
tempCtx.save();
tempCtx.translate(rect.left + rect.width/2, rect.top + rect.height/2);
tempCtx.rotate((img.style.transform.match(/rotate\((\d+)deg\)/) || [0,0])[1] * Math.PI / 180);
tempCtx.drawImage(img, -rect.width/2, -rect.height/2, rect.width, rect.height);
tempCtx.restore();
});
// Create download link
const link = document.createElement('a');
link.download = 'virtual-mirror-outfit.png';
link.href = tempCanvas.toDataURL('image/png');
link.click();
// Show confirmation
const confirmation = document.createElement('div');
confirmation.className = 'fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-black bg-opacity-70 text-white px-6 py-3 rounded-lg z-50';
confirmation.textContent = 'Outfit captured!';
document.body.appendChild(confirmation);
setTimeout(() => {
document.body.removeChild(confirmation);
}, 2000);
}
// Toggle mirror effect
function toggleMirrorEffect() {
if (canvas.style.opacity === '0') {
canvas.style.opacity = '1';
toggleMirrorBtn.innerHTML = '<i class="fas fa-eye"></i>';
} else {
canvas.style.opacity = '0';
toggleMirrorBtn.innerHTML = '<i class="fas fa-eye-slash"></i>';
}
}
// Reset all products
function resetProducts() {
productOverlay.innerHTML = '';
productInfo.style.display = 'none';
document.querySelectorAll('.product-thumbnail').forEach(el => {
el.classList.remove('active');
});
selectedProduct = null;
}
// Initialize when DOM is loaded
document.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=TDN-M/fitting-mirror" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>