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:
<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:
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:
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:
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:
- _namespace.js: defines the basic namespace called
Fretboard
. - chord.js: defines the
Finger
, Chord
and Notes
objects. - chords.js: defines all available chord families
- 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.
- neck.js: defines the main FretboardJs object
Neck
. - scale.js: defines the Scale object, similar is use to the Chord object
- scales.js: defines a library of scales groups into
- app.js: defines the sample app ''Chord Explorer'.
- 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.
- fretboard-1.3.0.js: the bundled library
- 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