Voronoi stippling

This notebook applies a weighted variant of Lloyd’s algorithm to implement stippling. Points are first positioned randomly using rejection sampling, then at each iteration, the Voronoi cell centroids are weighted by the lightness of the contained pixels. This technique is based on Weighted Voronoi Stippling by Adrian Secord; see also posts by Muhammad Firmansyah Kasim, Egor Larionov and Noah Veltman.

const height = data.height;
const n = Math.round(width * height / 40);
const canvas = display(document.createElement("canvas"));
canvas.width = width * devicePixelRatio;
canvas.height = height * devicePixelRatio;
canvas.style.width = `${width}px`;
const context = canvas.getContext("2d");
context.scale(devicePixelRatio, devicePixelRatio);
const worker = new Worker(script, {type: "module"});

function messaged({data: points}) {
  context.fillStyle = "#fff";
  context.fillRect(0, 0, width, height);
  context.beginPath();
  for (let i = 0, n = points.length; i < n; i += 2) {
    const x = points[i], y = points[i + 1];
    context.moveTo(x + 1.5, y);
    context.arc(x, y, 1.5, 0, 2 * Math.PI);
  }
  context.fillStyle = "#000";
  context.fill();
}

invalidation.then(() => worker.terminate());
worker.addEventListener("message", messaged);
worker.postMessage({data, width, height, n});
const image = await FileAttachment("data/obama.png").image();
const height = Math.round(width * image.height / image.width);
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
context.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
const {data: rgba} = context.getImageData(0, 0, width, height);
const data = new Float64Array(width * height);
for (let i = 0, n = rgba.length / 4; i < n; ++i) data[i] = Math.max(0, 1 - rgba[i * 4] / 254);
data.width = width;
data.height = height;
✎ Suggest changes to this page