以前にも一度**ストップウォッチを作成**しましたが、ちょっと無駄な部分も多かったので一から作り直してみました。
機能的にはスタート/ストップ/再開/ラップタイムと前回とほとんど変わりませんが、動作状態やストップ時間保持用に独立したパラメータを使わずに済むよう計測方法を少し変更しています。
localStorageに動作状態を保存してリロードでの再開に対応しているのも前回と同様ですが、今回はラップタイム履歴も保持するようにしてみました。
保存したストレージデータは、計測リセット時に削除しています。
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
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
'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です。
スタート時にはstartTimeにDate.now()を代入します。
表示する際はDate.now() - startTimeとすることで、スタートからの経過時間が得られます。
ストップ時にはstartTimeからDate.now()を引きます。
結果的にスタートからの経過時間が負数でstartTimeに入りますので、0 - startTimeとすると経過時間が得られます。
再開時にはstartTimeにDate.now()を加算します。
既にstartTimeには止めた瞬間の経過時間が負数で入っているので、その差がDate.now()に対して反映された値がstartTimeに入ることになります。
こうすることで、startTimeが0以下であれば停止中、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>