Fractals

Julia set

The same squaring rule, but now the constant is fixed and the starting point is the pixel. Every value of that constant gives a completely different shape, and the cursor sweeps through them.

EXP-037

Move the cursor over the canvas — it is part of the simulation.

Drag your finger across the canvas — it is part of the simulation.

What to look for

Move slowly near the edge of the shape. There is a boundary in cursor position where the figure stops being one connected object and explodes into disconnected dust. That transition is the Mandelbrot boundary, seen from the other side.

The maths

z_{n+1} = z_n² + c, z₀ = pixel

Identical iteration to Mandelbrot, with the roles swapped: there c was the pixel, here c is a constant you choose and the pixel is where the orbit starts.

c ∈ M ⟺ J_c connected

The two sets are linked. If c is inside the Mandelbrot set the Julia set is one connected piece; if it is outside the Julia set shatters into dust.

How it is built

Same escape-time loop and same smoothing as the Mandelbrot, same caching by view key. The only difference is which of the two numbers is held fixed, which is about four characters of code.

The code, step by step

Cut straight from lab.js. These are the real lines that run above, not a simplified version.

01

The cursor is the constant

const th = M.in ? (M.x / w) * TAU : t * 0.12;
const radial = M.in ? (M.y / h - 0.5) * 0.30 : 0;
const k1 = (1 + radial);
const jr = (0.5 * Math.cos(th) - 0.25 * Math.cos(2 * th)) * k1;
const ji = (0.5 * Math.sin(th) - 0.25 * Math.sin(2 * th)) * k1;

Cursor position maps straight onto the complex number c. That is why the shape reorganises continuously as you move instead of just panning: you are not moving the camera, you are changing which fractal exists.

02

Same loop, swapped roles

let x = x0, y = y0, n = 0, x2 = x * x, y2 = y * y;
// Igual que Mandelbrot pero c es fijo y z arranca en el píxel
while (x2 + y2 <= 4 && n < IT) { y = 2 * x * y + ji; x = x2 - y2 + jr; x2 = x * x; y2 = y * y; n++; }

In Mandelbrot the orbit starts at zero and c is the pixel. Here the orbit starts at the pixel and c is fixed. Two lines apart, and they generate entirely different families of shape.

Why it matters

Julia and Fatou worked these out around 1918 with no way to see them. They proved the structure existed and described it in writing, sixty years before anyone could render one.

The whole thing

Everything above, in one piece. This is the entire experiment as it lives in lab.js.

FULL

lab.js · julia

{ id: 'julia',
      name: L('Julia set', 'Conjunto de Julia'),
      note: L('Same rule, one constant. Move the cursor and the whole shape reorganises.',
              'La misma regla, una constante. Mueve el cursor y la forma entera se reorganiza.'),
      params: [
        { key: 'iter', label: L('Iterations', 'Iteraciones'), min: 40, max: 300, step: 10, def: 120 },
        { key: 'res',  label: L('Resolution', 'Resolución'),  min: 90, max: 260, step: 10, def: 170 },
        { key: 'orbit', label: L('Auto orbit', 'Órbita automática'), min: 0, max: 1, step: 1, def: 1 }
      ],
      make() {
        let img = null, off = null, key = '';
        return {
          step(ctx, w, h, t, acc, P, M) {
            const RES = P.res | 0, IT = P.iter | 0;
            // c se toma SOBRE el borde de la cardioide principal:
            //   c = ½e^{iθ} − ¼e^{2iθ}
            // Ahí viven los Julia interesantes. Mapear el cursor a todo el plano
            // deja la mayoría de las posiciones dentro, donde son manchas gordas.
            // Horizontal elige el punto del borde; vertical se aleja hacia adentro
            // (conexo) o hacia afuera (polvo disconexo).
            const th = M.in ? (M.x / w) * TAU : t * 0.12;
            const radial = M.in ? (M.y / h - 0.5) * 0.30 : 0;
            const k1 = (1 + radial);
            const jr = (0.5 * Math.cos(th) - 0.25 * Math.cos(2 * th)) * k1;
            const ji = (0.5 * Math.sin(th) - 0.25 * Math.sin(2 * th)) * k1;
            const k = [RES, IT, jr.toFixed(5), ji.toFixed(5), acc].join('|');
            if (k !== key) {
              key = k;
              if (!img || img.width !== RES) {
                img = ctx.createImageData(RES, RES);
                off = document.createElement('canvas'); off.width = off.height = RES;
              }
              const rgb = acc.split(',').map(Number), px = img.data, scale = 3.0;
              for (let j = 0; j < RES; j++) {
                const y0 = ((j / RES) - 0.5) * scale;
                for (let i = 0; i < RES; i++) {
                  const x0 = ((i / RES) - 0.5) * scale;
                  let x = x0, y = y0, n = 0, x2 = x * x, y2 = y * y;
                  // Igual que Mandelbrot pero c es fijo y z arranca en el píxel
                  while (x2 + y2 <= 4 && n < IT) { y = 2 * x * y + ji; x = x2 - y2 + jr; x2 = x * x; y2 = y * y; n++; }
                  const o = (j * RES + i) * 4;
                  if (n >= IT) { px[o] = px[o+1] = px[o+2] = 0; px[o+3] = 255; }
                  else {
                    const mu = n + 1 - Math.log(Math.log(Math.sqrt(x2 + y2))) / Math.LN2;
                    const v = Math.pow(Math.max(0, mu) / IT, 0.34);
                    // √mu en vez de mu: da ciclos también donde el escape es rápido
                    const band = 0.5 + 0.5 * Math.cos(Math.sqrt(Math.max(0, mu)) * 2.1 - 1.1);
                    const m1 = 0.30 + 0.70 * band, m2 = (1 - band) * 0.55;
                    px[o]   = Math.min(255, rgb[0] * v * m1 + 245 * v * m2);
                    px[o+1] = Math.min(255, rgb[1] * v * m1 + 248 * v * m2);
                    px[o+2] = Math.min(255, rgb[2] * v * m1 + 255 * v * m2);
                    px[o+3] = 255;
                  }
                }
              }
              off.getContext('2d').putImageData(img, 0, 0);
            }
            ctx.imageSmoothingEnabled = true;
            ctx.drawImage(off, 0, 0, w, h);
            ctx.fillStyle = `rgba(${acc},0.85)`; ctx.font = '11px monospace';
            ctx.fillText('c = ' + jr.toFixed(3) + (ji >= 0 ? ' + ' : ' − ') + Math.abs(ji).toFixed(3) + 'i', 12, h - 12);
          }
        };
      }
    },