#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Color {
Srgb {
r: f32,
g: f32,
b: f32,
a: f32,
},
Cmyk {
c: f32,
m: f32,
y: f32,
k: f32,
a: f32,
},
}
impl Color {
pub const BLACK: Self = Self::Srgb {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
pub const WHITE: Self = Self::Srgb {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
pub const TRANSPARENT: Self = Self::Srgb {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
};
#[must_use]
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
Self::Srgb { r, g, b, a: 1.0 }
}
#[must_use]
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
Self::Srgb { r, g, b, a }
}
#[must_use]
pub const fn cmyk(c: f32, m: f32, y: f32, k: f32) -> Self {
Self::Cmyk { c, m, y, k, a: 1.0 }
}
#[must_use]
pub const fn cmyka(c: f32, m: f32, y: f32, k: f32, a: f32) -> Self {
Self::Cmyk { c, m, y, k, a }
}
#[must_use]
pub fn rgb_clamped(r: f32, g: f32, b: f32) -> Self {
Self::Srgb {
r: clamp_component(r),
g: clamp_component(g),
b: clamp_component(b),
a: 1.0,
}
}
#[must_use]
pub fn rgba_clamped(r: f32, g: f32, b: f32, a: f32) -> Self {
Self::Srgb {
r: clamp_component(r),
g: clamp_component(g),
b: clamp_component(b),
a: clamp_component(a),
}
}
#[must_use]
pub fn cmyk_clamped(c: f32, m: f32, y: f32, k: f32) -> Self {
Self::Cmyk {
c: clamp_component(c),
m: clamp_component(m),
y: clamp_component(y),
k: clamp_component(k),
a: 1.0,
}
}
#[must_use]
pub fn cmyka_clamped(c: f32, m: f32, y: f32, k: f32, a: f32) -> Self {
Self::Cmyk {
c: clamp_component(c),
m: clamp_component(m),
y: clamp_component(y),
k: clamp_component(k),
a: clamp_component(a),
}
}
#[must_use]
pub fn from_rgb8(r: u8, g: u8, b: u8) -> Self {
Self::Srgb {
r: r as f32 / 255.0,
g: g as f32 / 255.0,
b: b as f32 / 255.0,
a: 1.0,
}
}
#[must_use]
pub fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
Self::Srgb {
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]
pub const fn is_srgb(&self) -> bool {
matches!(self, Self::Srgb { .. })
}
#[must_use]
pub const fn is_cmyk(&self) -> bool {
matches!(self, Self::Cmyk { .. })
}
#[must_use]
pub const fn alpha(&self) -> f32 {
match self {
Self::Srgb { a, .. } | Self::Cmyk { a, .. } => *a,
}
}
#[must_use]
pub fn has_transparency(self) -> bool {
self.alpha() < 1.0
}
#[must_use]
pub fn is_transparent(self) -> bool {
self.alpha() == 0.0
}
pub fn validate_components(&self) -> Vec<(&'static str, f32)> {
let mut invalid = Vec::new();
let check = |name: &'static str, val: f32, out: &mut Vec<(&'static str, f32)>| {
if !val.is_finite() || !(0.0..=1.0).contains(&val) {
out.push((name, val));
}
};
match *self {
Self::Srgb { r, g, b, a } => {
check("r", r, &mut invalid);
check("g", g, &mut invalid);
check("b", b, &mut invalid);
check("a", a, &mut invalid);
}
Self::Cmyk { c, m, y, k, a } => {
check("c", c, &mut invalid);
check("m", m, &mut invalid);
check("y", y, &mut invalid);
check("k", k, &mut invalid);
check("a", a, &mut invalid);
}
}
invalid
}
}
fn clamp_component(v: f32) -> f32 {
if v.is_nan() { 0.0 } else { v.clamp(0.0, 1.0) }
}
impl Default for Color {
fn default() -> Self {
Self::BLACK
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_rgb8() {
let c = Color::from_rgb8(255, 128, 0);
match c {
Color::Srgb { r, g, b, a } => {
assert_eq!(r, 1.0);
assert!((g - 128.0 / 255.0).abs() < 1e-6);
assert_eq!(b, 0.0);
assert_eq!(a, 1.0);
}
_ => panic!("expected Srgb variant"),
}
}
#[test]
fn transparency_detection() {
assert!(!Color::BLACK.has_transparency());
assert!(Color::TRANSPARENT.has_transparency());
assert!(Color::rgba(1.0, 0.0, 0.0, 0.5).has_transparency());
}
#[test]
fn default_is_black() {
assert_eq!(Color::default(), Color::BLACK);
}
#[test]
fn cmyk_constructors() {
let c = Color::cmyk(0.5, 0.3, 0.1, 0.0);
assert!(c.is_cmyk());
assert!(!c.is_srgb());
assert!(!c.has_transparency());
assert_eq!(c.alpha(), 1.0);
}
#[test]
fn cmyka_with_alpha() {
let c = Color::cmyka(1.0, 0.0, 0.0, 0.0, 0.5);
assert!(c.is_cmyk());
assert!(c.has_transparency());
assert_eq!(c.alpha(), 0.5);
}
#[test]
fn srgb_detection() {
assert!(Color::rgb(1.0, 0.0, 0.0).is_srgb());
assert!(!Color::rgb(1.0, 0.0, 0.0).is_cmyk());
}
#[test]
fn validate_components_srgb() {
let valid = Color::rgb(0.5, 0.5, 0.5);
assert!(valid.validate_components().is_empty());
let invalid = Color::rgba(1.5, 0.0, -0.1, 1.0);
let errs = invalid.validate_components();
assert_eq!(errs.len(), 2);
}
#[test]
fn validate_components_cmyk() {
let valid = Color::cmyk(0.0, 0.5, 1.0, 0.3);
assert!(valid.validate_components().is_empty());
let invalid = Color::cmyka(0.0, 1.5, 0.0, 0.0, 0.5);
let errs = invalid.validate_components();
assert_eq!(errs.len(), 1);
}
#[test]
fn rgb_clamped_clamps_out_of_range() {
let c = Color::rgb_clamped(1.5, -0.3, 0.5);
assert!(c.validate_components().is_empty());
match c {
Color::Srgb { r, g, b, .. } => {
assert_eq!(r, 1.0);
assert_eq!(g, 0.0);
assert_eq!(b, 0.5);
}
_ => panic!("expected Srgb"),
}
}
#[test]
fn cmyk_clamped_clamps_out_of_range() {
let c = Color::cmyk_clamped(2.0, -1.0, 0.5, 0.3);
assert!(c.validate_components().is_empty());
match c {
Color::Cmyk { c, m, y, k, .. } => {
assert_eq!(c, 1.0);
assert_eq!(m, 0.0);
assert_eq!(y, 0.5);
assert_eq!(k, 0.3);
}
_ => panic!("expected Cmyk"),
}
}
#[test]
fn clamped_handles_nan() {
let c = Color::rgba_clamped(f32::NAN, 0.5, f32::NAN, 1.0);
assert!(c.validate_components().is_empty());
match c {
Color::Srgb { r, g, b, a } => {
assert_eq!(r, 0.0);
assert_eq!(g, 0.5);
assert_eq!(b, 0.0);
assert_eq!(a, 1.0);
}
_ => panic!("expected Srgb"),
}
}
}