第3回 試験用キッチンタイマー制作|終了演出編

この記事は約36分で読めます。

前回は、タイマーの見た目を青いガラス・ネオン風へ変更し、パソコンだけでなくスマートフォンの縦画面・横画面でも使えるようにしました。

ただし、時間が0秒になったときは数字が止まるだけです。教室の後ろから見ると、終了したのか、単に止まっているのかが少し分かりにくい状態でした。

そこで第3回では、タイマーが終了した瞬間にパネルを回転させ、裏面へタイマー名と「終了」を大きく表示します。試験中に使うことを考え、音は鳴らしません。

今回追加する機能

  • 0秒になったら、タイマーパネルをY軸方向へ回転する
  • 回転時間は約0.7秒にする
  • 裏面へ「タイマー名」と「終了」を中央揃えで表示する
  • 終了済みのパネル全体を薄暗くする
  • 終了後はリセット以外の操作をできなくする
  • 試験中の邪魔にならないよう、終了音は鳴らさない

今回は終了演出だけに絞ります。複数タイマー、一斉開始、個数設定などは次回以降に追加します。

前回のフォルダをコピーする

第2回で完成した内容を壊さないように、最初に「02」フォルダをコピーして「03」フォルダを作ります。

KITCHEN-TIMER/
├─ 01/
│  ├─ index.html
│  ├─ style.css
│  └─ script.js
├─ 02/
│  ├─ index.html
│  ├─ style.css
│  └─ script.js
└─ 03/
   ├─ index.html
   ├─ style.css
   └─ script.js

Visual Studio Codeでは、コピーした「03」フォルダ内の3ファイルを編集します。

前回の完成版を残したまま新しい番号フォルダで作業すると、途中で問題が起きても前回の状態へ戻せます。

終了画面の見た目を考える

終了したパネルは、動作中のパネルより目立たなくしたいと考えました。そこで、画像編集ソフトを使い、元の画面へ半透明のレイヤーを重ねて見え方を確認しました。

不透明度を70%程度にすると、元の時計は完全には消えないものの、かなり読みにくくなります。終了済みであることが見た目だけでも伝わるため、この明るさを採用しました。

【画像を挿入:画像編集ソフトで終了済みパネルの薄さを確認した画面】

裏面には、タイマー名と「終了」を上下2段で中央揃えにします。

模擬試験1
終了

index.htmlを変更する

これまでは、タイマーパネルの中に時計とボタンだけを置いていました。今回はパネルを表面と裏面に分けます。

表面には、これまでのタイマー名、時計、操作ボタンを置きます。裏面には、終了したタイマー名、「終了」、リセットボタンを置きます。

index.html のソースコードを表示
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>試験用キッチンタイマー</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="timer-page">
    <section class="timer-panel" aria-labelledby="timer-title">
      <div class="timer-panel-inner">
        <div class="timer-face timer-face-front">
          <button
            id="timer-title"
            class="timer-title"
            type="button"
            aria-label="タイマー名を変更"
          >
            タイマー1
          </button>

          <p id="error-message" class="error-message" aria-live="polite"></p>

          <div class="time-editor" aria-label="時間設定">
            <div class="time-unit">
              <button
                class="adjust-button"
                type="button"
                data-target="minutes"
                data-change="1"
                aria-label="分を1増やす"
              >▲</button>

              <button
                id="minutes-display"
                class="time-number"
                type="button"
                aria-label="分を直接入力"
              >05</button>

              <button
                class="adjust-button"
                type="button"
                data-target="minutes"
                data-change="-1"
                aria-label="分を1減らす"
              >▼</button>
            </div>

            <span class="time-colon" aria-hidden="true">:</span>

            <div class="time-unit">
              <button
                class="adjust-button"
                type="button"
                data-target="seconds"
                data-change="1"
                aria-label="秒を1増やす"
              >▲</button>

              <button
                id="seconds-display"
                class="time-number"
                type="button"
                aria-label="秒を直接入力"
              >00</button>

              <button
                class="adjust-button"
                type="button"
                data-target="seconds"
                data-change="-1"
                aria-label="秒を1減らす"
              >▼</button>
            </div>
          </div>

          <div class="buttons">
            <button id="start-button" type="button">▶ スタート</button>
            <button id="pause-button" type="button" disabled>Ⅱ 一時停止</button>
            <button id="reset-button" type="button">↻ リセット</button>
          </div>
        </div>

        <div class="timer-face timer-face-back" aria-live="polite">
          <div class="finished-message">
            <p id="finished-title" class="finished-title">タイマー1</p>
            <p class="finished-text">終了</p>
          </div>

          <button id="finished-reset-button" class="finished-reset-button" type="button">
            ↻ リセット
          </button>
        </div>
      </div>
    </section>
  </main>

  <script src="script.js"></script>
