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);
});
}