Let's write a piano chord visualization application - a vanilla JS beginner-friendly tutorial—part II.
In part I, we learned the basic rules of creating a chord and created our HTML/CSS piano keyboard. In this part, we will finish the HTML/CSS template and make core JavaScript functionality.
Contents:
- Minimum music theory explanation for better topic understanding
- Setup
- [HTML/CSS] Model of a piano keyboard
3.1 Complete code - [HTML/CSS] Chord root and type selection
4.1 Complete code - [JS] Chord generation
5.1 Complete code - [HTML/JS] Putting all together
[HTML/CSS] Chord root and type selection
To complete our HTML/CSS part of the application, we have to add a chord selection form. To avoid scrolling through many note options, I decided to make a selection in the form of a group of buttons for each part.
Chord selection HTML/CSS result
I separated chord root note selection and type selection since we'll be changing them independently.
NOTE: Chord root note gives the chord a name and establishes the relationship between notes in a chord.
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<label for="root-notes-group" class="form-label"><i class="bi bi-music-note"></i> Root note</label>
<div class="btn-toolbar mb-3" role="toolbar" aria-label="Toolbar with button groups">
<div id="root-notes-group" class="btn-group me-2" role="group" aria-label="Root notes group">
<button type="button" class="btn btn-outline-light">C</button>
<button type="button" class="btn btn-outline-light">C#</button>
<button type="button" class="btn btn-outline-light">D</button>
<button type="button" class="btn btn-outline-light">D#</button>
<button type="button" class="btn btn-outline-light">E</button>
<button type="button" class="btn btn-outline-light">F</button>
<button type="button" class="btn btn-outline-light">F#</button>
<button type="button" class="btn btn-outline-light">G</button>
<button type="button" class="btn btn-outline-light">G#</button>
<button type="button" class="btn btn-outline-light">A</button>
<button type="button" class="btn btn-outline-light">A#</button>
<button type="button" class="btn btn-outline-light">B</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<label for="chord-types-group" class="form-label"><i class="bi bi-music-note-beamed"></i> Chord type</label>
<div class="btn-toolbar mb-3" role="toolbar2" aria-label="Toolbar with button groups">
<div id="chord-types-group" class="btn-group me-2" role="group2" aria-label="Chord types group">
<button type="button" class="btn btn-outline-light">Major</button>
<button type="button" class="btn btn-outline-light">Minor</button>
<button type="button" class="btn btn-outline-light">Diminished</button>
</div>
</div>
</div>
</div>
</div>
HTML structure is simple. In first group in <div id="root-notes-group" class="btn-group me-2" role="group" aria-label="Root notes group">
each button represents one root note. In second group <div id="chord-types-group" class="btn-group me-2" role="group2" aria-label="Chord types group">
each button represents one chord type.
All of the stylings are provided by the Bootstrap framework. You can find the details of the button group styling with Bootstrap here.
Our application will handle the display of three types of chords: major, minor and diminished.
Now in index.html, replace the part <!-- chord root and type selection -->
with chord type selection.
index.html should present the following result.
Complete HTML/CSS for the chord selection
[HTML/CSS] Chord root and type selection - complete code
In this section, we have only edited index.html file.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf8" />
<title>Piano chord visualizer</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.2/font/bootstrap-icons.css" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous" />
<link rel="stylesheet" href="piano-keyboard.css" />
</head>
<body class="d-flex flex-column h-100 min-vh-100">
<main class="bg-secondary text-white flex-grow-1">
<div class="container">
<div class="row mt-3">
<div class="col-md-12">
<h1>Piano chord visualizer</h1>
</div>
</div>
<div class="row mt-5">
<div class="col-md-6">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<label for="root-notes-group" class="form-label"><i class="bi bi-music-note"></i> Root note</label>
<div class="btn-toolbar mb-3" role="toolbar" aria-label="Toolbar with button groups">
<div id="root-notes-group" class="btn-group me-2" role="group" aria-label="Root notes group">
<button type="button" class="btn btn-outline-light">C</button>
<button type="button" class="btn btn-outline-light">C#</button>
<button type="button" class="btn btn-outline-light">D</button>
<button type="button" class="btn btn-outline-light">D#</button>
<button type="button" class="btn btn-outline-light">E</button>
<button type="button" class="btn btn-outline-light">F</button>
<button type="button" class="btn btn-outline-light">F#</button>
<button type="button" class="btn btn-outline-light">G</button>
<button type="button" class="btn btn-outline-light">G#</button>
<button type="button" class="btn btn-outline-light">A</button>
<button type="button" class="btn btn-outline-light">A#</button>
<button type="button" class="btn btn-outline-light">B</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<label for="chord-types-group" class="form-label"><i class="bi bi-music-note-beamed"></i> Chord type</label>
<div class="btn-toolbar mb-3" role="toolbar2" aria-label="Toolbar with button groups">
<div id="chord-types-group" class="btn-group me-2" role="group2" aria-label="Chord types group">
<button type="button" class="btn btn-outline-light">Major</button>
<button type="button" class="btn btn-outline-light">Minor</button>
<button type="button" class="btn btn-outline-light">Diminished</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 d-flex justify-content-center flex-wrap">
<div id="piano-keyboard">
<div class="key key-natural"></div>
<div class="key key-sharp"></div>
<div class="key key-natural"></div>
<div class="key key-sharp"></div>
<div class="key key-natural"></div>
<div class="key key-natural"></div>
<div class="key key-sharp"></div>
<div class="key key-natural"></div>
<div class="key key-sharp"></div>
<div class="key key-natural"></div>
<div class="key key-sharp"></div>
<div class="key key-natural"></div>
<div class="key key-natural"></div>
<div class="key key-sharp"></div>
<div class="key key-natural"></div>
<div class="key key-sharp"></div>
<div class="key key-natural"></div>
<div class="key key-natural"></div>
<div class="key key-sharp"></div>
<div class="key key-natural"></div>
<div class="key key-sharp"></div>
<div class="key key-natural"></div>
<div class="key key-sharp"></div>
<div class="key key-natural"></div>
<div class="key key-natural"></div>
</div>
</div>
</div>
</div>
</main>
</body>
</html>
Section knowledge base
Here you will find links to helpful articles on the topic I don't cover thoroughly in this section.
Button group styling in Bootstrap by getbootstrap.com - Bootstrap button group documentation
[JS] Chord generation
In this section, we start writing the JavaScript part of the application. This section only focuses on generating data we will later need to render changes. Code responsible for selection control and rendering selected options you will find in the next section.
All JS content we put in the index.js file.
// Array storing all existing notes on the piano
const notes = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"];
We start by defining the constant storing all notes. It will be our entry point for functions generating chord notes.
// Object storing chord types and its notes position
const chordScheme = {
maj: [0, 4, 7],
min: [0, 3, 7],
dim: [0, 3, 6],
};
We store our chord types in an object where object keys are chord types and values are the notes belonging to the chord. The numbers represent the place of the note in the notes array.
Quick reminder, "C major" chord notes distances are 4 and 3 semitones. Note "C" is a first string in the
notes
array, so its index is 0. Then we increment it by 4 to get the next note—finally, we increment it by 3 to get the last note of the chord.0, 4 [0 + 4], 7 [0 + 4 + 3]
This way, we can specify a universal scheme for storing our chord types. Then, you can start wondering,
"It works for C-based chords, but what about A, B, G, and other chords?"
We will make it work by shifting the notes
array; the way, the desired root note will be under index 0. To do so, we will create a function that:
- find an index of new root note in the notes array,
- split notes array into two arrays, where the split point is our found index,
- return new notes array by combining split arrays with swapped position
Function logic visualtization
For that, we create a function called getNotesFromRoot
which accepts rootNote
argument.
// Function altering the notes array to change the root note
const getNotesFromRoot = (rootNote) => {
};
First, we find an index of new root note by using the indexOf
function. notes.indexOf(rootNote.toLowerCase());
returns -1
or integer value with the position of the rootNote.toLowerCase()
. We use the toLowerCase()
method to make the search case insensitive. This eliminates the situation when, for example, we have passed the "G#" string, which is the correct note name but doesn't exist in the notes array. We store results in the slicePivot
constant. If the slicePivot
equals -1
, we return an empty array.
arrayTypeVar.indexOf(searchedValue) - method returning position of
searchedValue
in an array on which function was called
stringTypeVar.toLowerCase() - method returning lowercased value of the string variable on which function was called
// Function altering the notes array to change the root note
const getNotesFromRoot = (rootNote) => {
let slicePivot = notes.indexOf(rootNote.toLowerCase()); // get the position of new root note
if (slicePivot === -1) { // return empty array if root note doesn't exists
return [];
}
};
Next, we split the notes array into two smaller arrays. For that, we use the slice()
method. The first subpart we call fromPivot
contains notes from the position of the new root note to the end of the array. The second subpart is toPivot
and includes notes from the beginning to the first before the new root note. Finally, we combine two subparts in one by returning a new array of copied values of the arrays. For that, we use the array spread operator.
Alternatively, we can use the concat()
method. If then, the return statement will be return fromPivot.concat(toPivot);
.
Our function returning shifted notes array is now ready.
arrayTypeVar.slice(startIndex, endIndex) - method returning a shallow copy of an array containing elements from range <startIndex, endIndex); Element under the endIndex is not included in the copy. Ex.:
[1, 2, 3, 4, 5, 6].slice(2, 5)
will return[3, 4, 5]
;
arrayTypeVar.concat(array1, array2, ..., arrayN) - method returning a new array by concatenating array with the arrays provided in arguments. Ex.:
[1, 2, 3].concat([4, 5, 6])
will return[1, 2, 3, 4, 5, 6]
;
// Function altering the notes array to change the root note
const getNotesFromRoot = (rootNote) => {
let slicePivot = notes.indexOf(rootNote.toLowerCase()); // get the position of new root note
if (slicePivot === -1) { // return empty array if root note doesn't exists
return [];
}
const fromPivot = notes.slice(slicePivot, notes.length); // get part of array fom new root note to the end of array
const toPivot = notes.slice(0, slicePivot); // get part of array fom beginning to new root note
return [...fromPivot, ...toPivot]; // return new array combining fromPivot and toPivot together
};
Finally, we'll create a function returning chord notes. Let's call that function getChord
and make it accept arguments rootNote
and chordType
. The logic of function is following:
- We prepare an array with the notes starting from the chords root note - for that, we use the previously created
getNotesFromRoot.
- We check if the
notesFromRoot
is not empty. - We check if the
chordScheme[chordType]
returned a value. Valid properties ofchordScheme
object are "maj", "min" and "dim" which corresponds to "major", "minor" and "diminished."
We return mappedchordScheme[chordType]
if the checks have passed. We return a new array when replacing the notes indexes with notes themselves.
If checks have failed, we return an empty array.
arrayTypeVar.map((element) => {/* ... */})
- method returning a new array with modified elements as instructed in the passed mapping function as an argument. Ex.:[1, 2, 3].map((element) => {return element + 5;})
will return[6, 7, 8]
. Each element will be transformed by mapping function.
// Function returning chord notes
const getChord = (rootNote, chordType) => {
const notesFromRoot = getNotesFromRoot(rootNote); // get array of notes, beginning from the rootNote value
if (notesFromRoot.length > 0 && chordScheme[chordType]) {
return chordScheme[chordType].map((noteIndex) => notesFromRoot[noteIndex]); // map selected chord type array into array of note names
}
return [];
};
[JS] Chord generation - complete code
In this section, we have only edited index.js file.
index.js
// Array storing all existing notes on the piano
const notes = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"];
// Object storing chord types and its notes position
const chordScheme = {
maj: [0, 4, 7],
min: [0, 3, 7],
dim: [0, 3, 6],
};
// Function altering the notes array to change the root note
const getNotesFromRoot = (rootNote) => {
let slicePivot = notes.indexOf(rootNote.toLowerCase()); // get the position of new root note
if (slicePivot === -1) { // return empty array if root note doesn't exists
return [];
}
const fromPivot = notes.slice(slicePivot, notes.length); // get part of array fom new root note to the end of array
const toPivot = notes.slice(0, slicePivot); // get part of array fom beginning to new root note
return [...fromPivot, ...toPivot]; // return new array combining fromPivot and toPivot together
};
// Function returning chord notes
const getChord = (rootNote, chordType) => {
const notesFromRoot = getNotesFromRoot(rootNote); // get array of notes, beginning from the rootNote value
if (notesFromRoot.length > 0 && chordScheme[chordType]) {
return chordScheme[chordType].map((noteIndex) => notesFromRoot[noteIndex]); // map selected chord type array into array of note names
}
return [];
};
We can test what we have coded so far by running the index.html file in the browser. Then, in the browser's developer tools console, we can directly call the function getChord
.
Execution of getChord() function in the Chrome's Developer Tool console
Section knowledge base
Here you will find links to helpful articles on the topic I don't cover thoroughly in this section.
Array.prototype.slice() by developer.mozilla.org - Array slice() method
Spread syntax by developer.mozilla.org - Spread operator
Array.prototype.map() by developer.mozilla.org - Array map() method
In part III, we will add interactivity to our HTML template to mark the piano keys depending on the selected chord.
hello is part 3 ready ?
Just published :) Enjoy!
I joined this site to like this post. Thank you.
Thank you! Due to some circumstances, I didn’t finished part three, which I will this week. Thanks for bringing me motivation :)
In the end, it took waaaay to long., but it’s here https://www.codementor.io/@andrzejgorgonag/let-s-write-a-piano-chord-visualization-application-a-vanilla-js-beginner-friendly-tutorial-part-iii-1zt3hq20s0. I promise, next posts will be like 1 part and more regular. And question, if music related stuff is interesting :)
Thank you for the tutorial. Doesn’t index.js have to have a link in the index.html or is that in part iii?
Part three will come this week. Soooooorry for huge delay. But yes, js file has to be included in an html file in order to work.
Part 3 is here https://www.codementor.io/@andrzejgorgonag/let-s-write-a-piano-chord-visualization-application-a-vanilla-js-beginner-friendly-tutorial-part-iii-1zt3hq20s0 :)