use crate::processing::FrameData;
use crate::visualizations::render::HalfBlockCanvas;
use crate::visualizations::spectrum::ColorPalette;
use crate::visualizations::Visualization;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use std::f32::consts::PI;
pub struct RadialSpectrum {
spectrum: Vec<f32>,
rotation: f32,
rms: f32,
mirror: bool,
palette: ColorPalette,
beat_envelope: f32,
canvas: HalfBlockCanvas,
}
impl Default for RadialSpectrum {
fn default() -> Self {
Self::new()
}
}
impl RadialSpectrum {
pub fn new() -> Self {
Self {
spectrum: Vec::new(),
rotation: 0.0,
rms: 0.0,
mirror: false,
palette: ColorPalette::Neon,
beat_envelope: 0.0,
canvas: HalfBlockCanvas::new(0, 0),
}
}
}
impl Visualization for RadialSpectrum {
fn name(&self) -> &str {
"radial"
}
fn update(&mut self, frame: &FrameData) {
self.spectrum.resize(frame.spectrum.len(), 0.0);
self.spectrum.copy_from_slice(&frame.spectrum);
self.rms = frame.rms;
self.beat_envelope = frame.beat.envelope;
self.rotation += 0.005 + self.rms * 0.02 + self.beat_envelope * 0.05;
}
fn set_quantization_step(&mut self, step: u8) {
self.canvas.set_step(step);
}
fn render(&mut self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 || self.spectrum.is_empty() {
return;
}
self.canvas.resize_or_clear(area.width, area.height);
let pw = self.canvas.pixel_width() as f32;
let ph = self.canvas.pixel_height() as f32;
let cx = pw / 2.0;
let cy = ph / 2.0;
let max_len = cx.min(cy) * 0.85;
let num_rays = self.spectrum.len();
for (i, &magnitude) in self.spectrum.iter().enumerate() {
let t = i as f32 / num_rays as f32;
let angle = self.rotation + t * 2.0 * PI;
let base_color = self.palette.color(t);
let color = if let ratatui::style::Color::Rgb(r, g, b) = base_color {
let brightness = 0.6 + self.beat_envelope * 0.4;
ratatui::style::Color::Rgb(
(r as f32 * brightness) as u8,
(g as f32 * brightness) as u8,
(b as f32 * brightness) as u8,
)
} else {
base_color
};
let ray_len = magnitude * max_len * (1.0 + self.beat_envelope * 0.4);
let steps = ray_len as usize;
for s in 0..=steps {
let r = s as f32;
let px = (cx + angle.cos() * r) as usize;
let py = (cy + angle.sin() * r) as usize;
self.canvas.set(px, py, color);
if self.mirror {
let px_in = (cx - angle.cos() * r) as usize;
let py_in = (cy - angle.sin() * r) as usize;
self.canvas.set(px_in, py_in, color);
}
}
}
self.canvas.render(&area, buf);
}
fn help_keys(&self) -> &[(&str, &str)] {
&[("m", "toggle mirror"), ("p/P", "palette")]
}
fn on_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
match key.code {
crossterm::event::KeyCode::Char('m') => {
self.mirror = !self.mirror;
true
}
crossterm::event::KeyCode::Char('p') => {
let names: Vec<&str> = ColorPalette::ALL.iter().map(|p| p.name()).collect();
let idx = names
.iter()
.position(|n| *n == self.palette.name())
.unwrap_or(0);
self.palette =
ColorPalette::from_name(names[(idx + 1) % names.len()]).unwrap_or(self.palette);
true
}
crossterm::event::KeyCode::Char('P') => {
let names: Vec<&str> = ColorPalette::ALL.iter().map(|p| p.name()).collect();
let idx = names
.iter()
.position(|n| *n == self.palette.name())
.unwrap_or(0);
let prev = if idx == 0 { names.len() - 1 } else { idx - 1 };
self.palette = ColorPalette::from_name(names[prev]).unwrap_or(self.palette);
true
}
_ => false,
}
}
}