2024/07/12

ラップタイムをグラフに表示するストップウォッチWebアプリ

ストップウォッチアプリを作ってみよう!

最近、自分でWebアプリを作ってみたいと思ったことはありませんか?今回は、JavaScriptを使ってシンプルなストップウォッチアプリを作成してみます。

…Webアプリはもちろん、ブログ記事もChatGPTさんに任せようと「フランクに!」と付け加えたらちょっと軽すぎた感が…w


必要なもの

このプロジェクトでは、基本的なHTML、CSS、そしてJavaScriptが必要です。さらに、チャートを描画するためにChart.jsというライブラリも使用します。


グラフの表示

Chart.jsを使用して、計測したラップタイムをグラフで視覚化します。各ラップのタイムがグラフに反映され、平均ラップタイムも表示されます。




使い方

アプリを開始するには「スタート」ボタンをクリックし、必要に応じて「ラップ」ボタンでラップタイムを記録します。計測を終了するには「ストップ」ボタンを押し、「リセット」ボタンで全てを初期化できます。


iPhoneの場合、アドレスバー横のメニュー「ぁあ」から「ツールバーを非表示」を選ぶか、共有から「ホーム画面へ追加」を選ぶと、アプリっぽくスッキリします。


結び

結びというか、自力で書いた「本文」なのですが、元々もう10年以上前に勉強を教えていた時欲しいと思ったアプリです。

例えば計算問題をタイムアタックで解いた時に、時間が視覚化できるといいなと思い、当時探したのですが結局見つからず。

かといって自分で作ることもできず、試すこともできなかった、ある意味夢だった物です。


僕自身はこれから使うことはあるか分かりませんが、ずっと欲しかった物を手に入れられないまま悔いを残すのも嫌なので作り、またきっとどこかに昔の僕みたいに必要としている人はいるかもしれないので公開します。

もういい歳にもなって、このサイトも今年2024年の5月でもう20年にもなりますが、今になって分かった自分のことがひとつ…。

ああ、僕は何かが出来るようになる事が一番楽しいんだなと気づきました。

だから誰かが作った既製品の多くにはそれほど魅力を感じず、かといって出来るようになると飽きちゃうので、みんな中途半端…w

今回のようにAIの力を借りてでも、物が出来上がるのって楽しくてしょうがない。

いろんな面もあるけれど、ほんとに便利な時代になったなあ。


あとChatGPTさんに使用例もいくつか考えてと言ったのですが、ひとつも挙げてくれませんでしたw

普通にラップタイムを視覚化する他に、1秒を勘で計る遊びもできますw

最後にGPTさんの出力したまとめで締め(ちょっとのブログの主旨が違うような…)。


これで、あなたも自分だけのストップウォッチアプリを作成する準備が整いました!ぜひ、自分のアイデアや改良点を加えて、楽しんでください。


こちらからWebアプリを表示:ストップウォッチ


コードの構成

以下、HTML等のコードです。

ChatGPTさんに修正、変更を加えつつザッと作ったコードですので、動きはしますが余計なコードも入ったままな気もします…。

        
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>ストップウォッチ</title>
<link rel="stylesheet" href="stopwatch.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="stopwatch">
<div id="display" class="display">00:00.0</div>
<div class="buttons">
<div class="button-row">
<button id="startStopBtn" class="button">スタート</button>
<button id="resetBtn" class="button">リセット</button>
<button id="lapBtn" class="button">ラップ</button>
</div>

</div>
<div class="canvas-container">
<canvas id="lapChart"></canvas>
</div>
<div id="average" class="average">平均: 00:00.0</div>
</div>
<script src="stopwatch.js"></script>
</body>
</html>
CSSはこちらです
body {
font-family: Arial, sans-serif;
background-color: #282c34;
color: #eee;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;

}

.stopwatch {
text-align: center;
background-color: #3a3f47;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
margin: 0 auto;
}

.display {
font-size: 3rem;
margin-top: 20px;
margin-bottom: 20px;
}

.buttons {
display: flex;
flex-direction: column;
gap: 10px;
}

.button-row {
display: flex;
gap: 10px;
margin-bottom: 20px;
}

.button {
background-color: #0a6eda;
color: #ffffff;
border: none;
padding: 12px 24px;
margin: 5px;
border-radius: 22px;
font-size: 1.2rem;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s;
height: 6rem;
width: calc(100%/2 - 10px);
}

/* フラッシュアニメーション */
@keyframes flash {
0% {
background-color: #f0f0f0;
}
50% {
background-color: #d3d3d3;
}
100% {
background-color: #f0f0f0;
}
}

.button.flash {
animation: flash 0.3s;
}
#startStopBtn {
background-color: #0163cb;
}

