tachyonfx 0.25.0

A ratatui library for creating shader-like effects in TUIs.
Documentation
use alloc::boxed::Box;

use bon::Builder;
use ratatui_core::{buffer::Buffer, layout::Rect, style::Color};

use crate::{
    cell_filter::FilterProcessor,
    color_space::color_from_hsl,
    color_to_hsl, default_shader_impl,
    effect_timer::EffectTimer,
    pattern::{AnyPattern, InstancedPattern, Pattern},
    shader::Shader,
    CellFilter, ColorCache, Duration, Interpolatable,
};

#[derive(Builder, Clone, Default, Debug)]
pub(super) struct HslShift {
    #[builder(into)]
    timer: EffectTimer,
    hsl_mod_fg: Option<[f32; 3]>,
    hsl_mod_bg: Option<[f32; 3]>,
    area: Option<Rect>,
    cell_filter: Option<FilterProcessor>,
    #[builder(default)]
    pattern: AnyPattern,
}

impl Shader for HslShift {
    default_shader_impl!(area, timer, filter, clone);

    fn name(&self) -> &'static str {
        "hsl_shift"
    }

    fn execute(&mut self, _: Duration, area: Rect, buf: &mut Buffer) {
        let global_alpha = self.timer.alpha();

        let hsl_lerp = |c: &Color, hsl: [f32; 3], alpha: f32| -> Color {
            let (h, s, l) = color_to_hsl(c);

            let (h, s, l) = (
                ((h + 0.0.lerp(&hsl[0], alpha)) % 360.0 + 360.0) % 360.0,
                (s + 0.0.lerp(&hsl[1], alpha)).clamp(0.0, 100.0),
                (l + 0.0.lerp(&hsl[2], alpha)).clamp(0.0, 100.0),
            );

            color_from_hsl(h, s, l)
        };

        let hsl_mod_fg = self.hsl_mod_fg;
        let hsl_mod_bg = self.hsl_mod_bg;

        let mut pattern = self.pattern.clone().for_frame(global_alpha, area);

        let cell_iter = self.cell_iter(buf, area);
        let mut color_cache: ColorCache<u32, 8> = ColorCache::new();

        cell_iter.for_each_cell(|pos, cell| {
            if let Some(hsl_mod) = hsl_mod_fg {
                let alpha = pattern.map_alpha(pos);
                let alpha_bits = u32::from_le_bytes(alpha.to_le_bytes());
                let fg =
                    color_cache.memoize_fg(cell.fg, alpha_bits, |c| hsl_lerp(c, hsl_mod, alpha));
                cell.set_fg(fg);
            }
            if let Some(hsl_mod) = hsl_mod_bg {
                let alpha = pattern.map_alpha(pos);
                let alpha_bits = u32::from_le_bytes(alpha.to_le_bytes());
                let bg =
                    color_cache.memoize_bg(cell.bg, alpha_bits, |c| hsl_lerp(c, hsl_mod, alpha));
                cell.set_bg(bg);
            }
        });
    }

    fn set_pattern(&mut self, pattern: AnyPattern) {
        self.pattern = pattern;
    }

    #[cfg(feature = "dsl")]
    fn to_dsl(&self) -> Result<crate::dsl::EffectExpression, crate::dsl::DslError> {
        use crate::dsl::{DslFormat, EffectExpression};

        let hsl_mod_fg = self.hsl_mod_fg.map_or("None".to_string(), |hsl| {
            format!("Some([{}, {}, {}])", hsl[0], hsl[1], hsl[2])
        });

        let hsl_mod_bg = self.hsl_mod_bg.map_or("None".to_string(), |hsl| {
            format!("Some([{}, {}, {}])", hsl[0], hsl[1], hsl[2])
        });

        EffectExpression::parse(&format!(
            "{}({hsl_mod_fg}, {hsl_mod_bg}, {})",
            self.name(),
            self.timer.dsl_format(),
        ))
    }
}

#[cfg(test)]
#[cfg(feature = "dsl")]
mod tests {
    use indoc::indoc;

    use crate::{
        dsl::{EffectDsl, EffectExpression},
        fx, Effect,
        Interpolation::Linear,
    };

    #[test]
    fn hsl_shift() {
        let input = "fx::hsl_shift(Some([1.0, 2.0, 3.0]), Some([1.0, 2.0, 3.0]), (1000, Linear))";
        let expected = fx::hsl_shift(Some([1.0, 2.0, 3.0]), Some([1.0, 2.0, 3.0]), (1000, Linear));
        let result = compile_effect(input);
        assert_eq!(format!("{result:?}"), format!("{expected:?}"));
    }

    #[test]
    fn test_hsl_shift_fg() {
        let input = "fx::hsl_shift_fg([1.0, 2.0, 3.0], (1000, Linear))";
        let expected = fx::hsl_shift_fg([1.0, 2.0, 3.0], (1000, Linear));
        let result = compile_effect(input);
        assert_eq!(format!("{result:?}"), format!("{expected:?}"));
    }

    #[test]
    fn test_expr_to_dsl() {
        let input = "fx::hsl_shift(Some([1.0, 2.0, 3.0]), Some([1.0, 2.0, 3.0]), (1000, Linear))";
        let result = EffectExpression::parse(input).unwrap();
        assert_eq!(format!("{result}"), indoc! {
            "fx::hsl_shift(
                Some([1.0, 2.0, 3.0]),
                Some([1.0, 2.0, 3.0]),
                (1000, Interpolation::Linear)
            )"
        });
    }

    fn compile_effect(input: &str) -> Effect {
        EffectDsl::new()
            .compiler()
            .compile(input)
            .unwrap()
    }
}