Skip to main content

spatialrust_viz/
color.rs

1use crate::{VizError, VizResult};
2
3/// Linear RGBA color with components in the inclusive range `0.0..=1.0`.
4#[derive(Clone, Copy, Debug, PartialEq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct LinearRgba {
7    /// Red component.
8    pub red: f32,
9    /// Green component.
10    pub green: f32,
11    /// Blue component.
12    pub blue: f32,
13    /// Alpha component.
14    pub alpha: f32,
15}
16
17impl LinearRgba {
18    /// Opaque white.
19    pub const WHITE: Self = Self { red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0 };
20    /// Opaque black.
21    pub const BLACK: Self = Self { red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0 };
22
23    /// Creates a validated linear color.
24    pub fn try_new(red: f32, green: f32, blue: f32, alpha: f32) -> VizResult<Self> {
25        let components = [red, green, blue, alpha];
26        if components.iter().any(|value| !value.is_finite() || !(0.0..=1.0).contains(value)) {
27            return Err(VizError::InvalidStyle(
28                "RGBA components must be finite and in 0.0..=1.0".into(),
29            ));
30        }
31        Ok(Self { red, green, blue, alpha })
32    }
33}
34
35impl Default for LinearRgba {
36    fn default() -> Self {
37        Self::WHITE
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use super::LinearRgba;
44
45    #[test]
46    fn rejects_non_finite_and_out_of_range_components() {
47        assert!(LinearRgba::try_new(f32::NAN, 0.0, 0.0, 1.0).is_err());
48        assert!(LinearRgba::try_new(1.1, 0.0, 0.0, 1.0).is_err());
49        assert_eq!(LinearRgba::try_new(0.1, 0.2, 0.3, 0.4).unwrap().alpha, 0.4);
50    }
51}