bevy_color/
color_ops.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
use bevy_math::{Vec3, Vec4};

/// Methods for changing the luminance of a color. Note that these methods are not
/// guaranteed to produce consistent results across color spaces,
/// but will be within a given space.
pub trait Luminance: Sized {
    /// Return the luminance of this color (0.0 - 1.0).
    fn luminance(&self) -> f32;

    /// Return a new version of this color with the given luminance. The resulting color will
    /// be clamped to the valid range for the color space; for some color spaces, clamping
    /// may cause the hue or chroma to change.
    fn with_luminance(&self, value: f32) -> Self;

    /// Return a darker version of this color. The `amount` should be between 0.0 and 1.0.
    /// The amount represents an absolute decrease in luminance, and is distributive:
    /// `color.darker(a).darker(b) == color.darker(a + b)`. Colors are clamped to black
    /// if the amount would cause them to go below black.
    ///
    /// For a relative decrease in luminance, you can simply `mix()` with black.
    fn darker(&self, amount: f32) -> Self;

    /// Return a lighter version of this color. The `amount` should be between 0.0 and 1.0.
    /// The amount represents an absolute increase in luminance, and is distributive:
    /// `color.lighter(a).lighter(b) == color.lighter(a + b)`. Colors are clamped to white
    /// if the amount would cause them to go above white.
    ///
    /// For a relative increase in luminance, you can simply `mix()` with white.
    fn lighter(&self, amount: f32) -> Self;
}

/// Linear interpolation of two colors within a given color space.
pub trait Mix: Sized {
    /// Linearly interpolate between this and another color, by factor.
    /// Factor should be between 0.0 and 1.0.
    fn mix(&self, other: &Self, factor: f32) -> Self;

    /// Linearly interpolate between this and another color, by factor, storing the result
    /// in this color. Factor should be between 0.0 and 1.0.
    fn mix_assign(&mut self, other: Self, factor: f32) {
        *self = self.mix(&other, factor);
    }
}

/// Trait for returning a grayscale color of a provided lightness.
pub trait Gray: Mix + Sized {
    /// A pure black color.
    const BLACK: Self;
    /// A pure white color.
    const WHITE: Self;

    /// Returns a grey color with the provided lightness from (0.0 - 1.0). 0 is black, 1 is white.
    fn gray(lightness: f32) -> Self {
        Self::BLACK.mix(&Self::WHITE, lightness)
    }
}

/// Methods for manipulating alpha values.
pub trait Alpha: Sized {
    /// Return a new version of this color with the given alpha value.
    fn with_alpha(&self, alpha: f32) -> Self;

    /// Return a the alpha component of this color.
    fn alpha(&self) -> f32;

    /// Sets the alpha component of this color.
    fn set_alpha(&mut self, alpha: f32);

    /// Is the alpha component of this color less than or equal to 0.0?
    fn is_fully_transparent(&self) -> bool {
        self.alpha() <= 0.0
    }

    /// Is the alpha component of this color greater than or equal to 1.0?
    fn is_fully_opaque(&self) -> bool {
        self.alpha() >= 1.0
    }
}

/// Trait for manipulating the hue of a color.
pub trait Hue: Sized {
    /// Return a new version of this color with the hue channel set to the given value.
    fn with_hue(&self, hue: f32) -> Self;

    /// Return the hue of this color [0.0, 360.0].
    fn hue(&self) -> f32;

    /// Sets the hue of this color.
    fn set_hue(&mut self, hue: f32);

    /// Return a new version of this color with the hue channel rotated by the given degrees.
    fn rotate_hue(&self, degrees: f32) -> Self {
        let rotated_hue = (self.hue() + degrees).rem_euclid(360.);
        self.with_hue(rotated_hue)
    }
}

/// Trait with methods for converting colors to non-color types
pub trait ColorToComponents {
    /// Convert to an f32 array
    fn to_f32_array(self) -> [f32; 4];
    /// Convert to an f32 array without the alpha value
    fn to_f32_array_no_alpha(self) -> [f32; 3];
    /// Convert to a Vec4
    fn to_vec4(self) -> Vec4;
    /// Convert to a Vec3
    fn to_vec3(self) -> Vec3;
    /// Convert from an f32 array
    fn from_f32_array(color: [f32; 4]) -> Self;
    /// Convert from an f32 array without the alpha value
    fn from_f32_array_no_alpha(color: [f32; 3]) -> Self;
    /// Convert from a Vec4
    fn from_vec4(color: Vec4) -> Self;
    /// Convert from a Vec3
    fn from_vec3(color: Vec3) -> Self;
}

