Flash Transition
A white flash punctuates the cut: the view burns out to white like a camera flash, cuts while the screen is blank, and the next view comes back a little overexposed before it settles. As a page transition the page flashes out when a link is clicked and the next page fades in from the glare. This page uses it, so try any link.
How to use it
- Add the snippet to every page of your site.
- Add
data-flash-transitionto each page’s<body>.
Options
Add any of these attributes to the same element.
data-flash-transition="#hex" | Flash colour. Default #ffffff. |
data-duration | Time for the whole flash. Default 700 ms on a page, 900 ms in a slideshow. |
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 fades the flash off again. Put data-flash-transition on any other element with two or more children and it flashes 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 () {
// Flash Transition: a white flash punctuates the cut. The view burns out to
// white like a camera flash, cuts while the screen is blank, and the next
// view comes back slightly overexposed before settling.
// Usage: <body data-flash-transition> (add the snippet to every page)
// Optional: data-flash-transition="#hex" (flash colour, default #ffffff),
// data-duration (ms for the whole flash, default 700 on a page and 900 for
// a slideshow change).
// Contained mode: put data-flash-transition on any other element with two
// or more children. Every data-interval ms (default 2200) it flashes to the
// next child.
var DEFAULTS = { color: '#ffffff', duration: 700, slideDuration: 900, interval: 2200 };
var KEY = 'animacious-flash';
var css =
'html.fsh-pre body{opacity:0}' +
'.fsh-layer{position:fixed;inset:0;z-index:2147483000;pointer-events:none;opacity:0}' +
'.fsh-contained{position:relative;overflow:hidden;display:grid}' +
'.fsh-contained>*{grid-area:1/1}' +
'.fsh-contained>.fsh-flash{position:absolute;inset:0;z-index:2;pointer-events:none;opacity:0}';
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('fsh-pre'); }
if (arriving && !reduced()) {
document.documentElement.classList.add('fsh-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(); }
});
}
// A flash is a soft-edged burst: brightest in the middle, filling out to
// the edges as it peaks.
function light(color, k) {
var edge = Math.round(30 + 70 * k);
return 'radial-gradient(circle at 50% 50%,' + color + ' ' + edge + '%,' + color + 'cc 100%)';
}
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 hex(c) {
// Only #rgb / #rrggbb get the soft edge; other colours flash flat.
return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(c) ? (c.length === 4 ? '#' + c[1] + c[1] + c[2] + c[2] + c[3] + c[3] : c) : null;
}
function pageMode(body) {
var duration = +body.dataset.duration || DEFAULTS.duration;
var color = body.getAttribute('data-flash-transition') || DEFAULTS.color;
var soft = hex(color);
var layer = document.createElement('div');
layer.className = 'fsh-layer';
layer.setAttribute('aria-hidden', 'true');
layer.style.background = color;
document.body.appendChild(layer);
var anim, trip = 0;
// Leaving: a fast burn up to the flash colour. Arriving: the flash fades
// off the new page, more slowly.
function run(out, done) {
if (anim) anim.pause();
anim = tween(out ? duration * 0.4 : duration * 0.6, out ? 'in(2)' : 'out(2)', function (t) {
var k = out ? t : 1 - t;
layer.style.opacity = k.toFixed(3);
if (soft) layer.style.background = light(soft, k);
}, done);
}
if (arriving) run(false);
unhide();
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 * 0.4 + 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('fsh-contained');
var slides = Array.from(el.children);
var duration = +el.dataset.duration || DEFAULTS.slideDuration;
var color = el.getAttribute('data-flash-transition') || DEFAULTS.color;
var soft = hex(color);
var flash = document.createElement('div');
flash.className = 'fsh-flash';
flash.setAttribute('aria-hidden', 'true');
flash.style.background = color;
var i = 0;
slides.forEach(function (s, n) { s.style.visibility = n ? 'hidden' : 'visible'; });
if (still) return;
el.appendChild(flash);
var PEAK = 0.2; // the cut happens here, at the flash's brightest
var timer = anime.createTimer({
duration: (+el.dataset.interval || DEFAULTS.interval) + duration,
loop: true,
autoplay: false,
onLoop: function () {
if (el.closest('[hidden]')) return;
var n = (i + 1) % slides.length, from = slides[i], to = slides[n], cut = false;
tween(duration, 'linear', function (t) {
var k, s;
if (t < PEAK) {
s = t / PEAK;
k = s * s * s; // burns up fast
from.style.filter = 'brightness(' + (1 + 1.6 * k).toFixed(3) + ') saturate(' + (1 - 0.5 * k).toFixed(3) + ')';
from.style.transform = 'scale(' + (1 + 0.03 * k).toFixed(4) + ')';
} else {
if (!cut) {
cut = true;
from.style.visibility = 'hidden';
from.style.filter = from.style.transform = '';
to.style.visibility = 'visible';
}
s = (t - PEAK) / (1 - PEAK);
k = Math.pow(1 - s, 2.4); // fades off slowly
to.style.filter = k > 0.002 ? 'brightness(' + (1 + 1.2 * k).toFixed(3) + ') contrast(' + (1 - 0.25 * k).toFixed(3) + ')' : '';
to.style.transform = 'scale(' + (1 + 0.04 * k).toFixed(4) + ')';
}
flash.style.opacity = k.toFixed(3);
if (soft) flash.style.background = light(soft, k);
}, function () {
to.style.filter = to.style.transform = '';
flash.style.opacity = 0;
i = n;
});
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) timer.play(); else timer.pause();
}).observe(el);
}
function init() {
var still = reduced();
document.querySelectorAll('[data-flash-transition]').forEach(function (el) {
if (el._fsh) return;
el._fsh = 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-flash-transition>
…
</body>