← Seasonal

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

  1. Paste the snippet just before </head>.
  2. Add data-graduation-cap-toss to 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-loopThrow another volley every few seconds.
data-intervalTime between volleys when looping. Default 3600 ms.
data-countCaps in each volley. Default 7.
data-colorCap colour. Default #111827.
data-tasselTassel 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.

1 · Snippet
<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>
2 · Your HTML
<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>