0
3

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 3 years have passed since last update.

JavaScriptでストップウォッチを作成(改訂版)

Last updated at Posted at 2021-03-04

以前にも一度**ストップウォッチを作成**しましたが、ちょっと無駄な部分も多かったので一から作り直してみました。

機能的にはスタート/ストップ/再開/ラップタイムと前回とほとんど変わりませんが、動作状態やストップ時間保持用に独立したパラメータを使わずに済むよう計測方法を少し変更しています。

localStorageに動作状態を保存してリロードでの再開に対応しているのも前回と同様ですが、今回はラップタイム履歴も保持するようにしてみました。
保存したストレージデータは、計測リセット時に削除しています。

動作デモ
test.jpg

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>
    <link rel='stylesheet' href='./style.css'>
    <script src='./script.js'></script>
</head>
<body>
    <div id='main'>
        <div id='display'></div>
        <button id='start'>START</button>
        <button id='reset'>RESET</button>
    </div>
    <div id='lap'></div>
</body>
</html>

style

style.css
html {
    touch-action: manipulation;
    -webkit-touch-callout: none;
}
#main {
    position: sticky;
    top: 0px;
    background-color: #fff;
    padding-bottom: 12px;
}
#display {
    font-size: 22px;
    padding-bottom: 10px;
}
button {
    width: 48%;
    height: 50px;
    max-width: 120px;
    font-size: 20px;
    border-radius: 8px;
    border: 2px #888 solid;
    background-color: #fff;
    -webkit-appearance: none;
    -webkit-user-select: none;
    cursor: pointer;
}
#lap {
    font-size: 15px;
    margin-top: 20px;
}

script

script.js
'use strict';
const
    storageName = 'stopWatch_status',
    elem        = {};

let
    startTime  = 0,
    lapTime    = 0,
    lapCount   = 0,
    lapRecords = [];

window.addEventListener('DOMContentLoaded', function() {
    ['start', 'reset', 'display', 'lap'].forEach(function(id) {
        elem[id] = document.getElementById(id)
    });

    // ボタンへのイベントリスナー追加
    const e = window.ontouchstart !== undefined ? 'touchstart' : 'mousedown';
    elem.start.addEventListener(e, clickStart);
    elem.reset.addEventListener(e, clickReset);

    timePrint(0, 0);
    const storage = getStorage();
    // localStorageにデータがあれば状態復元
    if(Object.keys(storage).length > 0) {
        startTime  = storage.startTime;
        lapTime    = storage.lapTime;
        lapRecords = storage.lapRecords;
        // 動作中
        if(startTime > 0) {
            elem.start.textContent = 'STOP';
            elem.reset.textContent = 'LAP';
            countUp();
        }
        // 一時停止中
        else if(startTime < 0) {
            timePrint(-startTime, -lapTime);
        }
        // ラップタイムレコード復元
        if(lapRecords.length > 0) {
            let str = '';
            for(let i in lapRecords) {
                str = '[' + (++lapCount) + '] ' + lapRecords[i] + '<br>' + str;
            }
            elem.lap.innerHTML = str;
        }
    }
});

// START/STOPボタン押下
function clickStart() {
    const now = Date.now();
    // 停止時
    if(startTime <= 0) {
        // 計測開始
        startTime += now;
        lapTime   += now;
        countUp();
        elem.start.textContent = 'STOP';
        elem.reset.textContent = 'LAP';
    }
    // 動作時
    else {
        // 一時停止
        startTime -= now;
        lapTime   -= now;
        timePrint(-startTime, -lapTime);
        elem.start.textContent = 'START';
        elem.reset.textContent = 'RESET';
    }
    setStorage();
}

// RESET/LAPボタン押下
function clickReset() {
    // 計測中
    if(startTime > 0) {
        // LAP
        const now = Date.now();
        timePrint(now - startTime, now - lapTime);
        lapTimePrint();
        lapTime = now;
        setStorage();
        window.scrollTo(0, 0);
    }
    // 停止中
    else {
        // リセット
        startTime = lapTime = 0;
        timePrint(0, 0);
        elem.lap.textContent = '';
        lapCount = 0;
        lapRecords = [];
        clearStorage();
    }
}

