Embers
Glowing embers drift up from the bottom of a section, wavering on the hot air and flickering as they cool from yellow to orange to a dull red before they fade.
Into the night
How to use it
- Paste the snippet just before
</head>. - Add
data-embersto a section with a dark background.
Options
Add any of these attributes to the same element.
data-density | Embers per 100 × 100 px. Default 0.9. |
data-speed | Speed multiplier. Default 1. |
data-wind | Sideways drift, from -1 (left) to 1 (right). Default 0. |
data-colors | Hot, warm and cool colours, comma-separated. Default #fff3b0,#ff8a1f,#b91c1c. |
It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion the embers hang still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Embers: glowing embers drift up from the bottom of a section, wavering on
// the hot air and flickering as they cool from yellow to orange to a dull
// red, then fade out. Drawn on a transparent canvas behind your content,
// and paused when off screen.
// Usage: <section data-embers style="background:#120806">…</section>
// Optional: data-density (embers per 100 × 100 px, default 0.9),
// data-speed (default 1), data-wind (sideways drift, -1 to 1, default 0),
// data-colors (hot, warm and cool colours, default
// "#fff3b0,#ff8a1f,#b91c1c").
var DEFAULTS = { density: 0.9, speed: 1, wind: 0, colors: ['#fff3b0', '#ff8a1f', '#b91c1c'] };
var css =
'[data-embers]{position:relative;isolation:isolate;overflow:hidden}' +
'.emb-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 rgb(hex) {
var s = hex.trim().replace('#', '');
if (s.length === 3) s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2];
var n = parseInt(s, 16);
return isNaN(n) ? [255, 140, 40] : [n >> 16, (n >> 8) & 255, n & 255];
}
// Glow sprites along the cooling ramp: index 0 is hottest.
var STEPS = 24;
function sprites(colors) {
var c = colors.map(rgb), out = [];
for (var i = 0; i < STEPS; i++) {
var k = i / (STEPS - 1), a, b, f;
if (k < 0.4) { a = c[0]; b = c[1]; f = k / 0.4; } else { a = c[1]; b = c[2]; f = (k - 0.4) / 0.6; }
var col = [0, 1, 2].map(function (j) { return Math.round(a[j] + (b[j] - a[j]) * f); }).join(',');
var S = 48, cv = document.createElement('canvas');
cv.width = cv.height = S;
var g = cv.getContext('2d');
var grad = g.createRadialGradient(S / 2, S / 2, 0, S / 2, S / 2, S / 2);
grad.addColorStop(0, 'rgba(255,250,235,' + (1 - k * 0.6) + ')');
grad.addColorStop(0.1, 'rgba(' + col + ',1)');
grad.addColorStop(0.3, 'rgba(' + col + ',.35)');
grad.addColorStop(1, 'rgba(' + col + ',0)');
g.fillStyle = grad;
g.fillRect(0, 0, S, S);
out.push(cv);
}
return out;
}
function build(el) {
var d = el.dataset;
var canvas = document.createElement('canvas');
canvas.className = 'emb-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 img = sprites(d.colors ? d.colors.split(',') : DEFAULTS.colors);
var R = anime.utils.random;
var w = 0, h = 0, dpr = 1, t = 0, unit = 1;
var embers = [];
function ember(e, fresh) {
e.x = Math.random() * w;
e.y = h + R(0, 20);
e.vy = R(45, 110) * unit; // px/s upwards
e.size = R(1.4, 3.6, 2); // core radius
e.life = R(3.2, 6.5, 2);
e.age = fresh ? 0 : Math.random() * e.life;
if (!fresh) e.y = h - e.vy * e.age; // already on its way
e.freq = R(0.8, 2.2, 2);
e.phase = Math.random() * 6.28;
e.wobble = R(10, 34) * unit;
e.flicker = R(9, 18);
e.ox = e.x;
return e;
}
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(260, Math.max(18, (w * h * density) / 10000)));
while (embers.length < n) embers.push(ember({}, false));
embers.length = n;
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
ctx.globalCompositeOperation = 'lighter';
for (var i = 0; i < embers.length; i++) {
var e = embers[i], k = e.age / e.life;
// Fade in quickly, cool, then fade out over the last third.
var alpha = Math.min(1, k * 8) * (k > 0.65 ? 1 - (k - 0.65) / 0.35 : 1);
alpha *= 0.75 + 0.25 * Math.sin(t * e.flicker + e.phase);
if (alpha <= 0) continue;
var s = e.size * (6.5 - k * 3);
ctx.globalAlpha = alpha;
ctx.drawImage(img[Math.min(STEPS - 1, Math.floor(k * STEPS))], e.x - s, e.y - s, s * 2, s * 2);
}
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1;
}
function advance(dt) {
t += dt;
for (var i = 0; i < embers.length; i++) {
var e = embers[i];
e.age += dt;
if (e.age > e.life || e.y < -20) { ember(e, true); continue; }
// Rising air slows a little as it cools; the sideways waver grows.
e.y -= e.vy * (1 - e.age / e.life * 0.4) * dt * speed;
e.ox += wind * 40 * unit * dt * speed;
e.x = e.ox + Math.sin(t * e.freq + e.phase) * e.wobble * (0.3 + e.age / e.life)
+ Math.sin(t * 0.37 + e.y * 0.01) * 8 * unit;
if (e.ox < -40) e.ox += w + 80; else if (e.ox > w + 40) e.ox -= w + 80;
}
}
resize();
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
advance(0.01);
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-embers]').forEach(function (el) {
if (el._emb) return;
el._emb = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-embers style="background: #140806; color: #fff">
<h1>Into the night</h1>
</section>