use crate::processing::FrameData;
use crate::visualizations::render::BrailleCanvas;
use crate::visualizations::Visualization;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use std::collections::VecDeque;
use std::f32::consts::PI;
const RATIOS: [(f32, f32); 8] = [
(1.0, 2.0),
(2.0, 3.0),
(3.0, 4.0),
(3.0, 2.0),
(4.0, 3.0),
(5.0, 4.0),
(3.0, 5.0),
(5.0, 6.0),
];
pub struct Lissajous {
time: f32,
ratio_index: usize,
ratio_drift_timer: f32,
frozen: bool,
phase_x: f32,
phase_y: f32,
peak: f32,
rms: f32,
trail: VecDeque<Vec<(f32, f32)>>,
color: Color,
beat_envelope: f32,
beat_fired: bool,
canvas: BrailleCanvas,
}
impl Default for Lissajous {
fn default() -> Self {
Self::new()
}
}
impl Lissajous {
pub fn new() -> Self {
Self {
time: 0.0,
ratio_index: 0,
ratio_drift_timer: 0.0,
frozen: false,
phase_x: 0.0,
phase_y: 0.0,
peak: 0.0,
rms: 0.0,
trail: VecDeque::new(),
color: Color::Rgb(0, 255, 200),
beat_envelope: 0.0,
beat_fired: false,
canvas: BrailleCanvas::new(0, 0),
}
}
fn current_ratio(&self) -> (f32, f32) {
RATIOS[self.ratio_index % RATIOS.len()]
}
}
impl Visualization for Lissajous {
fn name(&self) -> &str {
"lissajous"
}
fn update(&mut self, frame: &FrameData) {
self.peak = frame.peak;
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 {
let bass =
frame.spectrum[..band_count / 3].iter().sum::<f32>() / (band_count / 3) as f32;
let mid = frame.spectrum[band_count / 3..2 * band_count / 3]
.iter()
.sum::<f32>()
/ (band_count / 3) as f32;
let envelope_boost = 1.0 + self.beat_envelope * 1.5;
self.phase_x += bass * 0.1 * envelope_boost;
self.phase_y += mid * 0.1 * envelope_boost;
}
if !self.frozen {
self.ratio_drift_timer += 1.0;
let should_advance = self.ratio_drift_timer > 240.0
|| (self.beat_fired && self.ratio_drift_timer > 60.0);
if should_advance {
self.ratio_drift_timer = 0.0;
self.ratio_index = (self.ratio_index + 1) % RATIOS.len();
}
}
let (a, b) = self.current_ratio();
let scale = 0.2 + self.peak * 0.4 + self.beat_envelope * 0.4;
let num_points = 500;
let max_trail = 5 + (self.rms * 15.0) as usize;
let mut curve = if self.trail.len() >= max_trail {
let mut c = self.trail.pop_front().unwrap();
c.clear();
c
} else {
Vec::with_capacity(num_points)
};
curve.extend((0..num_points).map(|i| {
let t = i as f32 / num_points as f32 * 2.0 * PI;
let x = (a * t + self.phase_x + self.time).sin() * scale;
let y = (b * t + self.phase_y + self.time * 0.7).sin() * scale;
(x, y)
}));
self.trail.push_back(curve);
while self.trail.len() > max_trail {
self.trail.pop_front();
}
self.time += 0.05 + self.rms * 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 {
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 scale = cx.min(cy) * 0.9;
for curve in &self.trail {
for &(x, y) in curve {
let px = (cx + x * scale) as usize;
let py = (cy + y * scale) as usize;
self.canvas.set(px, py);
}
}
let draw_color = if let Color::Rgb(r, g, b) = self.color {
let boost = 0.2 + self.beat_envelope * 0.8;
Color::Rgb(
(r as f32 * boost) as u8,
(g as f32 * boost) as u8,
(b as f32 * boost) as u8,
)
} else {
self.color
};
self.canvas.render(&area, buf, draw_color);
}
fn help_keys(&self) -> &[(&str, &str)] {
&[("f", "freeze ratios"), ("r", "next ratio")]
}
fn on_key(&mut self, key: crossterm::event::KeyEvent) -> bool {
match key.code {
crossterm::event::KeyCode::Char('f') => {
self.frozen = !self.frozen;
true
}
crossterm::event::KeyCode::Char('r') => {
self.ratio_index = (self.ratio_index + 1) % RATIOS.len();
self.ratio_drift_timer = 0.0;
true
}
_ => false,
}
}
}