Advent of JavaScript, Day 3
Challenge #3 is creating a keyboard game:
With the project files downloaded and codesandbox
’d into a live CodeSandbox, I’m ready to get going!
Again, let’s start with the User Requirements and speculate how I can solve these:
Users should be able to:
- See the keyboard centered on the page
Done!
- Whenever a user hovers over a specific key it will change colors
- White keys will change to yellow #ffd200
Done!
- Black keys will change to pink #f40082
Done!
- When a user clicks on a specific key, it will play an audio clip.
This seems straightforward – new Audio(url)
.
- The audio clips are numbered, but I did not specifically number the keys. You can pick which key should be associated with each audio file.
The files are named audio/key-{1-23}.mp3
, so the initial implementation can just increment by key position.
- If a user clicks on one key, then immediately clicks on a second key. The 2 files should both play. Meaning, clicking on one key will not stop an existing audio file from playing.
What this tells me is that there won’t be a re-use of variable references.
Initial Approach
-
Vanilla JS (no libraries), since there doesn’t seem to be much complexity here
-
document.querySelectorAll('.piano a')
to find all keys -
.addEventListener('click', ...)
to listen for clicks -
new Audio(url)
to create &.play
the audioThe URLs load asynchronously, so it may be best to initialize these immediately and
.play
once clicked.
Making it Work
This wasn’t much of a challenge. I was able to get this completed while an episode of SpongeBob was playing…
Anyway, the only surprise was with the SVG keys being out of order:
the <a>
elements aren’t in left-to-write order, but seemingly random in the SVG.
To solve this, I used .sort
to find the mid-point of each key and order those left-to-right:
const keyElements = Array.from(document.querySelectorAll('.piano a'))
keyElements.sort((a, b) => {
const aRect = a.getBoundingClientRect()
const aMiddle = aRect.left + aRect.width / 2
const bRect = b.getBoundingClientRect()
const bMiddle = bRect.left + bRect.width / 2
return aMiddle - bMiddle
})
Then, I mapped those keys to the sound they would play:
const keyAudios = keyElements.map((keyElement, i) => {
return new Audio(`audio/key-${i + 1}.mp3`)
})
(The browser automatically marks these as preload
, so they should be ready to go once you click a key.)
One way to associate clicks-to-keys is to keyElements.forEach(keyElement, ...)
and keyElement.addEventListener('click', ...)
.
An alternative is to use window.addEventListener('click', ...)
, so there’s one event listener rather than N (23).
This optimization isn’t necessary here, but can be a good idea when indiscriminately adding events to an unknown number of elements:
- Points on a graph
- Rows in a table
- Links on a page
- etc.
window.addEventListener('click', (event) => {
// click is on the `<path>`, so traverse up to the <a>
const a = event.target.parentNode
if (!keyElements.includes(a)) {
return
}
event.preventDefault()
const i = keyElements.indexOf(a)
const audio = keyAudios[i]
audio.currentTime = 0
audio.play()
})
Since each keyAudios
is separate, the sounds automatically overlap.
But, I wanted to make sure repeatedly clicking a key would repeat the sound from the beginning.
The Audio
API does not have a .stop
or .restart
method, so setting audio.currentTime = 0
effectively accomplishes the same thing.