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
- Add the snippet to every page of your site.
- Add
data-cube-rotate-pageto 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-direction | Which way the cube turns: left, right, up or down. Default left. |
data-duration | Time for each half of a page turn. Default 550 ms (1000 ms for a whole slideshow turn). |
data-interval | Contained 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.
<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><body data-cube-rotate-page>
…
</body>