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 /// Constructs a default [`ParticleRng`] value.
64 ///
65 /// # Returns
66 ///
67 /// - `ParticleRng` - A default-constructed instance with the documented initial state.
68 fn default() -> ParticleRng {
69 ParticleRng::with_seed(PARTICLE_DEFAULT_RNG_SEED)
70 }
71}
72
73/// Implements `Default` for `ParticleConfig` with sensible engine defaults.
74impl Default for ParticleConfig {
75 /// Constructs a default [`ParticleConfig`] value.
76 ///
77 /// # Returns
78 ///
79 /// - `ParticleConfig` - A default-constructed instance with the documented initial state.
80 fn default() -> ParticleConfig {
81 ParticleConfig {
82 emission_rate: PARTICLE_DEFAULT_EMISSION_RATE,
83 max_particles: PARTICLE_DEFAULT_MAX_COUNT,
84 lifetime_min: PARTICLE_DEFAULT_LIFETIME_MIN,
85 lifetime_max: PARTICLE_DEFAULT_LIFETIME_MAX,
86 speed_min: PARTICLE_DEFAULT_SPEED_MIN,
87 speed_max: PARTICLE_DEFAULT_SPEED_MAX,
88 angle: PARTICLE_DEFAULT_ANGLE,
89 spread: PARTICLE_DEFAULT_SPREAD,
90 gravity: Vector2D::zero(),
91 color_start: Color::white(),
92 color_end: Color::transparent(),
93 size_start: PARTICLE_DEFAULT_SIZE_START,
94 size_end: PARTICLE_DEFAULT_SIZE_END,
95 }
96 }
97}
98
99/// Implements creation, simulation, and rendering for `ParticleEmitter`.
100impl ParticleEmitter {
101 /// Creates an active emitter at the given position with the given config.
102 ///
103 /// # Arguments
104 ///
105 /// - `Vector2D` - The world-space emission point.
106 /// - `ParticleConfig` - The emitter configuration.
107 ///
108 /// # Returns
109 ///
110 /// - `ParticleEmitter` - The new emitter.
111 pub fn create(position: Vector2D, config: ParticleConfig) -> ParticleEmitter {
112 let mut emitter: ParticleEmitter = ParticleEmitter::new(position, config);
113 emitter.set_active(true);
114 emitter
115 }
116
117 /// Creates an active emitter at the given position using the default
118 /// configuration.
119 ///
120 /// # Arguments
121 ///
122 /// - `Vector2D` - The world-space emission point.
123 ///
124 /// # Returns
125 ///
126 /// - `ParticleEmitter` - The new emitter.
127 pub fn with_defaults(position: Vector2D) -> ParticleEmitter {
128 ParticleEmitter::create(position, ParticleConfig::default())
129 }
130
131 /// Advances the emitter by the given delta time: spawns new particles
132 /// from the emission budget while active, integrates motion and gravity,
133 /// and removes expired particles.
134 ///
135 /// # Arguments
136 ///
137 /// - `f64` - The time elapsed since the last update, in seconds.
138 pub fn update(&mut self, delta_time: f64) {
139 let delta_time: f64 = delta_time.max(0.0);
140 if self.get_active() {
141 *self.get_mut_emit_accumulator() += self.get_config().get_emission_rate() * delta_time;
142 let mut spawn_count: usize = self.get_emit_accumulator() as usize;
143 let capacity: usize = self
144 .get_config()
145 .get_max_particles()
146 .saturating_sub(self.get_particles().len());
147 spawn_count = spawn_count.min(capacity);
148 *self.get_mut_emit_accumulator() -= spawn_count as f64;
149 for _ in 0..spawn_count {
150 self.spawn_particle();
151 }
152 }
153 let gravity: Vector2D = self.get_config().get_gravity();
154 for particle in self.get_mut_particles().iter_mut() {
155 *particle.get_mut_velocity() += gravity.scaled(delta_time);
156 let velocity: Vector2D = particle.get_velocity();
157 *particle.get_mut_position() += velocity.scaled(delta_time);
158 *particle.get_mut_age() += delta_time;
159 }
160 self.get_mut_particles()
161 .retain(|particle: &Particle| particle.get_age() < particle.get_lifetime());
162 }
163
164 /// Spawns the given number of particles immediately, regardless of the
165 /// active flag, clamped to the remaining particle capacity.
166 ///
167 /// # Arguments
168 ///
169 /// - `usize` - The number of particles to spawn.
170 pub fn burst(&mut self, count: usize) {
171 let capacity: usize = self
172 .get_config()
173 .get_max_particles()
174 .saturating_sub(self.get_particles().len());
175 for _ in 0..count.min(capacity) {
176 self.spawn_particle();
177 }
178 }
179
180 /// Records all live particles into the given draw list as filled circles.
181 ///
182 /// Each particle's color and radius are interpolated between the
183 /// configured start and end values by its normalized age.
184 ///
185 /// # Arguments
186 ///
187 /// - `&mut DrawList` - The draw list to record commands into.
188 pub fn render(&self, draw_list: &mut DrawList) {
189 let config: ParticleConfig = self.get_config();
190 for particle in self.get_particles().iter() {
191 let t: f64 = if particle.get_lifetime() > 0.0 {
192 (particle.get_age() / particle.get_lifetime()).min(1.0)
193 } else {
194 1.0
195 };
196 let color: Color = config.get_color_start().lerp(config.get_color_end(), t);
197 let radius: f64 = Numeric::lerp(config.get_size_start(), config.get_size_end(), t);
198 if radius <= 0.0 || color.get_alpha() <= 0.0 {
199 continue;
200 }
201 draw_list.fill_circle(particle.get_position(), radius, color);
202 }
203 }
204
205 /// Returns the number of currently live particles.
206 ///
207 /// # Returns
208 ///
209 /// - `usize` - The live particle count.
210 pub fn alive_count(&self) -> usize {
211 self.get_particles().len()
212 }
213
214 /// Removes all live particles without changing the active flag.
215 pub fn clear(&mut self) {
216 self.get_mut_particles().clear();
217 self.set_emit_accumulator(0.0);
218 }
219
220 /// Spawns a single particle with randomized direction, speed, and
221 /// lifetime sampled from the configuration ranges.
222 fn spawn_particle(&mut self) {
223 let config: ParticleConfig = self.get_config();
224 let half_spread: f64 = config.get_spread() / 2.0;
225 let angle: f64 = config.get_angle() + self.get_mut_rng().range(-half_spread, half_spread);
226 let speed: f64 = self
227 .get_mut_rng()
228 .range(config.get_speed_min(), config.get_speed_max());
229 let lifetime: f64 = self
230 .get_mut_rng()
231 .range(config.get_lifetime_min(), config.get_lifetime_max())
232 .max(EPSILON);
233 let position: Vector2D = self.get_position();
234 let particle: Particle = Particle::new(
235 position,
236 Vector2D::from_angle(angle).scaled(speed),
237 0.0,
238 lifetime,
239 );
240 self.get_mut_particles().push(particle);
241 }
242}
243
244/// Forwards `ParticleEmitter::update` through the [`Updatable`] trait so
245/// emitters can participate in the same generic update loop as entities,
246/// animators, scenes, and physics worlds.
247impl Updatable for ParticleEmitter {
248 /// Advances the simulation by `delta_time` seconds.
249 ///
250 /// # Arguments
251 ///
252 /// - `f64` - Seconds elapsed since the previous update.
253 fn update(&mut self, delta_time: f64) {
254 ParticleEmitter::update(self, delta_time);
255 }
256}