← Page transitions

Cube Rotate Page

A page transition: when a link is clicked the page turns away round the edge of a cube, and the next page turns in as the neighbouring face. Choose which way the cube spins. This page uses it, so try any link.

How to use it

  1. Add the snippet to every page of your site.
  2. Add data-cube-rotate-page to each page’s <body>.

Options

Add any of these attributes to the same element.

data-cube-rotate-page="#hex"The colour behind the cube. Default #0f0b1a.
data-directionWhich way the cube turns: left, right, up or down. Default left.
data-durationTime for each half of a page turn. Default 550 ms (1000 ms for a whole slideshow turn).
data-intervalContained mode only: time each slide stays. Default 2200 ms.

Links to other sites, new tabs, downloads and #anchors are left alone, and if anything goes wrong the link still opens. Coming back with the Back button turns the page in again. Put data-cube-rotate-page on any other element with two or more children and the cube turns from one child to the next, like the demo above.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Cube Rotate Page: the view turns like the face of a cube. As a page
    // transition, the page turns away round the edge of a cube when a link is
    // clicked, and the next page turns in as the neighbouring face.
    // Usage: <body data-cube-rotate-page>   (add the snippet to every page)
    // Optional: data-cube-rotate-page="#hex" (the colour behind the cube,
    // default #0f0b1a), data-direction (left, right, up or down: which way the
    // cube turns, default left), data-duration (ms for each half on a page,
    // default 550; for a whole slideshow turn, default 1000).
    // Contained mode: put data-cube-rotate-page on any other element with two or
    // more children. Every data-interval ms (default 2200) the cube turns to
    // show the next child.
    var DEFAULTS = { color: '#0f0b1a', direction: 'left', duration: 550, slideDuration: 1000, interval: 2200 };
    var KEY = 'animacious-cube-rotate';

    var css =
      'html.crp-pre body{opacity:0}' +
      '.crp-contained{position:relative;overflow:hidden;display:grid}' +
      '.crp-contained>*{grid-area:1/1;backface-visibility:hidden;-webkit-backface-visibility:hidden}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    var arriving = false;
    try { arriving = sessionStorage.getItem(KEY) === '1'; sessionStorage.removeItem(KEY); } catch (e) {}
    function unhide() { document.documentElement.classList.remove('crp-pre'); }
    if (arriving && !reduced()) {
      document.documentElement.classList.add('crp-pre');
      setTimeout(unhide, 3000);
    }

    function tween(duration, ease, paint, done) {
      var s = { t: 0 };
      paint(0);
      return anime.animate(s, {
        t: 1, duration: duration, ease: ease,
        onUpdate: function () { paint(s.t); },
        onComplete: function () { paint(1); if (done) done(); }
      });
    }

    // The transform for a cube face turned by `deg` degrees from facing you,
    // for a cube of the given depth (the width or height of the view). `back`
    // pushes the whole cube a little further away while it turns.
    function face(dir, deg, depth, back) {
      var axis = dir === 'up' || dir === 'down' ? 'X' : 'Y';
      var sign = dir === 'left' || dir === 'down' ? -1 : 1;
      return 'translateZ(' + (-depth / 2 - back).toFixed(1) + 'px) rotate' + axis + '(' + (sign * deg).toFixed(2) + 'deg) translateZ(' + (depth / 2).toFixed(1) + 'px)';
    }

    function linkFor(e) {
      var a = e.target.closest && e.target.closest('a[href]');
      if (!a || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return null;
      if ((a.target && a.target !== '_self') || a.hasAttribute('download')) return null;
      var url = new URL(a.href, location.href);
      if (url.origin !== location.origin) return null;
      if (url.pathname === location.pathname && url.search === location.search && url.hash) return null;
      return url;
    }

    function pageMode(body) {
      var duration = +body.dataset.duration || DEFAULTS.duration;
      var dir = body.dataset.direction || DEFAULTS.direction;
      var color = body.getAttribute('data-cube-rotate-page') || DEFAULTS.color;
      var html = document.documentElement, saved = null, anim, trip = 0;

      // Leaving: this face turns away (0 to 90 degrees). Arriving: the next
      // face turns in from the other side (-90 to 0).
      function run(out, done) {
        if (anim) anim.pause();
        if (saved === null) saved = { bg: html.style.background, ox: html.style.overflowX };
        html.style.background = color;
        html.style.overflowX = 'hidden';
        var depth = dir === 'up' || dir === 'down' ? innerHeight : innerWidth;
        body.style.transformOrigin = '50% ' + (window.scrollY + innerHeight / 2) + 'px';
        anim = tween(duration, out ? 'in(2)' : 'out(3)', function (t) {
          var deg = out ? 90 * t : -90 * (1 - t);
          body.style.transform = 'perspective(' + Math.round(depth * 2.2) + 'px) ' + face(dir, deg, depth, depth * 0.15 * Math.sin(Math.abs(deg) / 90 * Math.PI / 2));
        }, function () {
          if (!out) {
            body.style.transform = body.style.transformOrigin = '';
            html.style.background = saved.bg;
            html.style.overflowX = saved.ox;
            saved = null;
          }
          if (done) done();
        });
      }
      run(false);

      document.addEventListener('click', function (e) {
        var url = linkFor(e);
        if (!url) return;
        e.preventDefault();
        var t = ++trip, gone = false;
        var go = function () { if (gone || t !== trip) return; gone = true; location.href = url.href; };
        try { sessionStorage.setItem(KEY, '1'); } catch (err) {}
        setTimeout(go, duration + 400);
        try { run(true, go); } catch (err) { go(); }
      });
      window.addEventListener('pageshow', function (e) {
        if (!e.persisted) return;
        trip++;
        try { sessionStorage.removeItem(KEY); } catch (err) {}
        run(false);
      });
    }

    function containedMode(el, still) {
      el.classList.add('crp-contained');
      el.style.background = el.getAttribute('data-cube-rotate-page') || DEFAULTS.color;
      var slides = Array.from(el.children);
      var duration = +el.dataset.duration || DEFAULTS.slideDuration;
      var dir = el.dataset.direction || DEFAULTS.direction;
      var i = 0;
      slides.forEach(function (s, n) { s.style.visibility = n ? 'hidden' : 'visible'; });
      if (still) return;

      var timer = anime.createTimer({
        duration: (+el.dataset.interval || DEFAULTS.interval) + duration,
        loop: true,
        autoplay: false,
        onLoop: function () {
          if (el.closest('[hidden]')) return;
          var from = slides[i], to = slides[(i + 1) % slides.length];
          var depth = dir === 'up' || dir === 'down' ? el.clientHeight : el.clientWidth;
          if (!depth) return;
          el.style.perspective = Math.round(depth * 2.2) + 'px';
          to.style.visibility = 'visible';
          tween(duration, 'inOut(3)', function (t) {
            var back = depth * 0.2 * Math.sin(t * Math.PI);
            // Faces darken as they turn away from the light.
            from.style.transform = face(dir, 90 * t, depth, back);
            from.style.filter = 'brightness(' + (1 - 0.55 * t).toFixed(3) + ')';
            to.style.transform = face(dir, -90 * (1 - t), depth, back);
            to.style.filter = 'brightness(' + (0.45 + 0.55 * t).toFixed(3) + ')';
          }, function () {
            from.style.visibility = 'hidden';
            from.style.transform = from.style.filter = '';
            to.style.transform = to.style.filter = '';
            i = (i + 1) % slides.length;
          });
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) timer.play(); else timer.pause();
      }).observe(el);
    }

    function init() {
      var still = reduced();
      document.querySelectorAll('[data-cube-rotate-page]').forEach(function (el) {
        if (el._crp) return;
        el._crp = true;
        if (el === document.body) { if (!still) pageMode(el); }
        else if (el.children.length > 1) containedMode(el, still);
      });
      unhide();
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<body data-cube-rotate-page>
  …
</body>