use crate::render::palette::{map_brightness_rgb, IntensityMapping, Palette, RgbColor};
use crate::simulation::config::TransitionStyle;
use crate::terminal::frame_buffer::{Cell, FrameBuffer};
pub const TRANSITION_SECS: f32 = 1.8;
const INK_DARK: RgbColor = RgbColor { r: 6, g: 8, b: 10 };
const PAPER: RgbColor = RgbColor {
r: 244,
g: 246,
b: 250,
};
const SHADE: [char; 6] = [' ', '·', '░', '▒', '▓', '█'];
pub struct TransitionView {
pub style: TransitionStyle,
pub name: String,
pub tagline: Option<String>,
pub elapsed: f32,
}
#[derive(Clone)]
pub struct PaletteCtx<'a> {
pub palette: Palette,
pub reverse: bool,
pub invert: bool,
pub hue_shift: f32,
pub mapping: Option<&'a IntensityMapping>,
}
fn envelope(t: f32) -> f32 {
let p = (t / TRANSITION_SECS).clamp(0.0, 1.0);
if p < 0.18 {
(p / 0.18).powf(0.7)
} else if p < 0.72 {
1.0
} else {
(1.0 - (p - 0.72) / 0.28).clamp(0.0, 1.0)
}
}
fn ease_out(p: f32) -> f32 {
let p = p.clamp(0.0, 1.0);
1.0 - (1.0 - p) * (1.0 - p)
}
fn lerp_u8(a: u8, b: u8, t: f32) -> u8 {
(a as f32 + (b as f32 - a as f32) * t)
.round()
.clamp(0.0, 255.0) as u8
}
fn lerp_rgb(a: RgbColor, b: RgbColor, t: f32) -> RgbColor {
RgbColor {
r: lerp_u8(a.r, b.r, t),
g: lerp_u8(a.g, b.g, t),
b: lerp_u8(a.b, b.b, t),
}
}
fn sample(ctx: &PaletteCtx, brightness: f32) -> RgbColor {
map_brightness_rgb(
brightness.clamp(0.0, 1.0),
ctx.palette.clone(),
ctx.reverse,
ctx.invert,
ctx.hue_shift,
ctx.mapping,
)
}
fn put_cell(buf: &mut FrameBuffer, x: usize, y: usize, ch: char, fg: RgbColor) {
let mut cell = Cell::new(ch);
cell.fg_color_rgb = Some(fg);
cell.fg_color_256 = Some(crate::render::palette::rgb_to_256(fg));
buf.set_cell(x, y, cell);
}
fn put_str(buf: &mut FrameBuffer, x: usize, y: usize, s: &str, fg: RgbColor) {
for (i, ch) in s.chars().enumerate() {
put_cell(buf, x + i, y, ch, fg);
}
}
fn dim_cell(buf: &mut FrameBuffer, x: usize, y: usize, amt: f32) {
if x >= buf.width || y * buf.width + x >= buf.cells.len() {
return;
}
let cur = read_fg(buf, x, y);
let dimmed = lerp_rgb(cur, INK_DARK, amt.clamp(0.0, 1.0));
let ch = buf.cells[y * buf.width + x].char;
put_cell(buf, x, y, ch, dimmed);
}
fn read_fg(buf: &FrameBuffer, x: usize, y: usize) -> RgbColor {
let idx = y * buf.width + x;
buf.cells
.get(idx)
.and_then(|c| c.fg_color_rgb)
.unwrap_or(INK_DARK)
}
fn glyph(ch: char) -> [&'static str; 6] {
match ch.to_ascii_uppercase() {
'A' => [".##.", "#..#", "#..#", "####", "#..#", "#..#"],
'B' => ["###.", "#..#", "###.", "#..#", "#..#", "###."],
'C' => [".###", "#...", "#...", "#...", "#...", ".###"],
'D' => ["###.", "#..#", "#..#", "#..#", "#..#", "###."],
'E' => ["####", "#...", "###.", "#...", "#...", "####"],
'F' => ["####", "#...", "###.", "#...", "#...", "#..."],
'G' => [".###", "#...", "#...", "#.##", "#..#", ".###"],
'H' => ["#..#", "#..#", "####", "#..#", "#..#", "#..#"],
'I' => ["####", ".##.", ".##.", ".##.", ".##.", "####"],
'J' => ["####", "...#", "...#", "...#", "#..#", ".##."],
'K' => ["#..#", "#.#.", "##..", "##..", "#.#.", "#..#"],
'L' => ["#...", "#...", "#...", "#...", "#...", "####"],
'M' => ["#..#", "####", "####", "#..#", "#..#", "#..#"],
'N' => ["#..#", "##.#", "##.#", "#.##", "#.##", "#..#"],
'O' => [".##.", "#..#", "#..#", "#..#", "#..#", ".##."],
'P' => ["###.", "#..#", "###.", "#...", "#...", "#..."],
'Q' => [".##.", "#..#", "#..#", "#..#", "#.#.", ".###"],
'R' => ["###.", "#..#", "###.", "##..", "#.#.", "#..#"],
'S' => [".###", "#...", ".##.", "...#", "...#", "###."],
'T' => ["####", ".##.", ".##.", ".##.", ".##.", ".##."],
'U' => ["#..#", "#..#", "#..#", "#..#", "#..#", ".##."],
'V' => ["#..#", "#..#", "#..#", "#..#", ".##.", ".##."],
'W' => ["#..#", "#..#", "#..#", "####", "####", "#..#"],
'X' => ["#..#", "#..#", ".##.", ".##.", "#..#", "#..#"],
'Y' => ["#..#", "#..#", ".##.", ".##.", ".##.", ".##."],
'Z' => ["####", "...#", ".##.", "##..", "#...", "####"],
'-' => ["....", "....", "####", "####", "....", "...."],
_ => ["....", "....", "....", "....", "....", "...."], }
}
const GLYPH_W: usize = 4;
const GLYPH_H: usize = 6;
fn figlet_width(name: &str, px_w: usize) -> usize {
let n = name.chars().count();
n * (GLYPH_W * px_w) + n.saturating_sub(1)
}
fn fit_px_w(name: &str, avail: usize) -> usize {
if figlet_width(name, 2) <= avail {
2
} else {
1
}
}
pub fn draw_transition(
buf: &mut FrameBuffer,
view: &TransitionView,
width: usize,
height: usize,
content_x: (usize, usize),
ctx: &PaletteCtx,
) {
if view.elapsed >= TRANSITION_SECS || width == 0 || height == 0 {
return;
}
let name = view.name.to_uppercase();
match view.style {
TransitionStyle::Off | TransitionStyle::Toast => {}
TransitionStyle::Figlet => draw_figlet(
buf,
&name,
view.tagline.as_deref(),
width,
height,
view.elapsed,
ctx,
),
TransitionStyle::Type => draw_type(
buf,
&name,
view.tagline.as_deref(),
width,
height,
content_x,
view.elapsed,
ctx,
),
}
}
fn draw_figlet(
buf: &mut FrameBuffer,
name: &str,
tagline: Option<&str>,
width: usize,
height: usize,
t: f32,
ctx: &PaletteCtx,
) {
let alpha = envelope(t);
if alpha <= 0.0 {
return;
}
let px_w = fit_px_w(name, width);
let adv = GLYPH_W * px_w + 1; let fw = figlet_width(name, px_w);
let block_h = if tagline.is_some() {
GLYPH_H + 2
} else {
GLYPH_H
};
let x0 = width.saturating_sub(fw) / 2;
let y0 = height.saturating_sub(block_h) / 2;
let pad_x = px_w;
let sx0 = x0.saturating_sub(pad_x);
let sx1 = (x0 + fw + pad_x).min(width);
let sy0 = y0.saturating_sub(1);
let sy1 = (y0 + block_h + 1).min(height);
let scrim = 0.82 * alpha;
for yy in sy0..sy1 {
for xx in sx0..sx1 {
dim_cell(buf, xx, yy, scrim);
}
}
for (gi, ch) in name.chars().enumerate() {
let rows = glyph(ch);
let gx = x0 + gi * adv;
for (ry, rowstr) in rows.iter().enumerate() {
let yy = y0 + ry;
if yy >= height {
continue;
}
let grad = ry as f32 / (GLYPH_H as f32 - 1.0);
let on = lerp_rgb(sample(ctx, 0.55), sample(ctx, 1.0), grad);
for (cx, pix) in rowstr.chars().enumerate() {
if pix != '#' {
continue;
}
for dx in 0..px_w {
let cc = gx + cx * px_w + dx;
if cc >= width {
continue;
}
let shown = lerp_rgb(read_fg(buf, cc, yy), on, alpha);
let g = SHADE[((alpha * 5.0).round() as usize).min(5)];
put_cell(buf, cc, yy, g, shown);
}
}
}
}
if let Some(tag) = tagline {
let tw = tag.chars().count();
let tx = width.saturating_sub(tw) / 2;
let ty = y0 + GLYPH_H + 1;
if ty < height {
let tcol = lerp_rgb(INK_DARK, sample(ctx, 0.7), alpha);
put_str(buf, tx, ty, tag, tcol);
}
}
}
#[allow(clippy::too_many_arguments)]
fn draw_type(
buf: &mut FrameBuffer,
name: &str,
tagline: Option<&str>,
width: usize,
height: usize,
content_x: (usize, usize),
t: f32,
ctx: &PaletteCtx,
) {
let p = (t / TRANSITION_SECS).clamp(0.0, 1.0);
let n = name.chars().count();
let typed = ((p / 0.45) * n as f32).floor().clamp(0.0, n as f32) as usize;
let fade = if p < 0.85 {
1.0
} else {
ease_out(1.0 - (p - 0.85) / 0.15)
};
if fade <= 0.0 {
return;
}
let y = height / 2;
let r0 = y.saturating_sub(1);
let band_bottom = if tagline.is_some() { y + 3 } else { y + 1 };
let r1 = band_bottom.min(height.saturating_sub(1));
let (bx0, bx1) = (content_x.0.min(width), content_x.1.min(width));
let amt = 0.86 * fade;
for ry in r0..=r1 {
for cx in bx0..bx1 {
dim_cell(buf, cx, ry, amt);
}
}
let shown: String = name.chars().take(typed).collect();
let spaced: String = shown
.chars()
.flat_map(|c| [c, ' '])
.collect::<String>()
.trim_end()
.to_string();
let caret = (t * 3.0).sin() > 0.0 && p < 0.6;
let line = if caret {
format!("{} \u{2588}", spaced)
} else {
spaced
};
let full_w = n * 2;
let x = width.saturating_sub(full_w) / 2;
let ink = lerp_rgb(sample(ctx, 1.0), PAPER, 0.55);
let col = lerp_rgb(INK_DARK, ink, fade);
if y < height {
put_str(buf, x, y, &line, col);
}
if typed >= n {
let rule_w = (n * 2).saturating_sub(1).min(width);
let ra = ((p - 0.45) / 0.2).clamp(0.0, 1.0) * fade;
let rcol = lerp_rgb(INK_DARK, sample(ctx, 0.85), ra);
if y + 1 < height {
for i in 0..rule_w {
if x + i < width {
put_cell(buf, x + i, y + 1, '─', rcol);
}
}
}
if let Some(tag) = tagline {
let ta = ((p - 0.45) / 0.25).clamp(0.0, 1.0) * fade;
let tw = tag.chars().count();
let tx = width.saturating_sub(tw) / 2;
if y + 3 < height {
let tcol = lerp_rgb(INK_DARK, sample(ctx, 0.7), ta);
put_str(buf, tx, y + 3, tag, tcol);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn style_parses_round_trips() {
for s in ["toast", "figlet", "type"] {
let p: TransitionStyle = s.parse().unwrap();
assert_eq!(p.to_string(), s);
}
assert!("nope".parse::<TransitionStyle>().is_err());
}
#[test]
fn figlet_width_grows_with_length() {
assert!(figlet_width("AB", 2) > figlet_width("A", 2));
assert_eq!(figlet_width("A", 2), GLYPH_W * 2);
}
#[test]
fn fit_px_w_shrinks_long_names() {
assert_eq!(fit_px_w("ABC", 200), 2);
assert_eq!(fit_px_w("CONSTELLATIONS", 100), 1);
}
#[test]
fn default_is_off() {
assert_eq!(TransitionStyle::default(), TransitionStyle::Off);
}
}