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

View File

@@ -1,5 +1,3 @@
use crate::parser::color::Color;
#[derive(Debug, Clone)]
pub struct AsciiArt {
lines: Vec<String>,
@@ -12,47 +10,49 @@ impl AsciiArt {
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 height = lines.len();
Self {
lines,
width,
height,
}
}
pub fn get_lines(&self) -> &[String] {
&self.lines
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
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()
}
/// Get all character positions
pub fn char_positions(&self) -> Vec<(usize, usize, char)> {
let mut positions = Vec::new();
for (y, line) in self.lines.iter().enumerate() {
for (x, ch) in line.chars().enumerate() {
if !ch.is_whitespace() {
@@ -60,72 +60,66 @@ impl AsciiArt {
}
}
}
positions
}
/// 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 {
return " ".repeat(self.width).repeat(self.height);
}
// For ASCII, we can simulate fade by replacing chars with lighter ones
let fade_chars = [' ', '.', '·', '-', '~', '=', '+', '*', '#', '@'];
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<_>>()
.join("\n")
}
/// Scale the ASCII art
pub fn scale(&self, factor: f64) -> Self {
if factor <= 0.0 {
return Self::new(String::new());
}
if (factor - 1.0).abs() < 0.01 {
return self.clone();
}
// 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()
};
Self::new(lines.join("\n"))
}
}
}

View File

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

View File

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