1use gpui::{Hsla, hsla};
5
6pub const LEGIBLE: f32 = 4.5;
8
9pub fn neutral(lightness: f32) -> Hsla {
13 let [v, _, _] = oklch_to_srgb(lightness, 0.0, 0.0);
14 hsla(0.0, 0.0, v, 1.0)
15}
16
17pub fn grey(value: u8) -> Hsla {
20 hsla(0.0, 0.0, value as f32 / 255.0, 1.0)
21}
22
23pub fn oklch(l: f32, c: f32, h_deg: f32) -> Hsla {
25 let [r, g, b] = oklch_to_srgb(l, c, h_deg);
26 let (h, s, l) = rgb_to_hsl(r, g, b);
27 hsla(h, s, l, 1.0)
28}
29
30pub fn oklch_to_srgb(l: f32, c: f32, h_deg: f32) -> [f32; 3] {
33 let [r, g, b] = oklch_to_linear(l, c, h_deg);
34 [gamma_encode(r), gamma_encode(g), gamma_encode(b)]
35}
36
37fn oklch_to_linear(l: f32, c: f32, h_deg: f32) -> [f32; 3] {
40 let h = h_deg.to_radians();
41 oklab_to_linear(l, c * h.cos(), c * h.sin())
42}
43
44fn oklab_to_linear(l: f32, a: f32, b: f32) -> [f32; 3] {
46 let l_ = l + 0.396_337_78 * a + 0.215_803_76 * b;
48 let m_ = l - 0.105_561_346 * a - 0.063_854_17 * b;
49 let s_ = l - 0.089_484_18 * a - 1.291_485_5 * b;
50 let (l3, m3, s3) = (l_ * l_ * l_, m_ * m_ * m_, s_ * s_ * s_);
51
52 let r = 4.076_741_7 * l3 - 3.307_711_6 * m3 + 0.230_969_93 * s3;
54 let g = -1.268_438 * l3 + 2.609_757_4 * m3 - 0.341_319_4 * s3;
55 let b = -0.004_196_086_3 * l3 - 0.703_418_6 * m3 + 1.707_614_7 * s3;
56
57 [r, g, b]
58}
59
60fn gamma_encode(x: f32) -> f32 {
61 let x = x.clamp(0.0, 1.0);
62 if x <= 0.003_130_8 {
63 12.92 * x
64 } else {
65 1.055 * x.powf(1.0 / 2.4) - 0.055
66 }
67}
68
69fn gamma_decode(x: f32) -> f32 {
70 let x = x.clamp(0.0, 1.0);
71 if x <= 0.040_449_936 {
72 x / 12.92
73 } else {
74 ((x + 0.055) / 1.055).powf(2.4)
75 }
76}
77
78fn linear_to_oklab([r, g, b]: [f32; 3]) -> [f32; 3] {
80 let l = (0.412_221_47 * r + 0.536_332_55 * g + 0.051_445_995 * b).cbrt();
81 let m = (0.211_903_5 * r + 0.680_699_5 * g + 0.107_396_96 * b).cbrt();
82 let s = (0.088_302_46 * r + 0.281_718_85 * g + 0.629_978_7 * b).cbrt();
83 [
84 0.210_454_26 * l + 0.793_617_8 * m - 0.004_072_047 * s,
85 1.977_998_5 * l - 2.428_592_2 * m + 0.450_593_7 * s,
86 0.025_904_037 * l + 0.782_771_77 * m - 0.808_675_77 * s,
87 ]
88}
89
90pub fn mix_oklab(from: Hsla, to: Hsla, t: f32) -> Hsla {
92 let t = t.clamp(0.0, 1.0);
93 let lab = |color: Hsla| {
94 let [r, g, b] = hsl_to_rgb(color.h, color.s, color.l);
95 linear_to_oklab([gamma_decode(r), gamma_decode(g), gamma_decode(b)])
96 };
97 let (a, b) = (lab(from), lab(to));
98 let [l, x, y] = std::array::from_fn(|ix| a[ix] + (b[ix] - a[ix]) * t);
99 let [r, g, bl] = oklab_to_linear(l, x, y);
100 let (h, s, l) = rgb_to_hsl(gamma_encode(r), gamma_encode(g), gamma_encode(bl));
101 hsla(h, s, l, from.a + (to.a - from.a) * t)
102}
103
104pub fn lightness(color: Hsla) -> f32 {
110 gamma_decode(color.l).cbrt()
111}
112
113fn fit_chroma(l: f32, chroma: f32, hue: f32) -> f32 {
121 let fits = |c: f32| {
122 oklch_to_linear(l, c, hue)
123 .iter()
124 .all(|x| (-1e-4..=1.0 + 1e-4).contains(x))
125 };
126 if fits(chroma) {
127 return chroma;
128 }
129 let (mut lo, mut hi) = (0.0, chroma);
130 for _ in 0..24 {
131 let mid = 0.5 * (lo + hi);
132 if fits(mid) { lo = mid } else { hi = mid }
133 }
134 lo
135}
136
137pub fn tint(color: Hsla, hue: f32, chroma: f32) -> Hsla {
140 if chroma <= 0.0 {
141 return color;
142 }
143 let l = lightness(color);
144 let mut out = oklch(l, fit_chroma(l, chroma, hue), hue);
145 out.a = color.a;
146 out
147}
148
149pub fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
151 let max = r.max(g).max(b);
152 let min = r.min(g).min(b);
153 let l = (max + min) / 2.0;
154 let delta = max - min;
155 if delta < f32::EPSILON {
156 return (0.0, 0.0, l);
157 }
158 let s = if l > 0.5 {
159 delta / (2.0 - max - min)
160 } else {
161 delta / (max + min)
162 };
163 let h = if (max - r).abs() < f32::EPSILON {
164 ((g - b) / delta).rem_euclid(6.0)
165 } else if (max - g).abs() < f32::EPSILON {
166 (b - r) / delta + 2.0
167 } else {
168 (r - g) / delta + 4.0
169 } / 6.0;
170 (h, s, l)
171}
172
173pub fn hsl_to_rgb(h: f32, s: f32, l: f32) -> [f32; 3] {
175 if s <= f32::EPSILON {
176 return [l, l, l];
177 }
178 let q = if l < 0.5 {
179 l * (1.0 + s)
180 } else {
181 l + s - l * s
182 };
183 let p = 2.0 * l - q;
184 let hue = |mut t: f32| {
185 t = t.rem_euclid(1.0);
186 if t < 1.0 / 6.0 {
187 p + (q - p) * 6.0 * t
188 } else if t < 0.5 {
189 q
190 } else if t < 2.0 / 3.0 {
191 p + (q - p) * (2.0 / 3.0 - t) * 6.0
192 } else {
193 p
194 }
195 };
196 [hue(h + 1.0 / 3.0), hue(h), hue(h - 1.0 / 3.0)]
197}
198
199pub fn relative_luminance(color: Hsla) -> f32 {
201 let lin = |c: f32| {
202 if c <= 0.040_45 {
203 c / 12.92
204 } else {
205 ((c + 0.055) / 1.055).powf(2.4)
206 }
207 };
208 let [r, g, b] = hsl_to_rgb(color.h, color.s, color.l);
209 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
210}
211
212pub fn contrast_ratio(a: Hsla, b: Hsla) -> f32 {
217 let (la, lb) = (relative_luminance(a), relative_luminance(b));
218 let (hi, lo) = if la >= lb { (la, lb) } else { (lb, la) };
219 (hi + 0.05) / (lo + 0.05)
220}
221
222pub fn legible_on(plate: Hsla, a: Hsla, b: Hsla) -> Hsla {
225 if contrast_ratio(plate, a) >= contrast_ratio(plate, b) {
226 a
227 } else {
228 b
229 }
230}
231
232pub fn stack(base: Hsla, layers: impl IntoIterator<Item = Hsla>) -> Hsla {
235 layers
236 .into_iter()
237 .fold(opaque(base), |under, layer| flatten(layer, under))
238}
239
240pub fn kept(text: Hsla, page: Hsla) -> f32 {
244 let page = opaque(page);
245 contrast_ratio(flatten(text, page), page).min(LEGIBLE)
246}
247
248fn opaque(color: Hsla) -> Hsla {
251 hsla(color.h, color.s, color.l, 1.0)
252}
253
254pub fn legible(text: Hsla, plate: Hsla, floor: f32) -> Hsla {
259 let reads = |color: Hsla| contrast_ratio(flatten(color, plate), plate) >= floor;
260 if reads(text) {
261 return text;
262 }
263 let inverted = hsla(text.h, text.s, 1.0 - text.l, text.a);
264 if reads(inverted) {
265 return inverted;
266 }
267 legible_on(plate, hsla(0.0, 0.0, 1.0, 1.0), hsla(0.0, 0.0, 0.0, 1.0))
268}
269
270pub fn flatten(fg: Hsla, bg: Hsla) -> Hsla {
273 let a = fg.a.clamp(0.0, 1.0);
274 let [fr, fg_, fb] = hsl_to_rgb(fg.h, fg.s, fg.l);
275 let [br, bg_, bb] = hsl_to_rgb(bg.h, bg.s, bg.l);
276 let (h, s, l) = rgb_to_hsl(
277 fr * a + br * (1.0 - a),
278 fg_ * a + bg_ * (1.0 - a),
279 fb * a + bb * (1.0 - a),
280 );
281 hsla(h, s, l, 1.0)
282}
283
284pub fn mix(a: Hsla, b: Hsla, t: f32) -> Hsla {
286 let t = t.clamp(0.0, 1.0);
287 let lerp = |x: f32, y: f32| x + (y - x) * t;
288 hsla(
291 lerp(a.h, b.h),
292 lerp(a.s, b.s),
293 lerp(a.l, b.l),
294 lerp(a.a, b.a),
295 )
296}