Fractal Tree: How to Create Beautiful Recursive Trees With Code
A fractal tree is one of the most beautiful demonstrations of recursion in nature and code. Starting from a single trunk, each branch splits into two or more smaller branches, which split again, and again, creating organic-looking structures from a handful of mathematical rules. The concept is simple enough for a beginner to understand, yet deep enough to produce genuinely stunning art.
Fractal trees appear everywhere in the natural world — not just in actual trees, but in river networks, lightning bolts, blood vessels, lung bronchi, and even the growth patterns of coral. The mathematician Benoît Mandelbrot called this “the geometry of nature,” and once you start seeing it, you cannot unsee it.
In this guide we build eight interactive fractal tree simulations, starting from the simplest recursive tree and ending with a generative forest that could hang in a gallery. Each example is self-contained, runs on a plain HTML Canvas with no external libraries, and stays under 50KB. Along the way you will learn recursion, L-systems, stochastic branching, wind physics, and techniques for turning algorithms into art.
The recursive tree: nature's simplest algorithm
The core algorithm for a fractal tree is breathtakingly simple. Draw a line (the trunk). At the end of the line, rotate left by some angle, draw a shorter line. Go back. Rotate right by the same angle, draw another shorter line. Repeat until the branches are too small to see. That is the entire algorithm — about 15 lines of code.
The key parameters are:
- Branch angle — how wide the fork is (typically 20–45 degrees)
- Length ratio — how much shorter each child branch is (typically 0.6–0.8)
- Depth — how many levels of branching (typically 8–14)
Small changes in these parameters produce dramatically different trees. A narrow angle gives a cypress-like shape. A wide angle gives a spreading oak. A high length ratio creates tall, sparse trees. A low ratio creates dense, bushy canopies.
Example 1: Basic recursive tree
This first example draws a classic symmetric binary tree. Click to regenerate with a new random angle.
const canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const angle = 0.4 + Math.random() * 0.3; // 23-40 degrees
const ratio = 0.68 + Math.random() * 0.1;
const maxDepth = 11;
function drawBranch(x, y, len, theta, depth) {
if (depth > maxDepth || len < 2) return;
const x2 = x + Math.sin(theta) * len;
const y2 = y - Math.cos(theta) * len;
const t = depth / maxDepth;
ctx.strokeStyle = `hsl(${30 + t * 100}, ${60 + t * 20}%, ${20 + t * 30}%)`;
ctx.lineWidth = Math.max(1, (maxDepth - depth) * 1.2);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x2, y2);
ctx.stroke();
// Draw leaves at the tips
if (depth >= maxDepth - 2) {
ctx.beginPath();
ctx.arc(x2, y2, 2 + Math.random() * 3, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${90 + Math.random() * 40}, 70%, 45%, 0.7)`;
ctx.fill();
}
drawBranch(x2, y2, len * ratio, theta - angle, depth + 1);
drawBranch(x2, y2, len * ratio, theta + angle, depth + 1);
}
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 800, 600);
drawBranch(400, 580, 120, 0, 0);
canvas.onclick = () => {
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, 800, 600);
drawBranch(400, 580, 120, 0, 0);
};
With just 30 lines of drawing code, we get a tree that looks remarkably organic. The color gradient transitions from brown (trunk) to green (leaves), and the lineWidth decreases with each generation just like real branches.
Stochastic trees: embracing randomness
Real trees are never perfectly symmetric. Wind, sunlight competition, soil nutrients, and random genetic variation create the beautiful irregularity we see in nature. By adding controlled randomness to our branching algorithm, we get trees that look far more natural.
The technique is simple: instead of fixed parameters, use random variations. Each branch gets a slightly different angle, length ratio, and even a small probability of not growing at all (pruning). The result: every tree is unique, just like in nature.
Example 2: Stochastic branching tree
Each tree is unique. Click to grow a new one. Notice how some branches are longer, some shorter, and some are pruned entirely — just like a real tree shaped by wind and light.
const canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
function drawStochastic(x, y, len, theta, depth, maxDepth) {
if (depth > maxDepth || len < 1.5) return;
// Random variations
const angleVar = (Math.random() - 0.5) * 0.3;
const lenVar = 0.62 + Math.random() * 0.18;
const pruneChance = depth > 6 ? 0.08 : 0;
const x2 = x + Math.sin(theta + angleVar * 0.3) * len;
const y2 = y - Math.cos(theta + angleVar * 0.3) * len;
const t = depth / maxDepth;
const hue = 25 + t * 100;
const sat = 40 + t * 30;
const light = 18 + t * 35;
ctx.strokeStyle = `hsl(${hue}, ${sat}%, ${light}%)`;
ctx.lineWidth = Math.max(0.5, (maxDepth - depth) * 1.5);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x2, y2);
ctx.stroke();
if (depth >= maxDepth - 3) {
const leafSize = 2 + Math.random() * 5;
ctx.beginPath();
ctx.arc(x2, y2, leafSize, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${80 + Math.random() * 50}, ${60 + Math.random() * 20}%, ${35 + Math.random() * 20}%, ${0.5 + Math.random() * 0.4})`;
ctx.fill();
}
const baseAngle = 0.35 + Math.random() * 0.25;
// Left branch (sometimes pruned)
if (Math.random() > pruneChance) {
drawStochastic(x2, y2, len * lenVar, theta - baseAngle + angleVar, depth + 1, maxDepth);
}
// Right branch (sometimes pruned)
if (Math.random() > pruneChance) {
drawStochastic(x2, y2, len * lenVar, theta + baseAngle + angleVar, depth + 1, maxDepth);
}
// Occasional third branch (creates denser canopy)
if (depth > 2 && depth < maxDepth - 2 && Math.random() < 0.15) {
drawStochastic(x2, y2, len * lenVar * 0.8, theta + angleVar * 2, depth + 1, maxDepth);
}
}
function generate() {
ctx.fillStyle = '#0a0e0a';
ctx.fillRect(0, 0, 800, 600);
// Ground
const grd = ctx.createLinearGradient(0, 550, 0, 600);
grd.addColorStop(0, '#1a2a0a');
grd.addColorStop(1, '#0d1a05');
ctx.fillStyle = grd;
ctx.fillRect(0, 550, 800, 50);
drawStochastic(400, 570, 110, 0, 0, 12);
}
generate();
canvas.onclick = generate;
The stochastic tree introduces three kinds of randomness: angle variation (each branch deviates slightly), length variation (some branches grow more than others), and pruning (some branches simply don’t grow). The optional third branch at 15% probability creates a denser, more natural canopy.
L-system trees: formal grammars for growth
In 1968, the Hungarian biologist Aristid Lindenmayer invented L-systems — a formal grammar for modeling plant growth. The idea is elegant: start with a string of symbols, and repeatedly apply rewriting rules. Then interpret the resulting string as drawing commands.
The classic symbols are:
- F — draw forward
- + — turn right
- − — turn left
- [ — save position (push to stack)
- ] — restore position (pop from stack)
A simple tree rule: F → FF-[-F+F+F]+[+F-F-F]. Starting from “F”, after 4 iterations this produces a complex tree with thousands of branches — all from one tiny rule.
Example 3: L-system tree renderer
This example implements a full L-system engine and renders three different tree species defined by their rules. Click to cycle through them.
const canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const trees = [
{
name: 'Deciduous',
axiom: 'X',
rules: { X: 'F[+X][-X]FX', F: 'FF' },
angle: 25.7,
iterations: 6,
startLen: 3
},
{
name: 'Conifer',
axiom: 'F',
rules: { F: 'FF+[+F-F-F]-[-F+F+F]' },
angle: 22.5,
iterations: 4,
startLen: 5
},
{
name: 'Bush',
axiom: 'F',
rules: { F: 'F[+F]F[-F][F]' },
angle: 20,
iterations: 5,
startLen: 4
}
];
let current = 0;
function generate(str, rules, n) {
for (let i = 0; i < n; i++) {
let next = '';
for (const ch of str) next += rules[ch] || ch;
str = next;
}
return str;
}
function render(tree) {
ctx.fillStyle = '#0a0a14';
ctx.fillRect(0, 0, 800, 600);
const str = generate(tree.axiom, tree.rules, tree.iterations);
const stack = [];
let x = 400, y = 580, theta = -Math.PI / 2;
const len = tree.startLen;
const rad = tree.angle * Math.PI / 180;
let maxStackDepth = 0, stackDepth = 0;
// First pass: find max depth for coloring
for (const ch of str) {
if (ch === '[') { stackDepth++; maxStackDepth = Math.max(maxStackDepth, stackDepth); }
if (ch === ']') stackDepth--;
}
stackDepth = 0;
for (const ch of str) {
if (ch === 'F') {
const nx = x + Math.cos(theta) * len;
const ny = y + Math.sin(theta) * len;
const t = stackDepth / (maxStackDepth || 1);
ctx.strokeStyle = `hsl(${30 + t * 90}, ${50 + t * 20}%, ${15 + t * 40}%)`;
ctx.lineWidth = Math.max(0.5, (1 - t) * 4);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(nx, ny);
ctx.stroke();
x = nx; y = ny;
} else if (ch === '+') {
theta += rad;
} else if (ch === '-') {
theta -= rad;
} else if (ch === '[') {
stack.push({ x, y, theta });
stackDepth++;
} else if (ch === ']') {
const s = stack.pop();
x = s.x; y = s.y; theta = s.theta;
stackDepth--;
}
}
ctx.fillStyle = '#fff';
ctx.font = '16px monospace';
ctx.fillText(tree.name + ' — click for next', 20, 30);
}
render(trees[current]);
canvas.onclick = () => { current = (current + 1) % trees.length; render(trees[current]); };
L-systems are powerful because the entire tree is defined by a single axiom and one or two replacement rules. The “Deciduous” rule F[+X][-X]FX creates opposite branching with growth at the tips. The “Conifer” rule creates the characteristic triangular shape of a pine tree. The “Bush” rule creates dense, multi-branched shrubs.
Wind animation: physics meets art
Static trees are beautiful, but trees in wind are mesmerizing. To simulate wind, we add a time-varying force that bends each branch. The key insight: branches at the tips should sway more than the trunk, and the motion should have a natural, organic rhythm using layered sine waves.
Example 4: Tree swaying in wind
Watch the tree respond to wind. The trunk barely moves while the outer branches dance. Move your mouse left and right to change wind direction and strength.
const canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
let mouseX = 400;
canvas.onmousemove = e => { mouseX = e.offsetX; };
function drawWindTree(time) {
ctx.fillStyle = 'rgba(5, 8, 15, 0.15)';
ctx.fillRect(0, 0, 800, 600);
const wind = (mouseX - 400) / 400; // -1 to 1
function branch(x, y, len, theta, depth, maxD) {
if (depth > maxD || len < 2) return;
// Wind effect increases with depth (tips move more)
const windForce = wind * 0.15 * (depth / maxD);
const sway = Math.sin(time * 0.002 + depth * 0.5) * 0.03 * (depth / maxD);
const gustSway = Math.sin(time * 0.005 + x * 0.01) * 0.02 * (depth / maxD);
const totalBend = windForce + sway + gustSway;
const x2 = x + Math.sin(theta + totalBend) * len;
const y2 = y - Math.cos(theta + totalBend) * len;
const t = depth / maxD;
ctx.strokeStyle = `hsl(${25 + t * 95}, ${45 + t * 25}%, ${15 + t * 35}%)`;
ctx.lineWidth = Math.max(0.5, (maxD - depth) * 1.3);
ctx.globalAlpha = 0.9;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x2, y2);
ctx.stroke();
if (depth >= maxD - 2) {
const leafSway = Math.sin(time * 0.003 + x2 * 0.05) * 3;
ctx.beginPath();
ctx.ellipse(x2 + leafSway, y2, 3 + Math.random() * 2, 5 + Math.random() * 3, theta + totalBend, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${85 + (x2 % 40)}, 65%, 40%, 0.6)`;
ctx.fill();
}
const angleBase = 0.4;
branch(x2, y2, len * 0.7, theta - angleBase + totalBend, depth + 1, maxD);
branch(x2, y2, len * 0.7, theta + angleBase + totalBend, depth + 1, maxD);
}
ctx.globalAlpha = 1;
branch(400, 580, 100, 0, 0, 10);
requestAnimationFrame(drawWindTree);
}
requestAnimationFrame(drawWindTree);
The wind simulation uses three layers of motion: a mouse-controlled wind direction, a slow sine wave for the overall sway rhythm, and a faster gust pattern that varies by horizontal position. Each layer is multiplied by depth / maxDepth, so the trunk is nearly stationary while the tips dance wildly — exactly like a real tree.
Seasonal trees: color tells the story of time
The same tree structure takes on completely different moods with different color palettes. A spring tree has fresh light-green buds. Summer is dense and dark green. Autumn explodes with oranges, reds, and golds. Winter is bare branches against a grey sky, with just a few stubborn brown leaves clinging on.
Example 5: Four seasons tree
Click to cycle through spring, summer, autumn, and winter. Same tree structure, completely different feeling.
const canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const seasons = [
{ name: 'Spring', sky: ['#c8e6ff', '#87ceeb'], trunk: '#5a3a1a', leafHue: [80, 140], leafSat: [50, 80], leafLight: [45, 65], leafAlpha: [0.4, 0.7], leafSize: [2, 5], leafChance: 0.8, blossoms: true },
{ name: 'Summer', sky: ['#4a90c2', '#87ceeb'], trunk: '#3d2810', leafHue: [75, 130], leafSat: [40, 70], leafLight: [25, 45], leafAlpha: [0.6, 0.9], leafSize: [3, 7], leafChance: 1.0, blossoms: false },
{ name: 'Autumn', sky: ['#d4a574', '#f0c88a'], trunk: '#4a2f15', leafHue: [0, 55], leafSat: [60, 90], leafLight: [35, 55], leafAlpha: [0.5, 0.85], leafSize: [3, 6], leafChance: 0.85, blossoms: false },
{ name: 'Winter', sky: ['#8a9aaa', '#c0ccd8'], trunk: '#3a3025', leafHue: [25, 40], leafSat: [20, 40], leafLight: [30, 45], leafAlpha: [0.3, 0.5], leafSize: [1, 3], leafChance: 0.05, blossoms: false }
];
let seasonIdx = 0;
function rng(min, max) { return min + Math.random() * (max - min); }
// Fixed seed for consistent tree shape
let seed = 42;
function seededRandom() { seed = (seed * 16807) % 2147483647; return (seed - 1) / 2147483646; }
function draw(s) {
seed = 42; // Reset seed for consistent shape
// Sky gradient
const grd = ctx.createLinearGradient(0, 0, 0, 600);
grd.addColorStop(0, s.sky[0]);
grd.addColorStop(1, s.sky[1]);
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 800, 600);
// Ground
ctx.fillStyle = s.name === 'Winter' ? '#e8e0d8' : '#2a4a15';
ctx.fillRect(0, 555, 800, 45);
function branch(x, y, len, theta, depth, maxD) {
if (depth > maxD || len < 2) return;
const sr = seededRandom;
const angleVar = (sr() - 0.5) * 0.2;
const x2 = x + Math.sin(theta + angleVar) * len;
const y2 = y - Math.cos(theta + angleVar) * len;
const t = depth / maxD;
ctx.strokeStyle = s.trunk;
ctx.lineWidth = Math.max(0.5, (maxD - depth) * 1.5);
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x2, y2);
ctx.stroke();
// Leaves
if (depth >= maxD - 3 && Math.random() < s.leafChance) {
const size = rng(s.leafSize[0], s.leafSize[1]);
ctx.beginPath();
ctx.arc(x2, y2, size, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${rng(s.leafHue[0], s.leafHue[1])}, ${rng(s.leafSat[0], s.leafSat[1])}%, ${rng(s.leafLight[0], s.leafLight[1])}%, ${rng(s.leafAlpha[0], s.leafAlpha[1])})`;
ctx.fill();
}
// Cherry blossoms in spring
if (s.blossoms && depth >= maxD - 2 && Math.random() < 0.3) {
ctx.beginPath();
ctx.arc(x2, y2, rng(2, 4), 0, Math.PI * 2);
ctx.fillStyle = `hsla(${rng(330, 350)}, 80%, ${rng(75, 90)}%, 0.7)`;
ctx.fill();
}
const base = 0.35 + sr() * 0.2;
branch(x2, y2, len * (0.65 + sr() * 0.12), theta - base, depth + 1, maxD);
branch(x2, y2, len * (0.65 + sr() * 0.12), theta + base, depth + 1, maxD);
}
branch(400, 555, 105, 0, 0, 11);
ctx.fillStyle = '#fff';
ctx.font = 'bold 20px sans-serif';
ctx.globalAlpha = 0.8;
ctx.fillText(s.name, 30, 35);
ctx.globalAlpha = 1;
}
draw(seasons[seasonIdx]);
canvas.onclick = () => { seasonIdx = (seasonIdx + 1) % 4; draw(seasons[seasonIdx]); };
The seasonal tree uses a seeded random number generator for the branching structure (so the tree shape stays consistent across seasons) but true randomness for the leaves. This way you can see the same tree transform through the year. Spring adds cherry blossoms. Autumn shifts the hue range to warm reds and oranges. Winter drops leaf probability to just 5%, leaving mostly bare branches.
3D fractal tree: depth from projection
With a small twist, we can make our 2D tree appear three-dimensional. Instead of branching only left and right, branches also grow “into” and “out of” the screen. We project 3D coordinates to 2D using simple perspective division, and use depth-based fog and size scaling to sell the illusion.
Example 6: 3D rotating fractal tree
The tree slowly rotates, revealing its full 3D structure. Branches grow in all directions, not just a flat plane.
const canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// 3D branch data (pre-computed then rendered)
function buildTree3D() {
const branches = [];
function branch(x, y, z, len, pitch, yaw, depth, maxD) {
if (depth > maxD || len < 3) return;
const dx = Math.sin(yaw) * Math.cos(pitch) * len;
const dy = -Math.cos(pitch) * Math.cos(yaw) * len;
const dz = Math.sin(pitch) * len;
const nx = x + dx, ny = y + dy, nz = z + dz;
branches.push({ x1: x, y1: y, z1: z, x2: nx, y2: ny, z2: nz, depth, maxD });
const angleSpread = 0.45;
const r = 0.67;
// Four branches: left, right, forward, back (alternating)
branch(nx, ny, nz, len * r, pitch, yaw - angleSpread, depth + 1, maxD);
branch(nx, ny, nz, len * r, pitch, yaw + angleSpread, depth + 1, maxD);
if (depth % 2 === 0) {
branch(nx, ny, nz, len * r * 0.8, pitch - angleSpread * 0.7, yaw, depth + 1, maxD);
} else {
branch(nx, ny, nz, len * r * 0.8, pitch + angleSpread * 0.7, yaw, depth + 1, maxD);
}
}
branch(0, 0, 0, 90, 0, 0, 0, 9);
return branches;
}
const branches = buildTree3D();
function rotateY(x, z, angle) {
const c = Math.cos(angle), s = Math.sin(angle);
return [x * c - z * s, x * s + z * c];
}
function render(time) {
ctx.fillStyle = '#080810';
ctx.fillRect(0, 0, 800, 600);
const rotAngle = time * 0.0003;
const fov = 500;
const cx = 400, cy = 500;
// Sort by z for painter's algorithm
const projected = branches.map(b => {
const [rx1, rz1] = rotateY(b.x1, b.z1, rotAngle);
const [rx2, rz2] = rotateY(b.x2, b.z2, rotAngle);
const s1 = fov / (fov + rz1 + 200);
const s2 = fov / (fov + rz2 + 200);
return {
sx1: cx + rx1 * s1, sy1: cy + b.y1 * s1,
sx2: cx + rx2 * s2, sy2: cy + b.y2 * s2,
z: (rz1 + rz2) / 2,
depth: b.depth, maxD: b.maxD, s: (s1 + s2) / 2
};
});
projected.sort((a, b) => a.z - b.z);
for (const p of projected) {
const t = p.depth / p.maxD;
const fog = Math.max(0.2, Math.min(1, p.s));
ctx.strokeStyle = `hsla(${30 + t * 90}, ${50 + t * 20}%, ${15 + t * 40}%, ${fog})`;
ctx.lineWidth = Math.max(0.3, (p.maxD - p.depth) * 1.2 * p.s);
ctx.beginPath();
ctx.moveTo(p.sx1, p.sy1);
ctx.lineTo(p.sx2, p.sy2);
ctx.stroke();
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
The 3D tree pre-computes all branch segments in 3D space, then each frame rotates them around the Y-axis and projects to screen coordinates using perspective division (fov / (fov + z)). The painter’s algorithm (sorting by z-depth) ensures far branches are drawn behind near ones. Fog (alpha based on scale factor) adds atmospheric depth.
Interactive growing tree: watch it unfold
Instead of drawing the tree all at once, we can animate the growth process — branches extending one at a time, leaves unfurling, the canopy slowly filling in. This creates a mesmerizing “time-lapse” effect.
Example 7: Animated tree growth
Watch the tree grow from a single seed. Branches extend smoothly over time, and leaves appear when a branch reaches full length. Click to plant a new seed.
const canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
let segments = [];
let startTime = 0;
function planTree(x, y, len, theta, depth, maxD, delay) {
if (depth > maxD || len < 2) return;
const angleVar = (Math.random() - 0.5) * 0.15;
const x2 = x + Math.sin(theta) * len;
const y2 = y - Math.cos(theta) * len;
segments.push({ x1: x, y1: y, x2, y2, depth, maxD, delay, duration: 300 + len * 3 });
const nextDelay = delay + 150 + Math.random() * 100;
const r = 0.65 + Math.random() * 0.1;
const a = 0.35 + Math.random() * 0.2;
planTree(x2, y2, len * r, theta - a + angleVar, depth + 1, maxD, nextDelay);
planTree(x2, y2, len * r, theta + a + angleVar, depth + 1, maxD, nextDelay + 80);
}
function init() {
segments = [];
startTime = performance.now();
planTree(400, 580, 110, 0, 0, 11, 0);
segments.sort((a, b) => a.delay - b.delay);
}
function render(now) {
ctx.fillStyle = '#0a0e0a';
ctx.fillRect(0, 0, 800, 600);
const elapsed = now - startTime;
for (const seg of segments) {
if (elapsed < seg.delay) continue;
const progress = Math.min(1, (elapsed - seg.delay) / seg.duration);
const ease = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2;
const cx = seg.x1 + (seg.x2 - seg.x1) * ease;
const cy = seg.y1 + (seg.y2 - seg.y1) * ease;
const t = seg.depth / seg.maxD;
ctx.strokeStyle = `hsl(${30 + t * 90}, ${50 + t * 25}%, ${18 + t * 35}%)`;
ctx.lineWidth = Math.max(0.5, (seg.maxD - seg.depth) * 1.3);
ctx.beginPath();
ctx.moveTo(seg.x1, seg.y1);
ctx.lineTo(cx, cy);
ctx.stroke();
// Leaf appears when branch is fully grown
if (progress >= 1 && seg.depth >= seg.maxD - 2) {
const leafGrow = Math.min(1, (elapsed - seg.delay - seg.duration) / 500);
if (leafGrow > 0) {
const size = leafGrow * (3 + (seg.x2 * 7 % 4));
ctx.beginPath();
ctx.arc(seg.x2, seg.y2, size, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${85 + (seg.x2 * 13 % 40)}, 65%, 42%, ${leafGrow * 0.7})`;
ctx.fill();
}
}
}
requestAnimationFrame(render);
}
init();
requestAnimationFrame(render);
canvas.onclick = init;
The growth animation works by pre-planning all branch segments with delay times. Deeper branches start growing later. Each branch extends with an ease-in-out curve, and leaves fade in only after their branch has finished growing. The result is an organic unfolding that mimics how real trees develop.
Generative forest art: the final composition
For our final example, we combine everything into a generative artwork: multiple trees of varying sizes, a atmospheric background with layered fog, falling leaves, and a color palette that shifts subtly across the canvas. This is gallery-worthy generative art from pure code.
Example 8: Generative forest art
A procedurally generated forest scene. Each reload creates a unique composition with 5-8 trees, atmospheric fog layers, and particle effects. Click to regenerate.
const canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 600;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
function generateForest() {
// Sky with gradient
const sky = ctx.createLinearGradient(0, 0, 0, 600);
sky.addColorStop(0, '#1a1028');
sky.addColorStop(0.4, '#2a1535');
sky.addColorStop(0.7, '#1a2030');
sky.addColorStop(1, '#0a1510');
ctx.fillStyle = sky;
ctx.fillRect(0, 0, 800, 600);
// Stars
for (let i = 0; i < 80; i++) {
const sx = Math.random() * 800;
const sy = Math.random() * 300;
const sr = 0.5 + Math.random() * 1.5;
ctx.beginPath();
ctx.arc(sx, sy, sr, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 255, 240, ${0.3 + Math.random() * 0.6})`;
ctx.fill();
}
// Moon
const moonX = 150 + Math.random() * 500;
ctx.beginPath();
ctx.arc(moonX, 80, 30, 0, Math.PI * 2);
const moonGrd = ctx.createRadialGradient(moonX, 80, 0, moonX, 80, 60);
moonGrd.addColorStop(0, 'rgba(255, 250, 230, 0.9)');
moonGrd.addColorStop(0.5, 'rgba(255, 250, 230, 0.1)');
moonGrd.addColorStop(1, 'rgba(255, 250, 230, 0)');
ctx.fillStyle = moonGrd;
ctx.fill();
// Fog layers (back to front)
for (let layer = 0; layer < 3; layer++) {
const fogY = 300 + layer * 80;
ctx.fillStyle = `rgba(${10 + layer * 5}, ${15 + layer * 8}, ${10 + layer * 5}, ${0.15 + layer * 0.1})`;
ctx.beginPath();
ctx.moveTo(0, fogY);
for (let fx = 0; fx <= 800; fx += 20) {
ctx.lineTo(fx, fogY + Math.sin(fx * 0.01 + layer * 2) * 20 + Math.sin(fx * 0.03) * 10);
}
ctx.lineTo(800, 600);
ctx.lineTo(0, 600);
ctx.fill();
}
// Trees (back to front for depth)
const treeCount = 5 + Math.floor(Math.random() * 4);
const treeDefs = [];
for (let i = 0; i < treeCount; i++) {
treeDefs.push({
x: 50 + Math.random() * 700,
y: 430 + Math.random() * 130,
size: 0.4 + Math.random() * 0.8,
hueShift: Math.random() * 40 - 20
});
}
treeDefs.sort((a, b) => a.y - b.y); // Back to front
for (const td of treeDefs) {
const depth = (td.y - 430) / 130; // 0 = far, 1 = near
const alpha = 0.4 + depth * 0.5;
function branch(x, y, len, theta, d, maxD) {
if (d > maxD || len < 1.5) return;
const av = (Math.random() - 0.5) * 0.25;
const x2 = x + Math.sin(theta + av) * len;
const y2 = y - Math.cos(theta + av) * len;
const t = d / maxD;
ctx.strokeStyle = `hsla(${260 + td.hueShift + t * 60}, ${30 + t * 30}%, ${10 + t * 25 + depth * 10}%, ${alpha})`;
ctx.lineWidth = Math.max(0.3, (maxD - d) * 1.2 * td.size);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x2, y2);
ctx.stroke();
if (d >= maxD - 2 && Math.random() < 0.7) {
ctx.beginPath();
ctx.arc(x2, y2, (2 + Math.random() * 3) * td.size, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${280 + td.hueShift + Math.random() * 40}, ${40 + Math.random() * 30}%, ${30 + Math.random() * 20}%, ${alpha * 0.7})`;
ctx.fill();
}
const a = 0.35 + Math.random() * 0.2;
const r = 0.64 + Math.random() * 0.1;
if (Math.random() > 0.05) branch(x2, y2, len * r, theta - a, d + 1, maxD);
if (Math.random() > 0.05) branch(x2, y2, len * r, theta + a, d + 1, maxD);
}
branch(td.x, td.y, 70 * td.size, 0, 0, 10);
}
// Ground cover
const ground = ctx.createLinearGradient(0, 540, 0, 600);
ground.addColorStop(0, 'rgba(10, 20, 10, 0.8)');
ground.addColorStop(1, '#050a05');
ctx.fillStyle = ground;
ctx.fillRect(0, 555, 800, 45);
// Fireflies
for (let i = 0; i < 20; i++) {
const fx = Math.random() * 800;
const fy = 350 + Math.random() * 200;
const fr = 1 + Math.random() * 2;
const glow = ctx.createRadialGradient(fx, fy, 0, fx, fy, fr * 4);
glow.addColorStop(0, 'rgba(200, 255, 100, 0.6)');
glow.addColorStop(1, 'rgba(200, 255, 100, 0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(fx, fy, fr * 4, 0, Math.PI * 2);
ctx.fill();
}
}
generateForest();
canvas.onclick = generateForest;
The generative forest combines depth sorting (far trees are drawn first and appear more transparent), atmospheric perspective (colors shift toward blue-purple with distance), layered fog, stars, a moon with glow, and fireflies. Each tree uses stochastic branching with a unique hue shift, creating a varied but cohesive forest. Every click generates a completely unique artwork.
The mathematics of fractal trees
Fractal trees connect to deep mathematics. The branching pattern follows a power law: if the trunk has diameter D, each child branch has diameter approximately D × r, where r is the scaling ratio. Leonardo da Vinci observed that tree branches follow a conservation law — the total cross-sectional area is roughly constant at every level. This means r ≈ 1/√n where n is the number of child branches.
The fractal dimension of a binary tree with scaling ratio r is d = log(2) / log(1/r). For a typical tree with r = 0.7, this gives d ≈ 1.94 — nearly filling the plane but not quite. Real trees have fractal dimensions between 1.5 and 2.0, which is why they look complex without being solid.
L-systems connect fractal trees to formal language theory. The strings generated by L-system rules form a context-free language, and the resulting trees are essentially parse trees of that language. This connection between grammar and geometry is one of the most beautiful ideas in computational biology.
Fractal trees in nature and technology
Fractal branching is not just aesthetically pleasing — it is functionally optimal. Trees use fractal branching to maximize leaf exposure to sunlight while minimizing the wood needed for structural support. The same principle appears in:
- Lung bronchi — 23 levels of branching maximize surface area for gas exchange in a compact volume
- River networks — tributaries merge into larger rivers following Horton’s laws, which are fractal scaling laws
- Lightning — electrical discharge follows the path of least resistance, creating fractal branching patterns
- Blood vessels — the circulatory system branches fractally to deliver blood to every cell
- Computer science — binary search trees, B-trees, and recursive data structures all use fractal-like branching
In computer graphics, fractal trees revolutionized movie visual effects. The trees in Avatar, The Lord of the Rings, and virtually every modern film with outdoor scenes use procedural fractal generation rather than hand-modeled geometry.
Going further
The eight examples in this guide barely scratch the surface. You could extend them with:
- Phototropism — branches growing toward a light source
- Space colonization — a more realistic algorithm where branches grow toward nearby “attraction points”
- Bark textures — drawing textured cylinders instead of lines
- Fruit and flowers — procedural ornaments at branch tips
- Forest ecosystems — trees competing for light, growing around each other
- Real-time interaction — users pruning or shaping the tree
Fractal trees are one of those rare topics where mathematics, biology, art, and code all converge. Every parameter change reveals something new. Every random seed tells a different story.
Want to see fractal trees in action? Explore the Lumitree interactive art tree, where visitors plant seeds that grow into unique micro-worlds — many of which use fractal algorithms as their visual foundation. The whole project is itself a fractal tree: it starts from a single trunk and grows infinitely through community participation.