[object Object]

← back to Wallco Ai

tone-on-tone hex picker: sort by hue + collapse near-duplicate swatches

c07a04ea6e0b4242b8028f8725d4819c18fbb483 · 2026-05-28 06:47:23 -0700 · Steve Abrams

- hex→HSL, dedupe by 15° hue × 5 sat × 5 light buckets (most-saturated rep wins)
- sort chromatic by hue (then sat desc, light asc); achromatic (s<0.08) at the end by lightness
- result on current FASHION_PALETTES: 44 unique hexes → 35 deduped (~20% collapse)
- pure visual-luxe ask from Steve — 'sort in color wheel order and only show 1 of each'

Files touched

Diff

commit c07a04ea6e0b4242b8028f8725d4819c18fbb483
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 28 06:47:23 2026 -0700

    tone-on-tone hex picker: sort by hue + collapse near-duplicate swatches
    
    - hex→HSL, dedupe by 15° hue × 5 sat × 5 light buckets (most-saturated rep wins)
    - sort chromatic by hue (then sat desc, light asc); achromatic (s<0.08) at the end by lightness
    - result on current FASHION_PALETTES: 44 unique hexes → 35 deduped (~20% collapse)
    - pure visual-luxe ask from Steve — 'sort in color wheel order and only show 1 of each'
---
 server.js | 47 ++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 44 insertions(+), 3 deletions(-)

diff --git a/server.js b/server.js
index 3f957fe..ce731df 100644
--- a/server.js
+++ b/server.js
@@ -12397,13 +12397,54 @@ ${(() => {
     try { return require('./scripts/fashion_palettes.js').FASHION_PALETTES; }
     catch { return []; }
   })();
+  // hex (#RRGGBB) -> [h(0-360), s(0-1), l(0-1)]
+  const hsl = (hex) => {
+    const m = /^#([0-9a-f]{6})$/i.exec(hex);
+    if (!m) return null;
+    const r = parseInt(m[1].slice(0,2),16)/255, g = parseInt(m[1].slice(2,4),16)/255, b = parseInt(m[1].slice(4,6),16)/255;
+    const mx = Math.max(r,g,b), mn = Math.min(r,g,b), L = (mx+mn)/2;
+    if (mx === mn) return [0, 0, L];
+    const d = mx-mn;
+    const S = L > 0.5 ? d/(2-mx-mn) : d/(mx+mn);
+    let H = 0;
+    if (mx === r) H = ((g-b)/d) + (g<b?6:0);
+    else if (mx === g) H = ((b-r)/d) + 2;
+    else H = ((r-g)/d) + 4;
+    return [H*60, S, L];
+  };
+  // 1) Unique exact-hex collect (preserve old uppercase + #RRGGBB validation).
   const seen = new Set();
-  const hexes = [];
+  const all = [];
   for (const p of brands) for (const h of p.hex) {
     const k = String(h).toUpperCase();
-    if (k.length === 7 && k.startsWith('#') && !seen.has(k)) { seen.add(k); hexes.push(k); }
+    if (k.length === 7 && k.startsWith('#') && !seen.has(k)) {
+      const v = hsl(k);
+      if (v) { seen.add(k); all.push({ hex: k, hsl: v }); }
+    }
   }
-  return hexes.map(h => {
+  // 2) Near-duplicate collapse — bucket quantized HSL, keep the most-saturated rep
+  //    per bucket. Hue 15-deg buckets * sat 5 bands * light 5 bands. Achromatic
+  //    (s<0.08) bucketed by lightness only so all grays don't collapse to one.
+  const bucket = new Map();
+  for (const x of all) {
+    const [H, S, L] = x.hsl;
+    const gray = S < 0.08;
+    const key = gray
+      ? 'g:' + Math.round(L*8)
+      : Math.round(H/15) + ':' + Math.round(S*5) + ':' + Math.round(L*5);
+    const prev = bucket.get(key);
+    if (!prev || x.hsl[1] > prev.hsl[1]) bucket.set(key, x);
+  }
+  // 3) Sort: chromatic by hue (then sat desc, then light asc); grays at the end by lightness.
+  const rows = Array.from(bucket.values()).sort((a, b) => {
+    const ag = a.hsl[1] < 0.08, bg = b.hsl[1] < 0.08;
+    if (ag !== bg) return ag ? 1 : -1;
+    if (ag && bg) return a.hsl[2] - b.hsl[2];
+    if (a.hsl[0] !== b.hsl[0]) return a.hsl[0] - b.hsl[0];
+    if (a.hsl[1] !== b.hsl[1]) return b.hsl[1] - a.hsl[1];
+    return a.hsl[2] - b.hsl[2];
+  });
+  return rows.map(({hex: h}) => {
     return `              <button data-hex="${h}" title="${h}" aria-label="Recolor in ${h}" style="width:100%;aspect-ratio:1;background:${h};border:1px solid rgba(0,0,0,.12);border-radius:50%;cursor:pointer;padding:0;transition:transform .12s,box-shadow .12s" onmouseover="this.style.transform='scale(1.15)';this.style.boxShadow='0 2px 8px rgba(0,0,0,.18)'" onmouseout="this.style.transform='';this.style.boxShadow=''"></button>`;
   }).join('\n');
 })()}

← e9fe6eb cactus-curator: 'By-base view' toggle — 1 pattern + thumbs o  ·  back to Wallco Ai  ·  etsy export: add print_ready_in / print_ready_ft computed fi 8794315 →