///|
/// Separable Gaussian blur with arbitrary radius.
///
/// Uses binomial-coefficient weights (`C(2r, k)`, the discrete Gaussian):
/// integer, exactly normalized by `4^r`, and separable — a horizontal then a
/// vertical 1D pass replace the O(k²) 2D convolution with O(2k) work per
/// pixel. At radius 1 the weights are `[1, 2, 1]`, the same kernel as
/// `Kernel::gaussian_blur()`.
///|
/// Builds the binomial weight row `C(2r, 0) .. C(2r, 2r)` via Pascal's rule.
fn binomial_weights(radius : Int) -> Array[Double] {
let n = 2 * radius + 1
let w : Array[Double] = Array::make(n, 1.0)
for row in 1..= 1; k = k - 1 {
w[k] = w[k] + w[k - 1]
}
}
w
}
///|
/// One 1D blur pass. `horizontal` selects the axis; out-of-bounds taps
/// clamp to the edge. RGB channels are blurred, alpha is preserved.
fn Image::blur_pass(
self : Image,
weights : Array[Double],
norm : Double,
horizontal : Bool,
) -> Image {
let radius = weights.length() / 2
let out = Image::new(self.width, self.height)
for y in 0.. Image {
if radius <= 0 {
return self.copy()
}
let r = if radius > 32 { 32 } else { radius }
let weights = binomial_weights(r)
let mut norm = 1.0
for _ in 0..