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

148
src/animation/renderer.rs Normal file
View 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);
}
}