1use super::*;
4use crate::VectorTree;
5use motion_core::{Path2D, Point, Rect, Signal, 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
19const DEFAULT_RATE: f64 = 100.0;
21
22const DEFAULT_LIFETIME_SECONDS: f64 = 2.0;
24
25const 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 pub fn rate(mut self, rate: f64) -> Self {
36 self.0.rate = EmissionRate::Constant(rate);
37 self
38 }
39 pub fn rate_animation(mut self, rate: impl Signal<f64> + 'static) -> Self {
42 self.0.rate = EmissionRate::Animated(Arc::new(rate));
43 self
44 }
45 pub fn lifetime(mut self, lifetime: Duration) -> Self {
47 self.0.lifetime = lifetime;
48 self
49 }
50 pub fn speed(mut self, speed: f64) -> Self {
52 self.0.speed = speed;
53 self
54 }
55 pub fn velocity(mut self, velocity: Vector2) -> Self {
57 self.0.velocity = velocity;
58 self
59 }
60 pub fn color(mut self, color: [f32; 4]) -> Self {
62 self.0.color = color;
63 self
64 }
65 pub fn size(mut self, size: f32) -> Self {
67 self.0.size = size;
68 self
69 }
70 pub fn depth_range(mut self, minimum: f32, maximum: f32) -> Self {
72 self.0.depth = [minimum, maximum];
73 self
74 }
75 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 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 pub fn new(point: Point) -> Self {
149 Self(EmitterSettings::new(EmitterSource::Point(point)))
150 }
151}
152
153impl DiskEmitter {
154 pub fn new(center: Point, radius: f64) -> Self {
156 Self(EmitterSettings::new(EmitterSource::Disk(center, radius)))
157 }
158}
159
160impl RectEmitter {
161 pub fn new(rect: Rect) -> Self {
163 Self(EmitterSettings::new(EmitterSource::Rect(rect)))
164 }
165}
166
167impl PathEmitter {
168 pub fn new(path: Arc<Path2D>) -> Self {
170 Self(EmitterSettings::new(EmitterSource::Path(path)))
171 }
172
173 pub fn along_path(mut self, progress: impl Signal<f64> + 'static) -> Self {
175 self.0.path_progress = Some(Arc::new(progress));
176 self
177 }
178}
179
180impl SurfaceEmitter {
181 pub fn new(tree: Arc<VectorTree>) -> Self {
183 Self(EmitterSettings::new(EmitterSource::Surface(tree)))
184 }
185
186 pub fn inherit_color(mut self, enabled: bool) -> Self {
188 self.0.inherit_color = enabled;
189 self
190 }
191
192 #[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#[derive(Clone)]
204pub struct BurstEmitter {
205 time: Time,
206 count: usize,
207 settings: EmitterSettings,
208}
209
210impl BurstEmitter {
211 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 pub fn lifetime(mut self, lifetime: Duration) -> Self {
222 self.settings.lifetime = lifetime;
223 self
224 }
225
226 pub fn speed(mut self, speed: f64) -> Self {
228 self.settings.speed = speed;
229 self
230 }
231
232 pub fn velocity(mut self, velocity: Vector2) -> Self {
234 self.settings.velocity = velocity;
235 self
236 }
237
238 pub fn color(mut self, color: [f32; 4]) -> Self {
240 self.settings.color = color;
241 self
242 }
243
244 pub fn size(mut self, size: f32) -> Self {
246 self.settings.size = size;
247 self
248 }
249
250 pub fn depth_range(mut self, minimum: f32, maximum: f32) -> Self {
252 self.settings.depth = [minimum, maximum];
253 self
254 }
255
256 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 Signal<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 Signal<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: Duration::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 <= Duration::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}