Moon phase calculator and visualizer

by thormeier

HTML

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="styles.css">
</head>

<h1 id="date-title">
  <!-- Will show the selected date -->
</h1>

<!-- The moon -->
<div class="sphere">
  <div class="light hemisphere"></div>
  <div class="dark hemisphere"></div>
  <div class="divider"></div>
</div>

<!-- The date input -->
<input type="date">

<script src="app.js"></script>
</html>

CSS

html {
    background-color: rgba(11,14,58,1);
    overflow: hidden;
}

body {
    text-align: center;
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

h1 {
    color: #F4F6F0;
    margin-bottom: 50px;
}

.sphere {
    border-radius: 100%;
    width: 250px;
    height: 250px;
    display: flex;
    overflow: hidden;
    align-items: center;
    position: relative;
    margin-bottom: 50px;
}

.hemisphere {
    width: 50%;
    height: 100%;
}

.light {
    background-color: #F4F6F0;
}

.dark {
    background-color: #575851;
}

.divider,
.divider:after {
    top: 0;
    left: 0;
    width: 250px;
    height: 250px;
    position: absolute;
    border-radius: 100%;
    transform-style: preserve-3d;
    backface-visibility: hidden;
}

.divider {
    background-color: #575851;
}

.divider:after {
    content: '';
    background-color: #F4F6F0;
    transform: rotateY(180deg);
}

JavaScript

const getMoonPhaseRotation = date => {
  const cycleLength = 29.5 // days

  const knownNewMoon = new Date('2022-03-02 18:34:00')
  const secondsSinceKnownNewMoon = (date - knownNewMoon) / 1000
  const daysSinceKnownNewMoon = secondsSinceKnownNewMoon / 60 / 60 / 24
  const currentMoonPhasePercentage = (daysSinceKnownNewMoon % cycleLength) / cycleLength

  return 360 - Math.floor(currentMoonPhasePercentage * 360)
}

const setMoonRotation = deg => {
  document.querySelector('.divider').style.transform = `rotate3d(0, 1, 0, ${deg}deg)`

  const hemispheres = document.querySelectorAll('.hemisphere')

  if (deg < 180) {
    // Left
    hemispheres[0].classList.remove('dark')
    hemispheres[0].classList.add('light')

    hemispheres[1].classList.add('dark')
    hemispheres[1].classList.remove('light')
  } else {
    hemispheres[0].classList.add('dark')
    hemispheres[0].classList.remove('light')

    hemispheres[1].classList.remove('dark')
    hemispheres[1].classList.add('light')
  }
}

const setMoonTitle = date => {
  document.querySelector('#date-title').innerHTML = `Moon phase for ${date.toUTCString()}`
}

const today = new Date()
const dateSelect = document.querySelector('input')

dateSelect.addEventListener('input', e => {
  const selectedDate = new Date(e.target.value)

  setMoonTitle(selectedDate)
  setMoonRotation(getMoonPhaseRotation(selectedDate))
})

dateSelect.value = today.toISOString().slice(0, 10)

setMoonTitle(today)
setMoonRotation(getMoonPhaseRotation(today))

const getStar = () => {
  const x = Math.round(Math.random() * 100)
  const y = Math.round(Math.random() * 100)

  return `
    radial-gradient(circle at ${x}% ${y}%, 
    rgba(255,255,255,1) 0%, 
    rgba(11,14,58,1) 3px, 
    rgba(11,14,58,0) 5px, 
    rgba(11,14,58,0) 100%) no-repeat border-box
  `
}

document.body.style.background = [...Array(100)].map(() => getStar()).join(', ')