use alloc::{format, string::String, vec::Vec};
use colorsys::{Hsl, Rgb};
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Color {
red: u8,
green: u8,
blue: u8,
}
impl From<(u8, u8, u8)> for Color {
fn from(rgb: (u8, u8, u8)) -> Self {
Color { red: rgb.0, green: rgb.1, blue: rgb.2 }
}
}
impl From<Rgb> for Color {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
fn from(rgb: Rgb) -> Self {
Color { red: rgb.red() as u8, green: rgb.green() as u8, blue: rgb.blue() as u8 }
}
}
impl From<Hsl> for Color {
fn from(hsl: Hsl) -> Self {
let rgb = Rgb::from(hsl);
Color::from(rgb)
}
}
impl From<Color> for Rgb {
fn from(color: Color) -> Self {
Rgb::new(f64::from(color.red), f64::from(color.green), f64::from(color.blue), None)
}
}
impl From<Color> for Hsl {
fn from(color: Color) -> Self {
let rgb: Rgb = color.into();
Hsl::from(rgb)
}
}
impl Color {
#[must_use]
pub fn pastel_red() -> Self {
Color { red: 240, green: 116, blue: 108 }
}
#[must_use]
pub fn pastel_blue() -> Self {
Color { red: 108, green: 116, blue: 240 }
}
#[must_use]
pub fn pastel_cyan() -> Self {
Color { red: 167, green: 239, blue: 240 }
}
#[must_use]
pub fn to_hex(self) -> String {
format!("#{:02x}{:02x}{:02x}", self.red, self.green, self.blue)
}
#[must_use]
pub fn maximally_distinct(n: u16, saturation: u8, lightness: u8) -> Vec<Color> {
let mut colors = Vec::with_capacity(usize::from(n));
let saturation = f64::from(saturation);
let lightness = f64::from(lightness);
for i in 0..n {
let hue = f64::from(i) * 360.0 / f64::from(n);
let hsl = Hsl::new(hue, saturation, lightness, None);
colors.push(hsl.into());
}
colors
}
#[must_use]
pub fn darken(self, amount: u8) -> Color {
let hsl: Hsl = self.into();
let new_lightness = (hsl.lightness() - f64::from(amount)).max(0.0);
let new_hsl = Hsl::new(hsl.hue(), hsl.saturation(), new_lightness, None);
new_hsl.into()
}
#[must_use]
pub fn lighten(self, amount: u8) -> Color {
let hsl: Hsl = self.into();
let new_lightness = (hsl.lightness() + f64::from(amount)).min(100.0);
let new_hsl = Hsl::new(hsl.hue(), hsl.saturation(), new_lightness, None);
new_hsl.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_from_tuple() {
let color = Color::from((255, 0, 0));
assert_eq!(color.red, 255);
assert_eq!(color.green, 0);
assert_eq!(color.blue, 0);
}
#[test]
fn test_color_to_hex() {
let color = Color::from((255, 0, 0));
assert_eq!(color.to_hex(), "#ff0000");
}
#[test]
fn test_pastel_colors() {
let red = Color::pastel_red();
assert_eq!(red.to_hex(), "#f0746c");
let blue = Color::pastel_blue();
assert_eq!(blue.to_hex(), "#6c74f0");
let cyan = Color::pastel_cyan();
assert_eq!(cyan.to_hex(), "#a7eff0");
}
#[test]
fn test_maximally_distinct() {
let colors = Color::maximally_distinct(3, 100, 50);
assert_eq!(colors.len(), 3);
assert_ne!(colors[0], colors[1]);
assert_ne!(colors[1], colors[2]);
assert_ne!(colors[0], colors[2]);
}
#[test]
fn test_darken_lighten() {
let color = Color::from((100, 100, 100));
let darkened = color.darken(10);
let lightened = color.lighten(10);
assert_ne!(color, darkened);
assert_ne!(color, lightened);
}
}