Barnsley Fern
A fern draws itself point by point. Each new point comes from the last by one of four simple stretch-and-turn rules, picked at random: one makes the stem, one the smaller copy above it, and two the first leaflets on either side. A few points at first, then thousands a second, until a whole fern appears.
How to use it
- Paste the snippet just before
</head>. - Add
data-barnsley-fernto a box with a height and a dark background.
Options
Add any of these attributes to the same element.
data-points | How many points in all. Default fits the box, 20,000 to 160,000. |
data-colors | Stem and tip colours. Default #166534,#86efac. |
data-duration | Roughly how long it takes to draw. Default 7000 ms. |
data-loop | Hold the finished fern, fade it and draw another, mirrored every other time. |
Michael Barnsley described it in 1988: four rules, each a stretch, a turn and a shift, picked 1%, 85%, 7% and 7% of the time. It’s paused when off screen, and with reduced motion the finished fern appears at once.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Barnsley Fern: a fern draws itself point by point. Each new point comes
// from the last by one of four simple stretch-and-turn rules, picked at
// random: one makes the stem, one the ever-smaller copy above it, and two
// the first leaflets on either side. A few points at first, then
// thousands a second, until a whole fern has appeared. Paused when off
// screen.
// Usage: <div data-barnsley-fern style="height:360px"></div>
// Optional: data-points (how many points in all, default fits the box),
// data-colors (stem and tip colours, default "#166534,#86efac"),
// data-duration (roughly how long it takes to draw in ms, default 7000),
// data-loop (hold the finished fern, fade it and draw another, mirrored
// every other time).
var DEFAULTS = { colors: '#166534,#86efac', duration: 7000, hold: 3200 };
// Barnsley's four maps: x' = a x + b y + e, y' = c x + d y + f, chance p.
var MAPS = [
[0, 0, 0, 0.16, 0, 0, 0.01],
[0.85, 0.04, -0.04, 0.85, 0, 1.6, 0.85],
[0.2, -0.26, 0.23, 0.22, 0, 1.6, 0.07],
[-0.15, 0.28, 0.26, 0.24, 0, 0.44, 0.07]
];
var SHADES = 24;
var css =
'[data-barnsley-fern]{position:relative;isolation:isolate;overflow:hidden}' +
'.bfn-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function rgb(hex) {
var n = parseInt(hex.trim().replace('#', ''), 16);
return [n >> 16, n >> 8 & 255, n & 255];
}
function build(el) {
var d = el.dataset;
var canvas = document.createElement('canvas');
canvas.className = 'bfn-canvas';
canvas.setAttribute('aria-hidden', 'true');
el.insertBefore(canvas, el.firstChild);
var ctx = canvas.getContext('2d');
var cs = (d.colors || DEFAULTS.colors).split(',').map(rgb);
if (cs.length < 2) cs.push(cs[0]);
var shades = [];
for (var i = 0; i < SHADES; i++) {
var k = i / (SHADES - 1);
shades.push('rgb(' + Math.round(cs[0][0] + (cs[1][0] - cs[0][0]) * k) + ',' + Math.round(cs[0][1] + (cs[1][1] - cs[0][1]) * k) + ',' + Math.round(cs[0][2] + (cs[1][2] - cs[0][2]) * k) + ')');
}
var duration = +d.duration || DEFAULTS.duration;
var loop = d.loop !== undefined && d.loop !== 'false';
var w = 0, h = 0, dpr = 1, total = 0, scale = 1, ox = 0, oy = 0, size = 1;
var x = 0, y = 0, placed = 0, age = 0, phase = 'draw', fading = false, flip = 1;
// Points binned by shade, so each frame sets the colour only a few times.
var bins = [];
function resize() {
var nw = el.clientWidth, nh = el.clientHeight;
if (nw === w && nh === h) return false;
w = nw; h = nh;
dpr = Math.min(2, window.devicePixelRatio || 1);
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(h * dpr);
// The fern spans x -2.2..2.7 and y 0..10.
var pad = Math.max(10, Math.min(w, h) * 0.05);
scale = Math.min((h - pad * 2) / 10, (w - pad * 2) / 4.9);
ox = w / 2;
oy = h - pad - (h - pad * 2 - scale * 10) / 2;
total = +d.points || Math.round(Math.max(20000, Math.min(160000, scale * scale * 50)));
size = Math.max(0.55, Math.min(1.1, scale / 40));
return true;
}
function reset() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
x = 0; y = 0;
placed = 0;
age = 0;
phase = 'draw';
}
// Place n more points.
function plot(n) {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
for (var b = 0; b < SHADES; b++) bins[b] = [];
for (var i = 0; i < n && placed < total; i++) {
var r = Math.random(), m = MAPS[3];
if (r < 0.01) m = MAPS[0];
else if (r < 0.86) m = MAPS[1];
else if (r < 0.93) m = MAPS[2];
var nx = m[0] * x + m[1] * y + m[4];
y = m[2] * x + m[3] * y + m[5];
x = nx;
placed++;
if (placed < 20) continue;
// Lighter towards the tips and the leaflet edges.
var shade = Math.min(SHADES - 1, Math.floor((y / 10 * 0.7 + Math.min(1, Math.abs(x - 0.25 - y * 0.05) / 2.2) * 0.5) * SHADES));
bins[shade].push(ox + (x - 0.24) * scale * flip, oy - y * scale);
}
ctx.globalAlpha = 0.8;
for (b = 0; b < SHADES; b++) {
var pts = bins[b];
if (!pts.length) continue;
ctx.fillStyle = shades[b];
for (var j = 0; j < pts.length; j += 2) ctx.fillRect(pts[j] - size / 2, pts[j + 1] - size / 2, size, size);
}
ctx.globalAlpha = 1;
}
function tick(dt) {
age += dt;
if (phase === 'draw') {
// Slowly at first, then faster: about duration ms in all.
var k = Math.min(1, age / duration);
var want = Math.round(total * Math.pow(k, 2.4));
plot(Math.max(2, want - placed));
if (placed >= total) { phase = 'hold'; age = 0; }
} else if (loop && age > DEFAULTS.hold && !fading) {
fading = true;
anime.animate(canvas, {
opacity: 0, duration: 700, ease: 'inOut(2)',
onComplete: function () { flip = -flip; reset(); fading = false; anime.utils.set(canvas, { opacity: 1 }); }
});
}
}
resize();
reset();
if (reduced()) {
plot(total);
if (window.ResizeObserver) new ResizeObserver(function () { if (resize()) { reset(); plot(total); } }).observe(el);
return;
}
if (window.ResizeObserver) new ResizeObserver(function () { if (resize()) reset(); }).observe(el);
var last = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
var dt = last ? Math.min(50, now - last) : 16;
last = now;
tick(dt);
}
});
// Pause while off screen to save battery.
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-barnsley-fern]').forEach(function (el) {
if (el._bfn) return;
el._bfn = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-barnsley-fern data-loop style="height: 360px; background: #06140c"></div>