Moon Phase Loader
A moon waxes from new to full and wanes back again, its shadow edge curving across the face just like the real thing. Craters show on the lit side and a soft glow grows round it as it fills. Let it glide, or hop through the eight phases. Size it with font-size and colour it with color.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-moon-phase-loader>where the loader should appear.
Options
Add any of these attributes to the same element.
data-speed | Time for a full cycle, new moon to new moon. Default 4000 ms. |
data-steps | Hold on each of this many phases (8 is the classic set) and hop to the next, instead of gliding. |
data-stars | Add a few twinkling stars round the moon. |
data-dark | Colour of the unlit part. Default a faint color. |
It’s 2.4em across. It only moves while it’s on screen. With reduced motion the moon sits still as a waxing gibbous.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Moon Phase Loader: a moon waxes from new to full and wanes back again,
// its shadow edge curving across the face like the real thing. A few
// craters show on the lit side and a soft glow grows as it fills.
// Sized with font-size (2.4em across), coloured with color.
// It only moves while it's on screen.
// Usage: <div data-moon-phase-loader aria-label="Loading"></div>
// Optional: data-speed (ms for a full cycle, default 4000), data-steps
// (jump through this many phases, e.g. 8, instead of gliding), data-stars
// (a few twinkling stars round it), data-dark (colour of the unlit part,
// default color at low opacity).
var DEFAULTS = { speed: 4000 };
var SIZE = 2.4; // em
var R = 20; // moon radius in the 50 x 50 view box
var css =
'.mpl{display:inline-block;width:' + SIZE + 'em;height:' + SIZE + 'em;vertical-align:middle}' +
'.mpl svg{display:block;width:100%;height:100%;overflow:visible}' +
'.mpl-dark{fill:currentColor;opacity:.16}' +
'.mpl-lit{fill:currentColor}' +
'.mpl-crater{fill:#000;opacity:.13}' +
'.mpl-glow{fill:currentColor}' +
'.mpl-star{fill:currentColor}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var NS = 'http://www.w3.org/2000/svg';
var uid = 0;
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function make(parent, tag, attrs) {
var n = document.createElementNS(NS, tag);
for (var k in attrs) n.setAttribute(k, attrs[k]);
parent.appendChild(n);
return n;
}
// The lit part of the moon at phase p (0 new, 0.5 full, 1 new again):
// half the rim on the lit side, closed by an elliptical shadow edge.
function litPath(p) {
var right = p < 0.5; // waxing: lit on the right
var crescent = p < 0.25 || p > 0.75; // less than half lit
var rx = Math.abs(Math.cos(p * Math.PI * 2)) * R;
var outer = right ? 1 : 0;
var term = right ? (crescent ? 0 : 1) : (crescent ? 1 : 0);
return 'M25,' + (25 - R) +
'A' + R + ',' + R + ' 0 0 ' + outer + ' 25,' + (25 + R) +
'A' + rx.toFixed(3) + ',' + R + ' 0 0 ' + term + ' 25,' + (25 - R) + 'Z';
}
function build(el) {
var d = el.dataset;
var id = 'mpl-' + (++uid);
var steps = Math.round(+d.steps) || 0;
el.classList.add('mpl');
if (!el.getAttribute('role')) el.setAttribute('role', 'progressbar');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Loading');
el.textContent = '';
var svg = make(el, 'svg', { viewBox: '0 0 50 50', 'aria-hidden': 'true' });
var defs = make(svg, 'defs', {});
var grad = make(defs, 'radialGradient', { id: id + '-g' });
make(grad, 'stop', { offset: '55%', 'stop-color': 'currentColor', 'stop-opacity': '.22' });
make(grad, 'stop', { offset: '100%', 'stop-color': 'currentColor', 'stop-opacity': '0' });
var clip = make(defs, 'clipPath', { id: id + '-c' });
var clipPath = make(clip, 'path', {});
var stars = [];
if (d.stars !== undefined && d.stars !== 'false') {
[[4, 8, 1.1], [45, 6, 0.9], [47, 40, 1.2], [6, 44, 0.8], [41, 24, 0.7]].forEach(function (s, i) {
stars.push(make(svg, 'circle', { class: 'mpl-star', cx: s[0], cy: s[1], r: s[2] }));
stars[i].style.opacity = '.5';
});
}
var glow = make(svg, 'circle', { class: 'mpl-glow', cx: 25, cy: 25, r: R * 1.32, fill: 'url(#' + id + '-g)' });
var dark = make(svg, 'circle', { class: 'mpl-dark', cx: 25, cy: 25, r: R });
if (d.dark) { dark.style.fill = d.dark; dark.style.opacity = '1'; }
var lit = make(svg, 'path', { class: 'mpl-lit' });
var craters = make(svg, 'g', { 'clip-path': 'url(#' + id + '-c)' });
[[18, 17, 4.2], [31, 30, 5.2], [33, 15, 2.4], [17, 32, 2.8], [26, 24, 1.8], [37, 23, 1.6]].forEach(function (c) {
make(craters, 'circle', { class: 'mpl-crater', cx: c[0], cy: c[1], r: c[2] });
});
el.appendChild(svg);
function draw(p, t) {
p = (p % 1 + 1) % 1;
var path = litPath(p);
lit.setAttribute('d', path);
clipPath.setAttribute('d', path);
// Full moon glows; new moon doesn't.
var full = (1 - Math.cos(p * Math.PI * 2)) / 2;
glow.style.opacity = (full * full).toFixed(3);
stars.forEach(function (s, i) {
s.style.opacity = (0.25 + 0.6 * (0.5 + 0.5 * Math.sin(t / 420 + i * 1.9))).toFixed(3);
});
}
draw(0.3, 0);
if (reduced()) return null;
var speed = Math.max(600, +d.speed || DEFAULTS.speed);
var t = 0.12 * speed, last = 0;
return anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
t += last ? Math.min(50, now - last) : 16;
last = now;
var p = t / speed;
if (steps > 1) {
// Hold on each phase, then ease quickly to the next.
var k = p * steps, i = Math.floor(k), f = k - i;
f = f < 0.7 ? 0 : (f - 0.7) / 0.3;
f = f < 0.5 ? 2 * f * f : 1 - Math.pow(-2 * f + 2, 2) / 2;
p = (i + f) / steps;
}
draw(p, t);
},
onPause: function () { last = 0; },
});
}
function init() {
document.querySelectorAll('[data-moon-phase-loader]').forEach(function (el) {
if (el._mpl) return;
el._mpl = true;
var timer = build(el);
if (!timer) return;
new IntersectionObserver(function (entries) {
entries[0].isIntersecting ? timer.play() : timer.pause();
}).observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-moon-phase-loader style="font-size: 24px; color: #6a0dad" aria-label="Loading"></div>
<!-- hopping through the eight phases: -->
<div data-moon-phase-loader data-steps="8" data-speed="6400"></div>