Compare commits
16 Commits
dependabot
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7435af3708 | ||
| b2ebba865d | |||
| 59cd854f55 | |||
| 4cc8d5c489 | |||
| 51c2c5a14a | |||
| 12793893c3 | |||
| 946289544d | |||
| 4e88ea5677 | |||
| b2a9b6df36 | |||
| 5a707df725 | |||
| 097fca6ed3 | |||
| 5a0ff3f5cc | |||
| 4035f2dc23 | |||
| 000823457d | |||
| d51fada172 | |||
| 0a75003451 |
169
.github/workflows/release.yml
vendored
169
.github/workflows/release.yml
vendored
@@ -5,37 +5,174 @@ on:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get version from tag
|
||||
id: get_version
|
||||
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: false
|
||||
body: |
|
||||
# Piglet v${{ steps.get_version.outputs.version }}
|
||||
|
||||
🐷 Animated and colorful figlet wrapper written in Rust
|
||||
|
||||
## Features
|
||||
|
||||
- 20+ motion effects (fade, slide, scale, typewriter, wave, rainbow, etc.)
|
||||
- 18+ easing functions (linear, ease-in/out, quad, cubic, elastic, bounce, etc.)
|
||||
- Full color support with gradients and palettes
|
||||
- CSS gradient syntax support
|
||||
- Cross-platform (Linux, macOS)
|
||||
|
||||
## Installation
|
||||
|
||||
### Linux (x86_64)
|
||||
```bash
|
||||
curl -L https://github.com/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.version }}/piglet-x86_64-unknown-linux-gnu -o piglet
|
||||
chmod +x piglet
|
||||
sudo mv piglet /usr/local/bin/
|
||||
```
|
||||
|
||||
### Linux (musl)
|
||||
```bash
|
||||
curl -L https://github.com/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.version }}/piglet-x86_64-unknown-linux-musl -o piglet
|
||||
chmod +x piglet
|
||||
sudo mv piglet /usr/local/bin/
|
||||
```
|
||||
|
||||
### macOS (Intel)
|
||||
```bash
|
||||
curl -L https://github.com/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.version }}/piglet-x86_64-apple-darwin -o piglet
|
||||
chmod +x piglet
|
||||
sudo mv piglet /usr/local/bin/
|
||||
```
|
||||
|
||||
### macOS (Apple Silicon)
|
||||
```bash
|
||||
curl -L https://github.com/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.version }}/piglet-aarch64-apple-darwin -o piglet
|
||||
chmod +x piglet
|
||||
sudo mv piglet /usr/local/bin/
|
||||
```
|
||||
|
||||
### Via Cargo
|
||||
```bash
|
||||
cargo install --git https://github.com/${{ github.repository }} --tag v${{ steps.get_version.outputs.version }}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```bash
|
||||
# Simple gradient
|
||||
piglet "Hello" -g "linear-gradient(90deg, red, blue)"
|
||||
|
||||
# Typewriter effect
|
||||
piglet "World" -m typewriter -i ease-out
|
||||
|
||||
# Wave with rainbow colors
|
||||
piglet "Cool!" -p "hotpink,cyan,gold" -m wave
|
||||
```
|
||||
|
||||
See the [README](https://github.com/${{ github.repository }}/blob/v${{ steps.get_version.outputs.version }}/README.md) for full documentation.
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-release:
|
||||
name: Build Release (${{ matrix.target }})
|
||||
needs: create-release
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
body: |
|
||||
## Changes in this Release
|
||||
|
||||
See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details.
|
||||
|
||||
## Installation
|
||||
|
||||
### Linux (x86_64)
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install musl tools (Linux musl)
|
||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||
run: sudo apt-get update && sudo apt-get install -y musl-tools
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-build-release-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Build release binary
|
||||
run: cargo build --release --target ${{ matrix.target }} --verbose
|
||||
|
||||
- name: Strip binary (Linux)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: strip target/${{ matrix.target }}/release/piglet
|
||||
|
||||
- name: Strip binary (macOS)
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: strip target/${{ matrix.target }}/release/piglet
|
||||
|
||||
- name: Rename binary
|
||||
run: |
|
||||
cp target/${{ matrix.target }}/release/piglet piglet-${{ matrix.target }}
|
||||
|
||||
- name: Upload release binary
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: piglet-${{ matrix.target }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish-crate:
|
||||
name: Publish to crates.io
|
||||
needs: build-release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Publish to crates.io
|
||||
run: cargo publish --token ${{ secrets.CARGO_TOKEN }}
|
||||
continue-on-error: true
|
||||
|
||||
@@ -21,8 +21,8 @@ palette = "0.7"
|
||||
# Terminal manipulation
|
||||
crossterm = "0.27"
|
||||
|
||||
# Async runtime (for timing)
|
||||
tokio = { version = "1.35", features = ["time", "rt-multi-thread", "macros"] }
|
||||
# Async runtime (for timing and signal handling)
|
||||
tokio = { version = "1.35", features = ["time", "rt-multi-thread", "macros", "signal", "sync"] }
|
||||
|
||||
# Process execution
|
||||
which = "5.0"
|
||||
@@ -36,7 +36,7 @@ anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
# Utilities
|
||||
itertools = "0.12"
|
||||
itertools = "0.14"
|
||||
lazy_static = "1.4"
|
||||
rand = "0.8"
|
||||
|
||||
|
||||
@@ -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
|
||||
pub fn get_effect(name: &str) -> Result<Box<dyn Effect>> {
|
||||
match name {
|
||||
@@ -417,6 +1003,36 @@ pub fn get_effect(name: &str) -> Result<Box<dyn Effect>> {
|
||||
"gradient-flow" => Ok(Box::new(GradientFlow)),
|
||||
"rotate-in" => Ok(Box::new(RotateIn)),
|
||||
"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),
|
||||
}
|
||||
}
|
||||
@@ -446,5 +1062,35 @@ pub fn list_effects() -> Vec<&'static str> {
|
||||
"gradient-flow",
|
||||
"rotate-in",
|
||||
"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
|
||||
}
|
||||
|
||||
pub async fn run(&self, terminal: &mut TerminalManager) -> Result<()> {
|
||||
pub async fn run(&self, terminal: &mut TerminalManager) -> Result<bool> {
|
||||
let renderer = renderer::Renderer::new(
|
||||
&self.ascii_art,
|
||||
self.duration_ms,
|
||||
|
||||
@@ -2,6 +2,12 @@ use crate::animation::{easing::EasingFunction, effects::Effect, timeline::Timeli
|
||||
use crate::color::{apply, ColorEngine};
|
||||
use crate::utils::{ansi, ascii::AsciiArt, terminal::TerminalManager};
|
||||
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;
|
||||
|
||||
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());
|
||||
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 {
|
||||
// Check for exit FIRST
|
||||
if should_exit.load(Ordering::Relaxed) {
|
||||
return Ok(true); // User requested exit
|
||||
}
|
||||
|
||||
let frame_start = std::time::Instant::now();
|
||||
|
||||
// Calculate progress with easing
|
||||
let linear_progress = timeline.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
|
||||
let effect_result = self.effect.apply(self.ascii_art, eased_progress);
|
||||
|
||||
@@ -51,6 +92,11 @@ impl<'a> Renderer<'a> {
|
||||
effect_result.text.clone()
|
||||
};
|
||||
|
||||
// Check before terminal operations
|
||||
if should_exit.load(Ordering::Relaxed) {
|
||||
return Ok(true); // User requested exit
|
||||
}
|
||||
|
||||
// Render to terminal
|
||||
terminal.clear()?;
|
||||
terminal.refresh_size()?;
|
||||
@@ -62,7 +108,11 @@ impl<'a> Renderer<'a> {
|
||||
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| ansi::visual_width(l)).max().unwrap_or(0) as i32;
|
||||
let text_width = lines
|
||||
.iter()
|
||||
.map(|l| ansi::visual_width(l))
|
||||
.max()
|
||||
.unwrap_or(0) as i32;
|
||||
|
||||
let base_x = (width as i32 - text_width) / 2;
|
||||
let base_y = (height as i32 - text_height) / 2;
|
||||
@@ -78,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
|
||||
if timeline.is_complete() {
|
||||
break;
|
||||
return Ok(false); // Animation completed naturally
|
||||
}
|
||||
|
||||
// Advance to next frame and wait
|
||||
@@ -89,11 +144,21 @@ impl<'a> Renderer<'a> {
|
||||
let elapsed = frame_start.elapsed();
|
||||
|
||||
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 {
|
||||
|
||||
@@ -61,8 +61,14 @@ async fn run_piglet(args: PigletCli) -> Result<()> {
|
||||
|
||||
// Run animation
|
||||
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 {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ pub fn strip_ansi(text: &str) -> String {
|
||||
// Skip ANSI escape sequence
|
||||
if chars.peek() == Some(&'[') {
|
||||
chars.next(); // consume '['
|
||||
// Skip until we hit a letter (the command character)
|
||||
// Skip until we hit a letter (the command character)
|
||||
while let Some(&c) = chars.peek() {
|
||||
chars.next();
|
||||
if c.is_ascii_alphabetic() {
|
||||
|
||||
@@ -69,7 +69,11 @@ impl TerminalManager {
|
||||
|
||||
pub fn print_centered(&self, text: &str) -> Result<()> {
|
||||
let lines: Vec<&str> = text.lines().collect();
|
||||
let max_width = lines.iter().map(|l| ansi::visual_width(l)).max().unwrap_or(0) as u16;
|
||||
let max_width = lines
|
||||
.iter()
|
||||
.map(|l| ansi::visual_width(l))
|
||||
.max()
|
||||
.unwrap_or(0) as u16;
|
||||
let height = lines.len() as u16;
|
||||
|
||||
let start_x = (self.width.saturating_sub(max_width)) / 2;
|
||||
|
||||
Reference in New Issue
Block a user