From 04fd4fc7c84e4b9734d1cb6f5220548fff6b0d4b Mon Sep 17 00:00:00 2001 From: Thomas Kolb Date: Thu, 25 May 2023 22:38:21 +0200 Subject: [PATCH] Added new fire animation (Work in progress) --- src/animation.rs | 1 + src/animation/fire.rs | 182 ++++++++++++++++ src/animation/racers.rs.orig | 396 +++++++++++++++++++++++++++++++++++ src/main.rs | 3 +- 4 files changed, 581 insertions(+), 1 deletion(-) create mode 100644 src/animation/fire.rs create mode 100644 src/animation/racers.rs.orig diff --git a/src/animation.rs b/src/animation.rs index 619c9e6..5db20b9 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -14,6 +14,7 @@ type Result = std::result::Result; pub mod particles; pub mod sparkles; pub mod racers; +pub mod fire; /////////// Error Type and Implementation //////////// diff --git a/src/animation/fire.rs b/src/animation/fire.rs new file mode 100644 index 0000000..7d06b4d --- /dev/null +++ b/src/animation/fire.rs @@ -0,0 +1,182 @@ +// vim: noet + +use crate::animation::{Color, Animation, Result}; +use crate::signal_processing::SignalProcessing; +use crate::config; + +use std::rc::Rc; +use std::cell::RefCell; +use std::collections::VecDeque; + +use rand::Rng; + +const COOLDOWN_FACTOR : f32 = 0.99995; +const MAX_ENERGY_PROPAGATION : f32 = 0.4; +const RM_ENERGY : f32 = 0.02; +const EXPONENT : f32 = 0.97; +const W_EXPONNET : f32 = 0.03; +const OVERDRIVE : f32 = 0.3; + +// A single-color flame. +// This struct contains the energy generation and propagation algorithm. +pub struct Flame +{ + energy: [ [f32; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS], +} + +impl Flame +{ + fn new() -> Flame + { + Flame { + energy: [ [0.0; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS] + } + } + + fn update(&mut self, new_energy: f32) + { + let mut rng = rand::thread_rng(); + + for strip in 0..config::NUM_STRIPS { + // Add new energy in the bottom row + self.energy[strip][0] += rng.gen::() * new_energy; + + // Remove energy at the top + self.energy[strip][config::NUM_LEDS_PER_STRIP-1] *= 1.0 - rng.gen::() * MAX_ENERGY_PROPAGATION; + + // move energy upwards + for led_out in (1..config::NUM_LEDS_PER_STRIP).rev() { + let led_in = led_out - 1; + let energy_moved = self.energy[strip][led_in] * rng.gen::() * MAX_ENERGY_PROPAGATION; + + self.energy[strip][led_in] -= energy_moved; + self.energy[strip][led_out] += energy_moved; + } + + // globally remove energy + for led in 0..config::NUM_LEDS_PER_STRIP { + if self.energy[strip][led] > RM_ENERGY { + self.energy[strip][led] -= RM_ENERGY; + } else { + self.energy[strip][led] = 0.0; + } + } + } + } + + fn print(&self) + { + println!("-----"); + for led in 0..config::NUM_LEDS_PER_STRIP { + for strip in 0..config::NUM_STRIPS { + print!("{:8.3} ", self.energy[strip][led]); + } + println!(); + } + println!("-END-"); + } + + fn get_energy(&self, strip: usize, led: usize) -> f32 + { + self.energy[strip][led] + } +} + + +// Merges multiple single-colored flames in a multi-colored one. +pub struct Fire +{ + r_flame: Flame, + g_flame: Flame, + b_flame: Flame, + w_flame: Flame, + + max_energy: Color, + + colorlists: [ [Color; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS], + + sigproc: Rc>, +} + +impl Animation for Fire +{ + fn new(sigproc: Rc>) -> Fire + { + Fire { + r_flame: Flame::new(), + g_flame: Flame::new(), + b_flame: Flame::new(), + w_flame: Flame::new(), + max_energy: Color{r: 1.0, g: 1.0, b: 1.0, w: 1.0}, + colorlists: [ [Color{r: 0.0, g: 0.0, b: 0.0, w: 0.0}; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS], + sigproc: sigproc, + } + } + + fn init(&mut self) -> Result<()> + { + Ok(()) + } + + fn periodic(&mut self) -> Result<()> + { + let sigproc = self.sigproc.borrow(); + + // extract frequency band energies + let cur_energy = Color{ + r: sigproc.get_energy_in_band( 0.0, 400.0), + g: sigproc.get_energy_in_band( 400.0, 4000.0), + b: sigproc.get_energy_in_band( 4000.0, 12000.0), + w: sigproc.get_energy_in_band(12000.0, 22000.0)}; + + // track the maximum energy with cooldown + self.max_energy.r *= COOLDOWN_FACTOR; + if cur_energy.r > self.max_energy.r { + self.max_energy.r = cur_energy.r; + } + + self.max_energy.g *= COOLDOWN_FACTOR; + if cur_energy.g > self.max_energy.g { + self.max_energy.g = cur_energy.g; + } + + self.max_energy.b *= COOLDOWN_FACTOR; + if cur_energy.b > self.max_energy.b { + self.max_energy.b = cur_energy.b; + } + + self.max_energy.w *= COOLDOWN_FACTOR; + if cur_energy.w > self.max_energy.w { + self.max_energy.w = cur_energy.w; + } + + // update the flames + self.r_flame.update((cur_energy.r / self.max_energy.r).powf(EXPONENT)); + self.g_flame.update((cur_energy.g / self.max_energy.g).powf(EXPONENT)); + self.b_flame.update((cur_energy.b / self.max_energy.b).powf(EXPONENT)); + self.w_flame.update((cur_energy.w / self.max_energy.w).powf(EXPONENT)); + + //self.r_flame.print(); + + // color rendering and post-processing + for strip in 0..config::NUM_STRIPS { + for led in 0..config::NUM_LEDS_PER_STRIP { + self.colorlists[strip][led] = Color { + r: self.r_flame.get_energy(strip, led), + g: self.g_flame.get_energy(strip, led), + b: self.b_flame.get_energy(strip, led), + w: self.w_flame.get_energy(strip, led) + }; + + self.colorlists[strip][led].limit(); + } + } + + Ok(()) + } + + fn get_colorlist(&self) -> &[ [Color; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS] + { + return &self.colorlists; + } +} diff --git a/src/animation/racers.rs.orig b/src/animation/racers.rs.orig new file mode 100644 index 0000000..95b6215 --- /dev/null +++ b/src/animation/racers.rs.orig @@ -0,0 +1,396 @@ +// vim: noet + +use crate::animation::{Color, Animation, Result}; +use crate::signal_processing::SignalProcessing; +use crate::config; + +use std::rc::Rc; +use std::cell::RefCell; + +use rand::Rng; + +const COOLDOWN_FACTOR : f32 = 0.99980; +const RGB_EXPONENT : f32 = 1.5; +const W_EXPONENT : f32 = 2.2; +const W_SCALE : f32 = 0.3; +const ENERGY_FILTER_ALPHA : f32 = 0.20; +const BRIGHTNESS_FILTER_ALPHA : f32 = 0.10; + +<<<<<<< Updated upstream +const NUM_RACERS_R : usize = 20 * config::NUM_LEDS_TOTAL / 300; +const NUM_RACERS_G : usize = 20 * config::NUM_LEDS_TOTAL / 300; +const NUM_RACERS_B : usize = 20 * config::NUM_LEDS_TOTAL / 300; +======= +const NUM_RACERS_R : usize = 20; +const NUM_RACERS_G : usize = 20; +const NUM_RACERS_B : usize = 20; +>>>>>>> Stashed changes + +const RACER_MIN_SPEED_R : f32 = 0.5 / config::FPS_ANIMATION; +const RACER_MAX_SPEED_R : f32 = 60.0 / config::FPS_ANIMATION; +const RACER_MIN_BRIGHTNESS_R : f32 = 0.01; +const RACER_MAX_BRIGHTNESS_R : f32 = 1.00; + +const RACER_MIN_SPEED_G : f32 = 0.5 / config::FPS_ANIMATION; +const RACER_MAX_SPEED_G : f32 = 60.0 / config::FPS_ANIMATION; +const RACER_MIN_BRIGHTNESS_G : f32 = 0.01; +const RACER_MAX_BRIGHTNESS_G : f32 = 1.00; + +const RACER_MIN_SPEED_B : f32 = 0.5 / config::FPS_ANIMATION; +const RACER_MAX_SPEED_B : f32 = 60.0 / config::FPS_ANIMATION; +const RACER_MIN_BRIGHTNESS_B : f32 = 0.01; +const RACER_MAX_BRIGHTNESS_B : f32 = 1.00; + +const SPEED_SCALE_RANGE : f32 = 0.10; + +fn dbg_bar(min: f32, current: f32, max: f32) +{ + const LEN: usize = 60; + let mut bar = ['.'; LEN]; + + let minpos = ((LEN as f32) * min / max) as usize; + let curpos = ((LEN as f32) * current / max) as usize; + + for idx in minpos..LEN { + bar[idx] = '-'; + } + + if curpos < LEN { + bar[curpos] = '#'; + } + + print!("{}", bar.iter().collect::()); +} + +/* + * A racer is a point of light that can move along the LED strips. + */ +struct Racer +{ + min_speed: f32, // LEDs per frame + max_speed: f32, // LEDs per frame + direction: i8, // either +1 or -1 + + min_brightness: f32, + max_brightness: f32, + + color: Color, + + pos: f32, + + brightness: f32, + flare_brightness: f32, +} + +impl Racer +{ + pub fn new(min_speed: f32, max_speed: f32, min_brightness: f32, max_brightness: f32, color: Color, start_pos: f32, direction: i8) -> Racer + { + Racer { + min_speed: min_speed, + max_speed: max_speed, + min_brightness: min_brightness, + max_brightness: max_brightness, + direction: direction, + color: color, + pos: start_pos, + brightness: min_brightness, + flare_brightness: 0.0, + } + } + + fn _pos2ledstrip(pos: i32) -> (i32, i32) + { + let strip = pos / (config::NUM_LEDS_PER_STRIP as i32); + + let mut led = pos % (config::NUM_LEDS_PER_STRIP as i32); + + if (strip % 2) == 1 { + led = (config::NUM_LEDS_PER_STRIP as i32) - led - 1; + } + + (strip, led) + } + + pub fn update(&mut self, brightness: f32, flare_brightness: f32) + { + // move along the strip + let cur_speed = self.min_speed + brightness * (self.max_speed - self.min_speed); + + self.pos += (self.direction as f32) * cur_speed; + + let maxpos = config::NUM_LEDS_TOTAL as f32; + + // if the end is reached, reverse the direction + if self.pos >= maxpos { + self.direction = -1; + self.pos = 2.0 * maxpos - self.pos; + } else if self.pos <= 0.0 { + self.direction = 1; + self.pos = -self.pos; + } + + self.brightness = brightness; + self.flare_brightness = flare_brightness; + } + + pub fn render(&self, colorlists: &mut [ [Color; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS]) + { + let brightness = self.min_brightness + self.brightness * (self.max_brightness - self.min_brightness); + + let fract_led = self.pos - self.pos.floor(); + + let led1_idx = self.pos.floor() as i32; + let led2_idx = self.pos.ceil() as i32; + + let mut color = self.color; + color.w += self.flare_brightness; + + let led1_color = color.scaled_copy((1.0 - fract_led) * brightness); + let led2_color = color.scaled_copy(fract_led * brightness); + + if led1_idx >= 0 && led1_idx < (config::NUM_LEDS_TOTAL as i32) { + let (strip, led) = Racer::_pos2ledstrip(led1_idx); + + colorlists[strip as usize][led as usize].add(&led1_color); + } + + if led2_idx >= 0 && led2_idx < (config::NUM_LEDS_TOTAL as i32) { + let (strip, led) = Racer::_pos2ledstrip(led2_idx); + + colorlists[strip as usize][led as usize].add(&led2_color); + } + } +} + +pub struct Racers +{ + max_energy : Color, + min_energy : Color, + filtered_energy : Color, + filtered_brightness : Color, + + racers_r : Vec, + racers_g : Vec, + racers_b : Vec, + + colorlists : [ [Color; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS], + + frame_count: usize, + + sigproc: Rc>, +} + +impl Animation for Racers +{ + fn new(sigproc: Rc>) -> Racers + { + Racers { + max_energy: Color{r: 1.0, g: 1.0, b: 1.0, w: 1.0}, + min_energy: Color{r: 0.0, g: 0.0, b: 0.0, w: 0.0}, + filtered_energy: Color{r: 0.0, g: 0.0, b: 0.0, w: 0.0}, + filtered_brightness: Color{r: 0.0, g: 0.0, b: 0.0, w: 0.0}, + racers_r: Vec::with_capacity(NUM_RACERS_R), + racers_g: Vec::with_capacity(NUM_RACERS_G), + racers_b: Vec::with_capacity(NUM_RACERS_B), + colorlists: [ [Color{r: 0.0, g: 0.0, b: 0.0, w: 0.0}; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS], + sigproc: sigproc, + frame_count: 0, + } + } + + fn init(&mut self) -> Result<()> + { + let mut rng = rand::thread_rng(); + + for _i in 0 .. NUM_RACERS_R { + let start_pos = rng.gen::() * (config::NUM_LEDS_TOTAL as f32); + let speed_scale = 1.0 + SPEED_SCALE_RANGE * (rng.gen::() - 0.5); + let mut dir = rng.gen::(); + if dir > 0 { + dir = 1; + } else { + dir = -1; + } + + self.racers_r.push(Racer::new( + RACER_MIN_SPEED_R * speed_scale, + RACER_MAX_SPEED_R * speed_scale, + RACER_MIN_BRIGHTNESS_R, + RACER_MAX_BRIGHTNESS_R, + Color{r: 1.0, g: 0.0, b: 0.0, w: 0.0}, + start_pos, + dir)); + } + + for _i in 0 .. NUM_RACERS_G { + let start_pos = rng.gen::() * (config::NUM_LEDS_TOTAL as f32); + let speed_scale = 1.0 + SPEED_SCALE_RANGE * (rng.gen::() - 0.5); + let mut dir = rng.gen::(); + if dir > 0 { + dir = 1; + } else { + dir = -1; + } + + self.racers_g.push(Racer::new( + RACER_MIN_SPEED_G * speed_scale, + RACER_MAX_SPEED_G * speed_scale, + RACER_MIN_BRIGHTNESS_G, + RACER_MAX_BRIGHTNESS_G, + Color{r: 0.0, g: 1.0, b: 0.0, w: 0.0}, + start_pos, + dir)); + } + + for _i in 0 .. NUM_RACERS_B { + let start_pos = rng.gen::() * (config::NUM_LEDS_TOTAL as f32); + let speed_scale = 1.0 + SPEED_SCALE_RANGE * (rng.gen::() - 0.5); + let mut dir = rng.gen::(); + if dir > 0 { + dir = 1; + } else { + dir = -1; + } + + self.racers_b.push(Racer::new( + RACER_MIN_SPEED_B * speed_scale, + RACER_MAX_SPEED_B * speed_scale, + RACER_MIN_BRIGHTNESS_B, + RACER_MAX_BRIGHTNESS_B, + Color{r: 0.0, g: 0.0, b: 1.0, w: 0.0}, + start_pos, + dir)); + } + + Ok(()) + } + + fn periodic(&mut self) -> Result<()> + { + let sigproc = self.sigproc.borrow(); + + // extract frequency band energies + let cur_energy = Color{ + r: sigproc.get_energy_in_band( 0.0, 400.0), + g: sigproc.get_energy_in_band( 400.0, 4000.0), + b: sigproc.get_energy_in_band( 4000.0, 12000.0), + w: sigproc.get_energy_in_band(12000.0, 22000.0)}; + + for i in 0..4 { + let f = self.filtered_energy.ref_by_index_mut(i).unwrap(); + let n = cur_energy.ref_by_index(i).unwrap(); + + *f = (1.0 - ENERGY_FILTER_ALPHA) * (*f) + ENERGY_FILTER_ALPHA * (*n); + } + + // track the maximum energy with cooldown + self.max_energy.r *= COOLDOWN_FACTOR; + if self.filtered_energy.r > self.max_energy.r { + self.max_energy.r = self.filtered_energy.r; + } + + self.max_energy.g *= COOLDOWN_FACTOR; + if self.filtered_energy.g > self.max_energy.g { + self.max_energy.g = self.filtered_energy.g; + } + + self.max_energy.b *= COOLDOWN_FACTOR; + if self.filtered_energy.b > self.max_energy.b { + self.max_energy.b = self.filtered_energy.b; + } + + self.max_energy.w *= COOLDOWN_FACTOR; + if self.filtered_energy.w > self.max_energy.w { + self.max_energy.w = self.filtered_energy.w; + } + + // track the minimum energy with warmup + self.min_energy.r += (1.0 - COOLDOWN_FACTOR) * (self.max_energy.r * 0.5 - self.min_energy.r); + if self.filtered_energy.r < self.min_energy.r { + self.min_energy.r = self.filtered_energy.r; + } + + self.min_energy.g += (1.0 - COOLDOWN_FACTOR) * (self.max_energy.g * 0.5 - self.min_energy.g); + if self.filtered_energy.g < self.min_energy.g { + self.min_energy.g = self.filtered_energy.g; + } + + self.min_energy.b += (1.0 - COOLDOWN_FACTOR) * (self.max_energy.b * 0.5 - self.min_energy.b); + if self.filtered_energy.b < self.min_energy.b { + self.min_energy.b = self.filtered_energy.b; + } + + self.min_energy.w += (1.0 - COOLDOWN_FACTOR) * (self.max_energy.w * 0.5 - self.min_energy.w); + if self.filtered_energy.w < self.min_energy.w { + self.min_energy.w = self.filtered_energy.w; + } + + // set all LEDs initially to black + for strip in 0..config::NUM_STRIPS { + for led in 0..config::NUM_LEDS_PER_STRIP { + //self.colorlists[strip][led].scale(FADE_FACTOR); + self.colorlists[strip][led] = Color{r: 0.0, g: 0.0, b: 0.0, w: 0.0}; + } + } + + // rescaling and normalization of the energies + let brightness = Color{ + r: ((self.filtered_energy.r - self.min_energy.r) / (self.max_energy.r - self.min_energy.r)).powf(RGB_EXPONENT), + g: ((self.filtered_energy.g - self.min_energy.g) / (self.max_energy.g - self.min_energy.g)).powf(RGB_EXPONENT), + b: ((self.filtered_energy.b - self.min_energy.b) / (self.max_energy.b - self.min_energy.b)).powf(RGB_EXPONENT), + w: ((self.filtered_energy.w - self.min_energy.w) / (self.max_energy.w - self.min_energy.w)).powf(W_EXPONENT) * W_SCALE, + }; + + // lowpass-filter brightness to reduce intensive fast flashing + for i in 0..4 { + let f = self.filtered_brightness.ref_by_index_mut(i).unwrap(); + let n = brightness.ref_by_index(i).unwrap(); + + *f = (1.0 - BRIGHTNESS_FILTER_ALPHA) * (*f) + BRIGHTNESS_FILTER_ALPHA * (*n); + } + + // update all racers + let f = &self.filtered_brightness; + self.racers_r.iter_mut().for_each(|x| x.update(f.r, f.w)); + self.racers_g.iter_mut().for_each(|x| x.update(f.g, f.w)); + self.racers_b.iter_mut().for_each(|x| x.update(f.b, f.w)); + + // render all racers + for racer in self.racers_r.iter() { + racer.render(&mut self.colorlists); + } + + for racer in self.racers_g.iter() { + racer.render(&mut self.colorlists); + } + + for racer in self.racers_b.iter() { + racer.render(&mut self.colorlists); + } + + // color post-processing + for strip in 0..config::NUM_STRIPS { + for led in 0..config::NUM_LEDS_PER_STRIP { + self.colorlists[strip][led].limit(); + } + } + + // debug stuff + self.frame_count += 1; + if self.frame_count % 100 == 0 { + println!("---"); + print!("Red "); dbg_bar(self.min_energy.r, self.filtered_brightness.r, self.max_energy.r); println!("{:11.2}", self.max_energy.r); + print!("Green "); dbg_bar(self.min_energy.g, self.filtered_brightness.g, self.max_energy.g); println!("{:11.2}", self.max_energy.g); + print!("Blue "); dbg_bar(self.min_energy.b, self.filtered_brightness.b, self.max_energy.b); println!("{:11.2}", self.max_energy.b); + print!("White "); dbg_bar(self.min_energy.w, self.filtered_brightness.w, self.max_energy.w); println!("{:11.2}", self.max_energy.w); + } + + Ok(()) + } + + fn get_colorlist(&self) -> &[ [Color; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS] + { + return &self.colorlists; + } +} diff --git a/src/main.rs b/src/main.rs index ffd3db6..2ecdad8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,7 +45,8 @@ fn main() // TODO: let the user select via the command line //let mut anim: animation::particles::Particles = animation::Animation::new(sigproc.clone()); //let mut anim: animation::sparkles::Sparkles = animation::Animation::new(sigproc.clone()); - let mut anim: animation::racers::Racers = animation::Animation::new(sigproc.clone()); + //let mut anim: animation::racers::Racers = animation::Animation::new(sigproc.clone()); + let mut anim: animation::fire::Fire = animation::Animation::new(sigproc.clone()); println!("Calling Animation::init()...");