Sleeping Zzz
A cat sleeps curled up on a cushion, breathing slowly in and out, while Z’s float up from its head, growing and wobbling as they rise before fading away. Now and then an ear flicks or the tail tip twitches.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-sleeping-zzz>where the scene should go. It fills the element with an SVG scene.
Options
Add any of these attributes to the same element.
data-color | Colour of the Z’s. Default #7c3aed. |
data-cat | Colour of the cat. Default #fb923c. |
data-cushion | Colour of the cushion. Default #c4b5fd. |
data-speed | Speed multiplier. Default 1. |
It is 8:5 on a transparent background by default. It pauses when off screen. With reduced motion it’s a still picture.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Sleeping Zzz: a cat sleeps curled up on a cushion, breathing slowly,
// while Z's float up from its head, growing and drifting as they rise
// before fading away. Its tail tip and an ear twitch now and then.
// Drawn as SVG that fills the element, and paused when off screen.
// Usage: <div data-sleeping-zzz></div>
// It is 8:5 by default on a transparent background.
// Optional: data-color (colour of the Z's, default #7c3aed),
// data-cat (colour of the cat, default #fb923c), data-cushion (colour of
// the cushion, default #c4b5fd), data-speed (default 1).
var DEFAULTS = { color: '#7c3aed', cat: '#fb923c', cushion: '#c4b5fd', speed: 1 };
var NS = 'http://www.w3.org/2000/svg';
var ZS = 4; // Z's in the air at once
var RISE = 4400; // ms for one Z to rise and fade
var css =
'[data-sleeping-zzz]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
'.szz-svg{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible}' +
'.szz-z{font:800 20px/1 system-ui,-apple-system,"Segoe UI",sans-serif}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// Mix a hex colour towards black (amt < 0) or white (amt > 0).
function shade(hex, amt) {
var h = hex.replace('#', '');
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
var out = '#';
for (var i = 0; i < 3; i++) {
var c = parseInt(h.substr(i * 2, 2), 16);
c = amt < 0 ? c * (1 + amt) : c + (255 - c) * amt;
out += ('0' + Math.round(c).toString(16)).slice(-2);
}
return out;
}
function build(el) {
var d = el.dataset;
var zc = d.color || DEFAULTS.color;
var cat = d.cat || DEFAULTS.cat;
var cush = d.cushion || DEFAULTS.cushion;
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
var dark = shade(cat, -0.25), light = shade(cat, 0.45);
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'A cat asleep on a cushion with Z’s floating up');
if (!el.hasAttribute('role')) el.setAttribute('role', 'img');
el.innerHTML =
'<svg class="szz-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
'<path d="M86 40A15 15 0 1 0 102 62A12 12 0 1 1 86 40Z" fill="#fde68a"/>' +
'<path class="szz-star" d="M60 74l1.6 4 4 1.6-4 1.6-1.6 4-1.6-4-4-1.6 4-1.6Z" fill="#fde68a"/>' +
'<path class="szz-star" d="M122 30l1.3 3.2 3.2 1.3-3.2 1.3-1.3 3.2-1.3-3.2-3.2-1.3 3.2-1.3Z" fill="#fde68a"/>' +
// Cushion.
'<ellipse cx="204" cy="222" rx="104" ry="10" fill="#1e1b2e" opacity=".08"/>' +
'<path d="M104 204Q100 186 128 184L284 184Q310 186 304 204Q300 222 270 222L136 222Q108 222 104 204Z" fill="' + shade(cush, -0.18) + '"/>' +
'<path d="M108 198Q106 182 130 180L280 180Q302 182 300 198Q296 212 268 212L138 212Q112 212 108 198Z" fill="' + cush + '"/>' +
'<path d="M128 186Q200 182 282 186" stroke="#fff" stroke-opacity=".4" stroke-width="3" fill="none" stroke-linecap="round"/>' +
// The cat: body breathes, tail wraps round the front.
'<g class="szz-body">' +
'<ellipse cx="214" cy="170" rx="60" ry="31" fill="' + cat + '"/>' +
'<path d="M196 142Q200 152 196 160M216 140Q220 151 216 160M236 143Q240 152 236 160" stroke="' + dark + '" stroke-width="5" stroke-linecap="round" fill="none" opacity=".55"/>' +
'<ellipse cx="226" cy="186" rx="34" ry="10" fill="' + light + '" opacity=".55"/>' +
'</g>' +
'<path class="szz-tail" fill="none" stroke="' + cat + '" stroke-width="13" stroke-linecap="round"/>' +
'<g class="szz-head">' +
'<path class="szz-ear" d="M130 162L132 136L152 152Z" fill="' + cat + '"/><path d="M134 156L135 143L145 151Z" fill="#fda4af"/>' +
'<path d="M156 150L174 136L176 162Z" fill="' + cat + '"/><path d="M162 151L172 143L172 156Z" fill="#fda4af"/>' +
'<ellipse cx="153" cy="172" rx="28" ry="24" fill="' + cat + '"/>' +
'<path d="M146 152Q153 147 160 152" stroke="' + dark + '" stroke-width="3" stroke-linecap="round" fill="none" opacity=".55"/>' +
'<ellipse cx="153" cy="186" rx="13" ry="8" fill="' + light + '"/>' +
'<path d="M136 172Q141 177 146 172M160 172Q165 177 170 172" stroke="#3b2414" stroke-width="2.2" stroke-linecap="round" fill="none"/>' +
'<path d="M150.5 181L155.5 181L153 184Z" fill="#f472b6"/>' +
'<path d="M153 184Q151 188 148 187M153 184Q155 188 158 187" stroke="#3b2414" stroke-width="1.3" stroke-linecap="round" fill="none"/>' +
'<path d="M130 182L118 180M130 186L119 188M176 182L188 180M176 186L187 188" stroke="#3b2414" stroke-opacity=".35" stroke-width="1" stroke-linecap="round"/>' +
'<circle cx="137" cy="180" r="3.4" fill="#f472b6" opacity=".35"/><circle cx="169" cy="180" r="3.4" fill="#f472b6" opacity=".35"/>' +
'</g>' +
'<ellipse cx="143" cy="200" rx="10" ry="6" fill="' + light + '"/><ellipse cx="164" cy="201" rx="10" ry="6" fill="' + light + '"/>' +
'<g class="szz-zs" fill="' + zc + '"></g>' +
'</svg>';
var svg = el.firstChild;
var body = svg.querySelector('.szz-body');
var head = svg.querySelector('.szz-head');
var ear = svg.querySelector('.szz-ear');
var tail = svg.querySelector('.szz-tail');
var stars = svg.querySelectorAll('.szz-star');
var zg = svg.querySelector('.szz-zs');
var zs = [];
for (var i = 0; i < ZS; i++) {
var z = document.createElementNS(NS, 'text');
z.setAttribute('class', 'szz-z');
z.setAttribute('text-anchor', 'middle');
z.textContent = 'Z';
zg.appendChild(z);
zs.push(z);
}
function frame(t) {
var s = t / 1000;
// Slow breathing: about one breath every 3.4 s.
var br = 0.5 - 0.5 * Math.cos(s * Math.PI * 2 / 3.4);
body.setAttribute('transform', 'translate(214 200) scale(' + (1 + 0.012 * br).toFixed(4) + ' ' + (1 + 0.06 * br).toFixed(4) + ') translate(-214 -200)');
head.setAttribute('transform', 'translate(0 ' + (-1.6 * br).toFixed(2) + ') rotate(' + (-2 * br).toFixed(2) + ' 153 196)');
// An ear flicks every 7 s or so.
var ef = (s % 7.3);
var flick = ef > 5 && ef < 5.35 ? Math.sin((ef - 5) / 0.35 * Math.PI * 2) * 9 : 0;
ear.setAttribute('transform', 'rotate(' + flick.toFixed(2) + ' 140 158)');
// Tail wraps round the front, its tip lifting now and then.
var tw = Math.max(0, Math.sin(s * 0.9)) * Math.sin(s * 5) * 5;
tail.setAttribute('d', 'M266 178Q284 206 236 207Q200 208 ' + (186).toFixed(1) + ' ' + (205 - 2 * br).toFixed(1) + 'Q' + (176 + tw * 0.4).toFixed(1) + ' ' + (203 + tw).toFixed(1) + ' ' + (170 + tw * 0.3).toFixed(1) + ' ' + (196 + tw * 1.4).toFixed(1));
for (var k = 0; k < stars.length; k++) stars[k].setAttribute('opacity', (0.35 + 0.65 * (0.5 + 0.5 * Math.sin(s * 1.7 + k * 2.4))).toFixed(2));
// Z's: each rises from above the head, growing, wobbling and fading.
zs.forEach(function (z, i) {
var p = ((t / RISE) + i / ZS) % 1;
var x = 174 + p * 70 + Math.sin(p * Math.PI * 2.4 + i) * 9;
var y = 140 - p * 118;
var sc = 0.5 + p * 1.2;
var o = Math.min(1, p / 0.12) * Math.min(1, (1 - p) / 0.35);
z.setAttribute('transform', 'translate(' + x.toFixed(1) + ' ' + y.toFixed(1) + ') rotate(' + (-12 + Math.sin(p * 7 + i) * 10).toFixed(1) + ') scale(' + sc.toFixed(3) + ')');
z.setAttribute('opacity', o.toFixed(2));
});
}
if (reduced()) { frame(RISE * 0.3); return; }
var t = 0, last = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
t += (last ? Math.min(50, now - last) : 16) * speed;
last = now;
frame(t);
}
});
frame(0);
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-sleeping-zzz]').forEach(function (el) {
if (el._szz) return; // idempotent
el._szz = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-sleeping-zzz></div>
<h2>Nothing happening yet</h2>
<!-- a grey cat: -->
<div data-sleeping-zzz data-cat="#94a3b8" data-cushion="#fda4af"></div>