off_rs/geometry/
color.rs

1use std::fmt::{Debug, Display, Formatter};
2
3/// Contains errors that occur while converting a color from or to a different format.
4#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
5pub enum Error {
6    FromF32(String),
7    FromU8(String),
8    ToU8(String),
9}
10
11impl Display for Error {
12    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
13        match self {
14            Self::FromF32(msg) => write!(f, "Failed to convert `f32` to `Color`: {}", msg),
15            Self::FromU8(msg) => write!(f, "Failed to convert `u8` to `Color`: {}", msg),
16            Self::ToU8(msg) => write!(f, "Failed to convert `Color` to `Vec<u8>`: {}", msg),
17        }
18    }
19}
20
21impl std::error::Error for Error {}
22
23/// A color stored as four [`f32`] values (red, green, blue, alpha) ranging from 0.0 to 1.0.
24#[derive(Copy, Clone, PartialEq, Debug)]
25pub struct Color {
26    pub red: f32,
27    pub green: f32,
28    pub blue: f32,
29    pub alpha: f32,
30}
31
32impl Color {
33    /// Creates a new [`Color`] from the given `red`, `green`, `blue` and `alpha` values and checks for validity.
34    ///
35    /// # Errors
36    ///
37    /// Returns an [`Error::FromF32`] of the color values are not between 0.0 and 1.0
38    pub fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Result<Self, Error> {
39        if !(0.0..=1.0).contains(&red)
40            || !(0.0..=1.0).contains(&green)
41            || !(0.0..=1.0).contains(&blue)
42            || !(0.0..=1.0).contains(&alpha)
43        {
44            Err(Error::FromF32(format!(
45                "Color values must be between 0.0 and 1.0, got: ({}, {}, {}, {})",
46                red, green, blue, alpha
47            )))
48        } else {
49            Ok(Self {
50                red,
51                green,
52                blue,
53                alpha,
54            })
55        }
56    }
57}
58
59impl Default for Color {
60    /// Returns the color white.
61    fn default() -> Self {
62        Self {
63            red: 1.0,
64            green: 1.0,
65            blue: 1.0,
66            alpha: 1.0,
67        }
68    }
69}
70
71impl From<Color> for Vec<f32> {
72    /// Converts a [`Color`] to a [`Vec`] of four [`f32`] values.
73    fn from(value: Color) -> Vec<f32> {
74        vec![value.red, value.green, value.blue, value.alpha]
75    }
76}
77
78#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
79impl TryFrom<Color> for Vec<u8> {
80    type Error = Error;
81
82    /// Converts a [`Color`] to a [`Vec<u8>`]
83    ///
84    /// # Errors
85    ///
86    /// Returns [`Error::ToU8`] if the elements of [`Color`] are not in the range of 0.0 to 1.0.
87    fn try_from(value: Color) -> Result<Vec<u8>, Error> {
88        if !(0.0..=1.0).contains(&value.red)
89            || !(0.0..=1.0).contains(&value.green)
90            || !(0.0..=1.0).contains(&value.blue)
91            || !(0.0..=1.0).contains(&value.alpha)
92        {
93            return Err(Error::ToU8(format!(
94                "Color values must be between 0.0 and 1.0, got: {:?}",
95                value
96            )));
97        }
98
99        Ok(vec![
100            (value.red * 255.0).round() as u8,
101            (value.green * 255.0).round() as u8,
102            (value.blue * 255.0).round() as u8,
103            (value.alpha * 255.0).round() as u8,
104        ])
105    }
106}
107
108impl TryFrom<Vec<f32>> for Color {
109    type Error = Error;
110
111    /// Converts a [`Vec<f32>`] to a [`Color`]
112    ///
113    /// # Errors
114    ///
115    /// Returns [`Error::FromF32`] if `value` contains less than three or more than four elements.
116    fn try_from(value: Vec<f32>) -> std::result::Result<Self, Self::Error> {
117        if 3 > value.len() || 4 < value.len() {
118            return Err(Self::Error::FromF32(format!(
119                "Invalid amount of arguments (expected: 3-4, actual: {})",
120                value.len()
121            )));
122        }
123
124        let alpha = if value.len() == 4 { value[3] } else { 1.0 };
125
126        Color::new(value[0], value[1], value[2], alpha)
127    }
128}
129
130impl TryFrom<Vec<u8>> for Color {
131    type Error = Error;
132
133    /// Converts a [`Vec<u8>`] to a [`Color`]
134    ///
135    /// # Errors
136    ///
137    /// Returns [`Error::FromU8`] if `value` contains less than four or more than four elements or if the elements are not in the range of 0 to 255.
138    fn try_from(value: Vec<u8>) -> std::result::Result<Self, Self::Error> {
139        if 3 > value.len() || 4 < value.len() {
140            return Err(Self::Error::FromU8(format!(
141                "Invalid amount of arguments (expected: 3-4, actual: {})",
142                value.len()
143            )));
144        }
145
146        let alpha = if value.len() == 4 { value[3] } else { 255 };
147        let val = [value[0], value[1], value[2], alpha];
148
149        if !(0..=255).contains(&val[0])
150            || !(0..=255).contains(&val[1])
151            || !(0..=255).contains(&val[2])
152            || !(0..=255).contains(&val[3])
153        {
154            return Err(Error::FromU8(format!(
155                "Color values must be between 0 and 255, got: {:?}",
156                val
157            )));
158        }
159
160        Color::new(
161            f32::from(val[0]) / 255.0,
162            f32::from(val[1]) / 255.0,
163            f32::from(val[2]) / 255.0,
164            f32::from(val[3]) / 255.0,
165        )
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    #[allow(clippy::float_cmp)]
175    fn color() {
176        let color = Color::new(0.1, 0.2, 0.3, 0.4).unwrap();
177        assert_eq!(color.red, 0.1);
178        assert_eq!(color.green, 0.2);
179        assert_eq!(color.blue, 0.3);
180        assert_eq!(color.alpha, 0.4);
181    }
182
183    #[test]
184    fn color_fail() {
185        let color = Color::new(1.0, 2.0, 3.0, 4.0);
186        assert!(matches!(color, Err(Error::FromF32(_))));
187    }
188
189    #[test]
190    fn color_from() {
191        let color = Color::new(0.1, 0.2, 0.3, 0.4).unwrap();
192        assert_eq!(Vec::<f32>::from(color), vec![0.1, 0.2, 0.3, 0.4]);
193    }
194
195    #[test]
196    fn color_from_u8() {
197        let color = Color::new(0.5, 0.7, 0.0, 0.33331).unwrap();
198        assert_eq!(Vec::<u8>::try_from(color), Ok(vec![128, 179, 0, 85]));
199    }
200
201    #[test]
202    fn color_from_u8_fail() {
203        let color = Color {
204            red: 1.0,
205            green: 2.0,
206            blue: 3.0,
207            alpha: 4.0,
208        };
209        assert!(matches!(Vec::<u8>::try_from(color), Err(Error::ToU8(_))));
210    }
211
212    #[test]
213    fn try_from_color_rgb() {
214        let vec = vec![0.1, 0.2, 0.3, 0.4];
215        let color = Color::try_from(vec);
216        assert!(color.is_ok());
217        assert_eq!(color.unwrap(), Color::new(0.1, 0.2, 0.3, 0.4).unwrap());
218    }
219
220    #[test]
221    fn try_from_color_rgba() {
222        let vec = vec![0.1, 0.2, 0.3, 0.4];
223        let color = Color::try_from(vec);
224        assert!(color.is_ok());
225        assert_eq!(color.unwrap(), Color::new(0.1, 0.2, 0.3, 0.4).unwrap());
226    }
227
228    #[test]
229    fn try_from_color_err_too_little_arguments() {
230        let vec = vec![1.0, 2.0];
231        let color = Color::try_from(vec);
232        assert!(color.is_err());
233        assert!(matches!(color.unwrap_err(), Error::FromF32(_)));
234    }
235
236    #[test]
237    fn try_from_color_err_too_many_arguments() {
238        let vec = vec![1.0, 2.0, 3.0, 4.0, 5.0];
239        let color = Color::try_from(vec);
240        assert!(color.is_err());
241        assert!(matches!(color.unwrap_err(), Error::FromF32(_)));
242    }
243
244    #[test]
245    fn try_from_color_u8() {
246        let vec = vec![128, 255, 0, 255];
247        let color = Color::try_from(vec);
248        assert!(color.is_ok());
249        assert_eq!(
250            color.unwrap(),
251            Color::new(0.501_960_8, 1.0, 0.0, 1.0).unwrap()
252        );
253    }
254}