Skip to main content

bevy_firework/
curve.rs

1use bevy::prelude::*;
2use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError};
3use serde::{Deserialize, Serialize};
4
5/// This is just a convenience wrapper over some serializable built-in Bevy curve types
6/// These curves are all intended to have unit domain ([0, 1]).
7#[derive(Serialize, Deserialize, Clone, Reflect, Debug)]
8pub enum FireworkCurve<T> {
9    SampleAuto(SampleAutoCurve<T>),
10    UnevenSampleAuto(UnevenSampleAutoCurve<T>),
11    Constant(ConstantCurve<T>),
12}
13
14impl<T> Curve<T> for FireworkCurve<T>
15where
16    T: StableInterpolate,
17{
18    fn domain(&self) -> Interval {
19        match self {
20            FireworkCurve::SampleAuto(c) => c.domain(),
21            FireworkCurve::UnevenSampleAuto(c) => c.domain(),
22            FireworkCurve::Constant(c) => c.domain(),
23        }
24    }
25
26    fn sample_unchecked(&self, t: f32) -> T {
27        match self {
28            FireworkCurve::SampleAuto(c) => c.sample_unchecked(t),
29            FireworkCurve::UnevenSampleAuto(c) => c.sample_unchecked(t),
30            FireworkCurve::Constant(c) => c.sample_unchecked(t),
31        }
32    }
33}
34
35impl<T: Clone> FireworkCurve<T> {
36    /// Creates an appropriate curve type based on the number of samples (e.g. if 1 sample, then
37    /// constant. If 2 samples, then UnevenSampleAutoCurve)
38    ///
39    /// If constant, the domain will be [0,1].
40    pub fn uneven_samples(samples: impl IntoIterator<Item = (f32, T)>) -> Self {
41        // PERF: We only really need to get the first two items out to figure out the curve type.
42        //       It shouldn't really affect performance much though, this function is not in hot path.
43        let samples = samples.into_iter().collect::<Vec<_>>();
44        match samples.len() {
45            0 => panic!("Cannot create curve from 0 samples"),
46            1 => FireworkCurve::Constant(ConstantCurve::new(
47                interval(0., 1.).unwrap(),
48                samples[0].1.clone(),
49            )),
50            _ => FireworkCurve::UnevenSampleAuto(UnevenSampleAutoCurve::new(samples).unwrap()),
51        }
52    }
53
54    /// Creates an appropriate curve type based on the number of samples (e.g. if 1 sample, then
55    /// constant. If 2 samples, then SampleAutoCurve)
56    pub fn even_samples(samples: impl IntoIterator<Item = T>) -> Self {
57        // PERF: We only really need to get the first two items out to figure out the curve type.
58        //       It shouldn't really affect performance much though, this function is not in hot path.
59        let samples = samples.into_iter().collect::<Vec<_>>();
60        match samples.len() {
61            0 => panic!("Cannot create curve from 0 samples"),
62            1 => FireworkCurve::Constant(ConstantCurve::new(
63                interval(0., 1.).unwrap(),
64                samples[0].clone(),
65            )),
66            _ => FireworkCurve::SampleAuto(
67                SampleAutoCurve::new(interval(0., 1.).unwrap(), samples).unwrap(),
68            ),
69        }
70    }
71
72    pub fn constant(sample: T) -> Self {
73        FireworkCurve::Constant(ConstantCurve::new(interval(0., 1.).unwrap(), sample))
74    }
75}
76
77/// A curve whose samples are defined by a collection of colors, with 0..1 domain
78#[derive(Clone, Debug, Reflect, Serialize, Deserialize)]
79pub struct ColorSampleAutoCurve<T> {
80    core: EvenCore<T>,
81}
82
83impl<T> ColorSampleAutoCurve<T>
84where
85    T: Mix + Clone,
86{
87    pub fn new(colors: impl IntoIterator<Item = T>) -> Result<Self, EvenCoreError> {
88        let colors = colors.into_iter().collect::<Vec<_>>();
89        if colors.len() < 2 {
90            Err(EvenCoreError::NotEnoughSamples {
91                samples: colors.len(),
92            })
93        } else {
94            Ok(Self {
95                core: EvenCore::new(Interval::new(0., 1.).unwrap(), colors)?,
96            })
97        }
98    }
99}
100
101impl<T> Curve<T> for ColorSampleAutoCurve<T>
102where
103    T: Mix + Clone,
104{
105    #[inline]
106    fn domain(&self) -> Interval {
107        interval(0., 1.).unwrap()
108    }
109
110    #[inline]
111    fn sample_clamped(&self, t: f32) -> T {
112        // `EvenCore::sample_with` clamps the input implicitly.
113        self.core.sample_with(t, T::mix)
114    }
115
116    #[inline]
117    fn sample_unchecked(&self, t: f32) -> T {
118        self.sample_clamped(t)
119    }
120}
121
122/// A curve whose samples are defined by a collection of colors, with 0..1 domain
123#[derive(Clone, Debug, Reflect, Serialize, Deserialize)]
124pub struct ColorSampleUnevenAutoCurve<T> {
125    core: UnevenCore<T>,
126}
127
128impl<T> ColorSampleUnevenAutoCurve<T>
129where
130    T: Mix + Clone,
131{
132    pub fn new(colors: impl IntoIterator<Item = (f32, T)>) -> Result<Self, UnevenCoreError> {
133        let colors = colors.into_iter().collect::<Vec<_>>();
134        if colors.len() < 2 {
135            Err(UnevenCoreError::NotEnoughSamples {
136                samples: colors.len(),
137            })
138        } else {
139            Ok(Self {
140                core: UnevenCore::new(colors)?,
141            })
142        }
143    }
144}
145
146impl<T> Curve<T> for ColorSampleUnevenAutoCurve<T>
147where
148    T: Mix + Clone,
149{
150    #[inline]
151    fn domain(&self) -> Interval {
152        interval(0., 1.).unwrap()
153    }
154
155    #[inline]
156    fn sample_clamped(&self, t: f32) -> T {
157        self.core.sample_with(t, T::mix)
158    }
159
160    #[inline]
161    fn sample_unchecked(&self, t: f32) -> T {
162        self.sample_clamped(t)
163    }
164}
165
166/// We currently cannot reuse [`FireworkCurve`] for colors because colors
167/// don't implement [`StableInterpolate`]. Instead, they implement [`Mix`] so we
168/// must define dedicated curve types to achieve the same goal.
169/// These curves are all intended to have unit domain ([0, 1]).
170#[derive(Serialize, Deserialize, Clone, Reflect, Debug)]
171pub enum FireworkGradient<T> {
172    ColorSampleAuto(ColorSampleAutoCurve<T>),
173    ColorSampleUnevenAuto(ColorSampleUnevenAutoCurve<T>),
174    Constant(ConstantCurve<T>),
175}
176
177impl<T> Curve<T> for FireworkGradient<T>
178where
179    T: Mix + Clone,
180{
181    fn domain(&self) -> Interval {
182        match self {
183            FireworkGradient::ColorSampleAuto(c) => c.domain(),
184            FireworkGradient::ColorSampleUnevenAuto(c) => c.domain(),
185            FireworkGradient::Constant(c) => c.domain(),
186        }
187    }
188
189    fn sample_unchecked(&self, t: f32) -> T {
190        match self {
191            FireworkGradient::ColorSampleAuto(c) => c.sample_unchecked(t),
192            FireworkGradient::ColorSampleUnevenAuto(c) => c.sample_unchecked(t),
193            FireworkGradient::Constant(c) => c.sample_unchecked(t),
194        }
195    }
196}
197
198impl<T> FireworkGradient<T>
199where
200    T: Mix + Clone,
201{
202    /// Creates an appropriate curve type based on the number of samples (e.g. if 1 sample, then
203    /// constant. If 2 samples, then UnevenSampleAutoCurve)
204    ///
205    /// If constant, the domain will be [0,1].
206    pub fn uneven_samples(samples: impl IntoIterator<Item = (f32, T)>) -> Self {
207        // PERF: We only really need to get the first two items out to figure out the curve type.
208        //       It shouldn't really affect performance much though, this function is not in hot path.
209        let samples = samples.into_iter().collect::<Vec<_>>();
210        match samples.len() {
211            0 => panic!("Cannot create curve from 0 samples"),
212            1 => FireworkGradient::Constant(ConstantCurve::new(
213                interval(0., 1.).unwrap(),
214                samples[0].1.clone(),
215            )),
216            _ => FireworkGradient::ColorSampleUnevenAuto(
217                ColorSampleUnevenAutoCurve::new(samples).unwrap(),
218            ),
219        }
220    }
221
222    /// Creates an appropriate curve type based on the number of samples (e.g. if 1 sample, then
223    /// constant. If 2 samples, then SampleAutoCurve)
224    pub fn even_samples(samples: impl IntoIterator<Item = T>) -> Self {
225        let samples = samples.into_iter().collect::<Vec<_>>();
226        match samples.len() {
227            0 => panic!("Cannot create curve from 0 samples"),
228            1 => FireworkGradient::Constant(ConstantCurve::new(
229                interval(0., 1.).unwrap(),
230                samples[0].clone(),
231            )),
232            _ => FireworkGradient::ColorSampleAuto(ColorSampleAutoCurve::new(samples).unwrap()),
233        }
234    }
235
236    pub fn constant(sample: T) -> Self {
237        FireworkGradient::Constant(ConstantCurve::new(interval(0., 1.).unwrap(), sample))
238    }
239}
240
241#[cfg(test)]
242mod test {
243    use super::*;
244
245    #[test]
246    fn test_curve_linear_rgba() {
247        let curve = FireworkGradient::ColorSampleAuto(
248            ColorSampleAutoCurve::new(vec![
249                Srgba::new(1.0, 0.0, 0.0, 1.0),
250                Srgba::new(0.0, 1.0, 0.0, 1.0),
251                Srgba::new(0.0, 0.0, 1.0, 1.0),
252            ])
253            .unwrap(),
254        );
255        assert_eq!(curve.sample_unchecked(0.0), Srgba::new(1.0, 0.0, 0.0, 1.0));
256        assert_eq!(curve.sample_unchecked(0.5), Srgba::new(0.0, 1.0, 0.0, 1.0));
257        assert_eq!(curve.sample_unchecked(1.0), Srgba::new(0.0, 0.0, 1.0, 1.0));
258    }
259}