syanthan/stylesync-outfit-wizard
0
1 2document.addEventListener('DOMContentLoaded', function() {3 // Enhanced clothing data with categories4 const clothingData = {5 tops: [6 { id: 1, name: 'White Kurta', category: 'ethnic', image: 'http://static.photos/white/320x240/1', colors: ['#FFFFFF', '#F3F4F6'] },7 { id: 2, name: 'Blue Crop Top', category: 'casual', image: 'http://static.photos/blue/320x240/2', colors: ['#3B82F6', '#93C5FD'] },8 { id: 3, name: 'Black Hoodie', category: 'casual', image: 'http://static.photos/black/320x240/3', colors: ['#1F2937', '#4B5563'] },9 { id: 4, name: 'Red Shirt', category: 'formal', image: 'http://static.photos/red/320x240/4', colors: ['#EF4444', '#FCA5A5'] },10 { id: 5, name: 'Green Blouse', category: 'ethnic', image: 'http://static.photos/green/320x240/5', colors: ['#10B981', '#6EE7B7'] },11 { id: 6, name: 'Yellow T-shirt', category: 'casual', image: 'http://static.photos/yellow/320x240/6', colors: ['#F59E0B', '#FCD34D'] }12],13 bottoms: [14 { id: 1, name: 'Black Jeans', category: 'casual', image: 'http://static.photos/black/320x240/7', colors: ['#1F2937', '#4B5563'] },15 { id: 2, name: 'Blue Cargos', category: 'casual', image: 'http://static.photos/blue/320x240/8', colors: ['#3B82F6', '#93C5FD'] },16 { id: 3, name: 'White Leggings', category: 'casual', image: 'http://static.photos/white/320x240/9', colors: ['#FFFFFF', '#F3F4F6'] },17 { id: 4, name: 'Red Palazzos', category: 'ethnic', image: 'http://static.photos/red/320x240/10', colors: ['#EF4444', '#FCA5A5'] },18 { id: 5, name: 'Green Skirt', category: 'casual', image: 'http://static.photos/green/320x240/11', colors: ['#10B981', '#6EE7B7'] },19 { id: 6, name: 'Denim Shorts', category: 'casual', image: 'http://static.photos/blue/320x240/12', colors: ['#1E40AF', '#3B82F6'] }20]21 };22 23 // Initialize Swiper carousels24 const topsSwiper = new Swiper('#tops-container', {25 slidesPerView: 'auto',26 spaceBetween: 20,27 navigation: {28 nextEl: '.swiper-button-next',29 prevEl: '.swiper-button-prev',30 },31 });32 33 const bottomsSwiper = new Swiper('#bottoms-container', {34 slidesPerView: 'auto',35 spaceBetween: 20,36 navigation: {37 nextEl: '.swiper-button-next',38 prevEl: '.swiper-button-prev',39 },40 });41// State management42 let state = {43 selectedTop: null,44 selectedBottom: null,45 selectedTopColor: null,46 selectedBottomColor: null,47 suggestedColors: []48 };49 50 // DOM elements51 const topsContainer = document.getElementById('tops-container');52 const bottomsContainer = document.getElementById('bottoms-container');53 const outfitPreview = document.querySelector('custom-outfit-preview');54 55 // Render clothing items56 function renderClothingItems() {57 // Clear containers58 topsContainer.innerHTML = '';59 bottomsContainer.innerHTML = '';60 61 // Render tops62 clothingData.tops.forEach(top => {63 const topElement = document.createElement('div');64 topElement.className = `clothing-item rounded-lg overflow-hidden ${state.selectedTop?.id === top.id ? 'selected' : ''}`;65 topElement.innerHTML = `66 <div class="relative pb-[125%] bg-gray-100 rounded-lg">67 <img src="${top.image}" alt="${top.name}" class="absolute h-full w-full object-cover">68 </div>69 <div class="mt-2">70 <h3 class="font-medium text-gray-800">${top.name}</h3>71 <div class="mt-1 flex">72 ${top.colors.map(color => `73 <div class="color-swatch ${state.selectedTopColor === color && state.selectedTop?.id === top.id ? 'active' : ''}" 74 style="background-color: ${color}" 75 data-item-id="${top.id}" 76 data-color="${color}" 77 data-type="top"></div>78 `).join('')}79 </div>80 </div>81 `;82 topsContainer.appendChild(topElement);83 });84 85 // Render bottoms86 clothingData.bottoms.forEach(bottom => {87 const bottomElement = document.createElement('div');88 bottomElement.className = `clothing-item rounded-lg overflow-hidden ${state.selectedBottom?.id === bottom.id ? 'selected' : ''}`;89 bottomElement.innerHTML = `90 <div class="relative pb-[125%] bg-gray-100 rounded-lg">91 <img src="${bottom.image}" alt="${bottom.name}" class="absolute h-full w-full object-cover">92 </div>93 <div class="mt-2">94 <h3 class="font-medium text-gray-800">${bottom.name}</h3>95 <div class="mt-1 flex">96 ${bottom.colors.map(color => `97 <div class="color-swatch ${state.selectedBottomColor === color && state.selectedBottom?.id === bottom.id ? 'active' : ''}" 98 style="background-color: ${color}" 99 data-item-id="${bottom.id}" 100 data-color="${color}" 101 data-type="bottom"></div>102 `).join('')}103 </div>104 </div>105 `;106 bottomsContainer.appendChild(bottomElement);107 });108 109 // Add event listeners110 document.querySelectorAll('.clothing-item').forEach(item => {111 item.addEventListener('click', function(e) {112 if (e.target.classList.contains('color-swatch')) return;113 114 const type = this.querySelector('.color-swatch')?.dataset.type;115 const id = parseInt(this.querySelector('.color-swatch')?.dataset.itemId);116 117 if (type === 'top') {118 const selectedTop = clothingData.tops.find(t => t.id === id);119 state.selectedTop = selectedTop;120 state.selectedTopColor = selectedTop.colors[0];121 } else if (type === 'bottom') {122 const selectedBottom = clothingData.bottoms.find(b => b.id === id);123 state.selectedBottom = selectedBottom;124 state.selectedBottomColor = selectedBottom.colors[0];125 }126 127 updateColorSuggestions();128 renderClothingItems();129 updateOutfitPreview();130 });131 });132 133 document.querySelectorAll('.color-swatch').forEach(swatch => {134 swatch.addEventListener('click', function(e) {135 e.stopPropagation();136 137 const type = this.dataset.type;138 const color = this.dataset.color;139 const id = parseInt(this.dataset.itemId);140 141 if (type === 'top') {142 state.selectedTop = clothingData.tops.find(t => t.id === id);143 state.selectedTopColor = color;144 } else if (type === 'bottom') {145 state.selectedBottom = clothingData.bottoms.find(b => b.id === id);146 state.selectedBottomColor = color;147 }148 149 updateColorSuggestions();150 renderClothingItems();151 updateOutfitPreview();152 });153 });154 }155// Enhanced color matching with FITBOARD algorithm156function getColorSuggestions(hex) {157 try {158 const {r,g,b} = hexToRgb(hex);159 const {h,s,l} = rgbToHsl(r,g,b);160 161 const tonal = [162 {label:'Lighter', hex:hslToHex(h, Math.max(0.02,s*0.6), Math.min(0.96,l+0.18))},163 {label:'Darker', hex:hslToHex(h, Math.max(0.02,s*0.85), Math.max(0.04,l-0.24))},164 {label:'Muted', hex:hslToHex(h, Math.max(0.02,s*0.4), Math.min(0.95,l+0.04))}165 ];166 167 const neutrals = [168 {label:'White', hex:'#ffffff'},169 {label:'Light Gray', hex:'#d1d5db'},170 {label:'Navy', hex:'#243447'},171 {label:'Beige', hex:'#d6c5a7'},172 {label:'Black', hex:'#0b0b0b'}173 ];174 175 const hue = ((h%360)+360)%360;176 let curated = [];177 178 if(hue >= 330 || hue < 30) {179 curated = [180 {label:'Navy', hex:'#243447'},181 {label:'Beige', hex:'#d6c5a7'},182 {label:'Denim', hex:'#1f3250'}183 ];184 } else if(hue >= 30 && hue < 90) {185 curated = [186 {label:'Navy', hex:'#243447'},187 {label:'Khaki', hex:'#8a7d4e'},188 {label:'Brown', hex:'#4b3426'}189 ];190 } else if(hue >= 90 && hue < 170) {191 curated = [192 {label:'Tan', hex:'#a57c4b'},193 {label:'Navy', hex:'#243447'},194 {label:'Cream', hex:'#f5efe6'}195 ];196 } else if(hue >= 170 && hue < 260) {197 curated = [198 {label:'White', hex:'#ffffff'},199 {label:'Khaki', hex:'#d6c5a7'},200 {label:'Burgundy', hex:'#6b2737'}201 ];202 } else {203 curated = [204 {label:'Gray', hex:'#9aa4b2'},205 {label:'Black', hex:'#0b0b0b'},206 {label:'Navy', hex:'#243447'}207 ];208 }209 210 const combined = [];211 tonal.forEach(t => combined.push({label:t.label, hex:t.hex.toLowerCase()}));212 curated.forEach(c => combined.push({label:c.label, hex:c.hex.toLowerCase()}));213 neutrals.forEach(n => combined.push({label:n.label, hex:n.hex.toLowerCase()}));214 215 const seen = new Set();216 return combined.filter(i => {217 if(!i.hex) return false;218 if(seen.has(i.hex)) return false;219 seen.add(i.hex);220 return true;221 });222 } catch(e) {223 return [];224 }225}226 227// Helper functions228function hexToRgb(hex) {229 hex = (hex || '').replace('#','');230 if(hex.length === 3) hex = hex.split('').map(c => c+c).join('');231 const int = parseInt(hex, 16) || 0;232 return {233 r: (int >> 16) & 255,234 g: (int >> 8) & 255,235 b: int & 255236 };237}238 239function rgbToHsl(r, g, b) {240 r /= 255; g /= 255; b /= 255;241 const max = Math.max(r,g,b), min = Math.min(r,g,b);242 let h = 0, s = 0, l = (max + min) / 2;243 244 if(max !== min) {245 const d = max - min;246 s = l > 0.5 ? d / (2 - max - min) : d / (max + min);247 switch(max) {248 case r: h = (g - b) / d + (g < b ? 6 : 0); break;249 case g: h = (b - r) / d + 2; break;250 case b: h = (r - g) / d + 4; break;251 }252 h = Math.round(h * 60);253 }254 return {h: s === 0 ? 0 : h, s: +(s.toFixed(3)), l: +(l.toFixed(3))};255}256 257function hslToRgb(h, s, l) {258 h = ((h % 360) + 360) % 360;259 s = Math.max(0, Math.min(1, s));260 l = Math.max(0, Math.min(1, l));261 262 if(s === 0) {263 const v = Math.round(l * 255);264 return {r: v, g: v, b: v};265 }266 267 const q = l < 0.5 ? l * (1 + s) : l + s - l * s;268 const p = 2 * l - q;269 const hk = h / 360;270 271 const tfunc = tt => {272 if(tt < 0) tt += 1;273 if(tt > 1) tt -= 1;274 if(tt < 1/6) return p + (q - p) * 6 * tt;275 if(tt < 1/2) return q;276 if(tt < 2/3) return p + (q - p) * (2/3 - tt) * 6;277 return p;278 };279 280 return {281 r: Math.round(tfunc(hk + 1/3) * 255),282 g: Math.round(tfunc(hk) * 255),283 b: Math.round(tfunc(hk - 1/3) * 255)284 };285}286 287function hslToHex(h, s, l) {288 const {r,g,b} = hslToRgb(h,s,l);289 return rgbToHex(r,g,b);290}291 292function rgbToHex(r, g, b) {293 const toHex = v => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0');294 return '#' + toHex(r) + toHex(g) + toHex(b);295}296// Update color suggestions with compatibility scoring297function updateColorSuggestions() {298 if (state.selectedTop && state.selectedTopColor) {299 state.suggestedColors = getColorSuggestions(state.selectedTopColor);300 301 // Update color palette component with enhanced suggestions302 const colorPalette = document.querySelector('custom-color-palette');303 if (colorPalette) {304 const enhancedSuggestions = state.suggestedColors.map(color => {305 const score = computeCompatibility(state.selectedTopColor, color.hex);306 return {307 ...color,308 score: score309 };310 }).sort((a, b) => b.score - a.score);311 312 colorPalette.setAttribute('colors', JSON.stringify({313 base: state.selectedTopColor,314 suggestions: enhancedSuggestions315 }));316 }317 } else if (state.selectedBottom && state.selectedBottomColor) {318 state.suggestedColors = getColorSuggestions(state.selectedBottomColor);319 320 // Update color palette component321 const colorPalette = document.querySelector('custom-color-palette');322 if (colorPalette) {323 const enhancedSuggestions = state.suggestedColors.map(color => {324 const score = computeCompatibility(state.selectedBottomColor, color.hex);325 return {326 ...color,327 score: score328 };329 }).sort((a, b) => b.score - a.score);330 331 colorPalette.setAttribute('colors', JSON.stringify({332 base: state.selectedBottomColor,333 suggestions: enhancedSuggestions334 }));335 }336 }337}338 339// Compatibility scoring function340function computeCompatibility(hexA, hexB) {341 try {342 const a = rgbToHsl(...Object.values(hexToRgb(hexA)));343 const b = rgbToHsl(...Object.values(hexToRgb(hexB)));344 345 const hueDist = Math.min(Math.abs(a.h - b.h), 360 - Math.abs(a.h - b.h));346 const toneSim = +(1 - (hueDist / 180)).toFixed(3);347 const satSim = +(1 - Math.abs(a.s - b.s)).toFixed(3);348 const lightSim = +(1 - Math.abs(a.l - b.l)).toFixed(3);349 350 const weights = {351 hue: 0.55,352 sat: 0.225,353 light: 0.225354 };355 356 const base = +(weights.hue * toneSim + weights.sat * satSim + weights.light * lightSim).toFixed(3);357 const neutralBoost = (a.s < 0.08 || b.s < 0.08) ? 0.12 : 0;358 const compBonus = (Math.abs(hueDist - 180) < 12) ? 0.07 : 0;359 360 let raw = base + neutralBoost + compBonus;361 raw = Math.max(0, Math.min(1, raw));362 363 return Math.round(raw * 100);364 } catch(e) {365 return 0;366 }367}368// Update outfit preview369 function updateOutfitPreview() {370 if (outfitPreview) {371 outfitPreview.setAttribute('top-image', state.selectedTop?.image || '');372 outfitPreview.setAttribute('bottom-image', state.selectedBottom?.image || '');373 outfitPreview.setAttribute('top-color', state.selectedTopColor || '');374 outfitPreview.setAttribute('bottom-color', state.selectedBottomColor || '');375 }376 }377 378 // Initialize379 renderClothingItems();380});