braillix/canvas/
dither.rs

1// TODO: figure out if/how to allow user to choose the matrix size
2const M: usize = 3;
3
4/// Get the (2^M) x (2^M) Bayer threshold matrix value for (x, y).
5pub fn threshold(x: usize, y: usize) -> usize {
6    // TODO: is this any faster than just saving the matrix and doing a lookup?
7
8    let n = 1 << M;
9    let x = x % n;
10    let y = y % n;
11
12    // Implementation adapted from the black magic at:
13    // https://bisqwit.iki.fi/story/howto/dither/jy/
14    let mut v = 0;
15    let mut mask = M.saturating_sub(1);
16    let xc = x;
17    let yc = x ^ y;
18    let mut bit = 0;
19
20    // This loop does an "interleave in reverse order" of x and (x ^ y).
21    while bit < 2 * M {
22        v |= ((xc >> mask) & 1) << bit;
23        bit += 1;
24
25        v |= ((yc >> mask) & 1) << bit;
26        bit += 1;
27
28        mask = mask.saturating_sub(1);
29    }
30
31    v
32}
33
34/// Get the highest threshold value for M.
35pub const fn max_brightness() -> usize {
36    (1 << M) * (1 << M)
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[rustfmt::skip]
44    const BAYER_3: [[usize; 1 << 3]; 1 << 3] = [
45        [ 0, 48, 12, 60,  3, 51, 15, 63],
46        [32, 16, 44, 28, 35, 19, 47, 31],
47        [ 8, 56,  4, 52, 11, 59,  7, 55],
48        [40, 24, 36, 20, 43, 27, 39, 23],
49        [ 2, 50, 14, 62,  1, 49, 13, 61],
50        [34, 18, 46, 30, 33, 17, 45, 29],
51        [10, 58,  6, 54,  9, 57,  5, 53],
52        [42, 26, 38, 22, 41, 25, 37, 21],
53    ];
54
55    #[test]
56    fn interleave_algorithm() {
57        #![allow(clippy::needless_range_loop)]
58
59        let dim3 = 1 << 3;
60        for y in 0..dim3 {
61            for x in 0..dim3 {
62                assert_eq!(threshold(x, y), BAYER_3[y][x]);
63            }
64        }
65    }
66}