Skip to content

Instantly share code, notes, and snippets.

@stanwmusic
Created February 10, 2022 00:16
Show Gist options
  • Save stanwmusic/fe3694fcf4cc220d647b5850fd441932 to your computer and use it in GitHub Desktop.
Save stanwmusic/fe3694fcf4cc220d647b5850fd441932 to your computer and use it in GitHub Desktop.
Typing Speedometer
- var ticks = 120;
- var tickLabels = 12;
- var tickInc = ticks / tickLabels;
main
.speedometer
.speedometer-inner
- for (var i = 0; i < ticks + 1; ++i) {
.tick
- for (var j = 0; j <= tickLabels; ++j) {
- if (i == tickInc * j) {
span
- }
- }
- }
.arrow
.unit
.wpm
span(id="h",class="_")
span(id="t",class="_")
span(id="o",class="_0")
form(action="")
label(for="typed-text")="Type something as fast as you can!"
textarea(id="typed-text",type="text",name="typed-text",cols="32",rows="5",value="")
button(type="reset")="Reset"
window.addEventListener("load",app);
function app() {
var oldStrLen = 0,
charsEachSec = [],
getWPM = () => {
let arrow = document.querySelector(".arrow"),
display = [
document.getElementById("h"),
document.getElementById("t"),
document.getElementById("o"),
],
strLen = document.querySelector("textarea").value.length,
wpm = 0;
// unless field is cleared, get WPM based on average characters typed per second
if (strLen > 0) {
let charsDurSec = strLen - oldStrLen,
charSum = 0,
wordLen = 5,
maxWords = 60;
charsEachSec.push(charsDurSec);
// use last n words for average
if (charsEachSec.length > maxWords)
charsEachSec.shift();
for (var c of charsEachSec)
charSum += c;
// calculate WPM
let avgChars = charSum / charsEachSec.length,
wps = avgChars / wordLen,
wpmCalc = Math.round(wps * 60),
hardLimit = 999;
if (wpmCalc > 0 && wpmCalc <= hardLimit)
wpm = wpmCalc;
else if (wpmCalc > hardLimit)
wpm = hardLimit;
} else {
charsEachSec = [];
}
// make old string length equal to newest one before calculating WPM again
oldStrLen = strLen;
// set ceiling for and rotate arrow
let maxWpm = 120,
arrowWpm = wpm < maxWpm ? wpm : maxWpm;
arrow.style.transform = "rotate(" + ((arrowWpm * 2) - 120) + "deg) translateY(-72%)";
// make WPM string, clean digits, redisplay digits
let wpmStr = wpm.toString();
for (var d of display)
d.className = "_";
for (var i in wpmStr)
display[display.length - 1 - i].className = "_" + wpmStr[wpmStr.length - 1 - i];
};
// runtime loop
var run = () => {
getWPM();
setTimeout(run,1e3);
};
run();
}
$ticks: 120;
$tickLabels: 12;
$tickInc: $ticks / $tickLabels;
$wpm: 0; // initial speed
* {
border: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
font-size: calc(16px + (24 - 16)*(100vw - 320px)/(1920 - 320));
}
body, label, textarea, button {
font: 1em Hind, sans-serif;
line-height: 1.5;
}
body, textarea {
color: #171717;
}
body {
background: #f1f1f1;
}
label, textarea, button {
-webkit-appearance: none;
appearance: none;
}
main {
margin: auto;
max-width: 30em;
padding: 1.5em;
}
textarea, button {
width: 100%;
}
textarea {
box-shadow: 0 0 0 1px #d9d9d9;
margin-bottom: 1.5em;
padding: 0.75em;
}
button {
background-color: #2762f3;
border-radius: 0.375em;
color: #f1f1f1;
cursor: pointer;
padding: 0.5em 1em;
transition: background-color 0.15s linear;
&:hover, &:focus {
background-color: #0c48db;
}
&:active {
transform: translateY(1px);
}
}
.speedometer {
font-family: "Play", sans-serif;
margin: 0 auto 1.5em auto;
overflow: hidden;
width: 16em;
height: 12em;
}
.speedometer-inner {
background-image:
radial-gradient(
100% 100% at 50% 50%,
#3d3d3d 7%,
#242424 7.25%,
#3d3d3d 8%,
#3d3d3d 46.5%,
#7f7f7f 45.5%,
#fff 47.5%,
#d9d9d9 48.75%,
rgba(0,0,0,0.34) 49%,
transparent 50%
);
color: #fff;
position: relative;
height: 16em;
> div {
position: absolute;
}
}
.tick, .unit, .wpm {
z-index: 0;
}
.tick {
background-image: linear-gradient(transparent 4%, rgb(255,255,255) 4%, rgb(255,255,255) 8%, transparent 8%);
top: 0;
left: calc(50% - 0.09em);
width: 0.1em;
height: 100%;
span {
display: block;
text-align: center;
width: 1.8em;
}
@for $i from 1 through $ticks + 1 {
&:nth-of-type(#{$i}) {
transform: rotate(-121deg + (240 / ($ticks + 1)) * $i);
}
}
@for $i from 1 through $tickLabels + 1 {
&:nth-of-type(#{($i - 1) * $tickInc + 1}) span {
transform: translate(-50%,2em) rotate(120deg - (($ticks / $tickLabels * 2) * ($i - 1)));
&:before {
content: "#{($i - 1) * $tickInc}";
}
}
}
&:nth-of-type(5n + 1) {
background-image: linear-gradient(transparent 4%, rgb(255,255,255) 4%, rgb(255,255,255) 10%, transparent 10%);
width: 0.1em;
}
&:nth-of-type(10n + 1) {
background-image: linear-gradient(transparent 4%, rgb(255,255,255) 4%, rgb(255,255,255) 12%, transparent 12%);
width: 0.15em;
}
}
.arrow {
background-color: rgb(255,222,24);
border-radius: 50% 50% 0 0;
box-shadow: 0 0 1px 1px rgb(160,64,0) inset, 0 0 1px 1px rgba(0,0,0,0.4);
top: 33%;
left: calc(50% - 0.225em);
width: 0.45em;
height: 5.4em;
transform: rotate(0deg + (($wpm * 2) - 120)) translateY(-73%);
transition: transform 1s linear;
z-index: 1;
}
.unit {
text-align: center;
top: 31%;
left: calc(50% - 1.5em);
width: 3em;
&:before {
content: "WPM";
}
}
.wpm {
background-color: rgb(255,255,255);
border-radius: 0.2em;
box-shadow: 0 (0.05em) (0.05em) rgba(0,0,0,0.5) inset;
color: rgb(0,0,0);
overflow: hidden;
top: 62%;
left: calc(50% - 1.5em);
padding: 0 0.4em;
height: 1.2em;
width: 3em;
> span {
/* Number sprites by lavarmsg from Vecteezy.com (https://www.vecteezy.com/vector-art/95999-digital-number-counter) */
background: {
image: url(https://static.vecteezy.com/system/resources/previews/000/095/999/original/vector-digital-number-counter.jpg);
size: 6em auto;
};
display: inline-block;
vertical-align: top;
height: 100%;
width: 0.7em;
transform: translateY(0.12em);
}
}
/* Number sprites */
$yPos1: -1.725em;
$yPos2: -2.87em;
._ {
background-position: -3.65em $yPos2;
opacity: 0.2;
}
._0 {
background-position: -0.725em $yPos1;
}
._1 {
background-position: -1.7em $yPos1;
}
._2 {
background-position: -2.675em $yPos1;
}
._3 {
background-position: -3.675em $yPos1;
}
._4 {
background-position: -4.65em $yPos1;
}
._5 {
background-position: -0.725em $yPos2;
}
._6 {
background-position: -1.7em $yPos2;
}
._7 {
background-position: -2.675em $yPos2;
}
._8 {
background-position: -3.65em $yPos2;
}
._9 {
background-position: -4.625em $yPos2;
}
@media screen and (prefers-color-scheme: dark) {
body, textarea {
color: #f1f1f1;
}
body {
background: #171717;
}
textarea {
background: #3d3d3d;
box-shadow: 0 0 0 1px #3d3d3d;
}
}
<link href="https://fonts.googleapis.com/css?family=Hind&amp;display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Play&amp;display=swap" rel="stylesheet" />

Typing Speedometer

A speedometer that measures your typing speed in real time. It works like those used by typing test websites such as typingtest.com. To make it work, I had to figure out how to turn the WPM formula ( (total characters / 5) / 1 minute ) into a real-time WPM formula. So after experimenting with the tests, this is what I came up with:

((total characters / (# of times characters were recorded each second)) / 5) * 60 seconds

I got the idea to create this demo after memories of playing some racing mini game in early versions of Mavis Beacon where your typing speed is your driving speed.

Update 1/9/20: Refactored some JS

A Pen by Stan Williams on CodePen.

License.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment