Emoji Swap
Every so often a random letter pops into an emoji, holds for a beat, then pops back. Where it can it picks one shaped like the letter, so an O might turn into a doughnut or a football. Hover over or tap a letter to swap it yourself.
Having fun today
How to use it
- Paste the snippet just before
</head>. - Add
data-emoji-swapto any heading or text element.
Options
Add any of these attributes to the same element.
data-emoji | Your own emoji to pick from, separated by commas, e.g. data-emoji="π,β¨,π₯". Turns off the look-alike matching. |
data-interval | Time between swaps. Default 650 ms. |
data-hold | How long each emoji stays. Default 1100 ms. |
data-max | How many letters can be emoji at once. Default 2. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Emoji Swap: every so often a random letter pops into an emoji, holds for
// a moment, then pops back. Where it can, it picks an emoji shaped like
// the letter (an O becomes a doughnut or a ball). Tap or hover over a
// letter to swap it yourself.
// Usage: <h1 data-emoji-swap>Having fun</h1>
// Optional: data-emoji (your own emoji to pick from, separated by commas;
// turns off the look-alike matching), data-interval (ms between swaps;
// default 650), data-hold (ms an emoji stays; default 1100), data-max (how
// many letters can be emoji at once; default 2).
var DEFAULTS = { interval: 650, hold: 1100, max: 2 };
// Emoji that look a bit like the letter, then a general mix.
var LOOKALIKE = {
o: ['π©', 'β½', 'π', 'πͺ', 'β', 'π'],
a: ['π', 'π
°οΈ'], b: ['π
±οΈ', 'π'],
c: ['π', 'π₯'], d: ['π'], e: ['π§', 'π₯'],
g: ['π'], h: ['πͺ'], i: ['π―οΈ', 'βΉοΈ', 'π‘'],
j: ['π'], l: ['π', 'π―οΈ'], m: ['βοΈ', 'γ°οΈ'],
n: ['π'], p: ['π
ΏοΈ', 'π'], q: ['π³'],
s: ['π²', 'π'], t: ['π΄', 'βοΈ'], u: ['π§²'],
v: ['β
', 'π½'], w: ['γ°οΈ'], x: ['β', 'βοΈ'],
y: ['πΈ', 'πͺ'], z: ['β‘', 'π€'], 0: ['π©', 'β½'],
1: ['π―οΈ'], 8: ['βΎοΈ', 'β'], '!': ['β'], '?': ['β']
};
var MIX = ['π', 'π', 'π€©', 'π', 'β¨', 'π₯', 'π', 'β', 'π', 'π', 'π', 'π¦'];
var css =
'.ejs-word{display:inline-block;white-space:nowrap}' +
'.ejs-ch{display:inline-block;position:relative}' +
'.ejs-glyph{display:inline-block}' +
'.ejs-emo{position:absolute;left:50%;top:50%;font-size:.78em;line-height:1;letter-spacing:0;transform:translate(-50%,-50%) scale(0);font-family:"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif;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 (w, i) {
if (i) el.appendChild(document.createTextNode(' '));
var word = document.createElement('span');
word.className = 'ejs-word';
word.setAttribute('aria-hidden', 'true');
Array.from(w).forEach(function (c) {
var s = document.createElement('span');
s.className = 'ejs-ch';
var g = document.createElement('span');
g.className = 'ejs-glyph';
g.textContent = c;
var e = document.createElement('span');
e.className = 'ejs-emo';
s.appendChild(g);
s.appendChild(e);
word.appendChild(s);
chars.push({ el: s, glyph: g, emo: e, ch: c, busy: false });
});
el.appendChild(word);
});
return chars;
}
function pick(list) { return list[Math.floor(Math.random() * list.length)]; }
function setup(el) {
var d = el.dataset;
var interval = +d.interval || DEFAULTS.interval;
var hold = +d.hold || DEFAULTS.hold;
var max = +d.max || DEFAULTS.max;
var custom = d.emoji ? d.emoji.split(',').map(function (s) { return s.trim(); }).filter(Boolean) : null;
var chars = split(el);
if (reduced() || !chars.length) return;
chars.forEach(function (c) { anime.utils.set(c.emo, { x: '-50%', y: '-50%', scale: 0 }); });
var active = 0;
function emojiFor(c) {
if (custom) return pick(custom);
var own = LOOKALIKE[c.ch.toLowerCase()];
return own && Math.random() < 0.75 ? pick(own) : pick(MIX);
}
function swap(c) {
if (c.busy) return;
c.busy = true; active++;
c.emo.textContent = emojiFor(c);
var tilt = anime.utils.random(-18, 18);
anime.animate(c.glyph, { scale: [1, 0], rotate: [0, -tilt], duration: 180, ease: 'in(2)' });
anime.animate(c.emo, {
scale: [{ from: 0, to: 1.15, duration: 300, delay: 120, ease: 'outBack(2.5)' }, { to: 1, duration: 140 }, { to: 0, delay: hold, duration: 180, ease: 'in(2)' }],
rotate: [{ from: tilt, to: 0, duration: 420, delay: 120, ease: 'outBack(2)' }, { to: -tilt, delay: hold, duration: 180 }]
});
setTimeout(function () {
anime.animate(c.glyph, {
scale: [{ from: 0, to: 1, duration: 420, ease: 'outBack(2.5)' }], rotate: [{ from: tilt, to: 0, duration: 420, ease: 'outBack(2)' }],
onComplete: function () { c.busy = false; active--; }
});
}, 120 + 440 + hold + 120);
}
// Tap or hover a letter to swap it.
chars.forEach(function (c) {
c.el.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') { lastReal = performance.now(); swap(c); } });
c.el.addEventListener('pointerdown', function () { lastReal = performance.now(); swap(c); });
});
var lastReal = -1e9, timer = null;
var letters = chars.filter(function (c) { return /\S/.test(c.ch); });
function tick() {
if (active < max && performance.now() - lastReal > 1500) {
var free = letters.filter(function (c) { return !c.busy; });
if (free.length) swap(pick(free));
}
timer = setTimeout(tick, interval * anime.utils.random(0.7, 1.3, 2));
}
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(tick, 400);
}).observe(el);
}
function init() {
document.querySelectorAll('[data-emoji-swap]').forEach(function (el) {
if (el._ejs) return;
el._ejs = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-emoji-swap>Having fun today</h1>
<!-- party emoji only, more at once: -->
<h1 data-emoji-swap data-emoji="π,β¨,π₯³,π" data-max="4">Party time</h1>