Let's build a guitar! Well, ok, not a physical guitar, but the next best thing: A digital one! Excited? Alright! Just like a good rock show, might as well jump right in!
Forging the instrument
I start with some boilerplating: A simple HTML file with an inline SVG. Inline, because I need to attach a lot of JS later on. I always loved the Gibson Flying V's design, so I will take its head and neck as an inspiration. I start with some linear gradients and a filter for a drop shadow:
<svg id="guitar" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 2400 800" preserveAspectRatio="xMidYMid meet" width="2400" height="800">
<defs>
<linearGradient id="fretboard" x1="42%" y1="0%" x2="0%" y2="90%">
<stop offset="0%" style="stop-color: rgb(56, 53, 53);" />
<stop offset="100%" style="stop-color: rgb(56, 49, 43);" />
</linearGradient>
<linearGradient id="fredboardBorder" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color: rgb(111, 111, 111);" />
<stop offset="53%" style="stop-color: rgb(255, 255, 255);" />
<stop offset="100%" style="stop-color: rgb(160, 160, 160);" />
</linearGradient>
<linearGradient id="fret" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color: rgb(122, 117, 113);" />
<stop offset="100%" style="stop-color: rgb(56, 49, 43);" />
</linearGradient>
<filter id="dropshadow" height="400%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="4" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="1.5"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- ... -->
</svg>
I use a polygon for the basic structure, rects and polygons for the strings, a path for the frets:
<svg ...>
<!-- ... -->
<polygon
points="
-10,300 1860,300 1950,230 2380,400
1950,570 1860,500 -10,500
"
fill="url(#fretboard)"
stroke-width="10"
stroke="url(#fredboardBorder)"
style="filter:url(#dropshadow)"
stroke-linejoin="round"
/>
<path
d="
M110 305 110 495 M220 305 220 495 M330 305 330 495 M440 305 440 495
M550 305 550 495 M660 305 660 495 M770 305 770 495 M880 305 880 495
M990 305 990 495 M1100 305 1100 495 M1210 305 1210 495 M1320 305 1320 495
M1430 305 1430 495 M1540 305 1540 495 M1650 305 1650 495 M1760 305 1760 495
M1858 305 1858 495
"
stroke-width="10"
stroke="rgb(122, 117, 113)"
/>
<rect class="string" x="0" y="324.3" width="1864" height="5" fill="#ccc" />
<rect class="string" x="0" y="353.6" width="1864" height="5" fill="#ccc" />
<rect class="string" x="0" y="382.9" width="1864" height="5" fill="#ccc" />
<rect class="string" x="0" y="412.2" width="1864" height="5" fill="#ccc" />
<rect class="string" x="0" y="441.5" width="1864" height="5" fill="#ccc" />
<rect class="string" x="0" y="470.8" width="1864" height="5" fill="#ccc" />
<polygon points="1863,324.3 1980,290 1980,295 1863,329.3" fill="#ccc" />
<polygon points="1863,353.6 2065,330 2065,335 1863,358.6" fill="#ccc" />
<polygon points="1863,382.9 2150,365 2150,370 1863,387.9" fill="#ccc" />
<polygon points="1863,412.2 2150,445 2150,450 1863,417.2" fill="#ccc" />
<polygon points="1863,441.5 2065,475 2065,480 1863,446.5" fill="#ccc" />
<polygon points="1863,470.8 1980,505 1980,510 1863,475.8" fill="#ccc" />
<circle cx="1980" cy="510" r="20" fill="url(#fretboard)" stroke-width="15" stroke="url(#fredboardBorder)" />
<circle cx="2065" cy="480" r="20" fill="url(#fretboard)" stroke-width="15" stroke="url(#fredboardBorder)" />
<circle cx="2150" cy="445" r="20" fill="url(#fretboard)" stroke-width="15" stroke="url(#fredboardBorder)" />
<circle cx="2150" cy="365" r="20" fill="url(#fretboard)" stroke-width="15" stroke="url(#fredboardBorder)" />
<circle cx="2065" cy="330" r="20" fill="url(#fretboard)" stroke-width="15" stroke="url(#fredboardBorder)" />
<circle cx="1980" cy="290" r="20" fill="url(#fretboard)" stroke-width="15" stroke="url(#fredboardBorder)" />
</svg>
And this is what it looks like:
Not the prettiest guitar ever, but gotta love it anyways! Now, let's make it playable with some JS and CSS!
Clamping the guitar strings in place
For those of you familiar with guitars/music theory, I will use the standard tune of E A d g h e
. These are the notes played when no fret is pressed down. Each fret increases these by half a note, so for the first string, that would be this:
E2 > F2 > Gb2 > G2 > Ab2 > A2 > Bb2 > H2 > C3 > Db3 > D3 Eb3 > E3 > ...
Once there's a wrap-around, the octave increaes by one and the circle starts anew. With a little help of my friends, I came up with this map of notes:
const noteMap = [
['Ab3', 'G3 ', 'Gb3', 'F3 ', 'E3 ', 'Eb3', 'D3 ', 'Db3', 'C3 ', 'B2 ', 'Bb2', 'A2 ', 'Ab2', 'G2 ', 'Gb2', 'F2 ', 'E2 '],
['Db4', 'C4 ', 'B3 ', 'Bb3', 'A3 ', 'Ab3', 'G3 ', 'Gb3', 'F3 ', 'E3 ', 'Eb3', 'D3 ', 'Db3', 'C3 ', 'B2 ', 'Bb2', 'A2 '],
['Gb4', 'F4 ', 'E4 ', 'Eb4', 'D4 ', 'Db4', 'C4 ', 'B3 ', 'Bb3', 'A3 ', 'Ab3', 'G3 ', 'Gb3', 'F3 ', 'E3 ', 'Eb3', 'D3 '],
['B4 ', 'Bb4', 'A4 ', 'Ab4', 'G4 ', 'Gb4', 'F4 ', 'E4 ', 'Eb4', 'D4 ', 'Db4', 'C4 ', 'B3 ', 'Bb3', 'A3 ', 'Ab3', 'G3 '],
['Eb5', 'D5 ', 'Db5', 'C5 ', 'B4 ', 'Bb4', 'A4 ', 'Ab4', 'G4 ', 'Gb3', 'F4 ', 'E4 ', 'Eb4', 'D4 ', 'Db4', 'C4 ', 'B3 '],
['Ab5', 'G5 ', 'Gb5', 'F5 ', 'E5 ', 'Eb5', 'D5 ', 'Db5', 'C5 ', 'B4 ', 'Bb4', 'A4 ', 'Ab4', 'G4 ', 'Gb4', 'F4 ', 'E4 ']
]
(Note that I'm going right to left here, because the lowest note is near the head.)
Now I need to make the strings clickable. Ideally, I add clickable areas to every fret for every string in order to figure out where a string was picked to figure out the note to play. I do that with JS by adding them to the SVG dynamically. I also add a global flag called isPlaying
to determine if the mouse is pressed or not. The playNote()
function currently outputs the note that will be played.
let isPlaying = false
function playNote (stringKey, note, force = false) {
if (isPlaying || force) {
console.log(note)
}
}
window.addEventListener('mousedown', () => {
isPlaying = true
})
window.addEventListener('mouseup', () => {
isPlaying = false
})
const svg = document.querySelector('#guitar')
noteMap.forEach((string, stringKey) => {
string.forEach((note, noteKey) => {
const area = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
area.setAttribute('x', noteKey * 110)
area.setAttribute('y', 315 + (29.3 * stringKey))
area.setAttribute('width', 110)
area.setAttribute('height', 20)
area.setAttribute('fill', '#fff')
area.setAttribute('opacity', '0')
area.addEventListener('click', () => {
playNote(stringKey, note, true)
})
area.addEventListener('mouseover', () => {
playNote(stringKey, note, false)
})
svg.appendChild(area)
})
})
Let's see it in action:
Next, I add an animation to the played string for three seconds to give the user a visual feedback on which string was picked:
const stringVibrationTimes = [0, 0, 0, 0, 0, 0]
const strings = Array.from(document.querySelectorAll('.string'))
setInterval(() => {
strings.forEach((stringEl, key) => {
if (stringVibrationTimes[key] > 0) {
stringEl.classList.add('vibrating')
} else {
stringEl.classList.remove('vibrating')
}
stringVibrationTimes[key] -= 50
if (stringVibrationTimes[key] < 0) {
stringVibrationTimes[key] = 0
}
})
}, 50)
function playNote (stringKey, note, force = false) {
if (isPlaying || force) {
console.log(note)
stringVibrationTimes[stringKey] = 3000
}
}
And some CSS:
@keyframes vibrate {
0% {
transform: translateY(-2px);
}
50% {
transform: translateY(2px);
}
100% {
transform: translateY(-2px);
}
}
.string {
transform: translateY(0);
}
.string.vibrating {
animation: vibrate .05s infinite;
}
Looks amazing:
We're half way there, now there's only the sound missing!
Crank up the amp!
To make it play sounds, I use a Midi sound font. I'll use midi-js-soundfonts because I like the sound of it. I'm using the instrument electric_guitar_clean
of FluidR3_GM
. I needed to download the sound font and put into a folder called sound/
in order to make it available to the browser. To play the sound, I use Audio
:
const soundFontUrl = './sound/'
function playNote (stringKey, note, force = false) {
if (isPlaying || force) {
console.log(note)
const audio = new Audio(soundFontUrl + note.trim() + '.mp3')
audio.play()
stringVibrationTimes[stringKey] = 3000
}
}
And here's the fully working demo - Playing either by clicking on the strings separately or by holding your mouse down and swiping across the strings:
EDIT: Grab the pick!
In the comments devgrv suggested to add a pick as the cursor - which is just what I did, thank you for this idea!
So, first I created an SVG for the guitar pick. I looked for a good shape online and redrew that with a path and some bezier curves:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1280 1280" preserveAspectRatio="xMidYMid meet" width="80" height="80">
<defs>
<linearGradient id="pickbg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:rgb(77, 22, 22);" />
<stop offset="100%" style="stop-color:rgb(150, 47, 47);" />
</linearGradient>
</defs>
<g transform="rotate(135, 640, 640)">
<path
d="M120 310 C 330 -10 950 -10 1160 310 Q 980 1100 640 1210 Q 300 1100 120 310 Z"
fill="url(#pickbg)"
/>
</g>
</svg>
It's important to make the SVG smaller (up to 128 by 128) with width
and height
attributes, because all larger SVGs are ignored by the browser. Next, I only needed to apply that new cursor image to the body:
body {
/* ... */
cursor: url(./pick.svg), auto;
}
And done:
Nice, all ready to rock!
Takeaway thoughts
That was even more fun than the self-made WYSIWYG markdown editor for Vue! Playing this thing is really hard and I'm sure the SVG could be optimized here and there, but it works. If you enjoyed this post, tell your friends and shout it out loud!
I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a ❤️ or a 🦄! I write tech articles in my free time and like to drink coffee every once in a while.
If you want to support my efforts, buy me a coffee ☕ or follow me on Twitter 🐦!
Top comments (48)
Reading that makes me wanna grab my guitar 😁
Anyway really cool project !
Glad you liked it! 😃 I miss concerts to be honest.
Same here
Stuff like this blows my mind. There's no end to creativity. Loved the idea
Thank you very much! I originally wanted to build this as an Easter egg for a website but thought that it's way too awesome not to share 😁
change the cursor to a pick on hover...It would be more awesome
And done, thank you for the suggestion. Really adds to the whole experience!
Awesome idea! I'll do this later today and edit the post, thank you very much!
Huge! 🤘
Never thought about something like that. That could be a good base for a webapp that plays music and displays notes at the same time.
Oh, that would be amazing to learn instruments! I was starting to think about a remote band simulator where a user can play an instrument (guitar, keyboard, drums, bass, etc.) and the other "players" can hear what everyone else is playing :D
Haha yeah, or even an online/improved "Band Hero". Lot of good ideas can emerge from this project of yours. 😀
That's a brilliant idea, actually! I think the next step could be to add a second instrument and connect two instances.
Amazing project
And I learned a lot through this 😅
Awesome to hear! Which part was most helpful to you?
Almost all of it, because I am a beginner and it is fascinating to learn what those commands are :grim:
I'm very glad this post was so helpful to you! It's important to keep learning, it keeps the brain fit and is fun. :)
Haha yess
That's a lot of event handlers. Why not use just one with a simple function on mouse x,y coordinates to resolve the fret?
You're absolutely right! I wanted to go with the path of least resistance here, since it's basically a prototype and a lot of things still need some improvement.
Working with X/Y coordinates only is always a bit tedious, since some browsers handle some things differently (or even output different values for the same functions). Also, the SVG is scaling with the browser window, which is added complexity to the calculation. Using all these handlers just seemed like a simpler option code-wise to get it working for the time being.
I could imagine a mixture of the two approaches, though: Add the notes of the frets as data attributes (for example:
data-note="E4"
), add single event listeners for mouseover and click, determine the the fret (viaelementFromPoint
or similar), read out the note and play it. This would also solve the mobile/touch support mentioned in a another comment by Isitar.What do you think?
That is awesome!!!
How did you get the .mp3 notes though?
Glad you liked it! I downloaded an open source midi soundfont I found on Github: github.com/gleitz/midi-js-soundfonts.
Ah thanks! I had a loon but didn't get into the folders, just the README!
There's a myriad of instruments there, actually. I used to write some music ages ago with a tool called Guitar Pro. That tool uses midi to represent the notes. That's how I knew about midi fonts in the first place.
This was some real 🔥🔥🔥🔥🔥🔥🔥🔥🔥
So glad you liked it! :D
Next project: Mobile support :)
That's a bit tricky, actually!
touchstart
andtouchend
can be used to replace theclick
,mousedown
andmouseup
events on mobile, but there's no equivalent ofmouseover
for touch events. But I could trytouchemove
, that might work. Got any other idea how to solve this?I guess
touchmove
is the only option you got here. Since you already havex
,y
,width
andheight
on your strings you can map thex
andy
of your touchmove event to the appropriate area. The simplest approach would be to select all areas and filter(maybe double check that y condition if y is not 0 on the top left ;), also maybe double check the borders and substitute
<=
with<
or>=
with>
)This could also be used to replace all the click handlers on all the rects and improve performance a bit on slower devices, too! I'll definitely do that in the second version of this.
How to sound open strings?
If by "open" you mean without holding any of the frets (I'm not really an expert in those terms) , they can be played right next to the head of the guitar. Does that answer your question? 🙂
Hi Pascal, yes I do mean without any frets pressed is an open string.
And yes, plucking the string between the machine and the nut does give the open note.
I'm very impressed! Have fun!
Thank you so much, I'm happy that you like it! 😄
Some comments may only be visible to logged-in visitors. Sign in to view all comments.