[JavaScript] Canvasでアナログ時計を作る

CanvasとJSを使って、こんな感じのアナログ時計を作ります。
<p class="codepen" data-height="500" data-theme-id="light" data-default-tab="js,result" data-user="non_123" data-slug-hash="WNvLqRJ" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Clock">
  <span>See the Pen <a href="https://codepen.io/non_123/pen/WNvLqRJ">
  Clock</a> by nzzzz (<a href="https://codepen.io/non_123">@non_123</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

#HTMLとCSS
htmlのCanvas要素で、描画領域の指定をします。
タグにwidthやheight属性を指定しない場合の描画領域は、canvasのデフォルト値300px * 150pxとなります。

```html:index.html
<div class="clock-wrap">
  <canvas class="clock" width="300" height="300"></canvas>
  <time class="timearea"></time>
</div>
```

時計の背景色などをお好みで整える。

```css:index.css
.clock-wrap {
  margin: auto;
  width: 350px;
  height: 350px;
  background-color: #f2f2f2;
}
.clock {
  display: block;
  margin: auto;
}
.timearea {
  display: inline-block;
  width: 100%;
  text-align: center;
}
```

#JavaScript
```JS:script.js
(function (d) {

    //canvas要素を取得
    const el = d.querySelector('.clock');

    //コンテキストを取得
    const ctx = el.getContext('2d');

    //時計描画と現在時刻表示の関数を、1000ミリ秒ごとに実行する
    setInterval(() => {
      activateClock(ctx);
      showCurTime('.timearea');
    }, 1000);

    //現在時刻を取得
    function _getCurTime() {
      const cur = new Date();
      const time = {
        hour : cur.getHours() % 12, //12時間制の数字
        hourOriginal : cur.getHours(), //24時間制の数字
        min :  cur.getMinutes(),
        sec : cur.getSeconds()
      };
      return time;
    }

    //現在時刻を表示
    function showCurTime(elm) {
        const insertArea = d.querySelector(elm);
        const h = _getCurTime().hourOriginal;
        const m = _getCurTime().min;
        const s = _getCurTime().sec;
        const msg = `${h}：${m}：${s}`;

        insertArea.innerHTML = msg;
    }

    //時計を描画
    function activateClock(ctx, time) {

      //背景の円を描画
      ctx.beginPath();
      ctx.arc(150, 150, 115, 0, 2 * Math.PI); //円のパスを設定　・・・補足１
      ctx.fill(); //円のパスを塗りつぶす

      //目盛を描画　・・・補足２
      for (let i = 0; i < 60; i++) {
          let r = 6 * Math.PI / 180 * i; 
          const w = i % 5 === 0 ? 4 : 1; 
          _setCtxStyle(ctx, 'black', 'white', w); 
          _drawCtx(ctx, r, 100, 4); 
      }

      //現在時刻を定数に代入
      const h = _getCurTime().hour;
      const min = _getCurTime().min;
      const sec = _getCurTime().sec;

      //短針を描画　・・・補足３
      const hourR = h * 30 * Math.PI / 180 + min * 0.5 * Math.PI / 180;
      _setCtxStyle(ctx, '', 'pink', 3);
      _drawCtx(ctx, hourR, 0, -60);

      //長針を描画　・・・補足３
      const minR = min * 6 * Math.PI / 180;
      _setCtxStyle(ctx, '', 'yellow', 3);
      _drawCtx(ctx, minR, 0, -90);

      //秒針を描画　・・・補足３
      const secR = sec * 6 * Math.PI / 180;
      _setCtxStyle(ctx, '', 'gray', 1);
      _drawCtx(ctx, secR, 0, -70);

    }

    //コンテキストの描画スタイルを設定する関数
    function _setCtxStyle(ctx, fillColor, strokeColor, lineWidth) {
      ctx.fillStyle = fillColor;
      ctx.strokeStyle = strokeColor;
      ctx.lineWidth = lineWidth;
      ctx.lineCap = 'round';
    }

    //線を描画する関数 ・・・補足４
    function _drawCtx(ctx, rotation, moveToY = 0, length) {
      ctx.save();
      ctx.translate(150, 150);
      ctx.rotate(rotation);
      ctx.beginPath();
      ctx.moveTo(0, moveToY);
      ctx.lineTo(0, moveToY + length);
      ctx.stroke();
      ctx.restore();
    }

})(document);
```

#補足１：円のパスを設定

```JS:script.js
ctx.arc(150, 150, 115, 0, 2 * Math.PI);
```

円のパスは、```.arc()``` メゾットで設定できます。

```
.arc(x, y, radius, startAngle, endAngle, anticlockwise);　　
```

- 座標x,yを中心にして、radiusの値を半径として、startAngle度からendAngle度まで描画する。
- anticlockwiseにtrueを指定すると、反時計回りになる。

startAngle、endAngleの角度は、「ラジアン」という、「360°を2πとして表す単位」に変換する必要があります。

```
ラジアン = 度数 × π ÷ 180
　↓ これをJSで表すと、
ラジアン = 度数 * Math.PI / 180;
```

今回は360°をラジアンに変換したいので、```ラジアン = 360 * Math.PI / 180 → 2 * Math.PI``` となったわけです。
また、```.arc()```メゾットはパスを描くだけのメゾットなので、描画には```.fill()```や```.stroke()```を実行する必要があります。

#補足２：目盛部分
```JS:script.js
//目盛を描画　
for (let i = 0; i < 60; i++) {
  let r = 6 * Math.PI / 180 * i; //①
  const w = i % 5 === 0 ? 4 : 1; //②
  _setCtxStyle(ctx, 'black', 'white', w); //③
  _drawCtx(ctx, r, 100, 4); //④
}
```
目盛描画のパーツとしては、２つあります。
1. 目盛をつける位置の設定と描画　・・・①④
2. 目盛の見た目を設定　・・・②③

##目盛をつける位置の設定と描画
①で、④の関数 ```_drawCtx()``` 内の ```.rotate()``` メゾットで使う回転軸の角度を求めています。
ここでの角度も、「補足１：円のパスを設定」で説明した「ラジアン」で指定します。

関数 ```_drawCtx()``` 内の ```.translate(x, y)``` メゾットで決めた回転軸の位置を変えずに、rラジアンずつ回転させて目盛位置を決めて、 ```.moveTo(x, y)```　```.lineTo(x, y)``` メゾットで目盛線を描画しています。

![New Project (2).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459055/87e7da71-4f7f-1f50-7996-e5a5318256de.jpeg)


##目盛の見た目を設定
②では、時刻を表す目盛を求めて目盛の太さを設定し、③の関数 ```_setCtxStyle()``` 内の ```.lineWidth``` プロパティに渡しています。

```
i % 5 === 0 ? 4 : 1; 
↓
i / 5 の余りが0なら、太さは４、それ以外なら１
```


#補足３：針の描画
```JS:script.js
//短針を描画
const hourR = h * 30 * Math.PI / 180 + min * 0.5 * Math.PI / 180; //・・・①
_setCtxStyle(ctx, '', 'pink', 3); //・・・②
_drawCtx(ctx, hourR, 0, -60);  //・・・③
```

こちらも、仕組みとしては「補足２：目盛部分」と同じです。
1. 目盛をつける位置の設定と描画　・・・①③
2. 目盛の見た目を設定　・・・②

短針の場合は、１時間で進む角度に加えて、１分間で進む角度も足すことで、よくある時計のようにじわじわと次の時刻に短針が近づいていくようにしています。


#補足４：線を描画する関数
```JS:script.js
//線を描画する関数
function _drawCtx(ctx, rotation, moveToY = 0, length) {
  ctx.save(); //・・・①
  
  //②
  ctx.translate(150, 150);
  ctx.rotate(rotation);
  ctx.beginPath();
  ctx.moveTo(0, moveToY);
  ctx.lineTo(0, moveToY + length);
  ctx.stroke();

  ctx.restore(); //・・・③
}
```

線を描画する関数の中身ですが、①③が肝になります。
①の ``.save()`` メゾットで初期設定を保存し、②で描画したのち、③の ``.restore()`` メゾットで①が実行された時の状態に戻しています。

針の描画の呼び出しは１回ずつですが、目盛の描画時はループ処理で複数回関数を呼び出しているので、``.save()`` ``.restore()`` の処理が必要になります。

#参考にしたもの
こちらの本でcanvas基礎から学ばせてもらいました。ありがとうございました。
KindleUnlimitedだと追加料金なしで読めるのでおすすめです。
[ゲームで学ぶJavaScript入門 HTML5&CSSも身につく！](https://www.amazon.co.jp/%E3%82%B2%E3%83%BC%E3%83%A0%E3%81%A7%E5%AD%A6%E3%81%B6JavaScript%E5%85%A5%E9%96%80-HTML5-CSS%E3%82%82%E8%BA%AB%E3%81%AB%E3%81%A4%E3%81%8F%EF%BC%81-%E7%94%B0%E4%B8%AD-%E8%B3%A2%E4%B8%80%E9%83%8E-ebook/dp/B019WTE1FE)