Rainfall
Rain streaks down a section at a slight slant and splashes along the bottom, each drop throwing up a few droplets and a little ripple. Near drops are long and bright, far ones faint and fine.
Stay in
How to use it
- Paste the snippet just before
</head>. - Add
data-rainfallto a section with a dark background.
Options
Add any of these attributes to the same element.
data-density | Drops per 100 × 100 px. Default 2.2. |
data-speed | Speed multiplier. Default 1. |
data-wind | How much the rain slants, from -1 (left) to 1 (right). Default 0.15. |
data-color | Colour of the rain. Default #c7dcff. |
data-splash="false" | No splashes: the rain falls past the bottom. |
It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion the rain hangs still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Rainfall: rain streaks down a section at a slant and splashes at the
// bottom, each drop throwing up a few droplets and a little ripple. Near
// drops are longer, brighter and faster, far ones faint, and land a little
// higher up, as if on the ground further back. Drawn on a transparent
// canvas behind your content, and paused when off screen.
// Usage: <section data-rainfall style="background:#111827">…</section>
// Optional: data-density (drops per 100 × 100 px, default 2.2),
// data-speed (default 1), data-wind (slant, -1 to 1, default 0.15),
// data-color (default pale blue #c7dcff), data-splash="false" (no
// splashes, for rain that falls past the bottom).
var DEFAULTS = { density: 2.2, speed: 1, wind: 0.15, color: '#c7dcff' };
var css =
'[data-rainfall]{position:relative;isolation:isolate;overflow:hidden}' +
'.rnf-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);
function build(el) {
var d = el.dataset;
var canvas = document.createElement('canvas');
canvas.className = 'rnf-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 color = d.color || DEFAULTS.color;
var splash = d.splash !== 'false';
var R = anime.utils.random;
var w = 0, h = 0, dpr = 1, unit = 1;
var drops = [], bits = [], rings = [];
// Where a drop lands: the bottom edge for near drops, a little higher
// for far ones.
function ground(z) { return splash ? h - 4 - (1 - z) * Math.min(26, h * 0.08) : h + 40; }
function drop(p, anywhere) {
p.z = Math.pow(Math.random(), 1.4);
p.v = (900 + p.z * 700) * R(0.9, 1.1, 2); // px/s
p.len = (10 + p.z * 22) * R(0.8, 1.2, 2);
p.width = 0.7 + p.z * 1.1;
p.alpha = 0.18 + p.z * 0.5;
p.land = ground(p.z);
// Start far enough up and to the side that the slant still covers the box.
p.x = Math.random() * (w + Math.abs(wind) * h) - (wind > 0 ? wind * h : 0);
p.y = anywhere ? Math.random() * p.land : -p.len - Math.random() * h * 0.3;
return p;
}
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.6, h / 500);
var n = Math.round(Math.min(600, Math.max(40, (w * h * density) / 10000)));
while (drops.length < n) drops.push(drop({}, true));
drops.length = n;
for (var i = 0; i < drops.length; i++) drops[i].land = ground(drops[i].z);
}
function land(p) {
if (!splash) return;
var x = p.x + wind * (p.land - p.y), y = p.land;
rings.push({ x: x, y: y, age: 0, life: 0.35 + p.z * 0.25, size: 6 + p.z * 12, alpha: p.alpha });
var n = p.z > 0.4 ? Math.round(R(2, 4)) : 1;
for (var i = 0; i < n; i++) {
var a = -Math.PI / 2 + R(-1.1, 1.1, 2);
var v = R(60, 150) * (0.5 + p.z) * unit;
bits.push({ x: x, y: y - 1, vx: Math.cos(a) * v + wind * 40, vy: Math.sin(a) * v, floor: y, age: 0, r: 0.6 + p.z * 0.9, alpha: p.alpha + 0.15 });
}
}
function advance(dt) {
dt *= speed;
for (var i = 0; i < drops.length; i++) {
var p = drops[i], dy = p.v * unit * dt;
p.y += dy;
p.x += wind * dy;
if (p.y >= p.land) { land(p); drop(p, false); }
}
for (i = bits.length - 1; i >= 0; i--) {
var b = bits[i];
b.age += dt;
b.vy += 900 * unit * dt;
b.x += b.vx * dt;
b.y += b.vy * dt;
if (b.y > b.floor + 2 || b.age > 0.6) bits.splice(i, 1);
}
for (i = rings.length - 1; i >= 0; i--) {
rings[i].age += dt;
if (rings[i].age > rings[i].life) rings.splice(i, 1);
}
if (bits.length > 800) bits.splice(0, bits.length - 800);
if (rings.length > 400) rings.splice(0, rings.length - 400);
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineCap = 'round';
// Streaks: a line along the direction of fall, tail behind the head.
var len = Math.sqrt(1 + wind * wind);
for (var i = 0; i < drops.length; i++) {
var p = drops[i];
if (p.y < -p.len) continue;
ctx.globalAlpha = p.alpha;
ctx.lineWidth = p.width;
ctx.beginPath();
ctx.moveTo(p.x - wind * p.len / len, p.y - p.len / len);
ctx.lineTo(p.x, p.y);
ctx.stroke();
}
// Ripples: flat ellipses that spread and fade.
ctx.lineWidth = 1;
for (i = 0; i < rings.length; i++) {
var r = rings[i], k = r.age / r.life;
ctx.globalAlpha = r.alpha * (1 - k) * 0.9;
ctx.beginPath();
ctx.ellipse(r.x, r.y, r.size * (0.2 + k), r.size * (0.2 + k) * 0.22, 0, 0, Math.PI * 2);
ctx.stroke();
}
// Droplets thrown up by the splash.
for (i = 0; i < bits.length; i++) {
var b = bits[i];
ctx.globalAlpha = Math.min(1, b.alpha) * (1 - b.age / 0.6);
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
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-rainfall]').forEach(function (el) {
if (el._rnf) return;
el._rnf = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-rainfall style="background: #111827; color: #fff">
<h1>Stay in</h1>
</section>