Graduation Cap Toss
A volley of mortarboards is flung up from the bottom of a section. They spin and tumble end over end, tassels swinging, then fall back down out of sight, with a little confetti thrown in. Tap to throw another.
Congratulations!
How to use it
- Paste the snippet just before
</head>. - Add
data-graduation-cap-tossto any section. The caps are thrown when it scrolls into view, and again on each click or tap.
Options
Add any of these attributes to the same element.
data-loop | Throw another volley every few seconds. |
data-interval | Time between volleys when looping. Default 3600 ms. |
data-count | Caps in each volley. Default 7. |
data-color | Cap colour. Default #111827. |
data-tassel | Tassel colour. Default gold #facc15. |
data-confetti="false" | Caps only, no confetti. |
data-tap="false" | No volley on click or tap. |
The caps fly over your content but never catch clicks. They are clipped to the section, sized to its width and paused when it is off screen. animacious.graduationCapToss(el) throws a volley from script. With reduced motion two caps hang still in the air.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Graduation Cap Toss: a volley of mortarboards is flung up from the
// bottom of a section. They spin and tumble end over end, tassels
// swinging, then fall back down out of sight, with a little confetti
// thrown in. A click or tap throws another volley. The caps fly over your
// content without catching clicks.
// Usage: <section data-graduation-cap-toss>…</section>
// Optional: data-loop (throw a volley every few seconds), data-interval
// (ms between volleys when looping, default 3600), data-count (caps per
// volley, default 7), data-color (cap colour, default #111827),
// data-tassel (tassel colour, default #facc15), data-confetti="false"
// (caps only), data-tap="false" (no volley on click or tap).
// Script use: animacious.graduationCapToss(el) throws a volley.
var DEFAULTS = { interval: 3600, count: 7, color: '#111827', tassel: '#facc15' };
var CONFETTI = ['#facc15', '#38bdf8', '#f472b6', '#4ade80', '#ffffff'];
var css =
'[data-graduation-cap-toss]{position:relative;overflow:hidden}' +
'.gct-layer{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:1;overflow:hidden;display:block}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var NS = 'http://www.w3.org/2000/svg';
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function f(n) { return (+n).toFixed(1); }
function shade(hex, amt) {
var h = hex.replace('#', '');
if (h.length === 3) h = h.replace(/./g, '$&$&');
var c = [0, 2, 4].map(function (i) { return parseInt(h.substr(i, 2), 16); });
return 'rgb(' + c.map(function (v) { return Math.round(amt > 0 ? v + (255 - v) * amt : v * (1 + amt)); }).join(',') + ')';
}
function capMarkup(color, tassel) {
// About 64 units wide, centred on the button.
return '<path d="M-17 3 L-17 14 Q0 22 17 14 L17 3 Z" fill="' + shade(color, -0.35) + '"/>' +
'<path d="M-17 13 Q0 21 17 13" fill="none" stroke="' + shade(color, 0.25) + '" stroke-width="1"/>' +
'<path d="M0 -12 L31 0 L0 12 L-31 0 Z" fill="' + color + '"/>' +
'<path d="M-31 0 L0 -12 L31 0" fill="none" stroke="' + shade(color, 0.35) + '" stroke-width="1.2" stroke-linejoin="round"/>' +
'<path d="M-31 0 L0 12 L31 0 L31 2.5 L0 14.5 L-31 2.5 Z" fill="' + shade(color, -0.45) + '"/>' +
'<g class="gct-tassel"><path d="M0 0 Q12 1 22 5 L22 16" fill="none" stroke="' + tassel + '" stroke-width="1.6" stroke-linecap="round"/>' +
'<path d="M19.5 15 L24.5 15 L26 27 L18 27 Z" fill="' + tassel + '"/></g>' +
'<circle r="2.4" fill="' + tassel + '"/>';
}
function build(el) {
var d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
var interval = Math.max(800, d.interval !== undefined ? +d.interval : DEFAULTS.interval);
var count = Math.max(1, Math.min(30, d.count !== undefined ? Math.round(+d.count) : DEFAULTS.count));
var color = d.color || DEFAULTS.color;
var tassel = d.tassel || DEFAULTS.tassel;
var confetti = d.confetti !== 'false';
var tap = d.tap !== 'false';
var svg = document.createElementNS(NS, 'svg');
svg.setAttribute('class', 'gct-layer');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('focusable', 'false');
el.appendChild(svg);
var W = 0, H = 0;
function layout() {
W = el.clientWidth; H = el.clientHeight;
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
}
layout();
if (window.ResizeObserver) new ResizeObserver(layout).observe(el);
var markup = capMarkup(color, tassel);
function capSize() { return Math.max(34, Math.min(72, W * 0.1)) / 64; }
var bits = [];
function addCap(x, y, apex, sc, still) {
var gEl = document.createElementNS(NS, 'g');
gEl.innerHTML = markup;
svg.appendChild(gEl);
var gr = H * 1.9;
var b = {
el: gEl, tas: gEl.querySelector('.gct-tassel'), cap: true,
x: x, y: y, vx: (Math.random() - 0.5) * W * 0.3, vy: -Math.sqrt(2 * gr * Math.max(20, y - apex)), g: gr,
rot: (Math.random() - 0.5) * 40, vr: (Math.random() < 0.5 ? -1 : 1) * (90 + Math.random() * 260),
flip: Math.random() * 6.3, vf: (Math.random() < 0.5 ? -1 : 1) * (3 + Math.random() * 5), sc: sc, ph: Math.random() * 6.3
};
if (still) { b.vx = b.vy = b.vr = b.vf = b.g = 0; b.flip = 0.5; }
bits.push(b);
place(b, 0);
}
function addConfetti(x, y, apex) {
var r = document.createElementNS(NS, 'rect');
var w = 4 + Math.random() * 4;
r.setAttribute('x', f(-w / 2)); r.setAttribute('y', '-2'); r.setAttribute('width', f(w)); r.setAttribute('height', '4'); r.setAttribute('rx', '1');
r.setAttribute('fill', CONFETTI[Math.floor(Math.random() * CONFETTI.length)]);
svg.insertBefore(r, svg.firstChild);
var gr = H * 1.2;
bits.push({
el: r, x: x, y: y, vx: (Math.random() - 0.5) * W * 0.5, vy: -Math.sqrt(2 * gr * Math.max(20, y - apex)), g: gr,
rot: Math.random() * 360, vr: (Math.random() - 0.5) * 900, flip: Math.random() * 6.3, vf: 6 + Math.random() * 8, sc: 1, drag: true
});
}
function place(b, s) {
var fy = Math.cos(b.flip);
b.el.setAttribute('transform', 'translate(' + f(b.x) + ' ' + f(b.y) + ') rotate(' + f(b.rot) + ') scale(' + (b.sc).toFixed(3) + ' ' + (b.sc * (Math.abs(fy) < 0.12 ? (fy < 0 ? -0.12 : 0.12) : fy)).toFixed(3) + ')');
if (b.tas) b.tas.setAttribute('transform', 'rotate(' + f(Math.sin(s * 9 + b.ph) * 18 - b.vr * 0.03) + ' 0 0)');
}
var t = 0;
function step(dt) {
t += dt;
var s = t / 1000, k = dt / 1000;
for (var i = bits.length - 1; i >= 0; i--) {
var b = bits[i];
b.vy += b.g * k;
if (b.drag) { b.vx *= 1 - 1.2 * k; if (b.vy > H * 0.35) b.vy = H * 0.35; }
b.x += b.vx * k; b.y += b.vy * k;
b.rot += b.vr * k; b.flip += b.vf * k;
if (b.vy > 0 && b.y > H + 60) { b.el.remove(); bits.splice(i, 1); continue; }
place(b, s);
}
}
function volley(cx) {
if (!W || !H) layout();
var sc = capSize();
var n = count;
for (var i = 0; i < n; i++) {
(function (i) {
setTimeout(function () {
var x = cx !== undefined ? cx + (Math.random() - 0.5) * W * 0.3 : W * (0.1 + 0.8 * ((i + Math.random()) / n));
addCap(x, H + 40 * sc, H * (0.06 + Math.random() * 0.3), sc * (0.8 + Math.random() * 0.35));
}, i * 70 + Math.random() * 60);
})(i);
}
if (confetti) {
var m = Math.round(Math.min(40, W / 12));
for (var j = 0; j < m; j++) addConfetti(cx !== undefined ? cx : W * (0.1 + 0.8 * Math.random()), H + 10, H * (0.05 + Math.random() * 0.45));
}
}
window.animacious = window.animacious || {};
window.animacious.graduationCapToss = function (target) { if (target && target._gctVolley) target._gctVolley(); };
if (reduced()) {
var sc = capSize();
addCap(W * 0.18, H * 0.28, 0, sc, true);
addCap(W * 0.82, H * 0.22, 0, sc * 0.9, true);
bits.forEach(function (b, i) { b.rot = i ? 14 : -16; place(b, 0); });
return;
}
var visible = false, last = 0, since = 0, thrown = false;
var timer = anime.createTimer({
loop: true, duration: 1000, autoplay: false,
onUpdate: function () {
var now = performance.now();
var dt = last ? Math.min(50, now - last) : 16;
last = now;
step(dt);
if (loop) {
since += dt;
if (since >= interval) { since = 0; volley(); }
}
}
});
el._gctVolley = function () { since = 0; volley(); };
if (tap) el.addEventListener('pointerdown', function (e) {
var r = el.getBoundingClientRect();
since = 0;
volley(e.clientX - r.left);
});
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
if (visible) {
last = 0; timer.play();
if (!thrown) { thrown = true; since = 0; setTimeout(volley, 250); }
} else timer.pause();
}, { threshold: 0.25 }).observe(el);
}
function init() {
document.querySelectorAll('[data-graduation-cap-toss]').forEach(function (el) {
if (el._gct) return; // idempotent
el._gct = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-graduation-cap-toss>
<h2>Congratulations!</h2>
</section>
<!-- navy caps with white tassels, again and again: -->
<section data-graduation-cap-toss data-loop data-color="#1e3a8a" data-tassel="#ffffff">…</section>