diff options
Diffstat (limited to 'scripts/theme-studio/theme-studio.html')
| -rw-r--r-- | scripts/theme-studio/theme-studio.html | 53 |
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; |
