diff --git a/README.md b/README.md index bb29f4d..5d2a5f9 100644 --- a/README.md +++ b/README.md @@ -75,17 +75,17 @@ Here's an example of a customized chart: ```javascript new SVGuitarChord('#some-selector') .chord({ - // array of [string, fret] + // array of [string, fret, text] fingers: [ - [1, 2], - [2, 3], + [1, 2, '2'], + [2, 3, '3'], [3, 3], [6, 'x'] ], // optional: barres for barre chords barres: [ - { fromString: 5, toString: 1, fret: 1 }, + { fromString: 5, toString: 1, fret: 1, text: '1' }, ], }) .configure({ @@ -139,7 +139,17 @@ new SVGuitarChord('#some-selector') * Color of a finger / nut */ nutColor: '#000', + + /** + * The color of text inside nuts + */ + nutTextColor: '#FFF', + /** + * The size of text inside nuts + */ + nutTextSize: 22, + /** * Height of a fret, relative to the space between two strings */ diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 79554a7..5701ca0 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -3,7 +3,7 @@ import { QuerySelector } from '@svgdotjs/svg.js' export enum Alignment { LEFT = 'left', MIDDLE = 'middle', - RIGHT = 'right' + RIGHT = 'right', } export interface GraphcisElement { @@ -41,7 +41,8 @@ export abstract class Renderer { fontSize: number, color: string, fontFamily: string, - alignment: Alignment + alignment: Alignment, + plain?: boolean ): GraphcisElement abstract circle( diff --git a/src/renderer/roughjs/roughjs-renderer.ts b/src/renderer/roughjs/roughjs-renderer.ts index 27ece3c..fc3b0fd 100644 --- a/src/renderer/roughjs/roughjs-renderer.ts +++ b/src/renderer/roughjs/roughjs-renderer.ts @@ -213,7 +213,8 @@ export class RoughJsRenderer extends Renderer { fontSize: number, color: string, fontFamily: string, - alignment: Alignment + alignment: Alignment, + plain?: boolean ): GraphcisElement { // Place the SVG namespace in a variable to easily reference it. const txtElem = document.createElementNS('http://www.w3.org/2000/svg', 'text') @@ -225,6 +226,10 @@ export class RoughJsRenderer extends Renderer { txtElem.setAttributeNS(null, 'align', alignment) txtElem.setAttributeNS(null, 'fill', color) + if (plain) { + txtElem.setAttributeNS(null, 'dominant-baseline', 'central') + } + txtElem.appendChild(document.createTextNode(text)) this.svgNode.appendChild(txtElem) diff --git a/src/renderer/svgjs/svg-js-renderer.ts b/src/renderer/svgjs/svg-js-renderer.ts index 331b894..298e0c2 100644 --- a/src/renderer/svgjs/svg-js-renderer.ts +++ b/src/renderer/svgjs/svg-js-renderer.ts @@ -49,10 +49,7 @@ export class SvgJsRenderer extends Renderer { } background(color: string) { - this.svg - .rect() - .size('100%', '100%') - .fill(color) + this.svg.rect().size('100%', '100%').fill(color) } text( @@ -62,17 +59,37 @@ export class SvgJsRenderer extends Renderer { fontSize: number, color: string, fontFamily: string, - alignment: Alignment + alignment: Alignment, + plain?: boolean ) { - const element = this.svg - .text(text) - .move(x, y) - .font({ - family: fontFamily, - size: fontSize, - anchor: alignment - }) - .fill(color) + let element + + if (plain) { + // create a text element centered at x,y. No SVG.js magic. + element = this.svg + .plain(text) + .attr({ + x, + y, + }) + .font({ + family: fontFamily, + size: fontSize, + anchor: alignment, + 'dominant-baseline': 'central', + }) + .fill(color) + } else { + element = this.svg + .text(text) + .move(x, y) + .font({ + family: fontFamily, + size: fontSize, + anchor: alignment, + }) + .fill(color) + } return this.boxToElement(element.bbox(), element.remove.bind(element)) } @@ -91,7 +108,7 @@ export class SvgJsRenderer extends Renderer { .fill(fill || 'none') .stroke({ color: strokeColor, - width: strokeWidth + width: strokeWidth, }) return this.boxToElement(element.bbox(), element.remove.bind(element)) @@ -113,7 +130,7 @@ export class SvgJsRenderer extends Renderer { .fill(fill || 'none') .stroke({ width: strokeWidth, - color: strokeColor + color: strokeColor, }) .radius(radius || 0) @@ -126,7 +143,7 @@ export class SvgJsRenderer extends Renderer { height: box.height, x: box.x, y: box.y, - remove + remove, } } } diff --git a/src/svguitar.ts b/src/svguitar.ts index a8039e0..efab0c7 100644 --- a/src/svguitar.ts +++ b/src/svguitar.ts @@ -1,13 +1,13 @@ import { QuerySelector } from '@svgdotjs/svg.js' import { range } from './utils' import { constants } from './constants' -import { SvgJsRenderer, RoughJsRenderer, Alignment, GraphcisElement, Renderer } from './renderer' +import { Alignment, GraphcisElement, Renderer, RoughJsRenderer, SvgJsRenderer } from './renderer' // Chord diagram input types (compatible with Vexchords input, see https://github.com/0xfe/vexchords) export type SilentString = 'x' export type OpenString = 0 export type Finger = [number, number | OpenString | SilentString, string?] -export type Barre = { fromString: number; toString: number; fret: number } +export type Barre = { fromString: number; toString: number; fret: number; text?: string } export type Chord = { fingers: Finger[]; barres: Barre[] } /** @@ -25,12 +25,12 @@ export const SILENT: SilentString = 'x' */ export enum FretLabelPosition { LEFT = 'left', - RIGHT = 'right' + RIGHT = 'right', } export enum ChordStyle { normal = 'normal', - handdrawn = 'handdrawn' + handdrawn = 'handdrawn', } export interface ChordSettings { @@ -83,6 +83,16 @@ export interface ChordSettings { */ nutColor?: string + /** + * The color of text inside nuts + */ + nutTextColor?: string + + /** + * The size of text inside nuts + */ + nutTextSize?: number + /** * Height of a fret, relative to the space between two strings */ @@ -192,6 +202,8 @@ interface RequiredChordSettings { fretLabelFontSize: number fretLabelPosition: FretLabelPosition nutSize: number + nutTextColor: string + nutTextSize: number sidePadding: number titleFontSize: number titleBottomMargin: number @@ -214,6 +226,8 @@ const defaultSettings: RequiredChordSettings = { fretLabelFontSize: 38, fretLabelPosition: FretLabelPosition.RIGHT, nutSize: 0.65, + nutTextColor: '#FFF', + nutTextSize: 24, sidePadding: 0.2, titleFontSize: 48, titleBottomMargin: 0, @@ -223,7 +237,7 @@ const defaultSettings: RequiredChordSettings = { topFretWidth: 10, fretSize: 1.5, barreChordRadius: 0.25, - fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' + fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif', } export class SVGuitarChord { @@ -295,7 +309,7 @@ export class SVGuitarChord { return { width: constants.width, - height: y + height: y, } } @@ -471,7 +485,7 @@ export class SVGuitarChord { const startX = constants.width * sidePadding const stringsSpacing = this.stringSpacing() - return range(strings).map(i => startX + stringsSpacing * i) + return range(strings).map((i) => startX + stringsSpacing * i) } private stringSpacing(): number { @@ -495,7 +509,7 @@ export class SVGuitarChord { const frets = this.settings.frets || defaultSettings.frets const fretSpacing = this.fretSpacing() - return range(frets, 1).map(i => startY + fretSpacing * i) + return range(frets, 1).map((i) => startY + fretSpacing * i) } private toArrayIndex(stringIndex: number): number { @@ -518,7 +532,7 @@ export class SVGuitarChord { const stroke = { color, - width: strokeWidth + width: strokeWidth, } this._chord.fingers @@ -567,47 +581,89 @@ export class SVGuitarChord { const nutSize = relativeNutSize * stringSpacing const nutColor = this.settings.nutColor || this.settings.color || defaultSettings.color const fretColor = this.settings.fretColor || this.settings.color || defaultSettings.color - const stringColor = this.settings.stringColor || this.settings.color || defaultSettings.color const barreChordRadius = this.settings.barreChordRadius || defaultSettings.barreChordRadius const strokeWidth = this.settings.strokeWidth || defaultSettings.strokeWidth + const fontFamily = this.settings.fontFamily || defaultSettings.fontFamily + const nutTextColor = this.settings.nutTextColor || defaultSettings.nutTextColor + const nutTextSize = this.settings.nutTextSize || defaultSettings.nutTextSize // draw frets - fretYPositions.forEach(fretY => { + fretYPositions.forEach((fretY) => { this.renderer.line(startX, fretY, endX, fretY, strokeWidth, fretColor) }) // draw strings - stringXPositions.forEach(stringX => { + stringXPositions.forEach((stringX) => { this.renderer.line(stringX, y, stringX, y + height + strokeWidth / 2, strokeWidth, fretColor) }) // draw fingers this._chord.fingers .filter(([_, value]) => value !== SILENT && value !== OPEN) - .map(([stringIndex, fretIndex]) => [this.toArrayIndex(stringIndex), fretIndex as number]) - .forEach(([stringIndex, fretIndex]) => { + .map<[number, number, string | undefined]>(([stringIndex, fretIndex, text]) => [ + this.toArrayIndex(stringIndex), + fretIndex as number, + text, + ]) + .forEach(([stringIndex, fretIndex, text]) => { + const nutCenterX = startX + stringIndex * stringSpacing + const nutCenterY = y + fretIndex * fretSpacing - fretSpacing / 2 + this.renderer.circle( - startX - nutSize / 2 + stringIndex * stringSpacing, - y - fretSpacing / 2 - nutSize / 2 + fretIndex * fretSpacing, + nutCenterX - nutSize / 2, + nutCenterY - nutSize / 2, nutSize, 0, nutColor, nutColor ) + + // draw text on the nut + if (text) { + this.renderer.text( + text, + nutCenterX, + nutCenterY, + nutTextSize, + nutTextColor, + fontFamily, + Alignment.MIDDLE, + true + ) + } }) // draw barre chords - this._chord.barres.forEach(({ fret, fromString, toString }) => { + this._chord.barres.forEach(({ fret, fromString, toString, text }) => { + const barreCenterY = fretYPositions[fret - 1] - fretSpacing / 2 + const fromStringX = stringXPositions[this.toArrayIndex(fromString)] + const toStringX = stringXPositions[this.toArrayIndex(toString)] + const distance = Math.abs(toString - fromString) * stringSpacing + this.renderer.rect( - stringXPositions[this.toArrayIndex(fromString)] - stringSpacing / 4, - fretYPositions[fret - 1] - fretSpacing / 2 - nutSize / 2, - Math.abs(toString - fromString) * stringSpacing + stringSpacing / 2, + fromStringX - stringSpacing / 4, + barreCenterY - nutSize / 2, + distance + stringSpacing / 2, nutSize, 0, nutColor, nutColor, nutSize * barreChordRadius ) + + // draw text on the barre chord + if (text) { + this.renderer.text( + text, + fromStringX + distance / 2, + barreCenterY, + nutTextSize, + nutTextColor, + fontFamily, + Alignment.MIDDLE, + true + ) + } }) return y + height diff --git a/test/svguitar.test.ts b/test/svguitar.test.ts index 7d75e76..5b0c696 100644 --- a/test/svguitar.test.ts +++ b/test/svguitar.test.ts @@ -34,24 +34,77 @@ describe('SVGuitarChord', () => { [2, 1], [3, 2], [4, 0], // fret 0 = open string - [5, 'x'] // fret x = muted string + [5, 'x'], // fret x = muted string ], - barres: [] + barres: [], }) .configure({ strings: 5, frets: 6, - title: 'Amaj7' + title: 'Amaj7', }) .draw() saveSvg('arbitrary chord', container.outerHTML) }) + it('Should render text on the nuts', () => { + svguitar + .chord({ + fingers: [ + [1, 2, 'A'], + [2, 1, 'B'], + [3, 2, 'C'], + [4, 0], // fret 0 = open string + [5, 'x'], // fret x = muted string + ], + barres: [], + }) + .configure({ + strings: 5, + frets: 6, + title: 'Text on Nuts', + nutTextColor: 'tomato', + }) + .draw() + + saveSvg('text on nuts', container.outerHTML) + }) + + it('Should render text on the barre chords', () => { + svguitar + .chord({ + fingers: [], + barres: [ + { + fret: 1, + fromString: 4, + toString: 1, + text: 'B', + }, + { + fret: 3, + fromString: 5, + toString: 2, + text: 'A', + }, + ], + }) + .configure({ + strings: 5, + frets: 5, + title: 'Text on Barres', + nutTextColor: 'lightgreen', + }) + .draw() + + saveSvg('text on barre chords', container.outerHTML) + }) + it('Should render a title nicely', () => { svguitar .configure({ - title: 'Test Title' + title: 'Test Title', }) .draw() @@ -61,7 +114,7 @@ describe('SVGuitarChord', () => { it('Should render a very long title nicely', () => { svguitar .configure({ - title: 'This is a very long title that does not fit easily' + title: 'This is a very long title that does not fit easily', }) .draw() @@ -71,10 +124,10 @@ describe('SVGuitarChord', () => { it('Should render 8 strings', () => { svguitar .configure({ - title: '8 Strings' + title: '8 Strings', }) .configure({ - strings: 8 + strings: 8, }) .draw() @@ -84,10 +137,10 @@ describe('SVGuitarChord', () => { it('Should render 8 frets', () => { svguitar .configure({ - title: '8 Frets' + title: '8 Frets', }) .configure({ - frets: 8 + frets: 8, }) .draw() @@ -98,7 +151,7 @@ describe('SVGuitarChord', () => { svguitar .configure({ position: 2, - fretLabelPosition: FretLabelPosition.LEFT + fretLabelPosition: FretLabelPosition.LEFT, }) .draw() @@ -109,7 +162,7 @@ describe('SVGuitarChord', () => { svguitar .configure({ position: 2, - fretLabelPosition: FretLabelPosition.RIGHT + fretLabelPosition: FretLabelPosition.RIGHT, }) .draw() @@ -120,7 +173,7 @@ describe('SVGuitarChord', () => { svguitar .configure({ strings: 5, - tuning: ['1', '2', '3', '4', '5'] + tuning: ['1', '2', '3', '4', '5'], }) .draw() @@ -131,7 +184,7 @@ describe('SVGuitarChord', () => { svguitar .configure({ strings: 5, - tuning: ['1', '2', '3', '4', '5', '6'] + tuning: ['1', '2', '3', '4', '5', '6'], }) .draw() @@ -142,7 +195,7 @@ describe('SVGuitarChord', () => { svguitar .configure({ strings: 5, - frets: 5 + frets: 5, }) .chord({ fingers: [], @@ -150,14 +203,14 @@ describe('SVGuitarChord', () => { { fret: 1, fromString: 4, - toString: 1 + toString: 1, }, { fret: 3, fromString: 5, - toString: 2 - } - ] + toString: 2, + }, + ], }) .draw() @@ -170,7 +223,7 @@ describe('SVGuitarChord', () => { color: '#f00', tuning: ['1', '2', '3', '4', '5', '6'], title: 'Test', - position: 3 + position: 3, }) .chord({ fingers: [ @@ -178,9 +231,9 @@ describe('SVGuitarChord', () => { [2, 1], [3, 2], [4, 0], // fret 0 = open string - [5, 'x'] // fret x = muted string + [5, 'x'], // fret x = muted string ], - barres: [] + barres: [], }) .draw() @@ -207,7 +260,7 @@ describe('SVGuitarChord', () => { topFretWidth: 10, fretSize: 1.5, barreChordRadius: 0.25, - fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif' + fontFamily: 'Arial, "Helvetica Neue", Helvetica, sans-serif', }) .chord({ fingers: [ @@ -215,9 +268,9 @@ describe('SVGuitarChord', () => { [2, 1], [3, 2], [4, 0], // fret 0 = open string - [5, 'x'] // fret x = muted string + [5, 'x'], // fret x = muted string ], - barres: [] + barres: [], }) .draw() @@ -232,9 +285,9 @@ describe('SVGuitarChord', () => { [2, 1], [3, 2], [4, 0], // fret 0 = open string - [5, 'x'] // fret x = muted string + [5, 'x'], // fret x = muted string ], - barres: [] + barres: [], }) .draw() @@ -246,7 +299,7 @@ describe('SVGuitarChord', () => { .configure({ title: 'Fat Strokes', strokeWidth: 10, - topFretWidth: 30 + topFretWidth: 30, }) .chord({ fingers: [ @@ -254,9 +307,9 @@ describe('SVGuitarChord', () => { [2, 1], [3, 2], [4, 0], // fret 0 = open string - [5, 'x'] // fret x = muted string + [5, 'x'], // fret x = muted string ], - barres: [] + barres: [], }) .draw() @@ -267,7 +320,7 @@ describe('SVGuitarChord', () => { svguitar .configure({ title: 'With Background', - backgroundColor: '#00FF00' + backgroundColor: '#00FF00', }) .draw() @@ -278,7 +331,7 @@ describe('SVGuitarChord', () => { svguitar .configure({ title: 'With Title', - fixedDiagramPosition: true + fixedDiagramPosition: true, }) .draw() saveSvg('fixed diagram position 1', container.outerHTML) @@ -286,18 +339,18 @@ describe('SVGuitarChord', () => { svguitar .configure({ title: undefined, - fixedDiagramPosition: true + fixedDiagramPosition: true, }) .draw() saveSvg('fixed diagram position 2', container.outerHTML) svguitar .configure({ - fixedDiagramPosition: true + fixedDiagramPosition: true, }) .chord({ fingers: [[5, 'x']], - barres: [] + barres: [], }) .draw() saveSvg('fixed diagram position 3', container.outerHTML)