use super::canvas::{Canvas, pack, rounded_rect_distance};
use crate::gpu::scene::Blur;
fn box_sizes(sigma: f32) -> [usize; 3] {
let ideal = (12.0 * sigma * sigma / 3.0 + 1.0).sqrt();
let mut lower = ideal.floor() as usize;
if lower.is_multiple_of(2) {
lower = lower.saturating_sub(1);
}
let lower = lower.max(1);
let low = lower as f32;
let rounded = ((12.0 * sigma * sigma - 3.0 * low * low - 12.0 * low - 9.0) / (-4.0 * low - 4.0)).round();
let count_lower = rounded.clamp(0.0, 3.0) as usize;
std::array::from_fn(|index| if index < count_lower { lower } else { lower + 2 }).map(|size| size.max(1))
}
fn box_pass(
source: &[f32],
target: &mut [f32],
lines: usize,
length: usize,
step: usize,
line_step: usize,
radius: usize,
) {
let window = (2 * radius + 1) as f32;
for line in 0..lines {
for channel in 0..3 {
let base = line * line_step + channel;
let at = |index: isize| source[base + index.clamp(0, length as isize - 1) as usize * step];
let mut sum = at(0) * (radius as f32 + 1.0);
for index in 1..=radius as isize {
sum += at(index);
}
for index in 0..length as isize {
target[base + index as usize * step] = sum / window;
sum += at(index + radius as isize + 1) - at(index - radius as isize);
}
}
}
}
pub fn blur(canvas: &mut Canvas, blur: &Blur) {
let sigma = blur.sigma * canvas.scale;
if sigma < 0.3 {
return;
}
let (columns, rows) = canvas.area(blur.bounds, 0.0, blur.clip);
if columns.is_empty() || rows.is_empty() {
return;
}
let margin = (sigma * 3.0).ceil() as u32;
let x_range = columns.start.saturating_sub(margin)..(columns.end + margin).min(canvas.width);
let y_range = rows.start.saturating_sub(margin)..(rows.end + margin).min(canvas.height);
let (width, height) = (x_range.len(), y_range.len());
let mut data = vec![0.0f32; width * height * 3];
for (row, y) in y_range.clone().enumerate() {
for (column, x) in x_range.clone().enumerate() {
let pixel = canvas.pixels[(y * canvas.width + x) as usize];
let at = (row * width + column) * 3;
data[at] = ((pixel >> 16) & 0xff) as f32 / 255.0;
data[at + 1] = ((pixel >> 8) & 0xff) as f32 / 255.0;
data[at + 2] = (pixel & 0xff) as f32 / 255.0;
}
}
let mut scratch = vec![0.0f32; data.len()];
for size in box_sizes(sigma) {
let radius = (size - 1) / 2;
box_pass(&data, &mut scratch, height, width, 3, width * 3, radius);
box_pass(&scratch, &mut data, width, height, width * 3, 3, radius);
}
let half = (blur.bounds[2] * 0.5, blur.bounds[3] * 0.5);
let center = (blur.bounds[0] + half.0, blur.bounds[1] + half.1);
for y in rows {
for x in columns.clone() {
let local = (canvas.logical(x) - center.0, canvas.logical(y) - center.1);
let coverage = (0.5 - rounded_rect_distance(local, half, blur.corner_radii)).clamp(0.0, 1.0);
if coverage <= 0.0 {
continue;
}
let at = ((y - y_range.start) as usize * width + (x - x_range.start) as usize) * 3;
let index = (y * canvas.width + x) as usize;
let old = canvas.pixels[index];
let mix = |new: f32, shift: u32| new + (((old >> shift) & 0xff) as f32 / 255.0 - new) * (1.0 - coverage);
canvas.pixels[index] = pack(mix(data[at], 16), mix(data[at + 1], 8), mix(data[at + 2], 0));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Bounds, Corners, Point, Size, px};
fn region(x: f32, y: f32, w: f32, h: f32) -> Bounds {
Bounds::new(Point::new(px(x), px(y)), Size::new(px(w), px(h)))
}
fn checker(size: u32) -> Vec<u32> {
(0..size * size)
.map(|index| if (index % size + index / size).is_multiple_of(2) { 0xffffff } else { 0x000000 })
.collect()
}
#[test]
fn las_cajas_suman_la_varianza_pedida() {
for sigma in [1.0f32, 4.0, 12.0, 30.0] {
let variance: f32 = box_sizes(sigma).iter().map(|&size| ((size * size) as f32 - 1.0) / 12.0).sum();
assert!((variance.sqrt() - sigma).abs() / sigma < 0.2, "σ {sigma}: {}", variance.sqrt());
}
}
#[test]
fn un_tablero_se_vuelve_gris_y_lo_de_fuera_no_cambia() {
let mut pixels = checker(40);
let original = pixels.clone();
let mut canvas = Canvas { pixels: &mut pixels, width: 40, height: 40, scale: 1.0 };
let all = region(0.0, 0.0, 40.0, 40.0);
let area = region(10.0, 10.0, 20.0, 20.0);
blur(&mut canvas, &Blur::new(area, all, Corners::all(px(0.0)), 3.0));
let center = (pixels[20 * 40 + 20] & 0xff) as i32;
assert!((center - 128).abs() < 30, "el centro queda gris, no {center}");
assert_eq!(pixels[5 * 40 + 5], original[5 * 40 + 5], "fuera de la caja no se toca");
assert_eq!(pixels[0], original[0]);
}
#[test]
fn el_radio_de_las_esquinas_recorta_el_efecto() {
let mut pixels = checker(40);
let original = pixels.clone();
let mut canvas = Canvas { pixels: &mut pixels, width: 40, height: 40, scale: 1.0 };
let all = region(0.0, 0.0, 40.0, 40.0);
blur(&mut canvas, &Blur::new(region(0.0, 0.0, 40.0, 40.0), all, Corners::all(px(20.0)), 3.0));
assert_eq!(pixels[0], original[0], "la esquina redondeada queda fuera");
assert_ne!(pixels[20 * 40 + 20], original[20 * 40 + 20]);
}
#[test]
fn sin_desviacion_no_hace_nada() {
let mut pixels = checker(8);
let original = pixels.clone();
let mut canvas = Canvas { pixels: &mut pixels, width: 8, height: 8, scale: 1.0 };
let all = region(0.0, 0.0, 8.0, 8.0);
blur(&mut canvas, &Blur::new(all, all, Corners::all(px(0.0)), 0.0));
assert_eq!(pixels, original);
}
}