/// Trait with methods for converting colors to packed non-color types
pub trait ColorToPacked {
    /// Convert to [u8; 4] where that makes sense (Srgba is most relevant)
    fn to_u8_array(self) -> [u8; 4];
    /// Convert to [u8; 3] where that makes sense (Srgba is most relevant)
    fn to_u8_array_no_alpha(self) -> [u8; 3];
    /// Convert from [u8; 4] where that makes sense (Srgba is most relevant)
    fn from_u8_array(color: [u8; 4]) -> Self;
    /// Convert to [u8; 3] where that makes sense (Srgba is most relevant)
    fn from_u8_array_no_alpha(color: [u8; 3]) -> Self;
}

/// Utility function for interpolating hue values. This ensures that the interpolation
/// takes the shortest path around the color wheel, and that the result is always between
/// 0 and 360.
pub(crate) fn lerp_hue(a: f32, b: f32, t: f32) -> f32 {
    let diff = (b - a + 180.0).rem_euclid(360.) - 180.;
    (a + diff * t).rem_euclid(360.0)
}

#[cfg(test)]
mod tests {
    use core::fmt::Debug;

    use super::*;
    use crate::{testing::assert_approx_eq, Hsla};

    #[test]
    fn test_rotate_hue() {
        let hsla = Hsla::hsl(180.0, 1.0, 0.5);
        assert_eq!(hsla.rotate_hue(90.0), Hsla::hsl(270.0, 1.0, 0.5));
        assert_eq!(hsla.rotate_hue(-90.0), Hsla::hsl(90.0, 1.0, 0.5));
        assert_eq!(hsla.rotate_hue(180.0), Hsla::hsl(0.0, 1.0, 0.5));
        assert_eq!(hsla.rotate_hue(-180.0), Hsla::hsl(0.0, 1.0, 0.5));
        assert_eq!(hsla.rotate_hue(0.0), hsla);
        assert_eq!(hsla.rotate_hue(360.0), hsla);
        assert_eq!(hsla.rotate_hue(-360.0), hsla);
    }

    #[test]
    fn test_hue_wrap() {
        assert_approx_eq!(lerp_hue(10., 20., 0.25), 12.5, 0.001);
        assert_approx_eq!(lerp_hue(10., 20., 0.5), 15., 0.001);
        assert_approx_eq!(lerp_hue(10., 20., 0.75), 17.5, 0.001);

        assert_approx_eq!(lerp_hue(20., 10., 0.25), 17.5, 0.001);
        assert_approx_eq!(lerp_hue(20., 10., 0.5), 15., 0.001);
        assert_approx_eq!(lerp_hue(20., 10., 0.75), 12.5, 0.001);

        assert_approx_eq!(lerp_hue(10., 350., 0.25), 5., 0.001);
        assert_approx_eq!(lerp_hue(10., 350., 0.5), 0., 0.001);
        assert_approx_eq!(lerp_hue(10., 350., 0.75), 355., 0.001);

        assert_approx_eq!(lerp_hue(350., 10., 0.25), 355., 0.001);
        assert_approx_eq!(lerp_hue(350., 10., 0.5), 0., 0.001);
        assert_approx_eq!(lerp_hue(350., 10., 0.75), 5., 0.001);
    }

    fn verify_gray<Col>()
    where
        Col: Gray + Debug + PartialEq,
    {
        assert_eq!(Col::gray(0.), Col::BLACK);
        assert_eq!(Col::gray(1.), Col::WHITE);
    }

    #[test]
    fn test_gray() {
        verify_gray::<Hsla>();
        verify_gray::<crate::Hsva>();
        verify_gray::<crate::Hwba>();
        verify_gray::<crate::Laba>();
        verify_gray::<crate::Lcha>();
        verify_gray::<crate::LinearRgba>();
        verify_gray::<crate::Oklaba>();
        verify_gray::<crate::Oklcha>();
        verify_gray::<crate::Xyza>();
    }
}