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
- Paste the snippet just before
</head>. - Wrap the word or short phrase in
<span data-circle-draw>.
Options
Add any of these attributes to the same element.
data-shape | single (default) or double, a second, looser loop over the first. |
data-color | Stroke colour. Default #a855f7. |
data-loop | Erase and redraw every few seconds. |
data-duration | Time to draw each loop. Default 1100 ms. |
data-hold | How 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.
<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><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>