firework_rs/
fireworks.rs

1//! `firework` module provides functions to define, create and update fireworks
2
3use std::{
4    collections::VecDeque,
5    time::{Duration, SystemTime},
6};
7
8use glam::Vec2;
9use rand::{seq::IteratorRandom, thread_rng};
10
11use crate::particle::{LifeState, Particle, ParticleConfig};
12
13/// Struct representing a single firework
14pub struct Firework {
15    /// The `SystemTime` when the object is initialized/defined
16    pub init_time: SystemTime,
17    /// Firework spawns after `spawn_after` from `init_time`
18    pub spawn_after: Duration,
19    pub time_elapsed: Duration,
20    pub center: Vec2,
21    pub state: FireworkState,
22    pub config: FireworkConfig,
23    pub form: ExplosionForm,
24    pub particles: Vec<ParticleConfig>,
25    pub current_particles: Vec<Particle>,
26}
27
28impl Default for Firework {
29    fn default() -> Self {
30        Self {
31            init_time: SystemTime::now(),
32            spawn_after: Duration::ZERO,
33            time_elapsed: Duration::ZERO,
34            center: Vec2::ZERO,
35            state: FireworkState::Waiting,
36            config: FireworkConfig::default(),
37            form: ExplosionForm::Instant { used: false },
38            particles: Vec::new(),
39            current_particles: Vec::new(),
40        }
41    }
42}
43
44impl Firework {
45    /// Update the `Firework`
46    ///
47    /// # Arguments
48    ///
49    /// * `now` - `SystemTime` of now
50    /// * `delta_time` - `Duration` since last update
51    pub fn update(&mut self, now: SystemTime, delta_time: Duration) {
52        // Spawn particles
53        if now >= self.init_time + self.spawn_after {
54            self.time_elapsed += delta_time;
55            match &mut self.form {
56                ExplosionForm::Instant { used } => {
57                    if !*used {
58                        self.particles.iter().for_each(|p| {
59                            self.current_particles.push(Particle {
60                                pos: p.init_pos,
61                                vel: p.init_vel,
62                                trail: init_trail(p.init_pos, p.trail_length),
63                                life_state: LifeState::Alive,
64                                time_elapsed: Duration::ZERO,
65                                config: *p,
66                            })
67                        })
68                    }
69                    *used = true;
70                }
71                ExplosionForm::Sustained {
72                    lasts,
73                    time_interval,
74                    timer,
75                } => {
76                    if self.time_elapsed <= *lasts {
77                        if *timer + delta_time <= *time_interval {
78                            *timer += delta_time;
79                        } else {
80                            let n =
81                                (*timer + delta_time).as_millis() / (*time_interval).as_millis();
82                            self.particles
83                                .iter()
84                                .choose_multiple(&mut thread_rng(), n as usize)
85                                .iter()
86                                .for_each(|p| {
87                                    self.current_particles.push(Particle {
88                                        pos: p.init_pos,
89                                        vel: p.init_vel,
90                                        trail: init_trail(p.init_pos, p.trail_length),
91                                        life_state: LifeState::Alive,
92                                        time_elapsed: Duration::ZERO,
93                                        config: **p,
94                                    })
95                                });
96                            *timer = Duration::from_millis(
97                                ((*timer + delta_time).as_millis() % (*time_interval).as_millis())
98                                    as u64,
99                            );
100                        }
101                    }
102                }
103            }
104            self.state = FireworkState::Alive;
105        }
106
107        self.current_particles
108            .iter_mut()
109            .for_each(|p| p.update(delta_time, &self.config));
110
111        // Clean the dead pariticles
112        self.current_particles
113            .retain(|p| p.life_state != LifeState::Dead);
114
115        match self.form {
116            ExplosionForm::Instant { used } => {
117                if used && self.state == FireworkState::Alive && self.current_particles.is_empty() {
118                    self.state = FireworkState::Gone;
119                }
120            }
121            ExplosionForm::Sustained { lasts, .. } => {
122                if self.time_elapsed > lasts
123                    && self.state == FireworkState::Alive
124                    && self.current_particles.is_empty()
125                {
126                    self.state = FireworkState::Gone;
127                }
128            }
129        }
130    }
131
132    /// Return true if the `FireworkState` is `Gone`
133    pub fn is_gone(&self) -> bool {
134        self.state == FireworkState::Gone
135    }
136
137    /// Reset `Firework` to its initial state
138    pub fn reset(&mut self) {
139        self.init_time = SystemTime::now();
140        self.state = FireworkState::Waiting;
141        self.time_elapsed = Duration::ZERO;
142        self.current_particles = Vec::new();
143        match &mut self.form {
144            ExplosionForm::Instant { used } => {
145                *used = false;
146            }
147            ExplosionForm::Sustained { timer, .. } => {
148                *timer = Duration::ZERO;
149            }
150        }
151    }
152}
153
154/// Struct representing state of a `Firework`
155///
156/// State goes from `Waiting` -> `Alive` -> `Gone`
157///
158/// # Notes
159///
160/// - `Firework` turns to `Alive` when it is spawned
161/// - `Firework` turns to `Gone` when all of its `Particles` are `Dead`
162#[derive(Debug, PartialEq, Default)]
163pub enum FireworkState {
164    #[default]
165    Waiting,
166    Alive,
167    Gone,
168}
169
170/// Enum that represents whether the `Firework` make one instantaneous explosion or continuously emit particles
171#[derive(Debug, PartialEq, Eq)]
172pub enum ExplosionForm {
173    Instant {
174        used: bool,
175    },
176    Sustained {
177        /// `Duration` that the sustained firework will last
178        lasts: Duration,
179        /// Time interval between two particle spawn
180        time_interval: Duration,
181        timer: Duration,
182    },
183}
184
185/// Struct representing the configuration of a single `Firework`
186///
187/// This applies to all `Particle` in the `Firework`
188pub struct FireworkConfig {
189    /// Larger `gravity_scale` tends to pull particles down
190    pub gravity_scale: f32,
191    /// Air resistance scale
192    /// Warning: too large or too small `ar_scale` may lead to unexpected behavior of `Particles`
193    pub ar_scale: f32,
194    pub additional_force: Box<dyn Fn(&Particle) -> Vec2>,
195    /// This field is a function that takes a float between 0 and 1, returns a float representing all `Particle`s' gradient
196    ///
197    /// `Particle`s' gradient changes according to its elapsed time and lifetime
198    /// The input `f32` equals to `time_elapsed`/`life_time`, which returns a `f32` affecting its color gradient
199    /// `gradient_scale` returns 1. means`Particle` will have the same colors as defined all over its lifetime
200    pub gradient_scale: fn(f32) -> f32,
201    /// Set wheter or not firework has color gradient
202    ///
203    /// # Notes
204    ///
205    /// - It is recommanded that your terminal window is non-transparent and has black bg color to get better visual effects
206    /// - Otherwise set it to `false`
207    pub enable_gradient: bool,
208}
209
210impl Default for FireworkConfig {
211    fn default() -> Self {
212        Self {
213            gravity_scale: 1.,
214            ar_scale: 0.28,
215            additional_force: Box::new(move |_| Vec2::ZERO),
216            gradient_scale: |_| 1.,
217            enable_gradient: false,
218        }
219    }
220}
221
222impl FireworkConfig {
223    /// Set `gradient_scale`
224    #[inline]
225    #[must_use]
226    pub fn with_gradient_scale(mut self, f: fn(f32) -> f32) -> Self {
227        self.gradient_scale = f;
228        self
229    }
230
231    /// Set `gravity_scale`
232    #[inline]
233    #[must_use]
234    pub fn with_gravity_scale(mut self, s: f32) -> Self {
235        self.gravity_scale = s;
236        self
237    }
238
239    /// Set `ar_scale`
240    #[inline]
241    #[must_use]
242    pub fn with_ar_scale(mut self, s: f32) -> Self {
243        self.ar_scale = s;
244        self
245    }
246
247    /// Set `additional_force`
248    #[inline]
249    #[must_use]
250    pub fn with_additional_force(mut self, af: impl Fn(&Particle) -> Vec2 + 'static) -> Self {
251        self.additional_force = Box::new(af);
252        self
253    }
254
255    /// Set `enable_gradient`
256    pub fn set_enable_gradient(&mut self, enable_gradient: bool) {
257        self.enable_gradient = enable_gradient;
258    }
259}
260
261/// `FireworkManager` manages all `Firework`s
262pub struct FireworkManager {
263    pub fireworks: Vec<Firework>,
264    /// If this is `true`, the whole fireworks show will restart when all the `Firework`s are `Gone`
265    pub enable_loop: bool,
266    /// Controls how fireworks are installed in `FireworkManager`
267    pub install_form: FireworkInstallForm,
268}
269
270impl Default for FireworkManager {
271    fn default() -> Self {
272        Self {
273            fireworks: Vec::new(),
274            enable_loop: false,
275            install_form: FireworkInstallForm::StaticInstall,
276        }
277    }
278}
279
280impl FireworkManager {
281    /// Create a new `FireworkManager` with `enable_loop` set to `false`
282    pub fn new(fireworks: Vec<Firework>) -> Self {
283        Self {
284            fireworks,
285            enable_loop: false,
286            install_form: FireworkInstallForm::StaticInstall,
287        }
288    }
289
290    /// Add a `Firework` to a existing `FireworkManager`
291    pub fn add_firework(&mut self, firework: Firework) {
292        self.fireworks.push(firework);
293    }
294
295    /// Add `Firework`s to a existing `FireworkManager`
296    pub fn add_fireworks(&mut self, mut fireworks: Vec<Firework>) {
297        self.fireworks.append(&mut fireworks);
298    }
299
300    /// Add a `Firework` to `FireworkManager`
301    #[inline]
302    #[must_use]
303    pub fn with_firework(mut self, firework: Firework) -> Self {
304        self.fireworks.push(firework);
305        self
306    }
307
308    // Add a vector of `Firework`s to `FireworkManager`
309    #[inline]
310    #[must_use]
311    pub fn with_fireworks(mut self, mut fireworks: Vec<Firework>) -> Self {
312        self.fireworks.append(&mut fireworks);
313        self
314    }
315
316    /// Set `enable_loop` to `true`
317    #[inline]
318    #[must_use]
319    pub fn enable_loop(mut self) -> Self {
320        self.enable_loop = true;
321        self
322    }
323
324    /// Set `enable_loop` to `false`
325    #[inline]
326    #[must_use]
327    pub fn disable_loop(mut self) -> Self {
328        self.enable_loop = false;
329        self
330    }
331
332    /// Reset the whole fireworks show
333    pub fn reset(&mut self) {
334        for ele in self.fireworks.iter_mut() {
335            ele.reset();
336        }
337    }
338
339    pub fn set_enable_loop(&mut self, enable_loop: bool) {
340        self.enable_loop = enable_loop;
341    }
342
343    /// The main update function
344    pub fn update(&mut self, now: SystemTime, delta_time: Duration) {
345        for ele in self.fireworks.iter_mut() {
346            ele.update(now, delta_time);
347        }
348        if self.install_form == FireworkInstallForm::DynamicInstall {
349            self.fireworks.retain(|f| f.state != FireworkState::Gone);
350        }
351        if self.install_form == FireworkInstallForm::StaticInstall
352            && self.enable_loop
353            && self.fireworks.iter().all(|f| f.is_gone())
354        {
355            self.reset();
356        }
357    }
358
359    /// Set `install_form` to `DynamicInstall`
360    pub fn enable_dyn_install(mut self) -> Self {
361        self.install_form = FireworkInstallForm::DynamicInstall;
362        self
363    }
364}
365
366/// `StaticInstall` keeps all the fireworks in `FireworkManager` and won't delete them
367///
368/// `DynamicInstall` automatically remove fireworks that are `Gone`, which let you add fireworks continuously
369///
370/// # Notes
371///
372///  - `FireworkManager` that has `DynamicInstall` can't loop, it will ignore the set `enable_loop` value
373#[derive(Debug, PartialEq)]
374pub enum FireworkInstallForm {
375    StaticInstall,
376    DynamicInstall,
377}
378
379fn init_trail(init_pos: Vec2, n: usize) -> VecDeque<Vec2> {
380    VecDeque::from(vec![init_pos; n])
381}