motion_content/particles/components/
modifiers.rs1use super::*;
4use motion_core::{Rect, Vector2};
5
6#[derive(Clone, Copy, Debug)]
8pub struct Gravity {
9 pub acceleration: Vector2,
11}
12
13impl Gravity {
14 pub fn new(acceleration: Vector2) -> Self {
16 Self { acceleration }
17 }
18}
19
20#[derive(Clone, Copy, Debug)]
22pub struct Wind {
23 pub acceleration: Vector2,
25}
26
27impl Wind {
28 pub fn new(acceleration: Vector2) -> Self {
30 Self { acceleration }
31 }
32}
33
34macro_rules! force {
35 ($name:ident) => {
36 impl ParticleModifier for $name {
37 fn apply(
38 &self,
39 particle: &mut Particle,
40 step: f32,
41 _: Time,
42 _: &mut Random,
43 ) -> Result<(), ValidationError> {
44 particle.velocity[0] += self.acceleration.x as f32 * step;
45 particle.velocity[1] += self.acceleration.y as f32 * step;
46 particle.validate()
47 }
48 }
49 };
50}
51force!(Gravity);
52force!(Wind);
53
54#[derive(Clone, Copy, Debug)]
56pub struct Drag {
57 pub coefficient: f64,
59}
60
61impl Drag {
62 pub fn new(coefficient: f64) -> Self {
64 Self { coefficient }
65 }
66}
67
68impl ParticleModifier for Drag {
69 fn apply(
70 &self,
71 particle: &mut Particle,
72 step: f32,
73 _: Time,
74 _: &mut Random,
75 ) -> Result<(), ValidationError> {
76 if !self.coefficient.is_finite() || self.coefficient < 0.0 {
77 return Err(invalid("drag", "Drag must be finite and nonnegative"));
78 }
79 let damping = libm::exp(-self.coefficient * f64::from(step)) as f32;
80 particle.velocity = particle.velocity.map(|value| value * damping);
81 Ok(())
82 }
83}
84
85#[derive(Clone, Copy, Debug)]
87pub struct Vortex {
88 pub center: Point,
90 pub strength: f64,
92}
93
94impl ParticleModifier for Vortex {
95 fn apply(
96 &self,
97 particle: &mut Particle,
98 step: f32,
99 _: Time,
100 _: &mut Random,
101 ) -> Result<(), ValidationError> {
102 let horizontal = f64::from(particle.position[0]) - self.center.x;
103 let vertical = f64::from(particle.position[1]) - self.center.y;
104 let distance = libm::hypot(horizontal, vertical).max(1.0);
105 particle.velocity[0] += (-vertical / distance * self.strength * f64::from(step)) as f32;
106 particle.velocity[1] += (horizontal / distance * self.strength * f64::from(step)) as f32;
107 particle.validate()
108 }
109}
110
111#[derive(Clone, Copy, Debug)]
113pub struct Attractor {
114 pub center: Point,
116 pub strength: f64,
118}
119
120impl ParticleModifier for Attractor {
121 fn apply(
122 &self,
123 particle: &mut Particle,
124 step: f32,
125 _: Time,
126 _: &mut Random,
127 ) -> Result<(), ValidationError> {
128 let horizontal = self.center.x - f64::from(particle.position[0]);
129 let vertical = self.center.y - f64::from(particle.position[1]);
130 let distance = libm::hypot(horizontal, vertical).max(1.0);
131 let amount = self.strength / (distance * distance * distance) * f64::from(step);
132 particle.velocity[0] += (horizontal * amount) as f32;
133 particle.velocity[1] += (vertical * amount) as f32;
134 particle.validate()
135 }
136}
137
138#[derive(Clone, Debug)]
140pub struct Seek {
141 pub targets: Arc<[Point]>,
143 pub strength: f64,
145}
146
147impl ParticleModifier for Seek {
148 fn apply(
149 &self,
150 particle: &mut Particle,
151 step: f32,
152 _: Time,
153 _: &mut Random,
154 ) -> Result<(), ValidationError> {
155 if self.targets.is_empty()
156 || self.targets.len() > 65536
157 || !self.strength.is_finite()
158 || self.strength < 0.0
159 {
160 return Err(invalid("seek", "Invalid target set/strength"));
161 }
162 let target = self.targets[particle.index as usize % self.targets.len()];
163 let amount = (self.strength * f64::from(step)).min(1.0);
164 for (axis, value) in [target.x, target.y].into_iter().enumerate() {
165 let desired = value - f64::from(particle.position[axis]);
166 particle.velocity[axis] =
167 (f64::from(particle.velocity[axis]) * (1.0 - amount) + desired * amount) as f32;
168 }
169 particle.validate()
170 }
171}
172
173#[derive(Clone, Copy, Debug)]
176pub struct BounceBox {
177 pub bounds: Rect,
179}
180
181impl BounceBox {
182 pub fn new(bounds: Rect) -> Self {
184 Self { bounds }
185 }
186}
187
188impl ParticleModifier for BounceBox {
189 fn apply(
190 &self,
191 particle: &mut Particle,
192 step: f32,
193 _: Time,
194 _: &mut Random,
195 ) -> Result<(), ValidationError> {
196 motion_core::drawing::validate_rect(self.bounds, "particles", "bounce_box")?;
197 for (axis, (low, high)) in [
198 (self.bounds.left, self.bounds.right),
199 (self.bounds.top, self.bounds.bottom),
200 ]
201 .into_iter()
202 .enumerate()
203 {
204 let extent = high - low;
205 if extent <= 0.0 {
206 return Err(invalid(
207 "bounce",
208 "Bounce box must have positive dimensions",
209 ));
210 }
211 let next = f64::from(particle.position[axis])
212 + f64::from(particle.velocity[axis]) * f64::from(step);
213 let phase = (next - low).rem_euclid(extent * 2.0);
214 let reflected = if phase <= extent {
215 low + phase
216 } else {
217 high - (phase - extent)
218 };
219 if phase > extent {
220 particle.velocity[axis] = -particle.velocity[axis];
221 }
222 particle.position[axis] =
223 (reflected - f64::from(particle.velocity[axis]) * f64::from(step)) as f32;
224 }
225 particle.validate()
226 }
227}
228
229#[derive(Clone, Debug)]
231pub struct ColorOverLifetime {
232 pub stops: Arc<[[f32; 4]]>,
234}
235
236impl ColorOverLifetime {
237 pub fn new(stops: impl Into<Arc<[[f32; 4]]>>) -> Self {
239 Self {
240 stops: stops.into(),
241 }
242 }
243}
244
245impl ParticleModifier for ColorOverLifetime {
246 fn apply(
247 &self,
248 particle: &mut Particle,
249 _: f32,
250 _: Time,
251 _: &mut Random,
252 ) -> Result<(), ValidationError> {
253 if self.stops.is_empty() || self.stops.len() > 256 {
254 return Err(invalid("colors", "Require 1..=256 color stops"));
255 }
256 for color in self.stops.iter() {
257 Particle {
258 color: *color,
259 ..Default::default()
260 }
261 .validate()?;
262 }
263 let position =
264 (particle.age / particle.lifetime).clamp(0.0, 1.0) * (self.stops.len() - 1) as f32;
265 let lower = libm::floorf(position) as usize;
266 let upper = (lower + 1).min(self.stops.len() - 1);
267 let amount = position - lower as f32;
268 particle.color = std::array::from_fn(|channel| {
269 self.stops[lower][channel] * (1.0 - amount) + self.stops[upper][channel] * amount
270 });
271 particle.validate()
272 }
273}
274
275#[derive(Clone, Copy, Debug)]
277pub struct SizeOverLifetime {
278 pub start: f64,
280 pub end: f64,
282}
283
284impl ParticleModifier for SizeOverLifetime {
285 fn apply(
286 &self,
287 particle: &mut Particle,
288 _: f32,
289 _: Time,
290 _: &mut Random,
291 ) -> Result<(), ValidationError> {
292 if !self.start.is_finite()
293 || !self.end.is_finite()
294 || !(0.0..=4096.0).contains(&self.start)
295 || !(0.0..=4096.0).contains(&self.end)
296 {
297 return Err(invalid("size", "Size stops must be finite in 0..=4096"));
298 }
299 let progress = f64::from(particle.age / particle.lifetime).clamp(0.0, 1.0);
300 particle.size = (self.start * (1.0 - progress) + self.end * progress) as f32;
301 particle.validate()
302 }
303}