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:
Florian Schroedl
2026-03-11 12:04:33 +01:00
parent 41811dba88
commit 59d46700bc
6 changed files with 222 additions and 175 deletions

View File

@@ -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