// 数据存储
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 = `
`;
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;
}
});