use ratatui::{
style::{Color, Style},
text::{Line, Span},
};
mod waves;
mod rain;
mod leaves;
mod stars;
mod fire;
mod aurora;
mod blossom;
mod sunset;
type PixBuf = Vec<Vec<Option<Color>>>;
fn new_buf(pw: usize, ph: usize) -> PixBuf {
vec![vec![None; pw]; ph]
}
fn px(buf: &PixBuf, x: usize, y: usize) -> Option<Color> {
buf.get(y).and_then(|r| r.get(x)).copied().flatten()
}
fn lum(c: Option<Color>) -> f32 {
match c {
Some(Color::Rgb(r, g, b)) => 0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32,
_ => 0.0,
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum RenderMode { Half, Quarter, Braille }
impl RenderMode {
fn next(self) -> Self {
match self { Self::Half => Self::Quarter, Self::Quarter => Self::Braille, Self::Braille => Self::Half }
}
fn prev(self) -> Self {
match self { Self::Half => Self::Braille, Self::Quarter => Self::Half, Self::Braille => Self::Quarter }
}
fn pw(self, char_w: usize) -> usize {
match self { Self::Half => char_w, Self::Quarter | Self::Braille => char_w * 2 }
}
fn ph(self, char_h: usize) -> usize {
match self { Self::Half | Self::Quarter => char_h * 2, Self::Braille => char_h * 4 }
}
}
fn render_half(buf: &PixBuf, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
to_lines(char_w, char_h, |col, row| {
let t = px(buf, col, row * 2);
let b = px(buf, col, row * 2 + 1);
match (t, b) {
(None, None ) => (' ', Style::new()),
(Some(c), None ) => ('▀', Style::new().fg(c)),
(None, Some(c)) => ('▄', Style::new().fg(c)),
(Some(a), Some(b)) if a == b => ('█', Style::new().fg(a)),
(Some(a), Some(b)) => ('▀', Style::new().fg(a).bg(b)),
}
})
}
const Q: [char; 16] = [
' ', '▗', '▖', '▄', '▝', '▐', '▞', '▟',
'▘', '▚', '▌', '▙', '▀', '▜', '▛', '█',
];
fn render_quarter(buf: &PixBuf, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
to_lines(char_w, char_h, |col, row| {
let tl = px(buf, col*2, row*2);
let tr = px(buf, col*2+1, row*2);
let bl = px(buf, col*2, row*2+1);
let br = px(buf, col*2+1, row*2+1);
let pixels = [tl, tr, bl, br];
let ls = pixels.map(lum);
let mut s = ls;
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let thresh = (s[1] + s[2]) / 2.0;
let on = ls.map(|l| l > thresh || (l == thresh && l > 0.0));
let idx = (on[0] as usize)<<3 | (on[1] as usize)<<2 | (on[2] as usize)<<1 | on[3] as usize;
let ch = Q[idx];
let (mut fr, mut fg, mut fb, mut fn_) = (0u32, 0, 0, 0);
let (mut br_, mut bg, mut bb, mut bn) = (0u32, 0, 0, 0);
for (i, c) in pixels.iter().enumerate() {
if let Some(Color::Rgb(r, g, b)) = c {
if on[i] { fr += *r as u32; fg += *g as u32; fb += *b as u32; fn_ += 1; }
else { br_ += *r as u32; bg += *g as u32; bb += *b as u32; bn += 1; }
}
}
let style = match (fn_ > 0, bn > 0) {
(true, true ) => Style::new()
.fg(Color::Rgb((fr/fn_) as u8, (fg/fn_) as u8, (fb/fn_) as u8))
.bg(Color::Rgb((br_/bn) as u8, (bg/bn) as u8, (bb/bn) as u8)),
(true, false) => Style::new()
.fg(Color::Rgb((fr/fn_) as u8, (fg/fn_) as u8, (fb/fn_) as u8)),
(false, true ) => Style::new()
.bg(Color::Rgb((br_/bn) as u8, (bg/bn) as u8, (bb/bn) as u8)),
(false, false) => Style::new(),
};
(ch, style)
})
}
fn render_braille(buf: &PixBuf, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
const BIT: [[u8; 2]; 4] = [
[0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80], ];
to_lines(char_w, char_h, |col, row| {
let mut mask: u8 = 0;
let mut fr = 0u32; let mut fg = 0u32; let mut fb = 0u32; let mut fn_ = 0u32;
let mut dr = 0u32; let mut dg = 0u32; let mut db = 0u32; let mut dn = 0u32;
let mut lums = [0f32; 8];
let mut cols = [None::<Color>; 8];
let mut k = 0;
for dy in 0..4usize {
for dx in 0..2usize {
let c = px(buf, col*2+dx, row*4+dy);
lums[k] = lum(c);
cols[k] = c;
k += 1;
}
}
let mut s = lums;
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let thresh = (s[3] + s[4]) / 2.0;
for dy in 0..4usize {
for dx in 0..2usize {
let i = dy * 2 + dx;
let c = cols[i];
if let Some(Color::Rgb(r, g, b)) = c {
dr += r as u32; dg += g as u32; db += b as u32; dn += 1;
}
if lums[i] > thresh || (lums[i] == thresh && lums[i] > 0.0) {
mask |= BIT[dy][dx];
if let Some(Color::Rgb(r, g, b)) = c {
fr += r as u32; fg += g as u32; fb += b as u32; fn_ += 1;
}
}
}
}
let ch = char::from_u32(0x2800 + mask as u32).unwrap_or(' ');
let style = if fn_ > 0 && dn > 0 {
let fg_c = Color::Rgb((fr/fn_) as u8, (fg/fn_) as u8, (fb/fn_) as u8);
let bg_c = Color::Rgb(
((dr/dn) as u16).saturating_sub(30) as u8,
((dg/dn) as u16).saturating_sub(30) as u8,
((db/dn) as u16).saturating_sub(30) as u8,
);
Style::new().fg(fg_c).bg(bg_c)
} else if fn_ > 0 {
Style::new().fg(Color::Rgb((fr/fn_) as u8, (fg/fn_) as u8, (fb/fn_) as u8))
} else {
Style::new()
};
(ch, style)
})
}
fn to_lines(
char_w: usize,
char_h: usize,
mut cell: impl FnMut(usize, usize) -> (char, Style),
) -> Vec<Line<'static>> {
(0..char_h).map(|row| {
let mut spans: Vec<Span<'static>> = Vec::new();
let mut run = String::new();
let mut cur = Style::new();
for col in 0..char_w {
let (ch, sty) = cell(col, row);
if sty == cur { run.push(ch); }
else {
if !run.is_empty() { spans.push(Span::styled(run.clone(), cur)); run.clear(); }
cur = sty;
run.push(ch);
}
}
if !run.is_empty() { spans.push(Span::styled(run, cur)); }
Line::from(spans)
}).collect()
}
fn set_px(buf: &mut PixBuf, x: isize, y: isize, c: Color) {
if y >= 0 && x >= 0 {
if let Some(row) = buf.get_mut(y as usize) {
if let Some(cell) = row.get_mut(x as usize) {
*cell = Some(c);
}
}
}
}
fn fill_r(buf: &mut PixBuf, x: isize, y: isize, w: usize, h: usize, c: Color) {
for dy in 0..h as isize {
for dx in 0..w as isize {
set_px(buf, x + dx, y + dy, c);
}
}
}
fn hash(mut x: u64) -> u64 {
x ^= x >> 30;
x = x.wrapping_mul(0xbf58476d1ce4e5b9);
x ^= x >> 27;
x = x.wrapping_mul(0x94d049bb133111eb);
x ^ (x >> 31)
}
fn dispatch(mode: RenderMode, buf: &PixBuf, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
match mode {
RenderMode::Half => render_half(buf, char_w, char_h),
RenderMode::Quarter => render_quarter(buf, char_w, char_h),
RenderMode::Braille => render_braille(buf, char_w, char_h),
}
}
type FillFn = fn(&mut PixBuf, usize, usize, u64);
struct Theme { fill: FillFn, color: Color }
const THEMES: &[Theme] = &[
Theme { fill: waves::fill_waves, color: Color::Rgb(0, 120, 210) },
Theme { fill: rain::fill_rain, color: Color::Rgb(80, 130, 210) },
Theme { fill: leaves::fill_leaves, color: Color::Rgb(210, 95, 15) },
Theme { fill: stars::fill_stars, color: Color::Rgb(140, 140, 255) },
Theme { fill: fire::fill_fire, color: Color::Rgb(255, 90, 0) },
Theme { fill: aurora::fill_aurora, color: Color::Rgb(20, 210, 150) },
Theme { fill: blossom::fill_blossom, color: Color::Rgb(245, 170, 195) },
Theme { fill: sunset::fill_sunset, color: Color::Rgb(220, 90, 20) },
];
pub struct Animation {
theme_idx: usize,
pub render_mode: RenderMode,
tick_count: u64,
}
impl Animation {
pub fn new() -> Self {
Self { theme_idx: 0, render_mode: RenderMode::Half, tick_count: 0 }
}
pub fn tick(&mut self) { self.tick_count += 1; }
pub fn next_theme(&mut self) { self.theme_idx = (self.theme_idx + 1) % THEMES.len(); }
pub fn prev_theme(&mut self) { self.theme_idx = (self.theme_idx + THEMES.len() - 1) % THEMES.len(); }
pub fn next_mode(&mut self) { self.render_mode = self.render_mode.next(); }
pub fn prev_mode(&mut self) { self.render_mode = self.render_mode.prev(); }
pub fn theme_color(&self) -> Color { THEMES[self.theme_idx].color }
pub fn render_lines(&self, _phase: &crate::timer::Phase, char_w: usize, char_h: usize) -> Vec<Line<'static>> {
if char_w == 0 || char_h == 0 { return vec![]; }
let mode = self.render_mode;
let theme = &THEMES[self.theme_idx];
let (pw, ph) = (mode.pw(char_w), mode.ph(char_h));
let mut buf = new_buf(pw, ph);
(theme.fill)(&mut buf, pw, ph, self.tick_count);
dispatch(mode, &buf, char_w, char_h)
}
}