use crate::{art::Art, rank::RankMap, width::glyph_cols};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Paint {
Blank { cols: u16 },
Ink { glyph: char, cols: u16 },
}
impl Paint {
#[inline]
pub fn cols(self) -> u16 {
match self {
Paint::Blank { cols } | Paint::Ink { cols, .. } => cols,
}
}
#[inline]
pub fn glyph(self) -> Option<char> {
match self {
Paint::Ink { glyph, .. } => Some(glyph),
Paint::Blank { .. } => None,
}
}
}
#[inline]
pub fn cell(art: &Art, ranks: &RankMap, progress: f32, x: u16, y: u16) -> Paint {
let glyph = art.glyph(x, y);
let cols = glyph_cols(glyph).max(1);
if ranks.visible_at(x, y, progress) {
Paint::Ink { glyph, cols }
} else {
Paint::Blank { cols }
}
}
pub fn row<'a>(
art: &'a Art,
ranks: &'a RankMap,
progress: f32,
y: u16,
) -> impl Iterator<Item = (u16, u16, Paint)> + 'a {
let mut col = 0u16;
(0..art.width()).map(move |x| {
let paint = cell(art, ranks, progress, x, y);
let at = col;
col = col.saturating_add(paint.cols());
(x, at, paint)
})
}
pub fn art_cols(art: &Art) -> u16 {
(0..art.height())
.map(|y| {
(0..art.width())
.map(|x| glyph_cols(art.glyph(x, y)).max(1))
.fold(0u16, u16::saturating_add)
})
.max()
.unwrap_or(0)
}
pub fn to_string(art: &Art, ranks: &RankMap, progress: f32) -> String {
let mut out = String::with_capacity(art.cell_count() + art.height() as usize);
let mut line = String::with_capacity(art.width() as usize);
for y in 0..art.height() {
line.clear();
for (_, _, paint) in row(art, ranks, progress, y) {
match paint {
Paint::Ink { glyph, .. } => line.push(glyph),
Paint::Blank { cols } => (0..cols).for_each(|_| line.push(' ')),
}
}
out.push_str(line.trim_end());
out.push('\n');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ordering::{Geodesic, Ordering};
#[test]
fn empty_at_zero_full_at_one() {
let art = Art::parse("/\\__/\\\n\\____/");
let ranks = Geodesic::default().rank(&art);
let none = to_string(&art, &ranks, -0.001);
let all = to_string(&art, &ranks, 1.0);
assert!(none.trim().chars().all(|c| c.is_whitespace()));
assert_eq!(all.replace([' ', '\n'], "").len(), art.ink_count());
}
#[test]
fn reveal_is_monotonic() {
let art = Art::parse("####\n# #\n####");
let ranks = Geodesic::default().rank(&art);
let mut last = 0;
for i in 0..=10 {
let shown = to_string(&art, &ranks, i as f32 / 10.0)
.chars()
.filter(|c| !c.is_whitespace())
.count();
assert!(shown >= last, "reveal went backwards at step {i}");
last = shown;
}
assert_eq!(last, art.ink_count());
}
#[test]
fn always_has_one_line_per_row() {
let art = Art::parse("#\n#\n#");
let ranks = Geodesic::default().rank(&art);
assert_eq!(to_string(&art, &ranks, 0.5).lines().count(), 3);
}
#[cfg(feature = "unicode")]
#[test]
fn row_width_is_constant_across_the_reveal() {
use crate::width::str_cols;
let art = Art::parse("世a界b");
let ranks = Geodesic::default().rank(&art);
let widths: Vec<u16> = (0..=10)
.map(|i| {
let text = to_string(&art, &ranks, i as f32 / 10.0);
let padded: String = row(&art, &ranks, i as f32 / 10.0, 0)
.map(|(_, _, p)| match p {
Paint::Ink { glyph, .. } => glyph.to_string(),
Paint::Blank { cols } => " ".repeat(cols as usize),
})
.collect();
assert!(text.lines().count() == 1);
str_cols(&padded)
})
.collect();
assert!(
widths.windows(2).all(|w| w[0] == w[1]),
"row width drifted during the reveal: {widths:?}"
);
assert_eq!(widths[0], 6); }
#[test]
fn art_cols_counts_display_width() {
let art = Art::parse("ab\nabc");
assert_eq!(art_cols(&art), 3);
}
}