Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

FretboardJs

5.00/5 (8 votes)
5 Aug 2014GPL39 min read 23.5K   16  
An SVG Guitar Fretboard Library

Introduction

The goal is to create a guitar fretboard framework for JavaScript. FretboardJs is rendered in an SVG element.

The consumer adds notes to the fretboard and uses the comprehensive chord and scale libraries to create sophisticated fretboard applications whilst ignoring details concerning fretboard logic.

An example app using FretboardJs can be found in the sample code. This app makes use of the chord library to render chord families. A guitarist enters chord names and the app presents the chord family, demonstrating various positions and fingerings.

Click here to view the 'Chord Explorer' demo app in action.

Using the Library

Let's see how to makes use of the FretboardJs library.

Firstly: include a link to the fretboard.version.js file and define an SVG with the following structure:

HTML
<svg id="svg" xmlns="http://www.w3.org/2000/svg">
    <g id="resources">
        <radialGradient id="fingering-dot-gradient" cx="60%" cy="40%" r="50%" fx="50%" fy="50%">
            <stop offset="0%" style="stop-color:rgb(255,0,0); stop-opacity:1"></stop>
            <stop offset="100%" style="stop-color:rgb(80,0,0);stop-opacity:1"></stop>
        </radialGradient>
        <linearGradient id="fretboard-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
            <stop offset="0%" style="stop-color:#2b2b2b"></stop>
            <stop offset="10%" style="stop-color:#191919"></stop>
            <stop offset="50%" style="stop-color:black"></stop>
            <stop offset="90%" style="stop-color:#191919"></stop>
            <stop offset="100%" style="stop-color:#2b2b2b"></stop>
        </linearGradient>
        <linearGradient id="head-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
            <stop offset="0%" style="stop-color:#3d3d3d"></stop>
            <stop offset="10%" style="stop-color:#191919"></stop>
            <stop offset="50%" style="stop-color:black"></stop>
            <stop offset="90%" style="stop-color:#191919"></stop>
            <stop offset="100%" style="stop-color:#3d3d3d"></stop>
        </linearGradient>
        <pattern id="pearl-inlay" patternUnits="userSpaceOnUse" width="100" height="100">
            <image xlink:href="/images/pearl-inlay.jpg" x="0" y="0" width="100" height="100"></image>
        </pattern>
    </g>
    <g id="fretboard-layer">
    </g>
    <g id="fingering-layer">
    </g>
</svg>

There are a number of important id tags here:

  • fingering-dot-gradient: radial gradient for fretboard dots.
  • fretboard-gradient: linear gradient for the fretboard neck.
  • head-gradient: linear gradient for the head piece.
  • pearl-inlay: image pattern for the fretboard inlay elements on frets 3, 5, 7, 9 and 12.
  • fretboard-layer: the SVG group where all static fretboard-neck elements (strings, frets, inlays) will be placed.
  • fingering-layer: the SVG group where all dots will be rendered.

