mod ascii;
mod block;
mod braille;
mod core;
mod density;
mod dot;
mod transform;
use crate::color::CanvasColor;
pub use ascii::AsciiCanvas;
pub use block::BlockCanvas;
pub use braille::BrailleCanvas;
pub(crate) use core::CanvasCore;
pub use density::DensityCanvas;
pub use dot::DotCanvas;
pub use transform::{AxisTransform, Scale, Transform2D};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CanvasType {
Ascii,
Block,
Braille,
Density,
Dot,
}
#[must_use]
pub const fn canvas_types() -> &'static [CanvasType] {
&[
CanvasType::Ascii,
CanvasType::Block,
CanvasType::Braille,
CanvasType::Density,
CanvasType::Dot,
]
}
pub trait Canvas {
fn pixel(&mut self, x: usize, y: usize, color: CanvasColor);
fn glyph_at(&self, col: usize, row: usize) -> char;
fn color_at(&self, col: usize, row: usize) -> CanvasColor;
fn char_width(&self) -> usize;
fn char_height(&self) -> usize;
fn pixel_width(&self) -> usize;
fn pixel_height(&self) -> usize;
fn transform(&self) -> &Transform2D;
fn transform_mut(&mut self) -> &mut Transform2D;
fn point(&mut self, x: f64, y: f64, color: CanvasColor) {
let Some(pixel_x) = self.transform().data_to_pixel_x(x) else {
return;
};
let Some(pixel_y) = self.transform().data_to_pixel_y(y) else {
return;
};
let Some(clamped_x) = clamp_pixel_coord(pixel_x, self.pixel_width()) else {
return;
};
let Some(clamped_y) = clamp_pixel_coord(pixel_y, self.pixel_height()) else {
return;
};
self.pixel(clamped_x, clamped_y, color);
}
fn points(&mut self, xs: &[f64], ys: &[f64], color: CanvasColor) {
if xs.len() != ys.len() {
return;
}
for (&x, &y) in xs.iter().zip(ys) {
self.point(x, y, color);
}
}
fn line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, color: CanvasColor) {
let Some(start_x) = self.transform().data_to_pixel_x(x1) else {
return;
};
let Some(start_y) = self.transform().data_to_pixel_y(y1) else {
return;
};
let Some(end_x) = self.transform().data_to_pixel_x(x2) else {
return;
};
let Some(end_y) = self.transform().data_to_pixel_y(y2) else {
return;
};
if segment_is_outside_bounds(start_x, end_x, self.pixel_width())
|| segment_is_outside_bounds(start_y, end_y, self.pixel_height())
{
return;
}
let dx = end_x - start_x;
let dy = end_y - start_y;
let steps_u32 = dx.unsigned_abs().max(dy.unsigned_abs());
let Ok(steps) = usize::try_from(steps_u32) else {
return;
};
if steps == 0 {
if let (Some(in_bounds_x), Some(in_bounds_y)) = (
in_bounds_pixel_coord(start_x, self.pixel_width()),
in_bounds_pixel_coord(start_y, self.pixel_height()),
) {
self.pixel(in_bounds_x, in_bounds_y, color);
}
return;
}
let step_denominator = i64::from(steps_u32);
for step in 0..=steps {
let Ok(step_i64) = i64::try_from(step) else {
continue;
};
let step_dx = floor_div_i64(i64::from(dx) * step_i64, step_denominator);
let step_dy = floor_div_i64(i64::from(dy) * step_i64, step_denominator);
let Ok(pixel_x) = i32::try_from(i64::from(start_x) + step_dx) else {
continue;
};
let Ok(pixel_y) = i32::try_from(i64::from(start_y) + step_dy) else {
continue;
};
if let (Some(in_bounds_x), Some(in_bounds_y)) = (
in_bounds_pixel_coord(pixel_x, self.pixel_width()),
in_bounds_pixel_coord(pixel_y, self.pixel_height()),
) {
self.pixel(in_bounds_x, in_bounds_y, color);
}
}
}
fn lines(&mut self, xs: &[f64], ys: &[f64], color: CanvasColor) {
if xs.len() != ys.len() {
return;
}
for (x_pair, y_pair) in xs.windows(2).zip(ys.windows(2)) {
let &[x1, x2] = x_pair else {
continue;
};
let &[y1, y2] = y_pair else {
continue;
};
self.line(x1, y1, x2, y2, color);
}
}
fn row_cells(&self, row: usize) -> impl Iterator<Item = (char, CanvasColor)> + '_ {
(0..self.char_width()).map(move |col| (self.glyph_at(col, row), self.color_at(col, row)))
}
}
fn clamp_pixel_coord(coord: i32, upper: usize) -> Option<usize> {
if upper == 0 {
return None;
}
let upper_i64 = i64::try_from(upper).ok()?;
let clamped = i64::from(coord).clamp(0, upper_i64);
let adjusted = if clamped == upper_i64 {
clamped - 1
} else {
clamped
};
usize::try_from(adjusted).ok()
}
fn in_bounds_pixel_coord(coord: i32, upper: usize) -> Option<usize> {
if upper == 0 {
return None;
}
if coord < 0 {
return None;
}
let upper_i64 = i64::try_from(upper).ok()?;
let coord_i64 = i64::from(coord);
if coord_i64 > upper_i64 {
return None;
}
let adjusted = if coord_i64 == upper_i64 {
coord_i64 - 1
} else {
coord_i64
};
usize::try_from(adjusted).ok()
}
fn segment_is_outside_bounds(start: i32, end: i32, upper: usize) -> bool {
let Ok(upper_bound) = i32::try_from(upper) else {
return false;
};
(start < 0 && end < 0) || (start > upper_bound && end > upper_bound)
}
fn floor_div_i64(numerator: i64, denominator: i64) -> i64 {
let quotient = numerator / denominator;
let remainder = numerator % denominator;
if remainder < 0 {
quotient - 1
} else {
quotient
}
}
#[cfg(test)]
pub(crate) fn write_colored_cell(
out: &mut String,
glyph: char,
color: CanvasColor,
enable_color: bool,
) {
use std::fmt::Write as _;
if !enable_color {
out.push(glyph);
return;
}
let _ = if color == CanvasColor::NORMAL {
write!(out, "\x1b[0m{glyph}")
} else {
write!(out, "\x1b[{}m{glyph}\x1b[39m", canvas_color_fg_code(color))
};
}
#[cfg(test)]
fn canvas_color_fg_code(color: CanvasColor) -> u8 {
match color.as_u8() {
1 => 34,
2 => 31,
3 => 35,
4 => 32,
5 => 36,
6 => 33,
7 => 37,
_ => 39,
}
}
#[cfg(test)]
const X1_DATA: &str = r"
0.226582 0.504629 0.933372 0.522172 0.505208
0.0997825 0.0443222 0.722906 0.812814 0.245457
0.11202 0.000341996 0.380001 0.505277 0.841177
0.326561 0.810857 0.850456 0.478053 0.179066
";
#[cfg(test)]
const Y1_DATA: &str = r"
0.44701 0.219519 0.677372 0.746407 0.735727
0.574789 0.538086 0.848053 0.110351 0.796793
0.987618 0.801862 0.365172 0.469959 0.306373
0.704691 0.540434 0.405842 0.805117 0.014829
";
#[cfg(test)]
const X2_DATA: &str = r"
0.486366 0.911547 0.900818 0.641951 0.546221
0.036135 0.931522 0.196704 0.710775 0.969291
0.32546 0.632833 0.815576 0.85278 0.577286
0.887004 0.231596 0.288337 0.881386 0.0952668
0.609881 0.393795 0.84808 0.453653 0.746048
0.924725 0.100012 0.754283 0.769802 0.997368
0.0791693 0.234334 0.361207 0.1037 0.713739
0.510725 0.649145 0.233949 0.812092 0.914384
0.106925 0.570467 0.594956 0.118498 0.699827
0.380363 0.843282 0.28761 0.541469 0.568466
";
#[cfg(test)]
const Y2_DATA: &str = r"
0.417777 0.774845 0.00230619 0.907031 0.971138
0.0524795 0.957415 0.328894 0.530493 0.193359
0.768422 0.783238 0.607772 0.0261113 0.0849032
0.461164 0.613067 0.785021 0.988875 0.131524
0.0657328 0.466453 0.560878 0.925428 0.238691
0.692385 0.203687 0.441146 0.229352 0.332706
0.113543 0.537354 0.965718 0.437026 0.960983
0.372294 0.0226533 0.593514 0.657878 0.450696
0.436169 0.445539 0.0534673 0.0882236 0.361795
0.182991 0.156862 0.734805 0.166076 0.1172
";
#[cfg(test)]
fn parse_points(data: &str) -> Vec<f64> {
data.split_ascii_whitespace()
.map(|value| {
value
.parse::<f64>()
.unwrap_or_else(|_| panic!("invalid fixture point value: {value}"))
})
.collect()
}
#[cfg(test)]
pub(crate) fn draw_reference_canvas_scene<C: Canvas>(canvas: &mut C) {
let x1 = parse_points(X1_DATA);
let y1 = parse_points(Y1_DATA);
let x2 = parse_points(X2_DATA);
let y2 = parse_points(Y2_DATA);
canvas.line(0.0, 0.0, 1.0, 1.0, CanvasColor::BLUE);
canvas.points(&x1, &y1, CanvasColor::WHITE);
canvas.pixel(2, 4, CanvasColor::CYAN);
canvas.points(&x2, &y2, CanvasColor::RED);
canvas.line(0.0, 1.0, 0.5, 0.0, CanvasColor::GREEN);
canvas.point(0.05, 0.3, CanvasColor::CYAN);
canvas.lines(&[1.0, 2.0], &[2.0, 1.0], CanvasColor::NORMAL);
canvas.line(0.0, 0.0, 9.0, 9999.0, CanvasColor::YELLOW);
canvas.line(0.0, 0.0, 1.0, 1.0, CanvasColor::BLUE);
canvas.line(0.1, 0.7, 0.9, 0.6, CanvasColor::RED);
}
#[cfg(test)]
pub(crate) fn render_canvas_show<C: Canvas>(canvas: C, color: bool) -> String {
use std::fmt::Write as _;
use crate::color::{NamedColor, TermColor};
use crate::plot::Plot;
use crate::render::build_rendered_plot;
let mut plot = Plot::new(canvas);
plot.margin = 0;
plot.padding = 0;
let rendered = build_rendered_plot(&plot);
let mut out = String::new();
for (row_index, row) in rendered.rows().iter().enumerate() {
let is_top_or_bottom = row_index == 0 || row_index + 1 == rendered.rows().len();
if color && is_top_or_bottom {
let border = row.iter().map(|cell| cell.glyph).collect::<String>();
let _ = write!(out, "\x1b[90m{border}\x1b[39m");
if row_index + 1 < rendered.rows().len() {
out.push('\n');
}
continue;
}
for (col_index, cell) in row.iter().enumerate() {
if !color {
out.push(cell.glyph);
continue;
}
let is_side_border = col_index == 0 || col_index + 1 == row.len();
if is_top_or_bottom || is_side_border {
let _ = write!(out, "\x1b[90m{}\x1b[39m", cell.glyph);
continue;
}
let code = match cell.color {
Some(TermColor::Named(NamedColor::Black)) => Some(30),
Some(TermColor::Named(NamedColor::Red)) => Some(31),
Some(TermColor::Named(NamedColor::Green)) => Some(32),
Some(TermColor::Named(NamedColor::Yellow)) => Some(33),
Some(TermColor::Named(NamedColor::Blue)) => Some(34),
Some(TermColor::Named(NamedColor::Magenta)) => Some(35),
Some(TermColor::Named(NamedColor::Cyan)) => Some(36),
Some(TermColor::Named(NamedColor::White)) => Some(37),
_ => None,
};
if let Some(code) = code {
let _ = write!(out, "\x1b[{code}m{}\x1b[39m", cell.glyph);
} else {
let _ = write!(out, "\x1b[0m{}", cell.glyph);
}
}
if row_index + 1 < rendered.rows().len() {
out.push('\n');
}
}
out
}
#[cfg(test)]
mod tests {
use super::{AxisTransform, Canvas, CanvasColor, Transform2D};
use crate::canvas::Scale;
#[derive(Debug)]
struct RecordingCanvas {
char_width: usize,
char_height: usize,
pixel_width: usize,
pixel_height: usize,
transform: Transform2D,
hits: Vec<(usize, usize, CanvasColor)>,
}
#[derive(Debug)]
struct RowCanvas {
transform: Transform2D,
glyphs: [char; 6],
colors: [CanvasColor; 6],
}
impl RowCanvas {
fn new(transform: Transform2D) -> Self {
Self {
transform,
glyphs: ['a', 'b', 'c', 'd', 'e', 'f'],
colors: [
CanvasColor::BLUE,
CanvasColor::GREEN,
CanvasColor::RED,
CanvasColor::CYAN,
CanvasColor::MAGENTA,
CanvasColor::YELLOW,
],
}
}
fn index(col: usize, row: usize) -> usize {
row * 3 + col
}
}
impl Canvas for RowCanvas {
fn pixel(&mut self, _x: usize, _y: usize, _color: CanvasColor) {}
fn glyph_at(&self, col: usize, row: usize) -> char {
self.glyphs[Self::index(col, row)]
}
fn color_at(&self, col: usize, row: usize) -> CanvasColor {
self.colors[Self::index(col, row)]
}
fn char_width(&self) -> usize {
3
}
fn char_height(&self) -> usize {
2
}
fn pixel_width(&self) -> usize {
3
}
fn pixel_height(&self) -> usize {
2
}
fn transform(&self) -> &Transform2D {
&self.transform
}
fn transform_mut(&mut self) -> &mut Transform2D {
&mut self.transform
}
}
impl RecordingCanvas {
fn new(transform: Transform2D) -> Self {
Self {
char_width: 10,
char_height: 10,
pixel_width: 10,
pixel_height: 10,
transform,
hits: Vec::new(),
}
}
}
impl Canvas for RecordingCanvas {
fn pixel(&mut self, x: usize, y: usize, color: CanvasColor) {
self.hits.push((x, y, color));
}
fn glyph_at(&self, _col: usize, _row: usize) -> char {
' '
}
fn color_at(&self, _col: usize, _row: usize) -> CanvasColor {
CanvasColor::NORMAL
}
fn char_width(&self) -> usize {
self.char_width
}
fn char_height(&self) -> usize {
self.char_height
}
fn pixel_width(&self) -> usize {
self.pixel_width
}
fn pixel_height(&self) -> usize {
self.pixel_height
}
fn transform(&self) -> &Transform2D {
&self.transform
}
fn transform_mut(&mut self) -> &mut Transform2D {
&mut self.transform
}
}
fn identity_transform() -> Transform2D {
let x = AxisTransform::new(0.0, 10.0, 10, Scale::Identity, false)
.unwrap_or_else(|| unreachable!("valid transform"));
let y = AxisTransform::new(0.0, 10.0, 10, Scale::Identity, false)
.unwrap_or_else(|| unreachable!("valid transform"));
Transform2D::new(x, y)
}
#[test]
fn dda_line_draws_expected_horizontal_vertical_and_diagonal_points() {
let mut horizontal = RecordingCanvas::new(identity_transform());
horizontal.line(1.0, 1.0, 9.0, 1.0, CanvasColor::BLUE);
let expected_horizontal: Vec<_> = (1..=9).map(|x| (x, 1, CanvasColor::BLUE)).collect();
assert_eq!(horizontal.hits, expected_horizontal);
let mut vertical = RecordingCanvas::new(identity_transform());
vertical.line(2.0, 1.0, 2.0, 9.0, CanvasColor::GREEN);
let expected_vertical: Vec<_> = (1..=9).map(|y| (2, y, CanvasColor::GREEN)).collect();
assert_eq!(vertical.hits, expected_vertical);
let mut diagonal = RecordingCanvas::new(identity_transform());
diagonal.line(1.0, 1.0, 9.0, 9.0, CanvasColor::RED);
let expected_diagonal: Vec<_> = (1..=9)
.map(|value| (value, value, CanvasColor::RED))
.collect();
assert_eq!(diagonal.hits, expected_diagonal);
}
#[test]
fn point_clamps_coordinates_to_canvas_edges() {
let mut canvas = RecordingCanvas::new(identity_transform());
canvas.point(-5.0, -5.0, CanvasColor::CYAN);
canvas.point(10.0, 10.0, CanvasColor::CYAN);
canvas.point(15.0, 5.0, CanvasColor::CYAN);
assert_eq!(
canvas.hits,
vec![
(0, 0, CanvasColor::CYAN),
(9, 9, CanvasColor::CYAN),
(9, 5, CanvasColor::CYAN)
]
);
}
#[test]
fn points_and_lines_ignore_mismatched_input_lengths() {
let mut points_canvas = RecordingCanvas::new(identity_transform());
points_canvas.points(&[0.0, 1.0], &[0.0], CanvasColor::WHITE);
assert!(points_canvas.hits.is_empty());
let mut lines_canvas = RecordingCanvas::new(identity_transform());
lines_canvas.lines(&[0.0, 1.0, 2.0], &[0.0, 1.0], CanvasColor::WHITE);
assert!(lines_canvas.hits.is_empty());
}
#[test]
fn line_handles_reverse_direction_and_zero_length_segments() {
let mut reverse = RecordingCanvas::new(identity_transform());
reverse.line(9.0, 1.0, 1.0, 1.0, CanvasColor::MAGENTA);
let expected_reverse: Vec<_> = (1..=9)
.rev()
.map(|x| (x, 1, CanvasColor::MAGENTA))
.collect();
assert_eq!(reverse.hits, expected_reverse);
let mut point = RecordingCanvas::new(identity_transform());
point.line(4.0, 7.0, 4.0, 7.0, CanvasColor::YELLOW);
assert_eq!(point.hits, vec![(4, 7, CanvasColor::YELLOW)]);
}
#[test]
fn line_rejects_segments_fully_outside_bounds() {
let mut canvas = RecordingCanvas::new(identity_transform());
canvas.line(-10.0, 5.0, -1.0, 5.0, CanvasColor::RED);
canvas.line(20.0, 5.0, 30.0, 5.0, CanvasColor::RED);
canvas.line(5.0, -10.0, 5.0, -1.0, CanvasColor::RED);
canvas.line(5.0, 20.0, 5.0, 30.0, CanvasColor::RED);
assert!(canvas.hits.is_empty());
}
#[test]
fn row_cells_iterates_in_column_order_with_matching_colors() {
let canvas = RowCanvas::new(identity_transform());
let row = canvas.row_cells(1).collect::<Vec<_>>();
assert_eq!(
row,
vec![
('d', CanvasColor::CYAN),
('e', CanvasColor::MAGENTA),
('f', CanvasColor::YELLOW)
]
);
}
}