<!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8"> <title>CodePen - Alphabet Speed Test </title> <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet"> <style> *, *::before, *::after { box-sizing: border-box; } html { font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } body { align-items: center; background-color: #b24592; background-image: linear-gradient(166deg, blueviolet, darkcyan); display: flex; justify-content: center; margin: 0; min-height: 100vh; padding: 0; } .container { background-color: rgba(255, 255, 255, 0.25); border-radius: 8px; box-shadow: 0 1px 6px rgba(0, 0, 0, 0.15); max-width: 700px; padding: 2rem 3rem; width: 90%; } .game-container[data-state="pre-game"] .timer { opacity: 0; } .game-container[data-state="pre-game"] .restart-button, .game-container[data-state="pre-game"] .restart-button:hover, .game-container[data-state="pre-game"] .restart-button:focus { background-color: #fff; color: #333; opacity: .6; } .highscore-container { font-size: .9rem; margin: 1rem 0; } .timer { margin: 1rem 0; transition: opacity .1s ease-in-out; } .letter__container { display: flex; flex-wrap: wrap; margin-left: -.25rem; width: calc(100% + .5rem); } .letter { --background-color: rgba(255, 255, 255, .4); background-color: var(--background-color); border-radius: 3px; color: #333; display: block; font-size: 1.5rem; height: 2em; line-height: 2em; margin: .25rem; overflow: hidden; position: relative; text-align: center; text-transform: uppercase; width: 2em; } .letter[data-state=active]::before { transform: scaleX(1); } .letter::before { background-color: #fff; bottom: 0; content: ''; left: 0; position: absolute; right: 0; top: 0; transform: scaleX(0); transform-origin: 0 center; transition: transform .1s ease-in-out; } .letter::after { content: attr(data-letter); position: relative; } .letter--template { display: none; } .restart-button__container { margin-top: 2rem; text-align: center; } .restart-button { background-color: #fff; border: 0; border-radius: 6px; color: #333; cursor: pointer; font-family: inherit; font-size: 1rem; padding: .5rem 2rem; transition: background-color .15s ease-in-out; } .restart-button:hover, .restart-button:focus { background-color: #f15f79; color: #ffffff; } </style> </head> <body> <div class="container"> <div class="game-container" data-state="pre-game"> <h1 class="title"></h1> <div class="highscore-container"></div> <div class="letter__container"> <div class="letter letter--template" data-state="inactive"></div> </div> <div class="restart-button__container"> <button class="restart-button" type="button">再玩一次</button> </div> </div> </div> <script> const GameState = { PRE_GAME: 'pre-game', RUNNING: 'running', POST_GAME: 'post-game' }; const LetterState = { ACTIVE: 'active', INACTIVE: 'inactive' }; class SpeedTest { constructor(gameContainer, letterContainer) { this.letters = []; this.remainingLetters = []; this.state = GameState.PRE_GAME; this.timer = null; this.gameContainer = gameContainer; this.letterContainer = letterContainer; this.highscore = Infinity; this.onKeyDown = this.onKeyDown.bind(this); this.restartGame = this.restartGame.bind(this); }; init(restart) { const availableLanguages = ['en', 'nb', 'nn', 'no']; const preferredLanguage = SpeedTest.getPreferredLanguage(availableLanguages); this.letters = SpeedTest.getLetters(preferredLanguage); this.remainingLetters = this.letters; this.highscore = this.getHighscore(); if (this.highscore && this.highscore !== Infinity) { this.updateHighscoreText(this.highscore); } const letterTemplate = this.letterContainer.querySelector( '.letter--template' ); this.setTitle('按键盘上的A键启动,按空格键重新启动'); this.renderLetters(letterTemplate); if (!restart) { this.addEventListeners(); this.getHighscore(); } }; static getPreferredLanguage(langs) { const defaultLang = 'en'; const navgLangs = navigator.languages; const preferredLang = navgLangs.filter(lang => langs.indexOf(lang) > -1)[0]; return preferredLang || defaultLang; }; static getLetters(preferredLang) { let letters = []; switch (preferredLang) { case 'nb': case 'nn': case 'no': letters = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'æ', 'ø', 'å' ]; break; default: letters = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' ]; break; } return letters; }; renderLetters(template) { let letterElement; this.letters.forEach((letter, index) => { letterElement = template.cloneNode(true); letterElement.classList.remove('letter--template'); letterElement.dataset.letter = letter; letterElement.dataset.index = index; this.letterContainer.appendChild(letterElement); }); // this.letterContainer.removeChild(template); }; addEventListeners() { document.addEventListener('keydown', this.onKeyDown, false); const restartButton = this.gameContainer.querySelector('.restart-button'); restartButton.addEventListener('click', this.restartGame, false); }; onKeyDown(event) { if (event.key === ' ') { this.restartGame(); } else if (this.remainingLetters[0].toUpperCase() === event.key.toUpperCase()) { if (this.state === GameState.PRE_GAME) { this.startGame(); } this.updateLetter(this.remainingLetters[0], LetterState.ACTIVE); this.remainingLetters = this.remainingLetters.slice(1); if (this.remainingLetters.length === 0) { this.stopGame(); } else if (this.remainingLetters.length < this.letters.length / 5) { this.setTitle('只剩下几个了,继续!'); } else if (this.remainingLetters.length < this.letters.length / 2) { this.setTitle('过半了!'); } } else { const nextLetterElement = this.letterContainer.querySelector( `[data-state=${LetterState.INACTIVE}]` ); this.wrongLetter(nextLetterElement); } }; startGame() { this.gameContainer.dataset.state = GameState.RUNNING; this.state = GameState.RUNNING; this.setTitle('GO GO GO'); this.startTimer(); }; stopGame() { const t2 = this.stopTimer(); const t1 = this.timer; this.gameContainer.dataset.state = GameState.POST_GAME; this.state = GameState.POST_GAME; let time = t2 - t1; time /= 1000; const timeString = this.formatTime(time); let newTitle = `您完成了! ✨ 时间是 ${timeString} 秒!`; if (this.highscore === null || time < this.highscore) { newTitle += ' 新纪录!'; this.highscore = time; this.updateHighscoreText(time); this.setHighscore(time); } this.setTitle(newTitle); }; restartGame() { this.state = GameState.PRE_GAME; this.gameContainer.dataset.state = GameState.PRE_GAME; const letterElements = this.letterContainer.querySelectorAll('[data-letter]'); for (let i = 0; i < letterElements.length; i++) { this.letterContainer.removeChild(letterElements[i]); } this.init(); }; startTimer() { this.timer = performance.now(); }; stopTimer() { return performance.now(); }; wrongLetter(element) { if (!('animate' in element)) { return; } const styles = getComputedStyle(element); const backgroundColor = styles.getPropertyValue('--background-color'); const errorRed = '#f15f79'; const blinkAnimation = [ { ['--background-color']: backgroundColor }, { ['--background-color']: errorRed }, { ['--background-color']: backgroundColor } ]; const blinkTiming = { duration: 100, iterations: 1 }; element.animate(blinkAnimation, blinkTiming); }; setTitle(text) { const title = this.gameContainer.querySelector('.title'); title.innerText = text; }; updateLetter(letter, state) { const element = this.letterContainer.querySelector( `[data-letter=${letter}]` ); element.dataset.state = state; }; getHighscore() { let highscore = Infinity; if ('localStorage' in window) { highscore = window.localStorage.getItem('highscore'); } if (highscore) { highscore = parseFloat(highscore); } else { highscore = Infinity; } return highscore; }; setHighscore(highscore) { if ('localStorage' in window) { window.localStorage.setItem('highscore', highscore); } }; updateHighscoreText(newHighscore) { const highscoreContainer = this.gameContainer.querySelector( '.highscore-container' ); highscoreContainer.innerText = `个人最佳: ${this.formatTime(newHighscore)} 秒`; }; formatTime(time) { return time.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } } const gameContainer = document.querySelector('.game-container'); const letterContainer = document.querySelector('.letter__container'); const game = new SpeedTest(gameContainer, letterContainer); game.init(); </script> </body> </html>