import copy import attrdict import svgwrite import yaml from .compat import StringIO from .utils import dict_merge # fretboard = Fretboard(strings=6, frets=(3, 8)) # fretboard.add_string_label(string=1, label='X', color='') # fretboard.add_barre(fret=1, strings=(0, 5), label='') # fretboard.add_marker(fret=1, string=1, label='', color='') DEFAULT_STYLE = ''' drawing: background_color: white font_color: dimgray font_family: Lato font_size: 15 height: 300 width: 250 spacing: 30 nut: color: darkslategray size: 10 fret: color: darkgray size: 2 inlays: color: black radius: 2 string: color: darkslategray size: 3 marker: border_color: darkslategray color: steelblue font_color: white radius: 12 stroke_width: 2 ''' class Fretboard(object): default_style = yaml.safe_load(DEFAULT_STYLE) # Guitars and basses have different inlay patterns than, e.g., ukulele # A double inlay will be added at the octave (12th fret) inlays = (3, 5, 7, 9) def __init__(self, strings=6, frets=(0, 5), inlays=None, style=None): self.frets = list(range(max(frets[0] - 1, 0), frets[1] + 1)) self.strings = [attrdict.AttrDict({ 'color': None, 'label': None, 'font_color': None, }) for x in range(strings)] self.markers = [] self.inlays = inlays if inlays is not None else self.inlays self.layout = attrdict.AttrDict() self.style = attrdict.AttrDict( dict_merge( copy.deepcopy(self.default_style), style or {} ) ) def add_string_label(self, string, label, font_color=None): self.strings[string].label = label self.strings[string].font_color = font_color def add_marker(self, string, fret, color=None, label=None, font_color=None): self.markers.append(attrdict.AttrDict({ 'fret': fret, 'string': string, 'color': color, 'label': label, 'font_color': font_color, })) def calculate_layout(self): # Bounding box of our fretboard self.layout.update({ 'x': self.style.drawing.spacing, 'y': self.style.drawing.spacing * 1.5, 'width': self.style.drawing.width - (self.style.drawing.spacing * 2.25), 'height': self.style.drawing.height - (self.style.drawing.spacing * 2), }) # Spacing between the strings self.layout['string_space'] = self.layout.width / (len(self.strings) - 1) # Spacing between the frets, with room at the top and bottom for the nut self.layout['fret_space'] = (self.layout.height - self.style.nut.size * 2) / (len(self.frets) - 1) def draw_frets(self): top = self.layout.y + self.style.nut.size for index, fret in enumerate(self.frets): if index == 0 and self.frets[0] == 0: # The first fret is the nut, don't draw it. continue else: self.drawing.add( self.drawing.line( start=(self.layout.x, top + (self.layout.fret_space * index)), end=(self.layout.x + self.layout.width, top + (self.layout.fret_space * index)), stroke=self.style.fret.color, stroke_width=self.style.fret.size, ) ) def draw_strings(self): top = self.layout.y bottom = top + self.layout.height label_y = self.layout.y + self.style.drawing.font_size - self.style.drawing.spacing for index, string in enumerate(self.strings): width = self.style.string.size - ((self.style.string.size * 1 / (len(self.strings) * 1.5)) * index) # Offset the first and last strings, so they're not drawn outside the edge of the nut. offset = 0 if index == 0: offset += width / 2. elif index == len(self.strings) - 1: offset -= width / 2. x = self.layout.x + (self.layout.string_space * index) + offset self.drawing.add( self.drawing.line( start=(x, top), end=(x, bottom), stroke=string.color or self.style.string.color, stroke_width=width ) ) # Draw the label obove the string if string.label is not None: self.drawing.add( self.drawing.text(string.label, insert=(x, label_y), font_family=self.style.drawing.font_family, font_size=self.style.drawing.font_size, font_weight='bold', fill=string.font_color or self.style.marker.color, text_anchor='middle', alignment_baseline='middle', ) ) def draw_nut(self): if self.frets[0] == 0: top = self.layout.y + (self.style.nut.size / 2) self.drawing.add( self.drawing.line( start=(self.layout.x, top), end=(self.layout.x + self.layout.width, top), stroke=self.style.nut.color, stroke_width=self.style.nut.size, ) ) def draw_inlays(self): x = (self.style.drawing.spacing) - (self.style.inlays.radius * 4) for index, fret in enumerate(self.frets): if index == 0: continue y = sum(( self.layout.y, self.style.nut.size, self.layout.fret_space * index, )) - self.layout.fret_space / 2 if fret in self.inlays or fret - 12 in self.inlays: # Single dot inlay self.drawing.add( self.drawing.circle( center=(x, y), r=self.style.inlays.radius, fill=self.style.inlays.color, ) ) elif fret > 0 and not fret % 12: # Double dot inlay self.drawing.add( self.drawing.circle( center=(x, y - (self.style.inlays.radius * 2)), r=self.style.inlays.radius, fill=self.style.inlays.color, ) ) self.drawing.add( self.drawing.circle( center=(x, y + (self.style.inlays.radius * 2)), r=self.style.inlays.radius, fill=self.style.inlays.color, ) ) def draw_fret_label(self): if self.frets[0] > 0: x = self.layout.width + self.style.drawing.spacing + self.style.inlays.radius y = self.layout.y + self.style.nut.size + (self.style.drawing.font_size * .2) self.drawing.add( self.drawing.text('{0}fr'.format(self.frets[0]), insert=(x, y), font_family=self.style.drawing.font_family, font_size=self.style.drawing.font_size, font_style='italic', font_weight='bold', fill=self.style.drawing.font_color, text_anchor='start', ) ) def draw_markers(self): for marker in self.markers: if isinstance(marker.string, (list, tuple)): self.draw_barre(marker) else: self.draw_marker(marker) def draw_marker(self, marker): # Fretted position, add the marker to the fretboard. x = self.style.drawing.spacing + (self.layout.string_space * marker.string) y = sum(( self.layout.y, self.style.nut.size, (self.layout.fret_space * (marker.fret - self.frets[0])) - (self.layout.fret_space / 2) )) self.drawing.add( self.drawing.circle( center=(x, y), r=self.style.marker.radius, fill=marker.color or self.style.marker.color, stroke=self.style.marker.border_color, stroke_width=self.style.marker.stroke_width ) ) # Draw the label if marker.label is not None: self.drawing.add( self.drawing.text(marker.label, insert=(x, y), font_family=self.style.drawing.font_family, font_size=self.style.drawing.font_size, font_weight='bold', fill=self.style.marker.font_color, text_anchor='middle', alignment_baseline='central' ) ) def draw_barre(self, marker): start_x = self.style.drawing.spacing + (self.layout.string_space * marker.string[0]) end_x = self.style.drawing.spacing + (self.layout.string_space * marker.string[1]) y = sum(( self.layout.y, self.style.nut.size, (self.layout.fret_space * (marker.fret - self.frets[0])) - (self.layout.fret_space / 2) )) # Lines don't support borders, so fake it by drawing # a slightly larger line behind it. self.drawing.add( self.drawing.line( start=(start_x, y), end=(end_x, y), stroke=self.style.marker.border_color, stroke_linecap='round', stroke_width=(self.style.marker.radius * 2) + (self.style.marker.stroke_width * 2) ) ) self.drawing.add( self.drawing.line( start=(start_x, y), end=(end_x, y), stroke=self.style.marker.color, stroke_linecap='round', stroke_width=self.style.marker.radius * 2 ) ) if marker.label is not None: self.drawing.add( self.drawing.text(marker.label, insert=(start_x, y), font_family=self.style.drawing.font_family, font_size=self.style.drawing.font_size, font_weight='bold', fill=self.style.marker.font_color, text_anchor='middle', alignment_baseline='central' ) ) def draw(self): self.drawing = svgwrite.Drawing(size=( self.style.drawing.width, self.style.drawing.height )) if self.style.drawing.background_color is not None: self.drawing.add( self.drawing.rect( insert=(0, 0), size=( self.style.drawing.width, self.style.drawing.height ), fill=self.style.drawing.background_color ) ) self.calculate_layout() self.draw_frets() self.draw_inlays() self.draw_fret_label() self.draw_strings() self.draw_nut() self.draw_markers() def render(self, output=None): self.draw() if output is None: output = StringIO() self.drawing.write(output) return output def save(self, filename): with open(filename, 'w') as output: self.render(output)