Fireflies
Soft glowing dots wander lazily about a section, each one swelling into light and dimming again on its own slow rhythm, like a summer evening. They drift over to the pointer, curious.
Midsummer
How to use it
- Paste the snippet just before
</head>. - Add
data-firefliesto a section with a dark background.
Options
Add any of these attributes to the same element.
data-count | Number of fireflies. Default about one per 100 × 100 px. |
data-color | Glow colour. Default #d9f99d. |
data-speed | Speed multiplier. Default 1. |
data-follow="false" | Ignore the pointer. |
It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion they glow, but stay put.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Fireflies: soft glowing dots wander lazily about a section, each
// glowing up and dimming on its own slow rhythm. They drift towards the
// pointer, curious. Drawn on a transparent canvas behind your content, and
// paused when off screen.
// Usage: <section data-fireflies style="background:#0b1d17">…</section>
// Optional: data-count (default: about 1 per 100 × 100 px, 12 to 120),
// data-color (default warm yellow-green #d9f99d), data-speed (default 1),
// data-follow="false" (ignore the pointer).
var DEFAULTS = { color: '#d9f99d', speed: 1 };
var css =
'[data-fireflies]{position:relative;isolation:isolate;overflow:hidden}' +
'.ffl-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);
// A pre-drawn glow: a bright core inside a wide soft halo.
function sprite(color) {
var S = 96, c = document.createElement('canvas');
c.width = c.height = S;
var g = c.getContext('2d');
g.fillStyle = color;
g.fillRect(0, 0, S, S);
g.globalCompositeOperation = 'destination-in';
var m = g.createRadialGradient(S / 2, S / 2, 0, S / 2, S / 2, S / 2);
m.addColorStop(0, '#000');
m.addColorStop(0.09, '#000');
m.addColorStop(0.22, 'rgba(0,0,0,.55)');
m.addColorStop(0.5, 'rgba(0,0,0,.18)');
m.addColorStop(1, 'rgba(0,0,0,0)');
g.fillStyle = m;
g.fillRect(0, 0, S, S);
// A white-hot centre.
g.globalCompositeOperation = 'source-over';
var core = g.createRadialGradient(S / 2, S / 2, 0, S / 2, S / 2, S * 0.06);
core.addColorStop(0, 'rgba(255,255,255,.95)');
core.addColorStop(1, 'rgba(255,255,255,0)');
g.fillStyle = core;
g.fillRect(0, 0, S, S);
return c;
}
function build(el) {
var d = el.dataset;
var canvas = document.createElement('canvas');
canvas.className = 'ffl-canvas';
canvas.setAttribute('aria-hidden', 'true');
el.insertBefore(canvas, el.firstChild);
var ctx = canvas.getContext('2d');
var img = sprite(d.color || DEFAULTS.color);
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
var follow = d.follow !== 'false';
var R = anime.utils.random;
var w = 0, h = 0, dpr = 1, t = 0;
var flies = [], pointer = null;
function fly() {
return {
x: Math.random() * w, y: Math.random() * h,
a: Math.random() * Math.PI * 2, // heading
turn: 0,
v: R(0.012, 0.03, 3), // px per ms
size: R(22, 44),
// Glow cycle: how long it takes and where in it this one starts.
period: R(2200, 5200),
offset: Math.random(),
depth: R(0.5, 1, 2)
};
}
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
w = el.clientWidth;
h = el.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
var n = +d.count || Math.round(Math.min(120, Math.max(12, (w * h) / 10000)));
while (flies.length < n) flies.push(fly());
flies.length = n;
}
// Brightness over a glow cycle: a quick swell, a slow fade, a dim rest.
function glow(f) {
var k = ((t / f.period + f.offset) % 1);
if (k < 0.15) return 0.15 + 0.85 * Math.sin((k / 0.15) * Math.PI / 2);
if (k < 0.55) return 0.15 + 0.85 * Math.pow(1 - (k - 0.15) / 0.4, 2);
return 0.15;
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
ctx.globalCompositeOperation = 'lighter';
for (var i = 0; i < flies.length; i++) {
var f = flies[i], b = glow(f), s = f.size * (0.55 + b * 0.6) * f.depth;
ctx.globalAlpha = b * f.depth;
ctx.drawImage(img, f.x - s, f.y - s, s * 2, s * 2);
}
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1;
}
function advance(dt) {
t += dt;
for (var i = 0; i < flies.length; i++) {
var f = flies[i];
// Wander: the heading drifts with a slowly changing turn.
f.turn += R(-1, 1, 3) * 0.0006 * dt;
f.turn *= 0.97;
f.a += f.turn * dt * 0.05;
if (pointer && Math.abs(pointer.x - f.x) + Math.abs(pointer.y - f.y) < 320) {
// Lean the heading towards the pointer, if it's near.
var want = Math.atan2(pointer.y - f.y, pointer.x - f.x);
var diff = Math.atan2(Math.sin(want - f.a), Math.cos(want - f.a));
f.a += diff * 0.0015 * dt;
}
// Steer back in from the edges.
var m = 20;
if (f.x < m || f.x > w - m || f.y < m || f.y > h - m) {
var home = Math.atan2(h / 2 - f.y, w / 2 - f.x);
f.a += Math.atan2(Math.sin(home - f.a), Math.cos(home - f.a)) * 0.004 * dt;
}
f.x += Math.cos(f.a) * f.v * dt * speed;
f.y += Math.sin(f.a) * f.v * dt * speed;
}
}
resize();
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
// A still night: every firefly mid-glow.
t = 600;
flies.forEach(function (f) { f.offset = Math.random() * 0.3; });
draw();
if (window.ResizeObserver) new ResizeObserver(function () { resize(); draw(); }).observe(el);
return;
}
if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
if (follow) {
el.addEventListener('pointermove', function (e) {
var r = el.getBoundingClientRect();
pointer = { x: e.clientX - r.left, y: e.clientY - r.top };
});
el.addEventListener('pointerleave', function () { pointer = null; });
}
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;
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-fireflies]').forEach(function (el) {
if (el._ffl) return;
el._ffl = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-fireflies style="background: #0b1d17; color: #fff">
<h1>Midsummer</h1>
</section>