use std::collections::HashMap;
use crate::clip::ClipRect;
use crate::framebuffer::{pack_rgba, Framebuffer};
use oxiui_core::Color;
#[derive(Debug, Default)]
pub struct GaussianCache {
kernels: HashMap<u32, Vec<f32>>,
}
impl GaussianCache {
pub fn new() -> Self {
Self::default()
}
pub fn kernel(&mut self, blur_radius: f32) -> &[f32] {
let key = blur_radius.to_bits();
self.kernels
.entry(key)
.or_insert_with(|| gaussian_kernel(blur_radius))
}
}
pub fn gaussian_kernel(blur_radius: f32) -> Vec<f32> {
let sigma = (blur_radius / 2.0).max(0.01);
let radius = (sigma * 3.0).ceil() as usize;
let mut weights: Vec<f32> = (0..=radius)
.map(|i| {
let x = i as f32;
(-x * x / (2.0 * sigma * sigma)).exp()
})
.collect();
let sum: f32 = weights[0] + 2.0 * weights[1..].iter().sum::<f32>();
for w in &mut weights {
*w /= sum;
}
weights
}
pub fn gaussian_blur_alpha(alpha: &mut [f32], width: usize, height: usize, kernel: &[f32]) {
if width == 0 || height == 0 || kernel.is_empty() {
return;
}
let mut tmp = vec![0.0f32; width * height];
for y in 0..height {
for x in 0..width {
let mut acc = alpha[y * width + x] * kernel[0];
for (ki, &w) in kernel.iter().enumerate().skip(1) {
let xl = x as i64 - ki as i64;
if xl >= 0 {
acc += alpha[y * width + xl as usize] * w;
}
let xr = x as i64 + ki as i64;
if xr < width as i64 {
acc += alpha[y * width + xr as usize] * w;
}
}
tmp[y * width + x] = acc;
}
}
for y in 0..height {
for x in 0..width {
let mut acc = tmp[y * width + x] * kernel[0];
for (ki, &w) in kernel.iter().enumerate().skip(1) {
let ya = y as i64 - ki as i64;
if ya >= 0 {
acc += tmp[ya as usize * width + x] * w;
}
let yb = y as i64 + ki as i64;
if yb < height as i64 {
acc += tmp[yb as usize * width + x] * w;
}
}
alpha[y * width + x] = acc.clamp(0.0, 1.0);
}
}
}
pub fn box_shadow(
fb: &mut Framebuffer,
rect: (f32, f32, f32, f32),
offset_x: f32,
offset_y: f32,
blur_radius: f32,
color: Color,
cache: &mut GaussianCache,
) {
let (rx, ry, rw, rh) = rect;
if rw <= 0.0 || rh <= 0.0 {
return;
}
let br = blur_radius.max(0.0);
let kernel = cache.kernel(br).to_vec();
let sx = (rx + offset_x - br).floor() as i64;
let sy = (ry + offset_y - br).floor() as i64;
let sw = (rw + br * 2.0).ceil() as usize + 1;
let sh = (rh + br * 2.0).ceil() as usize + 1;
if sw == 0 || sh == 0 {
return;
}
let mut alpha = vec![0.0f32; sw * sh];
let inner_x0 = (br as usize).min(sw);
let inner_y0 = (br as usize).min(sh);
let inner_x1 = (inner_x0 + rw.ceil() as usize).min(sw);
let inner_y1 = (inner_y0 + rh.ceil() as usize).min(sh);
let shadow_a = color.3 as f32 / 255.0;
for row in inner_y0..inner_y1 {
for col in inner_x0..inner_x1 {
alpha[row * sw + col] = shadow_a;
}
}
if br > 0.0 {
gaussian_blur_alpha(&mut alpha, sw, sh, &kernel);
}
let Color(sr, sg, sb, base_a) = color;
let base_a_f = base_a as f32 / 255.0;
let clip = ClipRect::full(fb.width(), fb.height());
for row in 0..sh {
let fy = sy + row as i64;
if fy < clip.y0 || fy >= clip.y1 {
continue;
}
for col in 0..sw {
let fx = sx + col as i64;
if fx < clip.x0 || fx >= clip.x1 {
continue;
}
let a_val = alpha[row * sw + col];
if a_val <= 0.0 {
continue;
}
let effective_a = (a_val * base_a_f * 255.0).round() as u8;
if effective_a == 0 {
continue;
}
let px = fx as u32;
let py = fy as u32;
fb.blend(px, py, pack_rgba(sr, sg, sb, effective_a));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kernel_sums_to_one() {
let k = gaussian_kernel(4.0);
let sum: f32 = k[0] + 2.0 * k[1..].iter().sum::<f32>();
assert!((sum - 1.0).abs() < 0.01, "kernel sum = {sum}");
}
#[test]
fn cache_returns_same_kernel() {
let mut cache = GaussianCache::new();
let k1 = cache.kernel(4.0).to_vec();
let k2 = cache.kernel(4.0).to_vec();
assert_eq!(k1, k2);
}
#[test]
fn blur_spreads_alpha() {
let w = 9;
let h = 9;
let mut alpha = vec![0.0f32; w * h];
alpha[4 * w + 4] = 1.0; let kernel = gaussian_kernel(2.0);
gaussian_blur_alpha(&mut alpha, w, h, &kernel);
assert!(alpha[4 * w + 5] > 0.0, "right neighbor should receive blur");
assert!(alpha[3 * w + 4] > 0.0, "top neighbor should receive blur");
assert!(alpha[4 * w + 4] >= alpha[4 * w + 5]);
}
#[test]
fn shadow_energy_offset() {
let mut fb = Framebuffer::with_fill(60, 60, oxiui_core::Color(0, 0, 0, 0));
let mut cache = GaussianCache::new();
box_shadow(
&mut fb,
(10.0, 10.0, 20.0, 20.0),
5.0, 5.0, 3.0, Color(200, 200, 200, 255),
&mut cache,
);
let (r_near, _, _, _) = fb.get_rgba(25, 25).unwrap_or((0, 0, 0, 0));
assert!(
r_near > 0,
"shadow center pixel should be non-zero (r={r_near})"
);
let (_, _, _, a_far) = fb.get_rgba(0, 0).unwrap_or((0, 0, 0, 99));
assert_eq!(a_far, 0, "far corner should remain transparent (a={a_far})");
}
}