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
19/// Returns `lhs` grown to cover `rhs`. A `rhs` with no area leaves `lhs` unchanged, and a
20/// `None` `lhs` becomes `rhs`.
21pub fn union_rect(lhs: Option<Rect>, rhs: Rect) -> Option<Rect> {
22    if rhs.width <= 0.0 || rhs.height <= 0.0 {
23        return lhs;
24    }
25
26    Some(match lhs {
27        Some(current) => current.union(rhs),
28        None => rhs,
29    })
30}
31
32/// How far, in device pixels, a blur of `radius_px` carries a source pixel:
33/// the kernel's taps at the scratch grid, the block each scratch texel
34/// averages on the way down and interpolates on the way back, and the
35/// source's own antialiased pixel, rounded up to whole blocks so the
36/// scratch grid sits on the source the same way whatever the margin. Past
37/// this distance the blur is exactly zero, so nothing reads or draws
38/// beyond it.
39pub fn blur_reach_px(radius_px: f32) -> f32 {
40    if radius_px.is_nan() || radius_px <= 0.0 {
41        return 1.0;
42    }
43    let block = blur_scratch_block(radius_px) as f32;
44    let reach = radius_px.min(BLUR_MAX_TAPS as f32 * block) + 3.0 * block + 1.0;
45    (reach / block).ceil() * block
46}
47
48/// [`blur_reach_px`] in logical pixels for a blur of `blur_radius` logical
49/// pixels drawn at `scale` device pixels per logical pixel.
50pub fn blur_reach(blur_radius: f32, scale: f32) -> f32 {
51    let scale = if scale.is_finite() && scale > 0.0 {
52        scale
53    } else {
54        1.0
55    };
56    blur_reach_px(blur_radius.max(0.0) * scale) / scale
57}
58
59/// An upper bound on [`blur_reach`] for every finite scale at least
60/// `minimum_scale`. A nonpositive or nonfinite minimum has no finite bound.
61pub fn blur_reach_for_minimum_scale(blur_radius: f32, minimum_scale: f32) -> f32 {
62    if !minimum_scale.is_finite() || minimum_scale <= 0.0 {
63        return f32::INFINITY;
64    }
65
66    let radius = blur_radius.max(0.0);
67    let minimum = f64::from(minimum_scale);
68    let max_block = f64::from(blur_scratch_block(f32::INFINITY));
69    let additive_bound = f64::from(radius) + (4.0 * max_block + 1.0) / minimum;
70    let saturated_bound = f64::from(blur_reach_px(f32::INFINITY)) / minimum;
71    let bound = additive_bound.min(saturated_bound);
72    let rounded = bound as f32;
73    if rounded.is_finite() && f64::from(rounded) < bound {
74        f32::from_bits(rounded.to_bits() + 1)
75    } else {
76        rounded
77    }
78}
79
80/// Grows `rect` on every side by the reach of a blur of `blur_radius` logical pixels drawn at
81/// `scale` device pixels per logical pixel, then intersects it with `clip` when one is given.
82/// Returns `None` when the grown rectangle does not meet `clip`.
83pub fn expand_blurred_rect(
84    mut rect: Rect,
85    blur_radius: f32,
86    scale: f32,
87    clip: Option<Rect>,
88) -> Option<Rect> {
89    let blur_margin = blur_reach(blur_radius, scale);
90    rect.x -= blur_margin;
91    rect.y -= blur_margin;
92    rect.width += blur_margin * 2.0;
93    rect.height += blur_margin * 2.0;
94    if let Some(clip) = clip {
95        rect = rect.intersect(clip)?;
96    }
97    Some(rect)
98}
99
100#[cfg(test)]
101#[path = "tests/geometry_tests.rs"]
102mod tests;
103
104/// The tap pairs of a kernel of `BLUR_MAX_TAPS` taps: the taps at i and
105/// i + 1 on one side share one bilinear fetch.
106pub const BLUR_TAP_PAIRS: usize = (BLUR_MAX_TAPS / 2) as usize;
107
108/// One pair of kernel taps on one side of the pixel: the Gaussian weights of
109/// the inner and outer tap, and the one bilinear fetch that stands for both,
110/// its `offset` in taps from the pixel and its `weight` their sum. The outer
111/// weight is zero past an odd tap count, which leaves the fetch on the inner
112/// tap alone.
113#[derive(Clone, Copy, Debug, Default, PartialEq)]
114pub struct BlurTapPair {
115    /// Gaussian weight of the inner tap, the nearer to the pixel.
116    pub inner: f32,
117    /// Gaussian weight of the outer tap, zero when the kernel has an odd tap count.
118    pub outer: f32,
119    /// Distance in taps from the pixel at which the single bilinear fetch samples.
120    pub offset: f32,
121    /// Combined weight of the two taps, the sum of `inner` and `outer`.
122    pub weight: f32,
123}
124
125/// The separable Gaussian kernel of a blur of `radius` source texels as the
126/// blur pass samples it: `pair_count` pairs on each side, `total_weight` the
127/// kernel's sum with the centre tap's one, computed once per draw.
128#[derive(Clone, Copy, Debug, PartialEq)]
129pub struct BlurKernel {
130    /// Tap pairs for one side of the pixel, ordered from the pixel outward.
131    pub pairs: [BlurTapPair; BLUR_TAP_PAIRS],
132    /// Number of entries of `pairs` in use; the rest are zero.
133    pub pair_count: u32,
134    /// Sum of every tap weight on both sides plus the centre tap, which is one.
135    pub total_weight: f32,
136}
137
138impl BlurKernel {
139    /// The kernel of a blur of `radius` texels: sigma is half the radius,
140    /// the taps on one side its ceiling, at most `BLUR_MAX_TAPS`.
141    pub fn of_radius(radius: f32) -> Self {
142        let radius = radius.max(0.0);
143        let sigma = (radius * 0.5).max(0.001);
144        let tap_count = (radius.ceil() as u32).min(BLUR_MAX_TAPS);
145        let inv_2sigma2 = 1.0 / (2.0 * sigma * sigma);
146        let mut pairs = [BlurTapPair::default(); BLUR_TAP_PAIRS];
147        let mut total_weight = 1.0f32;
148        let mut pair_count = 0;
149        for i in (1..=tap_count).step_by(2) {
150            let fi = i as f32;
151            let fj = fi + 1.0;
152            let inner = (-(fi * fi) * inv_2sigma2).exp();
153            let outer = if i < tap_count {
154                (-(fj * fj) * inv_2sigma2).exp()
155            } else {
156                0.0
157            };
158            total_weight += 2.0 * (inner + outer);
159            let weight = inner + outer;
160            let offset = if weight > 0.0 {
161                (fi * inner + fj * outer) / weight
162            } else {
163                0.0
164            };
165            pairs[pair_count] = BlurTapPair {
166                inner,
167                outer,
168                offset,
169                weight,
170            };
171            pair_count += 1;
172        }
173        Self {
174            pairs,
175            pair_count: pair_count as u32,
176            total_weight,
177        }
178    }
179}
180
181#[cfg(test)]
182#[path = "tests/geometry_blur_kernel_tests.rs"]
183mod blur_kernel_tests;