Hex the Moon

Regrets, dear reader, the moon and I are at it again.

The favicon on this humble site has been broken for a while now. This site is work in progress at all times, so I've let it linger. But looking at it tonight, I saw I'd mistaken actual words for a chain icon. "Undefined." Now, who am I to turn away from some debugging--but that's not why we're here.[1] It's been so long since I wrote this script, I'd forgotten how the thing works. So, join me in the righteous act of documentation and the thankless task of controlling the moon, won't you?

I'd made the landscape on this early in 2021, and by fall, I decided it would be fun to change the phase of the moon on the fly. Enter SunCalc, handy library to get the phase of the moon, among other things, based on the date:

const now = new Date();
const moon = sunCalc.getMoonIllumination(now);

Using SunCalc's phase-to-words chart, I needed a set of phases, which consisted of a limit or range of phase percentages, and a string that describes the phase:

const phases = [
	{ limit: 0, state: 'new' },
	{ limit: [
		0.000000000000001,
		0.249999999999999,
	], state: 'waxing-crescent' },
	{ limit: 0.25, state: 'first-quarter' },
	{ limit: [
		0.250000000000001,
		0.499999999999999,
	 ], state: 'waxing-gibbous' },
	{ limit: 0.5, state: 'full-moon' },
	{ limit: [
		0.500000000000001,
		0.749999999999999,
	 ], state: 'waning-gibbous' },
	{ limit: 0.75, state: 'last-quarter' },
	{ limit: [
		0.750000000000001,
		0.999999999999999,
	 ], state: 'waning-crescent' },
];

Then I walk through those phases, asking if today's moon looks familiar. If it does, we have found the state of the moon.

phases.forEach(phase => {
	if ('object' === typeof phase.limit) {
		if (moon.phase >= phase.limit[0] && moon.phase <= phase.limit[1]) {
			moon.state = phase.state;
		}
	} else if ('number' === typeof phase.limit) {
		if (moon.phase === phase.limit) {
			moon.state = phase.state;
		}
	}
});

Here we are. We know the state of the moon. The time has come to quietly change it both in the fav icon and the landscape on the bottom of the page.

For the fav icon, we have to create an SVG data URI that contains the emoji for the moon, then drop it into the link[rel="icon"]

const moon = new Moon();
const svgMoonPhaseMask = document.getElementById(`phase-mask--${moon.state}`);
const favIcon = document.querySelector('link[rel="icon"]');
const moonEmoji = {
	'new': '🌑',
	'waxing-crescent': '🌒',
	'first-quarter': '🌓',
	'waxing-gibbous': '🌔',
	'full': '🌕',
	'waning-gibbous': '🌖',
	'last-quarter': '🌗',
	'waning-crescent': '🌘',
};
const favIconData = `data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22 transform="rotate(45 50 50)">${moonEmoji[moon.state]}</text></svg>`;

svgMoonPhaseMask.classList.add(`phase--active`);
favIcon.href = favIconData;

The CSS handles the presentation. The landscape will always show a full moon by default. In fact, if the moon's phase is equal to 1, the script makes no change to the landscape DOM. Otherwise, it finds its corresponding g#phase mask--... and adds the phase--active class to bring the mask's opacity up.

The shapes are rough estimates, they fit the style of the landscape that way. I'm pretty pleased with how the moon has turned out, from drawing it by hand to a little lunar calendar.

Footnotes

  1. I was using the wrong property. Easy enough.