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
57/// An upper bound on [`blur_reach`] for every finite scale at least
58/// `minimum_scale`. A nonpositive or nonfinite minimum has no finite bound.
59pub fn blur_reach_for_minimum_scale(blur_radius: f32, minimum_scale: f32) -> f32 {
60    if !minimum_scale.is_finite() || minimum_scale <= 0.0 {
61        return f32::INFINITY;
62    }
63
64    let radius = blur_radius.max(0.0);
65    let minimum = f64::from(minimum_scale);
66    let max_block = f64::from(blur_scratch_block(f32::INFINITY));
67    let additive_bound = f64::from(radius) + (4.0 * max_block + 1.0) / minimum;
68    let saturated_bound = f64::from(blur_reach_px(f32::INFINITY)) / minimum;
69    let bound = additive_bound.min(saturated_bound);
70    let rounded = bound as f32;
71    if rounded.is_finite() && f64::from(rounded) < bound {
72        f32::from_bits(rounded.to_bits() + 1)
73    } else {
74        rounded
75    }
76}
77
78pub fn expand_blurred_rect(
79    mut rect: Rect,
80    blur_radius: f32,
81    scale: f32,
82    clip: Option<Rect>,
83) -> Option<Rect> {
84    let blur_margin = blur_reach(blur_radius, scale);
85    rect.x -= blur_margin;
86    rect.y -= blur_margin;
87    rect.width += blur_margin * 2.0;
88    rect.height += blur_margin * 2.0;
89    if let Some(clip) = clip {
90        rect = rect.intersect(clip)?;
91    }
92    Some(rect)
93}
94
95#[cfg(test)]
96#[path = "tests/geometry_tests.rs"]
97mod tests;
98
99/// The tap pairs of a kernel of `BLUR_MAX_TAPS` taps: the taps at i and
100/// i + 1 on one side share one bilinear fetch.
101pub const BLUR_TAP_PAIRS: usize = (BLUR_MAX_TAPS / 2) as usize;
102
103/// One pair of kernel taps on one side of the pixel: the Gaussian weights of
104/// the inner and outer tap, and the one bilinear fetch that stands for both,
105/// its `offset` in taps from the pixel and its `weight` their sum. The outer
106/// weight is zero past an odd tap count, which leaves the fetch on the inner
107/// tap alone.
108#[derive(Clone, Copy, Debug, Default, PartialEq)]
109pub struct BlurTapPair {
110    pub inner: f32,
111    pub outer: f32,
112    pub offset: f32,
113    pub weight: f32,
114}
115
116/// The separable Gaussian kernel of a blur of `radius` source texels as the
117/// blur pass samples it: `pair_count` pairs on each side, `total_weight` the
118/// kernel's sum with the centre tap's one, computed once per draw.
119#[derive(Clone, Copy, Debug, PartialEq)]
120pub struct BlurKernel {
121    pub pairs: [BlurTapPair; BLUR_TAP_PAIRS],
122    pub pair_count: u32,
123    pub total_weight: f32,
124}
125
126impl BlurKernel {
127    /// The kernel of a blur of `radius` texels: sigma is half the radius,
128    /// the taps on one side its ceiling, at most `BLUR_MAX_TAPS`.
129    pub fn of_radius(radius: f32) -> Self {
130        let radius = radius.max(0.0);
131        let sigma = (radius * 0.5).max(0.001);
132        let tap_count = (radius.ceil() as u32).min(BLUR_MAX_TAPS);
133        let inv_2sigma2 = 1.0 / (2.0 * sigma * sigma);
134        let mut pairs = [BlurTapPair::default(); BLUR_TAP_PAIRS];
135        let mut total_weight = 1.0f32;
136        let mut pair_count = 0;
137        for i in (1..=tap_count).step_by(2) {
138            let fi = i as f32;
139            let fj = fi + 1.0;
140            let inner = (-(fi * fi) * inv_2sigma2).exp();
141            let outer = if i < tap_count {
142                (-(fj * fj) * inv_2sigma2).exp()
143            } else {
144                0.0
145            };
146            total_weight += 2.0 * (inner + outer);
147            let weight = inner + outer;
148            let offset = if weight > 0.0 {
149                (fi * inner + fj * outer) / weight
150            } else {
151                0.0
152            };
153            pairs[pair_count] = BlurTapPair {
154                inner,
155                outer,
156                offset,
157                weight,
158            };
159            pair_count += 1;
160        }
161        Self {
162            pairs,
163            pair_count: pair_count as u32,
164            total_weight,
165        }
166    }
167}
168
169#[cfg(test)]
170#[path = "tests/geometry_blur_kernel_tests.rs"]
171mod blur_kernel_tests;