All articles
12 min read

Fractal Art: How to Create Infinite Mathematical Art With Code

fractal artgenerative artcreative codingmath artjavascripttutorial

Fractal art sits at the intersection of mathematics and visual beauty. A fractal is a shape that contains smaller copies of itself — zoom in, and you find the same patterns repeating at every scale, forever. This property, called self-similarity, produces some of the most complex and mesmerizing images in all of art, and every one of them can be generated with surprisingly simple code.

This guide will take you from "what is a fractal?" to rendering your own fractal art in a browser. No math degree required — just curiosity and a text editor.

What makes a fractal a fractal?

The word "fractal" was coined by mathematician Benoît Mandelbrot in 1975. He was studying shapes that traditional geometry couldn't describe — coastlines, mountain ridges, cloud boundaries, lightning bolts. These shapes share a strange property: their complexity doesn't simplify when you zoom in. A piece of coastline at 1km scale looks just as jagged as a piece at 1m scale.

In mathematical terms, fractals have three key properties:

  • Self-similarity — the shape contains smaller versions of itself
  • Infinite detail — you can zoom in forever and find new structure
  • Fractional dimension — they exist between traditional dimensions (a fractal curve is more than a line but less than a plane)

For artists, the important takeaway is this: simple rules, applied recursively, produce infinite complexity. That's what makes fractals perfect for creative coding.

The Mandelbrot set: where it all begins

The Mandelbrot set is the most famous fractal. It's defined by a deceptively simple formula: for each point c in the complex plane, repeatedly calculate z = z² + c starting from z = 0. If the value stays bounded (doesn't fly off to infinity), the point is in the set. If it escapes, color it based on how quickly it escaped.

Here's a complete Mandelbrot renderer in plain JavaScript and Canvas:

const canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const img = ctx.createImageData(800, 600);

const maxIter = 100;
const xMin = -2.5, xMax = 1, yMin = -1.2, yMax = 1.2;

for (let px = 0; px < 800; px++) {
  for (let py = 0; py < 600; py++) {
    let x0 = xMin + (px / 800) * (xMax - xMin);
    let y0 = yMin + (py / 600) * (yMax - yMin);
    let x = 0, y = 0, iter = 0;

    while (x * x + y * y <= 4 && iter < maxIter) {
      let xTemp = x * x - y * y + x0;
      y = 2 * x * y + y0;
      x = xTemp;
      iter++;
    }

    const i = (py * 800 + px) * 4;
    if (iter === maxIter) {
      img.data[i] = img.data[i+1] = img.data[i+2] = 0;
    } else {
      const hue = (iter / maxIter) * 360;
      const [r, g, b] = hslToRgb(hue / 360, 0.8, 0.5);
      img.data[i] = r; img.data[i+1] = g; img.data[i+2] = b;
    }
    img.data[i+3] = 255;
  }
}
ctx.putImageData(img, 0, 0);

function hslToRgb(h, s, l) {
  const a = s * Math.min(l, 1 - l);
  const f = n => {
    const k = (n + h * 12) % 12;
    return l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
  };
  return [Math.round(f(0)*255), Math.round(f(8)*255), Math.round(f(4)*255)];
}

That's ~40 lines for an infinite mathematical landscape. The magic is in the coloring: points that escape quickly get one color, points that take many iterations get another, and points that never escape (the black region) form the iconic Mandelbrot shape.

Julia sets: the Mandelbrot's infinite family

Every single point in the Mandelbrot set corresponds to a unique Julia set. The formula is the same — z = z² + c — but instead of varying c for each pixel and starting from z = 0, you fix c and vary the starting z for each pixel.

To turn the Mandelbrot renderer into a Julia set renderer, change just two lines:

// Instead of: let x0 = ..., y0 = ..., x = 0, y = 0;
// Use a fixed c and let each pixel be the starting z:
const cX = -0.7, cY = 0.27015; // try different values!
// ...
let x = xMin + (px / 800) * (xMax - xMin);
let y = yMin + (py / 600) * (yMax - yMin);

while (x * x + y * y <= 4 && iter < maxIter) {
  let xTemp = x * x - y * y + cX;
  y = 2 * x * y + cY;
  x = xTemp;
  iter++;
}

Different values of c produce wildly different Julia sets. Try (-0.8, 0.156) for spiraling dendrites, (0.355, 0.355) for a starfish-like shape, or (-0.4, 0.6) for organic branching structures. Each one is an entirely different world living inside the same simple equation.

