Skip to content

Zoomable Mandelbrot Set ​

The Mandelbrot set, shaded by the distance estimate to its boundary, as a GLSL fill. Scroll to zoom and drag to pan, with d3-zoom. The zoom is passed to the shader as numbers, which become uniforms, so it isn't recompiled. After Inigo Quilez's shader.

js
const width = 640;
const height = Math.round(width * 0.86);
const dpr = devicePixelRatio;
const renderer = cm.webgl({width, height});
const root = cm.flow().renderer(renderer);
const node = renderer.node();

// The distance from c to the set, or 0 inside it.
const estimate = cm.glsl`float estimate(vec2 c) {
  float c2 = dot(c, c);
  if (256.0 * c2 * c2 - 96.0 * c2 + 32.0 * c.x - 3.0 < 0.0) return 0.0;
  if (16.0 * (c2 + 2.0 * c.x + 1.0) - 1.0 < 0.0) return 0.0;
  vec2 z = vec2(0.0);
  vec2 dz = vec2(0.0);
  for (int i = 0; i < 300; i++) {
    dz = 2.0 * vec2(z.x * dz.x - z.y * dz.y, z.x * dz.y + z.y * dz.x) + vec2(1.0, 0.0);
    z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;
    if (dot(z, z) > 1024.0) return 0.5 * sqrt(dot(z, z) / dot(dz, dz)) * log(dot(z, z));
  }
  return 0.0;
}`;

function draw({x, y, k}) {
  const fill = cm.glsl`vec4 fill(vec2 coord, vec4 color) {
    vec2 p = vec2(coord.x, ${height * dpr} - coord.y) / ${dpr};
    vec2 uv = (p - vec2(${x}, ${y})) / ${k} / vec2(${width}, ${height});
    vec2 c = vec2(-2.18 + 3.0 * uv.x, -1.29 + 2.58 * uv.y);
    float b = clamp(pow(4.0 * ${estimate}(c) * ${k}, 0.2), 0.0, 1.0);
    return vec4(vec3(b), 1.0);
  }`;
  root.append(cm.clear, {fill: "black"});
  root.append(cm.rect, {x: 0, y: 0, width, height, fill});
  root.render();
}

node.style.cursor = "grab";
d3.select(node).call(
  d3
    .zoom()
    .scaleExtent([1, 1e5])
    .on("zoom", (event) => draw(event.transform)),
);

draw(d3.zoomIdentity);

document.body.append(node);