Compare commits
13 Commits
dependabot
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b2ebba865d | |||
| 59cd854f55 | |||
| 4cc8d5c489 | |||
| 51c2c5a14a | |||
| 12793893c3 | |||
| 946289544d | |||
| 4e88ea5677 | |||
| b2a9b6df36 | |||
| 5a707df725 | |||
| 097fca6ed3 | |||
| 5a0ff3f5cc | |||
| 4035f2dc23 | |||
| 000823457d |
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -5,6 +5,9 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
@@ -148,11 +151,14 @@ jobs:
|
|||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
run: strip target/${{ matrix.target }}/release/piglet
|
run: strip target/${{ matrix.target }}/release/piglet
|
||||||
|
|
||||||
|
- name: Rename binary
|
||||||
|
run: |
|
||||||
|
cp target/${{ matrix.target }}/release/piglet piglet-${{ matrix.target }}
|
||||||
|
|
||||||
- name: Upload release binary
|
- name: Upload release binary
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
files: target/${{ matrix.target }}/release/piglet
|
files: piglet-${{ matrix.target }}
|
||||||
name: piglet-${{ matrix.target }}
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ palette = "0.7"
|
|||||||
# Terminal manipulation
|
# Terminal manipulation
|
||||||
crossterm = "0.27"
|
crossterm = "0.27"
|
||||||
|
|
||||||
# Async runtime (for timing)
|
# Async runtime (for timing and signal handling)
|
||||||
tokio = { version = "1.35", features = ["time", "rt-multi-thread", "macros"] }
|
tokio = { version = "1.35", features = ["time", "rt-multi-thread", "macros", "signal", "sync"] }
|
||||||
|
|
||||||
# Process execution
|
# Process execution
|
||||||
which = "5.0"
|
which = "5.0"
|
||||||
|
|||||||
@@ -393,6 +393,592 @@ impl Effect for GradientFlow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 1: High-Impact Effects from Animista
|
||||||
|
|
||||||
|
// Shake effect - horizontal vibration
|
||||||
|
pub struct Shake;
|
||||||
|
impl Effect for Shake {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Fast oscillation that decreases over time
|
||||||
|
let frequency = 20.0;
|
||||||
|
let amplitude = 10.0 * (1.0 - progress);
|
||||||
|
let offset_x = (progress * frequency * std::f64::consts::PI * 2.0).sin() * amplitude;
|
||||||
|
EffectResult::new(ascii_art.render()).with_offset(offset_x as i32, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"shake"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wobble effect - rotation wobble (simulated with offset variations)
|
||||||
|
pub struct Wobble;
|
||||||
|
impl Effect for Wobble {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Wobble with decreasing amplitude
|
||||||
|
let angle = progress * std::f64::consts::PI * 4.0;
|
||||||
|
let amplitude = 15.0 * (1.0 - progress);
|
||||||
|
let offset_x = (angle.sin() * amplitude) as i32;
|
||||||
|
let offset_y = (angle.cos() * amplitude * 0.3) as i32;
|
||||||
|
EffectResult::new(ascii_art.render()).with_offset(offset_x, offset_y)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"wobble"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vibrate effect - rapid small movements
|
||||||
|
pub struct Vibrate;
|
||||||
|
impl Effect for Vibrate {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Very fast, small vibrations
|
||||||
|
let frequency = 50.0;
|
||||||
|
let amplitude = 3.0;
|
||||||
|
let offset_x = (progress * frequency * std::f64::consts::PI).sin() * amplitude;
|
||||||
|
let offset_y = (progress * frequency * std::f64::consts::PI * 1.3).cos() * amplitude;
|
||||||
|
EffectResult::new(ascii_art.render()).with_offset(offset_x as i32, offset_y as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"vibrate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat effect - pulsing scale with heartbeat rhythm
|
||||||
|
pub struct Heartbeat;
|
||||||
|
impl Effect for Heartbeat {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Two-beat pulse pattern like a heartbeat
|
||||||
|
let beat_progress = (progress * 2.0) % 1.0;
|
||||||
|
let scale = if beat_progress < 0.3 {
|
||||||
|
1.0 + (beat_progress / 0.3) * 0.15
|
||||||
|
} else if beat_progress < 0.4 {
|
||||||
|
1.15 - ((beat_progress - 0.3) / 0.1) * 0.15
|
||||||
|
} else if beat_progress < 0.6 {
|
||||||
|
1.0 + ((beat_progress - 0.4) / 0.2) * 0.1
|
||||||
|
} else if beat_progress < 0.7 {
|
||||||
|
1.1 - ((beat_progress - 0.6) / 0.1) * 0.1
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let scaled = ascii_art.scale(scale);
|
||||||
|
EffectResult::new(scaled.render()).with_scale(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"heartbeat"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flip horizontal - flip text horizontally
|
||||||
|
pub struct FlipHorizontal;
|
||||||
|
impl Effect for FlipHorizontal {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Scale horizontally from 1 to -1 (flip)
|
||||||
|
let scale = 1.0 - (progress * 2.0);
|
||||||
|
if scale <= 0.0 {
|
||||||
|
// Show reversed text when flipped
|
||||||
|
let lines: Vec<String> = ascii_art
|
||||||
|
.get_lines()
|
||||||
|
.iter()
|
||||||
|
.map(|line| line.chars().rev().collect())
|
||||||
|
.collect();
|
||||||
|
EffectResult::new(lines.join("\n"))
|
||||||
|
} else {
|
||||||
|
let scaled = ascii_art.scale(scale);
|
||||||
|
EffectResult::new(scaled.render()).with_scale(scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"flip-horizontal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flip vertical - flip text vertically
|
||||||
|
pub struct FlipVertical;
|
||||||
|
impl Effect for FlipVertical {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Scale vertically with midpoint flip
|
||||||
|
let scale = 1.0 - (progress * 2.0).min(1.0);
|
||||||
|
if progress > 0.5 {
|
||||||
|
// Show reversed lines when flipped
|
||||||
|
let mut lines: Vec<String> = ascii_art
|
||||||
|
.get_lines()
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
lines.reverse();
|
||||||
|
let result_scale = (progress - 0.5) * 2.0;
|
||||||
|
let scaled = AsciiArt::new(lines.join("\n")).scale(result_scale);
|
||||||
|
EffectResult::new(scaled.render()).with_scale(result_scale)
|
||||||
|
} else {
|
||||||
|
let scaled = ascii_art.scale(scale.max(0.1));
|
||||||
|
EffectResult::new(scaled.render()).with_scale(scale.max(0.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"flip-vertical"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swing effect - pendulum motion
|
||||||
|
pub struct Swing;
|
||||||
|
impl Effect for Swing {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Pendulum swing with decreasing amplitude
|
||||||
|
let swings = 2.0;
|
||||||
|
let angle = (progress * swings * std::f64::consts::PI * 2.0).sin() * (1.0 - progress);
|
||||||
|
let offset_x = (angle * 20.0) as i32;
|
||||||
|
let offset_y = (angle.abs() * 5.0) as i32;
|
||||||
|
EffectResult::new(ascii_art.render()).with_offset(offset_x, -offset_y)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"swing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sway effect - gentle swaying motion
|
||||||
|
pub struct Sway;
|
||||||
|
impl Effect for Sway {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Smooth, gentle sway
|
||||||
|
let angle = (progress * std::f64::consts::PI * 2.0).sin();
|
||||||
|
let offset_x = (angle * 8.0) as i32;
|
||||||
|
let offset_y = (angle.abs() * 2.0) as i32;
|
||||||
|
EffectResult::new(ascii_art.render()).with_offset(offset_x, offset_y)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"sway"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roll-in effect - roll in from left with rotation
|
||||||
|
pub struct RollIn;
|
||||||
|
impl Effect for RollIn {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Slide in from left while appearing to roll
|
||||||
|
let offset_x = ((1.0 - progress) * -(ascii_art.width() as f64 + 20.0)) as i32;
|
||||||
|
let rotation_effect = ((1.0 - progress) * 5.0) as i32;
|
||||||
|
let offset_y = (rotation_effect as f64 * (progress * std::f64::consts::PI).sin()) as i32;
|
||||||
|
EffectResult::new(ascii_art.render()).with_offset(offset_x, offset_y)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"roll-in"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roll-out effect - roll out to right with rotation
|
||||||
|
pub struct RollOut;
|
||||||
|
impl Effect for RollOut {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Slide out to right while appearing to roll
|
||||||
|
let offset_x = (progress * (ascii_art.width() as f64 + 20.0)) as i32;
|
||||||
|
let rotation_effect = (progress * 5.0) as i32;
|
||||||
|
let offset_y = (rotation_effect as f64 * (progress * std::f64::consts::PI).sin()) as i32;
|
||||||
|
EffectResult::new(ascii_art.render()).with_offset(offset_x, offset_y)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"roll-out"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Additional Slide, Blink, Focus, and Shadow Effects
|
||||||
|
|
||||||
|
// Slide-out-top effect - slide out to top
|
||||||
|
pub struct SlideOutTop;
|
||||||
|
impl Effect for SlideOutTop {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
let offset_y = -(progress * (ascii_art.height() as f64 + 10.0)) as i32;
|
||||||
|
EffectResult::new(ascii_art.render()).with_offset(0, offset_y)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"slide-out-top"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slide-out-bottom effect - slide out to bottom
|
||||||
|
pub struct SlideOutBottom;
|
||||||
|
impl Effect for SlideOutBottom {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
let offset_y = (progress * (ascii_art.height() as f64 + 10.0)) as i32;
|
||||||
|
EffectResult::new(ascii_art.render()).with_offset(0, offset_y)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"slide-out-bottom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slide-out-left effect - slide out to left
|
||||||
|
pub struct SlideOutLeft;
|
||||||
|
impl Effect for SlideOutLeft {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
let offset_x = -(progress * (ascii_art.width() as f64 + 10.0)) as i32;
|
||||||
|
EffectResult::new(ascii_art.render()).with_offset(offset_x, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"slide-out-left"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slide-out-right effect - slide out to right
|
||||||
|
pub struct SlideOutRight;
|
||||||
|
impl Effect for SlideOutRight {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
let offset_x = (progress * (ascii_art.width() as f64 + 10.0)) as i32;
|
||||||
|
EffectResult::new(ascii_art.render()).with_offset(offset_x, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"slide-out-right"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blink effect - rapid on/off blinking
|
||||||
|
pub struct Blink;
|
||||||
|
impl Effect for Blink {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Blink 3 times during animation
|
||||||
|
let blinks = 6.0;
|
||||||
|
let blink_state = ((progress * blinks).floor() % 2.0) as i32;
|
||||||
|
let opacity = if blink_state == 0 { 1.0 } else { 0.0 };
|
||||||
|
EffectResult::new(ascii_art.render()).with_opacity(opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"blink"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus-in effect - simulate coming into focus with scale and opacity
|
||||||
|
pub struct FocusIn;
|
||||||
|
impl Effect for FocusIn {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Start blurry (small scale, low opacity) and come into focus
|
||||||
|
let scale = 0.7 + (progress * 0.3);
|
||||||
|
let opacity = progress.powf(0.5);
|
||||||
|
let scaled = ascii_art.scale(scale);
|
||||||
|
EffectResult::new(scaled.render())
|
||||||
|
.with_scale(scale)
|
||||||
|
.with_opacity(opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"focus-in"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blur-out effect - simulate going out of focus
|
||||||
|
pub struct BlurOut;
|
||||||
|
impl Effect for BlurOut {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Go out of focus (reduce scale, reduce opacity)
|
||||||
|
let scale = 1.0 - (progress * 0.3);
|
||||||
|
let opacity = (1.0 - progress).powf(0.5);
|
||||||
|
let scaled = ascii_art.scale(scale);
|
||||||
|
EffectResult::new(scaled.render())
|
||||||
|
.with_scale(scale)
|
||||||
|
.with_opacity(opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"blur-out"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shadow-drop effect - drop down with shadow simulation
|
||||||
|
pub struct ShadowDrop;
|
||||||
|
impl Effect for ShadowDrop {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Drop down from above with increasing shadow (opacity)
|
||||||
|
let drop_distance = 20.0;
|
||||||
|
let offset_y = -((1.0 - progress) * drop_distance) as i32;
|
||||||
|
let opacity = 0.3 + (progress * 0.7); // Start semi-transparent
|
||||||
|
EffectResult::new(ascii_art.render())
|
||||||
|
.with_offset(0, offset_y)
|
||||||
|
.with_opacity(opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"shadow-drop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shadow-pop effect - pop forward with shadow simulation
|
||||||
|
pub struct ShadowPop;
|
||||||
|
impl Effect for ShadowPop {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Scale up quickly then settle, simulating popping forward
|
||||||
|
let pop_scale = if progress < 0.5 {
|
||||||
|
1.0 + (progress * 2.0) * 0.3
|
||||||
|
} else {
|
||||||
|
1.3 - ((progress - 0.5) * 2.0) * 0.3
|
||||||
|
};
|
||||||
|
let scaled = ascii_art.scale(pop_scale);
|
||||||
|
EffectResult::new(scaled.render()).with_scale(pop_scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"shadow-pop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate-center effect - rotate around center point
|
||||||
|
pub struct RotateCenter;
|
||||||
|
impl Effect for RotateCenter {
|
||||||
|
fn apply(&self, ascii_art: &AsciiArt, progress: f64) -> EffectResult {
|
||||||
|
// Simulate rotation with alternating line offsets
|
||||||
|
let rotations = 1.0;
|
||||||
|
let angle = progress * rotations * std::f64::consts::PI * 2.0;
|
||||||
|
let max_offset = 5.0;
|
||||||
|
|
||||||
|
let lines: Vec<String> = ascii_art
|
||||||
|
.get_lines()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, line)| {
|
||||||
|
let line_factor = (i as f64 / ascii_art.get_lines().len().max(1) as f64) - 0.5;
|
||||||
|
let offset = (angle.sin() * line_factor * max_offset) as i32;
|
||||||
|
if offset > 0 {
|
||||||
|
format!("{}{}", " ".repeat(offset as usize), line)
|
||||||
|
} else if offset < 0 {
|
||||||
|
line.chars().skip(offset.unsigned_abs() as usize).collect()
|
||||||
|
} else {
|
||||||
|
line.to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
EffectResult::new(lines.join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"rotate-center"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get effect by name
|
/// Get effect by name
|
||||||
pub fn get_effect(name: &str) -> Result<Box<dyn Effect>> {
|
pub fn get_effect(name: &str) -> Result<Box<dyn Effect>> {
|
||||||
match name {
|
match name {
|
||||||
@@ -417,6 +1003,36 @@ pub fn get_effect(name: &str) -> Result<Box<dyn Effect>> {
|
|||||||
"gradient-flow" => Ok(Box::new(GradientFlow)),
|
"gradient-flow" => Ok(Box::new(GradientFlow)),
|
||||||
"rotate-in" => Ok(Box::new(RotateIn)),
|
"rotate-in" => Ok(Box::new(RotateIn)),
|
||||||
"rotate-out" => Ok(Box::new(RotateOut)),
|
"rotate-out" => Ok(Box::new(RotateOut)),
|
||||||
|
"shake" => Ok(Box::new(Shake)),
|
||||||
|
"wobble" => Ok(Box::new(Wobble)),
|
||||||
|
"vibrate" => Ok(Box::new(Vibrate)),
|
||||||
|
"heartbeat" => Ok(Box::new(Heartbeat)),
|
||||||
|
"flip-horizontal" => Ok(Box::new(FlipHorizontal)),
|
||||||
|
"flip-vertical" => Ok(Box::new(FlipVertical)),
|
||||||
|
"swing" => Ok(Box::new(Swing)),
|
||||||
|
"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)),
|
||||||
|
"slide-out-top" => Ok(Box::new(SlideOutTop)),
|
||||||
|
"slide-out-bottom" => Ok(Box::new(SlideOutBottom)),
|
||||||
|
"slide-out-left" => Ok(Box::new(SlideOutLeft)),
|
||||||
|
"slide-out-right" => Ok(Box::new(SlideOutRight)),
|
||||||
|
"blink" => Ok(Box::new(Blink)),
|
||||||
|
"focus-in" => Ok(Box::new(FocusIn)),
|
||||||
|
"blur-out" => Ok(Box::new(BlurOut)),
|
||||||
|
"shadow-drop" => Ok(Box::new(ShadowDrop)),
|
||||||
|
"shadow-pop" => Ok(Box::new(ShadowPop)),
|
||||||
|
"rotate-center" => Ok(Box::new(RotateCenter)),
|
||||||
_ => bail!("Unknown effect: {}", name),
|
_ => bail!("Unknown effect: {}", name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -446,5 +1062,35 @@ pub fn list_effects() -> Vec<&'static str> {
|
|||||||
"gradient-flow",
|
"gradient-flow",
|
||||||
"rotate-in",
|
"rotate-in",
|
||||||
"rotate-out",
|
"rotate-out",
|
||||||
|
"shake",
|
||||||
|
"wobble",
|
||||||
|
"vibrate",
|
||||||
|
"heartbeat",
|
||||||
|
"flip-horizontal",
|
||||||
|
"flip-vertical",
|
||||||
|
"swing",
|
||||||
|
"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",
|
||||||
|
"slide-out-top",
|
||||||
|
"slide-out-bottom",
|
||||||
|
"slide-out-left",
|
||||||
|
"slide-out-right",
|
||||||
|
"blink",
|
||||||
|
"focus-in",
|
||||||
|
"blur-out",
|
||||||
|
"shadow-drop",
|
||||||
|
"shadow-pop",
|
||||||
|
"rotate-center",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ impl AnimationEngine {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(&self, terminal: &mut TerminalManager) -> Result<()> {
|
pub async fn run(&self, terminal: &mut TerminalManager) -> Result<bool> {
|
||||||
let renderer = renderer::Renderer::new(
|
let renderer = renderer::Renderer::new(
|
||||||
&self.ascii_art,
|
&self.ascii_art,
|
||||||
self.duration_ms,
|
self.duration_ms,
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ use crate::animation::{easing::EasingFunction, effects::Effect, timeline::Timeli
|
|||||||
use crate::color::{apply, ColorEngine};
|
use crate::color::{apply, ColorEngine};
|
||||||
use crate::utils::{ansi, ascii::AsciiArt, terminal::TerminalManager};
|
use crate::utils::{ansi, ascii::AsciiArt, terminal::TerminalManager};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
|
||||||
|
use std::sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
};
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
pub struct Renderer<'a> {
|
pub struct Renderer<'a> {
|
||||||
@@ -30,17 +36,52 @@ impl<'a> Renderer<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render(&self, terminal: &mut TerminalManager) -> Result<()> {
|
pub async fn render(&self, terminal: &mut TerminalManager) -> Result<bool> {
|
||||||
let mut timeline = Timeline::new(self.timeline.duration_ms(), self.timeline.fps());
|
let mut timeline = Timeline::new(self.timeline.duration_ms(), self.timeline.fps());
|
||||||
timeline.start();
|
timeline.start();
|
||||||
|
|
||||||
|
// Spawn background thread to listen for exit keys
|
||||||
|
let should_exit = Arc::new(AtomicBool::new(false));
|
||||||
|
let should_exit_clone = should_exit.clone();
|
||||||
|
|
||||||
|
std::thread::spawn(move || loop {
|
||||||
|
if let Ok(true) = event::poll(Duration::from_millis(100)) {
|
||||||
|
if let Ok(Event::Key(key)) = event::read() {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => {
|
||||||
|
should_exit_clone.store(true, Ordering::Relaxed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
|
should_exit_clone.store(true, Ordering::Relaxed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if should_exit_clone.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
// Check for exit FIRST
|
||||||
|
if should_exit.load(Ordering::Relaxed) {
|
||||||
|
return Ok(true); // User requested exit
|
||||||
|
}
|
||||||
|
|
||||||
let frame_start = std::time::Instant::now();
|
let frame_start = std::time::Instant::now();
|
||||||
|
|
||||||
// Calculate progress with easing
|
// Calculate progress with easing
|
||||||
let linear_progress = timeline.progress();
|
let linear_progress = timeline.progress();
|
||||||
let eased_progress = self.easing.ease(linear_progress);
|
let eased_progress = self.easing.ease(linear_progress);
|
||||||
|
|
||||||
|
// Check again before rendering
|
||||||
|
if should_exit.load(Ordering::Relaxed) {
|
||||||
|
return Ok(true); // User requested exit
|
||||||
|
}
|
||||||
|
|
||||||
// Apply effect
|
// Apply effect
|
||||||
let effect_result = self.effect.apply(self.ascii_art, eased_progress);
|
let effect_result = self.effect.apply(self.ascii_art, eased_progress);
|
||||||
|
|
||||||
@@ -51,6 +92,11 @@ impl<'a> Renderer<'a> {
|
|||||||
effect_result.text.clone()
|
effect_result.text.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check before terminal operations
|
||||||
|
if should_exit.load(Ordering::Relaxed) {
|
||||||
|
return Ok(true); // User requested exit
|
||||||
|
}
|
||||||
|
|
||||||
// Render to terminal
|
// Render to terminal
|
||||||
terminal.clear()?;
|
terminal.clear()?;
|
||||||
terminal.refresh_size()?;
|
terminal.refresh_size()?;
|
||||||
@@ -82,9 +128,14 @@ impl<'a> Renderer<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user wants to exit
|
||||||
|
if should_exit.load(Ordering::Relaxed) {
|
||||||
|
return Ok(true); // User requested exit
|
||||||
|
}
|
||||||
|
|
||||||
// Check if animation is complete before advancing
|
// Check if animation is complete before advancing
|
||||||
if timeline.is_complete() {
|
if timeline.is_complete() {
|
||||||
break;
|
return Ok(false); // Animation completed naturally
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance to next frame and wait
|
// Advance to next frame and wait
|
||||||
@@ -93,11 +144,21 @@ impl<'a> Renderer<'a> {
|
|||||||
let elapsed = frame_start.elapsed();
|
let elapsed = frame_start.elapsed();
|
||||||
|
|
||||||
if elapsed < frame_duration {
|
if elapsed < frame_duration {
|
||||||
sleep(frame_duration - elapsed).await;
|
let sleep_duration = frame_duration - elapsed;
|
||||||
|
// Break sleep into small chunks to check should_exit frequently
|
||||||
|
let chunk_duration = Duration::from_millis(5);
|
||||||
|
let mut remaining = sleep_duration;
|
||||||
|
|
||||||
|
while remaining > Duration::ZERO {
|
||||||
|
if should_exit.load(Ordering::Relaxed) {
|
||||||
|
return Ok(true); // User requested exit during sleep
|
||||||
|
}
|
||||||
|
let sleep_time = remaining.min(chunk_duration);
|
||||||
|
sleep(sleep_time).await;
|
||||||
|
remaining = remaining.saturating_sub(sleep_time);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_colors(&self, text: &str, progress: f64) -> String {
|
fn apply_colors(&self, text: &str, progress: f64) -> String {
|
||||||
|
|||||||
@@ -61,8 +61,14 @@ async fn run_piglet(args: PigletCli) -> Result<()> {
|
|||||||
|
|
||||||
// Run animation
|
// Run animation
|
||||||
loop {
|
loop {
|
||||||
animation_engine.run(&mut terminal).await?;
|
let user_exited = animation_engine.run(&mut terminal).await?;
|
||||||
|
|
||||||
|
// If user pressed exit key, stop looping
|
||||||
|
if user_exited {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not looping, stop after one animation
|
||||||
if !args.loop_animation {
|
if !args.loop_animation {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user