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