About this technique →
voronoi-2d/sketch.js
// Voronoi 2D
//
// Classic 2D Voronoi diagram: scatter N seed points, color each pixel
// by its nearest seed using the chosen distance metric.
//
// Distance metrics:
//   Euclidean  — circular cells (standard Voronoi)
//   Manhattan  — diamond-shaped cells
//   Chebyshev  — square cells
//
// Performance: sample every `step` pixels and fill a step×step block.

export const PARAMS = {
  count:     { value: 30, min: 5, max: 200, step: 1, label: "Seed Points", folder: "Structure", rebuildOnChange: true },
  metric:    { value: "Euclidean", options: ["Euclidean", "Manhattan", "Chebyshev"], label: "Distance Metric", folder: "Structure" },
  step:      { value: 4, min: 1, max: 16, step: 1, label: "Sample Step (px)", folder: "Performance" },
  hue:       { value: 200, min: 0, max: 360, step: 1, label: "Base Hue", folder: "Appearance" },
  hueSpread: { value: 240, min: 0, max: 360, step: 1, label: "Hue Spread", folder: "Appearance" },
  showSeeds: { value: true, label: "Show Seed Points", folder: "Appearance" },
  showEdges: { value: false, label: "Show Cell Edges", folder: "Appearance" },
};

// HSL to RGB conversion (all values 0–255)
function hslToRgb(h, s, l) {
  // h: 0-360, s: 0-1, l: 0-1
  const c = (1 - Math.abs(2 * l - 1)) * s;
  const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
  const m = l - c / 2;
  let r = 0, g = 0, b = 0;
  if (h < 60)       { r = c; g = x; b = 0; }
  else if (h < 120) { r = x; g = c; b = 0; }
  else if (h < 180) { r = 0; g = c; b = x; }
  else if (h < 240) { r = 0; g = x; b = c; }
  else if (h < 300) { r = x; g = 0; b = c; }
  else              { r = c; g = 0; b = x; }
  return [
    Math.round((r + m) * 255),
    Math.round((g + m) * 255),
    Math.round((b + m) * 255),
  ];
}

// Distance functions
function distEuclidean(x1, y1, x2, y2) {
  const dx = x1 - x2, dy = y1 - y2;
  return dx * dx + dy * dy; // squared is fine for comparison
}
function distManhattan(x1, y1, x2, y2) {
  return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
function distChebyshev(x1, y1, x2, y2) {
  return Math.max(Math.abs(x1 - x2), Math.abs(y1 - y2));
}

// Module-level state — p5 runtime does NOT pass state to sketchDraw
let state;

export function sketchSetup(p, w, h) {
  p.pixelDensity(1);

  // Generate max possible seeds up front; sketchDraw will slice to params.count
  const MAX_SEEDS = 200;
  const seeds = [];
  for (let i = 0; i < MAX_SEEDS; i++) {
    seeds.push({
      x: p.random(w),
      y: p.random(h),
      hueOffset: p.random(360),
    });
  }

  state = { seeds, w, h };
  return state;
}

export function sketchDraw(p, w, h, params) {
  if (!state) return;
  const { seeds } = state;
  const count = Math.min(params.count, seeds.length);
  const activeSeed = seeds.slice(0, count);

  const step = params.step;

  let distFn;
  if (params.metric === "Manhattan")  distFn = distManhattan;
  else if (params.metric === "Chebyshev") distFn = distChebyshev;
  else distFn = distEuclidean;

  // pixelDensity(1) MUST be called before loadPixels so pixel array is sized correctly
  p.pixelDensity(1);
  p.loadPixels();

  for (let y = 0; y < h; y += step) {
    for (let x = 0; x < w; x += step) {
      // Find nearest seed
      let minDist = Infinity;
      let nearestIdx = 0;
      for (let i = 0; i < count; i++) {
        const d = distFn(x, y, activeSeed[i].x, activeSeed[i].y);
        if (d < minDist) {
          minDist = d;
          nearestIdx = i;
        }
      }

      // Color from nearest seed's hue — hueOffset is already 0-360, spread maps it to hueSpread range
      const hue = (params.hue + (activeSeed[nearestIdx].hueOffset / 360) * params.hueSpread) % 360;
      const [r, g, b] = hslToRgb(hue, 0.7, 0.45);

      // Edge detection (optional): check neighbor pixel
      let isEdge = false;
      if (params.showEdges && (x + step < w || y + step < h)) {
        const nx = x + step, ny = y;
        let minD2 = Infinity, nearIdx2 = 0;
        for (let i = 0; i < count; i++) {
          const d = distFn(nx, ny, activeSeed[i].x, activeSeed[i].y);
          if (d < minD2) { minD2 = d; nearIdx2 = i; }
        }
        if (nearIdx2 !== nearestIdx) isEdge = true;

        if (!isEdge) {
          const nx2 = x, ny2 = y + step;
          let minD3 = Infinity, nearIdx3 = 0;
          for (let i = 0; i < count; i++) {
            const d = distFn(nx2, ny2, activeSeed[i].x, activeSeed[i].y);
            if (d < minD3) { minD3 = d; nearIdx3 = i; }
          }
          if (nearIdx3 !== nearestIdx) isEdge = true;
        }
      }

      const fr = isEdge ? 255 : r;
      const fg = isEdge ? 255 : g;
      const fb = isEdge ? 255 : b;

      // Fill block
      for (let dy = 0; dy < step && y + dy < h; dy++) {
        for (let dx = 0; dx < step && x + dx < w; dx++) {
          const idx = ((y + dy) * w + (x + dx)) * 4;
          p.pixels[idx]     = fr;
          p.pixels[idx + 1] = fg;
          p.pixels[idx + 2] = fb;
          p.pixels[idx + 3] = 255;
        }
      }
    }
  }

  p.updatePixels();

  // Draw seed points
  if (params.showSeeds) {
    p.noStroke();
    p.fill(255);
    for (let i = 0; i < count; i++) {
      p.circle(activeSeed[i].x, activeSeed[i].y, 8);
    }
    p.fill(0);
    for (let i = 0; i < count; i++) {
      p.circle(activeSeed[i].x, activeSeed[i].y, 4);
    }
  }
}