$gray250: rgb(250,250,250);
$gray245: rgb(245,245,245);
$gray240: rgb(240,240,240);
$gray230: rgb(230,230,230);
$gray220: rgb(220,220,220);
$gray200: rgb(200,200,200);
$gray180: rgb(180,180,180);
$gray150: rgb(150,150,150);
$gray120: rgb(120,120,120);
$gray90: rgb(90,90,90);
$gray60: rgb(60,60,60);
$gray30: rgb(30,30,30);
$blue: rgb(3,169,244);
$darkBlue: rgb(21,101,192);
$red: rgb(211,47,47);
$dark: rgb(21, 15, 30);
$shadow: rgba(0, 0, 0, 0.25) 0px 14px 45px, rgba(0, 0, 0, 0.22) 0px 10px 18px;
$dotShadow: rgba(0, 0, 0, 0.16) 0px 3px 10px inset, rgba(0, 0, 0, 0.23) 0px 3px 10px inset;
$horizontalMargins: 31;
@mixin center{
left: 50%;
position: absolute;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
html, body, #wrapper{
font-family: 'Roboto Mono', sans-serif;
height: 100%;
margin: 0px;
padding: 0px;
width: 100%;
}
#wrapper{
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: white;
}
}
#wrapper{
background: linear-gradient(to right, $blue, $darkBlue);
overflow-x: hidden;
overflow-y: auto;
position: relative;
#paper{
background-color: white;
border-radius: 2px;
box-shadow: $shadow;
height: 700px;
left: 50%;
margin: 50px 0px;
overflow: hidden;
position: absolute;
top: 0px;
transform: translateX(-50%);
width: 500px;
&.typing{
animation: rumble 0.82s cubic-bezier(.36,.07,.19,.97) infinite;
}
#cursor{
animation: blink 1.2s linear infinite;
background-color: $blue;
height: 20px;
left: 90px;
position: absolute;
top: 80px;
width: 2px;
}
.dot{
background-color: $blue;
border-radius: 1000px;
box-shadow: $dotShadow;
height: 25px;
left: 25px;
position: absolute;
width: 25px;
z-index: 2;
&#dot1{
top: 70px;
}
&#dot2{
top: 340px;
}
&#dot3{
top: 600px;
}
}
#vertical-margin{
background-color: rgba($red, 0.5);
height: 100%;
left: 80px;
position: absolute;
top: 0px;
width: 2px;
}
#horizontal-margins{
.horizontal-margin{
background-color: rgba($blue, 0.5);
color: transparent;
font-size: 0px;
height: 2px;
left: 0px;
position: absolute;
top: 80px;
width: 100%;
@for $i from 0 through $horizontalMargins{
&:nth-child(#{$i}){
top: 60px + (20px * $i);
}
}
}
}
#letters{
backface-visibility: hidden;
min-height: 100%;
overflow-x: hidden;
overflow-y: auto;
width: 100%;
}
}
}
.letter{
display: inline-block;
font-weight: 300;
height: 20px;
margin-left: 2px;
position: absolute;
text-align: center;
transition: all 0.5s, color 0.1s;
width: 10px;
&.off-screen{
font-weight: 700;
transform: scale3d(10, 10, 10) rotate(1080deg);
}
}
@keyframes blink{
0%, 49%{
opacity: 1;
}
50%, 99%{
opacity: 0;
}
}
@keyframes rumble {
10%, 90% {
transform: translateX(-50%) translate3d(-3px, 0, 0);
}
20%, 80% {
transform: translateX(-50%) translate3d(4px, 0, 0);
}
30%, 50%, 70% {
transform: translateX(-50%) translate3d(-2px, 0, 0);
}
40%, 60% {
transform: translateX(-50%) translate3d(4px, 0, 0);
}
}
View Compiled
const getEl = el => document.getElementById(el)
const setStyle = (el, prop, val) => el.style[prop] = val
const setAttr = (el, attr, val) => el.setAttribute(attr, val)
const addClass = (el, className) => el.classList.add(className)
const removeClass = (el, className) => el.classList.remove(className)
const resetStyles = el => el.removeAttribute('style')
const removeAllChildren = el => {
while (el.hasChildNodes()) el.removeChild(el.lastChild)
}
const WRAPPER = getEl('wrapper'),
PAPER = getEl('paper'),
LETTERS = getEl('letters'),
CURSOR = getEl('cursor')
let HAS_STARTED_TYPING = false,
LAST_TYPE_TIMESTAMP = 0
const MIN_COL = 9,
MAX_COL = (LETTERS.clientWidth / 10) - 2,
MIN_ROW = 4,
MAX_ROW = 34,
LETTER_WIDTH = 10,
LETTER_HEIGHT = 20,
COLORS = {
BLUE: 'rgb(3,169,244)',
RED: 'rgb(239,83,80)',
PURPLE: 'rgb(171,71,188)',
GREEN: 'rgb(67,160,71)',
YELLOW:' rgb(253,216,53)'
}
const STATE = {
range: 0.1,
pos: {
row: MIN_ROW,
col: MIN_COL
}
}
const getRandColor = () => {
const rand = Math.floor((Math.random() * 5) + 1)
switch(rand){
case 1:
return COLORS.BLUE
case 2:
return COLORS.RED
case 3:
return COLORS.PURPLE
case 4:
return COLORS.GREEN
case 5:
return COLORS.YELLOW
}
}
const getRandPosOffScreen = () => {
const lowX1 = 0 - (window.innerWidth * 0.3),
highX1 = 0 - (window.innerWidth * 0.2),
lowY1 = 0,
highY1 = window.innerHeight,
lowX2 = window.innerWidth * 1.2,
highX2 = window.innerWidth * 1.3,
lowY2 = 0,
highY2 = window.innerHeight,
lowX3 = 0,
highX3 = window.innerWidth,
lowY3 = 0 - (window.innerHeight * 0.3),
highY3 = 0 - (window.innerHeight * 0.2),
lowX4 = 0,
highX4 = window.innerWidth,
lowY4 = window.innerHeight * 1.2,
highY4 = window.innerHeight * 1.3
const rand = Math.floor((Math.random() * 4) + 1)
let x = 0,
y = 0
switch(rand){
case 1:
x = Math.floor(Math.random() * (highX1 - lowX1 + 1)) + lowX1
y = Math.floor(Math.random() * (highY1 - lowY1)) + lowY1
break
case 2:
x = Math.floor(Math.random() * (highX2 - lowX2 + 1)) + lowX2
y = Math.floor(Math.random() * (highY2 - lowY2)) + lowY2
break
case 3:
x = Math.floor(Math.random() * (highX3 - lowX3 + 1)) + lowX3
y = Math.floor(Math.random() * (highY3 - lowY3)) + lowY3
break
case 4:
x = Math.floor(Math.random() * (highX4 - lowX4 + 1)) + lowX4
y = Math.floor(Math.random() * (highY4 - lowY4)) + lowY4
break
}
return { x, y }
}
const setLetterPos = (letter, x, y) => {
setStyle(letter, 'left', x + 'px')
setStyle(letter, 'top', y + 'px')
}
const setLetterColor = letter => {
const color = getRandColor()
setStyle(letter, 'color', color)
}
const createLetter = key => {
const letter = document.createElement('div')
letter.innerHTML = key
setLetterColor(letter)
addClass(letter, 'off-screen')
addClass(letter, 'letter')
return letter
}
const setInitialLetterPos = letter => {
const pos = getRandPosOffScreen()
setLetterPos(letter, pos.x, pos.y)
}
const isValidLetter = e => {
return !e.ctrlKey
&& e.key !== 'Enter'
&& !(e.key === ' ' && STATE.pos.col === MIN_COL)
}
const isEndOfPage = () => {
return STATE.pos.row === MAX_ROW && STATE.pos.col === MAX_COL
}
const initializeLetter = key => {
const letter = createLetter(key)
setInitialLetterPos(letter)
LETTERS.appendChild(letter)
return letter
}
const bumpLetterPos = isUp => {
if(isUp){
if(STATE.pos.col < MAX_COL){
STATE.pos.col = Math.min(STATE.pos.col + 1, MAX_COL)
}
else{
STATE.pos.col = MIN_COL
STATE.pos.row = Math.min(STATE.pos.row + 1, MAX_ROW)
}
}
else{
if(STATE.pos.col > MIN_COL){
STATE.pos.col = Math.max(STATE.pos.col - 1, MIN_COL)
}
else{
STATE.pos.col = MAX_COL
STATE.pos.row = Math.max(STATE.pos.row - 1, MIN_ROW)
}
}
}
const bumpCursorPos = () => {
const x = STATE.pos.col * LETTER_WIDTH + CURSOR.clientWidth,
y = STATE.pos.row * LETTER_HEIGHT
setLetterPos(CURSOR, x, y)
}
const determineFinalLetterPos = () => {
let x = 0,
y = 0
if(STATE.pos.col <= MAX_COL){
x = STATE.pos.col * LETTER_WIDTH
y = STATE.pos.row * LETTER_HEIGHT
}
else{
x = STATE.pos.col * LETTER_WIDTH
y = (STATE.pos.row + 1) * LETTER_HEIGHT
}
bumpLetterPos(true)
bumpCursorPos()
return {x, y}
}
const setFinalLetterPos = letter => {
const pos = determineFinalLetterPos()
setLetterPos(letter, pos.x, pos.y)
}
const getLastLetter = () => {
const letters = LETTERS.childNodes
let letter = null
for(let i = letters.length - 1; i >= 0; i--){
if(!letters[i].dataset.removed){
letter = letters[i]
break
}
}
return letter
}
const setLeavingLetterPos = letter => {
const pos = getRandPosOffScreen()
setLetterPos(letter, pos.x, pos.y)
addClass(letter, 'off-screen')
}
const removeLetter = () => {
const letter = getLastLetter(),
color = getRandColor()
if(letter === null) return 0
LAST_TYPE_TIMESTAMP = moment()
setStyle(letter, 'color', color)
setLeavingLetterPos(letter)
setAttr(letter, 'data-removed', true)
bumpLetterPos(false)
bumpCursorPos()
setTimeout(() => {
LETTERS.removeChild(letter)
}, 500)
}
const handleAlternateKeys = e => {
switch(e.keyCode){
case 8: // Backspace
removeLetter()
break
case 9: // Tab
e.preventDefault()
break
case 13: // Enter
break
}
}
const typeLetter = key => {
LAST_TYPE_TIMESTAMP = moment()
const letter = initializeLetter(key)
setFinalLetterPos(letter)
setTimeout(() => {
removeClass(letter, 'off-screen')
setTimeout(() => setStyle(letter, 'color', 'black'), 500)
}, 13)
}
let typeInterval = null
const typeSentence = sentence => {
let i = 0
typeInterval = setInterval(() => {
typeLetter(sentence[i])
if(i === sentence.length - 1) clearInterval(typeInterval)
i++
}, 200)
}
const onInitialType = () => {
STATE.pos.row = MIN_ROW
STATE.pos.col = MIN_COL
removeAllChildren(LETTERS)
clearInterval(typeInterval)
HAS_STARTED_TYPING = true
}
const checkIfTyping = () => {
const timeToLastType = moment() - LAST_TYPE_TIMESTAMP
if(!PAPER.classList.contains('typing') && timeToLastType <= 300){
addClass(PAPER, 'typing')
}
else if(PAPER.classList.contains('typing') && timeToLastType > 300){
removeClass(PAPER, 'typing')
}
}
window.onkeypress = e => {
if(!HAS_STARTED_TYPING) onInitialType()
if(!isEndOfPage() && isValidLetter(e)) typeLetter(e.key)
}
window.onkeydown = e => {
if(!HAS_STARTED_TYPING) onInitialType()
handleAlternateKeys(e)
}
window.onload = () => {
typeSentence('Start typing something!')
setInterval(() => checkIfTyping(), 300)
}