use std::str::FromStr;
use crate::math::FloatExt;
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
#[inline]
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
#[inline]
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b, a: 1.0 }
}
#[inline]
pub fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
Self {
r: r as f32 / 255.0,
g: g as f32 / 255.0,
b: b as f32 / 255.0,
a: a as f32 / 255.0,
}
}
#[inline]
pub fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
Self::from_rgba8(r, g, b, 255)
}
pub fn from_hex(hex: &str) -> Result<Self, ColorParseError> {
let hex = hex.trim_start_matches('#');
let val = u32::from_str_radix(hex, 16)
.map_err(|_| ColorParseError::InvalidHex(hex.to_string()))?;
match hex.len() {
6 => Ok(Self::from_rgb8(
((val >> 16) & 0xFF) as u8,
((val >> 8) & 0xFF) as u8,
(val & 0xFF) as u8,
)),
8 => Ok(Self::from_rgba8(
((val >> 24) & 0xFF) as u8,
((val >> 16) & 0xFF) as u8,
((val >> 8) & 0xFF) as u8,
(val & 0xFF) as u8,
)),
_ => Err(ColorParseError::InvalidLength(hex.len())),
}
}
pub fn to_rgba32(self) -> u32 {
((self.r.clamp(0.0, 1.0) * 255.0) as u32) << 24
| ((self.g.clamp(0.0, 1.0) * 255.0) as u32) << 16
| ((self.b.clamp(0.0, 1.0) * 255.0) as u32) << 8
| (self.a.clamp(0.0, 1.0) * 255.0) as u32
}
#[inline]
pub fn premultiplied(self) -> Self {
Self {
r: self.r * self.a,
g: self.g * self.a,
b: self.b * self.a,
a: self.a,
}
}
#[inline]
pub fn luminance(self) -> f32 {
0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
}
pub fn lightened(self, amount: f32) -> Self {
Self {
r: (self.r + (1.0 - self.r) * amount).min(1.0),
g: (self.g + (1.0 - self.g) * amount).min(1.0),
b: (self.b + (1.0 - self.b) * amount).min(1.0),
a: self.a,
}
}
pub fn darkened(self, amount: f32) -> Self {
Self {
r: (self.r * (1.0 - amount)).max(0.0),
g: (self.g * (1.0 - amount)).max(0.0),
b: (self.b * (1.0 - amount)).max(0.0),
a: self.a,
}
}
#[inline]
pub fn with_alpha(self, a: f32) -> Self {
Self { a, ..self }
}
#[inline]
pub fn lerp(self, other: Color, t: f32) -> Color {
Color {
r: self.r.lerp(other.r, t),
g: self.g.lerp(other.g, t),
b: self.b.lerp(other.b, t),
a: self.a.lerp(other.a, t),
}
}
pub fn to_hsla(self) -> (f32, f32, f32, f32) {
let max = self.r.max(self.g).max(self.b);
let min = self.r.min(self.g).min(self.b);
let lightness = (max + min) / 2.0;
if (max - min).abs() < 1e-6 {
return (0.0, 0.0, lightness, self.a);
}
let d = max - min;
let saturation = if lightness > 0.5 {
d / (2.0 - max - min)
} else {
d / (max + min)
};
let hue = if (max - self.r).abs() < 1e-6 {
((self.g - self.b) / d + (if self.g < self.b { 6.0 } else { 0.0 })) * 60.0
} else if (max - self.g).abs() < 1e-6 {
((self.b - self.r) / d + 2.0) * 60.0
} else {
((self.r - self.g) / d + 4.0) * 60.0
};
(hue, saturation, lightness, self.a)
}
pub fn from_hsla(h: f32, s: f32, l: f32, a: f32) -> Self {
if (s - 0.0).abs() < 1e-6 {
return Color::new(l, l, l, a);
}
let hue2 = h / 60.0;
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let x = c * (1.0 - (hue2 % 2.0 - 1.0).abs());
let m = l - c / 2.0;
let (r1, g1, b1) = match hue2 as i32 {
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),
};
Color::new(r1 + m, g1 + m, b1 + m, a)
}
}
impl Default for Color {
fn default() -> Self {
Color::WHITE
}
}
impl std::fmt::Display for Color {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Color(#{:02X}{:02X}{:02X}{:02X})",
(self.r * 255.0) as u8,
(self.g * 255.0) as u8,
(self.b * 255.0) as u8,
(self.a * 255.0) as u8,
)
}
}
impl FromStr for Color {
type Err = ColorParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Color::from_hex(s)
}
}
#[derive(Debug, Clone)]
pub enum ColorParseError {
InvalidHex(String),
InvalidLength(usize),
}
impl std::fmt::Display for ColorParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ColorParseError::InvalidHex(h) => write!(f, "Invalid hex color: {}", h),
ColorParseError::InvalidLength(len) => {
write!(f, "Expected 6 or 8 hex digits, got {}", len)
}
}
}
}
impl std::error::Error for ColorParseError {}
impl Color {
pub const WHITE: Color = Color::new(1.0, 1.0, 1.0, 1.0);
pub const BLACK: Color = Color::new(0.0, 0.0, 0.0, 1.0);
pub const RED: Color = Color::new(1.0, 0.0, 0.0, 1.0);
pub const GREEN: Color = Color::new(0.0, 0.8, 0.0, 1.0);
pub const BLUE: Color = Color::new(0.0, 0.0, 1.0, 1.0);
pub const YELLOW: Color = Color::new(1.0, 1.0, 0.0, 1.0);
pub const CYAN: Color = Color::new(0.0, 1.0, 1.0, 1.0);
pub const MAGENTA: Color = Color::new(1.0, 0.0, 1.0, 1.0);
pub const ORANGE: Color = Color::new(1.0, 0.5, 0.0, 1.0);
pub const GRAY: Color = Color::new(0.5, 0.5, 0.5, 1.0);
pub const LIGHT_GRAY: Color = Color::new(0.75, 0.75, 0.75, 1.0);
pub const DARK_GRAY: Color = Color::new(0.25, 0.25, 0.25, 1.0);
pub const TRANSPARENT: Color = Color::new(0.0, 0.0, 0.0, 0.0);
pub const SKY_BLUE: Color = Color::new(0.53, 0.81, 0.92, 1.0);
pub const GOLD: Color = Color::new(1.0, 0.84, 0.0, 1.0);
pub const CORAL: Color = Color::new(1.0, 0.5, 0.31, 1.0);
pub const SALMON: Color = Color::new(0.98, 0.5, 0.45, 1.0);
pub const LIME: Color = Color::new(0.0, 1.0, 0.0, 1.0);
pub const PURPLE: Color = Color::new(0.5, 0.0, 0.5, 1.0);
pub const PINK: Color = Color::new(1.0, 0.75, 0.8, 1.0);
pub const TEAL: Color = Color::new(0.0, 0.5, 0.5, 1.0);
pub const NAVY: Color = Color::new(0.0, 0.0, 0.5, 1.0);
pub const MAROON: Color = Color::new(0.5, 0.0, 0.0, 1.0);
pub const OLIVE: Color = Color::new(0.5, 0.5, 0.0, 1.0);
pub const AQUA: Color = Color::new(0.0, 1.0, 1.0, 1.0);
pub const INDIGO: Color = Color::new(0.29, 0.0, 0.51, 1.0);
pub const VIOLET: Color = Color::new(0.58, 0.0, 0.83, 1.0);
pub const CRIMSON: Color = Color::new(0.86, 0.08, 0.24, 1.0);
pub const TURQUOISE: Color = Color::new(0.25, 0.88, 0.82, 1.0);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_hex() {
let c = Color::from_hex("#FF0080").unwrap();
assert!((c.r - 1.0).abs() < 1e-6);
assert!((c.g - 0.0).abs() < 1e-6);
assert!((c.b - 0.502).abs() < 0.01);
assert!((c.a - 1.0).abs() < 1e-6);
}
#[test]
fn test_lerp() {
let a = Color::BLACK;
let b = Color::WHITE;
let mid = a.lerp(b, 0.5);
assert!((mid.r - 0.5).abs() < 1e-6);
assert!((mid.g - 0.5).abs() < 1e-6);
}
#[test]
fn test_hsla_roundtrip() {
let original = Color::new(0.8, 0.3, 0.5, 0.9);
let (h, s, l, a) = original.to_hsla();
let roundtrip = Color::from_hsla(h, s, l, a);
assert!((roundtrip.r - original.r).abs() < 0.01);
assert!((roundtrip.g - original.g).abs() < 0.01);
assert!((roundtrip.b - original.b).abs() < 0.01);
}
}