About this technique →
mandelbrot/sketch.js
// Mandelbrot Set
//
// For each pixel, map screen coordinates to complex plane, iterate z = z² + c
// starting at z = 0. Color by smooth iteration count using log-log escape.

export const PARAMS = {
  centerX:    { value: -0.5,  min: -2,    max: 1,    step: 0.0001, label: "Center X",       folder: "View" },
  centerY:    { value: 0,     min: -1.5,  max: 1.5,  step: 0.0001, label: "Center Y",       folder: "View" },
  zoom:       { value: 1.0,   min: 0.1,   max: 100,  step: 0.001,  label: "Zoom",           folder: "View" },
  iterations: { value: 200,   min: 50,    max: 1000, step: 10,     label: "Max Iterations", folder: "Performance" },
  step:       { value: 2, options: ["1", "2", "3", "4"], label: "Pixel Step", folder: "Performance", rebuildOnChange: true },
  hue:        { value: 200,   min: 0,     max: 360,  step: 1,      label: "Base Hue",       folder: "Appearance" },
  hueSpread:  { value: 180,   min: 0,     max: 360,  step: 1,      label: "Hue Spread",     folder: "Appearance" },
};

// HSL to RGB (h: 0-360, s/l: 0-1) → [r,g,b] 0-255
function hslToRgb(h, s, l) {
  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),
  ];
}

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

export function sketchDraw(p, w, h, params) {
  const { centerX, centerY, zoom, iterations, hue, hueSpread } = params;
  const step = parseInt(params.step, 10) || 2;
  const maxIter = iterations | 0;

  // Scale: at zoom=1 the full width spans about 3.5 units
  const scale = 3.5 / (zoom * w);

  p.pixelDensity(1);
  p.loadPixels();

  for (let py = 0; py < h; py += step) {
    for (let px = 0; px < w; px += step) {
      // Map pixel to complex coordinate
      const cr = (px - w / 2) * scale + centerX;
      const ci = (py - h / 2) * scale + centerY;

      let zr = 0.0, zi = 0.0;
      let n = 0;
      while (n < maxIter) {
        const zr2 = zr * zr;
        const zi2 = zi * zi;
        if (zr2 + zi2 > 4.0) break;
        zi = 2 * zr * zi + ci;
        zr = zr2 - zi2 + cr;
        n++;
      }

      let r, g, b;
      if (n === maxIter) {
        r = 0; g = 0; b = 0;
      } else {
        // Smooth iteration count
        const log2 = Math.log(2);
        const absZ = Math.sqrt(zr * zr + zi * zi);
        const smooth = n + 1 - Math.log(Math.log(absZ) / log2) / log2;
        const t = (smooth / maxIter) * hueSpread;
        const h2 = ((hue + t * 3) % 360 + 360) % 360;
        const sat = 0.75;
        const lit = 0.45 + 0.2 * Math.sin(smooth * 0.3);
        [r, g, b] = hslToRgb(h2, sat, Math.min(0.85, Math.max(0.05, lit)));
      }

      // Fill pixel block
      for (let dy = 0; dy < step && py + dy < h; dy++) {
        for (let dx = 0; dx < step && px + dx < w; dx++) {
          const idx = ((py + dy) * w + (px + dx)) * 4;
          p.pixels[idx]     = r;
          p.pixels[idx + 1] = g;
          p.pixels[idx + 2] = b;
          p.pixels[idx + 3] = 255;
        }
      }
    }
  }

  p.updatePixels();
}