use std::fmt;
use std::str::FromStr;
use crate::color::{Color, Gradient, GradientDirection};
use crate::easing::Easing;
use crate::frame::{Cell, Frame};
use crate::rng::{hash, hash_f32, SmallRng};
use crate::text::{Glyph, TextGrid};
const TAU: f32 = std::f32::consts::PI * 2.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum EffectKind {
Beams,
BinaryPath,
Blackhole,
BouncyBalls,
Bubbles,
Burn,
ColorShift,
Crumble,
Decrypt,
ErrorCorrect,
Expand,
Fireworks,
Highlight,
LaserEtch,
Matrix,
MiddleOut,
OrbittingVolley,
Overflow,
Pour,
Print,
Rain,
RandomSequence,
Rings,
Scattered,
Slice,
Slide,
Smoke,
Spotlights,
Spray,
Swarm,
Sweep,
SynthGrid,
Thunderstorm,
Unstable,
VhsTape,
Waves,
Wipe,
}
impl EffectKind {
pub const ALL: [EffectKind; 37] = [
EffectKind::Beams,
EffectKind::BinaryPath,
EffectKind::Blackhole,
EffectKind::BouncyBalls,
EffectKind::Bubbles,
EffectKind::Burn,
EffectKind::ColorShift,
EffectKind::Crumble,
EffectKind::Decrypt,
EffectKind::ErrorCorrect,
EffectKind::Expand,
EffectKind::Fireworks,
EffectKind::Highlight,
EffectKind::LaserEtch,
EffectKind::Matrix,
EffectKind::MiddleOut,
EffectKind::OrbittingVolley,
EffectKind::Overflow,
EffectKind::Pour,
EffectKind::Print,
EffectKind::Rain,
EffectKind::RandomSequence,
EffectKind::Rings,
EffectKind::Scattered,
EffectKind::Slice,
EffectKind::Slide,
EffectKind::Smoke,
EffectKind::Spotlights,
EffectKind::Spray,
EffectKind::Swarm,
EffectKind::Sweep,
EffectKind::SynthGrid,
EffectKind::Thunderstorm,
EffectKind::Unstable,
EffectKind::VhsTape,
EffectKind::Waves,
EffectKind::Wipe,
];
pub fn all() -> &'static [EffectKind] {
&Self::ALL
}
pub fn name(self) -> &'static str {
match self {
EffectKind::Beams => "beams",
EffectKind::BinaryPath => "binarypath",
EffectKind::Blackhole => "blackhole",
EffectKind::BouncyBalls => "bouncyballs",
EffectKind::Bubbles => "bubbles",
EffectKind::Burn => "burn",
EffectKind::ColorShift => "colorshift",
EffectKind::Crumble => "crumble",
EffectKind::Decrypt => "decrypt",
EffectKind::ErrorCorrect => "errorcorrect",
EffectKind::Expand => "expand",
EffectKind::Fireworks => "fireworks",
EffectKind::Highlight => "highlight",
EffectKind::LaserEtch => "laseretch",
EffectKind::Matrix => "matrix",
EffectKind::MiddleOut => "middleout",
EffectKind::OrbittingVolley => "orbittingvolley",
EffectKind::Overflow => "overflow",
EffectKind::Pour => "pour",
EffectKind::Print => "print",
EffectKind::Rain => "rain",
EffectKind::RandomSequence => "randomsequence",
EffectKind::Rings => "rings",
EffectKind::Scattered => "scattered",
EffectKind::Slice => "slice",
EffectKind::Slide => "slide",
EffectKind::Smoke => "smoke",
EffectKind::Spotlights => "spotlights",
EffectKind::Spray => "spray",
EffectKind::Swarm => "swarm",
EffectKind::Sweep => "sweep",
EffectKind::SynthGrid => "synthgrid",
EffectKind::Thunderstorm => "thunderstorm",
EffectKind::Unstable => "unstable",
EffectKind::VhsTape => "vhstape",
EffectKind::Waves => "waves",
EffectKind::Wipe => "wipe",
}
}
}
impl fmt::Display for EffectKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
impl FromStr for EffectKind {
type Err = UnknownEffectError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let normalized = input.to_ascii_lowercase().replace(['_', '-', ' '], "");
let kind = match normalized.as_str() {
"beams" => EffectKind::Beams,
"binarypath" => EffectKind::BinaryPath,
"blackhole" => EffectKind::Blackhole,
"bouncyballs" => EffectKind::BouncyBalls,
"bubbles" => EffectKind::Bubbles,
"burn" => EffectKind::Burn,
"colorshift" => EffectKind::ColorShift,
"crumble" => EffectKind::Crumble,
"decrypt" => EffectKind::Decrypt,
"errorcorrect" => EffectKind::ErrorCorrect,
"expand" => EffectKind::Expand,
"fireworks" => EffectKind::Fireworks,
"highlight" => EffectKind::Highlight,
"laseretch" => EffectKind::LaserEtch,
"matrix" => EffectKind::Matrix,
"middleout" => EffectKind::MiddleOut,
"orbittingvolley" | "orbitingvolley" => EffectKind::OrbittingVolley,
"overflow" => EffectKind::Overflow,
"pour" => EffectKind::Pour,
"print" => EffectKind::Print,
"rain" => EffectKind::Rain,
"randomsequence" => EffectKind::RandomSequence,
"rings" => EffectKind::Rings,
"scattered" => EffectKind::Scattered,
"slice" => EffectKind::Slice,
"slide" => EffectKind::Slide,
"smoke" => EffectKind::Smoke,
"spotlights" => EffectKind::Spotlights,
"spray" => EffectKind::Spray,
"swarm" => EffectKind::Swarm,
"sweep" => EffectKind::Sweep,
"synthgrid" => EffectKind::SynthGrid,
"thunderstorm" => EffectKind::Thunderstorm,
"unstable" => EffectKind::Unstable,
"vhstape" | "vhs" => EffectKind::VhsTape,
"waves" => EffectKind::Waves,
"wipe" => EffectKind::Wipe,
_ => return Err(UnknownEffectError(input.to_string())),
};
Ok(kind)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnknownEffectError(String);
impl fmt::Display for UnknownEffectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown effect {:?}", self.0)
}
}
impl std::error::Error for UnknownEffectError {}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ExistingColorHandling {
Always,
Dynamic,
#[default]
Ignore,
}
#[derive(Clone, Debug)]
pub struct EffectConfig {
pub duration: usize,
pub hold_final_frames: usize,
pub seed: u64,
pub gradient: Gradient,
pub gradient_direction: GradientDirection,
pub easing: Easing,
pub existing_color_handling: ExistingColorHandling,
pub no_color: bool,
pub background: Option<Color>,
pub canvas_width: Option<usize>,
pub canvas_height: Option<usize>,
pub tab_width: usize,
}
impl Default for EffectConfig {
fn default() -> Self {
Self {
duration: 96,
hold_final_frames: 0,
seed: 1,
gradient: Gradient::terminal_text_default(),
gradient_direction: GradientDirection::Diagonal,
easing: Easing::InOutQuad,
existing_color_handling: ExistingColorHandling::Ignore,
no_color: false,
background: None,
canvas_width: None,
canvas_height: None,
tab_width: 4,
}
}
}
impl EffectConfig {
pub fn with_duration(mut self, duration: usize) -> Self {
self.duration = duration.max(1);
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn with_gradient(mut self, gradient: Gradient, direction: GradientDirection) -> Self {
self.gradient = gradient;
self.gradient_direction = direction;
self
}
pub fn with_canvas_size(mut self, width: usize, height: usize) -> Self {
self.canvas_width = Some(width.max(1));
self.canvas_height = Some(height.max(1));
self
}
}
#[derive(Clone, Debug)]
pub struct Effect {
kind: EffectKind,
input: String,
config: EffectConfig,
}
impl Effect {
pub fn new(kind: EffectKind, input: impl Into<String>) -> Self {
Self {
kind,
input: input.into(),
config: EffectConfig::default(),
}
}
pub fn with_config(kind: EffectKind, input: impl Into<String>, config: EffectConfig) -> Self {
Self {
kind,
input: input.into(),
config,
}
}
pub fn kind(&self) -> EffectKind {
self.kind
}
pub fn config(&self) -> &EffectConfig {
&self.config
}
pub fn config_mut(&mut self) -> &mut EffectConfig {
&mut self.config
}
pub fn iter(&self) -> EffectFrames {
EffectFrames::new(EffectPlan::new(self.kind, &self.input, self.config.clone()))
}
pub fn frames(&self) -> Vec<Frame> {
self.iter().collect()
}
pub fn final_frame(&self) -> Frame {
EffectPlan::new(self.kind, &self.input, self.config.clone()).final_frame()
}
}
pub struct EffectFrames {
plan: EffectPlan,
next_frame: usize,
total_frames: usize,
}
impl EffectFrames {
fn new(plan: EffectPlan) -> Self {
let total_frames = plan.config.duration.max(1) + plan.config.hold_final_frames;
Self {
plan,
next_frame: 0,
total_frames,
}
}
}
impl Iterator for EffectFrames {
type Item = Frame;
fn next(&mut self) -> Option<Self::Item> {
if self.next_frame >= self.total_frames {
return None;
}
let duration = self.plan.config.duration.max(1);
let t = if self.next_frame >= duration || duration == 1 {
1.0
} else {
self.next_frame as f32 / (duration - 1) as f32
};
let frame = self.plan.render(t, self.next_frame);
self.next_frame += 1;
Some(frame)
}
}
#[derive(Clone, Debug)]
struct GlyphPlan {
glyph: Glyph,
final_color: Color,
random_order: f32,
start_x: f32,
start_y: f32,
side_x: f32,
side_y: f32,
burst_x: f32,
burst_y: f32,
phase: f32,
radius: f32,
speed: f32,
}
#[derive(Clone, Debug)]
struct EffectPlan {
kind: EffectKind,
config: EffectConfig,
width: usize,
height: usize,
glyphs: Vec<GlyphPlan>,
seed: u64,
}
impl EffectPlan {
fn new(kind: EffectKind, input: &str, config: EffectConfig) -> Self {
let grid = TextGrid::parse(input, config.tab_width);
let width = config
.canvas_width
.unwrap_or(grid.width)
.max(grid.width)
.max(1);
let height = config
.canvas_height
.unwrap_or(grid.height)
.max(grid.height)
.max(1);
let seed = config.seed ^ hash(kind.name().len() as u64, kind as u64, 0, 0);
let mut rng = SmallRng::new(seed);
let len = grid.glyphs.len();
let mut shuffled: Vec<usize> = (0..len).collect();
rng.shuffle(&mut shuffled);
let mut random_rank = vec![0.0; len];
for (rank, index) in shuffled.into_iter().enumerate() {
random_rank[index] = if len <= 1 {
0.0
} else {
rank as f32 / (len - 1) as f32
};
}
let max_radius = width.max(height).max(2) as f32;
let mut glyphs = Vec::with_capacity(len);
for glyph in grid.glyphs.iter().cloned() {
let final_color = config.gradient.coordinate_color(
glyph.x,
glyph.y,
width,
height,
config.gradient_direction,
);
let side = rng.usize(4);
let (side_x, side_y) = match side {
0 => (-1.0, rng.range_f32(0.0, height.saturating_sub(1) as f32)),
1 => (
width as f32,
rng.range_f32(0.0, height.saturating_sub(1) as f32),
),
2 => (rng.range_f32(0.0, width.saturating_sub(1) as f32), -1.0),
_ => (
rng.range_f32(0.0, width.saturating_sub(1) as f32),
height as f32,
),
};
let burst_angle = rng.range_f32(0.0, TAU);
let burst_radius = rng.range_f32(1.0, max_radius * 0.35 + 1.0);
glyphs.push(GlyphPlan {
random_order: random_rank[glyph.index],
start_x: rng.range_f32(0.0, width.saturating_sub(1) as f32),
start_y: rng.range_f32(0.0, height.saturating_sub(1) as f32),
side_x,
side_y,
burst_x: glyph.x as f32 + burst_angle.cos() * burst_radius,
burst_y: glyph.y as f32 + burst_angle.sin() * burst_radius,
phase: rng.range_f32(0.0, TAU),
radius: rng.range_f32(1.0, max_radius * 0.5 + 1.0),
speed: rng.range_f32(0.65, 1.45),
final_color,
glyph,
});
}
Self {
kind,
config,
width,
height,
glyphs,
seed,
}
}
fn render(&self, t: f32, frame_number: usize) -> Frame {
if t >= 1.0 {
return self.final_frame();
}
match self.kind {
EffectKind::Beams => self.render_beams(t, frame_number),
EffectKind::BinaryPath => self.render_binary_path(t, frame_number),
EffectKind::Blackhole => self.render_blackhole(t, frame_number),
EffectKind::BouncyBalls => self.render_bouncy_balls(t),
EffectKind::Bubbles => self.render_bubbles(t),
EffectKind::Burn => self.render_burn(t, frame_number),
EffectKind::ColorShift => self.render_color_shift(t),
EffectKind::Crumble => self.render_crumble(t, frame_number),
EffectKind::Decrypt => self.render_decrypt(t, frame_number),
EffectKind::ErrorCorrect => self.render_error_correct(t, frame_number),
EffectKind::Expand => self.render_expand(t),
EffectKind::Fireworks => self.render_fireworks(t, frame_number),
EffectKind::Highlight => self.render_highlight(t),
EffectKind::LaserEtch => self.render_laser_etch(t, frame_number),
EffectKind::Matrix => self.render_matrix(t, frame_number),
EffectKind::MiddleOut => self.render_middle_out(t),
EffectKind::OrbittingVolley => self.render_orbitting_volley(t, frame_number),
EffectKind::Overflow => self.render_overflow(t, frame_number),
EffectKind::Pour => self.render_pour(t),
EffectKind::Print => self.render_print(t),
EffectKind::Rain => self.render_rain(t, frame_number),
EffectKind::RandomSequence => self.render_random_sequence(t, frame_number),
EffectKind::Rings => self.render_rings(t),
EffectKind::Scattered => self.render_scattered(t),
EffectKind::Slice => self.render_slice(t),
EffectKind::Slide => self.render_slide(t),
EffectKind::Smoke => self.render_smoke(t, frame_number),
EffectKind::Spotlights => self.render_spotlights(t),
EffectKind::Spray => self.render_spray(t, frame_number),
EffectKind::Swarm => self.render_swarm(t),
EffectKind::Sweep => self.render_sweep(t),
EffectKind::SynthGrid => self.render_synth_grid(t, frame_number),
EffectKind::Thunderstorm => self.render_thunderstorm(t, frame_number),
EffectKind::Unstable => self.render_unstable(t, frame_number),
EffectKind::VhsTape => self.render_vhs_tape(t, frame_number),
EffectKind::Waves => self.render_waves(t),
EffectKind::Wipe => self.render_wipe(t),
}
}
fn blank_frame(&self) -> Frame {
Frame::with_background(self.width, self.height, self.config.background)
}
fn final_frame(&self) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
frame.set(plan.glyph.x, plan.glyph.y, self.final_cell(plan));
}
frame
}
fn final_cell(&self, plan: &GlyphPlan) -> Cell {
let mut cell = plan.glyph.cell.clone();
match self.config.existing_color_handling {
ExistingColorHandling::Ignore => {
cell.colors.fg = Some(plan.final_color);
cell.colors.bg = None;
}
ExistingColorHandling::Always | ExistingColorHandling::Dynamic => {
if cell.colors.fg.is_none() {
cell.colors.fg = Some(plan.final_color);
}
}
}
if self.config.no_color {
cell.colors.fg = None;
cell.colors.bg = None;
}
cell
}
fn temp_cell(&self, ch: char, color: Color) -> Cell {
if self.config.no_color {
Cell::new(ch)
} else {
Cell::styled(ch, Some(color), None)
}
}
fn put_final(&self, frame: &mut Frame, plan: &GlyphPlan) {
frame.set(plan.glyph.x, plan.glyph.y, self.final_cell(plan));
}
fn put_final_at(&self, frame: &mut Frame, plan: &GlyphPlan, x: f32, y: f32) {
frame.set_i32(x.round() as i32, y.round() as i32, self.final_cell(plan));
}
fn put_temp_at(
&self,
frame: &mut Frame,
plan: &GlyphPlan,
x: f32,
y: f32,
ch: char,
color: Color,
) {
let mut cell = self.temp_cell(ch, color);
if matches!(
self.config.existing_color_handling,
ExistingColorHandling::Always
) {
cell.colors.fg = plan.glyph.cell.colors.fg.or(cell.colors.fg);
cell.colors.bg = plan.glyph.cell.colors.bg;
}
frame.set_i32(x.round() as i32, y.round() as i32, cell);
}
fn reveal(&self, t: f32, order: f32, spread: f32) -> f32 {
let start = (order * spread).clamp(0.0, 0.95);
((t - start) / (1.0 - start)).clamp(0.0, 1.0)
}
fn eased(&self, p: f32) -> f32 {
self.config.easing.apply(p)
}
fn x_order(&self, plan: &GlyphPlan) -> f32 {
ratio(plan.glyph.x, self.width)
}
fn y_order(&self, plan: &GlyphPlan) -> f32 {
ratio(plan.glyph.y, self.height)
}
fn diag_order(&self, plan: &GlyphPlan) -> f32 {
let denom = self.width.saturating_sub(1) + self.height.saturating_sub(1);
if denom == 0 {
0.0
} else {
(plan.glyph.x + plan.glyph.y) as f32 / denom as f32
}
}
fn center_order(&self, plan: &GlyphPlan) -> f32 {
let cx = (self.width.saturating_sub(1) as f32) / 2.0;
let cy = (self.height.saturating_sub(1) as f32) / 2.0;
let max_dist = (cx.powi(2) + cy.powi(2)).sqrt().max(1.0);
(((plan.glyph.x as f32 - cx).powi(2) + (plan.glyph.y as f32 - cy).powi(2)).sqrt()
/ max_dist)
.clamp(0.0, 1.0)
}
fn noise_symbol(&self, frame_number: usize, index: usize, salt: u64, symbols: &[char]) -> char {
if symbols.is_empty() {
return ' ';
}
let n = hash(self.seed, frame_number as u64, index as u64, salt) as usize;
symbols[n % symbols.len()]
}
fn render_wipe(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let p = self.eased(self.reveal(t, self.diag_order(plan), 0.82));
if p > 0.0 {
let mut cell = self.final_cell(plan);
if let Some(fg) = cell.colors.fg {
cell.colors.fg = Some(self.config.gradient.color_at(0.0).blend(fg, p));
}
frame.set(plan.glyph.x, plan.glyph.y, cell);
}
}
frame
}
fn render_sweep(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let order = self.diag_order(plan);
let gray_passed = t > order * 0.68;
let color_passed = t > 0.28 + order * 0.68;
if color_passed {
self.put_final(&mut frame, plan);
} else if gray_passed {
let band = (t - order).abs();
let color = if band < 0.06 {
Color::WHITE
} else {
Color::rgb(110, 118, 128)
};
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32,
plan.glyph.y as f32,
plan.glyph.cell.ch,
color,
);
}
}
frame
}
fn render_print(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
let count =
((self.eased(t) * self.glyphs.len() as f32).floor() as usize).min(self.glyphs.len());
for plan in self.glyphs.iter().take(count) {
self.put_final(&mut frame, plan);
}
if let Some(head) = self
.glyphs
.get(count.min(self.glyphs.len().saturating_sub(1)))
{
self.put_temp_at(
&mut frame,
head,
head.glyph.x as f32,
head.glyph.y as f32,
'█',
Color::WHITE,
);
}
frame
}
fn render_random_sequence(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let p = self.reveal(t, plan.random_order, 0.92);
if p >= 0.22 {
self.put_final(&mut frame, plan);
} else if p > 0.0 {
let ch =
self.noise_symbol(frame_number, plan.glyph.index, 11, &['░', '▒', '▓', '█']);
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32,
plan.glyph.y as f32,
ch,
plan.final_color.brighten(0.25),
);
}
}
frame
}
fn render_decrypt(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let symbols: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
.chars()
.collect();
for plan in &self.glyphs {
let start = ratio(plan.glyph.index, self.glyphs.len()).min(1.0) * 0.55;
let local = ((t - start) / 0.45).clamp(0.0, 1.0);
if local >= 1.0 {
self.put_final(&mut frame, plan);
} else if local > 0.0 {
let ch = self.noise_symbol(frame_number, plan.glyph.index, 17, &symbols);
let color = self
.config
.gradient
.color_at((local + plan.random_order) % 1.0)
.brighten(0.15);
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32,
plan.glyph.y as f32,
ch,
color,
);
}
}
frame
}
fn render_error_correct(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let symbols: Vec<char> = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
.chars()
.collect();
for plan in &self.glyphs {
let p = self.reveal(t, plan.random_order, 0.82);
if p > 0.72 {
self.put_final(&mut frame, plan);
} else {
let ch = self.noise_symbol(frame_number / 3, plan.glyph.index, 23, &symbols);
let color = if p > 0.55 {
Color::rgb(255, 80, 80)
} else {
Color::rgb(150, 150, 150)
};
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32,
plan.glyph.y as f32,
ch,
color,
);
}
}
frame
}
fn render_matrix(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let symbols: Vec<char> =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789アイウエオカキクケコサシスセソ"
.chars()
.collect();
for x in 0..self.width {
let offset = hash_f32(self.seed, x as u64, 31, 0) * self.height as f32;
let head = (t * (self.height as f32 + 12.0) * 1.7 + offset)
% (self.height as f32 + 12.0)
- 8.0;
for trail in 0..8 {
let y = head - trail as f32;
let shade = 1.0 - trail as f32 / 8.0;
let ch = self.noise_symbol(frame_number + trail, x, 37, &symbols);
frame.set_i32(
x as i32,
y.round() as i32,
self.temp_cell(ch, Color::rgb(30, (255.0 * shade) as u8, 80)),
);
}
}
for plan in &self.glyphs {
let p = self.reveal(t, self.y_order(plan), 0.86);
if p > 0.64 {
self.put_final(&mut frame, plan);
}
}
frame
}
fn render_rain(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let rain_symbols = ['|', '╵', '╷', '│', '┃'];
for plan in &self.glyphs {
let p = self.eased(self.reveal(t, plan.random_order, 0.72));
if p >= 1.0 {
self.put_final(&mut frame, plan);
} else if p > 0.0 {
let start_y = -((self.height as f32) * (0.3 + plan.random_order));
let y = lerp(start_y, plan.glyph.y as f32, Easing::OutBounce.apply(p));
let ch = if p > 0.8 {
plan.glyph.cell.ch
} else {
self.noise_symbol(frame_number, plan.glyph.index, 41, &rain_symbols)
};
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32,
y,
ch,
plan.final_color,
);
}
}
frame
}
fn render_bouncy_balls(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let p = self.reveal(t, plan.random_order, 0.65);
if p >= 1.0 {
self.put_final(&mut frame, plan);
} else if p > 0.0 {
let y = lerp(
-(self.height as f32) * 0.5,
plan.glyph.y as f32,
Easing::OutBounce.apply(p),
);
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32,
y,
plan.glyph.cell.ch,
plan.final_color.brighten(0.15),
);
}
}
frame
}
fn render_bubbles(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let p = self.reveal(t, plan.random_order, 0.72);
if p >= 1.0 {
self.put_final(&mut frame, plan);
} else if p > 0.0 {
let wobble = (p * TAU * 2.0 + plan.phase).sin() * (1.0 - p) * 2.0;
let y = lerp(
-2.0 - plan.random_order * self.height as f32,
plan.glyph.y as f32,
self.eased(p),
);
let ch = if p > 0.82 {
'*'
} else if plan.glyph.index % 2 == 0 {
'o'
} else {
'O'
};
let color = if p > 0.82 {
Color::rgb(255, 255, 255)
} else {
Color::rgb(80, 210, 255)
};
self.put_temp_at(&mut frame, plan, plan.glyph.x as f32 + wobble, y, ch, color);
}
}
frame
}
fn render_burn(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let flames = ['▄', '█', '▓', '▒', '░', '▲'];
let smoke = ['.', '·', '`', ' '];
for plan in &self.glyphs {
let order = 1.0 - self.y_order(plan);
let p = self.reveal(t, order, 0.78);
if p >= 0.62 {
let mut cell = self.final_cell(plan);
if let Some(fg) = cell.colors.fg {
cell.colors.fg =
Some(Color::rgb(255, 85, 0).blend(fg, ((p - 0.62) / 0.38).clamp(0.0, 1.0)));
}
frame.set(plan.glyph.x, plan.glyph.y, cell);
} else if p > 0.0 {
let ch = self.noise_symbol(frame_number, plan.glyph.index, 43, &flames);
let color = Color::rgb(255, (80.0 + 120.0 * p) as u8, 0);
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32,
plan.glyph.y as f32,
ch,
color,
);
let smoke_ch = self.noise_symbol(frame_number, plan.glyph.index, 44, &smoke);
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32,
plan.glyph.y as f32 - 1.0,
smoke_ch,
Color::rgb(90, 90, 90),
);
}
}
frame
}
fn render_color_shift(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let shift = (self.diag_order(plan) + t * 1.5) % 1.0;
let mut cell = self.final_cell(plan);
if !self.config.no_color {
cell.colors.fg = Some(self.config.gradient.color_at(shift));
}
frame.set(plan.glyph.x, plan.glyph.y, cell);
}
frame
}
fn render_crumble(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let dust = ['.', ':', '·', '░', '▒'];
for plan in &self.glyphs {
if t < 0.25 {
let jitter =
((hash_f32(self.seed, frame_number as u64, plan.glyph.index as u64, 51) - 0.5)
* t
* 6.0)
.round();
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32 + jitter,
plan.glyph.y as f32,
plan.glyph.cell.ch,
plan.final_color.dim(1.0 - t),
);
} else if t < 0.55 {
let p = (t - 0.25) / 0.30;
let y = plan.glyph.y as f32 + p * (2.0 + plan.random_order * self.height as f32);
let ch = self.noise_symbol(frame_number, plan.glyph.index, 53, &dust);
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32,
y,
ch,
Color::rgb(130, 120, 110),
);
} else if t < 0.72 {
let p = (t - 0.55) / 0.17;
let cx = self.width as f32 / 2.0;
let cy = self.height as f32 / 2.0;
self.put_temp_at(
&mut frame,
plan,
lerp(plan.burst_x, cx, p),
lerp(plan.burst_y, cy, p),
'·',
Color::rgb(120, 120, 120),
);
} else {
let p = self.reveal((t - 0.72) / 0.28, plan.random_order, 0.75);
if p > 0.0 {
self.put_final(&mut frame, plan);
}
}
}
frame
}
fn render_expand(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
let cx = (self.width.saturating_sub(1) as f32) / 2.0;
let cy = (self.height.saturating_sub(1) as f32) / 2.0;
for plan in &self.glyphs {
let p = self.eased(self.reveal(t, self.center_order(plan), 0.32));
if p > 0.0 {
self.put_final_at(
&mut frame,
plan,
lerp(cx, plan.glyph.x as f32, p),
lerp(cy, plan.glyph.y as f32, p),
);
}
}
frame
}
fn render_fireworks(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let sparks = ['*', '+', 'x', '·', '.'];
let launcher_x = self.width as f32 / 2.0;
let launcher_y = self.height as f32;
for plan in &self.glyphs {
let p = self.reveal(t, plan.random_order, 0.45);
if p >= 1.0 {
self.put_final(&mut frame, plan);
} else if p > 0.0 {
if p < 0.42 {
let q = p / 0.42;
self.put_temp_at(
&mut frame,
plan,
lerp(launcher_x, plan.burst_x, q),
lerp(launcher_y, plan.burst_y, Easing::OutQuad.apply(q)),
'|',
Color::rgb(255, 220, 120),
);
} else if p < 0.78 {
let q = (p - 0.42) / 0.36;
let x = lerp(plan.burst_x, plan.glyph.x as f32, q);
let y = lerp(plan.burst_y, plan.glyph.y as f32, q);
let ch = self.noise_symbol(frame_number, plan.glyph.index, 59, &sparks);
self.put_temp_at(
&mut frame,
plan,
x,
y,
ch,
Color::rgb(255, 180, 70).blend(plan.final_color, q),
);
} else {
self.put_final(&mut frame, plan);
}
}
}
frame
}
fn render_highlight(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
let band = t * 1.35 - 0.15;
for plan in &self.glyphs {
let order = self.diag_order(plan);
let distance = (order - band).abs();
let mut cell = self.final_cell(plan);
if let Some(fg) = cell.colors.fg {
let color = if distance < 0.055 {
fg.brighten(0.72)
} else {
fg.dim(0.55)
};
cell.colors.fg = Some(color);
}
frame.set(plan.glyph.x, plan.glyph.y, cell);
}
frame
}
fn render_laser_etch(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let scan = t * (self.width as f32 + self.height as f32);
for plan in &self.glyphs {
let order = plan.glyph.x as f32 + plan.glyph.y as f32;
if order < scan - 1.0 {
let mut cell = self.final_cell(plan);
if let Some(fg) = cell.colors.fg {
let heat = ((scan - order) / 12.0).clamp(0.0, 1.0);
cell.colors.fg = Some(Color::rgb(255, 60, 20).blend(fg, heat));
}
frame.set(plan.glyph.x, plan.glyph.y, cell);
}
}
let lx = scan.round() as i32;
for y in 0..self.height as i32 {
let x = lx - y;
frame.set_i32(x, y, self.temp_cell('█', Color::rgb(255, 30, 20)));
let spark = if frame_number % 2 == 0 { '*' } else { '+' };
frame.set_i32(x + 1, y, self.temp_cell(spark, Color::rgb(255, 210, 60)));
}
frame
}
fn render_beams(&self, t: f32, _frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let vertical = (t * (self.width as f32 + 2.0)).round() as i32 - 1;
let horizontal = (t * (self.height as f32 + 2.0)).round() as i32 - 1;
for y in 0..self.height as i32 {
frame.set_i32(vertical, y, self.temp_cell('│', Color::rgb(140, 220, 255)));
}
for x in 0..self.width as i32 {
frame.set_i32(
x,
horizontal,
self.temp_cell('─', Color::rgb(140, 220, 255)),
);
}
for plan in &self.glyphs {
let order = self.x_order(plan).min(self.y_order(plan));
if t > order * 0.86 {
self.put_final(&mut frame, plan);
}
}
frame
}
fn render_binary_path(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let p = self.eased(self.reveal(t, plan.random_order, 0.62));
if p >= 1.0 {
self.put_final(&mut frame, plan);
} else if p > 0.0 {
let (x, y) = if p < 0.5 {
(lerp(plan.side_x, plan.glyph.x as f32, p * 2.0), plan.side_y)
} else {
(
plan.glyph.x as f32,
lerp(plan.side_y, plan.glyph.y as f32, (p - 0.5) * 2.0),
)
};
let ch = self.noise_symbol(frame_number, plan.glyph.index, 61, &['0', '1']);
self.put_temp_at(&mut frame, plan, x, y, ch, Color::rgb(80, 255, 120));
}
}
frame
}
fn render_blackhole(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let cx = (self.width.saturating_sub(1) as f32) / 2.0;
let cy = (self.height.saturating_sub(1) as f32) / 2.0;
if (0.28..0.70).contains(&t) {
for radius in 0..3 {
let ch = ['●', '◉', '○'][radius];
frame.set_i32(
cx.round() as i32 + radius as i32 - 1,
cy.round() as i32,
self.temp_cell(ch, Color::rgb(100, 80, 180)),
);
}
}
for plan in &self.glyphs {
if t < 0.42 {
let p = Easing::InQuad.apply(t / 0.42);
let angle = plan.phase + p * TAU * 1.5;
let pull = 1.0 - p;
let x = cx
+ (plan.glyph.x as f32 - cx) * pull
+ angle.cos() * pull * plan.radius * 0.08;
let y = cy
+ (plan.glyph.y as f32 - cy) * pull
+ angle.sin() * pull * plan.radius * 0.08;
let ch = self.noise_symbol(frame_number, plan.glyph.index, 67, &['.', '*', '+']);
self.put_temp_at(&mut frame, plan, x, y, ch, Color::rgb(180, 180, 255));
} else if t > 0.62 {
let p = Easing::OutCubic.apply((t - 0.62) / 0.38);
self.put_final_at(
&mut frame,
plan,
lerp(cx, plan.glyph.x as f32, p),
lerp(cy, plan.glyph.y as f32, p),
);
}
}
frame
}
fn render_middle_out(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let p = self.eased(self.reveal(t, self.center_order(plan), 0.76));
if p > 0.0 {
self.put_final(&mut frame, plan);
}
}
frame
}
fn render_orbitting_volley(&self, t: f32, _frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let cx = (self.width.saturating_sub(1) as f32) / 2.0;
let cy = (self.height.saturating_sub(1) as f32) / 2.0;
let rx = self.width as f32 / 2.0;
let ry = self.height as f32 / 2.0;
for i in 0..4 {
let angle = t * TAU + i as f32 * TAU / 4.0;
let x = cx + angle.cos() * rx;
let y = cy + angle.sin() * ry;
let symbol = ['▲', '▶', '▼', '◀'][i];
frame.set_i32(
x.round() as i32,
y.round() as i32,
self.temp_cell(symbol, Color::rgb(255, 220, 120)),
);
}
for plan in &self.glyphs {
let p = self.eased(self.reveal(t, plan.random_order, 0.78));
if p >= 1.0 {
self.put_final(&mut frame, plan);
} else if p > 0.0 {
self.put_temp_at(
&mut frame,
plan,
lerp(plan.side_x, plan.glyph.x as f32, p),
lerp(plan.side_y, plan.glyph.y as f32, p),
plan.glyph.cell.ch,
plan.final_color,
);
}
}
frame
}
fn render_overflow(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let noise: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".chars().collect();
for plan in &self.glyphs {
let row_order = self.y_order(plan);
let p = self.reveal(t, row_order, 0.7);
if p >= 1.0 {
self.put_final(&mut frame, plan);
} else if p > 0.0 {
let offset = ((1.0 - p)
* self.width as f32
* (if plan.glyph.y % 2 == 0 { 1.0 } else { -1.0 }))
.round();
let ch = if p > 0.75 {
plan.glyph.cell.ch
} else {
self.noise_symbol(frame_number / 2, plan.glyph.index, 71, &noise)
};
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32 + offset,
plan.glyph.y as f32,
ch,
plan.final_color,
);
}
}
frame
}
fn render_pour(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let order = (self.x_order(plan) * 0.35 + self.y_order(plan) * 0.65).clamp(0.0, 1.0);
let p = self.eased(self.reveal(t, order, 0.66));
if p >= 1.0 {
self.put_final(&mut frame, plan);
} else if p > 0.0 {
let y = lerp(
-1.0 - self.height as f32 * (1.0 - order),
plan.glyph.y as f32,
p,
);
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32,
y,
plan.glyph.cell.ch,
plan.final_color,
);
}
}
frame
}
fn render_rings(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
let cx = (self.width.saturating_sub(1) as f32) / 2.0;
let cy = (self.height.saturating_sub(1) as f32) / 2.0;
for plan in &self.glyphs {
let p = self.eased(t);
if p > 0.86 {
let q = (p - 0.86) / 0.14;
let angle = plan.phase + t * TAU * plan.speed;
let ring_x = cx + angle.cos() * plan.radius;
let ring_y = cy + angle.sin() * plan.radius * 0.45;
self.put_final_at(
&mut frame,
plan,
lerp(ring_x, plan.glyph.x as f32, q),
lerp(ring_y, plan.glyph.y as f32, q),
);
} else {
let angle = plan.phase + t * TAU * plan.speed * 2.0;
self.put_temp_at(
&mut frame,
plan,
cx + angle.cos() * plan.radius,
cy + angle.sin() * plan.radius * 0.45,
plan.glyph.cell.ch,
plan.final_color,
);
}
}
frame
}
fn render_scattered(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let p = self.eased(self.reveal(t, plan.random_order, 0.4));
self.put_final_at(
&mut frame,
plan,
lerp(plan.start_x, plan.glyph.x as f32, p),
lerp(plan.start_y, plan.glyph.y as f32, p),
);
}
frame
}
fn render_slice(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let p = self.eased(self.reveal(t, self.y_order(plan), 0.35));
let from_left = plan.glyph.x < self.width / 2;
let start_x = if from_left {
-(self.width as f32)
} else {
self.width as f32 * 2.0
};
self.put_final_at(
&mut frame,
plan,
lerp(start_x, plan.glyph.x as f32, p),
plan.glyph.y as f32,
);
}
frame
}
fn render_slide(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let p = self.eased(self.reveal(t, self.y_order(plan), 0.58));
let dir = if plan.glyph.y % 2 == 0 { -1.0 } else { 1.0 };
let start_x = plan.glyph.x as f32 + dir * self.width as f32;
self.put_final_at(
&mut frame,
plan,
lerp(start_x, plan.glyph.x as f32, p),
plan.glyph.y as f32,
);
}
frame
}
fn render_smoke(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let smoke = ['░', '▒', '▓', '.', '·'];
for plan in &self.glyphs {
let order = (1.0 - self.y_order(plan)) * 0.55 + self.x_order(plan) * 0.45;
let p = self.reveal(t, order, 0.76);
if p > 0.78 {
self.put_final(&mut frame, plan);
} else if p > 0.0 {
let ch = self.noise_symbol(frame_number, plan.glyph.index, 79, &smoke);
let drift = (plan.phase + t * TAU).sin() * 2.0 * (1.0 - p);
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32 + drift,
plan.glyph.y as f32 - (1.0 - p) * 2.0,
ch,
Color::rgb(120, 130, 135),
);
}
}
frame
}
fn render_spotlights(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
let spots = [
(t * TAU, 0.28_f32),
(t * TAU + TAU / 3.0, 0.34_f32),
(t * TAU + 2.0 * TAU / 3.0, 0.30_f32),
];
let cx = (self.width.saturating_sub(1) as f32) / 2.0;
let cy = (self.height.saturating_sub(1) as f32) / 2.0;
let max_dim = self.width.max(self.height) as f32;
for plan in &self.glyphs {
let mut brightness: f32 = if t > 0.82 { (t - 0.82) / 0.18 } else { 0.18 };
for (angle, radius_scale) in spots {
let sx = cx + angle.cos() * self.width as f32 * 0.35 * (1.0 - t * 0.45);
let sy = cy + angle.sin() * self.height as f32 * 0.35 * (1.0 - t * 0.45);
let radius = max_dim * radius_scale;
let dist = ((plan.glyph.x as f32 - sx).powi(2)
+ (plan.glyph.y as f32 - sy).powi(2))
.sqrt();
brightness = brightness.max(1.0 - (dist / radius).clamp(0.0, 1.0));
}
let mut cell = self.final_cell(plan);
if let Some(fg) = cell.colors.fg {
cell.colors.fg = Some(fg.dim((0.22 + brightness * 0.9).clamp(0.0, 1.0)));
}
frame.set(plan.glyph.x, plan.glyph.y, cell);
}
frame
}
fn render_spray(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let particles = ['.', '*', '+', '•'];
let ox = 0.0;
let oy = self.height as f32;
for plan in &self.glyphs {
let p = self.eased(self.reveal(t, plan.random_order, 0.76));
if p >= 1.0 {
self.put_final(&mut frame, plan);
} else if p > 0.0 {
let arc = (p * std::f32::consts::PI).sin()
* (2.0 + plan.random_order * self.height as f32 * 0.25);
let ch = if p > 0.72 {
plan.glyph.cell.ch
} else {
self.noise_symbol(frame_number, plan.glyph.index, 83, &particles)
};
self.put_temp_at(
&mut frame,
plan,
lerp(ox, plan.glyph.x as f32, p),
lerp(oy, plan.glyph.y as f32, p) - arc,
ch,
plan.final_color,
);
}
}
frame
}
fn render_swarm(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
for plan in &self.glyphs {
let p = self.eased(self.reveal(t, plan.random_order, 0.42));
let swirl = (1.0 - p) * 4.0;
let x = lerp(plan.start_x, plan.glyph.x as f32, p)
+ (plan.phase + t * TAU * plan.speed).cos() * swirl;
let y = lerp(plan.start_y, plan.glyph.y as f32, p)
+ (plan.phase + t * TAU * plan.speed).sin() * swirl * 0.5;
self.put_final_at(&mut frame, plan, x, y);
}
frame
}
fn render_synth_grid(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let grid_color = Color::rgb(160, 70, 255);
let interval = 4usize.max((self.width.max(self.height) / 6).max(2));
for x in (0..self.width).step_by(interval) {
for y in 0..self.height {
frame.set(x, y, self.temp_cell('│', grid_color.dim(0.75)));
}
}
for y in (0..self.height).step_by(interval) {
for x in 0..self.width {
frame.set(x, y, self.temp_cell('─', grid_color.dim(0.75)));
}
}
let blocks = ['█', '▓', '▒', '░'];
for plan in &self.glyphs {
let p = self.reveal(t, self.center_order(plan), 0.72);
if p > 0.65 {
self.put_final(&mut frame, plan);
} else if p > 0.0 {
let ch = self.noise_symbol(frame_number, plan.glyph.index, 89, &blocks);
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32,
plan.glyph.y as f32,
ch,
Color::rgb(0, 240, 255),
);
}
}
frame
}
fn render_thunderstorm(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
for x in 0..self.width {
let y = ((frame_number + x * 3) % (self.height + 4)) as i32 - 2;
frame.set_i32(x as i32, y, self.temp_cell('╵', Color::rgb(80, 120, 180)));
}
if frame_number % 23 < 3 {
let start = (hash(self.seed, frame_number as u64, 97, 0) as usize) % self.width;
let mut x = start as i32;
for y in 0..self.height as i32 {
frame.set_i32(x, y, self.temp_cell('╱', Color::WHITE));
x += if hash(self.seed, frame_number as u64, y as u64, 98) & 1 == 0 {
-1
} else {
1
};
}
}
for plan in &self.glyphs {
let p = self.reveal(t, plan.random_order, 0.68);
if p > 0.42 {
let mut cell = self.final_cell(plan);
if let Some(fg) = cell.colors.fg {
cell.colors.fg = Some(if frame_number % 23 < 3 {
fg.brighten(0.75)
} else {
fg.dim(p)
});
}
frame.set(plan.glyph.x, plan.glyph.y, cell);
}
}
frame
}
fn render_unstable(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let cx = (self.width.saturating_sub(1) as f32) / 2.0;
let cy = (self.height.saturating_sub(1) as f32) / 2.0;
for plan in &self.glyphs {
if t < 0.34 {
let jitter_x =
(hash_f32(self.seed, frame_number as u64, plan.glyph.index as u64, 101) - 0.5)
* 3.0;
let jitter_y =
(hash_f32(self.seed, frame_number as u64, plan.glyph.index as u64, 102) - 0.5)
* 2.0;
self.put_final_at(
&mut frame,
plan,
plan.glyph.x as f32 + jitter_x,
plan.glyph.y as f32 + jitter_y,
);
} else if t < 0.62 {
let p = (t - 0.34) / 0.28;
self.put_temp_at(
&mut frame,
plan,
lerp(plan.glyph.x as f32, plan.burst_x, p),
lerp(plan.glyph.y as f32, plan.burst_y, p),
plan.glyph.cell.ch,
plan.final_color.brighten(0.2),
);
} else {
let p = self.eased((t - 0.62) / 0.38);
let sx = lerp(plan.burst_x, cx, 0.15);
let sy = lerp(plan.burst_y, cy, 0.15);
self.put_final_at(
&mut frame,
plan,
lerp(sx, plan.glyph.x as f32, p),
lerp(sy, plan.glyph.y as f32, p),
);
}
}
frame
}
fn render_vhs_tape(&self, t: f32, frame_number: usize) -> Frame {
let mut frame = self.blank_frame();
let noise = [' ', '░', '▒', '▓', '█'];
let instability = (1.0 - t).powi(2);
for plan in &self.glyphs {
let row_noise =
hash_f32(self.seed, frame_number as u64, plan.glyph.y as u64, 107) - 0.5;
let offset = (row_noise * instability * self.width as f32 * 0.55).round();
if hash_f32(self.seed, frame_number as u64, plan.glyph.index as u64, 108)
< instability * 0.22
{
let ch = self.noise_symbol(frame_number, plan.glyph.index, 109, &noise);
self.put_temp_at(
&mut frame,
plan,
plan.glyph.x as f32 + offset,
plan.glyph.y as f32,
ch,
Color::rgb(190, 190, 210),
);
} else {
self.put_final_at(
&mut frame,
plan,
plan.glyph.x as f32 + offset,
plan.glyph.y as f32,
);
}
}
frame
}
fn render_waves(&self, t: f32) -> Frame {
let mut frame = self.blank_frame();
let wave = t * 1.35 - 0.15;
for plan in &self.glyphs {
let order = self.x_order(plan);
if order < wave + 0.12 {
let amplitude = (1.0 - (order - wave).abs() / 0.18).clamp(0.0, 1.0);
let y = plan.glyph.y as f32 + (plan.phase + t * TAU * 2.0).sin() * amplitude;
self.put_final_at(&mut frame, plan, plan.glyph.x as f32, y);
}
}
frame
}
}
fn ratio(value: usize, len: usize) -> f32 {
if len <= 1 {
0.0
} else {
value as f32 / (len - 1) as f32
}
}
fn lerp(start: f32, end: f32, t: f32) -> f32 {
start + (end - start) * t.clamp(0.0, 1.0)
}