Pixelate-in Text
The text starts as a handful of chunky pixels and resolves, step by step, through finer and finer blocks into sharp letters, like an old game loading a picture.
Now loading
How to use it
- Paste the snippet just before
</head>. - Add
data-pixelate-in-textto any heading, text element or image.
Options
Add any of these attributes to the same element.
data-steps | How many block sizes it steps through. Default 7. |
data-step | Time on each block size. Default 120 ms. |
data-size | The first, biggest block, as a fraction of the font size. Default 0.34. |
data-loop | Resolve, hold, pixelate away and resolve again. |
data-hold | How long the text stays sharp before pixelating, when looping. Default 2000 ms. |
data-delay | Wait before starting. Default 0 ms. |
It pixelates the text as the browser draws it, so any font, colour or gradient works.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Pixelate-in Text: text starts as a few chunky pixels and resolves, step by
// step, through finer and finer blocks into sharp letters, like an old game
// loading a picture.
// Usage: <h1 data-pixelate-in-text>Now loading</h1>
// Optional: data-steps (how many block sizes it steps through, default 7),
// data-step (ms per step, default 120), data-size (the first, biggest block
// as a fraction of the font size, default 0.34), data-loop (pixelate away,
// then resolve again), data-hold (ms to stay sharp before pixelating away),
// data-delay (ms before starting).
// It works on the text's real rendering, so any font, colour or gradient
// pixelates as it is.
var DEFAULTS = { steps: 7, step: 120, size: 0.34, hold: 2000, delay: 0 };
var NS = 'http://www.w3.org/2000/svg';
var count = 0;
function reduced() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// An SVG filter that samples the text once per block and paints each sample
// as a solid square. It blurs and boosts first, so thin strokes still show
// up in big blocks.
function filter(el) {
var id = 'pxt-filter-' + (++count);
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="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">' +
'<feGaussianBlur in="SourceGraphic" stdDeviation="1" result="soft"/>' +
'<feComponentTransfer in="soft" result="bold"><feFuncA type="linear" slope="1.7"/></feComponentTransfer>' +
'<feFlood x="0" y="0" width="1" height="1" flood-color="#000"/>' +
'<feComposite width="10" height="10"/>' +
'<feTile result="grid"/>' +
'<feComposite in="bold" in2="grid" operator="in"/>' +
'<feMorphology operator="dilate" radius="5"/>' +
'</filter>';
el.appendChild(svg);
var f = svg.querySelector('filter');
return {
id: id,
blur: f.querySelector('feGaussianBlur'),
flood: f.querySelector('feFlood'),
cell: f.querySelectorAll('feComposite')[0],
grow: f.querySelector('feMorphology')
};
}
// Set the block size, in px.
function blocks(f, b) {
b = Math.max(2, Math.round(b / 2) * 2);
f.blur.setAttribute('stdDeviation', (b / 4).toFixed(1));
f.flood.setAttribute('x', b / 2 - 1);
f.flood.setAttribute('y', b / 2 - 1);
f.flood.setAttribute('width', 2);
f.flood.setAttribute('height', 2);
f.cell.setAttribute('width', b);
f.cell.setAttribute('height', b);
f.grow.setAttribute('radius', b / 2);
}
function sizes(el) {
var d = el.dataset;
var fs = parseFloat(getComputedStyle(el).fontSize);
var n = Math.max(2, +d.steps || DEFAULTS.steps);
var first = fs * (+d.size || DEFAULTS.size);
var list = [];
// Shrinking geometrically, from the first size down to about 2 px.
for (var i = 0; i < n; i++) list.push(first * Math.pow(2.5 / first, i / (n - 1)));
return list;
}
// Steps through the block sizes, then shows the real text. `out` goes the
// other way and ends hidden.
function run(el, out, done) {
var s = el._pxt, d = el.dataset;
var step = +d.step || DEFAULTS.step;
var list = sizes(el);
if (out) list.reverse();
var i = 0;
el.style.filter = 'url(#' + s.id + ')';
(function next() {
if (i === list.length) {
if (out) el.style.opacity = 0;
el.style.filter = '';
return done && done();
}
blocks(s, list[i]);
el.style.opacity = out ? 1 - i / list.length * 0.5 : Math.min(1, 0.45 + i * 0.25);
i++;
setTimeout(next, step * (out ? 0.8 : 1) * (i === 1 && !out ? 1.6 : 1));
})();
}
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); }, 450); });
}, +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-pixelate-in-text]').forEach(function (el) {
if (el._pxt) return;
el._pxt = true;
if (reduced()) return;
el._pxt = filter(el);
el.style.opacity = 0;
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-pixelate-in-text>Now loading</h1>
<!-- or, in more, slower steps, looping: -->
<h1 data-pixelate-in-text data-steps="10" data-step="160" data-loop>Now loading</h1>