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:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -16,13 +16,13 @@ csscolorparser = "0.6"
|
||||
palette = "0.7"
|
||||
|
||||
# Animation & Interpolation
|
||||
scirs2-interpolate = "0.1"
|
||||
# scirs2-interpolate = "0.1.0-rc.2" # Not needed, using custom easing functions
|
||||
|
||||
# Terminal manipulation
|
||||
crossterm = "0.27"
|
||||
|
||||
# Async runtime (for timing)
|
||||
tokio = { version = "1.35", features = ["time", "rt"] }
|
||||
tokio = { version = "1.35", features = ["time", "rt-multi-thread", "macros"] }
|
||||
|
||||
# Process execution
|
||||
which = "5.0"
|
||||
|
||||
@@ -1,49 +1,318 @@
|
||||
use anyhow::{Result, bail};
|
||||
use scirs2_interpolate::*;
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
pub trait EasingFunction: Send + Sync {
|
||||
fn ease(&self, t: f64) -> f64;
|
||||
#[allow(dead_code)]
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
// Linear
|
||||
pub struct Linear;
|
||||
impl EasingFunction for Linear {
|
||||
fn ease(&self, t: f64) -> f64 { t }
|
||||
fn name(&self) -> &str { "linear" }
|
||||
fn ease(&self, t: f64) -> f64 {
|
||||
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
|
||||
pub struct EaseInQuad;
|
||||
impl EasingFunction for EaseInQuad {
|
||||
fn ease(&self, t: f64) -> f64 { quad_ease_in(t, 0.0, 1.0, 1.0) }
|
||||
fn name(&self) -> &str { "ease-in-quad" }
|
||||
fn ease(&self, t: f64) -> f64 {
|
||||
t * t
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
fn name(&self) -> &str {
|
||||
"ease-in-quad"
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutQuad;
|
||||
impl EasingFunction for EaseOutQuad {
|
||||
fn ease(&self, t: f64) -> f64 { quad_ease_out(t, 0.0, 1.0, 1.0) }
|
||||
fn name(&self) -> &str { "ease-out-quad" }
|
||||
fn ease(&self, t: f64) -> f64 {
|
||||
t * (2.0 - t)
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
fn name(&self) -> &str {
|
||||
"ease-out-quad"
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseInOutQuad;
|
||||
impl EasingFunction for EaseInOutQuad {
|
||||
fn ease(&self, t: f64) -> f64 { quad_ease_in_out(t, 0.0, 1.0, 1.0) }
|
||||
fn name(&self) -> &str { "ease-in-out-quad" }
|
||||
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-quad"
|
||||
}
|
||||
}
|
||||
|
||||
// Cubic
|
||||
pub struct EaseInCubic;
|
||||
impl EasingFunction for EaseInCubic {
|
||||
fn ease(&self, t: f64) -> f64 { cubic_ease_in(t, 0.0, 1.0, 1.0) }
|
||||
fn name(&self) -> &str { "ease-in-cubic" }
|
||||
fn ease(&self, t: f64) -> f64 {
|
||||
t * t * t
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
fn name(&self) -> &str {
|
||||
"ease-in-cubic"
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EaseOutCubic;
|
||||
impl EasingFunction for EaseOutCubic {
|
||||
fn ease(&self, t: f64) -> f64 { cubic_ease_out(t, 0.0, 1.0, 1.0) }
|
||||
fn name(&self) -> &str { "ease-out-cubic" }
|
||||
fn ease(&self, t: f64) -> f64 {
|
||||
let t1 = t - 1.0;
|
||||
t1 * t1 * t1 + 1.0
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
fn name(&self) -> &str {
|
||||
"ease-out-cubic"
|
||||
}
|
||||
}
|
||||
|
||||
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
450
src/animation/effects.rs
Normal 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",
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod effects;
|
||||
pub mod easing;
|
||||
pub mod timeline;
|
||||
pub mod effects;
|
||||
pub mod renderer;
|
||||
pub mod timeline;
|
||||
|
||||
use crate::color::ColorEngine;
|
||||
use crate::utils::{ascii::AsciiArt, terminal::TerminalManager};
|
||||
|
||||
148
src/animation/renderer.rs
Normal file
148
src/animation/renderer.rs
Normal 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
123
src/animation/timeline.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::parser::gradient::Gradient;
|
||||
use crate::parser::color::Color;
|
||||
use crate::parser::gradient::Gradient;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GradientEngine {
|
||||
gradient: Gradient,
|
||||
}
|
||||
|
||||
83
src/color/mod.rs
Normal file
83
src/color/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,53 @@
|
||||
"#ffff00".to_string(),
|
||||
]).unwrap()
|
||||
use crate::parser::color::Color;
|
||||
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
|
||||
#[allow(dead_code)]
|
||||
pub fn ocean() -> Self {
|
||||
Self::from_strings(&[
|
||||
"#000080".to_string(),
|
||||
@@ -10,7 +55,8 @@
|
||||
"#4169e1".to_string(),
|
||||
"#87ceeb".to_string(),
|
||||
"#add8e6".to_string(),
|
||||
]).unwrap()
|
||||
])
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{Context, Result, bail};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::process::Command;
|
||||
use which::which;
|
||||
|
||||
@@ -42,29 +42,30 @@ impl FigletWrapper {
|
||||
cmd.arg(text);
|
||||
|
||||
// Execute and capture output
|
||||
let output = cmd.output()
|
||||
.context("Failed to execute figlet")?;
|
||||
let output = cmd.output().context("Failed to execute figlet")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("Figlet error: {}", stderr);
|
||||
}
|
||||
|
||||
let result = String::from_utf8(output.stdout)
|
||||
.context("Figlet output is not valid UTF-8")?;
|
||||
let result =
|
||||
String::from_utf8(output.stdout).context("Figlet output is not valid UTF-8")?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn check_installed() -> Result<()> {
|
||||
which("figlet")
|
||||
.context("figlet not found. Please install figlet first.\n\
|
||||
which("figlet").context(
|
||||
"figlet not found. Please install figlet first.\n\
|
||||
On Ubuntu/Debian: sudo apt-get install figlet\n\
|
||||
On macOS: brew install figlet\n\
|
||||
On Arch: sudo pacman -S figlet")?;
|
||||
On Arch: sudo pacman -S figlet",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn list_fonts() -> Result<Vec<String>> {
|
||||
let output = Command::new("figlet")
|
||||
.arg("-l")
|
||||
@@ -79,11 +80,7 @@ impl FigletWrapper {
|
||||
let fonts: Vec<String> = result
|
||||
.lines()
|
||||
.skip(1) // Skip header
|
||||
.filter_map(|line| {
|
||||
line.split_whitespace()
|
||||
.next()
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.filter_map(|line| line.split_whitespace().next().map(|s| s.to_string()))
|
||||
.collect();
|
||||
|
||||
Ok(fonts)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod cli;
|
||||
pub mod figlet;
|
||||
pub mod color;
|
||||
pub mod animation;
|
||||
pub mod cli;
|
||||
pub mod color;
|
||||
pub mod figlet;
|
||||
pub mod parser;
|
||||
pub mod utils;
|
||||
|
||||
|
||||
28
src/main.rs
28
src/main.rs
@@ -1,13 +1,13 @@
|
||||
mod cli;
|
||||
mod figlet;
|
||||
mod color;
|
||||
mod animation;
|
||||
mod cli;
|
||||
mod color;
|
||||
mod figlet;
|
||||
mod parser;
|
||||
mod utils;
|
||||
|
||||
use anyhow::Result;
|
||||
use cli::PigletCli;
|
||||
use clap::Parser;
|
||||
use cli::PigletCli;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
@@ -46,15 +46,11 @@ async fn run_piglet(args: PigletCli) -> Result<()> {
|
||||
|
||||
// Setup color engine
|
||||
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())?;
|
||||
|
||||
// Setup animation engine
|
||||
let animation_engine = AnimationEngine::new(
|
||||
ascii_art,
|
||||
duration_ms,
|
||||
args.fps,
|
||||
)
|
||||
let animation_engine = AnimationEngine::new(ascii_art, duration_ms, args.fps)
|
||||
.with_effect(&args.motion_effect)?
|
||||
.with_easing(&args.motion_ease)?
|
||||
.with_color_engine(color_engine);
|
||||
@@ -79,7 +75,8 @@ async fn run_piglet(args: PigletCli) -> Result<()> {
|
||||
}
|
||||
|
||||
fn show_welcome() {
|
||||
println!(r#"
|
||||
println!(
|
||||
r"
|
||||
____ _ __ __
|
||||
/ __ \(_)___ _/ /__ / /_
|
||||
/ /_/ / / __ `/ / _ \/ __/
|
||||
@@ -92,10 +89,11 @@ fn show_welcome() {
|
||||
Usage: piglet [TEXT] [OPTIONS]
|
||||
|
||||
Examples:
|
||||
piglet "Hello" -p "#FF5733,#33FF57"
|
||||
piglet "World" -g "linear-gradient(90deg, red, blue)" -e fade-in
|
||||
piglet "Cool!" -e typewriter -d 2s -i ease-out
|
||||
piglet Hello -p red,blue,green
|
||||
piglet World -g linear-gradient(90deg, red, blue) -e fade-in
|
||||
piglet Cool! -e typewriter -d 2s -i ease-out
|
||||
|
||||
Run 'piglet --help' for more information.
|
||||
"#);
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::{Result, Context};
|
||||
use anyhow::{Context, Result};
|
||||
use csscolorparser::Color as CssColor;
|
||||
use palette::rgb::Rgb;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Color {
|
||||
@@ -15,10 +14,33 @@ impl Color {
|
||||
}
|
||||
|
||||
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))?;
|
||||
|
||||
Ok(Self {
|
||||
r: (color.r * 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
use anyhow::{Result, bail};
|
||||
use regex::Regex;
|
||||
use anyhow::{bail, Result};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
|
||||
lazy_static! {
|
||||
static ref DURATION_REGEX: Regex = Regex::new(
|
||||
r"^(\d+(?:\.\d+)?)(ms|s|m|h)$"
|
||||
).unwrap();
|
||||
static ref DURATION_REGEX: Regex = Regex::new(r"^(\d+(?:\.\d+)?)(ms|s|m|h)$").unwrap();
|
||||
}
|
||||
|
||||
/// Parse duration string to milliseconds
|
||||
/// Supports: 3000ms, 0.3s, 5m, 0.5h
|
||||
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))?;
|
||||
|
||||
let value: f64 = caps[1].parse()
|
||||
let value: f64 = caps[1]
|
||||
.parse()
|
||||
.map_err(|_| anyhow::anyhow!("Invalid numeric value in duration"))?;
|
||||
|
||||
let unit = &caps[2];
|
||||
|
||||
121
src/parser/gradient.rs
Normal file
121
src/parser/gradient.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod duration;
|
||||
pub mod color;
|
||||
pub mod duration;
|
||||
pub mod gradient;
|
||||
@@ -1,5 +1,3 @@
|
||||
use crate::parser::color::Color;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AsciiArt {
|
||||
lines: Vec<String>,
|
||||
@@ -32,18 +30,20 @@ impl AsciiArt {
|
||||
self.height
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
pub fn render(&self) -> String {
|
||||
self.lines.join("\n")
|
||||
}
|
||||
|
||||
/// Get character at position
|
||||
#[allow(dead_code)]
|
||||
pub fn char_at(&self, x: usize, y: usize) -> Option<char> {
|
||||
self.lines.get(y)?.chars().nth(x)
|
||||
}
|
||||
|
||||
/// Count non-whitespace characters
|
||||
pub fn char_count(&self) -> usize {
|
||||
self.lines.iter()
|
||||
self.lines
|
||||
.iter()
|
||||
.flat_map(|line| line.chars())
|
||||
.filter(|c| !c.is_whitespace())
|
||||
.count()
|
||||
@@ -67,7 +67,7 @@ impl AsciiArt {
|
||||
/// Apply fade effect (0.0 = invisible, 1.0 = visible)
|
||||
pub fn apply_fade(&self, opacity: f64) -> String {
|
||||
if opacity >= 1.0 {
|
||||
return self.to_string();
|
||||
return self.render();
|
||||
}
|
||||
|
||||
if opacity <= 0.0 {
|
||||
@@ -79,16 +79,11 @@ impl AsciiArt {
|
||||
let index = (opacity * (fade_chars.len() - 1) as f64) as usize;
|
||||
let fade_char = fade_chars[index];
|
||||
|
||||
self.lines.iter()
|
||||
self.lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
line.chars()
|
||||
.map(|ch| {
|
||||
if ch.is_whitespace() {
|
||||
ch
|
||||
} else {
|
||||
fade_char
|
||||
}
|
||||
})
|
||||
.map(|ch| if ch.is_whitespace() { ch } else { fade_char })
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -106,23 +101,22 @@ impl AsciiArt {
|
||||
}
|
||||
|
||||
// Simple scaling by character repetition
|
||||
let lines = if factor > 1.0 {
|
||||
self.lines.iter()
|
||||
let lines: Vec<String> = if factor > 1.0 {
|
||||
self.lines
|
||||
.iter()
|
||||
.flat_map(|line| {
|
||||
let scaled_line: String = line.chars()
|
||||
.flat_map(|ch| std::iter::repeat(ch).take(factor as usize))
|
||||
let scaled_line: String = line
|
||||
.chars()
|
||||
.flat_map(|ch| std::iter::repeat_n(ch, factor as usize))
|
||||
.collect();
|
||||
std::iter::repeat(scaled_line).take(factor as usize)
|
||||
std::iter::repeat_n(scaled_line, factor as usize)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
self.lines.iter()
|
||||
self.lines
|
||||
.iter()
|
||||
.step_by((1.0 / factor) as usize)
|
||||
.map(|line| {
|
||||
line.chars()
|
||||
.step_by((1.0 / factor) as usize)
|
||||
.collect()
|
||||
})
|
||||
.map(|line| line.chars().step_by((1.0 / factor) as usize).collect())
|
||||
.collect()
|
||||
};
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
pub mod terminal;
|
||||
pub mod ascii;
|
||||
pub mod terminal;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
cursor,
|
||||
execute,
|
||||
cursor, execute,
|
||||
terminal::{self, ClearType},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use std::io::{stdout, Write};
|
||||
|
||||
@@ -25,22 +23,14 @@ impl TerminalManager {
|
||||
|
||||
pub fn setup(&mut self) -> Result<()> {
|
||||
terminal::enable_raw_mode()?;
|
||||
execute!(
|
||||
stdout(),
|
||||
terminal::EnterAlternateScreen,
|
||||
cursor::Hide
|
||||
)?;
|
||||
execute!(stdout(), terminal::EnterAlternateScreen, cursor::Hide)?;
|
||||
self.original_state = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cleanup(&mut self) -> Result<()> {
|
||||
if self.original_state {
|
||||
execute!(
|
||||
stdout(),
|
||||
cursor::Show,
|
||||
terminal::LeaveAlternateScreen
|
||||
)?;
|
||||
execute!(stdout(), cursor::Show, terminal::LeaveAlternateScreen)?;
|
||||
terminal::disable_raw_mode()?;
|
||||
self.original_state = false;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use anyhow::Result;
|
||||
use piglet::{
|
||||
figlet::FigletWrapper,
|
||||
parser::{duration::parse_duration, color::Color, gradient::Gradient},
|
||||
color::{ColorEngine, palette::ColorPalette},
|
||||
animation::easing::get_easing_function,
|
||||
animation::effects::get_effect,
|
||||
color::{palette::ColorPalette, ColorEngine},
|
||||
figlet::FigletWrapper,
|
||||
parser::{color::Color, duration::parse_duration, gradient::Gradient},
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
||||
#[test]
|
||||
fn test_figlet_wrapper() -> Result<()> {
|
||||
@@ -45,9 +45,8 @@ fn test_gradient_parser() -> Result<()> {
|
||||
let gradient = Gradient::parse("linear-gradient(90deg, red, blue)")?;
|
||||
assert_eq!(gradient.stops.len(), 2);
|
||||
|
||||
let gradient = Gradient::parse(
|
||||
"linear-gradient(to right, #FF5733 0%, #33FF57 50%, #3357FF 100%)"
|
||||
)?;
|
||||
let gradient =
|
||||
Gradient::parse("linear-gradient(to right, #FF5733 0%, #33FF57 50%, #3357FF 100%)")?;
|
||||
assert_eq!(gradient.stops.len(), 3);
|
||||
assert_eq!(gradient.stops[0].position, 0.0);
|
||||
assert_eq!(gradient.stops[1].position, 0.5);
|
||||
@@ -69,11 +68,8 @@ fn test_color_interpolation() {
|
||||
|
||||
#[test]
|
||||
fn test_color_palette() -> Result<()> {
|
||||
let palette = ColorPalette::from_strings(&[
|
||||
"red".to_string(),
|
||||
"green".to_string(),
|
||||
"blue".to_string(),
|
||||
])?;
|
||||
let palette =
|
||||
ColorPalette::from_strings(&["red".to_string(), "green".to_string(), "blue".to_string()])?;
|
||||
|
||||
assert_eq!(palette.len(), 3);
|
||||
|
||||
@@ -92,11 +88,11 @@ fn test_easing_functions() -> Result<()> {
|
||||
|
||||
let ease_in = get_easing_function("ease-in")?;
|
||||
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 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(())
|
||||
}
|
||||
@@ -117,8 +113,7 @@ fn test_effects() -> Result<()> {
|
||||
|
||||
#[test]
|
||||
fn test_color_engine() -> Result<()> {
|
||||
let engine = ColorEngine::new()
|
||||
.with_palette(Some(&["red".to_string(), "blue".to_string()]))?;
|
||||
let engine = ColorEngine::new().with_palette(Some(&["red".to_string(), "blue".to_string()]))?;
|
||||
|
||||
assert!(engine.has_colors());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user