From 1d51a332b96c008b9b22f7597fb590d4438be0d5 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Tue, 9 Jun 2026 18:39:58 -0500 Subject: feat(theme-studio): add the ramp generator core ramp(baseHex, {n, stepL, chromaEase}) in app-core.js turns one base color into a tonal ramp: 2n steps at offsets -n..-1 and +1..+n, ordered darkest to lightest, base excluded. It holds the OKLCH hue, steps lightness by stepL, eases chroma toward the extremes so only the farthest step loses most of its color, and gamut-clamps each step with its own clamped flag. Bad input returns a structured result rather than throwing: an unparseable base gives {steps: [], error: 'bad-hex'}, and out-of-range n/stepL/chromaEase clamp into range with the clamped knob named in adjusted. Defaults are n=2, stepL=0.08, chromaEase=0.5. This is Phase 1 of the palette-ramps spec: pure logic, no UI. Tests cover mid/near-white/near-black bases, hue-hold, chroma easing, knob clamping, and malformed hex. The integrity stripper for app-core.js now drops import lines too, since the core imports normHex and the colormath helpers for the Node tests (stripped on inline, where both are already in scope). --- scripts/theme-studio/theme-studio.html | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) (limited to 'scripts/theme-studio/theme-studio.html') diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html index 50e0a519..e81fb59c 100644 --- a/scripts/theme-studio/theme-studio.html +++ b/scripts/theme-studio/theme-studio.html @@ -386,6 +386,10 @@ function paletteWarnings(palette, threshold = 0.02, cap = 5) { // the browser runs the same code the tests import. The app.js wrappers (pname, // seedPkgmap, ddList, pkgEffFg, pkgEffBg) are thin delegators that pass the // live PALETTE / APPS / PKGMAP into these. +// +// The imports below are for the Node tests; generate.py strips them on inline, +// where normHex (app-util.js) and the colormath helpers are already present from +// the bodies inlined above this one. // Resolve a palette name (or a raw #hex) to a hex; null when the name is unknown. function nameToHex(n,palette){if(!n)return null;if(/^#/.test(n))return n;const p=palette.find(p=>p[1]===n);return p?p[0]:null;} @@ -410,6 +414,38 @@ function optList(cur,palette){const have=cur===''||palette.some(p=>p[0]===cur);r // Turn a theme name into a safe filename slug: collapse runs of disallowed // characters to a single dash, trim leading/trailing dashes, fall back to 'theme'. function slugify(name){return name.replace(/[^A-Za-z0-9._-]+/g,'-').replace(/^-+|-+$/g,'')||'theme';} + +// Generate a tonal ramp from one base color: 2n steps at offsets -n..-1 and +// +1..+n (the base itself is excluded — it already lives in the palette), +// ordered darkest -> lightest. Holds the OKLCH hue, steps lightness by stepL per +// stop, and eases chroma toward the extremes (quadratic in |offset|/n, so only +// the farthest step loses most of its color). Every step is gamut-clamped and +// carries its own clamped flag. Returns {steps:[{hex,clamped,offset}], adjusted} +// where adjusted names any knob clamped/rounded into range, or {steps:[], +// error:'bad-hex'} for an unparseable base. Pure — opts are clamped, never thrown. +function ramp(baseHex,opts){ + const hex=typeof baseHex==='string'?normHex(baseHex):null; + if(!hex)return {steps:[],error:'bad-hex'}; + const o=opts||{},adjusted=[]; + const knob=(name,def,lo,hi,isInt)=>{ + const v=o[name]; + if(typeof v!=='number'||!isFinite(v))return def; + const r=isInt?Math.round(v):v,c=Math.min(hi,Math.max(lo,r)); + if(c!==v)adjusted.push(name); + return c; + }; + const n=knob('n',2,1,4,true),stepL=knob('stepL',0.08,0.04,0.12,false),chromaEase=knob('chromaEase',0.5,0,1,false); + const {L:L0,C:C0,H:H0}=oklab2oklch(srgb2oklab(hex)); + const steps=[]; + for(let off=-n;off<=n;off++){ + if(off===0)continue; + const L=Math.min(1,Math.max(0,L0+off*stepL)); + const t=Math.abs(off)/n,C=C0*(1-chromaEase*t*t); + const {hex:h,clamped}=oklch2hex(L,C,H0); + steps.push({hex:h,clamped,offset:off}); + } + return {steps,adjusted}; +} // Pure color/UI-boundary helpers (normHex, ratingColor, textOn), inlined from // app-util.js. textOn uses rl from the colormath core above. // Pure color/UI-boundary helpers: hex-input parsing, the contrast-rating status -- cgit v1.2.3