ストップウォッチ作ってみた(HTML,CSS,JavaScript)

# ストップウォッチ作ってみた

HTML,CSS(SCSS),JavaScript(not jQuery)でストップウォッチ作ってみました。ストップウォッチ作成は初心者が通り道らしいので私も通ってみました。どこか変なところやアドバイスがあればコメントお願いしますー！

ちなみにSCSSは特に解説してません。

## 完成形

こんな感じのができました。0.5 倍で見ると見やすいです。CodePen の埋め込み機能を使ってみましたがデフォルトで 0.5 倍にできたらいいんですけどやり方わかりませんでした(笑)

<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="mkt-engr" data-slug-hash="zYqGmJz" 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="zYqGmJz">
  <span>See the Pen <a href="https://codepen.io/mkt-engr/pen/zYqGmJz">
  zYqGmJz</a> by mkt-engr (<a href="https://codepen.io/mkt-engr">@mkt-engr</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## 特長

- 時間、分、秒、ミリ秒まで表示する(時間までは使われなそうやけど一応実装した)
- START,STOP,RESET の 3 つのボタンがある
- 最初は START ボタンのみ活性化されている
- STOP ボタンを押すと START という文字が RESTART に変わる
- STOP ボタンを押すと STOP ボタンが非活性化し RESTART ボタンと RESET ボタンが活性化される
- RESET ボタンを押すと RESTART ボタンの文字が START に変わる

## 実装方針

`new Date().getTime()`がストップウォッチの主役です。[MDN の getTime のページ](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/getTime)によると

> 1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒で表した数値。

とあるので

```js
console.log(new Date().getTime());
```

とすると 1970 年 1 月 1 日 0 時 0 分からの経過時間をミリ秒で表示してくれます。これを利用して START ボタンや STOP ボタンを押した時刻を取得してストップウォッチを実装します。

ストップウォッチには START(RESTART),STOP,RESET の 3 つのボタンがありますがそれに対応して 3 つのイベントリスナーを実装します。

## 実装の詳細

一番 STOP ボタンの実装に苦労しました。

### HTML と JavaScript のイベントリスナー以外の記述

HTML はシンプルです。時刻を表示する部分(`<div class="display">`)とボタンを表示する部分(`<div class="buttons">`)に分かれているだけです。

```index.html
  <div class="stopwatch_wrapper">
    <div class="display">
      <span id="minutes" class="time">00</span>
      <span id="seconds" class="time">00</span>
      <span id="milli_seconds" class="time">000</span>
    </div>
    <div class="buttons">
      <button class="button" id="start">start</button>
      <button class="button" id="stop" disabled>stop</button>
      <button class="button" id="reset" disabled>reset</button>
    </div>
  </div>
```

ひとまずこれらのボタンや数値を操作するために以下の記述をします。

```script.js
//上から分、秒、ミリ秒
const minutes = document.getElementById('minutes');
const seconds = document.getElementById('seconds');
const milli_seconds = document.getElementById('milli_seconds');

//ボタンたち
const start = document.getElementById('start');
const stop = document.getElementById('stop');
const reset = document.getElementById('reset');
```

3 つのボタンでそれぞれにイベントリスナーがついています。それらのイベントリスナーが共通でアクセスする変数を定義します。1 番下の`past_moving_time`がストップウォッチの実装の肝かなと思ってます。

```script.js
// ストップウォッチを動かすときに用いるsetIntervalの返り値
let timer_id;

// ストップウォッチを動かし始めてからの時間
let stopwatch_time = 0;

// STARTボタンを押した時間
let press_start_time = 0;

// STOPボタンを押した時間
let press_stop_time = 0;

//ストップウォッチが動いていた時間の合計（STARTボタンを押してからSTOPボタンを押すまでの時間の合計）
let past_moving_time = 0;
```

## 3 つのイベントリスナー

### START ボタン

START ボタンのイベントリスナーのコードは以下の通りです。3 つ特長があるのでそれはコードの後で書きます。

```script.js
start.addEventListener('click', () => {
  press_start_time = new Date().getTime();
  timer_id = setInterval(() => {
    stopwatch_time = new Date().getTime() - press_start_time + past_moving_time;

    const time_milli_seconds = `00${Math.floor(stopwatch_time % 1000)}`.slice(
      -3
    );
    const time_seconds = `0${Math.floor((stopwatch_time / 1000) % 60)}`.slice(
      -2
    );
    const time_minutes = `0${
      Math.floor(stopwatch_time / 1000 / 60) % 60
    }`.slice(-2);
    const time_hours = `00${Math.floor(stopwatch_time / 1000 / 60 / 60)}`.slice(
      -3
    );

    //ブラウザに時間を描画する
    minutes.innerHTML = time_minutes;
    seconds.innerHTML = time_seconds;
    milli_seconds.innerHTML = time_milli_seconds;
  }, 1);

```

1. START(RESTART)ボタンを押しからの経過時間の取得

   まずボタンを押した時間を以下のようにして取得します。

   ```js
   press_start_time = new Date().getTime();
   ```

   現在の時間(`new Date().getTime()`)から START ボタンを押した時間を引けば現在の時間が得られます。

   ```js
   stopwatch_time = new Date().getTime() - press_start_time + past_moving_time;
   ```

   ストップウォッチを最初にもしくは RESET ボタンを押した後は`past_moving_time`は 0 なので一旦無視してください。これについては STOP ボタンで解説します。

2) slice に関して
   `.slice(-2)`とか`.slice(-3)`とかは`0`をパディングしてます。例えば秒を取得するとき 1 秒なら 01 を 23 秒なら 23 に変換しています。どんな秒数でも`0`を前にパディングしておいて後ろから 2 つ分を slice することでどんな秒数が来ても共通の処理ができます。具体的には以下の通りです。

   - 1→01→01 を取得
   - 23→023→23 を取得

