← SVG & icons

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.

animacious

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-logo-build-on to your logo’s inline <svg>. Each child of the SVG (a shape, or a <g> of shapes) is a part.
  3. 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-durationTime for each part to land. Default 1100 ms.
data-staggerGap between parts. Default 90 ms.
data-spreadHow far away they start, in logo widths. Default 0.6.
data-loopBuild, hold, fly apart and build again.
data-holdHow long the logo stays built, when looping. Default 2000 ms.

It starts when scrolled into view. With reduced motion the logo simply shows.

1 · Snippet
<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>
2 · Your HTML
<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>