Javascriptでアナログ時計

以前こういう課題を触る機会があったのだが、やってみると意外と面白かったので備忘録代わりに残す。
# HTMLとCSS
シンプル。position:relativeな文字盤の上にposition:absoluteな針を置いているだけ。初期状態で90度回った状態になっているので計算時にずらす。

```html:clock.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>clock</title>
</head>

<body>
  <div id="contents">
    <div id="clock">
      <div id="second-hand" class="hand"></div>
      <div id="minute-hand" class="hand"></div>
      <div id="hour-hand" class="hand"></div>
    </div>
  </div>
</body>

<script>
  // 省略
</script>

<style>
  body {
    margin: 0;
    padding: 0;
  }

  #contents {
    width: 100%;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  #clock {
    position: relative;
    width: 500px;
    height: 500px;
    background-color: lightgray;
    border: solid black 1px;
    border-radius: 50%;
  }

  .hand {
    position: absolute;
    top: 50%;
    left: 50%;
    border-style: solid;
    border-color: black;
    transform-origin: 0 center;
  }

  #second-hand {
    width: 45%;
    border-width: 1px;
  }

  #minute-hand {
    width: 40%;
    border-width: 2px;
  }

  #hour-hand {
    width: 30%;
    border-width: 4px;
  }
</style>

</html>
```

# Javascript
算数の問題みたいになっているので簡単に説明

```javascript:clock.html

  // 引数で指定した要素のtransformプロパティにdegを設定する関数
  function setDeg(el, deg) {
    el.setAttribute("style", `transform:rotate(${deg}deg);`)
  }
  // 引数で指定した時刻を元に時計の針を動かす関数
  function updateClock(d) {
    // 時計の針の要素を取得
    const secondHand = document.getElementById("second-hand");
    const minuteHand = document.getElementById("minute-hand");
    const hourHand = document.getElementById("hour-hand");
    // 時計の針が描画直後で90度回っているので戻す。
    const initDegOffset = 90 * (-1);
    // 時針をタイムゾーン分調整する
    const timeOffset = d.getTimezoneOffset() * 60 * 1000 * (-1);
    // ミリ秒を取得
    const timestamp = d.getTime();
    // 秒針は60秒で一周する
    const secUnit = 60 * 1000;
    //　60秒で割った時の余りを使って秒針の角度を求める
    const secDeg = (timestamp % secUnit / secUnit * 360) + initDegOffset;
    // 分針は60分で一周する
    const minUnit = 60 * secUnit;
    // 60分で割った時の余りを使って分針の角度を求める
    const minDeg = (timestamp % minUnit / minUnit * 360) + initDegOffset;
    // 時針は12時間で一周する
    const hourUnit = 12 * minUnit;
    // 12時間で割った時の余りを使って時針の角度を求める
    // 時差を考慮する
    const hourDeg = ((timestamp + timeOffset) % hourUnit / hourUnit * 360) + initDegOffset;
    // 針の角度を設定
    setDeg(secondHand, secDeg);
    setDeg(minuteHand, minDeg);
    setDeg(hourHand, hourDeg);
  }

  // ページの読み込みが完了したら実行される関数
  function main() {
    setInterval(() => {
      // 現在時刻を取得
      const d = new Date();
      // 時計の針を動かす
      updateClock(d);
    }, 50);
  }

  // ページの読み込みが完了したらmain関数を実行
  window.addEventListener("load", main)
```

## 現在時刻の取得
まず現在時刻をタイムスタンプに変換する。タイムスタンプは「協定世界時 (UTC) の 1970 年 1 月 1 日深夜 0 時」からの経過ミリ秒数となっている。

```javascript
    // ミリ秒を取得
    const timestamp = d.getTime();
```
## 各針の角度計算
タイムスタンプが0ミリ秒の時は全ての針の角度が0度となっている。これを基準にして各針の角度を求める。算数の問題みたく比を使って表すとこうなる。要するに針の角度の割合と針の示す時間の割合が同じになる事を利用している。

```math
針の角度:360= (現在のタイムスタンプ)\% (1周にかかる時間) :(1周にかかる時間)
```

これを掛け算にすると

```math
針の角度= (現在のタイムスタンプ\% 1周にかかる時間)/(1周にかかる時間)*360
```

となるので、例えば秒針だと

```javascript
    // ミリ秒を取得
    const timestamp = d.getTime();
    // 秒針は60秒で一周する
    const secUnit = 60 * 1000;
    //　60秒で割った時の余りを使って秒針の角度を求める
    const secDeg = (timestamp % secUnit / secUnit * 360);
```
となる。あとは1周にかかる時間を変えていけば残りの針の角度も求めることができる。

## 時差分ずらす
時差に対応させたいのでgetTimezoneOffset()メソッドを使う。
ちなみに秒針と分針を時差分動かしても6000ミリ秒で割り切れる値なので角度は変わらない。影響があるのは時針のみ。

