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;
#[derive(Clone, Copy, PartialEq)]
enum TunnelShape {
Circle,
Hexagon,
Square,
}
impl TunnelShape {
fn next(self) -> Self {
match self {
TunnelShape::Circle => TunnelShape::Hexagon,
TunnelShape::Hexagon => TunnelShape::Square,
TunnelShape::Square => TunnelShape::Circle,
}
}
#[allow(dead_code)]
fn name(self) -> &'static str {
match self {
TunnelShape::Circle => "circle",
TunnelShape::Hexagon => "hexagon",
TunnelShape::Square => "square",
}
}
}
struct Ring {
radius: f32,
color_t: f32,
}
pub struct Tunnel {
rings: Vec<Ring>,
shape: TunnelShape,
palette: ColorPalette,
time: f32,
rms: f32,
bass: f32,
treble: f32,
spawn_timer: f32,
beat_envelope: f32,
beat_fired: bool,
canvas: HalfBlockCanvas,
}
impl Default for Tunnel {
fn default() -> Self {
Self::new()
}
}
impl Tunnel {
pub fn new() -> Self {
Self {
rings: Vec::new(),
shape: TunnelShape::Circle,
palette: ColorPalette::Synthwave,
time: 0.0,
rms: 0.0,
bass: 0.0,
treble: 0.0,
spawn_timer: 0.0,
beat_envelope: 0.0,
beat_fired: false,
canvas: HalfBlockCanvas::new(0, 0),
}
}
fn shape_points(&self, cx: f32, cy: f32, radius: f32) -> Vec<(f32, f32)> {
let n = match self.shape {
TunnelShape::Circle => 48,
TunnelShape::Hexagon => 6,
TunnelShape::Square => 4,
};
let angle_offset = match self.shape {
TunnelShape::Square => PI / 4.0,
_ => 0.0,
};
(0..=n)
.map(|i| {
let angle = angle_offset + 2.0 * PI * i as f32 / n as f32;
(cx + angle.cos() * radius, cy + angle.sin() * radius)
})
.collect()
}
}
impl Visualization for Tunnel {
fn name(&self) -> &str {
"tunnel"
}
fn update(&mut self, frame: &FrameData) {
self.rms = frame.rms;
self.beat_envelope = frame.beat.envelope;
self.beat_fired = frame.beat.beat;
let band_count = frame.spectrum.len();
if band_count > 0 {
self.bass =
frame.spectrum[..band_count / 4].iter().sum::<f32>() / (band_count / 4) as f32;
self.treble =
frame.spectrum[3 * band_count / 4..].iter().sum::<f32>() / (band_count / 4) as f32;
}
let speed = 0.01 + self.rms * 0.03 + self.beat_envelope * 0.06;
for ring in &mut self.rings {
ring.radius += speed;
}
self.rings.retain(|r| r.radius < 1.5);
self.spawn_timer += 1.0 + self.bass * 3.0;
let spawn_interval = 8.0 - self.rms * 4.0; while self.spawn_timer >= spawn_interval {
self.spawn_timer -= spawn_interval;
self.rings.push(Ring {
radius: 0.01,
color_t: self.treble.clamp(0.0, 1.0),
});
}
if self.beat_fired {
for i in 0..6 {
self.rings.push(Ring {
radius: 0.01 + i as f32 * 0.03,
color_t: (self.treble + i as f32 * 0.15).fract(),
});
}
}
self.time += 0.016;
}
fn set_quantization_step(&mut self, step: u8) {
self.canvas.set_step(step);
}
fn heavy_rendering(&self) -> bool {
true
}
fn render(&mut self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 {
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_radius = cx.min(cy);
for ring in &self.rings {
let r = ring.radius * max_radius;
let envelope_boost = 0.5 + self.beat_envelope * 0.5;
let brightness =
((1.0 - ring.radius) * envelope_boost + self.beat_envelope * 0.3).clamp(0.0, 1.0);
let color = self.palette.color(ring.color_t);
let color = if let ratatui::style::Color::Rgb(cr, cg, cb) = color {
ratatui::style::Color::Rgb(
(cr as f32 * brightness) as u8,
(cg as f32 * brightness) as u8,
(cb as f32 * brightness) as u8,
)
} else {
color
};
let points = self.shape_points(cx, cy, r);
for window in points.windows(2) {
let (x0, y0) = window[0];
let (x1, y1) = window[1];
let dx = (x1 - x0).abs();
let dy = (y1 - y0).abs();
let steps = (dx.max(dy) as usize).max(1);
for s in 0..=steps {
let t = s as f32 / steps as f32;
let px = (x0 + (x1 - x0) * t) as usize;
let py = (y0 + (y1 - y0) * t) as usize;
self.canvas.set(px, py, color);
}
}
}
self.canvas.render(&area, buf);
}
fn help_keys(&self) -> &[(&str, &str)] {
&[("s", "cycle shape"), ("p/P", "palette")]
}
fn on_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
match key.code {
crossterm::event::KeyCode::Char('s') => {
self.shape = self.shape.next();
true
}
crossterm::event::KeyCode::Char('p') => {
self.palette = ColorPalette::from_name(
ColorPalette::ALL
.iter()
.cycle()
.skip_while(|p| p.name() != self.palette.name())
.nth(1)
.map(|p| p.name())
.unwrap_or("neon"),
)
.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_idx = if idx == 0 { names.len() - 1 } else { idx - 1 };
self.palette = ColorPalette::from_name(names[prev_idx]).unwrap_or(self.palette);
true
}
_ => false,
}
}
}