1use super::*;
12use motion_core::{Point, Rect, Vector2};
13
14pub use crate::Preset;
15
16#[derive(Clone)]
18pub struct PresetOptions {
19 pub seed: u64,
21 pub duration: Duration,
23 pub bounds: Rect,
25 pub density: f64,
27 pub speed: f64,
29 pub intensity: f32,
31 pub logo: Option<Arc<crate::VectorTree>>,
33}
34
35impl PresetOptions {
36 pub fn new(seed: u64, duration: Duration, bounds: Rect) -> Self {
38 Self {
39 seed,
40 duration,
41 bounds,
42 density: 1.0,
43 speed: 1.0,
44 intensity: 1.0,
45 logo: None,
46 }
47 }
48}
49
50impl Preset {
51 pub const ALL: [Self; 13] = [
53 Self::Sparks,
54 Self::Embers,
55 Self::Dust,
56 Self::Motes,
57 Self::Snow,
58 Self::Rain,
59 Self::Smoke,
60 Self::Fire,
61 Self::Confetti,
62 Self::Fireworks,
63 Self::Fireflies,
64 Self::LogoDissolve,
65 Self::LogoAssemble,
66 ];
67
68 pub fn name(self) -> &'static str {
70 match self {
71 Self::Sparks => "Sparks",
72 Self::Embers => "Embers",
73 Self::Dust => "Dust",
74 Self::Motes => "Motes",
75 Self::Snow => "Snow",
76 Self::Rain => "Rain",
77 Self::Smoke => "Smoke",
78 Self::Fire => "Fire",
79 Self::Confetti => "Confetti",
80 Self::Fireworks => "Fireworks",
81 Self::Fireflies => "Fireflies",
82 Self::LogoDissolve => "Logo Dissolve",
83 Self::LogoAssemble => "Logo Assemble",
84 }
85 }
86
87 pub fn simulation(
89 self,
90 options: PresetOptions,
91 ) -> Result<ParticleSimulationBuilder, ValidationError> {
92 motion_core::drawing::validate_rect(options.bounds, "particles.preset", "bounds")?;
93 if options.duration <= Duration::ZERO
94 || options.duration.as_seconds() > 3600.0
95 || options.bounds.width() <= 0.0
96 || options.bounds.height() <= 0.0
97 || !options.density.is_finite()
98 || !(0.0..=16.0).contains(&options.density)
99 || options.density == 0.0
100 || !options.speed.is_finite()
101 || !(0.0..=16.0).contains(&options.speed)
102 || options.speed == 0.0
103 || !options.intensity.is_finite()
104 || !(0.0..=64.0).contains(&options.intensity)
105 {
106 return Err(invalid(
107 "preset",
108 "Invalid recipe bounds, duration, density, speed or intensity",
109 ));
110 }
111 let bounds = options.bounds;
112 let center = Point::new(
113 (bounds.left + bounds.right) * 0.5,
114 (bounds.top + bounds.bottom) * 0.5,
115 );
116 let bottom = Rect::new(
117 bounds.left,
118 bounds.bottom - 2.0,
119 bounds.right,
120 bounds.bottom,
121 );
122 let top = Rect::new(bounds.left, bounds.top, bounds.right, bounds.top + 2.0);
123 let speed = options.speed;
124 let count = |amount: f64| (amount * options.density).ceil() as usize;
125 let trail = ParticleStyle::Trails {
126 length: Duration::from_seconds(0.12),
127 width: 2.0,
128 }
129 .trails(TrailOptions {
130 segments: 16,
131 taper: Some(ParticleCurve::linear(0.0, 1.0)),
132 fade: Some(ParticleCurve::linear(0.0, 1.0)),
133 smooth: true,
134 ..Default::default()
135 })
136 .blend(ParticleBlend::Plus);
137 let mut simulation = ParticleSimulation::new(options.seed).duration(options.duration);
138 let curl_seed = options.seed as u32 ^ (options.seed >> 32) as u32;
139 simulation = match self {
140 Self::Sparks => simulation
141 .emitter(
142 BurstEmitter::new(Time::ZERO, count(140.0), center)
143 .lifetime(Duration::from_seconds(1.8))
144 .speed(100.0 * speed),
145 )
146 .modifier(Gravity::new(Vector2::new(0.0, 95.0 * speed)))
147 .modifier(Drag::new(0.6))
148 .modifier(Temperature::new(ParticleCurve::linear(6500.0, 1200.0)))
149 .style(trail),
150 Self::Embers => simulation
151 .emitter(
152 RectEmitter::new(bottom)
153 .rate(90.0 * options.density)
154 .speed(12.0)
155 .velocity(Vector2::new(0.0, -24.0 * speed))
156 .lifetime(Duration::from_seconds(3.0)),
157 )
158 .modifier(CurlNoise {
159 frequency: 0.03,
160 strength: 20.0 * speed,
161 seed: curl_seed,
162 phase_speed: 1.0,
163 })
164 .modifier(Temperature::new(ParticleCurve::linear(4200.0, 1100.0)))
165 .prewarm(Duration::from_seconds(1.0))
166 .style(ParticleStyle::GlowDots { radius: 1.8 }),
167 Self::Dust | Self::Motes => simulation
168 .emitter(
169 RectEmitter::new(bounds)
170 .rate(35.0 * options.density)
171 .speed(3.0 * speed)
172 .lifetime(Duration::from_seconds(5.0))
173 .depth_range(-2.0, 2.0)
174 .color([0.45, 0.65, 0.75, 0.75]),
175 )
176 .modifier(Drift {
177 seed: options.seed,
178 strength: 3.0 * speed,
179 })
180 .prewarm(Duration::from_seconds(1.5))
181 .style(
182 ParticleStyle::Bokeh {
183 radius: if self == Self::Dust { 1.0 } else { 2.0 },
184 aperture: 0.8,
185 focus: 0.0,
186 shape: BokehShape::Polygon(6),
187 ring: 0.3,
188 hardness: 0.6,
189 highlight: 1.0,
190 }
191 .parallax([4.0, 1.0])
192 .blend(ParticleBlend::Screen),
193 ),
194 Self::Snow | Self::Rain => {
195 for depth in [-1.0, 0.0, 1.0] {
196 simulation = simulation.emitter(
197 RectEmitter::new(top)
198 .rate(50.0 * options.density)
199 .speed(0.0)
200 .velocity(Vector2::new(
201 5.0 * speed,
202 if self == Self::Snow {
203 32.0 * speed
204 } else {
205 160.0 * speed
206 },
207 ))
208 .lifetime(Duration::from_seconds(4.0))
209 .depth_range(depth, depth)
210 .color([0.65, 0.85, 1.0, 0.8]),
211 );
212 }
213 simulation = simulation.modifier(Wind::new(Vector2::new(2.0, 0.0)));
214 if self == Self::Rain {
215 simulation = simulation
216 .modifier(SegmentCollider {
217 start: Point::new(bounds.left, bounds.bottom),
218 end: Point::new(bounds.right, bounds.bottom),
219 response: CollisionResponse {
220 restitution: 0.3,
221 friction: 0.2,
222 kill_on_hit: true,
223 },
224 })
225 .sub_emitter(SubEmitter {
226 on: ParticleEventKind::Collision,
227 emitter: Arc::new(StageBurst {
228 parent: 0.0,
229 stage: 1.0,
230 burst: BurstEmitter::new(Time::ZERO, 4, Point::ORIGIN)
231 .lifetime(Duration::from_seconds(0.2))
232 .speed(20.0),
233 }),
234 inherit: Inherit::Color,
235 probability: 1.0,
236 });
237 simulation.style(
238 ParticleStyle::Shape {
239 radius: 1.2,
240 shape: ParticleShape::Capsule { aspect: 1.0 },
241 }
242 .stretch_by_speed(0.03, 8.0)
243 .parallax([3.0, 0.0]),
244 )
245 } else {
246 simulation
247 .prewarm(Duration::from_seconds(1.0))
248 .style(ParticleStyle::Dots { radius: 1.8 }.parallax([8.0, 0.0]))
249 }
250 }
251 Self::Smoke | Self::Fire => {
252 let sprite = if self == Self::Smoke {
253 ProceduralSprite::Smoke {
254 scale: 3.0,
255 evolution: 0.0,
256 fractal: motion_core::noise::Fractal::default(),
257 }
258 } else {
259 ProceduralSprite::FireCore
260 }
261 .generate(48, options.seed)?;
262 let smoke = self == Self::Smoke;
263 simulation = simulation
264 .emitter(
265 RectEmitter::new(bottom)
266 .rate(if smoke { 24.0 } else { 70.0 } * options.density)
267 .speed(8.0)
268 .velocity(Vector2::new(0.0, -20.0 * speed))
269 .lifetime(Duration::from_seconds(if smoke { 3.0 } else { 1.0 })),
270 )
271 .modifier(CurlNoise {
272 frequency: 0.025,
273 strength: 10.0,
274 seed: curl_seed,
275 phase_speed: 1.0,
276 })
277 .modifier(Wind::new(Vector2::new(0.0, -12.0 * speed)))
278 .modifier(SizeOverLifetime {
279 start: if smoke { 0.4 } else { 1.0 },
280 end: if smoke { 2.0 } else { 0.1 },
281 })
282 .modifier(ColorOverLifetime::new(Arc::from(if smoke {
283 [[0.18, 0.2, 0.22, 0.25], [0.0; 4]]
284 } else {
285 [[1.0; 4], [0.0; 4]]
286 })));
287 if !smoke {
288 simulation = simulation
289 .modifier(Temperature::new(ParticleCurve::linear(6500.0, 1100.0)));
290 }
291 simulation.style(
292 ParticleStyle::Sprites {
293 image: motion_core::Assets::default()
294 .preload(
295 motion_core::Source::Bytes {
296 key: "particle-preset-sprite".into(),
297 data: Arc::from([]),
298 },
299 sprite,
300 )
301 .map_err(|_| {
302 invalid(
303 "sprite",
304 "Procedural preset material exceeds resource allowance",
305 )
306 })?,
307 size: if smoke { 30.0 } else { 14.0 },
308 align_to_velocity: false,
309 }
310 .blend(if smoke {
311 ParticleBlend::SourceOver
312 } else {
313 ParticleBlend::Plus
314 }),
315 )
316 }
317 Self::Confetti => simulation
318 .emitter(
319 RectEmitter::new(top)
320 .rate(60.0 * options.density)
321 .speed(25.0 * speed)
322 .lifetime(Duration::from_seconds(4.0))
323 .palette(Arc::from([
324 [0.8, 0.1, 0.2, 1.0],
325 [0.05, 0.7, 0.3, 1.0],
326 [0.1, 0.3, 0.9, 1.0],
327 [0.9, 0.7, 0.05, 1.0],
328 ])),
329 )
330 .modifier(Gravity::new(Vector2::new(0.0, 50.0 * speed)))
331 .modifier(Drag::new(0.3))
332 .modifier(FlipPhase(options.seed))
333 .style(
334 ParticleStyle::Shape {
335 radius: 2.5,
336 shape: ParticleShape::Quad { aspect: 1.5 },
337 }
338 .flip(),
339 ),
340 Self::Fireworks => simulation
341 .emitter(
342 BurstEmitter::new(Time::ZERO, 1, Point::new(center.x, bounds.bottom - 5.0))
343 .lifetime(Duration::from_seconds(0.8))
344 .speed(0.0)
345 .velocity(Vector2::new(0.0, -160.0 * speed)),
346 )
347 .modifier(Gravity::new(Vector2::new(0.0, 70.0 * speed)))
348 .sub_emitter(SubEmitter {
349 on: ParticleEventKind::Death,
350 emitter: Arc::new(StageBurst {
351 parent: 0.0,
352 stage: 1.0,
353 burst: BurstEmitter::new(Time::ZERO, count(70.0), Point::ORIGIN)
354 .lifetime(Duration::from_seconds(0.7))
355 .speed(80.0 * speed),
356 }),
357 inherit: Inherit::Position,
358 probability: 1.0,
359 })
360 .sub_emitter(SubEmitter {
361 on: ParticleEventKind::Death,
362 emitter: Arc::new(StageBurst {
363 parent: 1.0,
364 stage: 2.0,
365 burst: BurstEmitter::new(Time::ZERO, 2, Point::ORIGIN)
366 .lifetime(Duration::from_seconds(0.25))
367 .speed(22.0),
368 }),
369 inherit: Inherit::Position,
370 probability: 0.5,
371 })
372 .modifier(Temperature::new(ParticleCurve::linear(6500.0, 1300.0)))
373 .style(trail),
374 Self::Fireflies => simulation
375 .emitter(
376 RectEmitter::new(bounds)
377 .rate(10.0 * options.density)
378 .speed(2.0 * speed)
379 .lifetime(Duration::from_seconds(5.0))
380 .color([1.0, 1.0, 0.1, 1.0]),
381 )
382 .modifier(Drift {
383 seed: options.seed,
384 strength: 5.0 * speed,
385 })
386 .modifier(Breathe(options.seed))
387 .style(ParticleStyle::GlowDots { radius: 2.0 }),
388 Self::LogoDissolve | Self::LogoAssemble => {
389 let tree = options
390 .logo
391 .clone()
392 .ok_or_else(|| invalid("logo", "Logo presets require a shared VectorTree"))?;
393 tree.surface()?;
394 simulation
395 .emitter(LogoEmitter {
396 tree,
397 count: count(600.0),
398 lifetime: options.duration.as_seconds() as f32,
399 seed: options.seed,
400 distance: bounds.width() * 0.65,
401 assemble: self == Self::LogoAssemble,
402 })
403 .modifier(LogoMotion {
404 seed: options.seed,
405 distance: bounds.width() * 0.65,
406 assemble: self == Self::LogoAssemble,
407 })
408 .style(ParticleStyle::Dots { radius: 1.0 })
409 }
410 };
411 let (start, end) = match self {
412 Self::Sparks | Self::Fireworks => (8.0, 0.0),
413 Self::Embers | Self::Fire => (4.0, 0.0),
414 _ => (1.0, 1.0),
415 };
416 Ok(
417 simulation.modifier(IntensityOverLifetime::new(ParticleCurve::linear(
418 options.intensity * start,
419 options.intensity * end,
420 ))),
421 )
422 }
423}
424
425struct StageBurst {
426 parent: f32,
427 stage: f32,
428 burst: BurstEmitter,
429}
430
431impl ParticleEmitter for StageBurst {
432 fn accepts_event(&self, event: &ParticleEvent) -> bool {
433 event.particle.custom[3] == self.parent
434 }
435
436 fn emit(
437 &self,
438 step: u64,
439 time: Time,
440 random: &mut Random,
441 output: &mut Vec<Particle>,
442 ) -> Result<(), ValidationError> {
443 let start = output.len();
444 self.burst.emit(step, time, random, output)?;
445 for particle in &mut output[start..] {
446 particle.custom[3] = self.stage;
447 }
448 Ok(())
449 }
450}
451
452struct Drift {
453 seed: u64,
454 strength: f64,
455}
456
457impl ParticleModifier for Drift {
458 fn apply(
459 &self,
460 particle: &mut Particle,
461 step: f32,
462 time: Time,
463 _: &mut Random,
464 ) -> Result<(), ValidationError> {
465 let phase =
466 Random::for_particle(self.seed, particle.index, 0).next_float() * std::f64::consts::TAU;
467 particle.velocity[0] +=
468 (libm::sin(time.as_seconds() + phase) * self.strength) as f32 * step;
469 particle.velocity[1] +=
470 (libm::cos(time.as_seconds() * 0.7 + phase) * self.strength) as f32 * step;
471 particle.validate()
472 }
473}
474
475struct Breathe(u64);
476
477impl ParticleModifier for Breathe {
478 fn resets_color(&self) -> bool {
479 true
480 }
481
482 fn apply(
483 &self,
484 particle: &mut Particle,
485 _: f32,
486 time: Time,
487 _: &mut Random,
488 ) -> Result<(), ValidationError> {
489 let phase =
490 Random::for_particle(self.0, particle.index, 0).next_float() * std::f64::consts::TAU;
491 let energy = (1.5 + 1.4 * libm::sin(time.as_seconds() * 2.0 + phase)) as f32;
492 for channel in &mut particle.color[..3] {
493 *channel *= energy;
494 }
495 particle.validate()
496 }
497}
498
499struct FlipPhase(u64);
500
501impl ParticleModifier for FlipPhase {
502 fn apply(
503 &self,
504 particle: &mut Particle,
505 _: f32,
506 _: Time,
507 _: &mut Random,
508 ) -> Result<(), ValidationError> {
509 let phase = Random::for_particle(self.0, particle.index, 0).next_float() as f32
510 * std::f32::consts::TAU;
511 particle.custom[2] = phase;
512 particle.rotation = phase + particle.age * 0.5;
513 particle.validate()
514 }
515}
516
517struct LogoEmitter {
518 tree: Arc<crate::VectorTree>,
519 count: usize,
520 lifetime: f32,
521 seed: u64,
522 distance: f64,
523 assemble: bool,
524}
525
526impl ParticleEmitter for LogoEmitter {
527 fn emit(
528 &self,
529 step: u64,
530 _: Time,
531 random: &mut Random,
532 output: &mut Vec<Particle>,
533 ) -> Result<(), ValidationError> {
534 if step != 0 {
535 return Ok(());
536 }
537 let seed = (random.next_float() * u64::MAX as f64) as u64;
538 for (index, (point, color)) in self
539 .tree
540 .sample_colored_points(self.count, seed)?
541 .into_iter()
542 .enumerate()
543 {
544 let mut keyed = Random::for_particle(self.seed, index as u64, 0);
545 let direction = keyed.unit_vector();
546 let distance = self.distance * (0.5 + keyed.next_float());
547 let position = if self.assemble {
548 [
549 (point.x + direction.x * distance) as f32,
550 (point.y + direction.y * distance) as f32,
551 ]
552 } else {
553 [point.x as f32, point.y as f32]
554 };
555 output.push(Particle {
556 position,
557 custom: [point.x as f32, 0.0, point.y as f32, 0.0],
558 color,
559 lifetime: self.lifetime,
560 ..Default::default()
561 });
562 }
563 Ok(())
564 }
565}
566
567struct LogoMotion {
568 seed: u64,
569 distance: f64,
570 assemble: bool,
571}
572
573impl ParticleModifier for LogoMotion {
574 fn apply(
575 &self,
576 particle: &mut Particle,
577 _: f32,
578 _: Time,
579 _: &mut Random,
580 ) -> Result<(), ValidationError> {
581 let progress = (particle.age / particle.lifetime).clamp(0.0, 1.0);
582 let eased = progress * progress * (3.0 - 2.0 * progress);
583 let amount = if self.assemble { 1.0 - eased } else { eased };
584 let mut random = Random::for_particle(self.seed, particle.index, 0);
585 let direction = random.unit_vector();
586 let distance = self.distance * (0.5 + random.next_float());
587 particle.position = [
588 particle.custom[0] + (direction.x * distance) as f32 * amount,
589 particle.custom[2] + (direction.y * distance) as f32 * amount,
590 ];
591 particle.velocity = [0.0; 2];
592 particle.validate()
593 }
594}