Skip to content

Project 04: 掐秒表 (JS)

完整代码

html
<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="./style.css">
  <script src="./index.js"></script>
  <title>Stopwatch Game</title>
</head>

<body>
  <h1>掐秒表</h1>
  <div class="container">
    <p>你的目标是:<span id="target">00.0000</span></p>
    <p id="timer">00.0000</p>
    <div class="buttons">
      <button id="start" class="operate-button">开始</button>
      <button id="stop" class="operate-button">停止</button>
      <button id="reset" class="operate-button">重置</button>
    </div>
    <div id="result" style="visibility: hidden;">
      <div><span id="diff"></span>s</div>
      <div><span id="grade"></span> Level</div>
    </div>
    <div id="history"></div>
  </div>
</body>

</html>
css
body {
    background-color: khaki;
    padding: 0.5rem 1rem;
}

h1 {
    text-align: center;
}

.container {
    text-align: center;
}

#timer {
    font-size: 3rem;
}

.buttons {
    display: flex;
    justify-content: center;
    gap: 1rem;
}

.operate-button {
    font-size: 1.5rem;
    padding: 0.25rem 0.5rem;
    background-color: skyblue;
    border-radius: 0.5rem;
}

#result {
    margin: 2rem auto;
    width: 16rem;
    font-size: 1.25rem;
    background-color: darkkhaki;
    border-radius: 1rem;
    color: white;
    padding: 1rem 1rem;
}

#diff,
#grade {
    font-size: 2rem;
    font-weight: bold;
    font-family: numberonly, monospace, sans-serif;
}

#history {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    justify-content: center;
    max-width: 30rem;
    margin: 0 auto;
    gap: 0.75rem;
}

#history>div {
    font-weight: bold;
    background-color: darkkhaki;
    padding: 0.25rem 0.5rem;
    border-radius: 1rem;
}
javascript
let startTimestamp = performance.now();
let nowTimestamp = performance.now();
let running = false;
let handle = 0;
const target = 1000 * 5;
const maxHistoryItems = 15;

const grades = [
  { diff: 2.00, grade: "E", color: "gray" },
  { diff: 1.00, grade: "D", color: "green" },
  { diff: 0.50, grade: "D+", color: "green" },
  { diff: 0.30, grade: "C-", color: "blue" },
  { diff: 0.20, grade: "C", color: "blue" },
  { diff: 0.15, grade: "C+", color: "blue" },
  { diff: 0.13, grade: "B-", color: "red" },
  { diff: 0.11, grade: "B", color: "red" },
  { diff: 0.10, grade: "B+", color: "red" },
  { diff: 0.08, grade: "A", color: "purple" },
  { diff: 0.06, grade: "A+", color: "purple" },
  { diff: 0.05, grade: "S-", color: "aquamarine" },
  { diff: 0.04, grade: "S", color: "aquamarine" },
  { diff: 0.03, grade: "S+", color: "aquamarine" },
  { diff: 0.02, grade: "SS", color: "yellow" },
  { diff: 0.015, grade: "SS+", color: "yellow" },
  { diff: 0.01, grade: "SSS", color: "yellow" },
  { diff: 0.005, grade: "U", color: "deeppink" },
  { diff: 0.002, grade: "U+", color: "deeppink" },
  { diff: 0.0005, grade: "U++", color: "deeppink" },
].reverse();

const historyRecord = JSON.parse(localStorage.getItem("SG_history") ?? "[]");

function format_time(totalMs) {
  const s = totalMs / 1000;
  return s < 10 ? ("0" + s.toFixed(4)) : s.toFixed(4);
}

function setTimestamp(timestamp) {
  nowTimestamp = Math.max(timestamp, startTimestamp);
  document.querySelector("#timer").textContent = format_time(nowTimestamp - startTimestamp);
  handle = requestAnimationFrame(setTimestamp);
}

function start() {
  if (running) {
    return;
  }
  running = true;
  document.querySelector("#result").style.visibility = "hidden";
  startTimestamp = performance.now();
  handle = requestAnimationFrame(setTimestamp);
}

function gen_history_element(grade) {
  const el = document.createElement("div");
  el.textContent = grade;
  el.style.color = grades.find(g => g.grade === grade).color;
  return el;
}

function add_history(grade) {
  historyRecord.push({ grade });
  const elHistory = document.querySelector("#history");
  if (historyRecord.length > maxHistoryItems) {
    historyRecord.shift();
    elHistory.removeChild(elHistory.lastChild);
  }
  localStorage.setItem("SG_history", JSON.stringify(historyRecord));
  elHistory.insertBefore(gen_history_element(grade), elHistory.firstChild);
}

function stop() {
  if (!running) {
    return;
  }
  running = false;
  cancelAnimationFrame(handle);
  setTimestamp(performance.now());
  cancelAnimationFrame(handle);
  const diff = Math.abs(nowTimestamp - startTimestamp - target) / 1000;
  const grade = grades.find(grade => grade.diff > diff + 1e-5) ?? grades[grades.length - 1];
  document.querySelector("#result").style.visibility = "visible";
  document.querySelector("#diff").textContent = (nowTimestamp - startTimestamp >= target ? "+" : "-") + format_time(diff * 1000);
  document.querySelector("#grade").textContent = grade.grade;
  document.querySelector("#diff").style.color = grade.color;
  document.querySelector("#grade").style.color = grade.color;
  add_history(grade.grade);
}

function reset() {
  startTimestamp = performance.now();
  nowTimestamp = startTimestamp;
  document.querySelector("#timer").textContent = format_time(0);
  if (running) {
    stop();
  }
  document.querySelector("#result").style.visibility = "hidden";
}

window.onload = () => {
  const attachments = [
    { id: "start", func: start },
    { id: "stop", func: stop },
    { id: "reset", func: reset },
  ];
  attachments.forEach((attachment) => {
    document.querySelector(`#${attachment.id}`).addEventListener('click', attachment.func);
  });
  document.querySelector("#target").textContent = format_time(target);
  historyRecord.forEach(grade => {
    const elHistory = document.querySelector("#history");
    elHistory.insertBefore(gen_history_element(grade.grade), elHistory.firstChild);
  });
}

Built by Vitepress | Apache 2.0 Licensed