tachyonfx

repository·development·Indexed 22 days ago

https://github.com/ratatui/tachyonfx

A Ratatui library for creating shader-like effects and animations in TUIs. It allows developers to build complex terminal animations by composing stateful effects, applying spatial patterns (such as Radial, Spiral, and Wave), and using CellFilters to target specific colors or regions. The library includes a built-in Effect DSL for compiling effect definitions from strings at runtime, as well as support for custom effects via the Shader trait.

Tokens
14.3K
Snippets
38
Records
62
Agent score
78%

What's inside tachyonfx

  1. How effects and composition work

    development

    TachyonFX is built on three core concepts:

    1. Effects are stateful: You create an effect once and apply it every frame.
    2. Effects transform rendered content: Effects are applied to the buffer after your widgets have been rendered.
    3. Effects compose: You can build complex animations by layering or chaining simple effects.

    Composition Patterns

    Parallel Execution: Run multiple effects at the same time using fx::parallel.

    let effects = fx::parallel(&[
        fx::fade_from_fg(Color::Red, 500),
        fx::sweep_in(Motion::LeftToRight, 10, 0, Color::Black, 800),
    ]);

    Sequential Execution: Chain effects to run one after another using fx::sequence.

    let effects = fx::sequence(&[
        fx::fade_from_fg(Color::Black, 300),
        fx::coalesce(500),
    ]);
    // Run multiple effects in parallel
    let effects = fx::parallel(&[
        fx::fade_from_fg(Color::Red, 500),
        fx::sweep_in(Motion::LeftToRight, 10, 0, Color::Black, 800),
    ]);
    
    // Or sequence them
    let effects = fx::sequence(&[
        fx::fade_from_fg(Color::Black, 300),
        fx::coalesce(500),
    ]);
  2. Apply spatial patterns to effects

    development

    Most effects can be modified with spatial patterns using the .with_pattern() method. Supported patterns include:

    • RadialPattern: RadialPattern::center(), RadialPattern::new(x, y)
    • DiamondPattern: DiamondPattern::center(), DiamondPattern::new(x, y)
    • SpiralPattern: SpiralPattern::center(), SpiralPattern::new(x, y)
    • DiagonalPattern: DiagonalPattern::top_left_to_bottom_right(), etc.
    • CheckerboardPattern: CheckerboardPattern::default(), CheckerboardPattern::with_cell_size()
    • SweepPattern: SweepPattern::left_to_right(), SweepPattern::right_to_left(), etc.
    • Organic Patterns: CoalescePattern::new(), DissolvePattern::new()
    • WavePattern: WavePattern::new(wave_layer)
    • CombinedPattern: CombinedPattern::multiply(a, b), CombinedPattern::max(a, b), CombinedPattern::min(a, b), CombinedPattern::average(a, b)
    • BlendPattern: BlendPattern::new(a, b)
    • InvertedPattern: InvertedPattern::new(pattern)
    RadialPattern::center()
        .with_transition_width(2.5)
        .with_center(0.3, 0.7)
    
    SpiralPattern::center()
        .with_arms(6)
        .with_transition_width(1.5)
  3. Understand the limitations of the tachyonfx Effect DSL

    development

    The tachyonfx Effect DSL is designed for declarative effect creation using basic data types. Because it focuses on simplicity and serialization compatibility, it has several constraints:

    • No Mutable Variables: You can only use immutable bindings.
    • No Control Flow: You cannot use if/else, match, or loop constructs within the DSL.
    • Limited Functions: The DSL primarily supports method calls and object construction.
    • Comments: While you can write comments in the DSL, they are not preserved during serialization.
    • Runtime Dependencies: Effects that require closures, buffers, or channels are not supported.
  4. Apply spatial patterns to effects

    development

    You can control how an effect spreads across the terminal using spatial patterns. This is done by calling .with_pattern() on an effect.

    Common Patterns

    • Radial: Expands outward from a center point.
    • Diagonal: Sweeps across diagonally.
    • Checkerboard: Alternates cell-by-cell in a grid.
    • Spiral: Spiral arm reveals with configurable arm count.
    • Wave: Wave interference patterns with FM/AM modulation.
    // Radial dissolve from center
    let effect = fx::dissolve(800)
        .with_pattern(RadialPattern::center());
    
    // Diagonal fade with transition width
    let effect = fx::fade_to_fg(Color::Cyan, 1000)
        .with_pattern(
            DiagonalPattern::top_left_to_bottom_right()
                .with_transition_width(3.0)
        );
  5. Filter effects by cell criteria

    development

    Use CellFilter to apply effects selectively to specific parts of the terminal. This allows you to target specific colors or regions.

    Example Filters

    • Color-based: Target cells with a specific foreground color.
    • Region-based: Target specific areas (e.g., using Margin) or specific types of content (e.g., Text).
    // Only apply to cells with specific colors
    fx::dissolve(500)
        .with_filter(CellFilter::FgColor(Color::Red))
    
    // Target specific regions
    let filter = CellFilter::AllOf(vec![
        CellFilter::Outer(Margin::new(1, 1)),
        CellFilter::Text,
    ]);
  6. Use the tachyonfx Effect DSL

    development

    The tachyonfx Effect DSL allows you to create, combine, and manipulate terminal effects using a text-based syntax that mirrors Rust. This is useful for runtime configuration, live reloading, serialization, and user customization.

    Note: The DSL requires the "dsl" feature, which is part of the default feature set but depends on "std" (it is not available in no_std environments).

    use tachyonfx::dsl::EffectDsl;
    
    // Create a new DSL compiler with all standard effects registered
    let dsl = EffectDsl::new();
    
    // Compile a simple dissolve effect
    let effect = dsl.compiler()
        .compile("fx::dissolve(500)")
        .expect("Valid effect");
  7. Core TachyonFX API Surface

    development

    TachyonFX is a library for creating shader-like effects in Ratatui terminal UIs. The public API provides tools for managing effects, manipulating colors, and rendering animations.

    Key components include:

    • Effect and IntoEffect: The primary traits/types for defining visual transformations.
    • EffectManager: Orchestrates the lifecycle and application of multiple effects.
    • EffectTimer: Manages time-based progression for animations.
    • EffectRenderer: Handles the actual rendering logic of effects onto the terminal buffer.
    • Shader: Provides low-level shader-like capabilities for cell manipulation.
    • CellIterator: An iterator for traversing terminal cells.
    • ColorSpace and ColorCache: Utilities for advanced color manipulation and performance optimization.
  8. Use let bindings in DSL expressions

    development

    The DSL supports let bindings to define local variables within an expression. This allows you to define complex objects (like Style or Layout) once and reuse them, or simply make long expressions more readable.

    A DSL expression can consist of multiple let bindings followed by a final effect expression.

    let input = r#"
        let motion = Motion::LeftToRight;
        let c = Color::from_u32(0x1d2021);
    
        fx::sweep_in(motion, 10, 0, c, (1000, QuadOut))
    "#;
    
    let dsl = EffectDsl::new();
    let effect = dsl.compiler().compile(input).unwrap();
  9. Implement custom visual effects using the Shader trait

    development

    The Shader trait is the primary interface for creating custom visual effects in tachyonfx. A shader is an object that applies visual changes to terminal cells over time.

    To implement a new effect, you typically only need to override the execute() method. The default process() implementation handles the lifecycle and timer management for you, calling execute() with the appropriate alpha value (progress) based on the elapsed time.

    Key lifecycle methods:

    • execute(&mut self, duration: Duration, area: Rect, buf: &mut Buffer): The main entry point for your effect logic.
    • process(&mut self, duration: Duration, buf: &mut Buffer, area: Rect) -> Option<Duration>: Manages the timer and calls execute. Override this only if you need custom timer logic.
    • done(&self) -> bool: Returns true when the effect has finished its lifecycle.
    • reset(&mut self): Resets the shader's internal timer to its initial state.
  10. How CellFilter works to target specific cells

    development

    A CellFilter is a mechanism used to restrict an effect to a specific subset of cells within an area. You apply a filter to an effect using the Effect::with_filter method.

    Filters are categorized into two types for performance optimization:

    • Static filters: Depend only on geometry (e.g., Area, Inner, Outer, Layout). These can be pre-computed as bitmasks.
    • Dynamic filters: Depend on cell content (e.g., FgColor, Text, NonEmpty, EvalCell). These must be evaluated every frame.

    You can compose complex selection patterns using logical operators like AllOf (AND), AnyOf (OR), NoneOf (NOR), and Not (NOT).

    use tachyonfx::{fx, CellFilter};
    use ratatui_core::style::Color;
    use ratatui_core::layout::Margin;
    
    // Apply fade effect only to red text
    let effect = fx::fade_to_fg(Color::Blue, (1000, tachyonfx::Interpolation::Linear))
        .with_filter(CellFilter::FgColor(Color::Red));
    
    // Apply effect to border area (outer margin)
    let border_effect = fx::dissolve(320)
        .with_filter(CellFilter::Outer(Margin::new(1, 1)));
    
    // Combine filters with logical operations
    let complex_filter = CellFilter::AllOf(vec![
        CellFilter::Text,
        CellFilter::Inner(Margin::new(2, 1))
    ]);
  11. Performance comparison: `for_each_cell` vs `Iterator`

    development

    When iterating over cells in tachyonfx, choose your method based on your performance needs and functional requirements:

    1. for_each_cell (Fastest): Use this for bulk processing or simple loops. It avoids expensive division/modulo operations used to derive (x, y) from a linear index.
    2. Iterator trait (Flexible): Use this only when you need functional combinators like .filter(), .map(), or .take(). It is computationally more expensive per iteration.