// 数据存储 let wardrobe = { tops: [], // 上衣 bottoms: [], // 下装 shoes: [] // 鞋子 }; // 当前旋转角度 let rotations = { top: 0, bottom: 0, shoes: 0 }; // 当前选中的索引 let selectedIndices = { top: 0, bottom: 0, shoes: 0 }; // 拖动状态 let isDragging = false; let currentRing = null; let startX = 0; let startRotation = 0; // 初始化 document.addEventListener('DOMContentLoaded', () => { loadFromStorage(); setupEventListeners(); setupRings(); updateUI(); updateCounts(); }); // 设置事件监听 function setupEventListeners() { // 文件上传 const photoInput = document.getElementById('photo-input'); photoInput.addEventListener('change', handlePhotoUpload); // 圆环拖动 const rings = ['ring-top', 'ring-bottom', 'ring-shoes']; rings.forEach(ringId => { const ring = document.getElementById(ringId); const type = ringId.replace('ring-', ''); ring.addEventListener('mousedown', (e) => startDrag(e, type)); ring.addEventListener('touchstart', (e) => startDrag(e, type), {passive: false}); }); document.addEventListener('mousemove', drag); document.addEventListener('touchmove', drag, {passive: false}); document.addEventListener('mouseup', endDrag); document.addEventListener('touchend', endDrag); // 滚轮支持 document.getElementById('ring-top').addEventListener('wheel', (e) => handleWheel(e, 'top')); document.getElementById('ring-bottom').addEventListener('wheel', (e) => handleWheel(e, 'bottom')); document.getElementById('ring-shoes').addEventListener('wheel', (e) => handleWheel(e, 'shoes')); } // 设置圆环初始状态 function setupRings() { updateRingPositions(); } // 处理照片上传 async function handlePhotoUpload(e) { const file = e.target.files[0]; if (!file) return; // 显示处理模态框 showProcessingModal(); try { // 读取图片 const img = await loadImage(file); // 模拟AI处理进度 await simulateProcessing(); // 分割图片(模拟) const segments = await segmentClothing(img); // 添加到衣橱 wardrobe.tops.push(segments.top); wardrobe.bottoms.push(segments.bottom); wardrobe.shoes.push(segments.shoes); // 保存并更新 saveToStorage(); updateUI(); updateCounts(); updateRingPositions(); // 选中新添加的 selectedIndices.top = wardrobe.tops.length - 1; selectedIndices.bottom = wardrobe.bottoms.length - 1; selectedIndices.shoes = wardrobe.shoes.length - 1; // 旋转到新位置 rotateToItem('top', selectedIndices.top); rotateToItem('bottom', selectedIndices.bottom); rotateToItem('shoes', selectedIndices.shoes); hideProcessingModal(); // 显示成功提示 showNotification('服饰添加成功!', 'success'); } catch (error) { console.error('处理失败:', error); hideProcessingModal(); showNotification('处理失败,请重试', 'error'); } // 清空input e.target.value = ''; } // 加载图片 function loadImage(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = e.target.result; }; reader.readAsDataURL(file); }); } // 模拟处理过程 async function simulateProcessing() { const steps = [ { text: '正在分析人体轮廓...', progress: 20 }, { text: '检测上衣区域...', progress: 40 }, { text: '检测下装区域...', progress: 60 }, { text: '检测鞋履区域...', progress: 80 }, { text: '优化分割边缘...', progress: 95 }, { text: '处理完成!', progress: 100 } ]; for (const step of steps) { document.getElementById('processing-text').textContent = step.text; document.getElementById('progress-bar').style.width = step.progress + '%'; await sleep(400); } } // 模拟服装分割(使用canvas裁剪不同区域模拟) async function segmentClothing(img) { const canvas = document.getElementById('process-canvas'); const ctx = canvas.getContext('2d'); // 设置画布尺寸 canvas.width = 300; canvas.height = 400; // 绘制原图 ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // 模拟分割区域 // 上衣:上半部分(约35%) const topCanvas = document.createElement('canvas'); topCanvas.width = 300; topCanvas.height = 140; const topCtx = topCanvas.getContext('2d'); topCtx.drawImage(canvas, 0, 0, 300, 140, 0, 0, 300, 140); // 下装:中间部分(约40%) const bottomCanvas = document.createElement('canvas'); bottomCanvas.width = 300; bottomCanvas.height = 160; const bottomCtx = bottomCanvas.getContext('2d'); bottomCtx.drawImage(canvas, 0, 140, 300, 160, 0, 0, 300, 160); // 鞋子:底部(约25%) const shoesCanvas = document.createElement('canvas'); shoesCanvas.width = 300; shoesCanvas.height = 100; const shoesCtx = shoesCanvas.getContext('2d'); shoesCtx.drawImage(canvas, 0, 300, 300, 100, 0, 0, 300, 100); return { top: topCanvas.toDataURL('image/jpeg', 0.9), bottom: bottomCanvas.toDataURL('image/jpeg', 0.9), shoes: shoesCanvas.toDataURL('image/jpeg', 0.9) }; } // 更新UI显示 function updateUI() { updateContainer('tops-container', wardrobe.tops, 'top'); updateContainer('bottoms-container', wardrobe.bottoms, 'bottom'); updateContainer('shoes-container', wardrobe.shoes, 'shoes'); updatePreviews(); } // 更新容器内容 function updateContainer(containerId, items, type) { const container = document.getElementById(containerId); container.innerHTML = ''; items.forEach((src, index) => { const div = document.createElement('div'); div.className = `clothing-item ${selectedIndices[type] === index ? 'active' : ''}`; div.style.setProperty('--angle', (360 / Math.max(items.length, 1)) * index + 'deg'); div.innerHTML = `${type}`; div.onclick = () => selectItem(type, index); container.appendChild(div); }); } // 选择特定项目 function selectItem(type, index) { selectedIndices[type] = index; updateUI(); rotateToItem(type, index); } // 旋转到特定项目 function rotateToItem(type, index) { const items = wardrobe[type === 'top' ? 'tops' : type === 'bottom' ? 'bottoms' : 'shoes']; if (items.length === 0) return; const anglePerItem = 360 / items.length; const targetRotation = -(index * anglePerItem); // 找到最短旋转路径 let diff = targetRotation - rotations[type]; while (diff > 180) diff -= 360; while (diff < -180) diff += 360; rotations[type] += diff; updateRingRotation(type); updatePreviews(); } // 更新圆环位置 function updateRingPositions() { ['top', 'bottom', 'shoes'].forEach(type => { updateRingRotation(type); }); } // 更新单个圆环旋转 function updateRingRotation(type) { const container = document.getElementById( type === 'top' ? 'tops-container' : type === 'bottom' ? 'bottoms-container' : 'shoes-container' ); if (container) { container.style.transform = `rotateY(${rotations[type]}deg)`; } } // 开始拖动 function startDrag(e, type) { isDragging = true; currentRing = type; startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX; startRotation = rotations[type]; e.preventDefault(); } // 拖动中 function drag(e) { if (!isDragging || !currentRing) return; e.preventDefault(); const x = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX; const diff = (x - startX) * 0.5; rotations[currentRing] = startRotation + diff; updateRingRotation(currentRing); // 实时更新选中状态 updateSelectionFromRotation(currentRing); } // 根据旋转角度更新选中 function updateSelectionFromRotation(type) { const items = wardrobe[type === 'top' ? 'tops' : type === 'bottom' ? 'bottoms' : 'shoes']; if (items.length === 0) return; const normalizedRotation = ((rotations[type] % 360) + 360) % 360; const anglePerItem = 360 / items.length; const selectedIndex = Math.round((360 - normalizedRotation) / anglePerItem) % items.length; if (selectedIndices[type] !== selectedIndex) { selectedIndices[type] = selectedIndex; updateUI(); } } // 结束拖动 function endDrag() { if (!isDragging) return; // 吸附到最近的项目 if (currentRing) { const items = wardrobe[currentRing === 'top' ? 'tops' : currentRing === 'bottom' ? 'bottoms' : 'shoes']; if (items.length > 0) { const anglePerItem = 360 / items.length; const currentIndex = selectedIndices[currentRing]; const targetRotation = -(currentIndex * anglePerItem); // 平滑动画到目标位置 animateToRotation(currentRing, targetRotation); } } isDragging = false; currentRing = null; } // 平滑动画到目标角度 function animateToRotation(type, targetRotation) { const startRotation = rotations[type]; const diff = targetRotation - startRotation; const duration = 300; const startTime = performance.now(); function animate(currentTime) { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / duration, 1); const easeProgress = 1 - Math.pow(1 - progress, 3); // easeOutCubic rotations[type] = startRotation + diff * easeProgress; updateRingRotation(type); if (progress < 1) { requestAnimationFrame(animate); } else { updatePreviews(); } } requestAnimationFrame(animate); } // 滚轮支持 function handleWheel(e, type) { e.preventDefault(); const delta = e.deltaY > 0 ? 30 : -30; rotations[type] += delta; updateRingRotation(type); updateSelectionFromRotation(type); // 防抖更新预览 clearTimeout(window.wheelTimeout); window.wheelTimeout = setTimeout(() => { updatePreviews(); }, 150); } // 更新预览图 function updatePreviews() { const types = ['top', 'bottom', 'shoes']; const containers = ['preview-top', 'preview-bottom', 'preview-shoes']; types.forEach((type, i) => { const container = document.getElementById(containers[i]); const items = wardrobe[type + 's']; const index = selectedIndices[type]; if (items.length > 0 && items[index]) { container.innerHTML = ``; } else { container.innerHTML = `未选择`; } }); // 更新生成按钮状态 const hasSelection = wardrobe.tops.length > 0 || wardrobe.bottoms.length > 0 || wardrobe.shoes.length > 0; document.getElementById('generate-model-btn').disabled = !hasSelection; } // 生成AI模特 async function generateModel() { const modal = document.getElementById('model-modal'); const imgContainer = document.getElementById('model-image-container'); const img = document.getElementById('model-image'); const tagsContainer = document.getElementById('model-outfit-tags'); // 显示模态框 modal.classList.remove('hidden'); modal.classList.add('flex'); img.classList.add('hidden'); // 准备提示词 const outfit = getCurrentOutfit(); const prompt = generatePrompt(outfit); // 显示标签 tagsContainer.innerHTML = ''; const tags = []; if (outfit.top) tags.push('上衣'); if (outfit.bottom) tags.push('下装'); if (outfit.shoes) tags.push('鞋履'); tags.forEach(tag => { const span = document.createElement('span'); span.className = 'px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-sm font-medium whitespace-nowrap'; span.textContent = tag; tagsContainer.appendChild(span); }); try { // 使用 Pollinations.ai 免费API生成图片 const encodedPrompt = encodeURIComponent(prompt); const seed = Math.floor(Math.random() * 10000); const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?width=768&height=1024&seed=${seed}&nologo=true`; // 加载图片 await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; img.src = imageUrl; }); img.classList.remove('hidden'); imgContainer.querySelector('.animate-pulse').parentElement.classList.add('hidden'); } catch (error) { console.error('生成失败:', error); // 如果API失败,使用备用方案 img.src = generatePlaceholderModel(outfit); img.classList.remove('hidden'); imgContainer.querySelector('.animate-pulse').parentElement.classList.add('hidden'); } lucide.createIcons(); } // 生成提示词 function generatePrompt(outfit) { const basePrompt = "Full body fashion model, standing pose, white background, professional photography, high quality, detailed clothing"; const clothing = []; if (outfit.top) clothing.push('wearing top garment'); if (outfit.bottom) clothing.push('wearing bottom garment pants or skirt'); if (outfit.shoes) clothing.push('wearing shoes'); return `${basePrompt}, ${clothing.join(', ')}, fashion editorial style, clean lighting`; } // 获取当前搭配 function getCurrentOutfit() { return { top: wardrobe.tops[selectedIndices.top] || null, bottom: wardrobe.bottoms[selectedIndices.bottom] || null, shoes: wardrobe.shoes[selectedIndices.shoes] || null }; } // 生成备用模特图(使用占位图) function generatePlaceholderModel(outfit) { // 使用 static.photos 生成时尚相关图片 const seed = Math.floor(Math.random() * 1000); return `https://static.photos/workspace/768x1024/${seed}`; } // 关闭模特模态框 function closeModelModal() { const modal = document.getElementById('model-modal'); modal.classList.add('hidden'); modal.classList.remove('flex'); } // 下载模特图片 function downloadModel() { const img = document.getElementById('model-image'); const link = document.createElement('a'); link.download = `outfit-${Date.now()}.jpg`; link.href = img.src; link.click(); } // 分享搭配 async function shareModel() { const outfit = getCurrentOutfit(); const shareData = { title: '我的虚拟搭配', text: `看看我在3D虚拟试衣间创建的搭配!`, url: window.location.href }; try { if (navigator.share) { await navigator.share(shareData); } else { // 复制到剪贴板 await navigator.clipboard.writeText(window.location.href); showNotification('链接已复制到剪贴板!', 'success'); } } catch (err) { console.log('分享失败:', err); } } // 数据持久化 function saveToStorage() { localStorage.setItem('virtualWardrobe', JSON.stringify(wardrobe)); localStorage.setItem('selectedIndices', JSON.stringify(selectedIndices)); } function loadFromStorage() { const saved = localStorage.getItem('virtualWardrobe'); const savedIndices = localStorage.getItem('selectedIndices'); if (saved) { wardrobe = JSON.parse(saved); } if (savedIndices) { selectedIndices = JSON.parse(savedIndices); } } // 更新计数 function updateCounts() { document.getElementById('count-tops').textContent = wardrobe.tops.length; document.getElementById('count-bottoms').textContent = wardrobe.bottoms.length; document.getElementById('count-shoes').textContent = wardrobe.shoes.length; } // 显示处理模态框 function showProcessingModal() { const modal = document.getElementById('processing-modal'); modal.classList.remove('hidden'); modal.classList.add('flex'); document.getElementById('progress-bar').style.width = '0%'; } function hideProcessingModal() { const modal = document.getElementById('processing-modal'); modal.classList.add('hidden'); modal.classList.remove('flex'); } // 通知提示 function showNotification(message, type = 'success') { const notification = document.createElement('div'); notification.className = `fixed top-4 left-1/2 transform -translate-x-1/2 px-6 py-3 rounded-full text-white font-medium z-50 transition-all duration-300 ${type === 'success' ? 'bg-green-500' : 'bg-red-500'}`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.style.opacity = '0'; notification.style.transform = 'translate(-50%, -20px)'; setTimeout(() => notification.remove(), 300); }, 3000); } // 工具函数 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 键盘导航支持 document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeModelModal(); hideProcessingModal(); } // 方向键控制圆环 const step = 30; switch(e.key) { case 'ArrowUp': e.preventDefault(); rotations.top += step; updateRingRotation('top'); updateSelectionFromRotation('top'); break; case 'ArrowDown': e.preventDefault(); rotations.bottom += step; updateRingRotation('bottom'); updateSelectionFromRotation('bottom'); break; case 'ArrowLeft': e.preventDefault(); rotations.shoes -= step; updateRingRotation('shoes'); updateSelectionFromRotation('shoes'); break; case 'ArrowRight': e.preventDefault(); rotations.shoes += step; updateRingRotation('shoes'); updateSelectionFromRotation('shoes'); break; } });