#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub w: Option<u8>, }
impl Color {
pub fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b, w: None }
}
pub fn from_hex(hex: &str) -> Result<Self, Box<dyn std::error::Error>> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Err("Invalid hex color format".into());
}
let r = u8::from_str_radix(&hex[0..2], 16)?;
let g = u8::from_str_radix(&hex[2..4], 16)?;
let b = u8::from_str_radix(&hex[4..6], 16)?;
Ok(Color { r, g, b, w: None })
}
pub fn from_name(name: &str) -> Result<Self, Box<dyn std::error::Error>> {
match name.to_lowercase().as_str() {
"red" => Ok(Color {
r: 255,
g: 0,
b: 0,
w: None,
}),
"green" => Ok(Color {
r: 0,
g: 255,
b: 0,
w: None,
}),
"blue" => Ok(Color {
r: 0,
g: 0,
b: 255,
w: None,
}),
"white" => Ok(Color {
r: 255,
g: 255,
b: 255,
w: None,
}),
"black" => Ok(Color {
r: 0,
g: 0,
b: 0,
w: None,
}),
"yellow" => Ok(Color {
r: 255,
g: 255,
b: 0,
w: None,
}),
"cyan" => Ok(Color {
r: 0,
g: 255,
b: 255,
w: None,
}),
"magenta" => Ok(Color {
r: 255,
g: 0,
b: 255,
w: None,
}),
"orange" => Ok(Color {
r: 255,
g: 165,
b: 0,
w: None,
}),
"purple" => Ok(Color {
r: 128,
g: 0,
b: 128,
w: None,
}),
_ => Err(format!("Unknown color name: {}", name).into()),
}
}
pub fn from_hsv(h: f64, s: f64, v: f64) -> Self {
let c = v * s;
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
let m = v - c;
let sector = (h / 60.0).floor() as u8 % 6;
let (r, g, b) = match sector {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x), };
Self {
r: ((r + m) * 255.0) as u8,
g: ((g + m) * 255.0) as u8,
b: ((b + m) * 255.0) as u8,
w: None,
}
}
pub fn lerp(&self, other: &Color, t: f64) -> Self {
let t = t.clamp(0.0, 1.0);
let t_inv = 1.0 - t;
let lerp_u8 = |a: u8, b: u8| -> u8 { (a as f64 * t_inv + b as f64 * t) as u8 };
Self {
r: lerp_u8(self.r, other.r),
g: lerp_u8(self.g, other.g),
b: lerp_u8(self.b, other.b),
w: match (self.w, other.w) {
(Some(w1), Some(w2)) => Some(lerp_u8(w1, w2)),
(Some(w1), None) => Some((w1 as f64 * t_inv) as u8),
(None, Some(w2)) => Some((w2 as f64 * t) as u8),
(None, None) => None,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_rgb() {
let c = Color::new(10, 20, 30);
assert_eq!(c.r, 10);
assert_eq!(c.g, 20);
assert_eq!(c.b, 30);
assert_eq!(c.w, None);
}
#[test]
fn from_hex_valid() {
let c = Color::from_hex("#FF8000").unwrap();
assert_eq!(c.r, 255);
assert_eq!(c.g, 128);
assert_eq!(c.b, 0);
}
#[test]
fn from_hex_without_hash() {
let c = Color::from_hex("00FF00").unwrap();
assert_eq!(c, Color::new(0, 255, 0));
}
#[test]
fn from_hex_black() {
let c = Color::from_hex("#000000").unwrap();
assert_eq!(c, Color::new(0, 0, 0));
}
#[test]
fn from_hex_white() {
let c = Color::from_hex("#FFFFFF").unwrap();
assert_eq!(c, Color::new(255, 255, 255));
}
#[test]
fn from_hex_lowercase() {
let c = Color::from_hex("#ff0000").unwrap();
assert_eq!(c, Color::new(255, 0, 0));
}
#[test]
fn from_hex_invalid_length() {
assert!(Color::from_hex("#FFF").is_err());
assert!(Color::from_hex("#FFFFFFF").is_err());
}
#[test]
fn from_hex_invalid_chars() {
assert!(Color::from_hex("#GGHHII").is_err());
}
#[test]
fn from_name_all_known_colors() {
let cases = vec![
("red", (255, 0, 0)),
("green", (0, 255, 0)),
("blue", (0, 0, 255)),
("white", (255, 255, 255)),
("black", (0, 0, 0)),
("yellow", (255, 255, 0)),
("cyan", (0, 255, 255)),
("magenta", (255, 0, 255)),
("orange", (255, 165, 0)),
("purple", (128, 0, 128)),
];
for (name, (r, g, b)) in cases {
let c = Color::from_name(name).unwrap();
assert_eq!(c.r, r, "failed for {}", name);
assert_eq!(c.g, g, "failed for {}", name);
assert_eq!(c.b, b, "failed for {}", name);
}
}
#[test]
fn from_name_case_insensitive() {
assert_eq!(
Color::from_name("RED").unwrap(),
Color::from_name("red").unwrap()
);
assert_eq!(
Color::from_name("Blue").unwrap(),
Color::from_name("blue").unwrap()
);
}
#[test]
fn from_name_unknown() {
assert!(Color::from_name("chartreuse").is_err());
}
#[test]
fn from_hsv_red() {
let c = Color::from_hsv(0.0, 1.0, 1.0);
assert_eq!(c.r, 255);
assert_eq!(c.g, 0);
assert_eq!(c.b, 0);
}
#[test]
fn from_hsv_green() {
let c = Color::from_hsv(120.0, 1.0, 1.0);
assert_eq!(c.r, 0);
assert_eq!(c.g, 255);
assert_eq!(c.b, 0);
}
#[test]
fn from_hsv_blue() {
let c = Color::from_hsv(240.0, 1.0, 1.0);
assert_eq!(c.r, 0);
assert_eq!(c.g, 0);
assert_eq!(c.b, 255);
}
#[test]
fn from_hsv_black() {
let c = Color::from_hsv(0.0, 0.0, 0.0);
assert_eq!(c.r, 0);
assert_eq!(c.g, 0);
assert_eq!(c.b, 0);
}
#[test]
fn from_hsv_white() {
let c = Color::from_hsv(0.0, 0.0, 1.0);
assert_eq!(c.r, 255);
assert_eq!(c.g, 255);
assert_eq!(c.b, 255);
}
#[test]
fn from_hsv_half_brightness() {
let c = Color::from_hsv(0.0, 1.0, 0.5);
assert_eq!(c.r, 127);
assert_eq!(c.g, 0);
assert_eq!(c.b, 0);
}
#[test]
fn lerp_at_zero() {
let a = Color::new(0, 0, 0);
let b = Color::new(255, 255, 255);
let result = a.lerp(&b, 0.0);
assert_eq!(result, a);
}
#[test]
fn lerp_at_one() {
let a = Color::new(0, 0, 0);
let b = Color::new(255, 255, 255);
let result = a.lerp(&b, 1.0);
assert_eq!(result, b);
}
#[test]
fn lerp_at_midpoint() {
let a = Color::new(0, 0, 0);
let b = Color::new(254, 254, 254);
let result = a.lerp(&b, 0.5);
assert_eq!(result.r, 127);
assert_eq!(result.g, 127);
assert_eq!(result.b, 127);
}
#[test]
fn lerp_clamps_below_zero() {
let a = Color::new(100, 100, 100);
let b = Color::new(200, 200, 200);
let result = a.lerp(&b, -1.0);
assert_eq!(result, a);
}
#[test]
fn lerp_clamps_above_one() {
let a = Color::new(100, 100, 100);
let b = Color::new(200, 200, 200);
let result = a.lerp(&b, 2.0);
assert_eq!(result, b);
}
#[test]
fn lerp_with_white_channels() {
let a = Color {
r: 0,
g: 0,
b: 0,
w: Some(0),
};
let b = Color {
r: 255,
g: 255,
b: 255,
w: Some(254),
};
let result = a.lerp(&b, 0.5);
assert_eq!(result.w, Some(127));
}
#[test]
fn lerp_no_white_channels() {
let a = Color::new(0, 0, 0);
let b = Color::new(255, 255, 255);
let result = a.lerp(&b, 0.5);
assert_eq!(result.w, None);
}
}