1use crate::math::{Vec2, FloatExt};
12use crate::color::Color;
13use quad_rand as rand;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum Easing {
22 Linear,
24 EaseOut,
26 EaseIn,
28 EaseInOut,
30 QuadOut,
32}
33
34impl Easing {
35 pub fn apply(self, t: f32) -> f32 {
37 let t = t.clamp(0.0, 1.0);
38 match self {
39 Easing::Linear => t,
40 Easing::EaseOut => 1.0 - (1.0 - t).powi(3),
41 Easing::EaseIn => t.powi(3),
42 Easing::EaseInOut => {
43 if t < 0.5 {
44 4.0 * t * t * t
45 } else {
46 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
47 }
48 }
49 Easing::QuadOut => 1.0 - (1.0 - t) * (1.0 - t),
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
60pub struct Particle {
61 pub position: Vec2,
63 pub velocity: Vec2,
65 pub acceleration: Vec2,
67 pub color: Color,
69 pub color_start: Color,
71 pub color_end: Color,
73 pub size: f32,
75 pub size_start: f32,
77 pub size_end: f32,
79 pub rotation: f32,
81 pub rotation_speed: f32,
83 pub age: f32,
85 pub lifetime: f32,
87 pub alive: bool,
89 pub alpha_easing: Easing,
91 pub size_easing: Easing,
93}
94
95impl Particle {
96 fn new() -> Self {
98 Self {
99 position: Vec2::ZERO,
100 velocity: Vec2::ZERO,
101 acceleration: Vec2::ZERO,
102 color: Color::WHITE,
103 color_start: Color::WHITE,
104 color_end: Color::TRANSPARENT,
105 size: 5.0,
106 size_start: 5.0,
107 size_end: 0.0,
108 rotation: 0.0,
109 rotation_speed: 0.0,
110 age: 0.0,
111 lifetime: 1.0,
112 alive: true,
113 alpha_easing: Easing::Linear,
114 size_easing: Easing::QuadOut,
115 }
116 }
117
118 fn update(&mut self, dt: f32, drag: f32, gravity: Vec2) {
120 if !self.alive {
121 return;
122 }
123
124 self.age += dt;
125 if self.age >= self.lifetime {
126 self.alive = false;
127 return;
128 }
129
130 let t = self.age / self.lifetime; self.velocity += (self.acceleration + gravity) * dt;
134 self.velocity *= 1.0 - drag * dt;
135 self.position += self.velocity * dt;
136 self.rotation += self.rotation_speed * dt;
137
138 self.color = self.color_start.lerp(self.color_end, Easing::Linear.apply(t));
140 self.color.a = self.color_start.a * (1.0 - Easing::Linear.apply(t));
141
142 self.size = self.size_start.lerp(self.size_end, self.size_easing.apply(t));
144 }
145}
146
147#[derive(Debug, Clone, Copy)]
153pub enum EmitterShape {
154 Point,
156 Line { x1: f32, y1: f32, x2: f32, y2: f32 },
158 Circle { cx: f32, cy: f32, radius: f32 },
160 Rect { x: f32, y: f32, w: f32, h: f32 },
162}
163
164#[derive(Debug, Clone)]
170pub struct ParticleEmitter {
171 pub position: Vec2,
173 pub shape: EmitterShape,
175 pub rate: f32,
177 emit_accumulator: f32,
179 pub burst: u32,
181 burst_emitted: u32,
183 pub template: Particle,
185 pub speed_range: (f32, f32),
187 pub angle_range: (f32, f32),
189 pub size_range: (f32, f32),
191 pub lifetime_range: (f32, f32),
193 pub gravity: Vec2,
195 pub drag: f32,
197 pub max_particles: usize,
199 particles: Vec<Particle>,
201 pub active: bool,
203 pub looping: bool,
205}
206
207impl ParticleEmitter {
208 pub fn new(position: Vec2) -> Self {
210 Self {
211 position,
212 shape: EmitterShape::Point,
213 rate: 10.0,
214 emit_accumulator: 0.0,
215 burst: 0,
216 burst_emitted: 0,
217 template: Particle::new(),
218 speed_range: (50.0, 150.0),
219 angle_range: (0.0, std::f32::consts::TAU),
220 size_range: (2.0, 8.0),
221 lifetime_range: (0.5, 2.0),
222 gravity: Vec2::new(0.0, 100.0),
223 drag: 0.0,
224 max_particles: 1000,
225 particles: Vec::with_capacity(1000),
226 active: true,
227 looping: false,
228 }
229 }
230
231 pub fn configure(&mut self, f: impl FnOnce(&mut ParticleEmitterConfig)) {
233 let mut cfg = ParticleEmitterConfig { emitter: self };
234 f(&mut cfg);
235 }
236
237 pub fn particles(&self) -> &[Particle] {
239 &self.particles
240 }
241
242 pub fn particles_mut(&mut self) -> &mut Vec<Particle> {
244 &mut self.particles
245 }
246
247 pub fn alive_count(&self) -> usize {
249 self.particles.iter().filter(|p| p.alive).count()
250 }
251
252 fn emit_one(&mut self) {
254 let pos = match self.shape {
255 EmitterShape::Point => self.position,
256 EmitterShape::Line { x1, y1, x2, y2 } => {
257 let t = rand::gen_range(0.0, 1.0);
258 Vec2::new(x1.lerp(x2, t), y1.lerp(y2, t))
259 }
260 EmitterShape::Circle { cx, cy, radius } => {
261 let angle = rand::gen_range(0.0, std::f32::consts::TAU);
262 let r = radius.sqrt() * rand::gen_range(0.0, 1.0);
263 Vec2::new(cx + angle.cos() * r, cy + angle.sin() * r)
264 }
265 EmitterShape::Rect { x, y, w, h } => {
266 Vec2::new(x + rand::gen_range(0.0, w), y + rand::gen_range(0.0, h))
267 }
268 };
269
270 let speed = rand::gen_range(self.speed_range.0, self.speed_range.1);
271 let angle = rand::gen_range(self.angle_range.0, self.angle_range.1);
272 let vel = Vec2::new(angle.cos() * speed, angle.sin() * speed);
273
274 let size = rand::gen_range(self.size_range.0, self.size_range.1);
275 let lifetime = rand::gen_range(self.lifetime_range.0, self.lifetime_range.1);
276
277 let mut p = self.template.clone();
278 p.position = pos;
279 p.velocity = vel;
280 p.size_start = size;
281 p.size = size;
282 p.size_end = self.template.size_end;
283 p.lifetime = lifetime;
284 p.color_start = self.template.color_start;
285 p.color_end = self.template.color_end;
286
287 self.particles.push(p);
288 }
289
290 pub fn update(&mut self, dt: f32) {
292 if !self.active {
293 return;
294 }
295
296 if self.burst > 0 {
298 if self.burst_emitted < self.burst {
299 let to_emit = self.burst - self.burst_emitted;
300 for _ in 0..to_emit {
301 if self.alive_count() < self.max_particles {
302 self.emit_one();
303 self.burst_emitted += 1;
304 }
305 }
306 if !self.looping {
307 self.active = false;
308 }
309 } else if self.looping {
310 if self.alive_count() == 0 {
312 self.burst_emitted = 0;
313 }
314 }
315 } else {
316 self.emit_accumulator += dt;
317 let interval = 1.0 / self.rate;
318 while self.emit_accumulator >= interval {
319 self.emit_accumulator -= interval;
320 if self.alive_count() < self.max_particles {
321 self.emit_one();
322 }
323 }
324 }
325
326 for p in &mut self.particles {
328 p.update(dt, self.drag, self.gravity);
329 }
330
331 let dead_count = self.particles.iter().filter(|p| !p.alive).count();
334 if dead_count > self.max_particles / 4 {
335 self.particles.retain(|p| p.alive);
336 }
337 }
338
339 pub fn clear(&mut self) {
341 self.particles.clear();
342 self.emit_accumulator = 0.0;
343 self.burst_emitted = 0;
344 self.active = true;
345 }
346
347 pub fn burst_now(&mut self, count: u32) {
349 for _ in 0..count {
350 if self.alive_count() < self.max_particles {
351 self.emit_one();
352 }
353 }
354 }
355}
356
357pub struct ParticleEmitterConfig<'a> {
359 emitter: &'a mut ParticleEmitter,
360}
361
362impl<'a> ParticleEmitterConfig<'a> {
363 pub fn rate(&mut self, rate: f32) -> &mut Self { self.emitter.rate = rate; self }
364 pub fn shape(&mut self, shape: EmitterShape) -> &mut Self { self.emitter.shape = shape; self }
365 pub fn speed(&mut self, min: f32, max: f32) -> &mut Self { self.emitter.speed_range = (min, max); self }
366 pub fn angle(&mut self, min: f32, max: f32) -> &mut Self { self.emitter.angle_range = (min, max); self }
367 pub fn size(&mut self, min: f32, max: f32) -> &mut Self { self.emitter.size_range = (min, max); self }
368 pub fn lifetime(&mut self, min: f32, max: f32) -> &mut Self { self.emitter.lifetime_range = (min, max); self }
369 pub fn gravity(&mut self, x: f32, y: f32) -> &mut Self { self.emitter.gravity = Vec2::new(x, y); self }
370 pub fn drag(&mut self, drag: f32) -> &mut Self { self.emitter.drag = drag; self }
371 pub fn max_particles(&mut self, max: usize) -> &mut Self { self.emitter.max_particles = max; self }
372 pub fn color_start(&mut self, color: Color) -> &mut Self { self.emitter.template.color_start = color; self }
373 pub fn color_end(&mut self, color: Color) -> &mut Self { self.emitter.template.color_end = color; self }
374 pub fn size_end(&mut self, size: f32) -> &mut Self { self.emitter.template.size_end = size; self }
375 pub fn burst(&mut self, count: u32) -> &mut Self { self.emitter.burst = count; self }
376 pub fn looping(&mut self, looping: bool) -> &mut Self { self.emitter.looping = looping; self }
377 pub fn rotation_speed(&mut self, min: f32, max: f32) -> &mut Self {
378 self.emitter.template.rotation_speed = rand::gen_range(min, max);
379 self
380 }
381}