環境構築からWEBアプリ開発・スマホアプリ開発まで。ときには動画制作やゲームも。

supilog
すぴろぐ

【ルービックキューブのタイマーを作る】第2回 急所のタイマーをつくる回

【ルービックキューブのタイマーを作る】第2回 急所のタイマーをつくる回

今回は本アプリの急所のタイマーを作っていく。

簡単な仕様説明

まずタイマーの操作については、スペースキーを長押しして、キーを離したらタイマースタートという動作をさせたい。誤動作を防止するために、0.6から0.7秒くらい長押ししていないとスタートしないようにしたい。

タイマーがスタートしたら、次にスペースキーを押した時には、タイマーを停止。表示は計測結果が出力されているが、状態としては初期と同じで、次の長押しを検知できる状態としたい。

状態として、下記の4つのステータスを行き来し、「長押し待機中」はタイマーの色を赤に。「準備完了」時は、タイマーの色を緑に。それ以外は、黒にしたい。

  • ニュートラル
    何も操作していない通常状態。
  • 長押し待機中
    長押しをしている状態。最長でも0.7秒程度の時間。
  • 準備完了
    長押しが完了して、キーを離すまでの間。
  • タイマー計測中
    スペースキーを離して、タイマーが動作している状態。

こんなところです。

長押し機能

index.blade.php

Laravelのbladeテンプレートファイルです。idがtimerのdivタグの中身である0.00の表記を加工していきます。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<div id="timer" class="sm:flex sm:justify-center text-9xl">0.00</div>
... 略 ...
@vite('resources/js/timer.js')
<div id="timer" class="sm:flex sm:justify-center text-9xl">0.00</div> ... 略 ... @vite('resources/js/timer.js')
<div id="timer" class="sm:flex sm:justify-center text-9xl">0.00</div>

... 略 ...

@vite('resources/js/timer.js')

