← Text reveals

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

  1. Paste the snippet just before </head>.
  2. Add data-pixelate-in-text to any heading, text element or image.

Options

Add any of these attributes to the same element.

data-stepsHow many block sizes it steps through. Default 7.
data-stepTime on each block size. Default 120 ms.
data-sizeThe first, biggest block, as a fraction of the font size. Default 0.34.
data-loopResolve, hold, pixelate away and resolve again.
data-holdHow long the text stays sharp before pixelating, when looping. Default 2000 ms.
data-delayWait before starting. Default 0 ms.

It pixelates the text as the browser draws it, so any font, colour or gradient works.

1 · Snippet
<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>
2 · Your HTML
<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>