diff --git a/shop-mode.html b/shop-mode.html index 2b1a536..aa03c0f 100644 --- a/shop-mode.html +++ b/shop-mode.html @@ -170,18 +170,23 @@
- Colore estratto: -
- +
+ Colore Brand (media palette): +
+ +
+
+ Palette estratta: +
+ +
+
+
> @@ -675,6 +680,7 @@ }, logoColor: null, + colorPalette: [], cliente: { nome: "", @@ -725,131 +731,153 @@ reader.readAsDataURL(file); }, - extractDominantColor(imageData) { - const img = new Image(); - img.onload = () => { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - canvas.width = img.width; - canvas.height = img.height; - ctx.drawImage(img, 0, 0); + +extractDominantColor(imageData) { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); - try { - const imgData = ctx.getImageData( - 0, - 0, - canvas.width, - canvas.height, - ); - const data = imgData.data; + try { + const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imgData.data; - // Collect color palette with counts - const colorMap = {}; - for (let i = 0; i < data.length; i += 16) { - // Sample every 4 pixels for better coverage - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - const a = data[i + 3]; + // Step 1: Collect all valid colors + const colors = []; + for (let i = 0; i < data.length; i += 16) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const a = data[i + 3]; - // Skip transparent pixels - if (a < 125) continue; + // Skip transparent + if (a < 125) continue; - // Skip whites (> 240 in all channels) - if (r > 240 && g > 240 && b > 240) continue; + // Skip whites (> 235) + if (r > 235 && g > 235 && b > 235) continue; - // Skip blacks (< 15 in all channels) - if (r < 15 && g < 15 && b < 15) continue; + // Skip blacks (< 20) + if (r < 20 && g < 20 && b < 20) continue; - // Skip grays (low saturation: when R≈G≈B) - const maxChan = Math.max(r, g, b); - const minChan = Math.min(r, g, b); - const saturation = - maxChan === 0 - ? 0 - : (maxChan - minChan) / maxChan; - if (saturation < 0.15) continue; // Skip low saturation (grays) + // Calculate saturation to skip grays + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const saturation = max === 0 ? 0 : (max - min) / max; + if (saturation < 0.2) continue; // Skip grays - // Quantize colors to reduce variations (group similar colors) - const qr = Math.round(r / 10) * 10; - const qg = Math.round(g / 10) * 10; - const qb = Math.round(b / 10) * 10; - const rgb = `${qr},${qg},${qb}`; - colorMap[rgb] = (colorMap[rgb] || 0) + 1; - } + colors.push({ r, g, b }); + } - // Build palette with color scores - const palette = []; - for (const [rgb, count] of Object.entries( - colorMap, - )) { - const [r, g, b] = rgb - .split(",") - .map(Number); + if (colors.length === 0) { + this.logoColor = "#10b981"; + this.colorPalette = []; + console.log("No valid colors found, using fallback"); + return; + } - // Calculate HSL for better color selection - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const l = (max + min) / 2 / 255; // Lightness 0-1 - const s = - max === min - ? 0 - : (max - min) / (max + min); // Saturation 0-1 + // Step 2: K-means clustering to extract palette (5-7 colors) + const numClusters = Math.min(6, Math.max(3, Math.floor(colors.length / 50))); + const palette = this.kMeansClustering(colors, numClusters); - // Skip too light (> 0.85) or too dark (< 0.20) - if (l > 0.85 || l < 0.2) continue; + // Step 3: Filter palette colors by lightness + const filteredPalette = palette.filter(color => { + const max = Math.max(color.r, color.g, color.b); + const min = Math.min(color.r, color.g, color.b); + const l = (max + min) / 2 / 255; + return l > 0.25 && l < 0.80; // Keep mid-range lightness + }); - // Calculate color "score" based on: - // - Saturation (higher is better) - // - Frequency (more common is better) - // - Ideal lightness (0.4-0.6 is best) - const lightnessScore = - 1 - Math.abs(l - 0.5) * 2; // Peaks at 0.5 - const score = - s * 2 + count / 100 + lightnessScore; + if (filteredPalette.length === 0) { + this.logoColor = "#10b981"; + this.colorPalette = []; + console.log("No suitable colors in palette, using fallback"); + return; + } - palette.push({ - r, - g, - b, - count, - saturation: s, - lightness: l, - score, - }); - } + // Step 4: Calculate average color from palette + const avgR = Math.round(filteredPalette.reduce((sum, c) => sum + c.r, 0) / filteredPalette.length); + const avgG = Math.round(filteredPalette.reduce((sum, c) => sum + c.g, 0) / filteredPalette.length); + const avgB = Math.round(filteredPalette.reduce((sum, c) => sum + c.b, 0) / filteredPalette.length); - // Sort by score (best colors first) - palette.sort((a, b) => b.score - a.score); + // Store results + this.logoColor = this.rgbToHex(avgR, avgG, avgB); + this.colorPalette = filteredPalette.map(c => this.rgbToHex(c.r, c.g, c.b)); - // Get the best color from palette - if (palette.length > 0) { - const bestColor = palette[0]; - this.logoColor = this.rgbToHex( - bestColor.r, - bestColor.g, - bestColor.b, - ); - console.log( - "Extracted color:", - this.logoColor, - "from palette of", - palette.length, - "colors", - ); - } else { - this.logoColor = "#10b981"; // Fallback emerald - console.log( - "No suitable colors found, using fallback", - ); - } - } catch (err) { - console.error("Error extracting color:", err); - this.logoColor = "#10b981"; // Fallback emerald - } - }; - img.src = imageData; - }, + console.log("Extracted palette:", this.colorPalette); + console.log("Average brand color:", this.logoColor); + } catch (err) { + console.error("Error extracting color:", err); + this.logoColor = "#10b981"; + this.colorPalette = []; + } + }; + img.src = imageData; +}, + +kMeansClustering(colors, k) { + // Initialize centroids randomly + let centroids = []; + const shuffled = [...colors].sort(() => Math.random() - 0.5); + for (let i = 0; i < k; i++) { + centroids.push({ ...shuffled[i % shuffled.length] }); + } + + // K-means iterations + for (let iter = 0; iter < 10; iter++) { + // Assign colors to nearest centroid + const clusters = Array(k).fill(null).map(() => []); + + colors.forEach(color => { + let minDist = Infinity; + let closestIdx = 0; + + centroids.forEach((centroid, idx) => { + const dist = Math.sqrt( + Math.pow(color.r - centroid.r, 2) + + Math.pow(color.g - centroid.g, 2) + + Math.pow(color.b - centroid.b, 2) + ); + if (dist < minDist) { + minDist = dist; + closestIdx = idx; + } + }); + + clusters[closestIdx].push(color); + }); + + // Update centroids + const newCentroids = clusters.map(cluster => { + if (cluster.length === 0) return centroids[0]; // Fallback + + const avgR = Math.round(cluster.reduce((sum, c) => sum + c.r, 0) / cluster.length); + const avgG = Math.round(cluster.reduce((sum, c) => sum + c.g, 0) / cluster.length); + const avgB = Math.round(cluster.reduce((sum, c) => sum + c.b, 0) / cluster.length); + + return { r: avgR, g: avgG, b: avgB }; + }); + + // Check convergence + const converged = centroids.every((c, i) => + c.r === newCentroids[i].r && + c.g === newCentroids[i].g && + c.b === newCentroids[i].b + ); + + centroids = newCentroids; + if (converged) break; + } + + // Sort by saturation (most saturated first) + return centroids.sort((a, b) => { + const satA = (Math.max(a.r, a.g, a.b) - Math.min(a.r, a.g, a.b)) / Math.max(a.r, a.g, a.b); + const satB = (Math.max(b.r, b.g, b.b) - Math.min(b.r, b.g, b.b)) / Math.max(b.r, b.g, b.b); + return satB - satA; + }); +}, rgbToHex(r, g, b) { return (