Butterfly
A butterfly flutters along a winding path over a little flower bed, flapping hard, gliding for a moment with its wings open, then flapping again. It leans into its turns and leaves a faint dotted trail.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-butterfly>where the scene should go. It fills the element with an SVG scene.
Options
Add any of these attributes to the same element.
data-color | Colour of the wings. Default #f472b6. |
data-accent | Colour of the wing edges, spots and trail. Default #7c3aed. |
data-speed | Speed multiplier. Default 1. |
data-trail="false" | No dotted trail. |
data-flowers="false" | No flower bed, just the butterfly. |
It is 8:5 on a transparent background by default. It pauses when off screen. With reduced motion the butterfly rests in mid-air.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Butterfly: a butterfly flutters along a winding path over a little
// flower bed, flapping hard, gliding for a moment, then flapping again,
// and leaving a faint dotted trail. Drawn as SVG that fills the element,
// and paused when off screen.
// Usage: <div data-butterfly></div>
// It is 8:5 by default on a transparent background.
// Optional: data-color (wing colour, default #f472b6), data-accent (wing
// edges and spots, default #7c3aed), data-speed (default 1),
// data-trail="false" (no trail), data-flowers="false" (no flower bed).
var DEFAULTS = { color: '#f472b6', accent: '#7c3aed', speed: 1 };
var NS = 'http://www.w3.org/2000/svg';
var TRAIL = 30;
var css =
'[data-butterfly]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
'.bfy-svg{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// Where the butterfly is at time s (seconds): two slow sways each way.
function pathAt(s) {
return [
200 + 138 * Math.sin(s * 0.42) + 18 * Math.sin(s * 1.3 + 0.5),
100 + 42 * Math.sin(s * 0.77 + 1) + 14 * Math.sin(s * 2.1)
];
}
function wings(color, accent) {
return '<path d="M0 -2C6 -22 30 -30 33 -14C35 -2 20 4 0 2Z" fill="' + color + '" stroke="' + accent + '" stroke-width="2.4" stroke-linejoin="round"/>' +
'<path d="M0 3C16 4 27 12 21 23C15 32 3 20 0 7Z" fill="' + color + '" stroke="' + accent + '" stroke-width="2.4" stroke-linejoin="round"/>' +
'<circle cx="22" cy="-14" r="4" fill="' + accent + '"/><circle cx="23" cy="-14" r="1.6" fill="#fff"/>' +
'<circle cx="14" cy="15" r="2.6" fill="#fff" opacity=".85"/>' +
'<path d="M2 -2C10 -10 18 -14 26 -16" fill="none" stroke="#fff" stroke-width="1.2" opacity=".5"/>';
}
// A leaf growing out of a stem at (x, y), pointing up and to side s.
function leaf(x, y, s) {
return 'M' + x + ' ' + y + 'C' + (x + s * 4) + ' ' + (y - 1) + ' ' + (x + s * 13) + ' ' + (y - 5) + ' ' + (x + s * 16) + ' ' + (y - 15) +
'C' + (x + s * 8) + ' ' + (y - 15) + ' ' + (x + s * 2) + ' ' + (y - 9) + ' ' + x + ' ' + y + 'Z';
}
function flowerBed() {
var html = '<g class="bfy-bed">';
[[58, 50, '#f59e0b'], [118, 34, '#a78bfa'], [178, 56, '#fb7185'], [236, 30, '#fbbf24'], [292, 48, '#c084fc'], [348, 36, '#f472b6']].forEach(function (f, i) {
var x = f[0], top = 232 - f[1];
html += '<g class="bfy-flower" data-x="' + x + '"><path d="M' + x + ' 236Q' + (x + (i % 2 ? 6 : -6)) + ' ' + (top + 20) + ' ' + x + ' ' + top + '" fill="none" stroke="#16a34a" stroke-width="3" stroke-linecap="round"/>' +
'<path d="' + leaf(x + (i % 2 ? 2 : -2), top + 30, i % 2 ? -1 : 1) + '" fill="#22c55e"/>' +
'<g transform="translate(' + x + ' ' + top + ')" fill="' + f[2] + '">';
for (var p = 0; p < 5; p++) {
var a = p * Math.PI * 2 / 5;
html += '<circle cx="' + (Math.cos(a) * 6).toFixed(2) + '" cy="' + (Math.sin(a) * 6).toFixed(2) + '" r="5.5"/>';
}
html += '<circle r="4" fill="#fde68a"/></g></g>';
});
html += '<path d="M10 238C80 232 320 232 390 238" stroke="#86efac" stroke-width="6" stroke-linecap="round" fill="none"/></g>';
return html;
}
function build(el) {
var d = el.dataset;
var color = d.color || DEFAULTS.color;
var accent = d.accent || DEFAULTS.accent;
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'A butterfly fluttering over flowers');
if (!el.hasAttribute('role')) el.setAttribute('role', 'img');
el.innerHTML =
'<svg class="bfy-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
(d.flowers === 'false' ? '' : flowerBed()) +
'<g class="bfy-trail" fill="' + accent + '"></g>' +
'<g class="bfy-fly"><g transform="scale(1.05)">' +
'<g class="bfy-r">' + wings(color, accent) + '</g>' +
'<g class="bfy-l">' + wings(color, accent) + '</g>' +
'<ellipse cx="0" cy="4" rx="3" ry="14" fill="#1e1b2e"/><circle cx="0" cy="-11" r="3.6" fill="#1e1b2e"/>' +
'<path d="M-1 -13C-3 -20 -6 -24 -9 -26M1 -13C3 -20 6 -24 9 -26" fill="none" stroke="#1e1b2e" stroke-width="1.3" stroke-linecap="round"/>' +
'<circle cx="-9" cy="-26" r="1.6" fill="#1e1b2e"/><circle cx="9" cy="-26" r="1.6" fill="#1e1b2e"/>' +
'</g></g>' +
'</svg>';
var svg = el.firstChild;
var fly = svg.querySelector('.bfy-fly');
var wr = svg.querySelector('.bfy-r'), wl = svg.querySelector('.bfy-l');
var dots = [];
if (d.trail !== 'false') {
var tg = svg.querySelector('.bfy-trail');
for (var i = 0; i < TRAIL; i++) {
var c = document.createElementNS(NS, 'circle');
c.setAttribute('r', 1.6);
tg.appendChild(c);
dots.push(c);
}
}
var flowers = Array.prototype.slice.call(svg.querySelectorAll('.bfy-flower'));
var phase = 0, rate = 0.05, gap = 0;
var hist = [];
function frame(dt, t) {
var s = t / 1000;
// Flap hard, then glide with wings held open now and then.
var gliding = Math.sin(s * 0.9) > 0.72;
rate += ((gliding ? 0.008 : 0.05) - rate) * Math.min(1, dt * 0.006);
phase += rate * dt;
var open = gliding ? 0.75 + 0.25 * Math.abs(Math.cos(phase)) : 0.18 + 0.82 * Math.abs(Math.cos(phase));
wr.setAttribute('transform', 'scale(' + open.toFixed(3) + ' 1)');
wl.setAttribute('transform', 'scale(' + (-open).toFixed(3) + ' 1)');
var p = pathAt(s), q = pathAt(s + 0.05);
var lift = Math.sin(phase * 2) * 2.2 * (gliding ? 0.2 : 1);
var tilt = Math.max(-28, Math.min(28, (q[0] - p[0]) * 3.2));
fly.setAttribute('transform', 'translate(' + p[0].toFixed(1) + ' ' + (p[1] + lift).toFixed(1) + ') rotate(' + tilt.toFixed(1) + ')');
if (dots.length) {
gap -= dt;
if (gap <= 0) { gap = 70; hist.unshift([p[0], p[1] + 8]); if (hist.length > TRAIL) hist.pop(); }
for (var i = 0; i < dots.length; i++) {
var h = hist[i];
if (!h || i < 2) { dots[i].setAttribute('opacity', 0); continue; }
dots[i].setAttribute('cx', h[0].toFixed(1));
dots[i].setAttribute('cy', h[1].toFixed(1));
dots[i].setAttribute('opacity', (0.5 * (1 - i / dots.length)).toFixed(2));
}
}
flowers.forEach(function (f, i) {
var x = +f.getAttribute('data-x');
f.setAttribute('transform', 'rotate(' + (Math.sin(s * 1.1 + i) * 2.5).toFixed(2) + ' ' + x + ' 236)');
});
}
if (reduced()) { frame(0, 3000); return; }
var t = 0, 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) * speed;
last = now;
t += dt;
frame(dt, t);
}
});
frame(0, 0);
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-butterfly]').forEach(function (el) {
if (el._bfy) return; // idempotent
el._bfy = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-butterfly></div>
<!-- a blue one, on its own: -->
<div data-butterfly data-color="#38bdf8" data-accent="#1e3a8a" data-flowers="false"></div>