///|
/// 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..