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.
//! Plan de un desenfoque: recortes y parámetros de sus dos pasadas gaussianas.

use crate::gpu::scene::Blur;
use bytemuck::{Pod, Zeroable};

pub(super) const PARAMS_SIZE: u64 = 32;
/// Las texturas intermedias miden la mitad: con la gaussiana grande da igual y cuestan una cuarta parte.
pub(super) const DOWNSCALE: f32 = 2.0;
/// Máximo de muestras a cada lado; con más desviación se separan las muestras en lugar de añadir más.
pub(super) const MAX_TAPS: f32 = 24.0;
/// Fotogramas sin desenfoques tras los cuales se sueltan las texturas.
pub(super) const IDLE_FRAMES: u32 = 120;

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub(super) struct Params {
    pub(super) inv_size: [f32; 2],
    pub(super) direction: [f32; 2],
    pub(super) sigma: f32,
    pub(super) stride: f32,
    pub(super) taps: f32,
    pub(super) padding: f32,
}

/// Recorte (x, y, ancho, alto) en píxeles de la textura de media resolución; `None` si no queda nada que pintar.
pub(super) type Scissor = Option<[u32; 4]>;

/// Recortes y parámetros de las dos pasadas de un desenfoque. Las texturas intermedias miden la mitad, así que
/// la desviación se reparte: lo que no cabe en `MAX_TAPS` muestras se logra separándolas.
pub(super) fn plan(blur: &Blur, scale: f32, low: (u32, u32)) -> (Scissor, Scissor, [Params; 2]) {
    let sigma_low = (blur.sigma * scale / DOWNSCALE).max(0.0);
    let stride = (sigma_low / (MAX_TAPS / 3.0)).max(1.0);
    let sigma = sigma_low / stride;
    let taps = (sigma * 3.0).ceil().clamp(1.0, MAX_TAPS);
    let reach = (taps * stride).ceil() as i64 + 1;

    let (left, top) = (blur.bounds[0].max(blur.clip[0]), blur.bounds[1].max(blur.clip[1]));
    let right = (blur.bounds[0] + blur.bounds[2]).min(blur.clip[0] + blur.clip[2]);
    let bottom = (blur.bounds[1] + blur.bounds[3]).min(blur.clip[1] + blur.clip[3]);
    let to_low = |value: f32| (value * scale / DOWNSCALE).floor() as i64;
    let rect = (
        to_low(left),
        to_low(top),
        (right * scale / DOWNSCALE).ceil() as i64,
        (bottom * scale / DOWNSCALE).ceil() as i64,
    );
    let clamp = |(x0, y0, x1, y1): (i64, i64, i64, i64)| -> Scissor {
        let (x0, y0) = (x0.clamp(0, low.0 as i64), y0.clamp(0, low.1 as i64));
        let (x1, y1) = (x1.clamp(0, low.0 as i64), y1.clamp(0, low.1 as i64));
        (x1 > x0 && y1 > y0).then(|| [x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32])
    };
    // La vertical lee filas de arriba y abajo que la horizontal tiene que haber escrito.
    let first = clamp((rect.0 - 1, rect.1 - reach, rect.2 + 1, rect.3 + reach));
    let second = clamp((rect.0 - 1, rect.1 - 1, rect.2 + 1, rect.3 + 1));
    let inv_size = [1.0 / low.0 as f32, 1.0 / low.1 as f32];
    let params = |direction: [f32; 2]| Params { inv_size, direction, sigma, stride, taps, padding: 0.0 };
    (first, second, [params([1.0, 0.0]), params([0.0, 1.0])])
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::{Bounds, Corners, Point, Size, px};

    fn blur(x: f32, y: f32, w: f32, h: f32, sigma: f32) -> Blur {
        let area = Bounds::new(Point::new(px(x), px(y)), Size::new(px(w), px(h)));
        Blur::new(
            area,
            Bounds::new(Point::new(px(0.0), px(0.0)), Size::new(px(1000.0), px(1000.0))),
            Corners::all(px(0.0)),
            sigma,
        )
    }

    #[test]
    fn el_recorte_de_la_vertical_es_el_de_la_caja_y_el_de_la_horizontal_lo_amplia() {
        let (first, second, params) = plan(&blur(100.0, 100.0, 200.0, 100.0, 8.0), 2.0, (500, 500));
        let (first, second) = (first.unwrap(), second.unwrap());
        assert_eq!((second[0], second[1], second[2], second[3]), (99, 99, 202, 102), "caja en media resolución ± 1");
        assert!(first[1] < second[1] && first[3] > second[3], "la horizontal cubre más filas");
        assert_eq!(first[0], second[0], "pero las mismas columnas");
        assert_eq!(params[0].direction, [1.0, 0.0]);
        assert_eq!(params[1].direction, [0.0, 1.0]);
    }

    #[test]
    fn mucha_desviacion_separa_las_muestras_en_vez_de_anadir_mas() {
        let (_, _, small) = plan(&blur(0.0, 0.0, 10.0, 10.0, 4.0), 1.0, (100, 100));
        let (_, _, large) = plan(&blur(0.0, 0.0, 10.0, 10.0, 64.0), 1.0, (100, 100));
        assert_eq!(small[0].stride, 1.0);
        assert!(large[0].stride > 1.0 && large[0].taps <= MAX_TAPS);
        let reach = |p: &Params| p.stride * p.sigma;
        assert!((reach(&large[0]) - 32.0).abs() < 0.01, "σ en media resolución = 64 / 2");
    }

    #[test]
    fn un_rectangulo_fuera_de_la_pantalla_no_pinta_nada() {
        let (first, second, _) = plan(&blur(5000.0, 5000.0, 100.0, 100.0, 8.0), 1.0, (100, 100));
        assert!(first.is_none() && second.is_none());
    }
}