The resources group defines some gradient colors used by the rendering code (placing it in the markup makes it easy for the user to edit these values. For example, the pearl-inlay image may simply be substituted for a solid-color or gradient or some other image. Or, the neck gradient can be easily altered directly in the markup.

Next, once the markup above is in place and the framework is loaded, the consuming code simply executes the following statement:

Fretboard.Neck.Draw(svg);

and this renders the entire fretboard within the SVG element. The result will look as follows:

Simple FretboardJs rendered neck

Fingerings are defined using the Finger object:

new Finger(fret, string, finger)

and we can add notes or 'dots' to the fretboard with:

Fretboard.Neck.AddDot(Finger);

so, any arbitrary note can be rendered with:

Fretboard.Neck.AddDot(new Finger(3, 5, 2));

which produces:

Fretboard with single note

What about Chords?

The Chord object is defined as a named collection of fingerings like this:

new Chord(name,
[
    new Finger(...),
    new Finger(...),
    new Finger(...),
    new Finger(...)
]

for example the collection of fingerings named 'AØ' looks like this:

new Chord('AØ',
[
    new Finger(5, 6, 2),
    new Finger(5, 4, 3),
    new Finger(5, 3, 4),
    new Finger(4, 2, 1),
]

There exists, however, a rich chord library in the Fretboard.Chords namespace (see below for the discussion of the Chord library). The Fretboard.Chords.Dominant7th object for instance is an array of Chord objects that represent dominant 7th fingerings. Similarly for Fretboard.Chords.MajorTriad and and many others.

Once we have a chord instance, we can call the DrawChord method, like so:

Fretboard.Neck.AddChord(Fretboard.Chords.Dominant7th[0]);

Resulting in:

A7 chord rendered on the fretboard

And that's it, pretty simple right?

For an example of the kind of app that can be built using FretboardJs, see the demo app 'Chord Explorer', which utilizes the FretboardJs library to explore different fingering positions for families of chords. An input text field allows a guitarist to enter a chord name and iterate through various fingerings. The chords are chosen from the chord library mentioned above.

Implementation

This section discusses implementation details of FretboardJs.

After defining the global namespace Fretboard, two objects that specify overall fretboard behavior are defined: Fretboard.Metrics and Fretboard.Neck.

Metrics

Fretboard.Metrics provides measures for values such as fretboard width, n-th fret length, string-position, dot-position.

Calibration

The Fretboard.Metric object is calibrated on initialization, at which point the available width of the SVG element is retrieved directly from the SVG's parent element. The maximum available with is used to set the width of the SVG element itself to 95% of the parent element's width. This Width becomes the 'length' of the fretboard, which is then used to determine the breadth of the neck or 'Height' rather, and the height of SVG element is set to this Height value. The Height is arbitrarily chosen to be 22.75% of the Width. (There is potential confusion in referring to these terms width/height/length/breadth, so we drop the distinction between neck specific terminology like length of neck or width of neck, and now refer only to the Width and Height of the SVG element.)

The Calibrate function then sets the position of the nut at 3% of Width and stores this value in private variable barPosition.

Since the SVG element uses 95% of the parent element's width, setting the left and right margins of the SVG element to 2.5% will create a fully responsive rendering.

function Calibrate(svg) {
    var fretboardApp = svg.parentNode;

    self.Width = Math.round(.95 * fretboardApp.clientWidth);
    self.Height = Math.round(0.2275 * self.Width);

    svg.setAttribute('width', self.Width + "px");
    svg.setAttribute('height', self.Height + "px");

    barPosition = self.Width * 0.0375;
}

Fret Position

Frets are position using the Fretboard.Metric.FretPosition(n) function. This function returns the position along the x-axis for the given fret using the standard luthier's function for fret widths.

function FretPosition(n) {
    var length = self.Width * 1.85;
    var position = barPosition + length - (length / Math.pow(2, (n / 12)));
    return position;
}

Finger Position

Fretboard.Metric.FingerPosition(n) returns the distance along the x-axis for a given fret n. This method returns the half-way point between fret n and fret (n-1). For example, the 1st fret finger position is half way between the 0-th fret and the first fret.

function FingerPosition(n) {
    if (n < 0 || n > highestFret) {
        throw "Argument out of range: N-th Fret must be between 0 and 9 inclusive. n = " + n;
    }
    var p = FretPosition(n - 1);
    var q = FretPosition(n);

    var position = p + (q - p) / 2;

    return position;
}

String Position

Finally, Fretboard.Metric.StringPosition(n) returns the position along the y-axis for the n-th string.

function StringPosition(n) {
    if (n < 1 || n > 7) {
        throw "Argument out of range: N-th String must be between 1 and 6 inclusive. n = " + n;
    }
    var result = 0.086 * self.Height + (n - 1) * 0.165 * self.Height;
    return result;
}

Neck

Fretboard.Neck is responsible for drawing the SVG elements including strings, frets, inlays, etc., and also for drawing fingerings. 

the method DrawFretboard(svg) takes an SVG element, configures the Metrics object by calling the Calibration function and then proceeds to render the neck, the inlays, the frets, strings and nut (or head).

The two important SVG groups 'fretboard-layer' and 'fingering-layer' are also retrieved from the SVG element and stored in private variables fretboardLayer and fingeringLayer.

function DrawFretboard(svg) {
    app = svg.parentNode;
    fretboardLayer = svg.getElementById('fretboard-layer');
    fingeringLayer = svg.getElementById('fingering-layer');

    Fretboard.Metrics.Calibrate(svg);

    AddNeckDetail(3);
    AddNeckDetail(5);
    AddNeckDetail(7);
    AddNeckDetail(9);
    AddNeckDetail(12);

    AddFrets();
    AddStrings();
    DrawNut();
}

Draw Neck

DrawNeck now creates a rectangle in the fretboardLayer group (a private variable initialized in the DrawFretboard method). The top left position of the rectangle is position to rest against the nut and 3 pixels down from the edge of the SVG border. The rectangle extends to the full width of the SVG element and down to 3 pixel above the bottom SVG border. And the background fill is assigned the id value 'fretboard-gradient', which was defined as a linear gradient in the markup for the SVG.

function DrawNeck() {
    var shape = document.createElementNS(Fretboard.NS, "rect");
    shape.x.baseVal.value = Fretboard.Metrics.BarPosition;
    shape.y.baseVal.value = 3;
    shape.width.baseVal.value = width;
    shape.height.baseVal.value = height - 6;
    shape.setAttribute("height", height - 6);
    shape.style.stroke = 'black';
    shape.style.strokeWidth = 2;
    shape.style.fill = 'url(#fretboard-gradient)';
    fretboardLayer.appendChild(shape);
    return shape;
}

It may be wondered at this point: why define elements such as this fretboard-rectangle in JavaScript, or any of the other elements, the strings, frets etc., rather than in the markup? The problem with that approach is that the metrics for programmatic elements like fingerings would then be defined distinctly from markup elements.

So, as with the code above, each of the other fretboard components are rendered into the fretboardLayer group in the same way. 

Add Frets

Each fret is drawn using the DrawFret method which uses the Fretboard.Metrics.FretPosition function for each fret and draws a white vertical line at the given distance along the x-axis. This method is called for each fret up to Fretboard.Metrics.HighestFret.

function DrawFret(x1, y1, x2, y2) {
    var shape = document.createElementNS(Fretboard.NS, "line");
    shape.x1.baseVal.value = x1;
    shape.x2.baseVal.value = x2;
    shape.y1.baseVal.value = y1;
    shape.y2.baseVal.value = y2;
    shape.style.stroke = 'white';
    shape.style.strokeWidth = width * 0.0035;
    fretboardLayer.appendChild(shape);
    return shape;
}    

function AddFrets() {
    for (var i = 1; i <= Fretboard.Metrics.HighestFret; i++) {
        var position = Fretboard.Metrics.FretPosition(i);
        DrawFret(position, 2, position, height - 2);
    }
}

Add String

The same procedure is repeated to add the strings. The AddStrings function retries the n-th string position along the y-axis for each of the 6 string and draws a line from the nut to the right end of the fretboard.

function DrawString(x1, y1, x2, y2, guage) {
    var shape = document.createElementNS(Fretboard.NS, "line");
    shape.x1.baseVal.value = x1;
    shape.x2.baseVal.value = x2;
    shape.y1.baseVal.value = y1;
    shape.y2.baseVal.value = y2;
    shape.style.stroke = '#cbcbcb';
    shape.style.strokeWidth = guage;
    fretboardLayer.appendChild(shape);
    return shape;
}

function AddStrings() {
    for (var i = 1; i < 7; i++) {
        var position = Fretboard.Metrics.StringPosition(i);
        DrawString(Fretboard.Metrics.BarPosition, position, width, position, 
                   Fretboard.Metrics.StringGague(i));
    }
}

 

Adding Dots

Fingerings are added to the fretboard by adding dots. Dots are added to the fingeringLayer SVG group.

The AddDot function takes a Finger object and an optional ghost argument. If ghost is true then the dot is rendered partially transparent in a faded white color. The Finger object specifies the Fret, String and Finger for the dot. 

The function now creates the dot using an SVG circle element, and depending on whether ghost is true or false, fills the dot with partially transparent white, or the radial gradient with the id value 'fingering-dot-gradient', defined in the markup.

Next, the function determines whether Fret is 0, in which case the fill is transparent, and the circle is given a broad blue stroke width.

The dot is then appended to the fingeringLayer SVG group.

Finally, if Fret is not 0, an SVG text element showing value of Finger is created and centred in the dot.

function AddDot(finger, ghost) {
    var dot = document.createElementNS(Fretboard.NS, "circle");
    dot.Finger = finger;
    dot.setAttributeNS(null, "cx", Fretboard.Metrics.FingerPosition(finger.Fret));
    dot.setAttributeNS(null, "cy", Fretboard.Metrics.StringPosition(finger.String));
    dot.setAttributeNS(null, "r", .017 * width);

    if (ghost) {
        dot.setAttributeNS(null, "fill", "white");
        dot.setAttributeNS(null, "opacity", ".1");
    }
    else {
        dot.setAttributeNS(null, "fill", "url(#fingering-dot-gradient)");
    }

    if (finger.Fret == 0) {
        dot.setAttributeNS(null, "cx", .011 * width + width * 0.004);
        dot.setAttributeNS(null, "r", .008 * width);
        dot.setAttributeNS(null, "fill", "transparent");
        dot.style.stroke = '#0090ff';
        dot.style.strokeWidth = width * 0.004;
    }

    fingeringLayer.appendChild(dot);

    if (!ghost && !!finger.Fret && !!finger.Finger) {
        var text = document.createElementNS(Fretboard.NS, "text");
        text.setAttribute('x', Fretboard.Metrics.FingerPosition(finger.Fret) - width * 0.006);
        text.setAttribute('y', Fretboard.Metrics.StringPosition(finger.String) + width * 0.007);
        text.textContent = finger.Finger;
        text.setAttributeNS(null, "fill", "white");
        text.style.fontSize = width * .0225 + 'px';
        text.style.fontWeight = 'lighter';
        fingeringLayer.appendChild(text);
        fingeringText.push(text);
    }

    dot.addEventListener('click', OnClickDot);

    return dot;
}

Erase Fingerings

The fretboard can be cleared of all fingerings by calling the Fretboard.Neck.EraseFingerings function, which simply removes all inner content of the fingeringLayer SVG group.

function EraseFingerings() {
    fingeringLayer.textContent = '';
}

Adding Chords and Scales

The AddChord and AddScale functions take a Chord and Scale object, respectively, and call the AddDot function for each of the fingerings defined in the given Chord or Scale.

The following shows the AddChord function. (The AddScale function is identical.) These functions accept a ghost argument also, and passes that value to the AddDot function.

function AddChord(chord, ghost) {
    chord.Fingering.forEach(function (a) {
        AddDot(a, ghost);
    });
}

Chords, Scales and Fingerings

The Chord, Scales and Finger objects are defined in global scope. Unfortunately, this was an error of original design, but will be corrected.

Finger

The Finger object fully specifies the fingering for any note on the fretboard. Most FretboardJs components have a dependency on the Finger object. The Finger object contains the following properties:

  • Fret
  • String
  • Finger
  • Degree

This Finger object also contains optional information specifying which finger is used and the context of the note within a scale or Chord if one exists.

function Finger(fret, string, finger, degree) {
    var Finger = finger || 0;
    var Fret = fret;
    var String = string;
    var Degree = degree || 0;

    var self = {
        Degree: Degree,
        Finger: Finger,
        Fret: Fret,
        String: String,
    };

    return self;
}

Chord

The Chord object is a named array of Finger objects.

The included chord library is based on standard guitar tuning, but can be redefined for alternate tunings. Chord naming is arbitrary and leaves open the possibility of any fingering/naming combination. 

This gives the essential structure of the Chord object as:

function Chord(def) {
    var Name = def.name;
    var Fingering = def.fingering;

    var self = {
        Fingering: Fingering,
        Name: Name,
    };

    return self;
}

The Chord object defines a number of functions however.

For example, the Transpose function, which adjusts a copy of the Chord a given number of semi-tones.

function Transpose(n) {
    var result = self.Copy();
    result.Fingering.forEach(function (a) {
        a.Fret += n;
    });
    return result;
}

Sharpen or Flatten Chord Degrees

The included chord library is defined for standard tunings only. However, functions on the Chord object are available to systematically modify Chords: 

function Flaten(degree) {
    var result = self.Copy();
    result.Fingering.forEach(function (a) {
        if (a.Degree == degree) {
            a.Fret--;
        }
    });
    return result;
}
function Sharpen(degree) {
    var result = self.Copy();
    result.Fingering.forEach(function (a) {
        if (a.Degree == degree) {
            a.Fret++;
        }
    });
    return result;
}

Scale

The scale object is virtually identical to the Chord object so will not be discussed further here.

The Chord Library

The chord library is defined in the namespace Fretboard.Chords.

Fretboard.Chords = {};

And the library consists of named Chord groups. For example, the following shows the definition of the MajorTriad chord group containing two chords:

<span style="font-size: 14px;">Fretboard.Chords.MajorTriad = function () {</span>

    var self = [
    new Chord({
        name: 'E',
        fingering: [
            new Finger(0, 6, 1, 1),
            new Finger(2, 5, 3, 5),
            new Finger(2, 4, 4, 1),
            new Finger(1, 3, 2, 3),
            new Finger(0, 2, 1, 5),
            new Finger(0, 1, 1, 1),
        ],
    }),
    new Chord({
        name: 'D',
        fingering: [
            new Finger(0, 4, 1, 1),
            new Finger(2, 3, 2, 5),
            new Finger(3, 2, 4, 1),
            new Finger(2, 1, 3, 3),
        ],
        OpenOnly: true
    })
    ];

    return self;
}();

Chord groups can be defined algorithmically from another Chord group as the following code shows. Here, MinorTriad Chord group is defined by flattening the 3rd degree of each chord from the MajorTriad group. A validation rule can be defined to determine if a modification to a given Chord is valid. In this case the rule requires that the finger span of a chord not exceed four frets.

Fretboard.Chords.MinorTriad = function () {
    var self = [];
    for (var i = 0; i < Fretboard.Chords.MajorTriad.length; i++) {
        var chord = Fretboard.Chords.MajorTriad[i].Flaten3rd();

        if (chord.Span() > 4)
        {
            continue;
        }

        self.push(chord);
    }
    return self;
}();

Chord groups in the library can be overwritten.

Scripts

The scripts folder contains all the code to this discussion. This folder includes the following files:

  1. _namespace.js: defines the basic namespace called Fretboard.
  2. chord.js: defines the FingerChord and Notes objects.
  3. chords.js: defines all available chord families
  4. metrics.js: defines global metrics and calculations to determine various coordinate values for things like the position along the x-axis for the n-th fret.
  5. neck.js: defines the main FretboardJs object Neck.
  6. scale.js: defines the Scale object, similar is use to the Chord object
  7. scales.js: defines a library of scales groups into 
  8. app.js: defines the sample app ''Chord Explorer'.
  9. chord-utility.js: defines an associative table in the form of a list of regular expression comparisons to return a generic chord family matching a given chord spelling.
  10. fretboard-1.3.0.js: the bundled library
  11. fretboard-1.3.0.min.js: the minified bundle

History

  • 2014, July 30 - initial post including preliminary discussion of the demo app 'Chord Explorer', and usage of FretboardJs chord library.
  • 2014, August 1 - version 1.3, include design and implementation discussion

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)