Glyph Rain Reveal
A column of random characters rains down into each letter’s place, scrambling as it falls. The columns slow as they land, each at its own moment, and leave the real letters behind.
Decoded
How to use it
- Paste the snippet just before
</head>. - Add
data-glyph-rain-revealto a heading or short line of text.
Options
Add any of these attributes to the same element.
data-color | The colour of the falling trail. Default #a855f7. |
data-trail | How many characters trail above each one. Default 5. |
data-duration | How long a column takes to fall. Default 1500 ms. |
data-stagger | The random spread in when columns start. Default 900 ms. |
data-loop | Rain in, hold, fade away and rain in again. |
data-hold | How long the text stays before fading, when looping. Default 1800 ms. |
data-delay | Wait before starting. Default 0 ms. |
The trails fall from just above the text, so leave a little room over it.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Glyph Rain Reveal: a column of random characters falls into each letter's
// place, slows down as it arrives and leaves the real letter behind.
// Usage: <h1 data-glyph-rain-reveal>Decoded</h1>
// Optional: data-color (the falling trail, default "#a855f7"), data-trail
// (characters in each column, default 5), data-duration (ms for a column to
// fall, default 1500), data-stagger (ms of random spread between columns,
// default 900), data-loop (fade away and rain in again), data-hold (ms to
// stay before fading, when looping), data-delay (ms before starting).
var DEFAULTS = { color: '#a855f7', trail: 5, duration: 1500, stagger: 900, hold: 1800, delay: 0 };
var GLYPHS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&@*+=<>?/';
var css =
'.grr-on{clip-path:inset(-1.4em -0.3em -0.3em -0.3em)}' +
'.grr-word{display:inline-block;white-space:nowrap}' +
'.grr-ch{display:inline-block;position:relative;transition:color .6s ease}' +
'.grr-hide{color:transparent!important}' +
'.grr-col{position:absolute;left:50%;bottom:0;display:flex;flex-direction:column;align-items:center;' +
'pointer-events:none;will-change:transform;' +
'-webkit-mask-image:linear-gradient(to bottom,transparent,#000 70%);mask-image:linear-gradient(to bottom,transparent,#000 70%)}' +
'.grr-col span{display:block;text-align:center}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
function glyph() { return GLYPHS[(Math.random() * GLYPHS.length) | 0]; }
function split(el) {
var text = el.textContent.replace(/\s+/g, ' ').trim();
el.setAttribute('aria-label', text);
el.textContent = '';
el.classList.add('grr-on');
var chars = [];
text.split(' ').forEach(function (word, i) {
if (i) el.appendChild(document.createTextNode(' '));
var w = document.createElement('span');
w.className = 'grr-word';
w.setAttribute('aria-hidden', 'true');
Array.from(word).forEach(function (c) {
var s = document.createElement('span');
s.className = 'grr-ch';
s.textContent = c;
w.appendChild(s);
chars.push(s);
});
el.appendChild(w);
});
return chars;
}
// Builds the falling column for one letter: trail glyphs on top, the head
// (which becomes the real letter) at the bottom.
function column(ch, n, color) {
var col = document.createElement('span');
col.className = 'grr-col';
col.setAttribute('aria-hidden', 'true');
var h = ch.offsetHeight;
var cells = [];
for (var i = 0; i <= n; i++) {
var g = document.createElement('span');
g.style.height = h + 'px';
g.style.lineHeight = h + 'px';
g.textContent = glyph();
if (i < n) {
g.style.color = color;
g.style.opacity = (0.25 + 0.6 * (i + 1) / n).toFixed(2);
}
col.appendChild(g);
cells.push(g);
}
ch.appendChild(col);
return { col: col, cells: cells, h: h, ch: ch, real: ch.firstChild.nodeValue, landed: false };
}
function rain(el, done) {
var s = el._grr, d = el.dataset;
var n = Math.max(1, +d.trail || DEFAULTS.trail);
var color = d.color || DEFAULTS.color;
var duration = +d.duration || DEFAULTS.duration;
var spread = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
var left = s.chars.length;
var cols = s.chars.map(function (ch) {
ch.classList.add('grr-hide');
ch.style.opacity = '';
ch.style.transform = '';
return column(ch, n, color);
});
// Scramble the glyphs while they fall, slower as each column slows.
var start = performance.now();
(function flicker() {
var live = false;
cols.forEach(function (c) {
if (c.landed) return;
live = true;
c.cells.forEach(function (g, i) {
var settling = c.p > 0.7 && i === n;
if (settling) g.textContent = c.real;
else if (Math.random() < 0.9 - (c.p || 0) * 0.6) g.textContent = glyph();
});
});
if (live) setTimeout(flicker, 60 + (performance.now() - start) / 40);
})();
cols.forEach(function (c) {
c.p = 0;
var dur = duration * (0.75 + Math.random() * 0.5);
var from = -(n + 2 + Math.random() * 3) * c.h;
c.col.style.transform = 'translate(-50%,' + from + 'px)';
anime.animate(c, {
p: [0, 1],
duration: dur,
delay: Math.random() * spread,
ease: 'outCubic',
onUpdate: function () {
c.col.style.transform = 'translate(-50%,' + (from * (1 - c.p)).toFixed(1) + 'px)';
},
onComplete: function () {
c.landed = true;
// The real letter takes over from the head, flashing the trail colour.
c.ch.style.transition = 'none';
c.ch.style.color = color;
c.ch.classList.remove('grr-hide');
c.cells[n].textContent = '';
void c.ch.offsetWidth;
c.ch.style.transition = '';
requestAnimationFrame(function () { c.ch.style.color = ''; });
anime.animate(c.col, {
opacity: [1, 0],
duration: 500,
ease: 'outQuad',
onComplete: function () {
c.col.remove();
if (--left === 0 && done) done();
}
});
}
});
});
}
function play(el) {
var d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
rain(el, function () {
if (!loop) return;
setTimeout(function () {
anime.animate(el._grr.chars, {
opacity: [1, 0],
y: ['0em', '0.4em'],
duration: 500,
ease: 'inQuad',
delay: anime.stagger(30),
onComplete: function () { setTimeout(function () { play(el); }, 300); }
});
}, +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.2 });
document.querySelectorAll('[data-glyph-rain-reveal]').forEach(function (el) {
if (el._grr) return;
el._grr = { chars: split(el) };
if (reduced()) return;
el._grr.chars.forEach(function (c) { c.classList.add('grr-hide'); });
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-glyph-rain-reveal>Decoded</h1>
<!-- or, with green trails, looping: -->
<h1 data-glyph-rain-reveal data-color="#22c55e" data-loop>Decoded</h1>