diff options
Diffstat (limited to 'scripts/theme-studio/theme-studio.html')
| -rw-r--r-- | scripts/theme-studio/theme-studio.html | 40 |
1 files changed, 26 insertions, 14 deletions
diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index 8ef83cd2..ac8c285f 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -557,6 +557,11 @@ function lMax(hue,chroma,fgSet,target){ function oklchOf(hex){return oklab2oklch(srgb2oklab(hex));} function isReservedGroundLikeName(name){return /^(bg|fg)(?:[-_+].+|\d.*)$/i.test(name||'');} function isPureEndpointHex(hex){const h=(hex||'').toLowerCase();return h==='#ffffff'||h==='#000000';} +function interpOklabHex(a,b,t,offset){ + const lab={L:a.L+(b.L-a.L)*t,a:a.a+(b.a-a.a)*t,b:a.b+(b.b-a.b)*t}; + const lrgb=oklab2lrgb(lab.L,lab.a,lab.b); + return {hex:lrgb2hex(lrgb),offset,clamped:!inGamut(lrgb)}; +} function columnStem(name){name=name||'color';if(/^color-\d+$/.test(name))return name;name=name.replace(/[+-]\d+$/,'');return name.replace(/\d+$/,'')||'color';} function columnOffset(name){const m=(name||'').match(/([+-]\d+)$/);return m?parseInt(m[1],10):0;} function legacyColumnStem(name){return isReservedGroundLikeName(name)?name:columnStem(name);} @@ -654,17 +659,23 @@ function columnsFromPalette(palette,ground){ } return {ground:groundStrip,columns}; } -// Regenerate a column's members as a symmetric ramp around the base: n=0 is the -// base alone (without ramp()'s 1-4 clamp), n>=1 is base plus ramp() steps, sorted -// by offset. {members:[{hex,offset,clamped}]} or {members:[],error:'bad-hex'}. +// Regenerate a column's members as a symmetric span around the base: n=0 is the +// base alone, n>=1 divides the OKLab intervals black..base and base..white into +// n interior steps per side. Pure black/white endpoint duplicates and rounded +// base duplicates are skipped. {members:[{hex,offset,clamped}]} or +// {members:[],error:'bad-hex'}. function regenColumn(baseHex,n,opts){ const hex=typeof baseHex==='string'?normHex(baseHex):null; if(!hex)return {members:[],error:'bad-hex'}; - const k=Math.min(4,Math.max(0,Math.round(n||0))); + const k=Math.min(8,Math.max(0,Math.round(n||0))); if(k===0)return {members:[{hex,offset:0,clamped:false}]}; - const r=ramp(hex,Object.assign({},opts,{n:k})); - if(r.error)return {members:[],error:r.error}; - const members=[...r.steps.filter(s=>!isPureEndpointHex(s.hex)),{hex,offset:0,clamped:false}].sort((a,b)=>a.offset-b.offset); + const base=srgb2oklab(hex),black=srgb2oklab('#000000'),white=srgb2oklab('#ffffff'),steps=[]; + for(let i=1;i<=k;i++){ + const dark=interpOklabHex(black,base,i/(k+1),i-k-1); + const light=interpOklabHex(base,white,i/(k+1),i); + steps.push(dark,light); + } + const members=[...steps.filter(s=>!isPureEndpointHex(s.hex)&&s.hex.toLowerCase()!==hex),{hex,offset:0,clamped:false}].sort((a,b)=>a.offset-b.offset); return {members}; } // Rank a column's current member hexes by lightness and give each a signed offset @@ -1046,9 +1057,8 @@ function renderPalette(){ sw.innerHTML=`<input class="nm" value="${m.name||'ground'}" disabled style="color:${tc}"><div class="hx" style="color:${tc}">${m.hex}</div>`;gs.appendChild(sw);} }); } - // The too-similar warning stays on the full flat palette: a generated ramp's - // steps are a stepL apart (well above the warning's ΔE threshold), so they never - // trigger it, and any pair that does is a genuine near-duplicate worth flagging. + // The too-similar warning stays on the full flat palette, so large spans can + // still expose genuinely hard-to-distinguish neighboring colors. const ordered=sortColumns(columns); ordered.forEach((f,pos)=>{ const s=strip('');s.dataset.column=f.column||f.base; @@ -1064,11 +1074,11 @@ function renderPalette(){ function columnCountControl(f){ const per=Math.max(0,...rankByLightness(f.members.map(m=>m.hex),f.base).map(m=>Math.abs(m.offset))); const d=document.createElement('div');d.className='fcount'; - d.innerHTML=`<span title="set the column span: N generated steps on each side of the base — this replaces the column">span ± <input type="number" min="0" max="4" value="${per}"></span>`; - d.querySelector('input').onchange=(e)=>setColumnCount(f.base,Math.max(0,Math.min(4,parseInt(e.target.value,10)||0))); + d.innerHTML=`<span title="set the column span: N generated steps on each side of the base — this replaces the column">span ± <input type="number" min="0" max="8" value="${per}"></span>`; + d.querySelector('input').onchange=(e)=>setColumnCount(f.base,Math.max(0,Math.min(8,parseInt(e.target.value,10)||0))); return d; } -// Regenerate a column as a symmetric base ±N ramp, replacing its current members. +// Regenerate a column as a symmetric base ±N span, replacing its current members. // References to a surviving position (matched by signed lightness rank) follow the // new hex; references to a position removed by lowering N leave their old hex, // which is no longer in the palette and so renders as "(gone)". @@ -2162,6 +2172,8 @@ if(location.hash==='#counttest'){let ok=true;const notes=[];const A=(c,n)=>{if(! UIMAP['region']={fg:null,bg:innerOld,bold:false,italic:false,underline:false,strike:false}; UIMAP['highlight']={fg:null,bg:outerOld,bold:false,italic:false,underline:false,strike:false}; selectedIdx=null;renderPalette(); + const blueSpanInput=document.querySelector('#pals .fstrip[data-column="blue"] .fcount input'); + A(blueSpanInput&&blueSpanInput.max==='8','normal column span control allows up to 8 per side'); setColumnCount('#67809c',1); const palHexes=new Set(PALETTE.map(p=>p[0].toLowerCase())); A(!palHexes.has(outerOld.toLowerCase()),'outer step removed from palette on count down'); @@ -2171,7 +2183,7 @@ if(location.hash==='#counttest'){let ok=true;const notes=[];const A=(c,n)=>{if(! setColumnCount('#67809c',3); const want3=regenColumn('#67809c',3).members.map(m=>m.hex.toLowerCase()); const have=new Set(PALETTE.map(p=>p[0].toLowerCase())); - A(want3.every(h=>have.has(h)),'count up to 3 adds all 7 ramp colors to the palette'); + A(want3.every(h=>have.has(h)),'count up to 3 adds all 7 span colors to the palette'); PALETTE=saveP;for(const k in MAP)delete MAP[k];Object.assign(MAP,saveM);for(const f in UIMAP)delete UIMAP[f];Object.assign(UIMAP,saveU);selectedIdx=saveSel;renderPalette(); document.title='COUNTTEST '+(ok?'PASS':'FAIL'); const d=document.createElement('div');d.id='counttest';d.textContent='COUNTTEST '+(ok?'PASS':'FAIL')+(notes.length?' | '+notes.join(' ; '):'');document.body.appendChild(d);} |
