tui_banner/
gradient.rs

1// Copyright (c) 2025 Lei Zhang
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
13use crate::color::{Color, Palette};
14use crate::grid::Grid;
15
16/// Gradient definition for coloring a grid.
17#[derive(Clone, Debug)]
18pub struct Gradient {
19    stops: Vec<Color>,
20    direction: GradientDirection,
21}
22
23/// Gradient direction.
24#[derive(Clone, Copy, Debug)]
25pub enum GradientDirection {
26    /// Top to bottom.
27    Vertical,
28    /// Left to right.
29    Horizontal,
30    /// Top-left to bottom-right.
31    Diagonal,
32}
33
34impl Gradient {
35    /// Create a gradient from color stops and direction.
36    pub fn new(stops: Vec<Color>, direction: GradientDirection) -> Self {
37        Self { stops, direction }
38    }
39
40    /// Vertical gradient (top -> bottom).
41    pub fn vertical(palette: Palette) -> Self {
42        Self::new(palette.colors().to_vec(), GradientDirection::Vertical)
43    }
44
45    /// Horizontal gradient (left -> right).
46    pub fn horizontal(palette: Palette) -> Self {
47        Self::new(palette.colors().to_vec(), GradientDirection::Horizontal)
48    }
49
50    /// Diagonal gradient (top-left -> bottom-right).
51    pub fn diagonal(palette: Palette) -> Self {
52        Self::new(palette.colors().to_vec(), GradientDirection::Diagonal)
53    }
54
55    /// Apply the gradient to a grid in-place.
56    pub fn apply(&self, grid: &mut Grid) {
57        if self.stops.is_empty() {
58            return;
59        }
60
61        let height = grid.height().max(1);
62        let width = grid.width().max(1);
63
64        for r in 0..height {
65            for c in 0..width {
66                let t = match self.direction {
67                    GradientDirection::Vertical => {
68                        if height <= 1 {
69                            0.0
70                        } else {
71                            r as f32 / (height - 1) as f32
72                        }
73                    }
74                    GradientDirection::Horizontal => {
75                        if width <= 1 {
76                            0.0
77                        } else {
78                            c as f32 / (width - 1) as f32
79                        }
80                    }
81                    GradientDirection::Diagonal => {
82                        if width + height <= 2 {
83                            0.0
84                        } else {
85                            (r + c) as f32 / (width + height - 2) as f32
86                        }
87                    }
88                };
89
90                if let Some(cell) = grid.cell_mut(r, c)
91                    && cell.visible
92                {
93                    cell.fg = Some(color_at(&self.stops, t));
94                }
95            }
96        }
97    }
98}
99
100fn color_at(stops: &[Color], t: f32) -> Color {
101    if stops.len() == 1 {
102        return stops[0];
103    }
104
105    let t = t.clamp(0.0, 1.0);
106    let max_index = stops.len() - 1;
107    let scaled = t * max_index as f32;
108    let idx = scaled.floor() as usize;
109    let next = idx.min(max_index - 1) + 1;
110    let local_t = scaled - idx as f32;
111
112    stops[idx].lerp(stops[next], local_t)
113}