Floating Astronaut 404
A little astronaut tumbles slowly through space, waving, as they drift past a big 404 whose zero is a ringed planet. Stars twinkle behind. Made for “page not found” pages.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-floating-astronaut-404>where the scene should go. It fills the element with an SVG scene.
Options
Add any of these attributes to the same element.
data-duration | How long the astronaut takes to drift across. Default 16000 ms. |
data-color | Colour of the planet and the visor. Default #a78bfa. |
It is 8:5 with a deep-space background by default; set your own height or background on the element to change that. It pauses when off screen. With reduced motion the astronaut stays put.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Floating Astronaut 404: a little astronaut tumbles slowly through space,
// waving, drifting past a big 404 whose zero is a ringed planet. Stars
// twinkle behind. Drawn as SVG that fills the element, and paused when
// off screen.
// Usage: <div data-floating-astronaut-404></div>
// It is 8:5 by default with a deep-space background; give it your own
// height or background to change that.
// Optional: data-duration (ms to drift across, default 16000),
// data-color (planet and visor colour, default #a78bfa).
var DEFAULTS = { duration: 16000, color: '#a78bfa' };
var uid = 0;
var css =
'[data-floating-astronaut-404]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5;' +
'background:radial-gradient(ellipse 80% 70% at 50% 45%,#2a1b5e 0%,#140f33 55%,#08071a 100%)}' +
'.fa4-svg{position:absolute;inset:0;width:100%;height:100%;display:block;overflow:visible}' +
'.fa4-num{font:800 128px/1 system-ui,-apple-system,"Segoe UI",sans-serif;letter-spacing:-4px}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function build(el) {
var d = el.dataset;
var n = ++uid, id = 'fa4' + n;
var color = d.color || DEFAULTS.color;
var duration = +d.duration || DEFAULTS.duration;
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', '404: an astronaut floating lost in space');
if (!el.hasAttribute('role')) el.setAttribute('role', 'img');
var stars = '';
for (var i = 0; i < 90; i++) {
stars += '<circle class="fa4-star" cx="' + (Math.random() * 1400 - 500).toFixed(1) + '" cy="' + (Math.random() * 500 - 125).toFixed(1) +
'" r="' + (Math.random() * 1 + 0.4).toFixed(2) + '" fill="#fff" opacity="' + (Math.random() * 0.6 + 0.2).toFixed(2) + '"/>';
}
el.innerHTML =
'<svg class="fa4-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
'<defs>' +
'<linearGradient id="' + id + '-visor" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#1e1b4b"/><stop offset="1" stop-color="' + color + '"/></linearGradient>' +
'<radialGradient id="' + id + '-planet" cx=".35" cy=".3"><stop offset="0" stop-color="#fff" stop-opacity=".55"/><stop offset=".35" stop-color="' + color + '"/><stop offset="1" stop-color="#3b0764"/></radialGradient>' +
'</defs>' +
stars +
'<circle cx="-40" cy="10" r="9" fill="#475569" opacity=".6"/><circle cx="440" cy="225" r="5" fill="#64748b" opacity=".5"/>' +
// The 404: two 4s and a ringed planet for the 0.
'<text class="fa4-num" x="112" y="170" text-anchor="middle" fill="#e9e3ff">4</text>' +
'<text class="fa4-num" x="288" y="170" text-anchor="middle" fill="#e9e3ff">4</text>' +
'<g transform="translate(200 124) rotate(-18)">' +
'<path d="M-74 0A74 17 0 0 1 74 0" fill="none" stroke="#c4b5fd" stroke-width="5" opacity=".75"/>' +
'<circle r="44" fill="url(#' + id + '-planet)"/>' +
'<circle cx="-14" cy="12" r="7" fill="#000" opacity=".12"/><circle cx="16" cy="-14" r="4" fill="#000" opacity=".1"/>' +
'<path d="M74 0A74 17 0 0 1 -74 0" fill="none" stroke="#c4b5fd" stroke-width="5" opacity=".95"/>' +
'</g>' +
// The astronaut, drawn around (0, 0).
'<g class="fa4-astro"><g class="fa4-spin">' +
'<rect x="-18" y="-10" width="36" height="32" rx="7" fill="#94a3b8"/>' +
'<rect x="-28" y="-2" width="11" height="25" rx="5.5" fill="#e2e8f0" transform="rotate(28 -22 0)"/>' +
'<rect x="-13" y="22" width="11" height="20" rx="5.5" fill="#e2e8f0" transform="rotate(12 -8 24)"/>' +
'<rect x="2" y="22" width="11" height="20" rx="5.5" fill="#e2e8f0" transform="rotate(-20 8 24)"/>' +
'<rect x="-15" y="-4" width="30" height="31" rx="10" fill="#f8fafc"/>' +
'<rect x="-7" y="5" width="14" height="9" rx="2" fill="' + color + '"/>' +
'<circle cx="-3" cy="9.5" r="1.4" fill="#fff"/><circle cx="2" cy="9.5" r="1.4" fill="#fde68a"/>' +
'<g transform="translate(13 0)"><g class="fa4-wave"><rect x="-5.5" y="-24" width="11" height="25" rx="5.5" fill="#e2e8f0"/><circle cx="0" cy="-24" r="6" fill="#f8fafc"/></g></g>' +
'<circle cx="0" cy="-20" r="18" fill="#f8fafc"/>' +
'<rect x="-13" y="-30" width="26" height="18" rx="9" fill="url(#' + id + '-visor)"/>' +
'<ellipse cx="-6" cy="-25" rx="4" ry="2.2" fill="#fff" opacity=".7"/>' +
'</g></g>' +
'</svg>';
var svg = el.firstChild;
var astro = svg.querySelector('.fa4-astro');
var spin = svg.querySelector('.fa4-spin');
var wave = svg.querySelector('.fa4-wave');
function draw(t) {
// Drift on a gentle diagonal from off the left to off the right.
var k = (t % duration) / duration;
var x = -90 + k * 580;
var y = 200 - k * 150 + Math.sin(t / 1300) * 10;
astro.setAttribute('transform', 'translate(' + x.toFixed(2) + ' ' + y.toFixed(2) + ') scale(1.25)');
spin.setAttribute('transform', 'rotate(' + ((t / 70) % 360).toFixed(2) + ')');
wave.setAttribute('transform', 'rotate(' + (Math.sin(t / 260) * 32 + 10).toFixed(2) + ')');
}
if (reduced()) {
// A still frame: the astronaut floating just off the planet.
astro.setAttribute('transform', 'translate(318 190) scale(1.25)');
spin.setAttribute('transform', 'rotate(-24)');
return;
}
var t = duration * 0.38, 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;
last = now;
draw(t);
}
});
draw(t);
var tw = anime.animate(svg.querySelectorAll('.fa4-star'), {
opacity: function () { return anime.utils.random(0.05, 0.9, 2); },
duration: function () { return anime.utils.random(1200, 3400); },
delay: function () { return anime.utils.random(0, 2000); },
ease: 'inOutSine',
loop: true,
alternate: true,
autoplay: false
});
// Pause while off screen to save battery.
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); tw.play(); } else { timer.pause(); tw.pause(); }
}).observe(el);
}
function init() {
document.querySelectorAll('[data-floating-astronaut-404]').forEach(function (el) {
if (el._fa4) return; // idempotent
el._fa4 = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-floating-astronaut-404></div>
<h1>Lost in space</h1>
<p>The page you were looking for has drifted off.</p>