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