// 計測
function countUp() {
    if(startTime > 0) {
        const now = Date.now();
        timePrint(now - startTime, now - lapTime);
        requestAnimationFrame(countUp);
    }
}

// タイム表示
function timePrint(t, l) {
    elem.display.textContent = timeFormat(t) + ' / ' + timeFormat(l);
}

// 時間表示フォーマット
function timeFormat(t) {
    return Math.floor(t / 36e5) + new Date(t).toISOString().slice(13, 23);
}

// ラップタイムレコード追加
function lapTimePrint() {
    const str = display.textContent;
    lapRecords.push(str);
    elem.lap.innerHTML = '[' + (++lapCount) + '] ' + str + '<br>' + lap.innerHTML;
}

// localStorageデータ保存
function setStorage() {
    localStorage.setItem(storageName, JSON.stringify({
        startTime : startTime,
        lapTime   : lapTime,
        lapRecords: lapRecords,
    }));
}

// localStorageデータ削除
function clearStorage() {
    localStorage.removeItem(storageName);
}

// localStorageデータ取得
function getStorage() {
    const params = localStorage.getItem(storageName);
    return params ? JSON.parse(params) : {};
}

####基本的な計測動作の流れ####

Date.now()と、操作時の値を保持する変数startTimeのみで計測しています。

初期状態ではstartTimeの値は0です。
スタート時にはstartTimeDate.now()を代入します。
表示する際はDate.now() - startTime
とすることで、スタートからの経過時間が得られます。

ストップ時にはstartTimeからDate.now()を引きます。
結果的にスタートからの経過時間が負数でstartTime
に入りますので、0 - startTimeとすると経過時間が得られます。

再開時にはstartTimeDate.now()を加算します。
既にstartTime
には止めた瞬間の経過時間が負数で入っているので、その差がDate.now()に対して反映された値がstartTimeに入ることになります。

こうすることで、startTime0以下であれば停止中、1以上であれば動作中と判断できるため、動作状態を保持する変数を別に用意する必要もなくなります。


スタート、ストップ、再開、リセットのみでラップタイムや状態保持のない簡易バージョン。
動作デモ

<!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 lite</title>
</head>
<body>
<input type='text' id='view' readonly><br>
<button id='start'>START</button>
<button id='reset'>RESET</button>
<script>
'use strict';
const
    view     = document.getElementById('view'),
    startBtn = document.getElementById('start'),
    resetBtn = document.getElementById('reset');

let startTime = 0;

window.addEventListener('DOMContentLoaded', function() {
    const e = window.ontouchstart !== undefined ? 'touchstart' : 'mousedown';
    startBtn.addEventListener(e, clickStart);
    resetBtn.addEventListener(e, clickReset);
    view.value = timeFormat(0);
});

// STARTボタン押下時
function clickStart() {
    // 停止状態なら
    if(startTime <= 0) {
        // 計測開始
        startTime += Date.now();
        timePrint();
        startBtn.textContent = 'STOP';
        resetBtn.disabled = true;
    }
    // 計測中なら
    else {
        // 停止
        startTime -= Date.now();
        startBtn.textContent = 'START';
        resetBtn.disabled = false;
    }
}

// RESETボタン押下時
function clickReset() {
    // 停止状態なら
    if(startTime < 0) {
        // リセット
        startTime = 0;
        view.value = timeFormat(0);
    }
}

// 時間表示
function timePrint() {
    if(startTime > 0) {
        view.value = timeFormat(Date.now() - startTime);
        requestAnimationFrame(timePrint);
    }
}

// 表示フォーマット
function timeFormat(t) {
    return Math.floor(t / 36e5) + new Date(t).toISOString().slice(13, 23);
}
</script>
</body>
</html>
0
3
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
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?