Skip to main content

euv_engine/particle/
impl.rs

1use super::*;
2
3/// Implements deterministic pseudo-random number generation for `ParticleRng`.
4impl ParticleRng {
5    /// Creates a generator from the given seed. A zero seed is replaced with
6    /// the default seed, since xorshift degenerates at zero.
7    ///
8    /// # Arguments
9    ///
10    /// - `u64` - The seed value.
11    ///
12    /// # Returns
13    ///
14    /// - `ParticleRng` - The seeded generator.
15    pub fn with_seed(seed: u64) -> ParticleRng {
16        if seed == 0 {
17            ParticleRng::new(PARTICLE_DEFAULT_RNG_SEED)
18        } else {
19            ParticleRng::new(seed)
20        }
21    }
22
23    /// Advances the generator and returns the next 64-bit value.
24    ///
25    /// # Returns
26    ///
27    /// - `u64` - The next pseudo-random value.
28    pub fn next_u64(&mut self) -> u64 {
29        let mut state: u64 = self.get_state();
30        state ^= state >> 12;
31        state ^= state << 25;
32        state ^= state >> 27;
33        self.set_state(state);
34        state.wrapping_mul(0x2545_F491_4F6C_DD1D)
35    }
36
37    /// Returns the next pseudo-random value uniformly distributed in [0.0, 1.0).
38    ///
39    /// # Returns
40    ///
41    /// - `f64` - The next value in the unit interval.
42    pub fn next_f64(&mut self) -> f64 {
43        (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
44    }
45
46    /// Returns the next pseudo-random value uniformly distributed in [min, max).
47    ///
48    /// # Arguments
49    ///
50    /// - `f64` - The inclusive lower bound.
51    /// - `f64` - The exclusive upper bound.
52    ///
53    /// # Returns
54    ///
55    /// - `f64` - The next value in the given range.
56    pub fn range(&mut self, min: f64, max: f64) -> f64 {
57        min + (max - min) * self.next_f64()
58    }
59}
60
61/// Implements `Default` for `ParticleRng` using the default seed.
62impl Default for ParticleRng {
63    fn default() -> ParticleRng {
64        ParticleRng::with_seed(PARTICLE_DEFAULT_RNG_SEED)
65    }
66}
67
68/// Implements `Default` for `ParticleConfig` with sensible engine defaults.
69impl Default for ParticleConfig {
70    fn default() -> ParticleConfig {
71        ParticleConfig {
72            emission_rate: PARTICLE_DEFAULT_EMISSION_RATE,
73            max_particles: PARTICLE_DEFAULT_MAX_COUNT,
74            lifetime_min: PARTICLE_DEFAULT_LIFETIME_MIN,
75            lifetime_max: PARTICLE_DEFAULT_LIFETIME_MAX,
76            speed_min: PARTICLE_DEFAULT_SPEED_MIN,
77            speed_max: PARTICLE_DEFAULT_SPEED_MAX,
78            angle: PARTICLE_DEFAULT_ANGLE,
79            spread: PARTICLE_DEFAULT_SPREAD,
80            gravity: Vector2D::zero(),
81            color_start: Color::white(),
82            color_end: Color::transparent(),
83            size_start: PARTICLE_DEFAULT_SIZE_START,
84            size_end: PARTICLE_DEFAULT_SIZE_END,
85        }
86    }
87}
88
89/// Implements creation, simulation, and rendering for `ParticleEmitter`.
90impl ParticleEmitter {
91    /// Creates an active emitter at the given position with the given config.
92    ///
93    /// # Arguments
94    ///
95    /// - `Vector2D` - The world-space emission point.
96    /// - `ParticleConfig` - The emitter configuration.
97    ///
98    /// # Returns
99    ///
100    /// - `ParticleEmitter` - The new emitter.
101    pub fn create(position: Vector2D, config: ParticleConfig) -> ParticleEmitter {
102        let mut emitter: ParticleEmitter = ParticleEmitter::new(position, config);
103        emitter.set_active(true);
104        emitter
105    }
106
107    /// Creates an active emitter at the given position using the default
108    /// configuration.
109    ///
110    /// # Arguments
111    ///
112    /// - `Vector2D` - The world-space emission point.
113    ///
114    /// # Returns
115    ///
116    /// - `ParticleEmitter` - The new emitter.
117    pub fn with_defaults(position: Vector2D) -> ParticleEmitter {
118        ParticleEmitter::create(position, ParticleConfig::default())
119    }
120
121    /// Advances the emitter by the given delta time: spawns new particles
122    /// from the emission budget while active, integrates motion and gravity,
123    /// and removes expired particles.
124    ///
125    /// # Arguments
126    ///
127    /// - `f64` - The time elapsed since the last update, in seconds.
128    pub fn update(&mut self, delta_time: f64) {
129        let delta_time: f64 = delta_time.max(0.0);
130        if self.get_active() {
131            *self.get_mut_emit_accumulator() += self.get_config().get_emission_rate() * delta_time;
132            let mut spawn_count: usize = self.get_emit_accumulator() as usize;
133            let capacity: usize = self
134                .get_config()
135                .get_max_particles()
136                .saturating_sub(self.get_particles().len());
137            spawn_count = spawn_count.min(capacity);
138            *self.get_mut_emit_accumulator() -= spawn_count as f64;
139            for _ in 0..spawn_count {
140                self.spawn_particle();
141            }
142        }
143        let gravity: Vector2D = self.get_config().get_gravity();
144        for particle in self.get_mut_particles().iter_mut() {
145            *particle.get_mut_velocity() += gravity.scaled(delta_time);
146            let velocity: Vector2D = particle.get_velocity();
147            *particle.get_mut_position() += velocity.scaled(delta_time);
148            *particle.get_mut_age() += delta_time;
149        }
150        self.get_mut_particles()
151            .retain(|particle: &Particle| particle.get_age() < particle.get_lifetime());
152    }
153
154    /// Spawns the given number of particles immediately, regardless of the
155    /// active flag, clamped to the remaining particle capacity.
156    ///
157    /// # Arguments
158    ///
159    /// - `usize` - The number of particles to spawn.
160    pub fn burst(&mut self, count: usize) {
161        let capacity: usize = self
162            .get_config()
163            .get_max_particles()
164            .saturating_sub(self.get_particles().len());
165        for _ in 0..count.min(capacity) {
166            self.spawn_particle();
167        }
168    }
169
170    /// Records all live particles into the given draw list as filled circles.
171    ///
172    /// Each particle's color and radius are interpolated between the
173    /// configured start and end values by its normalized age.
174    ///
175    /// # Arguments
176    ///
177    /// - `&mut DrawList` - The draw list to record commands into.
178    pub fn render(&self, draw_list: &mut DrawList) {
179        let config: ParticleConfig = self.get_config();
180        for particle in self.get_particles().iter() {
181            let t: f64 = if particle.get_lifetime() > 0.0 {
182                (particle.get_age() / particle.get_lifetime()).min(1.0)
183            } else {
184                1.0
185            };
186            let color: Color = config.get_color_start().lerp(config.get_color_end(), t);
187            let radius: f64 = Numeric::lerp(config.get_size_start(), config.get_size_end(), t);
188            if radius <= 0.0 || color.get_alpha() <= 0.0 {
189                continue;
190            }
191            draw_list.fill_circle(particle.get_position(), radius, color);
192        }
193    }
194
195    /// Returns the number of currently live particles.
196    ///
197    /// # Returns
198    ///
199    /// - `usize` - The live particle count.
200    pub fn alive_count(&self) -> usize {
201        self.get_particles().len()
202    }
203
204    /// Removes all live particles without changing the active flag.
205    pub fn clear(&mut self) {
206        self.get_mut_particles().clear();
207        self.set_emit_accumulator(0.0);
208    }
209
210    /// Spawns a single particle with randomized direction, speed, and
211    /// lifetime sampled from the configuration ranges.
212    fn spawn_particle(&mut self) {
213        let config: ParticleConfig = self.get_config();
214        let half_spread: f64 = config.get_spread() / 2.0;
215        let angle: f64 = config.get_angle() + self.get_mut_rng().range(-half_spread, half_spread);
216        let speed: f64 = self
217            .get_mut_rng()
218            .range(config.get_speed_min(), config.get_speed_max());
219        let lifetime: f64 = self
220            .get_mut_rng()
221            .range(config.get_lifetime_min(), config.get_lifetime_max())
222            .max(EPSILON);
223        let position: Vector2D = self.get_position();
224        let particle: Particle = Particle::new(
225            position,
226            Vector2D::from_angle(angle).scaled(speed),
227            0.0,
228            lifetime,
229        );
230        self.get_mut_particles().push(particle);
231    }
232}
233
234/// Forwards `ParticleEmitter::update` through the [`Updatable`] trait so
235/// emitters can participate in the same generic update loop as entities,
236/// animators, scenes, and physics worlds.
237impl Updatable for ParticleEmitter {
238    fn update(&mut self, delta_time: f64) {
239        ParticleEmitter::update(self, delta_time);
240    }
241}