Compare commits
24 Commits
dependabot
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b2ebba865d | |||
| 59cd854f55 | |||
| 4cc8d5c489 | |||
| 51c2c5a14a | |||
| 12793893c3 | |||
| 946289544d | |||
| 4e88ea5677 | |||
| b2a9b6df36 | |||
| 5a707df725 | |||
| 097fca6ed3 | |||
| 5a0ff3f5cc | |||
| 4035f2dc23 | |||
| 000823457d | |||
| d51fada172 | |||
| 0a75003451 | |||
| 70093c82d7 | |||
| 24ca6f0262 | |||
| 09665d3250 | |||
| 6ce7ce03c6 | |||
| 9e2b97490d | |||
| 24f2dae24f | |||
| 6d85c31779 | |||
| a520e7d69b | |||
| b1ad87fc26 |
2
.github/workflows/benchmark.yml
vendored
2
.github/workflows/benchmark.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
run: cargo bench --no-fail-fast
|
run: cargo bench --no-fail-fast
|
||||||
|
|
||||||
- name: Upload benchmark results
|
- name: Upload benchmark results
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: benchmark-results
|
name: benchmark-results
|
||||||
path: target/criterion/
|
path: target/criterion/
|
||||||
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
rust: [stable, beta]
|
rust: [stable, beta]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -76,12 +76,6 @@ jobs:
|
|||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
run: brew install figlet
|
run: brew install figlet
|
||||||
|
|
||||||
- name: Install figlet (Windows)
|
|
||||||
if: matrix.os == 'windows-latest'
|
|
||||||
run: |
|
|
||||||
choco install figlet -y
|
|
||||||
echo "C:\ProgramData\chocolatey\lib\figlet\tools" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
|
||||||
|
|
||||||
- name: Cache cargo registry
|
- name: Cache cargo registry
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
@@ -120,8 +114,6 @@ jobs:
|
|||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
- os: windows-latest
|
|
||||||
target: x86_64-pc-windows-msvc
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -157,11 +149,10 @@ jobs:
|
|||||||
run: cargo build --release --target ${{ matrix.target }} --verbose
|
run: cargo build --release --target ${{ matrix.target }} --verbose
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: piglet-${{ matrix.target }}
|
name: piglet-${{ matrix.target }}
|
||||||
path: |
|
path: target/${{ matrix.target }}/release/piglet
|
||||||
target/${{ matrix.target }}/release/piglet${{ matrix.os == 'windows-latest' && '.exe' || '' }}
|
|
||||||
|
|
||||||
security-audit:
|
security-audit:
|
||||||
name: Security Audit
|
name: Security Audit
|
||||||
|
|||||||
2
.github/workflows/coverage.yml
vendored
2
.github/workflows/coverage.yml
vendored
@@ -32,5 +32,5 @@ jobs:
|
|||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
files: lcov.info
|
files: lcov.info
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: false
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
169
.github/workflows/release.yml
vendored
169
.github/workflows/release.yml
vendored
@@ -5,37 +5,174 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-release:
|
create-release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
version: ${{ steps.get_version.outputs.version }}
|
version: ${{ steps.get_version.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Get version from tag
|
- name: Get version from tag
|
||||||
id: get_version
|
id: get_version
|
||||||
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
uses: softprops/action-gh-release@v1
|
||||||
uses: actions/create-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:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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:
|
with:
|
||||||
tag_name: ${{ github.ref }}
|
targets: ${{ matrix.target }}
|
||||||
release_name: Release ${{ github.ref }}
|
|
||||||
body: |
|
- name: Install musl tools (Linux musl)
|
||||||
## Changes in this Release
|
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y musl-tools
|
||||||
See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details.
|
|
||||||
|
- name: Cache cargo registry
|
||||||
## Installation
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
### Linux (x86_64)
|
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
|
||||||
|
|||||||
2
.github/workflows/security.yml
vendored
2
.github/workflows/security.yml
vendored
@@ -31,4 +31,4 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Dependency Review
|
- name: Dependency Review
|
||||||
uses: actions/dependency-review-action@v3
|
uses: actions/dependency-review-action@v4
|
||||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/target/
|
||||||
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
@@ -16,13 +16,13 @@ csscolorparser = "0.6"
|
|||||||
palette = "0.7"
|
palette = "0.7"
|
||||||
|
|
||||||
# Animation & Interpolation
|
# Animation & Interpolation
|
||||||
scirs2-interpolate = "0.1"
|
# scirs2-interpolate = "0.1.0-rc.2" # Not needed, using custom easing functions
|
||||||
|
|
||||||
# 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"] }
|
tokio = { version = "1.35", features = ["time", "rt-multi-thread", "macros", "signal", "sync"] }
|
||||||
|
|
||||||
# Process execution
|
# Process execution
|
||||||
which = "5.0"
|
which = "5.0"
|
||||||
|
|||||||
@@ -1,49 +1,318 @@
|
|||||||
use anyhow::{Result, bail};
|
use anyhow::{bail, Result};
|
||||||
use scirs2_interpolate::*;
|
|
||||||
|
|
||||||
pub trait EasingFunction: Send + Sync {
|
pub trait EasingFunction: Send + Sync {
|
||||||
fn ease(&self, t: f64) -> f64;
|
fn ease(&self, t: f64) -> f64;
|
||||||
|
#[allow(dead_code)]
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linear
|
// Linear
|
||||||
pub struct Linear;
|
pub struct Linear;
|
||||||
impl EasingFunction for Linear {
|
impl EasingFunction for Linear {
|
||||||
fn ease(&self, t: f64) -> f64 { t }
|
fn ease(&self, t: f64) -> f64 {
|
||||||
fn name(&self) -> &str { "linear" }
|
t
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"linear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic easing
|
||||||
|
pub struct EaseIn;
|
||||||
|
impl EasingFunction for EaseIn {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
t * t
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-in"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseOut;
|
||||||
|
impl EasingFunction for EaseOut {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
t * (2.0 - t)
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-out"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseInOut;
|
||||||
|
impl EasingFunction for EaseInOut {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
if t < 0.5 {
|
||||||
|
2.0 * t * t
|
||||||
|
} else {
|
||||||
|
-1.0 + (4.0 - 2.0 * t) * t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-in-out"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quadratic
|
// Quadratic
|
||||||
pub struct EaseInQuad;
|
pub struct EaseInQuad;
|
||||||
impl EasingFunction for EaseInQuad {
|
impl EasingFunction for EaseInQuad {
|
||||||
fn ease(&self, t: f64) -> f64 { quad_ease_in(t, 0.0, 1.0, 1.0) }
|
fn ease(&self, t: f64) -> f64 {
|
||||||
fn name(&self) -> &str { "ease-in-quad" }
|
t * t
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-in-quad"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EaseOutQuad;
|
pub struct EaseOutQuad;
|
||||||
impl EasingFunction for EaseOutQuad {
|
impl EasingFunction for EaseOutQuad {
|
||||||
fn ease(&self, t: f64) -> f64 { quad_ease_out(t, 0.0, 1.0, 1.0) }
|
fn ease(&self, t: f64) -> f64 {
|
||||||
fn name(&self) -> &str { "ease-out-quad" }
|
t * (2.0 - t)
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-out-quad"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EaseInOutQuad;
|
pub struct EaseInOutQuad;
|
||||||
impl EasingFunction for EaseInOutQuad {
|
impl EasingFunction for EaseInOutQuad {
|
||||||
fn ease(&self, t: f64) -> f64 { quad_ease_in_out(t, 0.0, 1.0, 1.0) }
|
fn ease(&self, t: f64) -> f64 {
|
||||||
fn name(&self) -> &str { "ease-in-out-quad" }
|
if t < 0.5 {
|
||||||
|
2.0 * t * t
|
||||||
|
} else {
|
||||||
|
-1.0 + (4.0 - 2.0 * t) * t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-in-out-quad"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cubic
|
// Cubic
|
||||||
pub struct EaseInCubic;
|
pub struct EaseInCubic;
|
||||||
impl EasingFunction for EaseInCubic {
|
impl EasingFunction for EaseInCubic {
|
||||||
fn ease(&self, t: f64) -> f64 { cubic_ease_in(t, 0.0, 1.0, 1.0) }
|
fn ease(&self, t: f64) -> f64 {
|
||||||
fn name(&self) -> &str { "ease-in-cubic" }
|
t * t * t
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-in-cubic"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EaseOutCubic;
|
pub struct EaseOutCubic;
|
||||||
impl EasingFunction for EaseOutCubic {
|
impl EasingFunction for EaseOutCubic {
|
||||||
fn ease(&self, t: f64) -> f64 { cubic_ease_out(t, 0.0, 1.0, 1.0) }
|
fn ease(&self, t: f64) -> f64 {
|
||||||
fn name(&self) -> &str { "ease-out-cubic" }
|
let t1 = t - 1.0;
|
||||||
|
t1 * t1 * t1 + 1.0
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-out-cubic"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EaseInOutCubic;
|
pub struct EaseInOutCubic;
|
||||||
impl EasingFunction for EaseInOut
|
impl EasingFunction for EaseInOutCubic {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
if t < 0.5 {
|
||||||
|
4.0 * t * t * t
|
||||||
|
} else {
|
||||||
|
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-in-out-cubic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back
|
||||||
|
pub struct EaseInBack;
|
||||||
|
impl EasingFunction for EaseInBack {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
let c1 = 1.70158;
|
||||||
|
let c3 = c1 + 1.0;
|
||||||
|
c3 * t * t * t - c1 * t * t
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-in-back"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseOutBack;
|
||||||
|
impl EasingFunction for EaseOutBack {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
let c1 = 1.70158;
|
||||||
|
let c3 = c1 + 1.0;
|
||||||
|
1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-out-back"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseInOutBack;
|
||||||
|
impl EasingFunction for EaseInOutBack {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
let c1 = 1.70158;
|
||||||
|
let c2 = c1 * 1.525;
|
||||||
|
if t < 0.5 {
|
||||||
|
((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0 * t - c2)) / 2.0
|
||||||
|
} else {
|
||||||
|
((2.0 * t - 2.0).powi(2) * ((c2 + 1.0) * (t * 2.0 - 2.0) + c2) + 2.0) / 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-in-out-back"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elastic
|
||||||
|
pub struct EaseInElastic;
|
||||||
|
impl EasingFunction for EaseInElastic {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
if t == 0.0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
if t == 1.0 {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
let c4 = (2.0 * std::f64::consts::PI) / 3.0;
|
||||||
|
-(2.0_f64.powf(10.0 * t - 10.0)) * ((t * 10.0 - 10.75) * c4).sin()
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-in-elastic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseOutElastic;
|
||||||
|
impl EasingFunction for EaseOutElastic {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
if t == 0.0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
if t == 1.0 {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
let c4 = (2.0 * std::f64::consts::PI) / 3.0;
|
||||||
|
2.0_f64.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-out-elastic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseInOutElastic;
|
||||||
|
impl EasingFunction for EaseInOutElastic {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
if t == 0.0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
if t == 1.0 {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
let c5 = (2.0 * std::f64::consts::PI) / 4.5;
|
||||||
|
if t < 0.5 {
|
||||||
|
-(2.0_f64.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0
|
||||||
|
} else {
|
||||||
|
(2.0_f64.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0 + 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-in-out-elastic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounce
|
||||||
|
fn bounce_out(t: f64) -> f64 {
|
||||||
|
let n1 = 7.5625;
|
||||||
|
let d1 = 2.75;
|
||||||
|
|
||||||
|
if t < 1.0 / d1 {
|
||||||
|
n1 * t * t
|
||||||
|
} else if t < 2.0 / d1 {
|
||||||
|
let t = t - 1.5 / d1;
|
||||||
|
n1 * t * t + 0.75
|
||||||
|
} else if t < 2.5 / d1 {
|
||||||
|
let t = t - 2.25 / d1;
|
||||||
|
n1 * t * t + 0.9375
|
||||||
|
} else {
|
||||||
|
let t = t - 2.625 / d1;
|
||||||
|
n1 * t * t + 0.984375
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseInBounce;
|
||||||
|
impl EasingFunction for EaseInBounce {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
1.0 - bounce_out(1.0 - t)
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-in-bounce"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseOutBounce;
|
||||||
|
impl EasingFunction for EaseOutBounce {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
bounce_out(t)
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-out-bounce"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EaseInOutBounce;
|
||||||
|
impl EasingFunction for EaseInOutBounce {
|
||||||
|
fn ease(&self, t: f64) -> f64 {
|
||||||
|
if t < 0.5 {
|
||||||
|
(1.0 - bounce_out(1.0 - 2.0 * t)) / 2.0
|
||||||
|
} else {
|
||||||
|
(1.0 + bounce_out(2.0 * t - 1.0)) / 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"ease-in-out-bounce"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_easing_function(name: &str) -> Result<Box<dyn EasingFunction>> {
|
||||||
|
match name {
|
||||||
|
"linear" => Ok(Box::new(Linear)),
|
||||||
|
"ease-in" => Ok(Box::new(EaseIn)),
|
||||||
|
"ease-out" => Ok(Box::new(EaseOut)),
|
||||||
|
"ease-in-out" => Ok(Box::new(EaseInOut)),
|
||||||
|
"ease-in-quad" => Ok(Box::new(EaseInQuad)),
|
||||||
|
"ease-out-quad" => Ok(Box::new(EaseOutQuad)),
|
||||||
|
"ease-in-out-quad" => Ok(Box::new(EaseInOutQuad)),
|
||||||
|
"ease-in-cubic" => Ok(Box::new(EaseInCubic)),
|
||||||
|
"ease-out-cubic" => Ok(Box::new(EaseOutCubic)),
|
||||||
|
"ease-in-out-cubic" => Ok(Box::new(EaseInOutCubic)),
|
||||||
|
"ease-in-back" => Ok(Box::new(EaseInBack)),
|
||||||
|
"ease-out-back" => Ok(Box::new(EaseOutBack)),
|
||||||
|
"ease-in-out-back" => Ok(Box::new(EaseInOutBack)),
|
||||||
|
"ease-in-elastic" => Ok(Box::new(EaseInElastic)),
|
||||||
|
"ease-out-elastic" => Ok(Box::new(EaseOutElastic)),
|
||||||
|
"ease-in-out-elastic" => Ok(Box::new(EaseInOutElastic)),
|
||||||
|
"ease-in-bounce" => Ok(Box::new(EaseInBounce)),
|
||||||
|
"ease-out-bounce" => Ok(Box::new(EaseOutBounce)),
|
||||||
|
"ease-in-out-bounce" => Ok(Box::new(EaseInOutBounce)),
|
||||||
|
_ => bail!("Unknown easing function: {}", name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
1096
src/animation/effects.rs
Normal file
1096
src/animation/effects.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
pub mod effects;
|
|
||||||
pub mod easing;
|
pub mod easing;
|
||||||
pub mod timeline;
|
pub mod effects;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
|
pub mod timeline;
|
||||||
|
|
||||||
use crate::color::ColorEngine;
|
use crate::color::ColorEngine;
|
||||||
use crate::utils::{ascii::AsciiArt, terminal::TerminalManager};
|
use crate::utils::{ascii::AsciiArt, terminal::TerminalManager};
|
||||||
@@ -27,23 +27,23 @@ impl AnimationEngine {
|
|||||||
color_engine: ColorEngine::new(),
|
color_engine: ColorEngine::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_effect(mut self, effect_name: &str) -> Result<Self> {
|
pub fn with_effect(mut self, effect_name: &str) -> Result<Self> {
|
||||||
self.effect = effects::get_effect(effect_name)?;
|
self.effect = effects::get_effect(effect_name)?;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_easing(mut self, easing_name: &str) -> Result<Self> {
|
pub fn with_easing(mut self, easing_name: &str) -> Result<Self> {
|
||||||
self.easing = easing::get_easing_function(easing_name)?;
|
self.easing = easing::get_easing_function(easing_name)?;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_color_engine(mut self, color_engine: ColorEngine) -> Self {
|
pub fn with_color_engine(mut self, color_engine: ColorEngine) -> Self {
|
||||||
self.color_engine = color_engine;
|
self.color_engine = color_engine;
|
||||||
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,
|
||||||
@@ -52,7 +52,7 @@ impl AnimationEngine {
|
|||||||
&*self.easing,
|
&*self.easing,
|
||||||
&self.color_engine,
|
&self.color_engine,
|
||||||
);
|
);
|
||||||
|
|
||||||
renderer.render(terminal).await
|
renderer.render(terminal).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
218
src/animation/renderer.rs
Normal file
218
src/animation/renderer.rs
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
use crate::animation::{easing::EasingFunction, effects::Effect, timeline::Timeline};
|
||||||
|
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> {
|
||||||
|
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<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);
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check before terminal operations
|
||||||
|
if should_exit.load(Ordering::Relaxed) {
|
||||||
|
return Ok(true); // User requested exit
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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| 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;
|
||||||
|
|
||||||
|
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)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
return Ok(false); // Animation completed naturally
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance to next frame and wait
|
||||||
|
timeline.next_frame();
|
||||||
|
let frame_duration = timeline.frame_duration();
|
||||||
|
let elapsed = frame_start.elapsed();
|
||||||
|
|
||||||
|
if elapsed < frame_duration {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/animation/timeline.rs
Normal file
123
src/animation/timeline.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
pub struct Timeline {
|
||||||
|
duration_ms: u64,
|
||||||
|
fps: u32,
|
||||||
|
start_time: Option<Instant>,
|
||||||
|
current_frame: usize,
|
||||||
|
total_frames: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timeline {
|
||||||
|
pub fn new(duration_ms: u64, fps: u32) -> Self {
|
||||||
|
let total_frames = ((duration_ms as f64 / 1000.0) * fps as f64).ceil() as usize;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
duration_ms,
|
||||||
|
fps,
|
||||||
|
start_time: None,
|
||||||
|
current_frame: 0,
|
||||||
|
total_frames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self) {
|
||||||
|
self.start_time = Some(Instant::now());
|
||||||
|
self.current_frame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.start_time = None;
|
||||||
|
self.current_frame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_complete(&self) -> bool {
|
||||||
|
self.current_frame >= self.total_frames
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn progress(&self) -> f64 {
|
||||||
|
if self.total_frames == 0 {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
(self.current_frame as f64 / self.total_frames as f64).min(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_frame(&mut self) -> bool {
|
||||||
|
if self.is_complete() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.current_frame += 1;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn frame_duration(&self) -> Duration {
|
||||||
|
Duration::from_millis(1000 / self.fps as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn elapsed(&self) -> Duration {
|
||||||
|
self.start_time
|
||||||
|
.map(|start| start.elapsed())
|
||||||
|
.unwrap_or(Duration::ZERO)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn current_frame(&self) -> usize {
|
||||||
|
self.current_frame
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn total_frames(&self) -> usize {
|
||||||
|
self.total_frames
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fps(&self) -> u32 {
|
||||||
|
self.fps
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn duration_ms(&self) -> u64 {
|
||||||
|
self.duration_ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timeline_creation() {
|
||||||
|
let timeline = Timeline::new(1000, 30);
|
||||||
|
assert_eq!(timeline.total_frames(), 30);
|
||||||
|
assert_eq!(timeline.fps(), 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timeline_progress() {
|
||||||
|
let mut timeline = Timeline::new(1000, 10);
|
||||||
|
timeline.start();
|
||||||
|
|
||||||
|
assert_eq!(timeline.progress(), 0.0);
|
||||||
|
|
||||||
|
for _ in 0..5 {
|
||||||
|
timeline.next_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(timeline.progress(), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timeline_completion() {
|
||||||
|
let mut timeline = Timeline::new(1000, 10);
|
||||||
|
timeline.start();
|
||||||
|
|
||||||
|
assert!(!timeline.is_complete());
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
timeline.next_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(timeline.is_complete());
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/cli.rs
28
src/cli.rs
@@ -8,30 +8,30 @@ pub struct PigletCli {
|
|||||||
/// Text to render with figlet
|
/// Text to render with figlet
|
||||||
#[arg(value_name = "TEXT")]
|
#[arg(value_name = "TEXT")]
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
|
||||||
/// Duration of animation (e.g., 3000ms, 0.3s, 0.5h, 5m)
|
/// Duration of animation (e.g., 3000ms, 0.3s, 0.5h, 5m)
|
||||||
#[arg(short, long, default_value = "3s")]
|
#[arg(short, long, default_value = "3s")]
|
||||||
pub duration: String,
|
pub duration: String,
|
||||||
|
|
||||||
/// Color palette (hex or CSS4 colors, comma-separated)
|
/// Color palette (hex or CSS4 colors, comma-separated)
|
||||||
/// Example: "#FF5733,#33FF57,#3357FF" or "red,green,blue"
|
/// Example: "#FF5733,#33FF57,#3357FF" or "red,green,blue"
|
||||||
#[arg(short = 'p', long, value_delimiter = ',')]
|
#[arg(short = 'p', long, value_delimiter = ',')]
|
||||||
pub color_palette: Option<Vec<String>>,
|
pub color_palette: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Color gradient (CSS4 gradient definition)
|
/// Color gradient (CSS4 gradient definition)
|
||||||
/// Example: "linear-gradient(90deg, red, blue)"
|
/// Example: "linear-gradient(90deg, red, blue)"
|
||||||
#[arg(short = 'g', long)]
|
#[arg(short = 'g', long)]
|
||||||
pub color_gradient: Option<String>,
|
pub color_gradient: Option<String>,
|
||||||
|
|
||||||
/// Motion easing function
|
/// Motion easing function
|
||||||
/// Options: linear, ease-in, ease-out, ease-in-out, ease-in-quad,
|
/// Options: linear, ease-in, ease-out, ease-in-out, ease-in-quad,
|
||||||
/// ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-out-cubic,
|
/// ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-out-cubic,
|
||||||
/// ease-in-out-cubic, ease-in-back, ease-out-back, ease-in-out-back,
|
/// ease-in-out-cubic, ease-in-back, ease-out-back, ease-in-out-back,
|
||||||
/// ease-in-elastic, ease-out-elastic, ease-in-out-elastic,
|
/// ease-in-elastic, ease-out-elastic, ease-in-out-elastic,
|
||||||
/// ease-in-bounce, ease-out-bounce, ease-in-out-bounce
|
/// ease-in-bounce, ease-out-bounce, ease-in-out-bounce
|
||||||
#[arg(short = 'i', long, default_value = "ease-in-out")]
|
#[arg(short = 'i', long, default_value = "ease-in-out")]
|
||||||
pub motion_ease: String,
|
pub motion_ease: String,
|
||||||
|
|
||||||
/// Motion effect name
|
/// Motion effect name
|
||||||
/// Options: fade-in, fade-out, fade-in-out, slide-in-top, slide-in-bottom,
|
/// Options: fade-in, fade-out, fade-in-out, slide-in-top, slide-in-bottom,
|
||||||
/// slide-in-left, slide-in-right, scale-up, scale-down, pulse,
|
/// slide-in-left, slide-in-right, scale-up, scale-down, pulse,
|
||||||
@@ -39,33 +39,33 @@ pub struct PigletCli {
|
|||||||
/// jello, color-cycle, rainbow, gradient-flow, rotate-in, rotate-out
|
/// jello, color-cycle, rainbow, gradient-flow, rotate-in, rotate-out
|
||||||
#[arg(short, long, default_value = "fade-in")]
|
#[arg(short, long, default_value = "fade-in")]
|
||||||
pub motion_effect: String,
|
pub motion_effect: String,
|
||||||
|
|
||||||
/// Figlet font
|
/// Figlet font
|
||||||
#[arg(short = 'f', long)]
|
#[arg(short = 'f', long)]
|
||||||
pub font: Option<String>,
|
pub font: Option<String>,
|
||||||
|
|
||||||
/// Additional figlet options (use after --)
|
/// Additional figlet options (use after --)
|
||||||
/// Example: piglet "Text" -- -w 200 -c
|
/// Example: piglet "Text" -- -w 200 -c
|
||||||
#[arg(last = true)]
|
#[arg(last = true)]
|
||||||
pub figlet_args: Vec<String>,
|
pub figlet_args: Vec<String>,
|
||||||
|
|
||||||
/// Loop animation infinitely
|
/// Loop animation infinitely
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub loop_animation: bool,
|
pub loop_animation: bool,
|
||||||
|
|
||||||
/// Frame rate (fps)
|
/// Frame rate (fps)
|
||||||
#[arg(long, default_value = "30")]
|
#[arg(long, default_value = "30")]
|
||||||
pub fps: u32,
|
pub fps: u32,
|
||||||
|
|
||||||
/// List all available effects
|
/// List all available effects
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub list_effects: bool,
|
pub list_effects: bool,
|
||||||
|
|
||||||
/// List all available easing functions
|
/// List all available easing functions
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub list_easing: bool,
|
pub list_easing: bool,
|
||||||
|
|
||||||
/// List all available CSS4 colors
|
/// List all available CSS4 colors
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub list_colors: bool,
|
pub list_colors: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ use crossterm::style::Color as CrosstermColor;
|
|||||||
|
|
||||||
pub fn apply_color_to_char(ch: char, color: Color) -> String {
|
pub fn apply_color_to_char(ch: char, color: Color) -> String {
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
|
|
||||||
let crossterm_color = CrosstermColor::Rgb {
|
let crossterm_color = CrosstermColor::Rgb {
|
||||||
r: color.r,
|
r: color.r,
|
||||||
g: color.g,
|
g: color.g,
|
||||||
b: color.b,
|
b: color.b,
|
||||||
};
|
};
|
||||||
|
|
||||||
format!("{}", ch.to_string().with(crossterm_color))
|
format!("{}", ch.to_string().with(crossterm_color))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ pub fn apply_color_to_line(line: &str, colors: &[Color]) -> String {
|
|||||||
if colors.is_empty() {
|
if colors.is_empty() {
|
||||||
return line.to_string();
|
return line.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
line.chars()
|
line.chars()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, ch)| {
|
.map(|(i, ch)| {
|
||||||
@@ -34,14 +34,14 @@ pub fn apply_color_to_line(line: &str, colors: &[Color]) -> String {
|
|||||||
pub fn apply_gradient_to_text(text: &str, colors: &[Color]) -> String {
|
pub fn apply_gradient_to_text(text: &str, colors: &[Color]) -> String {
|
||||||
let lines: Vec<&str> = text.lines().collect();
|
let lines: Vec<&str> = text.lines().collect();
|
||||||
let total_chars: usize = lines.iter().map(|l| l.chars().count()).sum();
|
let total_chars: usize = lines.iter().map(|l| l.chars().count()).sum();
|
||||||
|
|
||||||
if total_chars == 0 || colors.is_empty() {
|
if total_chars == 0 || colors.is_empty() {
|
||||||
return text.to_string();
|
return text.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
let mut char_index = 0;
|
let mut char_index = 0;
|
||||||
|
|
||||||
for (line_idx, line) in lines.iter().enumerate() {
|
for (line_idx, line) in lines.iter().enumerate() {
|
||||||
for ch in line.chars() {
|
for ch in line.chars() {
|
||||||
if ch.is_whitespace() {
|
if ch.is_whitespace() {
|
||||||
@@ -53,11 +53,11 @@ pub fn apply_gradient_to_text(text: &str, colors: &[Color]) -> String {
|
|||||||
char_index += 1;
|
char_index += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if line_idx < lines.len() - 1 {
|
if line_idx < lines.len() - 1 {
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::parser::gradient::Gradient;
|
|
||||||
use crate::parser::color::Color;
|
use crate::parser::color::Color;
|
||||||
|
use crate::parser::gradient::Gradient;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct GradientEngine {
|
pub struct GradientEngine {
|
||||||
gradient: Gradient,
|
gradient: Gradient,
|
||||||
}
|
}
|
||||||
@@ -10,17 +11,17 @@ impl GradientEngine {
|
|||||||
pub fn new(gradient: Gradient) -> Self {
|
pub fn new(gradient: Gradient) -> Self {
|
||||||
Self { gradient }
|
Self { gradient }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_string(gradient_str: &str) -> Result<Self> {
|
pub fn from_string(gradient_str: &str) -> Result<Self> {
|
||||||
let gradient = Gradient::parse(gradient_str)?;
|
let gradient = Gradient::parse(gradient_str)?;
|
||||||
Ok(Self::new(gradient))
|
Ok(Self::new(gradient))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn color_at(&self, t: f64) -> Color {
|
pub fn color_at(&self, t: f64) -> Color {
|
||||||
self.gradient.color_at(t)
|
self.gradient.color_at(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn colors(&self, steps: usize) -> Vec<Color> {
|
pub fn colors(&self, steps: usize) -> Vec<Color> {
|
||||||
self.gradient.colors(steps)
|
self.gradient.colors(steps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
83
src/color/mod.rs
Normal file
83
src/color/mod.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
pub mod apply;
|
||||||
|
pub mod gradient;
|
||||||
|
pub mod palette;
|
||||||
|
|
||||||
|
use crate::parser::color::Color;
|
||||||
|
use anyhow::Result;
|
||||||
|
pub use gradient::GradientEngine;
|
||||||
|
pub use palette::ColorPalette;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ColorMode {
|
||||||
|
None,
|
||||||
|
Palette(ColorPalette),
|
||||||
|
Gradient(GradientEngine),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ColorEngine {
|
||||||
|
mode: ColorMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorEngine {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
mode: ColorMode::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_palette(mut self, palette: Option<&[String]>) -> Result<Self> {
|
||||||
|
if let Some(colors) = palette {
|
||||||
|
if !colors.is_empty() {
|
||||||
|
let palette = ColorPalette::from_strings(colors)?;
|
||||||
|
self.mode = ColorMode::Palette(palette);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_gradient(mut self, gradient: Option<&str>) -> Result<Self> {
|
||||||
|
if let Some(gradient_str) = gradient {
|
||||||
|
let gradient = GradientEngine::from_string(gradient_str)?;
|
||||||
|
self.mode = ColorMode::Gradient(gradient);
|
||||||
|
}
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_colors(&self) -> bool {
|
||||||
|
!matches!(self.mode, ColorMode::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn get_color(&self, t: f64, index: usize) -> Option<Color> {
|
||||||
|
match &self.mode {
|
||||||
|
ColorMode::None => None,
|
||||||
|
ColorMode::Palette(palette) => Some(palette.get_color(index)),
|
||||||
|
ColorMode::Gradient(gradient) => Some(gradient.color_at(t)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn get_colors(&self, steps: usize) -> Vec<Color> {
|
||||||
|
match &self.mode {
|
||||||
|
ColorMode::None => vec![],
|
||||||
|
ColorMode::Palette(palette) => (0..steps).map(|i| palette.get_color(i)).collect(),
|
||||||
|
ColorMode::Gradient(gradient) => gradient.colors(steps),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color_at(&self, t: f64) -> Option<Color> {
|
||||||
|
match &self.mode {
|
||||||
|
ColorMode::None => None,
|
||||||
|
ColorMode::Palette(palette) => {
|
||||||
|
Some(palette.get_color((t * palette.len() as f64) as usize))
|
||||||
|
}
|
||||||
|
ColorMode::Gradient(gradient) => Some(gradient.color_at(t)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ColorEngine {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,53 @@
|
|||||||
"#ffff00".to_string(),
|
use crate::parser::color::Color;
|
||||||
]).unwrap()
|
use anyhow::Result;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ColorPalette {
|
||||||
|
colors: Vec<Color>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorPalette {
|
||||||
|
pub fn new(colors: Vec<Color>) -> Self {
|
||||||
|
Self { colors }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_strings(color_strs: &[String]) -> Result<Self> {
|
||||||
|
let colors: Result<Vec<Color>> = color_strs.iter().map(|s| Color::parse(s)).collect();
|
||||||
|
Ok(Self::new(colors?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_color(&self, index: usize) -> Color {
|
||||||
|
if self.colors.is_empty() {
|
||||||
|
return Color::new(255, 255, 255);
|
||||||
|
}
|
||||||
|
self.colors[index % self.colors.len()]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.colors.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.colors.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create rainbow palette
|
||||||
|
pub fn rainbow() -> Self {
|
||||||
|
Self::from_strings(&[
|
||||||
|
"#ff0000".to_string(),
|
||||||
|
"#ff7f00".to_string(),
|
||||||
|
"#ffff00".to_string(),
|
||||||
|
"#00ff00".to_string(),
|
||||||
|
"#0000ff".to_string(),
|
||||||
|
"#4b0082".to_string(),
|
||||||
|
"#9400d3".to_string(),
|
||||||
|
])
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
/// Create ocean palette
|
/// Create ocean palette
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn ocean() -> Self {
|
pub fn ocean() -> Self {
|
||||||
Self::from_strings(&[
|
Self::from_strings(&[
|
||||||
"#000080".to_string(),
|
"#000080".to_string(),
|
||||||
@@ -10,7 +55,8 @@
|
|||||||
"#4169e1".to_string(),
|
"#4169e1".to_string(),
|
||||||
"#87ceeb".to_string(),
|
"#87ceeb".to_string(),
|
||||||
"#add8e6".to_string(),
|
"#add8e6".to_string(),
|
||||||
]).unwrap()
|
])
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,4 +64,4 @@ impl Default for ColorPalette {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::rainbow()
|
Self::rainbow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{bail, Context, Result};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use which::which;
|
use which::which;
|
||||||
|
|
||||||
@@ -14,78 +14,75 @@ impl FigletWrapper {
|
|||||||
args: Vec::new(),
|
args: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_font(mut self, font: Option<&str>) -> Self {
|
pub fn with_font(mut self, font: Option<&str>) -> Self {
|
||||||
self.font = font.map(|s| s.to_string());
|
self.font = font.map(|s| s.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_args(mut self, args: Vec<String>) -> Self {
|
pub fn with_args(mut self, args: Vec<String>) -> Self {
|
||||||
self.args = args;
|
self.args = args;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&self, text: &str) -> Result<String> {
|
pub fn render(&self, text: &str) -> Result<String> {
|
||||||
let mut cmd = Command::new("figlet");
|
let mut cmd = Command::new("figlet");
|
||||||
|
|
||||||
// Add font if specified
|
// Add font if specified
|
||||||
if let Some(font) = &self.font {
|
if let Some(font) = &self.font {
|
||||||
cmd.arg("-f").arg(font);
|
cmd.arg("-f").arg(font);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add additional arguments
|
// Add additional arguments
|
||||||
for arg in &self.args {
|
for arg in &self.args {
|
||||||
cmd.arg(arg);
|
cmd.arg(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the text
|
// Add the text
|
||||||
cmd.arg(text);
|
cmd.arg(text);
|
||||||
|
|
||||||
// Execute and capture output
|
// Execute and capture output
|
||||||
let output = cmd.output()
|
let output = cmd.output().context("Failed to execute figlet")?;
|
||||||
.context("Failed to execute figlet")?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
bail!("Figlet error: {}", stderr);
|
bail!("Figlet error: {}", stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = String::from_utf8(output.stdout)
|
let result =
|
||||||
.context("Figlet output is not valid UTF-8")?;
|
String::from_utf8(output.stdout).context("Figlet output is not valid UTF-8")?;
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_installed() -> Result<()> {
|
pub fn check_installed() -> Result<()> {
|
||||||
which("figlet")
|
which("figlet").context(
|
||||||
.context("figlet not found. Please install figlet first.\n\
|
"figlet not found. Please install figlet first.\n\
|
||||||
On Ubuntu/Debian: sudo apt-get install figlet\n\
|
On Ubuntu/Debian: sudo apt-get install figlet\n\
|
||||||
On macOS: brew install figlet\n\
|
On macOS: brew install figlet\n\
|
||||||
On Arch: sudo pacman -S figlet")?;
|
On Arch: sudo pacman -S figlet",
|
||||||
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn list_fonts() -> Result<Vec<String>> {
|
pub fn list_fonts() -> Result<Vec<String>> {
|
||||||
let output = Command::new("figlet")
|
let output = Command::new("figlet")
|
||||||
.arg("-l")
|
.arg("-l")
|
||||||
.output()
|
.output()
|
||||||
.context("Failed to list figlet fonts")?;
|
.context("Failed to list figlet fonts")?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
bail!("Failed to list fonts");
|
bail!("Failed to list fonts");
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = String::from_utf8_lossy(&output.stdout);
|
let result = String::from_utf8_lossy(&output.stdout);
|
||||||
let fonts: Vec<String> = result
|
let fonts: Vec<String> = result
|
||||||
.lines()
|
.lines()
|
||||||
.skip(1) // Skip header
|
.skip(1) // Skip header
|
||||||
.filter_map(|line| {
|
.filter_map(|line| line.split_whitespace().next().map(|s| s.to_string()))
|
||||||
line.split_whitespace()
|
|
||||||
.next()
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(fonts)
|
Ok(fonts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,13 +96,13 @@ impl Default for FigletWrapper {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_figlet_installed() {
|
fn test_figlet_installed() {
|
||||||
// This test will fail if figlet is not installed
|
// This test will fail if figlet is not installed
|
||||||
assert!(FigletWrapper::check_installed().is_ok());
|
assert!(FigletWrapper::check_installed().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_basic_render() {
|
fn test_basic_render() {
|
||||||
let figlet = FigletWrapper::new();
|
let figlet = FigletWrapper::new();
|
||||||
@@ -115,4 +112,4 @@ mod tests {
|
|||||||
assert!(!ascii.is_empty());
|
assert!(!ascii.is_empty());
|
||||||
assert!(ascii.contains("H") || ascii.contains("_") || ascii.contains("|"));
|
assert!(ascii.contains("H") || ascii.contains("_") || ascii.contains("|"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
pub mod cli;
|
|
||||||
pub mod figlet;
|
|
||||||
pub mod color;
|
|
||||||
pub mod animation;
|
pub mod animation;
|
||||||
|
pub mod cli;
|
||||||
|
pub mod color;
|
||||||
|
pub mod figlet;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
pub use cli::PigletCli;
|
pub use cli::PigletCli;
|
||||||
|
|||||||
82
src/main.rs
82
src/main.rs
@@ -1,31 +1,31 @@
|
|||||||
mod cli;
|
|
||||||
mod figlet;
|
|
||||||
mod color;
|
|
||||||
mod animation;
|
mod animation;
|
||||||
|
mod cli;
|
||||||
|
mod color;
|
||||||
|
mod figlet;
|
||||||
mod parser;
|
mod parser;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use cli::PigletCli;
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use cli::PigletCli;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
// Parse CLI arguments
|
// Parse CLI arguments
|
||||||
let args = PigletCli::parse();
|
let args = PigletCli::parse();
|
||||||
|
|
||||||
// Show banner on first run
|
// Show banner on first run
|
||||||
if std::env::args().len() == 1 {
|
if std::env::args().len() == 1 {
|
||||||
show_welcome();
|
show_welcome();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify figlet is installed
|
// Verify figlet is installed
|
||||||
figlet::FigletWrapper::check_installed()?;
|
figlet::FigletWrapper::check_installed()?;
|
||||||
|
|
||||||
// Run the piglet magic
|
// Run the piglet magic
|
||||||
run_piglet(args).await?;
|
run_piglet(args).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,69 +33,73 @@ async fn run_piglet(args: PigletCli) -> Result<()> {
|
|||||||
use crate::animation::AnimationEngine;
|
use crate::animation::AnimationEngine;
|
||||||
use crate::color::ColorEngine;
|
use crate::color::ColorEngine;
|
||||||
use crate::utils::terminal::TerminalManager;
|
use crate::utils::terminal::TerminalManager;
|
||||||
|
|
||||||
// Parse duration
|
// Parse duration
|
||||||
let duration_ms = parser::duration::parse_duration(&args.duration)?;
|
let duration_ms = parser::duration::parse_duration(&args.duration)?;
|
||||||
|
|
||||||
// Create figlet wrapper and render base ASCII art
|
// Create figlet wrapper and render base ASCII art
|
||||||
let figlet = figlet::FigletWrapper::new()
|
let figlet = figlet::FigletWrapper::new()
|
||||||
.with_font(args.font.as_deref())
|
.with_font(args.font.as_deref())
|
||||||
.with_args(args.figlet_args);
|
.with_args(args.figlet_args);
|
||||||
|
|
||||||
let ascii_art = figlet.render(&args.text)?;
|
let ascii_art = figlet.render(&args.text)?;
|
||||||
|
|
||||||
// Setup color engine
|
// Setup color engine
|
||||||
let color_engine = ColorEngine::new()
|
let color_engine = ColorEngine::new()
|
||||||
.with_palette(args.color_palette.as_deref())
|
.with_palette(args.color_palette.as_deref())?
|
||||||
.with_gradient(args.color_gradient.as_deref())?;
|
.with_gradient(args.color_gradient.as_deref())?;
|
||||||
|
|
||||||
// Setup animation engine
|
// Setup animation engine
|
||||||
let animation_engine = AnimationEngine::new(
|
let animation_engine = AnimationEngine::new(ascii_art, duration_ms, args.fps)
|
||||||
ascii_art,
|
.with_effect(&args.motion_effect)?
|
||||||
duration_ms,
|
.with_easing(&args.motion_ease)?
|
||||||
args.fps,
|
.with_color_engine(color_engine);
|
||||||
)
|
|
||||||
.with_effect(&args.motion_effect)?
|
|
||||||
.with_easing(&args.motion_ease)?
|
|
||||||
.with_color_engine(color_engine);
|
|
||||||
|
|
||||||
// Setup terminal
|
// Setup terminal
|
||||||
let mut terminal = TerminalManager::new()?;
|
let mut terminal = TerminalManager::new()?;
|
||||||
terminal.setup()?;
|
terminal.setup()?;
|
||||||
|
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
terminal.cleanup()?;
|
terminal.cleanup()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_welcome() {
|
fn show_welcome() {
|
||||||
println!(r#"
|
println!(
|
||||||
____ _ __ __
|
r"
|
||||||
|
____ _ __ __
|
||||||
/ __ \(_)___ _/ /__ / /_
|
/ __ \(_)___ _/ /__ / /_
|
||||||
/ /_/ / / __ `/ / _ \/ __/
|
/ /_/ / / __ `/ / _ \/ __/
|
||||||
/ ____/ / /_/ / / __/ /_
|
/ ____/ / /_/ / / __/ /_
|
||||||
/_/ /_/\__, /_/\___/\__/
|
/_/ /_/\__, /_/\___/\__/
|
||||||
/____/
|
/____/
|
||||||
|
|
||||||
🐷 Piglet - Animated Figlet Wrapper
|
🐷 Piglet - Animated Figlet Wrapper
|
||||||
|
|
||||||
Usage: piglet [TEXT] [OPTIONS]
|
Usage: piglet [TEXT] [OPTIONS]
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
piglet "Hello" -p "#FF5733,#33FF57"
|
piglet Hello -p red,blue,green
|
||||||
piglet "World" -g "linear-gradient(90deg, red, blue)" -e fade-in
|
piglet World -g linear-gradient(90deg, red, blue) -e fade-in
|
||||||
piglet "Cool!" -e typewriter -d 2s -i ease-out
|
piglet Cool! -e typewriter -d 2s -i ease-out
|
||||||
|
|
||||||
Run 'piglet --help' for more information.
|
Run 'piglet --help' for more information.
|
||||||
"#);
|
"
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use anyhow::{Result, Context};
|
use anyhow::{Context, Result};
|
||||||
use csscolorparser::Color as CssColor;
|
use csscolorparser::Color as CssColor;
|
||||||
use palette::rgb::Rgb;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct Color {
|
pub struct Color {
|
||||||
@@ -13,12 +12,35 @@ impl Color {
|
|||||||
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
||||||
Self { r, g, b }
|
Self { r, g, b }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_hex(hex: &str) -> Result<Self> {
|
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))?;
|
.context(format!("Failed to parse hex color: {}", hex))?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
r: (color.r * 255.0) as u8,
|
r: (color.r * 255.0) as u8,
|
||||||
g: (color.g * 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 anyhow::{bail, Result};
|
||||||
use regex::Regex;
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref DURATION_REGEX: Regex = Regex::new(
|
static ref DURATION_REGEX: Regex = Regex::new(r"^(\d+(?:\.\d+)?)(ms|s|m|h)$").unwrap();
|
||||||
r"^(\d+(?:\.\d+)?)(ms|s|m|h)$"
|
|
||||||
).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse duration string to milliseconds
|
/// Parse duration string to milliseconds
|
||||||
/// Supports: 3000ms, 0.3s, 5m, 0.5h
|
/// Supports: 3000ms, 0.3s, 5m, 0.5h
|
||||||
pub fn parse_duration(duration: &str) -> Result<u64> {
|
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))?;
|
.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"))?;
|
.map_err(|_| anyhow::anyhow!("Invalid numeric value in duration"))?;
|
||||||
|
|
||||||
let unit = &caps[2];
|
let unit = &caps[2];
|
||||||
|
|
||||||
let milliseconds = match unit {
|
let milliseconds = match unit {
|
||||||
"ms" => value,
|
"ms" => value,
|
||||||
"s" => value * 1000.0,
|
"s" => value * 1000.0,
|
||||||
@@ -26,47 +26,47 @@ pub fn parse_duration(duration: &str) -> Result<u64> {
|
|||||||
"h" => value * 60.0 * 60.0 * 1000.0,
|
"h" => value * 60.0 * 60.0 * 1000.0,
|
||||||
_ => bail!("Unknown time unit: {}", unit),
|
_ => bail!("Unknown time unit: {}", unit),
|
||||||
};
|
};
|
||||||
|
|
||||||
if milliseconds < 0.0 {
|
if milliseconds < 0.0 {
|
||||||
bail!("Duration cannot be negative");
|
bail!("Duration cannot be negative");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(milliseconds as u64)
|
Ok(milliseconds as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_milliseconds() {
|
fn test_parse_milliseconds() {
|
||||||
assert_eq!(parse_duration("3000ms").unwrap(), 3000);
|
assert_eq!(parse_duration("3000ms").unwrap(), 3000);
|
||||||
assert_eq!(parse_duration("500ms").unwrap(), 500);
|
assert_eq!(parse_duration("500ms").unwrap(), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_seconds() {
|
fn test_parse_seconds() {
|
||||||
assert_eq!(parse_duration("3s").unwrap(), 3000);
|
assert_eq!(parse_duration("3s").unwrap(), 3000);
|
||||||
assert_eq!(parse_duration("0.5s").unwrap(), 500);
|
assert_eq!(parse_duration("0.5s").unwrap(), 500);
|
||||||
assert_eq!(parse_duration("1.5s").unwrap(), 1500);
|
assert_eq!(parse_duration("1.5s").unwrap(), 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_minutes() {
|
fn test_parse_minutes() {
|
||||||
assert_eq!(parse_duration("1m").unwrap(), 60000);
|
assert_eq!(parse_duration("1m").unwrap(), 60000);
|
||||||
assert_eq!(parse_duration("0.5m").unwrap(), 30000);
|
assert_eq!(parse_duration("0.5m").unwrap(), 30000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_hours() {
|
fn test_parse_hours() {
|
||||||
assert_eq!(parse_duration("1h").unwrap(), 3600000);
|
assert_eq!(parse_duration("1h").unwrap(), 3600000);
|
||||||
assert_eq!(parse_duration("0.5h").unwrap(), 1800000);
|
assert_eq!(parse_duration("0.5h").unwrap(), 1800000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_format() {
|
fn test_invalid_format() {
|
||||||
assert!(parse_duration("invalid").is_err());
|
assert!(parse_duration("invalid").is_err());
|
||||||
assert!(parse_duration("10").is_err());
|
assert!(parse_duration("10").is_err());
|
||||||
assert!(parse_duration("10x").is_err());
|
assert!(parse_duration("10x").is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
122
src/parser/gradient.rs
Normal file
122
src/parser/gradient.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
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 color;
|
||||||
pub mod gradient;
|
pub mod duration;
|
||||||
|
pub mod gradient;
|
||||||
|
|||||||
54
src/utils/ansi.rs
Normal file
54
src/utils/ansi.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/// Strip ANSI escape sequences from a string to get visual width
|
||||||
|
pub fn strip_ansi(text: &str) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut chars = text.chars().peekable();
|
||||||
|
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
if ch == '\x1b' {
|
||||||
|
// Skip ANSI escape sequence
|
||||||
|
if chars.peek() == Some(&'[') {
|
||||||
|
chars.next(); // consume '['
|
||||||
|
// Skip until we hit a letter (the command character)
|
||||||
|
while let Some(&c) = chars.peek() {
|
||||||
|
chars.next();
|
||||||
|
if c.is_ascii_alphabetic() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the visual width of a string (excluding ANSI codes)
|
||||||
|
pub fn visual_width(text: &str) -> usize {
|
||||||
|
strip_ansi(text).chars().count()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_strip_ansi() {
|
||||||
|
let text = "\x1b[38;2;255;87;51mHello\x1b[0m";
|
||||||
|
assert_eq!(strip_ansi(text), "Hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_visual_width() {
|
||||||
|
let text = "\x1b[38;2;255;87;51mHi\x1b[0m";
|
||||||
|
assert_eq!(visual_width(text), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_ansi() {
|
||||||
|
let text = "Plain text";
|
||||||
|
assert_eq!(strip_ansi(text), "Plain text");
|
||||||
|
assert_eq!(visual_width(text), 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
use crate::parser::color::Color;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AsciiArt {
|
pub struct AsciiArt {
|
||||||
lines: Vec<String>,
|
lines: Vec<String>,
|
||||||
@@ -12,47 +10,49 @@ impl AsciiArt {
|
|||||||
let lines: Vec<String> = text.lines().map(|s| s.to_string()).collect();
|
let lines: Vec<String> = text.lines().map(|s| s.to_string()).collect();
|
||||||
let width = lines.iter().map(|l| l.len()).max().unwrap_or(0);
|
let width = lines.iter().map(|l| l.len()).max().unwrap_or(0);
|
||||||
let height = lines.len();
|
let height = lines.len();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
lines,
|
lines,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_lines(&self) -> &[String] {
|
pub fn get_lines(&self) -> &[String] {
|
||||||
&self.lines
|
&self.lines
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn width(&self) -> usize {
|
pub fn width(&self) -> usize {
|
||||||
self.width
|
self.width
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn height(&self) -> usize {
|
pub fn height(&self) -> usize {
|
||||||
self.height
|
self.height
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_string(&self) -> String {
|
pub fn render(&self) -> String {
|
||||||
self.lines.join("\n")
|
self.lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get character at position
|
/// Get character at position
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn char_at(&self, x: usize, y: usize) -> Option<char> {
|
pub fn char_at(&self, x: usize, y: usize) -> Option<char> {
|
||||||
self.lines.get(y)?.chars().nth(x)
|
self.lines.get(y)?.chars().nth(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count non-whitespace characters
|
/// Count non-whitespace characters
|
||||||
pub fn char_count(&self) -> usize {
|
pub fn char_count(&self) -> usize {
|
||||||
self.lines.iter()
|
self.lines
|
||||||
|
.iter()
|
||||||
.flat_map(|line| line.chars())
|
.flat_map(|line| line.chars())
|
||||||
.filter(|c| !c.is_whitespace())
|
.filter(|c| !c.is_whitespace())
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all character positions
|
/// Get all character positions
|
||||||
pub fn char_positions(&self) -> Vec<(usize, usize, char)> {
|
pub fn char_positions(&self) -> Vec<(usize, usize, char)> {
|
||||||
let mut positions = Vec::new();
|
let mut positions = Vec::new();
|
||||||
|
|
||||||
for (y, line) in self.lines.iter().enumerate() {
|
for (y, line) in self.lines.iter().enumerate() {
|
||||||
for (x, ch) in line.chars().enumerate() {
|
for (x, ch) in line.chars().enumerate() {
|
||||||
if !ch.is_whitespace() {
|
if !ch.is_whitespace() {
|
||||||
@@ -60,72 +60,66 @@ impl AsciiArt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
positions
|
positions
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply fade effect (0.0 = invisible, 1.0 = visible)
|
/// Apply fade effect (0.0 = invisible, 1.0 = visible)
|
||||||
pub fn apply_fade(&self, opacity: f64) -> String {
|
pub fn apply_fade(&self, opacity: f64) -> String {
|
||||||
if opacity >= 1.0 {
|
if opacity >= 1.0 {
|
||||||
return self.to_string();
|
return self.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
if opacity <= 0.0 {
|
if opacity <= 0.0 {
|
||||||
return " ".repeat(self.width).repeat(self.height);
|
return " ".repeat(self.width).repeat(self.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For ASCII, we can simulate fade by replacing chars with lighter ones
|
// For ASCII, we can simulate fade by replacing chars with lighter ones
|
||||||
let fade_chars = [' ', '.', '·', '-', '~', '=', '+', '*', '#', '@'];
|
let fade_chars = [' ', '.', '·', '-', '~', '=', '+', '*', '#', '@'];
|
||||||
let index = (opacity * (fade_chars.len() - 1) as f64) as usize;
|
let index = (opacity * (fade_chars.len() - 1) as f64) as usize;
|
||||||
let fade_char = fade_chars[index];
|
let fade_char = fade_chars[index];
|
||||||
|
|
||||||
self.lines.iter()
|
self.lines
|
||||||
|
.iter()
|
||||||
.map(|line| {
|
.map(|line| {
|
||||||
line.chars()
|
line.chars()
|
||||||
.map(|ch| {
|
.map(|ch| if ch.is_whitespace() { ch } else { fade_char })
|
||||||
if ch.is_whitespace() {
|
|
||||||
ch
|
|
||||||
} else {
|
|
||||||
fade_char
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scale the ASCII art
|
/// Scale the ASCII art
|
||||||
pub fn scale(&self, factor: f64) -> Self {
|
pub fn scale(&self, factor: f64) -> Self {
|
||||||
if factor <= 0.0 {
|
if factor <= 0.0 {
|
||||||
return Self::new(String::new());
|
return Self::new(String::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (factor - 1.0).abs() < 0.01 {
|
if (factor - 1.0).abs() < 0.01 {
|
||||||
return self.clone();
|
return self.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple scaling by character repetition
|
// Simple scaling by character repetition
|
||||||
let lines = if factor > 1.0 {
|
let lines: Vec<String> = if factor > 1.0 {
|
||||||
self.lines.iter()
|
self.lines
|
||||||
|
.iter()
|
||||||
.flat_map(|line| {
|
.flat_map(|line| {
|
||||||
let scaled_line: String = line.chars()
|
let scaled_line: String = line
|
||||||
.flat_map(|ch| std::iter::repeat(ch).take(factor as usize))
|
.chars()
|
||||||
|
.flat_map(|ch| std::iter::repeat_n(ch, factor as usize))
|
||||||
.collect();
|
.collect();
|
||||||
std::iter::repeat(scaled_line).take(factor as usize)
|
std::iter::repeat_n(scaled_line, factor as usize)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
self.lines.iter()
|
self.lines
|
||||||
|
.iter()
|
||||||
.step_by((1.0 / factor) as usize)
|
.step_by((1.0 / factor) as usize)
|
||||||
.map(|line| {
|
.map(|line| line.chars().step_by((1.0 / factor) as usize).collect())
|
||||||
line.chars()
|
|
||||||
.step_by((1.0 / factor) as usize)
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::new(lines.join("\n"))
|
Self::new(lines.join("\n"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
pub mod ansi;
|
||||||
|
pub mod ascii;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
pub mod ascii;
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor,
|
cursor, execute,
|
||||||
execute,
|
|
||||||
terminal::{self, ClearType},
|
terminal::{self, ClearType},
|
||||||
ExecutableCommand,
|
|
||||||
};
|
};
|
||||||
use std::io::{stdout, Write};
|
use std::io::{stdout, Write};
|
||||||
|
|
||||||
|
use super::ansi;
|
||||||
|
|
||||||
pub struct TerminalManager {
|
pub struct TerminalManager {
|
||||||
width: u16,
|
width: u16,
|
||||||
height: u16,
|
height: u16,
|
||||||
@@ -22,74 +22,70 @@ impl TerminalManager {
|
|||||||
original_state: false,
|
original_state: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup(&mut self) -> Result<()> {
|
pub fn setup(&mut self) -> Result<()> {
|
||||||
terminal::enable_raw_mode()?;
|
terminal::enable_raw_mode()?;
|
||||||
execute!(
|
execute!(stdout(), terminal::EnterAlternateScreen, cursor::Hide)?;
|
||||||
stdout(),
|
|
||||||
terminal::EnterAlternateScreen,
|
|
||||||
cursor::Hide
|
|
||||||
)?;
|
|
||||||
self.original_state = true;
|
self.original_state = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cleanup(&mut self) -> Result<()> {
|
pub fn cleanup(&mut self) -> Result<()> {
|
||||||
if self.original_state {
|
if self.original_state {
|
||||||
execute!(
|
execute!(stdout(), cursor::Show, terminal::LeaveAlternateScreen)?;
|
||||||
stdout(),
|
|
||||||
cursor::Show,
|
|
||||||
terminal::LeaveAlternateScreen
|
|
||||||
)?;
|
|
||||||
terminal::disable_raw_mode()?;
|
terminal::disable_raw_mode()?;
|
||||||
self.original_state = false;
|
self.original_state = false;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&self) -> Result<()> {
|
pub fn clear(&self) -> Result<()> {
|
||||||
execute!(stdout(), terminal::Clear(ClearType::All))?;
|
execute!(stdout(), terminal::Clear(ClearType::All))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_to(&self, x: u16, y: u16) -> Result<()> {
|
pub fn move_to(&self, x: u16, y: u16) -> Result<()> {
|
||||||
execute!(stdout(), cursor::MoveTo(x, y))?;
|
execute!(stdout(), cursor::MoveTo(x, y))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_size(&self) -> (u16, u16) {
|
pub fn get_size(&self) -> (u16, u16) {
|
||||||
(self.width, self.height)
|
(self.width, self.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_size(&mut self) -> Result<()> {
|
pub fn refresh_size(&mut self) -> Result<()> {
|
||||||
let (width, height) = terminal::size()?;
|
let (width, height) = terminal::size()?;
|
||||||
self.width = width;
|
self.width = width;
|
||||||
self.height = height;
|
self.height = height;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_at(&self, x: u16, y: u16, text: &str) -> Result<()> {
|
pub fn print_at(&self, x: u16, y: u16, text: &str) -> Result<()> {
|
||||||
self.move_to(x, y)?;
|
self.move_to(x, y)?;
|
||||||
print!("{}", text);
|
print!("{}", text);
|
||||||
stdout().flush()?;
|
stdout().flush()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_centered(&self, text: &str) -> Result<()> {
|
pub fn print_centered(&self, text: &str) -> Result<()> {
|
||||||
let lines: Vec<&str> = text.lines().collect();
|
let lines: Vec<&str> = text.lines().collect();
|
||||||
let max_width = lines.iter().map(|l| l.len()).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 height = lines.len() as u16;
|
||||||
|
|
||||||
let start_x = (self.width.saturating_sub(max_width)) / 2;
|
let start_x = (self.width.saturating_sub(max_width)) / 2;
|
||||||
let start_y = (self.height.saturating_sub(height)) / 2;
|
let start_y = (self.height.saturating_sub(height)) / 2;
|
||||||
|
|
||||||
for (i, line) in lines.iter().enumerate() {
|
for (i, line) in lines.iter().enumerate() {
|
||||||
let line_width = line.len() as u16;
|
let line_width = ansi::visual_width(line) as u16;
|
||||||
let x = start_x + (max_width.saturating_sub(line_width)) / 2;
|
let x = start_x + (max_width.saturating_sub(line_width)) / 2;
|
||||||
let y = start_y + i as u16;
|
let y = start_y + i as u16;
|
||||||
self.print_at(x, y, line)?;
|
self.print_at(x, y, line)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,4 +94,4 @@ impl Drop for TerminalManager {
|
|||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let _ = self.cleanup();
|
let _ = self.cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
use anyhow::Result;
|
||||||
use piglet::{
|
use piglet::{
|
||||||
figlet::FigletWrapper,
|
|
||||||
parser::{duration::parse_duration, color::Color, gradient::Gradient},
|
|
||||||
color::{ColorEngine, palette::ColorPalette},
|
|
||||||
animation::easing::get_easing_function,
|
animation::easing::get_easing_function,
|
||||||
animation::effects::get_effect,
|
animation::effects::get_effect,
|
||||||
|
color::{palette::ColorPalette, ColorEngine},
|
||||||
|
figlet::FigletWrapper,
|
||||||
|
parser::{color::Color, duration::parse_duration, gradient::Gradient},
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_figlet_wrapper() -> Result<()> {
|
fn test_figlet_wrapper() -> Result<()> {
|
||||||
@@ -31,12 +31,12 @@ fn test_color_parser() -> Result<()> {
|
|||||||
assert_eq!(color.r, 255);
|
assert_eq!(color.r, 255);
|
||||||
assert_eq!(color.g, 87);
|
assert_eq!(color.g, 87);
|
||||||
assert_eq!(color.b, 51);
|
assert_eq!(color.b, 51);
|
||||||
|
|
||||||
let color = Color::parse("red")?;
|
let color = Color::parse("red")?;
|
||||||
assert_eq!(color.r, 255);
|
assert_eq!(color.r, 255);
|
||||||
assert_eq!(color.g, 0);
|
assert_eq!(color.g, 0);
|
||||||
assert_eq!(color.b, 0);
|
assert_eq!(color.b, 0);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,15 +44,14 @@ fn test_color_parser() -> Result<()> {
|
|||||||
fn test_gradient_parser() -> Result<()> {
|
fn test_gradient_parser() -> Result<()> {
|
||||||
let gradient = Gradient::parse("linear-gradient(90deg, red, blue)")?;
|
let gradient = Gradient::parse("linear-gradient(90deg, red, blue)")?;
|
||||||
assert_eq!(gradient.stops.len(), 2);
|
assert_eq!(gradient.stops.len(), 2);
|
||||||
|
|
||||||
let gradient = Gradient::parse(
|
let gradient =
|
||||||
"linear-gradient(to right, #FF5733 0%, #33FF57 50%, #3357FF 100%)"
|
Gradient::parse("linear-gradient(to right, #FF5733 0%, #33FF57 50%, #3357FF 100%)")?;
|
||||||
)?;
|
|
||||||
assert_eq!(gradient.stops.len(), 3);
|
assert_eq!(gradient.stops.len(), 3);
|
||||||
assert_eq!(gradient.stops[0].position, 0.0);
|
assert_eq!(gradient.stops[0].position, 0.0);
|
||||||
assert_eq!(gradient.stops[1].position, 0.5);
|
assert_eq!(gradient.stops[1].position, 0.5);
|
||||||
assert_eq!(gradient.stops[2].position, 1.0);
|
assert_eq!(gradient.stops[2].position, 1.0);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +60,7 @@ fn test_color_interpolation() {
|
|||||||
let red = Color::new(255, 0, 0);
|
let red = Color::new(255, 0, 0);
|
||||||
let blue = Color::new(0, 0, 255);
|
let blue = Color::new(0, 0, 255);
|
||||||
let purple = red.interpolate(&blue, 0.5);
|
let purple = red.interpolate(&blue, 0.5);
|
||||||
|
|
||||||
assert_eq!(purple.r, 127);
|
assert_eq!(purple.r, 127);
|
||||||
assert_eq!(purple.g, 0);
|
assert_eq!(purple.g, 0);
|
||||||
assert_eq!(purple.b, 127);
|
assert_eq!(purple.b, 127);
|
||||||
@@ -69,19 +68,16 @@ fn test_color_interpolation() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_color_palette() -> Result<()> {
|
fn test_color_palette() -> Result<()> {
|
||||||
let palette = ColorPalette::from_strings(&[
|
let palette =
|
||||||
"red".to_string(),
|
ColorPalette::from_strings(&["red".to_string(), "green".to_string(), "blue".to_string()])?;
|
||||||
"green".to_string(),
|
|
||||||
"blue".to_string(),
|
|
||||||
])?;
|
|
||||||
|
|
||||||
assert_eq!(palette.len(), 3);
|
assert_eq!(palette.len(), 3);
|
||||||
|
|
||||||
let color = palette.get_color(0);
|
let color = palette.get_color(0);
|
||||||
assert_eq!(color.r, 255);
|
assert_eq!(color.r, 255);
|
||||||
assert_eq!(color.g, 0);
|
assert_eq!(color.g, 0);
|
||||||
assert_eq!(color.b, 0);
|
assert_eq!(color.b, 0);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,15 +85,15 @@ fn test_color_palette() -> Result<()> {
|
|||||||
fn test_easing_functions() -> Result<()> {
|
fn test_easing_functions() -> Result<()> {
|
||||||
let linear = get_easing_function("linear")?;
|
let linear = get_easing_function("linear")?;
|
||||||
assert_eq!(linear.ease(0.5), 0.5);
|
assert_eq!(linear.ease(0.5), 0.5);
|
||||||
|
|
||||||
let ease_in = get_easing_function("ease-in")?;
|
let ease_in = get_easing_function("ease-in")?;
|
||||||
let result = ease_in.ease(0.5);
|
let result = ease_in.ease(0.5);
|
||||||
assert!(result >= 0.0 && result <= 1.0);
|
assert!((0.0..=1.0).contains(&result));
|
||||||
|
|
||||||
let ease_out_bounce = get_easing_function("ease-out-bounce")?;
|
let ease_out_bounce = get_easing_function("ease-out-bounce")?;
|
||||||
let result = ease_out_bounce.ease(0.5);
|
let result = ease_out_bounce.ease(0.5);
|
||||||
assert!(result >= 0.0 && result <= 1.5); // Bounce can overshoot
|
assert!((0.0..=1.5).contains(&result)); // Bounce can overshoot
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,45 +101,44 @@ fn test_easing_functions() -> Result<()> {
|
|||||||
fn test_effects() -> Result<()> {
|
fn test_effects() -> Result<()> {
|
||||||
let fade_in = get_effect("fade-in")?;
|
let fade_in = get_effect("fade-in")?;
|
||||||
assert_eq!(fade_in.name(), "fade-in");
|
assert_eq!(fade_in.name(), "fade-in");
|
||||||
|
|
||||||
let typewriter = get_effect("typewriter")?;
|
let typewriter = get_effect("typewriter")?;
|
||||||
assert_eq!(typewriter.name(), "typewriter");
|
assert_eq!(typewriter.name(), "typewriter");
|
||||||
|
|
||||||
let bounce = get_effect("bounce-in")?;
|
let bounce = get_effect("bounce-in")?;
|
||||||
assert_eq!(bounce.name(), "bounce-in");
|
assert_eq!(bounce.name(), "bounce-in");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_color_engine() -> Result<()> {
|
fn test_color_engine() -> Result<()> {
|
||||||
let engine = ColorEngine::new()
|
let engine = ColorEngine::new().with_palette(Some(&["red".to_string(), "blue".to_string()]))?;
|
||||||
.with_palette(Some(&["red".to_string(), "blue".to_string()]))?;
|
|
||||||
|
|
||||||
assert!(engine.has_colors());
|
assert!(engine.has_colors());
|
||||||
|
|
||||||
let color = engine.get_color(0.0, 0);
|
let color = engine.get_color(0.0, 0);
|
||||||
assert!(color.is_some());
|
assert!(color.is_some());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gradient_color_at() -> Result<()> {
|
fn test_gradient_color_at() -> Result<()> {
|
||||||
let gradient = Gradient::parse("linear-gradient(red, blue)")?;
|
let gradient = Gradient::parse("linear-gradient(red, blue)")?;
|
||||||
|
|
||||||
let color_start = gradient.color_at(0.0);
|
let color_start = gradient.color_at(0.0);
|
||||||
assert_eq!(color_start.r, 255);
|
assert_eq!(color_start.r, 255);
|
||||||
assert_eq!(color_start.b, 0);
|
assert_eq!(color_start.b, 0);
|
||||||
|
|
||||||
let color_end = gradient.color_at(1.0);
|
let color_end = gradient.color_at(1.0);
|
||||||
assert_eq!(color_end.r, 0);
|
assert_eq!(color_end.r, 0);
|
||||||
assert_eq!(color_end.b, 255);
|
assert_eq!(color_end.b, 255);
|
||||||
|
|
||||||
let color_mid = gradient.color_at(0.5);
|
let color_mid = gradient.color_at(0.5);
|
||||||
assert!(color_mid.r > 0 && color_mid.r < 255);
|
assert!(color_mid.r > 0 && color_mid.r < 255);
|
||||||
assert!(color_mid.b > 0 && color_mid.b < 255);
|
assert!(color_mid.b > 0 && color_mid.b < 255);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,4 +163,4 @@ fn test_invalid_effect() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_easing() {
|
fn test_invalid_easing() {
|
||||||
assert!(get_easing_function("not-an-easing").is_err());
|
assert!(get_easing_function("not-an-easing").is_err());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user