← Text reveals

Circle Draw

A loose, hand-drawn circle loops around a word and overshoots where it started, as if someone just ringed it with a pen. Go round once, or twice for emphasis.

The best part

How to use it

  1. Paste the snippet just before </head>.
  2. Wrap the word or short phrase in <span data-circle-draw>.

Options

Add any of these attributes to the same element.

data-shapesingle (default) or double, a second, looser loop over the first.
data-colorStroke colour. Default #a855f7.
data-loopErase and redraw every few seconds.
data-durationTime to draw each loop. Default 1100 ms.
data-holdHow long it stays, when looping. Default 1800 ms.

The circle stretches to fit the word and its stroke scales with the text’s size. Best on one to three words.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Circle Draw: a loose, hand-drawn circle loops around a word, overshooting
    // where it started, as if someone just ringed it with a pen.
    // Usage: <h1>Don't miss <span data-circle-draw>this</span></h1>
    // Optional: data-color (default #a855f7), data-shape ("single" or "double",
    // a second, looser loop over the first), data-duration (ms), data-loop
    // (erase and redraw), data-hold (ms).
    var DEFAULTS = { color: '#a855f7', duration: 1100, hold: 1800 };
    // Drawn in a 100 × 40 box that stretches to fit the word. Each loop starts
    // top right, goes round anticlockwise and carries on past its start.
    var SHAPES = {
      single: ['M74 4.5 C 52 0.5, 14 3, 5 16 C -2 27, 22 37.5, 52 37 C 80 36.5, 99 29, 97 16 C 95.5 6, 72 1.5, 40 4.5'],
      double: [
        'M74 4.5 C 52 0.5, 14 3, 5 16 C -2 27, 22 37.5, 52 37 C 80 36.5, 99 29, 97 16 C 95.5 6, 72 1.5, 40 4.5',
        'M44 2.5 C 22 4, 1 11, 3 22 C 5 33, 34 39.5, 62 38 C 88 36.5, 101 26, 96 14.5 C 92 5.5, 70 2.8, 54 3.4'
      ]
    };

    var css =
      '.cdw{position:relative;display:inline-block;white-space:nowrap}' +
      '.cdw-svg{position:absolute;left:-12%;width:124%;top:-22%;height:144%;overflow:visible;pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function build(el) {
      var d = el.dataset;
      var paths = SHAPES[d.shape] || SHAPES.single;
      var width = Math.max(2, parseFloat(getComputedStyle(el).fontSize) * 0.06);
      el.classList.add('cdw');
      var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      svg.setAttribute('class', 'cdw-svg');
      svg.setAttribute('viewBox', '0 0 100 40');
      svg.setAttribute('preserveAspectRatio', 'none');
      svg.setAttribute('aria-hidden', 'true');
      svg.innerHTML = paths.map(function (p, i) {
        return '<path d="' + p + '" fill="none" stroke="' + (d.color || DEFAULTS.color) + '" stroke-width="' + width * (i ? 0.7 : 1) +
          '" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke"/>';
      }).join('');
      el.appendChild(svg);
      return anime.svg.createDrawable(svg.querySelectorAll('path'));
    }

    function run(el, strokes) {
      var d = el.dataset;
      var duration = +d.duration || DEFAULTS.duration;
      var loop = d.loop !== undefined && d.loop !== 'false';
      (function drawIn() {
        anime.animate(strokes, {
          draw: ['0 0', '0 1'],
          duration: duration,
          // A pen speeds up round the far side and slows as it comes back.
          ease: 'inOut(1.6)',
          delay: anime.stagger(duration * 0.8),
          onComplete: loop ? function () {
            setTimeout(function () {
              // Erase from the start of the stroke, so it unwinds the way it was drawn.
              anime.animate(strokes, {
                draw: '1 1', duration: duration * 0.6, ease: 'inOut(2)',
                onComplete: function () { anime.utils.set(strokes, { draw: '0 0' }); setTimeout(drawIn, 300); }
              });
            }, +d.hold || DEFAULTS.hold);
          } : undefined
        });
      })();
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          run(e.target, e.target._cdw);
        });
      }, { threshold: 0.6 });
      document.querySelectorAll('[data-circle-draw]').forEach(function (el) {
        if (el._cdw) return;
        var strokes = (el._cdw = build(el));
        if (reduced) return;
        anime.utils.set(strokes, { draw: '0 0' });
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1>Don't miss <span data-circle-draw>this</span></h1>

<!-- twice round, in pink: -->
<span data-circle-draw data-shape="double" data-color="#ec4899">best</span>