3) ミリ秒、秒、分、時間の取得

`stopwatch_time`はあくまでミリ秒です。こいつから秒、分、時間を取り出します。ここでは`stopwatch_time=123467123`とします。

- ミリ秒

  `stopwatch_time`は 123467.123 秒を表しています。なので下 3 桁を取得するために以下のように 1000 で割った余りを計算します。

  ```js
  const time_milli_seconds = `00${stopwatch_time % 1000}`.slice(-3);
  ```

- 秒

  `stopwatch_time`の 4，5 桁目を取得することを考えます。なのでまずは 1000 で割り`Math.floor`することで`123467`を取得します。ストップウォッチに表示される秒数は 2 桁なので下 2 桁を取得します。60 以上になったら分に繰り上げる必要があるので`123467`を 60 で割ったあまりを以下のようにして取得します。

  ```js
  const time_seconds = `0${Math.floor((stopwatch_time / 1000) % 60)}`.slice(-2);
  ```

  こうすることで 60 秒未満の場合でも 60 秒以上 99 秒以下の場合でも同じ処理で対応できます。例えば `123467 % 60` なら `7` となり`123456 % 60` なら `56`みたいな感じです。

- 分,時間

  上と同様のロジックで分と時間を以下のようにして取得します。

  ```js
  const time_minutes = `0${Math.floor(stopwatch_time / 1000 / 60) % 60}`.slice(
    -2
  );
  const time_hours = `0${Math.floor(stopwatch_time / 1000 / 60 / 60)}`.slice(
    -2
  );
  ```

### STOP ボタン

実装方針ストップウォッチ実装の肝と言っていた`past_moving_time`について解説します。シンプルに STOP ボタンが押されて`clearInterval`をするだけだと RESTART ボタンを押したときに再び 0 秒から始まってしまいます。なぜかというと START(RESTART)ボタンを押すたびにイベントリスナーが走って以下のように`press_start_time`が更新されるからです。

START ボタンのイベントリスナーには以下のような記述がありました。

```script.js
start.addEventListener('click', () => {
  press_start_time = new Date().getTime();
  timer_id = setInterval(() => {
  stopwatch_time = new Date().getTime() - press_start_time + past_moving_time;
```

RESTART を押すとストップウォッチが再び 0 から始まってしまうことを避けるために定義した変数が`past_moving_time`です。

STOP ボタンのイベントリスナーのコードは以下の通りです。

