Category: Tutorials
Share:

A piano powered by JavaScript keyboard event.

DEMOCODEPEN

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.

DEMOCODEPEN

This experiment is my own spin on a tutorial based on key listeners by Wes Bos, which I recommend in its entirety.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.