Inflating Letters
Hover over a letter and it puffs up like a balloon, rising a little, catching a glossy highlight and nudging its neighbours to swell too. Then it slowly lets the air out and settles back into the line.
Pump it up
How to use it
- Paste the snippet just before
</head>. - Add
data-inflating-lettersto a heading. Leave a little room above it, as the letters grow upwards.
Options
Add any of these attributes to the same element.
data-size | How much a letter grows: 0.6 is 60% bigger. Default 0.55. |
data-spread | How much the letters either side swell, from 0 to 1. Default 0.4. |
data-deflate | How long a letter takes to go back down. Default 2600 ms. |
data-autoplay | Letters inflate by themselves every so often while nobody is hovering or touching. Handy for demos and touch screens. |
On a touch screen, tap a letter or slide a finger along the line to blow them up.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Inflating Letters: hover over a letter and it puffs up like a balloon,
// rising a little and catching a glossy highlight, with its neighbours
// swelling a bit too. Then it slowly deflates back to normal.
// Usage: <h1 data-inflating-letters>Pump it up</h1>
// Optional: data-size (how much a letter grows, 0.6 = 60% bigger; default
// 0.55), data-spread (how much its neighbours swell, 0 to 1; default 0.4),
// data-deflate (ms it takes to go back down; default 2600), data-autoplay
// (letters inflate by themselves while nobody is hovering or touching).
var DEFAULTS = { size: 0.55, spread: 0.4, deflate: 2600 };
var css =
'.infl-word{display:inline-block;white-space:nowrap}' +
'.infl-ch{display:inline-block;position:relative;transform-origin:50% 85%;will-change:transform}' +
'.infl-gl{position:absolute;left:0;top:0;right:0;bottom:0;color:transparent;-webkit-text-fill-color:transparent;' +
'background:radial-gradient(60% 45% at 38% 32%,rgba(255,255,255,.9),rgba(255,255,255,0) 70%);-webkit-background-clip:text;background-clip:text;opacity:0;text-shadow:none;pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function split(el) {
var text = el.textContent.replace(/\s+/g, ' ').trim();
el.setAttribute('aria-label', text);
el.textContent = '';
var chars = [];
text.split(' ').forEach(function (word, i) {
if (i) el.appendChild(document.createTextNode(' '));
var w = document.createElement('span');
w.className = 'infl-word';
w.setAttribute('aria-hidden', 'true');
Array.from(word).forEach(function (c) {
var s = document.createElement('span');
s.className = 'infl-ch';
s.textContent = c;
var g = document.createElement('span');
g.className = 'infl-gl';
g.textContent = c;
s.appendChild(g);
w.appendChild(s);
chars.push({ el: s, gloss: g, s: 0, tilt: 0, anim: null, i: chars.length });
});
el.appendChild(w);
});
return chars;
}
function setup(el) {
var d = el.dataset;
var size = d.size !== undefined ? +d.size : DEFAULTS.size;
var spread = d.spread !== undefined ? +d.spread : DEFAULTS.spread;
var deflate = +d.deflate || DEFAULTS.deflate;
var chars = split(el);
if (reduced() || !chars.length) return;
function draw(c) {
var s = c.s;
c.el.style.transform = 'translateY(' + (-0.1 * s).toFixed(3) + 'em) rotate(' + (c.tilt * s).toFixed(2) + 'deg) scale(' +
(1 + size * 0.85 * s).toFixed(3) + ',' + (1 + size * s).toFixed(3) + ')';
c.el.style.textShadow = s > 0.01 ? '0 ' + (0.12 * s).toFixed(3) + 'em ' + (0.14 * s).toFixed(3) + 'em rgba(30,10,60,' + (0.22 * s).toFixed(3) + ')' : '';
c.el.style.zIndex = s > 0.01 ? Math.round(s * 10) + 1 : '';
c.gloss.style.opacity = Math.min(1, s * 1.3).toFixed(3);
}
function inflate(c, amount) {
if (amount <= c.s + 0.02) return;
if (c.anim) c.anim.pause();
if (c.s < 0.05) c.tilt = anime.utils.random(-7, 7);
var tl = anime.createTimeline({ onUpdate: function () { draw(c); } });
tl.add(c, { s: amount, duration: 520, ease: 'outElastic(1, .55)' })
.add(c, { s: amount * 0.9, duration: 500, ease: 'inOut(2)' })
// Slowly lose air, with a last little wobble as it settles.
.add(c, { s: 0, duration: deflate, ease: 'inOut(3)' })
.add(c, { tilt: 0, duration: 1 });
c.anim = tl;
}
function poke(c) {
inflate(c, 1);
[-1, 1].forEach(function (k) {
var n = chars[c.i + k];
if (n && spread > 0) inflate(n, spread);
});
}
var lastReal = -1e9;
chars.forEach(function (c) {
c.el.addEventListener('pointerenter', function (e) {
if (e.pointerType !== 'mouse') return;
lastReal = performance.now();
poke(c);
});
});
// Touch: inflate what the finger is on, and what it slides over.
function touchAt(e) {
if (e.pointerType === 'mouse' && e.type !== 'pointerdown') return;
lastReal = performance.now();
var hit = document.elementFromPoint(e.clientX, e.clientY);
var ch = hit && hit.closest && hit.closest('.infl-ch');
chars.forEach(function (c) { if (c.el === ch) poke(c); });
}
el.addEventListener('pointerdown', touchAt);
el.addEventListener('pointermove', function (e) { if (e.pointerType !== 'mouse' && e.buttons) touchAt(e); });
if (d.autoplay === undefined || d.autoplay === 'false') return;
var letters = chars.filter(function (c) { return /\S/.test(c.el.firstChild.nodeValue); });
var timer = null;
function tick() {
if (performance.now() - lastReal > 2500) {
// Mostly a random letter, now and then a run along the word.
if (Math.random() < 0.25) {
letters.forEach(function (c, i) { setTimeout(function () { inflate(c, 0.6); }, i * 70); });
} else {
poke(letters[Math.floor(Math.random() * letters.length)]);
}
}
timer = setTimeout(tick, anime.utils.random(700, 1100));
}
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(tick, 300);
}).observe(el);
}
function init() {
document.querySelectorAll('[data-inflating-letters]').forEach(function (el) {
if (el._infl) return;
el._infl = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-inflating-letters>Pump it up</h1>
<!-- bigger balloons that go down faster: -->
<h1 data-inflating-letters data-size="0.8" data-deflate="1600">Pump it up</h1>