Skip to main content

tachyonfx/
effect.rs

1use alloc::boxed::Box;
2use core::fmt::Debug;
3
4use ratatui_core::{buffer::Buffer, layout::Rect};
5
6use crate::{
7    pattern::AnyPattern, shader::Shader, widget::EffectSpan, CellFilter, ColorSpace, Duration,
8    EffectTimer, SimpleRng,
9};
10
11/// Represents an effect that can be applied to terminal cells.
12/// The `Effect` struct wraps a shader, allowing it to be configured
13/// and applied to a specified area and cell selection.
14#[derive(Debug)]
15pub struct Effect {
16    shader: Box<dyn Shader>,
17}
18
19impl Effect {
20    /// Creates a new `Effect` with the specified shader.
21    ///
22    /// # Arguments
23    /// * `shader` - The shader to be used for the effect. It must implement the `Shader`
24    ///   trait and have a static lifetime.
25    ///
26    /// # Returns
27    /// * A new `Effect` instance.
28    pub fn new<S>(shader: S) -> Self
29    where
30        S: Shader + 'static,
31    {
32        Self { shader: Box::new(shader) }
33    }
34
35    /// Creates a new `Effect` with the specified area.
36    ///
37    /// # Arguments
38    /// * `area` - The rectangular area where the effect will be applied.
39    ///
40    /// # Returns
41    /// * A new `Effect` instance with the specified area.
42    ///
43    /// # Example
44    /// ```
45    /// use tachyonfx::{Effect, EffectTimer, fx, Interpolation};
46    /// use ratatui_core::layout::Rect;
47    ///
48    /// fx::dissolve(EffectTimer::from_ms(120, Interpolation::CircInOut))
49    ///     .with_area(Rect::new(0, 0, 10, 10));
50    /// ```
51    pub fn with_area(mut self, area: Rect) -> Self {
52        self.shader.set_area(area);
53        self
54    }
55
56    /// Creates a new `Effect` with the specified cell filter.
57    ///
58    /// # Arguments
59    /// * `mode` - The cell filter to be used for the effect.
60    ///
61    /// # Returns
62    /// * A new `Effect` instance with the specified filter.
63    ///
64    /// # Notes
65    /// This method only applies the filter if the effect doesn't already have a filter
66    /// set, preserving any existing filters during effect composition.
67    ///
68    /// # Example
69    /// ```
70    /// use ratatui_core::style::Color;
71    /// use tachyonfx::{Effect, CellFilter, fx, Interpolation};
72    /// use tachyonfx::color_from_hsl;
73    ///
74    /// let color = color_from_hsl(180.0, 85.0, 62.0);
75    /// let shader = fx::fade_to_fg(color, (300, Interpolation::SineIn))
76    ///     .with_filter(CellFilter::Text);
77    /// ```
78    pub fn with_filter(mut self, mode: CellFilter) -> Self {
79        self.filter(mode);
80        self
81    }
82
83    #[deprecated(since = "0.11.0", note = "Use `with_filter` instead")]
84    pub fn with_cell_selection(&self, mode: CellFilter) -> Self {
85        self.clone().with_filter(mode)
86    }
87
88    /// Gets the current color space used for color interpolation.
89    /// Returns the default color space (HSL) if not supported.
90    ///
91    /// # Returns
92    /// * The `ColorSpace` currently in use by this effect.
93    pub fn color_space(&self) -> ColorSpace {
94        self.shader.color_space()
95    }
96
97    /// Sets the color space used for color interpolation.
98    ///
99    /// # Arguments
100    /// * `color_space` - The color space to use for color interpolation.
101    pub fn set_color_space(&mut self, color_space: ColorSpace) {
102        self.shader.set_color_space(color_space);
103    }
104
105    /// Sets the color space for color interpolation in the effect.
106    ///
107    /// This method controls how colors are blended during transitions, which can
108    /// significantly affect the visual appearance of the animation. Different color
109    /// spaces produce different intermediate colors during transitions.
110    ///
111    /// # Color Spaces
112    ///
113    /// - [`ColorSpace::Rgb`](crate::ColorSpace::Rgb) - Linear RGB interpolation (fastest,
114    ///   but can produce muddy colors)
115    /// - [`ColorSpace::Hsl`](crate::ColorSpace::Hsl) - HSL interpolation (default, smooth
116    ///   hue transitions)
117    /// - [`ColorSpace::Hsv`](crate::ColorSpace::Hsv) - HSV interpolation (vibrant,
118    ///   perceptually uniform)
119    ///
120    /// # Supported Effects
121    ///
122    /// The following effects support `with_color_space()`:
123    /// - [`fx::fade_to`](crate::fx::fade_to), [`fx::fade_from`](crate::fx::fade_from) -
124    ///   Controls foreground and background color transitions
125    /// - [`fx::fade_to_fg`](crate::fx::fade_to_fg),
126    ///   [`fx::fade_from_fg`](crate::fx::fade_from_fg) - Controls foreground color
127    ///   transitions
128    /// - [`fx::sweep_in`](crate::fx::sweep_in), [`fx::sweep_out`](crate::fx::sweep_out) -
129    ///   Controls color fading during sweep
130    ///
131    /// For effects that don't use color interpolation, this is a no-op.
132    ///
133    /// # Examples
134    ///
135    /// ```no_run
136    /// use ratatui_core::style::Color;
137    /// use tachyonfx::{ColorSpace, fx, Interpolation};
138    ///
139    /// let effect = fx::fade_to_fg(Color::Red, (300, Interpolation::SineIn))
140    ///     .with_color_space(ColorSpace::Rgb);
141    /// ```
142    pub fn with_color_space(mut self, color_space: ColorSpace) -> Self {
143        self.set_color_space(color_space);
144        self
145    }
146
147    /// Sets the random number generator for the effect, enabling reproducible animations.
148    ///
149    /// This method allows you to control the randomness in effects that use random
150    /// number generation, making it possible to create deterministic, reproducible
151    /// animations by seeding the RNG with a fixed value.
152    ///
153    /// # Supported Effects
154    ///
155    /// The following effects support `with_rng()`:
156    /// - [`fx::glitch`](crate::fx::glitch) - Controls random cell selection and glitch
157    ///   types
158    /// - [`fx::dissolve`](crate::fx::dissolve),
159    ///   [`fx::dissolve_to`](crate::fx::dissolve_to),
160    ///   [`fx::coalesce`](crate::fx::coalesce),
161    ///   [`fx::coalesce_from`](crate::fx::coalesce_from) - Controls random cell
162    ///   thresholds
163    /// - [`fx::explode`](crate::fx::explode) - Controls explosion forces and trajectories
164    /// - [`fx::slide_in`](crate::fx::slide_in), [`fx::slide_out`](crate::fx::slide_out) -
165    ///   Controls random positional variance
166    /// - [`fx::sweep_in`](crate::fx::sweep_in), [`fx::sweep_out`](crate::fx::sweep_out) -
167    ///   Controls random positional variance
168    ///
169    /// For effects that don't use randomness, this is a no-op.
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use tachyonfx::{fx, SimpleRng, Effect};
175    ///
176    /// // Create two effects with the same seed for reproducible behavior
177    /// let effect1 = fx::dissolve(1000).with_rng(SimpleRng::new(42));
178    /// let effect2 = fx::dissolve(1000).with_rng(SimpleRng::new(42));
179    /// // effect1 and effect2 will dissolve cells in the exact same pattern
180    /// ```
181    pub fn with_rng(mut self, rng: SimpleRng) -> Self {
182        self.shader.set_rng(rng);
183        self
184    }
185
186    /// Creates a new `Effect` with the shader's reverse flag toggled.
187    ///
188    /// # Returns
189    /// * A new `Effect` instance with the shader's reverse flag toggled.
190    pub fn reversed(mut self) -> Self {
191        self.reverse();
192        self
193    }
194}
195
196impl Clone for Effect {
197    fn clone(&self) -> Self {
198        Self { shader: self.shader.clone_box() }
199    }
200}
201
202impl Effect {
203    /// Returns the name of the underlying shader.
204    ///
205    /// # Returns
206    /// * The name of the shader as a static string.
207    pub fn name(&self) -> &'static str {
208        self.shader.name()
209    }
210
211    /// Processes the effect for the given duration. This:
212    /// 1. Updates the shader's timer with the given duration
213    /// 2. Executes the shader effect
214    /// 3. Returns any overflow duration
215    ///
216    /// # Arguments
217    /// * `duration` - The duration to process the effect for.
218    /// * `buf` - A mutable reference to the `Buffer` where the effect will be applied.
219    /// * `area` - The rectangular area within the buffer where the effect will be
220    ///   applied. If the effect has its own area set, that takes precedence.
221    ///
222    /// # Returns
223    /// * An `Option` containing the overflow duration if the effect is done, or `None` if
224    ///   it is still running.
225    ///
226    /// # Example
227    /// ```no_run
228    /// use ratatui_core::buffer::Buffer;
229    /// use ratatui_core::layout::Rect;
230    /// use tachyonfx::{Effect, fx, Interpolation, Duration};
231    ///
232    /// let mut effect = fx::dissolve((100, Interpolation::Linear));
233    /// let area = Rect::new(0, 0, 10, 10);
234    /// let mut buffer = Buffer::empty(area);
235    /// let overflow = effect.process(Duration::from_millis(50), &mut buffer, area);
236    /// ```
237    pub fn process(
238        &mut self,
239        duration: Duration,
240        buf: &mut Buffer,
241        area: Rect,
242    ) -> Option<Duration> {
243        let area = self.shader.area().unwrap_or(area);
244        if let Some(processor) = self.shader.filter_processor_mut() {
245            processor.update(buf, area);
246        }
247
248        self.shader.process(duration, buf, area)
249    }
250
251    /// Returns true if the effect is done.
252    ///
253    /// # Returns
254    /// * `true` if the effect is done, `false` otherwise.
255    pub fn done(&self) -> bool {
256        self.shader.done()
257    }
258
259    /// Returns true if the effect is still running.
260    ///
261    /// # Returns
262    /// * `true` if the effect is running, `false` otherwise.
263    pub fn running(&self) -> bool {
264        self.shader.running()
265    }
266
267    /// Returns the area where the effect is applied.
268    ///
269    /// # Returns
270    /// * An `Option` containing the rectangular area if set, or `None` if not set.
271    pub fn area(&self) -> Option<Rect> {
272        self.shader.area()
273    }
274
275    /// Sets the area where the effect will be applied.
276    ///
277    /// # Arguments
278    /// * `area` - The rectangular area to set.
279    pub fn set_area(&mut self, area: Rect) {
280        self.shader.set_area(area);
281    }
282
283    /// Sets the cell selection strategy for the effect. Only applies the filter
284    /// if the effect doesn't already have one set.
285    ///
286    /// # Arguments
287    /// * `strategy` - The cell filter strategy to set.
288    ///
289    /// # Example
290    /// ```no_run
291    /// use tachyonfx::{CellFilter, fx, Interpolation};
292    ///
293    /// let mut effect = fx::dissolve((100, Interpolation::Linear));
294    /// effect.filter(CellFilter::Not(CellFilter::Text.into()));
295    /// ```
296    pub fn filter(&mut self, strategy: CellFilter) {
297        self.shader.propagate_filter(strategy);
298    }
299
300    /// Reverses the effect's playback direction.
301    ///
302    /// This toggles the effect to play in the opposite direction from its
303    /// current state. Can be used to create back-and-forth animations.
304    pub fn reverse(&mut self) {
305        self.shader.reverse();
306    }
307
308    /// Returns the timer associated with this effect.
309    ///
310    /// This method provides information about the duration and timing of the effect,
311    /// useful for effect composition and synchronization.
312    ///
313    /// # Returns
314    /// An `Option<EffectTimer>`:
315    /// - `Some(EffectTimer)` if the effect has an associated timer.
316    /// - `None` if the effect doesn't have a specific duration (e.g., for indefinite
317    ///   effects).
318    ///
319    /// # Notes
320    /// - For composite effects (like parallel or sequential effects), this may return an
321    ///   approximation of the total duration based on the timers of child effects.
322    /// - Some effects may modify the returned timer to reflect their specific behavior
323    ///   (e.g., a ping-pong effect might double the duration).
324    /// - The returned timer should reflect the total expected duration of the effect,
325    ///   which may differ from the current remaining time.
326    pub fn timer(&self) -> Option<EffectTimer> {
327        self.shader.timer()
328    }
329
330    /// Returns a mutable reference to the effect's timer, if any.
331    ///
332    /// # Returns
333    /// * An `Option` containing a mutable reference to the effect's `EffectTimer`, or
334    ///   `None` if not applicable.
335    ///
336    /// # Example
337    /// ```no_run
338    /// use tachyonfx::{fx, Interpolation};
339    ///
340    /// let mut effect = fx::dissolve((100, Interpolation::Linear));
341    /// if let Some(timer) = effect.timer_mut() {
342    ///     timer.reset();
343    /// }
344    /// ```
345    pub fn timer_mut(&mut self) -> Option<&mut EffectTimer> {
346        self.shader.timer_mut()
347    }
348
349    /// Returns the cell selection strategy for the effect, if any.
350    ///
351    /// # Returns
352    /// * An `Option` containing the effect's `CellFilter`, or `None` if not applicable.
353    pub fn cell_filter(&self) -> Option<&CellFilter> {
354        self.shader.cell_filter()
355    }
356
357    /// Resets the effect. Used by effects like ping_pong and repeat to reset
358    /// the hosted effect to its initial state.
359    pub fn reset(&mut self) {
360        self.shader.reset();
361    }
362
363    /// Sets a pattern for spatial alpha progression on pattern-compatible effects.
364    /// This is a no-op for effects that don't support patterns.
365    ///
366    /// # Arguments
367    /// * `pattern` - An AnyPattern enum containing the pattern to apply
368    pub(crate) fn set_pattern(&mut self, pattern: AnyPattern) {
369        self.shader.set_pattern(pattern);
370    }
371
372    /// Applies a pattern to this effect for spatial alpha progression.
373    /// This is a no-op for effects that don't support patterns.
374    ///
375    /// # Arguments
376    /// * `pattern` - A pattern that implements `Into<AnyPattern>`
377    ///
378    /// # Returns
379    /// * The same effect with the pattern applied (if supported)
380    ///
381    /// # Example
382    /// ```
383    /// use tachyonfx::{fx, pattern};
384    ///
385    /// let effect = fx::dissolve(1000)
386    ///     .with_pattern(pattern::RadialPattern::center());
387    /// ```
388    pub fn with_pattern<P>(mut self, pattern: P) -> Self
389    where
390        P: Into<AnyPattern>,
391    {
392        let any_pattern = pattern.into();
393        self.set_pattern(any_pattern);
394        self
395    }
396
397    /// Creates an `EffectSpan` representation of this effect.
398    ///
399    /// # Deprecation
400    ///
401    /// This method was used by the now-removed `EffectTimeline` widget and no longer
402    /// serves any purpose. It is deprecated and scheduled for removal in a future
403    /// release.
404    #[deprecated(since = "0.23.0", note = "EffectSpan is being removed")]
405    #[allow(deprecated)]
406    pub fn as_effect_span(&self, offset: Duration) -> EffectSpan
407    where
408        Self: Sized + Clone,
409    {
410        self.shader.as_ref().as_effect_span(offset)
411    }
412
413    /// Attempts to convert this effect to a DSL effect expression.
414    ///
415    /// # Returns
416    ///
417    /// Returns a `Result` containing either:
418    /// - `Ok(EffectExpression)` if conversion is successful
419    /// - `Err(DslError::EffectExpressionNotSupported)` containing the effect name if this
420    ///   effect type doesn't support conversion to the DSL format
421    ///
422    /// # Errors
423    ///
424    /// This method returns an error if the underlying shader doesn't support DSL
425    /// conversion.
426    #[cfg(feature = "dsl")]
427    pub fn to_dsl(&self) -> Result<crate::dsl::EffectExpression, crate::dsl::DslError> {
428        self.shader.to_dsl()
429    }
430}
431
432/// Trait for converting shader types into Effect instances.
433pub trait IntoEffect {
434    /// Converts this shader into an Effect.
435    fn into_effect(self) -> Effect;
436}
437
438impl<S> IntoEffect for S
439where
440    S: Shader + 'static,
441{
442    fn into_effect(self) -> Effect {
443        Effect::new(self)
444    }
445}
446
447/// Extension trait for shader filter propagation logic.
448pub(crate) trait ShaderExt {
449    /// Propagates the cell filter to the shader if it is not already set.
450    ///
451    /// This method only applies the filter if the shader doesn't already have one,
452    /// preserving existing filters during effect composition.
453    fn propagate_filter(&mut self, cell_filter: CellFilter);
454}
455
456impl<S: Shader + 'static> ShaderExt for S {
457    fn propagate_filter(&mut self, cell_filter: CellFilter) {
458        if self.cell_filter().is_none() {
459            self.filter(cell_filter);
460        }
461    }
462}
463
464impl ShaderExt for dyn Shader {
465    fn propagate_filter(&mut self, cell_filter: CellFilter) {
466        if self.cell_filter().is_none() {
467            self.filter(cell_filter);
468        }
469    }
470}
471
472// PatternedEffect is no longer needed since Effect now has .with_pattern() directly