Logo Build-on
The parts of a logo fly in from all around, spinning, and snap together into place with a springy landing, one after another. Each part comes from its own side of the logo, so it looks assembled rather than scattered.
How to use it
- Paste the snippet just before
</head>. - Add
data-logo-build-onto your logo’s inline<svg>. Each child of the SVG (a shape, or a<g>of shapes) is a part. - To choose the parts yourself, or to build an HTML logo, mark them with
data-part.
Options
Add any of these attributes to the same element.
data-duration | Time for each part to land. Default 1100 ms. |
data-stagger | Gap between parts. Default 90 ms. |
data-spread | How far away they start, in logo widths. Default 0.6. |
data-loop | Build, hold, fly apart and build again. |
data-hold | How long the logo stays built, when looping. Default 2000 ms. |
It starts when scrolled into view. With reduced motion the logo simply shows.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Logo Build-on: the parts of a logo fly in from all around, spinning, and
// snap together into place with a springy landing, one after another.
// Each part flies in from its own side of the logo, so it looks
// assembled rather than scattered. Starts when scrolled into view.
// Usage: <svg data-logo-build-on viewBox="…">…one shape or <g> per part…</svg>
// Every child of the SVG is a part. To pick the parts yourself (or to use
// HTML instead of an SVG), mark them with data-part.
// Optional: data-duration (ms per part, default 1100), data-stagger (ms
// between parts, default 90), data-spread (how far they fly from, in logo
// widths, default 0.6), data-loop (build, hold, fly apart and build again),
// data-hold (ms the logo stays built when looping, default 2000).
var DEFAULTS = { duration: 1100, stagger: 90, spread: 0.6, hold: 2000 };
var css = '.lbo-part{transform-box:fill-box;transform-origin:center}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function parts(el) {
var marked = el.querySelectorAll('[data-part]');
if (marked.length) return Array.from(marked);
return Array.from(el.children).filter(function (c) {
return ['defs', 'title', 'desc', 'style', 'script'].indexOf(c.tagName.toLowerCase()) < 0;
});
}
// Where each part comes from: straight out from the middle of the logo
// through the middle of the part, plus a random spin.
function scatter(el, list) {
var box = el.getBoundingClientRect();
var cx = box.left + box.width / 2, cy = box.top + box.height / 2;
var far = Math.max(box.width, box.height) * (+el.dataset.spread || DEFAULTS.spread);
return list.map(function (p, i) {
var r = p.getBoundingClientRect();
var dx = r.left + r.width / 2 - cx, dy = r.top + r.height / 2 - cy;
var len = Math.hypot(dx, dy);
// A part at the very centre shoots up from below instead.
if (len < 1) { dx = 0; dy = 1; len = 1; }
// SVG parts move in the SVG's own units.
var svg = p.ownerSVGElement;
var unit = 1;
if (svg && svg.viewBox.baseVal && svg.viewBox.baseVal.width) unit = svg.viewBox.baseVal.width / svg.getBoundingClientRect().width;
return {
x: (dx / len) * far * unit, y: (dy / len) * far * unit,
rotate: (i % 2 ? 1 : -1) * (90 + Math.random() * 120)
};
});
}
function run(el) {
var d = el.dataset;
var list = el._lbo;
var from = scatter(el, list);
var loop = d.loop !== undefined && d.loop !== 'false';
var duration = +d.duration || DEFAULTS.duration;
var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
var tl = anime.createTimeline({ loop: loop, loopDelay: 300 });
tl.add(list, {
// translateX/Y, not x/y: on SVG shapes such as <text> those are attributes.
translateX: [function (p, i) { return from[i].x; }, 0],
translateY: [function (p, i) { return from[i].y; }, 0],
rotate: [function (p, i) { return from[i].rotate; }, 0],
scale: [{ from: 0.3, to: 1, ease: 'outElastic(1, .65)' }],
opacity: [{ from: 0, to: 1, duration: duration * 0.25, ease: 'out(2)' }],
duration: duration,
delay: anime.stagger(stagger),
ease: 'outElastic(1, .75)'
});
// Once it's together, a small settle of the whole logo.
tl.add(el, { scale: [1, 1.04, 1], duration: 500, ease: 'inOut(2)' }, '-=' + duration * 0.45);
if (loop) {
tl.add(list, {
translateX: function (p, i) { return from[i].x * 0.8; },
translateY: function (p, i) { return from[i].y * 0.8; },
rotate: function (p, i) { return -from[i].rotate * 0.5; },
scale: 0.3,
opacity: [{ to: 0, duration: 380, delay: 120, ease: 'in(2)' }],
duration: 500,
delay: anime.stagger(stagger * 0.5, { from: 'last' }),
ease: 'in(3)'
}, '+=' + (+d.hold || DEFAULTS.hold));
}
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
run(e.target);
});
}, { threshold: 0.4 });
document.querySelectorAll('[data-logo-build-on]').forEach(function (el) {
if (el._lbo || reduced) return;
el._lbo = parts(el);
// Hide the parts until they fly in (they stay in place, so they can
// still be measured).
el._lbo.forEach(function (p) { p.classList.add('lbo-part'); p.style.opacity = 0; });
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><svg data-logo-build-on viewBox="0 0 100 100" role="img" aria-label="Your brand">
<polygon points="50,50 15,30 50,10" fill="#d8b4fe"/>
<polygon points="50,50 50,10 85,30" fill="#e9d5ff"/>
<!-- …one shape or <g> per part… -->
</svg>