Recursive trees: fractals from branching

Not all fractals come from complex number math. Some of the most natural-looking fractals emerge from simple branching rules. A fractal tree starts with a trunk, which splits into two branches, each of which splits into two more, and so on — exactly the kind of growth you see in real trees, rivers, and blood vessels.

const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');

function drawBranch(x, y, len, angle, depth) {
  if (depth === 0 || len < 2) return;

  const x2 = x + Math.cos(angle) * len;
  const y2 = y + Math.sin(angle) * len;

  ctx.strokeStyle = `hsl(${120 + depth * 15}, 60%, ${30 + depth * 5}%)`;
  ctx.lineWidth = depth * 0.8;
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(x2, y2);
  ctx.stroke();

  const spread = 0.4 + Math.random() * 0.2;
  const shrink = 0.65 + Math.random() * 0.1;

  drawBranch(x2, y2, len * shrink, angle - spread, depth - 1);
  drawBranch(x2, y2, len * shrink, angle + spread, depth - 1);
}

ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, 600, 600);
drawBranch(300, 580, 120, -Math.PI / 2, 12);

This is recursive art at its purest. Each branch is a smaller version of the whole tree. Add randomness to the angles and lengths and you get organic, never-repeating trees. This is exactly how several micro-worlds on Lumitree generate their visuals.

L-systems: fractals from string rewriting

L-systems (Lindenmayer systems) are a different approach to fractal generation, invented by biologist Aristid Lindenmayer to model plant growth. The idea: start with a string of characters, apply replacement rules repeatedly, then interpret the final string as drawing instructions.

Here's the Koch snowflake as an L-system:

// Rules: F → F+F−−F+F (replace each F with this pattern)
// F = draw forward, + = turn left 60°, − = turn right 60°

let axiom = 'F--F--F'; // starting triangle
const rules = { F: 'F+F--F+F' };
const angle = Math.PI / 3; // 60 degrees
const iterations = 4;

// Expand the string
let str = axiom;
for (let i = 0; i < iterations; i++) {
  str = str.split('').map(c => rules[c] || c).join('');
}

// Draw it
const canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 700;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, 800, 700);

let x = 50, y = 500, dir = 0;
const step = 2;
ctx.strokeStyle = '#4af';
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(x, y);

for (const c of str) {
  if (c === 'F') {
    x += Math.cos(dir) * step;
    y += Math.sin(dir) * step;
    ctx.lineTo(x, y);
  } else if (c === '+') {
    dir -= angle;
  } else if (c === '-') {
    dir += angle;
  }
}
ctx.stroke();

Change the rules and you get entirely different fractals. The Sierpinski triangle: axiom = 'A', rules A → B-A-B, B → A+B+A, angle 60°. Dragon curve: axiom = 'FX', rules X → X+YF+, Y → -FX-Y, angle 90°. Hilbert curve, Peano curve, fern-like structures — all emerge from a few characters and one or two replacement rules.

The Sierpinski triangle: simplicity creates complexity

The Sierpinski triangle demonstrates how fractals arise from the simplest possible rule. Take a triangle, remove the middle triangle, and repeat for each remaining triangle. Alternatively, use the "chaos game": pick a random point, repeatedly move it halfway toward a randomly chosen vertex, and plot each position.

const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 520;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, 600, 520);

// Three vertices of the triangle
const v = [[300, 10], [10, 510], [590, 510]];
let x = 300, y = 250; // random start

for (let i = 0; i < 50000; i++) {
  const vertex = v[Math.floor(Math.random() * 3)];
  x = (x + vertex[0]) / 2;
  y = (y + vertex[1]) / 2;

  if (i > 10) { // skip first few points
    ctx.fillStyle = `hsl(${(i * 0.01) % 360}, 70%, 60%)`;
    ctx.fillRect(x, y, 1, 1);
  }
}

50,000 random moves following one rule — "go halfway to a random vertex" — produce a perfect Sierpinski triangle. No recursion, no complex math, just randomness constrained by a simple rule. This is the deep lesson of fractal art: order and chaos aren't opposites. They're collaborators.

Coloring: where math becomes art

