Skip to content

Commit

Permalink
fx::prolong_start and prolong_end
Browse files Browse the repository at this point in the history
  • Loading branch information
junkdog committed Aug 19, 2024
1 parent 0c2c9e2 commit ce092d1
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 16 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ widget as an ansi-escaped string, suitable for saving to a file or straight to `
- `fx::offscreen_buffer()`: wraps an existing effect and redirects its rendering
to a separate buffer. This allows for more complex effect compositions and can
improve performance for certain types of effects.
- `fx::prolong_start`: extends the start of an effect by a specified duration.
- `fx::prolong_end`: extends the end of an effect by a specified duration.
- `fx::translate_buf()`: translates the contents of an auxiliary buffer onto the main buffer.
- `widget::EffectTimeline`: a widget for visualizing the composition of effects.
- `EffectTimeline::save_to_file()`: saves the effect timeline to a file.
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ The library includes a variety of effects, loosely categorized as follows:
- **consume_tick:** Consumes a single tick.
- **never_complete:** Makes an effect run indefinitely.
- **ping_pong:** Plays the effect forwards and then backwards.
- **prolong_start**: Extends the start of an effect by a specified duration.
- **prolong_end**: Extends the end of an effect by a specified duration.
- **repeat:** Repeats an effect indefinitely or for a specified number of times or duration.
- **repeating:** Repeats the effect indefinitely.
- **sleep:** Pauses for a specified duration.
Expand Down Expand Up @@ -123,6 +125,17 @@ cargo run --release --example=basic-effects
cargo run --release --example=open-window
```

### Example: fx-chart
![fx-chart](images/effect-timeline-widget.png)

A demo of the `EffectTimelineWidget` showcasing the composition of effects. The widget is a "plain" widget
without any effects as part of its rendering. The effects are instead applied after rendering the widget.

```
cargo run --release --example=open-window
```


[API Badge]: https://docs.rs/tachyonfx/badge.svg
[API]: https://docs.rs/tachyonfx
[Crate Badge]: https://img.shields.io/crates/v/tachyonfx.svg
Expand Down
2 changes: 1 addition & 1 deletion examples/basic-effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ impl EffectsRepository {
fx::slide_in(Direction::UpToDown, 20, Dark0Hard, medium),
]),
fx::sleep(medium),
fx::timed_never_complete(medium * 2,
fx::prolong_end(medium,
fx::slide_out(Direction::LeftToRight, 80, Dark0Hard, medium),
),
]))),
Expand Down
24 changes: 10 additions & 14 deletions examples/fx-chart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use ratatui::widgets::{Block, Clear, StatefulWidget, Widget};
use ratatui::Frame;

