1use std::io::{self, IsTerminal, Write};
15use std::time::{Duration, Instant};
16
17use crossterm::{
18 cursor::{Hide, MoveTo, Show},
19 execute, queue,
20 style::{Color, Print, ResetColor, SetForegroundColor},
21 terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
22};
23
24use crate::{art::Art, easing::Easing, rank::RankMap};
25
26const GLOW_LEVELS: u8 = 8;
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32pub enum Palette {
33 #[default]
35 Glow,
36 Rainbow,
38}
39
40#[derive(Clone, Copy, Debug)]
42pub struct Style {
43 pub feather: f32,
46 pub body: (u8, u8, u8),
48 pub head: (u8, u8, u8),
50 pub color: bool,
52 pub palette: Palette,
54}
55
56impl Default for Style {
57 fn default() -> Self {
58 Style {
59 feather: 0.07,
60 body: (120, 134, 168),
61 head: (255, 226, 138),
62 color: std::env::var_os("NO_COLOR").is_none(),
63 palette: Palette::Glow,
64 }
65 }
66}
67
68impl Style {
69 pub fn rainbow() -> Self {
72 Style {
73 palette: Palette::Rainbow,
74 ..Style::default()
75 }
76 }
77}
78
79#[derive(Clone, Copy, PartialEq, Eq)]
81enum CellState {
82 Hidden,
83 Lit(u8),
85}
86
87pub struct Reveal<'a> {
111 art: &'a Art,
112 ranks: &'a RankMap,
113 style: Style,
114 state: Vec<CellState>,
115 out: io::Stdout,
116 origin: (u16, u16),
117 active: bool,
119}
120
121impl<'a> Reveal<'a> {
122 pub fn new(art: &'a Art, ranks: &'a RankMap, style: Style) -> io::Result<Self> {
125 let mut out = io::stdout();
126 let active = out.is_terminal();
127 let origin = if active {
128 let (cols, _) = terminal::size().unwrap_or((art.width(), art.height()));
129 (cols.saturating_sub(art.width()) / 2, 1)
130 } else {
131 (0, 0)
132 };
133 if active {
134 execute!(out, EnterAlternateScreen, Hide, Clear(ClearType::All))?;
135 }
136 Ok(Reveal {
137 art,
138 ranks,
139 style,
140 state: vec![CellState::Hidden; art.cell_count()],
141 out,
142 origin,
143 active,
144 })
145 }
146
147 pub fn render(&mut self, progress: f32) -> io::Result<()> {
149 if !self.active {
150 return Ok(());
151 }
152 paint(
153 &mut self.out,
154 self.art,
155 self.ranks,
156 &self.style,
157 &mut self.state,
158 progress,
159 self.origin,
160 )
161 }
162
163 pub fn finish(mut self) -> io::Result<()> {
165 self.restore()?;
166 write!(
167 self.out,
168 "{}",
169 crate::frame::to_string(self.art, self.ranks, 1.0)
170 )?;
171 self.out.flush()
172 }
173
174 fn restore(&mut self) -> io::Result<()> {
175 if self.active {
176 self.active = false;
177 execute!(self.out, ResetColor, Show, LeaveAlternateScreen)?;
178 }
179 Ok(())
180 }
181}
182
183impl Drop for Reveal<'_> {
184 fn drop(&mut self) {
185 let _ = self.restore();
186 }
187}
188
189pub fn animate(
194 art: &Art,
195 ranks: &RankMap,
196 style: Style,
197 duration: Duration,
198 easing: Easing,
199) -> io::Result<()> {
200 if !io::stdout().is_terminal() {
201 print!("{}", crate::frame::to_string(art, ranks, 1.0));
202 return Ok(());
203 }
204
205 let mut reveal = Reveal::new(art, ranks, style)?;
206 let total = duration.as_secs_f32().max(0.001);
207 let frame = Duration::from_millis(16); let start = Instant::now();
209
210 for tick in 1.. {
211 let t = (start.elapsed().as_secs_f32() / total).min(1.0);
212 reveal.render(easing.apply(t))?;
213 if t >= 1.0 {
214 break;
215 }
216 if let Some(remaining) = (start + frame * tick).checked_duration_since(Instant::now()) {
219 std::thread::sleep(remaining);
220 }
221 }
222 reveal.finish()
223}
224
225fn paint(
227 out: &mut io::Stdout,
228 art: &Art,
229 ranks: &RankMap,
230 style: &Style,
231 state: &mut [CellState],
232 progress: f32,
233 (ox, oy): (u16, u16),
234) -> io::Result<()> {
235 let mut dirty = false;
236 for y in 0..art.height() {
237 for x in 0..art.width() {
238 let idx = art.index(x, y);
239 let target = match ranks.rank_at(x, y) {
240 Some(r) if r <= progress => {
241 let level = match style.palette {
242 Palette::Rainbow => GLOW_LEVELS,
245 Palette::Glow if style.feather <= 0.0 => GLOW_LEVELS,
246 Palette::Glow => {
247 let a = ((progress - r) / style.feather).clamp(0.0, 1.0);
248 (a * GLOW_LEVELS as f32).round() as u8
249 }
250 };
251 CellState::Lit(level)
252 }
253 _ => CellState::Hidden,
254 };
255
256 if state[idx] == target {
257 continue;
258 }
259 queue!(out, MoveTo(ox + x, oy + y))?;
260 match target {
261 CellState::Hidden => queue!(out, Print(' '))?,
262 CellState::Lit(level) => {
263 if style.color {
264 let (r, g, b) = match style.palette {
265 Palette::Rainbow => rainbow_rgb(x, y, 0.0),
266 Palette::Glow => {
267 blend(style.head, style.body, level as f32 / GLOW_LEVELS as f32)
268 }
269 };
270 queue!(out, SetForegroundColor(Color::Rgb { r, g, b }))?;
271 }
272 queue!(out, Print(art.glyph(x, y)))?;
273 }
274 }
275 state[idx] = target;
276 dirty = true;
277 }
278 }
279
280 if dirty {
281 queue!(out, ResetColor)?;
282 out.flush()?;
283 }
284 Ok(())
285}
286
287fn blend(a: (u8, u8, u8), b: (u8, u8, u8), s: f32) -> (u8, u8, u8) {
289 let lerp = |x: u8, y: u8| {
290 (x as f32 + (y as f32 - x as f32) * s)
291 .round()
292 .clamp(0.0, 255.0) as u8
293 };
294 (lerp(a.0, b.0), lerp(a.1, b.1), lerp(a.2, b.2))
295}
296
297pub(crate) fn frontier_rgb(style: &Style, progress: f32, rank: f32) -> (u8, u8, u8) {
300 if style.feather <= 0.0 {
301 return style.body;
302 }
303 let a = ((progress - rank) / style.feather).clamp(0.0, 1.0);
304 blend(style.head, style.body, a)
305}
306
307pub(crate) fn cell_rgb(
310 style: &Style,
311 progress: f32,
312 rank: f32,
313 x: u16,
314 y: u16,
315 t: f32,
316) -> (u8, u8, u8) {
317 match style.palette {
318 Palette::Glow => frontier_rgb(style, progress, rank),
319 Palette::Rainbow => rainbow_rgb(x, y, t),
320 }
321}
322
323fn rainbow_rgb(x: u16, y: u16, t: f32) -> (u8, u8, u8) {
325 let hue = (x as f32 * 0.05 + y as f32 * 0.12 + t * 0.4).rem_euclid(1.0);
326 hsl_to_rgb(hue, 0.95, 0.62)
327}
328
329fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
331 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
332 let hp = h * 6.0;
333 let x = c * (1.0 - (hp.rem_euclid(2.0) - 1.0).abs());
334 let (r, g, b) = match hp as u32 {
335 0 => (c, x, 0.0),
336 1 => (x, c, 0.0),
337 2 => (0.0, c, x),
338 3 => (0.0, x, c),
339 4 => (x, 0.0, c),
340 _ => (c, 0.0, x),
341 };
342 let m = l - c / 2.0;
343 let to = |v: f32| ((v + m) * 255.0).round().clamp(0.0, 255.0) as u8;
344 (to(r), to(g), to(b))
345}