1use crate::{art::Art, rank::RankMap, width::glyph_cols};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum Paint {
18 Blank { cols: u16 },
20 Ink { glyph: char, cols: u16 },
22}
23
24impl Paint {
25 #[inline]
27 pub fn cols(self) -> u16 {
28 match self {
29 Paint::Blank { cols } | Paint::Ink { cols, .. } => cols,
30 }
31 }
32
33 #[inline]
35 pub fn glyph(self) -> Option<char> {
36 match self {
37 Paint::Ink { glyph, .. } => Some(glyph),
38 Paint::Blank { .. } => None,
39 }
40 }
41}
42
43#[inline]
45pub fn cell(art: &Art, ranks: &RankMap, progress: f32, x: u16, y: u16) -> Paint {
46 let glyph = art.glyph(x, y);
47 let cols = glyph_cols(glyph).max(1);
48 if ranks.visible_at(x, y, progress) {
49 Paint::Ink { glyph, cols }
50 } else {
51 Paint::Blank { cols }
52 }
53}
54
55pub fn row<'a>(
60 art: &'a Art,
61 ranks: &'a RankMap,
62 progress: f32,
63 y: u16,
64) -> impl Iterator<Item = (u16, u16, Paint)> + 'a {
65 let mut col = 0u16;
66 (0..art.width()).map(move |x| {
67 let paint = cell(art, ranks, progress, x, y);
68 let at = col;
69 col = col.saturating_add(paint.cols());
70 (x, at, paint)
71 })
72}
73
74pub fn art_cols(art: &Art) -> u16 {
76 (0..art.height())
77 .map(|y| {
78 (0..art.width())
79 .map(|x| glyph_cols(art.glyph(x, y)).max(1))
80 .fold(0u16, u16::saturating_add)
81 })
82 .max()
83 .unwrap_or(0)
84}
85
86pub fn to_string(art: &Art, ranks: &RankMap, progress: f32) -> String {
91 let mut out = String::with_capacity(art.cell_count() + art.height() as usize);
92 let mut line = String::with_capacity(art.width() as usize);
93 for y in 0..art.height() {
94 line.clear();
95 for (_, _, paint) in row(art, ranks, progress, y) {
96 match paint {
97 Paint::Ink { glyph, .. } => line.push(glyph),
98 Paint::Blank { cols } => (0..cols).for_each(|_| line.push(' ')),
101 }
102 }
103 out.push_str(line.trim_end());
104 out.push('\n');
105 }
106 out
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::ordering::{Geodesic, Ordering};
113
114 #[test]
115 fn empty_at_zero_full_at_one() {
116 let art = Art::parse("/\\__/\\\n\\____/");
117 let ranks = Geodesic::default().rank(&art);
118
119 let none = to_string(&art, &ranks, -0.001);
122 let all = to_string(&art, &ranks, 1.0);
123
124 assert!(none.trim().chars().all(|c| c.is_whitespace()));
125 assert_eq!(all.replace([' ', '\n'], "").len(), art.ink_count());
126 }
127
128 #[test]
129 fn reveal_is_monotonic() {
130 let art = Art::parse("####\n# #\n####");
131 let ranks = Geodesic::default().rank(&art);
132 let mut last = 0;
133 for i in 0..=10 {
134 let shown = to_string(&art, &ranks, i as f32 / 10.0)
135 .chars()
136 .filter(|c| !c.is_whitespace())
137 .count();
138 assert!(shown >= last, "reveal went backwards at step {i}");
139 last = shown;
140 }
141 assert_eq!(last, art.ink_count());
142 }
143
144 #[test]
145 fn always_has_one_line_per_row() {
146 let art = Art::parse("#\n#\n#");
147 let ranks = Geodesic::default().rank(&art);
148 assert_eq!(to_string(&art, &ranks, 0.5).lines().count(), 3);
149 }
150
151 #[cfg(feature = "unicode")]
154 #[test]
155 fn row_width_is_constant_across_the_reveal() {
156 use crate::width::str_cols;
157 let art = Art::parse("世a界b");
158 let ranks = Geodesic::default().rank(&art);
159 let widths: Vec<u16> = (0..=10)
160 .map(|i| {
161 let text = to_string(&art, &ranks, i as f32 / 10.0);
162 let padded: String = row(&art, &ranks, i as f32 / 10.0, 0)
164 .map(|(_, _, p)| match p {
165 Paint::Ink { glyph, .. } => glyph.to_string(),
166 Paint::Blank { cols } => " ".repeat(cols as usize),
167 })
168 .collect();
169 assert!(text.lines().count() == 1);
170 str_cols(&padded)
171 })
172 .collect();
173 assert!(
174 widths.windows(2).all(|w| w[0] == w[1]),
175 "row width drifted during the reveal: {widths:?}"
176 );
177 assert_eq!(widths[0], 6); }
179
180 #[test]
181 fn art_cols_counts_display_width() {
182 let art = Art::parse("ab\nabc");
183 assert_eq!(art_cols(&art), 3);
184 }
185}