Build a Guitar with Reactjs (3): Fretboard-SVG

Build a Guitar with Reactjs (3): Fretboard-SVG

·

12 min read

Hi and welcome to part 3 of "How to build a Guitar with React" 🎸

After implementing the guitar sounds in part 2, this part will be all about visuals: The goal is to build a realistic fretboard, with the width of each fret gradually decreasing.

🎸 The Fretboard Component

I'll start with a reminder what the final fretboard will look like:

fretboard.jpg

It will have three child components:

  • a stateless <FretboardBg /> component (the background doesn't have to constantly re-render when I move the mouse over the fretboard, or clutter up the components that are responsible for functionality)
  • a <FretMarkers /> component where all the action happens
  • a <MuteBtns /> component

I'll start with <FretboardBg />. There's certainly a number of ways how to accomplish this - I could simply stuff some evenly-sized fret-<div>s into a container-<div>, but how lame would that be 😬 when we have JavaScript to build a realistic fretboard. I could also just use an image, but I'd have to know where each fret starts and ends to place the markers at their correct positions later. So I've decided to build an SVG for this.

🎸 Fretboard Setup

First of all, let's add the files and import them where we need them. In the Components folder, I'll create a parent component called Fretboard.js and its first child component FretboardBg.js.

FretboardBg.js

import React from 'react';

function FretboardBg(){

    return (
        <div className="fretboard-bg">

        </div>
    )
}

export default FretboardBg;

Fretboard.js

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

function Fretboard(){
    return (
        <div className="fretboard-container">
            <FretboardBg />
        </div>
    )
}

export default Fretboard;

Guitar.js

import React, { useState } from 'react';
import GuitarBody from './GuitarBody.js';
import Fretboard from './Fretboard.js';

function Guitar(){

    const [currFrets, setCurrFrets] = useState([0,0,0,0,0,0]);

    return (
        <div className="guitar">
            <GuitarBody currFrets={currFrets} />
            <Fretboard />
        </div>
    )
}

export default Guitar;

Because most of the styling will be happening inside the SVG, there's only little CSS needed for this part:

App.css

.fretboard-container {
    display:flex;
    overflow:hidden;
    position:relative;
}
.fretboard-container svg {
    display:block;
}
.fretboard-bg {
    background:#402f1f;
}

🎸 Fretboard background SVG

If you've never used inline-SVGs before, here's your entry point to the MDN docs about SVG. The things I'll use are nothing fancy and fairly straightforward, though, and I'll explain things along the way. The only elements we need are line, rect and circle.

What's nice about SVG is that you have a coordinate system (similar to the canvas element), so you can then use JavaScript to put all the lines for the strings and frets where they belong.

To start, I'll add another file to the utils folder called fretboardValues.js, where I do all the calculations.

The src folder should now look like this:

project-folder-structure-2.jpg

Setting up the SVG

For the size of the fretboard, I guess I could just hardcode some values, but I'd like to keep it a little more customisable, so the first thing that goes into my fretboardValues.js is an object for the dimensions.

const fbSize = {
    width:720,
    height:96
}

export { fbSize }

Now in FretboardBg.js, I can import that and start with an empty SVG container:

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

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

export default FretboardBg;

The viewBox attribute on the SVG defines the coordinate system. It goes from x=0, y=0 (upper left corner) to x=720, y=96 (lower right corner). The width and height attributes are the CSS properties that define the actual size (in px). In my SVG, they have the same values as the viewBox, but they absolutely don't have to.

So far, the fretboard is just a brown rectangle, but we'll soon start filling it with strings and frets. If you've applied the same CSS as me, the guitar has a width of 768px with 16px padding, so the total width available for the fretboard is 736px. The SVG is only 720px in width, because the remaining 16px are reserved for the mute buttons on the left of the fretboard.

I'm using fixed units and overflow:hidden on the .fretboard-container because I don't want the SVG to responsively shrink when the size of the browser window gets narrower. Instead, there'll just be less frets visible, while everything else in the layout stays the same.

Alright, the SVG is set up - now we need to figure out the coordinates of the strings and frets.

The fretboardValues.js file - some fretboard maths

The (vertical) positions of the strings are easily calculated, as they're evenly distributed along the y-axis. Adding the function that'll do that for me to fretboardValues.js:

function createStringPositions(){
    return Array(6).fill(0).map((s,i) => (5.5-i) * fbSize.height/6);
}

The distances of the frets from the nut depend on the fret number and the total length of the string (=scale length). Fretting a string means effectively making it shorter - if you have a string with a certain length, a string with length · 2^(-1/12) is one half tone higher. We can put this into a formula:

guitar-scale-length-formula.jpg

I don't have the whole guitar in my SVG, only the fretboard, so I don't know the scale length, but I know that I'd like my guitar to have 20 frets, and I'd like the last fret to be located at 95% of the total width of the fretboard (that will give some space to the right).

I'd also like some space to the left, so the first entry in my array of distances won't be a fret, but the position of the nut. Turning that into a function:

function createFretPositions(){
    const frets = 20;
    const dMax = 0.95; 

    const scaleLen = dMax / (1 - Math.pow(2, -(frets + 1)/12));

    const perc = Array(frets + 1).fill(0)
        .map((d, n) => scaleLen * (1 - Math.pow(2, -(n + 1)/12)));

    return perc.map(f => f * fbSize.width);
}

And we're done with this file, those values are all we need to build the fretboard. Exporting everything:

const stringPositions = createStringPositions();
const fretPositions = createFretPositions();

export { fbSize, stringPositions, fretPositions }

Building the SVG

Updating the imports in FretboardBg.js:

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

The strings and frets will be line elements, which are defined by their start and end coordinates (x1,y1 and x2,y2). We can add them by mapping over the stringPositions and fretPositions arrays and setting their coordinates and other attributes (the id isn't really necessary, but it helps when inspecting the DOM):

FretboardBg.js

const strings = stringPositions.map((pos, i) => {
        return (
            <line
                key={`string-${i}`}
                id={`string-${i}`}
                x1="0"
                y1={pos}
                x2={fbSize.width}
                y2={pos}
                stroke="#ddd"
                strokeWidth="2"
            />
        )
    })

const frets = fretPositions.map((pos, i) => {
        return (
            <line
                key={`fret-${i}`}
                id={`fret-${i}`}
                x1={pos}
                y1="0"
                x2={pos}
                y2={fbSize.height}
                stroke="#b93"
                strokeWidth="3"
            />
        )
    })

I'll also add a dark rectangle as some visual indication for where the nut is and where the first fret begins:

const nut = <rect
        x="0"
        y="0"
        width={fretPositions[0]}
        height={fbSize.height}
        fill="rgba(0,0,0,0.5)"
    />

Throwing it all into the SVG...

return (
        <div className="fretboard-bg">
            <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox={`0 0 ${fbSize.width} ${fbSize.height}`}
                width={`${fbSize.width}`}
                height={`${fbSize.height}`}
            >
                {nut}
                {frets}
                {strings}
            </svg>
        </div>
    )

...and voilà 😃

Bonus: Fretboard dots

A real guitar fretboard has dots on certain frets that serve as a visual guide. Often, the 12th fret (where the note is 12 halftones = 1 octave higher) has two dots. I want my fretboard to be pitch perfect ✨ so those have to be added no matter what.

A circle element in an SVG is defined by its radius r and the coordinates cx,cy of its center point. The values of those can again be retrieved from the fretPositions and fbSize:

const dotFrets = [3, 5, 7, 9, 12, 15, 17, 19];

const dots = dotFrets.map((fret,i) => {
    if (fret === 12){
        return (
            <g key={`dot-${fret}`} id={`dots-${fret}`}>
                <circle
                    cx={(fretPositions[fret]+fretPositions[fret-1]) / 2}
                    cy={2 * fbSize.height/6}
                    r="3"
                    fill="#a98"
                />
                <circle
                    cx={(fretPositions[fret]+fretPositions[fret-1]) / 2}
                    cy={4 * fbSize.height/6}
                    r="3"
                    fill="#a98"
                />
            </g>
        )
    }
    return (
        <circle
            key={`dot-${fret}`}
             id={`dot-${fret}`}
             cx={(fretPositions[fret]+fretPositions[fret-1]) / 2}
             cy={fbSize.height/2}
             r="3"
             fill="#a98"
         />
    )
})


return (
        <div className="fretboard-bg">
            <svg
               ...
            >
                {nut}
                {frets}
                {strings}
                {dots}
            </svg>
        </div>
    )

The <g> (= group) element is just a container to wrap both circles, similar to a <div> in HTML.

That's it, we have a fully realistic looking fretboard now 🤟


🎸 Outlook for the next part

In part 4, we'll...

  • add the <MuteBtns /> component
  • let it update the so far unused currFrets array (finally that linter warning will go away)
  • make some changes to sounds.js and Guitar.js to implement the mute functionality

(Code for this part here 👉 on GitHub)


🎸 Thank you for reading!

I hope you enjoyed coding along so far. 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 👋