function createFretboard({
showFretNumbers = false,
showStringNumbers = false,
totalWidth = width,
totalHeight = Math.max(totalWidth / 6, 120),
margin = {
top: 10,
right: 10,
left: showStringNumbers ? 30 : 10,
bottom: showFretNumbers ? 30 : 10
},
minFret = 0,
maxFret = Math.round(totalWidth / 80),
tuning = STANDARD_TUNING
} = {}) {
const svg = d3
.create("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);
const parsedTuning = tuning.map(tonal.Note.get);
const numStrings = tuning.length;
const stringToY = d3
.scaleLinear()
.domain([1, numStrings])
.range([margin.top, totalHeight - margin.bottom]);
const fretToX = d3
.scalePow()
.exponent(1 - 1 / 12)
.domain([minFret, maxFret])
.range([margin.left, totalWidth - margin.right]);
const markerSize = Math.min((stringToY(1) - stringToY(0)) / 2 - 2, 4);
const smallMarkers = [3, 5, 7, 9, 15, 17, 19, 21].filter(
(d) => d >= minFret && d <= maxFret
);
const bigMarkers = [12, 24].filter((d) => d >= minFret && d <= maxFret);
svg
.selectAll(".small-marker")
.data(smallMarkers)
.enter()
.append("circle")
.attr("class", "small-marker")
.attr("fill", "gainsboro")
.attr("cx", (d) => (fretToX(d) + fretToX(d - 1)) / 2)
.attr("cy", (d) => stringToY((numStrings + 1) / 2))
.attr("r", markerSize);
const bigMarkerGroup = svg
.selectAll(".big-marker")
.data(bigMarkers)
.enter()
.append("g")
.attr("class", "big-marker");
// Assuming 6 strings to place these big bois
bigMarkerGroup
.append("circle")
.attr("fill", "gainsboro")
.attr("cx", (d) => (fretToX(d) + fretToX(d - 1)) / 2)
.attr("cy", (d) => (stringToY(3) + stringToY(2)) / 2)
.attr("r", markerSize);
bigMarkerGroup
.append("circle")
.attr("fill", "gainsboro")
.attr("cx", (d) => (fretToX(d) + fretToX(d - 1)) / 2)
.attr("cy", (d) => (stringToY(4) + stringToY(5)) / 2)
.attr("r", markerSize);
svg
.selectAll(".string")
.data(d3.range(1, numStrings + 1))
.enter()
.append("line")
.attr("class", "string")
.attr("stroke", "slategray")
.attr("x1", fretToX(minFret))
.attr("x2", fretToX(maxFret))
.attr("y1", (d) => stringToY(d))
.attr("y2", (d) => stringToY(d));
svg
.selectAll(".fret")
.data(d3.range(minFret, maxFret + 1))
.enter()
.append("line")
.attr("class", "fret")
.attr("stroke", "slategray")
.attr("y1", stringToY(1) - 0.5)
.attr("y2", stringToY(numStrings) + 0.5)
.attr("x1", (d) => fretToX(d))
.attr("x2", (d) => fretToX(d))
.attr("stroke-width", (d) => (d === 0 ? 5 : 1));
if (showStringNumbers) {
svg
.append("g")
.attr("opacity", 0.5)
.attr("transform", `translate(${margin.left - 10}, -0.5)`)
.call(d3.axisLeft().scale(stringToY).ticks(numStrings));
}
if (showFretNumbers) {
svg
.append("g")
.attr("opacity", 0.5)
.attr("transform", `translate(-0.5, ${totalHeight - margin.bottom + 8})`)
.call(
d3
.axisBottom()
.scale(fretToX)
.ticks(maxFret - minFret)
);
}
function placeNote(noteOrStr) {
const note = tonal.Note.get(noteOrStr);
return parsedTuning
.map((string) => note.height - string.height)
.map((fret, stringIndex) =>
fret >= minFret && fret <= maxFret
? { note, fret, string: stringIndex + 1 }
: null
)
.filter(Boolean);
}
const minNote = tonal.Note.sortedNames(
parsedTuning.map((n) =>
tonal.Note.transpose(n, tonal.Interval.fromSemitones(minFret))
)
)[0];
const maxNote = tonal.Note.sortedNames(
parsedTuning.map((n) =>
tonal.Note.transpose(n, tonal.Interval.fromSemitones(maxFret))
)
)[parsedTuning.length - 1];
return Object.assign(svg.node(), {
svg,
stringToY,
fretToX,
placeNote,
minNote,
maxNote,
totalWidth,
totalHeight
});
}