timer.js

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const cubelog_timer = {
// ステータス
currentStatus: '',
// ステータス値
status: {
neutral: 0,
wait: 1,
ready: 2,
timer: 3
},
// タイマーオブジェクト
cubeTimer: '',
// タイマー表示インターバル
cubeTimerInterval: 39,
// 開始時間
startTime: 0,
// 長押しタイマー
longPushTimer: '',
// 長押し判定値
longPushWaitTime: 600,
// キーコード
keys: {
Space: 32,
},
init: function () {
this.currentStatus = this.status.neutral;
window.addEventListener('keydown', (event) => {
this.keyDown(event);
});
window.addEventListener('keyup', (event) => {
this.keyUp(event);
});
},
keyDown: function (event) {
if (event.keyCode !== this.keys.Space) {
return false;
}
// 長押し開始時
if (this.currentStatus === this.status.neutral) {
this.currentStatus = this.status.wait;
document.getElementById('timer').classList.add('wait');
this.longPushTimer = setTimeout(function () {
this.ready();
}.bind(this), this.longPushWaitTime);
}
},
keyUp: function (event) {
if (event.keyCode !== this.keys.Space) {
return false;
}
// 共通処理
clearTimeout(this.longPushTimer);
document.getElementById('timer').classList.remove('wait');
// 長押し完了前の処理
if (this.currentStatus === this.status.wait) {
this.currentStatus = this.status.neutral;
}
// 長押し完了後の処理
if (this.currentStatus === this.status.ready) {
this.currentStatus = this.status.timer;
this.startTime = Date.now();
this.timer();
}
},
ready: function () {
if (this.currentStatus !== this.status.wait) {
return false;
}
// 長押し完了処理
this.currentStatus = this.status.ready;
document.getElementById('timer').textContent = '0.00';
document.getElementById('timer').classList.remove('wait');
document.getElementById('timer').classList.add('ready');
},
timer: function () {
console.log('タイマースタートしたよ');
}
};
cubelog_timer.init();
const cubelog_timer = { // ステータス currentStatus: '', // ステータス値 status: { neutral: 0, wait: 1, ready: 2, timer: 3 }, // タイマーオブジェクト cubeTimer: '', // タイマー表示インターバル cubeTimerInterval: 39, // 開始時間 startTime: 0, // 長押しタイマー longPushTimer: '', // 長押し判定値 longPushWaitTime: 600, // キーコード keys: { Space: 32, }, init: function () { this.currentStatus = this.status.neutral; window.addEventListener('keydown', (event) => { this.keyDown(event); }); window.addEventListener('keyup', (event) => { this.keyUp(event); }); }, keyDown: function (event) { if (event.keyCode !== this.keys.Space) { return false; } // 長押し開始時 if (this.currentStatus === this.status.neutral) { this.currentStatus = this.status.wait; document.getElementById('timer').classList.add('wait'); this.longPushTimer = setTimeout(function () { this.ready(); }.bind(this), this.longPushWaitTime); } }, keyUp: function (event) { if (event.keyCode !== this.keys.Space) { return false; } // 共通処理 clearTimeout(this.longPushTimer); document.getElementById('timer').classList.remove('wait'); // 長押し完了前の処理 if (this.currentStatus === this.status.wait) { this.currentStatus = this.status.neutral; } // 長押し完了後の処理 if (this.currentStatus === this.status.ready) { this.currentStatus = this.status.timer; this.startTime = Date.now(); this.timer(); } }, ready: function () { if (this.currentStatus !== this.status.wait) { return false; } // 長押し完了処理 this.currentStatus = this.status.ready; document.getElementById('timer').textContent = '0.00'; document.getElementById('timer').classList.remove('wait'); document.getElementById('timer').classList.add('ready'); }, timer: function () { console.log('タイマースタートしたよ'); } }; cubelog_timer.init();
const cubelog_timer = {
    // ステータス
    currentStatus: '',
    // ステータス値
    status: {
        neutral: 0,
        wait: 1,
        ready: 2,
        timer: 3
    },
    // タイマーオブジェクト
    cubeTimer: '',
    // タイマー表示インターバル
    cubeTimerInterval: 39,
    // 開始時間
    startTime: 0,
    // 長押しタイマー
    longPushTimer: '',
    // 長押し判定値
    longPushWaitTime: 600,
    // キーコード
    keys: {
        Space: 32,
    },

    init: function () {
        this.currentStatus = this.status.neutral;
        window.addEventListener('keydown', (event) => {
            this.keyDown(event);
        });
        window.addEventListener('keyup', (event) => {
            this.keyUp(event);
        });
    },
    keyDown: function (event) {
        if (event.keyCode !== this.keys.Space) {
            return false;
        }

        // 長押し開始時
        if (this.currentStatus === this.status.neutral) {
            this.currentStatus = this.status.wait;
            document.getElementById('timer').classList.add('wait');
            this.longPushTimer = setTimeout(function () {
                this.ready();
            }.bind(this), this.longPushWaitTime);
        }
    },
    keyUp: function (event) {
        if (event.keyCode !== this.keys.Space) {
            return false;
        }

        // 共通処理
        clearTimeout(this.longPushTimer);
        document.getElementById('timer').classList.remove('wait');
        // 長押し完了前の処理
        if (this.currentStatus === this.status.wait) {
            this.currentStatus = this.status.neutral;
        }
        // 長押し完了後の処理
        if (this.currentStatus === this.status.ready) {
            this.currentStatus = this.status.timer;
            this.startTime = Date.now();
            this.timer();
        }
    },
    ready: function () {
        if (this.currentStatus !== this.status.wait) {
            return false;
        }
        // 長押し完了処理
        this.currentStatus = this.status.ready;
        document.getElementById('timer').textContent = '0.00';
        document.getElementById('timer').classList.remove('wait');
        document.getElementById('timer').classList.add('ready');
    },
    timer: function () {
        console.log('タイマースタートしたよ');
    }
};

cubelog_timer.init();

スペースキーを長押ししてみる

ちゃんと反応してくれてますね。良かった。

解説

init関数でキーが押された時と、キーが離された時に、イベントが発生するように設定
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
window.addEventListener('keydown', (event) => {
this.keyDown(event);
});
window.addEventListener('keyup', (event) => {
this.keyUp(event);
});
window.addEventListener('keydown', (event) => { this.keyDown(event); }); window.addEventListener('keyup', (event) => { this.keyUp(event); });
        window.addEventListener('keydown', (event) => {
            this.keyDown(event);
        });
        window.addEventListener('keyup', (event) => {
            this.keyUp(event);
        });
ステータス管理を細かく行う

例えば、キー押下時は、「長押しを始める時」と「長押し中」と「タイマーをストップする時」の3パターンある。同じスペースキー押下で、異なる処理を行うために、ステータス管理(this.currentStatus)をしている。

長押し完了はsetTimeoutで

一定時間スペースキーを押すことで、タイマーがスタート出来る機能の実装には、setTimeoutを使用している。setTimeoutは一定時間後に一度だけ処理を実行させる命令なので、長押し完了に必要な時間が経過したら、ready関数が実行されるようにした。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
this.longPushTimer = setTimeout(function () {
this.ready();
}.bind(this), this.longPushWaitTime);
this.longPushTimer = setTimeout(function () { this.ready(); }.bind(this), this.longPushWaitTime);
            this.longPushTimer = setTimeout(function () {
                this.ready();
            }.bind(this), this.longPushWaitTime);

また、長押しが足りない場合でもsetTimeoutは一定時間後に処理されてしまう。しかし、長押しが足りないということは、keyUpの処理が実行されていることになるので、keyUpの処理でsetTimeoutをクリアしてあげることで、readyが実行されないようになっている。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
clearTimeout(this.longPushTimer);
clearTimeout(this.longPushTimer);
        clearTimeout(this.longPushTimer);

タイマー機能

それでは、タイマー機能を作成していく。内部でタイマー処理を行うと同時に、WEB上でもタイマーが動作しているようにする。

先ほどのtimer.jsに少し書き加えたのがこちら。

timer.js

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const cubelog_timer = {
// ステータス
currentStatus: '',
// ステータス値
status: {
neutral: 0,
wait: 1,
ready: 2,
timer: 3
},
// タイマーオブジェクト
cubeTimer: '',
// タイマー表示インターバル
cubeTimerInterval: 37,
// 開始時間
startTime: 0,
// 長押しタイマー
longPushTimer: '',
// 長押し判定値
longPushWaitTime: 600,
// キーコード
keys: {
Space: 32,
},
init: function () {
this.currentStatus = this.status.neutral;
window.addEventListener('keydown', (event) => {
this.keyDown(event);
});
window.addEventListener('keyup', (event) => {
this.keyUp(event);
});
},
keyDown: function (event) {
if (event.keyCode !== this.keys.Space) {
return false;
}
// 長押し開始時
if (this.currentStatus === this.status.neutral) {
document.getElementById('timer').classList.add('wait');
this.currentStatus = this.status.wait;
this.longPushTimer = setTimeout(function () {
this.ready();
}.bind(this), this.longPushWaitTime);
}
// タイマー終了時
if (this.currentStatus === this.status.timer) {
this.currentStatus = this.status.neutral;
clearTimeout(this.cubeTimer);
}
},
keyUp: function (event) {
if (event.keyCode !== this.keys.Space) {
return false;
}
// 共通処理
clearTimeout(this.longPushTimer);
document.getElementById('timer').classList.remove('wait');
// 長押し完了前の処理
if (this.currentStatus === this.status.wait) {
this.currentStatus = this.status.neutral;
}
// 長押し完了後の処理
if (this.currentStatus === this.status.ready) {
this.currentStatus = this.status.timer;
this.startTime = Date.now();
this.timer();
}
},
ready: function () {
if (this.currentStatus !== this.status.wait) {
return false;
}
// 長押し完了処理
this.currentStatus = this.status.ready;
document.getElementById('timer').textContent = '0.00';
document.getElementById('timer').classList.remove('wait');
document.getElementById('timer').classList.add('ready');
},
timer: function () {
document.getElementById('timer').classList.remove('wait');
document.getElementById('timer').classList.remove('ready');
// タイマー表示処理
this.cubeTimer = setInterval(function () {
const elapsedTime = Date.now() - this.startTime;
let minutes = Math.floor((elapsedTime / 1000 / 60) % 60);
let seconds = Math.floor((elapsedTime / 1000) % 60);
let milliseconds = Math.floor((elapsedTime % 1000) / 10);
if (minutes > 0) {
document.getElementById('timer').textContent = String(minutes) + ':' + String(seconds).padStart(2, '0') + '.' + String(milliseconds).padStart(2, '0');
} else {
document.getElementById('timer').textContent = String(seconds) + '.' + String(milliseconds).padStart(2, '0');
}
}.bind(this), this.cubeTimerInterval);
}
};
cubelog_timer.init();
const cubelog_timer = { // ステータス currentStatus: '', // ステータス値 status: { neutral: 0, wait: 1, ready: 2, timer: 3 }, // タイマーオブジェクト cubeTimer: '', // タイマー表示インターバル cubeTimerInterval: 37, // 開始時間 startTime: 0, // 長押しタイマー longPushTimer: '', // 長押し判定値 longPushWaitTime: 600, // キーコード keys: { Space: 32, }, init: function () { this.currentStatus = this.status.neutral; window.addEventListener('keydown', (event) => { this.keyDown(event); }); window.addEventListener('keyup', (event) => { this.keyUp(event); }); }, keyDown: function (event) { if (event.keyCode !== this.keys.Space) { return false; } // 長押し開始時 if (this.currentStatus === this.status.neutral) { document.getElementById('timer').classList.add('wait'); this.currentStatus = this.status.wait; this.longPushTimer = setTimeout(function () { this.ready(); }.bind(this), this.longPushWaitTime); } // タイマー終了時 if (this.currentStatus === this.status.timer) { this.currentStatus = this.status.neutral; clearTimeout(this.cubeTimer); } }, keyUp: function (event) { if (event.keyCode !== this.keys.Space) { return false; } // 共通処理 clearTimeout(this.longPushTimer); document.getElementById('timer').classList.remove('wait'); // 長押し完了前の処理 if (this.currentStatus === this.status.wait) { this.currentStatus = this.status.neutral; } // 長押し完了後の処理 if (this.currentStatus === this.status.ready) { this.currentStatus = this.status.timer; this.startTime = Date.now(); this.timer(); } }, ready: function () { if (this.currentStatus !== this.status.wait) { return false; } // 長押し完了処理 this.currentStatus = this.status.ready; document.getElementById('timer').textContent = '0.00'; document.getElementById('timer').classList.remove('wait'); document.getElementById('timer').classList.add('ready'); }, timer: function () { document.getElementById('timer').classList.remove('wait'); document.getElementById('timer').classList.remove('ready'); // タイマー表示処理 this.cubeTimer = setInterval(function () { const elapsedTime = Date.now() - this.startTime; let minutes = Math.floor((elapsedTime / 1000 / 60) % 60); let seconds = Math.floor((elapsedTime / 1000) % 60); let milliseconds = Math.floor((elapsedTime % 1000) / 10); if (minutes > 0) { document.getElementById('timer').textContent = String(minutes) + ':' + String(seconds).padStart(2, '0') + '.' + String(milliseconds).padStart(2, '0'); } else { document.getElementById('timer').textContent = String(seconds) + '.' + String(milliseconds).padStart(2, '0'); } }.bind(this), this.cubeTimerInterval); } }; cubelog_timer.init();
const cubelog_timer = {
    // ステータス
    currentStatus: '',
    // ステータス値
    status: {
        neutral: 0,
        wait: 1,
        ready: 2,
        timer: 3
    },
    // タイマーオブジェクト
    cubeTimer: '',
    // タイマー表示インターバル
    cubeTimerInterval: 37,
    // 開始時間
    startTime: 0,
    // 長押しタイマー
    longPushTimer: '',
    // 長押し判定値
    longPushWaitTime: 600,
    // キーコード
    keys: {
        Space: 32,
    },

    init: function () {
        this.currentStatus = this.status.neutral;
        window.addEventListener('keydown', (event) => {
            this.keyDown(event);
        });
        window.addEventListener('keyup', (event) => {
            this.keyUp(event);
        });
    },
    keyDown: function (event) {
        if (event.keyCode !== this.keys.Space) {
            return false;
        }

        // 長押し開始時
        if (this.currentStatus === this.status.neutral) {
            document.getElementById('timer').classList.add('wait');
            this.currentStatus = this.status.wait;
            this.longPushTimer = setTimeout(function () {
                this.ready();
            }.bind(this), this.longPushWaitTime);
        }

        // タイマー終了時
        if (this.currentStatus === this.status.timer) {
            this.currentStatus = this.status.neutral;
            clearTimeout(this.cubeTimer);
        }

    },
    keyUp: function (event) {
        if (event.keyCode !== this.keys.Space) {
            return false;
        }

        // 共通処理
        clearTimeout(this.longPushTimer);
        document.getElementById('timer').classList.remove('wait');
        // 長押し完了前の処理
        if (this.currentStatus === this.status.wait) {
            this.currentStatus = this.status.neutral;
        }
        // 長押し完了後の処理
        if (this.currentStatus === this.status.ready) {
            this.currentStatus = this.status.timer;
            this.startTime = Date.now();
            this.timer();
        }
    },
    ready: function () {
        if (this.currentStatus !== this.status.wait) {
            return false;
        }
        // 長押し完了処理
        this.currentStatus = this.status.ready;
        document.getElementById('timer').textContent = '0.00';
        document.getElementById('timer').classList.remove('wait');
        document.getElementById('timer').classList.add('ready');
    },
    timer: function () {
        document.getElementById('timer').classList.remove('wait');
        document.getElementById('timer').classList.remove('ready');
        // タイマー表示処理
        this.cubeTimer = setInterval(function () {
            const elapsedTime = Date.now() - this.startTime;
            let minutes = Math.floor((elapsedTime / 1000 / 60) % 60);
            let seconds = Math.floor((elapsedTime / 1000) % 60);
            let milliseconds = Math.floor((elapsedTime % 1000) / 10);
            if (minutes > 0) {
                document.getElementById('timer').textContent = String(minutes) + ':' + String(seconds).padStart(2, '0') + '.' + String(milliseconds).padStart(2, '0');
            } else {
                document.getElementById('timer').textContent = String(seconds) + '.' + String(milliseconds).padStart(2, '0');
            }
        }.bind(this), this.cubeTimerInterval);
    }
};

cubelog_timer.init();

解説

タイマー表示処理をtimerView関数にまとめた

1分を越えると表示が変わるので、処理を分けている。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
timerView: function () {
const elapsedTime = Date.now() - this.startTime;
const minutes = Math.floor((elapsedTime / 1000 / 60) % 60);
const seconds = Math.floor((elapsedTime / 1000) % 60);
const milliseconds = Math.floor((elapsedTime % 1000) / 10);
if (minutes > 0) {
document.getElementById('timer').textContent = String(minutes) + ':' + String(seconds).padStart(2, '0') + '.' + String(milliseconds).padStart(2, '0');
} else {
document.getElementById('timer').textContent = String(seconds) + '.' + String(milliseconds).padStart(2, '0');
}
},
timerView: function () { const elapsedTime = Date.now() - this.startTime; const minutes = Math.floor((elapsedTime / 1000 / 60) % 60); const seconds = Math.floor((elapsedTime / 1000) % 60); const milliseconds = Math.floor((elapsedTime % 1000) / 10); if (minutes > 0) { document.getElementById('timer').textContent = String(minutes) + ':' + String(seconds).padStart(2, '0') + '.' + String(milliseconds).padStart(2, '0'); } else { document.getElementById('timer').textContent = String(seconds) + '.' + String(milliseconds).padStart(2, '0'); } },
    timerView: function () {
        const elapsedTime = Date.now() - this.startTime;
        const minutes = Math.floor((elapsedTime / 1000 / 60) % 60);
        const seconds = Math.floor((elapsedTime / 1000) % 60);
        const milliseconds = Math.floor((elapsedTime % 1000) / 10);
        if (minutes > 0) {
            document.getElementById('timer').textContent = String(minutes) + ':' + String(seconds).padStart(2, '0') + '.' + String(milliseconds).padStart(2, '0');
        } else {
            document.getElementById('timer').textContent = String(seconds) + '.' + String(milliseconds).padStart(2, '0');
        }
    },
timer関数でタイマー機能を実装

setInterval関数は、setTimeoutとは違って、一定時間毎に繰り返し処理をするものである。一定時間毎に、this.timerViewが実行される。ストップウォッチのように数字が動く処理は、timerViewを何度も実行することで、実装している。

cubeTimerIntervalで指定している値(msec)が、timerViewが実行される間隔である。100などのキリの良い数字にすると、見た目が面白くないので、あえて適当に37にした。とくに意味はない。10だと0.01秒単位で変化してくれるが、ちょっと残像が過剰で目が疲れる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
timer: function () {
document.getElementById('timer').classList.remove('wait');
document.getElementById('timer').classList.remove('ready');
// タイマー表示処理
this.cubeTimer = setInterval(function () {
this.timerView();
}.bind(this), this.cubeTimerInterval);
},
timer: function () { document.getElementById('timer').classList.remove('wait'); document.getElementById('timer').classList.remove('ready'); // タイマー表示処理 this.cubeTimer = setInterval(function () { this.timerView(); }.bind(this), this.cubeTimerInterval); },
    timer: function () {
        document.getElementById('timer').classList.remove('wait');
        document.getElementById('timer').classList.remove('ready');
        // タイマー表示処理
        this.cubeTimer = setInterval(function () {
            this.timerView();
        }.bind(this), this.cubeTimerInterval);
    },
keyDown関数に終了処理を追加

最終タイム処理をtimerView側にまかせてしまうと、cubeTimerInterval間隔でしか実行されていなかった弊害で、コンマ数秒のズレが発生するのがもったいないので、タイマー停止と同時に最終タイマー表示を行っている。

現在は空の処理だが、timerStore関数でデータ保存をする予定。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// タイマー終了時
if (this.currentStatus === this.status.timer) {
// タイマー停止
clearTimeout(this.cubeTimer);
// 最終タイマー表示
this.timerView();
// データ保存処理
this.timerStore();
// 初期化処理
this.currentStatus = this.status.neutral;
}
// タイマー終了時 if (this.currentStatus === this.status.timer) { // タイマー停止 clearTimeout(this.cubeTimer); // 最終タイマー表示 this.timerView(); // データ保存処理 this.timerStore(); // 初期化処理 this.currentStatus = this.status.neutral; }
        // タイマー終了時
        if (this.currentStatus === this.status.timer) {
            // タイマー停止
            clearTimeout(this.cubeTimer);
            // 最終タイマー表示
            this.timerView();
            // データ保存処理
            this.timerStore();
            // 初期化処理
            this.currentStatus = this.status.neutral;
        }

動作確認

無事に実行されるでしょうか。

予定していた動作をしてくれていますね。やりました。

まとめ

なんとかタイマーは動きましたので、実はもう計測はできちゃうんですね。わぁ嬉しい。これでルービックキューブの練習もはかどるってもんですっ!!

さて次回は、スクランブルの表示機能に入っていきたいと思います。それでは。

ソースコード

HTML&JSファイル

本日の実装が完了した状態のタグが「v1.0.2」です。

https://github.com/supilog/cube/blob/v1.0.2/resources/views/index.blade.php

https://github.com/supilog/cube/blob/v1.0.2/resources/js/timer.js

リンク

「ルービックキューブのタイマーを作る」シリーズ