use tui_vfx_types::{Cell, Color, Grid, Rect};
use crate::types::ShadowConfig;
pub struct GradientRenderer;
impl GradientRenderer {
pub fn render_with_colors<G: Grid>(
grid: &mut G,
element_rect: Rect,
config: &ShadowConfig,
colors: &[Color],
progress: f64,
) {
if colors.is_empty() || progress <= 0.0 {
return;
}
let layers = colors.len();
let rect_x = element_rect.x as i32;
let rect_y = element_rect.y as i32;
let rect_w = element_rect.width as i32;
let rect_h = element_rect.height as i32;
let ox = config.offset_x as i32;
let oy = config.offset_y as i32;
let edges = config.edges;
for (layer_idx, color) in colors.iter().enumerate().rev() {
let layer_mult = (layers - layer_idx) as i32;
let layer_ox = ox * layer_mult;
let layer_oy = oy * layer_mult;
let layer_color = if progress < 1.0 {
let alpha = (color.a as f64 * progress).round() as u8;
color.with_alpha(alpha)
} else {
*color
};
Self::render_layer(
grid,
rect_x,
rect_y,
rect_w,
rect_h,
layer_ox,
layer_oy,
edges,
layer_color,
);
}
}
pub fn render<G: Grid>(
grid: &mut G,
element_rect: Rect,
config: &ShadowConfig,
layers: u8,
progress: f64,
) {
let base_color = config.color_at_progress(progress);
if base_color.a == 0 {
return;
}
let layers = layers.clamp(1, 4) as usize;
let rect_x = element_rect.x as i32;
let rect_y = element_rect.y as i32;
let rect_w = element_rect.width as i32;
let rect_h = element_rect.height as i32;
let ox = config.offset_x as i32;
let oy = config.offset_y as i32;
let edges = config.edges;
for layer in (0..layers).rev() {
let layer_mult = (layer + 1) as i32;
let layer_ox = ox * layer_mult;
let layer_oy = oy * layer_mult;
let intensity = 1.0 - (layer as f32 / layers as f32);
let layer_alpha = (base_color.a as f32 * intensity).round() as u8;
let layer_color = base_color.with_alpha(layer_alpha);
Self::render_layer(
grid,
rect_x,
rect_y,
rect_w,
rect_h,
layer_ox,
layer_oy,
edges,
layer_color,
);
}
}
#[allow(clippy::too_many_arguments)]
fn render_layer<G: Grid>(
grid: &mut G,
rect_x: i32,
rect_y: i32,
rect_w: i32,
rect_h: i32,
ox: i32,
oy: i32,
edges: crate::types::ShadowEdges,
color: Color,
) {
if color.a == 0 {
return;
}
let cell = Cell::new(' ').with_bg(color).with_mod_alpha(Some(255));
if edges.has_right() && ox > 0 {
let start_x = (rect_x + rect_w).max(0) as usize;
let end_x = (rect_x + rect_w + ox).max(0) as usize;
let start_y = (rect_y + oy.max(0) + 1).max(0) as usize;
let end_y = (rect_y + rect_h + oy.min(0)).max(0) as usize;
Self::fill_region(
grid,
start_x,
start_y,
end_x.saturating_sub(start_x),
end_y.saturating_sub(start_y),
cell,
);
}
if edges.has_bottom() && oy > 0 {
let start_x = (rect_x + ox.max(0) + 1).max(0) as usize;
let end_x = (rect_x + rect_w + ox.min(0)).max(0) as usize;
let start_y = (rect_y + rect_h).max(0) as usize;
let end_y = (rect_y + rect_h + oy).max(0) as usize;
Self::fill_region(
grid,
start_x,
start_y,
end_x.saturating_sub(start_x),
end_y.saturating_sub(start_y),
cell,
);
}
if edges.has_left() && ox < 0 {
let start_x = (rect_x + ox).max(0) as usize;
let end_x = rect_x.max(0) as usize;
let start_y = (rect_y + oy.max(0)).max(0) as usize;
let end_y = (rect_y + rect_h + oy.min(0)).max(0) as usize;
Self::fill_region(
grid,
start_x,
start_y,
end_x.saturating_sub(start_x),
end_y.saturating_sub(start_y),
cell,
);
}
if edges.has_top() && oy < 0 {
let start_x = (rect_x + ox.max(0)).max(0) as usize;
let end_x = (rect_x + rect_w + ox.min(0)).max(0) as usize;
let start_y = (rect_y + oy).max(0) as usize;
let end_y = rect_y.max(0) as usize;
Self::fill_region(
grid,
start_x,
start_y,
end_x.saturating_sub(start_x),
end_y.saturating_sub(start_y),
cell,
);
}
if edges.has_right() && edges.has_bottom() && ox > 0 && oy > 0 {
let start_x = (rect_x + rect_w).max(0) as usize;
let start_y = (rect_y + rect_h).max(0) as usize;
Self::fill_region(grid, start_x, start_y, ox as usize, oy as usize, cell);
}
if edges.has_left() && edges.has_top() && ox < 0 && oy < 0 {
let start_x = (rect_x + ox).max(0) as usize;
let start_y = (rect_y + oy).max(0) as usize;
Self::fill_region(grid, start_x, start_y, (-ox) as usize, (-oy) as usize, cell);
}
}
fn fill_region<G: Grid>(grid: &mut G, x: usize, y: usize, w: usize, h: usize, cell: Cell) {
for dy in 0..h {
for dx in 0..w {
let px = x + dx;
let py = y + dy;
if grid.in_bounds(px, py) {
grid.set(px, py, cell);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ShadowEdges;
use tui_vfx_types::OwnedGrid;
#[test]
fn test_render_single_layer() {
let mut grid = OwnedGrid::new(30, 15);
let rect = Rect::new(5, 2, 10, 6);
let config = ShadowConfig::new(Color::BLACK.with_alpha(200))
.with_offset(1, 1)
.with_edges(ShadowEdges::BOTTOM_RIGHT);
GradientRenderer::render(&mut grid, rect, &config, 1, 1.0);
let cell = grid.get(15, 4).unwrap();
assert_ne!(cell.bg, Color::TRANSPARENT);
}
#[test]
fn test_render_multiple_layers() {
let mut grid = OwnedGrid::new(30, 15);
let rect = Rect::new(5, 2, 10, 6);
let config = ShadowConfig::new(Color::BLACK.with_alpha(200))
.with_offset(1, 1)
.with_edges(ShadowEdges::BOTTOM_RIGHT);
GradientRenderer::render(&mut grid, rect, &config, 3, 1.0);
let outer_cell = grid.get(17, 9).unwrap(); let inner_cell = grid.get(15, 8).unwrap();
assert_ne!(outer_cell.bg, Color::TRANSPARENT);
assert_ne!(inner_cell.bg, Color::TRANSPARENT);
}
#[test]
fn test_zero_progress_renders_nothing() {
let mut grid = OwnedGrid::new(30, 15);
let rect = Rect::new(5, 2, 10, 6);
let config = ShadowConfig::new(Color::BLACK);
GradientRenderer::render(&mut grid, rect, &config, 3, 0.0);
for y in 0..15 {
for x in 0..30 {
let cell = grid.get(x, y).unwrap();
assert_eq!(cell.bg, Color::TRANSPARENT);
}
}
}
}