Take a number, square it, add the one you started with. Repeat. Some numbers stay near the origin forever and some run off to infinity, and the border between those two sets is this.
EXP-036
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 the cursor to drift the view and raise the zoom. The default centre is a Misiurewicz point, where the boundary is self-similar: keep zooming and you keep finding the same structure at every scale. Raise the iterations when the detail starts to smear.
The maths
z_{n+1} = z_n² + c, z₀ = 0
The entire definition. c is the pixel you are looking at, treated as a complex number. There is nothing else in the rule.
|z| > 2 ⟹ escapes
A proven bound: once the orbit leaves the disc of radius two it can never come back. That is what makes the set computable at all.
μ = n + 1 − log₂(log|z|)
Smooth escape count. Using the raw integer n gives visible bands; this continuous version removes them and is why the gradients look clean.
How it is built
The set is rendered into an ImageData buffer and scaled up. The interesting part is the caching: the buffer is only recomputed when the view actually changes, so panning is expensive and sitting still is free. Without that this would eat a full core.
The code, step by step
Cut straight from lab.js. These are the real lines that run above, not a simplified version.
01
The whole fractal, one line
while (x2 + y2 <= 4 && n < IT) { y = 2 * x * y + y0; x = x2 - y2 + x0; x2 = x * x; y2 = y * y; n++; }
Complex multiplication written out in real arithmetic, which avoids allocating objects per pixel. x2 and y2 are carried between iterations so each step costs three multiplications instead of five.
02
Smooth the escape count
const mu = n + 1 - Math.log(Math.log(Math.sqrt(x2 + y2))) / Math.LN2;
const v = Math.pow(Math.max(0, mu) / IT, 0.32);
// Paleta cíclica: una rampa plana esconde los filamentos,
// los ciclos los revelan como capas de cebolla.
// √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.0 - 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;
The raw iteration count is an integer, so colouring by it produces hard concentric bands. Subtracting the log of the log of the final magnitude turns it continuous, and the gradients come out clean.
03
Cache by view
const k = [RES, IT, P.zoom.toFixed(3), cx.toFixed(12), cy.toFixed(12), acc].join('|');
if (k !== key) {
The frame is only recomputed when something that affects it changed: resolution, iterations, zoom, centre or theme colour. Sitting still costs one drawImage. Without this the fractal would burn a core continuously.
Why it matters
Mandelbrot could only see this because he had a computer at IBM in 1980. It is the clearest case of a shape too complex for anyone to have drawn by hand, generated by a rule short enough to memorise.
The whole thing
Everything above, in one piece. This is the entire experiment as it lives in lab.js.
FULL
lab.js · mandelbrot
{ id: 'mandelbrot',
name: L('Mandelbrot set', 'Conjunto de Mandelbrot'),
note: L('One line of algebra, repeated. The edge never stops having detail.',
'Una línea de álgebra, repetida. El borde nunca deja de tener detalle.'),
params: [
{ key: 'zoom', label: L('Zoom', 'Zoom'), min: 0, max: 14, step: 0.05, def: 0 },
{ key: 'iter', label: L('Iterations', 'Iteraciones'), min: 40, max: 400, step: 10, def: 140 },
{ key: 'res', label: L('Resolution', 'Resolución'), min: 90, max: 260, step: 10, def: 170 }
],
make() {
let img = null, off = null, key = '';
let cx = -0.743643887037151, cy = 0.13182590420533; // punto de Misiurewicz
return {
reset() { cx = -0.743643887037151; cy = 0.13182590420533; },
step(ctx, w, h, t, acc, P, M) {
const RES = P.res | 0, IT = P.iter | 0;
const scale = 3.2 / Math.pow(2, P.zoom);
// El cursor reencuadra: su posición se vuelve el nuevo centro
if (M.in) {
// Paneo suave y acotado: sin el clamp el centro se va fuera del
// conjunto en un par de segundos y la vista queda en negro plano.
cx += ((M.x / w - 0.5) * scale) * 0.006;
cy += ((M.y / h - 0.5) * scale * (h / w)) * 0.006;
cx = Math.max(-2.3, Math.min(0.9, cx));
cy = Math.max(-1.3, Math.min(1.3, cy));
}
const k = [RES, IT, P.zoom.toFixed(3), cx.toFixed(12), cy.toFixed(12), 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;
for (let j = 0; j < RES; j++) {
const y0 = cy + ((j / RES) - 0.5) * scale;
for (let i = 0; i < RES; i++) {
const x0 = cx + ((i / RES) - 0.5) * scale;
let x = 0, y = 0, n = 0, x2 = 0, y2 = 0;
// z ← z² + c, hasta escapar del disco de radio 2
while (x2 + y2 <= 4 && n < IT) { y = 2 * x * y + y0; x = x2 - y2 + x0; 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 {
// Suavizado del contador: quita las bandas del escape entero
const mu = n + 1 - Math.log(Math.log(Math.sqrt(x2 + y2))) / Math.LN2;
const v = Math.pow(Math.max(0, mu) / IT, 0.32);
// Paleta cíclica: una rampa plana esconde los filamentos,
// los ciclos los revelan como capas de cebolla.
// √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.0 - 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('zoom ×' + Math.pow(2, P.zoom).toFixed(0), 12, h - 12);
}
};
}
},