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:
@@ -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 {
|
||||
@@ -13,12 +12,35 @@ impl Color {
|
||||
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
||||
Self { r, g, b }
|
||||
}
|
||||
|
||||
|
||||
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,24 +1,24 @@
|
||||
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];
|
||||
|
||||
|
||||
let milliseconds = match unit {
|
||||
"ms" => value,
|
||||
"s" => value * 1000.0,
|
||||
@@ -26,47 +26,47 @@ pub fn parse_duration(duration: &str) -> Result<u64> {
|
||||
"h" => value * 60.0 * 60.0 * 1000.0,
|
||||
_ => bail!("Unknown time unit: {}", unit),
|
||||
};
|
||||
|
||||
|
||||
if milliseconds < 0.0 {
|
||||
bail!("Duration cannot be negative");
|
||||
}
|
||||
|
||||
|
||||
Ok(milliseconds as u64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_parse_milliseconds() {
|
||||
assert_eq!(parse_duration("3000ms").unwrap(), 3000);
|
||||
assert_eq!(parse_duration("500ms").unwrap(), 500);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_parse_seconds() {
|
||||
assert_eq!(parse_duration("3s").unwrap(), 3000);
|
||||
assert_eq!(parse_duration("0.5s").unwrap(), 500);
|
||||
assert_eq!(parse_duration("1.5s").unwrap(), 1500);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_parse_minutes() {
|
||||
assert_eq!(parse_duration("1m").unwrap(), 60000);
|
||||
assert_eq!(parse_duration("0.5m").unwrap(), 30000);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_parse_hours() {
|
||||
assert_eq!(parse_duration("1h").unwrap(), 3600000);
|
||||
assert_eq!(parse_duration("0.5h").unwrap(), 1800000);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_invalid_format() {
|
||||
assert!(parse_duration("invalid").is_err());
|
||||
assert!(parse_duration("10").is_err());
|
||||
assert!(parse_duration("10x").is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 gradient;
|
||||
pub mod duration;
|
||||
pub mod gradient;
|
||||
|
||||
Reference in New Issue
Block a user