The mathematics of fractals produce structure. The coloring is where they become art. Here are techniques that transform raw iteration counts into stunning visuals:

  • Smooth coloring — Instead of integer iteration counts, use iter - Math.log2(Math.log2(x*x + y*y)) for continuous, band-free gradients
  • Orbit traps — Track how close the orbit passes to geometric shapes (circles, lines, crosses) and color based on minimum distance
  • Histogram coloring — Count how many pixels escape at each iteration, then distribute colors based on frequency rather than raw value
  • Multi-palette — Use different color palettes for different regions, or blend palettes based on the angle of escape
  • Interior coloring — For points inside the set, use the final orbit position or period detection to add detail to the black regions

Experiment with HSL color mapping: map iteration count to hue for rainbow effects, to lightness for dramatic contrast, or to saturation for watercolor-like softness.

Making it interactive

Static fractal images are beautiful, but interactive fractals are captivating. Add zoom-on-click to your Mandelbrot renderer:

let centerX = -0.5, centerY = 0, zoom = 1;

canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect();
  const px = e.clientX - rect.left;
  const py = e.clientY - rect.top;

  // Convert pixel to fractal coordinates
  centerX += ((px / canvas.width) - 0.5) * (3.5 / zoom);
  centerY += ((py / canvas.height) - 0.5) * (2.4 / zoom);
  zoom *= 2;

  render(); // re-render with new center and zoom
});

Each click doubles the zoom and recenters. You can dive into the boundary of the Mandelbrot set and discover spirals within spirals, seahorse valleys, miniature copies of the whole set embedded at every scale. The deeper you go, the more iterations you need — increase maxIter proportionally to Math.log2(zoom) for best results.

Fractals in nature and in code

Fractals aren't just mathematical abstractions. They're everywhere in nature:

  • Ferns — each frond is a smaller copy of the whole plant (Barnsley's fern is a famous fractal model)
  • Coastlines — Mandelbrot's original observation: coastline length depends on your measuring stick
  • Lightning — branches randomly but maintains statistical self-similarity
  • Blood vessels — fractal branching maximizes surface area in minimum volume
  • Snowflakes — hexagonal symmetry with fractal branching at each arm
  • Romanesco broccoli — the most visually obvious fractal in the grocery store

This is why fractal art feels inherently organic even though it's generated by mathematics. The same patterns that evolution discovered, the same geometry that physics favors, emerge naturally from simple recursive rules.

Tools for fractal exploration

While writing your own renderer teaches you the most, dedicated fractal tools let you explore faster:

  • Mandelbulber / Mandelbulb3D — 3D fractal rendering (yes, fractals work in 3D too)
  • Fractal Explorer — lightweight, fast 2D fractal viewer with many coloring options
  • Shadertoy — write GPU-powered fractal shaders that run at 60fps (search "mandelbrot" for hundreds of examples)
  • p5.js — great for creative coding beginners experimenting with recursive fractals
  • Ultra Fractal — the professional-grade fractal art tool with layering and animation

For web-based fractal art, raw Canvas or WebGL gives you the most control. The Mandelbrot set is a perfect first shader project — the per-pixel calculation maps naturally to GPU parallel processing, and you'll get 100x the performance of a CPU renderer.

From math to gallery wall

Fractal art has a long history in galleries and digital art exhibitions. Artists like Hal Tenny, Julius Horsthuis, and Tom Beddard (subblue) have shown that fractals aren't just math curiosities — they're a legitimate art medium. The key is moving beyond default rainbow coloring into intentional aesthetic choices: limited palettes, careful framing (treating the fractal like a landscape photograph), and finding the regions where mathematics produces emotion.

The 50KB constraint that Lumitree uses for its micro-worlds is perfect for fractal art. A complete, interactive Mandelbrot explorer fits in under 5KB. A Julia set animator with smooth coloring fits in under 10KB. With the remaining space, you can add mouse interaction, animation, and multiple rendering modes.

Start exploring

Copy any of the code examples above into an HTML file, open it in a browser, and start changing numbers. The Mandelbrot set alone contains infinite detail — mathematicians have been exploring it for decades and still find new structures. And that's just one equation.

The beauty of fractal art is that the gap between beginner and advanced is smaller than you think. The Mandelbrot set is rendered by a while loop and some multiplication. A fractal tree is a function that calls itself. An L-system is string replacement. Simple rules, infinite art.

Ready to see fractals in action? Explore the micro-worlds on Lumitree — several branches use fractal techniques to generate their visuals, or try the code demos featuring recursive patterns you can copy and remix. And if you want to start drawing with code from scratch, all you need is a browser.

Related articles