use crate::math::{Vec2, FloatExt};
use crate::color::Color;
use quad_rand as rand;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Easing {
Linear,
EaseOut,
EaseIn,
EaseInOut,
QuadOut,
}
impl Easing {
pub fn apply(self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
Easing::Linear => t,
Easing::EaseOut => 1.0 - (1.0 - t).powi(3),
Easing::EaseIn => t.powi(3),
Easing::EaseInOut => {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
Easing::QuadOut => 1.0 - (1.0 - t) * (1.0 - t),
}
}
}
#[derive(Debug, Clone)]
pub struct Particle {
pub position: Vec2,
pub velocity: Vec2,
pub acceleration: Vec2,
pub color: Color,
pub color_start: Color,
pub color_end: Color,
pub size: f32,
pub size_start: f32,
pub size_end: f32,
pub rotation: f32,
pub rotation_speed: f32,
pub age: f32,
pub lifetime: f32,
pub alive: bool,
pub alpha_easing: Easing,
pub size_easing: Easing,
}
impl Particle {
fn new() -> Self {
Self {
position: Vec2::ZERO,
velocity: Vec2::ZERO,
acceleration: Vec2::ZERO,
color: Color::WHITE,
color_start: Color::WHITE,
color_end: Color::TRANSPARENT,
size: 5.0,
size_start: 5.0,
size_end: 0.0,
rotation: 0.0,
rotation_speed: 0.0,
age: 0.0,
lifetime: 1.0,
alive: true,
alpha_easing: Easing::Linear,
size_easing: Easing::QuadOut,
}
}
fn update(&mut self, dt: f32, drag: f32, gravity: Vec2) {
if !self.alive {
return;
}
self.age += dt;
if self.age >= self.lifetime {
self.alive = false;
return;
}
let t = self.age / self.lifetime;
self.velocity += (self.acceleration + gravity) * dt;
self.velocity *= 1.0 - drag * dt;
self.position += self.velocity * dt;
self.rotation += self.rotation_speed * dt;
self.color = self.color_start.lerp(self.color_end, Easing::Linear.apply(t));
self.color.a = self.color_start.a * (1.0 - Easing::Linear.apply(t));
self.size = self.size_start.lerp(self.size_end, self.size_easing.apply(t));
}
}
#[derive(Debug, Clone, Copy)]
pub enum EmitterShape {
Point,
Line { x1: f32, y1: f32, x2: f32, y2: f32 },
Circle { cx: f32, cy: f32, radius: f32 },
Rect { x: f32, y: f32, w: f32, h: f32 },
}
#[derive(Debug, Clone)]
pub struct ParticleEmitter {
pub position: Vec2,
pub shape: EmitterShape,
pub rate: f32,
emit_accumulator: f32,
pub burst: u32,
burst_emitted: u32,
pub template: Particle,
pub speed_range: (f32, f32),
pub angle_range: (f32, f32),
pub size_range: (f32, f32),
pub lifetime_range: (f32, f32),
pub gravity: Vec2,
pub drag: f32,
pub max_particles: usize,
particles: Vec<Particle>,
pub active: bool,
pub looping: bool,
}
impl ParticleEmitter {
pub fn new(position: Vec2) -> Self {
Self {
position,
shape: EmitterShape::Point,
rate: 10.0,
emit_accumulator: 0.0,
burst: 0,
burst_emitted: 0,
template: Particle::new(),
speed_range: (50.0, 150.0),
angle_range: (0.0, std::f32::consts::TAU),
size_range: (2.0, 8.0),
lifetime_range: (0.5, 2.0),
gravity: Vec2::new(0.0, 100.0),
drag: 0.0,
max_particles: 1000,
particles: Vec::with_capacity(1000),
active: true,
looping: false,
}
}
pub fn configure(&mut self, f: impl FnOnce(&mut ParticleEmitterConfig)) {
let mut cfg = ParticleEmitterConfig { emitter: self };
f(&mut cfg);
}
pub fn particles(&self) -> &[Particle] {
&self.particles
}
pub fn particles_mut(&mut self) -> &mut Vec<Particle> {
&mut self.particles
}
pub fn alive_count(&self) -> usize {
self.particles.iter().filter(|p| p.alive).count()
}
fn emit_one(&mut self) {
let pos = match self.shape {
EmitterShape::Point => self.position,
EmitterShape::Line { x1, y1, x2, y2 } => {
let t = rand::gen_range(0.0, 1.0);
Vec2::new(x1.lerp(x2, t), y1.lerp(y2, t))
}
EmitterShape::Circle { cx, cy, radius } => {
let angle = rand::gen_range(0.0, std::f32::consts::TAU);
let r = radius.sqrt() * rand::gen_range(0.0, 1.0);
Vec2::new(cx + angle.cos() * r, cy + angle.sin() * r)
}
EmitterShape::Rect { x, y, w, h } => {
Vec2::new(x + rand::gen_range(0.0, w), y + rand::gen_range(0.0, h))
}
};
let speed = rand::gen_range(self.speed_range.0, self.speed_range.1);
let angle = rand::gen_range(self.angle_range.0, self.angle_range.1);
let vel = Vec2::new(angle.cos() * speed, angle.sin() * speed);
let size = rand::gen_range(self.size_range.0, self.size_range.1);
let lifetime = rand::gen_range(self.lifetime_range.0, self.lifetime_range.1);
let mut p = self.template.clone();
p.position = pos;
p.velocity = vel;
p.size_start = size;
p.size = size;
p.size_end = self.template.size_end;
p.lifetime = lifetime;
p.color_start = self.template.color_start;
p.color_end = self.template.color_end;
self.particles.push(p);
}
pub fn update(&mut self, dt: f32) {
if !self.active {
return;
}
if self.burst > 0 {
if self.burst_emitted < self.burst {
let to_emit = self.burst - self.burst_emitted;
for _ in 0..to_emit {
if self.alive_count() < self.max_particles {
self.emit_one();
self.burst_emitted += 1;
}
}
if !self.looping {
self.active = false;
}
} else if self.looping {
if self.alive_count() == 0 {
self.burst_emitted = 0;
}
}
} else {
self.emit_accumulator += dt;
let interval = 1.0 / self.rate;
while self.emit_accumulator >= interval {
self.emit_accumulator -= interval;
if self.alive_count() < self.max_particles {
self.emit_one();
}
}
}
for p in &mut self.particles {
p.update(dt, self.drag, self.gravity);
}
let dead_count = self.particles.iter().filter(|p| !p.alive).count();
if dead_count > self.max_particles / 4 {
self.particles.retain(|p| p.alive);
}
}
pub fn clear(&mut self) {
self.particles.clear();
self.emit_accumulator = 0.0;
self.burst_emitted = 0;
self.active = true;
}
pub fn burst_now(&mut self, count: u32) {
for _ in 0..count {
if self.alive_count() < self.max_particles {
self.emit_one();
}
}
}
}
pub struct ParticleEmitterConfig<'a> {
emitter: &'a mut ParticleEmitter,
}
impl<'a> ParticleEmitterConfig<'a> {
pub fn rate(&mut self, rate: f32) -> &mut Self { self.emitter.rate = rate; self }
pub fn shape(&mut self, shape: EmitterShape) -> &mut Self { self.emitter.shape = shape; self }
pub fn speed(&mut self, min: f32, max: f32) -> &mut Self { self.emitter.speed_range = (min, max); self }
pub fn angle(&mut self, min: f32, max: f32) -> &mut Self { self.emitter.angle_range = (min, max); self }
pub fn size(&mut self, min: f32, max: f32) -> &mut Self { self.emitter.size_range = (min, max); self }
pub fn lifetime(&mut self, min: f32, max: f32) -> &mut Self { self.emitter.lifetime_range = (min, max); self }
pub fn gravity(&mut self, x: f32, y: f32) -> &mut Self { self.emitter.gravity = Vec2::new(x, y); self }
pub fn drag(&mut self, drag: f32) -> &mut Self { self.emitter.drag = drag; self }
pub fn max_particles(&mut self, max: usize) -> &mut Self { self.emitter.max_particles = max; self }
pub fn color_start(&mut self, color: Color) -> &mut Self { self.emitter.template.color_start = color; self }
pub fn color_end(&mut self, color: Color) -> &mut Self { self.emitter.template.color_end = color; self }
pub fn size_end(&mut self, size: f32) -> &mut Self { self.emitter.template.size_end = size; self }
pub fn burst(&mut self, count: u32) -> &mut Self { self.emitter.burst = count; self }
pub fn looping(&mut self, looping: bool) -> &mut Self { self.emitter.looping = looping; self }
pub fn rotation_speed(&mut self, min: f32, max: f32) -> &mut Self {
self.emitter.template.rotation_speed = rand::gen_range(min, max);
self
}
}