Complete piglet implementation with animations and effects

- Implement complete animation system with 20+ motion effects
- Add 18+ easing functions (quad, cubic, elastic, back, bounce)
- Implement color system with palette and gradient support
- Add parser for durations, colors, and CSS gradients
- Create comprehensive test suite (14 tests passing)
- Add linting and formatting with clippy/rustfmt
- Support for figlet integration with custom fonts
- Terminal rendering with crossterm
- Fix all clippy warnings and lint issues

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-09 03:00:20 +01:00
parent f6fac85bc4
commit b1ad87fc26
23 changed files with 1510 additions and 264 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
/target/
**/*.rs.bk
Cargo.lock
.DS_Store
*.swp
*.swo
*~
.vscode/
.idea/

View File

@@ -16,13 +16,13 @@ csscolorparser = "0.6"
palette = "0.7" palette = "0.7"
# Animation & Interpolation # Animation & Interpolation
scirs2-interpolate = "0.1" # scirs2-interpolate = "0.1.0-rc.2" # Not needed, using custom easing functions
# Terminal manipulation # Terminal manipulation
crossterm = "0.27" crossterm = "0.27"
# Async runtime (for timing) # Async runtime (for timing)
tokio = { version = "1.35", features = ["time", "rt"] } tokio = { version = "1.35", features = ["time", "rt-multi-thread", "macros"] }
# Process execution # Process execution
which = "5.0" which = "5.0"

View File

@@ -1,49 +1,318 @@
use anyhow::{Result, bail}; use anyhow::{bail, Result};
use scirs2_interpolate::*;
pub trait EasingFunction: Send + Sync { pub trait EasingFunction: Send + Sync {
fn ease(&self, t: f64) -> f64; fn ease(&self, t: f64) -> f64;
#[allow(dead_code)]
fn name(&self) -> &str; fn name(&self) -> &str;
} }
// Linear // Linear
pub struct Linear; pub struct Linear;
impl EasingFunction for Linear { impl EasingFunction for Linear {
fn ease(&self, t: f64) -> f64 { t } fn ease(&self, t: f64) -> f64 {
fn name(&self) -> &str { "linear" } t
}
#[allow(dead_code)]
fn name(&self) -> &str {
"linear"
}
}
// Basic easing
pub struct EaseIn;
impl EasingFunction for EaseIn {
fn ease(&self, t: f64) -> f64 {
t * t
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-in"
}
}
pub struct EaseOut;
impl EasingFunction for EaseOut {
fn ease(&self, t: f64) -> f64 {
t * (2.0 - t)
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-out"
}
}
pub struct EaseInOut;
impl EasingFunction for EaseInOut {
fn ease(&self, t: f64) -> f64 {
if t < 0.5 {
2.0 * t * t
} else {
-1.0 + (4.0 - 2.0 * t) * t
}
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-in-out"
}
} }
// Quadratic // Quadratic
pub struct EaseInQuad; pub struct EaseInQuad;
impl EasingFunction for EaseInQuad { impl EasingFunction for EaseInQuad {
fn ease(&self, t: f64) -> f64 { quad_ease_in(t, 0.0, 1.0, 1.0) } fn ease(&self, t: f64) -> f64 {
fn name(&self) -> &str { "ease-in-quad" } t * t
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-in-quad"
}
} }
pub struct EaseOutQuad; pub struct EaseOutQuad;
impl EasingFunction for EaseOutQuad { impl EasingFunction for EaseOutQuad {
fn ease(&self, t: f64) -> f64 { quad_ease_out(t, 0.0, 1.0, 1.0) } fn ease(&self, t: f64) -> f64 {
fn name(&self) -> &str { "ease-out-quad" } t * (2.0 - t)
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-out-quad"
}
} }
pub struct EaseInOutQuad; pub struct EaseInOutQuad;
impl EasingFunction for EaseInOutQuad { impl EasingFunction for EaseInOutQuad {
fn ease(&self, t: f64) -> f64 { quad_ease_in_out(t, 0.0, 1.0, 1.0) } fn ease(&self, t: f64) -> f64 {
fn name(&self) -> &str { "ease-in-out-quad" } if t < 0.5 {
2.0 * t * t
} else {
-1.0 + (4.0 - 2.0 * t) * t
}
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-in-out-quad"
}
} }
// Cubic // Cubic
pub struct EaseInCubic; pub struct EaseInCubic;
impl EasingFunction for EaseInCubic { impl EasingFunction for EaseInCubic {
fn ease(&self, t: f64) -> f64 { cubic_ease_in(t, 0.0, 1.0, 1.0) } fn ease(&self, t: f64) -> f64 {
fn name(&self) -> &str { "ease-in-cubic" } t * t * t
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-in-cubic"
}
} }
pub struct EaseOutCubic; pub struct EaseOutCubic;
impl EasingFunction for EaseOutCubic { impl EasingFunction for EaseOutCubic {
fn ease(&self, t: f64) -> f64 { cubic_ease_out(t, 0.0, 1.0, 1.0) } fn ease(&self, t: f64) -> f64 {
fn name(&self) -> &str { "ease-out-cubic" } let t1 = t - 1.0;
t1 * t1 * t1 + 1.0
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-out-cubic"
}
} }
pub struct EaseInOutCubic; pub struct EaseInOutCubic;
impl EasingFunction for EaseInOut impl EasingFunction for EaseInOutCubic {
fn ease(&self, t: f64) -> f64 {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-in-out-cubic"
}
}
// Back
pub struct EaseInBack;
impl EasingFunction for EaseInBack {
fn ease(&self, t: f64) -> f64 {
let c1 = 1.70158;
let c3 = c1 + 1.0;
c3 * t * t * t - c1 * t * t
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-in-back"
}
}
pub struct EaseOutBack;
impl EasingFunction for EaseOutBack {
fn ease(&self, t: f64) -> f64 {
let c1 = 1.70158;
let c3 = c1 + 1.0;
1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-out-back"
}
}
pub struct EaseInOutBack;
impl EasingFunction for EaseInOutBack {
fn ease(&self, t: f64) -> f64 {
let c1 = 1.70158;
let c2 = c1 * 1.525;
if t < 0.5 {
((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0 * t - c2)) / 2.0
} else {
((2.0 * t - 2.0).powi(2) * ((c2 + 1.0) * (t * 2.0 - 2.0) + c2) + 2.0) / 2.0
}
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-in-out-back"
}
}
// Elastic
pub struct EaseInElastic;
impl EasingFunction for EaseInElastic {
fn ease(&self, t: f64) -> f64 {
if t == 0.0 {
return 0.0;
}
if t == 1.0 {
return 1.0;
}
let c4 = (2.0 * std::f64::consts::PI) / 3.0;
-(2.0_f64.powf(10.0 * t - 10.0)) * ((t * 10.0 - 10.75) * c4).sin()
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-in-elastic"
}
}
pub struct EaseOutElastic;
impl EasingFunction for EaseOutElastic {
fn ease(&self, t: f64) -> f64 {
if t == 0.0 {
return 0.0;
}
if t == 1.0 {
return 1.0;
}
let c4 = (2.0 * std::f64::consts::PI) / 3.0;
2.0_f64.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-out-elastic"
}
}
pub struct EaseInOutElastic;
impl EasingFunction for EaseInOutElastic {
fn ease(&self, t: f64) -> f64 {
if t == 0.0 {
return 0.0;
}
if t == 1.0 {
return 1.0;
}
let c5 = (2.0 * std::f64::consts::PI) / 4.5;
if t < 0.5 {
-(2.0_f64.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0
} else {
(2.0_f64.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0 + 1.0
}
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-in-out-elastic"
}
}
// Bounce
fn bounce_out(t: f64) -> f64 {
let n1 = 7.5625;
let d1 = 2.75;
if t < 1.0 / d1 {
n1 * t * t
} else if t < 2.0 / d1 {
let t = t - 1.5 / d1;
n1 * t * t + 0.75
} else if t < 2.5 / d1 {
let t = t - 2.25 / d1;
n1 * t * t + 0.9375
} else {
let t = t - 2.625 / d1;
n1 * t * t + 0.984375
}
}
pub struct EaseInBounce;
impl EasingFunction for EaseInBounce {
fn ease(&self, t: f64) -> f64 {
1.0 - bounce_out(1.0 - t)
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-in-bounce"
}
}
pub struct EaseOutBounce;
impl EasingFunction for EaseOutBounce {
fn ease(&self, t: f64) -> f64 {
bounce_out(t)
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-out-bounce"
}
}
pub struct EaseInOutBounce;
impl EasingFunction for EaseInOutBounce {
fn ease(&self, t: f64) -> f64 {
if t < 0.5 {
(1.0 - bounce_out(1.0 - 2.0 * t)) / 2.0
} else {
(1.0 + bounce_out(2.0 * t - 1.0)) / 2.0
}
}
#[allow(dead_code)]
fn name(&self) -> &str {
"ease-in-out-bounce"
}
}
pub fn get_easing_function(name: &str) -> Result<Box<dyn EasingFunction>> {
match name {
"linear" => Ok(Box::new(Linear)),
"ease-in" => Ok(Box::new(EaseIn)),
"ease-out" => Ok(Box::new(EaseOut)),
"ease-in-out" => Ok(Box::new(EaseInOut)),
"ease-in-quad" => Ok(Box::new(EaseInQuad)),
"ease-out-quad" => Ok(Box::new(EaseOutQuad)),
"ease-in-out-quad" => Ok(Box::new(EaseInOutQuad)),
"ease-in-cubic" => Ok(Box::new(EaseInCubic)),
"ease-out-cubic" => Ok(Box::new(EaseOutCubic)),
"ease-in-out-cubic" => Ok(Box::new(EaseInOutCubic)),
"ease-in-back" => Ok(Box::new(EaseInBack)),
"ease-out-back" => Ok(Box::new(EaseOutBack)),
"ease-in-out-back" => Ok(Box::new(EaseInOutBack)),
"ease-in-elastic" => Ok(Box::new(EaseInElastic)),
"ease-out-elastic" => Ok(Box::new(EaseOutElastic)),
"ease-in-out-elastic" => Ok(Box::new(EaseInOutElastic)),
"ease-in-bounce" => Ok(Box::new(EaseInBounce)),
"ease-out-bounce" => Ok(Box::new(EaseOutBounce)),
"ease-in-out-bounce" => Ok(Box::new(EaseInOutBounce)),
_ => bail!("Unknown easing function: {}", name),
}
}

450
src/animation/effects.rs Normal file
View File

@@ -0,0 +1,450 @@
use crate::utils::ascii::AsciiArt;
use anyhow::{bail, Result};
pub trait Effect: Send + Sync {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult;
fn name(&self) -> &str;
}
#[derive(Debug, Clone)]
pub struct EffectResult {
pub text: String,
pub opacity: f64,
pub offset_x: i32,
pub offset_y: i32,
pub scale: f64,
}
impl EffectResult {
pub fn new(text: String) -> Self {
Self {
text,
opacity: 1.0,
offset_x: 0,
offset_y: 0,
scale: 1.0,
}
}
pub fn with_opacity(mut self, opacity: f64) -> Self {
self.opacity = opacity;
self
}
pub fn with_offset(mut self, x: i32, y: i32) -> Self {
self.offset_x = x;
self.offset_y = y;
self
}
pub fn with_scale(mut self, scale: f64) -> Self {
self.scale = scale;
self
}
}
// Fade effects
pub struct FadeIn;
impl Effect for FadeIn {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let text = ascii_art.apply_fade(progress);
EffectResult::new(text).with_opacity(progress)
}
fn name(&self) -> &str {
"fade-in"
}
}
pub struct FadeOut;
impl Effect for FadeOut {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let opacity = 1.0 - progress;
let text = ascii_art.apply_fade(opacity);
EffectResult::new(text).with_opacity(opacity)
}
fn name(&self) -> &str {
"fade-out"
}
}
pub struct FadeInOut;
impl Effect for FadeInOut {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let opacity = if progress < 0.5 {
progress * 2.0
} else {
(1.0 - progress) * 2.0
};
let text = ascii_art.apply_fade(opacity);
EffectResult::new(text).with_opacity(opacity)
}
fn name(&self) -> &str {
"fade-in-out"
}
}
// Slide effects
pub struct SlideInTop;
impl Effect for SlideInTop {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let offset_y = ((1.0 - progress) * -(ascii_art.height() as f64)) as i32;
EffectResult::new(ascii_art.render()).with_offset(0, offset_y)
}
fn name(&self) -> &str {
"slide-in-top"
}
}
pub struct SlideInBottom;
impl Effect for SlideInBottom {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let offset_y = ((1.0 - progress) * ascii_art.height() as f64) as i32;
EffectResult::new(ascii_art.render()).with_offset(0, offset_y)
}
fn name(&self) -> &str {
"slide-in-bottom"
}
}
pub struct SlideInLeft;
impl Effect for SlideInLeft {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let offset_x = ((1.0 - progress) * -(ascii_art.width() as f64)) as i32;
EffectResult::new(ascii_art.render()).with_offset(offset_x, 0)
}
fn name(&self) -> &str {
"slide-in-left"
}
}
pub struct SlideInRight;
impl Effect for SlideInRight {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let offset_x = ((1.0 - progress) * ascii_art.width() as f64) as i32;
EffectResult::new(ascii_art.render()).with_offset(offset_x, 0)
}
fn name(&self) -> &str {
"slide-in-right"
}
}
// Scale effects
pub struct ScaleUp;
impl Effect for ScaleUp {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let scale = progress;
let scaled = ascii_art.scale(scale);
EffectResult::new(scaled.render()).with_scale(scale)
}
fn name(&self) -> &str {
"scale-up"
}
}
pub struct ScaleDown;
impl Effect for ScaleDown {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let scale = 2.0 - progress;
let scaled = ascii_art.scale(scale);
EffectResult::new(scaled.render()).with_scale(scale)
}
fn name(&self) -> &str {
"scale-down"
}
}
// Pulse effect
pub struct Pulse;
impl Effect for Pulse {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let scale = 1.0 + (progress * std::f64::consts::PI * 2.0).sin() * 0.1;
let scaled = ascii_art.scale(scale);
EffectResult::new(scaled.render()).with_scale(scale)
}
fn name(&self) -> &str {
"pulse"
}
}
// Bounce effects
pub struct BounceIn;
impl Effect for BounceIn {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let offset_y = if progress < 0.8 {
((1.0 - progress / 0.8) * -(ascii_art.height() as f64)) as i32
} else {
let bounce_progress = (progress - 0.8) / 0.2;
(bounce_progress * 10.0 * (1.0 - bounce_progress)) as i32
};
EffectResult::new(ascii_art.render()).with_offset(0, offset_y)
}
fn name(&self) -> &str {
"bounce-in"
}
}
pub struct BounceOut;
impl Effect for BounceOut {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let offset_y = if progress < 0.2 {
-(progress * 10.0 * (1.0 - progress / 0.2)) as i32
} else {
(((progress - 0.2) / 0.8) * ascii_art.height() as f64) as i32
};
EffectResult::new(ascii_art.render()).with_offset(0, offset_y)
}
fn name(&self) -> &str {
"bounce-out"
}
}
// Typewriter effect
pub struct Typewriter;
impl Effect for Typewriter {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let total_chars = ascii_art.char_count();
let visible_chars = (total_chars as f64 * progress) as usize;
let positions = ascii_art.char_positions();
let lines = ascii_art.get_lines();
let mut result_lines: Vec<String> = lines
.iter()
.map(|l| {
l.chars()
.map(|c| if c.is_whitespace() { c } else { ' ' })
.collect()
})
.collect();
for (i, (x, y, ch)) in positions.iter().enumerate() {
if i < visible_chars {
if let Some(line) = result_lines.get_mut(*y) {
let mut chars: Vec<char> = line.chars().collect();
if *x < chars.len() {
chars[*x] = *ch;
*line = chars.iter().collect();
}
}
}
}
EffectResult::new(result_lines.join("\n"))
}
fn name(&self) -> &str {
"typewriter"
}
}
pub struct TypewriterReverse;
impl Effect for TypewriterReverse {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let total_chars = ascii_art.char_count();
let visible_chars = (total_chars as f64 * (1.0 - progress)) as usize;
let positions = ascii_art.char_positions();
let lines = ascii_art.get_lines();
let mut result_lines: Vec<String> = lines
.iter()
.map(|l| {
l.chars()
.map(|c| if c.is_whitespace() { c } else { ' ' })
.collect()
})
.collect();
for (i, (x, y, ch)) in positions.iter().enumerate() {
if i < visible_chars {
if let Some(line) = result_lines.get_mut(*y) {
let mut chars: Vec<char> = line.chars().collect();
if *x < chars.len() {
chars[*x] = *ch;
*line = chars.iter().collect();
}
}
}
}
EffectResult::new(result_lines.join("\n"))
}
fn name(&self) -> &str {
"typewriter-reverse"
}
}
// Wave effect
pub struct Wave;
impl Effect for Wave {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let lines: Vec<String> = ascii_art
.get_lines()
.iter()
.enumerate()
.map(|(i, line)| {
let wave_offset =
((progress * std::f64::consts::PI * 2.0 + i as f64 * 0.5).sin() * 3.0) as usize;
format!("{}{}", " ".repeat(wave_offset), line)
})
.collect();
EffectResult::new(lines.join("\n"))
}
fn name(&self) -> &str {
"wave"
}
}
// Jello effect
pub struct Jello;
impl Effect for Jello {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let wobble = (progress * std::f64::consts::PI * 4.0).sin() * (1.0 - progress);
let scale = 1.0 + wobble * 0.1;
let scaled = ascii_art.scale(scale.abs());
EffectResult::new(scaled.render()).with_scale(scale.abs())
}
fn name(&self) -> &str {
"jello"
}
}
// Rotate effects
pub struct RotateIn;
impl Effect for RotateIn {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
// Simulate rotation with scale and offset
let angle = (1.0 - progress) * std::f64::consts::PI;
let scale = progress;
let scaled = ascii_art.scale(scale);
let offset_x = (angle.cos() * 10.0 * (1.0 - progress)) as i32;
EffectResult::new(scaled.render())
.with_scale(scale)
.with_offset(offset_x, 0)
}
fn name(&self) -> &str {
"rotate-in"
}
}
pub struct RotateOut;
impl Effect for RotateOut {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
let angle = progress * std::f64::consts::PI;
let scale = 1.0 - progress;
let scaled = ascii_art.scale(scale);
let offset_x = (angle.cos() * 10.0 * progress) as i32;
EffectResult::new(scaled.render())
.with_scale(scale)
.with_offset(offset_x, 0)
}
fn name(&self) -> &str {
"rotate-out"
}
}
// Color effects (these will be enhanced by color engine)
pub struct ColorCycle;
impl Effect for ColorCycle {
fn apply(&self, ascii_art: &AsciiArt, _progress: f64) -> EffectResult {
EffectResult::new(ascii_art.render())
}
fn name(&self) -> &str {
"color-cycle"
}
}
pub struct Rainbow;
impl Effect for Rainbow {
fn apply(&self, ascii_art: &AsciiArt, _progress: f64) -> EffectResult {
EffectResult::new(ascii_art.render())
}
fn name(&self) -> &str {
"rainbow"
}
}
pub struct GradientFlow;
impl Effect for GradientFlow {
fn apply(&self, ascii_art: &AsciiArt, _progress: f64) -> EffectResult {
EffectResult::new(ascii_art.render())
}
fn name(&self) -> &str {
"gradient-flow"
}
}
/// Get effect by name
pub fn get_effect(name: &str) -> Result<Box<dyn Effect>> {
match name {
"fade-in" => Ok(Box::new(FadeIn)),
"fade-out" => Ok(Box::new(FadeOut)),
"fade-in-out" => Ok(Box::new(FadeInOut)),
"slide-in-top" => Ok(Box::new(SlideInTop)),
"slide-in-bottom" => Ok(Box::new(SlideInBottom)),
"slide-in-left" => Ok(Box::new(SlideInLeft)),
"slide-in-right" => Ok(Box::new(SlideInRight)),
"scale-up" => Ok(Box::new(ScaleUp)),
"scale-down" => Ok(Box::new(ScaleDown)),
"pulse" => Ok(Box::new(Pulse)),
"bounce-in" => Ok(Box::new(BounceIn)),
"bounce-out" => Ok(Box::new(BounceOut)),
"typewriter" => Ok(Box::new(Typewriter)),
"typewriter-reverse" => Ok(Box::new(TypewriterReverse)),
"wave" => Ok(Box::new(Wave)),
"jello" => Ok(Box::new(Jello)),
"color-cycle" => Ok(Box::new(ColorCycle)),
"rainbow" => Ok(Box::new(Rainbow)),
"gradient-flow" => Ok(Box::new(GradientFlow)),
"rotate-in" => Ok(Box::new(RotateIn)),
"rotate-out" => Ok(Box::new(RotateOut)),
_ => bail!("Unknown effect: {}", name),
}
}
/// List all available effects
#[allow(dead_code)]
pub fn list_effects() -> Vec<&'static str> {
vec![
"fade-in",
"fade-out",
"fade-in-out",
"slide-in-top",
"slide-in-bottom",
"slide-in-left",
"slide-in-right",
"scale-up",
"scale-down",
"pulse",
"bounce-in",
"bounce-out",
"typewriter",
"typewriter-reverse",
"wave",
"jello",
"color-cycle",
"rainbow",
"gradient-flow",
"rotate-in",
"rotate-out",
]
}

View File

@@ -1,7 +1,7 @@
pub mod effects;
pub mod easing; pub mod easing;
pub mod timeline; pub mod effects;
pub mod renderer; pub mod renderer;
pub mod timeline;
use crate::color::ColorEngine; use crate::color::ColorEngine;
use crate::utils::{ascii::AsciiArt, terminal::TerminalManager}; use crate::utils::{ascii::AsciiArt, terminal::TerminalManager};
@@ -27,22 +27,22 @@ impl AnimationEngine {
color_engine: ColorEngine::new(), color_engine: ColorEngine::new(),
} }
} }
pub fn with_effect(mut self, effect_name: &str) -> Result<Self> { pub fn with_effect(mut self, effect_name: &str) -> Result<Self> {
self.effect = effects::get_effect(effect_name)?; self.effect = effects::get_effect(effect_name)?;
Ok(self) Ok(self)
} }
pub fn with_easing(mut self, easing_name: &str) -> Result<Self> { pub fn with_easing(mut self, easing_name: &str) -> Result<Self> {
self.easing = easing::get_easing_function(easing_name)?; self.easing = easing::get_easing_function(easing_name)?;
Ok(self) Ok(self)
} }
pub fn with_color_engine(mut self, color_engine: ColorEngine) -> Self { pub fn with_color_engine(mut self, color_engine: ColorEngine) -> Self {
self.color_engine = color_engine; self.color_engine = color_engine;
self self
} }
pub async fn run(&self, terminal: &mut TerminalManager) -> Result<()> { pub async fn run(&self, terminal: &mut TerminalManager) -> Result<()> {
let renderer = renderer::Renderer::new( let renderer = renderer::Renderer::new(
&self.ascii_art, &self.ascii_art,
@@ -52,7 +52,7 @@ impl AnimationEngine {
&*self.easing, &*self.easing,
&self.color_engine, &self.color_engine,
); );
renderer.render(terminal).await renderer.render(terminal).await
} }
} }

148
src/animation/renderer.rs Normal file
View File

@@ -0,0 +1,148 @@
use crate::animation::{easing::EasingFunction, effects::Effect, timeline::Timeline};
use crate::color::{apply, ColorEngine};
use crate::utils::{ascii::AsciiArt, terminal::TerminalManager};
use anyhow::Result;
use tokio::time::sleep;
pub struct Renderer<'a> {
ascii_art: &'a AsciiArt,
timeline: Timeline,
effect: &'a dyn Effect,
easing: &'a dyn EasingFunction,
color_engine: &'a ColorEngine,
}
impl<'a> Renderer<'a> {
pub fn new(
ascii_art: &'a AsciiArt,
duration_ms: u64,
fps: u32,
effect: &'a dyn Effect,
easing: &'a dyn EasingFunction,
color_engine: &'a ColorEngine,
) -> Self {
Self {
ascii_art,
timeline: Timeline::new(duration_ms, fps),
effect,
easing,
color_engine,
}
}
pub async fn render(&self, terminal: &mut TerminalManager) -> Result<()> {
let mut timeline = Timeline::new(self.timeline.duration_ms(), self.timeline.fps());
timeline.start();
while !timeline.is_complete() {
let frame_start = std::time::Instant::now();
// Calculate progress with easing
let linear_progress = timeline.progress();
let eased_progress = self.easing.ease(linear_progress);
// Apply effect
let effect_result = self.effect.apply(self.ascii_art, eased_progress);
// Apply colors if available
let colored_text = if self.color_engine.has_colors() {
self.apply_colors(&effect_result.text, linear_progress)
} else {
effect_result.text.clone()
};
// Render to terminal
terminal.clear()?;
terminal.refresh_size()?;
// Apply offsets and render
if effect_result.offset_x == 0 && effect_result.offset_y == 0 {
terminal.print_centered(&colored_text)?;
} else {
let (width, height) = terminal.get_size();
let lines: Vec<&str> = colored_text.lines().collect();
let text_height = lines.len() as i32;
let text_width = lines.iter().map(|l| l.len()).max().unwrap_or(0) as i32;
let base_x = (width as i32 - text_width) / 2;
let base_y = (height as i32 - text_height) / 2;
let x = (base_x + effect_result.offset_x).max(0) as u16;
let y = (base_y + effect_result.offset_y).max(0) as u16;
for (i, line) in lines.iter().enumerate() {
let line_y = y.saturating_add(i as u16);
if line_y < height {
terminal.print_at(x, line_y, line)?;
}
}
}
// Wait for next frame
timeline.next_frame();
let frame_duration = timeline.frame_duration();
let elapsed = frame_start.elapsed();
if elapsed < frame_duration {
sleep(frame_duration - elapsed).await;
}
}
Ok(())
}
fn apply_colors(&self, text: &str, progress: f64) -> String {
match self.effect.name() {
"rainbow" | "color-cycle" => {
// For rainbow/color-cycle effects, use gradient across characters
let char_count = text.chars().filter(|c| !c.is_whitespace()).count();
let colors = self.color_engine.get_colors(char_count);
apply::apply_gradient_to_text(text, &colors)
}
"gradient-flow" => {
// For gradient-flow, shift colors based on progress
let char_count = text.chars().filter(|c| !c.is_whitespace()).count();
let mut colors = self.color_engine.get_colors(char_count * 2);
let offset = (progress * colors.len() as f64) as usize;
let len = colors.len();
colors.rotate_left(offset % len);
colors.truncate(char_count);
apply::apply_gradient_to_text(text, &colors)
}
_ => {
// For other effects, use gradient based on progress
if let Some(color) = self.color_engine.color_at(progress) {
let lines: Vec<String> = text
.lines()
.map(|line| apply::apply_color_to_line(line, &[color]))
.collect();
lines.join("\n")
} else {
let char_count = text.chars().filter(|c| !c.is_whitespace()).count();
let colors = self.color_engine.get_colors(char_count.max(10));
apply::apply_gradient_to_text(text, &colors)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::animation::easing::Linear;
use crate::animation::effects::FadeIn;
#[test]
fn test_renderer_creation() {
let ascii_art = AsciiArt::new("Test".to_string());
let effect = FadeIn;
let easing = Linear;
let color_engine = ColorEngine::new();
let renderer = Renderer::new(&ascii_art, 1000, 30, &effect, &easing, &color_engine);
assert_eq!(renderer.timeline.duration_ms(), 1000);
assert_eq!(renderer.timeline.fps(), 30);
}
}

123
src/animation/timeline.rs Normal file
View File

@@ -0,0 +1,123 @@
use std::time::{Duration, Instant};
pub struct Timeline {
duration_ms: u64,
fps: u32,
start_time: Option<Instant>,
current_frame: usize,
total_frames: usize,
}
impl Timeline {
pub fn new(duration_ms: u64, fps: u32) -> Self {
let total_frames = ((duration_ms as f64 / 1000.0) * fps as f64).ceil() as usize;
Self {
duration_ms,
fps,
start_time: None,
current_frame: 0,
total_frames,
}
}
pub fn start(&mut self) {
self.start_time = Some(Instant::now());
self.current_frame = 0;
}
#[allow(dead_code)]
pub fn reset(&mut self) {
self.start_time = None;
self.current_frame = 0;
}
pub fn is_complete(&self) -> bool {
self.current_frame >= self.total_frames
}
pub fn progress(&self) -> f64 {
if self.total_frames == 0 {
return 1.0;
}
(self.current_frame as f64 / self.total_frames as f64).min(1.0)
}
pub fn next_frame(&mut self) -> bool {
if self.is_complete() {
return false;
}
self.current_frame += 1;
true
}
pub fn frame_duration(&self) -> Duration {
Duration::from_millis(1000 / self.fps as u64)
}
#[allow(dead_code)]
pub fn elapsed(&self) -> Duration {
self.start_time
.map(|start| start.elapsed())
.unwrap_or(Duration::ZERO)
}
#[allow(dead_code)]
pub fn current_frame(&self) -> usize {
self.current_frame
}
#[allow(dead_code)]
pub fn total_frames(&self) -> usize {
self.total_frames
}
pub fn fps(&self) -> u32 {
self.fps
}
pub fn duration_ms(&self) -> u64 {
self.duration_ms
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timeline_creation() {
let timeline = Timeline::new(1000, 30);
assert_eq!(timeline.total_frames(), 30);
assert_eq!(timeline.fps(), 30);
}
#[test]
fn test_timeline_progress() {
let mut timeline = Timeline::new(1000, 10);
timeline.start();
assert_eq!(timeline.progress(), 0.0);
for _ in 0..5 {
timeline.next_frame();
}
assert_eq!(timeline.progress(), 0.5);
}
#[test]
fn test_timeline_completion() {
let mut timeline = Timeline::new(1000, 10);
timeline.start();
assert!(!timeline.is_complete());
for _ in 0..10 {
timeline.next_frame();
}
assert!(timeline.is_complete());
}
}

View File

@@ -8,30 +8,30 @@ pub struct PigletCli {
/// Text to render with figlet /// Text to render with figlet
#[arg(value_name = "TEXT")] #[arg(value_name = "TEXT")]
pub text: String, pub text: String,
/// Duration of animation (e.g., 3000ms, 0.3s, 0.5h, 5m) /// Duration of animation (e.g., 3000ms, 0.3s, 0.5h, 5m)
#[arg(short, long, default_value = "3s")] #[arg(short, long, default_value = "3s")]
pub duration: String, pub duration: String,
/// Color palette (hex or CSS4 colors, comma-separated) /// Color palette (hex or CSS4 colors, comma-separated)
/// Example: "#FF5733,#33FF57,#3357FF" or "red,green,blue" /// Example: "#FF5733,#33FF57,#3357FF" or "red,green,blue"
#[arg(short = 'p', long, value_delimiter = ',')] #[arg(short = 'p', long, value_delimiter = ',')]
pub color_palette: Option<Vec<String>>, pub color_palette: Option<Vec<String>>,
/// Color gradient (CSS4 gradient definition) /// Color gradient (CSS4 gradient definition)
/// Example: "linear-gradient(90deg, red, blue)" /// Example: "linear-gradient(90deg, red, blue)"
#[arg(short = 'g', long)] #[arg(short = 'g', long)]
pub color_gradient: Option<String>, pub color_gradient: Option<String>,
/// Motion easing function /// Motion easing function
/// Options: linear, ease-in, ease-out, ease-in-out, ease-in-quad, /// Options: linear, ease-in, ease-out, ease-in-out, ease-in-quad,
/// ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-out-cubic, /// ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-out-cubic,
/// ease-in-out-cubic, ease-in-back, ease-out-back, ease-in-out-back, /// ease-in-out-cubic, ease-in-back, ease-out-back, ease-in-out-back,
/// ease-in-elastic, ease-out-elastic, ease-in-out-elastic, /// ease-in-elastic, ease-out-elastic, ease-in-out-elastic,
/// ease-in-bounce, ease-out-bounce, ease-in-out-bounce /// ease-in-bounce, ease-out-bounce, ease-in-out-bounce
#[arg(short = 'i', long, default_value = "ease-in-out")] #[arg(short = 'i', long, default_value = "ease-in-out")]
pub motion_ease: String, pub motion_ease: String,
/// Motion effect name /// Motion effect name
/// Options: fade-in, fade-out, fade-in-out, slide-in-top, slide-in-bottom, /// Options: fade-in, fade-out, fade-in-out, slide-in-top, slide-in-bottom,
/// slide-in-left, slide-in-right, scale-up, scale-down, pulse, /// slide-in-left, slide-in-right, scale-up, scale-down, pulse,
@@ -39,33 +39,33 @@ pub struct PigletCli {
/// jello, color-cycle, rainbow, gradient-flow, rotate-in, rotate-out /// jello, color-cycle, rainbow, gradient-flow, rotate-in, rotate-out
#[arg(short, long, default_value = "fade-in")] #[arg(short, long, default_value = "fade-in")]
pub motion_effect: String, pub motion_effect: String,
/// Figlet font /// Figlet font
#[arg(short = 'f', long)] #[arg(short = 'f', long)]
pub font: Option<String>, pub font: Option<String>,
/// Additional figlet options (use after --) /// Additional figlet options (use after --)
/// Example: piglet "Text" -- -w 200 -c /// Example: piglet "Text" -- -w 200 -c
#[arg(last = true)] #[arg(last = true)]
pub figlet_args: Vec<String>, pub figlet_args: Vec<String>,
/// Loop animation infinitely /// Loop animation infinitely
#[arg(short, long)] #[arg(short, long)]
pub loop_animation: bool, pub loop_animation: bool,
/// Frame rate (fps) /// Frame rate (fps)
#[arg(long, default_value = "30")] #[arg(long, default_value = "30")]
pub fps: u32, pub fps: u32,
/// List all available effects /// List all available effects
#[arg(long)] #[arg(long)]
pub list_effects: bool, pub list_effects: bool,
/// List all available easing functions /// List all available easing functions
#[arg(long)] #[arg(long)]
pub list_easing: bool, pub list_easing: bool,
/// List all available CSS4 colors /// List all available CSS4 colors
#[arg(long)] #[arg(long)]
pub list_colors: bool, pub list_colors: bool,
} }

View File

@@ -3,13 +3,13 @@ use crossterm::style::Color as CrosstermColor;
pub fn apply_color_to_char(ch: char, color: Color) -> String { pub fn apply_color_to_char(ch: char, color: Color) -> String {
use crossterm::style::Stylize; use crossterm::style::Stylize;
let crossterm_color = CrosstermColor::Rgb { let crossterm_color = CrosstermColor::Rgb {
r: color.r, r: color.r,
g: color.g, g: color.g,
b: color.b, b: color.b,
}; };
format!("{}", ch.to_string().with(crossterm_color)) format!("{}", ch.to_string().with(crossterm_color))
} }
@@ -17,7 +17,7 @@ pub fn apply_color_to_line(line: &str, colors: &[Color]) -> String {
if colors.is_empty() { if colors.is_empty() {
return line.to_string(); return line.to_string();
} }
line.chars() line.chars()
.enumerate() .enumerate()
.map(|(i, ch)| { .map(|(i, ch)| {
@@ -34,14 +34,14 @@ pub fn apply_color_to_line(line: &str, colors: &[Color]) -> String {
pub fn apply_gradient_to_text(text: &str, colors: &[Color]) -> String { pub fn apply_gradient_to_text(text: &str, colors: &[Color]) -> String {
let lines: Vec<&str> = text.lines().collect(); let lines: Vec<&str> = text.lines().collect();
let total_chars: usize = lines.iter().map(|l| l.chars().count()).sum(); let total_chars: usize = lines.iter().map(|l| l.chars().count()).sum();
if total_chars == 0 || colors.is_empty() { if total_chars == 0 || colors.is_empty() {
return text.to_string(); return text.to_string();
} }
let mut result = String::new(); let mut result = String::new();
let mut char_index = 0; let mut char_index = 0;
for (line_idx, line) in lines.iter().enumerate() { for (line_idx, line) in lines.iter().enumerate() {
for ch in line.chars() { for ch in line.chars() {
if ch.is_whitespace() { if ch.is_whitespace() {
@@ -53,11 +53,11 @@ pub fn apply_gradient_to_text(text: &str, colors: &[Color]) -> String {
char_index += 1; char_index += 1;
} }
} }
if line_idx < lines.len() - 1 { if line_idx < lines.len() - 1 {
result.push('\n'); result.push('\n');
} }
} }
result result
} }

View File

@@ -1,7 +1,8 @@
use crate::parser::gradient::Gradient;
use crate::parser::color::Color; use crate::parser::color::Color;
use crate::parser::gradient::Gradient;
use anyhow::Result; use anyhow::Result;
#[derive(Debug, Clone)]
pub struct GradientEngine { pub struct GradientEngine {
gradient: Gradient, gradient: Gradient,
} }
@@ -10,17 +11,17 @@ impl GradientEngine {
pub fn new(gradient: Gradient) -> Self { pub fn new(gradient: Gradient) -> Self {
Self { gradient } Self { gradient }
} }
pub fn from_string(gradient_str: &str) -> Result<Self> { pub fn from_string(gradient_str: &str) -> Result<Self> {
let gradient = Gradient::parse(gradient_str)?; let gradient = Gradient::parse(gradient_str)?;
Ok(Self::new(gradient)) Ok(Self::new(gradient))
} }
pub fn color_at(&self, t: f64) -> Color { pub fn color_at(&self, t: f64) -> Color {
self.gradient.color_at(t) self.gradient.color_at(t)
} }
pub fn colors(&self, steps: usize) -> Vec<Color> { pub fn colors(&self, steps: usize) -> Vec<Color> {
self.gradient.colors(steps) self.gradient.colors(steps)
} }
} }

83
src/color/mod.rs Normal file
View File

@@ -0,0 +1,83 @@
pub mod apply;
pub mod gradient;
pub mod palette;
use crate::parser::color::Color;
use anyhow::Result;
pub use gradient::GradientEngine;
pub use palette::ColorPalette;
#[derive(Debug, Clone)]
pub enum ColorMode {
None,
Palette(ColorPalette),
Gradient(GradientEngine),
}
pub struct ColorEngine {
mode: ColorMode,
}
impl ColorEngine {
pub fn new() -> Self {
Self {
mode: ColorMode::None,
}
}
pub fn with_palette(mut self, palette: Option<&[String]>) -> Result<Self> {
if let Some(colors) = palette {
if !colors.is_empty() {
let palette = ColorPalette::from_strings(colors)?;
self.mode = ColorMode::Palette(palette);
}
}
Ok(self)
}
pub fn with_gradient(mut self, gradient: Option<&str>) -> Result<Self> {
if let Some(gradient_str) = gradient {
let gradient = GradientEngine::from_string(gradient_str)?;
self.mode = ColorMode::Gradient(gradient);
}
Ok(self)
}
pub fn has_colors(&self) -> bool {
!matches!(self.mode, ColorMode::None)
}
#[allow(dead_code)]
pub fn get_color(&self, t: f64, index: usize) -> Option<Color> {
match &self.mode {
ColorMode::None => None,
ColorMode::Palette(palette) => Some(palette.get_color(index)),
ColorMode::Gradient(gradient) => Some(gradient.color_at(t)),
}
}
#[allow(dead_code)]
pub fn get_colors(&self, steps: usize) -> Vec<Color> {
match &self.mode {
ColorMode::None => vec![],
ColorMode::Palette(palette) => (0..steps).map(|i| palette.get_color(i)).collect(),
ColorMode::Gradient(gradient) => gradient.colors(steps),
}
}
pub fn color_at(&self, t: f64) -> Option<Color> {
match &self.mode {
ColorMode::None => None,
ColorMode::Palette(palette) => {
Some(palette.get_color((t * palette.len() as f64) as usize))
}
ColorMode::Gradient(gradient) => Some(gradient.color_at(t)),
}
}
}
impl Default for ColorEngine {
fn default() -> Self {
Self::new()
}
}

View File

@@ -1,8 +1,53 @@
"#ffff00".to_string(), use crate::parser::color::Color;
]).unwrap() use anyhow::Result;
#[derive(Debug, Clone)]
pub struct ColorPalette {
colors: Vec<Color>,
}
impl ColorPalette {
pub fn new(colors: Vec<Color>) -> Self {
Self { colors }
} }
pub fn from_strings(color_strs: &[String]) -> Result<Self> {
let colors: Result<Vec<Color>> = color_strs.iter().map(|s| Color::parse(s)).collect();
Ok(Self::new(colors?))
}
pub fn get_color(&self, index: usize) -> Color {
if self.colors.is_empty() {
return Color::new(255, 255, 255);
}
self.colors[index % self.colors.len()]
}
pub fn len(&self) -> usize {
self.colors.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.colors.is_empty()
}
/// Create rainbow palette
pub fn rainbow() -> Self {
Self::from_strings(&[
"#ff0000".to_string(),
"#ff7f00".to_string(),
"#ffff00".to_string(),
"#00ff00".to_string(),
"#0000ff".to_string(),
"#4b0082".to_string(),
"#9400d3".to_string(),
])
.unwrap()
}
/// Create ocean palette /// Create ocean palette
#[allow(dead_code)]
pub fn ocean() -> Self { pub fn ocean() -> Self {
Self::from_strings(&[ Self::from_strings(&[
"#000080".to_string(), "#000080".to_string(),
@@ -10,7 +55,8 @@
"#4169e1".to_string(), "#4169e1".to_string(),
"#87ceeb".to_string(), "#87ceeb".to_string(),
"#add8e6".to_string(), "#add8e6".to_string(),
]).unwrap() ])
.unwrap()
} }
} }
@@ -18,4 +64,4 @@ impl Default for ColorPalette {
fn default() -> Self { fn default() -> Self {
Self::rainbow() Self::rainbow()
} }
} }

View File

@@ -1,4 +1,4 @@
use anyhow::{Context, Result, bail}; use anyhow::{bail, Context, Result};
use std::process::Command; use std::process::Command;
use which::which; use which::which;
@@ -14,78 +14,75 @@ impl FigletWrapper {
args: Vec::new(), args: Vec::new(),
} }
} }
pub fn with_font(mut self, font: Option<&str>) -> Self { pub fn with_font(mut self, font: Option<&str>) -> Self {
self.font = font.map(|s| s.to_string()); self.font = font.map(|s| s.to_string());
self self
} }
pub fn with_args(mut self, args: Vec<String>) -> Self { pub fn with_args(mut self, args: Vec<String>) -> Self {
self.args = args; self.args = args;
self self
} }
pub fn render(&self, text: &str) -> Result<String> { pub fn render(&self, text: &str) -> Result<String> {
let mut cmd = Command::new("figlet"); let mut cmd = Command::new("figlet");
// Add font if specified // Add font if specified
if let Some(font) = &self.font { if let Some(font) = &self.font {
cmd.arg("-f").arg(font); cmd.arg("-f").arg(font);
} }
// Add additional arguments // Add additional arguments
for arg in &self.args { for arg in &self.args {
cmd.arg(arg); cmd.arg(arg);
} }
// Add the text // Add the text
cmd.arg(text); cmd.arg(text);
// Execute and capture output // Execute and capture output
let output = cmd.output() let output = cmd.output().context("Failed to execute figlet")?;
.context("Failed to execute figlet")?;
if !output.status.success() { if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Figlet error: {}", stderr); bail!("Figlet error: {}", stderr);
} }
let result = String::from_utf8(output.stdout) let result =
.context("Figlet output is not valid UTF-8")?; String::from_utf8(output.stdout).context("Figlet output is not valid UTF-8")?;
Ok(result) Ok(result)
} }
pub fn check_installed() -> Result<()> { pub fn check_installed() -> Result<()> {
which("figlet") which("figlet").context(
.context("figlet not found. Please install figlet first.\n\ "figlet not found. Please install figlet first.\n\
On Ubuntu/Debian: sudo apt-get install figlet\n\ On Ubuntu/Debian: sudo apt-get install figlet\n\
On macOS: brew install figlet\n\ On macOS: brew install figlet\n\
On Arch: sudo pacman -S figlet")?; On Arch: sudo pacman -S figlet",
)?;
Ok(()) Ok(())
} }
#[allow(dead_code)]
pub fn list_fonts() -> Result<Vec<String>> { pub fn list_fonts() -> Result<Vec<String>> {
let output = Command::new("figlet") let output = Command::new("figlet")
.arg("-l") .arg("-l")
.output() .output()
.context("Failed to list figlet fonts")?; .context("Failed to list figlet fonts")?;
if !output.status.success() { if !output.status.success() {
bail!("Failed to list fonts"); bail!("Failed to list fonts");
} }
let result = String::from_utf8_lossy(&output.stdout); let result = String::from_utf8_lossy(&output.stdout);
let fonts: Vec<String> = result let fonts: Vec<String> = result
.lines() .lines()
.skip(1) // Skip header .skip(1) // Skip header
.filter_map(|line| { .filter_map(|line| line.split_whitespace().next().map(|s| s.to_string()))
line.split_whitespace()
.next()
.map(|s| s.to_string())
})
.collect(); .collect();
Ok(fonts) Ok(fonts)
} }
} }
@@ -99,13 +96,13 @@ impl Default for FigletWrapper {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_figlet_installed() { fn test_figlet_installed() {
// This test will fail if figlet is not installed // This test will fail if figlet is not installed
assert!(FigletWrapper::check_installed().is_ok()); assert!(FigletWrapper::check_installed().is_ok());
} }
#[test] #[test]
fn test_basic_render() { fn test_basic_render() {
let figlet = FigletWrapper::new(); let figlet = FigletWrapper::new();
@@ -115,4 +112,4 @@ mod tests {
assert!(!ascii.is_empty()); assert!(!ascii.is_empty());
assert!(ascii.contains("H") || ascii.contains("_") || ascii.contains("|")); assert!(ascii.contains("H") || ascii.contains("_") || ascii.contains("|"));
} }
} }

View File

@@ -1,8 +1,8 @@
pub mod cli;
pub mod figlet;
pub mod color;
pub mod animation; pub mod animation;
pub mod cli;
pub mod color;
pub mod figlet;
pub mod parser; pub mod parser;
pub mod utils; pub mod utils;
pub use cli::PigletCli; pub use cli::PigletCli;

View File

@@ -1,31 +1,31 @@
mod cli;
mod figlet;
mod color;
mod animation; mod animation;
mod cli;
mod color;
mod figlet;
mod parser; mod parser;
mod utils; mod utils;
use anyhow::Result; use anyhow::Result;
use cli::PigletCli;
use clap::Parser; use clap::Parser;
use cli::PigletCli;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
// Parse CLI arguments // Parse CLI arguments
let args = PigletCli::parse(); let args = PigletCli::parse();
// Show banner on first run // Show banner on first run
if std::env::args().len() == 1 { if std::env::args().len() == 1 {
show_welcome(); show_welcome();
return Ok(()); return Ok(());
} }
// Verify figlet is installed // Verify figlet is installed
figlet::FigletWrapper::check_installed()?; figlet::FigletWrapper::check_installed()?;
// Run the piglet magic // Run the piglet magic
run_piglet(args).await?; run_piglet(args).await?;
Ok(()) Ok(())
} }
@@ -33,69 +33,67 @@ async fn run_piglet(args: PigletCli) -> Result<()> {
use crate::animation::AnimationEngine; use crate::animation::AnimationEngine;
use crate::color::ColorEngine; use crate::color::ColorEngine;
use crate::utils::terminal::TerminalManager; use crate::utils::terminal::TerminalManager;
// Parse duration // Parse duration
let duration_ms = parser::duration::parse_duration(&args.duration)?; let duration_ms = parser::duration::parse_duration(&args.duration)?;
// Create figlet wrapper and render base ASCII art // Create figlet wrapper and render base ASCII art
let figlet = figlet::FigletWrapper::new() let figlet = figlet::FigletWrapper::new()
.with_font(args.font.as_deref()) .with_font(args.font.as_deref())
.with_args(args.figlet_args); .with_args(args.figlet_args);
let ascii_art = figlet.render(&args.text)?; let ascii_art = figlet.render(&args.text)?;
// Setup color engine // Setup color engine
let color_engine = ColorEngine::new() let color_engine = ColorEngine::new()
.with_palette(args.color_palette.as_deref()) .with_palette(args.color_palette.as_deref())?
.with_gradient(args.color_gradient.as_deref())?; .with_gradient(args.color_gradient.as_deref())?;
// Setup animation engine // Setup animation engine
let animation_engine = AnimationEngine::new( let animation_engine = AnimationEngine::new(ascii_art, duration_ms, args.fps)
ascii_art, .with_effect(&args.motion_effect)?
duration_ms, .with_easing(&args.motion_ease)?
args.fps, .with_color_engine(color_engine);
)
.with_effect(&args.motion_effect)?
.with_easing(&args.motion_ease)?
.with_color_engine(color_engine);
// Setup terminal // Setup terminal
let mut terminal = TerminalManager::new()?; let mut terminal = TerminalManager::new()?;
terminal.setup()?; terminal.setup()?;
// Run animation // Run animation
loop { loop {
animation_engine.run(&mut terminal).await?; animation_engine.run(&mut terminal).await?;
if !args.loop_animation { if !args.loop_animation {
break; break;
} }
} }
// Cleanup // Cleanup
terminal.cleanup()?; terminal.cleanup()?;
Ok(()) Ok(())
} }
fn show_welcome() { fn show_welcome() {
println!(r#" println!(
____ _ __ __ r"
____ _ __ __
/ __ \(_)___ _/ /__ / /_ / __ \(_)___ _/ /__ / /_
/ /_/ / / __ `/ / _ \/ __/ / /_/ / / __ `/ / _ \/ __/
/ ____/ / /_/ / / __/ /_ / ____/ / /_/ / / __/ /_
/_/ /_/\__, /_/\___/\__/ /_/ /_/\__, /_/\___/\__/
/____/ /____/
🐷 Piglet - Animated Figlet Wrapper 🐷 Piglet - Animated Figlet Wrapper
Usage: piglet [TEXT] [OPTIONS] Usage: piglet [TEXT] [OPTIONS]
Examples: Examples:
piglet "Hello" -p "#FF5733,#33FF57" piglet Hello -p red,blue,green
piglet "World" -g "linear-gradient(90deg, red, blue)" -e fade-in piglet World -g linear-gradient(90deg, red, blue) -e fade-in
piglet "Cool!" -e typewriter -d 2s -i ease-out piglet Cool! -e typewriter -d 2s -i ease-out
Run 'piglet --help' for more information. Run 'piglet --help' for more information.
"#); "
} );
}

View File

@@ -1,6 +1,5 @@
use anyhow::{Result, Context}; use anyhow::{Context, Result};
use csscolorparser::Color as CssColor; use csscolorparser::Color as CssColor;
use palette::rgb::Rgb;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Color { pub struct Color {
@@ -13,12 +12,35 @@ impl Color {
pub fn new(r: u8, g: u8, b: u8) -> Self { pub fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b } Self { r, g, b }
} }
pub fn from_hex(hex: &str) -> Result<Self> { pub fn from_hex(hex: &str) -> Result<Self> {
let color = CssColor::parse(hex) let color = hex
.parse::<CssColor>()
.context(format!("Failed to parse hex color: {}", hex))?; .context(format!("Failed to parse hex color: {}", hex))?;
Ok(Self { Ok(Self {
r: (color.r * 255.0) as u8, r: (color.r * 255.0) as u8,
g: (color.g * 255.0) as u8, g: (color.g * 255.0) as u8,
b b: (color.b * 255.0) as u8,
})
}
pub fn parse(color_str: &str) -> Result<Self> {
Self::from_hex(color_str)
}
pub fn interpolate(&self, other: &Color, t: f64) -> Color {
let t = t.clamp(0.0, 1.0);
Color {
r: (self.r as f64 + (other.r as f64 - self.r as f64) * t) as u8,
g: (self.g as f64 + (other.g as f64 - self.g as f64) * t) as u8,
b: (self.b as f64 + (other.b as f64 - self.b as f64) * t) as u8,
}
}
#[allow(dead_code)]
#[allow(clippy::wrong_self_convention)]
pub fn to_ansi(&self) -> String {
format!("\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
}
}

View File

@@ -1,24 +1,24 @@
use anyhow::{Result, bail}; use anyhow::{bail, Result};
use regex::Regex;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex;
lazy_static! { lazy_static! {
static ref DURATION_REGEX: Regex = Regex::new( static ref DURATION_REGEX: Regex = Regex::new(r"^(\d+(?:\.\d+)?)(ms|s|m|h)$").unwrap();
r"^(\d+(?:\.\d+)?)(ms|s|m|h)$"
).unwrap();
} }
/// Parse duration string to milliseconds /// Parse duration string to milliseconds
/// Supports: 3000ms, 0.3s, 5m, 0.5h /// Supports: 3000ms, 0.3s, 5m, 0.5h
pub fn parse_duration(duration: &str) -> Result<u64> { pub fn parse_duration(duration: &str) -> Result<u64> {
let caps = DURATION_REGEX.captures(duration.trim()) let caps = DURATION_REGEX
.captures(duration.trim())
.ok_or_else(|| anyhow::anyhow!("Invalid duration format: {}", duration))?; .ok_or_else(|| anyhow::anyhow!("Invalid duration format: {}", duration))?;
let value: f64 = caps[1].parse() let value: f64 = caps[1]
.parse()
.map_err(|_| anyhow::anyhow!("Invalid numeric value in duration"))?; .map_err(|_| anyhow::anyhow!("Invalid numeric value in duration"))?;
let unit = &caps[2]; let unit = &caps[2];
let milliseconds = match unit { let milliseconds = match unit {
"ms" => value, "ms" => value,
"s" => value * 1000.0, "s" => value * 1000.0,
@@ -26,47 +26,47 @@ pub fn parse_duration(duration: &str) -> Result<u64> {
"h" => value * 60.0 * 60.0 * 1000.0, "h" => value * 60.0 * 60.0 * 1000.0,
_ => bail!("Unknown time unit: {}", unit), _ => bail!("Unknown time unit: {}", unit),
}; };
if milliseconds < 0.0 { if milliseconds < 0.0 {
bail!("Duration cannot be negative"); bail!("Duration cannot be negative");
} }
Ok(milliseconds as u64) Ok(milliseconds as u64)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_parse_milliseconds() { fn test_parse_milliseconds() {
assert_eq!(parse_duration("3000ms").unwrap(), 3000); assert_eq!(parse_duration("3000ms").unwrap(), 3000);
assert_eq!(parse_duration("500ms").unwrap(), 500); assert_eq!(parse_duration("500ms").unwrap(), 500);
} }
#[test] #[test]
fn test_parse_seconds() { fn test_parse_seconds() {
assert_eq!(parse_duration("3s").unwrap(), 3000); assert_eq!(parse_duration("3s").unwrap(), 3000);
assert_eq!(parse_duration("0.5s").unwrap(), 500); assert_eq!(parse_duration("0.5s").unwrap(), 500);
assert_eq!(parse_duration("1.5s").unwrap(), 1500); assert_eq!(parse_duration("1.5s").unwrap(), 1500);
} }
#[test] #[test]
fn test_parse_minutes() { fn test_parse_minutes() {
assert_eq!(parse_duration("1m").unwrap(), 60000); assert_eq!(parse_duration("1m").unwrap(), 60000);
assert_eq!(parse_duration("0.5m").unwrap(), 30000); assert_eq!(parse_duration("0.5m").unwrap(), 30000);
} }
#[test] #[test]
fn test_parse_hours() { fn test_parse_hours() {
assert_eq!(parse_duration("1h").unwrap(), 3600000); assert_eq!(parse_duration("1h").unwrap(), 3600000);
assert_eq!(parse_duration("0.5h").unwrap(), 1800000); assert_eq!(parse_duration("0.5h").unwrap(), 1800000);
} }
#[test] #[test]
fn test_invalid_format() { fn test_invalid_format() {
assert!(parse_duration("invalid").is_err()); assert!(parse_duration("invalid").is_err());
assert!(parse_duration("10").is_err()); assert!(parse_duration("10").is_err());
assert!(parse_duration("10x").is_err()); assert!(parse_duration("10x").is_err());
} }
} }

121
src/parser/gradient.rs Normal file
View File

@@ -0,0 +1,121 @@
use crate::parser::color::Color;
use anyhow::{bail, Result};
#[derive(Debug, Clone)]
pub struct ColorStop {
pub color: Color,
pub position: f64,
}
#[derive(Debug, Clone)]
pub struct Gradient {
pub stops: Vec<ColorStop>,
#[allow(dead_code)] pub angle: f64,
}
impl Gradient {
pub fn new(stops: Vec<ColorStop>, angle: f64) -> Self {
Self { stops, angle }
}
pub fn parse(gradient_str: &str) -> Result<Self> {
let gradient_str = gradient_str.trim();
if !gradient_str.starts_with("linear-gradient(") {
bail!("Only linear-gradient is supported");
}
let content = gradient_str
.strip_prefix("linear-gradient(")
.and_then(|s| s.strip_suffix(")"))
.ok_or_else(|| anyhow::anyhow!("Invalid gradient syntax"))?;
let parts: Vec<&str> = content.split(',').map(|s| s.trim()).collect();
if parts.is_empty() {
bail!("Gradient must have at least one color");
}
let mut angle = 180.0;
let mut color_parts = parts.as_slice();
if let Some(first) = parts.first() {
if first.ends_with("deg") {
angle = first
.trim_end_matches("deg")
.trim()
.parse()
.unwrap_or(180.0);
color_parts = &parts[1..];
} else if first.starts_with("to ") {
angle = match first.trim() {
"to right" => 90.0,
"to left" => 270.0,
"to top" => 0.0,
"to bottom" => 180.0,
_ => 180.0,
};
color_parts = &parts[1..];
}
}
let mut stops = Vec::new();
let count = color_parts.len();
for (i, part) in color_parts.iter().enumerate() {
let part = part.trim();
let mut color_str = part;
let mut position = i as f64 / (count - 1).max(1) as f64;
// Check if there's a percentage (e.g., "#FF5733 50%" or "red 50%")
if let Some(percent_pos) = part.rfind('%') {
// Find the last space before the percentage
if let Some(space_pos) = part[..percent_pos].rfind(|c: char| c.is_whitespace()) {
color_str = part[..space_pos].trim();
let percent_str = part[space_pos + 1..percent_pos].trim();
if let Ok(p) = percent_str.parse::<f64>() {
position = p / 100.0;
}
}
}
let color = Color::parse(color_str)?;
stops.push(ColorStop { color, position });
}
Ok(Self::new(stops, angle))
}
pub fn color_at(&self, t: f64) -> Color {
if self.stops.is_empty() {
return Color::new(255, 255, 255);
}
if self.stops.len() == 1 {
return self.stops[0].color;
}
let t = t.clamp(0.0, 1.0);
for i in 0..self.stops.len() - 1 {
let stop1 = &self.stops[i];
let stop2 = &self.stops[i + 1];
if t >= stop1.position && t <= stop2.position {
let local_t = (t - stop1.position) / (stop2.position - stop1.position);
return stop1.color.interpolate(&stop2.color, local_t);
}
}
self.stops.last().unwrap().color
}
pub fn colors(&self, steps: usize) -> Vec<Color> {
(0..steps)
.map(|i| {
let t = i as f64 / (steps - 1).max(1) as f64;
self.color_at(t)
})
.collect()
}
}

View File

@@ -1,3 +1,3 @@
pub mod duration;
pub mod color; pub mod color;
pub mod gradient; pub mod duration;
pub mod gradient;

View File

@@ -1,5 +1,3 @@
use crate::parser::color::Color;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AsciiArt { pub struct AsciiArt {
lines: Vec<String>, lines: Vec<String>,
@@ -12,47 +10,49 @@ impl AsciiArt {
let lines: Vec<String> = text.lines().map(|s| s.to_string()).collect(); let lines: Vec<String> = text.lines().map(|s| s.to_string()).collect();
let width = lines.iter().map(|l| l.len()).max().unwrap_or(0); let width = lines.iter().map(|l| l.len()).max().unwrap_or(0);
let height = lines.len(); let height = lines.len();
Self { Self {
lines, lines,
width, width,
height, height,
} }
} }
pub fn get_lines(&self) -> &[String] { pub fn get_lines(&self) -> &[String] {
&self.lines &self.lines
} }
pub fn width(&self) -> usize { pub fn width(&self) -> usize {
self.width self.width
} }
pub fn height(&self) -> usize { pub fn height(&self) -> usize {
self.height self.height
} }
pub fn to_string(&self) -> String { pub fn render(&self) -> String {
self.lines.join("\n") self.lines.join("\n")
} }
/// Get character at position /// Get character at position
#[allow(dead_code)]
pub fn char_at(&self, x: usize, y: usize) -> Option<char> { pub fn char_at(&self, x: usize, y: usize) -> Option<char> {
self.lines.get(y)?.chars().nth(x) self.lines.get(y)?.chars().nth(x)
} }
/// Count non-whitespace characters /// Count non-whitespace characters
pub fn char_count(&self) -> usize { pub fn char_count(&self) -> usize {
self.lines.iter() self.lines
.iter()
.flat_map(|line| line.chars()) .flat_map(|line| line.chars())
.filter(|c| !c.is_whitespace()) .filter(|c| !c.is_whitespace())
.count() .count()
} }
/// Get all character positions /// Get all character positions
pub fn char_positions(&self) -> Vec<(usize, usize, char)> { pub fn char_positions(&self) -> Vec<(usize, usize, char)> {
let mut positions = Vec::new(); let mut positions = Vec::new();
for (y, line) in self.lines.iter().enumerate() { for (y, line) in self.lines.iter().enumerate() {
for (x, ch) in line.chars().enumerate() { for (x, ch) in line.chars().enumerate() {
if !ch.is_whitespace() { if !ch.is_whitespace() {
@@ -60,72 +60,66 @@ impl AsciiArt {
} }
} }
} }
positions positions
} }
/// Apply fade effect (0.0 = invisible, 1.0 = visible) /// Apply fade effect (0.0 = invisible, 1.0 = visible)
pub fn apply_fade(&self, opacity: f64) -> String { pub fn apply_fade(&self, opacity: f64) -> String {
if opacity >= 1.0 { if opacity >= 1.0 {
return self.to_string(); return self.render();
} }
if opacity <= 0.0 { if opacity <= 0.0 {
return " ".repeat(self.width).repeat(self.height); return " ".repeat(self.width).repeat(self.height);
} }
// For ASCII, we can simulate fade by replacing chars with lighter ones // For ASCII, we can simulate fade by replacing chars with lighter ones
let fade_chars = [' ', '.', '·', '-', '~', '=', '+', '*', '#', '@']; let fade_chars = [' ', '.', '·', '-', '~', '=', '+', '*', '#', '@'];
let index = (opacity * (fade_chars.len() - 1) as f64) as usize; let index = (opacity * (fade_chars.len() - 1) as f64) as usize;
let fade_char = fade_chars[index]; let fade_char = fade_chars[index];
self.lines.iter() self.lines
.iter()
.map(|line| { .map(|line| {
line.chars() line.chars()
.map(|ch| { .map(|ch| if ch.is_whitespace() { ch } else { fade_char })
if ch.is_whitespace() {
ch
} else {
fade_char
}
})
.collect::<String>() .collect::<String>()
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n") .join("\n")
} }
/// Scale the ASCII art /// Scale the ASCII art
pub fn scale(&self, factor: f64) -> Self { pub fn scale(&self, factor: f64) -> Self {
if factor <= 0.0 { if factor <= 0.0 {
return Self::new(String::new()); return Self::new(String::new());
} }
if (factor - 1.0).abs() < 0.01 { if (factor - 1.0).abs() < 0.01 {
return self.clone(); return self.clone();
} }
// Simple scaling by character repetition // Simple scaling by character repetition
let lines = if factor > 1.0 { let lines: Vec<String> = if factor > 1.0 {
self.lines.iter() self.lines
.iter()
.flat_map(|line| { .flat_map(|line| {
let scaled_line: String = line.chars() let scaled_line: String = line
.flat_map(|ch| std::iter::repeat(ch).take(factor as usize)) .chars()
.flat_map(|ch| std::iter::repeat_n(ch, factor as usize))
.collect(); .collect();
std::iter::repeat(scaled_line).take(factor as usize) std::iter::repeat_n(scaled_line, factor as usize)
}) })
.collect() .collect()
} else { } else {
self.lines.iter() self.lines
.iter()
.step_by((1.0 / factor) as usize) .step_by((1.0 / factor) as usize)
.map(|line| { .map(|line| line.chars().step_by((1.0 / factor) as usize).collect())
line.chars()
.step_by((1.0 / factor) as usize)
.collect()
})
.collect() .collect()
}; };
Self::new(lines.join("\n")) Self::new(lines.join("\n"))
} }
} }

View File

@@ -1,2 +1,2 @@
pub mod ascii;
pub mod terminal; pub mod terminal;
pub mod ascii;

View File

@@ -1,9 +1,7 @@
use anyhow::Result; use anyhow::Result;
use crossterm::{ use crossterm::{
cursor, cursor, execute,
execute,
terminal::{self, ClearType}, terminal::{self, ClearType},
ExecutableCommand,
}; };
use std::io::{stdout, Write}; use std::io::{stdout, Write};
@@ -22,74 +20,66 @@ impl TerminalManager {
original_state: false, original_state: false,
}) })
} }
pub fn setup(&mut self) -> Result<()> { pub fn setup(&mut self) -> Result<()> {
terminal::enable_raw_mode()?; terminal::enable_raw_mode()?;
execute!( execute!(stdout(), terminal::EnterAlternateScreen, cursor::Hide)?;
stdout(),
terminal::EnterAlternateScreen,
cursor::Hide
)?;
self.original_state = true; self.original_state = true;
Ok(()) Ok(())
} }
pub fn cleanup(&mut self) -> Result<()> { pub fn cleanup(&mut self) -> Result<()> {
if self.original_state { if self.original_state {
execute!( execute!(stdout(), cursor::Show, terminal::LeaveAlternateScreen)?;
stdout(),
cursor::Show,
terminal::LeaveAlternateScreen
)?;
terminal::disable_raw_mode()?; terminal::disable_raw_mode()?;
self.original_state = false; self.original_state = false;
} }
Ok(()) Ok(())
} }
pub fn clear(&self) -> Result<()> { pub fn clear(&self) -> Result<()> {
execute!(stdout(), terminal::Clear(ClearType::All))?; execute!(stdout(), terminal::Clear(ClearType::All))?;
Ok(()) Ok(())
} }
pub fn move_to(&self, x: u16, y: u16) -> Result<()> { pub fn move_to(&self, x: u16, y: u16) -> Result<()> {
execute!(stdout(), cursor::MoveTo(x, y))?; execute!(stdout(), cursor::MoveTo(x, y))?;
Ok(()) Ok(())
} }
pub fn get_size(&self) -> (u16, u16) { pub fn get_size(&self) -> (u16, u16) {
(self.width, self.height) (self.width, self.height)
} }
pub fn refresh_size(&mut self) -> Result<()> { pub fn refresh_size(&mut self) -> Result<()> {
let (width, height) = terminal::size()?; let (width, height) = terminal::size()?;
self.width = width; self.width = width;
self.height = height; self.height = height;
Ok(()) Ok(())
} }
pub fn print_at(&self, x: u16, y: u16, text: &str) -> Result<()> { pub fn print_at(&self, x: u16, y: u16, text: &str) -> Result<()> {
self.move_to(x, y)?; self.move_to(x, y)?;
print!("{}", text); print!("{}", text);
stdout().flush()?; stdout().flush()?;
Ok(()) Ok(())
} }
pub fn print_centered(&self, text: &str) -> Result<()> { pub fn print_centered(&self, text: &str) -> Result<()> {
let lines: Vec<&str> = text.lines().collect(); let lines: Vec<&str> = text.lines().collect();
let max_width = lines.iter().map(|l| l.len()).max().unwrap_or(0) as u16; let max_width = lines.iter().map(|l| l.len()).max().unwrap_or(0) as u16;
let height = lines.len() as u16; let height = lines.len() as u16;
let start_x = (self.width.saturating_sub(max_width)) / 2; let start_x = (self.width.saturating_sub(max_width)) / 2;
let start_y = (self.height.saturating_sub(height)) / 2; let start_y = (self.height.saturating_sub(height)) / 2;
for (i, line) in lines.iter().enumerate() { for (i, line) in lines.iter().enumerate() {
let line_width = line.len() as u16; let line_width = line.len() as u16;
let x = start_x + (max_width.saturating_sub(line_width)) / 2; let x = start_x + (max_width.saturating_sub(line_width)) / 2;
let y = start_y + i as u16; let y = start_y + i as u16;
self.print_at(x, y, line)?; self.print_at(x, y, line)?;
} }
Ok(()) Ok(())
} }
} }
@@ -98,4 +88,4 @@ impl Drop for TerminalManager {
fn drop(&mut self) { fn drop(&mut self) {
let _ = self.cleanup(); let _ = self.cleanup();
} }
} }

View File

@@ -1,11 +1,11 @@
use anyhow::Result;
use piglet::{ use piglet::{
figlet::FigletWrapper,
parser::{duration::parse_duration, color::Color, gradient::Gradient},
color::{ColorEngine, palette::ColorPalette},
animation::easing::get_easing_function, animation::easing::get_easing_function,
animation::effects::get_effect, animation::effects::get_effect,
color::{palette::ColorPalette, ColorEngine},
figlet::FigletWrapper,
parser::{color::Color, duration::parse_duration, gradient::Gradient},
}; };
use anyhow::Result;
#[test] #[test]
fn test_figlet_wrapper() -> Result<()> { fn test_figlet_wrapper() -> Result<()> {
@@ -31,12 +31,12 @@ fn test_color_parser() -> Result<()> {
assert_eq!(color.r, 255); assert_eq!(color.r, 255);
assert_eq!(color.g, 87); assert_eq!(color.g, 87);
assert_eq!(color.b, 51); assert_eq!(color.b, 51);
let color = Color::parse("red")?; let color = Color::parse("red")?;
assert_eq!(color.r, 255); assert_eq!(color.r, 255);
assert_eq!(color.g, 0); assert_eq!(color.g, 0);
assert_eq!(color.b, 0); assert_eq!(color.b, 0);
Ok(()) Ok(())
} }
@@ -44,15 +44,14 @@ fn test_color_parser() -> Result<()> {
fn test_gradient_parser() -> Result<()> { fn test_gradient_parser() -> Result<()> {
let gradient = Gradient::parse("linear-gradient(90deg, red, blue)")?; let gradient = Gradient::parse("linear-gradient(90deg, red, blue)")?;
assert_eq!(gradient.stops.len(), 2); assert_eq!(gradient.stops.len(), 2);
let gradient = Gradient::parse( let gradient =
"linear-gradient(to right, #FF5733 0%, #33FF57 50%, #3357FF 100%)" Gradient::parse("linear-gradient(to right, #FF5733 0%, #33FF57 50%, #3357FF 100%)")?;
)?;
assert_eq!(gradient.stops.len(), 3); assert_eq!(gradient.stops.len(), 3);
assert_eq!(gradient.stops[0].position, 0.0); assert_eq!(gradient.stops[0].position, 0.0);
assert_eq!(gradient.stops[1].position, 0.5); assert_eq!(gradient.stops[1].position, 0.5);
assert_eq!(gradient.stops[2].position, 1.0); assert_eq!(gradient.stops[2].position, 1.0);
Ok(()) Ok(())
} }
@@ -61,7 +60,7 @@ fn test_color_interpolation() {
let red = Color::new(255, 0, 0); let red = Color::new(255, 0, 0);
let blue = Color::new(0, 0, 255); let blue = Color::new(0, 0, 255);
let purple = red.interpolate(&blue, 0.5); let purple = red.interpolate(&blue, 0.5);
assert_eq!(purple.r, 127); assert_eq!(purple.r, 127);
assert_eq!(purple.g, 0); assert_eq!(purple.g, 0);
assert_eq!(purple.b, 127); assert_eq!(purple.b, 127);
@@ -69,19 +68,16 @@ fn test_color_interpolation() {
#[test] #[test]
fn test_color_palette() -> Result<()> { fn test_color_palette() -> Result<()> {
let palette = ColorPalette::from_strings(&[ let palette =
"red".to_string(), ColorPalette::from_strings(&["red".to_string(), "green".to_string(), "blue".to_string()])?;
"green".to_string(),
"blue".to_string(),
])?;
assert_eq!(palette.len(), 3); assert_eq!(palette.len(), 3);
let color = palette.get_color(0); let color = palette.get_color(0);
assert_eq!(color.r, 255); assert_eq!(color.r, 255);
assert_eq!(color.g, 0); assert_eq!(color.g, 0);
assert_eq!(color.b, 0); assert_eq!(color.b, 0);
Ok(()) Ok(())
} }
@@ -89,15 +85,15 @@ fn test_color_palette() -> Result<()> {
fn test_easing_functions() -> Result<()> { fn test_easing_functions() -> Result<()> {
let linear = get_easing_function("linear")?; let linear = get_easing_function("linear")?;
assert_eq!(linear.ease(0.5), 0.5); assert_eq!(linear.ease(0.5), 0.5);
let ease_in = get_easing_function("ease-in")?; let ease_in = get_easing_function("ease-in")?;
let result = ease_in.ease(0.5); let result = ease_in.ease(0.5);
assert!(result >= 0.0 && result <= 1.0); assert!((0.0..=1.0).contains(&result));
let ease_out_bounce = get_easing_function("ease-out-bounce")?; let ease_out_bounce = get_easing_function("ease-out-bounce")?;
let result = ease_out_bounce.ease(0.5); let result = ease_out_bounce.ease(0.5);
assert!(result >= 0.0 && result <= 1.5); // Bounce can overshoot assert!((0.0..=1.5).contains(&result)); // Bounce can overshoot
Ok(()) Ok(())
} }
@@ -105,45 +101,44 @@ fn test_easing_functions() -> Result<()> {
fn test_effects() -> Result<()> { fn test_effects() -> Result<()> {
let fade_in = get_effect("fade-in")?; let fade_in = get_effect("fade-in")?;
assert_eq!(fade_in.name(), "fade-in"); assert_eq!(fade_in.name(), "fade-in");
let typewriter = get_effect("typewriter")?; let typewriter = get_effect("typewriter")?;
assert_eq!(typewriter.name(), "typewriter"); assert_eq!(typewriter.name(), "typewriter");
let bounce = get_effect("bounce-in")?; let bounce = get_effect("bounce-in")?;
assert_eq!(bounce.name(), "bounce-in"); assert_eq!(bounce.name(), "bounce-in");
Ok(()) Ok(())
} }
#[test] #[test]
fn test_color_engine() -> Result<()> { fn test_color_engine() -> Result<()> {
let engine = ColorEngine::new() let engine = ColorEngine::new().with_palette(Some(&["red".to_string(), "blue".to_string()]))?;
.with_palette(Some(&["red".to_string(), "blue".to_string()]))?;
assert!(engine.has_colors()); assert!(engine.has_colors());
let color = engine.get_color(0.0, 0); let color = engine.get_color(0.0, 0);
assert!(color.is_some()); assert!(color.is_some());
Ok(()) Ok(())
} }
#[test] #[test]
fn test_gradient_color_at() -> Result<()> { fn test_gradient_color_at() -> Result<()> {
let gradient = Gradient::parse("linear-gradient(red, blue)")?; let gradient = Gradient::parse("linear-gradient(red, blue)")?;
let color_start = gradient.color_at(0.0); let color_start = gradient.color_at(0.0);
assert_eq!(color_start.r, 255); assert_eq!(color_start.r, 255);
assert_eq!(color_start.b, 0); assert_eq!(color_start.b, 0);
let color_end = gradient.color_at(1.0); let color_end = gradient.color_at(1.0);
assert_eq!(color_end.r, 0); assert_eq!(color_end.r, 0);
assert_eq!(color_end.b, 255); assert_eq!(color_end.b, 255);
let color_mid = gradient.color_at(0.5); let color_mid = gradient.color_at(0.5);
assert!(color_mid.r > 0 && color_mid.r < 255); assert!(color_mid.r > 0 && color_mid.r < 255);
assert!(color_mid.b > 0 && color_mid.b < 255); assert!(color_mid.b > 0 && color_mid.b < 255);
Ok(()) Ok(())
} }
@@ -168,4 +163,4 @@ fn test_invalid_effect() {
#[test] #[test]
fn test_invalid_easing() { fn test_invalid_easing() {
assert!(get_easing_function("not-an-easing").is_err()); assert!(get_easing_function("not-an-easing").is_err());
} }