Word Cloud Build
Words fly in from all sides, tumbling and growing, and settle into a tight cloud sized by their weight, the heaviest first. The cloud packs itself from the middle outwards and scales to fit its box, on a phone or a wide screen.
motioneasingtimingstaggerspringscrollhoverdelightSVGbouncefadeloopcanvasmorphdelayparallaxkeyframesrevealopacityphysicspathflowscalerotateblurtimelinetweenorbit
How to use it
- Paste the snippet just before
</head>. - Add
data-word-cloud-buildto a container with a height, with one child per word anddata-weightfor its size.
Options
Add any of these attributes to the same element.
data-colors | Comma-separated colours, used in turn. Default purple, pink, blue… |
data-color | On a word: its colour. |
data-rotate | Turn some of the words on their side. |
data-duration | Time for each word to fly in. Default 900 ms. |
data-stagger | Gap between words. Default 60 ms. |
data-loop | Scatter the words and build again. |
Words that can’t find a place are left out, so very long lists are trimmed from the lightest end.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Word Cloud Build: words fly in from all sides, tumbling and growing, and
// settle into a tight cloud sized by their weight, the heaviest first.
// The cloud is packed from the middle outwards and scaled to fit its box.
// Usage:
// <div data-word-cloud-build style="height:260px">
// <span data-weight="10">motion</span>
// <span data-weight="6">easing</span>
// <span data-weight="3">spring</span>
// </div>
// Each child is one word (or short phrase); data-weight sets its size.
// Optional: data-colors (comma-separated, used in turn, default purple,
// pink, blue…), data-color on a word, data-rotate (turn some words on
// their side), data-duration (ms for each word to fly in, default 900),
// data-stagger (gap between words, default 60), data-loop (scatter and
// build again).
var DEFAULTS = { duration: 900, stagger: 60, hold: 2600 };
var PALETTE = ['#a855f7', '#ec4899', '#0ea5e9', '#f59e0b', '#10b981', '#6366f1', '#6a0dad', '#f43f5e'];
var css =
'.wcl{position:relative;overflow:hidden}' +
'.wcl-in{position:absolute;left:50%;top:50%;width:0;height:0}' +
'.wcl-w{position:absolute;left:0;top:0;white-space:nowrap;font-weight:800;letter-spacing:-.02em;line-height:1;opacity:0;will-change:transform}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var colors = d.colors ? d.colors.split(',') : PALETTE;
var words = [];
Array.prototype.forEach.call(el.children, function (ch, i) {
var t = ch.textContent.trim();
if (!t) return;
var w = parseFloat(ch.getAttribute('data-weight'));
words.push({ text: t, weight: isNaN(w) ? 1 : w, color: ch.getAttribute('data-color') || colors[i % colors.length], order: i });
});
words.sort(function (a, b) { return b.weight - a.weight; });
el.classList.add('wcl');
el.setAttribute('role', 'img');
el.setAttribute('aria-label', 'Word cloud: ' + words.map(function (w) { return w.text; }).join(', ') + '.');
el.innerHTML = '';
var inner = document.createElement('div');
inner.className = 'wcl-in';
inner.setAttribute('aria-hidden', 'true');
el.appendChild(inner);
words.forEach(function (w, i) {
w.el = document.createElement('span');
w.el.className = 'wcl-w';
w.el.textContent = w.text;
w.el.style.color = w.color;
// Every third word after the biggest few turns on its side.
w.side = d.rotate !== undefined && d.rotate !== 'false' && i > 2 && i % 3 === 0;
inner.appendChild(w.el);
});
var c = { el: el, inner: inner, words: words };
el._wcl = c;
layout(c);
return c;
}
function hits(a, list) {
for (var i = 0; i < list.length; i++) {
var b = list[i];
if (a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y) return true;
}
return false;
}
// Lay the words out on a spiral from the middle, then scale the cloud to fit.
function layout(c) {
var W = c.el.clientWidth, H = c.el.clientHeight;
if (!W || !H) return;
c.W = W; c.H = H;
var lo = Math.min.apply(null, c.words.map(function (w) { return w.weight; }));
var hi = Math.max.apply(null, c.words.map(function (w) { return w.weight; }));
var big = Math.max(18, Math.min(H * 0.3, W * 0.14, 72)), small = Math.max(10, big * 0.26);
var placed = [], aspect = W / H, pad = Math.max(2, big * 0.06);
c.words.forEach(function (w) {
var f = hi > lo ? (w.weight - lo) / (hi - lo) : 1;
w.size = small + Math.sqrt(f) * (big - small);
w.el.style.fontSize = w.size.toFixed(1) + 'px';
var bw = w.el.offsetWidth, bh = w.el.offsetHeight * 0.92;
w.bw = bw; w.bh = bh;
var rw = (w.side ? bh : bw) + pad, rh = (w.side ? bw : bh) + pad;
w.placed = false;
for (var i = 0; i < 10000; i++) {
var a = i * 0.21, r = 3 * Math.sqrt(i);
var cx = Math.cos(a) * r * Math.sqrt(aspect), cy = Math.sin(a) * r / Math.sqrt(aspect);
var box = { x: cx - rw / 2, y: cy - rh / 2, w: rw, h: rh };
if (!hits(box, placed)) { placed.push(box); w.cx = cx; w.cy = cy; w.placed = true; break; }
}
});
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
placed.forEach(function (b) {
minX = Math.min(minX, b.x); minY = Math.min(minY, b.y);
maxX = Math.max(maxX, b.x + b.w); maxY = Math.max(maxY, b.y + b.h);
});
var s = Math.min(1.25, (W - 8) / (maxX - minX), (H - 8) / (maxY - minY));
var ox = (minX + maxX) / 2, oy = (minY + maxY) / 2;
c.inner.style.transform = 'scale(' + s.toFixed(3) + ')';
c.scale = s;
c.words.forEach(function (w) {
w.el.style.display = w.placed ? '' : 'none';
w.el.style.left = (w.cx - ox - w.bw / 2).toFixed(1) + 'px';
w.el.style.top = (w.cy - oy - w.bh / 2).toFixed(1) + 'px';
w.rot = w.side ? -90 : 0;
if (c.done) anime.utils.set(w.el, { x: 0, y: 0, rotate: w.rot, scale: 1, opacity: 1 });
});
}
function play(c) {
var d = c.el.dataset;
var dur = +d.duration || DEFAULTS.duration, stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
var R = Math.max(c.W, c.H) * 0.75 / (c.scale || 1);
c.done = false;
var shown = c.words.filter(function (w) { return w.placed; });
shown.forEach(function (w, i) {
var a = Math.random() * Math.PI * 2;
anime.animate(w.el, {
x: [Math.cos(a) * R, 0],
y: [Math.sin(a) * R, 0],
rotate: [w.rot + anime.utils.random(-50, 50), w.rot],
scale: [0.3, 1],
opacity: { to: 1, duration: dur * 0.3, ease: 'linear' },
duration: dur,
delay: i * stagger,
ease: 'out(4)'
});
});
var total = dur + (shown.length - 1) * stagger;
setTimeout(function () {
c.done = true;
if (d.loop === undefined || d.loop === 'false') return;
setTimeout(function () {
shown.forEach(function (w, i) {
var a = Math.random() * Math.PI * 2;
anime.animate(w.el, {
x: Math.cos(a) * R, y: Math.sin(a) * R, rotate: w.rot + anime.utils.random(-60, 60), scale: 0.4, opacity: 0,
duration: 700, delay: (shown.length - 1 - i) * 18, ease: 'in(3)'
});
});
setTimeout(function () { play(c); }, 700 + shown.length * 18 + 300);
}, DEFAULTS.hold);
}, total);
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
play(e.target._wcl);
});
}, { threshold: 0.3 });
var ro = new ResizeObserver(function (entries) {
entries.forEach(function (e) {
var c = e.target._wcl;
if (c && (e.target.clientWidth !== c.W || e.target.clientHeight !== c.H)) layout(c);
});
});
document.querySelectorAll('[data-word-cloud-build]').forEach(function (el) {
if (el._wcl) return;
var c = setup(el);
ro.observe(el);
if (reduced()) { c.done = true; layout(c); return; }
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-word-cloud-build style="height: 260px">
<span data-weight="10">motion</span>
<span data-weight="7">easing</span>
<span data-weight="5">stagger</span>
<span data-weight="3">spring</span>
<span data-weight="2">delight</span>
</div>