refactor(theme): switch color generation from HSL to OKLCH
OKLCH is a perceptually uniform color space — equal lightness values produce equal perceived brightness across all hues, unlike HSL where blue at 50% looks much darker than yellow at 50%. Color scales now output oklch() CSS values directly: --gray-500: oklch(0.530 0.035 285); --accent-500: oklch(0.595 0.230 286); The browser handles gamut mapping natively. Scale definitions in tokens.edn use [label lightness chroma] tuples where L is 0-1 perceptual lightness, C is chroma (colorfulness), H is hue degrees. Theme adapter updated: sliders now control OKLCH hue/chroma, swatches render with oklch() CSS, Copy EDN outputs OKLCH config. gen.clj includes oklch->srgb and oklch->hex for validation/tools.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
{;; ─── Scales ────────────────────────────────────────────────────────
|
||||
;; Generated into :root only. Not duplicated in dark theme blocks.
|
||||
;; Color scales use HSL: [label lightness] or [label lightness saturation].
|
||||
;; Change hue/saturation to shift the entire palette (e.g. purplish gray).
|
||||
;; Color scales use OKLCH: [label lightness] or [label lightness chroma].
|
||||
;; OKLCH is perceptually uniform — equal L steps = equal perceived brightness.
|
||||
;; Change hue/chroma to shift the entire palette (e.g. purplish gray).
|
||||
|
||||
:scales
|
||||
{:size {:base 0.25 :unit "rem" :steps 16}
|
||||
@@ -17,76 +18,76 @@
|
||||
[5 "3xl"]]}
|
||||
|
||||
:color
|
||||
{;; Gray — purplish tint (hue 240). Change hue for warm/cool grays.
|
||||
;; Saturation tapers at lighter end for subtlety.
|
||||
:gray {:hue 240 :saturation 18
|
||||
:steps [[50 97 14]
|
||||
[100 95 14]
|
||||
[200 90 12]
|
||||
[300 82 10]
|
||||
[400 64 10]
|
||||
[500 46 10]
|
||||
[600 34 12]
|
||||
[700 26 16]
|
||||
[800 15 18]
|
||||
[900 9 18]
|
||||
[950 5 18]]}
|
||||
{;; Gray — purplish tint (hue 285 in OKLCH). Change hue for warm/cool grays.
|
||||
;; Chroma tapers at light/dark extremes for subtlety, peaks in mid-range.
|
||||
:gray {:hue 285 :chroma 0.025
|
||||
:steps [[50 0.975 0.003]
|
||||
[100 0.955 0.005]
|
||||
[200 0.915 0.010]
|
||||
[300 0.850 0.012]
|
||||
[400 0.690 0.025]
|
||||
[500 0.530 0.035]
|
||||
[600 0.425 0.035]
|
||||
[700 0.350 0.035]
|
||||
[800 0.245 0.025]
|
||||
[900 0.190 0.016]
|
||||
[950 0.145 0.011]]}
|
||||
|
||||
;; Accent — vivid purple, matching activity-tracker (#7c5cfc ≈ hsl 252 96 67)
|
||||
:accent {:hue 252 :saturation 96
|
||||
:steps [[50 97 100]
|
||||
[100 94 95]
|
||||
[200 89 95]
|
||||
[300 82 95]
|
||||
[400 75 93]
|
||||
[500 67 96]
|
||||
[600 57 84]
|
||||
[700 49 72]
|
||||
[800 41 69]
|
||||
[900 34 67]
|
||||
[950 22 73]]}
|
||||
;; Accent — vivid purple, matching activity-tracker (#7c5cfc ≈ oklch 0.60 0.23 286)
|
||||
:accent {:hue 286 :chroma 0.23
|
||||
:steps [[50 0.965 0.020]
|
||||
[100 0.925 0.040]
|
||||
[200 0.860 0.075]
|
||||
[300 0.770 0.125]
|
||||
[400 0.690 0.170]
|
||||
[500 0.595 0.230]
|
||||
[600 0.505 0.255]
|
||||
[700 0.450 0.245]
|
||||
[800 0.395 0.210]
|
||||
[900 0.350 0.175]
|
||||
[950 0.260 0.130]]}
|
||||
|
||||
;; Danger — red
|
||||
:danger {:hue 0 :saturation 84
|
||||
:steps [[50 97 86]
|
||||
[100 94 93]
|
||||
[200 87 95]
|
||||
[300 77 90]
|
||||
[400 66 86]
|
||||
[500 55 84]
|
||||
[600 47 80]
|
||||
[700 40 74]
|
||||
[800 33 70]
|
||||
[900 27 65]
|
||||
[950 15 75]]}
|
||||
:danger {:hue 25 :chroma 0.22
|
||||
:steps [[50 0.970 0.014]
|
||||
[100 0.935 0.032]
|
||||
[200 0.860 0.073]
|
||||
[300 0.765 0.127]
|
||||
[400 0.675 0.184]
|
||||
[500 0.610 0.226]
|
||||
[600 0.560 0.220]
|
||||
[700 0.490 0.184]
|
||||
[800 0.425 0.153]
|
||||
[900 0.365 0.124]
|
||||
[950 0.255 0.086]]}
|
||||
|
||||
;; Success — green
|
||||
:success {:hue 142 :saturation 71
|
||||
:steps [[50 97 78]
|
||||
[100 93 77]
|
||||
[200 87 73]
|
||||
[300 77 72]
|
||||
[400 61 72]
|
||||
[500 44 69]
|
||||
[600 36 64]
|
||||
[700 30 57]
|
||||
[800 24 51]
|
||||
[900 20 46]
|
||||
[950 12 50]]}
|
||||
:success {:hue 152 :chroma 0.18
|
||||
:steps [[50 0.980 0.016]
|
||||
[100 0.960 0.038]
|
||||
[200 0.930 0.065]
|
||||
[300 0.885 0.112]
|
||||
[400 0.815 0.178]
|
||||
[500 0.705 0.185]
|
||||
[600 0.595 0.150]
|
||||
[700 0.510 0.119]
|
||||
[800 0.425 0.090]
|
||||
[900 0.370 0.071]
|
||||
[950 0.270 0.051]]}
|
||||
|
||||
;; Warning — amber
|
||||
:warning {:hue 38 :saturation 92
|
||||
:steps [[50 97 89]
|
||||
[100 93 95]
|
||||
[200 87 96]
|
||||
[300 78 95]
|
||||
[400 68 93]
|
||||
[500 55 92]
|
||||
[600 47 88]
|
||||
[700 40 80]
|
||||
[800 33 75]
|
||||
[900 27 70]
|
||||
[950 18 72]]}}}
|
||||
:warning {:hue 76 :chroma 0.16
|
||||
:steps [[50 0.985 0.012]
|
||||
[100 0.965 0.032]
|
||||
[200 0.935 0.058]
|
||||
[300 0.890 0.095]
|
||||
[400 0.845 0.129]
|
||||
[500 0.790 0.159]
|
||||
[600 0.725 0.153]
|
||||
[700 0.625 0.130]
|
||||
[800 0.540 0.107]
|
||||
[900 0.465 0.088]
|
||||
[950 0.355 0.065]]}}}
|
||||
|
||||
;; ─── Semantic tokens (light theme) ────────────────────────────────
|
||||
;; Reference color scale variables with var(--scale-step).
|
||||
|
||||
@@ -30,6 +30,43 @@
|
||||
(str/replace #"0+$" "")
|
||||
(str/replace #"\.$" ""))))
|
||||
|
||||
;; ── OKLCH ────────────────────────────────────────────────────────
|
||||
|
||||
(defn oklch->srgb
|
||||
"Convert OKLCH [L C H] to sRGB [r g b] (0-255 clamped).
|
||||
L: 0-1, C: 0-~0.4, H: 0-360 degrees."
|
||||
[[l c h]]
|
||||
(let [h-rad (* h (/ Math/PI 180))
|
||||
;; OKLCH → OKLab
|
||||
a (* c (Math/cos h-rad))
|
||||
b (* c (Math/sin h-rad))
|
||||
;; OKLab → LMS (cube roots)
|
||||
l_ (+ l (* 0.3963377774 a) (* 0.2158037573 b))
|
||||
m_ (+ l (* -0.1055613458 a) (* -0.0638541728 b))
|
||||
s_ (+ l (* -0.0894841775 a) (* -1.2914855480 b))
|
||||
;; Cube to get LMS
|
||||
l3 (* l_ l_ l_)
|
||||
m3 (* m_ m_ m_)
|
||||
s3 (* s_ s_ s_)
|
||||
;; LMS → linear sRGB
|
||||
r-lin (+ (* 4.0767416621 l3) (* -3.3077115913 m3) (* 0.2309699292 s3))
|
||||
g-lin (+ (* -1.2684380046 l3) (* 2.6097574011 m3) (* -0.3413193965 s3))
|
||||
b-lin (+ (* -0.0041960863 l3) (* -0.7034186147 m3) (* 1.7076147010 s3))
|
||||
;; Linear sRGB → sRGB (gamma)
|
||||
gamma (fn [x]
|
||||
(if (<= x 0.0031308)
|
||||
(* 12.92 x)
|
||||
(- (* 1.055 (Math/pow (max 0.0 x) (/ 1.0 2.4))) 0.055)))
|
||||
clamp (fn [x] (max 0 (min 255 (int (Math/round (* 255.0 (gamma (max 0.0 x))))))))]
|
||||
[(clamp r-lin) (clamp g-lin) (clamp b-lin)]))
|
||||
|
||||
(defn oklch->hex
|
||||
"Convert OKLCH [L C H] to hex string. Clamps to sRGB gamut."
|
||||
[[l c h]]
|
||||
(color/rgb->hex (oklch->srgb [l c h])))
|
||||
|
||||
;; ── Scale generation ─────────────────────────────────────────────
|
||||
|
||||
(defn generate-size-scale
|
||||
"Generate linear size scale: --size-N = base * N."
|
||||
[{:keys [base unit steps]}]
|
||||
@@ -49,17 +86,20 @@
|
||||
(str/join "\n")))
|
||||
|
||||
(defn generate-color-scale
|
||||
"Generate CSS variables for a named color scale.
|
||||
Each step is [label lightness] or [label lightness saturation].
|
||||
Uses hsl->hex from jon.color-tools for conversion."
|
||||
[scale-name {:keys [hue saturation steps]}]
|
||||
"Generate CSS variables for a named OKLCH color scale.
|
||||
Each step is [label lightness] or [label lightness chroma].
|
||||
Outputs oklch() CSS values for perceptual uniformity."
|
||||
[scale-name {:keys [hue chroma steps]}]
|
||||
(->> steps
|
||||
(map (fn [step]
|
||||
(let [[label lightness sat] (if (= 3 (count step))
|
||||
(let [[label lightness chr] (if (= 3 (count step))
|
||||
step
|
||||
[(first step) (second step) saturation])
|
||||
hex (color/hsl->hex [hue sat lightness])]
|
||||
(str " --" (name scale-name) "-" label ": " hex ";"))))
|
||||
[(first step) (second step) chroma])
|
||||
;; Format: oklch(L C H)
|
||||
css-val (str "oklch(" (format "%.3f" (double lightness))
|
||||
" " (format "%.4f" (double chr))
|
||||
" " (format "%.1f" (double hue)) ")")]
|
||||
(str " --" (name scale-name) "-" label ": " css-val ";"))))
|
||||
(str/join "\n")))
|
||||
|
||||
(defn generate-color-scales
|
||||
|
||||
Reference in New Issue
Block a user