bevy_color/
color_gradient.rs

1use crate::Mix;
2use alloc::vec::Vec;
3use bevy_math::curve::{
4    cores::{EvenCore, EvenCoreError},
5    Curve, Interval,
6};
7
8/// A curve whose samples are defined by a collection of colors.
9#[derive(Clone, Debug)]
10#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
11#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
12pub struct ColorCurve<T> {
13    core: EvenCore<T>,
14}
15
16impl<T> ColorCurve<T>
17where
18    T: Mix + Clone,
19{
20    /// Create a new [`ColorCurve`] from a collection of [mixable] types. The domain of this curve
21    /// will always be `[0.0, len - 1]` where `len` is the amount of mixable objects in the
22    /// collection.
23    ///
24    /// This fails if there's not at least two mixable things in the collection.
25    ///
26    /// [mixable]: `Mix`
27    ///
28    /// # Example
29    ///
30    /// ```
31    /// # use bevy_color::palettes::basic::*;
32    /// # use bevy_color::Mix;
33    /// # use bevy_color::Srgba;
34    /// # use bevy_color::ColorCurve;
35    /// # use bevy_math::curve::Interval;
36    /// # use bevy_math::curve::Curve;
37    /// let broken = ColorCurve::new([RED]);
38    /// assert!(broken.is_err());
39    /// let gradient = ColorCurve::new([RED, GREEN, BLUE]);
40    /// assert!(gradient.is_ok());
41    /// assert_eq!(gradient.unwrap().domain(), Interval::new(0.0, 2.0).unwrap());
42    /// ```
43    pub fn new(colors: impl IntoIterator<Item = T>) -> Result<Self, EvenCoreError> {
44        let colors = colors.into_iter().collect::<Vec<_>>();
45        Interval::new(0.0, colors.len().saturating_sub(1) as f32)
46            .map_err(|_| EvenCoreError::NotEnoughSamples {
47                samples: colors.len(),
48            })
49            .and_then(|domain| EvenCore::new(domain, colors))
50            .map(|core| Self { core })
51    }
52}
53
54impl<T> Curve<T> for ColorCurve<T>
55where
56    T: Mix + Clone,
57{
58    #[inline]
59    fn domain(&self) -> Interval {
60        self.core.domain()
61    }
62
63    #[inline]
64    fn sample_clamped(&self, t: f32) -> T {
65        // `EvenCore::sample_with` clamps the input implicitly.
66        self.core.sample_with(t, T::mix)
67    }
68
69    #[inline]
70    fn sample_unchecked(&self, t: f32) -> T {
71        self.sample_clamped(t)
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::{palettes::basic, Srgba};
79    use bevy_math::curve::{Curve, CurveExt};
80
81    #[test]
82    fn test_color_curve() {
83        let broken = ColorCurve::new([basic::RED]);
84        assert!(broken.is_err());
85
86        let gradient = [basic::RED, basic::LIME, basic::BLUE];
87        let curve = ColorCurve::new(gradient).unwrap();
88
89        assert_eq!(curve.domain(), Interval::new(0.0, 2.0).unwrap());
90
91        let brighter_curve = curve.map(|c: Srgba| c.mix(&basic::WHITE, 0.5));
92
93        [
94            (-0.1, None),
95            (0.0, Some([1.0, 0.5, 0.5, 1.0])),
96            (0.5, Some([0.75, 0.75, 0.5, 1.0])),
97            (1.0, Some([0.5, 1.0, 0.5, 1.0])),
98            (1.5, Some([0.5, 0.75, 0.75, 1.0])),
99            (2.0, Some([0.5, 0.5, 1.0, 1.0])),
100            (2.1, None),
101        ]
102        .map(|(t, maybe_rgba)| {
103            let maybe_srgba = maybe_rgba.map(|[r, g, b, a]| Srgba::new(r, g, b, a));
104            (t, maybe_srgba)
105        })
106        .into_iter()
107        .for_each(|(t, maybe_color)| {
108            assert_eq!(brighter_curve.sample(t), maybe_color);
109        });
110    }
111}