Build a Guitar with Reactjs (5): The Final Fretboard

Build a Guitar with Reactjs (5): The Final Fretboard

Subscribe to my newsletter and never miss my upcoming articles

Hallo and welcome to the final part of "How to build a Guitar with React" 🎸

In part 4, we've added the mute buttons to the fretboard. In this part, we'll finally add the possibility to play different notes on the fretboard, to mark a whole chord, and to play it on the body.

If you remember from playing with the demo, you'll see a marker (white circle) when you hover over the fretboard, to indicate which string and fret you're currently on. A click on the board will then

  • play the corresponding note
  • set the position of the marker to the played fret
  • update the currFrets array

The <FretMarkers /> component will be very similar to the <FretboardBg /> component, in that it'll also be an SVG with the same dimensions, with its position set to absolute to layer it above the background. Additionally, it'll contain all the logic for playing the fretboard and updating the fret markers.

Once again for reference:

fretboard.jpg


🎸 Setting up the SVG

The SVG will be set up identically to the fretboard background SVG, so I'll import the fretboard size object from my fretboardValues.js, and start with an empty SVG container again:

FretMarkers.js

import React from 'react';
import { fbSize } from '../utils/fretboardValues.js';

function FretMarkers({ currFrets, updateCurrFrets }){
    return (
        <div className="fret-markers">
            <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox={`0 0 ${fbSize.width} ${fbSize.height}`}
                width={`${fbSize.width}`}
                height={`${fbSize.height}`}
            >
            </svg>
        </div>
    )
}

Updating the fretboard component:

Fretboard.js

import React from 'react';
import MuteBtns from './MuteBtns.js';
import FretboardBg from './FretboardBg.js';
import FretMarkers from './FretMarkers.js';

function Fretboard({ currFrets, updateCurrFrets }){

    return (
        <div className="fretboard-container">
            <MuteBtns
                currFrets={currFrets}
                updateCurrFrets={updateCurrFrets}
            />
            <FretboardBg />
            <FretMarkers
                currFrets={currFrets}
                updateCurrFrets={updateCurrFrets}
            />
        </div>
    )
}

export default Fretboard;

And a little CSS:

.fret-markers {
    position:absolute;
    top:0;
    left:16px;
}

.fret-markers circle {
    pointer-events:none;
}

The first thing I'm going to add to the SVG is the single marker that will only be visible on hover.


🎸 The single marker

The single marker will be a circle element, with its coordinates depending on the hovered string and fret. I'll use the position arrays from fretboardValues.js for this, and I'll also store the string and fret in state. Updating the imports for this:

FretMarkers.js

import React, { useState } from 'react';
import { fbSize, fretPositions, stringPositions } from '../utils/fretboardValues.js';

Setting up state:

const [string, setString] = useState(-1);
const [fret, setFret] = useState(0);

I'm initialising the string variable with -1 to indicate that I'm currently not hovering over the board, so the marker isn't visible. It should only render when string has a value between 0 and 5, which I'll do with a short-circuited conditional again.

But first, the JSX for the circle:

const fretMarker = <circle
    cx={`${fretPositions[fret] - 6}`}
    cy={`${stringPositions[string]}`}
    r="6"
    fill="rgba(255,255,255,0.7)"
    stroke="#fff"
/>

The fretboard will also get two mouse event listeners to show/hide the marker, which boils down to setting the state variables string and fret according to the mouse position.

    return (
        <div className="fret-markers">
            <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox={`0 0 ${fbSize.width} ${fbSize.height}`}
                width={`${fbSize.width}`}
                height={`${fbSize.height}`}

                onMouseMove={showMarker}
                onMouseLeave={hideMarker}
            >

            {string > -1 && fretMarker}

            </svg>
        </div>
    )

The mouse event callbacks

The hideMarker function is simple, it just sets the string value to -1:

function hideMarker(){
    setString(-1);
}

For the showMarker function, I'll get the x and y coordinates from the event's offsetX and offsetY. From there, I can derive the current fret and string and update state accordingly:

function showMarker(e){
    let s = Math.floor(6/fbSize.height * (fbSize.height - 1 - e.nativeEvent.offsetY));
    let f = fretPositions.findIndex(pos => e.nativeEvent.offsetX < pos);
    f = f === -1 ? 0 : f;
    setString(s);
    setFret(f);
}

The third line of the function takes care of the case when I'm hovering over the far right of the fretboard. There's no fret anymore, so the .findIndex() method returns -1. That's not the correct value for my fret, though. If you remember from the previous posts, a fret value of -1 means that a string is fretted at the first fret, but muted, which is not the case here. Hovering over the far right would mean that I'm playing an open string, so the fret value should be 0.

