← 3D & depth

Isometric Assembly

An isometric illustration builds itself piece by piece: the ground, then the tree, the house, its door and windows, the roof and the chimney, each dropping into place with a little bounce. Loop it and the pieces lift away in reverse before it builds again.

How to use it

  1. Paste the snippet just before </head>.
  2. Draw your isometric picture as an SVG, one <g> per piece, in the order they should arrive (back to front).
  3. Add data-isometric-assembly to the SVG and data-piece to each piece.

Options

Add any of these attributes to the same element.

data-loopLift the pieces off in reverse, then build it again.
data-staggerGap between pieces. Default 260 ms.
data-durationHow long each piece takes to land. Default 750 ms.
data-dropHow far each piece falls, in px. Default 60.
data-holdHow long the finished picture stays before looping. Default 2200 ms.

Without data-piece, the SVG’s direct children are the pieces. It starts when scrolled into view; with reduced motion the picture is simply there.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Isometric Assembly: an isometric illustration builds itself piece by
    // piece, each part dropping into place with a little bounce, in the order
    // the pieces appear in your markup.
    // Usage:
    //   <svg data-isometric-assembly viewBox="…">
    //     <g data-piece>…</g>   <!-- the floor -->
    //     <g data-piece>…</g>   <!-- the walls -->
    //     <g data-piece>…</g>   <!-- the roof -->
    //   </svg>
    // Any element works too: its [data-piece] children are the parts. Without
    // [data-piece], the direct children of the element (or of its first <svg>)
    // are used.
    // Optional: data-loop (lift the pieces off in reverse and build it again),
    // data-stagger (ms between pieces, default 260), data-duration (ms each
    // piece takes to land, default 750), data-drop (how far each piece falls,
    // px, default 60), data-hold (ms the finished picture stays, default 2200).
    var DEFAULTS = { stagger: 260, duration: 750, drop: 60, hold: 2200 };

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function pieces(el) {
      var list = el.querySelectorAll('[data-piece]');
      if (list.length) return Array.from(list);
      var svg = el.tagName.toLowerCase() === 'svg' ? el : el.querySelector('svg');
      return Array.from((svg || el).children).filter(function (c) {
        return !/^(defs|title|desc|style)$/i.test(c.tagName);
      });
    }

    function setup(el) {
      var parts = pieces(el);
      if (!parts.length) return;
      el._iasParts = parts;
      anime.utils.set(parts, { opacity: 0 });
    }

    function play(el) {
      var d = el.dataset, parts = el._iasParts;
      var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
      var duration = +d.duration || DEFAULTS.duration;
      var drop = d.drop !== undefined ? +d.drop : DEFAULTS.drop;
      var hold = d.hold !== undefined ? +d.hold : DEFAULTS.hold;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var tl = anime.createTimeline({ loop: loop, loopDelay: 400 });
      tl.add(parts, {
        y: { from: -drop, to: 0 },
        opacity: { to: 1, duration: duration * 0.35, ease: 'linear' },
        duration: duration,
        ease: 'outBack(2)',
        delay: anime.stagger(stagger)
      });
      if (loop) {
        tl.add(parts, {
          y: -drop * 0.6,
          opacity: 0,
          duration: 420,
          ease: 'in(3)',
          delay: anime.stagger(Math.max(40, stagger * 0.3), { from: 'last' })
        }, '+=' + hold);
      }
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          play(e.target);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-isometric-assembly]').forEach(function (el) {
        if (el._ias) return;
        el._ias = true;
        if (reduced()) return;   // the finished picture, as drawn
        setup(el);
        if (el._iasParts) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<svg data-isometric-assembly data-loop viewBox="0 0 400 300">
  <g data-piece><!-- the ground --></g>
  <g data-piece><!-- the walls --></g>
  <g data-piece><!-- the roof --></g>
</svg>