pub mod cell_gradient;
pub mod mono;
pub mod orb;
pub mod starfield;
use crate::caps::{Capabilities, ColorDepth};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Rgb {
pub const fn new(r: u8, g: u8, b: u8) -> Rgb {
Rgb { r, g, b }
}
pub const BLACK: Rgb = Rgb::new(0, 0, 0);
pub fn lerp(a: Rgb, b: Rgb, t: f32) -> Rgb {
let t = t.clamp(0.0, 1.0);
let mix = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t).round() as u8;
Rgb::new(mix(a.r, b.r), mix(a.g, b.g), mix(a.b, b.b))
}
pub fn luma(self) -> f32 {
(0.299 * self.r as f32 + 0.587 * self.g as f32 + 0.114 * self.b as f32) / 255.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GlyphCell {
pub ch: char,
pub fg: Rgb,
}
#[derive(Clone, Debug)]
pub struct Surface {
width: usize,
height: usize,
pixels: Vec<Rgb>,
glyphs: Vec<Option<GlyphCell>>,
}
impl Surface {
pub fn new(width: usize, height: usize, background: Rgb) -> Surface {
Surface {
width,
height,
pixels: vec![background; width * height],
glyphs: vec![None; width * (height / 2)],
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn fill(&mut self, color: Rgb) {
self.pixels.iter_mut().for_each(|p| *p = color);
}
pub fn get(&self, x: usize, y: usize) -> Rgb {
self.pixels[y * self.width + x]
}
pub fn set(&mut self, x: usize, y: usize, color: Rgb) {
if x < self.width && y < self.height {
self.pixels[y * self.width + x] = color;
}
}
pub fn blend(&mut self, x: usize, y: usize, color: Rgb, alpha: f32) {
if x < self.width && y < self.height {
let under = self.get(x, y);
self.set(x, y, Rgb::lerp(under, color, alpha));
}
}
fn cell_rows(&self) -> usize {
self.height / 2
}
pub fn set_glyph(&mut self, x: usize, cell_y: usize, ch: char, fg: Rgb) {
if x < self.width && cell_y < self.cell_rows() {
self.glyphs[cell_y * self.width + x] = Some(GlyphCell { ch, fg });
}
}
pub fn clear_glyph(&mut self, x: usize, cell_y: usize) {
if x < self.width && cell_y < self.cell_rows() {
self.glyphs[cell_y * self.width + x] = None;
}
}
pub fn glyph(&self, x: usize, cell_y: usize) -> Option<GlyphCell> {
if x < self.width && cell_y < self.cell_rows() {
self.glyphs[cell_y * self.width + x]
} else {
None
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Tier {
CellGradient,
Mono,
}
impl Tier {
pub fn select(caps: &Capabilities) -> Tier {
match caps.color {
ColorDepth::Truecolor | ColorDepth::Ansi256 => Tier::CellGradient,
ColorDepth::Ansi16 | ColorDepth::None => Tier::Mono,
}
}
}
pub trait Renderer {
fn encode(&self, surface: &Surface) -> String;
}
pub fn renderer_for(caps: &Capabilities) -> Box<dyn Renderer> {
match Tier::select(caps) {
Tier::CellGradient => Box::new(cell_gradient::CellGradient::new(caps.color)),
Tier::Mono => Box::new(mono::Mono),
}
}
pub(crate) const RESET: &str = "\x1b[0m";
pub(crate) fn to_ansi256(c: Rgb) -> u8 {
let component = |v: u8| -> u8 {
if v < 48 {
0
} else if v < 115 {
1
} else {
((v as u16 - 35) / 40) as u8
}
};
let (r, g, b) = (component(c.r), component(c.g), component(c.b));
16 + 36 * r + 6 * g + b
}
#[cfg(test)]
mod tests {
use super::*;
use crate::caps::ColorDepth;
#[test]
fn glyph_layer_is_noop_when_unset() {
let mut s = Surface::new(2, 2, Rgb::new(10, 12, 16));
s.set(0, 0, Rgb::new(96, 138, 102));
let cg = cell_gradient::CellGradient::new(ColorDepth::Truecolor).encode(&s);
assert!(cg.contains('▀'));
assert!(!cg.contains('✦'));
let mono = mono::Mono.encode(&s);
assert!(!mono.contains('\x1b'));
}
#[test]
fn cell_gradient_emits_glyph_in_fg_over_bottom_bg() {
let mut s = Surface::new(1, 2, Rgb::new(6, 8, 14));
s.set_glyph(0, 0, '✦', Rgb::new(200, 220, 200));
let out = cell_gradient::CellGradient::new(ColorDepth::Truecolor).encode(&s);
assert!(out.contains('✦'));
assert!(!out.contains('▀'));
assert!(out.contains("\x1b[38;2;200;220;200m")); assert!(out.contains("\x1b[48;2;6;8;14m")); }
#[test]
fn transparent_bg_blanks_pure_background_cells() {
let bg = Rgb::new(10, 12, 16);
let mut s = Surface::new(2, 2, bg); s.set(0, 0, Rgb::new(96, 138, 102)); let mut cg = cell_gradient::CellGradient::new(ColorDepth::Truecolor);
cg.set_transparent_bg(Some(bg));
let out = cg.encode(&s);
assert!(out.contains("\x1b[49m ")); assert!(out.contains('▀')); let opaque = cell_gradient::CellGradient::new(ColorDepth::Truecolor).encode(&s);
assert!(!opaque.contains("\x1b[49m"));
}
#[test]
fn mono_emits_glyph_as_presence_without_color() {
let mut s = Surface::new(1, 2, Rgb::BLACK);
s.set_glyph(0, 0, '·', Rgb::new(200, 200, 200));
let out = mono::Mono.encode(&s);
assert!(out.contains('·'));
assert!(!out.contains('\x1b'));
}
#[test]
fn set_glyph_ignores_out_of_bounds_and_clear_works() {
let mut s = Surface::new(2, 2, Rgb::BLACK);
s.set_glyph(9, 9, '✦', Rgb::BLACK);
assert_eq!(s.glyph(9, 9), None);
s.set_glyph(0, 0, '✦', Rgb::BLACK);
assert!(s.glyph(0, 0).is_some());
s.clear_glyph(0, 0);
assert_eq!(s.glyph(0, 0), None);
}
}