Logo Shatter Hover
Point at the logo and it breaks into glassy shards that burst away from the pointer, spinning as they go. Move away and they fly back and lock together. On a phone a tap shatters it, and it puts itself back together.
animacious
Point at the logo, or tap it
How to use it
- Paste the snippet just before
</head>. - Add
data-logo-shatter-hoverto the element holding your logo: an<img>, an inline<svg>, a wordmark or all three. - Style the logo with a class rather than by its attribute, as the shards are copies of it without the attribute.
Options
Add any of these attributes to the same element.
data-cols | Columns the shards are cut from. Default 6. |
data-rows | Rows the shards are cut from. Default 3. |
data-force | How far the shards fly. Default 1. |
data-autoplay | It shatters itself every few seconds while in view, pausing when someone interacts. Handy for demos. |
Each shard is a copy of the whole logo clipped to a triangle, so keep the logo light (one image or a simple SVG); the copies are hidden from screen readers and their ids removed. animacious.logoShatter(el) shatters it once from code. With reduced motion it stays whole.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Logo Shatter Hover: point at the logo and it breaks into glassy shards
// that burst away from the pointer, spinning; move away and they fly back
// and lock together again. On a touch screen a tap shatters it and it
// reassembles by itself.
// Usage: <div data-logo-shatter-hover><img src="/logo.svg" alt="Your brand"></div>
// Works with any logo: an <img>, an inline <svg>, a wordmark or all three.
// Optional: data-cols and data-rows (the grid the shards are cut from,
// default 6 by 3), data-force (how far they fly, default 1), data-autoplay
// (it shatters itself every few seconds while in view, pausing when someone
// interacts; handy for demos).
// Script use: animacious.logoShatter(el) shatters and reassembles it once.
var DEFAULTS = { cols: 6, rows: 3, force: 1, every: 3400, hold: 900 };
var css =
'.lsh{position:relative;cursor:default;-webkit-tap-highlight-color:transparent}' +
'.lsh-whole{display:contents}' +
'.lsh.lsh-broken>.lsh-whole{visibility:hidden}' +
'.lsh-shards{position:absolute;inset:0;pointer-events:none;visibility:hidden}' +
'.lsh.lsh-broken>.lsh-shards{visibility:visible}' +
'.lsh-shard{position:absolute!important;left:0!important;top:0!important;width:100%!important;height:100%!important;margin:0!important;box-sizing:border-box;will-change:transform}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function rnd(a, b) { return a + Math.random() * (b - a); }
// Cut the box into a jittered grid, each cell split corner to corner into
// two triangles. Points are percentages of the logo's box.
function cut(cols, rows) {
var pts = [], r, c;
for (r = 0; r <= rows; r++) {
pts.push([]);
for (c = 0; c <= cols; c++) {
var x = (c / cols) * 100, y = (r / rows) * 100;
if (c > 0 && c < cols) x += rnd(-0.32, 0.32) * (100 / cols);
if (r > 0 && r < rows) y += rnd(-0.32, 0.32) * (100 / rows);
pts[r].push([x, y]);
}
}
var tris = [];
for (r = 0; r < rows; r++) {
for (c = 0; c < cols; c++) {
var a = pts[r][c], b = pts[r][c + 1], d = pts[r + 1][c], e = pts[r + 1][c + 1];
if ((r + c) % 2) { tris.push([a, b, e], [a, e, d]); }
else { tris.push([a, b, d], [b, e, d]); }
}
}
return tris;
}
function setup(el) {
var d = el.dataset;
// The shards are copies of the whole logo, each clipped to its triangle.
var copy = el.cloneNode(true);
copy.removeAttribute('data-logo-shatter-hover');
copy.removeAttribute('id');
copy.removeAttribute('role');
copy.removeAttribute('aria-label');
copy.querySelectorAll('[id]').forEach(function (n) { n.removeAttribute('id'); });
el.classList.add('lsh');
var whole = document.createElement('span');
whole.className = 'lsh-whole';
while (el.firstChild) whole.appendChild(el.firstChild);
el.appendChild(whole);
var layer = document.createElement('span');
layer.className = 'lsh-shards';
layer.setAttribute('aria-hidden', 'true');
var shards = cut(Math.max(1, +d.cols || DEFAULTS.cols), Math.max(1, +d.rows || DEFAULTS.rows)).map(function (t) {
var s = copy.cloneNode(true);
s.classList.add('lsh-shard');
s.style.clipPath = 'polygon(' + t.map(function (p) { return p[0].toFixed(2) + '% ' + p[1].toFixed(2) + '%'; }).join(',') + ')';
layer.appendChild(s);
return { el: s, cx: (t[0][0] + t[1][0] + t[2][0]) / 300, cy: (t[0][1] + t[1][1] + t[2][1]) / 300 };
});
el.appendChild(layer);
return { el: el, shards: shards, broken: false, busy: 0, quietUntil: 0 };
}
// Burst away from the point (px, py), given as fractions of the box.
function shatter(o, px, py) {
if (o.broken) return;
o.broken = true;
clearTimeout(o.back);
var w = o.el.offsetWidth, h = o.el.offsetHeight;
var force = +o.el.dataset.force || DEFAULTS.force;
var reach = Math.min(110, Math.max(w, h) * 0.22) * force;
o.el.classList.add('lsh-broken');
o.shards.forEach(function (s) {
var dx = (s.cx - px) * w, dy = (s.cy - py) * h;
var dist = Math.sqrt(dx * dx + dy * dy) || 1;
// Shards near the point of impact fly furthest.
var push = reach * (0.55 + 0.9 * Math.max(0, 1 - dist / Math.max(w, h))) * rnd(0.75, 1.25);
anime.utils.remove(s.el);
anime.animate(s.el, {
x: (dx / dist) * push + rnd(-6, 6),
y: (dy / dist) * push * 0.8 + rnd(-6, 10),
rotate: rnd(-50, 50),
scale: rnd(0.82, 0.96),
opacity: rnd(0.7, 0.95),
duration: rnd(520, 760),
ease: 'out(4)'
});
});
}
function mend(o) {
if (!o.broken) return;
o.broken = false;
var last = 0;
o.shards.forEach(function (s) {
var delay = rnd(0, 120), dur = rnd(560, 720);
last = Math.max(last, delay + dur);
anime.utils.remove(s.el);
anime.animate(s.el, { x: 0, y: 0, rotate: 0, scale: 1, opacity: 1, delay: delay, duration: dur, ease: 'inOut(3)' });
});
clearTimeout(o.back);
o.back = setTimeout(function () { if (!o.broken) o.el.classList.remove('lsh-broken'); }, last + 20);
}
function point(o, e) {
var r = o.el.getBoundingClientRect();
return [(e.clientX - r.left) / (r.width || 1), (e.clientY - r.top) / (r.height || 1)];
}
function once(o, px, py, hold) {
shatter(o, px, py);
clearTimeout(o.mendTimer);
o.mendTimer = setTimeout(function () { mend(o); }, hold || DEFAULTS.hold);
}
function bind(o) {
var el = o.el;
var hover = window.matchMedia('(hover: hover) and (pointer: fine)');
el.addEventListener('pointerenter', function (e) {
if (e.pointerType !== 'mouse' || !hover.matches) return;
o.quietUntil = Date.now() + 6000;
clearTimeout(o.mendTimer);
var p = point(o, e);
shatter(o, p[0], p[1]);
});
el.addEventListener('pointerleave', function (e) {
if (e.pointerType !== 'mouse' || !hover.matches) return;
o.quietUntil = Date.now() + 4000;
mend(o);
});
el.addEventListener('pointerdown', function (e) {
if (e.pointerType === 'mouse' && hover.matches) return;
o.quietUntil = Date.now() + 6000;
var p = point(o, e);
once(o, p[0], p[1], 1000);
});
}
function autoplay(o) {
var visible = false;
var tick = function () {
if (visible && !o.broken && Date.now() > o.quietUntil) once(o, rnd(0.3, 0.7), rnd(0.3, 0.7));
};
new IntersectionObserver(function (entries) { visible = entries[0].isIntersecting; }).observe(o.el);
setTimeout(tick, 900);
setInterval(tick, DEFAULTS.every);
}
function init() {
document.querySelectorAll('[data-logo-shatter-hover]').forEach(function (el) {
if (el._lsh) return;
var o = (el._lsh = setup(el));
if (reduced()) return;
bind(o);
if (el.dataset.autoplay !== undefined && el.dataset.autoplay !== 'false') autoplay(o);
});
}
window.animacious = window.animacious || {};
window.animacious.logoShatter = function (el) {
if (el && el._lsh && !reduced()) once(el._lsh, 0.5, 0.5);
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><a href="/" class="logo" data-logo-shatter-hover>
<img src="/logo.svg" alt="Your brand" width="180">
</a>