JavaScript | 1 日 120 分 × 7 日アプリ学習:タイマー & ストップウォッチ

JavaScript
スポンサーリンク

3日目のゴールと今日やること

3日目のテーマは
「ストップウォッチの知識を“カウントダウンタイマー”に応用する」
ことです。

キーワードは変わりません。

setTimeout
setInterval
時間管理ロジック
開始 / 停止 / リセット
ミリ秒表示
多重起動防止

ただし今日は、
「0 から増えていくストップウォッチ」ではなく
“指定時間から減っていくカウントダウン”
を作ります。

同じ「時間アプリ」でも、
ロジックの考え方が少し変わるところを
しっかり噛み砕いていきます。


カウントダウンタイマーの考え方をストップウォッチと比べる

ストップウォッチとカウントダウンの違い

ストップウォッチ
→ 0 からスタートして「経過時間」を増やしていく

カウントダウン
→ 指定した時間から「残り時間」を減らしていく

どちらも「時間を扱うアプリ」ですが、
“何を基準にしているか” が違います。

ストップウォッチ:
基準は「スタートした瞬間」

カウントダウン:
基準は「終了予定時刻」

この違いを意識すると、
ロジックがスッと頭に入ります。

終了予定時刻を使う発想

カウントダウンでは、
次のように考えるとシンプルです。

  1. ユーザーが「10秒」と入力する
  2. 今の時刻に 10 秒を足して「終了予定時刻」を計算する
  3. 現在時刻との差を取ることで「残り時間」を求める

コードで書くとこうなります。

const durationMs = 10 * 1000; // 10秒
const endTime = Date.now() + durationMs;
JavaScript

あとは update のたびに、

const remaining = endTime - Date.now();
JavaScript

とすれば、
「残りミリ秒」が常に計算できます。


カウントダウンの基本ロジックを組み立てる

変数の設計

まずは必要な状態を整理します。

let endTime = 0;        // 終了予定時刻(ミリ秒)
let remainingMs = 0;    // 残り時間(ミリ秒)
let timerId = null;     // setInterval の ID
let state = "stopped";  // "stopped" | "running" | "paused"
JavaScript

ここでのポイントは、
「残り時間」と「終了予定時刻」を分けて考える
ことです。

running 中は endTime を基準に計算し、
paused 中は remainingMs を保持しておきます。

開始処理の流れ

ユーザーが「秒数」を入力したとします。

const inputSeconds = Number(document.getElementById("secondsInput").value);
const durationMs = inputSeconds * 1000;
JavaScript

開始ボタンを押したときの処理はこうなります。

function start() {
  if (state === "running") return;

  if (state === "stopped") {
    const inputSeconds = Number(secondsInput.value);
    if (Number.isNaN(inputSeconds) || inputSeconds <= 0) {
      showError("1秒以上を入力してください。");
      return;
    }
    clearError();

    remainingMs = inputSeconds * 1000;
  }

  endTime = Date.now() + remainingMs;
  timerId = setInterval(update, 10);
  state = "running";
}
JavaScript

ここで重要なのは、

stopped のときだけ「入力値 → 残り時間」に変換する
paused からの再開では、remainingMs をそのまま使う

という「状態による分岐」です。


update 関数で「残り時間」を計算して表示する

残り時間の計算

update の中身はこうなります。

function update() {
  const now = Date.now();
  remainingMs = endTime - now;

  if (remainingMs <= 0) {
    remainingMs = 0;
    display(remainingMs);
    finish();
    return;
  }

  display(remainingMs);
}
JavaScript

ここでやっていることは、

現在時刻との差 → 残り時間
0 以下になったら「終了」とみなす

というシンプルなロジックです。

終了処理

function finish() {
  clearInterval(timerId);
  state = "stopped";
  // ここで音を鳴らしたり、メッセージを出したりできる
}
JavaScript

深掘り:なぜ「残り時間を変数に持ち直す」のか?

remainingMs を変数として持っておくことで、

一時停止
再開
リセット

のときに、
「今どれくらい残っているか」を
いつでも参照できます。


停止(一時停止)と再開のロジック

停止(pause)の考え方

停止ボタンを押したときは、

  1. 今の残り時間を計算して remainingMs に保存
  2. setInterval を止める
  3. state を “paused” にする

という流れになります。

function pause() {
  if (state !== "running") return;

  const now = Date.now();
  remainingMs = endTime - now;
  if (remainingMs < 0) remainingMs = 0;

  clearInterval(timerId);
  state = "paused";
}
JavaScript

再開の考え方

再開するときは、

  1. endTime を「今の時刻 + remainingMs」に再計算
  2. setInterval を再開
  3. state を “running” に戻す
function resume() {
  if (state !== "paused") return;

  endTime = Date.now() + remainingMs;
  timerId = setInterval(update, 10);
  state = "running";
}
JavaScript

深掘り:ストップウォッチとの違い

ストップウォッチでは
「累積時間(elapsedBefore)」を足していきましたが、
カウントダウンでは
「残り時間(remainingMs)」を減らしていく
という違いがあります。

ただしどちらも、

running 中は「現在時刻との差」で計算する
paused 中は「途中経過」を変数に保持する

という構造は同じです。


リセットのロジックと UI の一貫性

リセットの動き

