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

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

View File

@@ -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,
}
@@ -10,17 +11,17 @@ impl GradientEngine {
pub fn new(gradient: Gradient) -> Self {
Self { gradient }
}
pub fn from_string(gradient_str: &str) -> Result<Self> {
let gradient = Gradient::parse(gradient_str)?;
Ok(Self::new(gradient))
}
pub fn color_at(&self, t: f64) -> Color {
self.gradient.color_at(t)
}
pub fn colors(&self, steps: usize) -> Vec<Color> {
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(),
]).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()
}
}
@@ -18,4 +64,4 @@ impl Default for ColorPalette {
fn default() -> Self {
Self::rainbow()
}
}
}