use tachyonfx::fx::{never_complete, parallel, sequence, with_duration, Direction};
use tachyonfx::widget::EffectTimeline;
use tachyonfx::widget::{EffectTimeline, EffectTimelineRects};
use tachyonfx::CellFilter::{AllOf, Inner, Not, Outer, Text};
use tachyonfx::{fx, BufferRenderer, CenteredShrink, Effect, EffectRenderer, Interpolation, Shader};
use Interpolation::*;
Expand All @@ -40,8 +40,8 @@ struct App {

#[derive(Default)]
struct Effects {
aux_buf_fx: Option<Effect>,
post_process: Vec<Effect>,
pub aux_buf_fx: Option<Effect>,
pub post_process: Vec<Effect>,
}

impl Effects {
Expand Down Expand Up @@ -126,7 +126,7 @@ mod effects {

sequence(vec![
timed_never_complete(out_duration + Duration::from_millis(500), out_fx),
// update_inspected_effect,
// todo: update_inspected_effect(),
fx_in,
])
}
Expand Down Expand Up @@ -194,10 +194,8 @@ mod effects {
let step = Duration::from_millis(100);
let bg = Color::Black;

sequence(vec![
timed_never_complete(step * 4, fade_to_fg(bg, 0)),
sweep_in(Direction::RightToLeft, 5, bg, step * 3),
]).with_area(area)
prolong_start(step * 4, sweep_in(Direction::RightToLeft, 5, bg, step * 3))
.with_area(area)
}

fn chart_fx_2(area: Rect) -> Effect {
Expand Down Expand Up @@ -238,10 +236,8 @@ mod effects {
parallel(vec![
sweep_in(Direction::DownToUp, 1, Color::Black, (base_delay, QuadOut))
.with_area(column_area),
sequence(vec![
timed_never_complete(base_delay, fade_to_fg(Color::Black, 0)),
fade_from_fg(Color::Black, (700, QuadOut))
]).with_area(legend_area),
prolong_start(base_delay * 5, fade_from_fg(Color::Black, (700, QuadOut)))
.with_area(legend_area),
])
}

Expand All @@ -254,7 +250,7 @@ mod effects {
Direction::DownToUp => Offset { x: 0, y: screen.height as i32 },
};

translate_buf(offset, buf.clone(), (450, CircIn)).reversed()
translate_buf(offset, buf.clone(), (750, CircIn)).reversed()
}
}

Expand Down Expand Up @@ -291,7 +287,7 @@ fn run_app(
let mut last_frame_instant = std::time::Instant::now();

let mut effects = Effects::default();
effects.push(effects::effect_in_1(app.timeline.layout(app.screen_area)));
effects.aux_buf_fx = Some(effects::move_in_fx(Direction::LeftToRight, app.aux_buffer.clone()));
app.refresh_aux_buffer();

loop {
Expand Down
70 changes: 70 additions & 0 deletions src/fx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use ratatui::style::Color;

pub use glitch::Glitch;
use ping_pong::PingPong;
use prolong::{Prolong, ProlongPosition};
pub use shader_fn::*;
use slide::SlideCell;
pub use sweep_in::Direction;
Expand Down Expand Up @@ -48,6 +49,7 @@ mod shader_fn;
mod slide;
mod moving_window;
mod offscreen_buffer;
mod prolong;

/// Creates a custom effect using a user-defined function.
///
Expand Down Expand Up @@ -565,6 +567,74 @@ pub fn delay<T: Into<EffectTimer>>(duration: T, effect: Effect) -> Effect {
sequence(vec![sleep(duration), effect])
}

/// Creates an effect that prolongs the start of another effect.
///
/// This function wraps the given effect with additional duration at its beginning.
/// The original effect will not progress until the additional duration has elapsed.
/// During this time, the wrapped effect will be processed with zero duration.
///
/// # Arguments
///
/// * `duration` - The additional duration to add before the effect starts. This can be
/// any type that can be converted into an `EffectTimer`.
/// * `effect` - The original effect to be prolonged.
///
/// # Returns
///
/// A new `Effect` that includes the additional duration at the start.
///
/// # Examples
///
/// ```
/// use std::time::Duration;
/// use ratatui::style::Color;
/// use tachyonfx::{Effect, fx, EffectTimer, Interpolation};
///
/// fx::prolong_start(500, // 500ms
/// fx::fade_from_fg(Color::Red, EffectTimer::from_ms(1000, Interpolation::Linear))
/// )
/// ```
/// This example creates an effect that waits for 500ms before starting a fade effect from red to
/// the original color over 1000ms. The total duration of this combined effect will be 1500ms.
pub fn prolong_start<T: Into<EffectTimer>>(duration: T, effect: Effect) -> Effect {
Prolong::new(ProlongPosition::Start, duration.into(), effect).into_effect()
}

/// Creates an effect that prolongs the end of another effect.
///
/// This function wraps the given effect with additional duration at its end.
/// The original effect will complete its normal progression, then the additional
/// duration will keep the effect in its final state for the specified time.
///
/// # Arguments
///
/// * `duration` - The additional duration to add after the effect completes. This can be
/// any type that can be converted into an `EffectTimer`.
/// * `effect` - The original effect to be prolonged.
///
/// # Returns
///
/// A new `Effect` that includes the additional duration at the end.
///
/// # Examples
///
/// ```
/// use std::time::Duration;
/// use ratatui::style::Color;
/// use tachyonfx::{Effect, fx, EffectTimer, Interpolation};
///
/// fx::prolong_end(500, // 500ms
/// fx::fade_to_fg(Color::Red, EffectTimer::from_ms(1000, Interpolation::Linear))
/// )
/// ```
///
/// This example creates an effect that fades the foreground color to red over 1000ms,
/// then holds the red color for an additional 500ms. The total duration of this combined
/// effect will be 1500ms.
pub fn prolong_end<T: Into<EffectTimer>>(duration: T, effect: Effect) -> Effect {
Prolong::new(ProlongPosition::End, duration.into(), effect).into_effect()
}

/// Creates an effect that consumes a single tick of processing time.
///
/// This function creates an effect that does nothing but mark itself as complete
Expand Down
113 changes: 113 additions & 0 deletions src/fx/prolong.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use std::time::Duration;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use crate::{CellFilter, CellIterator, Effect, EffectTimer, Shader};
use crate::Interpolation::Linear;
use crate::widget::EffectSpan;

/// Specifies the position where the additional duration should be applied in a `Prolong` effect.
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum ProlongPosition {
Start,
End,
}

#[derive(Clone)]
pub struct Prolong {
inner: Effect,
timer: EffectTimer,
position: ProlongPosition,
}

impl Prolong {
pub fn new(
position: ProlongPosition,
additional_duration: EffectTimer,
inner: Effect,
) -> Self {
Self {
inner,
timer: additional_duration,
position,
}
}
}

/// A shader that wraps an inner effect and prolongs its duration either at the start or end.
impl Shader for Prolong {
fn name(&self) -> &'static str {
match self.position {
ProlongPosition::Start => "prolong_start",
ProlongPosition::End => "prolong_end",
}
}

fn process(&mut self, duration: Duration, buf: &mut Buffer, area: Rect) -> Option<Duration> {
match self.position {
ProlongPosition::Start => {
let overflow = self.timer.process(duration);
self.inner.process(overflow.unwrap_or_default(), buf, area)
}
ProlongPosition::End => {
let overflow = self.inner.process(duration, buf, area);
self.timer.process(overflow?)
}
}
}

fn execute(&mut self, alpha: f32, area: Rect, cell_iter: CellIterator) {}

/// Checks if the prolonged effect is done.
///
/// # Returns
///
/// `true` if both the additional duration and inner effect are done, `false` otherwise.
fn done(&self) -> bool {
self.timer.done() && self.inner.done()
}

fn clone_box(&self) -> Box<dyn Shader> {
Box::new(self.clone())
}

fn area(&self) -> Option<Rect> {
self.inner.area()
}

fn set_area(&mut self, area: Rect) {
self.inner.set_area(area);
}

fn set_cell_selection(&mut self, strategy: CellFilter) {
self.inner.set_cell_selection(strategy);
}

/// Returns the total duration of the prolonged effect.
///
/// # Returns
///
/// An `EffectTimer` representing the sum of the additional duration and the inner effect's duration.
fn timer(&self) -> Option<EffectTimer> {
let self_duration = self.timer.duration();
let inner_duration = self.inner.timer().unwrap_or_default().duration();

Some(EffectTimer::new(self_duration + inner_duration, Linear))
}

fn cell_selection(&self) -> Option<CellFilter> {
self.inner.cell_selection()
}

fn as_effect_span(&self, offset: Duration) -> EffectSpan {
let inner_offset = match self.position {
ProlongPosition::Start => offset + self.inner.timer().unwrap_or_default().duration(),
ProlongPosition::End => offset
};
EffectSpan::new(self, offset, vec![self.inner.as_effect_span(inner_offset)])
}

fn reset(&mut self) {
self.timer.reset();
self.inner.reset();
}
}
2 changes: 1 addition & 1 deletion src/widget/effect_chart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ fn as_background_area_line(bar: &str, base_color: Color) -> Line<'static> {
}
}

#[derive(Clone, Copy)]
#[derive(Clone, Copy, Default)]
pub struct EffectTimelineRects {
pub widget: Rect, // all
pub tree: Rect,
Expand Down

0 comments on commit ce092d1

Please sign in to comment.