#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[repr(C)]
pub struct Srgb {
pub r: f32,
pub g: f32,
pub b: f32,
}
impl Srgb {
pub const BLACK: Self = Self::new(0.0, 0.0, 0.0);
pub const WHITE: Self = Self::new(1.0, 1.0, 1.0);
pub const RED: Self = Self::new(1.0, 0.0, 0.0);
pub const GREEN: Self = Self::new(0.0, 1.0, 0.0);
pub const BLUE: Self = Self::new(0.0, 0.0, 1.0);
#[must_use]
pub const fn new(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b }
}
#[must_use]
pub fn from_u32(packed: u32) -> Self {
let r = f32::from(((packed >> 16) & 0xFF) as u8) / 255.0;
let g = f32::from(((packed >> 8) & 0xFF) as u8) / 255.0;
let b = f32::from((packed & 0xFF) as u8) / 255.0;
Self { r, g, b }
}
#[must_use]
pub fn to_u32(self) -> u32 {
use crate::space::math::channel_to_u8;
let r = u32::from(channel_to_u8(self.r));
let g = u32::from(channel_to_u8(self.g));
let b = u32::from(channel_to_u8(self.b));
(r << 16) | (g << 8) | b
}
#[must_use]
pub fn from_hex(s: &str) -> Option<Self> {
let s = s.strip_prefix('#')?;
match s.len() {
3 => {
let r = u8::from_str_radix(&s[0..1], 16).ok()?;
let g = u8::from_str_radix(&s[1..2], 16).ok()?;
let b = u8::from_str_radix(&s[2..3], 16).ok()?;
Some(Self {
r: f32::from(r | (r << 4)) / 255.0,
g: f32::from(g | (g << 4)) / 255.0,
b: f32::from(b | (b << 4)) / 255.0,
})
}
6 => {
let r = u8::from_str_radix(&s[0..2], 16).ok()?;
let g = u8::from_str_radix(&s[2..4], 16).ok()?;
let b = u8::from_str_radix(&s[4..6], 16).ok()?;
Some(Self {
r: f32::from(r) / 255.0,
g: f32::from(g) / 255.0,
b: f32::from(b) / 255.0,
})
}
_ => None,
}
}
#[must_use]
#[allow(clippy::suboptimal_flops)]
pub fn lerp(self, other: Self, t: f32) -> Self {
use crate::space::math::lerp_f32;
Self {
r: lerp_f32(self.r, other.r, t),
g: lerp_f32(self.g, other.g, t),
b: lerp_f32(self.b, other.b, t),
}
}
#[must_use]
pub const fn clamp(self) -> Self {
use crate::space::math::clamp;
Self {
r: clamp(self.r, 0.0, 1.0),
g: clamp(self.g, 0.0, 1.0),
b: clamp(self.b, 0.0, 1.0),
}
}
#[must_use]
#[allow(clippy::suboptimal_flops)]
pub fn luminance(self) -> f32 {
use crate::space::math::srgb_to_linear_channel as lin;
0.2126 * lin(self.r) + 0.7152 * lin(self.g) + 0.0722 * lin(self.b)
}
#[must_use]
pub fn is_dark(self) -> bool {
self.luminance() < 0.5
}
}
impl From<[f32; 3]> for Srgb {
fn from([r, g, b]: [f32; 3]) -> Self {
Self { r, g, b }
}
}
impl From<Srgb> for [f32; 3] {
fn from(c: Srgb) -> Self {
[c.r, c.g, c.b]
}
}
impl core::fmt::Display for Srgb {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "#{:06x}", self.to_u32())
}
}
impl core::fmt::LowerHex for Srgb {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
core::fmt::LowerHex::fmt(&self.to_u32(), f)
}
}
impl core::fmt::UpperHex for Srgb {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
core::fmt::UpperHex::fmt(&self.to_u32(), f)
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp, // Exact float equality is intentional in unit tests
clippy::float_cmp_const, // Comparing against named constants is fine
)]
mod tests {
use super::*;
#[test]
fn new() {
let c = Srgb::new(1.0, 0.5, 0.0);
assert_eq!(c.r, 1.0);
assert_eq!(c.g, 0.5);
assert_eq!(c.b, 0.0);
}
#[test]
fn from_u32_roundtrip() {
let c = Srgb::from_u32(0xFF_8000);
assert_eq!(c.to_u32(), 0xFF_8000);
}
#[test]
fn from_hex_six_digit() {
let c = Srgb::from_hex("#ff0000").unwrap();
assert!((c.r - 1.0).abs() < 1e-6, "r={}", c.r);
assert!(c.g.abs() < 1e-6);
assert!(c.b.abs() < 1e-6);
}
#[test]
fn from_hex_three_digit() {
let a = Srgb::from_hex("#f00").unwrap();
let b = Srgb::from_hex("#ff0000").unwrap();
assert!((a.r - b.r).abs() < 1e-6);
assert!((a.g - b.g).abs() < 1e-6);
assert!((a.b - b.b).abs() < 1e-6);
}
#[test]
fn from_hex_uppercase() {
assert!(Srgb::from_hex("#FF0000").is_some());
}
#[test]
fn from_hex_invalid() {
assert!(Srgb::from_hex("ff0000").is_none()); assert!(Srgb::from_hex("#gg0000").is_none()); assert!(Srgb::from_hex("#ffff").is_none()); }
#[test]
fn lerp_midpoint() {
let mid = Srgb::RED.lerp(Srgb::BLUE, 0.5);
assert!((mid.r - 0.5).abs() < 1e-6);
assert!((mid.g).abs() < 1e-6);
assert!((mid.b - 0.5).abs() < 1e-6);
}
#[test]
fn lerp_endpoints() {
assert_eq!(Srgb::RED.lerp(Srgb::BLUE, 0.0), Srgb::RED);
assert_eq!(Srgb::RED.lerp(Srgb::BLUE, 1.0), Srgb::BLUE);
}
#[test]
#[cfg(feature = "std")]
fn clamp_out_of_range() {
let c = Srgb::new(2.0, -1.0, 0.5).clamp();
assert_eq!(c.r, 1.0);
assert_eq!(c.g, 0.0);
assert_eq!(c.b, 0.5);
}
#[test]
#[cfg(feature = "std")]
fn display() {
assert_eq!(Srgb::RED.to_string(), "#ff0000");
assert_eq!(Srgb::BLACK.to_string(), "#000000");
assert_eq!(Srgb::WHITE.to_string(), "#ffffff");
}
#[test]
fn from_array_roundtrip() {
let arr: [f32; 3] = Srgb::RED.into();
assert_eq!(arr, [1.0, 0.0, 0.0]);
assert_eq!(Srgb::from(arr), Srgb::RED);
}
#[test]
fn luminance_white_black() {
assert!((Srgb::WHITE.luminance() - 1.0).abs() < 1e-3);
assert!(Srgb::BLACK.luminance() < 1e-6);
}
#[test]
fn is_dark() {
assert!(Srgb::BLACK.is_dark());
assert!(!Srgb::WHITE.is_dark());
}
}