Liquid Wave Transition
A page transition: when a link is clicked, a rolling wave of colour rises up the screen until it covers it, and on the next page the wave carries on up and over the top. This page uses it, so try any link.
How to use it
- Add the snippet to every page of your site.
- Add
data-liquid-wave-transitionto each page’s<body>.
Options
Add any of these attributes to the same element.
data-liquid-wave-transition="#hex" | Wave colour. Default #6a0dad. |
data-duration | Time for the wave to rise, and again to leave. Default 900 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, and if anything goes wrong the link still opens. Coming back with the Back button rolls the wave away again. Put data-liquid-wave-transition on any other element with two or more children and it becomes a slideshow.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Liquid Wave Transition: a page transition. When a link is clicked a wave
// of colour rolls up from the bottom of the screen until it covers it; on
// the next page the wave carries on up and over the top, uncovering it.
// Usage: <body data-liquid-wave-transition> (add the snippet to every page)
// Optional: data-liquid-wave-transition="#hex" (wave colour, default
// #6a0dad), data-duration (ms for each half, default 900).
// Contained mode: put data-liquid-wave-transition on any other element with
// two or more children and a wave rolls over between them every
// data-interval ms (default 2200).
var DEFAULTS = { color: '#6a0dad', duration: 900, interval: 2200 };
var css =
'.lwt-layer{position:fixed;inset:0;width:100%;height:100%;z-index:2147483000;pointer-events:none;visibility:hidden;display:block}' +
'.lwt-contained{position:relative;overflow:hidden;display:grid}' +
'.lwt-contained>*:not(.lwt-layer){grid-area:1/1}' +
'.lwt-contained>.lwt-layer{position:absolute;z-index:2}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function layer(parent, color) {
var c = document.createElement('canvas');
c.className = 'lwt-layer';
c.setAttribute('aria-hidden', 'true');
parent.appendChild(c);
var L = { c: c, ctx: c.getContext('2d'), color: color, w: 0, h: 0 };
fit(L);
return L;
}
function fit(L) {
var w = L.c.clientWidth, h = L.c.clientHeight, dpr = Math.min(2, window.devicePixelRatio || 1);
if (L.c.width !== Math.round(w * dpr) || L.c.height !== Math.round(h * dpr)) {
L.c.width = Math.round(w * dpr);
L.c.height = Math.round(h * dpr);
}
L.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
L.w = w;
L.h = h;
}
// Height of a rolling wave edge at x: two swells moving opposite ways.
function edge(x, w, base, amp, time, shift) {
return base + amp * Math.sin(x / w * 7 + time * 3 + shift) + amp * 0.45 * Math.sin(x / w * 13 - time * 4.2 + shift * 2);
}
// Fill between the top of the box (fromTop) or its bottom and a wavy edge.
function fill(ctx, w, h, base, amp, time, shift, fromTop) {
ctx.beginPath();
ctx.moveTo(0, fromTop ? 0 : h);
for (var x = 0; x <= w + 6; x += 6) ctx.lineTo(x, edge(x, w, base, amp, time, shift));
ctx.lineTo(w, fromTop ? 0 : h);
ctx.closePath();
ctx.fill();
}
function ease(t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; }
// Rising: a paler crest leads the wave up. Leaving: the wave's tail rises
// after it, the paler wash trailing behind.
function paint(L, rising, t) {
var ctx = L.ctx, w = L.w, h = L.h;
var amp = Math.min(36, Math.max(8, h * 0.045)), gap = Math.max(18, h * 0.08);
var time = performance.now() / 1000;
var travel = h + amp * 3 + gap;
var base = h + amp * 1.5 + gap - ease(t) * travel;
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = L.color;
if (rising) {
ctx.globalAlpha = 0.4;
fill(ctx, w, h, base - gap, amp, time, 1.3, false);
ctx.globalAlpha = 1;
fill(ctx, w, h, base, amp, time, 0, false);
} else {
ctx.globalAlpha = 0.4;
fill(ctx, w, h, base, amp, time, 1.3, true);
ctx.globalAlpha = 1;
fill(ctx, w, h, base - gap, amp, time, 0, true);
}
}
function run(L, rising, duration, done) {
if (L.anim) L.anim.pause();
fit(L);
var s = { t: 0 };
L.c.style.visibility = 'visible';
paint(L, rising, 0);
L.anim = anime.animate(s, {
t: 1,
duration: duration,
ease: 'linear',
onUpdate: function () { paint(L, rising, s.t); },
onComplete: function () {
paint(L, rising, 1);
if (!rising) L.c.style.visibility = 'hidden';
if (done) done();
}
});
}
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 L = layer(body, body.getAttribute('data-liquid-wave-transition') || DEFAULTS.color);
var trip = 0;
run(L, false, duration);
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 {
run(L, true, duration, go);
setTimeout(go, duration + 400);
} catch (err) { go(); }
});
window.addEventListener('pageshow', function (e) {
if (!e.persisted) return;
trip++;
run(L, false, duration);
});
}
function containedMode(el, reduced) {
el.classList.add('lwt-contained');
var slides = Array.from(el.children);
var duration = +el.dataset.duration || DEFAULTS.duration;
var L = layer(el, el.getAttribute('data-liquid-wave-transition') || DEFAULTS.color);
var i = 0;
slides.forEach(function (s, n) { s.style.visibility = n ? 'hidden' : 'visible'; });
if (reduced) return;
var timer = anime.createTimer({
duration: (+el.dataset.interval || DEFAULTS.interval) + duration * 2 + 80,
loop: true,
autoplay: false,
onLoop: function () {
if (el.closest('[hidden]')) return;
run(L, true, duration, function () {
slides[i].style.visibility = 'hidden';
i = (i + 1) % slides.length;
slides[i].style.visibility = 'visible';
setTimeout(function () { run(L, false, duration); }, 80);
});
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) timer.play(); else timer.pause();
}).observe(el);
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('[data-liquid-wave-transition]').forEach(function (el) {
if (el._lwt) return;
el._lwt = true;
if (el === document.body) { if (!reduced) pageMode(el); }
else if (el.children.length > 1) containedMode(el, reduced);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><body data-liquid-wave-transition>
…
</body>