use crate::render::Cell;
use crate::style::Color;
use crate::widget::theme::DARK_GRAY;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WaveStyle {
#[default]
Line,
Filled,
Mirrored,
Bars,
Dots,
Smooth,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Interpolation {
#[default]
Linear,
Bezier,
CatmullRom,
Step,
}
#[derive(Debug, Clone)]
pub struct Waveline {
data: Vec<f64>,
style: WaveStyle,
interpolation: Interpolation,
color: Color,
gradient_color: Option<Color>,
baseline: f64,
amplitude: f64,
show_baseline: bool,
baseline_color: Color,
bg_color: Option<Color>,
height: Option<u16>,
max_points: Option<usize>,
label: Option<String>,
props: WidgetProps,
}
impl Default for Waveline {
fn default() -> Self {
Self::new(Vec::new())
}
}
impl Waveline {
pub fn new(data: Vec<f64>) -> Self {
Self {
data,
style: WaveStyle::Line,
interpolation: Interpolation::Linear,
color: Color::CYAN,
gradient_color: None,
baseline: 0.5,
amplitude: 1.0,
show_baseline: false,
baseline_color: DARK_GRAY,
bg_color: None,
height: None,
max_points: None,
label: None,
props: WidgetProps::new(),
}
}
pub fn data(mut self, data: Vec<f64>) -> Self {
self.data = data;
self
}
pub fn style(mut self, style: WaveStyle) -> Self {
self.style = style;
self
}
pub fn interpolation(mut self, method: Interpolation) -> Self {
self.interpolation = method;
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn gradient(mut self, start: Color, end: Color) -> Self {
self.color = start;
self.gradient_color = Some(end);
self
}
pub fn baseline(mut self, position: f64) -> Self {
self.baseline = position.clamp(0.0, 1.0);
self
}
pub fn amplitude(mut self, amp: f64) -> Self {
self.amplitude = amp;
self
}
pub fn show_baseline(mut self, show: bool) -> Self {
self.show_baseline = show;
self
}
pub fn baseline_color(mut self, color: Color) -> Self {
self.baseline_color = color;
self
}
pub fn bg(mut self, color: Color) -> Self {
self.bg_color = Some(color);
self
}
pub fn height(mut self, height: u16) -> Self {
self.height = Some(height);
self
}
pub fn max_points(mut self, max: usize) -> Self {
self.max_points = Some(max);
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
fn get_color(&self, ratio: f64) -> Color {
if let Some(end) = self.gradient_color {
let r = (self.color.r as f64 * (1.0 - ratio) + end.r as f64 * ratio).round() as u8;
let g = (self.color.g as f64 * (1.0 - ratio) + end.g as f64 * ratio).round() as u8;
let b = (self.color.b as f64 * (1.0 - ratio) + end.b as f64 * ratio).round() as u8;
Color::rgb(r, g, b)
} else {
self.color
}
}
fn get_interpolated_value(&self, data: &[f64], x: usize, width: usize) -> f64 {
if data.is_empty() {
return 0.0;
}
let ratio = x as f64 / (width - 1).max(1) as f64;
let idx = ratio * (data.len() - 1) as f64;
let idx_floor = idx.floor() as usize;
let idx_ceil = (idx_floor + 1).min(data.len() - 1);
let t = idx - idx_floor as f64;
match self.interpolation {
Interpolation::Linear => data[idx_floor] * (1.0 - t) + data[idx_ceil] * t,
Interpolation::Step => data[idx_floor],
Interpolation::Bezier | Interpolation::CatmullRom => {
let p0_idx = idx_floor.saturating_sub(1);
let p3_idx = (idx_ceil + 1).min(data.len() - 1);
let p0 = data[p0_idx];
let p1 = data[idx_floor];
let p2 = data[idx_ceil];
let p3 = data[p3_idx];
let t2 = t * t;
let t3 = t2 * t;
0.5 * ((2.0 * p1)
+ (-p0 + p2) * t
+ (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
+ (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3)
}
}
}
}
impl View for Waveline {
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let height = self.height.unwrap_or(area.height);
if area.width < 2 || height < 1 {
return;
}
let mut chart_y = 0u16;
let mut chart_height = height.min(area.height);
if let Some(bg) = self.bg_color {
for y in 0..chart_height {
for x in 0..area.width {
let mut cell = Cell::new(' ');
cell.bg = Some(bg);
ctx.set(x, y, cell);
}
}
}
if let Some(ref label) = self.label {
for (i, ch) in label.chars().enumerate() {
let mut cell = Cell::new(ch);
cell.fg = Some(Color::WHITE);
cell.bg = self.bg_color;
ctx.set(i as u16, chart_y, cell);
}
chart_y += 1;
chart_height = chart_height.saturating_sub(1);
}
if chart_height < 1 || self.data.is_empty() {
return;
}
let data = if let Some(max) = self.max_points {
if self.data.len() > max {
&self.data[self.data.len() - max..]
} else {
&self.data[..]
}
} else {
&self.data[..]
};
let width = area.width as usize;
if self.show_baseline {
let baseline_row = ((1.0 - self.baseline) * (chart_height - 1) as f64) as u16;
let y = chart_y + baseline_row;
for x in 0..area.width {
let mut cell = Cell::new('─');
cell.fg = Some(self.baseline_color);
ctx.set(x, y, cell);
}
}
match self.style {
WaveStyle::Line | WaveStyle::Smooth => {
for x in 0..width {
let val = (self.get_interpolated_value(data, x, width) * self.amplitude)
.clamp(-1.0, 1.0);
let y_ratio = self.baseline + val * (1.0 - self.baseline);
let y = chart_y + ((1.0 - y_ratio) * (chart_height - 1) as f64) as u16;
if y >= chart_y && y < chart_y + chart_height {
let screen_x = x as u16;
let mut cell = Cell::new('●');
cell.fg = Some(self.get_color(y_ratio));
ctx.set(screen_x, y, cell);
}
}
}
WaveStyle::Filled => {
let baseline_row = ((1.0 - self.baseline) * (chart_height - 1) as f64) as u16;
for x in 0..width {
let val = (self.get_interpolated_value(data, x, width) * self.amplitude)
.clamp(-1.0, 1.0);
let y_ratio = self.baseline + val * (1.0 - self.baseline);
let y = ((1.0 - y_ratio) * (chart_height - 1) as f64) as u16;
let screen_x = x as u16;
let (start_y, end_y) = if y <= baseline_row {
(y, baseline_row)
} else {
(baseline_row, y)
};
for dy in start_y..=end_y {
if dy < chart_height {
let screen_y = chart_y + dy;
let ch = if dy == y { '█' } else { '▓' };
let ratio = 1.0 - dy as f64 / (chart_height - 1) as f64;
let mut cell = Cell::new(ch);
cell.fg = Some(self.get_color(ratio));
ctx.set(screen_x, screen_y, cell);
}
}
}
}
WaveStyle::Mirrored => {
let center_y = chart_height / 2;
for x in 0..width {
let val = (self.get_interpolated_value(data, x, width).abs() * self.amplitude)
.clamp(0.0, 1.0);
let half_height = (val * center_y as f64) as u16;
let screen_x = x as u16;
for dy in 0..=half_height {
let screen_y = chart_y + center_y.saturating_sub(dy);
if screen_y >= chart_y {
let intensity = 1.0 - dy as f64 / center_y as f64;
let ch = if dy == half_height { '▀' } else { '█' };
let mut cell = Cell::new(ch);
cell.fg = Some(self.get_color(0.5 + intensity * 0.5));
ctx.set(screen_x, screen_y, cell);
}
}
for dy in 0..=half_height {
let screen_y = chart_y + center_y + dy;
if screen_y < chart_y + chart_height {
let intensity = 1.0 - dy as f64 / center_y as f64;
let ch = if dy == half_height { '▄' } else { '█' };
let mut cell = Cell::new(ch);
cell.fg = Some(self.get_color(0.5 + intensity * 0.5));
ctx.set(screen_x, screen_y, cell);
}
}
}
}
WaveStyle::Bars => {
let baseline_row = ((1.0 - self.baseline) * (chart_height - 1) as f64) as u16;
let bar_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
for x in 0..width {
let val = (self.get_interpolated_value(data, x, width) * self.amplitude)
.clamp(-1.0, 1.0);
let y_ratio = self.baseline + val * (1.0 - self.baseline);
let target_y = ((1.0 - y_ratio) * (chart_height - 1) as f64) as u16;
let screen_x = x as u16;
if val >= 0.0 {
for dy in target_y..=baseline_row {
if dy < chart_height {
let screen_y = chart_y + dy;
let ch = if dy == target_y {
let frac = (y_ratio * 8.0).fract();
bar_chars[(frac * 8.0) as usize % 8]
} else {
'█'
};
let mut cell = Cell::new(ch);
cell.fg = Some(self.get_color(y_ratio));
ctx.set(screen_x, screen_y, cell);
}
}
} else {
for dy in baseline_row..=target_y {
if dy < chart_height {
let screen_y = chart_y + dy;
let ch = if dy == target_y {
let frac = 1.0 - (y_ratio * 8.0).fract();
bar_chars[(frac * 8.0) as usize % 8]
} else {
'█'
};
let mut cell = Cell::new(ch);
cell.fg = Some(self.get_color(y_ratio));
ctx.set(screen_x, screen_y, cell);
}
}
}
}
}
WaveStyle::Dots => {
for x in 0..width {
let val = (self.get_interpolated_value(data, x, width) * self.amplitude)
.clamp(-1.0, 1.0);
let y_ratio = self.baseline + val * (1.0 - self.baseline);
let y = chart_y + ((1.0 - y_ratio) * (chart_height - 1) as f64) as u16;
if y >= chart_y && y < chart_y + chart_height {
let screen_x = x as u16;
let mut cell = Cell::new('⣿');
cell.fg = Some(self.get_color(y_ratio));
ctx.set(screen_x, y, cell);
}
}
}
}
}
crate::impl_view_meta!("Waveline");
}
impl_styled_view!(Waveline);
impl_props_builders!(Waveline);
pub fn waveline(data: Vec<f64>) -> Waveline {
Waveline::new(data)
}
pub fn audio_waveform(samples: Vec<f64>) -> Waveline {
Waveline::new(samples)
.style(WaveStyle::Mirrored)
.gradient(Color::CYAN, Color::BLUE)
}
pub fn signal_wave(data: Vec<f64>) -> Waveline {
Waveline::new(data)
.style(WaveStyle::Line)
.interpolation(Interpolation::CatmullRom)
.color(Color::GREEN)
.show_baseline(true)
}
pub fn area_wave(data: Vec<f64>) -> Waveline {
Waveline::new(data)
.style(WaveStyle::Filled)
.color(Color::MAGENTA)
.baseline(1.0)
}
pub fn spectrum(data: Vec<f64>) -> Waveline {
Waveline::new(data)
.style(WaveStyle::Bars)
.color(Color::YELLOW)
.baseline(1.0)
}
pub fn sine_wave(samples: usize, frequency: f64, amplitude: f64) -> Vec<f64> {
(0..samples)
.map(|i| {
let t = i as f64 / samples as f64 * std::f64::consts::PI * 2.0 * frequency;
t.sin() * amplitude
})
.collect()
}
pub fn square_wave(samples: usize, frequency: f64, amplitude: f64) -> Vec<f64> {
(0..samples)
.map(|i| {
let t = i as f64 / samples as f64 * frequency;
if t.fract() < 0.5 {
amplitude
} else {
-amplitude
}
})
.collect()
}
pub fn sawtooth_wave(samples: usize, frequency: f64, amplitude: f64) -> Vec<f64> {
(0..samples)
.map(|i| {
let t = i as f64 / samples as f64 * frequency;
(t.fract() * 2.0 - 1.0) * amplitude
})
.collect()
}