Rolling Fog
Layers of soft mist roll sideways behind a section. The far layers sit high, thin and slow; the near ones low, thick and quicker, so the fog seems to drift past in depth.
Into the mist
How to use it
- Paste the snippet just before
</head>. - Add
data-rolling-fogto a section with a dark background.
Options
Add any of these attributes to the same element.
data-color | Colour of the mist. Default pale grey #e2e8f0. |
data-layers | How many layers of mist. Default 4. |
data-density | How thick the mist is, as a multiplier. Default 1. |
data-speed | Speed multiplier. Default 1. |
data-direction | left or right. Default right. |
Each layer is painted once, at half size, and then just slid along, so it costs very little. It pauses when off screen. With reduced motion the mist holds still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Rolling Fog: layers of soft mist drift sideways behind a section, the
// near ones lower, thicker and faster than the far ones, so the fog seems
// to roll past in depth. Drawn on a transparent canvas behind your content,
// and paused when off screen.
// Usage: <section data-rolling-fog style="background:#1e293b">…</section>
// Optional: data-color (mist colour, default #e2e8f0), data-layers (how
// many layers, default 4), data-density (how thick, as a multiplier,
// default 1), data-speed (default 1), data-direction ("left" or "right",
// default right).
var DEFAULTS = { color: '#e2e8f0', layers: 4, density: 1, speed: 1 };
var css =
'[data-rolling-fog]{position:relative;isolation:isolate;overflow:hidden}' +
'.rfg-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);
var TAU = Math.PI * 2;
var probe = document.createElement('canvas').getContext('2d');
function rgb(c) {
probe.fillStyle = '#000';
probe.fillStyle = c;
var v = probe.fillStyle;
if (v[0] === '#') return [parseInt(v.slice(1, 3), 16), parseInt(v.slice(3, 5), 16), parseInt(v.slice(5, 7), 16)];
return v.match(/[\d.]+/g).slice(0, 3).map(Number);
}
// A roughly bell-shaped random number around 0.
function bell() { return (Math.random() + Math.random() + Math.random()) / 1.5 - 1; }
function build(el) {
var d = el.dataset;
var col = rgb(d.color || DEFAULTS.color).join(',');
var count = Math.max(1, Math.min(8, +d.layers || DEFAULTS.layers));
var density = d.density !== undefined ? +d.density : DEFAULTS.density;
var speed = (d.speed !== undefined ? +d.speed : DEFAULTS.speed) * (d.direction === 'left' ? -1 : 1);
var canvas = document.createElement('canvas');
canvas.className = 'rfg-canvas';
canvas.setAttribute('aria-hidden', 'true');
el.insertBefore(canvas, el.firstChild);
var ctx = canvas.getContext('2d');
var R = anime.utils.random;
var w = 0, h = 0, dpr = 1, layers = [], t = 0;
var still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// The mist is soft, so each layer is painted at half size and scaled up.
var LO = 0.5;
// Paint one layer of mist into its own canvas, a strip that tiles
// seamlessly from side to side.
function paint(i) {
var depth = count > 1 ? i / (count - 1) : 1; // 0 far, 1 near
var tw = Math.max(w, 480) * (1.2 + depth * 0.3);
var bandY = h * (0.42 + depth * 0.4);
var bandH = h * (0.35 + depth * 0.25);
var top = Math.max(0, bandY - bandH), bottom = Math.min(h * 1.15, bandY + bandH);
var c = document.createElement('canvas');
c.width = Math.ceil(tw * LO);
c.height = Math.ceil((bottom - top) * LO);
var g = c.getContext('2d');
g.scale(LO, LO);
var scale = 0.7 + depth * 0.7;
var n = Math.round((tw / 22) * density);
for (var k = 0; k < n; k++) {
var r = R(50, 130) * scale * Math.min(1, h / 360 + 0.3);
var x = R(0, tw, 1);
var y = bandY + bell() * bandH * 0.45 - top;
var a = R(4, 10) / 100 * (0.6 + depth * 0.5);
// Draw at x and its neighbours either side so the strip wraps.
for (var s = -1; s <= 1; s++) {
var cx = x + s * tw;
if (cx + r < 0 || cx - r > tw) continue;
var rg = g.createRadialGradient(cx, y, 0, cx, y, r);
rg.addColorStop(0, 'rgba(' + col + ',' + a.toFixed(3) + ')');
rg.addColorStop(1, 'rgba(' + col + ',0)');
g.fillStyle = rg;
g.beginPath();
g.ellipse(cx, y, r, r * 0.55, 0, 0, TAU);
g.fill();
}
}
// A light blur smooths away the banding of so many faint layers.
var soft = document.createElement('canvas');
soft.width = c.width;
soft.height = c.height;
var sg = soft.getContext('2d');
if ('filter' in sg) {
sg.filter = 'blur(3px)';
// Blur the strip with copies either side, so its ends still meet.
sg.drawImage(c, -c.width, 0);
sg.drawImage(c, c.width, 0);
sg.drawImage(c, 0, 0);
c = soft;
}
return { canvas: c, tw: tw, top: top, height: bottom - top, v: (8 + depth * 30), bob: R(0, TAU, 2), x: R(0, tw, 1) };
}
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
var nw = el.clientWidth, nh = el.clientHeight;
if (nw === w && nh === h && layers.length) return;
w = nw;
h = nh;
canvas.width = w * dpr;
canvas.height = h * dpr;
layers = [];
for (var i = 0; i < count; i++) layers.push(paint(i));
draw();
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
for (var i = 0; i < layers.length; i++) {
var L = layers[i];
var off = ((L.x + t * L.v * speed) % L.tw + L.tw) % L.tw;
var y = L.top + Math.sin(t * 0.25 + L.bob) * h * 0.02;
for (var x = off - L.tw; x < w; x += L.tw) {
ctx.drawImage(L.canvas, x, y, L.tw, L.height);
}
}
}
resize();
if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
if (still) return;
var last = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
t += (last ? Math.min(50, now - last) : 16) / 1000;
last = now;
draw();
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-rolling-fog]').forEach(function (el) {
if (el._rfg) return;
el._rfg = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-rolling-fog style="background: #1e293b; color: #fff">
<h1>Into the mist</h1>
</section>