firework_rs/
particle.rs

1//! `particle` module provides functions to define, create and update particles
2
3use std::{collections::VecDeque, time::Duration};
4
5use glam::Vec2;
6
7use crate::fireworks::FireworkConfig;
8
9/// The struct represents the states in a `Particle`'s lifetime
10///
11/// Every `Particle` goes from `Alive` -> `Declining` -> `Dying` -> `Dead`
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum LifeState {
14    Alive,
15    Declining,
16    Dying,
17    Dead,
18}
19
20/// The struct representing a single particle
21#[derive(Debug, Clone)]
22pub struct Particle {
23    pub pos: Vec2,
24    pub vel: Vec2,
25    /// Records a `trail_length` of previous positions of the `Particle`
26    pub trail: VecDeque<Vec2>,
27    pub life_state: LifeState,
28    /// `Duration` since initialization of this `Particle`
29    pub time_elapsed: Duration,
30    pub config: ParticleConfig,
31}
32
33impl Default for Particle {
34    fn default() -> Self {
35        Self {
36            pos: Vec2::ZERO,
37            vel: Vec2::ZERO,
38            trail: VecDeque::new(),
39            life_state: LifeState::Alive,
40            time_elapsed: Duration::ZERO,
41            config: ParticleConfig::default(),
42        }
43    }
44}
45
46impl Particle {
47    /// Create a new `Particle`
48    pub fn new(
49        pos: Vec2,
50        vel: Vec2,
51        trail_length: usize,
52        life_time: Duration,
53        color: (u8, u8, u8),
54    ) -> Self {
55        let trail = VecDeque::from(vec![pos; trail_length]);
56        let life_state = LifeState::Alive;
57        Self {
58            pos,
59            vel,
60            trail,
61            life_state,
62            time_elapsed: Duration::ZERO,
63            config: ParticleConfig::new(pos, vel, trail_length, life_time, color),
64        }
65    }
66
67    /// Return true if `Particle`'s `LifeState` is `Dead`
68    pub fn is_dead(&self) -> bool {
69        self.life_state == LifeState::Dead
70    }
71
72    /// Reset `Particle` to its initial state
73    pub fn reset(&mut self) {
74        self.pos = self.config.init_pos;
75        self.vel = self.config.init_vel;
76        (0..self.config.trail_length).for_each(|i| self.trail[i] = self.pos);
77        self.life_state = LifeState::Alive;
78        self.time_elapsed = Duration::ZERO;
79    }
80
81    /// Update the `Particle` based on delta time
82    ///
83    /// # Arguments
84    ///
85    /// * - `duration` - `Duration` since last update
86    pub fn update(&mut self, duration: Duration, config: &FireworkConfig) {
87        const TIME_STEP: f32 = 0.001;
88        self.time_elapsed += duration;
89        self.life_state = cal_life_state(self.config.life_time, self.time_elapsed);
90        let mut t = 0.;
91        while t < duration.as_secs_f32() {
92            self.vel += TIME_STEP
93                * (Vec2::Y * 10. * config.gravity_scale
94                    - self.vel.normalize() * self.vel.length().powi(2) * config.ar_scale
95                    + (config.additional_force)(self));
96            self.pos += TIME_STEP * self.vel;
97            t += TIME_STEP;
98        }
99        self.trail.pop_front();
100        self.trail.push_back(self.pos);
101    }
102}
103
104/// Struct that defines the configuration of `Particle`
105#[derive(Debug, Copy, Clone, PartialEq)]
106pub struct ParticleConfig {
107    pub init_pos: Vec2,
108    pub init_vel: Vec2,
109    pub trail_length: usize,
110    /// `Duration` from `Particle`'s initialization to its `Dead`
111    pub life_time: Duration,
112    /// Color in RGB (from 0 to 255)
113    pub color: (u8, u8, u8),
114}
115
116impl Default for ParticleConfig {
117    fn default() -> Self {
118        Self {
119            init_pos: Vec2::ZERO,
120            init_vel: Vec2::ZERO,
121            trail_length: 2,
122            life_time: Duration::from_secs(3),
123            color: (255, 255, 255),
124        }
125    }
126}
127
128impl ParticleConfig {
129    /// Create a new `ParticleConfig`
130    pub fn new(
131        init_pos: Vec2,
132        init_vel: Vec2,
133        trail_length: usize,
134        life_time: Duration,
135        color: (u8, u8, u8),
136    ) -> Self {
137        Self {
138            init_pos,
139            init_vel,
140            trail_length,
141            life_time,
142            color,
143        }
144    }
145}
146
147fn cal_life_state(life_time: Duration, current_elapsed: Duration) -> LifeState {
148    let p = current_elapsed.as_millis() as f32 / life_time.as_millis() as f32;
149    if p < 0.4 {
150        LifeState::Alive
151    } else if p < 0.65 {
152        LifeState::Declining
153    } else if p < 1. {
154        LifeState::Dying
155    } else {
156        LifeState::Dead
157    }
158}