processingp5.jsdithering

Dithering (Floyd-Steinberg) only updates part of graphics object in p5.js


I'm trying to implement Floyd-Steinberg dithering in a P5.js sketch by pre-dithering a bunch of circles in a graphics object (in setup) and then drawing them later.

However, I keep running into the issue where only part of the circle is dithered, and the rest looks normal. Any suggestions are welcome as I'm really stumped as to what is going on.

example

setup():

let circs;
function setup() {
  //...
  createCanvas(1000,1000);
  let size = 200;
  circs = [];
  circs.push({
    gfx: createGraphics(size, size),
    size: size,
    color: color(random(255))
  });
  for (let i = 0; i < circs.length; i++)
    dither(circs[i]);
  // ...
}

draw():

function draw() {
  if (!paused) {
    background(bg);
    drawShadow(4);  // just a call to the drawingContext shadow

    for (let i = 0; i < circs.length; i++) {
      push();
      translate(width / 2, height / 2);
      imageMode(CENTER);
      image(circs[i].gfx, 0, 0);
      pop();
    }
  }
}

floyd-steinberg - based on https://openprocessing.org/sketch/1192123

function index(x, y, g) {
  return (x + y * g.width) * 4;
}

function dither(g) {
  g.loadPixels();
  for (let y = 0; y < g.height - 1; y++) {
    for (let x = 1; x < g.width - 1; x++) {
      let oldr = g.pixels[index(x, y, g)];
      let oldg = g.pixels[index(x, y, g) + 1];
      let oldb = g.pixels[index(x, y, g) + 2];

      let factor = 1.0;
      let newr = round((factor * oldr) / 255) * (255 / factor);
      let newg = round((factor * oldg) / 255) * (255 / factor);
      let newb = round((factor * oldb) / 255) * (255 / factor);

      g.pixels[index(x, y, g)] = newr;
      g.pixels[index(x, y, g) + 1] = newg;
      g.pixels[index(x, y, g) + 2] = newb;

      g.pixels[index(x + 1, y, g)] += ((oldr - newr) * 7) / 16.0;
      g.pixels[index(x + 1, y, g) + 1] += ((oldr - newr) * 7) / 16.0;
      g.pixels[index(x + 1, y, g) + 2] += ((oldr - newr) * 7) / 16.0;

      g.pixels[index(x - 1, y + 1, g)] += ((oldr - newr) * 3) / 16.0;
      g.pixels[index(x - 1, y + 1, g) + 1] += ((oldr - newr) * 3) / 16.0;
      g.pixels[index(x - 1, y + 1, g) + 2] += ((oldr - newr) * 3) / 16.0;

      g.pixels[index(x, y + 1, g)] += ((oldr - newr) * 5) / 16.0;
      g.pixels[index(x, y + 1, g) + 1] += ((oldr - newr) * 5) / 16.0;
      g.pixels[index(x, y + 1, g) + 2] += ((oldr - newr) * 5) / 16.0;

      g.pixels[index(x + 1, y + 1, g)] += ((oldr - newr) * 1) / 16.0;
      g.pixels[index(x + 1, y + 1, g) + 1] += ((oldr - newr) * 1) / 16.0;
      g.pixels[index(x + 1, y + 1, g) + 2] += ((oldr - newr) * 1) / 16.0;
    }
  }
  g.updatePixels();
}

I'm not sure what I'm missing as the dithering algorithm loops over the height and width and then should be updating, but I think I'm missing something.


Solution

  • p5.Graphics objects have a pixelDensity inherited from the sketch. When the pixel density is > 1 as it is for high DPI displays you need to account for this when you are computing your pixels indices:

    function index(x, y, g) {
      const d = g.pixelDensity();
      return (x + y * g.width * d) * 4;
    }
    

    And when you are processing pixels you will need to double the maximum values for x and y.

    Here's a demonstration of the effects of pixelDensity (and whether or not you handle it):

    let g;
    function setup() {
      createCanvas(400, 400);
      g = createGraphics(width, height);
      redrawGraphics();
      noLoop();
      setInterval(
        () => {
          redrawGraphics(frameCount % 2);
          redraw();
        },
        2000
      );
    }
    
    function index(x, y, g, d) {
      return (x + y * g.width * d) * 4;
    }
    
    function redrawGraphics(hdpi) {
      const d = hdpi ? pixelDensity() : 1;
      
      g.background(0);
      g.loadPixels();
      for (let y = 0; y < height * 2; y++) {
        for (let x = 0; x < width * 2; x++) {
          let ix = index(x, y, g, d);
          let r = map(sin((x - y) / width * TWO_PI), -1, 1, 0, 255);
          
          g.pixels[ix] = r;
          g.pixels[ix + 1] = 0;
          g.pixels[ix + 2] = 0;
          g.pixels[ix + 3] = 255;
        }
      }
      g.updatePixels();
    }
    
    function draw() {
      image(g, 0, 0);
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>