← Text reveals

Rubber Stamp

The text thumps down onto the page like an inked rubber stamp: it drops from above, hits with a jolt, and leaves a slightly crooked, framed print with a rough, patchy ink texture.

APPROVED

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-rubber-stamp to a short word or phrase. Capitals look most like a real stamp.

Options

Add any of these attributes to the same element.

data-colorThe ink colour. Default #e11d48.
data-rotateHow crooked it lands, in degrees. Default -8.
data-framefalse for no border round the text.
data-loopStamp, hold, lift off and stamp again, a little differently each time.
data-holdHow long the print stays before lifting, when looping. Default 1800 ms.
data-delayWait before stamping. Default 0 ms.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Rubber Stamp: the text thumps down like an inked rubber stamp, slightly
    // rotated, framed, with a rough, patchy print texture.
    // Usage: <h2 data-rubber-stamp>APPROVED</h2>
    // Optional: data-color (the ink, default "#e11d48"), data-rotate (degrees,
    // default -8), data-frame ("false" for no border), data-loop (lift off and
    // stamp again, a little differently each time), data-hold (ms to stay,
    // when looping), data-delay (ms before starting).
    var DEFAULTS = { color: '#e11d48', rotate: -8, hold: 1800, delay: 0 };

    var css =
      '.rbs-on{display:inline-block;color:var(--rbs-ink);will-change:transform;' +
      '-webkit-mask-size:180px 180px;mask-size:180px 180px}' +
      '.rbs-frame{border:.08em solid currentColor;border-radius:.14em;padding:.06em .32em .1em;' +
      'outline:.03em solid currentColor;outline-offset:-.2em}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // A rough ink texture, made once: solid with specks, scratches and thin
    // patches knocked out of it, used as a mask.
    var texture;
    function ink() {
      if (texture) return texture;
      var n = 180, c = document.createElement('canvas');
      c.width = c.height = n;
      var x = c.getContext('2d');
      x.fillStyle = '#000';
      x.fillRect(0, 0, n, n);
      x.globalCompositeOperation = 'destination-out';
      var i;
      for (i = 0; i < 14; i++) {
        var g = x.createRadialGradient(0, 0, 0, 0, 0, 1);
        g.addColorStop(0, 'rgba(0,0,0,.35)');
        g.addColorStop(1, 'rgba(0,0,0,0)');
        x.save();
        x.translate(Math.random() * n, Math.random() * n);
        x.scale(10 + Math.random() * 26, 6 + Math.random() * 14);
        x.fillStyle = g;
        x.beginPath();
        x.arc(0, 0, 1, 0, Math.PI * 2);
        x.fill();
        x.restore();
      }
      x.fillStyle = '#000';
      for (i = 0; i < 700; i++) {
        x.globalAlpha = 0.5 + Math.random() * 0.5;
        x.beginPath();
        x.arc(Math.random() * n, Math.random() * n, 0.3 + Math.random() * 1.3, 0, Math.PI * 2);
        x.fill();
      }
      x.strokeStyle = '#000';
      for (i = 0; i < 10; i++) {
        x.globalAlpha = 0.6;
        x.lineWidth = 0.5 + Math.random();
        var sx = Math.random() * n, sy = Math.random() * n, a = Math.random() * Math.PI;
        x.beginPath();
        x.moveTo(sx, sy);
        x.lineTo(sx + Math.cos(a) * (8 + Math.random() * 24), sy + Math.sin(a) * (8 + Math.random() * 24));
        x.stroke();
      }
      texture = 'url(' + c.toDataURL() + ')';
      return texture;
    }

    function reduced() {
      return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    }

    function setup(el) {
      var d = el.dataset;
      el.classList.add('rbs-on');
      if (d.frame !== 'false') el.classList.add('rbs-frame');
      el.style.setProperty('--rbs-ink', d.color || DEFAULTS.color);
      el.style.webkitMaskImage = el.style.maskImage = ink();
      el.style.transform = 'rotate(' + (d.rotate !== undefined ? +d.rotate : DEFAULTS.rotate) + 'deg)';
    }

    function stamp(el, first, done) {
      var d = el.dataset;
      var base = d.rotate !== undefined ? +d.rotate : DEFAULTS.rotate;
      var rot = first ? base : base + (Math.random() * 6 - 3);
      var pos = (Math.random() * 180 | 0) + 'px ' + (Math.random() * 180 | 0) + 'px';
      el.style.webkitMaskPosition = el.style.maskPosition = pos;
      var tl = anime.createTimeline({ onComplete: done });
      // Swing down from above the page and hit hard.
      tl.add(el, { opacity: [0, 1], duration: 90, ease: 'linear' }, 0);
      tl.add(el, {
        scale: [2.3, 1],
        rotate: [rot - 9, rot],
        duration: 300,
        ease: 'in(3)'
      }, 0);
      // The thump: a little squash, a jolt, and the wet ink settling.
      tl.add(el, { scale: [1, 0.96, 1], duration: 260, ease: 'outQuad' }, 300);
      tl.add(el, { x: ['0px', '2px', '-2px', '1px', '0px'], duration: 220, ease: 'linear' }, 300);
      tl.add(el, { filter: ['blur(1.2px) brightness(.8)', 'blur(0px) brightness(1)'], duration: 600, ease: 'outQuad' }, 300);
    }

    function play(el, first) {
      var d = el.dataset;
      var loop = d.loop !== undefined && d.loop !== 'false';
      stamp(el, first, function () {
        if (!loop) return;
        setTimeout(function () {
          anime.animate(el, {
            opacity: [1, 0],
            scale: [1, 1.08],
            duration: 450,
            ease: 'inQuad',
            onComplete: function () { setTimeout(function () { play(el, false); }, 350); }
          });
        }, +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, true); }, +el.dataset.delay || DEFAULTS.delay);
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-rubber-stamp]').forEach(function (el) {
        if (el._rbs) return;
        el._rbs = true;
        setup(el);
        if (reduced()) return;
        el.style.opacity = 0;
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h2 data-rubber-stamp>APPROVED</h2>

<!-- or, in blue ink, tilted the other way: -->
<h2 data-rubber-stamp data-color="#2563eb" data-rotate="6">PAID</h2>