That's it for the single marker. Next, the functionality to actually play the marked string and fret.


🎸 Playing on the fretboard

I already have a playNote function in my sounds.js which takes a string and a fret as parameter, so I'll export/import that:

sounds.js

export { playNote, playGuitarBody }

FretMarkers.js

import { playNote } from '../utils/sounds.js';

Now that I have the current string and fret, adding the click event to play a note is quickly done:

    return (
        <div className="fret-markers">
            <svg
                /* ... */

                onMouseMove={showMarker}
                onMouseLeave={hideMarker}

                onClick={()=>{
                    playNote(string, fret);
                }}
            >

            {string > -1 && fretMarker}

            </svg>
        </div>
    )

Finally, we can play all notes 🎵 😃 🎵

🎸 The other fret markers

The current frets should be indicated by additional markers, one for each string. I'm already passing down the currFrets array from the <Guitar /> component, so I'll map over that to create the JSX. If the fret isn't muted, I'll append a circle, and set its coordinates according to the current fret and string it's on. I'll also export and import that little helper function isMuted from sounds.js:

sounds.js

export { playGuitarBody, playNote, isMuted }

FretMarkers.js

import { playNote, isMuted } from '../utils/sounds.js';

Creating and rendering the circles:

FretMarkers.js

const fretMarkers = currFrets.map((f, i) => {
        if (!isMuted(f)){
            return (
                <circle
                    key={i}
                    cx={`${fretPositions[f] - 6}`}
                    cy={`${stringPositions[i]}`}
                    r="6"
                    fill="rgba(255,255,255,0.5)"
                    stroke="#fff"
                />
            )
        };
        return true;
    });

    return (
        <div className="fret-markers">
            <svg
                /* ... */
            >

            {string > -1 && fretMarker}
            {fretMarkers}

            </svg>
        </div>
    )

And there are the markers! I can already mute each one of them, essentially making them disappear, I just can't change their positions yet, but to test if the markers are placed correctly, I could initialise the currFrets in my <Guitar /> component with [-0,0,2,2,1,0], for example - and the fret markers would now be set to show an A-minor chord.

🎸 Updating the current frets

The function that updates the current frets already exists, but so far, it's only called by the mute buttons. I'll have to add a couple of things to make the same function work with a click on the fretboard. The current version in Guitar.js looks like this:

function updateCurrFrets(string){
    let newFrets = [...currFrets];
    newFrets[string] = newFrets[string] * -1;
    setCurrFrets(newFrets);
}

It only takes a guitar string as parameter and mutes/unmutes that string. To add the functionality that I need for a fretboard click, I'll also have to pass in a fret value. If the function is called with a fret parameter, the fret should be updated accordingly - otherwise, mute/unmute the string by multiplying it with -1:

Guitar.js

function updateCurrFrets(string, fret){
    let newFrets = [...currFrets];
    newFrets[string] = fret !== undefined ? fret : newFrets[string] * -1;
    setCurrFrets(newFrets);
}

A little addition to the click callback:

    return (
        <div className="fret-markers">
            <svg
                /* ... */

                onMouseMove={showMarker}
                onMouseLeave={hideMarker}

                onClick={()=>{
                    playNote(string, fret);
                    updateCurrFrets(string, fret);
                }}
            >

            {string > -1 && fretMarker}
            {fretMarkers}

            </svg>
        </div>
    )

And that's it, the guitar is finished and ready to be played! We can

  • play notes on the fretboard
  • set the markers so they form a chord
  • mute and unmute strings
  • play the whole chord or parts of it on the body

Happy strumming 🤟

Feel free to customise and optimise. One important part of functionality that I soon missed when playing is the option to store chords, and access them through a button. This would obviously add a whole new layer of complexity, but at the same time it's an interesting challenge, and it would add a lot of value to the application. So, there's a good chance that this tutorial might be continued at some point.


(Code for the final part here 👉 on GitHub)


🎸 Thank you for reading!

I hope you enjoyed coding along. This was my first tutorial series, so I'm curious whether you found this helpful and easy to follow, or if something was annoying or confusing.

As usual - if you found any mistakes, if something isn't working as expected, if you see room for improvement anywhere, or if you just happen to like this post, I'd appreciate your feedback in the comment section below. You can also @ me on Twitter. If you'd like to read more like this, I invite you to subsribe to my newsletter. Until next time 👋

 
Share this