Let's build an actual working Guitar🎸 with JavaScript 💻🤘
Tags: javascript, css, webdev, tutorial
Published: 2021-02-02 18:04:54

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](https://youtu.be/SwYN7mTi6HM) 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](https://en.m.wikipedia.org/wiki/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:

```html
<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:

```html
<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:

![Image of a guitar neck and head](https://dev-to-uploads.s3.amazonaws.com/i/we0fwe6qiw59fjq2tef8.png)

Not the prettiest guitar ever, but [gotta love it](https://www.youtube.com/watch?v=i7auNuGLFlU) 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](https://youtu.be/0C58ttB2-Qg), I came up with this map of notes:

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

```javascript
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:

![A gif showing the SVG guitar with strings in action.](https://dev-to-uploads.s3.amazonaws.com/i/tbm5w0vh47opcl227d4k.gif)

Next, I add an animation to the played string for three seconds to give the user a visual feedback on which string was picked:

```javascript
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:

```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:

![Vibrating guitar strings closeup, as programmed above](https://dev-to-uploads.s3.amazonaws.com/i/shmt6yqf3wn8uvmt55ps.gif)

[We're half way there](https://www.youtube.com/watch?v=lDK9QqIzhwk), 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](https://github.com/gleitz/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`:

```javascript
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:

{% codesandbox js-guitar-rf7cr %}

# EDIT: Grab the pick!

In the comments [devgrv](https://dev.to/devgourav) 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:

```html
<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:

```css
body {
  /* ... */
  cursor: url(./pick.svg), auto;
}
```

And done:

![Gif of the guitar with a pick as cursor](https://dev-to-uploads.s3.amazonaws.com/i/wo6lewchwda3vhqghpa3.gif)

Nice, all ready to rock!

# Takeaway thoughts

That was even more fun than the self-made [WYSIWYG markdown editor for Vue](https://dev.to/thormeier/build-your-own-wysiwyg-markdown-editor-for-vue-318j)! 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](https://www.youtube.com/watch?v=sohB922c2uQ)!

---

*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* ☕](http://buymeacoffee.com/pthormeier) *or* [*follow me on Twitter* 🐦](https://twitter.com/pthormeier)*!*

[![Buy me a coffee button](https://dev-to-uploads.s3.amazonaws.com/i/x55hp6jopwyy161d8e2u.png)](http://buymeacoffee.com/pthormeier)