use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use crate::visualizer::{BANDS, Spectrum};
const EIGHTHS: [&str; 8] = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇"];
const SLOT: u16 = 2;
const HUE_SPAN: f32 = 300.0;
pub(super) fn draw(buf: &mut Buffer, area: Rect, spectrum: &Spectrum) {
let bars = (area.width + 1) / SLOT;
if bars == 0 || area.height == 0 {
return;
}
let heights = spectrum.heights();
let rows = usize::from(area.height);
let left = area.x + (area.width - (bars * SLOT - 1)) / 2;
for bar in 0..bars {
let across = if bars == 1 {
0.5
} else {
f32::from(bar) / f32::from(bars - 1)
};
let eighths = (level(heights, across) * (rows * 8) as f32).round() as usize;
let hue = spectrum.hue() + across * HUE_SPAN;
let x = left + bar * SLOT;
for row in 0..rows {
let filled = eighths.saturating_sub(row * 8).min(8);
if filled == 0 {
break;
}
let symbol = if filled == 8 { "█" } else { EIGHTHS[filled] };
let value = 0.6 + 0.4 * (row + 1) as f32 / rows as f32;
buf[(x, area.bottom() - 1 - row as u16)]
.set_symbol(symbol)
.set_fg(hsv(hue, 0.75, value));
}
}
}
fn level(heights: &[f32; BANDS], across: f32) -> f32 {
let at = across.clamp(0.0, 1.0) * (BANDS - 1) as f32;
let i = (at.floor() as usize).min(BANDS - 2);
let t = at - i as f32;
(heights[i] * (1.0 - t) + heights[i + 1] * t).clamp(0.0, 1.0)
}
fn hsv(hue: f32, saturation: f32, value: f32) -> Color {
let hue = hue.rem_euclid(360.0) / 60.0;
let chroma = value * saturation;
let x = chroma * (1.0 - (hue % 2.0 - 1.0).abs());
let (r, g, b) = match hue as u32 {
0 => (chroma, x, 0.0),
1 => (x, chroma, 0.0),
2 => (0.0, chroma, x),
3 => (0.0, x, chroma),
4 => (x, 0.0, chroma),
_ => (chroma, 0.0, x),
};
let m = value - chroma;
let byte = |c: f32| ((c + m) * 255.0).round().clamp(0.0, 255.0) as u8;
Color::Rgb(byte(r), byte(g), byte(b))
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Instant;
#[test]
fn each_bar_is_its_bands_height_or_between_two() {
let mut heights = [0.0; BANDS];
heights[3] = 1.0;
heights[4] = 0.5;
let at = |band: usize| band as f32 / (BANDS - 1) as f32;
assert_eq!(level(&heights, at(3)), 1.0);
assert_eq!(level(&heights, at(4)), 0.5);
assert!((level(&heights, (at(3) + at(4)) / 2.0) - 0.75).abs() < 1e-5);
assert_eq!(level(&heights, at(9)), 0.0);
assert_eq!(level(&[1.0; BANDS], 1.0), 1.0, "the right edge");
}
#[test]
fn bars_are_lines_with_a_gap_between_them() {
let mut spectrum = Spectrum::default();
for _ in 0..12 {
spectrum.hear([0.0; BANDS], Instant::now());
}
let area = Rect::new(0, 0, 10, 2);
let mut buf = Buffer::empty(area);
draw(&mut buf, area, &spectrum);
let row: String = (0..10).map(|x| buf[(x, 1)].symbol()).collect();
assert_eq!(row, "█ █ █ █ █ ");
let colors: std::collections::HashSet<String> = (0..5)
.map(|i| format!("{:?}", buf[(i * 2, 1)].fg))
.collect();
assert_eq!(colors.len(), 5, "each its own color");
}
#[test]
fn colors_go_round_the_rainbow() {
assert_eq!(hsv(0.0, 1.0, 1.0), Color::Rgb(255, 0, 0));
assert_eq!(hsv(120.0, 1.0, 1.0), Color::Rgb(0, 255, 0));
assert_eq!(hsv(240.0, 1.0, 1.0), Color::Rgb(0, 0, 255));
assert_eq!(hsv(360.0 + 120.0, 1.0, 1.0), hsv(120.0, 1.0, 1.0));
assert_eq!(hsv(-120.0, 1.0, 1.0), hsv(240.0, 1.0, 1.0));
}
}