Falling Leaves
Autumn leaves tumble down a section, swinging from side to side like a pendulum, tilting into each swing and turning over as they fall. Maple, oak and plain oval leaves, in reds, oranges and golds.
Autumn collection
How to use it
- Paste the snippet just before
</head>. - Add
data-falling-leavesto a section, hero or card.
Options
Add any of these attributes to the same element.
data-density | Leaves per 100 × 100 px. Default 0.5. |
data-size | Size of the biggest leaves, in px. Default 44. |
data-speed | Speed multiplier. Default 1. |
data-wind | Sideways drift, from -1 (left) to 1 (right). Default 0.2. |
data-colors | Comma-separated leaf colours. |
It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion the leaves hang still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Falling Leaves: autumn leaves tumble down a section, swinging from side
// to side like a pendulum, tilting into each swing and flipping over as
// they fall. Drawn on a transparent canvas behind your content, and paused
// when off screen.
// Usage: <section data-falling-leaves>…</section>
// Optional: data-density (leaves per 100 × 100 px, default 0.5),
// data-speed (default 1), data-wind (sideways drift, -1 to 1, default
// 0.2), data-size (size of the biggest leaves in px, default 44),
// data-colors="#c2410c,#ea580c,…".
var DEFAULTS = {
density: 0.5, speed: 1, wind: 0.2, size: 44,
colors: ['#c2410c', '#ea580c', '#d97706', '#b91c1c', '#ca8a04', '#9a3412']
};
var css =
'[data-falling-leaves]{position:relative;isolation:isolate;overflow:hidden}' +
'.flv-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// Leaf outlines on a 64 × 64 box, stem at the bottom, tip at the top.
var SHAPES = [
// Maple: five pointed lobes.
function (g) {
g.moveTo(32, 60);
g.lineTo(31, 50);
g.lineTo(18, 54); g.lineTo(20, 47); g.lineTo(6, 38); g.lineTo(11, 35); g.lineTo(5, 25);
g.lineTo(16, 27); g.lineTo(17, 21); g.lineTo(26, 29); g.lineTo(23, 12); g.lineTo(28, 14);
g.lineTo(32, 3);
g.lineTo(36, 14); g.lineTo(41, 12); g.lineTo(38, 29); g.lineTo(47, 21); g.lineTo(48, 27);
g.lineTo(59, 25); g.lineTo(53, 35); g.lineTo(58, 38); g.lineTo(44, 47); g.lineTo(46, 54);
g.lineTo(33, 50);
g.closePath();
},
// Oval leaf with a pointed tip.
function (g) {
g.moveTo(32, 58);
g.bezierCurveTo(12, 48, 12, 22, 32, 4);
g.bezierCurveTo(52, 22, 52, 48, 32, 58);
g.closePath();
},
// Oak: wavy rounded lobes.
function (g) {
g.moveTo(32, 58);
var L = [[24, 52], [16, 48], [22, 42], [13, 36], [20, 30], [14, 22], [23, 18], [22, 9], [32, 5]];
g.quadraticCurveTo(26, 56, 24, 52);
for (var i = 1; i < L.length; i++) g.quadraticCurveTo(L[i - 1][0] - 6, (L[i - 1][1] + L[i][1]) / 2, L[i][0], L[i][1]);
for (i = L.length - 2; i >= 0; i--) g.quadraticCurveTo(70 - L[i + 1][0], (L[i + 1][1] + L[i][1]) / 2, 64 - L[i][0], L[i][1]);
g.quadraticCurveTo(38, 56, 32, 58);
g.closePath();
}
];
function sprites(colors) {
var out = [];
SHAPES.forEach(function (shape) {
colors.forEach(function (color) {
var c = document.createElement('canvas');
c.width = c.height = 64;
var g = c.getContext('2d');
g.beginPath();
shape(g);
g.fillStyle = color.trim();
g.fill();
// A soft light-to-dark sheen, then the veins and stem.
g.globalCompositeOperation = 'source-atop';
var sheen = g.createLinearGradient(10, 6, 54, 60);
sheen.addColorStop(0, 'rgba(255,255,255,.22)');
sheen.addColorStop(1, 'rgba(0,0,0,.2)');
g.fillStyle = sheen;
g.fillRect(0, 0, 64, 64);
g.globalCompositeOperation = 'source-over';
g.strokeStyle = 'rgba(60,20,0,.35)';
g.lineWidth = 1.4;
g.lineCap = 'round';
g.beginPath();
g.moveTo(32, 63); g.lineTo(32, 10);
g.moveTo(32, 44); g.lineTo(20, 34);
g.moveTo(32, 44); g.lineTo(44, 34);
g.moveTo(32, 32); g.lineTo(23, 22);
g.moveTo(32, 32); g.lineTo(41, 22);
g.stroke();
out.push(c);
});
});
return out;
}
function build(el) {
var d = el.dataset;
var canvas = document.createElement('canvas');
canvas.className = 'flv-canvas';
canvas.setAttribute('aria-hidden', 'true');
el.insertBefore(canvas, el.firstChild);
var ctx = canvas.getContext('2d');
var density = +d.density || DEFAULTS.density;
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
var wind = d.wind !== undefined ? +d.wind : DEFAULTS.wind;
var maxSize = +d.size || DEFAULTS.size;
var img = sprites(d.colors ? d.colors.split(',') : DEFAULTS.colors);
var R = anime.utils.random;
var w = 0, h = 0, dpr = 1, unit = 1;
var leaves = [];
function leaf(f, top) {
f.z = Math.sqrt(Math.random()); // 0 far, 1 near
f.size = maxSize * (0.5 + f.z * 0.5) * Math.max(0.75, Math.min(1, w / 600));
f.x = Math.random() * w;
f.y = top ? -f.size - Math.random() * 60 : Math.random() * h;
f.img = img[Math.floor(Math.random() * img.length)];
f.fall = R(36, 60) * (0.6 + f.z * 0.5); // px/s
f.swing = R(18, 46) * (0.6 + f.z * 0.6); // px either side
f.freq = R(1.1, 2, 2);
f.phase = Math.random() * 6.28;
f.flip = R(1.5, 4.5, 2) * (Math.random() < 0.5 ? -1 : 1); // tumble speed
f.flipPhase = Math.random() * 6.28;
f.base = R(-0.8, 0.8, 2); // resting tilt
f.alpha = 0.65 + f.z * 0.35;
return f;
}
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
w = el.clientWidth;
h = el.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
unit = Math.max(0.7, h / 400);
var n = Math.round(Math.min(90, Math.max(7, (w * h * density) / 10000)));
while (leaves.length < n) leaves.push(leaf({}, false));
leaves.length = n;
// Far leaves first, so near ones pass in front.
leaves.sort(function (a, b) { return a.z - b.z; });
}
function draw() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < leaves.length; i++) {
var f = leaves[i];
var s = Math.sin(f.phase), c = Math.cos(f.phase);
// Pendulum: out to the side and back, tilted into the swing.
var x = f.x + s * f.swing, rot = f.base + c * 0.7;
// Tumbling: the leaf turns over, so it looks thinner, then shows its back.
var flip = Math.cos(f.flipPhase);
var sx = Math.max(0.12, Math.abs(flip));
var cr = Math.cos(rot), sr = Math.sin(rot), k = dpr * f.size / 64;
ctx.setTransform(cr * k * sx, sr * k * sx, -sr * k, cr * k, x * dpr, f.y * dpr);
ctx.globalAlpha = f.alpha * (flip < 0 ? 0.82 : 1);
ctx.drawImage(f.img, -32, -32);
}
ctx.globalAlpha = 1;
}
function advance(dt) {
for (var i = 0; i < leaves.length; i++) {
var f = leaves[i];
f.phase += f.freq * dt * speed;
f.flipPhase += f.flip * dt * speed;
// It drops fastest at the bottom of each swing and hangs at the ends.
var c = Math.abs(Math.cos(f.phase));
f.y += f.fall * (0.35 + c * 1.1) * unit * dt * speed;
f.x += wind * 40 * (0.5 + f.z) * dt * speed;
if (f.y - f.size > h + 10) leaf(f, true);
var m = f.swing + f.size;
if (f.x < -m) f.x += w + m * 2; else if (f.x > w + m) f.x -= w + m * 2;
}
}
resize();
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
draw();
if (window.ResizeObserver) new ResizeObserver(function () { resize(); draw(); }).observe(el);
return;
}
if (window.ResizeObserver) new ResizeObserver(resize).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) / 1000 : 1 / 60;
last = now;
advance(dt);
draw();
}
});
// 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-falling-leaves]').forEach(function (el) {
if (el._flv) return;
el._flv = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-falling-leaves>
<h1>Autumn collection</h1>
</section>