ストップウォッチを作ろう
こんにちはkazutoです今回は、ストップウォッチを作成をしていきます。
事前準備
まずは、実装の準備を整えましょう。今回、作成するストップウォッチは、下記です。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="style.css">
<script src="new.js"defer></script>
</head>
<body>
<main>
<div class="block">
<div class="blackBox">
<p id = "text"></p>
<p class ="timer"id="timer">00:00.000</p>
<button class="start" id="start">Start</button>
<button class="reset" id="reset">Reset</button>
</div>
</div>
<dic class ="changeColor" id ="changeColor">
<div class="boderColor"></div>
</div>
</main>
</body>
</html>
body{
background-color: #111111;
}
main{
height: 500px;
width: 800px;
margin: auto;
margin-top: 50px;
}
.block{
width: 500px;
height: 500px;
border-radius: 300px;
background-color: black;
margin: auto;
position: relative;
}
.blackBox{
position: absolute;
font-size: 50px;
text-align: center;
top: 100px;
bottom: 100px;
left: 100px;
right: 100px;
}
.timer{
background-color: black;
border: solid 3px #222222;
border-radius: 10px;
color: #66FF00;
font-size: 50px;
}
#text{
color: #66FF00;
font-size: 30px;
}
button{
border-radius: 100px;
}
.start{
color: #66FF00;
background-color: black;
font-size: 30px;
padding: 0 20px;
margin-right: 20px
}
.reset{
color: #66FF00;
background-color: black;
font-size: 30px;
padding: 0 10px;
}
.changeColor{
width: 100px;
height: 100px;
border-radius:100px ;
background-color: black;
position: relative;
bottom: -29px;
right: 6px;
}
.boderColor{
width: 50px;
height: 50px;
border-radius:50px ;
border:10px solid #66FF00;
margin: auto;
background-color: black ;
}
.active{
background-color:#222222;
color: brown;
}
.aquaColor{
color: aqua;
}
.aquaBoder{
border: 10px solid aqua;
}
※上記のソースコードは、必ずコピペをしてください
。正しく動作しない恐れがあります。また、webフォントについて各自で設定をお願いします。
マウスカーソルが乗ったらボタンのデザインを変えよう
まずは、マウスカーソルが乗ったらボタンのデザインを変えてみましょう。
「ドロップダウンメニューを作ろう」でドロップダウンメニューを実装した際に使った
- mouseoverイベント
- mouseleaveイベント
を用いて実装していきます。
- ノードの取得
- addEventListenerメソッドでイベント登録
- イベントの発火後の処理を実装
ノードを取得
まずはノードを取得をしていきましょう。
function DomAcquisition(element){
return document.querySelector(element)
}
const start = DomAcquisition("#start")
const reset = DomAcquisition("#reset")
const buttons = [start,reset]
今回は、「ノードの取得用の関数」,DomAcquisitionを定義をしています。毎回、
document.querySelector(element)
と記述するのは大変だと思い、DomAcquisitionを定義をしました。
- start
- reset
を配列に格納している意図は、実装する処理が同じなためです。
あえて一度、配列化し、forEachメソッドを用いることで、同じ処理を記述をする必要がなくなり、処理を簡略化できます。
配列化した意図
が把握できた所で、ノードを取得できているか、デバックをしてみましょう。
buttons.forEach(button=>console.log(button));
正しく、ノードを取得できている事が確認できたら、次にステップに進みましょう。
addEventListenerメソッドでイベント登録
ノードを取得できたら、addEventListenerメソッドを用いて、イベントを登録をしていきましょう。登録するイベントは、
- mouseoverイベント
- mouseleaveイベント
function addEvent(eventTarget){
eventTarget.addEventListener("mouseover",function(){
this.addEventListener("mouseleave",()=>{})
})
}
buttons.forEach(button=>addEvent(button))
少し一手間、を加えたので、簡単に解説していきます。定数buttonsにからforEachメソッドを用いて、各ノードを取り出し、そのノードに対してaddEventを呼び出して、イベントを登録
をしています。
複雑に感じる方は、「イベントを登録をしているな・・・」程度で覚えていただければOKです。
処理の簡単な解説が終わった所で、実際にイベントが登録できているか確認をしてみましょう。
function addEvent(eventTarget){
eventTarget.addEventListener("mouseover",function(){
console.log("マウスオーバ");
this.addEventListener("mouseleave",()=>{console.log("マウスアウト")})
})
}
- マウスオーバ
- マウスアウト
という、文字列が交互に表示されている事が確認できましたので、イベント登録ができた事が分かりました。なので次にステップに進みましょう。続いてイベントの中身を実装していきます。
イベントの発火後の処理を実装
イベントが登録できたので、イベント発火後の処理を実装をしていきましょう。
処理内容を言語化をすると
- mouseoverイベントが発火したタイミングで、メニュー表示をする
→addメソッドでクラスを追加 - mouseleaveイベントが発火したタイミングで、メニューを非表示にする→removeメソッドでクラスを削除
となります。
では実際にソースコードに翻訳をしてみましょう。
function classChange(element,className,num){
switch (num){
case 1:
element.classList.add(className) ;
break;
case 2:
element.classList.remove(className) ; ;
break;
case 3:
element.classList.toggle(className) ; ;
break;
}
}
function addEvent(eventTarget){
eventTarget.addEventListener("mouseover",function(){
classChange(eventTarget,"active",1)
this.addEventListener("mouseleave",()=>classChange(eventTarget,"active",2))
})
}
buttons.forEach(button=>addEvent(button))
function classChange(element,className,num){
switch (num){
case 1:
element.classList.add(className) ;
break;
case 2:
element.classList.remove(className) ; ;
break;
case 3:
element.classList.toggle(className) ; ;
break;
}
}
classChangeという関数は、クラスを
- 追加(add)
- 削除(remove)
- 追加&削除(tggole)
を行う関数です。
switch (条件){
case 条件:
処理
break
}
switch文を用いて、引数のnumに格納をされている値によって条件分岐をしています。classChangeの場合は、
- numの値が1な場合
→クラスを追加をする - numの値が2な場合
→クラスを追加をする - numの値が3な場合
→クラスを追加&削除をする
と上記の通りです。
function addEvent(eventTarget){
eventTarget.addEventListener("mouseover",function(){
classChange(eventTarget,"active",1)
this.addEventListener("mouseleave",()=>classChange(eventTarget,"active",2))
})
case 1:
element.classList.add(className) ;
break;
mouseoverイベントが発火する場合、classChangeの引数numの値が1なので、クラスを追加をしています。
case 2:
element.classList.remove(className) ; ;
break;
mouseleaveイベントが発火する場合、classChangeの引数numの値が2なので、クラスを削除をしています。
activeクラスはの プロパティの内容は
.active{
background-color:#222222;
color: brown;
}
となります。
以上で、「マウスカーソルが乗ったらボタンのデザインを変えよう」の実装は、終わりです。
明るさを切り替えよう
続いて、ストップウォッチの明るさを切り替えてみましょう。
「DOM3」で用いたclickイベント
を用いて実装をしていきます。
- ノードの取得
- addEventListenerメソッドでイベント登録
- イベントの発火後の処理を実装
ノードの取得
まずは、ノードを取得をしましょう。
// 追加
const timer = DomAcquisition("#timer")
const change = DomAcquisition("#changeColor")
const boderColor = DomAcquisition(".boderColor")
const elements = [timer,start,reset, boderColor]
実際にノードを取得できているか、デバックをしてみましょう。
function forlogs(elemens){
elemens.forEach(elment=>console.log(elment))
}
forlogs([timer,change,boderColor])
正しく、ノードを取得できている事が確認できたら、次にステップに進みましょう。
addEventListenerメソッドでイベント登録
ノードを取得できたとこで、addEventListenerメソッドでイベント登録をしていきましょう。今回は、定数change にclickイベント
を登録をしていきます。
change.addEventListener("click",function(){})
実際にイベントが登録できたか、デバックをしてみましょう。
change.addEventListener("click",function(){console.log("test")})
testという、文字列が出力されており、イベント登録ができた事が分かりました。なので次のステップに進みましょう。続いてイベントの中身を実装していきます。
イベントの発火後の処理を実装
イベントが登録できたので、イベント発火後の処理を実装をしていきましょう。
処理内容を言語化をすると
定数changeがクリックされたら、配列elementsからforEachメソッドを用いて、各ノードを取り出して、そのノードに対して,aquaColorクラスを追加&削除を行う。その中で、定数boderColorに対してのみ、aquaBoderというクラスを追加&削除を行う
。
という内容になります。
言語化した内容をソースコードに翻訳をしてみましょう。
change.addEventListener("click",function(){elements.forEach(element=> (element===boderColor ? classChange(element,"aquaBoder",3) : classChange(element,"aquaColor",3)))})
(element===boderColor ? classChange(element,"aquaBoder",3) : classChange(element,"aquaColor",3))
(条件❓ 真 : 偽)
上記は、三項演算子
を用いて条件分岐を行っております。条件が一致した場合は、真
の方の処理が発火し、一致しなかった場合、偽
の方の処理が発火します。三項演算子
を用いる事で、簡単なif文をワンライナー
で処理を実装する事ができます。
今回は、配列elementsをforEachメソッドを用いて各ノードを取り出し、その中で、定数boderColorを取り出した場合、aquaBoderクラスを追加&削除を行っています。該当しない場合、aquaColorクラスを追加&削除を行います
。
少し難しい方と感じた方は、下記のソースコードでも構いません。(実装している内容については同じです。)
change.addEventListener("click",function(){
elements.forEach(elment=>{
if (element===boderColor){
classChange(element,"aquaBoder",3)
}else{
classChange(element,"aquaColor",3)
}
})
})
まあ余裕が出てきたら三項演算子を使ってみてください。以上で、「明るさを切り替えよう」の実装は、終わりです。
タイマーを実装しよう
最後にタイマーを実装していきましょう。
- 変数の定義
- 関数の定義[概念編]
- 関数の定義[実践編]
- addEventListenerメソッドでイベント登録
- イベント発火後の処理を記述
- リファクタリングをしよう
変数の定義
先ほどのトピックで、必要なノードは、全て定義をしました。なのでこちらのトピックでは、ストップウォッチを実装するにあたり必要な変数を定義をしていきます。
//開始時間
let startTime
//経過時間
let elapsedTime
//停止したまでの蓄積時間
let addStopTime=0
//setIntervalの返り値を受け取る変数
let timeId
//処理が動いているか評価
let run =false
[タイマー公式]
経過時間 = 現在時刻 - ( 開始時間 + 停止したまでの蓄積時間)
タイマーについては、上記の図の公式を使って実装していきます。現在時刻から(開始時間 + 停止したまでの蓄積時間)を引いた差(経過時間)がタイマーに表示されていきます。
停止したまでの蓄積時間は、Stopボタンが押されてから値が変化をしていきます。なのでStartボタンを押された段階では、
経過時間 = 現在時刻 - 開始時間
となります。以上で変数の定義は終わりです。
関数の定義(概念編)
続いて、必要な関数を用意をしていきます。まずは、関数の関係性をみてみましょう。下記の画像をご覧ください。
今回
- Start
- Stop&Reset
によって少し関数の扱いが異なってきます。理由としまして、Stop&Resetボタンをクリックをした後に、変わらず、timeCountを呼び出してしまうと、エラーが起きるからです
。
具体的には
- Stopボタン
→Stopボタンをクリックして時間を停止をし、もう一度Startボタンを押したら、時間が蓄積されず、時間が初期化されてしまう
- Resetボタン
→時間がリセットされない
という問題が生じてしまいます。なのでtimeCountを経由せずに直接updateTimeを呼び出しています
。
以上で、関数定義(概念編)の解説は終わりです。続いて、実際にソースコードに書き換えていきましょう。
関数の定義[実践編]
関数の定義の概念的な所の解説が終わった所で、実際にソースコードに落とし込んでいきましょう。
function InsertText(el,content){
el.textContent = content
}
function updateTime(t) {
let d = new Date(t)
let m = d.getMinutes()
let s = d.getSeconds()
let ms= d.getMilliseconds()
m = m.toString().padStart(2,`0`)
s = s.toString().padStart(2,`0`)
ms = ms.toString().padStart(3,`0`)
InsertText(timer,`${m}:${s}.${ms}`)
}
function timeCount() {
timerId = setInterval(() => {
elapsedTime = Date.now() -startTime+addStopTime
timeLeft= elapsedTime
updateTime(timeLeft)
}, 10);
}
まずは,timeCountについて解説をしていきます。
function timeCount() {
timerId = setInterval(() => {
elapsedTime = Date.now() -startTime+addStopTime
updateTime(elapsedTime)
}, 10);
}
timeCountの処理を言語化をすると、10ミリ秒ごとにsetIntervalのコールバック関数の処理を繰り返し、経過時間を10ミリ秒ごとに更新をしていく
という事になります。
elapsedTime = Date.now() -startTime+addStopTime
updateTime(elapsedTime)
こちらは先ほどの解説した、「タイマーの公式」をソースコードに落とし込んだけです。そして、elapsedTimeをupdateTimeの引数として呼び出しております。
timerId = setInterval(コールバック関数 , ミリ秒);
setIntervalの構文は上記の通りになります。setIntervalは、戻り値としintervalID
を返します。setIntervalの繰り返し処理を止めたい場合は、intervalID
が必要になります。intervalID
とは、setInterval()
を呼び出して作成したタイマーを識別する、0 ではない正の整数値です。intervalID
を clearInterval
の引数に渡す事で、setIntervalの繰り返し処理を止める事ができます。
今回の場合は、変数timerIdがintervalID
を格納しており、
- Stopボタン
- Resetボタン
をクリックをした際にclearInterval
の引数にtimerIdを渡し、呼び出す事によりsetIntervalの繰り返し処理を止めます。
以上で、timeCountの解説を終わります。少し、難しいと思いますが他の関数などとロジックを組んでいくと「なるほど‼️」となるので安心してください。
続いて、updateTimeについて解説をしていきます。
function updateTime(t) {
let d = new Date(t)
let m = d.getMinutes()
let s = d.getSeconds()
let ms= d.getMilliseconds()
m = m.toString().padStart(2,`0`)
s = s.toString().padStart(2,`0`)
ms = ms.toString().padStart(3,`0`)
InsertText(timer,`${m}:${s}.${ms}`)
}
updateTimeの処理を言語化をすると
引数のtの情報をもとに新たに時間を生成をし、その時間を
分
秒
ミリ秒
に直し、更にゼロパディングに直し、最終的にタイマー画面に挿入をする。
という内容です。
let d = new Date(t)
let m = d.getMinutes()
let s = d.getSeconds()
let ms= d.getMilliseconds()
上記のソースコードは、
- d
→引数tの情報を元に時間を生成 - m
→getMinutesメソッドを用いて分を取得。 - s
→getSecondsメソッドを用いて秒を取得 - ms
→getMillisecondsメソッドを用いてミリ秒を取得
と引数tの情報を元にして実装をしています。
m = m.toString().padStart(2,`0`)
s = s.toString().padStart(2,`0`)
ms = ms.toString().padStart(3,`0`)
こちらは、まずtoStringメソッドを用いて、数値型から文字列型に型変換をし、その後、padStartメソッドを用いて、ゼロパディング
を行っています。
ゼロパディング
とは、桁数に満たない数値の場合に、足りない桁数を足して合わせる仕組みの事です。
function InsertText(el,content){
el.textContent = content
}
InsertText(timer,`${m}:${s}.${ms}`)
最終的にInsertTextを呼び出して、タイマー画面に挿入をしています。
以上で、「関数の定義[実践編]」の解説は終わります。いきなり莫大な知識が出てきてしまっているので、混乱をするかとは、思いますが、引き続きがんばっていきましょう。
addEventListenerメソッドでイベント登録
続いて、addEventListenerメソッドでイベントを登録をしていきましょう。
- 定数start
- 定数reset
に対してclickイベント
を登録していきます。
start.addEventListener("click",function(){
})
reset.addEventListener("click",function(){
})
イベントが登録できた所で、console.logを用いてデバックをしてみましょう。
start.addEventListener("click",function(){
console.log("startボタンをクリック")
})
reset.addEventListener("click",function(){
console.log("resetボタンをクリック")
})
無事、イベントが登録できているのが確認できました。
では、最後にイベント発火後の処理を記載していきましょう。
イベント発火後の処理を記述
イベント発火後の処理を実装していきます。
start.addEventListener("click",function(){
if (run===false){
startTime = Date.now()
console.log(startTime);
InsertText(start,`Stop`)
run= true
timeCount()
}else{
InsertText(start,`Start`)
run= false
addStopTime = elapsedTime
console.log(timerId);
clearInterval(timerId)
updateTime(addStopTime)
}
})
reset.addEventListener("click",function(){
clearInterval(timerId)
elapsedTime = 0
addStopTime = 0
InsertText(start,`Start`)
run = false
return updateTime(elapsedTime)
})
まずはstartボタンについて実装していきます。
start.addEventListener("click",function(){
if (run===false){
startTime = Date.now()
InsertText(start,`Stop`)
run= true
timeCount()
}else{
InsertText(start,`Start`)
run= false
addStopTime = elapsedTime
clearInterval(timerId)
updateTime(addStopTime)
}
})
startボタンは2つの機能を持っています。
- 開始機能
- 停止機能
です。
let run ==false
if (run===false){
}else{
}
2つの機能を1つのボタンで再現をするには、真偽値を使う必要があります。なので変数runを使って、条件分岐をします。
- runの値が、falseの場合
→Starボタン - runの値が、stopの場合
→Stopボタン
if(run===false){
startTime = Date.now()
InsertText(start,`Stop`)
run= true
timeCount()
}
変数runがfalseの時は、Startボタンの役割を担います。処理内容を言語化をすると、
Startボタンがクリックしてイベントが発火した且つ、変数runの値が、falseの場合は、開始した時間を取得して,変数startTimeに格納する。その後、変数runの値をtrueに変更し、ボタンの文字列をStopに変更し、timeCountを呼び出す
。
という内容です。
ポイントはrunの値をtrueに変更
している点です。真偽値を変更をしないと挙動を分ける事ができないので、書き忘れに注意してください。
else{
clearInterval(timerId)
InsertText(start,`Start`)
run= false
addStopTime = elapsedTime
updateTime(addStopTime)
}
clearInterval(timerId)
runの値がtrueの場合は、timeCountのsetIntervalの繰り返し処理を止めて、時間を停止をしなければいけません。なので、clearIntervalの引数にtimerIdを渡して処理を止めてます。
run = false
変数runの値をfalseにしています。
addStopTime = elapsedTime
updateTime(addStopTime)
変数addStopTime にelapsedTimeを格納している理由は、再度、Startボタンクリックしたら、elapsedTimeの値が初期化してしまい、また0からタイマーが始まってしまうからです。
function timeCount() {
timerId = setInterval(() => {
elapsedTime = Date.now() -startTime+addStopTime
updateTime(elapsedTime)
}, 10);
}
elapsedTime = Date.now() -startTime+addStopTime
Startボタン押した場合、timeCountが呼び出されます。その中で、elapsedTime対してDate.nowを呼び出して、現在時刻を取得をしてしまい、値を初期化してしまいます。
したがってStopボタンをクリックしたタイミングで、elapsedTimeの値を逃さないといけません
。この様な理由のため、イベントが発火したタイミングでaddStopTimeにelapsedTimeを格納
しています。
今回の「ストップウォッチを作ろう」の中で一番大切なので、必ずポイントを抑えて
おきましょう。
addStopTime = elapsedTime
続いて、resetボタンについて解説をしていきます。
reset.addEventListener("click",function(){
clearInterval(timerId)
elapsedTime = 0
addStopTime = 0
InsertText(start,`Start`)
run = false
return updateTime(elapsedTime)
})
clearInterval(timerId)
停止機能と同様、まずは、clearIntervalを用いて繰り返し処理を止めます。
elapsedTime = 0
addStopTime = 0
- elapsedTime
- addStopTime
の値を初期化します。
InsertText(start,`Start`)
run = false
リセットするという事は初期化するという事ですので、処理が走っている場合は、止めないとおかしな挙動になります。なので、変数runの値をfalseにして、InsertTextを用いて、ボタンをStartという文字列に変更します。
return updateTime(elapsedTime)
最後に初期化したelapsedTimeをupdateTimeの引数に渡し、呼び出して終了です。
以上で「ストップウォッチを作ろう」の実装を終えます。リファクタリングした、完成版のソースコードは、下記に貼り付けておきます。
※リファクタリング内容については、解説を省かせていただきます。
// 変数・定数・配列
const timer = DomAcquisition("#timer")
const start = DomAcquisition("#start")
const reset = DomAcquisition("#reset")
const change = DomAcquisition("#changeColor")
const boderColor = DomAcquisition(".boderColor")
const elements = [timer,start,reset, boderColor]
const buttons = [start,reset,change]
let startTime
let elapsedTime
let timeId
let run =false
let addStopTime=0
//関数
function forlogs(elemens){
elemens.forEach(elment=>console.log(elment))
}
function DomAcquisition(element){
return document.querySelector(element)
}
function InsertText(el,content){
el.textContent = content
}
function classChange(element,className,num){
switch (num){
case 1:
element.classList.add(className) ;
break;
case 2:
element.classList.remove(className) ; ;
break;
case 3:
element.classList.toggle(className) ; ;
break;
}
}
function updateTime(t) {
let d = new Date(t)
let m = d.getMinutes()
let s = d.getSeconds()
let ms= d.getMilliseconds()
m = m.toString().padStart(2,`0`)
s = s.toString().padStart(2,`0`)
ms = ms.toString().padStart(3,`0`)
InsertText(timer,`${m}:${s}.${ms}`)
}
function timeCount() {
timerId = setInterval(() => {
elapsedTime = Date.now() -startTime+addStopTime
updateTime(elapsedTime)
}, 10);
}
function addEvent(eventTarget,eventName,callBack,){
eventTarget.addEventListener(eventName,callBack)
}
function startClickEvent(){
if (run===false){
startTime = Date.now()
console.log(startTime);
InsertText(start,`Stop`)
run= true
timeCount()
}else{
clearInterval(timerId)
addStopTime =elapsedTime
InsertText(start,`Start`)
run= false
updateTime(addStopTime)
}
}
function resetClickEvent(){
clearInterval(timerId)
elapsedTime = 0
addStopTime = 0
InsertText(start,`Start`)
run = false
return updateTime(elapsedTime)
}
function changeClickEvent(){
elements.forEach(element=> (element===boderColor ? classChange(element,"aquaBoder",3) : classChange(element,"aquaColor",3)))
}
function MouseoverAndMouseleaveEvent(){
classChange(this,"active",1)
this.addEventListener("mouseleave",()=>classChange(this,"active",2))
}
function buttonsEvent(eventName){
buttons.forEach(eventTarget=>{
if(eventName=="mouseover"){
addEvent(eventTarget,eventName,MouseoverAndMouseleaveEvent)
}else{
switch (eventTarget){
case start:
addEvent(eventTarget,eventName,startClickEvent);
break;
case reset:
addEvent(eventTarget,eventName,resetClickEvent) ;
break;
case change:
addEvent(eventTarget,eventName,changeClickEvent) ;
break;
}
}
})
}
// 処理の呼び出し
const eventNames = ["mouseover","click"]
eventNames.forEach(eventName=>buttonsEvent(eventName))
まとめ:ストップウォッチを作ろう
今回は、ストップウォッチを作成しました。かなり重い内容になってしまいましたが、ストップウォッチを実装する事ができれば、JavaScriptの最低限度の基礎的は身に付いているはずです。
なので、ご自身で、何か作成をしてみる事をお勧めします。
例えば
- 簡易的なチャットボット
- カウントダウンタイマー
- タイピングゲーム
などです。
以上、kazutoでした。
関連記事
※ストップウォッチを作成するにあたりDOMの知識が必要になります。DOMについて不安要素がある方は、下記の関連記事を一通り参照してから実装をする事を推奨します。