#lapBtn {
background-color: #064689;
}


.canvas-container {
position: relative;
height: 450px; /* グラフが描画された後の高さ */
}

.average {
font-size: 1rem;
margin-top: 10px;
}
次に、JavaScriptでストップウォッチの機能を実装します。スタートボタンを押すと時間が計測され、ラップボタンでラップタイムが記録されます。
        
let startTime;
let elapsedTime = 0;
let timerInterval;
let laps = [];
let lapChart;
let lastLapTime = 0;
let lastButtonPressTime = 0;

function timeToString(time) {
let diffInHrs = time / 3600000;
let hh = Math.floor(diffInHrs);

let diffInMin = (diffInHrs - hh) * 60;
let mm = Math.floor(diffInMin);

let diffInSec = (diffInMin - mm) * 60;
let ss = Math.floor(diffInSec);

let diffInMs = (diffInSec - ss) * 1000;
let ms = Math.floor(diffInMs / 100);

let formattedMM = mm.toString().padStart(2, "0");
let formattedSS = ss.toString().padStart(2, "0");
let formattedMS = ms.toString();

return `${formattedMM}:${formattedSS}.${formattedMS}`;
}

function print(txt) {
document.getElementById("display").innerHTML = txt;
}

function flashButton(button) {
button.classList.add('flash');
setTimeout(() => {
button.classList.remove('flash');
}, 50); // フラッシュアニメーションの時間
}

function start() {
if (Date.now() - lastButtonPressTime < 50) return; // ボタン連打防止
lastButtonPressTime = Date.now();
startTime = Date.now() - elapsedTime;
timerInterval = setInterval(() => {
elapsedTime = Date.now() - startTime;
print(timeToString(elapsedTime));
}, 100);
showButtons("RUNNING");
}

function stop() {
if (Date.now() - lastButtonPressTime < 50) return; // ボタン連打防止
lastButtonPressTime = Date.now();
const lapTime = elapsedTime - lastLapTime;
lastLapTime = elapsedTime;
laps.push(lapTime);
updateChart();
updateAverage();
clearInterval(timerInterval);
showButtons("STOPPED");
}

function reset() {
if (Date.now() - lastButtonPressTime < 50) return; // ボタン連打防止
lastButtonPressTime = Date.now();
clearInterval(timerInterval);
print("00:00.0");
elapsedTime = 0;
laps = [];
lastLapTime = 0;
updateChart();
document.getElementById("average").innerText = "平均: 00:00.0";
showButtons("RESET");
}

function lap() {
if (Date.now() - lastButtonPressTime < 100) return; // ボタン連打防止
lastButtonPressTime = Date.now();
const lapTime = elapsedTime - lastLapTime;
lastLapTime = elapsedTime;
laps.push(lapTime);
updateChart();
updateAverage();
flashButton(document.getElementById("startStopBtn"));
}

function showButtons(state) {
const startStopButton = document.getElementById("startStopBtn");
const resetButton = document.getElementById("resetBtn");
const lapButton = document.getElementById("lapBtn");

if (state === "RUNNING") {
startStopButton.innerHTML = "ラップ";
startStopButton.removeEventListener("click", start);
startStopButton.addEventListener("click", lap);

lapButton.style.display = "inline-block";
lapButton.innerHTML = "ストップ";
lapButton.removeEventListener("click", lap);
lapButton.addEventListener("click", stop);

resetButton.style.display = "none";
} else {
startStopButton.innerHTML = "スタート";
startStopButton.removeEventListener("click", lap);
startStopButton.addEventListener("click", start);

lapButton.style.display = "none";
resetButton.style.display = "inline-block";
}
}

document.getElementById("startStopBtn").addEventListener("click", start);
document.getElementById("lapBtn").addEventListener("click", lap);
document.getElementById("resetBtn").addEventListener("click", reset);

// 初期状態
showButtons("STOPPED");

function updateChart() {
const ctx = document.getElementById('lapChart').getContext('2d');
ctx.canvas.height = 400; // グラフの高さを設定

const labels = laps.map((lap, index) => `${index + 1}: ${timeToString(lap)}`);
const data = laps.map(lap => lap / 1000);
if (lapChart) {
lapChart.destroy(); // 古いグラフを破棄
}
lapChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Lap Time (s)',
data: data,
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return `Lap ${context.label}: ${context.parsed.y.toFixed(1)}s`;
}
}
}
}
}
});
}

function updateAverage() {
if (laps.length === 0) {
document.getElementById("average").innerText = "平均: 00:00.0";
return;
}

const total = laps.reduce((acc, lap) => acc + lap, 0);
const average = total / laps.length;
document.getElementById("average").innerText = `平均: ${timeToString(average)}`;
}