JavaScript ストップウォッチを作成

JavaScriptで簡単なストップウォッチを作ってみました。

![01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618708/4c0f8659-21e7-bbaa-9bb0-4110217c10a6.jpeg)
![02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618708/31c68694-9cea-fff2-46f7-498537f649f7.jpeg)

機能的には基本のスタート/ストップのほか、ラップタイム/スプリットタイムも実装しています。

ボタンを押すごとに状態をlocalStorageへ保存することで、計測中にブラウザをリロードしたり閉じたりしてしまっても再表示すれば計測状態を復帰できるようにしてあります。
長時間計測するような場合、計測中はブラウザを閉じておく、というような使い方にも対応できます。
(ラップタイム/スプリットタイムの履歴は保存しません。保存したパラメータは計測停止時に削除されます。)

[設置デモ](https://akebi.jp/temp/stopwatch/)

~~~html:index.html
<!doctype html>
<html lang='ja'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='user-scalable=no,width=device-width,initial-scale=1'>
<title>stopwatch</title>
<style>
    html {
        touch-action: manipulation;
        -webkit-touch-callout: none;
    }
    div.button {
        box-sizing: border-box;
        position: relative;
        width: 100%;
        text-align: center;
    }
    input[type='button'] {
        width: 48%;
        height: 50px;
        max-width: 180px;
        font-size: 25px;
        border-radius: 8px;
        background-color: #fff;
        -webkit-appearance: none;
        -webkit-user-select: none;
        cursor: pointer;
    }
    div#disp {
        position: relative;
        text-align: center;
        font-size: 5.5vw;
        margin-bottom: 20px;
        -webkit-user-select: none;
    }
    div#lap {
        position: relative;
        text-align: center;
        font-size: 18px;
        height: 300px;
        overflow-y: scroll;
        width: 100%;
        min-height: 200px;
        margin-top: 60px;
        margin-bottom: 10px;
        left: 0px;
    }
    div#mask {
        position: relative;
        width: 100%;
        overflow: hidden;
        margin: auto;
    }
    div#lap>div {
        font-size: 4vw;
    }
    #container {
        width: 100%;
        text-align: center;
    }
</style>
</head>
<body>
    <div id='container'>
        <div id='mask'>
            <div id='lap'></div>
        </div>
        <div id='disp'>0:00:00.000 / 0:00:00.000</div>
        <div class='button'>
        <input type='button' id='start' value='START'>
        <input type='button' id='reset' value='RESET'>
        </div>
    </div>
    <script src='./stopwatch.js'></script>
</body>
</html>
~~~

~~~js:stopwatch.js
"use strict";

const lapCount  = 200; // ラップ保持数
let lapNum = 1;

const storage = getStorage();

let state       = storage.state       ?? 0, // 動作状態
    startTime   = storage.startTime   ?? 0, // スタートタイム
    stopTime    = storage.stopTime    ?? 0, // ストップタイム
    lapTime     = storage.lapTime     ?? 0, // ラップスタートタイム
    lapStopTime = storage.lapStopTime ?? 0, // ラップストップタイム
    id; // setInterval ID

onload = function() {
    // localStorageに動作状態が保存されていた場合は動作状態復元
    if(state === 1) {
        if(id = setInterval(printTime, 1)) {
            document.querySelector('#start').value = 'STOP';
            document.querySelector('#reset').value = 'LAP';
        }
        setStorage();
    }
}

const eventHandlerType =
    window.ontouchstart !== undefined ? 'touchstart' : 'mousedown';

// START押下時イベント
document.querySelector('#start').addEventListener(eventHandlerType, function() {
    // 停止中
    if(state === 0) {
        // カウント開始
        if(id = startCount()) {
            // ボタンのラベル変更
            document.querySelector('#start').value = 'STOP';
            document.querySelector('#reset').value = 'LAP';
            // 動作状態を変更
            state = 1;
            setStorage();
        }
    }
    // 動作中
    else {
        // カウントインターバルが動作中
        if(id) {
            // インターバル停止
            clearInterval(id);
            // カウント停止
            stopCount();
            // ボタンのラベルを戻す
            document.querySelector('#start').value = 'START';
            document.querySelector('#reset').value = 'RESET';
            // 動作状態を変更
            state = 0;
            deleteStorage();
        }
    }
}, false);

// RESET押下時イベント
document.querySelector('#reset').addEventListener(eventHandlerType, function() {
    // 停止中ならリセット
    if(state === 0) {
        stopTime    = 0;
        lapStopTime = 0;
        // 表示初期化
        document.querySelector('#lap').innerHTML = '';
        document.querySelector('#disp').textContent = '0:00:00.000 / 0:00:00.000';
    }
    // 動作中ならLAP動作
    else {
        // #lap内の最後にdiv要素追加
        document.querySelector('#lap').appendChild(document.createElement("div"));

        // 追加した要素に経過時間表示
        document.querySelector('#lap>div:last-of-type').textContent = (lapNum++) + ' : ' + getTimeString();

        lapTime = Date.now();

        // lap保持数を超えたら先頭の子要素を削除
        if(document.querySelector('#lap').childElementCount > lapCount)
            document.querySelector('#lap').removeChild(document.querySelector('#lap').childNodes[0]);

        // スクロール位置を最下部に
        document.querySelector('#lap').scrollTop = document.querySelector('#lap').scrollHeight;

        setStorage();
    }
}, false);

// カウント開始
function startCount() {
    const now = Date.now();
    startTime = now - stopTime;
    lapTime   = now - lapStopTime;
    return setInterval(printTime, 1);
}

// カウント停止
function stopCount() {
    const now   = Date.now()
    stopTime    = now - startTime;
    lapStopTime = now - lapTime;
}

// 経過時間表示
function printTime() {
    document.querySelector('#disp').textContent = getTimeString();
}

function getTimeString() {
    const
        now       = Date.now(),
        time      = now - startTime,
        splitTime = now - lapTime,

        main =
            Math.floor(time / 3600000) + ':' +
            String(Math.floor(time / 60000) % 60).padStart(2, '0') + ':' +
            String(Math.floor(time / 1000) % 60).padStart(2, '0') + '.' +
            String(time % 1000).padStart(3, '0'),

        split =
            Math.floor(splitTime / 3600000) + ':' +
            String(Math.floor(splitTime / 60000) % 60).padStart(2, '0') + ':' +
            String(Math.floor(splitTime / 1000) % 60).padStart(2, '0') + '.' +
            String(splitTime % 1000).padStart(3, '0');

    return main + ' / ' + split;
}

// localStorage保存
function setStorage() {
    localStorage.setItem('stopwatch_params', JSON.stringify({
        state: state,
        startTime: startTime,
        stopTime: stopTime,
        lapTime: lapTime,
        lapStopTime: lapStopTime,
    }));
}

// localStorage削除
function deleteStorage() {
    localStorage.removeItem('stopwatch_params');
}

// localStorage取得
function getStorage() {
    const params = localStorage.getItem('stopwatch_params');
    return params ? JSON.parse(params) : {};
}
~~~

**追記**
機能的には同等で処理を簡略化した[**改訂版**](https://qiita.com/akebi_mh/items/f5f7dbe4a745c8c2fce5)及び計測ユニットを任意に増減可能な[**複数計測対応版**](https://qiita.com/akebi_mh/items/4279b038fe3436136aea)を作成しました。