New animation: sparkles

This commit is contained in:
Thomas Kolb 2021-04-09 17:38:42 +02:00
parent c1202bb41c
commit d01c5b459c
3 changed files with 308 additions and 4 deletions

View file

@ -61,6 +61,22 @@ impl Color
self.w *= factor;
}
pub fn scaled_copy(&self, factor: f32) -> Color
{
let mut c = *self;
c.scale(factor);
c
}
pub fn add(&mut self, other: &Color)
{
self.r += other.r;
self.g += other.g;
self.b += other.b;
self.w += other.w;
}
fn _limit_component(c: &mut f32)
{
if *c > 1.0 {
@ -212,8 +228,6 @@ pub mod particles
let mut rng = rand::thread_rng();
// FIXME: how to call this code for green, blue and white as well without too much
// duplication?
for coloridx in 0..=3 {
let new_energy_ref = new_energy.ref_by_index(coloridx).unwrap();
let rem_energy_ref = remaining_energy.ref_by_index_mut(coloridx).unwrap();
@ -256,3 +270,288 @@ pub mod particles
}
}
}
pub mod sparkles
{
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 RGB_EXPONENT : f32 = 1.5;
const W_EXPONENT : f32 = 2.2;
const FADE_FACTOR : f32 = 0.97;
const AVG_LEDS_ACTIVATED : f32 = 0.03;
const WHITE_EXTRA_SCALE : f32 = 0.3;
const CONDENSATION_FACTOR : f32 = 5.0;
const SPARK_FADE_STEP : f32 = 2.500 / config::FPS_ANIMATION;
const SPARK_VSPEED_MIDS : f32 = 1.000 * config::NUM_LEDS_PER_STRIP as f32 / config::FPS_ANIMATION;
const SPARK_VSPEED_HIGHS : f32 = 0.800 * config::NUM_LEDS_PER_STRIP as f32 / config::FPS_ANIMATION;
const SPARK_VSPEED_XHIGHS : f32 = 0.500 * config::NUM_LEDS_PER_STRIP as f32 / config::FPS_ANIMATION;
/*
* A spark is a point of light that can move vertically along the LED strips.
*/
struct Spark
{
pub vspeed: f32, // LEDs per frame
pub brightness: f32,
pub color: Color,
strip: u16,
led: f32,
has_expired: bool,
}
impl Spark
{
pub fn new(vspeed: f32, brightness: f32, color: Color, strip: u16, led: f32) -> Spark
{
Spark {
vspeed: vspeed,
brightness: brightness,
color: color,
strip: strip,
led: led,
has_expired: false
}
}
pub fn update(&mut self)
{
if self.has_expired {
return;
}
self.led += self.vspeed;
self.brightness -= SPARK_FADE_STEP;
if (self.led >= config::NUM_LEDS_PER_STRIP as f32) || (self.led <= -1.0) {
// moved outside of the LED array -> no need to update this any more
self.has_expired = true;
}
if self.brightness <= 0.0 {
// moved outside of the LED array -> no need to update this any more
self.has_expired = true;
}
}
pub fn has_expired(&self) -> bool
{
self.has_expired
}
pub fn render(&self, colorlists: &mut [ [Color; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS])
{
if self.has_expired {
// do not render if this Spark has expired
return;
}
let fract_led = self.led - self.led.floor();
let led1_idx = self.led.floor() as i32;
let led2_idx = self.led.ceil() as usize;
let led1_color = self.color.scaled_copy(fract_led * self.brightness);
let led2_color = self.color.scaled_copy((1.0 - fract_led) * self.brightness);
if led1_idx >= 0 {
colorlists[self.strip as usize][led1_idx as usize].add(&led1_color);
}
if led2_idx < config::NUM_LEDS_PER_STRIP {
colorlists[self.strip as usize][led2_idx as usize].add(&led2_color);
}
}
}
pub struct Sparkles
{
max_energy : Color,
sparks : VecDeque<Spark>,
colorlists : [ [Color; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS],
sigproc: Rc<RefCell<SignalProcessing>>,
}
impl Animation for Sparkles
{
fn new(sigproc: Rc<RefCell<SignalProcessing>>) -> Sparkles
{
Sparkles {
max_energy: Color{r: 1.0, g: 1.0, b: 1.0, w: 1.0},
sparks: VecDeque::with_capacity(1024),
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;
}
// fade all LEDs towards black
for strip in 0..config::NUM_STRIPS {
for led in 0..config::NUM_LEDS_PER_STRIP {
self.colorlists[strip][led].scale(FADE_FACTOR);
}
}
// distribute the energy for each color
let new_energy = Color{
r: (cur_energy.r / self.max_energy.r).powf(RGB_EXPONENT),
g: (cur_energy.g / self.max_energy.g).powf(RGB_EXPONENT),
b: (cur_energy.b / self.max_energy.b).powf(RGB_EXPONENT),
w: (cur_energy.w / self.max_energy.w).powf(W_EXPONENT),
};
let mut remaining_energy = new_energy.r;
remaining_energy *= AVG_LEDS_ACTIVATED * config::NUM_LEDS_TOTAL as f32;
let mut rng = rand::thread_rng();
// Red (bass) uses exactly the same algorithm as for the “Particles” animation.
while remaining_energy > 0.0 {
let mut rnd_energy = rng.gen::<f32>() * new_energy.r * CONDENSATION_FACTOR;
let rnd_strip = rng.gen_range(0..config::NUM_STRIPS);
let rnd_led = rng.gen_range(0..config::NUM_LEDS_PER_STRIP);
if rnd_energy > remaining_energy {
rnd_energy = remaining_energy;
remaining_energy = 0.0;
} else {
remaining_energy -= rnd_energy;
}
self.colorlists[rnd_strip][rnd_led].r += rnd_energy;
}
// update all existing sparks
self.sparks.iter_mut().for_each(|x| x.update());
// Create green sparks for middle frequencies.
// They originate in the center and can go both up and down from there.
self.sparks.push_back(Spark::new(
match rng.gen::<bool>() {
true => SPARK_VSPEED_MIDS,
false => -SPARK_VSPEED_MIDS,
},
new_energy.g,
Color{r: 0.0, g: 1.0, b: 0.0, w: 0.0},
rng.gen_range(0..config::NUM_STRIPS) as u16,
(config::NUM_LEDS_PER_STRIP as f32 / 2.0) - 0.5));
// Create blue sparks for high frequencies.
// They originate either in the top, moving down, or in the bottom, moving up
{
let start_from_top = rng.gen::<bool>();
let start_led = match start_from_top {
true => config::NUM_LEDS_PER_STRIP-1,
false => 0} as f32;
let vspeed = match start_from_top {
true => -SPARK_VSPEED_HIGHS,
false => SPARK_VSPEED_HIGHS};
self.sparks.push_back(Spark::new(
vspeed,
new_energy.b,
Color{r: 0.0, g: 0.0, b: 1.0, w: 0.0},
rng.gen_range(0..config::NUM_STRIPS) as u16,
start_led));
}
// Create white sparks for very high frequencies.
// They originate either in the top, moving down, or in the bottom, moving up
{
let start_from_top = rng.gen::<bool>();
let start_led = match start_from_top {
true => config::NUM_LEDS_PER_STRIP-1,
false => 0} as f32;
let vspeed = match start_from_top {
true => -SPARK_VSPEED_XHIGHS,
false => SPARK_VSPEED_XHIGHS};
self.sparks.push_back(Spark::new(
vspeed,
new_energy.w * WHITE_EXTRA_SCALE,
Color{r: 0.0, g: 0.0, b: 0.0, w: 1.0},
rng.gen_range(0..config::NUM_STRIPS) as u16,
start_led));
}
// remove expired sparks in the beginning of the deque
while self.sparks.front().map_or(false, |s| s.has_expired()) {
self.sparks.pop_front();
}
// render all remaining sparks
for spark in self.sparks.iter() {
spark.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();
}
}
Ok(())
}
fn get_colorlist(&self) -> &[ [Color; config::NUM_LEDS_PER_STRIP]; config::NUM_STRIPS]
{
return &self.colorlists;
}
}
}

View file

@ -13,3 +13,6 @@ pub const NUM_LEDS_TOTAL: usize = NUM_STRIPS * NUM_LEDS_PER_STRIP;
// network configuration
pub const UDP_SERVER_ADDR: &str = "192.168.23.118:2703";
pub const FPS_ANIMATION: f32 = SAMP_RATE / SAMPLES_PER_UPDATE as f32;
pub const FPS_LEDS: f32 = 60.0;

View file

@ -68,7 +68,9 @@ fn main()
println!("Contructing Animation...");
let mut anim: animation::particles::Particles = animation::Animation::new(sigproc.clone());
// 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());
println!("Calling Animation::init()...");
@ -79,7 +81,7 @@ fn main()
// Timing setup
let block_period = Duration::from_nanos((0.95 * (config::SAMPLES_PER_UPDATE as f32) * 1e9 / config::SAMP_RATE) as u64);
let send_period = Duration::from_nanos(1000000000 / 60);
let send_period = Duration::from_nanos((1000000000.0 / config::FPS_LEDS) as u64);
let max_lag = 5*send_period;