月齢と月の満ち欠けを表示

JavaScriptで簡易的な月齢の計算及び月の満ち欠け表示を作成してみました。

![image.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618708/4ba55a01-3759-fc80-8ab1-f5e0bf5e624c.jpeg)

**[動作デモ](https://akebi.jp/temp/moonage/)**

日時及び[平均朔望月](https://ja.wikipedia.org/wiki/%E6%9C%94%E6%9C%9B%E6%9C%88#%E5%B9%B3%E5%9D%87%E6%9C%94%E6%9C%9B%E6%9C%88)からおおよその月齢を出し、月齢に応じた満ち欠けをcanvasを使用して描画しています。

満ち欠けの状態に応じた形をその都度描いているわけではなく、新月(暗 真円)、上弦及び下弦(明 半円)、リサイズ用真円(明暗切替)の3枚を重ね合わせて、CSSで反転や変形させることで表現しています。
言葉では伝わりにくいかと思いますが、こちらの[満ち欠けテストページ](https://akebi.jp/temp/moonage/test.html)で適当に操作していただければ分かりやすいかもしれません。

~~~html:index.html
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta name="viewport" content="user-scalable=no,width=device-width,initial-scale=1">
<meta charset='utf-8'>
<title>月齢表示</title>
<link rel='stylesheet' href='./style.css' type='text/css'>
<script src='./script.js'></script>
</head>
<body>
<div class='d'>
    <div class='moonAge'>
        <canvas id='a0'></canvas>
        <canvas id='a1'></canvas>
        <canvas id='a2'></canvas>
    </div>
    <div id='disp'>
    </div>
</div>
<input type='date' id='c' onchange='chg(this.value)'>
</body>
</html>
~~~

~~~css:style.css
.d {
    background: black;
    width: 150px;
}
#disp {
    color: #ffff00;
}
.moonAge {
    position: relative;
    height: 200px;
}
canvas[id*='a'] {
    position: absolute;
    top: 0px;
    left: 0px;
}
~~~

~~~js:script.js
'use strict';
const size = 200;

const
    pi = Math.PI,
    pi2 = pi * 2,
    topAngle = pi + pi / 2 * 3,
    bottomAngle = pi + pi / 2,
    halfSize = size / 2,

    c = [],
    ctx = [],
    start = [0, topAngle, 0],
    end = [pi2, bottomAngle, pi2];

window.addEventListener('DOMContentLoaded', function(){
    document.querySelector('.moonAge').style.height = `${size}px`;
    document.querySelector('.d').style.width = `${size}px`;
    for(let i = 0; i < 3; i++) {
        c[i] = document.getElementById(`a${i}`);
        c[i].style.width = `${size}px`;
        c[i].style.height = `${size}px`;
        c[i].width = size;
        c[i].height = size;
        ctx[i] = c[i].getContext('2d');
        ctx[i].fillStyle = i === 0 ? '#444444' : '#ffff00';
        ctx[i].arc(halfSize, halfSize, halfSize * .95, start[i], end[i]);
        ctx[i].fill();
    }
    const e = document.querySelector('#c');
    e.value = new Date().toLocaleDateString('sv');
    chg(e.value);
}, false);

function chg(d){
    const date = new Date(d);
    if(isNaN(date.getTime())) return;
    date.setHours(12);

    const day = date.getTime() / 864e5 - 6.475,
          // 平均朔望月
          r = 29.530588853 +
                2.162e-9 * ((date.getTime() - 946727935816) / 315576e5),
          age = day > 0 ? day % r : (r + day % r) % r;
    document.querySelector('#disp').innerHTML =
        `${date.toLocaleDateString()}<br>月齢:${age.toFixed(1)}`;
    appearance(age, r);
}

function appearance(age, m) {
    const s  = Math.cos(pi2 * age / m),
          s2 = Math.sin(pi2 * age / m),
          r  = Math.abs(halfSize * s);
    c[1].style.transform = `rotate(${s2 > 0 ? 180 : 0}deg)`;
    ctx[2].clearRect(0, 0, size, size);
    ctx[2].beginPath();
    ctx[2].fillStyle = s > 0 ? '#444444' : '#ffff00';
    ctx[2].arc(halfSize, halfSize, halfSize * .95, 0, pi2);
    ctx[2].fill();
    c[2].style.width = `${r * 2}px`;
    c[2].style.left = `${halfSize - r}px`;
}
~~~