```script.js
stop.addEventListener('click', () => {
  clearInterval(timer_id);
  start.innerHTML = 'restart';

  press_stop_time = new Date().getTime();
  past_moving_time += press_stop_time - press_start_time;

  //STOPボタンを1度押すと非活性され、STARTボタンとRESETボタンは活性化される
  stop.disabled = true;
  start.disabled = false;
  reset.disabled = false;
});
```

### ストップウォッチを動かしている時間の取得

past_moving_time に関してやってることはめっちゃシンプルです。STOP ボタンを押した時間から START ボタンを押した時間を引けばストップウォッチが動いていた時間を以下のように導出できます。

```.js
 past_moving_time += press_stop_time - press_start_time;
```

ここで`+=`をしているのは何度も STOP,RESTART が押されることを想定してのことです。

START ボタンのイベントリスナーの記述を見ると以下のように`past_moving_time`(ストップウォッチが動いていた時間の合計)が`stopwatch_time`に加算されています。

```script.js
start.addEventListener('click', () => {
  press_start_time = new Date().getTime();
  timer_id = setInterval(() => {
    stopwatch_time = new Date().getTime() - press_start_time + past_moving_time;
```

### RESET ボタン

RESET ボタンの実装が一番簡単です。やることは以下の 2 つです。

- ストップウォッチの停止(`clearInterval`)
- 初期化(ブラウザの表示,ストップウォッチの表示をするために用いた変数)

RESET ボタンのイベントリスナーのコードは以下の通りです。

```script.js
reset.addEventListener('click', () => {
  clearInterval(timer_id);

  start.innerHTML = 'start';

  //ブラウザの表示を初期化
  minutes.innerHTML = '00';
  seconds.innerHTML = '00';
  milli_seconds.innerHTML = '000';

  //変数を初期化
  stopwatch_time = 0;
  press_start_time = 0;
  press_stop_time = 0;
  past_moving_time = 0;

  //RESETボタンを押したらSTARTボタンしか押せない状態にする
  start.disabled = false;
  stop.disabled = true;
  reset.disabled = true;
});

```

## CSS ファイル

SCSS を使っていない方もいると思うのでコンパイルした CSS をここに書いておきます。ちなみに VS Code のプラグインを使うと Webpack とかの準備をすることなく簡単に SCSS が使えるのでおすすめです。[ここ](https://qiita.com/SIKE/items/14356a133fe25eeaa699)にプラグインに関して分かりやすく書いてありました。

<details><summary>style.css</summary><div>

```style.css
body {
  margin: 0;
  padding: 0;
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
}

html {
  font-size: 20px;
}

.buttons {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
      -ms-flex-pack: center;
          justify-content: center;
  margin-bottom: 1rem;
  margin-top: 1rem;
}

.buttons .button {
  text-transform: uppercase;
  padding: 1rem 2rem;
  margin-right: 1rem;
  color: white;
  border: none;
  cursor: pointer;
  -webkit-transition: all 0.1s ease-out;
  transition: all 0.1s ease-out;
  background: #4676d7;
  border-radius: 5px;
  font-size: 1.5rem;
  border: 2px solid transparent;
  -webkit-box-shadow: 0 0 8px gray;
          box-shadow: 0 0 8px gray;
  min-width: 225px;
}

.buttons .button:hover {
  background-color: transparent;
  color: #252020;
  border-color: #4676d7;
}

.buttons .button:disabled {
  background-color: #ccc;
}

.buttons .button:disabled:hover {
  color: white;
  border-color: transparent;
  cursor: default;
}

.display {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
      -ms-flex-pack: center;
          justify-content: center;
  padding: 2rem;
  background-color: #d3b3ab;
}

.display .time {
  font-size: 5rem;
}

.display .time:nth-child(1)::after {
  content: ':';
}

.display .time:nth-child(2)::after {
  content: ':';
}

.display .time:nth-child(3)::after {
  content: '.';
}
/*# sourceMappingURL=style.css.map */
```

</div></details>

## 展望

- START ボタンを STOP ボタンは 1 つにするべきかな？
- React や Vue でも作りたい