Add Phase 2 specialty & combination animation effects

Implements 10 new advanced animation effects:
- puff-in: Scale up from tiny with fade in
- puff-out: Scale down to tiny with fade out
- slide-rotate-hor: Slide from left with rotation
- slide-rotate-ver: Slide from top with rotation
- flicker: Fast flickering that stabilizes over time
- tracking-in: Letter spacing contracts from wide to normal
- tracking-out: Letter spacing expands from normal to wide
- bounce-top: Bounce down from top with easing
- bounce-bottom: Bounce up from bottom with easing
- tilt-in: Tilt in with perspective simulation

All effects combine multiple transformations (scale, offset,
opacity) for rich visual experiences. Registered in get_effect()
and list_effects() for CLI usage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-09 11:51:48 +01:00
parent 4cc8d5c489
commit 59cd854f55

View File

@@ -589,6 +589,219 @@ impl Effect for RollOut {
}
}
// Phase 2: Specialty & Combination Effects
// Puff-in effect - scale up from tiny with fade in
pub struct PuffIn;
impl Effect for PuffIn {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
// Start very small and expand while fading in
let scale = 0.1 + (progress * 0.9);
let opacity = progress;
let scaled = ascii_art.scale(scale);
EffectResult::new(scaled.render())
.with_scale(scale)
.with_opacity(opacity)
}
fn name(&self) -> &str {
"puff-in"
}
}
// Puff-out effect - scale down to tiny with fade out
pub struct PuffOut;
impl Effect for PuffOut {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
// Shrink down while fading out
let scale = 1.0 - (progress * 0.9);
let opacity = 1.0 - progress;
let scaled = ascii_art.scale(scale.max(0.1));
EffectResult::new(scaled.render())
.with_scale(scale)
.with_opacity(opacity)
}
fn name(&self) -> &str {
"puff-out"
}
}
// Slide-rotate horizontal - slide from left with rotation
pub struct SlideRotateHor;
impl Effect for SlideRotateHor {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
// Slide in from left while rotating
let offset_x = ((1.0 - progress) * -(ascii_art.width() as f64 + 10.0)) as i32;
let rotation_progress = 1.0 - progress;
let offset_y =
(rotation_progress * 10.0 * (rotation_progress * std::f64::consts::PI).sin()) as i32;
EffectResult::new(ascii_art.render()).with_offset(offset_x, offset_y)
}
fn name(&self) -> &str {
"slide-rotate-hor"
}
}
// Slide-rotate vertical - slide from top with rotation
pub struct SlideRotateVer;
impl Effect for SlideRotateVer {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
// Slide in from top while rotating
let offset_y = ((1.0 - progress) * -(ascii_art.height() as f64 + 5.0)) as i32;
let rotation_progress = 1.0 - progress;
let offset_x =
(rotation_progress * 15.0 * (rotation_progress * std::f64::consts::PI).cos()) as i32;
EffectResult::new(ascii_art.render()).with_offset(offset_x, offset_y)
}
fn name(&self) -> &str {
"slide-rotate-ver"
}
}
// Flicker effect - random flickering opacity
pub struct Flicker;
impl Effect for Flicker {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
// Fast flickering that stabilizes
let flicker_speed = 30.0;
let stability = progress; // Gets more stable over time
let flicker = ((progress * flicker_speed).sin() + 1.0) / 2.0;
let opacity = stability + (1.0 - stability) * flicker;
EffectResult::new(ascii_art.render()).with_opacity(opacity)
}
fn name(&self) -> &str {
"flicker"
}
}
// Tracking-in effect - letters expand from center
pub struct TrackingIn;
impl Effect for TrackingIn {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
// Simulate letter spacing by adding spaces between characters
let spacing = ((1.0 - progress) * 3.0) as usize;
if spacing == 0 {
EffectResult::new(ascii_art.render())
} else {
let lines: Vec<String> = ascii_art
.get_lines()
.iter()
.map(|line| {
line.chars()
.map(|c| {
if c == ' ' {
" ".repeat(spacing + 1)
} else {
format!("{}{}", c, " ".repeat(spacing))
}
})
.collect::<String>()
})
.collect();
EffectResult::new(lines.join("\n"))
}
}
fn name(&self) -> &str {
"tracking-in"
}
}
// Tracking-out effect - letters contract to center
pub struct TrackingOut;
impl Effect for TrackingOut {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
// Simulate letter spacing by adding spaces between characters
let spacing = (progress * 3.0) as usize;
if spacing == 0 {
EffectResult::new(ascii_art.render())
} else {
let lines: Vec<String> = ascii_art
.get_lines()
.iter()
.map(|line| {
line.chars()
.map(|c| {
if c == ' ' {
" ".repeat(spacing + 1)
} else {
format!("{}{}", c, " ".repeat(spacing))
}
})
.collect::<String>()
})
.collect();
EffectResult::new(lines.join("\n"))
}
}
fn name(&self) -> &str {
"tracking-out"
}
}
// Bounce-top effect - bounce down from top
pub struct BounceTop;
impl Effect for BounceTop {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
// Bounce from top with easing
let bounces = 2.0;
let bounce_height = ascii_art.height() as f64 + 10.0;
let base_offset = (1.0 - progress) * bounce_height;
let bounce_factor =
(progress * bounces * std::f64::consts::PI).sin().abs() * (1.0 - progress);
let offset_y = -(base_offset + bounce_factor * 5.0) as i32;
EffectResult::new(ascii_art.render()).with_offset(0, offset_y)
}
fn name(&self) -> &str {
"bounce-top"
}
}
// Bounce-bottom effect - bounce up from bottom
pub struct BounceBottom;
impl Effect for BounceBottom {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
// Bounce from bottom with easing
let bounces = 2.0;
let bounce_height = ascii_art.height() as f64 + 10.0;
let base_offset = (1.0 - progress) * bounce_height;
let bounce_factor =
(progress * bounces * std::f64::consts::PI).sin().abs() * (1.0 - progress);
let offset_y = (base_offset + bounce_factor * 5.0) as i32;
EffectResult::new(ascii_art.render()).with_offset(0, offset_y)
}
fn name(&self) -> &str {
"bounce-bottom"
}
}
// Tilt-in effect - tilt in with perspective simulation
pub struct TiltIn;
impl Effect for TiltIn {
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
// Simulate tilting in with combined scale and offset
let tilt_progress = 1.0 - progress;
let scale = 0.5 + (progress * 0.5);
let offset_x = (tilt_progress * 20.0 * (tilt_progress * std::f64::consts::PI).sin()) as i32;
let offset_y = -(tilt_progress * 15.0) as i32;
let scaled = ascii_art.scale(scale);
EffectResult::new(scaled.render())
.with_scale(scale)
.with_offset(offset_x, offset_y)
}
fn name(&self) -> &str {
"tilt-in"
}
}
/// Get effect by name
pub fn get_effect(name: &str) -> Result<Box<dyn Effect>> {
match name {
@@ -623,6 +836,16 @@ pub fn get_effect(name: &str) -> Result<Box<dyn Effect>> {
"sway" => Ok(Box::new(Sway)),
"roll-in" => Ok(Box::new(RollIn)),
"roll-out" => Ok(Box::new(RollOut)),
"puff-in" => Ok(Box::new(PuffIn)),
"puff-out" => Ok(Box::new(PuffOut)),
"slide-rotate-hor" => Ok(Box::new(SlideRotateHor)),
"slide-rotate-ver" => Ok(Box::new(SlideRotateVer)),
"flicker" => Ok(Box::new(Flicker)),
"tracking-in" => Ok(Box::new(TrackingIn)),
"tracking-out" => Ok(Box::new(TrackingOut)),
"bounce-top" => Ok(Box::new(BounceTop)),
"bounce-bottom" => Ok(Box::new(BounceBottom)),
"tilt-in" => Ok(Box::new(TiltIn)),
_ => bail!("Unknown effect: {}", name),
}
}
@@ -662,5 +885,15 @@ pub fn list_effects() -> Vec<&'static str> {
"sway",
"roll-in",
"roll-out",
"puff-in",
"puff-out",
"slide-rotate-hor",
"slide-rotate-ver",
"flicker",
"tracking-in",
"tracking-out",
"bounce-top",
"bounce-bottom",
"tilt-in",
]
}