JavaScript tutorial on the fern fractal
There’s a quiet magic in the Barnsley fern—a simple set of rules that blossoms into a living, leafy pattern. This tutorial guides you from the core math to a complete JavaScript implementation, with clear steps and an interactive canvas to make the fern dance.
- What the fern fractal is
- The math behind the fern
- Project setup with canvas
- Implementing the fern in JavaScript
What the fern fractal is
The Barnsley fern is generated using an Iterated Function System (IFS): you repeatedly apply one of several affine transformations to a point, chosen at random with specific probabilities. Over many iterations, the points settle into the shape of a fern.
- Core idea: A point (x, y) is updated by a randomly chosen transform fᵢ(x, y) with probability pᵢ.
- Outcome: The repeated application paints the fern’s stalk, leaflets, and finer details without explicitly drawing lines.
The math behind the fern
Each fern point update uses one of four affine transformations of the form:
f(x, y) =
| a b | | x | + | e |
| c d | | y | | f |
The classic Barnsley fern IFS uses these transforms with associated probabilities:
Stem (1%):
x' = 0.00 * x + 0.00 * y + 0.00
y' = 0.00 * x + 0.16 * y + 0.00
Successive leaflet (85%):
x' = 0.85 * x + 0.04 * y + 0.00
y' = -0.04 * x + 0.85 * y + 1.60
Left leaflet (7%):
x' = 0.20 * x - 0.26 * y + 0.00
y' = 0.23 * x + 0.22 * y + 1.60
Right leaflet (7%):
x' = -0.15 * x + 0.28 * y + 0.00
y' = 0.26 * x + 0.24 * y + 0.44
Project setup with canvas
- HTML: Create a canvas and some minimal controls.
- Scale mapping: Fern coordinates are roughly within x ∈ [-3, 3] and y ∈ [0, 10]. You’ll map fern space to pixel space.
- Iteration count: The fern emerges after tens to hundreds of thousands of points. Start with 100k points and increase for quality.
<!-- Minimal HTML skeleton -->
<div id="controls">
<label>Points: <span id="countLabel">100000</span></label>
<input id="count" type="range" min="20000" max="500000" step="10000" value="100000">
<label>Hue: <span id="hueLabel">140</span></label>
<input id="hue" type="range" min="0" max="360" step="1" value="140">
<button id="draw">Draw</button>
<button id="animate">Animate</button>
<button id="clear">Clear</button>
</div>
<canvas id="fern" width="600" height="900"></canvas>
Implementing the fern in JavaScript
Core logic and drawing
- Transforms: Define the four transforms and their cumulative probabilities.
- Random selection: Draw a random value r ∈ [0, 1) and choose the transform by thresholds.
- Coordinate mapping: Convert fern space (x, y) to canvas pixels (u, v) with inverted y for screen coordinates.
// Inline, runnable implementation
(() => {
const canvas = document.getElementById('fern');
const ctx = canvas.getContext('2d');
const countSlider = document.getElementById('count');
const hueSlider = document.getElementById('hue');
const countLabel = document.getElementById('countLabel');
const hueLabel = document.getElementById('hueLabel');
const btnDraw = document.getElementById('draw');
const btnAnimate = document.getElementById('animate');
const btnClear = document.getElementById('clear');
// Fern coordinate bounds
const bounds = { xmin: -3, xmax: 3, ymin: 0, ymax: 10 };
function toPixel(x, y) {
const w = canvas.width, h = canvas.height;
const sx = (x - bounds.xmin) / (bounds.xmax - bounds.xmin) * w;
const sy = h - (y - bounds.ymin) / (bounds.ymax - bounds.ymin) * h; // invert y
return [sx, sy];
}
// Affine transforms with probabilities
const transforms = [
// stem
{ a: 0.0, b: 0.0, c: 0.0, d: 0.16, e: 0.0, f: 0.0, p: 0.01 },
// main leaflet
{ a: 0.85, b: 0.04, c: -0.04, d: 0.85, e: 0.0, f: 1.6, p: 0.85 },
// left leaflet
{ a: 0.20, b: -0.26, c: 0.23, d: 0.22, e: 0.0, f: 1.6, p: 0.07 },
// right leaflet
{ a: -0.15, b: 0.28, c: 0.26, d: 0.24, e: 0.0, f: 0.44, p: 0.07 },
];
// Convert probabilities to cumulative thresholds
const thresholds = (() => {
let acc = 0;
return transforms.map(t => (acc += t.p));
})();
function nextPoint(x, y) {
const r = Math.random();
let i = 0;
while (r > thresholds[i]) i++;
const t = transforms[i];
const nx = t.a * x + t.b * y + t.e;
const ny = t.c * x + t.d * y + t.f;
return [nx, ny];
}
function clearCanvas() {
ctx.fillStyle = '#0b0f14';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawFern(nPoints, hueBase = 140) {
clearCanvas();
// Use additive blending for glow
ctx.globalCompositeOperation = 'lighter';
let x = 0, y = 0; // start at origin
// Skip initial iterations to reach attractor faster
for (let i = 0; i < 50; i++) ([x, y] = nextPoint(x, y));
for (let i = 0; i < nPoints; i++) {
[x, y] = nextPoint(x, y);
const [u, v] = toPixel(x, y);
const lightness = 40 + 20 * Math.random(); // subtle variation
const hue = hueBase + (Math.sin(i * 0.0002) * 40);
ctx.fillStyle = `hsl(${hue}, 70%, ${lightness}%)`;
ctx.fillRect(u, v, 1, 1);
}
ctx.globalCompositeOperation = 'source-over';
}
// UI wiring
function updateLabels() {
countLabel.textContent = countSlider.value;
hueLabel.textContent = hueSlider.value;
}
updateLabels();
countSlider.addEventListener('input', updateLabels);
hueSlider.addEventListener('input', updateLabels);
btnDraw.addEventListener('click', () => {
drawFern(parseInt(countSlider.value, 10), parseInt(hueSlider.value, 10));
});
let animId = null;
btnAnimate.addEventListener('click', () => {
if (animId) { cancelAnimationFrame(animId); animId = null; return; }
clearCanvas();
ctx.globalCompositeOperation = 'lighter';
let x = 0, y = 0;
for (let i = 0; i < 50; i++) ([x, y] = nextPoint(x, y));
let i = 0;
const target = parseInt(countSlider.value, 10);
const hueBase = parseInt(hueSlider.value, 10);
function step() {
for (let k = 0; k < 2000 && i < target; k++, i++) {
[x, y] = nextPoint(x, y);
const [u, v] = toPixel(x, y);
const hue = hueBase + (Math.sin(i * 0.0003) * 60);
ctx.fillStyle = `hsl(${hue}, 75%, 55%)`;
ctx.fillRect(u, v, 1, 1);
}
if (i < target) animId = requestAnimationFrame(step);
else { ctx.globalCompositeOperation = 'source-over'; animId = null; }
}
step();
});
btnClear.addEventListener('click', () => {
if (animId) { cancelAnimationFrame(animId); animId = null; }
clearCanvas();
});
// Draw initial fern
drawFern(parseInt(countSlider.value, 10), parseInt(hueSlider.value, 10));
})();
Live demo: Use the sliders and buttons below to render and animate the fern.
Check out these JavaScript tutorials:
Animated Julia fractal (32 lines)
Minesweeper game (100 lines)
Optical illusion (18 lines)
Oldschool fire effect (20 lines)
8-bit style sine text scroller (30 lines)
Physics engine for beginners
Starfield (21 lines)
Yin Yang with a twist (4 circles and 20 lines)
Interactive animated sprites
Your first program in JavaScript: you need 5 minutes and a notepad
Fractals in Excel