リセットは、

タイマーを完全に止める
残り時間を 0 にする
表示を 00:00.000 に戻す
状態を “stopped” に戻す

という動きになります。

function reset() {
  clearInterval(timerId);
  timerId = null;
  remainingMs = 0;
  endTime = 0;
  state = "stopped";
  display(0);
}
JavaScript

深掘り:リセットは「状態を初期状態に戻す」操作

リセットは単に「止める」だけではなく、
“アプリを最初の状態に戻す” 操作です。

タイマー系アプリでは、

start / pause / resume / reset

のそれぞれが
「状態をどう変えるか」を
はっきりさせておくことが大事です。


ミリ秒表示をカウントダウンに適用する

表示関数の再利用

ストップウォッチで使った表示関数を
そのままカウントダウンにも使えます。

function display(ms) {
  const milliseconds = ms % 1000;
  const totalSeconds = Math.floor(ms / 1000);
  const seconds = totalSeconds % 60;
  const minutes = Math.floor(totalSeconds / 60);

  const text =
    `${String(minutes).padStart(2, "0")}:` +
    `${String(seconds).padStart(2, "0")}.` +
    `${String(milliseconds).padStart(3, "0")}`;

  document.getElementById("time").textContent = text;
}
JavaScript

深掘り:ロジックは「経過時間」でも「残り時間」でも同じ

display は
「ミリ秒 → 分・秒・ミリ秒」
に変換しているだけなので、

経過時間(ストップウォッチ)
残り時間(カウントダウン)

どちらにも使えます。

“時間をどう解釈するか”は呼び出し側の責任
という分離ができているのがポイントです。


多重起動防止をカウントダウンにも適用する

状態による制御

start / pause / resume / reset
のそれぞれで、
「どの状態から呼べるか」を決めておきます。

例:

stopped → start だけ有効
running → pause / reset / (ラップなど)
paused → resume / reset

これをコードに落とすとこうなります。

function start() {
  if (state !== "stopped") return;
  // 入力チェック → remainingMs 設定 → endTime 設定 → setInterval
  state = "running";
}

function pause() {
  if (state !== "running") return;
  // 残り時間計算 → clearInterval
  state = "paused";
}

function resume() {
  if (state !== "paused") return;
  // endTime 再計算 → setInterval
  state = "running";
}

function reset() {
  if (state === "stopped") return;
  // 全部クリア
  state = "stopped";
}
JavaScript

深掘り:状態マシン的な発想

これはいわゆる
「状態マシン(ステートマシン)」
の考え方です。

状態を文字列で表し、
「どの状態からどの状態に遷移できるか」を
コードで表現しています。

タイマー・ゲーム・フォームウィザードなど、
“段階がある UI” では
この発想がとても役に立ちます。


3日目のまとめコード(重要部分だけ)

let endTime = 0;
let remainingMs = 0;
let timerId = null;
let state = "stopped"; // "stopped" | "running" | "paused"

const secondsInput = document.getElementById("secondsInput");
const timeDisplay = document.getElementById("time");

function start() {
  if (state !== "stopped") return;

  const inputSeconds = Number(secondsInput.value);
  if (Number.isNaN(inputSeconds) || inputSeconds <= 0) {
    showError("1秒以上を入力してください。");
    return;
  }
  clearError();

  remainingMs = inputSeconds * 1000;
  endTime = Date.now() + remainingMs;

  timerId = setInterval(update, 10);
  state = "running";
}

function update() {
  const now = Date.now();
  remainingMs = endTime - now;

  if (remainingMs <= 0) {
    remainingMs = 0;
    display(remainingMs);
    finish();
    return;
  }

  display(remainingMs);
}

function pause() {
  if (state !== "running") return;

  const now = Date.now();
  remainingMs = endTime - now;
  if (remainingMs < 0) remainingMs = 0;

  clearInterval(timerId);
  state = "paused";
}

function resume() {
  if (state !== "paused") return;

  endTime = Date.now() + remainingMs;
  timerId = setInterval(update, 10);
  state = "running";
}

function reset() {
  clearInterval(timerId);
  timerId = null;
  remainingMs = 0;
  endTime = 0;
  state = "stopped";
  display(0);
}

function display(ms) {
  const milliseconds = ms % 1000;
  const totalSeconds = Math.floor(ms / 1000);
  const seconds = totalSeconds % 60;
  const minutes = Math.floor(totalSeconds / 60);

  const text =
    `${String(minutes).padStart(2, "0")}:` +
    `${String(seconds).padStart(2, "0")}.` +
    `${String(milliseconds).padStart(3, "0")}`;

  timeDisplay.textContent = text;
}
JavaScript

今日いちばん深く理解してほしいこと

カウントダウンタイマーは、

終了予定時刻(endTime)
残り時間(remainingMs)
状態(state)

この 3 つを軸に動いています。

ストップウォッチとカウントダウンは
見た目は似ていても、
「経過時間を増やすか」「残り時間を減らすか」
という発想の違いがあります。

でもどちらも、

Date.now() で時間差を計算する
setInterval で定期的に update する
状態で操作を制御する

という共通の骨格を持っています。

ここまで理解できているあなたは、
もう「時間を扱うアプリ」を
自分で設計できるレベルにいます。

タイトルとURLをコピーしました