Playing around with Ordered Dithering and Bayer Matrices.
Interactive demo with multiple dithering sizes (2x2, 4x4, 8x8 and 16x16): https://joaocosta.eu/Demos/Dither/
This was much simpler than I thought. The whole dithering logic is just:
def buildBayerMatrix(n: Int): Vector[Vector[Int]] =
if (n <= 1) Vector(Vector(0))
else {
val mn2 = buildBayerMatrix(n / 2) // M_(n/2)
val q1 = mn2.map(_.map(_ * 4)) // 4*M_(n/2)
val q2 = q1.map(_.map(_ + 2)) // 4*M_(n/2) + 2*J_(n/2)
val q3 = q1.map(_.map(_ + 3)) // 4*M_(n/2) + 3*J_(n/2)
val q4 = q1.map(_.map(_ + 1)) // 4*M_(n/2) + 1*J_(n/2)
q1.zip(q2).map(_ ++ _) ++ q3.zip(q4).map(_ ++ _)
}
def buildDitherSurface(n: Int): RamSurface = {
val mask = buildBayerMatrix(n)
val max = n * n
RamSurface(mask.map(_.map(x => Color.grayscale((x * 255) / max))))
}
def ditherImage(image: Surface, ditherMask: Surface): Surface =
image.view.zipWith(
ditherMask.view.repeating,
(color, mask) =>
Color(
if (color.r >= mask.r) 255 else 0,
if (color.g >= mask.g) 255 else 0,
if (color.b >= mask.b) 255 else 0
)
)