Skip to main content

cranpose_render_common/
geometry.rs

1use cranpose_ui_graphics::Rect;
2
3/// The most taps a blur pass takes on one side of a pixel; a kernel wider
4/// than this in scratch texels truncates there.
5pub const BLUR_MAX_TAPS: u32 = 32;
6
7/// The block of device pixels one scratch texel of a blur stands for: a
8/// wide blur runs at a coarser grid, its kernel scaled with it.
9pub fn blur_scratch_block(radius_px: f32) -> u32 {
10    if radius_px < 6.0 {
11        1
12    } else if radius_px < 16.0 {
13        2
14    } else {
15        4
16    }
17}
18
19pub fn union_rect(lhs: Option<Rect>, rhs: Rect) -> Option<Rect> {
20    if rhs.width <= 0.0 || rhs.height <= 0.0 {
21        return lhs;
22    }
23
24    Some(match lhs {
25        Some(current) => current.union(rhs),
26        None => rhs,
27    })
28}
29
30/// How far, in device pixels, a blur of `radius_px` carries a source pixel:
31/// the kernel's taps at the scratch grid, the block each scratch texel
32/// averages on the way down and interpolates on the way back, and the
33/// source's own antialiased pixel, rounded up to whole blocks so the
34/// scratch grid sits on the source the same way whatever the margin. Past
35/// this distance the blur is exactly zero, so nothing reads or draws
36/// beyond it.
37pub fn blur_reach_px(radius_px: f32) -> f32 {
38    if radius_px.is_nan() || radius_px <= 0.0 {
39        return 1.0;
40    }
41    let block = blur_scratch_block(radius_px) as f32;
42    let reach = radius_px.min(BLUR_MAX_TAPS as f32 * block) + 3.0 * block + 1.0;
43    (reach / block).ceil() * block
44}
45
46/// [`blur_reach_px`] in logical pixels for a blur of `blur_radius` logical
47/// pixels drawn at `scale` device pixels per logical pixel.
48pub fn blur_reach(blur_radius: f32, scale: f32) -> f32 {
49    let scale = if scale.is_finite() && scale > 0.0 {
50        scale
51    } else {
52        1.0
53    };
54    blur_reach_px(blur_radius.max(0.0) * scale) / scale
55}
56
57pub fn expand_blurred_rect(
58    mut rect: Rect,
59    blur_radius: f32,
60    scale: f32,
61    clip: Option<Rect>,
62) -> Option<Rect> {
63    let blur_margin = blur_reach(blur_radius, scale);
64    rect.x -= blur_margin;
65    rect.y -= blur_margin;
66    rect.width += blur_margin * 2.0;
67    rect.height += blur_margin * 2.0;
68    if let Some(clip) = clip {
69        rect = rect.intersect(clip)?;
70    }
71    Some(rect)
72}
73
74#[cfg(test)]
75#[path = "tests/geometry_tests.rs"]
76mod tests;
77
78/// The tap pairs of a kernel of `BLUR_MAX_TAPS` taps: the taps at i and
79/// i + 1 on one side share one bilinear fetch.
80pub const BLUR_TAP_PAIRS: usize = (BLUR_MAX_TAPS / 2) as usize;
81
82/// One pair of kernel taps on one side of the pixel: the Gaussian weights of
83/// the inner and outer tap, and the one bilinear fetch that stands for both,
84/// its `offset` in taps from the pixel and its `weight` their sum. The outer
85/// weight is zero past an odd tap count, which leaves the fetch on the inner
86/// tap alone.
87#[derive(Clone, Copy, Debug, Default, PartialEq)]
88pub struct BlurTapPair {
89    pub inner: f32,
90    pub outer: f32,
91    pub offset: f32,
92    pub weight: f32,
93}
94
95/// The separable Gaussian kernel of a blur of `radius` source texels as the
96/// blur pass samples it: `pair_count` pairs on each side, `total_weight` the
97/// kernel's sum with the centre tap's one, computed once per draw.
98#[derive(Clone, Copy, Debug, PartialEq)]
99pub struct BlurKernel {
100    pub pairs: [BlurTapPair; BLUR_TAP_PAIRS],
101    pub pair_count: u32,
102    pub total_weight: f32,
103}
104
105impl BlurKernel {
106    /// The kernel of a blur of `radius` texels: sigma is half the radius,
107    /// the taps on one side its ceiling, at most `BLUR_MAX_TAPS`.
108    pub fn of_radius(radius: f32) -> Self {
109        let radius = radius.max(0.0);
110        let sigma = (radius * 0.5).max(0.001);
111        let tap_count = (radius.ceil() as u32).min(BLUR_MAX_TAPS);
112        let inv_2sigma2 = 1.0 / (2.0 * sigma * sigma);
113        let mut pairs = [BlurTapPair::default(); BLUR_TAP_PAIRS];
114        let mut total_weight = 1.0f32;
115        let mut pair_count = 0;
116        for i in (1..=tap_count).step_by(2) {
117            let fi = i as f32;
118            let fj = fi + 1.0;
119            let inner = (-(fi * fi) * inv_2sigma2).exp();
120            let outer = if i < tap_count {
121                (-(fj * fj) * inv_2sigma2).exp()
122            } else {
123                0.0
124            };
125            total_weight += 2.0 * (inner + outer);
126            let weight = inner + outer;
127            let offset = if weight > 0.0 {
128                (fi * inner + fj * outer) / weight
129            } else {
130                0.0
131            };
132            pairs[pair_count] = BlurTapPair {
133                inner,
134                outer,
135                offset,
136                weight,
137            };
138            pair_count += 1;
139        }
140        Self {
141            pairs,
142            pair_count: pair_count as u32,
143            total_weight,
144        }
145    }
146}
147
148#[cfg(test)]
149#[path = "tests/geometry_blur_kernel_tests.rs"]
150mod blur_kernel_tests;