← sketches

voronoi

Drifting Voronoi cells with shifting colors.

p5.js

  import p5 from 'p5';
  import { hslToRgb } from '../../../utils/color';

  new p5((p: p5) => {
    let pts: { x: number; y: number; vx: number; vy: number; hue: number; hueSpeed: number }[] = [];
    let buf: p5.Graphics;
    let prevCells = 0;

    const c = controls({
      cells: { label: 'cells', min: 5, max: 150, value: 50 },
      speed: { label: 'speed', min: 1, max: 30, rawValue: 10, value: 1.0, parse: (r) => parseInt(r) / 10, format: (v) => v.toFixed(1) },
      hueshift: { label: 'hue shift', min: 0, max: 100, rawValue: 50, value: 0.50, parse: (r) => parseInt(r) / 100, format: (v) => v.toFixed(2) },
      edge: { label: 'edge width', min: 1, max: 20, rawValue: 5, value: 0.05, parse: (r) => parseInt(r) / 100, format: (v) => v.toFixed(2) },
      res: { label: 'resolution', min: 1, max: 8, value: 4 },
    });

    function initPoints(count: number) {
      pts = [];
      for (let i = 0; i < count; i++) {
        pts.push({
          x: p.random(p.width),
          y: p.random(p.height),
          vx: p.random(-1, 1),
          vy: p.random(-1, 1),
          hue: (i / count) * 360,
          hueSpeed: p.random(0.2, 0.8),
        });
      }
      prevCells = count;
    }

    function rebuildBuffer() {
      buf = p.createGraphics(Math.ceil(p.width / c.res), Math.ceil(p.height / c.res));
      buf.pixelDensity(1);
    }

    p.setup = () => {
      const el = document.getElementById('canvas')!.parentElement!;
      const cv = p.createCanvas(el.clientWidth, el.clientHeight);
      cv.parent(el);
      document.getElementById('canvas')?.remove();
      p.noSmooth();
      rebuildBuffer();
      initPoints(c.cells);
    };

    p.draw = () => {
      // Sync point count
      if (c.cells !== prevCells) {
        if (c.cells > prevCells) {
          for (let i = prevCells; i < c.cells; i++) {
            pts.push({
              x: p.random(p.width),
              y: p.random(p.height),
              vx: p.random(-1, 1),
              vy: p.random(-1, 1),
              hue: (i / c.cells) * 360,
              hueSpeed: p.random(0.2, 0.8),
            });
          }
        } else {
          pts.length = c.cells;
        }
        prevCells = c.cells;
      }

      // Rebuild buffer if resolution changed
      const targetW = Math.ceil(p.width / c.res);
      if (buf.width !== targetW) rebuildBuffer();

      const scale = c.res;
      const bw = buf.width;
      const bh = buf.height;
      const t = p.frameCount * 0.01;
      const edgeThreshold = c.edge;
      const numPts = pts.length;

      buf.loadPixels();
      const px = buf.pixels;

      for (let by = 0; by < bh; by++) {
        for (let bx = 0; bx < bw; bx++) {
          const wx = bx * scale;
          const wy = by * scale;

          let minDist = Infinity;
          let secondDist = Infinity;
          let closest = 0;

          for (let i = 0; i < numPts; i++) {
            const dx = wx - pts[i].x;
            const dy = wy - pts[i].y;
            const dist = dx * dx + dy * dy;
            if (dist < minDist) {
              secondDist = minDist;
              minDist = dist;
              closest = i;
            } else if (dist < secondDist) {
              secondDist = dist;
            }
          }

          const pt = pts[closest];
          const d1 = Math.sqrt(minDist);
          const d2 = Math.sqrt(secondDist);
          const edgeDist = (d2 - d1) / (d2 + d1 + 1);

          const h = ((pt.hue + d1 * 0.3 + Math.sin(t + wx * 0.01) * 20) % 360 + 360) % 360;
          const s = (70 + edgeDist * 25) / 100;
          const l = edgeDist < edgeThreshold
            ? (5 + edgeDist * (100 / (edgeThreshold / 0.05))) / 100
            : (15 + edgeDist * 35) / 100;

          const [r, g, b] = hslToRgb(h / 360, s, l);
          const idx = (by * bw + bx) * 4;
          px[idx] = r;
          px[idx + 1] = g;
          px[idx + 2] = b;
          px[idx + 3] = 255;
        }
      }

      buf.updatePixels();
      p.image(buf, 0, 0, p.width, p.height);

      const spd = c.speed;
      const hueSpd = c.hueshift;
      for (const pt of pts) {
        pt.x += (pt.vx + Math.sin(t * 2 + pt.hue) * 0.3) * spd;
        pt.y += (pt.vy + Math.cos(t * 2 + pt.hue) * 0.3) * spd;
        if (pt.x < 0 || pt.x > p.width) pt.vx *= -1;
        if (pt.y < 0 || pt.y > p.height) pt.vy *= -1;
        pt.x = p.constrain(pt.x, 0, p.width);
        pt.y = p.constrain(pt.y, 0, p.height);
        pt.hue = (pt.hue + pt.hueSpeed * hueSpd) % 360;
      }
    };


  });