DEV Community

Cover image for Let's build an actual working Guitar🎸 with JavaScript 💻🤘
Pascal Thormeier
Pascal Thormeier

Posted on • Edited on

Let's build an actual working Guitar🎸 with JavaScript 💻🤘

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

And this is what it looks like:

Image of a guitar neck and head

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 > ...


Enter fullscreen mode Exit fullscreen mode

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 ']
]


Enter fullscreen mode Exit fullscreen mode

(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)
  })
})


Enter fullscreen mode Exit fullscreen mode

Let's see it in action:

A gif showing the SVG guitar with strings 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
  }
}


Enter fullscreen mode Exit fullscreen mode

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;
}


Enter fullscreen mode Exit fullscreen mode

Looks amazing:

Vibrating guitar strings closeup, as programmed above

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
  }
}


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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;
}


Enter fullscreen mode Exit fullscreen mode

And done:

Gif of the guitar with a pick as cursor

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 🐦!

Buy me a coffee button

Top comments (48)

Collapse
 
keisay profile image
Kevin Sengsay

Reading that makes me wanna grab my guitar 😁
Anyway really cool project !

Collapse
 
thormeier profile image
Pascal Thormeier

Glad you liked it! 😃 I miss concerts to be honest.

Collapse
 
himanshutiwari15 profile image
Himanshu Tiwari 🌼

Same here

Collapse
 
monfernape profile image
Usman Khalil

Stuff like this blows my mind. There's no end to creativity. Loved the idea

Collapse
 
thormeier profile image
Pascal Thormeier

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 😁

Collapse
 
devgourav profile image
devgrv

change the cursor to a pick on hover...It would be more awesome

Collapse
 
thormeier profile image
Pascal Thormeier

And done, thank you for the suggestion. Really adds to the whole experience!

Collapse
 
thormeier profile image
Pascal Thormeier

Awesome idea! I'll do this later today and edit the post, thank you very much!

Collapse
 
laegel profile image
Laegel

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.

Collapse
 
thormeier profile image
Pascal Thormeier

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

Collapse
 
laegel profile image
Laegel

Haha yeah, or even an online/improved "Band Hero". Lot of good ideas can emerge from this project of yours. 😀

Thread Thread
 
thormeier profile image
Pascal Thormeier

That's a brilliant idea, actually! I think the next step could be to add a second instrument and connect two instances.

Collapse
 
himanshutiwari15 profile image
Himanshu Tiwari 🌼

Amazing project
And I learned a lot through this 😅

Collapse
 
thormeier profile image
Pascal Thormeier

Awesome to hear! Which part was most helpful to you?

Collapse
 
himanshutiwari15 profile image
Himanshu Tiwari 🌼

Almost all of it, because I am a beginner and it is fascinating to learn what those commands are :grim:

Thread Thread
 
thormeier profile image
Pascal Thormeier

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. :)

Thread Thread
 
himanshutiwari15 profile image
Himanshu Tiwari 🌼

Haha yess

Collapse
 
ashleyo profile image
Ashley Oliver

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?

Collapse
 
thormeier profile image
Pascal Thormeier

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 (via elementFromPoint 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?

Collapse
 
victorigualada profile image
victorigualada

That is awesome!!!
How did you get the .mp3 notes though?

Collapse
 
thormeier profile image
Pascal Thormeier

Glad you liked it! I downloaded an open source midi soundfont I found on Github: github.com/gleitz/midi-js-soundfonts.

Collapse
 
victorigualada profile image
victorigualada

Ah thanks! I had a loon but didn't get into the folders, just the README!

Thread Thread
 
thormeier profile image
Pascal Thormeier

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.

Collapse
 
bharathkalyans profile image
Bharath Kalyan S

This was some real 🔥🔥🔥🔥🔥🔥🔥🔥🔥

Collapse
 
thormeier profile image
Pascal Thormeier

So glad you liked it! :D

Collapse
 
isitar profile image
Isitar (Pascal Lüscher)

Next project: Mobile support :)

Collapse
 
thormeier profile image
Pascal Thormeier

That's a bit tricky, actually! touchstart and touchend can be used to replace the click, mousedown and mouseup events on mobile, but there's no equivalent of mouseover for touch events. But I could try touchemove, that might work. Got any other idea how to solve this?

Collapse
 
isitar profile image
Isitar (Pascal Lüscher) • Edited

I guess touchmove is the only option you got here. Since you already have x,y,width and height on your strings you can map the x and y of your touchmove event to the appropriate area. The simplest approach would be to select all areas and filter

const area = document.querySelectorAll('rect')
    .find(area => event.x >= area.x 
        && event.x <= area.x + area.width 
        && event.y >= area.y 
        && event.y <= area.y + area.height
    )

area.click();
Enter fullscreen mode Exit fullscreen mode

(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 > )

Thread Thread
 
thormeier profile image
Pascal Thormeier

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.

Collapse
 
paulserridge profile image
paulserridge

How to sound open strings?

Collapse
 
thormeier profile image
Pascal Thormeier

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? 🙂

Collapse
 
paulserridge profile image
paulserridge

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!

Thread Thread
 
thormeier profile image
Pascal Thormeier

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.