aura-gui 0.3.5

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Desenfoque por CPU: tres pasadas de caja por eje, que aproximan una gaussiana (la misma idea que `feGaussianBlur`
//! en los navegadores) con coste lineal, sin importar el radio.

use super::canvas::{Canvas, pack, rounded_rect_distance};
use crate::gpu::scene::Blur;

/// Anchos de las tres cajas cuya combinación se parece a una gaussiana de desviación `sigma`.
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))
}

/// Media móvil de radio `radius` sobre cada línea (borde replicado). `step` separa elementos de una línea y
/// `line_step` una línea de la siguiente.
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);
            }
        }
    }
}

/// Desenfoca lo que ya hay en el lienzo dentro del rectángulo redondeado de `blur`.
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;
    }
    // Se lee un margen alrededor para que el borde de la región mezcle con lo que hay fuera, como en la GPU.
    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);
    }
}