1mod ascii;
2mod block;
3mod braille;
4mod core;
5mod density;
6mod dot;
7mod transform;
8
9use crate::color::CanvasColor;
10
11pub use ascii::AsciiCanvas;
12pub use block::BlockCanvas;
13pub use braille::BrailleCanvas;
14pub(crate) use core::CanvasCore;
15pub use density::DensityCanvas;
16pub use dot::DotCanvas;
17pub use transform::{AxisTransform, Scale, Transform2D};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum CanvasType {
23 Ascii,
25 Block,
27 Braille,
29 Density,
31 Dot,
33}
34
35#[must_use]
37pub const fn canvas_types() -> &'static [CanvasType] {
38 &[
39 CanvasType::Ascii,
40 CanvasType::Block,
41 CanvasType::Braille,
42 CanvasType::Density,
43 CanvasType::Dot,
44 ]
45}
46
47pub trait Canvas {
53 fn pixel(&mut self, x: usize, y: usize, color: CanvasColor);
55
56 fn glyph_at(&self, col: usize, row: usize) -> char;
58
59 fn color_at(&self, col: usize, row: usize) -> CanvasColor;
61
62 fn char_width(&self) -> usize;
64
65 fn char_height(&self) -> usize;
67
68 fn pixel_width(&self) -> usize;
70
71 fn pixel_height(&self) -> usize;
73
74 fn transform(&self) -> &Transform2D;
76
77 fn transform_mut(&mut self) -> &mut Transform2D;
79
80 fn point(&mut self, x: f64, y: f64, color: CanvasColor) {
82 let Some(pixel_x) = self.transform().data_to_pixel_x(x) else {
83 return;
84 };
85 let Some(pixel_y) = self.transform().data_to_pixel_y(y) else {
86 return;
87 };
88 let Some(clamped_x) = clamp_pixel_coord(pixel_x, self.pixel_width()) else {
89 return;
90 };
91 let Some(clamped_y) = clamp_pixel_coord(pixel_y, self.pixel_height()) else {
92 return;
93 };
94
95 self.pixel(clamped_x, clamped_y, color);
96 }
97
98 fn points(&mut self, xs: &[f64], ys: &[f64], color: CanvasColor) {
100 if xs.len() != ys.len() {
101 return;
102 }
103
104 for (&x, &y) in xs.iter().zip(ys) {
105 self.point(x, y, color);
106 }
107 }
108
109 fn line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, color: CanvasColor) {
111 let Some(start_x) = self.transform().data_to_pixel_x(x1) else {
112 return;
113 };
114 let Some(start_y) = self.transform().data_to_pixel_y(y1) else {
115 return;
116 };
117 let Some(end_x) = self.transform().data_to_pixel_x(x2) else {
118 return;
119 };
120 let Some(end_y) = self.transform().data_to_pixel_y(y2) else {
121 return;
122 };
123
124 if segment_is_outside_bounds(start_x, end_x, self.pixel_width())
125 || segment_is_outside_bounds(start_y, end_y, self.pixel_height())
126 {
127 return;
128 }
129
130 let dx = end_x - start_x;
131 let dy = end_y - start_y;
132 let steps_u32 = dx.unsigned_abs().max(dy.unsigned_abs());
133 let Ok(steps) = usize::try_from(steps_u32) else {
134 return;
135 };
136
137 if steps == 0 {
138 if let (Some(in_bounds_x), Some(in_bounds_y)) = (
139 in_bounds_pixel_coord(start_x, self.pixel_width()),
140 in_bounds_pixel_coord(start_y, self.pixel_height()),
141 ) {
142 self.pixel(in_bounds_x, in_bounds_y, color);
143 }
144 return;
145 }
146
147 let step_denominator = i64::from(steps_u32);
148 for step in 0..=steps {
149 let Ok(step_i64) = i64::try_from(step) else {
150 continue;
151 };
152
153 let step_dx = floor_div_i64(i64::from(dx) * step_i64, step_denominator);
154 let step_dy = floor_div_i64(i64::from(dy) * step_i64, step_denominator);
155 let Ok(pixel_x) = i32::try_from(i64::from(start_x) + step_dx) else {
156 continue;
157 };
158 let Ok(pixel_y) = i32::try_from(i64::from(start_y) + step_dy) else {
159 continue;
160 };
161
162 if let (Some(in_bounds_x), Some(in_bounds_y)) = (
163 in_bounds_pixel_coord(pixel_x, self.pixel_width()),
164 in_bounds_pixel_coord(pixel_y, self.pixel_height()),
165 ) {
166 self.pixel(in_bounds_x, in_bounds_y, color);
167 }
168 }
169 }
170
171 fn lines(&mut self, xs: &[f64], ys: &[f64], color: CanvasColor) {
173 if xs.len() != ys.len() {
174 return;
175 }
176
177 for (x_pair, y_pair) in xs.windows(2).zip(ys.windows(2)) {
178 let &[x1, x2] = x_pair else {
179 continue;
180 };
181 let &[y1, y2] = y_pair else {
182 continue;
183 };
184 self.line(x1, y1, x2, y2, color);
185 }
186 }
187
188 fn row_cells(&self, row: usize) -> impl Iterator<Item = (char, CanvasColor)> + '_ {
190 (0..self.char_width()).map(move |col| (self.glyph_at(col, row), self.color_at(col, row)))
191 }
192}
193
194fn clamp_pixel_coord(coord: i32, upper: usize) -> Option<usize> {
195 if upper == 0 {
196 return None;
197 }
198
199 let upper_i64 = i64::try_from(upper).ok()?;
200 let clamped = i64::from(coord).clamp(0, upper_i64);
201 let adjusted = if clamped == upper_i64 {
202 clamped - 1
203 } else {
204 clamped
205 };
206
207 usize::try_from(adjusted).ok()
208}
209
210fn in_bounds_pixel_coord(coord: i32, upper: usize) -> Option<usize> {
211 if upper == 0 {
212 return None;
213 }
214
215 if coord < 0 {
216 return None;
217 }
218
219 let upper_i64 = i64::try_from(upper).ok()?;
220 let coord_i64 = i64::from(coord);
221 if coord_i64 > upper_i64 {
222 return None;
223 }
224
225 let adjusted = if coord_i64 == upper_i64 {
226 coord_i64 - 1
227 } else {
228 coord_i64
229 };
230
231 usize::try_from(adjusted).ok()
232}
233
234fn segment_is_outside_bounds(start: i32, end: i32, upper: usize) -> bool {
235 let Ok(upper_bound) = i32::try_from(upper) else {
236 return false;
237 };
238
239 (start < 0 && end < 0) || (start > upper_bound && end > upper_bound)
240}
241
242fn floor_div_i64(numerator: i64, denominator: i64) -> i64 {
243 let quotient = numerator / denominator;
244 let remainder = numerator % denominator;
245 if remainder < 0 {
246 quotient - 1
247 } else {
248 quotient
249 }
250}
251
252#[cfg(test)]
253pub(crate) fn write_colored_cell(
254 out: &mut String,
255 glyph: char,
256 color: CanvasColor,
257 enable_color: bool,
258) {
259 use std::fmt::Write as _;
260
261 if !enable_color {
262 out.push(glyph);
263 return;
264 }
265
266 let _ = if color == CanvasColor::NORMAL {
267 write!(out, "\x1b[0m{glyph}")
268 } else {
269 write!(out, "\x1b[{}m{glyph}\x1b[39m", canvas_color_fg_code(color))
270 };
271}
272
273#[cfg(test)]
274fn canvas_color_fg_code(color: CanvasColor) -> u8 {
275 match color.as_u8() {
276 1 => 34,
277 2 => 31,
278 3 => 35,
279 4 => 32,
280 5 => 36,
281 6 => 33,
282 7 => 37,
283 _ => 39,
284 }
285}
286
287#[cfg(test)]
288const X1_DATA: &str = r"
289 0.226582 0.504629 0.933372 0.522172 0.505208
290 0.0997825 0.0443222 0.722906 0.812814 0.245457
291 0.11202 0.000341996 0.380001 0.505277 0.841177
292 0.326561 0.810857 0.850456 0.478053 0.179066
293";
294
295#[cfg(test)]
296const Y1_DATA: &str = r"
297 0.44701 0.219519 0.677372 0.746407 0.735727
298 0.574789 0.538086 0.848053 0.110351 0.796793
299 0.987618 0.801862 0.365172 0.469959 0.306373
300 0.704691 0.540434 0.405842 0.805117 0.014829
301";
302
303#[cfg(test)]
304const X2_DATA: &str = r"
305 0.486366 0.911547 0.900818 0.641951 0.546221
306 0.036135 0.931522 0.196704 0.710775 0.969291
307 0.32546 0.632833 0.815576 0.85278 0.577286
308 0.887004 0.231596 0.288337 0.881386 0.0952668
309 0.609881 0.393795 0.84808 0.453653 0.746048
310 0.924725 0.100012 0.754283 0.769802 0.997368
311 0.0791693 0.234334 0.361207 0.1037 0.713739
312 0.510725 0.649145 0.233949 0.812092 0.914384
313 0.106925 0.570467 0.594956 0.118498 0.699827
314 0.380363 0.843282 0.28761 0.541469 0.568466
315";
316
317#[cfg(test)]
318const Y2_DATA: &str = r"
319 0.417777 0.774845 0.00230619 0.907031 0.971138
320 0.0524795 0.957415 0.328894 0.530493 0.193359
321 0.768422 0.783238 0.607772 0.0261113 0.0849032
322 0.461164 0.613067 0.785021 0.988875 0.131524
323 0.0657328 0.466453 0.560878 0.925428 0.238691
324 0.692385 0.203687 0.441146 0.229352 0.332706
325 0.113543 0.537354 0.965718 0.437026 0.960983
326 0.372294 0.0226533 0.593514 0.657878 0.450696
327 0.436169 0.445539 0.0534673 0.0882236 0.361795
328 0.182991 0.156862 0.734805 0.166076 0.1172
329";
330
331#[cfg(test)]
332fn parse_points(data: &str) -> Vec<f64> {
333 data.split_ascii_whitespace()
334 .map(|value| {
335 value
336 .parse::<f64>()
337 .unwrap_or_else(|_| panic!("invalid fixture point value: {value}"))
338 })
339 .collect()
340}
341
342#[cfg(test)]
343pub(crate) fn draw_reference_canvas_scene<C: Canvas>(canvas: &mut C) {
344 let x1 = parse_points(X1_DATA);
345 let y1 = parse_points(Y1_DATA);
346 let x2 = parse_points(X2_DATA);
347 let y2 = parse_points(Y2_DATA);
348
349 canvas.line(0.0, 0.0, 1.0, 1.0, CanvasColor::BLUE);
350 canvas.points(&x1, &y1, CanvasColor::WHITE);
351 canvas.pixel(2, 4, CanvasColor::CYAN);
352 canvas.points(&x2, &y2, CanvasColor::RED);
353 canvas.line(0.0, 1.0, 0.5, 0.0, CanvasColor::GREEN);
354 canvas.point(0.05, 0.3, CanvasColor::CYAN);
355 canvas.lines(&[1.0, 2.0], &[2.0, 1.0], CanvasColor::NORMAL);
356 canvas.line(0.0, 0.0, 9.0, 9999.0, CanvasColor::YELLOW);
357 canvas.line(0.0, 0.0, 1.0, 1.0, CanvasColor::BLUE);
358 canvas.line(0.1, 0.7, 0.9, 0.6, CanvasColor::RED);
359}
360
361#[cfg(test)]
362pub(crate) fn render_canvas_show<C: Canvas>(canvas: C, color: bool) -> String {
363 use std::fmt::Write as _;
364
365 use crate::color::{NamedColor, TermColor};
366 use crate::plot::Plot;
367 use crate::render::build_rendered_plot;
368
369 let mut plot = Plot::new(canvas);
370 plot.margin = 0;
371 plot.padding = 0;
372
373 let rendered = build_rendered_plot(&plot);
374 let mut out = String::new();
375 for (row_index, row) in rendered.rows().iter().enumerate() {
376 let is_top_or_bottom = row_index == 0 || row_index + 1 == rendered.rows().len();
377 if color && is_top_or_bottom {
378 let border = row.iter().map(|cell| cell.glyph).collect::<String>();
379 let _ = write!(out, "\x1b[90m{border}\x1b[39m");
380 if row_index + 1 < rendered.rows().len() {
381 out.push('\n');
382 }
383 continue;
384 }
385
386 for (col_index, cell) in row.iter().enumerate() {
387 if !color {
388 out.push(cell.glyph);
389 continue;
390 }
391
392 let is_side_border = col_index == 0 || col_index + 1 == row.len();
393 if is_top_or_bottom || is_side_border {
394 let _ = write!(out, "\x1b[90m{}\x1b[39m", cell.glyph);
395 continue;
396 }
397
398 let code = match cell.color {
399 Some(TermColor::Named(NamedColor::Black)) => Some(30),
400 Some(TermColor::Named(NamedColor::Red)) => Some(31),
401 Some(TermColor::Named(NamedColor::Green)) => Some(32),
402 Some(TermColor::Named(NamedColor::Yellow)) => Some(33),
403 Some(TermColor::Named(NamedColor::Blue)) => Some(34),
404 Some(TermColor::Named(NamedColor::Magenta)) => Some(35),
405 Some(TermColor::Named(NamedColor::Cyan)) => Some(36),
406 Some(TermColor::Named(NamedColor::White)) => Some(37),
407 _ => None,
408 };
409
410 if let Some(code) = code {
411 let _ = write!(out, "\x1b[{code}m{}\x1b[39m", cell.glyph);
412 } else {
413 let _ = write!(out, "\x1b[0m{}", cell.glyph);
414 }
415 }
416
417 if row_index + 1 < rendered.rows().len() {
418 out.push('\n');
419 }
420 }
421
422 out
423}
424
425#[cfg(test)]
426mod tests {
427 use super::{AxisTransform, Canvas, CanvasColor, Transform2D};
428 use crate::canvas::Scale;
429
430 #[derive(Debug)]
431 struct RecordingCanvas {
432 char_width: usize,
433 char_height: usize,
434 pixel_width: usize,
435 pixel_height: usize,
436 transform: Transform2D,
437 hits: Vec<(usize, usize, CanvasColor)>,
438 }
439
440 #[derive(Debug)]
441 struct RowCanvas {
442 transform: Transform2D,
443 glyphs: [char; 6],
444 colors: [CanvasColor; 6],
445 }
446
447 impl RowCanvas {
448 fn new(transform: Transform2D) -> Self {
449 Self {
450 transform,
451 glyphs: ['a', 'b', 'c', 'd', 'e', 'f'],
452 colors: [
453 CanvasColor::BLUE,
454 CanvasColor::GREEN,
455 CanvasColor::RED,
456 CanvasColor::CYAN,
457 CanvasColor::MAGENTA,
458 CanvasColor::YELLOW,
459 ],
460 }
461 }
462
463 fn index(col: usize, row: usize) -> usize {
464 row * 3 + col
465 }
466 }
467
468 impl Canvas for RowCanvas {
469 fn pixel(&mut self, _x: usize, _y: usize, _color: CanvasColor) {}
470
471 fn glyph_at(&self, col: usize, row: usize) -> char {
472 self.glyphs[Self::index(col, row)]
473 }
474
475 fn color_at(&self, col: usize, row: usize) -> CanvasColor {
476 self.colors[Self::index(col, row)]
477 }
478
479 fn char_width(&self) -> usize {
480 3
481 }
482
483 fn char_height(&self) -> usize {
484 2
485 }
486
487 fn pixel_width(&self) -> usize {
488 3
489 }
490
491 fn pixel_height(&self) -> usize {
492 2
493 }
494
495 fn transform(&self) -> &Transform2D {
496 &self.transform
497 }
498
499 fn transform_mut(&mut self) -> &mut Transform2D {
500 &mut self.transform
501 }
502 }
503
504 impl RecordingCanvas {
505 fn new(transform: Transform2D) -> Self {
506 Self {
507 char_width: 10,
508 char_height: 10,
509 pixel_width: 10,
510 pixel_height: 10,
511 transform,
512 hits: Vec::new(),
513 }
514 }
515 }
516
517 impl Canvas for RecordingCanvas {
518 fn pixel(&mut self, x: usize, y: usize, color: CanvasColor) {
519 self.hits.push((x, y, color));
520 }
521
522 fn glyph_at(&self, _col: usize, _row: usize) -> char {
523 ' '
524 }
525
526 fn color_at(&self, _col: usize, _row: usize) -> CanvasColor {
527 CanvasColor::NORMAL
528 }
529
530 fn char_width(&self) -> usize {
531 self.char_width
532 }
533
534 fn char_height(&self) -> usize {
535 self.char_height
536 }
537
538 fn pixel_width(&self) -> usize {
539 self.pixel_width
540 }
541
542 fn pixel_height(&self) -> usize {
543 self.pixel_height
544 }
545
546 fn transform(&self) -> &Transform2D {
547 &self.transform
548 }
549
550 fn transform_mut(&mut self) -> &mut Transform2D {
551 &mut self.transform
552 }
553 }
554
555 fn identity_transform() -> Transform2D {
556 let x = AxisTransform::new(0.0, 10.0, 10, Scale::Identity, false)
557 .unwrap_or_else(|| unreachable!("valid transform"));
558 let y = AxisTransform::new(0.0, 10.0, 10, Scale::Identity, false)
559 .unwrap_or_else(|| unreachable!("valid transform"));
560 Transform2D::new(x, y)
561 }
562
563 #[test]
564 fn dda_line_draws_expected_horizontal_vertical_and_diagonal_points() {
565 let mut horizontal = RecordingCanvas::new(identity_transform());
566 horizontal.line(1.0, 1.0, 9.0, 1.0, CanvasColor::BLUE);
567 let expected_horizontal: Vec<_> = (1..=9).map(|x| (x, 1, CanvasColor::BLUE)).collect();
568 assert_eq!(horizontal.hits, expected_horizontal);
569
570 let mut vertical = RecordingCanvas::new(identity_transform());
571 vertical.line(2.0, 1.0, 2.0, 9.0, CanvasColor::GREEN);
572 let expected_vertical: Vec<_> = (1..=9).map(|y| (2, y, CanvasColor::GREEN)).collect();
573 assert_eq!(vertical.hits, expected_vertical);
574
575 let mut diagonal = RecordingCanvas::new(identity_transform());
576 diagonal.line(1.0, 1.0, 9.0, 9.0, CanvasColor::RED);
577 let expected_diagonal: Vec<_> = (1..=9)
578 .map(|value| (value, value, CanvasColor::RED))
579 .collect();
580 assert_eq!(diagonal.hits, expected_diagonal);
581 }
582
583 #[test]
584 fn point_clamps_coordinates_to_canvas_edges() {
585 let mut canvas = RecordingCanvas::new(identity_transform());
586 canvas.point(-5.0, -5.0, CanvasColor::CYAN);
587 canvas.point(10.0, 10.0, CanvasColor::CYAN);
588 canvas.point(15.0, 5.0, CanvasColor::CYAN);
589
590 assert_eq!(
591 canvas.hits,
592 vec![
593 (0, 0, CanvasColor::CYAN),
594 (9, 9, CanvasColor::CYAN),
595 (9, 5, CanvasColor::CYAN)
596 ]
597 );
598 }
599
600 #[test]
601 fn points_and_lines_ignore_mismatched_input_lengths() {
602 let mut points_canvas = RecordingCanvas::new(identity_transform());
603 points_canvas.points(&[0.0, 1.0], &[0.0], CanvasColor::WHITE);
604 assert!(points_canvas.hits.is_empty());
605
606 let mut lines_canvas = RecordingCanvas::new(identity_transform());
607 lines_canvas.lines(&[0.0, 1.0, 2.0], &[0.0, 1.0], CanvasColor::WHITE);
608 assert!(lines_canvas.hits.is_empty());
609 }
610
611 #[test]
612 fn line_handles_reverse_direction_and_zero_length_segments() {
613 let mut reverse = RecordingCanvas::new(identity_transform());
614 reverse.line(9.0, 1.0, 1.0, 1.0, CanvasColor::MAGENTA);
615 let expected_reverse: Vec<_> = (1..=9)
616 .rev()
617 .map(|x| (x, 1, CanvasColor::MAGENTA))
618 .collect();
619 assert_eq!(reverse.hits, expected_reverse);
620
621 let mut point = RecordingCanvas::new(identity_transform());
622 point.line(4.0, 7.0, 4.0, 7.0, CanvasColor::YELLOW);
623 assert_eq!(point.hits, vec![(4, 7, CanvasColor::YELLOW)]);
624 }
625
626 #[test]
627 fn line_rejects_segments_fully_outside_bounds() {
628 let mut canvas = RecordingCanvas::new(identity_transform());
629 canvas.line(-10.0, 5.0, -1.0, 5.0, CanvasColor::RED);
630 canvas.line(20.0, 5.0, 30.0, 5.0, CanvasColor::RED);
631 canvas.line(5.0, -10.0, 5.0, -1.0, CanvasColor::RED);
632 canvas.line(5.0, 20.0, 5.0, 30.0, CanvasColor::RED);
633
634 assert!(canvas.hits.is_empty());
635 }
636
637 #[test]
638 fn row_cells_iterates_in_column_order_with_matching_colors() {
639 let canvas = RowCanvas::new(identity_transform());
640 let row = canvas.row_cells(1).collect::<Vec<_>>();
641
642 assert_eq!(
643 row,
644 vec![
645 ('d', CanvasColor::CYAN),
646 ('e', CanvasColor::MAGENTA),
647 ('f', CanvasColor::YELLOW)
648 ]
649 );
650 }
651}