use std::io::{self, Write};
use std::thread;
use std::time::Duration;
use crate::color::Palette;
use crate::color::{Color, ColorMode};
use crate::effects::dither::apply_dot_dither;
use crate::effects::light_sweep::{LightSweep, SweepDirection, apply_light_sweep_tint};
use crate::effects::outline::{EdgeShade, apply_edge_shade};
use crate::effects::shadow::{Shadow, apply_shadow};
use crate::emit::emit_ansi;
use crate::fill::{Dither, Fill, apply_fill};
use crate::font::{self, Font, render_text};
use crate::gradient::Gradient;
use crate::grid::{Align, Grid, Padding};
use crate::style::Style;
use crate::terminal::detect_color_mode;
#[derive(Clone, Debug)]
pub struct Banner {
text: String,
font: Font,
gradient: Option<Gradient>,
fill: Fill,
light_sweep: Option<LightSweep>,
shadow: Option<Shadow>,
edge_shade: Option<EdgeShade>,
dot_dither: Option<Dither>,
dot_dither_targets: Option<Vec<char>>,
align: Align,
padding: Padding,
width: Option<usize>,
max_width: Option<usize>,
kerning: usize,
line_gap: usize,
color_mode: ColorMode,
}
#[derive(Debug)]
pub enum BannerError {
Font(font::figlet::FigletError),
}
impl std::fmt::Display for BannerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BannerError::Font(err) => write!(f, "font parse error: {err:?}"),
}
}
}
impl std::error::Error for BannerError {}
impl From<font::figlet::FigletError> for BannerError {
fn from(err: font::figlet::FigletError) -> Self {
BannerError::Font(err)
}
}
impl Banner {
pub fn new(text: impl Into<String>) -> Result<Self, BannerError> {
Ok(Self {
text: text.into(),
font: Font::dos_rebel()?,
gradient: None,
fill: Fill::Blocks,
light_sweep: None,
shadow: None,
edge_shade: None,
dot_dither: None,
dot_dither_targets: None,
align: Align::Left,
padding: Padding::uniform(0),
width: None,
max_width: None,
kerning: 1,
line_gap: 0,
color_mode: ColorMode::Auto,
})
}
pub fn font(mut self, font: Font) -> Self {
self.font = font;
self
}
pub fn style(mut self, style: Style) -> Self {
self.color_mode = ColorMode::TrueColor;
self.gradient = Some(Gradient::vertical(Palette::preset(style.preset())));
self.fill = Fill::Keep;
self
}
pub fn gradient(mut self, gradient: Gradient) -> Self {
self.gradient = Some(gradient);
self
}
pub fn fill(mut self, fill: Fill) -> Self {
self.fill = fill;
self
}
pub fn shadow(mut self, offset: (i32, i32), alpha: f32) -> Self {
self.shadow = Some(Shadow { offset, alpha });
self
}
pub fn light_sweep(mut self, sweep: LightSweep) -> Self {
self.light_sweep = Some(sweep);
self
}
pub fn edge_shade(mut self, darken: f32, ch: char) -> Self {
self.edge_shade = Some(EdgeShade { ch, darken });
self
}
pub fn dot_dither(mut self, dither: Dither) -> Self {
self.dot_dither = Some(dither);
self
}
pub fn dot_dither_targets(mut self, targets: &[char]) -> Self {
self.dot_dither_targets = Some(targets.to_vec());
self
}
pub fn dot_dither_targets_str(mut self, targets: &str) -> Self {
self.dot_dither_targets = Some(targets.chars().collect());
self
}
pub fn dither(self) -> DotDitherBuilder {
DotDitherBuilder::new(self)
}
pub fn align(mut self, align: Align) -> Self {
self.align = align;
self
}
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
pub fn width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
pub fn max_width(mut self, width: usize) -> Self {
self.max_width = Some(width);
self
}
pub fn kerning(mut self, kerning: usize) -> Self {
self.kerning = kerning;
self
}
pub fn line_gap(mut self, line_gap: usize) -> Self {
self.line_gap = line_gap;
self
}
pub fn color_mode(mut self, mode: ColorMode) -> Self {
self.color_mode = mode;
self
}
pub fn render(&self) -> String {
self.render_with_sweep(None, None)
}
pub fn animate_sweep(&self, speed_ms: u64, highlight: Option<Color>) -> io::Result<()> {
let mut stdout = io::stdout();
write!(stdout, "\x1b[2J\x1b[?25l")?;
stdout.flush()?;
let frames = 180;
let frame_time = Duration::from_millis(speed_ms);
let highlight = highlight.unwrap_or(Color::Rgb(255, 255, 255));
let base = self.light_sweep.unwrap_or_else(|| {
LightSweep::new(SweepDirection::DiagonalDown)
.width(0.25)
.intensity(0.9)
.softness(2.5)
});
let start = base.center - 0.75;
let end = base.center + 0.75;
for frame in 0..frames {
let t = frame as f32 / frames as f32;
let center = start + t * (end - start);
let sweep = base.center(center);
let banner = self.render_with_sweep(Some(sweep), Some(highlight));
write!(stdout, "\x1b[H{banner}")?;
stdout.flush()?;
thread::sleep(frame_time);
}
writeln!(stdout, "\x1b[?25h")?;
Ok(())
}
pub fn animate_wave(
&self,
speed_ms: u64,
dim_strength: Option<f32>,
bright_strength: Option<f32>,
) -> io::Result<()> {
let mut stdout = io::stdout();
write!(stdout, "\x1b[2J\x1b[?25l")?;
stdout.flush()?;
let frames = 180;
let frame_time = Duration::from_millis(speed_ms);
let base = self.render_grid_with_sweep(None, None);
let dim_strength = dim_strength.unwrap_or(0.35).clamp(0.0, 1.0);
let bright_strength = bright_strength.unwrap_or(0.2).clamp(0.0, 1.0);
let mode = match self.color_mode {
ColorMode::Auto => detect_color_mode(),
other => other,
};
for frame in 0..frames {
let t = frame as f32 / frames as f32;
let phase = t * std::f32::consts::TAU;
let waved = apply_wave_breathe(&base, phase, dim_strength, bright_strength);
let banner = emit_ansi(&waved, mode);
write!(stdout, "\x1b[H{banner}")?;
stdout.flush()?;
thread::sleep(frame_time);
}
writeln!(stdout, "\x1b[?25h")?;
Ok(())
}
fn render_with_sweep(
&self,
sweep_override: Option<LightSweep>,
highlight: Option<Color>,
) -> String {
let grid = self.render_grid_with_sweep(sweep_override, highlight);
let mode = match self.color_mode {
ColorMode::Auto => detect_color_mode(),
other => other,
};
emit_ansi(&grid, mode)
}
fn render_grid_with_sweep(
&self,
sweep_override: Option<LightSweep>,
highlight: Option<Color>,
) -> Grid {
let mut grid = render_text(&self.text, &self.font, self.kerning, self.line_gap);
apply_fill(&mut grid, self.fill);
if let Some(gradient) = &self.gradient {
gradient.apply(&mut grid);
}
if let Some(sweep) = sweep_override.or(self.light_sweep) {
let highlight = highlight.unwrap_or(Color::Rgb(255, 255, 255));
apply_light_sweep_tint(&mut grid, sweep, highlight);
}
if let Some(dither) = self.dot_dither {
let default_targets = ['â–‘', 'â–’'];
let targets = self
.dot_dither_targets
.as_deref()
.unwrap_or(&default_targets);
grid = apply_dot_dither(&grid, dither, targets);
}
if let Some(shade) = self.edge_shade {
grid = apply_edge_shade(&grid, shade);
}
if let Some(shadow) = self.shadow {
grid = apply_shadow(&grid, shadow);
}
apply_layout(grid, self.padding, self.width, self.max_width, self.align)
}
}
pub struct DotDitherBuilder {
banner: Banner,
targets: Vec<char>,
dots: (char, char),
}
impl DotDitherBuilder {
fn new(banner: Banner) -> Self {
Self {
banner,
targets: vec!['â–‘', 'â–’'],
dots: ('â–‘', 'â–‘'),
}
}
pub fn targets(mut self, targets: &str) -> Self {
self.targets = targets.chars().collect();
self
}
pub fn targets_vec(mut self, targets: &[char]) -> Self {
self.targets = targets.to_vec();
self
}
pub fn dots(mut self, dots: &str) -> Self {
self.dots = parse_dots(dots);
self
}
pub fn checker(mut self, period: u8) -> Banner {
let dither = Dither {
mode: crate::fill::DitherMode::Checker { period },
dot: self.dots.0,
alt: self.dots.1,
};
self.banner = self
.banner
.dot_dither(dither)
.dot_dither_targets(&self.targets);
self.banner
}
pub fn noise(mut self, seed: u32, threshold: u8) -> Banner {
let dither = Dither {
mode: crate::fill::DitherMode::Noise { seed, threshold },
dot: self.dots.0,
alt: self.dots.1,
};
self.banner = self
.banner
.dot_dither(dither)
.dot_dither_targets(&self.targets);
self.banner
}
}
fn parse_dots(dots: &str) -> (char, char) {
let mut iter = dots.chars();
let first = iter.next().unwrap_or('·');
let second = iter.next().unwrap_or(first);
(first, second)
}
fn apply_layout(
mut grid: Grid,
padding: Padding,
width: Option<usize>,
max_width: Option<usize>,
align: Align,
) -> Grid {
let height = grid.height();
let width_now = grid.width();
let padded_width = width_now + padding.left + padding.right;
let padded_height = height + padding.top + padding.bottom;
let mut padded = Grid::new(padded_height, padded_width);
padded.blit(&grid, padding.top, padding.left);
grid = padded;
let mut target_width = width;
if let Some(max_width) = max_width {
let limit = grid.width().min(max_width);
target_width = Some(target_width.map_or(limit, |w| w.min(max_width)));
}
if let Some(target) = target_width {
if target > grid.width() {
let extra = target - grid.width();
let left_extra = match align {
Align::Left => 0,
Align::Center => extra / 2,
Align::Right => extra,
};
let right_extra = extra - left_extra;
let mut expanded = Grid::new(grid.height(), target);
expanded.blit(&grid, 0, left_extra);
if right_extra > 0 {
}
grid = expanded;
} else if target < grid.width() {
grid = clip_width(&grid, target, align);
}
}
grid
}
fn clip_width(grid: &Grid, target: usize, align: Align) -> Grid {
if target == 0 {
return Grid::new(grid.height(), 0);
}
let start = match align {
Align::Left => 0,
Align::Center => (grid.width().saturating_sub(target)) / 2,
Align::Right => grid.width().saturating_sub(target),
};
let mut out = Grid::new(grid.height(), target);
for r in 0..grid.height() {
for c in 0..target {
if let (Some(cell), Some(target_cell)) = (grid.cell(r, start + c), out.cell_mut(r, c)) {
*target_cell = cell.clone();
}
}
}
out
}
fn apply_wave_breathe(grid: &Grid, phase: f32, dim_strength: f32, bright_strength: f32) -> Grid {
let height = grid.height();
let width = grid.width();
if height == 0 || width == 0 {
return grid.clone();
}
let mut out = grid.clone();
for row in 0..height {
for col in 0..width {
let wave = scale_wave(phase, row, col, width, height);
let (dim, bright) = if wave < 0.5 {
let t = (0.5 - wave) / 0.5;
(dim_strength * t, 0.0)
} else {
let t = (wave - 0.5) / 0.5;
(0.0, bright_strength * t)
};
let Some(cell) = out.cell_mut(row, col) else {
continue;
};
if !cell.visible {
continue;
}
if let Some(color) = cell.fg {
cell.fg = Some(apply_breathe_color(color, dim, bright));
}
}
}
out
}
fn scale_wave(phase: f32, row: usize, col: usize, width: usize, height: usize) -> f32 {
let fx = if width > 1 {
col as f32 / (width - 1) as f32
} else {
0.0
};
let fy = if height > 1 {
row as f32 / (height - 1) as f32
} else {
0.0
};
let freq_x = 5.0;
let freq_y = 3.0;
let phase_offset = (fx * freq_x + fy * freq_y) * std::f32::consts::TAU;
((phase + phase_offset).sin() + 1.0) * 0.5
}
fn apply_breathe_color(color: Color, dim: f32, bright: f32) -> Color {
let dimmed = if dim > 0.0 {
color.lerp(Color::Rgb(0, 0, 0), dim.clamp(0.0, 1.0))
} else {
color
};
if bright > 0.0 {
dimmed.lerp(Color::Rgb(255, 255, 255), bright.clamp(0.0, 1.0))
} else {
dimmed
}
}