├── 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 |
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 |
--------------------------------------------------------------------------------