Dissolve-in Text
The text appears through a grainy noise dissolve, speck by speck, with a thin fringe of coloured grain running just ahead of the ink.
Out of the noise
How to use it
- Paste the snippet just before
</head>. - Add
data-dissolve-in-textto any heading, text element or image.
Options
Add any of these attributes to the same element.
data-color | The leading grain’s colour. Default #a855f7; none for no fringe. |
data-grain | Size of the grain, in px. Default 2. |
data-duration | Time to dissolve in. Default 1600 ms. |
data-loop | Dissolve in, hold, dissolve away and in again. |
data-hold | How long the text stays before dissolving away, when looping. Default 1800 ms. |
data-delay | Wait before starting. Default 0 ms. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Dissolve-in Text: text appears through a grainy noise dissolve, speck by
// speck, with a thin fringe of coloured grain running just ahead of the ink.
// Usage: <h1 data-dissolve-in-text>Out of the noise</h1>
// Optional: data-color (the leading grain, default #a855f7; "none" for no
// fringe), data-grain (grain size in px, default 2), data-duration (ms,
// default 1600), data-loop (dissolve away, then in again), data-hold (ms to
// stay before dissolving away), data-delay (ms before starting).
var DEFAULTS = { color: '#a855f7', grain: 2, duration: 1600, hold: 1800, delay: 0 };
var NS = 'http://www.w3.org/2000/svg';
var count = 0;
// How hard the noise is thresholded (higher = crisper specks), and how far
// the coloured fringe runs ahead.
var SHARP = 28, FRINGE = 0.09;
function reduced() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// Noise, thresholded at a moving level, decides which specks of the text
// show. A second, slightly lower threshold paints the fringe.
function filter(el) {
var d = el.dataset;
var id = 'dit-filter-' + (++count);
var color = d.color || DEFAULTS.color;
var fringe = color !== 'none';
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('width', '0');
svg.setAttribute('height', '0');
svg.style.position = 'absolute';
svg.innerHTML =
'<filter id="' + id + '" x="-5%" y="-10%" width="110%" height="120%" color-interpolation-filters="sRGB">' +
'<feTurbulence type="fractalNoise" baseFrequency="' + (1 / (+d.grain || DEFAULTS.grain)).toFixed(3) +
'" numOctaves="2" seed="' + (count * 7) + '" result="noise"/>' +
'<feColorMatrix in="noise" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0" result="n"/>' +
'<feComponentTransfer in="n" result="m1"><feFuncA type="linear" slope="' + SHARP + '" intercept="-99"/></feComponentTransfer>' +
'<feComposite in="SourceGraphic" in2="m1" operator="in" result="ink"/>' +
(fringe
? '<feComponentTransfer in="n" result="m2"><feFuncA type="linear" slope="' + SHARP + '" intercept="-99"/></feComponentTransfer>' +
'<feFlood flood-color="' + color + '"/>' +
'<feComposite in2="m2" operator="in"/>' +
'<feComposite in2="SourceAlpha" operator="in" result="edge"/>' +
'<feMerge><feMergeNode in="edge"/><feMergeNode in="ink"/></feMerge>'
: '') +
'</filter>';
el.appendChild(svg);
var funcs = svg.querySelectorAll('feFuncA');
return { id: id, ink: funcs[0], edge: funcs[1] };
}
// `t` is the threshold: specks whose noise is above it are visible.
function level(f, t) {
f.ink.setAttribute('intercept', (-SHARP * t).toFixed(3));
if (f.edge) f.edge.setAttribute('intercept', (-SHARP * (t - FRINGE)).toFixed(3));
}
function run(el, out, done) {
var s = el._dit, d = el.dataset;
var state = { t: out ? 0.1 : 0.82 };
level(s, state.t);
el.style.filter = 'url(#' + s.id + ')';
el.style.opacity = '';
anime.animate(state, {
t: out ? 0.82 : 0.1,
duration: (+d.duration || DEFAULTS.duration) * (out ? 0.7 : 1),
ease: out ? 'in(1.5)' : 'inOut(1.4)',
onUpdate: function () { level(s, state.t); },
onComplete: function () {
if (out) el.style.opacity = 0;
else el.style.filter = '';
if (done) done();
}
});
}
function play(el) {
var d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
run(el, false, function () {
if (!loop) return;
setTimeout(function () {
run(el, true, function () { setTimeout(function () { play(el); }, 400); });
}, +d.hold || DEFAULTS.hold);
});
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
var el = e.target;
setTimeout(function () { play(el); }, +el.dataset.delay || DEFAULTS.delay);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-dissolve-in-text]').forEach(function (el) {
if (el._dit) return;
el._dit = true;
if (reduced()) return;
el._dit = filter(el);
el.style.opacity = 0;
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-dissolve-in-text>Out of the noise</h1>
<!-- or, coarser grain with no coloured fringe: -->
<h1 data-dissolve-in-text data-grain="4" data-color="none">Out of the noise</h1>