</body>
</html>

追加した主な部分

  • timer-panel-inner:表面と裏面をまとめて回転させる部分
  • timer-face-front:通常時に見える表面
  • timer-face-back:終了後に見える裏面
  • finished-title:終了したタイマー名を表示する部分
  • finished-reset-button:終了後に使用できるリセットボタン

表面と裏面は同じ場所へ重ねて配置します。回転前は表面、180度回転した後は裏面が見える仕組みです。

style.cssへ回転と終了画面のデザインを追加する

CSSでは、パネルを立体的に回転させる設定と、終了画面の文字サイズ、明るさ、配置を追加します。

style.css のソースコードを表示
* {
  box-sizing: border-box;
}

:root {
  color-scheme: dark;
  font-family:
    "Segoe UI",
    "Yu Gothic UI",
    "Meiryo",
    sans-serif;
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  background:
    radial-gradient(circle at center, #17243a 0%, #070b12 60%, #000000 100%);
  color: #f7f9ff;
}

button,
input {
  font: inherit;
}

button {
  -webkit-tap-highlight-color: transparent;
}

.timer-page {
  display: grid;
  min-height: 100vh;
  place-items: center;
  padding: 24px;
}

.timer-panel {
  width: min(1200px, 100%);
  perspective: 1800px;
  transition: opacity 0.35s ease;
}

.timer-panel-inner {
  display: grid;
  width: 100%;
  transform-style: preserve-3d;
  transition: transform 0.7s ease-in-out;
}

.timer-panel.is-finished {
  opacity: 0.7;
}

.timer-panel.is-finished .timer-panel-inner {
  transform: rotateY(180deg);
}

.timer-face {
  grid-area: 1 / 1;
  width: 100%;
  padding: clamp(20px, 3vw, 42px);
  border: 1px solid rgba(182, 211, 255, 0.55);
  border-radius: clamp(24px, 4vw, 56px);
  background:
    linear-gradient(
      145deg,
      rgba(32, 50, 78, 0.92),
      rgba(6, 12, 23, 0.96)
    );
  box-shadow:
    0 0 30px rgba(116, 168, 255, 0.18),
    inset 0 0 28px rgba(158, 200, 255, 0.08);
  text-align: center;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

.timer-face-front {
  position: relative;
}

.timer-face-back {
  display: grid;
  min-height: 100%;
  align-content: center;
  justify-items: center;
  gap: clamp(28px, 5vw, 60px);
  transform: rotateY(180deg);
}

.timer-title {
  max-width: 100%;
  margin: 0 auto clamp(8px, 1.6vw, 18px);
  padding: 4px 18px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  color: #eef5ff;
  font-size: clamp(2rem, 5vw, 4.8rem);
  font-weight: 700;
  line-height: 1.15;
  cursor: text;
  text-shadow: 0 0 16px rgba(159, 198, 255, 0.32);
  overflow-wrap: anywhere;
}

.timer-title:hover,
.timer-title:focus-visible {
  border-bottom-color: rgba(164, 202, 255, 0.7);
  outline: none;
}

.error-message {
  min-height: 1.5em;
  margin: 0;
  color: #ff9a9a;
  font-weight: 700;
}

.time-editor {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: clamp(4px, 1vw, 18px);
  width: 100%;
  margin: 0 auto clamp(20px, 4vw, 42px);
}

.time-unit {
  display: grid;
  min-width: 0;
  justify-items: center;
}

.time-number {
  width: 100%;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: #ffffff;
  font-family: Consolas, "Roboto Mono", "Courier New", monospace;
  font-size: clamp(6rem, 23vw, 20rem);
  font-weight: 700;
  line-height: 0.9;
  letter-spacing: -0.08em;
  cursor: text;
  text-shadow:
    0 0 12px rgba(255, 255, 255, 0.5),
    0 0 28px rgba(153, 194, 255, 0.2);
}

.time-number:hover,
.time-number:focus-visible {
  outline: none;
  text-shadow:
    0 0 16px rgba(255, 255, 255, 0.72),
    0 0 36px rgba(125, 177, 255, 0.34);
}

.time-colon {
  align-self: center;
  padding-bottom: clamp(18px, 4vw, 52px);
  font-family: Consolas, "Roboto Mono", "Courier New", monospace;
  font-size: clamp(5rem, 13vw, 13rem);
  font-weight: 700;
  line-height: 1;
  text-shadow: 0 0 16px rgba(255, 255, 255, 0.45);
}

.adjust-button {
  width: clamp(44px, 5vw, 64px);
  min-height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  color: #91a8c8;
  font-size: clamp(1rem, 2vw, 1.8rem);
  cursor: pointer;
  opacity: 0.72;
  transition:
    color 0.15s ease,
    opacity 0.15s ease,
    transform 0.15s ease;
}

.adjust-button:hover,
.adjust-button:focus-visible {
  color: #d9e8ff;
  opacity: 1;
  outline: none;
  transform: scale(1.12);
}

.buttons {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(10px, 1.8vw, 22px);
}

.buttons button,
.finished-reset-button {
  min-height: 64px;
  padding: 14px 18px;
  border: 1px solid rgba(151, 184, 227, 0.58);
  border-radius: 18px;
  background: rgba(21, 34, 54, 0.9);
  color: #eaf2ff;
  font-size: clamp(1rem, 2vw, 1.8rem);
  font-weight: 700;
  cursor: pointer;
  box-shadow: inset 0 0 16px rgba(122, 169, 236, 0.06);
}

#start-button {
  border-color: #84b6ff;
  background: linear-gradient(145deg, #376cff, #1541b8);
  box-shadow:
    0 0 22px rgba(71, 126, 255, 0.45),
    inset 0 0 12px rgba(255, 255, 255, 0.12);
}

#reset-button {
  color: #dce8fa;
}

.buttons button:hover:not(:disabled),
.buttons button:focus-visible:not(:disabled),
.finished-reset-button:hover,
.finished-reset-button:focus-visible {
  filter: brightness(1.14);
  outline: 3px solid rgba(155, 198, 255, 0.32);
  outline-offset: 3px;
}

.buttons button:disabled,
.adjust-button:disabled,
.time-number:disabled,
.timer-title:disabled {
  cursor: not-allowed;
  opacity: 0.35;
}

.timer-panel.is-running .adjust-button {
  visibility: hidden;
}

.finished-message {
  display: grid;
  justify-items: center;
  gap: clamp(12px, 2vw, 24px);
  width: 100%;
}

.finished-title,
.finished-text {
  max-width: 100%;
  margin: 0;
  color: #ffffff;
  font-weight: 700;
  line-height: 1.05;
  overflow-wrap: anywhere;
  text-shadow:
    0 0 16px rgba(255, 255, 255, 0.4),
    0 0 34px rgba(153, 194, 255, 0.24);
}

.finished-title {
  font-size: clamp(2.5rem, 7vw, 7rem);
}

.finished-text {
  font-size: clamp(5rem, 16vw, 15rem);
  letter-spacing: 0.08em;
}

.finished-reset-button {
  width: min(360px, 80%);
}

@media (max-width: 720px) {
  .timer-page {
    padding: 12px;
  }

  .timer-face {
    padding: 20px 14px;
    border-radius: 28px;
  }

  .time-editor {
    gap: 0;
  }

  .time-number {
    font-size: clamp(5rem, 28vw, 10rem);
  }

  .time-colon {
    padding-bottom: 28px;
    font-size: clamp(3.8rem, 13vw, 6rem);
  }

  .buttons {
    grid-template-columns: 1fr;
  }

  .buttons button {
    width: 100%;
    min-height: 56px;
  }

  .timer-face-back {
    gap: 28px;
  }

  .finished-title {
    font-size: clamp(2.2rem, 10vw, 4.5rem);
  }

  .finished-text {
    font-size: clamp(4.5rem, 24vw, 9rem);
  }
}

@media (max-height: 560px) and (orientation: landscape) {
  .timer-page {
    padding: 8px;
  }

  .timer-face {
    padding: 12px 22px;
  }

  .timer-title {
    margin-bottom: 0;
    font-size: clamp(1.5rem, 4vw, 2.5rem);
  }

  .error-message {
    min-height: 1.2em;
    font-size: 0.85rem;
  }

  .time-editor {
    margin-bottom: 8px;
  }

  .time-number {
    font-size: clamp(4rem, 17vw, 8rem);
  }

  .time-colon {
    padding-bottom: 24px;
    font-size: clamp(3rem, 9vw, 5rem);
  }

  .adjust-button {
    min-height: 30px;
  }

  .buttons {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .buttons button {
    min-height: 48px;
    padding: 8px;
    font-size: 1rem;
  }

  .timer-face-back {
    gap: 16px;
  }

  .finished-title {
    font-size: clamp(1.8rem, 5vw, 3.2rem);
  }

  .finished-text {
    font-size: clamp(4rem, 12vw, 7rem);
  }

  .finished-reset-button {
    min-height: 48px;
    padding: 8px;
    font-size: 1rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    scroll-behavior: auto !important;
    transition: none !important;
  }
}

今回初めて使った主なCSS

perspective

perspectiveは、回転する要素をどのくらい立体的に見せるかを指定します。数値が小さいほど遠近感が強くなり、数値が大きいほど平面的に見えます。今回は1800pxにしました。

.timer-panel {
  perspective: 1800px;
}

transform-style: preserve-3d

子要素を3D空間のまま扱うための設定です。表面と裏面を立体的に回転させるため、timer-panel-innerへ指定しています。

.timer-panel-inner {
  transform-style: preserve-3d;
}

backface-visibility: hidden

要素の裏側を見えなくする設定です。これを指定しないと、回転途中や回転後に、表面の文字が左右反転して見える場合があります。

.timer-face {
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

rotateY(180deg)

Y軸を中心に180度回転させます。is-finishedクラスが付いたときだけ、パネル全体が裏返ります。

.timer-panel.is-finished .timer-panel-inner {
  transform: rotateY(180deg);
}

transition: transform 0.7s

回転にかかる時間を0.7秒にしています。短すぎると一瞬で切り替わり、長すぎると待たされる感じがしたため、この速さにしました。

.timer-panel-inner {
  transition: transform 0.7s ease-in-out;
}

opacity: 0.7

終了後はパネル全体の不透明度を70%にします。時計が止まっただけではなく、終了済みのパネルであることを色でも判断しやすくします。

.timer-panel.is-finished {
  opacity: 0.7;
}

今回は不透明度70%を採用しました。今後、複数タイマーを並べたときに暗すぎる、または明るすぎると感じた場合は再調整します。

script.jsへ終了状態を追加する

JavaScriptでは、タイマーが終了したかどうかを記録する変数と、終了時に実行する処理を追加します。

script.js のソースコードを表示
const panel = document.querySelector(".timer-panel");
const titleButton = document.getElementById("timer-title");
const minutesDisplay = document.getElementById("minutes-display");
const secondsDisplay = document.getElementById("seconds-display");
const adjustButtons = document.querySelectorAll(".adjust-button");
const errorMessage = document.getElementById("error-message");

const startButton = document.getElementById("start-button");
const pauseButton = document.getElementById("pause-button");
const resetButton = document.getElementById("reset-button");

const finishedTitle = document.getElementById("finished-title");
const finishedResetButton = document.getElementById("finished-reset-button");

let selectedMinutes = 5;
let selectedSeconds = 0;

let timerId = null;
let endTime = 0;
let remainingMilliseconds = 0;
let isRunning = false;
let isPaused = false;
let isFinished = false;

function padNumber(value) {
  return String(value).padStart(2, "0");
}

function formatTime(milliseconds) {
  const totalSeconds = Math.max(0, Math.ceil(milliseconds / 1000));
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;

  return {
    minutes: padNumber(minutes),
    seconds: padNumber(seconds)
  };
}

function setError(message = "") {
  errorMessage.textContent = message;
}

function updateSelectedDisplay() {
  minutesDisplay.textContent = padNumber(selectedMinutes);
  secondsDisplay.textContent = padNumber(selectedSeconds);
}

function updateRunningDisplay(milliseconds) {
  const formatted = formatTime(milliseconds);
  minutesDisplay.textContent = formatted.minutes;
  secondsDisplay.textContent = formatted.seconds;
}

function setEditingEnabled(enabled) {
  titleButton.disabled = !enabled;
  minutesDisplay.disabled = !enabled;
  secondsDisplay.disabled = !enabled;

  adjustButtons.forEach((button) => {
    button.disabled = !enabled;
  });
}

function getSelectedMilliseconds() {
  if (selectedMinutes === 0 && selectedSeconds === 0) {
    setError("0分0秒では開始できません。");
    return null;
  }

  setError();
  return (selectedMinutes * 60 + selectedSeconds) * 1000;
}

function changeTime(target, change) {
  if (isRunning || isPaused || isFinished) {
    return;
  }

  if (target === "minutes") {
    selectedMinutes = Math.max(0, selectedMinutes + change);
  }

  if (target === "seconds") {
    selectedSeconds = Math.min(59, Math.max(0, selectedSeconds + change));
  }

  updateSelectedDisplay();
  setError();
}

function askForNumber(label, currentValue, minimum, maximum = null) {
  const value = window.prompt(
    `${label}を入力してください。`,
    String(currentValue)
  );

  if (value === null) {
    return null;
  }

  const number = Number(value);

  if (
    !Number.isInteger(number) ||
    number < minimum ||
    (maximum !== null && number > maximum)
  ) {
    const range =
      maximum === null
        ? `${minimum}以上の整数`
        : `${minimum}~${maximum}の整数`;

    setError(`${label}は${range}で入力してください。`);
    return null;
  }

  setError();
  return number;
}

function editMinutes() {
  if (isFinished) {
    return;
  }

  const value = askForNumber("分", selectedMinutes, 0);

  if (value === null) {
    return;
  }

  selectedMinutes = value;
  updateSelectedDisplay();
}

function editSeconds() {
  if (isFinished) {
    return;
  }

  const value = askForNumber("秒", selectedSeconds, 0, 59);

  if (value === null) {
    return;
  }

  selectedSeconds = value;
  updateSelectedDisplay();
}

function editTitle() {
  if (isFinished) {
    return;
  }

  const value = window.prompt(
    "タイマー名を入力してください。",
    titleButton.textContent.trim()
  );

  if (value === null) {
    return;
  }

  const normalizedTitle = value.trim();
  titleButton.textContent = normalizedTitle || "タイマー1";
}

function updateDisplay() {
  if (!isRunning) {
    return;
  }

  remainingMilliseconds = Math.max(0, endTime - Date.now());
  updateRunningDisplay(remainingMilliseconds);

  if (remainingMilliseconds <= 0) {
    finishTimer();
  }
}

function startTimer() {
  if (isRunning || isFinished) {
    return;
  }

  if (!isPaused) {
    const selectedMilliseconds = getSelectedMilliseconds();

    if (selectedMilliseconds === null) {
      return;
    }

    remainingMilliseconds = selectedMilliseconds;
  }

  endTime = Date.now() + remainingMilliseconds;
  isRunning = true;
  isPaused = false;

  panel.classList.add("is-running");
  setEditingEnabled(false);

  startButton.textContent = "▶ スタート";
  startButton.disabled = true;
  pauseButton.disabled = false;

  updateDisplay();
  timerId = window.setInterval(updateDisplay, 200);
}

function pauseTimer() {
  if (!isRunning || isFinished) {
    return;
  }

  remainingMilliseconds = Math.max(0, endTime - Date.now());

  window.clearInterval(timerId);
  timerId = null;

  isRunning = false;
  isPaused = true;

  panel.classList.remove("is-running");
  updateRunningDisplay(remainingMilliseconds);

  startButton.textContent = "▶ 再開";
  startButton.disabled = false;
  pauseButton.disabled = true;
}

function resetTimer() {
  window.clearInterval(timerId);
  timerId = null;

  isRunning = false;
  isPaused = false;
  isFinished = false;
  endTime = 0;
  remainingMilliseconds = 0;

  panel.classList.remove("is-running", "is-finished");
  setEditingEnabled(true);

  selectedMinutes = 5;
  selectedSeconds = 0;
  updateSelectedDisplay();

  startButton.textContent = "▶ スタート";
  startButton.disabled = false;
  pauseButton.disabled = true;

  setError();
}

function finishTimer() {
  window.clearInterval(timerId);
  timerId = null;

  isRunning = false;
  isPaused = false;
  isFinished = true;
  remainingMilliseconds = 0;

  panel.classList.remove("is-running");
  updateRunningDisplay(0);

  finishedTitle.textContent = titleButton.textContent.trim() || "タイマー1";

  startButton.disabled = true;
  pauseButton.disabled = true;
  resetButton.disabled = true;
  setEditingEnabled(false);

  panel.classList.add("is-finished");
}

adjustButtons.forEach((button) => {
  button.addEventListener("click", () => {
    const target = button.dataset.target;
    const change = Number(button.dataset.change);
    changeTime(target, change);
  });
});

titleButton.addEventListener("click", editTitle);
minutesDisplay.addEventListener("click", editMinutes);
secondsDisplay.addEventListener("click", editSeconds);

startButton.addEventListener("click", startTimer);
pauseButton.addEventListener("click", pauseTimer);
resetButton.addEventListener("click", resetTimer);
finishedResetButton.addEventListener("click", resetTimer);

updateSelectedDisplay();

今回初めて追加した主なJavaScript

isFinished

isFinishedは、タイマーが終了済みかどうかを記録する変数です。開始前と動作中はfalse、終了後はtrueにします。

let isFinished = false;

終了後にスタートや時間変更の処理が実行されないよう、各処理の最初でisFinishedを確認します。

if (isRunning || isFinished) {
  return;
}

returnが実行されると、その関数の処理をそこで終了します。終了済みの場合は、その後に書かれている開始処理へ進みません。

classList.add

classList.addは、HTML要素へCSSのクラスを追加する命令です。タイマー終了時にis-finishedクラスを追加すると、CSSで指定した回転と薄暗化が始まります。

panel.classList.add("is-finished");

classList.remove

classList.removeは、付いているクラスを削除する命令です。リセット時にis-finishedを削除すると、パネルが表面へ戻ります。

panel.classList.remove("is-running", "is-finished");

textContent

textContentは、要素の文字を読み取ったり、別の文字へ変更したりするときに使います。終了時は、表面のタイマー名を読み取り、裏面のfinished-titleへ入れます。

finishedTitle.textContent =
  titleButton.textContent.trim() || "タイマー1";

trim()は文字列の先頭と末尾にある余分な空白を取り除きます。タイマー名が空だった場合は、「タイマー1」を表示します。

終了時の処理

finishTimer関数では、タイマーを停止し、終了状態へ変更し、操作を無効化してから、パネルへis-finishedクラスを追加します。

function finishTimer() {
  window.clearInterval(timerId);
  timerId = null;

  isRunning = false;
  isPaused = false;
  isFinished = true;
  remainingMilliseconds = 0;

  panel.classList.remove("is-running");
  updateRunningDisplay(0);

  finishedTitle.textContent =
    titleButton.textContent.trim() || "タイマー1";

  startButton.disabled = true;
  pauseButton.disabled = true;
  resetButton.disabled = true;
  setEditingEnabled(false);

  panel.classList.add("is-finished");
}

JavaScriptは回転そのものを細かく描いているわけではありません。終了時にis-finishedクラスを追加し、実際の回転はCSSへ任せています。

裏面のリセットボタン

表面のリセットボタンは終了後に操作できなくなります。そのため、裏面に専用のリセットボタンを置き、同じresetTimer関数を実行させます。

finishedResetButton.addEventListener("click", resetTimer);

addEventListenerは、クリックなどの操作が発生したときに実行する処理を登録する命令です。今回は、裏面のリセットボタンがクリックされたらresetTimerを実行します。

動作を確認する

最初から5分待つ必要はないため、確認時は0分5秒など短い時間を設定します。

  1. タイマー名を「模擬試験1」などへ変更する
  2. 時間を0分5秒にする
  3. スタートを押す
  4. 0秒になったときにパネルが回転するか確認する
  5. 裏面へタイマー名と「終了」が表示されるか確認する
  6. パネル全体が薄暗くなっているか確認する
  7. リセット以外の操作ができないことを確認する
  8. リセットすると表面へ戻り、5分00秒になることを確認する

回転速度、文字の大きさ、終了後の明るさは、実際に何度か動かしながら確認しました。今回は最初の状態で十分見やすく、追加の修正は必要ありませんでした。

スマートフォンでも確認する

第2回でスマートフォン対応を行っているため、今回追加した終了画面も縦画面と横画面の両方で確認します。

縦画面では、タイマー名、「終了」、リセットボタンが中央へまとまりました。文字切れや画面外へのはみ出しもありません。

横画面でも、タイマー名と「終了」が大きく表示され、リセットボタンを操作できました。

今回の主な利用環境はパソコンと教室の大型画面です。スマートフォンは補助用途ですが、縦・横のどちらでも表示が崩れず操作できることを確認しました。

Xserverへ第3回の完成版を公開する

ローカルで問題なく動いたら、Xserverのファイルマネージャーへ第3回の3ファイルをアップロードします。

/public_html/tools/kitchen-timer/03/
├─ index.html
├─ style.css
└─ script.js

公開後は、次のURLをブラウザで開いて動作を確認します。

https://nonbiri-lab.jp/tools/kitchen-timer/03/
  • パソコンでページが開くか
  • 時間を設定して開始できるか
  • 0秒で回転するか
  • 終了画面が表示されるか
  • リセットで初期状態へ戻るか
  • スマートフォンでも表示できるか

第3回の完成版

今回作成した第3回の完成版を、下へ埋め込みます。

第3回の完成版を別画面で開く

記事内では画面の高さに制限があります。大きな画面で確認したい場合は「第3回の完成版を別画面で開く」を利用してください。

今回のまとめ

第3回では、タイマー終了時の演出を追加しました。

  • 0秒でパネルが約0.7秒かけて回転する
  • 裏面へタイマー名と「終了」を表示する
  • 終了済みのパネル全体を薄暗くする
  • 試験中に使うため音は鳴らさない
  • 終了後はリセット以外の操作をできなくする
  • パソコンとスマートフォンの縦・横画面で動作を確認する

数字が0秒で止まるだけだった第2回と比べて、離れた場所からでも終了したことが分かりやすくなりました。

また、終了したパネルを薄くする仕組みは、次回以降にタイマーを複数並べたときにも役立ちます。

次回は複数タイマーへ

ここまで単体のタイマーを作ってきましたが、実際の模擬試験ではタイマーが1個だけでは足りません。

次回は、複数のタイマーを上段と下段へ並べ、上段、下段、全体をまとめて開始できる構成へ進みます。