motion_content/particles/
curves.rs1use super::*;
4use motion_core::Easing;
5
6#[derive(Clone)]
8pub struct ParticleCurve {
9 pub stops: Arc<[(f32, f32)]>,
11 pub easing: Easing,
13}
14
15impl ParticleCurve {
16 pub fn linear(start: f32, end: f32) -> Self {
18 Self {
19 stops: Arc::from([(0.0, start), (1.0, end)]),
20 easing: Easing::Linear,
21 }
22 }
23
24 pub fn sample(&self, progress: f32) -> Result<f32, ValidationError> {
26 validate_times(self.stops.iter().map(|stop| stop.0), self.stops.len())?;
27 if !progress.is_finite()
28 || self
29 .stops
30 .iter()
31 .any(|stop| !stop.1.is_finite() || !(0.0..=65504.0).contains(&stop.1))
32 {
33 return Err(invalid(
34 "curve",
35 "Curve progress/values must be finite and nonnegative",
36 ));
37 }
38 let (first, second, amount) = interval(&self.stops, progress, &self.easing)?;
39 let value = first.1 * (1.0 - amount) + second.1 * amount;
40 if !value.is_finite() || !(0.0..=65504.0).contains(&value) {
41 return Err(invalid("curve", "Easing produced an invalid scalar value"));
42 }
43 Ok(value)
44 }
45}
46
47fn validate_times(times: impl Iterator<Item = f32>, count: usize) -> Result<(), ValidationError> {
48 let mut previous = -1.0;
49 if count == 0 || count > 256 {
50 return Err(invalid("curve", "Require 1..=256 keyframes"));
51 }
52 for time in times {
53 if !time.is_finite() || !(0.0..=1.0).contains(&time) || time <= previous {
54 return Err(invalid(
55 "curve",
56 "Keyframe times must be finite, normalized and strictly increasing",
57 ));
58 }
59 previous = time;
60 }
61 Ok(())
62}
63
64type Interval<'a, Value> = (&'a (f32, Value), &'a (f32, Value), f32);
66
67fn interval<'a, Value>(
68 stops: &'a [(f32, Value)],
69 progress: f32,
70 easing: &Easing,
71) -> Result<Interval<'a, Value>, ValidationError> {
72 let upper = stops
73 .partition_point(|stop| stop.0 <= progress)
74 .min(stops.len() - 1);
75 let lower = if progress >= stops[upper].0 {
76 upper
77 } else {
78 upper.saturating_sub(1)
79 };
80 let amount = if lower == upper {
81 0.0
82 } else {
83 (progress - stops[lower].0) / (stops[upper].0 - stops[lower].0)
84 };
85 Ok((
86 &stops[lower],
87 &stops[upper],
88 easing.try_apply(f64::from(amount.clamp(0.0, 1.0)))? as f32,
89 ))
90}
91
92#[derive(Clone)]
94pub struct ColorCurve {
95 pub stops: Arc<[(f32, [f32; 4])]>,
97 pub easing: Easing,
99}
100
101impl ColorCurve {
102 pub fn sample(&self, progress: f32) -> Result<[f32; 4], ValidationError> {
104 validate_times(self.stops.iter().map(|stop| stop.0), self.stops.len())?;
105 if !progress.is_finite() {
106 return Err(invalid("curve", "Nonfinite color progress"));
107 }
108 let (first, second, amount) = interval(&self.stops, progress, &self.easing)?;
109 for stop in [first, second] {
110 Particle {
111 color: stop.1,
112 ..Particle::default()
113 }
114 .validate()?;
115 }
116 let color = std::array::from_fn(|channel| {
117 first.1[channel] * (1.0 - amount) + second.1[channel] * amount
118 });
119 Particle {
120 color,
121 ..Particle::default()
122 }
123 .validate()?;
124 Ok(color)
125 }
126}
127
128impl ParticleModifier for ColorCurve {
129 fn apply(
130 &self,
131 particle: &mut Particle,
132 _: f32,
133 _: Time,
134 _: &mut Random,
135 ) -> Result<(), ValidationError> {
136 particle.color = self.sample(particle.age / particle.lifetime)?;
137 particle.validate()
138 }
139}
140
141impl ColorOverLifetime {
142 pub fn curve(self, easing: Easing) -> ColorCurve {
144 let denominator = self.stops.len().saturating_sub(1).max(1) as f32;
145 ColorCurve {
146 stops: self
147 .stops
148 .iter()
149 .enumerate()
150 .map(|(index, color)| (index as f32 / denominator, *color))
151 .collect(),
152 easing,
153 }
154 }
155}
156
157#[derive(Clone)]
159pub struct IntensityOverLifetime {
160 pub curve: ParticleCurve,
162}
163
164impl IntensityOverLifetime {
165 pub fn new(curve: ParticleCurve) -> Self {
167 Self { curve }
168 }
169}
170
171impl ParticleModifier for IntensityOverLifetime {
172 fn apply(
173 &self,
174 particle: &mut Particle,
175 _: f32,
176 _: Time,
177 _: &mut Random,
178 ) -> Result<(), ValidationError> {
179 let intensity = self.curve.sample(particle.age / particle.lifetime)?;
180 for channel in &mut particle.color[..3] {
181 *channel *= intensity;
182 }
183 particle.validate()
184 }
185
186 fn resets_color(&self) -> bool {
187 true
188 }
189}
190
191#[derive(Clone)]
195pub struct Temperature {
196 pub curve: ParticleCurve,
198}
199
200impl Temperature {
201 pub fn new(curve: ParticleCurve) -> Self {
203 Self { curve }
204 }
205
206 pub fn rgb(kelvin: f32) -> Result<[f32; 3], ValidationError> {
208 if !kelvin.is_finite() || !(1000.0..=40000.0).contains(&kelvin) {
209 return Err(invalid("temperature", "Require 1000..=40000 Kelvin"));
210 }
211 let temperature = f64::from(kelvin) / 100.0;
212 let red = if temperature <= 66.0 {
213 255.0
214 } else {
215 329.698727446 * libm::pow(temperature - 60.0, -0.1332047592)
216 };
217 let green = if temperature <= 66.0 {
218 99.4708025861 * libm::log(temperature) - 161.1195681661
219 } else {
220 288.1221695283 * libm::pow(temperature - 60.0, -0.0755148492)
221 };
222 let blue = if temperature >= 66.0 {
223 255.0
224 } else if temperature <= 19.0 {
225 0.0
226 } else {
227 138.5177312231 * libm::log(temperature - 10.0) - 305.0447927307
228 };
229 Ok([red, green, blue]
230 .map(|value| motion_core::srgb_to_linear((value / 255.0).clamp(0.0, 1.0) as f32)))
231 }
232}
233
234impl ParticleModifier for Temperature {
235 fn apply(
236 &self,
237 particle: &mut Particle,
238 _: f32,
239 _: Time,
240 _: &mut Random,
241 ) -> Result<(), ValidationError> {
242 let rgb = Self::rgb(self.curve.sample(particle.age / particle.lifetime)?)?;
243 for (channel, value) in rgb.into_iter().enumerate() {
244 particle.color[channel] = value * particle.color[3];
245 }
246 particle.validate()
247 }
248}
249
250#[derive(Clone, Copy, Debug)]
252pub enum FieldCoordinate {
253 X,
255 Y,
257 Speed,
259}
260
261#[derive(Clone)]
263pub struct ColorField {
264 pub coordinate: FieldCoordinate,
266 pub range: [f32; 2],
268 pub colors: ColorCurve,
270}
271
272impl ParticleModifier for ColorField {
273 fn apply(
274 &self,
275 particle: &mut Particle,
276 _: f32,
277 _: Time,
278 _: &mut Random,
279 ) -> Result<(), ValidationError> {
280 if self.range.iter().any(|value| !value.is_finite()) || self.range[0] >= self.range[1] {
281 return Err(invalid("field", "Invalid coordinate range"));
282 }
283 let value = match self.coordinate {
284 FieldCoordinate::X => particle.position[0],
285 FieldCoordinate::Y => particle.position[1],
286 FieldCoordinate::Speed => libm::hypotf(particle.velocity[0], particle.velocity[1]),
287 };
288 particle.color = self
289 .colors
290 .sample(((value - self.range[0]) / (self.range[1] - self.range[0])).clamp(0.0, 1.0))?;
291 particle.validate()
292 }
293}