use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
pub const WHITE: Self = Self {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
pub const BLACK: Self = Self {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
pub const RED: Self = Self {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
pub const GREEN: Self = Self {
r: 0.0,
g: 1.0,
b: 0.0,
a: 1.0,
};
pub const BLUE: Self = Self {
r: 0.0,
g: 0.0,
b: 1.0,
a: 1.0,
};
pub const TRANSPARENT: Self = Self {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
};
pub const CORNFLOWER_BLUE: Self = Self {
r: 0.392,
g: 0.584,
b: 0.929,
a: 1.0,
};
#[must_use]
#[inline]
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
debug_assert!(
r.is_finite() && g.is_finite() && b.is_finite() && a.is_finite(),
"Color components must be finite"
);
Self { r, g, b, a }
}
#[must_use]
#[inline]
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b, a: 1.0 }
}
#[must_use]
#[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,
}
}
#[must_use]
#[inline]
pub fn from_hex(hex: u32) -> Self {
Self::from_rgba8(
((hex >> 24) & 0xFF) as u8,
((hex >> 16) & 0xFF) as u8,
((hex >> 8) & 0xFF) as u8,
(hex & 0xFF) as u8,
)
}
#[must_use]
#[inline]
pub fn to_array(self) -> [f32; 4] {
[self.r, self.g, self.b, self.a]
}
#[must_use]
#[inline]
pub fn to_wgpu(self) -> wgpu::Color {
wgpu::Color {
r: self.r as f64,
g: self.g as f64,
b: self.b as f64,
a: self.a as f64,
}
}
#[must_use]
#[inline]
pub fn luminance(self) -> f32 {
0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
}
#[must_use]
#[inline]
pub fn lerp(self, other: Self, t: f32) -> Self {
let t = t.clamp(0.0, 1.0);
Self {
r: self.r + (other.r - self.r) * t,
g: self.g + (other.g - self.g) * t,
b: self.b + (other.b - self.b) * t,
a: self.a + (other.a - self.a) * t,
}
}
}
impl Default for Color {
fn default() -> Self {
Self::WHITE
}
}
impl From<[f32; 4]> for Color {
fn from(arr: [f32; 4]) -> Self {
Self::new(arr[0], arr[1], arr[2], arr[3])
}
}
impl From<[f32; 3]> for Color {
fn from(arr: [f32; 3]) -> Self {
Self::rgb(arr[0], arr[1], arr[2])
}
}
impl From<Color> for [f32; 4] {
fn from(c: Color) -> Self {
c.to_array()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color_constants() {
assert_eq!(Color::WHITE.to_array(), [1.0, 1.0, 1.0, 1.0]);
assert_eq!(Color::BLACK.to_array(), [0.0, 0.0, 0.0, 1.0]);
assert_eq!(Color::TRANSPARENT.a, 0.0);
}
#[test]
fn color_from_rgba8() {
let c = Color::from_rgba8(255, 128, 0, 255);
assert!((c.r - 1.0).abs() < f32::EPSILON);
assert!((c.g - 128.0 / 255.0).abs() < 0.001);
assert_eq!(c.b, 0.0);
}
#[test]
fn color_from_hex() {
let red = Color::from_hex(0xFF0000FF);
assert_eq!(red.r, 1.0);
assert_eq!(red.g, 0.0);
assert_eq!(red.b, 0.0);
assert_eq!(red.a, 1.0);
}
#[test]
fn color_lerp() {
let a = Color::BLACK;
let b = Color::WHITE;
let mid = a.lerp(b, 0.5);
assert!((mid.r - 0.5).abs() < f32::EPSILON);
assert!((mid.g - 0.5).abs() < f32::EPSILON);
}
#[test]
fn color_lerp_clamp() {
let a = Color::BLACK;
let b = Color::WHITE;
let over = a.lerp(b, 2.0);
assert_eq!(over.r, 1.0);
}
#[test]
fn color_from_array() {
let c: Color = [0.1, 0.2, 0.3, 0.4].into();
assert_eq!(c.r, 0.1);
assert_eq!(c.a, 0.4);
let c3: Color = [0.5, 0.6, 0.7].into();
assert_eq!(c3.a, 1.0);
}
#[test]
fn color_to_array() {
let arr: [f32; 4] = Color::RED.into();
assert_eq!(arr, [1.0, 0.0, 0.0, 1.0]);
}
#[test]
fn color_serde_roundtrip() {
let c = Color::new(0.1, 0.2, 0.3, 0.9);
let json = serde_json::to_string(&c).unwrap();
let decoded: Color = serde_json::from_str(&json).unwrap();
assert_eq!(c, decoded);
}
#[test]
fn color_to_wgpu() {
let c = Color::RED;
let w = c.to_wgpu();
assert_eq!(w.r, 1.0);
assert_eq!(w.g, 0.0);
}
#[test]
fn color_default() {
assert_eq!(Color::default(), Color::WHITE);
}
#[test]
fn color_luminance() {
assert!((Color::WHITE.luminance() - 1.0).abs() < 0.01);
assert_eq!(Color::BLACK.luminance(), 0.0);
assert!(Color::RED.luminance() < Color::GREEN.luminance());
}
#[test]
fn color_rgb_constructor() {
let c = Color::rgb(0.1, 0.2, 0.3);
assert_eq!(c.a, 1.0);
}
#[test]
fn color_lerp_boundaries() {
let a = Color::RED;
let b = Color::BLUE;
assert_eq!(a.lerp(b, 0.0), a);
assert_eq!(a.lerp(b, 1.0), b);
assert_eq!(a.lerp(b, -1.0), a);
}
#[test]
fn color_from_hex_zero() {
assert_eq!(Color::from_hex(0x00000000), Color::TRANSPARENT);
}
#[test]
fn color_from_rgba8_boundary() {
assert_eq!(Color::from_rgba8(0, 0, 0, 0), Color::TRANSPARENT);
assert_eq!(Color::from_rgba8(255, 255, 255, 255), Color::WHITE);
}
#[test]
fn color_lerp_midpoints() {
let a = Color::new(0.0, 0.0, 0.0, 0.0);
let b = Color::new(1.0, 1.0, 1.0, 1.0);
let q = a.lerp(b, 0.25);
assert!((q.r - 0.25).abs() < f32::EPSILON);
assert!((q.a - 0.25).abs() < f32::EPSILON);
}
#[test]
fn color_from_hex_components() {
let c = Color::from_hex(0xAABBCCDD);
assert!((c.r - 0xAA as f32 / 255.0).abs() < 0.001);
assert!((c.g - 0xBB as f32 / 255.0).abs() < 0.001);
assert!((c.b - 0xCC as f32 / 255.0).abs() < 0.001);
assert!((c.a - 0xDD as f32 / 255.0).abs() < 0.001);
}
#[test]
fn color_rgba8_roundtrip() {
for val in [0u8, 1, 127, 128, 254, 255] {
let c = Color::from_rgba8(val, val, val, val);
let r_back = (c.r * 255.0).round() as u8;
assert_eq!(r_back, val, "roundtrip failed for {val}");
}
}
#[test]
fn color_constants_values() {
assert_eq!(Color::RED.r, 1.0);
assert_eq!(Color::RED.g, 0.0);
assert_eq!(Color::RED.b, 0.0);
assert_eq!(Color::GREEN.g, 1.0);
assert_eq!(Color::GREEN.r, 0.0);
assert_eq!(Color::BLUE.b, 1.0);
assert_eq!(Color::BLUE.r, 0.0);
assert_eq!(Color::TRANSPARENT.a, 0.0);
assert_eq!(Color::TRANSPARENT.r, 0.0);
assert!((Color::CORNFLOWER_BLUE.r - 0.392).abs() < f32::EPSILON);
assert!((Color::CORNFLOWER_BLUE.g - 0.584).abs() < f32::EPSILON);
assert!((Color::CORNFLOWER_BLUE.b - 0.929).abs() < f32::EPSILON);
assert_eq!(Color::CORNFLOWER_BLUE.a, 1.0);
}
#[test]
fn color_to_array_roundtrip() {
let original = Color::new(0.2, 0.4, 0.6, 0.8);
let arr = original.to_array();
let restored: Color = arr.into();
assert_eq!(original, restored);
}
#[test]
fn color_rgb_constructor_alpha() {
let c = Color::rgb(0.5, 0.5, 0.5);
assert_eq!(c.a, 1.0);
assert_eq!(c.r, 0.5);
assert_eq!(c.g, 0.5);
assert_eq!(c.b, 0.5);
}
}