```javascript
    // 時針をタイムゾーン分調整する
    const timeOffset = d.getTimezoneOffset() * 60 * 1000 * (-1);
```
時針の角度の算出はこんな感じになる。
```javascript
    // 時計の針が描画直後で90度回っているので戻す。
    const initDegOffset = 90 * (-1);
    // 時針をタイムゾーン分調整する
    const timeOffset = d.getTimezoneOffset() * 60 * 1000 * (-1);
    // ミリ秒を取得
    const timestamp = d.getTime();
    // 秒針は60秒で一周する
    const secUnit = 60 * 1000;
    // 分針は60分で一周する
    const minUnit = 60 * secUnit;
    // 時針は12時間で一周する
    const hourUnit = 12 * minUnit;
    // 12時間で割った時の余りを使って時針の角度を求める
    // 時差を考慮する
    const hourDeg = ((timestamp + timeOffset) % hourUnit / hourUnit * 360) + initDegOffset;
```

## setIntervalで一定時間ごとに実行
あとはこれを一体時間ごとに実行するだけ
```javascript
    setInterval(() => {
      // 現在時刻を取得
      const d = new Date();
      // 時計の針を動かす
      updateClock(d);
    }, 50);
```

# 結果
こうなっていれば正解
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/425022/05d6143b-ad5b-eeb0-b55a-1519f2b8e755.png)

# おまけ:針の角度の更新をCSS変数で行う。
コメントで頂いたcss custom propertyでのスタイル変更をやってみる。
## CSS
:rootで針の角度を指定する変数を作成、各針で読み込む。
```css
 :root {
    --sec-deg: 0deg;
    --min-deg: 0deg;
    --hour-deg: 0deg;
  }

  #second-hand {
    width: 45%;
    border-width: 1px;
    transform: rotate(var(--sec-deg));
  }

  #minute-hand {
    width: 40%;
    border-width: 2px;
    transform: rotate(var(--min-deg));
  }

  #hour-hand {
    width: 30%;
    border-width: 4px;
    transform: rotate(var(--hour-deg));
  }
```
## Javascript
setDeg関数を修正する。要素ではなく変数名を指定するよう変更する。
```javascript
  function setDeg(cssVar, deg) {
    const body = document.querySelector("body");
    body.style.setProperty(cssVar, `${deg}deg`);
  }
```

これで動くはず。修正後のコード

```html:clock.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>clock</title>
</head>

<body>
  <div id="contents">
    <div id="clock">
      <div id="second-hand" class="hand"></div>
      <div id="minute-hand" class="hand"></div>
      <div id="hour-hand" class="hand"></div>
    </div>
  </div>
</body>

<script>

  // 引数で指定したCSS変数にdegを設定する関数
  function setDeg(cssVar, deg) {
    const body = document.querySelector("body");
    body.style.setProperty(cssVar, `${deg}deg`);
  }
  // 引数で指定した時刻を元に時計の針を動かす関数
  function updateClock(d) {
    // 時計の針の要素を取得
    const secondHand = document.getElementById("second-hand");
    const minuteHand = document.getElementById("minute-hand");
    const hourHand = document.getElementById("hour-hand");
    // 時計の針が描画直後で90度回っているので戻す。
    const initDegOffset = 90 * (-1);
    // 時針をタイムゾーン分調整する
    const timeOffset = d.getTimezoneOffset() * 60 * 1000 * (-1);
    // ミリ秒を取得
    const timestamp = d.getTime();
    // 秒針は60秒で一周する
    const secUnit = 60 * 1000;
    // 60秒で割った時の余りを使って秒針の角度を求める
    const secDeg = (timestamp % secUnit / secUnit * 360) + initDegOffset;
    // 分針は60分で一周する
    const minUnit = 60 * secUnit;
    // 60分で割った時の余りを使って分針の角度を求める
    const minDeg = (timestamp % minUnit / minUnit * 360) + initDegOffset;
    // 時針は12時間で一周する
    const hourUnit = 12 * minUnit;
    // 12時間で割った時の余りを使って時針の角度を求める
    // 時差を考慮する
    const hourDeg = ((timestamp + timeOffset) % hourUnit / hourUnit * 360) + initDegOffset;
    // 針の角度を設定
    setDeg("--sec-deg", secDeg);
    setDeg("--min-deg", minDeg);
    setDeg("--hour-deg", hourDeg);
  }

  // ページの読み込みが完了したら実行される関数
  function main() {
    setInterval(() => {
      // 現在時刻を取得
      const d = new Date();
      // 時計の針を動かす
      updateClock(d);
    }, 50);
  }

  // ページの読み込みが完了したらmain関数を実行
  window.addEventListener("load", main)

</script>

<style>
  :root {
    --sec-deg: 0deg;
    --min-deg: 0deg;
    --hour-deg: 0deg;
  }

  body {
    margin: 0;
    padding: 0;
  }

  #contents {
    width: 100%;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  #clock {
    position: relative;
    width: 500px;
    height: 500px;
    background-color: lightgray;
    border: solid black 1px;
    border-radius: 50%;
  }

  .hand {
    position: absolute;
    top: 50%;
    left: 50%;
    border-style: solid;
    border-color: black;
    transform-origin: 0 center;
  }

  #second-hand {
    width: 45%;
    border-width: 1px;
    transform: rotate(var(--sec-deg));
  }

  #minute-hand {
    width: 40%;
    border-width: 2px;
    transform: rotate(var(--min-deg));
  }

  #hour-hand {
    width: 30%;
    border-width: 4px;
    transform: rotate(var(--hour-deg));
  }
</style>

</html>
```
いちいちstyleを全部いじらなくてもよくなった。