use std::fmt;
use std::str::FromStr;
use crate::color::{Color, Gradient, GradientDirection};
use crate::frame::{Cell, Frame};
use crate::rng::hash_f32;
const LINE_SPINNER: [char; 4] = ['-', '\\', '|', '/'];
const TAU: f32 = std::f32::consts::PI * 2.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LoaderKind {
Bar,
Tqdm,
Blocks,
Meter,
Battery,
Gauge,
Dots,
Pulse,
Line,
Bounce,
Scanner,
Snake,
Pong,
Marquee,
Steps,
Download,
Upload,
Equalizer,
Wave,
Matrix,
Spark,
Orbit,
Brackets,
Crawl,
Braided,
Burn,
Decrypt,
Rain,
Thunderstorm,
LaserEtch,
VhsTape,
Blackhole,
Fireworks,
Pour,
SynthGrid,
Beams,
Smoke,
BinaryPath,
BouncyBalls,
Bubbles,
ColorShift,
Crumble,
ErrorCorrect,
Expand,
Highlight,
MiddleOut,
OrbittingVolley,
Overflow,
Print,
RandomSequence,
Rings,
Scattered,
Slice,
Slide,
Spotlights,
Spray,
Swarm,
Sweep,
Unstable,
Waves,
Wipe,
Ember,
Cipher,
Glitch,
Vortex,
StormFlicker,
LaserSweep,
Cascade,
GridPulse,
}
impl LoaderKind {
pub const ALL: [LoaderKind; 69] = [
LoaderKind::Bar,
LoaderKind::Tqdm,
LoaderKind::Blocks,
LoaderKind::Meter,
LoaderKind::Battery,
LoaderKind::Gauge,
LoaderKind::Dots,
LoaderKind::Pulse,
LoaderKind::Line,
LoaderKind::Bounce,
LoaderKind::Scanner,
LoaderKind::Snake,
LoaderKind::Pong,
LoaderKind::Marquee,
LoaderKind::Steps,
LoaderKind::Download,
LoaderKind::Upload,
LoaderKind::Equalizer,
LoaderKind::Wave,
LoaderKind::Matrix,
LoaderKind::Spark,
LoaderKind::Orbit,
LoaderKind::Brackets,
LoaderKind::Crawl,
LoaderKind::Braided,
LoaderKind::Burn,
LoaderKind::Decrypt,
LoaderKind::Rain,
LoaderKind::Thunderstorm,
LoaderKind::LaserEtch,
LoaderKind::VhsTape,
LoaderKind::Blackhole,
LoaderKind::Fireworks,
LoaderKind::Pour,
LoaderKind::SynthGrid,
LoaderKind::Beams,
LoaderKind::Smoke,
LoaderKind::BinaryPath,
LoaderKind::BouncyBalls,
LoaderKind::Bubbles,
LoaderKind::ColorShift,
LoaderKind::Crumble,
LoaderKind::ErrorCorrect,
LoaderKind::Expand,
LoaderKind::Highlight,
LoaderKind::MiddleOut,
LoaderKind::OrbittingVolley,
LoaderKind::Overflow,
LoaderKind::Print,
LoaderKind::RandomSequence,
LoaderKind::Rings,
LoaderKind::Scattered,
LoaderKind::Slice,
LoaderKind::Slide,
LoaderKind::Spotlights,
LoaderKind::Spray,
LoaderKind::Swarm,
LoaderKind::Sweep,
LoaderKind::Unstable,
LoaderKind::Waves,
LoaderKind::Wipe,
LoaderKind::Ember,
LoaderKind::Cipher,
LoaderKind::Glitch,
LoaderKind::Vortex,
LoaderKind::StormFlicker,
LoaderKind::LaserSweep,
LoaderKind::Cascade,
LoaderKind::GridPulse,
];
pub fn all() -> &'static [LoaderKind] {
&Self::ALL
}
pub fn name(self) -> &'static str {
match self {
LoaderKind::Bar => "bar",
LoaderKind::Tqdm => "tqdm",
LoaderKind::Blocks => "blocks",
LoaderKind::Meter => "meter",
LoaderKind::Battery => "battery",
LoaderKind::Gauge => "gauge",
LoaderKind::Dots => "dots",
LoaderKind::Pulse => "pulse",
LoaderKind::Line => "line",
LoaderKind::Bounce => "bounce",
LoaderKind::Scanner => "scanner",
LoaderKind::Snake => "snake",
LoaderKind::Pong => "pong",
LoaderKind::Marquee => "marquee",
LoaderKind::Steps => "steps",
LoaderKind::Download => "download",
LoaderKind::Upload => "upload",
LoaderKind::Equalizer => "equalizer",
LoaderKind::Wave => "wave",
LoaderKind::Matrix => "matrix",
LoaderKind::Spark => "spark",
LoaderKind::Orbit => "orbit",
LoaderKind::Brackets => "brackets",
LoaderKind::Crawl => "crawl",
LoaderKind::Braided => "braided",
LoaderKind::Burn => "burn",
LoaderKind::Decrypt => "decrypt",
LoaderKind::Rain => "rain",
LoaderKind::Thunderstorm => "thunderstorm",
LoaderKind::LaserEtch => "laseretch",
LoaderKind::VhsTape => "vhstape",
LoaderKind::Blackhole => "blackhole",
LoaderKind::Fireworks => "fireworks",
LoaderKind::Pour => "pour",
LoaderKind::SynthGrid => "synthgrid",
LoaderKind::Beams => "beams",
LoaderKind::Smoke => "smoke",
LoaderKind::BinaryPath => "binarypath",
LoaderKind::BouncyBalls => "bouncyballs",
LoaderKind::Bubbles => "bubbles",
LoaderKind::ColorShift => "colorshift",
LoaderKind::Crumble => "crumble",
LoaderKind::ErrorCorrect => "errorcorrect",
LoaderKind::Expand => "expand",
LoaderKind::Highlight => "highlight",
LoaderKind::MiddleOut => "middleout",
LoaderKind::OrbittingVolley => "orbittingvolley",
LoaderKind::Overflow => "overflow",
LoaderKind::Print => "print",
LoaderKind::RandomSequence => "randomsequence",
LoaderKind::Rings => "rings",
LoaderKind::Scattered => "scattered",
LoaderKind::Slice => "slice",
LoaderKind::Slide => "slide",
LoaderKind::Spotlights => "spotlights",
LoaderKind::Spray => "spray",
LoaderKind::Swarm => "swarm",
LoaderKind::Sweep => "sweep",
LoaderKind::Unstable => "unstable",
LoaderKind::Waves => "waves",
LoaderKind::Wipe => "wipe",
LoaderKind::Ember => "ember",
LoaderKind::Cipher => "cipher",
LoaderKind::Glitch => "glitch",
LoaderKind::Vortex => "vortex",
LoaderKind::StormFlicker => "stormflicker",
LoaderKind::LaserSweep => "lasersweep",
LoaderKind::Cascade => "cascade",
LoaderKind::GridPulse => "gridpulse",
}
}
pub fn is_effect_style(self) -> bool {
matches!(
self,
LoaderKind::Equalizer
| LoaderKind::Matrix
| LoaderKind::Burn
| LoaderKind::Decrypt
| LoaderKind::Rain
| LoaderKind::Thunderstorm
| LoaderKind::LaserEtch
| LoaderKind::VhsTape
| LoaderKind::Blackhole
| LoaderKind::Fireworks
| LoaderKind::Pour
| LoaderKind::SynthGrid
| LoaderKind::Beams
| LoaderKind::Smoke
| LoaderKind::BinaryPath
| LoaderKind::BouncyBalls
| LoaderKind::Bubbles
| LoaderKind::ColorShift
| LoaderKind::Crumble
| LoaderKind::ErrorCorrect
| LoaderKind::Expand
| LoaderKind::Highlight
| LoaderKind::MiddleOut
| LoaderKind::OrbittingVolley
| LoaderKind::Overflow
| LoaderKind::Print
| LoaderKind::RandomSequence
| LoaderKind::Rings
| LoaderKind::Scattered
| LoaderKind::Slice
| LoaderKind::Slide
| LoaderKind::Spotlights
| LoaderKind::Spray
| LoaderKind::Swarm
| LoaderKind::Sweep
| LoaderKind::Unstable
| LoaderKind::Waves
| LoaderKind::Wipe
| LoaderKind::Ember
| LoaderKind::Cipher
| LoaderKind::Glitch
| LoaderKind::Vortex
| LoaderKind::StormFlicker
| LoaderKind::LaserSweep
| LoaderKind::Cascade
| LoaderKind::GridPulse
)
}
}
impl fmt::Display for LoaderKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
impl FromStr for LoaderKind {
type Err = UnknownLoaderError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let normalized = input.to_ascii_lowercase().replace(['_', '-', ' '], "");
let kind = match normalized.as_str() {
"bar" | "progress" | "progressbar" => LoaderKind::Bar,
"tqdm" | "tqdmstyle" | "pythonprogress" => LoaderKind::Tqdm,
"blocks" | "block" => LoaderKind::Blocks,
"meter" => LoaderKind::Meter,
"battery" => LoaderKind::Battery,
"gauge" => LoaderKind::Gauge,
"dots" | "ellipsis" => LoaderKind::Dots,
"pulse" => LoaderKind::Pulse,
"line" | "spinner" | "linespinner" => LoaderKind::Line,
"bounce" | "bouncing" => LoaderKind::Bounce,
"scanner" | "scan" => LoaderKind::Scanner,
"snake" => LoaderKind::Snake,
"pong" => LoaderKind::Pong,
"marquee" | "scroll" => LoaderKind::Marquee,
"steps" | "pipeline" => LoaderKind::Steps,
"download" | "downloadbar" => LoaderKind::Download,
"upload" | "uploadbar" => LoaderKind::Upload,
"equalizer" | "eq" => LoaderKind::Equalizer,
"wave" => LoaderKind::Wave,
"matrix" | "binary" => LoaderKind::Matrix,
"spark" | "sparks" => LoaderKind::Spark,
"orbit" | "orbiter" => LoaderKind::Orbit,
"brackets" | "bracket" => LoaderKind::Brackets,
"crawl" | "crawler" => LoaderKind::Crawl,
"braided" | "braid" => LoaderKind::Braided,
"burn" | "fire" => LoaderKind::Burn,
"decrypt" | "decryptdown" => LoaderKind::Decrypt,
"rain" => LoaderKind::Rain,
"thunderstorm" | "storm" | "lightning" => LoaderKind::Thunderstorm,
"laseretch" | "laser" | "etch" => LoaderKind::LaserEtch,
"vhstape" | "vhs" => LoaderKind::VhsTape,
"blackhole" | "gravity" => LoaderKind::Blackhole,
"fireworks" | "firework" => LoaderKind::Fireworks,
"pour" => LoaderKind::Pour,
"synthgrid" | "grid" => LoaderKind::SynthGrid,
"beams" | "beam" => LoaderKind::Beams,
"smoke" | "smolder" => LoaderKind::Smoke,
"binarypath" | "binarytrail" | "path" => LoaderKind::BinaryPath,
"bouncyballs" | "bouncy" | "balls" => LoaderKind::BouncyBalls,
"bubbles" | "bubble" => LoaderKind::Bubbles,
"colorshift" | "colorcycle" | "shift" => LoaderKind::ColorShift,
"crumble" | "fragment" | "fragments" => LoaderKind::Crumble,
"errorcorrect" | "errorcorrection" | "correct" => LoaderKind::ErrorCorrect,
"expand" | "expansion" => LoaderKind::Expand,
"highlight" | "hilite" => LoaderKind::Highlight,
"middleout" | "centerout" => LoaderKind::MiddleOut,
"orbittingvolley" | "orbitingvolley" | "volley" => LoaderKind::OrbittingVolley,
"overflow" => LoaderKind::Overflow,
"print" | "printer" | "type" => LoaderKind::Print,
"randomsequence" | "random" => LoaderKind::RandomSequence,
"rings" | "ring" => LoaderKind::Rings,
"scattered" | "scatter" => LoaderKind::Scattered,
"slice" => LoaderKind::Slice,
"slide" => LoaderKind::Slide,
"spotlights" | "spotlight" => LoaderKind::Spotlights,
"spray" => LoaderKind::Spray,
"swarm" => LoaderKind::Swarm,
"sweep" => LoaderKind::Sweep,
"unstable" | "jitter" => LoaderKind::Unstable,
"waves" => LoaderKind::Waves,
"wipe" => LoaderKind::Wipe,
"ember" | "firepulse" => LoaderKind::Ember,
"cipher" | "cipherspin" => LoaderKind::Cipher,
"glitch" | "vhsdistort" => LoaderKind::Glitch,
"vortex" | "swirl" => LoaderKind::Vortex,
"stormflicker" | "stormlight" | "flicker" => LoaderKind::StormFlicker,
"lasersweep" | "sweepbeam" => LoaderKind::LaserSweep,
"cascade" | "cascading" => LoaderKind::Cascade,
"gridpulse" | "pulsegrid" => LoaderKind::GridPulse,
_ => return Err(UnknownLoaderError(input.to_string())),
};
Ok(kind)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnknownLoaderError(String);
impl fmt::Display for UnknownLoaderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown loader {:?}", self.0)
}
}
impl std::error::Error for UnknownLoaderError {}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LoaderProgress {
pub fraction: f32,
pub current: Option<u64>,
pub total: Option<u64>,
}
impl LoaderProgress {
pub fn new(fraction: f32) -> Self {
Self {
fraction: fraction.clamp(0.0, 1.0),
current: None,
total: None,
}
}
pub fn from_counts(current: u64, total: u64) -> Self {
let fraction = if total == 0 {
0.0
} else {
current as f32 / total as f32
};
Self {
fraction: fraction.clamp(0.0, 1.0),
current: Some(current),
total: Some(total),
}
}
pub fn from_current(current: u64) -> Self {
Self {
fraction: 0.0,
current: Some(current),
total: None,
}
}
pub fn fraction(self) -> f32 {
self.fraction.clamp(0.0, 1.0)
}
pub fn percent(self) -> usize {
(self.fraction() * 100.0).round() as usize
}
}
impl From<f32> for LoaderProgress {
fn from(value: f32) -> Self {
Self::new(value)
}
}
impl From<(u64, u64)> for LoaderProgress {
fn from(value: (u64, u64)) -> Self {
Self::from_counts(value.0, value.1)
}
}
#[derive(Clone, Debug)]
pub struct LoaderConfig {
pub duration: usize,
pub width: usize,
pub height: usize,
pub bar_width: Option<usize>,
pub seed: u64,
pub label: String,
pub unit: String,
pub show_percent: bool,
pub show_fraction: bool,
pub gradient: Gradient,
pub gradient_direction: GradientDirection,
pub text_color: Color,
pub track_color: Color,
pub background: Option<Color>,
pub no_color: bool,
}
impl Default for LoaderConfig {
fn default() -> Self {
Self {
duration: 120,
width: 48,
height: 1,
bar_width: None,
seed: 1,
label: "loading".to_string(),
unit: String::new(),
show_percent: true,
show_fraction: false,
gradient: Gradient::terminal_text_default(),
gradient_direction: GradientDirection::Horizontal,
text_color: Color::rgb(220, 230, 236),
track_color: Color::rgb(72, 82, 91),
background: None,
no_color: false,
}
}
}
impl LoaderConfig {
pub fn with_duration(mut self, duration: usize) -> Self {
self.duration = duration.max(1);
self
}
pub fn with_size(mut self, width: usize, height: usize) -> Self {
self.width = width.max(1);
self.height = height.max(1);
self
}
pub fn with_width(mut self, width: usize) -> Self {
self.width = width.max(1);
self
}
pub fn with_height(mut self, height: usize) -> Self {
self.height = height.max(1);
self
}
pub fn with_bar_width(mut self, width: usize) -> Self {
self.bar_width = Some(width.max(1));
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
self.unit = unit.into();
self
}
pub fn with_percent(mut self, show_percent: bool) -> Self {
self.show_percent = show_percent;
self
}
pub fn with_fraction(mut self, show_fraction: bool) -> Self {
self.show_fraction = show_fraction;
self
}
pub fn with_gradient(mut self, gradient: Gradient, direction: GradientDirection) -> Self {
self.gradient = gradient;
self.gradient_direction = direction;
self
}
pub fn with_background(mut self, background: Color) -> Self {
self.background = Some(background);
self
}
pub fn without_color(mut self) -> Self {
self.no_color = true;
self
}
}
#[derive(Clone, Debug)]
pub struct Loader {
kind: LoaderKind,
config: LoaderConfig,
}
impl Loader {
pub fn new(kind: LoaderKind) -> Self {
Self {
kind,
config: LoaderConfig::default(),
}
}
pub fn with_config(kind: LoaderKind, config: LoaderConfig) -> Self {
Self { kind, config }
}
pub fn kind(&self) -> LoaderKind {
self.kind
}
pub fn config(&self) -> &LoaderConfig {
&self.config
}
pub fn config_mut(&mut self) -> &mut LoaderConfig {
&mut self.config
}
pub fn frame(&self, tick: usize, progress: impl Into<LoaderProgress>) -> Frame {
render_loader(self.kind, &self.config, tick, progress.into())
}
pub fn line(&self, tick: usize, progress: impl Into<LoaderProgress>) -> String {
trim_plain(&self.frame(tick, progress))
}
pub fn iter(&self) -> LoaderFrames {
LoaderFrames::new(self.clone())
}
pub fn frames(&self) -> Vec<Frame> {
self.iter().collect()
}
}
pub struct LoaderFrames {
loader: Loader,
next_frame: usize,
total_frames: usize,
}
impl LoaderFrames {
fn new(loader: Loader) -> Self {
let total_frames = loader.config.duration.max(1);
Self {
loader,
next_frame: 0,
total_frames,
}
}
}
impl Iterator for LoaderFrames {
type Item = Frame;
fn next(&mut self) -> Option<Self::Item> {
if self.next_frame >= self.total_frames {
return None;
}
let progress = if self.total_frames == 1 {
1.0
} else {
self.next_frame as f32 / (self.total_frames - 1) as f32
};
let frame = self.loader.frame(self.next_frame, progress);
self.next_frame += 1;
Some(frame)
}
}
fn render_loader(
kind: LoaderKind,
config: &LoaderConfig,
tick: usize,
progress: LoaderProgress,
) -> Frame {
match kind {
LoaderKind::Equalizer => render_equalizer(config, tick, progress),
LoaderKind::Matrix => render_matrix(config, tick, progress),
LoaderKind::Burn => render_burn(config, tick, progress),
LoaderKind::Decrypt => render_decrypt(config, tick, progress),
LoaderKind::Rain => render_rain(config, tick, progress),
LoaderKind::Thunderstorm => render_thunderstorm(config, tick, progress),
LoaderKind::LaserEtch => render_laseretch(config, tick, progress),
LoaderKind::VhsTape => render_vhstape(config, tick, progress),
LoaderKind::Blackhole => render_blackhole(config, tick, progress),
LoaderKind::Fireworks => render_fireworks(config, tick, progress),
LoaderKind::Pour => render_pour(config, tick, progress),
LoaderKind::SynthGrid => render_synthgrid(config, tick, progress),
LoaderKind::Beams => render_beams(config, tick, progress),
LoaderKind::Smoke => render_smoke(config, tick, progress),
LoaderKind::BinaryPath => render_binary_path(config, tick, progress),
LoaderKind::BouncyBalls => render_bouncy_balls(config, tick, progress),
LoaderKind::Bubbles => render_bubbles(config, tick, progress),
LoaderKind::ColorShift => render_color_shift(config, tick, progress),
LoaderKind::Crumble => render_crumble(config, tick, progress),
LoaderKind::ErrorCorrect => render_error_correct(config, tick, progress),
LoaderKind::Expand => render_expand(config, tick, progress),
LoaderKind::Highlight => render_highlight(config, tick, progress),
LoaderKind::MiddleOut => render_middle_out(config, tick, progress),
LoaderKind::OrbittingVolley => render_orbitting_volley(config, tick, progress),
LoaderKind::Overflow => render_overflow(config, tick, progress),
LoaderKind::Print => render_print_loader(config, tick, progress),
LoaderKind::RandomSequence => render_random_sequence(config, tick, progress),
LoaderKind::Rings => render_rings(config, tick, progress),
LoaderKind::Scattered => render_scattered(config, tick, progress),
LoaderKind::Slice => render_slice(config, tick, progress),
LoaderKind::Slide => render_slide(config, tick, progress),
LoaderKind::Spotlights => render_spotlights(config, tick, progress),
LoaderKind::Spray => render_spray(config, tick, progress),
LoaderKind::Swarm => render_swarm(config, tick, progress),
LoaderKind::Sweep => render_sweep(config, tick, progress),
LoaderKind::Unstable => render_unstable(config, tick, progress),
LoaderKind::Waves => render_waves(config, tick, progress),
LoaderKind::Wipe => render_wipe(config, tick, progress),
LoaderKind::Ember => render_ember(config, tick, progress),
LoaderKind::Cipher => render_cipher(config, tick, progress),
LoaderKind::Glitch => render_glitch(config, tick, progress),
LoaderKind::Vortex => render_vortex(config, tick, progress),
LoaderKind::StormFlicker => render_storm_flicker(config, tick, progress),
LoaderKind::LaserSweep => render_laser_sweep(config, tick, progress),
LoaderKind::Cascade => render_cascade(config, tick, progress),
LoaderKind::GridPulse => render_grid_pulse(config, tick, progress),
_ => render_line_loader(kind, config, tick, progress),
}
}
fn render_line_loader(
kind: LoaderKind,
config: &LoaderConfig,
tick: usize,
progress: LoaderProgress,
) -> Frame {
let mut frame = empty_frame(config);
let y = config.height / 2;
let line = loader_line(kind, config, tick, progress);
draw_text(&mut frame, config, y, &line, tick, progress.fraction());
frame
}
fn render_equalizer(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let height = frame.height();
let width = frame.width();
let usable_height = height.saturating_sub(1).max(1);
for x in 0..width {
let phase = x as f32 * 0.42 + tick as f32 * 0.22;
let noise = hash_f32(config.seed, x as u64, tick as u64 / 2, 17);
let wave = ((phase.sin() + 1.0) * 0.5 * 0.75 + noise * 0.25).clamp(0.0, 1.0);
let bar_height = (wave * usable_height as f32).round() as usize + 1;
let active = x as f32 / width.max(1) as f32 <= ratio || ratio <= 0.0;
for dy in 0..bar_height.min(height) {
let y = height - 1 - dy;
let ch = if active { '#' } else { '.' };
frame.set(
x,
y,
styled_cell(config, ch, x, y, width, height, tick, active),
);
}
}
if height > 1 {
let headline = append_suffix(config.label.clone(), progress_suffix(config, progress));
draw_text(&mut frame, config, 0, &headline, tick, ratio);
}
frame
}
fn render_matrix(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let height = frame.height();
let width = frame.width();
for y in 0..height {
for x in 0..width {
let noise = hash_f32(config.seed ^ 0x51a7, x as u64, y as u64, (tick / 2) as u64);
if noise > 0.42 {
let bit = if noise > 0.71 { '1' } else { '0' };
let active = x as f32 / width.max(1) as f32 <= ratio || ratio <= 0.0;
frame.set(
x,
y,
styled_cell(config, bit, x, y, width, height, tick, active),
);
}
}
}
let headline = append_suffix(config.label.clone(), progress_suffix(config, progress));
draw_text(&mut frame, config, 0, &headline, tick, ratio);
frame
}
fn render_burn(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
for x in 0..width {
let burning = x < active;
let base = if burning { '#' } else { '-' };
let color = if burning {
fire_color(0, tick + x)
} else {
config.track_color
};
frame.set(
x,
height - 1,
effect_cell(config, base, color, burning, !burning),
);
if burning && height > 1 {
let flame = 1
+ (hash_f32(config.seed ^ 0xb12d, x as u64, tick as u64, 0)
* height.saturating_sub(1) as f32)
.round() as usize;
for dy in 1..=flame.min(height - 1) {
let y = height - 1 - dy;
let ch = match (x + tick + dy) % 4 {
0 => '^',
1 => '*',
2 => '+',
_ => '.',
};
frame.set(
x,
y,
effect_cell(config, ch, fire_color(dy, tick + x), true, dy > 3),
);
}
} else if height > 2 && hash_f32(config.seed, x as u64, tick as u64, 9) > 0.94 {
let y = height - 2;
frame.set(
x,
y,
effect_cell(config, '.', Color::rgb(120, 116, 108), false, true),
);
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_decrypt(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let target = append_suffix(config.label.clone(), progress_suffix(config, progress));
let target: Vec<char> = target.chars().take(width).collect();
let chars = ['0', '1', 'A', 'F', '7', '#', '$', '%'];
for x in 0..width {
let arrival = x as f32 / target.len().max(1) as f32 * 0.72;
let local = ((ratio - arrival) / 0.24).clamp(0.0, 1.0);
let drop_y = (local * height.saturating_sub(1) as f32).round() as usize;
if x < target.len() {
for y in 0..=drop_y.min(height - 1) {
let final_row = local >= 1.0 && y == height - 1;
let ch = if final_row {
target[x]
} else {
chars[(x + y + tick) % chars.len()]
};
let color = if final_row {
Color::rgb(180, 255, 220)
} else {
Color::rgb(54, 255, 148).dim(0.55 + local * 0.45)
};
frame.set(x, y, effect_cell(config, ch, color, true, !final_row));
}
} else if hash_f32(config.seed ^ 0xdec0, x as u64, tick as u64 / 2, 0) > 0.88 {
let y = (tick + x) % height;
let ch = chars[(x + tick) % chars.len()];
frame.set(
x,
y,
effect_cell(config, ch, Color::rgb(40, 170, 110), false, true),
);
}
}
frame
}
fn render_rain(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
for x in 0..width {
let speed = 1 + (hash_f32(config.seed, x as u64, 0, 0) * 3.0).round() as usize;
let offset = (hash_f32(config.seed ^ 0xa17, x as u64, 0, 0) * height as f32) as usize;
let head = (tick / speed + offset) % (height + 3);
for trail in 0..3 {
if let Some(y) = head.checked_sub(trail) {
if y < height {
let ch = if trail == 0 { 'v' } else { '|' };
let color = if trail == 0 {
Color::rgb(154, 218, 255)
} else {
Color::rgb(62, 128, 180).dim(1.0 - trail as f32 * 0.22)
};
frame.set(x, y, effect_cell(config, ch, color, trail == 0, trail > 0));
}
}
}
}
draw_progress_row(&mut frame, config, height - 1, ratio, tick, '#', '.');
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_thunderstorm(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = render_rain(config, tick, progress);
let width = frame.width();
let height = frame.height();
if height > 1 && tick % 19 < 5 {
let mut x = ((tick * 7 + config.seed as usize) % width).min(width - 1);
for y in 0..height {
frame.set(
x,
y,
effect_cell(config, '/', Color::rgb(255, 245, 150), true, false),
);
if y % 2 == 1 && x + 1 < width {
x += 1;
} else if x > 0 {
x -= 1;
}
}
}
frame
}
fn render_laseretch(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let scan_x = (ratio * width.saturating_sub(1) as f32).round() as usize;
let row = height / 2;
for x in 0..width {
let ch = if x < scan_x {
'#'
} else if x == scan_x {
'*'
} else {
'-'
};
let color = if x < scan_x {
Color::rgb(60, 240, 210)
} else if x == scan_x {
Color::rgb(255, 242, 126)
} else {
config.track_color
};
frame.set(
x,
row,
effect_cell(config, ch, color, x <= scan_x, x > scan_x),
);
}
for y in 0..height {
frame.set(
scan_x,
y,
effect_cell(config, '|', Color::rgb(255, 90, 72), true, false),
);
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_vhstape(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
for y in 0..height {
for x in 0..width {
let noise = hash_f32(config.seed ^ 0x745, x as u64, y as u64, tick as u64 / 2);
if y % 2 == 0 && noise > 0.55 {
let ch = if noise > 0.88 { '#' } else { '-' };
frame.set(
x,
y,
effect_cell(config, ch, Color::rgb(90, 110, 130), false, true),
);
} else if noise > 0.96 {
frame.set(
x,
y,
effect_cell(config, '*', Color::rgb(255, 80, 210), true, false),
);
}
}
}
let text = append_suffix(config.label.clone(), progress_suffix(config, progress));
let jitter = (hash_f32(config.seed, tick as u64, 0, 0) * 5.0).round() as i32 - 2;
let y = height / 2;
for (x, ch) in text.chars().take(width).enumerate() {
let gx = x as i32 + jitter;
if gx >= 0 {
let color = if x as f32 / width.max(1) as f32 <= ratio {
Color::rgb(116, 244, 255)
} else {
Color::rgb(255, 90, 190)
};
frame.set_i32(gx, y as i32, effect_cell(config, ch, color, true, false));
}
}
frame
}
fn render_blackhole(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let cx = width as f32 / 2.0;
let cy = height as f32 / 2.0;
let max_radius = cx.max(cy).max(1.0);
let radius = (1.0 - ratio) * max_radius + 1.0;
for y in 0..height {
for x in 0..width {
let dx = x as f32 - cx;
let dy = y as f32 - cy;
let dist = (dx * dx + dy * dy).sqrt();
let orbit = (dist - radius).abs() < 0.8;
let noise = hash_f32(config.seed ^ 0xb1a, x as u64, y as u64, tick as u64 / 3);
if orbit || noise > 0.97 {
let ch = if dist < 1.5 { '@' } else { '*' };
let color = Color::rgb(132, 105, 255).blend(Color::rgb(60, 238, 255), ratio);
frame.set(x, y, effect_cell(config, ch, color, true, !orbit));
}
}
}
draw_progress_row(&mut frame, config, height - 1, ratio, tick, '#', '.');
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_fireworks(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
for burst in 0..5usize {
let bx = ((burst * 17 + tick / 2) % width) as f32;
let by = (height as f32 * (0.25 + hash_f32(config.seed, burst as u64, 0, 0) * 0.45))
.min(height.saturating_sub(1) as f32);
let radius = ((tick + burst * 7) % 12) as f32 / 2.8 + ratio * 2.0;
for y in 0..height {
for x in 0..width {
let dx = x as f32 - bx;
let dy = y as f32 - by;
let dist = (dx * dx + dy * dy).sqrt();
if (dist - radius).abs() < 0.38 {
let color = config
.gradient
.color_at(((burst as f32 / 5.0) + ratio).fract());
frame.set(x, y, effect_cell(config, '*', color, true, false));
}
}
}
}
draw_progress_row(&mut frame, config, height - 1, ratio, tick, '#', '.');
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_pour(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
for x in 0..width {
let fill = if x < active {
height
} else if x == active {
(tick % height.max(1)) + 1
} else {
0
};
for dy in 0..fill.min(height) {
let y = height - 1 - dy;
let ch = if dy == fill.saturating_sub(1) {
'v'
} else {
'#'
};
let color = Color::rgb(74, 190, 255)
.blend(Color::rgb(0, 255, 160), dy as f32 / height.max(1) as f32);
frame.set(x, y, effect_cell(config, ch, color, true, false));
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_synthgrid(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
for y in 0..height {
for x in 0..width {
let diag = (x + y + tick / 2) % 7 == 0 || (x + height - y + tick / 3) % 9 == 0;
if diag {
let active = x as f32 / width.max(1) as f32 <= ratio;
let color = if active {
Color::rgb(255, 70, 220)
} else {
Color::rgb(56, 72, 110)
};
frame.set(
x,
y,
effect_cell(
config,
if (x + y) % 2 == 0 { '/' } else { '\\' },
color,
active,
!active,
),
);
}
}
}
draw_progress_row(&mut frame, config, height - 1, ratio, tick, '#', '.');
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_beams(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let vertical = tick % width.max(1);
let horizontal = tick % height.max(1);
for x in 0..width {
let active = x as f32 / width.max(1) as f32 <= ratio;
frame.set(
x,
horizontal,
effect_cell(config, '=', Color::rgb(255, 210, 90), true, !active),
);
}
for y in 0..height {
frame.set(
vertical,
y,
effect_cell(config, '|', Color::rgb(80, 220, 255), true, false),
);
}
draw_progress_row(&mut frame, config, height - 1, ratio, tick, '#', '.');
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_smoke(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
for x in 0..width {
if x < active {
frame.set(
x,
height - 1,
effect_cell(config, '#', Color::rgb(255, 112, 76), true, false),
);
}
for y in 0..height.saturating_sub(1) {
let noise = hash_f32(config.seed ^ 0x5a0, x as u64, y as u64, tick as u64 / 2);
let drift = (tick + y * 3) % width.max(1);
if x < active && (x + drift) % 5 == 0 && noise > 0.35 {
let ch = match (x + y + tick) % 3 {
0 => '.',
1 => 'o',
_ => '~',
};
let fade = 0.95 - y as f32 / height.max(1) as f32 * 0.55;
frame.set(
x,
y,
effect_cell(config, ch, Color::rgb(160, 166, 170).dim(fade), false, true),
);
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_binary_path(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let row = height / 2;
let active = active_columns(width, ratio);
for x in 0..width {
let start_y = if x % 2 == 0 { 0 } else { height - 1 };
let arrival = x as f32 / width.max(1) as f32 * 0.75;
let local = ((ratio - arrival) / 0.22).clamp(0.0, 1.0);
let y = (start_y as f32 + (row as f32 - start_y as f32) * local).round() as usize;
let bit = if (x + tick) % 2 == 0 { '1' } else { '0' };
let color = if x < active {
Color::rgb(88, 255, 158)
} else {
Color::rgb(48, 150, 108)
};
frame.set(
x,
y.min(height - 1),
effect_cell(config, bit, color, x < active, x >= active),
);
if x < active {
frame.set(
x,
row,
effect_cell(config, '#', Color::rgb(182, 255, 220), true, false),
);
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_bouncy_balls(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
draw_progress_row(&mut frame, config, height - 1, ratio, tick, '=', '.');
for ball in 0..7usize {
let base =
(ratio * width as f32 + ball as f32 * 7.0 + tick as f32 * 0.35) % width.max(1) as f32;
let bounce = ((tick as f32 * 0.28 + ball as f32 * 0.9).sin().abs()
* height.saturating_sub(2).max(1) as f32)
.round() as usize;
let x = base.round() as usize % width.max(1);
let y = height
.saturating_sub(2)
.saturating_sub(bounce.min(height.saturating_sub(2)));
let color = config.gradient.color_at(ball as f32 / 6.0);
frame.set(x, y, effect_cell(config, 'O', color, true, false));
if y + 1 < height {
frame.set(
x,
y + 1,
effect_cell(config, '.', color.dim(0.45), false, true),
);
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_bubbles(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
draw_progress_row(&mut frame, config, height - 1, ratio, tick, '#', '.');
for x in 0..active.min(width) {
let offset = (hash_f32(config.seed ^ 0xb0b, x as u64, 0, 0) * height as f32) as usize;
let y = height - 1 - ((tick / 2 + offset) % height.max(1));
let size = hash_f32(config.seed, x as u64, tick as u64 / 8, 0);
let ch = if size > 0.72 {
'O'
} else if size > 0.38 {
'o'
} else {
'.'
};
let color = Color::rgb(90, 226, 255).blend(Color::rgb(188, 255, 240), size);
frame.set(x, y, effect_cell(config, ch, color, ch != '.', ch == '.'));
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_color_shift(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let row = height / 2;
let active = active_columns(width, ratio);
for x in 0..width {
let phase = ((x + tick) % width.max(1)) as f32 / width.max(1) as f32;
let color = config.gradient.color_at(phase);
let ch = if x < active { '#' } else { '-' };
frame.set(
x,
row,
effect_cell(config, ch, color, x < active, x >= active),
);
if height > 2 && x % 3 == tick % 3 {
frame.set(
x,
row.saturating_sub(1),
effect_cell(config, '.', color.dim(0.55), false, true),
);
frame.set(
x,
(row + 1).min(height - 1),
effect_cell(config, '.', color.dim(0.55), false, true),
);
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_crumble(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
for x in 0..width {
if x < active {
frame.set(
x,
height - 1,
effect_cell(config, '#', Color::rgb(220, 212, 190), true, false),
);
} else {
let noise = hash_f32(config.seed ^ 0xc9, x as u64, tick as u64 / 2, 0);
let y = ((noise * height as f32) as usize + tick / 3) % height.max(1);
let ch = if noise > 0.7 { '*' } else { '.' };
frame.set(
x,
y,
effect_cell(config, ch, Color::rgb(174, 144, 108), false, true),
);
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_error_correct(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let row = height / 2;
let active = active_columns(width, ratio);
let wrong = ['x', '?', '!', '0', '1', '%'];
for x in 0..width {
let near = x.abs_diff(active) < 4;
let ch = if x < active {
'#'
} else if near {
wrong[(x + tick) % wrong.len()]
} else {
'.'
};
let color = if x < active {
Color::rgb(98, 255, 164)
} else if near {
Color::rgb(255, 78, 78)
} else {
config.track_color
};
frame.set(
x,
row,
effect_cell(config, ch, color, x < active || near, x >= active && !near),
);
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_expand(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let cx = width / 2;
let cy = height / 2;
let radius = (ratio * width.max(height) as f32 / 2.0).round() as usize;
for y in 0..height {
for x in 0..width {
let d = x.abs_diff(cx) + y.abs_diff(cy);
if d <= radius {
let color = config.gradient.coordinate_color(
x,
y,
width,
height,
config.gradient_direction,
);
frame.set(x, y, effect_cell(config, '#', color, true, false));
} else if d == radius + 1 && tick % 2 == 0 {
frame.set(
x,
y,
effect_cell(config, '.', Color::rgb(98, 116, 132), false, true),
);
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_highlight(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let row = height / 2;
let active = active_columns(width, ratio);
let band = tick % width.max(1);
for x in 0..width {
let lit = x.abs_diff(band) <= 2;
let done = x < active;
let color = if lit {
Color::rgb(255, 246, 142)
} else if done {
Color::rgb(86, 220, 255)
} else {
config.track_color
};
frame.set(
x,
row,
effect_cell(
config,
if done { '#' } else { '-' },
color,
done || lit,
!done && !lit,
),
);
if lit && row > 0 {
frame.set(
x,
row - 1,
effect_cell(config, '*', color.dim(0.75), true, false),
);
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_middle_out(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let row = height / 2;
let center = width / 2;
let radius = (ratio * width as f32 / 2.0).round() as usize;
for x in 0..width {
let done = x.abs_diff(center) <= radius;
let color = if done {
Color::rgb(255, 182, 82)
} else {
config.track_color
};
frame.set(
x,
row,
effect_cell(config, if done { '#' } else { '-' }, color, done, !done),
);
}
if tick % 5 == 0 && width > 0 {
frame.set(
center,
row,
effect_cell(config, '*', Color::rgb(255, 252, 164), true, false),
);
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_orbitting_volley(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let cx = width as f32 / 2.0;
let cy = height as f32 / 2.0;
let orbit = (height.min(width).max(2) as f32 / 2.4).max(1.0);
for launcher in 0..4usize {
let angle = tick as f32 * 0.15 + launcher as f32 * TAU / 4.0;
let x = (cx + angle.cos() * orbit).round() as i32;
let y = (cy + angle.sin() * orbit * 0.55).round() as i32;
frame.set_i32(
x,
y,
effect_cell(config, '@', Color::rgb(255, 170, 78), true, false),
);
let target_x = (ratio * width.saturating_sub(1) as f32).round() as i32;
let target_y = (height - 1) as i32;
for step in 1..=3i32 {
let t = step as f32 / 4.0;
let bx = (x as f32 + (target_x as f32 - x as f32) * t).round() as i32;
let by = (y as f32 + (target_y as f32 - y as f32) * t).round() as i32;
frame.set_i32(
bx,
by,
effect_cell(config, '*', Color::rgb(255, 226, 128), true, false),
);
}
}
draw_progress_row(&mut frame, config, height - 1, ratio, tick, '#', '.');
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_overflow(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
for y in 0..height {
let offset = if y % 2 == 0 {
tick
} else {
width + width - tick % width.max(1)
};
for x in 0..width {
let flow = (x + offset + y * 3) % 9;
if flow < 4 {
let active = x as f32 / width.max(1) as f32 <= ratio;
let color = if active {
Color::rgb(92, 214, 255)
} else {
Color::rgb(74, 82, 94)
};
frame.set(
x,
y,
effect_cell(
config,
if flow == 0 { '>' } else { '=' },
color,
active,
!active,
),
);
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_print_loader(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let text = append_suffix(config.label.clone(), progress_suffix(config, progress));
let count = active_columns(text.chars().count(), ratio);
for (x, ch) in text.chars().take(count.min(width)).enumerate() {
frame.set(
x,
height / 2,
effect_cell(config, ch, Color::rgb(224, 238, 244), true, false),
);
}
let head = count.min(width.saturating_sub(1));
for y in 0..height {
frame.set(
head,
y,
effect_cell(config, '|', Color::rgb(255, 190, 82), true, false),
);
}
draw_progress_row(&mut frame, config, height - 1, ratio, tick, '#', '.');
frame
}
fn render_random_sequence(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
for y in 0..height {
for x in 0..width {
let rank = hash_f32(config.seed ^ 0x5e9, x as u64, y as u64, 0);
if rank < ratio || hash_f32(config.seed, x as u64, y as u64, tick as u64 / 3) > 0.96 {
let color = config.gradient.coordinate_color(
x,
y,
width,
height,
config.gradient_direction,
);
frame.set(
x,
y,
effect_cell(config, '#', color, rank < ratio, rank >= ratio),
);
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_rings(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let cx = width as f32 / 2.0;
let cy = height as f32 / 2.0;
for y in 0..height {
for x in 0..width {
let dx = x as f32 - cx;
let dy = (y as f32 - cy) * 2.0;
let dist = (dx * dx + dy * dy).sqrt();
let ring = (dist - (ratio * width as f32 / 2.0 + (tick % 5) as f32)).abs() < 0.7;
if ring || (dist % 5.0) < 0.25 {
let color = Color::rgb(172, 130, 255).blend(Color::rgb(64, 255, 210), ratio);
frame.set(
x,
y,
effect_cell(config, if ring { 'o' } else { '.' }, color, ring, !ring),
);
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_scattered(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let row = height / 2;
for x in 0..width {
let start_y = (hash_f32(config.seed ^ 0x5ca, x as u64, 0, 0) * height as f32) as usize;
let local = ((ratio - x as f32 / width.max(1) as f32 * 0.6) / 0.4).clamp(0.0, 1.0);
let y = (start_y as f32 + (row as f32 - start_y as f32) * local).round() as usize;
let color = if local >= 1.0 {
Color::rgb(124, 234, 255)
} else {
Color::rgb(128, 140, 170)
};
frame.set(
x,
y.min(height - 1),
effect_cell(
config,
if local >= 1.0 { '#' } else { '*' },
color,
local > 0.8,
local < 0.8,
),
);
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_slice(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let row = height / 2;
let active = active_columns(width, ratio);
for x in 0..active.min(width) {
let top_y = ((row as f32) * (1.0 - ratio)).round() as usize;
let bot_y = height - 1 - (((height - 1 - row) as f32) * (1.0 - ratio)).round() as usize;
frame.set(
x,
top_y.min(height - 1),
effect_cell(config, '/', Color::rgb(255, 112, 188), true, false),
);
frame.set(
x,
bot_y.min(height - 1),
effect_cell(config, '\\', Color::rgb(84, 220, 255), true, false),
);
frame.set(
x,
row,
effect_cell(config, '#', Color::rgb(238, 238, 246), true, false),
);
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_slide(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
for y in 0..height {
let offset = ((1.0 - ratio) * width as f32).round() as usize;
for i in 0..active.min(width) {
let x = if y % 2 == 0 {
i.saturating_sub(offset)
} else {
width
.saturating_sub(1)
.saturating_sub(i)
.saturating_add(offset)
.min(width - 1)
};
let color =
config
.gradient
.coordinate_color(x, y, width, height, config.gradient_direction);
frame.set(x, y, effect_cell(config, '#', color, true, false));
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_spotlights(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
let centers = [
tick % width.max(1),
(tick * 3 + width / 3) % width.max(1),
(tick * 5 + width * 2 / 3) % width.max(1),
];
for x in 0..width {
let lit = centers.iter().any(|center| x.abs_diff(*center) <= 3);
let done = x < active;
for y in 0..height {
if lit || (done && y == height - 1) {
let color = if lit {
Color::rgb(255, 246, 168)
} else {
Color::rgb(76, 190, 255)
};
let ch = if y == height - 1 { '#' } else { '.' };
frame.set(x, y, effect_cell(config, ch, color, lit || done, !lit));
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_spray(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
for particle in 0..width.min(80) {
let target_x = particle.min(active.saturating_sub(1));
let local = ((tick + particle * 3) % 32) as f32 / 31.0;
let arc = (local * std::f32::consts::PI).sin();
let x = (target_x as f32 * local).round() as usize;
let y = (height.saturating_sub(1) as f32 * (1.0 - arc * 0.85)).round() as usize;
let color = config
.gradient
.color_at(particle as f32 / width.max(1) as f32);
frame.set(
x.min(width - 1),
y.min(height - 1),
effect_cell(config, '*', color, true, false),
);
}
draw_progress_row(&mut frame, config, height - 1, ratio, tick, '#', '.');
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_swarm(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let cursor = active_columns(width, ratio).min(width.saturating_sub(1));
for particle in 0..width.min(90) {
let angle = tick as f32 * 0.16 + particle as f32 * 0.61;
let radius =
1.0 + hash_f32(config.seed, particle as u64, 0, 0) * width.min(height * 3) as f32 / 5.0;
let x = cursor as f32 + angle.cos() * radius;
let y = height as f32 / 2.0 + angle.sin() * radius * 0.45;
let color = Color::rgb(72, 255, 166).blend(
Color::rgb(255, 92, 230),
particle as f32 / width.max(1) as f32,
);
frame.set_i32(
x.round() as i32,
y.round() as i32,
effect_cell(config, '*', color, true, false),
);
}
draw_progress_row(&mut frame, config, height - 1, ratio, tick, '#', '.');
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_sweep(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
let sweep = tick % width.max(1);
for x in 0..width {
let done = x < active;
let in_band = x.abs_diff(sweep) <= 4;
for y in 0..height {
if done || in_band {
let color = if in_band {
Color::rgb(255, 238, 132)
} else {
Color::rgb(84, 228, 204)
};
frame.set(
x,
y,
effect_cell(
config,
if in_band { '|' } else { '#' },
color,
in_band || done,
!done,
),
);
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_unstable(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
for x in 0..width {
let done = x < active;
let jitter =
(hash_f32(config.seed ^ 0x99, x as u64, tick as u64, 0) * 5.0).round() as i32 - 2;
let y = (height / 2) as i32 + if done { jitter } else { 0 };
let ch = if done && tick % 7 == 0 {
'*'
} else if done {
'#'
} else {
'-'
};
let color = if done {
Color::rgb(255, 116, 86)
} else {
config.track_color
};
frame.set_i32(x as i32, y, effect_cell(config, ch, color, done, !done));
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_waves(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
for x in 0..width {
let phase = x as f32 * 0.36 + tick as f32 * 0.22;
let y = ((phase.sin() + 1.0) * 0.5 * height.saturating_sub(1) as f32).round() as usize;
let done = x < active;
let color = if done {
Color::rgb(86, 220, 255)
} else {
Color::rgb(58, 78, 118)
};
frame.set(
x,
y.min(height - 1),
effect_cell(config, if done { '~' } else { '-' }, color, done, !done),
);
if done && y + 1 < height {
frame.set(
x,
y + 1,
effect_cell(config, '.', color.dim(0.45), false, true),
);
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_wipe(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
for y in 0..height {
for x in 0..width {
if x < active {
let color = config.gradient.coordinate_color(
x,
y,
width,
height,
config.gradient_direction,
);
frame.set(x, y, effect_cell(config, '#', color, true, false));
} else if x == active && tick % 2 == 0 {
frame.set(
x,
y,
effect_cell(config, '|', Color::rgb(255, 244, 128), true, false),
);
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn loader_line(
kind: LoaderKind,
config: &LoaderConfig,
tick: usize,
progress: LoaderProgress,
) -> String {
let ratio = progress.fraction();
let suffix = progress_suffix(config, progress);
let label = config.label.as_str();
match kind {
LoaderKind::Bar => {
let prefix = format!("{label} [");
let bar_width = bar_width(config, char_count(&prefix) + char_count(&suffix) + 2);
append_suffix(
format!(
"{label} [{}]",
progress_bar(bar_width, ratio, '#', '>', '-')
),
suffix,
)
}
LoaderKind::Tqdm => {
let spinner = LINE_SPINNER[tick % LINE_SPINNER.len()];
let prefix = format!("{label} {spinner} [");
let bar_width = bar_width(config, char_count(&prefix) + char_count(&suffix) + 2);
append_suffix(
format!(
"{label} {spinner} [{}]",
progress_bar(bar_width, ratio, '#', '>', '.')
),
suffix,
)
}
LoaderKind::Blocks => append_suffix(
format!(
"{label} {}",
progress_bar(
bar_width(config, char_count(label) + char_count(&suffix) + 2),
ratio,
'#',
'#',
'.'
)
),
suffix,
),
LoaderKind::Meter => append_suffix(
format!(
"{label} |{}|",
progress_bar(
bar_width(config, char_count(label) + char_count(&suffix) + 4),
ratio,
'=',
'>',
'.'
)
),
suffix,
),
LoaderKind::Battery => append_suffix(
format!(
"{label} [{}]",
progress_bar(
bar_width(config, char_count(label) + char_count(&suffix) + 4),
ratio,
'=',
'=',
' '
)
),
suffix,
),
LoaderKind::Gauge => {
let width = bar_width(config, char_count(label) + char_count(&suffix) + 4);
append_suffix(format!("{label} [{}]", gauge(width, ratio)), suffix)
}
LoaderKind::Dots => {
let dots = match tick % 4 {
0 => "",
1 => ".",
2 => "..",
_ => "...",
};
append_suffix(format!("{label}{dots}"), suffix)
}
LoaderKind::Pulse => {
let width = bar_width(config, char_count(label) + char_count(&suffix) + 2);
append_suffix(format!("{label} {}", pulse(width, tick)), suffix)
}
LoaderKind::Line => {
let spinner = LINE_SPINNER[tick % LINE_SPINNER.len()];
append_suffix(format!("{spinner} {label}"), suffix)
}
LoaderKind::Bounce => {
let width = bar_width(config, char_count(label) + char_count(&suffix) + 4);
append_suffix(
format!("{label} [{}]", marker_track(width, tick, 'o', '.')),
suffix,
)
}
LoaderKind::Scanner => {
let width = bar_width(config, char_count(label) + char_count(&suffix) + 4);
append_suffix(format!("{label} [{}]", scanner(width, tick)), suffix)
}
LoaderKind::Snake => {
let width = bar_width(config, char_count(label) + char_count(&suffix) + 4);
append_suffix(format!("{label} [{}]", snake(width, tick)), suffix)
}
LoaderKind::Pong => {
let width = bar_width(config, char_count(label) + char_count(&suffix) + 4);
append_suffix(
format!("{label} |{}|", marker_track(width, tick, 'o', ' ')),
suffix,
)
}
LoaderKind::Marquee => {
let width = bar_width(config, char_count(&suffix) + 1);
append_suffix(marquee(label, width, tick), suffix)
}
LoaderKind::Steps => append_suffix(steps_line(ratio), suffix),
LoaderKind::Download => {
let arrow = if tick % 2 == 0 { 'v' } else { 'V' };
let width = bar_width(config, char_count(label) + char_count(&suffix) + 6);
append_suffix(
format!(
"{label} {arrow} [{}]",
progress_bar(width, ratio, '#', '>', '.')
),
suffix,
)
}
LoaderKind::Upload => {
let arrow = if tick % 2 == 0 { '^' } else { 'A' };
let width = bar_width(config, char_count(label) + char_count(&suffix) + 6);
append_suffix(
format!(
"{label} {arrow} [{}]",
progress_bar(width, ratio, '#', '>', '.')
),
suffix,
)
}
LoaderKind::Wave => {
let width = bar_width(config, char_count(label) + char_count(&suffix) + 2);
append_suffix(format!("{label} {}", wave(width, tick)), suffix)
}
LoaderKind::Spark => {
let width = bar_width(config, char_count(label) + char_count(&suffix) + 2);
append_suffix(
format!("{label} {}", spark(width, tick, config.seed)),
suffix,
)
}
LoaderKind::Orbit => {
let spinner = LINE_SPINNER[tick % LINE_SPINNER.len()];
append_suffix(format!("{label} ({spinner})"), suffix)
}
LoaderKind::Brackets => append_suffix(brackets(label, tick), suffix),
LoaderKind::Crawl => {
let width = bar_width(config, char_count(label) + char_count(&suffix) + 2);
append_suffix(format!("{label} {}", crawl(width, tick)), suffix)
}
LoaderKind::Braided => {
let width = bar_width(config, char_count(label) + char_count(&suffix) + 2);
append_suffix(format!("{label} {}", braided(width, tick)), suffix)
}
_ => String::new(),
}
}
fn render_ember(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let height = frame.height();
let width = frame.width();
let chars = ['*', '+', '.', ' ', ' '];
for y in 0..height {
for x in 0..width {
let n = hash_f32(config.seed ^ 0x7e91, x as u64, y as u64, (tick / 2) as u64);
let heat = ((tick as f32 * 0.18 + x as f32 * 0.37 + y as f32 * 0.53).sin() + 1.0) * 0.5;
let alive = n > 0.35 && (n < 0.85 || heat > 0.6);
if alive {
let ci = (heat * (chars.len() as f32 - 1.0)).round() as usize;
let ch = chars[ci.min(chars.len() - 1)];
let color = fire_color(ci, tick);
let active = x as f32 / width.max(1) as f32 <= ratio || ratio <= 0.0;
frame.set(x, y, effect_cell(config, ch, color, active, !active));
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_cipher(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let height = frame.height();
let width = frame.width();
let glyphs: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&*!"
.chars()
.collect();
for y in 0..height {
for x in 0..width {
let n = hash_f32(config.seed ^ 0xc117, x as u64, y as u64, tick as u64);
let resolved = x as f32 / width.max(1) as f32 <= ratio;
let ch = if resolved {
' '
} else {
let idx = (n * glyphs.len() as f32).floor() as usize;
glyphs[idx.min(glyphs.len() - 1)]
};
let color = if resolved {
config.track_color
} else {
let shift = ((tick % 32) as f32 / 32.0) * 0.4;
config
.gradient
.color_at((x as f32 / width.max(1) as f32 + shift).fract())
};
frame.set(x, y, effect_cell(config, ch, color, !resolved, resolved));
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_glitch(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let height = frame.height();
let width = frame.width();
for y in 0..height {
let band = hash_f32(config.seed ^ 0x51a7, y as u64, tick as u64 / 3, 42) > 0.7;
let shift = if band {
((tick as f32 * 0.4 + y as f32).sin() * 4.0).round() as isize
} else {
0
};
for x in 0..width {
let sx = x as isize + shift;
if sx < 0 || sx >= width as isize {
continue;
}
let n = hash_f32(config.seed ^ 0xa17c, sx as u64, y as u64, tick as u64 / 2);
let active = sx as f32 / width.max(1) as f32 <= ratio || ratio <= 0.0;
if band && n > 0.3 {
let ch = if n > 0.7 {
'|'
} else if n > 0.5 {
'/'
} else {
'\\'
};
let color = if active {
Color::rgb(180, 80, 255).blend(config.text_color, 0.3)
} else {
config.track_color
};
frame.set(x, y, effect_cell(config, ch, color, active, !active));
} else if active && n > 0.5 {
let ch = '#';
frame.set(
x,
y,
effect_cell(config, ch, config.text_color, true, false),
);
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_vortex(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let height = frame.height();
let width = frame.width();
let cx = width as f32 * 0.5;
let cy = height as f32 * 0.5;
let max_r = ((cx * cx + cy * cy).sqrt()).max(1.0);
for y in 0..height {
for x in 0..width {
let dx = x as f32 - cx;
let dy = y as f32 - cy;
let dist = ((dx * dx + dy * dy).sqrt()) / max_r;
let angle = dy.atan2(dx);
let swirl = angle + tick as f32 * 0.25 - dist * 2.0;
let n = hash_f32(config.seed ^ 0xb012, x as u64, y as u64, (tick / 2) as u64);
let alive = n > 0.45 && dist > 0.05;
if alive {
let spiral = (swirl.sin() + 1.0) * 0.5;
let ch = if spiral > 0.7 {
'@'
} else if spiral > 0.4 {
'o'
} else {
'.'
};
let color = config.gradient.coordinate_color(
x,
y,
width,
height,
config.gradient_direction,
);
let active = dist < ratio || ratio <= 0.0;
frame.set(
x,
y,
effect_cell(
config,
ch,
color.blend(config.text_color, spiral * 0.5),
active,
!active,
),
);
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_storm_flicker(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let height = frame.height();
let width = frame.width();
let flash = tick % 7 == 0;
for y in 0..height {
for x in 0..width {
let n = hash_f32(config.seed ^ 0x5a01, x as u64, y as u64, tick as u64);
let rain = hash_f32(config.seed ^ 0x71a23, x as u64, (tick + y) as u64, 0);
let active = x as f32 / width.max(1) as f32 <= ratio || ratio <= 0.0;
if flash && n > 0.85 {
frame.set(
x,
y,
effect_cell(config, '*', Color::rgb(255, 255, 220), true, false),
);
} else if rain > 0.55 {
let ch = if rain > 0.8 { '|' } else { '.' };
let color = Color::rgb(100, 140, 200).blend(config.track_color, rain);
frame.set(x, y, effect_cell(config, ch, color, active, !active));
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_laser_sweep(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let height = frame.height();
let width = frame.width();
let beam_x = (tick as f32 * 1.8).round() as usize % width.max(1);
let beam_width = 3.max(width / 12);
for y in 0..height {
for x in 0..width {
let dist_from_beam = x.abs_diff(beam_x);
let active = x as f32 / width.max(1) as f32 <= ratio || ratio <= 0.0;
if dist_from_beam <= beam_width {
let intensity = 1.0 - dist_from_beam as f32 / beam_width as f32;
let ch = if intensity > 0.8 {
'#'
} else if intensity > 0.4 {
'='
} else {
'-'
};
let color = Color::rgb(80, 255, 160).blend(config.text_color, 1.0 - intensity);
frame.set(x, y, effect_cell(config, ch, color, true, false));
} else if active {
let n = hash_f32(config.seed ^ 0x1a5e, x as u64, y as u64, tick as u64 / 3);
if n > 0.7 {
frame.set(
x,
y,
effect_cell(config, '.', config.track_color, false, true),
);
}
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_cascade(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let height = frame.height();
let width = frame.width();
let drops: Vec<f32> = (0..width)
.map(|x| hash_f32(config.seed ^ 0xca5c, x as u64, 0, 99) * 0.8 + 0.1)
.collect();
for y in 0..height {
for x in 0..width {
let speed = drops[x];
let phase = (tick as f32 * speed + x as f32 * 0.7) % (height as f32 + 4.0);
let drop_y = phase as usize;
let active = x as f32 / width.max(1) as f32 <= ratio || ratio <= 0.0;
if y == drop_y.min(height - 1) {
frame.set(
x,
y,
effect_cell(config, 'o', Color::rgb(100, 180, 255), true, false),
);
} else if y > 0 && drop_y > 0 && y < drop_y && drop_y - y <= 3 {
let trail = if drop_y - y == 1 { ',' } else { '.' };
let color = config.track_color.brighten(speed * 0.3);
frame.set(x, y, effect_cell(config, trail, color, active, !active));
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn render_grid_pulse(config: &LoaderConfig, tick: usize, progress: LoaderProgress) -> Frame {
let mut frame = empty_frame(config);
let ratio = progress.fraction();
let height = frame.height();
let width = frame.width();
let pulse_phase = (tick as f32 * 0.2).sin();
for y in 0..height {
for x in 0..width {
let n = hash_f32(config.seed ^ 0x9f01, x as u64, y as u64, tick as u64 / 2);
let wave = ((x as f32 * 0.5 + y as f32 * 0.3 + tick as f32 * 0.15).sin() + 1.0) * 0.5;
let bright = wave + pulse_phase * 0.3;
let alive = n > 0.3 && bright > 0.25;
if alive {
let ch = if bright > 0.75 {
'O'
} else if bright > 0.5 {
'o'
} else {
'.'
};
let active = x as f32 / width.max(1) as f32 <= ratio || ratio <= 0.0;
let color = config
.gradient
.coordinate_color(x, y, width, height, config.gradient_direction)
.blend(config.text_color, bright * 0.6);
frame.set(x, y, effect_cell(config, ch, color, active, !active));
}
}
}
draw_status(&mut frame, config, tick, progress);
frame
}
fn draw_status(frame: &mut Frame, config: &LoaderConfig, tick: usize, progress: LoaderProgress) {
let text = append_suffix(config.label.clone(), progress_suffix(config, progress));
draw_text(frame, config, 0, &text, tick, progress.fraction());
}
fn draw_progress_row(
frame: &mut Frame,
config: &LoaderConfig,
y: usize,
ratio: f32,
tick: usize,
fill: char,
empty: char,
) {
let width = frame.width();
let height = frame.height();
let active = active_columns(width, ratio);
for x in 0..width {
let is_active = x < active;
let ch = if is_active {
fill
} else if x == active && ratio > 0.0 && ratio < 1.0 {
'>'
} else {
empty
};
let color = if is_active {
config
.gradient
.coordinate_color(x, y, width, height, config.gradient_direction)
} else {
config.track_color
};
frame.set(
x,
y.min(height - 1),
effect_cell(config, ch, color, is_active || x == active, !is_active),
);
}
if tick % 11 == 0 && width > 0 && ratio > 0.0 && ratio < 1.0 {
let spark_x = active.min(width - 1);
frame.set(
spark_x,
y.min(height - 1),
effect_cell(config, '*', Color::rgb(255, 248, 160), true, false),
);
}
}
fn active_columns(width: usize, ratio: f32) -> usize {
(ratio.clamp(0.0, 1.0) * width as f32).round() as usize
}
fn effect_cell(config: &LoaderConfig, ch: char, color: Color, bold: bool, dim: bool) -> Cell {
let mut cell = Cell::new(ch);
if !config.no_color && ch != ' ' {
cell.colors.fg = Some(color);
cell.bold = bold;
cell.dim = dim;
}
cell
}
fn fire_color(depth: usize, tick: usize) -> Color {
let heat = match depth {
0 | 1 => Color::rgb(255, 216, 90),
2 => Color::rgb(255, 132, 48),
3 => Color::rgb(226, 54, 34),
_ => Color::rgb(105, 90, 86),
};
if tick % 3 == 0 {
heat.brighten(0.12)
} else {
heat
}
}
fn empty_frame(config: &LoaderConfig) -> Frame {
Frame::with_background(config.width.max(1), config.height.max(1), config.background)
}
fn draw_text(
frame: &mut Frame,
config: &LoaderConfig,
y: usize,
text: &str,
tick: usize,
ratio: f32,
) {
let width = frame.width();
let height = frame.height();
for (x, ch) in text.chars().take(width).enumerate() {
frame.set(
x,
y,
styled_cell(config, ch, x, y, width, height, tick, ratio > 0.0),
);
}
}
fn styled_cell(
config: &LoaderConfig,
ch: char,
x: usize,
y: usize,
width: usize,
height: usize,
tick: usize,
active: bool,
) -> Cell {
let mut cell = Cell::new(ch);
if config.no_color || ch == ' ' {
return cell;
}
let track = matches!(
ch,
'-' | '.' | '_' | '[' | ']' | '|' | '(' | ')' | '{' | '}'
);
let color = if track && !active {
config.track_color
} else if track {
config.track_color.brighten(0.22)
} else if ch.is_ascii_alphabetic() && ch != 'V' && ch != 'A' {
config.text_color
} else {
let base = config
.gradient
.coordinate_color(x, y, width, height, config.gradient_direction);
let shift = ((tick % 64) as f32 / 64.0) * 0.35;
base.blend(
config
.gradient
.color_at((x as f32 / width.max(1) as f32 + shift).fract()),
0.45,
)
};
cell.colors.fg = Some(color);
cell.bold = active && !track;
cell.dim = track && !active;
cell
}
fn progress_suffix(config: &LoaderConfig, progress: LoaderProgress) -> String {
let mut parts = Vec::new();
if config.show_percent {
parts.push(format!("{:>3}%", progress.percent()));
}
if config.show_fraction {
if let (Some(current), Some(total)) = (progress.current, progress.total) {
parts.push(format!(
"{}/{}",
count_with_unit(current, &config.unit),
count_with_unit(total, &config.unit)
));
} else if let Some(current) = progress.current {
parts.push(count_with_unit(current, &config.unit));
}
}
parts.join(" ")
}
fn count_with_unit(value: u64, unit: &str) -> String {
if unit.is_empty() {
value.to_string()
} else {
format!("{value}{unit}")
}
}
fn append_suffix(mut text: String, suffix: String) -> String {
if !suffix.is_empty() {
text.push(' ');
text.push_str(&suffix);
}
text
}
fn bar_width(config: &LoaderConfig, fixed_chars: usize) -> usize {
let available = config.width.saturating_sub(fixed_chars).max(3);
config
.bar_width
.unwrap_or(available)
.min(config.width)
.max(1)
}
fn progress_bar(width: usize, ratio: f32, fill: char, head: char, empty: char) -> String {
let width = width.max(1);
let ratio = ratio.clamp(0.0, 1.0);
let filled = (ratio * width as f32).floor() as usize;
let mut out = String::with_capacity(width);
for i in 0..width {
let ch = if ratio >= 1.0 || i < filled {
fill
} else if ratio > 0.0 && i == filled {
head
} else {
empty
};
out.push(ch);
}
out
}
fn gauge(width: usize, ratio: f32) -> String {
let width = width.max(1);
let needle = (ratio.clamp(0.0, 1.0) * width.saturating_sub(1) as f32).round() as usize;
(0..width)
.map(|index| if index == needle { '^' } else { '-' })
.collect()
}
fn pulse(width: usize, tick: usize) -> String {
let width = width.max(3);
let center = width / 2;
let radius = tick % (center.max(1) + 1);
(0..width)
.map(|index| {
let distance = index.abs_diff(center);
if distance <= radius && (radius - distance) < 3 {
'='
} else {
'.'
}
})
.collect()
}
fn marker_track(width: usize, tick: usize, marker: char, empty: char) -> String {
let width = width.max(1);
let pos = bounce_position(width, tick);
(0..width)
.map(|index| if index == pos { marker } else { empty })
.collect()
}
fn scanner(width: usize, tick: usize) -> String {
let width = width.max(1);
let pos = bounce_position(width, tick);
(0..width)
.map(|index| if index.abs_diff(pos) <= 1 { '>' } else { '.' })
.collect()
}
fn snake(width: usize, tick: usize) -> String {
let width = width.max(1);
let pos = tick % width;
(0..width)
.map(|index| {
let distance = (index + width - pos) % width;
match distance {
0 => 'O',
1 | 2 => 'o',
_ => '.',
}
})
.collect()
}
fn bounce_position(width: usize, tick: usize) -> usize {
if width <= 1 {
return 0;
}
let span = width * 2 - 2;
let pos = tick % span;
if pos < width {
pos
} else {
span - pos
}
}
fn marquee(label: &str, width: usize, tick: usize) -> String {
let width = width.max(1);
let source = format!("{label} {label} ");
let chars: Vec<char> = source.chars().collect();
let offset = tick % chars.len().max(1);
(0..width)
.map(|index| chars[(offset + index) % chars.len()])
.collect()
}
fn steps_line(ratio: f32) -> String {
let stages = ["fetch", "connect", "download", "verify", "done"];
let current = (ratio.clamp(0.0, 1.0) * stages.len() as f32).floor() as usize;
let mut parts = Vec::with_capacity(stages.len());
for (index, stage) in stages.iter().enumerate() {
let mark = if index < current {
'x'
} else if index == current.min(stages.len() - 1) {
'>'
} else {
'.'
};
parts.push(format!("[{mark}] {stage}"));
}
parts.join(" ")
}
fn wave(width: usize, tick: usize) -> String {
let width = width.max(1);
(0..width)
.map(|index| match (index + tick) % 6 {
0 | 5 => '_',
1 | 4 => '-',
_ => '~',
})
.collect()
}
fn spark(width: usize, tick: usize, seed: u64) -> String {
let width = width.max(1);
(0..width)
.map(|index| {
let n = hash_f32(seed ^ 0xa11c_e001, index as u64, tick as u64 / 2, 0);
if n > 0.88 {
'*'
} else if n > 0.62 {
'.'
} else {
' '
}
})
.collect()
}
fn brackets(label: &str, tick: usize) -> String {
let pairs = [('[', ']'), ('(', ')'), ('{', '}'), ('<', '>')];
let (left, right) = pairs[tick % pairs.len()];
format!("{left} {label} {right}")
}
fn crawl(width: usize, tick: usize) -> String {
let width = width.max(1);
let pos = tick % width;
(0..width)
.map(|index| {
if index == pos {
'>'
} else if index < pos {
'.'
} else {
' '
}
})
.collect()
}
fn braided(width: usize, tick: usize) -> String {
let pattern = ['>', '<', '=', '<', '>', '='];
(0..width.max(1))
.map(|index| pattern[(index + tick) % pattern.len()])
.collect()
}
fn char_count(value: &str) -> usize {
value.chars().count()
}
fn trim_plain(frame: &Frame) -> String {
let plain = frame.to_plain_string();
plain
.lines()
.map(str::trim_end)
.collect::<Vec<_>>()
.join("\n")
}