Skip to main content

bevy_color/
color_ops.rs

1use bevy_math::{ops, Vec3, Vec4};
2
3/// Methods for changing the luminance of a color. Note that these methods are not
4/// guaranteed to produce consistent results across color spaces,
5/// but will be within a given space.
6pub trait Luminance: Sized {
7    /// Return the luminance of this color. SDR colors are in `[0.0, 1.0]`. An HDR
8    /// color's luminance can be above or below 1.0.
9    fn luminance(&self) -> f32;
10
11    /// Return a new version of this color with the given luminance. The result is not
12    /// clamped, so an out-of-range target is stored as-is.
13    fn with_luminance(&self, value: f32) -> Self;
14
15    /// Return a darker version of this color. The `amount` should be between 0.0 and 1.0.
16    /// The amount represents an absolute decrease in luminance, and is distributive:
17    /// `color.darker(a).darker(b) == color.darker(a + b)`. Colors are clamped to black
18    /// if the amount would cause them to go below black.
19    ///
20    /// For a relative decrease in luminance, you can simply `mix()` with black.
21    fn darker(&self, amount: f32) -> Self;
22
23    /// Return a lighter version of this color. The `amount` should be between 0.0 and 1.0.
24    /// The amount represents an absolute increase in luminance, and is distributive:
25    /// `color.lighter(a).lighter(b) == color.lighter(a + b)`. Colors are clamped to white
26    /// if the amount would cause them to go above white, so this operation is not suitable
27    /// for HDR colors. To scale an HDR color, use [`with_luminance`](Luminance::with_luminance).
28    ///
29    /// For a relative increase in luminance, you can simply `mix()` with white.
30    fn lighter(&self, amount: f32) -> Self;
31}
32
33/// Linear interpolation of two colors within a given color space.
34pub trait Mix: Sized {
35    /// Linearly interpolate between this and another color, by factor.
36    /// Factor should be between 0.0 and 1.0.
37    fn mix(&self, other: &Self, factor: f32) -> Self;
38
39    /// Linearly interpolate between this and another color, by factor, storing the result
40    /// in this color. Factor should be between 0.0 and 1.0.
41    fn mix_assign(&mut self, other: Self, factor: f32) {
42        *self = self.mix(&other, factor);
43    }
44}
45
46/// Trait for returning a grayscale color of a provided lightness.
47pub trait Gray: Mix + Sized {
48    /// A pure black color.
49    const BLACK: Self;
50    /// A pure white color.
51    const WHITE: Self;
52
53    /// Returns a grey color with the provided lightness from (0.0 - 1.0). 0 is black, 1 is white.
54    fn gray(lightness: f32) -> Self {
55        Self::BLACK.mix(&Self::WHITE, lightness)
56    }
57}
58
59/// Methods for manipulating alpha values.
60pub trait Alpha: Sized {
61    /// Return a new version of this color with the given alpha value.
62    fn with_alpha(&self, alpha: f32) -> Self;
63
64    /// Return the alpha component of this color.
65    fn alpha(&self) -> f32;
66
67    /// Sets the alpha component of this color.
68    fn set_alpha(&mut self, alpha: f32);
69
70    /// Is the alpha component of this color less than or equal to 0.0?
71    fn is_fully_transparent(&self) -> bool {
72        self.alpha() <= 0.0
73    }
74
75    /// Is the alpha component of this color greater than or equal to 1.0?
76    fn is_fully_opaque(&self) -> bool {
77        self.alpha() >= 1.0
78    }
79}
80
81impl Alpha for f32 {
82    fn with_alpha(&self, alpha: f32) -> Self {
83        alpha
84    }
85
86    fn alpha(&self) -> f32 {
87        *self
88    }
89
90    fn set_alpha(&mut self, alpha: f32) {
91        *self = alpha;
92    }
93}
94
95/// Trait for manipulating the hue of a color.
96pub trait Hue: Sized {
97    /// Return a new version of this color with the hue channel set to the given value.
98    fn with_hue(&self, hue: f32) -> Self;
99
100    /// Return the hue of this color [0.0, 360.0].
101    fn hue(&self) -> f32;
102
103    /// Sets the hue of this color.
104    fn set_hue(&mut self, hue: f32);
105
106    /// Return a new version of this color with the hue channel rotated by the given degrees.
107    fn rotate_hue(&self, degrees: f32) -> Self {
108        let rotated_hue = ops::rem_euclid(self.hue() + degrees, 360.);
109        self.with_hue(rotated_hue)
110    }
111}
112
113/// Trait for manipulating the saturation of a color.
114///
115/// When working with color spaces that do not have native saturation components
116/// the operations are performed in [`Hsla`](`crate::Hsla`).
117pub trait Saturation: Sized {
118    /// Return a new version of this color with the saturation channel set to the given value.
119    fn with_saturation(&self, saturation: f32) -> Self;
120
121    /// Return the saturation of this color [0.0, 1.0].
122    fn saturation(&self) -> f32;
123
124    /// Sets the saturation of this color.
125    fn set_saturation(&mut self, saturation: f32);
126}
127
128/// Trait with methods for converting colors to non-color types
129pub trait ColorToComponents {
130    /// Convert to an `f32` array
131    fn to_f32_array(self) -> [f32; 4];
132    /// Convert to an `f32` array without the alpha value
133    fn to_f32_array_no_alpha(self) -> [f32; 3];
134    /// Convert to a `Vec4`
135    fn to_vec4(self) -> Vec4;
136    /// Convert to a `Vec3`
137    fn to_vec3(self) -> Vec3;
138    /// Convert from an `f32` array
139    fn from_f32_array(color: [f32; 4]) -> Self;
140    /// Convert from an `f32` array without the alpha value
141    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self;
142    /// Convert from a `Vec4`
143    fn from_vec4(color: Vec4) -> Self;
144    /// Convert from a `Vec3`
145    fn from_vec3(color: Vec3) -> Self;
146}
147
148/// Trait with methods for converting colors to packed non-color types
149pub trait ColorToPacked {
150    /// Convert to `[u8; 4]` where that makes sense (Srgba is most relevant)
151    fn to_u8_array(self) -> [u8; 4];
152    /// Convert to `[u8; 3]` where that makes sense (Srgba is most relevant)
153    fn to_u8_array_no_alpha(self) -> [u8; 3];
154    /// Convert from `[u8; 4]` where that makes sense (Srgba is most relevant)
155    fn from_u8_array(color: [u8; 4]) -> Self;
156    /// Convert to `[u8; 3]` where that makes sense (Srgba is most relevant)
157    fn from_u8_array_no_alpha(color: [u8; 3]) -> Self;
158}
159
160/// Utility function for interpolating hue values. This ensures that the interpolation
161/// takes the shortest path around the color wheel, and that the result is always between
162/// 0 and 360.
163pub(crate) fn lerp_hue(a: f32, b: f32, t: f32) -> f32 {
164    let diff = ops::rem_euclid(b - a + 180.0, 360.) - 180.;
165    ops::rem_euclid(a + diff * t, 360.)
166}
167
168#[cfg(test)]
169mod tests {
170    use core::fmt::Debug;
171
172    use super::*;
173    use crate::{testing::assert_approx_eq, Hsla};
174
175    #[test]
176    fn test_rotate_hue() {
177        let hsla = Hsla::hsl(180.0, 1.0, 0.5);
178        assert_eq!(hsla.rotate_hue(90.0), Hsla::hsl(270.0, 1.0, 0.5));
179        assert_eq!(hsla.rotate_hue(-90.0), Hsla::hsl(90.0, 1.0, 0.5));
180        assert_eq!(hsla.rotate_hue(180.0), Hsla::hsl(0.0, 1.0, 0.5));
181        assert_eq!(hsla.rotate_hue(-180.0), Hsla::hsl(0.0, 1.0, 0.5));
182        assert_eq!(hsla.rotate_hue(0.0), hsla);
183        assert_eq!(hsla.rotate_hue(360.0), hsla);
184        assert_eq!(hsla.rotate_hue(-360.0), hsla);
185    }
186
187    #[test]
188    fn test_hue_wrap() {
189        assert_approx_eq!(lerp_hue(10., 20., 0.25), 12.5, 0.001);
190        assert_approx_eq!(lerp_hue(10., 20., 0.5), 15., 0.001);
191        assert_approx_eq!(lerp_hue(10., 20., 0.75), 17.5, 0.001);
192
193        assert_approx_eq!(lerp_hue(20., 10., 0.25), 17.5, 0.001);
194        assert_approx_eq!(lerp_hue(20., 10., 0.5), 15., 0.001);
195        assert_approx_eq!(lerp_hue(20., 10., 0.75), 12.5, 0.001);
196
197        assert_approx_eq!(lerp_hue(10., 350., 0.25), 5., 0.001);
198        assert_approx_eq!(lerp_hue(10., 350., 0.5), 0., 0.001);
199        assert_approx_eq!(lerp_hue(10., 350., 0.75), 355., 0.001);
200
201        assert_approx_eq!(lerp_hue(350., 10., 0.25), 355., 0.001);
202        assert_approx_eq!(lerp_hue(350., 10., 0.5), 0., 0.001);
203        assert_approx_eq!(lerp_hue(350., 10., 0.75), 5., 0.001);
204    }
205
206    fn verify_gray<Col>()
207    where
208        Col: Gray + Debug + PartialEq,
209    {
210        assert_eq!(Col::gray(0.), Col::BLACK);
211        assert_eq!(Col::gray(1.), Col::WHITE);
212    }
213
214    #[test]
215    fn test_gray() {
216        verify_gray::<Hsla>();
217        verify_gray::<crate::Hsva>();
218        verify_gray::<crate::Hwba>();
219        verify_gray::<crate::Laba>();
220        verify_gray::<crate::Lcha>();
221        verify_gray::<crate::LinearRgba>();
222        verify_gray::<crate::Oklaba>();
223        verify_gray::<crate::Oklcha>();
224        verify_gray::<crate::Xyza>();
225    }
226}