Spaces:
Running
Running
| // 数据存储 | |
| 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 = `<img src="${src}" alt="${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 = `<img src="${items[index]}" class="w-full h-full object-cover rounded-lg">`; | |
| } else { | |
| container.innerHTML = `<span class="text-xs text-gray-400">未选择</span>`; | |
| } | |
| }); | |
| // 更新生成按钮状态 | |
| 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; | |
| } | |
| }); |