For lack of a better place, let me include it here: an elegant implementation of the Mandelbrot set, created in part using assistance from the AI (Claude).

Yes, you can pan and zoom.

And the source code:

<!DOCTYPE html>
<html>
<head>
  <title>Mandelbrot Set</title>
  <style>
    canvas
    {
      border: 1px solid black;
    }
    .tooltip
    {
      position: absolute;
      background-color: rgba(255, 240, 230, 0.8);
      color: black;
      padding: 2px;
      border-radius: 3px;
      border: 1px solid #333;
      font-size: 12px;
      pointer-events: none;
      opacity: 0;
      transition: opacity 0.3s;
      font-family: monospace;
    }
  </style>
</head>
<body>
  <canvas id="canvas" width="800" height="600"></canvas>
  <div id="tooltip" class="tooltip"></div>
  <script>
    function doWork(event)
    {
      function hslToRgb(h, s, l)
      {
        let r, g, b;
      
        if (s === 0)
        {
          r = g = b = l;
        }
        else
        {
          const hue2rgb = (p, q, t) =>
          {
            if (t < 0) t += 1;
            if (t > 1) t -= 1;
            if (t < 1 / 6) return p + (q - p) * 6 * t;
            if (t < 1 / 2) return q;
            if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
            return p;
          };

          const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
          const p = 2 * l - q;
          r = hue2rgb(p, q, h + 1 / 3);
          g = hue2rgb(p, q, h);
          b = hue2rgb(p, q, h - 1 / 3);
        }

        return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
      }

      const { width, height, zoom, offsetX, offsetY } = event.data;
      const size = Math.min(width, height);
      const maxIterations = 100;
      const pixels = new Uint8ClampedArray(width * height * 4);

      for (let x = 0; x < width; x++)
      {
        for (let y = 0; y < height; y++)
        {
          let zx = 0;
          let zy = 0;
          let cx = (x - width / 2) / (0.5 * zoom * size) + offsetX;
          let cy = (y - height / 2) / (0.5 * zoom * size) + offsetY;
          let i = 0;

          while (zx * zx + zy * zy < 4 && i < maxIterations)
          {
            const tempX = zx * zx - zy * zy + cx;
            zy = 2 * zx * zy + cy;
            zx = tempX;
            i++;
          }

          const index = (y * width + x) * 4;
          if (i === maxIterations)
          {
            pixels[index] = 0;
            pixels[index + 1] = 0;
            pixels[index + 2] = 0;
          }
          else
          {
            const hue = i * 5;
            const rgb = hslToRgb(hue / 360, 1, 0.5);
            pixels[index] = rgb[0];
            pixels[index + 1] = rgb[1];
            pixels[index + 2] = rgb[2];
          }
          pixels[index + 3] = 255;
        }
      }

      postMessage({ pixels: pixels }, [pixels.buffer]);
    }


    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');

    let zoom = 1;
    let offsetX = -0.5;
    let offsetY = 0;
    let isDragging = false;
    let lastX = 0;
    let lastY = 0;
    let redrawTimeout = null;

    const worker = new Worker(window.URL.createObjectURL(
                   new Blob(["onmessage=" + doWork.toString()], {type: "text/javascript"})));

    worker.onmessage = (event) =>
    {
      const imageData = new ImageData(event.data.pixels, canvas.width, canvas.height);
      ctx.putImageData(imageData, 0, 0);
      document.body.style.cursor = 'default';
    };

    function drawMandelbrot()
    {
      document.body.style.cursor = 'wait';
      worker.postMessage({
        width: canvas.width,
        height: canvas.height,
        zoom: zoom,
        offsetX: offsetX,
        offsetY: offsetY,
      });
    }

    canvas.addEventListener('wheel', (event) =>
    {
      event.preventDefault();

      const tooltip = document.getElementById('tooltip');
      tooltip.style.opacity = 0;

      const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9;
      const mouseX = event.offsetX;
      const mouseY = event.offsetY;
      const size = Math.min(canvas.width, canvas.height);
      const dx = (mouseX - canvas.width / 2) / (0.5 * size * zoom);
      const dy = (mouseY - canvas.height / 2) / (0.5 * size * zoom);
      zoom *= zoomFactor;
      offsetX -= dx - dx * zoomFactor;
      offsetY -= dy - dy * zoomFactor;
      drawMandelbrot();
    });

    canvas.addEventListener('mousedown', (event) =>
    {
      isDragging = true;
      lastX = event.offsetX;
      lastY = event.offsetY;
    });

    let hoverTimeout = null;

    canvas.addEventListener('mousemove', (event) =>
    {
      const tooltip = document.getElementById('tooltip');
      tooltip.style.opacity = 0;
      const size = Math.min(canvas.width, canvas.height);
      if (isDragging)
      {
        const dx = (event.offsetX - lastX) / (0.5 * size * zoom);
        const dy = (event.offsetY - lastY) / (0.5 * size * zoom);
        offsetX -= dx;
        offsetY -= dy;
        lastX = event.offsetX;
        lastY = event.offsetY;

        // Clear the previous timeout
        if (redrawTimeout)
        {
          clearTimeout(redrawTimeout);
        }

        // Set a new timeout to redraw after a short delay
        redrawTimeout = setTimeout(() =>
        {
          drawMandelbrot();
        }, 16); // Adjust the delay as needed (e.g., 16ms for 60 FPS)
      }
      else
      {
        const x = (event.offsetX - canvas.width / 2) / (0.5 * zoom * size) + offsetX;
        const y = (event.offsetY - canvas.height / 2) / (0.5 * zoom * size) + offsetY;

        // Clear the previous timeout
        if (hoverTimeout)
        {
          clearTimeout(hoverTimeout);
        }

        function formatFloat(x)
        {
          if (x == 0 || (Math.abs(x) >= 0.1 && Math.abs(x) < 10000)) return x.toFixed(4);
          else return x.toExponential(3);
        }

        // Set a new timeout to show the tooltip after 1 second
        hoverTimeout = setTimeout(() =>
        {
          tooltip.textContent =
              `x:${formatFloat(x)}, y:${-formatFloat(y)}, zoom:${formatFloat(zoom)}`;

          tooltip.style.left = `${event.pageX + 10}px`;
          tooltip.style.top = `${event.pageY + 10}px`;
          tooltip.style.opacity = 1;
        }, 1000);
      }
    });

    canvas.addEventListener('mouseout', () =>
    {
      // Clear the timeout and hide the tooltip when the mouse leaves the canvas
      if (hoverTimeout)
      {
        clearTimeout(hoverTimeout);
      }
      const tooltip = document.getElementById('tooltip');
      tooltip.style.opacity = 0;
    });

    canvas.addEventListener('mouseup', () =>
    {
      isDragging = false;
      if (redrawTimeout)
      {
        clearTimeout(redrawTimeout);
        drawMandelbrot();
      }
    });

    canvas.addEventListener('mouseleave', () =>
    {
      isDragging = false;
      if (redrawTimeout)
      {
        clearTimeout(redrawTimeout);
        drawMandelbrot();
      }
    });

    window.addEventListener('keydown', function(e)
    {
      if (e.ctrlKey && e.key === '0')
      {
        zoom = 1;
        offsetX = -0.5;
        offsetY = 0;
        drawMandelbrot();
      }
    });

    drawMandelbrot();
  </script>
</body>
</html>