use std::io::{self, IsTerminal, Write};
const PALETTE_GRASS: &[u8] = &[28, 34, 40, 46, 82, 118, 154, 190];
pub fn print_banner(animation_enabled: bool) {
let stdout = io::stdout();
let no_color = std::env::var_os("NO_COLOR").is_some();
if !animation_enabled || !stdout.is_terminal() || no_color {
plain_header();
return;
}
if !animate(stdout.lock()).is_ok() {
plain_header();
}
}
fn plain_header() {
println!();
println!(" hopper init - interactive scaffold");
println!(" zero-copy Solana state, in 60 seconds");
println!();
}
fn animate(mut out: impl Write) -> io::Result<()> {
use std::{thread, time::Duration};
#[rustfmt::skip]
let figlet: [&str; 6] = [
"██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗██████╗ ",
"██║ ██║██╔═══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗",
"███████║██║ ██║██████╔╝██████╔╝█████╗ ██████╔╝",
"██╔══██║██║ ██║██╔═══╝ ██╔═══╝ ██╔══╝ ██╔══██╗",
"██║ ██║╚██████╔╝██║ ██║ ███████╗██║ ██║",
"╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝",
];
let fig: Vec<Vec<char>> = figlet.iter().map(|l| l.chars().collect()).collect();
let fig_w = fig.iter().map(|l| l.len()).max().unwrap_or(0);
let fig_h = fig.len();
let canvas_w: usize = 70;
let fig_off = canvas_w.saturating_sub(fig_w) / 2;
let tagline = "zero-copy Solana state, in 60 seconds";
let tag_chars: Vec<char> = tagline.chars().collect();
let tag_off = canvas_w.saturating_sub(tag_chars.len()) / 2;
let byline = "by hopperzero.dev";
let by_chars: Vec<char> = byline.chars().collect();
let by_off = canvas_w.saturating_sub(by_chars.len()) / 2;
let h: usize = 10;
let n_frames: usize = 18;
write!(out, "\x1b[?25l")?;
writeln!(out)?;
for _ in 0..h {
writeln!(out)?;
}
out.flush()?;
for frame in 0..n_frames {
let is_final = frame == n_frames - 1;
write!(out, "\x1b[{h}A")?;
let t_raw = frame as f32 / (n_frames - 1).max(1) as f32;
let t = ease_out_back(t_raw);
for li in 0..h {
write!(out, "\x1b[2K ")?;
match li {
1..=6 => {
let row_idx = li - 1;
let row = &fig[row_idx];
let chars_visible_f = t * (fig_w as f32 + 8.0) - row_idx as f32 * 1.5;
let chars_visible = chars_visible_f.max(0.0) as usize;
for _ in 0..fig_off {
write!(out, " ")?;
}
for (col, &ch) in row.iter().enumerate() {
if col < chars_visible {
if ch != ' ' {
write!(out, "\x1b[38;5;45m{ch}\x1b[0m")?;
} else {
write!(out, " ")?;
}
} else if col < chars_visible + 4 {
let g_idx = (frame + col + row_idx * 3) % PALETTE_GRASS.len();
let code = PALETTE_GRASS[g_idx];
if fig_h > 0 && row_idx == fig_h - 1 {
write!(out, "\x1b[38;5;{code}m·\x1b[0m")?;
} else {
write!(out, " ")?;
}
} else {
write!(out, " ")?;
}
}
}
8 if is_final => {
for _ in 0..tag_off {
write!(out, " ")?;
}
write!(out, "\x1b[1m{tagline}\x1b[0m")?;
}
9 if is_final => {
for _ in 0..by_off {
write!(out, " ")?;
}
write!(out, "\x1b[2mby \x1b[38;5;46mhopperzero.dev\x1b[0m")?;
}
_ => {}
}
writeln!(out)?;
}
out.flush()?;
if !is_final {
thread::sleep(Duration::from_millis(50));
}
}
write!(out, "\x1b[?25h")?;
writeln!(out)?;
out.flush()?;
Ok(())
}
fn ease_out_back(t: f32) -> f32 {
let c1 = 1.70158_f32;
let c3 = c1 + 1.0;
let p = t - 1.0;
1.0 + c3 * p * p * p + c1 * p * p
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ease_out_back_hits_one_at_end() {
let v = ease_out_back(1.0);
assert!((v - 1.0).abs() < 1e-5, "got {v}");
}
#[test]
fn ease_out_back_overshoots() {
let v = ease_out_back(0.7);
assert!(v > 0.7, "expected overshoot past linear, got {v}");
}
}