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
- Paste the snippet just before
</head>. - Add
data-rubber-stampto a short word or phrase. Capitals look most like a real stamp.
Options
Add any of these attributes to the same element.
data-color | The ink colour. Default #e11d48. |
data-rotate | How crooked it lands, in degrees. Default -8. |
data-frame | false for no border round the text. |
data-loop | Stamp, hold, lift off and stamp again, a little differently each time. |
data-hold | How long the print stays before lifting, when looping. Default 1800 ms. |
data-delay | Wait before stamping. Default 0 ms. |
<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><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>