A piano powered by JavaScript keyboard event.
The plan
Let’s start by listing what this feature is going to do, step by step:
- Connect keyboard keystrokes to audio files (musical notes)
- When pressing one of the assigned keyboard keys:
- The key itself is animated on the screen
- Respective musical note audio plays
- The musical note is indicated at the top of the screen
- When hovering over the screen keyboard, hints will appear one by one
Getting started: markup + styles
The idea is to connect the keystrokes to audio, in this case emulating a piano keyboard and its respective notes. To accomplish this effect, we start with a simple markup that forms the keyboard followed by the audio of each note:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | <section id="wrap"> <header> <h1>JS Piano</h1> <h2>Use your keyboard. Hover for hints.</h2> </header> <section id="main"> <div class="nowplaying"></div> <div class="keys"> <div data-key="65" class="key" data-note="C"> <span class="hints">A</span> </div> <div data-key="87" class="key sharp" data-note="C#"> <span class="hints">W</span> </div> <div data-key="83" class="key" data-note="D"> <span class="hints">S</span> </div> <div data-key="69" class="key sharp" data-note="D#"> <span class="hints">E</span> </div> <div data-key="68" class="key" data-note="E"> <span class="hints">D</span> </div> <div data-key="70" class="key" data-note="F"> <span class="hints">F</span> </div> <div data-key="84" class="key sharp" data-note="F#"> <span class="hints">T</span> </div> <div data-key="71" class="key" data-note="G"> <span class="hints">G</span> </div> <div data-key="89" class="key sharp" data-note="G#"> <span class="hints">Y</span> </div> <div data-key="72" class="key" data-note="A"> <span class="hints">H</span> </div> <div data-key="85" class="key sharp" data-note="A#"> <span class="hints">U</span> </div> <div data-key="74" class="key" data-note="B"> <span class="hints">J</span> </div> <div data-key="75" class="key" data-note="C"> <span class="hints">K</span> </div> <div data-key="79" class="key sharp" data-note="C#"> <span class="hints">O</span> </div> <div data-key="76" class="key" data-note="D"> <span class="hints">L</span> </div> <div data-key="80" class="key sharp" data-note="D#"> <span class="hints">P</span> </div> <div data-key="186" class="key" data-note="E"> <span class="hints">;</span> </div> </div> <audio data-key="65" src="sounds/040.wav"></audio> <audio data-key="87" src="sounds/041.wav"></audio> <audio data-key="83" src="sounds/042.wav"></audio> <audio data-key="69" src="sounds/043.wav"></audio> <audio data-key="68" src="sounds/044.wav"></audio> <audio data-key="70" src="sounds/045.wav"></audio> <audio data-key="84" src="sounds/046.wav"></audio> <audio data-key="71" src="sounds/047.wav"></audio> <audio data-key="89" src="sounds/048.wav"></audio> <audio data-key="72" src="sounds/049.wav"></audio> <audio data-key="85" src="sounds/050.wav"></audio> <audio data-key="74" src="sounds/051.wav"></audio> <audio data-key="75" src="sounds/052.wav"></audio> <audio data-key="79" src="sounds/053.wav"></audio> <audio data-key="76" src="sounds/054.wav"></audio> <audio data-key="80" src="sounds/055.wav"></audio> <audio data-key="186" src="sounds/056.wav"></audio> </section> </section> <video playsinline autoplay muted loop id="bgvid" poster="video/bg.jpg"><source src="video/bg.mp4" type="video/mp4"></video> |
Note that data-key attribute values indicate the keycode our js will be listening to, while span.hints contains the actual face value of that key. Additionally, a data-note attribute was added which will carry the specific musical note we are expecting as result. In short: when typing “A”, which has a corresponding keycode of “65” the musical note “C” will play.
Black keys received a modifier class of sharp that sets their specific styles.
Now add styles to the mix so it actually looks like a piano keyboard:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | html { background: #000; font-family: 'Noto Serif', serif; -webkit-font-smoothing: antialiased; text-align: center; } video#bgvid { position: fixed; top: 50%; left: 50%; min-width: 100%; min-height: 100%; width: auto; height: auto; z-index: -100; transform: translateX(-50%) translateY(-50%); background-size: cover; } header { position: relative; margin: 30px 0; } header:after { content: ''; width: 460px; height: 15px; background: url(images/intro-div.svg) no-repeat center; display: inline-block; text-align: center; background-size: 70%; } h1 { color: #fff; font-size: 50px; font-weight: 400; letter-spacing: 0.18em; text-transform: uppercase; margin: 0; } h2 { color: #fff; font-size: 24px; font-style: italic; font-weight: 400; margin: 0 0 30px; } .nowplaying { font-size: 120px; line-height: 1; color: #eee; text-shadow: 0 0 5rem #028ae9; transition: all .07s ease; min-height: 120px; } .keys { display: block; width: 100%; height: 350px; max-width: 880px; position: relative; margin: 40px auto 0; cursor: none; } .key { position: relative; border: 4px solid black; border-radius: .5rem; transition: all .07s ease; display: block; box-sizing: border-box; z-index: 2; } .key:not(.sharp) { float: left; width: 10%; height: 100%; background: rgba(255, 255, 255, .8); } .key.sharp { position: absolute; width: 6%; height: 60%; background: #000; color: #eee; top: 0; z-index: 3; } .key[data-key="87"] { left: 7%; } .key[data-key="69"] { left: 17%; } .key[data-key="84"] { left: 37%; } .key[data-key="89"] { left: 47%; } .key[data-key="85"] { left: 57%; } .key[data-key="79"] { left: 77%; } .key[data-key="80"] { left: 87%; } .playing { transform: scale(.95); border-color: #028ae9; box-shadow: 0 0 1rem #028ae9; } .hints { display: block; width: 100%; opacity: 0; position: absolute; bottom: 7px; transition: opacity .3s ease-out; font-size: 20px; } .keys:hover .hints { opacity: 1; } |
Here we are setting up the container, keyboard and hiding hints, which should only be visible on hover. Similarly, .playing and .nowplaying will only take effect when a note is played.
Time to play with JavaScript
Listen for Keystrokes
First step is to create the actual listener that will execute a function named playNote (line 12 in the code block below). Then we create the playNote function (line 1). Within it we are setting two variables audio and key making use of ES6 syntax to reach their data attributes (lines 2 and 3 below):
1 2 3 4 5 6 7 8 9 10 11 12 | function playNote(e){ const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`), key = document.querySelector(`.key[data-key="${e.keyCode}"]`); if(!key) return; key.classList.add('playing'); audio.play(); } window.addEventListener('keydown', playNote); |
Remember that e in this case is the keyboardEvent, therefore keyCode is just one its properties.
In case an unexpected key is pressed – e.g. spacebar – we add a check for key to avoid errors (line 5 above). Following, we add a class of playing to our piano key (line 7 above) with the purpose of of triggering the animation of border, shadow and scale already set as part of the CSS.
Lastly, the respective audio is played (line 8 above).
Playing Details
We start by reseting the audio before it plays – a necessary step to avoid an overlap if keystrokes happen quickly enough and the audio is already playing (line 8 below):
1 2 3 4 5 6 7 8 9 10 11 12 | function playNote(e){ const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`), key = document.querySelector(`.key[data-key="${e.keyCode}"]`); if(!key) return; key.classList.add('playing'); audio.currentTime = 0; audio.play(); } window.addEventListener('keydown', playNote); |
Once the key is pressed and sound played, we need to clear the styles applied:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | const keys = document.querySelectorAll('.key'); function playNote(e){ const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`), key = document.querySelector(`.key[data-key="${e.keyCode}"]`); if(!key) return; key.classList.add('playing'); audio.currentTime = 0; audio.play(); } function removeTransition(e){ if(e.propertyName !== 'transform') return; this.classList.remove('playing'); } keys.forEach(key => key.addEventListener('transitionend', removeTransition)); window.addEventListener('keydown', playNote); |
We start by adding a variable keys that holds all the .key elements in the document (line 1 above). Next we add an event listener (line 19 above) based on the end of the animation/transition, which will trigger the removeTransition function. This function (line 14 above) watches for the “transform” aspect of animation only, otherwise we would have multiple hits (one for each property being animated: border, color, etc). Finally, when the condition is met, playing class is removed.
Displaying Musical Notes
Now that the key is animated, we will add the sections that display the musical note played:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | const keys = document.querySelectorAll('.key'), note = document.querySelector('.nowplaying'); function playNote(e){ const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`), key = document.querySelector(`.key[data-key="${e.keyCode}"]`); if(!key) return; const keyNote = key.getAttribute('data-note'); key.classList.add('playing'); note.innerHTML = keyNote; audio.currentTime = 0; audio.play(); } function removeTransition(e){ if(e.propertyName !== 'transform') return; //skip if not a transform animation this.classList.remove('playing'); } keys.forEach(key => key.addEventListener('transitionend', removeTransition)); window.addEventListener('keydown', playNote); |
We start by assigning a variable note which is the placeholder (line 2 above). Within the playNote function, we create another variable keyNote to get the musical note listed as data attribute which we will use to display the note played (line 10 above). Next, we add within the function note.innerHTML = keyNote; which will output the note in the placeholder area.
Showing Hints
Lastly, we address hints:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | const keys = document.querySelectorAll('.key'), note = document.querySelector('.nowplaying'), hints = document.querySelectorAll('.hints'); function playNote(e){ const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`), key = document.querySelector(`.key[data-key="${e.keyCode}"]`); if(!key) return; const keyNote = key.getAttribute('data-note'); key.classList.add('playing'); note.innerHTML = keyNote; audio.currentTime = 0; audio.play(); } function removeTransition(e){ if(e.propertyName !== 'transform') return; this.classList.remove('playing'); } function hintsOn(e, index){ e.setAttribute('style', 'transition-delay:' + index * 50 + 'ms'); } hints.forEach(hintsOn); keys.forEach(key => key.addEventListener('transitionend', removeTransition)); window.addEventListener('keydown', playNote); |
First step is to assign a variable hints to hold all the span.hints instances (line 3 above). Next, we create a function named hintsOn where the animation delay is set to increase with each instance (line 24 above). To wrap it up, we call this function for each occurrence of the element: hints.forEach(hintsOn);.
The results can be seen on the demo link below.
This experiment is my own spin on a tutorial based on key listeners by Wes Bos, which I recommend in its entirety.