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)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn union_rect_ignores_empty_rhs() {
80        let lhs = Some(Rect {
81            x: 1.0,
82            y: 2.0,
83            width: 3.0,
84            height: 4.0,
85        });
86        let rhs = Rect {
87            x: 5.0,
88            y: 6.0,
89            width: 0.0,
90            height: 7.0,
91        };
92
93        assert_eq!(union_rect(lhs, rhs), lhs);
94    }
95
96    #[test]
97    fn union_rect_merges_extents() {
98        let lhs = Some(Rect {
99            x: 8.0,
100            y: 4.0,
101            width: 3.0,
102            height: 5.0,
103        });
104        let rhs = Rect {
105            x: 2.0,
106            y: 7.0,
107            width: 12.0,
108            height: 4.0,
109        };
110
111        assert_eq!(
112            union_rect(lhs, rhs),
113            Some(Rect {
114                x: 2.0,
115                y: 4.0,
116                width: 12.0,
117                height: 7.0,
118            })
119        );
120    }
121
122    #[test]
123    fn a_blur_reaches_its_kernel_and_its_scratch_blocks_past_the_source() {
124        assert_eq!(blur_reach_px(0.0), 1.0);
125        assert_eq!(blur_reach_px(-5.0), 1.0);
126        assert_eq!(blur_reach_px(2.0), 6.0);
127        assert_eq!(blur_reach_px(10.0), 18.0);
128        assert_eq!(blur_reach_px(44.0), 60.0);
129        assert_eq!(blur_reach_px(200.0), 144.0);
130    }
131
132    #[test]
133    fn the_logical_reach_follows_the_device_scale() {
134        assert_eq!(blur_reach(2.0, 1.0), 6.0);
135        assert!((blur_reach(20.0, 2.25) - 60.0 / 2.25).abs() < 1e-5);
136        assert_eq!(blur_reach(2.0, 0.0), 6.0);
137        assert_eq!(blur_reach(2.0, f32::NAN), 6.0);
138    }
139
140    #[test]
141    fn expand_blurred_rect_applies_margin_and_clip() {
142        let expanded = expand_blurred_rect(
143            Rect {
144                x: 10.0,
145                y: 20.0,
146                width: 30.0,
147                height: 40.0,
148            },
149            2.0,
150            1.0,
151            Some(Rect {
152                x: 8.0,
153                y: 18.0,
154                width: 20.0,
155                height: 20.0,
156            }),
157        );
158
159        assert_eq!(
160            expanded,
161            Some(Rect {
162                x: 8.0,
163                y: 18.0,
164                width: 20.0,
165                height: 20.0,
166            })
167        );
168    }
169}
170
171/// The tap pairs of a kernel of `BLUR_MAX_TAPS` taps: the taps at i and
172/// i + 1 on one side share one bilinear fetch.
173pub const BLUR_TAP_PAIRS: usize = (BLUR_MAX_TAPS / 2) as usize;
174
175/// One pair of kernel taps on one side of the pixel: the Gaussian weights of
176/// the inner and outer tap, and the one bilinear fetch that stands for both,
177/// its `offset` in taps from the pixel and its `weight` their sum. The outer
178/// weight is zero past an odd tap count, which leaves the fetch on the inner
179/// tap alone.
180#[derive(Clone, Copy, Debug, Default, PartialEq)]
181pub struct BlurTapPair {
182    pub inner: f32,
183    pub outer: f32,
184    pub offset: f32,
185    pub weight: f32,
186}
187
188/// The separable Gaussian kernel of a blur of `radius` source texels as the
189/// blur pass samples it: `pair_count` pairs on each side, `total_weight` the
190/// kernel's sum with the centre tap's one, computed once per draw.
191#[derive(Clone, Copy, Debug, PartialEq)]
192pub struct BlurKernel {
193    pub pairs: [BlurTapPair; BLUR_TAP_PAIRS],
194    pub pair_count: u32,
195    pub total_weight: f32,
196}
197
198impl BlurKernel {
199    /// The kernel of a blur of `radius` texels: sigma is half the radius,
200    /// the taps on one side its ceiling, at most `BLUR_MAX_TAPS`.
201    pub fn of_radius(radius: f32) -> Self {
202        let radius = radius.max(0.0);
203        let sigma = (radius * 0.5).max(0.001);
204        let tap_count = (radius.ceil() as u32).min(BLUR_MAX_TAPS);
205        let inv_2sigma2 = 1.0 / (2.0 * sigma * sigma);
206        let mut pairs = [BlurTapPair::default(); BLUR_TAP_PAIRS];
207        let mut total_weight = 1.0f32;
208        let mut pair_count = 0;
209        for i in (1..=tap_count).step_by(2) {
210            let fi = i as f32;
211            let fj = fi + 1.0;
212            let inner = (-(fi * fi) * inv_2sigma2).exp();
213            let outer = if i < tap_count {
214                (-(fj * fj) * inv_2sigma2).exp()
215            } else {
216                0.0
217            };
218            total_weight += 2.0 * (inner + outer);
219            let weight = inner + outer;
220            let offset = if weight > 0.0 {
221                (fi * inner + fj * outer) / weight
222            } else {
223                0.0
224            };
225            pairs[pair_count] = BlurTapPair {
226                inner,
227                outer,
228                offset,
229                weight,
230            };
231            pair_count += 1;
232        }
233        Self {
234            pairs,
235            pair_count: pair_count as u32,
236            total_weight,
237        }
238    }
239}
240
241#[cfg(test)]
242mod blur_kernel_tests {
243    use super::*;
244
245    fn gaussian(tap: u32, radius: f32) -> f32 {
246        let sigma = radius * 0.5;
247        (-((tap * tap) as f32) / (2.0 * sigma * sigma)).exp()
248    }
249
250    #[test]
251    fn a_kernel_pairs_its_taps_where_their_weights_meet() {
252        let kernel = BlurKernel::of_radius(5.5);
253        assert_eq!(
254            kernel.pair_count, 3,
255            "a radius of 5.5 takes six taps a side"
256        );
257        let mut total = 1.0;
258        for (k, pair) in kernel.pairs[..3].iter().enumerate() {
259            let (i, j) = (2 * k as u32 + 1, 2 * k as u32 + 2);
260            assert!((pair.inner - gaussian(i, 5.5)).abs() <= 1e-6);
261            assert!((pair.outer - gaussian(j, 5.5)).abs() <= 1e-6);
262            assert_eq!(pair.weight, pair.inner + pair.outer);
263            assert!(pair.offset > i as f32 && pair.offset < j as f32);
264            total += 2.0 * (pair.inner + pair.outer);
265        }
266        assert_eq!(kernel.total_weight, total);
267        assert_eq!(kernel.pairs[3], BlurTapPair::default());
268    }
269
270    #[test]
271    fn an_odd_tap_count_leaves_its_last_fetch_on_the_inner_tap() {
272        let kernel = BlurKernel::of_radius(7.0);
273        assert_eq!(kernel.pair_count, 4);
274        let tail = kernel.pairs[3];
275        assert_eq!(tail.outer, 0.0);
276        assert_eq!(tail.offset, 7.0);
277        assert_eq!(tail.weight, tail.inner);
278    }
279
280    #[test]
281    fn a_kernel_stops_at_the_tap_cap_and_a_zero_radius_has_no_pairs() {
282        let capped = BlurKernel::of_radius(100.0);
283        assert_eq!(capped.pair_count, BLUR_TAP_PAIRS as u32);
284        assert!(capped.pairs[BLUR_TAP_PAIRS - 1].outer > 0.0);
285        let none = BlurKernel::of_radius(0.0);
286        assert_eq!((none.pair_count, none.total_weight), (0, 1.0));
287    }
288}