Iris Wipe
A page transition: a circle closes in on the spot you clicked until the screen goes dark, then opens out again on the next page, like the end of an old cartoon. This page uses it, so try any link.
How to use it
- Add the snippet to every page of your site.
- Add
data-iris-wipeto each page’s<body>.
Options
Add any of these attributes to the same element.
data-iris-wipe="#hex" | Colour. Default #0a0a0b. |
data-duration | Time for each close or open. Default 700 ms. |
data-interval | Contained mode only: time each slide stays. Default 2200 ms. |
Links to other sites, new tabs, downloads and #anchors are left alone. Put data-iris-wipe on any other element with two or more children and it becomes a slideshow that irises between them.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Iris Wipe: a page transition. When a link is clicked, a circle closes in
// on the spot you clicked until the screen is covered; on the next page it
// opens out again, like the end of an old cartoon.
// Usage: <body data-iris-wipe> (add the snippet to every page)
// Optional: data-iris-wipe="#0a0a0b" (colour, default near-black),
// data-duration (ms, default 700).
// Contained mode: put data-iris-wipe on any other element with two or more
// children and it irises between them every data-interval ms (a slideshow).
var DEFAULTS = { color: '#0a0a0b', duration: 700, interval: 2200 };
var KEY = 'animacious-iris';
var css =
'.iwx-panel{position:fixed;inset:0;z-index:2147483000;pointer-events:none;visibility:hidden;' +
'-webkit-mask-image:radial-gradient(circle at var(--iwx-x) var(--iwx-y),transparent var(--iwx-r),#000 calc(var(--iwx-r) + 1px));' +
'mask-image:radial-gradient(circle at var(--iwx-x) var(--iwx-y),transparent var(--iwx-r),#000 calc(var(--iwx-r) + 1px))}' +
'.iwx-contained{position:relative;overflow:hidden;display:grid}' +
'.iwx-contained>*:not(.iwx-panel){grid-area:1/1}' +
'.iwx-contained>.iwx-panel{position:absolute}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function panel(parent, color) {
var p = document.createElement('div');
p.className = 'iwx-panel';
p.setAttribute('aria-hidden', 'true');
p.style.background = color;
parent.appendChild(p);
return p;
}
// Animate the hole's radius from one size to another around (x, y).
function iris(p, x, y, from, to, duration, done) {
var box = p.getBoundingClientRect();
// Big enough to clear the farthest corner.
var full = Math.hypot(Math.max(x, box.width - x), Math.max(y, box.height - y)) + 2;
var s = { r: from * full };
p.style.setProperty('--iwx-x', x + 'px');
p.style.setProperty('--iwx-y', y + 'px');
var apply = function () { p.style.setProperty('--iwx-r', s.r + 'px'); };
apply();
p.style.visibility = 'visible';
// A new iris replaces one still running (e.g. a click while the page opens).
if (p._iwx) p._iwx.pause();
p._iwx = anime.animate(s, {
r: to * full, duration: duration, ease: to ? 'in(3)' : 'inOut(3)', onUpdate: apply,
onComplete: function () { if (to) p.style.visibility = 'hidden'; if (done) done(); }
});
}
function pageMode(body) {
var color = body.getAttribute('data-iris-wipe') || DEFAULTS.color;
var duration = +body.dataset.duration || DEFAULTS.duration;
var p = panel(body, color);
// Open out from where the visitor clicked on the last page, if we know it.
var at = null;
try { at = JSON.parse(sessionStorage.getItem(KEY)); sessionStorage.removeItem(KEY); } catch (e) { /* storage blocked */ }
var x = at ? at[0] * innerWidth : innerWidth / 2, y = at ? at[1] * innerHeight : innerHeight / 2;
iris(p, x, y, 0, 1, duration);
document.addEventListener('click', function (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;
if (a.target && a.target !== '_self') return;
if (a.hasAttribute('download')) return;
var url = new URL(a.href, location.href);
if (url.origin !== location.origin) return;
// Same page, only the #hash changes: let the browser scroll.
if (url.pathname === location.pathname && url.search === location.search && url.hash) return;
e.preventDefault();
try { sessionStorage.setItem(KEY, JSON.stringify([e.clientX / innerWidth, e.clientY / innerHeight])); } catch (err) { /* ignore */ }
iris(p, e.clientX, e.clientY, 1, 0, duration, function () { location.href = url.href; });
});
// Coming back with the Back button can restore the covered page: open it.
window.addEventListener('pageshow', function (e) {
if (e.persisted) p.style.visibility = 'hidden';
});
}
function containedMode(el) {
el.classList.add('iwx-contained');
var slides = Array.from(el.children);
var color = el.getAttribute('data-iris-wipe') || DEFAULTS.color;
var duration = +el.dataset.duration || DEFAULTS.duration;
var p = panel(el, color);
var i = 0;
slides.forEach(function (s, n) { s.style.visibility = n ? 'hidden' : 'visible'; });
setInterval(function () {
if (el.closest('[hidden]')) return;
var w = el.clientWidth, h = el.clientHeight;
// Close in on a spot a little off-centre, then open out from the middle.
iris(p, w * (0.35 + Math.random() * 0.3), h * (0.35 + Math.random() * 0.3), 1, 0, duration, function () {
slides[i].style.visibility = 'hidden';
i = (i + 1) % slides.length;
slides[i].style.visibility = 'visible';
setTimeout(function () { iris(p, w / 2, h / 2, 0, 1, duration); }, 150);
});
}, (+el.dataset.interval || DEFAULTS.interval) + duration * 2);
}
function init() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
document.querySelectorAll('[data-iris-wipe]').forEach(function (el) {
if (el._iwx) return;
el._iwx = true;
if (el === document.body) pageMode(el);
else if (el.children.length > 1) containedMode(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><body data-iris-wipe>
…
</body>