DEV Community

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

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

Oldest 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
 
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
 
tawn33y profile image
Tony

This is pretty cool!!

Collapse
 
thormeier profile image
Pascal Thormeier

Thank you! Glad you like it :D

Collapse
 
devgourav profile image
devgrv

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

Collapse
 
thormeier profile image
Pascal Thormeier

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

Collapse
 
thormeier profile image
Pascal Thormeier

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

Collapse
 
fakorededamilola profile image
Fakorede Damilola

This is just awesome, thank you for this.

Collapse
 
thormeier profile image
Pascal Thormeier

You're very welcome! I love to share my ideas and hope to inspire people by doing that :)

Collapse
 
boypanjaitan16 profile image
Boy Panjaitan

Just amazing

Collapse
 
thormeier profile image
Pascal Thormeier

Thank you for your kind feedback!

Collapse
 
boypanjaitan16 profile image
Boy Panjaitan • Edited

Just Amazing

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

Some comments may only be visible to logged-in visitors. Sign in to view all comments.