///|
/// Floyd–Steinberg error-diffusion dithering. Quantization error at each
/// pixel is pushed to the not-yet-visited neighbors with the classic
/// weights (7/16 right, 3/16 below-left, 5/16 below, 1/16 below-right), so a
/// small palette still reproduces smooth gradients as stipple patterns.
///|
/// Diffuses `err` from pixel `(x, y)` into the working buffer `buf`
/// (one Int channel per pixel) using the Floyd–Steinberg weights.
fn diffuse(
buf : Array[Int],
w : Int,
h : Int,
x : Int,
y : Int,
err : Int,
) -> Unit {
if x + 1 < w {
buf[y * w + x + 1] = buf[y * w + x + 1] + err * 7 / 16
}
if y + 1 < h {
if x > 0 {
buf[(y + 1) * w + x - 1] = buf[(y + 1) * w + x - 1] + err * 3 / 16
}
buf[(y + 1) * w + x] = buf[(y + 1) * w + x] + err * 5 / 16
if x + 1 < w {
buf[(y + 1) * w + x + 1] = buf[(y + 1) * w + x + 1] + err * 1 / 16
}
}
}
///|
/// Quantizes `v` (0..255) to the nearest of `levels` evenly spaced tones and
/// returns that output value in 0..255.
fn quantize(v : Int, levels : Int) -> Int {
let step = 255 / (levels - 1)
let q = (v + step / 2) / step
let capped = if q > levels - 1 { levels - 1 } else if q < 0 { 0 } else { q }
capped * 255 / (levels - 1)
}
///|
/// Grayscale Floyd–Steinberg dithering to `levels` tones (clamped to
/// `[2, 256]`). The image is first reduced to luma; error diffuses in a
/// left-to-right, top-to-bottom scan. Output is gray; alpha preserved.
pub fn Image::dither_grayscale(self : Image, levels : Int) -> Image {
let l = if levels < 2 { 2 } else if levels > 256 { 256 } else { levels }
let w = self.width
let h = self.height
let buf : Array[Int] = Array::make(w * h, 0)
for p in 0..<(w * h) {
let base = p * 4
buf[p] = (
299 * self.data[base].to_int() +
587 * self.data[base + 1].to_int() +
114 * self.data[base + 2].to_int()
) /
1000
}
let out = Image::new(w, h)
for y in 0.. Image {
self.dither_grayscale(2)
}