Initial commit: Piglet - Animated figlet wrapper
Add complete Rust implementation with: - 20+ motion effects (fade, slide, scale, typewriter, wave, bounce, etc.) - 18+ easing functions (linear, quad, cubic, elastic, back, bounce) - Color support (CSS4 colors, hex codes, gradients) - Figlet integration with custom fonts and options - Cross-platform support (Linux, macOS, Windows) - Comprehensive CI/CD workflows - Full test suite with integration tests - Documentation (README.md, CLAUDE.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
131
src/utils/ascii.rs
Normal file
131
src/utils/ascii.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use crate::parser::color::Color;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AsciiArt {
|
||||
lines: Vec<String>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
|
||||
impl AsciiArt {
|
||||
pub fn new(text: String) -> Self {
|
||||
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 {
|
||||
self.lines.join("\n")
|
||||
}
|
||||
|
||||
/// Get character at position
|
||||
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()
|
||||
.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() {
|
||||
positions.push((x, y, ch));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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()
|
||||
.map(|line| {
|
||||
line.chars()
|
||||
.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()
|
||||
.flat_map(|line| {
|
||||
let scaled_line: String = line.chars()
|
||||
.flat_map(|ch| std::iter::repeat(ch).take(factor as usize))
|
||||
.collect();
|
||||
std::iter::repeat(scaled_line).take(factor as usize)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
self.lines.iter()
|
||||
.step_by((1.0 / factor) as usize)
|
||||
.map(|line| {
|
||||
line.chars()
|
||||
.step_by((1.0 / factor) as usize)
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
Self::new(lines.join("\n"))
|
||||
}
|
||||
}
|
||||
2
src/utils/mod.rs
Normal file
2
src/utils/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod terminal;
|
||||
pub mod ascii;
|
||||
101
src/utils/terminal.rs
Normal file
101
src/utils/terminal.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
cursor,
|
||||
execute,
|
||||
terminal::{self, ClearType},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use std::io::{stdout, Write};
|
||||
|
||||
pub struct TerminalManager {
|
||||
width: u16,
|
||||
height: u16,
|
||||
original_state: bool,
|
||||
}
|
||||
|
||||
impl TerminalManager {
|
||||
pub fn new() -> Result<Self> {
|
||||
let (width, height) = terminal::size()?;
|
||||
Ok(Self {
|
||||
width,
|
||||
height,
|
||||
original_state: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn setup(&mut self) -> Result<()> {
|
||||
terminal::enable_raw_mode()?;
|
||||
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
|
||||
)?;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerminalManager {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.cleanup();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user