use rand::Rng;
#[derive(Debug, Clone)]
pub struct Particle {
pub x: f32,
pub y: f32,
pub vx: f32,
pub vy: f32,
pub life: f32,
pub max_life: f32,
pub color: [u8; 4],
pub size: f32,
pub rotation: f32,
pub rotation_speed: f32,
pub scale: f32,
pub scale_speed: f32,
pub fade_in: f32,
pub fade_out: f32,
pub acceleration_x: f32,
pub acceleration_y: f32,
pub color_shift: [f32; 4],
pub turbulence: f32,
}
impl Particle {
pub fn new(x: f32, y: f32, vx: f32, vy: f32, life: f32, color: [u8; 4], size: f32) -> Self {
Self {
x,
y,
vx,
vy,
life,
max_life: life,
color,
size,
rotation: 0.0,
rotation_speed: 0.0,
scale: 1.0,
scale_speed: 0.0,
fade_in: 0.0,
fade_out: 0.2,
acceleration_x: 0.0,
acceleration_y: 0.0,
color_shift: [0.0, 0.0, 0.0, 0.0],
turbulence: 0.0,
}
}
pub fn with_rotation(mut self, rotation: f32, rotation_speed: f32) -> Self {
self.rotation = rotation;
self.rotation_speed = rotation_speed;
self
}
pub fn with_scale(mut self, scale: f32, scale_speed: f32) -> Self {
self.scale = scale;
self.scale_speed = scale_speed;
self
}
pub fn with_fade(mut self, fade_in: f32, fade_out: f32) -> Self {
self.fade_in = fade_in;
self.fade_out = fade_out;
self
}
pub fn with_acceleration(mut self, ax: f32, ay: f32) -> Self {
self.acceleration_x = ax;
self.acceleration_y = ay;
self
}
pub fn with_color_shift(mut self, shift: [f32; 4]) -> Self {
self.color_shift = shift;
self
}
pub fn with_turbulence(mut self, turbulence: f32) -> Self {
self.turbulence = turbulence;
self
}
pub fn update(&mut self, dt: f32, gravity: f32, drag: f32) {
self.vx += self.acceleration_x * dt;
self.vy += self.acceleration_y * dt;
if self.turbulence > 0.0 {
use rand::Rng;
let mut rng = rand::thread_rng();
self.vx += rng.gen_range(-self.turbulence..self.turbulence);
self.vy += rng.gen_range(-self.turbulence..self.turbulence);
}
self.x += self.vx * dt;
self.y += self.vy * dt;
self.vy += gravity * dt;
self.vx *= 1.0 - drag * dt;
self.vy *= 1.0 - drag * dt;
self.rotation += self.rotation_speed * dt;
self.scale += self.scale_speed * dt;
self.scale = self.scale.max(0.0);
let life_ratio = self.life / self.max_life;
for i in 0..3 {
let shift = self.color_shift[i] * (1.0 - life_ratio);
self.color[i] = (self.color[i] as f32 + shift).clamp(0.0, 255.0) as u8;
}
self.life -= dt;
}
pub fn is_alive(&self) -> bool {
self.life > 0.0
}
pub fn alpha(&self) -> u8 {
let life_ratio = self.life / self.max_life;
let age = 1.0 - life_ratio;
let mut alpha = 1.0;
if age < self.fade_in {
alpha = age / self.fade_in;
}
if life_ratio < self.fade_out {
alpha = alpha.min(life_ratio / self.fade_out);
}
(alpha * 255.0) as u8
}
pub fn get_size(&self) -> f32 {
self.size * self.scale
}
}
pub struct ParticleSystem {
pub particles: Vec<Particle>,
pub max_particles: usize,
pub drag: f32,
pub blend_mode: BlendMode,
pub global_gravity: f32,
pub wind: (f32, f32),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BlendMode {
Normal,
Additive,
Multiply,
}
impl ParticleSystem {
pub fn new(max_particles: usize) -> Self {
Self {
particles: Vec::with_capacity(max_particles),
max_particles,
drag: 0.0,
blend_mode: BlendMode::Normal,
global_gravity: 0.0,
wind: (0.0, 0.0),
}
}
pub fn with_drag(mut self, drag: f32) -> Self {
self.drag = drag.clamp(0.0, 1.0);
self
}
pub fn with_blend_mode(mut self, blend_mode: BlendMode) -> Self {
self.blend_mode = blend_mode;
self
}
pub fn with_wind(mut self, wind_x: f32, wind_y: f32) -> Self {
self.wind = (wind_x, wind_y);
self
}
pub fn emit(&mut self, particle: Particle) {
if self.particles.len() < self.max_particles {
self.particles.push(particle);
}
}
pub fn emit_burst(&mut self, x: f32, y: f32, count: usize, config: ParticleConfig) {
let mut rng = rand::thread_rng();
for _ in 0..count.min(self.max_particles - self.particles.len()) {
let angle = rng.gen_range(0.0..std::f32::consts::TAU);
let speed = rng.gen_range(config.min_speed..config.max_speed);
let vx = angle.cos() * speed;
let vy = angle.sin() * speed;
let life = rng.gen_range(config.min_life..config.max_life);
let size = rng.gen_range(config.min_size..config.max_size);
let particle = Particle::new(x, y, vx, vy, life, config.color, size);
self.particles.push(particle);
}
}
pub fn emit_stream(&mut self, x: f32, y: f32, rate: f32, dt: f32, config: ParticleConfig) {
let count = (rate * dt) as usize;
self.emit_burst(x, y, count, config);
}
pub fn update(&mut self, dt: f32, gravity: f32) {
let total_gravity = gravity + self.global_gravity;
for particle in &mut self.particles {
particle.vx += self.wind.0 * dt;
particle.vy += self.wind.1 * dt;
particle.update(dt, total_gravity, self.drag);
}
self.particles.retain(|p| p.is_alive());
}
pub fn render(&self, pixels: &mut [u8], camera_x: f32, camera_y: f32, viewport_width: u32, viewport_height: u32) {
for particle in &self.particles {
let screen_x = (particle.x - camera_x) as i32;
let screen_y = (particle.y - camera_y) as i32;
let size = particle.get_size();
let half_size = (size / 2.0) as i32;
for dy in -half_size..=half_size {
for dx in -half_size..=half_size {
let px = screen_x + dx;
let py = screen_y + dy;
if px < 0 || py < 0 || px >= viewport_width as i32 || py >= viewport_height as i32 {
continue;
}
let dist_sq = dx * dx + dy * dy;
if dist_sq <= half_size * half_size {
let index = ((py as u32 * viewport_width) + px as u32) as usize * 4;
if index + 3 < pixels.len() {
let mut color = particle.color;
color[3] = particle.alpha();
let edge_factor = 1.0 - (dist_sq as f32 / (half_size * half_size) as f32).sqrt();
color[3] = (color[3] as f32 * edge_factor) as u8;
self.blend_pixel(&mut pixels[index..index + 4], &color);
}
}
}
}
}
}
fn blend_pixel(&self, dest: &mut [u8], src: &[u8; 4]) {
let alpha = src[3] as f32 / 255.0;
match self.blend_mode {
BlendMode::Normal => {
for i in 0..3 {
dest[i] = ((src[i] as f32 * alpha) + (dest[i] as f32 * (1.0 - alpha))) as u8;
}
}
BlendMode::Additive => {
for i in 0..3 {
dest[i] = ((dest[i] as f32 + src[i] as f32 * alpha).min(255.0)) as u8;
}
}
BlendMode::Multiply => {
for i in 0..3 {
let blend = (dest[i] as f32 * src[i] as f32 / 255.0) as u8;
dest[i] = ((blend as f32 * alpha) + (dest[i] as f32 * (1.0 - alpha))) as u8;
}
}
}
}
pub fn clear(&mut self) {
self.particles.clear();
}
pub fn count(&self) -> usize {
self.particles.len()
}
}
#[derive(Debug, Clone, Copy)]
pub struct ParticleConfig {
pub color: [u8; 4],
pub min_speed: f32,
pub max_speed: f32,
pub min_life: f32,
pub max_life: f32,
pub min_size: f32,
pub max_size: f32,
}
impl ParticleConfig {
pub fn new(color: [u8; 4]) -> Self {
Self {
color,
min_speed: 50.0,
max_speed: 150.0,
min_life: 0.5,
max_life: 2.0,
min_size: 2.0,
max_size: 4.0,
}
}
pub fn explosion() -> Self {
Self::new([255, 200, 0, 255])
.with_speed(100.0, 300.0)
.with_life(0.3, 1.0)
.with_size(3.0, 6.0)
}
pub fn smoke() -> Self {
Self::new([100, 100, 100, 200])
.with_speed(10.0, 30.0)
.with_life(1.0, 3.0)
.with_size(4.0, 8.0)
}
pub fn sparkle() -> Self {
Self::new([255, 255, 255, 255])
.with_speed(20.0, 80.0)
.with_life(0.2, 0.8)
.with_size(1.0, 3.0)
}
pub fn fire() -> Self {
Self::new([255, 100, 0, 255])
.with_speed(5.0, 20.0)
.with_life(0.5, 1.5)
.with_size(3.0, 6.0)
}
pub fn magic() -> Self {
Self::new([200, 100, 255, 255])
.with_speed(30.0, 80.0)
.with_life(0.5, 1.5)
.with_size(2.0, 5.0)
}
pub fn blood() -> Self {
Self::new([180, 0, 0, 255])
.with_speed(50.0, 150.0)
.with_life(0.3, 0.8)
.with_size(2.0, 4.0)
}
pub fn snow() -> Self {
Self::new([255, 255, 255, 200])
.with_speed(5.0, 15.0)
.with_life(2.0, 5.0)
.with_size(2.0, 4.0)
}
pub fn rain() -> Self {
Self::new([100, 150, 255, 180])
.with_speed(200.0, 300.0)
.with_life(0.5, 1.0)
.with_size(1.0, 2.0)
}
pub fn with_speed(mut self, min: f32, max: f32) -> Self {
self.min_speed = min;
self.max_speed = max;
self
}
pub fn with_life(mut self, min: f32, max: f32) -> Self {
self.min_life = min;
self.max_life = max;
self
}
pub fn with_size(mut self, min: f32, max: f32) -> Self {
self.min_size = min;
self.max_size = max;
self
}
}