├── .npmrc ├── .gitignore ├── GMajor.png ├── .npmignore ├── babel.config.js ├── assets └── fretboard.css ├── Makefile ├── jest.config.js ├── webpack.config.js ├── demos ├── drui.html ├── note_names_colors.html ├── dynamic.html ├── demo.html ├── dom-chords.html └── quatriads-drop2.html ├── package.json ├── tests └── unit │ ├── fretboard.spec.js │ └── helpers.spec.js ├── README.md └── src └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | readme.html 2 | dist/ 3 | node_modules/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /GMajor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/txels/fretboards/HEAD/GMajor.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib/* 2 | test/* 3 | Makefile 4 | karma.conf.js 5 | *.html 6 | *.md 7 | *.map 8 | .python-version 9 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [], 3 | plugins: [ 4 | "@babel/plugin-transform-arrow-functions", 5 | "@babel/plugin-transform-destructuring", 6 | "@babel/plugin-transform-for-of", 7 | "@babel/plugin-transform-modules-umd", 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /assets/fretboard.css: -------------------------------------------------------------------------------- 1 | .fretboard { 2 | position: relative; 3 | } 4 | .fretboard .tuning, 5 | .fretboard .fretnum { 6 | position: absolute; 7 | margin: 0; 8 | padding: 0; 9 | font-family: Helvetica; 10 | text-transform: uppercase; 11 | } 12 | .fretboard .tuning { 13 | font-size: 10px; 14 | } 15 | .fretboard .fretnum { 16 | font-size: 8px; 17 | } 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | python3 -m http.server 8007 3 | 4 | test: 5 | npm run test 6 | 7 | build: 8 | npm run build 9 | 10 | # bump-micro: 11 | # yarn version --micro 12 | 13 | # bump-minor: 14 | # yarn version --minor 15 | 16 | publish: build 17 | git push --tags 18 | npm publish 19 | make publish-demos 20 | 21 | publish-demos: 22 | scp -r demos dist txels.com:fretboard/ 23 | 24 | 25 | .PHONY: run test build bump-micro bump-minor publish publish-demos 26 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "jsdom", 3 | moduleNameMapper: { 4 | "^@/(.*)$": "/src/$1", 5 | }, 6 | testMatch: ["**/tests/unit/**/*.spec.[jt]s?(x)", "**/__tests__/*.[jt]s?(x)"], 7 | verbose: true, 8 | transform: { 9 | ".+\\.(css|styl|less|sass|scss|jpg|jpeg|png|svg|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|avif)$": 10 | require.resolve("jest-transform-stub"), 11 | "^.+\\.jsx?$": require.resolve("babel-jest"), 12 | }, 13 | // transformIgnorePatterns: ['/node_modules/'], 14 | transformIgnorePatterns: ["/node_modules/(?!d3-selection/.*)"], 15 | }; 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | devtool: "source-map", 5 | entry: "./src/index.js", 6 | output: { 7 | path: path.resolve(__dirname, "dist"), 8 | filename: "fretboard.js", 9 | library: "fretboard", 10 | libraryTarget: "umd", 11 | // devtoolLineToLine: true, 12 | sourceMapFilename: "fretboard.js.map", 13 | pathinfo: true, 14 | }, 15 | mode: "production", 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.css$/, 20 | use: ["style-loader", "css-loader"], 21 | }, 22 | { 23 | test: /\.js$/, 24 | loader: "babel-loader", 25 | exclude: /d3-selection/, 26 | }, 27 | ], 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /demos/drui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Drui

10 |
15 |
20 |
25 |
30 |

Solos

31 |
32 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fretboards", 3 | "version": "0.6.0", 4 | "description": "Display fretboards and notes on your browser", 5 | "keywords": [ 6 | "d3", 7 | "guitar", 8 | "fretboard" 9 | ], 10 | "homepage": "https://github.com/txels/fretboards", 11 | "author": "Carles Barrobés (http://txels.barrobes.com/)", 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:txels/fretboards.git" 15 | }, 16 | "dependencies": { 17 | "d3-selection": "^3.0" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.9.0", 21 | "@babel/preset-env": "^7.9.5", 22 | "babel-jest": "^29.7.0", 23 | "babel-loader": "^9.0", 24 | "css-loader": "^7.1", 25 | "jest": "^29.7.0", 26 | "jest-environment-jsdom": "^29.7.0", 27 | "jest-transform-stub": "^2.0.0", 28 | "style-loader": "^4.0", 29 | "webpack": "^5.0", 30 | "webpack-cli": "^5.0" 31 | }, 32 | "files": [ 33 | "assets/**", 34 | "dist/**/*.js", 35 | "src/**/*.js" 36 | ], 37 | "scripts": { 38 | "test": "jest", 39 | "build": "webpack", 40 | "preversion": "yarn build && yarn test" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/txels/fretboards/issues" 44 | }, 45 | "main": "dist/fretboard.js", 46 | "module": "src/index.js", 47 | "directories": { 48 | "lib": "lib", 49 | "test": "test" 50 | }, 51 | "license": "0BSD" 52 | } 53 | -------------------------------------------------------------------------------- /tests/unit/fretboard.spec.js: -------------------------------------------------------------------------------- 1 | import * as fretboard from "@/index"; 2 | 3 | describe("instrument helper factories", () => { 4 | it("6-string guitar is the default", () => { 5 | let g = fretboard.Fretboard(); 6 | expect(g.strings).toEqual(6); 7 | expect(g.frets).toEqual(12); 8 | expect(g.tuning).toEqual(fretboard.Tunings.guitar6.standard); 9 | }); 10 | 11 | it("7-string guitar", () => { 12 | let g = fretboard.Guitar(7); 13 | expect(g.strings).toEqual(7); 14 | expect(g.frets).toEqual(12); 15 | expect(g.tuning).toEqual(fretboard.Tunings.guitar7.standard); 16 | }); 17 | 18 | it("bass", () => { 19 | let b = fretboard.Bass(); 20 | expect(b.strings).toEqual(4); 21 | expect(b.frets).toEqual(12); 22 | expect(b.tuning).toEqual(fretboard.Tunings.bass4.standard); 23 | }); 24 | }); 25 | 26 | describe("dynamic behavior", () => { 27 | it("added notes are accumulated", () => { 28 | let fb = fretboard.Fretboard(); 29 | 30 | fb.addNoteOnString("g2", 6, "black"); 31 | fb.addNoteOnString("b2", 5); 32 | fb.add("3:a3"); 33 | 34 | expect(fb.getNotes()).toEqual([ 35 | { string: 6, note: "g2", color: "black" }, 36 | { string: 5, note: "b2", color: undefined }, 37 | { string: 3, note: "a3", color: undefined }, 38 | ]); 39 | }); 40 | 41 | it("notes can be cleared", () => { 42 | let fb = fretboard.Fretboard(); 43 | fb.add("4:f3 3:a3"); 44 | 45 | fb.clearNotes(); 46 | 47 | expect(fb.getNotes()).toEqual([]); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/unit/helpers.spec.js: -------------------------------------------------------------------------------- 1 | import * as fretboard from "@/index"; 2 | 3 | describe("Music scales", () => { 4 | let sut; 5 | 6 | describe("asOffset", () => { 7 | it("c is 0", () => { 8 | expect(fretboard.asOffset("c")).toEqual(0); 9 | }); 10 | 11 | it("ignores case", () => { 12 | expect(fretboard.asOffset("C")).toEqual(0); 13 | }); 14 | 15 | it("Eb is 3", () => { 16 | expect(fretboard.asOffset("Eb")).toEqual(3); 17 | }); 18 | }); 19 | 20 | describe("absNote", () => { 21 | it("c2 is 24", () => { 22 | expect(fretboard.absNote("c2")).toEqual(24); 23 | }); 24 | 25 | it("e3 is 40", () => { 26 | expect(fretboard.absNote("e3")).toEqual(40); 27 | }); 28 | }); 29 | 30 | describe("asNotes generates a scale", () => { 31 | it("reference C scale are predefined", () => { 32 | expect(fretboard.asNotes("c major")).toEqual("c d e f g a b"); 33 | expect(fretboard.asNotes("c lydian")).toEqual("c d e f# g a b"); 34 | }); 35 | 36 | it("on other roots, it transposes", () => { 37 | expect(fretboard.asNotes("g major")).toEqual("g a b c d e f#"); 38 | expect(fretboard.asNotes("a major")).toEqual("a b c# d e f# g#"); 39 | }); 40 | 41 | it("also works for altered roots", () => { 42 | // we do not expect enharmonization to be correct ATM, 43 | // all alterations are computed as sharps 44 | expect(fretboard.asNotes("ab major")).toEqual("g# a# c c# d# f g"); 45 | expect(fretboard.asNotes("g# major")).toEqual("g# a# c c# d# f g"); 46 | }); 47 | }); 48 | 49 | describe("auto-detect what to draw", () => { 50 | it("a scale/chord", () => { 51 | expect(fretboard.whatIs("a major")).toEqual("scale"); 52 | expect(fretboard.whatIs("eb 7")).toEqual("scale"); 53 | }); 54 | 55 | it("notes by name", () => { 56 | expect(fretboard.whatIs("a b")).toEqual("addNotes"); 57 | expect(fretboard.whatIs("c# d# f g a#")).toEqual("addNotes"); 58 | }); 59 | 60 | it("specifically placed notes", () => { 61 | expect(fretboard.whatIs("5:c3 4:e3 3:bb3 2:d4 1:g4")).toEqual( 62 | "placeNotes" 63 | ); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /demos/note_names_colors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Color Circles With Names

10 | This is the a-minor hexatonic (blues) scale with root notes colored red and the "blues notes" colored blue. 11 |
12 | 13 | Note that when creating a fretboard diagram, you can turn on note names and specify both the circle 14 | fill colors and circle radius. If you provide less than seven colors for the fill and note names, 15 | your colors will be recycled. Here is an example 16 | 17 |
18 |       var fb = fretboard.Fretboard(
19 |         {
20 |             radius: 8,
21 |             showNames: true,
22 |             fillColors: ["red", "white", "white", "blue", "white", "white", "white"],
23 |             nameColors: ["white", "black", "black", "white", "black", "black"],
24 |         })
25 |         fb.add("a minor-blues").paint();   
26 |     
27 |

Left Handed

28 | Same as above, left handed view. 29 |
30 | 31 | 54 | 55 | -------------------------------------------------------------------------------- /demos/dynamic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 22 | 23 | 24 | 25 |
26 |

27 |
28 | 29 |
30 |

G dorian

31 |

32 |
33 | 34 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /demos/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Introduction to 4ths tuning

10 |

11 | 4ths tuning is based on making all strings tuned one just 4th apart. This 12 | has the advantage (over the standard guitar tuning) that every fingering 13 | (provided no open strings are involved) is moveable across the fretboard, 14 | so your learning of fingerings is minimised. 15 |

16 | 17 |

Major scales

18 |

19 | As an example, this is what a G major scale looks like on a guitar tuned 20 | in E 4ths (the root notes, G, are red). Note the symmetry of the patterns 21 | repeating all over the fingerboard: 22 |

23 |
24 |

25 | Notice how e.g. starting from any of the G notes with a given finger, you 26 | can play the exact same fingering pattern starting from different frets 27 | and different strings. If you have already been playing guitar for a 28 | while, you will realise how much simpler this is to memorise than with 29 | standard guitar tuning. 30 |

31 |

32 | Look at this basic layout for one octave of G major, if you start from 33 | your first finger: 34 |

35 |
39 |

40 | Starting from your second finger: 41 |

42 |
46 |

47 | Starting from your fourth finger: 48 |

49 |
53 |

54 | All of the notes of the G major scale based on a fixed position starting 55 | on the 3rd fret, which you can view as chaining the former fingerings: 56 |

57 |
61 |

62 | Similarly, but based on the 2nd fret: 63 |

64 |
68 |

69 | One of the things you may have started to notice is that all scales that 70 | derive from a major scale can be comfortably played using 3 notes per 71 | string. 72 |

73 |

74 | As a final example, this is C Major all over the first twelve frets: 75 |

76 |
77 | 78 |
79 | 80 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fretboards on a browser 2 | 3 | Instantiate a fretboard, and display some notes, scales, chord voicings, etc. 4 | 5 | This is an example of how it looks like: 6 | 7 | ![](GMajor.png) 8 | 9 | A live demo is available at http://fretboard.txels.com/demos/dynamic.html 10 | 11 | ## Installation 12 | 13 | As a script tag, from CDN: 14 | 15 | ```html 16 | 17 | ``` 18 | 19 | In a modern Javascript environment, with ES2015 or higher, install with: 20 | 21 | npm install fretboards 22 | 23 | And then import the whole package: 24 | 25 | ```js 26 | import * as fretboards from "fretboards"; 27 | ``` 28 | 29 | ...or just the bits you need: 30 | 31 | ```js 32 | import { Fretboard, Tunings } from "fretboards"; 33 | ``` 34 | 35 | ## Usage examples 36 | 37 | ### The javascript API: 38 | 39 | ```js 40 | // Layout a specific scale 41 | var fb = fretboard.Fretboard(); 42 | fb.add("a phrygian").paint(); 43 | 44 | // Use alternative tunings 45 | var fbDropD = fretboard.Fretboard({ tuning: fretboard.Tunings.guitar6.Drop_D }); 46 | fbDropD.add("a phrygian").paint(); 47 | 48 | // Place specific notes on specific strings, e.g. for chord voicings 49 | var c7add9 = fretboard.Fretboard({ frets: 5 }); 50 | c7add9.add("5:c3 4:e3 3:bb3 2:d4 1:g4").paint(); 51 | ``` 52 | 53 | ### Property updates 54 | 55 | Once the fretboard is rendered, you can dynamically update configuration 56 | properties and the fretboard will redraw, keeping the notes. Examples include: 57 | 58 | ```js 59 | fb.set("fretWidth", 30); 60 | fb.set("leftHanded", true); 61 | fb.set("frets", 12); 62 | ``` 63 | 64 | Check the full example at `demos/dynamic.html`. 65 | 66 | ### The 'document' API 67 | 68 | You can also use HTML attributes for declaratively including fretboard 69 | instances in your page, and using `Fretboard.drawAll(selector)` as in the 70 | example below: 71 | 72 | ```html 73 |
77 |
78 | 79 | 80 | 83 | ``` 84 | 85 | You can pass initialization configuration options to `drawAll`, e.g.: 86 | 87 | ```html 88 | 94 | ``` 95 | 96 | ## Configuration options 97 | 98 | These are the configuration options and their default values: 99 | 100 | ```js 101 | config = { 102 | frets: 12, // Number of frets to display 103 | startFret: 0, // Initial fret 104 | strings: 6, // Strings 105 | tuning: Tunings.guitar6.standard, // Tuning: default = Standard Guitar 106 | fretWidth: 50, // Display width of frets in pixels 107 | fretHeight: 20, // Display heigh of frets in pixels 108 | leftHanded: false, // Show mirror image for left handed players 109 | showTitle: false, // Set the note name as the title, so it will display on hover 110 | }; 111 | ``` 112 | -------------------------------------------------------------------------------- /demos/dom-chords.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Dominant Chord Fingering Study

10 | 11 |

G Dom 7h inversions sorted by lead note

12 | Collection of inversion on the top 4 strings, no repeated notes. Fingerings 13 | are shown both in 4ths and in standard tuning. 14 | 15 |

root

16 | G7 17 |
22 | G7(13) 23 |
28 | G7(omit 5) 29 |
34 | 35 |

9th

36 | G7(9) 37 |
42 | G7(9 omit 5) 43 |
48 | G7(9 13) 49 |
54 | 55 |

#9th

56 | G7(#9) 57 |
62 | G7(#9 omit 5) 63 |
68 | G7(#9 13) 69 |
74 | 75 |

3rd

76 | G7 77 |
82 | G7(9) 83 |
88 | 89 |

11th

90 | G7(sus4) 91 |
96 | G7(9 sus4) 97 |
102 | 103 |

5th

104 | G7 105 |
110 | G7(9) 111 |
116 | G7(#9) 117 |
122 | 123 |

6th/13th

124 | G7(9 13) 125 |
130 | G7(#9 13) 131 |
136 | G6 137 |
142 | G6(9) 143 |
148 | 149 |

7th

150 | G7 151 |
156 | G7(9) 157 |
162 | G7(9 13) 163 |
168 | 169 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /demos/quatriads-drop2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Drop2 Chords

10 | 11 | GMaj7 12 |
17 | G7 18 |
23 | G-7 24 |
29 | G-7b5 30 |
35 | 36 | GMaj7 37 |
42 | G7 43 |
48 | G-7 49 |
54 | G-7b5 55 |
60 | 61 | GMaj7 62 |
67 | G7 68 |
73 | G-7 74 |
79 | G-7b5 80 |
85 | 86 | GMaj7 87 |
92 | G7 93 |
98 | G-7 99 |
104 | G-7b5 105 |
110 | 111 |

Drop3 Chords

112 | 113 | GMaj7 114 |
119 |
124 |
129 |
134 | G7 135 |
140 |
145 |
150 |
155 | G-7 156 |
161 |
166 |
171 |
176 | G-7b5 177 |
182 |
187 |
192 |
197 | 198 |

Shell Chords

199 | 200 |

Strings 6-4-3

201 | 202 | GMaj7 203 |
204 | G7 205 |
206 | G-7 207 |
208 | 209 |

Strings 6-5-4

210 | 211 | GMaj7 212 |
213 | G7 214 |
215 | G-7 216 |
217 | 218 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3-selection"; 2 | import "../assets/fretboard.css"; 3 | 4 | // Music 5 | const allNotes = [ 6 | "c", 7 | "c#", 8 | "d", 9 | "d#", 10 | "e", 11 | "f", 12 | "f#", 13 | "g", 14 | "g#", 15 | "a", 16 | "a#", 17 | "b", 18 | ]; 19 | const allNotesEnharmonic = [ 20 | "c", 21 | "db", 22 | "d", 23 | "eb", 24 | "fb", 25 | "f", 26 | "gb", 27 | "g", 28 | "ab", 29 | "a", 30 | "bb", 31 | "cb", 32 | ]; 33 | const colors = [ 34 | "red", 35 | "green", 36 | "blue", 37 | "black", 38 | "purple", 39 | "gray", 40 | "orange", 41 | "lightgray", 42 | ]; 43 | 44 | export const Scales = { 45 | // scales 46 | lydian: "c d e f# g a b", 47 | major: "c d e f g a b", 48 | mixolydian: "c d e f g a bb", 49 | dorian: "c d eb f g a bb", 50 | aeolian: "c d eb f g ab bb", 51 | phrygian: "c db eb f g ab bb", 52 | locrian: "c db eb f gb ab bb", 53 | "harmonic-minor": "c d eb f g ab b", 54 | "melodic-minor": "c d eb f g a b", 55 | "minor-pentatonic": "c eb f g bb", 56 | "minor-blues": "c eb f f# g bb", 57 | "major-pentatonic": "c d e g a", 58 | "major-blues": "c d d# e g a", 59 | "composite-blues": "c d d# e f f# g a bb", 60 | "dom-pentatonic": "c e f g bb", 61 | japanese: "c db f g ab", 62 | // chords 63 | maj: "c e g", 64 | aug: "c e g#", 65 | min: "c eb g", 66 | dim: "c eb gb", 67 | maj7: "c e g b", 68 | 7: "c e g bb", 69 | min7: "c eb g bb", 70 | m7b5: "c eb gb bb", 71 | dim7: "c eb gb a", 72 | _: function (scale) { 73 | return Scales[scale].split(" "); 74 | }, 75 | }; 76 | 77 | let createArray = function (x, l) { 78 | x = [].concat(x); // guarantee we are starting with an array 79 | while (x.length < l) { 80 | x = x.concat(x); 81 | } 82 | return x.slice(0, l); 83 | }; 84 | 85 | export function whatIs(sequence) { 86 | let sections = sequence.split(" "); 87 | if (sections.length === 2 && typeof Scales[sections[1]] === "string") { 88 | return "scale"; 89 | } 90 | if (sections[0].indexOf(":") > 0) { 91 | return "placeNotes"; 92 | } else { 93 | return "addNotes"; 94 | } 95 | } 96 | 97 | export function asOffset(note) { 98 | note = note.toLowerCase(); 99 | let offset = allNotes.indexOf(note); 100 | if (offset === -1) { 101 | offset = allNotesEnharmonic.indexOf(note); 102 | } 103 | return offset; 104 | } 105 | 106 | export function absNote(note) { 107 | let octave = note[note.length - 1]; 108 | let pitch = asOffset(note.slice(0, -1)); 109 | if (pitch > -1) { 110 | return pitch + octave * 12; 111 | } 112 | } 113 | 114 | export function noteName(absPitch) { 115 | let octave = Math.floor(absPitch / 12); 116 | let note = allNotes[absPitch % 12]; 117 | return note + octave.toString(); 118 | } 119 | 120 | export function asNotes(scale) { 121 | let [root, type] = scale.split(" "); 122 | let scaleInC = Scales._(type); 123 | let offset = asOffset(root); 124 | let scaleTransposed = scaleInC.map(function (note) { 125 | return allNotes[(asOffset(note) + offset) % 12]; 126 | }); 127 | return scaleTransposed.join(" "); 128 | } 129 | 130 | // Fretboard 131 | export const Tunings = { 132 | bass4: { 133 | standard: ["e1", "a1", "d2", "g2", "b2", "e3"], 134 | }, 135 | guitar6: { 136 | standard: ["e2", "a2", "d3", "g3", "b3", "e4"], 137 | E_4ths: ["e2", "a2", "d3", "g3", "c4", "f4"], 138 | Drop_D: ["d2", "a2", "d3", "g3", "b3", "e4"], 139 | G_open: ["d2", "g2", "d3", "g3", "b3", "d4"], 140 | DADGAD: ["d2", "a2", "d3", "g3", "a3", "d4"], 141 | }, 142 | guitar7: { 143 | standard: ["b2", "e2", "a2", "d3", "g3", "b3", "e4"], 144 | E_4ths: ["b2", "e2", "a2", "d3", "g3", "c3", "f4"], 145 | }, 146 | }; 147 | 148 | export const Fretboard = function (config) { 149 | config = config || {}; 150 | let where = config.where || "body"; 151 | 152 | let id = "fretboard-" + Math.floor(Math.random() * 1000000); 153 | 154 | let fillColors = config.fillColors || "white"; 155 | let nameColors = config.nameColors || "gray"; 156 | let lineColors = config.colors || "gray"; 157 | fillColors = createArray(fillColors, 7); 158 | nameColors = createArray(nameColors, 7); 159 | lineColors = createArray(lineColors, 7); 160 | 161 | let instance = { 162 | frets: 12, 163 | startFret: 0, 164 | strings: 6, 165 | tuning: Tunings.guitar6.standard, 166 | fretWidth: 50, 167 | fretHeight: 20, 168 | leftHanded: false, 169 | notes: [], 170 | radius: 6, 171 | dotRadius: 4, 172 | showTitle: false, 173 | showNames: false, 174 | nameColor: "gray", 175 | ...config, 176 | }; 177 | 178 | instance.fillColors = fillColors; 179 | instance.nameColors = nameColors; 180 | instance.colors = lineColors; 181 | 182 | // METHODS for dynamic prop changes --------------------------- 183 | 184 | instance.set = (prop, value) => { 185 | instance[prop] = value; 186 | instance.repaint(); 187 | }; 188 | 189 | // METHODS for managing notes --------------------------------- 190 | 191 | instance.addNoteOnString = function (note, string, color, fill, nameColor) { 192 | instance.notes.push({ note, string, color, fill, nameColor }); 193 | return instance; 194 | }; 195 | 196 | instance.addNote = function (note, color, fill, nameColor) { 197 | for (let string = 1; string <= instance.strings; string++) { 198 | instance.addNoteOnString(note, string, color, fill, nameColor); 199 | } 200 | return instance; 201 | }; 202 | 203 | instance.addNotes = function (notes, color, fill, nameColor) { 204 | let allNotes = notes.split(" "); 205 | for (let i = 0; i < allNotes.length; i++) { 206 | let showColor = color || colors[i]; 207 | let showFill = fill || instance.fillColors[i]; 208 | let showNameColor = nameColor || instance.nameColors[i]; 209 | let note = allNotes[i]; 210 | for (let octave = 1; octave < 7; octave++) { 211 | instance.addNote(note + octave, showColor, showFill, showNameColor); 212 | } 213 | } 214 | return instance; 215 | }; 216 | 217 | instance.scale = function (scaleName) { 218 | instance.addNotes(asNotes(scaleName)); 219 | return instance; 220 | }; 221 | 222 | instance.placeNotes = function (sequence) { 223 | // Sequence of string:note 224 | // e.g. "6:g2 5:b2 4:d3 3:g3 2:d4 1:g4" 225 | let pairs = sequence.split(" "); 226 | pairs.forEach(function (pair, i) { 227 | const [string, note] = pair.split(":"); 228 | instance.addNoteOnString(note, parseInt(string)); // , i==0? "red" : "black"); 229 | }); 230 | return instance; 231 | }; 232 | 233 | instance.add = function (something) { 234 | let sections = something.trim().replace(/\s\s+/g, " ").split(";"); 235 | sections.forEach(function (section) { 236 | section = section.trim(); 237 | let what = whatIs(section); 238 | instance[what](section); 239 | }); 240 | return instance; 241 | }; 242 | 243 | instance.clearNotes = function () { 244 | instance.notes = []; 245 | instance.svgContainer.selectAll(".note").remove(); 246 | return instance; 247 | }; 248 | 249 | // METHODS for drawing ------------------------------------------- 250 | 251 | let fretFitsIn = function (fret) { 252 | return fret > instance.startFret && fret <= instance.frets; 253 | }; 254 | 255 | let fretsWithDots = function () { 256 | let allDots = [3, 5, 7, 9, 15, 17, 19, 21]; 257 | return allDots.filter(fretFitsIn); 258 | }; 259 | 260 | let fretsWithDoubleDots = function () { 261 | let allDots = [12, 24]; 262 | return allDots.filter(fretFitsIn); 263 | }; 264 | 265 | let fretboardHeight = function () { 266 | return (instance.strings - 1) * instance.fretHeight + 2; 267 | }; 268 | 269 | let fretboardWidth = function () { 270 | return (instance.frets - instance.startFret) * instance.fretWidth + 2; 271 | }; 272 | 273 | let XMARGIN = function () { 274 | return instance.fretWidth; 275 | }; 276 | let YMARGIN = function () { 277 | return instance.fretHeight; 278 | }; 279 | 280 | let makeContainer = function (elem) { 281 | instance.width = fretboardWidth() + XMARGIN() * 2; 282 | instance.height = fretboardHeight() + YMARGIN() * 2; 283 | 284 | let container = d3 285 | .select(elem) 286 | .append("div") 287 | .attr("class", "fretboard") 288 | .attr("id", id) 289 | .append("svg") 290 | .attr("width", instance.width) 291 | .attr("height", instance.height); 292 | 293 | if (instance.leftHanded) { 294 | container = container 295 | .append("g") 296 | .attr( 297 | "transform", 298 | "scale(-1,1) translate(-" + (instance.width - XMARGIN()) + ",0)" 299 | ); 300 | } 301 | 302 | return container; 303 | }; 304 | 305 | let drawFrets = function () { 306 | for (let i = instance.startFret; i <= instance.frets; i++) { 307 | // BEWARE: the coordinate system for SVG elements uses a transformation 308 | // for lefties, however the HTML elements we use for fret numbers and 309 | // tuning we transform by hand. 310 | let x = (i - instance.startFret) * instance.fretWidth + 1 + XMARGIN(); 311 | let fretNumX = x; 312 | if (instance.leftHanded) { 313 | fretNumX = instance.width - XMARGIN() - x; 314 | } 315 | // fret 316 | instance.svgContainer 317 | .append("line") 318 | .attr("x1", x) 319 | .attr("y1", YMARGIN()) 320 | .attr("x2", x) 321 | .attr("y2", YMARGIN() + fretboardHeight()) 322 | .attr("stroke", "lightgray") 323 | .attr("stroke-width", i === 0 ? 8 : 2); 324 | // number 325 | d3.select("#" + id) 326 | .append("p") 327 | .attr("class", "fretnum") 328 | .style("top", fretboardHeight() + YMARGIN() + 5 + "px") 329 | .style("left", fretNumX - 4 + "px") 330 | .text(i); 331 | } 332 | }; 333 | 334 | let drawStrings = function () { 335 | for (let i = 0; i < instance.strings; i++) { 336 | instance.svgContainer 337 | .append("line") 338 | .attr("x1", XMARGIN()) 339 | .attr("y1", i * instance.fretHeight + 1 + YMARGIN()) 340 | .attr("x2", XMARGIN() + fretboardWidth()) 341 | .attr("y2", i * instance.fretHeight + 1 + YMARGIN()) 342 | .attr("stroke", "black") 343 | .attr("stroke-width", 1); 344 | } 345 | let placeTuning = function (d, i) { 346 | return (instance.strings - i) * instance.fretHeight - 4 + "px"; 347 | }; 348 | 349 | let toBaseFretNote = function (note) { 350 | return noteName(absNote(note) + instance.startFret); 351 | }; 352 | 353 | let hPosition = instance.leftHanded 354 | ? instance.width - XMARGIN() - 16 + "px" 355 | : "4px"; 356 | 357 | d3.select("#" + id) 358 | .selectAll(".tuning") 359 | .data(instance.tuning.slice(0, instance.strings)) 360 | .style("top", placeTuning) 361 | .text(toBaseFretNote) 362 | .enter() 363 | .append("p") 364 | .attr("class", "tuning") 365 | .style("top", placeTuning) 366 | .style("left", hPosition) 367 | .text(toBaseFretNote); 368 | }; 369 | 370 | let drawDots = function () { 371 | let p = instance.svgContainer.selectAll("circle").data(fretsWithDots()); 372 | 373 | function dotX(d) { 374 | return ( 375 | (d - instance.startFret - 1) * instance.fretWidth + 376 | instance.fretWidth / 2 + 377 | XMARGIN() 378 | ); 379 | } 380 | 381 | function dotY(ylocation) { 382 | let margin = YMARGIN(); 383 | 384 | if (instance.strings % 2 === 0) { 385 | return ( 386 | ((instance.strings + 3) / 2 - ylocation) * instance.fretHeight + 387 | margin 388 | ); 389 | } else { 390 | return (fretboardHeight() * ylocation) / 4 + margin; 391 | } 392 | } 393 | 394 | p.enter() 395 | .append("circle") 396 | .attr("cx", dotX) 397 | .attr("cy", dotY(2)) 398 | .attr("r", instance.dotRadius) 399 | .style("fill", "#ddd"); 400 | 401 | p = instance.svgContainer.selectAll(".octave").data(fretsWithDoubleDots()); 402 | 403 | p.enter() 404 | .append("circle") 405 | .attr("class", "octave") 406 | .attr("cx", dotX) 407 | .attr("cy", dotY(3)) 408 | .attr("r", instance.dotRadius) 409 | .style("fill", "#ddd"); 410 | p.enter() 411 | .append("circle") 412 | .attr("class", "octave") 413 | .attr("cx", dotX) 414 | .attr("cy", dotY(1)) 415 | .attr("r", instance.dotRadius) 416 | .style("fill", "#ddd"); 417 | }; 418 | 419 | instance.drawBoard = function () { 420 | instance.delete(); 421 | instance.svgContainer = makeContainer(where); 422 | drawFrets(); 423 | drawDots(); 424 | drawStrings(); 425 | return instance; 426 | }; 427 | 428 | function paintNote(note, string, color, fill, nameColor) { 429 | if (string > instance.strings) { 430 | return false; 431 | } 432 | let absPitch = absNote(note); 433 | let actualColor = color || "black"; 434 | let actualFill = fill || "white"; 435 | let actualNameColor = nameColor || "gray"; 436 | let absString = instance.strings - string; 437 | let basePitch = absNote(instance.tuning[absString]) + instance.startFret; 438 | if ( 439 | absPitch >= basePitch && 440 | absPitch <= basePitch + instance.frets - instance.startFret 441 | ) { 442 | const circle = instance.svgContainer 443 | .append("circle") 444 | .attr("class", "note") 445 | .attr("stroke-width", 1) 446 | // 0.75 is the offset into the fret (higher is closest to fret) 447 | .attr("cx", (absPitch - basePitch + 0.75) * instance.fretWidth) 448 | .attr("cy", (string - 1) * instance.fretHeight + 1 + YMARGIN()) 449 | .attr("r", instance.radius) 450 | .style("stroke", actualColor) 451 | .style("fill", actualFill) 452 | .on("click", function () { 453 | this.setAttribute( 454 | "stroke-width", 455 | 5 - parseInt(this.getAttribute("stroke-width")) 456 | ); 457 | }); 458 | 459 | if (instance.showTitle) { 460 | circle.append("title").text(note.toUpperCase()); 461 | } 462 | 463 | var orientation = 1; 464 | var scale = "scale(1,1)"; 465 | if (instance.leftHanded) { 466 | orientation = -1; 467 | scale = "scale(-1,1)"; 468 | } 469 | 470 | if (instance.showNames) { 471 | instance.svgContainer 472 | .append("text") 473 | .text(note.substring(0, note.length - 1)) 474 | .attr( 475 | "dx", 476 | orientation * (absPitch - basePitch + 0.75) * instance.fretWidth 477 | ) 478 | .attr("dy", (string - 1) * instance.fretHeight + 4 + YMARGIN()) 479 | .attr("class", "fretnum") 480 | .style("text-anchor", "middle") 481 | .style("fill", actualNameColor) 482 | .attr("transform", scale); 483 | } 484 | return true; 485 | } 486 | return false; 487 | } 488 | 489 | instance.paint = function () { 490 | for (let { note, string, color, fill, nameColor } of instance.notes) { 491 | paintNote(note, string, color, fill, nameColor); 492 | } 493 | }; 494 | 495 | instance.repaint = function () { 496 | instance.drawBoard(); 497 | instance.paint(); 498 | }; 499 | 500 | instance.clear = function () { 501 | instance.clearNotes(); 502 | const el = document.getElementById(id); 503 | el.parentNode.removeChild(el); 504 | instance.drawBoard(); 505 | return instance; 506 | }; 507 | 508 | instance.delete = function () { 509 | d3.select("#" + id).remove(); 510 | }; 511 | 512 | instance.getNotes = function () { 513 | return instance.notes; 514 | }; 515 | 516 | return instance.drawBoard(); 517 | }; 518 | 519 | Fretboard.drawAll = function (selector, config) { 520 | config = config || {}; 521 | let fretboards = document.querySelectorAll(selector); 522 | 523 | fretboards.forEach(function (e) { 524 | let fretdef = e.dataset["frets"]; 525 | if (fretdef && fretdef.indexOf("-") !== -1) { 526 | [config.startFret, config.frets] = fretdef.split("-").map(function (x) { 527 | return parseInt(x); 528 | }); 529 | } else { 530 | [config.startFret, config.frets] = [0, parseInt(fretdef) || 8]; 531 | } 532 | let notes = e.dataset["notes"]; 533 | config.where = e; 534 | 535 | let fretboard = Fretboard(config); 536 | if (notes) { 537 | fretboard.add(notes); 538 | } 539 | fretboard.paint(); 540 | }); 541 | 542 | return fretboards; 543 | }; 544 | 545 | export function Guitar(strings, frets) { 546 | strings = strings || 6; 547 | frets = frets || 12; 548 | return Fretboard({ 549 | strings: strings, 550 | frets: frets, 551 | tuning: Tunings["guitar" + strings].standard, 552 | }); 553 | } 554 | 555 | export function Bass(strings, frets) { 556 | strings = strings || 4; 557 | frets = frets || 12; 558 | return Fretboard({ 559 | strings: strings, 560 | frets: frets, 561 | tuning: Tunings["bass" + strings].standard, 562 | }); 563 | } 564 | --------------------------------------------------------------------------------