Skip to main content

motion_content/particles/
components.rs

1//! Particle emitters and emission settings; modifiers live in `modifiers`.
2
3use super::*;
4use crate::VectorTree;
5use motion_core::{Animation, Path2D, Point, Rect, Vector2};
6
7mod image;
8mod modifiers;
9
10pub use image::ImageDensity;
11use image::ImageDistribution;
12pub use modifiers::{
13    Attractor, BounceBox, ColorOverLifetime, Drag, Gravity, Seek, SizeOverLifetime, Vortex, Wind,
14};
15
16#[cfg(test)]
17mod tests;
18
19/// Default constant emission rate in particles per second.
20const DEFAULT_RATE: f64 = 100.0;
21
22/// Default particle lifetime in seconds.
23const DEFAULT_LIFETIME_SECONDS: f64 = 2.0;
24
25/// Default isotropic launch speed in design units per second.
26const DEFAULT_SPEED: f64 = 40.0;
27
28macro_rules! emitter {
29    ($name:ident,$doc:literal) => {
30        #[doc=$doc]
31        #[derive(Clone)]
32        pub struct $name(EmitterSettings);
33        impl $name {
34            /// Replaces particles/second; constant schedules quantize to step boundaries.
35            pub fn rate(mut self, rate: f64) -> Self {
36                self.0.rate = EmissionRate::Constant(rate);
37                self
38            }
39            /// Uses seeded stochastic fractional births at each fixed step; its
40            /// expected birth count follows the animation, without hidden cursors.
41            pub fn rate_animation(mut self, rate: impl Animation<f64> + 'static) -> Self {
42                self.0.rate = EmissionRate::Animated(Arc::new(rate));
43                self
44            }
45            /// Replaces half-open particle lifetime.
46            pub fn lifetime(mut self, lifetime: Duration) -> Self {
47                self.0.lifetime = lifetime;
48                self
49            }
50            /// Replaces isotropic launch speed (path sources use path tangents).
51            pub fn speed(mut self, speed: f64) -> Self {
52                self.0.speed = speed;
53                self
54            }
55            /// Adds a fixed design-unit velocity to the sampled launch direction.
56            pub fn velocity(mut self, velocity: Vector2) -> Self {
57                self.0.velocity = velocity;
58                self
59            }
60            /// Sets finite premultiplied birth color.
61            pub fn color(mut self, color: [f32; 4]) -> Self {
62                self.0.color = color;
63                self
64            }
65            /// Sets birth size in 0..=4096.
66            pub fn size(mut self, size: f32) -> Self {
67                self.0.size = size;
68                self
69            }
70            /// Samples depth into `custom[1]`; a constant range consumes no random draw.
71            pub fn depth_range(mut self, minimum: f32, maximum: f32) -> Self {
72                self.0.depth = [minimum, maximum];
73                self
74            }
75            /// Multiplies the birth color by a solid or gradient paint evaluated at the
76            /// sampled emission point; `bounds` is the paint's objectBoundingBox.
77            /// Consumes no random draws, so positions and timing are unchanged.
78            pub fn paint(
79                mut self,
80                paint: motion_core::Paint,
81                bounds: Rect,
82            ) -> Result<Self, ValidationError> {
83                motion_core::drawing::validate_rect(bounds, "particles.emitter", "paint bounds")?;
84                if !matches!(
85                    paint,
86                    motion_core::Paint::Solid(_)
87                        | motion_core::Paint::Linear(_)
88                        | motion_core::Paint::Radial(_)
89                        | motion_core::Paint::Sweep(_)
90                ) {
91                    return Err(invalid("paint", "Emitter paint must be solid or gradient"));
92                }
93                self.0.paint = Some(Arc::new((paint, bounds)));
94                Ok(self)
95            }
96            /// Selects a bounded deterministic premultiplied birth palette.
97            pub fn palette(mut self, colors: Arc<[[f32; 4]]>) -> Self {
98                self.0.palette = Some(colors);
99                self
100            }
101        }
102        impl ParticleEmitter for $name {
103            fn emit(
104                &self,
105                step: u64,
106                time: Time,
107                random: &mut Random,
108                output: &mut Vec<Particle>,
109            ) -> Result<(), ValidationError> {
110                self.0.emit(step, time, random, output)
111            }
112        }
113    };
114}
115
116emitter!(
117    PointEmitter,
118    "A point source with stable fixed-boundary births."
119);
120
121emitter!(
122    DiskEmitter,
123    "A uniform area disk source, not a circumference distribution."
124);
125
126emitter!(
127    RectEmitter,
128    "A uniform rectangular source in local design coordinates."
129);
130
131emitter!(
132    PathEmitter,
133    "An arc-length path source inheriting its tangent direction."
134);
135
136emitter!(
137    SurfaceEmitter,
138    "An area-uniform shared VectorSurface source."
139);
140
141emitter!(
142    ImageEmitter,
143    "An image source weighted by alpha or linear luminance, inheriting pixel colors."
144);
145
146impl PointEmitter {
147    /// Creates a point source.
148    pub fn new(point: Point) -> Self {
149        Self(EmitterSettings::new(EmitterSource::Point(point)))
150    }
151}
152
153impl DiskEmitter {
154    /// Creates a uniform disk source.
155    pub fn new(center: Point, radius: f64) -> Self {
156        Self(EmitterSettings::new(EmitterSource::Disk(center, radius)))
157    }
158}
159
160impl RectEmitter {
161    /// Creates a uniform rectangle source.
162    pub fn new(rect: Rect) -> Self {
163        Self(EmitterSettings::new(EmitterSource::Rect(rect)))
164    }
165}
166
167impl PathEmitter {
168    /// Captures immutable curves without flattening.
169    pub fn new(path: Arc<Path2D>) -> Self {
170        Self(EmitterSettings::new(EmitterSource::Path(path)))
171    }
172
173    /// Advances along an animated normalized arc length, retaining tangent launch velocity.
174    pub fn along_path(mut self, progress: impl Animation<f64> + 'static) -> Self {
175        self.0.path_progress = Some(Arc::new(progress));
176        self
177    }
178}
179
180impl SurfaceEmitter {
181    /// Captures a shared tree; its default area triangulation is cached.
182    pub fn new(tree: Arc<VectorTree>) -> Self {
183        Self(EmitterSettings::new(EmitterSource::Surface(tree)))
184    }
185
186    /// Enables fill-color inheritance without changing area density or sampled positions.
187    pub fn inherit_color(mut self, enabled: bool) -> Self {
188        self.0.inherit_color = enabled;
189        self
190    }
191
192    /// Captures already-shaped glyph outlines through the shared vector surface.
193    #[cfg(feature = "text")]
194    pub fn from_text(
195        text: &crate::Text,
196        context: &motion_core::DrawContext,
197    ) -> Result<Self, ValidationError> {
198        Ok(Self::new(text.vector_tree(context)?).inherit_color(true))
199    }
200}
201
202/// A point burst emitted once at the first fixed boundary at/after its authored time.
203#[derive(Clone)]
204pub struct BurstEmitter {
205    time: Time,
206    count: usize,
207    settings: EmitterSettings,
208}
209
210impl BurstEmitter {
211    /// Creates a bounded timed point burst.
212    pub fn new(time: Time, count: usize, position: Point) -> Self {
213        Self {
214            time,
215            count,
216            settings: EmitterSettings::new(EmitterSource::Point(position)),
217        }
218    }
219
220    /// Replaces lifetime for every burst record.
221    pub fn lifetime(mut self, lifetime: Duration) -> Self {
222        self.settings.lifetime = lifetime;
223        self
224    }
225
226    /// Replaces random radial speed.
227    pub fn speed(mut self, speed: f64) -> Self {
228        self.settings.speed = speed;
229        self
230    }
231
232    /// Adds a fixed launch velocity to the radial burst.
233    pub fn velocity(mut self, velocity: Vector2) -> Self {
234        self.settings.velocity = velocity;
235        self
236    }
237
238    /// Sets finite premultiplied birth color.
239    pub fn color(mut self, color: [f32; 4]) -> Self {
240        self.settings.color = color;
241        self
242    }
243
244    /// Sets birth size.
245    pub fn size(mut self, size: f32) -> Self {
246        self.settings.size = size;
247        self
248    }
249
250    /// Sets a finite ordered depth range.
251    pub fn depth_range(mut self, minimum: f32, maximum: f32) -> Self {
252        self.settings.depth = [minimum, maximum];
253        self
254    }
255
256    /// Selects a deterministic birth palette.
257    pub fn palette(mut self, colors: Arc<[[f32; 4]]>) -> Self {
258        self.settings.palette = Some(colors);
259        self
260    }
261}
262
263impl ParticleEmitter for BurstEmitter {
264    fn emit(
265        &self,
266        step: u64,
267        time: Time,
268        random: &mut Random,
269        output: &mut Vec<Particle>,
270    ) -> Result<(), ValidationError> {
271        if self.time < Time::ZERO || self.count > 65536 {
272            return Err(invalid("burst", "Invalid burst time/count"));
273        }
274        let previous = if step == 0 {
275            Time::from_ticks(-1)
276        } else {
277            Time::from_ticks(time.ticks() - (time.ticks() / step as i64))
278        };
279        if self.time > previous && self.time <= time {
280            for _ in 0..self.count {
281                output.push(self.settings.particle(random, time)?);
282            }
283        }
284        Ok(())
285    }
286}
287
288#[derive(Clone)]
289enum EmitterSource {
290    Point(Point),
291    Disk(Point, f64),
292    Rect(Rect),
293    Path(Arc<Path2D>),
294    Surface(Arc<VectorTree>),
295    Image(Arc<ImageDistribution>),
296}
297
298#[derive(Clone)]
299enum EmissionRate {
300    Constant(f64),
301    Animated(Arc<dyn Animation<f64>>),
302}
303
304#[derive(Clone)]
305struct EmitterSettings {
306    source: EmitterSource,
307    rate: EmissionRate,
308    lifetime: Duration,
309    speed: f64,
310    velocity: Vector2,
311    inherit_color: bool,
312    color: [f32; 4],
313    size: f32,
314    depth: [f32; 2],
315    palette: Option<Arc<[[f32; 4]]>>,
316    path_progress: Option<Arc<dyn Animation<f64>>>,
317    paint: Option<Arc<(motion_core::Paint, Rect)>>,
318}
319
320impl EmitterSettings {
321    fn new(source: EmitterSource) -> Self {
322        Self {
323            source,
324            rate: EmissionRate::Constant(DEFAULT_RATE),
325            lifetime: Time::from_seconds(DEFAULT_LIFETIME_SECONDS),
326            speed: DEFAULT_SPEED,
327            velocity: Vector2::ZERO,
328            inherit_color: false,
329            color: [1.0; 4],
330            size: 1.0,
331            depth: [0.0; 2],
332            palette: None,
333            path_progress: None,
334            paint: None,
335        }
336    }
337
338    fn paint_color(&self, point: Point) -> Result<Option<[f32; 4]>, ValidationError> {
339        let Some(paint) = &self.paint else {
340            return Ok(None);
341        };
342        let (paint, bounds) = (&paint.0, paint.1);
343        let sample = match paint {
344            motion_core::Paint::Solid(color) => color.to_linear_premultiplied(),
345            motion_core::Paint::Linear(gradient) => gradient.sample(point, bounds)?,
346            motion_core::Paint::Radial(gradient) => gradient.sample(point, bounds)?,
347            motion_core::Paint::Sweep(gradient) => gradient.sample(point, bounds)?,
348            _ => {
349                return Err(invalid("paint", "Emitter paint must be solid or gradient"));
350            }
351        };
352        Ok(Some(sample.map(|value| value as f32)))
353    }
354
355    fn particle(&self, random: &mut Random, time: Time) -> Result<Particle, ValidationError> {
356        if self.lifetime <= Time::ZERO
357            || self.lifetime.as_seconds() > f64::from(MAX_LIFETIME_SECONDS)
358            || !self.speed.is_finite()
359            || self.speed < 0.0
360        {
361            return Err(invalid("emitter", "Invalid emitter lifetime/speed"));
362        }
363        let mut direction = random.unit_vector();
364        let mut color = self.color;
365        let point = match &self.source {
366            EmitterSource::Point(point) => *point,
367            EmitterSource::Disk(center, radius) => {
368                if !radius.is_finite() || *radius < 0.0 {
369                    return Err(invalid(
370                        "radius",
371                        "Disk radius must be finite and nonnegative",
372                    ));
373                }
374                let distance = libm::sqrt(random.next_float()) * radius;
375                Point::new(
376                    center.x + direction.x * distance,
377                    center.y + direction.y * distance,
378                )
379            }
380            EmitterSource::Rect(rect) => {
381                motion_core::drawing::validate_rect(*rect, "particles.emitter", "rect")?;
382                Point::new(
383                    rect.left + rect.width() * random.next_float(),
384                    rect.top + rect.height() * random.next_float(),
385                )
386            }
387            EmitterSource::Path(path) => {
388                let progress = if let Some(animation) = &self.path_progress {
389                    animation.sample(time)
390                } else {
391                    random.next_float()
392                };
393                let (point, tangent) = path.try_point_at(progress)?;
394                direction = tangent;
395                point
396            }
397            EmitterSource::Surface(tree) => {
398                let seed = (random.next_float() * u64::MAX as f64) as u64;
399                if self.inherit_color {
400                    let (point, sample) = tree.sample_colored_points(1, seed)?[0];
401                    color = std::array::from_fn(|channel| color[channel] * sample[channel]);
402                    point
403                } else {
404                    tree.sample_points(1, seed)?[0]
405                }
406            }
407            EmitterSource::Image(distribution) => {
408                let (point, sample) = distribution.sample(random)?;
409                color = std::array::from_fn(|channel| color[channel] * sample[channel]);
410                point
411            }
412        };
413        if let Some(sample) = self.paint_color(point)? {
414            color = std::array::from_fn(|channel| color[channel] * sample[channel]);
415        }
416        if let Some(palette) = &self.palette {
417            if palette.is_empty() || palette.len() > 256 {
418                return Err(invalid(
419                    "palette",
420                    "Require 1..=256 finite premultiplied colors",
421                ));
422            }
423            for color in palette.iter() {
424                Particle {
425                    color: *color,
426                    ..Particle::default()
427                }
428                .validate()?;
429            }
430            let index =
431                ((random.next_float() * palette.len() as f64) as usize).min(palette.len() - 1);
432            color = std::array::from_fn(|channel| color[channel] * palette[index][channel]);
433        }
434        if self.depth.iter().any(|value| !value.is_finite()) || self.depth[0] > self.depth[1] {
435            return Err(invalid("depth", "Depth range must be finite and ordered"));
436        }
437        let depth = if self.depth[0] == self.depth[1] {
438            self.depth[0]
439        } else {
440            self.depth[0] + (self.depth[1] - self.depth[0]) * random.next_float() as f32
441        };
442        let particle = Particle {
443            position: [point.x as f32, point.y as f32],
444            velocity: [
445                (direction.x * self.speed + self.velocity.x) as f32,
446                (direction.y * self.speed + self.velocity.y) as f32,
447            ],
448            lifetime: self.lifetime.as_seconds() as f32,
449            color,
450            size: self.size,
451            custom: [0.0, depth, 0.0, 0.0],
452            ..Default::default()
453        };
454        particle.validate()?;
455        Ok(particle)
456    }
457
458    fn emit(
459        &self,
460        step: u64,
461        time: Time,
462        random: &mut Random,
463        output: &mut Vec<Particle>,
464    ) -> Result<(), ValidationError> {
465        let rate = match &self.rate {
466            EmissionRate::Constant(rate) => *rate,
467            EmissionRate::Animated(animation) => animation.sample(time),
468        };
469        if !rate.is_finite() || !(0.0..=1_000_000.0).contains(&rate) {
470            return Err(invalid(
471                "rate",
472                "Emitter rate must be finite in 0..=1000000",
473            ));
474        }
475        let seconds = time.as_seconds();
476        let delta = if step == 0 {
477            1.0 / 240.0
478        } else {
479            Time::from_ticks(time.ticks() / step as i64).as_seconds()
480        };
481        let previous = if step == 0 {
482            0.0
483        } else {
484            Time::from_ticks(time.ticks() - time.ticks() / step as i64).as_seconds()
485        };
486        let count = match self.rate {
487            EmissionRate::Constant(_) if rate == 0.0 => 0,
488            EmissionRate::Constant(_) => {
489                if step == 0 {
490                    1
491                } else {
492                    (libm::floor(seconds * rate) - libm::floor(previous * rate)).max(0.0) as usize
493                }
494            }
495            EmissionRate::Animated(_) if step == 0 => 0,
496            EmissionRate::Animated(_) => {
497                let expected = rate * delta;
498                libm::floor(expected) as usize
499                    + usize::from(random.next_float() < expected - libm::floor(expected))
500            }
501        };
502        if count > 65536 {
503            return Err(limit("Emitter step exceeds 65536 births"));
504        }
505        for _ in 0..count {
506            output.push(self.particle(random, time)?);
507        }
508        Ok(())
509    }
510}