├── favicon.png ├── favicon1.png ├── index.html ├── chart.js ├── style.css └── typing.js /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhijeetSinghRajput/TypingGuru/HEAD/favicon.png -------------------------------------------------------------------------------- /favicon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhijeetSinghRajput/TypingGuru/HEAD/favicon1.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | monkey type 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 | wpm: 21 | 80 22 |
23 |
24 | accuracy: 25 | 96% 26 |
27 |
28 |
29 | 30 | 31 |
32 |
33 |
34 | 35 |
36 |
37 |
30
38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /chart.js: -------------------------------------------------------------------------------- 1 | const DATA_COUNT = 6; 2 | let labels = []; 3 | for (let i = 0; i < DATA_COUNT; ++i) { 4 | labels.push(i.toString()); 5 | } 6 | let wpmData = [0, 20, 20, 60, 60, 80]; 7 | let rawData = [50, 10, 16, 70, 50, 20]; 8 | 9 | 10 | const [PRIMARY_COLOR, SECONDARY_COLOR, DANGER_COLOR] = ['#e6db74', '#a6e22e', '#f92672']; 11 | const data = { 12 | labels: labels, 13 | datasets: [ 14 | { 15 | label: 'WPM', 16 | data: wpmData, 17 | borderColor: SECONDARY_COLOR, 18 | pointBackgroundColor: SECONDARY_COLOR, 19 | fill: true, 20 | cubicInterpolationMode: 'monotone', 21 | tension: 0.4, 22 | yAxisID: 'y', 23 | }, 24 | { 25 | label: 'Raw', 26 | data: rawData, 27 | borderColor: PRIMARY_COLOR, 28 | pointBackgroundColor: PRIMARY_COLOR, 29 | fill: true, 30 | cubicInterpolationMode: 'monotone', 31 | tension: 0.4, 32 | yAxisID: 'y', 33 | }, 34 | { 35 | label: 'Errors', 36 | data: [5, 1, 1, 7, 5, 2], 37 | borderColor: DANGER_COLOR, 38 | pointBackgroundColor: DANGER_COLOR, 39 | fill: false, 40 | cubicInterpolationMode: 'monotone', 41 | tension: 0.4, 42 | showLine: false, 43 | pointStyle: 'crossRot', 44 | yAxisID: 'y1', 45 | pointRadius: 5, 46 | pointBorderWidth: 2, 47 | }, 48 | ] 49 | }; 50 | 51 | 52 | const config = { 53 | type: 'line', 54 | data: data, 55 | options: { 56 | maintainAspectRatio: false, 57 | responsive: true, 58 | interaction: { 59 | intersect: false, 60 | }, 61 | scales: { 62 | x: { 63 | display: true, 64 | ticks: { 65 | color: PRIMARY_COLOR, 66 | font: { 67 | weight: 'bold', 68 | }, 69 | }, 70 | grid: { 71 | color: '#1d1d18', 72 | }, 73 | }, 74 | y: { 75 | display: true, 76 | title: { 77 | display: true, 78 | text: 'Words per minute', 79 | color: PRIMARY_COLOR, 80 | font: { 81 | weight: 'bold', 82 | } 83 | }, 84 | suggestedMin: 0, 85 | ticks: { 86 | color: PRIMARY_COLOR, 87 | font: { 88 | weight: 'bold', 89 | } 90 | }, 91 | grid: { 92 | color: '#1d1d18', 93 | }, 94 | }, 95 | y1: { 96 | type: 'linear', 97 | position: 'right', 98 | display: true, 99 | title: { 100 | display: true, 101 | text: 'Errors', 102 | color: PRIMARY_COLOR, 103 | font: { 104 | weight: 'bold', 105 | } 106 | }, 107 | suggestedMin: 0, 108 | ticks: { 109 | stepSize: 1, 110 | }, 111 | grid: { 112 | display: false, 113 | }, 114 | ticks: { 115 | color: PRIMARY_COLOR, 116 | font: { 117 | weight: 'bold', 118 | } 119 | } 120 | } 121 | }, 122 | plugins: { 123 | legend: { 124 | display: false 125 | }, 126 | tooltip: { 127 | mode: 'index', 128 | } 129 | } 130 | } 131 | }; 132 | 133 | 134 | // Get the 2D context of the canvas and create the chart 135 | const ctx = document.getElementById('myChart').getContext('2d'); 136 | 137 | const myChart = new Chart(ctx, config); 138 | 139 | 140 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-family: monospace; 6 | list-style: none; 7 | text-decoration: none; 8 | user-select: none; 9 | } 10 | 11 | :root { 12 | --bg-dark-color: #272822; 13 | --primary-color: #e6db74; 14 | --secondary-color: #a6e22e; 15 | --danger-color: #f92672; 16 | --white-color: #e2e2dc; 17 | --cursor-color: #66d9ef; 18 | } 19 | .container{ 20 | max-width: 900px; 21 | margin: auto; 22 | /* border: 1px solid white; */ 23 | height: 100%; 24 | } 25 | body { 26 | width: 100vw; 27 | height: 100svh; 28 | background-color: var(--bg-dark-color); 29 | color: var(--primary-color); 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-direction: column; 34 | } 35 | 36 | 37 | .statistics{ 38 | padding-top: 20vh; 39 | position: fixed; 40 | top: 0; 41 | left: 0; 42 | width: 100%; 43 | height: 100%; 44 | background-color: var(--bg-dark-color); 45 | z-index: 100; 46 | display: none; 47 | } 48 | .statistics.active{ 49 | display: block; 50 | } 51 | .statistics .result{ 52 | display: flex; 53 | align-items: center; 54 | justify-content: space-between; 55 | padding: 30px; 56 | font-size: 30px; 57 | } 58 | .statistics .result > div{ 59 | display: flex; 60 | align-items: center; 61 | gap: 8px; 62 | } 63 | .statistics .result .value{ 64 | color: var(--secondary-color); 65 | font-size: 40px; 66 | } 67 | .btn-wrapper{ 68 | display: flex; 69 | align-items: center; 70 | justify-content: center; 71 | margin-top: 40px; 72 | gap: 20px; 73 | } 74 | .btn{ 75 | cursor: pointer; 76 | padding: 8px 16px; 77 | border-radius: 3px; 78 | outline: none; 79 | border: none; 80 | background-color: var(--primary-color); 81 | background-color: transparent; 82 | color: var(--primary-color); 83 | border: 1px solid white; 84 | font-size: 16px; 85 | text-transform: capitalize; 86 | min-width: 100px; 87 | } 88 | .btn:hover{ 89 | background-color: var(--primary-color); 90 | color: var(--bg-dark-color); 91 | } 92 | .chart{ 93 | grid-area: chart; 94 | width: 100%; 95 | max-height: 200px; 96 | height: 200px; 97 | } 98 | canvas{ 99 | display: block; 100 | /* object-fit: contain; */ 101 | height: 100%; 102 | width: 100% !important; 103 | } 104 | .container > .wrapper{ 105 | margin-top: 16vh; 106 | } 107 | 108 | #time { 109 | font-size: 30px; 110 | margin-bottom: 10px; 111 | } 112 | 113 | #main { 114 | position: relative; 115 | border: none; 116 | outline: none; 117 | } 118 | #main input{ 119 | height: 0; 120 | width: 0; 121 | position: absolute; 122 | border: none; 123 | } 124 | #cursor { 125 | visibility: hidden; 126 | position: absolute; 127 | width: 3px; 128 | background-color: var(--primary-color); 129 | background-color: var(--cursor-color); 130 | top: 4px; 131 | left: 0px; 132 | /* animation: blink 1s infinite; */ 133 | transition: .1s; 134 | transform-origin: top left; 135 | border-radius: 10px; 136 | } 137 | 138 | 139 | @keyframes blink { 140 | 141 | 0%, 142 | 100% { 143 | opacity: 0; 144 | } 145 | 146 | 50% { 147 | opacity: 1; 148 | } 149 | } 150 | 151 | .heading { 152 | display: flex; 153 | align-items: center; 154 | justify-content: space-between; 155 | /* border: 1px solid white; */ 156 | font-size: 30px; 157 | margin-bottom: 20px; 158 | } 159 | 160 | 161 | #words{ 162 | /* border: 1px solid white; */ 163 | width: 90vw; 164 | max-width: 100%; 165 | display: flex; 166 | flex-wrap: wrap; 167 | align-content: flex-start; 168 | overflow: hidden; 169 | } 170 | 171 | .word{ 172 | display: flex; 173 | margin: 4px; 174 | } 175 | 176 | letter { 177 | font-size: 30px; 178 | } 179 | 180 | letter.correct { 181 | color: var(--white-color); 182 | } 183 | 184 | letter.incorrect { 185 | color: var(--danger-color); 186 | } 187 | 188 | @media screen and (max-width: 550px) { 189 | .statistics .result { 190 | margin-top: 20px; 191 | padding: 20px; 192 | font-size: 22px; 193 | } 194 | .statistics .result .value{ 195 | font-size: 30px; 196 | } 197 | } -------------------------------------------------------------------------------- /typing.js: -------------------------------------------------------------------------------- 1 | const texts = [...new Set("When I was in my 5th semester watching a Harvard University lecture on Artificial Intelligence I got know how search tree work in board games and decision making I then created a tic-tac-toe game in a single day This experience ignited a spark in me; an idea took root that I could create a chess engine so advanced that it would be challenging even for seasoned players From that day forward I embarked on a journey into the realm of chess programming diving into a plethora of documents research papers and resources to make this vision a reality".split(' '))]; 2 | 3 | // html elements 4 | const wordWrapper = document.getElementById('words'); 5 | const main = document.getElementById('main'); 6 | const cursor = document.getElementById('cursor'); 7 | const time = document.getElementById('time'); // textContent = 30 8 | const statistics = document.querySelector('.statistics'); 9 | const wpm = document.querySelector('.wpm .value'); 10 | const accuracy = document.querySelector('.accuracy .value'); 11 | const hiddenInput = document.getElementById('hiddenInput'); 12 | const LINES = 3; 13 | let debugMode = false; 14 | 15 | let totalTypedChar = 0; 16 | let totalCorrectTypedChar = 0; 17 | let totalTypedWords = 0; 18 | let correctWordChar = 0; 19 | let incorrectWordChar = 0; 20 | 21 | 22 | let currentWord, currentLetter; 23 | let letterWidth, lineHeight; 24 | let isStart = false; 25 | let startTime = null; 26 | 27 | main.onfocus = () => { 28 | hiddenInput.focus(); 29 | } 30 | hiddenInput.onfocus = () => { 31 | cursor.style.visibility = 'visible'; 32 | } 33 | hiddenInput.onblur = () => { 34 | cursor.style.visibility = 'hidden'; 35 | } 36 | newGame(); 37 | function newGame() { 38 | statistics.classList.remove('active'); 39 | currentLetter = null; 40 | currentWord = null; 41 | 42 | renderWords(200); 43 | 44 | cursor.style.top = currentLetter.offsetTop + 'px'; 45 | cursor.style.left = currentLetter.offsetLeft + 'px'; 46 | } 47 | 48 | let intervalId; 49 | function start() { 50 | wpmData = []; 51 | rawData = []; 52 | 53 | isStart = true; 54 | startTime = Date.now(); 55 | intervalId = setInterval(() => { 56 | calculateTypingStats(); 57 | time.textContent -= 1; 58 | if (time.textContent === '0') terminate(); 59 | }, 1000); 60 | } 61 | 62 | function terminate() { 63 | console.log('over'); 64 | clearInterval(intervalId); 65 | labels = Array.from({ length: wpmData.length }, (_, i) => i); 66 | isStart = false; 67 | newGame(); 68 | time.textContent = '30'; 69 | myChart.data.datasets[0].data = wpmData; 70 | myChart.data.datasets[1].data = rawData; 71 | myChart.data.datasets[2].data = []; // errors 72 | myChart.data.labels = labels; 73 | myChart.update(); 74 | statistics.classList.add('active'); 75 | wpm.textContent = wpmData[wpmData.length - 1]; 76 | accuracy.textContent = Math.round((totalCorrectTypedChar / totalTypedChar) * 100); 77 | } 78 | 79 | function getRandomWord() { 80 | let randomIndex = Math.floor(Math.random() * texts.length); 81 | return texts[randomIndex]; 82 | } 83 | 84 | function renderWords(length) { 85 | function letterFragments(word) { 86 | return word.split('').map(letter => { 87 | return `${letter}` 88 | }) 89 | } 90 | 91 | let htmlFragments = ''; 92 | for (let i = 0; i < length; ++i) { 93 | const word = getRandomWord(); 94 | htmlFragments += ` 95 |
96 | ${letterFragments(word).join('')} 97 |
`; 98 | } 99 | 100 | wordWrapper.innerHTML = htmlFragments; 101 | document.querySelector('.word:last-child').innerHTML += '.'; 102 | 103 | // Initializing variables 104 | currentWord = document.querySelector('.word'); 105 | currentLetter = document.querySelector('letter'); 106 | 107 | letterWidth = currentLetter.offsetWidth; 108 | cursor.style.height = currentWord.offsetHeight + 'px'; 109 | lineHeight = 8 + currentWord.offsetHeight; 110 | wordWrapper.style.height = (LINES * lineHeight) + 'px'; 111 | } 112 | 113 | window.addEventListener('resize', () => { 114 | if (currentLetter) { 115 | cursor.style.top = currentLetter.offsetTop + 'px'; 116 | cursor.style.left = currentLetter.offsetLeft + 'px'; 117 | } 118 | else { 119 | cursor.style.top = currentWord.lastElementChild.offsetTop + 'px'; 120 | cursor.style.left = currentWord.lastElementChild.offsetLeft + currentWord.lastElementChild.offsetWidth + 'px'; 121 | } 122 | 123 | letterWidth = currentWord.firstElementChild.offsetWidth; 124 | }) 125 | 126 | 127 | main.addEventListener('keyup', ({ key, ctrlKey }) => { 128 | const expected = currentLetter ? currentLetter.textContent : ' '; 129 | const isLetter = key.length === 1 && key !== ' '; 130 | const isSpace = key === ' '; 131 | const isBackspace = key === 'Backspace'; 132 | 133 | if (!isStart && !debugMode) { 134 | start(); 135 | } 136 | if (key == expected) { 137 | totalCorrectTypedChar++; 138 | } 139 | 140 | // !currentLetter indicated the end of word 141 | if (isLetter) { 142 | if (!currentLetter) { 143 | const extraLetter = document.createElement('letter'); 144 | extraLetter.className = 'incorrect extra'; 145 | extraLetter.textContent = key; 146 | currentWord.appendChild(extraLetter); 147 | } 148 | else { 149 | currentLetter.className = (key === expected) ? 'correct' : 'incorrect'; 150 | currentLetter = currentLetter.nextElementSibling; 151 | } 152 | totalTypedChar++; 153 | } 154 | //handling space 155 | else if (isSpace) { 156 | let temp = currentLetter; 157 | while (temp) { 158 | temp.className = 'incorrect'; 159 | temp = temp.nextElementSibling; 160 | } 161 | validateCurrentWord(); 162 | 163 | currentWord = currentWord.nextElementSibling; 164 | if (!currentWord) { 165 | terminate(); 166 | return; 167 | } 168 | currentLetter = currentWord.firstElementChild; 169 | totalTypedChar++; 170 | totalTypedWords++; 171 | } 172 | //backspace 173 | handleBackspace(isBackspace, ctrlKey); 174 | 175 | // move the cursor 176 | if (currentLetter) { 177 | cursor.style.top = currentLetter.offsetTop + 'px'; 178 | cursor.style.left = currentLetter.offsetLeft + 'px'; 179 | } 180 | else if (isLetter) { 181 | // place cursor at the end of the word 182 | let x = currentWord.lastElementChild.offsetLeft; 183 | let y = currentWord.lastElementChild.offsetTop; 184 | cursor.style.left = x + letterWidth + 'px'; 185 | cursor.style.top = y + 'px'; 186 | } 187 | 188 | // remove the first line 189 | const lastLine = (LINES - 1) * lineHeight; 190 | if (parseInt(cursor.style.top) > lastLine) { 191 | //clear the first line 192 | const wordsOnFirstLine = [...wordWrapper.children].filter(child => child.offsetTop === 4); 193 | wordsOnFirstLine.forEach(w => w.remove()); 194 | 195 | cursor.style.top = currentLetter.offsetTop + 'px'; 196 | } 197 | 198 | // end of line (temporary new game) 199 | if (isGameOver()) { 200 | totalTypedWords++; 201 | debugMode? newGame() : terminate(); 202 | } 203 | }) 204 | 205 | function handleBackspace(isBackspace, ctrlKey) { 206 | if (!isBackspace) return; 207 | 208 | //EOW then curren is last letter of current word 209 | if (!currentLetter) { 210 | currentLetter = currentWord.lastElementChild; 211 | } 212 | else { 213 | const prevWord = currentWord.previousElementSibling; 214 | const prevLetter = currentLetter.previousElementSibling; 215 | 216 | if (prevLetter) { 217 | currentLetter = prevLetter; 218 | } 219 | //at the beginning of current word (move to the previous word. if any) 220 | else if (prevWord) { 221 | currentWord = prevWord; 222 | currentLetter = prevWord.lastElementChild; 223 | } 224 | } 225 | 226 | //erase the extra incorrect word 227 | if (currentLetter.classList.contains('extra')) { 228 | currentLetter.remove(); 229 | currentLetter = null; 230 | 231 | // move the cursor at the end of the word 232 | cursor.style.top = currentWord.lastElementChild.offsetTop + 'px'; 233 | cursor.style.left = currentWord.lastElementChild.offsetLeft + letterWidth + 'px'; 234 | } 235 | // erase the whole word 236 | if (ctrlKey) { 237 | [...currentWord.children].forEach(letter => { 238 | if (letter.classList.contains('extra')) { 239 | letter.remove(); 240 | } 241 | else { 242 | letter.className = ''; 243 | } 244 | }) 245 | currentLetter = currentWord.firstElementChild; 246 | } 247 | 248 | if (currentLetter) currentLetter.className = ''; 249 | } 250 | 251 | function validateCurrentWord() { 252 | //not incorrect letter 253 | if (!currentWord.querySelector('.incorrect')) { 254 | currentWord.classList.add('correct'); 255 | correctWordChar += currentWord.childElementCount; 256 | } 257 | else { 258 | currentWord.classList.add('incorrect'); 259 | incorrectWordChar += currentWord.querySelectorAll('letter:not(.extra)').length; 260 | } 261 | } 262 | function isGameOver() { 263 | return (!currentLetter && !currentWord.nextElementSibling); 264 | } 265 | 266 | let correctWords = 0, incorrectWords = 0; 267 | 268 | function calculateTypingStats() { 269 | const duration = (Date.now() - startTime) / 1000; // unit(second) 270 | WPM = Math.ceil(correctWordChar / 5) * 60 / duration; 271 | raw_WPM = Math.ceil((correctWordChar + incorrectWordChar) / 5) * 60 / duration; 272 | 273 | 274 | wpmData.push(Math.round(WPM)); 275 | rawData.push(Math.round(raw_WPM)); 276 | } 277 | 278 | --------------------------------------------------------------------------------