use gpui_kit::{Hsla, Rgba};
pub fn hex(value: &str) -> Hsla {
try_hex(value).unwrap_or_else(|error| panic!("{error}"))
}
pub fn try_hex(value: &str) -> Result<Hsla, String> {
Rgba::try_from(value)
.map(Hsla::from)
.map_err(|error| error.to_string())
}
pub fn to_hex(color: Hsla) -> String {
let rgba = color.to_rgb();
let channel = |value: f32| (value.clamp(0., 1.) * 255.).round() as u8;
let (r, g, b, a) = (
channel(rgba.r),
channel(rgba.g),
channel(rgba.b),
channel(rgba.a),
);
if a == 255 {
format!("#{r:02x}{g:02x}{b:02x}")
} else {
format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
}
}
pub fn mix(from: Hsla, to: Hsla, amount: f32) -> Hsla {
let amount = amount.clamp(0., 1.);
let a = to_oklab(from);
let b = to_oklab(to);
let lab = [
a[0] + (b[0] - a[0]) * amount,
a[1] + (b[1] - a[1]) * amount,
a[2] + (b[2] - a[2]) * amount,
];
let alpha = from.a + (to.a - from.a) * amount;
from_oklab(lab, alpha)
}
pub fn lightness(color: Hsla) -> f32 {
to_oklab(color)[0]
}
fn to_oklab(color: Hsla) -> [f32; 3] {
let rgba = color.to_rgb();
let r = srgb_to_linear(rgba.r);
let g = srgb_to_linear(rgba.g);
let b = srgb_to_linear(rgba.b);
let l = 0.412_221_47 * r + 0.536_332_55 * g + 0.051_445_995 * b;
let m = 0.211_903_5 * r + 0.680_699_5 * g + 0.107_396_96 * b;
let s = 0.088_302_46 * r + 0.281_718_85 * g + 0.629_978_7 * b;
let l = l.cbrt();
let m = m.cbrt();
let s = s.cbrt();
[
0.210_454_26 * l + 0.793_617_8 * m - 0.004_072_047 * s,
1.977_998_5 * l - 2.428_592_2 * m + 0.450_593_7 * s,
0.025_904_037 * l + 0.782_771_77 * m - 0.808_675_77 * s,
]
}
fn from_oklab(lab: [f32; 3], alpha: f32) -> Hsla {
let [big_l, a, b] = lab;
let l = big_l + 0.396_337_78 * a + 0.215_803_76 * b;
let m = big_l - 0.105_561_346 * a - 0.063_854_17 * b;
let s = big_l - 0.089_484_18 * a - 1.291_485_5 * b;
let l = l * l * l;
let m = m * m * m;
let s = s * s * s;
let r = 4.076_741_7 * l - 3.307_711_6 * m + 0.230_969_94 * s;
let g = -1.268_438 * l + 2.609_757_4 * m - 0.341_319_38 * s;
let b = -0.004_196_086_3 * l - 0.703_418_6 * m + 1.707_614_7 * s;
Hsla::from(Rgba {
r: linear_to_srgb(r),
g: linear_to_srgb(g),
b: linear_to_srgb(b),
a: alpha,
})
}
pub fn contrast_ratio(a: Hsla, b: Hsla) -> f32 {
let luminance = |color: Hsla| {
let rgb = color.to_rgb();
0.2126 * srgb_to_linear(rgb.r)
+ 0.7152 * srgb_to_linear(rgb.g)
+ 0.0722 * srgb_to_linear(rgb.b)
};
let (a, b) = (luminance(a), luminance(b));
(a.max(b) + 0.05) / (a.min(b) + 0.05)
}
fn srgb_to_linear(value: f32) -> f32 {
if value <= 0.040_45 {
value / 12.92
} else {
((value + 0.055) / 1.055).powf(2.4)
}
}
fn linear_to_srgb(value: f32) -> f32 {
let value = value.clamp(0., 1.);
if value <= 0.003_130_8 {
value * 12.92
} else {
1.055 * value.powf(1. / 2.4) - 0.055
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_round_trips() {
for literal in ["#ffffff", "#181818", "#339cff", "#1a1c1f", "#00a240"] {
assert_eq!(to_hex(hex(literal)), literal);
}
assert_eq!(to_hex(hex("#339cff80")), "#339cff80");
assert!(try_hex("339cff").is_err());
}
#[test]
fn mix_endpoints_and_midpoint() {
let white = hex("#ffffff");
let black = hex("#000000");
assert_eq!(to_hex(mix(white, black, 0.)), "#ffffff");
assert_eq!(to_hex(mix(white, black, 1.)), "#000000");
let mid = lightness(mix(white, black, 0.5));
assert!((mid - 0.5).abs() < 0.02, "midpoint lightness {mid}");
}
#[test]
fn a_small_step_toward_ink_stays_near_the_surface() {
let surface = hex("#ffffff");
let ink = hex("#1a1c1f");
let step = mix(surface, ink, 0.05);
let l = lightness(step);
assert!(l > 0.94 && l < 0.98, "lightness {l}");
let surface = hex("#181818");
let ink = hex("#ffffff");
let step = mix(surface, ink, 0.05);
assert!(lightness(step) > lightness(surface));
}
}