aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/theme-studio.html
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/theme-studio/theme-studio.html')
-rw-r--r--scripts/theme-studio/theme-studio.html53
1 files changed, 34 insertions, 19 deletions
diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html
index 99349a38..7d3e4fe5 100644
--- a/scripts/theme-studio/theme-studio.html
+++ b/scripts/theme-studio/theme-studio.html
@@ -339,6 +339,29 @@ function rgb2hsv(r, g, b) {
function rgb2hex(r, g, b) {
return '#' + [r, g, b].map(x => Math.max(0, Math.min(255, x)).toString(16).padStart(2, '0')).join('');
}
+
+// One Chroma×Lightness plane cell at a fixed hue: the sRGB color if the (L,C,H)
+// is reachable, else flagged out of gamut. Forward-only (one conversion + a
+// range check) — the binary-search clamp is reserved for committing a color.
+function planeCell(L, C, H) {
+ const lab = oklch2oklab(L, C, H), lrgb = oklab2lrgb(lab.L, lab.a, lab.b);
+ return inGamut(lrgb) ? { inGamut: true, hex: lrgb2hex(lrgb) } : { inGamut: false, hex: null };
+}
+
+// Pairwise palette analysis. palette is [[hex, name], ...]. Returns the pairs
+// closer than threshold (OKLab ΔE), closest-first and capped, the overflow count
+// beyond the cap, and each color's nearest-neighbor distance for its chip title.
+function paletteWarnings(palette, threshold = 0.02, cap = 5) {
+ const n = palette.length, nearest = new Array(n).fill(Infinity), pairs = [];
+ for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) {
+ const d = deltaE(palette[i][0], palette[j][0]);
+ if (d < nearest[i]) nearest[i] = d;
+ if (d < nearest[j]) nearest[j] = d;
+ if (d < threshold) pairs.push({ i, j, aName: palette[i][1], bName: palette[j][1], dE: d });
+ }
+ pairs.sort((a, b) => a.dE - b.dE);
+ return { warnings: pairs.slice(0, cap), overflow: Math.max(0, pairs.length - cap), nearest };
+}
function textOn(h){const L=rl(h);return ((L+0.05)/0.05)>(1.05/(L+0.05))?'#000':'#fff';}
function ratingColor(r){return r>=7?'#5d9b86':r>=4.5?'#a9b2bb':'#cb6b4d';}
function cid(l){return l.replace(/\W/g,'');}
@@ -387,26 +410,18 @@ function buildTable(){
let dragFrom=null,selectedIdx=null;
// Pairwise OKLab ΔE over the palette. Returns the sub-threshold pairs (sorted
// closest-first) and each color's nearest-neighbor distance for its chip title.
-function paletteDeltas(){
- const n=PALETTE.length,nearest=new Array(n).fill(Infinity),pairs=[];
- for(let i=0;i<n;i++)for(let j=i+1;j<n;j++){const d=deltaE(PALETTE[i][0],PALETTE[j][0]);
- if(d<nearest[i])nearest[i]=d;if(d<nearest[j])nearest[j]=d;
- if(d<DELTAE_MIN)pairs.push({i,j,d});}
- pairs.sort((a,b)=>a.d-b.d);
- return {pairs,nearest};
-}
-function renderPaletteWarnings(pairs){
+// Pure pairwise ΔE analysis lives in colormath.js (paletteWarnings); this renders it.
+function renderPaletteWarnings(warnings,overflow){
const w=document.getElementById('palwarn');if(!w)return;
- if(!pairs.length){w.style.display='none';w.innerHTML='';return;}
- const cap=5,shown=pairs.slice(0,cap);
+ if(!warnings.length){w.style.display='none';w.innerHTML='';return;}
let html='<div class="pwh">too-similar colors</div>';
- html+=shown.map(p=>`<div class="pwl">${esc(PALETTE[p.i][1]+' / '+PALETTE[p.j][1])} — \u0394E ${p.d.toFixed(3)}, hard to distinguish</div>`).join('');
- if(pairs.length>cap)html+=`<div class="pwl">and ${pairs.length-cap} more</div>`;
+ html+=warnings.map(p=>`<div class="pwl">${esc(p.aName+' / '+p.bName)} — \u0394E ${p.dE.toFixed(3)}, hard to distinguish</div>`).join('');
+ if(overflow>0)html+=`<div class="pwl">and ${overflow} more</div>`;
w.innerHTML=html;w.style.display='block';
}
function renderPalette(){
const p=document.getElementById('pals');p.innerHTML='';
- const {pairs,nearest}=paletteDeltas();
+ const {warnings,overflow,nearest}=paletteWarnings(PALETTE,DELTAE_MIN,5);
PALETTE.forEach((pc,i)=>{const [hex,name]=pc;const tc=textOn(hex);
const nde=nearest[i];
const locked=(hex===MAP['bg']||hex===MAP['p']);
@@ -427,7 +442,7 @@ function renderPalette(){
d.ondragleave=()=>d.classList.remove('over');
d.ondrop=(e)=>{e.preventDefault();d.classList.remove('over');if(dragFrom===null||dragFrom===i)return;const m=PALETTE.splice(dragFrom,1)[0];PALETTE.splice(i,0,m);dragFrom=null;selectedIdx=null;renderPalette();buildTable();buildUITable();};
p.appendChild(d);});
- renderPaletteWarnings(pairs);
+ renderPaletteWarnings(warnings,overflow);
buildUITable();if(document.getElementById('pkgbody'))buildPkgTable();
}
function notify(msg,err){const m=document.getElementById('palmsg');if(!m)return;m.textContent=msg;m.style.color=err?'#cb6b4d':'#8a9496';m.style.opacity='1';clearTimeout(m._t);m._t=setTimeout(()=>{m.style.opacity='0';},err?4000:2800);}
@@ -466,10 +481,10 @@ function paintOklchPlane(H){
if(_planeCache.key===key&&_planeCache.data){ctx.putImageData(_planeCache.data,0,0);return;}
const step=4;
for(let x=0;x<w;x+=step){const C=(x/w)*OKLCH_CMAX;
- for(let y=0;y<h;y+=step){const L=1-y/h,lab=oklch2oklab(L,C,H),lrgb=oklab2lrgb(lab.L,lab.a,lab.b);
- if(!inGamut(lrgb)){ctx.fillStyle='#15120f';ctx.fillRect(x,y,step,step);continue;}
- const hex=lrgb2hex(lrgb);ctx.fillStyle=hex;ctx.fillRect(x,y,step,step);
- if(T&&contrast(hex,MAP['bg'])<T){ctx.fillStyle='rgba(8,7,6,0.66)';ctx.fillRect(x,y,step,step);}}}
+ for(let y=0;y<h;y+=step){const L=1-y/h,cell=planeCell(L,C,H);
+ if(!cell.inGamut){ctx.fillStyle='#15120f';ctx.fillRect(x,y,step,step);continue;}
+ ctx.fillStyle=cell.hex;ctx.fillRect(x,y,step,step);
+ if(T&&contrast(cell.hex,MAP['bg'])<T){ctx.fillStyle='rgba(8,7,6,0.66)';ctx.fillRect(x,y,step,step);}}}
_planeCache={key,data:ctx.getImageData(0,0,w,h)};
}
function paintPicker(){const sv=document.getElementById('sv');if(!sv)return;