Burst Lines
Short cartoon lines shoot out all round an element and vanish, the way a comic shows a “pow!” or a sudden surprise, while the element swells for a moment. It bursts when scrolled into view, when tapped or clicked, and when the mouse comes over it.
Wow!
Tap or hover either one to burst it
How to use it
- Paste the snippet just before
</head>. - Add
data-burst-linesto any element: a word, an icon, a button. - Add
data-loopto burst again every so often, or callanimacious.burstLines(el)to burst it yourself, say when something is saved.
Options
Add any of these attributes to the same element.
data-loop | Burst again every data-interval while on screen. |
data-interval | Time between bursts, when looping. Default 1800 ms. |
data-count | Number of lines. Default 10. |
data-length | Length of the longer lines. Default 16 px. |
data-gap | Space between the element and the lines. Default 8 px. |
data-width | Line width. Default 3 px. |
data-color | Line colour, or several separated by commas to take turns. Default the text colour. |
data-duration | How long a burst lasts. Default 520 ms. |
data-pop | How much the element swells. Default 0.08; 0 for none. |
data-round | Arrange the lines on a circle, for round icons and avatars. |
The swell uses the element’s transform, so use it on an inline-block or block element without a transform of its own. The lines are hidden from screen readers, and nothing moves with reduced motion.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Burst Lines: short cartoon lines shoot out all round an element for
// emphasis, the way a comic draws a "pow!" or a sudden surprise. It bursts
// when the element scrolls into view, when it's clicked or tapped, and when
// the mouse comes over it.
// Usage: <span data-burst-lines>Wow!</span>
// Optional: data-loop (burst again every data-interval ms, default 1800),
// data-count (lines, default 10), data-length (px, default 16),
// data-gap (space between the element and the lines, px, default 8),
// data-width (line width, px, default 3), data-color (one colour, or several
// separated by commas to take turns; default the text colour),
// data-duration (ms, default 520), data-pop (how much the element swells,
// default 0.08; 0 for none), data-round (lines sit on a circle rather than
// round a box: for round icons and avatars).
// Script use: animacious.burstLines(el)
var DEFAULTS = { interval: 1800, count: 10, length: 16, gap: 8, width: 3, duration: 520, pop: 0.08 };
var SVGNS = 'http://www.w3.org/2000/svg';
var css = '.bsl-svg{position:absolute;pointer-events:none;overflow:visible;max-width:none;max-height:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function num(v, f) { return v !== undefined && v !== '' && !isNaN(+v) ? +v : f; }
function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
function easeIn(t) { return t * t; }
function clamp(t) { return t < 0 ? 0 : t > 1 ? 1 : t; }
function setup(el) {
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
var svg = document.createElementNS(SVGNS, 'svg');
svg.setAttribute('class', 'bsl-svg');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke-linecap', 'round');
el.appendChild(svg);
el._bslSvg = svg;
}
function burst(el) {
if (!el._bslSvg) return;
var d = el.dataset, svg = el._bslSvg;
var count = Math.max(3, num(d.count, DEFAULTS.count));
var len = num(d.length, DEFAULTS.length);
var gap = num(d.gap, DEFAULTS.gap);
var width = num(d.width, DEFAULTS.width);
var dur = num(d.duration, DEFAULTS.duration);
var pop = num(d.pop, DEFAULTS.pop);
var colors = (d.color || 'currentColor').split(',');
var round = d.round !== undefined && d.round !== 'false';
// Size the overlay to the element plus room for the lines.
var w = el.offsetWidth, h = el.offsetHeight, m = gap + len * 1.5 + width;
svg.style.left = svg.style.top = -m + 'px';
svg.style.width = (w + 2 * m) + 'px';
svg.style.height = (h + 2 * m) + 'px';
svg.setAttribute('viewBox', '0 0 ' + (w + 2 * m) + ' ' + (h + 2 * m));
svg.textContent = '';
var cx = m + w / 2, cy = m + h / 2;
var a = w / 2 + gap, b = h / 2 + gap;
if (round) a = b = Math.max(w, h) / 2 + gap;
// A squarish curve round a box (a superellipse) and a circle round a round thing.
var n = round ? 2 : 5;
var spin = Math.random() * Math.PI * 2, stepA = Math.PI * 2 / count;
var lines = [];
for (var i = 0; i < count; i++) {
var th = spin + i * stepA + (Math.random() - 0.5) * stepA * 0.35;
var c = Math.cos(th), s = Math.sin(th);
var r = Math.pow(Math.pow(Math.abs(c / a), n) + Math.pow(Math.abs(s / b), n), -1 / n);
// Long and short lines take turns, each a little different.
var L = len * (i % 2 ? 0.62 : 1) * (0.85 + Math.random() * 0.3);
var ln = document.createElementNS(SVGNS, 'line');
ln.setAttribute('stroke', colors[i % colors.length].trim());
ln.setAttribute('stroke-width', width);
svg.appendChild(ln);
lines.push({ el: ln, x: cx + c * r, y: cy + s * r, dx: c, dy: s, L: L, wait: Math.random() * dur * 0.12 });
}
if (pop > 0 && window.anime) {
anime.animate(el, { scale: [1, 1 + pop, 1], duration: dur * 0.9, ease: 'out(3)' });
}
var start = 0;
cancelAnimationFrame(el._bslRaf);
function frame(now) {
if (!start) start = now;
var done = true;
lines.forEach(function (o) {
var t = clamp((now - start - o.wait) / dur);
if (t < 1) done = false;
// The head shoots out first, then the tail catches it up, while the
// whole line drifts a little further out.
var head = easeOut(clamp(t / 0.55)), tail = easeIn(clamp((t - 0.3) / 0.7));
var drift = o.L * 0.45 * easeOut(t);
var h1 = drift + o.L * head, t1 = drift + o.L * tail;
o.el.setAttribute('x1', o.x + o.dx * t1);
o.el.setAttribute('y1', o.y + o.dy * t1);
o.el.setAttribute('x2', o.x + o.dx * h1);
o.el.setAttribute('y2', o.y + o.dy * h1);
o.el.setAttribute('opacity', t >= 1 || h1 - t1 < 0.3 ? 0 : 1);
});
el._bslRaf = done ? 0 : requestAnimationFrame(frame);
}
el._bslRaf = requestAnimationFrame(frame);
}
function init() {
var still = reduced();
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var el = e.target;
el._bslIn = e.isIntersecting;
if (!e.isIntersecting) return;
if (!el._bslSeen) { el._bslSeen = true; burst(el); }
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-burst-lines]').forEach(function (el) {
if (el._bsl) return;
el._bsl = true;
if (still) return;
setup(el);
el.addEventListener('pointerdown', function () { el._bslTouch = Date.now(); burst(el); });
el.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') burst(el); });
io.observe(el);
var d = el.dataset;
if (d.loop !== undefined && d.loop !== 'false') {
var every = num(d.interval, DEFAULTS.interval);
setInterval(function () {
// Skip a beat after a real tap, and stay quiet off screen.
if (el._bslIn && !document.hidden && Date.now() - (el._bslTouch || 0) > every) burst(el);
}, every);
}
});
}
window.animacious = window.animacious || {};
window.animacious.burstLines = burst;
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><span data-burst-lines data-color="#8b5cf6,#ec4899"
style="display: inline-block">Wow!</span>