1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

月の満ち欠けの簡易表示

Last updated at Posted at 2023-09-19
  • 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>&nbsp;</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>&nbsp;&nbsp;
              <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>&nbsp;&nbsp;
              <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>
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?