use ratatui_core::style::Color;
use crate::{color_ext::ToRgbComponents, math};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorSpace {
Rgb,
#[default]
Hsl,
Hsv,
}
pub fn color_from_hsl(h: f32, s: f32, l: f32) -> Color {
let (r, g, b) = hsl_to_rgb(h, s, l);
Color::Rgb(r, g, b)
}
pub fn color_from_hsv(h: f32, s: f32, v: f32) -> Color {
let (r, g, b) = hsv_to_rgb(h, s, v);
Color::Rgb(r, g, b)
}
pub fn color_to_hsv(color: &Color) -> (f32, f32, f32) {
let (r, g, b) = color.to_rgb();
rgb_to_hsv(r, g, b)
}
pub fn color_to_hsl(color: &Color) -> (f32, f32, f32) {
let (r, g, b) = color.to_rgb();
rgb_to_hsl(r, g, b)
}
#[inline]
fn adjust_lightness_linear(r: u8, g: u8, b: u8, amount: f32) -> (u8, u8, u8) {
if amount >= 0.0 {
let a = (amount * 256.0) as u32;
let r = r as u32 + (((255 - r as u32) * a) >> 8);
let g = g as u32 + (((255 - g as u32) * a) >> 8);
let b = b as u32 + (((255 - b as u32) * a) >> 8);
(r as u8, g as u8, b as u8)
} else {
let factor = ((1.0 + amount) * 256.0) as u32;
let r = (r as u32 * factor) >> 8;
let g = (g as u32 * factor) >> 8;
let b = (b as u32 * factor) >> 8;
(r as u8, g as u8, b as u8)
}
}
#[inline]
fn adjust_saturation_weighted(r: u8, g: u8, b: u8, factor: f32) -> (u8, u8, u8) {
let lum = ((r as u32 * 77 + g as u32 * 150 + b as u32 * 29) >> 8) as i32;
let f = (factor * 256.0) as i32;
let r = (lum + (((r as i32 - lum) * f) >> 8)).clamp(0, 255) as u8;
let g = (lum + (((g as i32 - lum) * f) >> 8)).clamp(0, 255) as u8;
let b = (lum + (((b as i32 - lum) * f) >> 8)).clamp(0, 255) as u8;
(r, g, b)
}
#[inline]
fn lerp_toward_extreme(value: f32, amount: f32, max: f32) -> f32 {
if amount >= 0.0 {
value + (max - value) * amount
} else {
value + value * amount
}
}
impl ColorSpace {
pub fn saturate(&self, color: &Color, factor: f32) -> Color {
match self {
ColorSpace::Rgb => {
let (r, g, b) = color.to_rgb();
let (r, g, b) = adjust_saturation_weighted(r, g, b, factor);
Color::Rgb(r, g, b)
},
ColorSpace::Hsl => {
let (r, g, b) = color.to_rgb();
let (h, s, l) = rgb_to_hsl(r, g, b);
let s = (s * factor).clamp(0.0, 100.0);
let (r, g, b) = hsl_to_rgb(h, s, l);
Color::Rgb(r, g, b)
},
ColorSpace::Hsv => {
let (r, g, b) = color.to_rgb();
let (h, s, v) = rgb_to_hsv(r, g, b);
let s = (s * factor).clamp(0.0, 100.0);
let (r, g, b) = hsv_to_rgb(h, s, v);
Color::Rgb(r, g, b)
},
}
}
pub fn lighten(&self, color: &Color, amount: f32) -> Color {
let amount = amount.clamp(-1.0, 1.0);
match self {
ColorSpace::Rgb => {
let (r, g, b) = color.to_rgb();
let (r, g, b) = adjust_lightness_linear(r, g, b, amount);
Color::Rgb(r, g, b)
},
ColorSpace::Hsl => {
let (r, g, b) = color.to_rgb();
let (h, s, l) = rgb_to_hsl(r, g, b);
let l = lerp_toward_extreme(l, amount, 100.0);
let (r, g, b) = hsl_to_rgb(h, s, l);
Color::Rgb(r, g, b)
},
ColorSpace::Hsv => {
let (r, g, b) = color.to_rgb();
let (h, s, v) = rgb_to_hsv(r, g, b);
let v = lerp_toward_extreme(v, amount, 100.0);
let (r, g, b) = hsv_to_rgb(h, s, v);
Color::Rgb(r, g, b)
},
}
}
pub fn lerp(&self, from: &Color, to: &Color, alpha: f32) -> Color {
use ColorSpace::*;
let alpha = alpha.clamp(0.0, 1.0);
if alpha == 0.0 {
return *from;
} else if alpha == 1.0 {
return *to;
}
match self {
Rgb => Self::lerp_rgb(from.to_rgb(), to.to_rgb(), alpha),
Hsl => Self::lerp_hsl(color_to_hsl(from), color_to_hsl(to), alpha),
Hsv => Self::lerp_hsv(color_to_hsv(from), color_to_hsv(to), alpha),
}
}
fn lerp_rgb((r1, g1, b1): (u8, u8, u8), (r2, g2, b2): (u8, u8, u8), alpha: f32) -> Color {
let alpha = (alpha * 0x1_0000 as f32) as u32;
let inv_alpha = 0x1_0000 - alpha;
let lerp =
|c1: u8, c2: u8| -> u8 { ((c1 as u32 * inv_alpha + c2 as u32 * alpha) >> 16) as u8 };
Color::Rgb(lerp(r1, r2), lerp(g1, g2), lerp(b1, b2))
}
fn lerp_hsv((h1, s1, v1): (f32, f32, f32), (h2, s2, v2): (f32, f32, f32), alpha: f32) -> Color {
let mut h_diff = h2 - h1;
if h_diff > 180.0 {
h_diff -= 360.0;
} else if h_diff < -180.0 {
h_diff += 360.0;
}
let mut h = h1 + h_diff * alpha;
if h < 0.0 {
h += 360.0;
}
if h >= 360.0 {
h -= 360.0;
}
let s = s1 + (s2 - s1) * alpha;
let v = v1 + (v2 - v1) * alpha;
let (r, g, b) = hsv_to_rgb(h, s, v);
Color::Rgb(r, g, b)
}
fn lerp_hsl((h1, s1, l1): (f32, f32, f32), (h2, s2, l2): (f32, f32, f32), alpha: f32) -> Color {
let mut h_diff = h2 - h1;
if h_diff > 180.0 {
h_diff -= 360.0;
} else if h_diff < -180.0 {
h_diff += 360.0;
}
let mut h = h1 + h_diff * alpha;
if h < 0.0 {
h += 360.0;
}
if h >= 360.0 {
h -= 360.0;
}
let s = s1 + (s2 - s1) * alpha;
let l = l1 + (l2 - l1) * alpha;
let (r, g, b) = hsl_to_rgb(h, s, l);
Color::Rgb(r, g, b)
}
}
fn rgb_to_hsv(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let delta = max - min;
if delta == 0 {
return (0.0, 0.0, max as f32 * (100.0 / 255.0));
}
let v = max as f32 * (100.0 / 255.0);
let inv_delta = 1.0 / delta as f32;
let inv_max = 1.0 / max as f32;
let s = delta as f32 * 100.0 * inv_max;
let hr = (g as f32 - b as f32) * inv_delta;
let hg = (b as f32 - r as f32) * inv_delta + 2.0;
let hb = (r as f32 - g as f32) * inv_delta + 4.0;
let r_mask = ((r >= g) as u8 & (r >= b) as u8) as f32;
let g_mask = (g >= b) as u8 as f32 * (1.0 - r_mask);
let b_mask = 1.0 - r_mask - g_mask;
let hr = hr + (g < b) as u8 as f32 * 6.0;
let h = (r_mask * hr + g_mask * hg + b_mask * hb) * 60.0;
(h, s, v)
}
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
let s = s / 100.0;
let v = v / 100.0;
let h = ((h % 360.0) + 360.0) % 360.0;
if s <= 0.0 {
return (
math::round(v * 255.0) as u8,
math::round(v * 255.0) as u8,
math::round(v * 255.0) as u8,
);
}
let h = h / 60.0;
let i = math::floor(h) as i32;
let f = h - i as f32;
let p = v * (1.0 - s);
let q = v * (1.0 - s * f);
let t = v * (1.0 - s * (1.0 - f));
let (r, g, b) = match i {
0 => (v, t, p),
1 => (q, v, p),
2 => (p, v, t),
3 => (p, q, v),
4 => (t, p, v),
_ => (v, p, q),
};
(
math::round(r * 255.0) as u8,
math::round(g * 255.0) as u8,
math::round(b * 255.0) as u8,
)
}
pub(crate) fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let delta = max - min; let sum = max as u16 + min as u16;
if delta == 0 {
return (0.0, 0.0, sum as f32 * (50.0 / 255.0));
}
let l = sum as f32 * (50.0 / 255.0);
let abs_diff = sum.abs_diff(255);
let denom = (255 - abs_diff) as f32;
let inv_delta = 1.0 / delta as f32;
let inv_denom = 1.0 / denom;
let s = delta as f32 * 100.0 * inv_denom;
let hr = (g as f32 - b as f32) * inv_delta;
let hg = (b as f32 - r as f32) * inv_delta + 2.0;
let hb = (r as f32 - g as f32) * inv_delta + 4.0;
let r_mask = ((r >= g) as u8 & (r >= b) as u8) as f32;
let g_mask = (g >= b) as u8 as f32 * (1.0 - r_mask);
let b_mask = 1.0 - r_mask - g_mask;
let hr = hr + (g < b) as u8 as f32 * 6.0;
let h = (r_mask * hr + g_mask * hg + b_mask * hb) * 60.0;
(h, s, l)
}
pub(crate) fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
let s = s / 100.0;
let l = l / 100.0;
if s == 0.0 {
let gray = math::round(l * 255.0) as u8;
return (gray, gray, gray);
}
let h = ((h % 360.0) + 360.0) % 360.0 / 60.0;
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let m = l - c * 0.5;
let sector = h as u32;
let f = h - sector as f32;
let h2 = (sector & 1) as f32 + f;
let x = c * (1.0 - (h2 - 1.0).abs());
let (r, g, b) = match sector {
0 => (c + m, x + m, m),
1 => (x + m, c + m, m),
2 => (m, c + m, x + m),
3 => (m, x + m, c + m),
4 => (x + m, m, c + m),
_ => (c + m, m, x + m),
};
(
math::round(r * 255.0) as u8,
math::round(g * 255.0) as u8,
math::round(b * 255.0) as u8,
)
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_approx_eq(a: f32, b: f32, epsilon: f32) {
assert!(
(a - b).abs() < epsilon,
"Expected {a} to be approximately equal to {b}"
);
}
fn assert_rgb_eq(a: (u8, u8, u8), b: (u8, u8, u8)) {
let a = Color::Rgb(a.0, a.1, a.2);
let b = Color::Rgb(b.0, b.1, b.2);
assert_eq!(a, b);
}
#[test]
fn test_rgb_to_hsl() {
let (h, s, l) = rgb_to_hsl(255, 0, 0); assert_approx_eq(h, 0.0, 0.1);
assert_approx_eq(s, 100.0, 0.1);
assert_approx_eq(l, 50.0, 0.1);
let (h, s, l) = rgb_to_hsl(0, 255, 0); assert_approx_eq(h, 120.0, 0.1);
assert_approx_eq(s, 100.0, 0.1);
assert_approx_eq(l, 50.0, 0.1);
let (h, s, l) = rgb_to_hsl(0, 0, 255); assert_approx_eq(h, 240.0, 0.1);
assert_approx_eq(s, 100.0, 0.1);
assert_approx_eq(l, 50.0, 0.1);
let (h, s, l) = rgb_to_hsl(255, 255, 0); assert_approx_eq(h, 60.0, 0.1);
assert_approx_eq(s, 100.0, 0.1);
assert_approx_eq(l, 50.0, 0.1);
let (h, s, l) = rgb_to_hsl(0, 0, 0); assert_approx_eq(h, 0.0, 0.1);
assert_approx_eq(s, 0.0, 0.1);
assert_approx_eq(l, 0.0, 0.1);
let (h, s, l) = rgb_to_hsl(255, 255, 255); assert_approx_eq(h, 0.0, 0.1);
assert_approx_eq(s, 0.0, 0.1);
assert_approx_eq(l, 100.0, 0.1);
let (h, s, l) = rgb_to_hsl(128, 128, 128); assert_approx_eq(h, 0.0, 0.1);
assert_approx_eq(s, 0.0, 0.1);
assert_approx_eq(l, 50.0, 0.2);
}
#[test]
fn test_hsl_to_rgb() {
let (r, g, b) = hsl_to_rgb(0.0, 100.0, 50.0); assert_eq!(r, 255);
assert_eq!(g, 0);
assert_eq!(b, 0);
let (r, g, b) = hsl_to_rgb(120.0, 100.0, 50.0); assert_eq!(r, 0);
assert_eq!(g, 255);
assert_eq!(b, 0);
let (r, g, b) = hsl_to_rgb(240.0, 100.0, 50.0); assert_eq!(r, 0);
assert_eq!(g, 0);
assert_eq!(b, 255);
let (r, g, b) = hsl_to_rgb(0.0, 0.0, 0.0); assert_eq!(r, 0);
assert_eq!(g, 0);
assert_eq!(b, 0);
let (r, g, b) = hsl_to_rgb(0.0, 0.0, 100.0); assert_eq!(r, 255);
assert_eq!(g, 255);
assert_eq!(b, 255);
let (r, g, b) = hsl_to_rgb(0.0, 0.0, 50.0); assert_eq!(r, 128);
assert_eq!(g, 128);
assert_eq!(b, 128);
}
#[test]
fn test_rgb_to_hsv() {
let (h, s, v) = rgb_to_hsv(255, 0, 0); assert_approx_eq(h, 0.0, 0.1);
assert_approx_eq(s, 100.0, 0.1);
assert_approx_eq(v, 100.0, 0.1);
let (h, s, v) = rgb_to_hsv(0, 255, 0); assert_approx_eq(h, 120.0, 0.1);
assert_approx_eq(s, 100.0, 0.1);
assert_approx_eq(v, 100.0, 0.1);
let (h, s, v) = rgb_to_hsv(0, 0, 255); assert_approx_eq(h, 240.0, 0.1);
assert_approx_eq(s, 100.0, 0.1);
assert_approx_eq(v, 100.0, 0.1);
let (h, s, v) = rgb_to_hsv(0, 0, 0); assert_approx_eq(h, 0.0, 0.1);
assert_approx_eq(s, 0.0, 0.1);
assert_approx_eq(v, 0.0, 0.1);
let (h, s, v) = rgb_to_hsv(255, 255, 255); assert_approx_eq(h, 0.0, 0.1);
assert_approx_eq(s, 0.0, 0.1);
assert_approx_eq(v, 100.0, 0.1);
let (h, s, v) = rgb_to_hsv(128, 128, 128); assert_approx_eq(h, 0.0, 0.1);
assert_approx_eq(s, 0.0, 0.1);
assert_approx_eq(v, 50.2, 0.1); }
#[test]
fn test_hsv_to_rgb() {
let (r, g, b) = hsv_to_rgb(0.0, 100.0, 100.0); assert_eq!(r, 255);
assert_eq!(g, 0);
assert_eq!(b, 0);
let (r, g, b) = hsv_to_rgb(120.0, 100.0, 100.0); assert_eq!(r, 0);
assert_eq!(g, 255);
assert_eq!(b, 0);
let (r, g, b) = hsv_to_rgb(240.0, 100.0, 100.0); assert_eq!(r, 0);
assert_eq!(g, 0);
assert_eq!(b, 255);
let (r, g, b) = hsv_to_rgb(0.0, 0.0, 0.0); assert_eq!(r, 0);
assert_eq!(g, 0);
assert_eq!(b, 0);
let (r, g, b) = hsv_to_rgb(0.0, 0.0, 100.0); assert_eq!(r, 255);
assert_eq!(g, 255);
assert_eq!(b, 255);
let (r, g, b) = hsv_to_rgb(0.0, 0.0, 50.0); assert_eq!(r, 128);
assert_eq!(g, 128);
assert_eq!(b, 128);
}
#[test]
fn test_round_trip_conversions() {
for r in [0, 64, 128, 192, 255].iter() {
for g in [0, 64, 128, 192, 255].iter() {
for b in [0, 64, 128, 192, 255].iter() {
let original = (*r, *g, *b);
let (h, s, l) = rgb_to_hsl(*r, *g, *b);
let rgb_from_hsl = hsl_to_rgb(h, s, l);
assert_rgb_eq(original, rgb_from_hsl);
let (h, s, v) = rgb_to_hsv(*r, *g, *b);
let rgb_from_hsv = hsv_to_rgb(h, s, v);
assert_rgb_eq(original, rgb_from_hsv); }
}
}
}
#[test]
fn test_interpolate_rgb() {
let from = Color::Rgb(0, 0, 0); let to = Color::Rgb(255, 255, 255);
let result = ColorSpace::Rgb.lerp(&from, &to, 0.0);
assert_eq!(result, Color::Rgb(0, 0, 0));
let result = ColorSpace::Rgb.lerp(&from, &to, 0.25);
assert_eq!(result, Color::Rgb(63, 63, 63));
let result = ColorSpace::Rgb.lerp(&from, &to, 0.5);
assert_eq!(result, Color::Rgb(127, 127, 127));
let result = ColorSpace::Rgb.lerp(&from, &to, 0.75);
assert_eq!(result, Color::Rgb(191, 191, 191));
let result = ColorSpace::Rgb.lerp(&from, &to, 1.0);
assert_eq!(result, Color::Rgb(255, 255, 255));
let from = Color::Rgb(100, 150, 200);
let to = Color::Rgb(200, 100, 50);
let result = ColorSpace::Rgb.lerp(&from, &to, 0.5);
assert_eq!(result, Color::Rgb(150, 125, 125));
}
#[test]
fn test_interpolate_hsl() {
let from = Color::Rgb(255, 0, 0); let to = Color::Rgb(0, 0, 255);
let result = ColorSpace::Hsl.lerp(&from, &to, 0.5);
assert_rgb_eq(result.to_rgb(), (255, 0, 255));
let from = Color::Rgb(255, 0, 0); let to = Color::Rgb(0, 255, 255);
let result = ColorSpace::Hsl.lerp(&from, &to, 0.5);
let (h, _, _) = rgb_to_hsl(result.to_rgb().0, result.to_rgb().1, result.to_rgb().2);
assert_approx_eq(h, 90.0, 5.0);
let from = Color::Rgb(255, 0, 0); let to = Color::Rgb(128, 128, 128);
let result = ColorSpace::Hsl.lerp(&from, &to, 0.5);
let (_, s, _) = rgb_to_hsl(result.to_rgb().0, result.to_rgb().1, result.to_rgb().2);
assert_approx_eq(s, 50.0, 5.0); }
#[test]
fn test_interpolate_hsv() {
let from = Color::Rgb(255, 0, 0); let to = Color::Rgb(0, 0, 255);
let result = ColorSpace::Hsv.lerp(&from, &to, 0.5);
let (h, s, v) = rgb_to_hsv(result.to_rgb().0, result.to_rgb().1, result.to_rgb().2);
assert_approx_eq(h, 300.0, 5.0); assert_approx_eq(s, 100.0, 0.1); assert_approx_eq(v, 100.0, 0.1);
assert_rgb_eq(result.to_rgb(), (255, 0, 255));
}
#[test]
fn test_edge_cases() {
let from = Color::Rgb(255, 0, 0);
let to = Color::Rgb(0, 255, 255);
let rgb_mid = ColorSpace::Rgb.lerp(&from, &to, 0.5);
let hsl_mid = ColorSpace::Hsl.lerp(&from, &to, 0.5);
assert_eq!(rgb_mid.to_rgb().0, rgb_mid.to_rgb().1);
assert_eq!(rgb_mid.to_rgb().1, rgb_mid.to_rgb().2);
let (hsl_h, _, _) = rgb_to_hsl(hsl_mid.to_rgb().0, hsl_mid.to_rgb().1, hsl_mid.to_rgb().2);
assert_approx_eq(hsl_h, 90.0, 5.0);
}
#[test]
fn test_lighten_all_color_spaces() {
let spaces = [ColorSpace::Rgb, ColorSpace::Hsl, ColorSpace::Hsv];
let red = Color::Rgb(200, 50, 50);
let gray = Color::Rgb(128, 128, 128);
for cs in spaces {
assert_eq!(
cs.lighten(&red, 0.0).to_rgb(),
red.to_rgb(),
"{cs:?}: amount=0 should return original"
);
let (r, g, b) = cs.lighten(&red, 1.0).to_rgb();
match cs {
ColorSpace::Rgb | ColorSpace::Hsl => {
assert!(
r >= 254 && g >= 254 && b >= 254,
"{cs:?}: amount=1.0 should produce white, got ({r}, {g}, {b})"
);
},
ColorSpace::Hsv => {
assert_eq!(r, 255, "{cs:?}: amount=1.0 should max out dominant channel");
},
}
let (r, g, b) = cs.lighten(&red, -1.0).to_rgb();
assert!(
r <= 1 && g <= 1 && b <= 1,
"{cs:?}: amount=-1.0 should produce black, got ({r}, {g}, {b})"
);
let orig = gray.to_rgb();
let lighter = cs.lighten(&gray, 0.5).to_rgb();
assert!(
lighter.0 > orig.0 && lighter.1 > orig.1 && lighter.2 > orig.2,
"{cs:?}: lighten(0.5) should increase all channels for gray"
);
let darker = cs.lighten(&gray, -0.5).to_rgb();
assert!(
darker.0 < orig.0 && darker.1 < orig.1 && darker.2 < orig.2,
"{cs:?}: lighten(-0.5) should decrease all channels for gray"
);
}
}
#[test]
fn test_saturate_all_color_spaces() {
let spaces = [ColorSpace::Rgb, ColorSpace::Hsl, ColorSpace::Hsv];
let teal = Color::Rgb(50, 180, 160);
for cs in spaces {
assert_eq!(
cs.saturate(&teal, 1.0).to_rgb(),
teal.to_rgb(),
"{cs:?}: factor=1.0 should return original"
);
let (r, g, b) = cs.saturate(&teal, 0.0).to_rgb();
let max_diff = (r as i32 - g as i32)
.abs()
.max((g as i32 - b as i32).abs())
.max((r as i32 - b as i32).abs());
assert!(
max_diff <= 1,
"{cs:?}: factor=0.0 should produce grayscale, got ({r}, {g}, {b})"
);
let gray_lum = (r as u32 * 77 + g as u32 * 150 + b as u32 * 29) >> 8;
let orig = teal.to_rgb();
let orig_lum = (orig.0 as u32 * 77 + orig.1 as u32 * 150 + orig.2 as u32 * 29) >> 8;
let tolerance = match cs {
ColorSpace::Rgb => 5, ColorSpace::Hsl => 30, ColorSpace::Hsv => 50, };
assert!((gray_lum as i32 - orig_lum as i32).unsigned_abs() < tolerance,
"{cs:?}: desaturated luminance ({gray_lum}) should be close to original ({orig_lum})");
let desat = cs.saturate(&teal, 0.5).to_rgb();
let orig_spread = orig.0.abs_diff(orig.1) as u32
+ orig.1.abs_diff(orig.2) as u32
+ orig.0.abs_diff(orig.2) as u32;
let desat_spread = desat.0.abs_diff(desat.1) as u32
+ desat.1.abs_diff(desat.2) as u32
+ desat.0.abs_diff(desat.2) as u32;
assert!(
desat_spread < orig_spread,
"{cs:?}: factor=0.5 should reduce channel spread ({desat_spread} < {orig_spread})"
);
let oversat = cs.saturate(&teal, 1.5).to_rgb();
let oversat_spread = oversat.0.abs_diff(oversat.1) as u32
+ oversat.1.abs_diff(oversat.2) as u32
+ oversat.0.abs_diff(oversat.2) as u32;
assert!(oversat_spread > orig_spread,
"{cs:?}: factor=1.5 should increase channel spread ({oversat_spread} > {orig_spread})");
}
}
#[test]
fn test_lighten_clamps_out_of_range() {
let color = Color::Rgb(100, 150, 200);
for cs in [ColorSpace::Rgb, ColorSpace::Hsl, ColorSpace::Hsv] {
assert_eq!(
cs.lighten(&color, 2.0).to_rgb(),
cs.lighten(&color, 1.0).to_rgb(),
"{cs:?}: amount > 1.0 should clamp to 1.0"
);
assert_eq!(
cs.lighten(&color, -5.0).to_rgb(),
cs.lighten(&color, -1.0).to_rgb(),
"{cs:?}: amount < -1.0 should clamp to -1.0"
);
}
}
#[test]
fn test_hsl_to_rgb_negative_hue() {
let from_negative = hsl_to_rgb(-30.0, 100.0, 50.0);
let from_positive = hsl_to_rgb(330.0, 100.0, 50.0);
assert_eq!(
from_negative, from_positive,
"hsl_to_rgb(-30°) should equal hsl_to_rgb(330°)"
);
}
#[test]
fn test_hsv_to_rgb_negative_hue() {
let from_negative = hsv_to_rgb(-90.0, 100.0, 100.0);
let from_positive = hsv_to_rgb(270.0, 100.0, 100.0);
assert_eq!(
from_negative, from_positive,
"hsv_to_rgb(-90°) should equal hsv_to_rgb(270°)"
);
}
#[test]
fn test_lighten_saturate_gray_invariance() {
let gray = Color::Rgb(128, 128, 128);
for cs in [ColorSpace::Rgb, ColorSpace::Hsl, ColorSpace::Hsv] {
for factor in [0.0_f32, 0.5, 1.0, 1.5, 2.0] {
let result = cs.saturate(&gray, factor).to_rgb();
let (r, g, b) = result;
let max_diff = (r as i32 - 128)
.abs()
.max((g as i32 - 128).abs())
.max((b as i32 - 128).abs());
assert!(max_diff <= 1,
"{cs:?}: saturate({factor}) on gray should be ~(128,128,128), got ({r},{g},{b})");
}
}
}
}