- CanvasAPI を使って、月相らしき簡易形状を表示します。
- スライダーを動かすと形状が変化します。
- 日時や月齢などの計算は一切行っていません。
See the Pen 月の満ち欠け by Ikiuo (@ikiuo) on CodePen.
輪郭の計算
以下のパラメータ
\begin{eqnarray}
L & = & 視点からの距離 (380,000) \\
R & = & 球体の半径 (1,738) \\
h & = & 球体の経度 (0=右端, \pi=左端) \\
p & = & 球体の緯度 \\
\end{eqnarray}
から球体の表面座標を
\begin{eqnarray}
x & = & R \cos{p} \cos{h} \\
y & = & R \sin{p} \\
z & = & L - R \cos{p} \sin{h} \\
\end{eqnarray}
として、透視変換したもの
\begin{eqnarray}
X & = & \frac{x}{z} \\
Y & = & \frac{y}{z} \\
\end{eqnarray}
を使用しています。見かけの大きさ(角度)は、球の中心から球面への接線方向の角度
h_a = \sin^{-1}{ \frac{R}{L} }
の2倍で、経度 $0$ への角度
h_0 = \tan^{-1}{ \frac{R}{L} }
よりも大きくなります。$(h_0 \lt h_a)$
ソース コード
sample.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>月の満ち欠け</title>
</head>
<body>
<table>
<tr>
<td>
<fieldset>
<legend>各種設定</legend>
<div>
<span>画像:</span>
<label>幅</label>
<input id="ScreenWidth" type="number"
value="500" min="10" max="10000" step="1"
oninput="updateParameter()">
<label>高</label>
<input id="ScreenHeight" type="number"
value="500" min="10" max="10000" step="1"
oninput="updateParameter()">
<span> </span>
<input type="button" value="リセット"
onclick="resetScreenSize()">
</div>
<div>
<label>月の大きさ:</label>
<input id="MoonSizeValue" type="number"
value="500" min="10" max="10000" step="1"
oninput="updateParameter()">
<input type="button" value="自動"
onclick="resetMoonSize()">
</div>
<div>
<label>月の色:</label>
<input id="MoonLightColor" type="text" value="yellow"
size="12" oninput="updateParameter()">
<input id="MoonLight" type="checkbox" checked
onclick="updateParameter()">
</div>
<div>
<label>影の色:</label>
<input id="MoonShadowColor" type="text" value="darkblue"
size="12" oninput="updateParameter()">
<input id="MoonShadow" type="checkbox" checked
onclick="updateParameter()">
</div>
<div>
<label>背景色:</label>
<input id="BackgroundColor" type="text" value="black"
size="12" oninput="updateParameter()">
<input id="Background" type="checkbox" checked
onclick="updateParameter()">
</div>
<div>
<label>傾斜:</label>
<input id="MoonRotateValue" type="number"
value="0" min="-1.0" max="1.0" step="0.001"
oninput="updateRotate()">
<input type="button" value="リセット"
onclick="resetMoonRotate()">
<br>
<input id="MoonRotateSlider" type="range"
value="0" min="-1.0" max="1.0" step="0.001"
style="width: 20em;"
oninput="updateRotateSlider()">
</div>
<div>
<label>満ち欠け:</label>
<input id="MoonPhaseValue" type="number"
value="0" min="-1.0" max="1.0" step="0.001"
oninput="updatePhase()">
<input type="button" value="リセット"
onclick="resetMoonPhase()">
<br>
<input id="MoonPhaseSlider" type="range"
value="0" min="-1.0" max="1.0" step="0.001"
style="width: 20em;"
oninput="updatePhaseSlider()">
</div>
</fieldset>
<canvas id="Screen" width="500" height="500"
style="margin: 2px; border: 0px; background-color: gray;">
</canvas>
</td>
</tr>
</table>
<script>
class TinyMoonPhase {
/*
* コンストラクタ
*
* radius = 視半径
* phase = 月光の位置
* -1.0 = -180 度 : 新月
* 0.0 = 0 度 : 満月
* +1.0 = +180 度 : 新月
*/
constructor(radius, phase, distance, sphereSize) {
if (distance == null)
distance = 3800000;
if (sphereSize == null)
sphereSize = 1738;
this.S = TinyMoonPhase;
this.radius = radius;
this.phase = phase;
this.distance = distance;
this.sphereSize = sphereSize;
this.latitudeMax = Math.acos(sphereSize / distance);
this.initCircle();
this.initPhase();
}
initCircle() {
const PI = Math.PI;
const quaterPI = PI * 0.25;
const radius = this.radius;
const count = radius >> 1;
const co1 = [...Array(count + 1)].map(function(_, n){
const r = quaterPI * n / count;
return [radius * Math.sin(r), radius * Math.cos(r)]
});
const co2 = co1.map(v => [v[1], v[0]]);
const cq1 = co1.concat(co2.toReversed().slice(1));
const cq2 = cq1.map(v => [v[0], -v[1]]);
const ch1 = cq1.concat(cq2.toReversed().slice(1));
const ch2 = ch1.map(v => [-v[0], v[1]]);
this.circleLeft = ch2;
this.circleRight = ch1;
}
initPhase() {
const PI = Math.PI;
const halfPI = PI * 0.5;
const radius = this.radius;
const distance = this.distance;
const sphereSize = this.sphereSize;
const mr = halfPI - this.latitudeMax;
const ml = PI - mr;
{
const x = sphereSize * Math.cos(mr);
const z = distance - sphereSize * Math.sin(mr);
this.viewScale = radius * z / x;
}
const pr = this.phase * PI;
const pl = pr + PI;
this.light = null;
this.shadow = null;
let ptype, sep;
if (ml <= pr || pl <= mr)
ptype = 2;
else if (pr > mr) {
ptype = 0;
sep = pr;
} else if (pl < ml) {
ptype = 1;
sep = pl;
} else
ptype = 3;
const cl = this.circleLeft;
const cr = this.circleRight;
if ((ptype & 2) != 0) {
const cd = cr.concat(cl.toReversed().slice(1, -1));
if ((ptype & 1) != 0)
this.light = cd;
else
this.shadow = cd;
} else {
const cc = this.getSeparator(sep);
const vr = cr.concat(cc.toReversed());
const vl = cc.concat(cl.toReversed());
if ((ptype & 1) != 0) {
this.light = vr;
this.shadow = vl;
} else {
this.light = vl;
this.shadow = vr;
}
}
}
getSeparator(sep) {
const radius = this.radius;
const distance = this.distance;
const sphereSize = this.sphereSize;
const viewScale = this.viewScale;
const latitudeMax = this.latitudeMax;
const sepCos = Math.cos(sep);
const sepSin = Math.sin(sep);
const count = radius >> 0;
const shp = [...Array(count + 1)].map(function(_, n) {
const t = latitudeMax * n / count;
const x = sphereSize * Math.cos(t) * sepCos;
const y = sphereSize * Math.sin(t);
const z = distance - sphereSize * Math.cos(t) * sepSin;
const v = viewScale / z;
return [v * x, v * y];
});
const shm = shp.map(v => [v[0], -v[1]]);
shp.reverse();
return shp.concat(shm.slice(1))
}
}
function getTagValue(tag) {
return Math.min(tag.max, Math.max(tag.min, tag.value));
}
function updateParameter() {
const halfPI = Math.PI / 2;
const canvas = document.getElementById('Screen');
const ctx = canvas.getContext("2d");
const w = getTagValue(ScreenWidth);
const h = getTagValue(ScreenHeight);
const cr = getTagValue(MoonSizeValue) / 2.0;
const mp = getTagValue(MoonPhaseValue);
const mr = getTagValue(MoonRotateValue);
const Ra = - mr * halfPI;
const cx = w / 2;
const cy = h / 2;
canvas.width = w;
canvas.height = h;
const moon = new TinyMoonPhase(cr, -mp);
getPath = function(pos) {
const rc = Math.cos(Ra);
const rs = Math.sin(Ra);
const path = new Path2D();
const vpos = pos.map(v => [
cx + (v[0] * rc - v[1] * rs),
cy - (v[1] * rc + v[0] * rs)
]);
vpos.forEach((v, n) => n ?
path.lineTo(v[0], v[1]) :
path.moveTo(v[0], v[1]));
path.closePath();
return path;
}
ctx.clearRect(0, 0, w, h);
if (Background.checked) {
ctx.fillStyle = BackgroundColor.value;
ctx.fillRect(0, 0, w, h);
}
if (moon.shadow && MoonShadow.checked) {
ctx.fillStyle = MoonShadowColor.value;
ctx.fill(getPath(moon.shadow));
}
if (moon.light && MoonLight.checked) {
ctx.fillStyle = MoonLightColor.value;
ctx.fill(getPath(moon.light));
}
}
function resetScreenSize() {
ScreenWidth.value = 500;
ScreenHeight.value = 500;
updateParameter();
}
function resetMoonSize() {
const w = getTagValue(ScreenWidth);
const h = getTagValue(ScreenHeight);
MoonSizeValue.value = Math.min(w, h);
updateParameter();
}
function resetMoonRotate() {
MoonRotateValue.value = 0.0;
MoonRotateSlider.value = 0.0;
updateParameter();
}
function resetMoonPhase() {
MoonPhaseValue.value = 0.0;
MoonPhaseSlider.value = 0.0;
updateParameter();
}
function updatePhase() {
const phase = getTagValue(MoonPhaseValue);
MoonPhaseSlider.value = phase;
updateParameter();
}
function updatePhaseSlider() {
const phase = getTagValue(MoonPhaseSlider);
MoonPhaseValue.value = phase;
updateParameter();
}
function updateRotate() {
const angle = getTagValue(MoonRotateValue);
MoonRotateSlider.value = angle;
updateParameter();
}
function updateRotateSlider() {
const angle = getTagValue(MoonRotateSlider);
MoonRotateValue.value = angle;
updateParameter();
}
window.onload = updateParameter;
</script>
</body>
</html>