diff --git a/src/chord_diagram/chord_diagram.ts b/src/chord_diagram/chord_diagram.ts
new file mode 100644
index 00000000..1ee2518b
--- /dev/null
+++ b/src/chord_diagram/chord_diagram.ts
@@ -0,0 +1,222 @@
+import { Renderer } from './renderer';
+
+type FingerNumber = 1 | 2 | 3 | 4 | 5;
+type FretNumber = 1 | 2 | 3 | 4 | 5;
+type StringNumber = 1 | 2 | 3 | 4 | 5 | 6;
+
+interface Barre { from: StringNumber, to: StringNumber, fret: FretNumber }
+type StringList = StringNumber[];
+interface StringMarker { string: StringNumber, fret: FretNumber, finger?: FingerNumber }
+
+interface ChordDiagramOptions {
+ barres: Barre[];
+ chord: string;
+ fretCount: number;
+ markers: StringMarker[];
+ openStrings: StringList;
+ stringCount: number;
+ unusedStrings: StringList;
+}
+
+function repeat(count: number, callback: (i: number) => void): void {
+ Array.from({ length: count }).forEach((_, i) => callback(i));
+}
+
+class ChordDiagram {
+ options: ChordDiagramOptions;
+
+ renderer?: Renderer;
+
+ titleY = 36;
+
+ neckWidth = 240;
+
+ neckHeight = 426;
+
+ nutThickness = 10;
+
+ fretThickness = 2;
+
+ stringIndicatorSize = 28.8;
+
+ constructor(options: ChordDiagramOptions) {
+ this.options = options;
+ }
+
+ get neckX() {
+ return (this.stringIndicatorSize / 2) + 1;
+ }
+
+ get neckY() {
+ return this.stringIndicatorY + 29.3;
+ }
+
+ get stringIndicatorY() {
+ return this.titleY + 38.7;
+ }
+
+ get fingerNumberIndicatorsY() {
+ return this.neckY + this.neckHeight + 36;
+ }
+
+ get nutThicknessCorrection() {
+ return this.nutThickness - this.fretThickness;
+ }
+
+ render(renderer: Renderer) {
+ this.renderTitle(renderer);
+ this.renderStrings(renderer);
+ this.renderNut(renderer);
+ this.renderFrets(renderer);
+ this.renderOpenStringIndicators(renderer);
+ this.renderUnusedStringIndicators(renderer);
+ this.renderStringMarkers(renderer);
+ this.renderBarres(renderer);
+ this.renderFingerNumberIndicators(renderer);
+ }
+
+ renderTitle(renderer: Renderer) {
+ const { chord } = this.options;
+
+ renderer.text(
+ chord,
+ {
+ fontSize: 48,
+ x: (this.neckX + (this.neckWidth / 2)),
+ y: this.titleY,
+ },
+ );
+ }
+
+ renderStrings(renderer: Renderer) {
+ const { stringCount } = this.options;
+
+ repeat(stringCount, (stringIndex) => {
+ renderer.line({
+ x1: this.neckX + (stringIndex * (this.neckWidth / (stringCount - 1))),
+ y1: this.neckY,
+ x2: this.neckX + (stringIndex * (this.neckWidth / (stringCount - 1))),
+ y2: this.neckY + this.neckHeight,
+ thickness: 2,
+ });
+ });
+ }
+
+ renderNut(renderer: Renderer) {
+ renderer.line({
+ x1: this.neckX,
+ y1: this.neckY,
+ x2: this.neckX + this.neckWidth,
+ y2: this.neckY,
+ thickness: this.nutThickness,
+ });
+ }
+
+ renderFrets(renderer: Renderer) {
+ const { fretCount } = this.options;
+ const fretSpacing = (this.neckHeight - this.nutThicknessCorrection) / fretCount;
+
+ repeat(fretCount, (fretIndex) => {
+ renderer.line({
+ x1: this.neckX - this.fretThickness,
+ y1: this.neckY + this.nutThicknessCorrection + ((fretIndex + 1) * fretSpacing),
+ x2: this.neckX - this.fretThickness + this.neckWidth + (2 * this.fretThickness),
+ y2: this.neckY + this.nutThicknessCorrection + ((fretIndex + 1) * fretSpacing),
+ thickness: this.fretThickness,
+ });
+ });
+ }
+
+ renderOpenStringIndicators(renderer: Renderer) {
+ const { openStrings, stringCount } = this.options;
+
+ openStrings.forEach((stringNumber: StringNumber) => {
+ renderer.circle({
+ size: this.stringIndicatorSize,
+ x: this.neckX + ((stringNumber - 1) * (this.neckWidth / (stringCount - 1))),
+ y: this.stringIndicatorY,
+ thickness: 2,
+ });
+ });
+ }
+
+ renderUnusedStringIndicators(renderer: Renderer) {
+ const { stringCount, unusedStrings } = this.options;
+
+ unusedStrings.forEach((stringNumber: StringNumber) => {
+ const x = this.neckX + ((stringNumber - 1) * (this.neckWidth / (stringCount - 1)));
+ const y = this.stringIndicatorY;
+ const size = this.stringIndicatorSize;
+
+ renderer.line({
+ x1: x - size / 2,
+ y1: y - size / 2,
+ x2: x + size / 2,
+ y2: y + size / 2,
+ thickness: 2,
+ });
+
+ renderer.line({
+ x1: x + size / 2,
+ y1: y - size / 2,
+ x2: x - size / 2,
+ y2: y + size / 2,
+ thickness: 2,
+ });
+ });
+ }
+
+ renderStringMarkers(renderer: Renderer) {
+ const { fretCount, markers } = this.options;
+ const fretSpacing = (this.neckHeight - this.nutThicknessCorrection) / fretCount;
+
+ markers.forEach(({ string, fret }: StringMarker) => {
+ renderer.circle({
+ x: this.neckX + ((string - 1) * (this.neckWidth / 5)),
+ y: this.neckY + this.nutThicknessCorrection + (fret * fretSpacing) - (fretSpacing / 2),
+ size: 31.2,
+ fill: true,
+ thickness: 2,
+ });
+ });
+ }
+
+ renderBarres(renderer: Renderer) {
+ const { barres, fretCount, stringCount } = this.options;
+ const fretSpacing = (this.neckHeight - this.nutThicknessCorrection) / fretCount;
+ const barreHeight = fretSpacing / 3.0;
+ const stringSpacing = this.neckWidth / (stringCount - 1);
+
+ barres.forEach(({ from, to, fret }: Barre) => {
+ const stringSpaceCount = to - from;
+
+ renderer.rect({
+ x: this.neckX + ((from - 0.5) * stringSpacing),
+ y: this.neckY + this.nutThicknessCorrection + ((fret - 0.5) * fretSpacing) - (barreHeight / 2),
+ width: stringSpaceCount * stringSpacing,
+ height: barreHeight,
+ thickness: 2,
+ radius: 15.6,
+ fill: true,
+ });
+ });
+ }
+
+ renderFingerNumberIndicators(renderer: Renderer) {
+ const { markers, stringCount } = this.options;
+ const stringSpacing = this.neckWidth / (stringCount - 1);
+
+ markers.forEach(({ string, finger }: StringMarker) => {
+ renderer.text(
+ `${finger}`,
+ {
+ fontSize: 28,
+ x: this.neckX + ((string - 1) * stringSpacing),
+ y: this.fingerNumberIndicatorsY,
+ },
+ );
+ });
+ }
+}
+
+export default ChordDiagram;
diff --git a/src/chord_diagram/js_pdf_renderer.ts b/src/chord_diagram/js_pdf_renderer.ts
new file mode 100644
index 00000000..5edacebd
--- /dev/null
+++ b/src/chord_diagram/js_pdf_renderer.ts
@@ -0,0 +1,116 @@
+import JsPDF from 'jspdf';
+import {
+ DimensionOpts,
+ FillOpts, PositionOpts, RadiusOpts, Renderer, SizeOpts, ThicknessOpts,
+} from './renderer';
+
+class JsPDFRenderer implements Renderer {
+ doc: JsPDF;
+
+ #x: number;
+
+ #y: number;
+
+ #scale: number;
+
+ #defaultWidth = 280;
+
+ #defaultHeight = 568;
+
+ renderedHeight: number;
+
+ constructor(doc: JsPDF, { x, y, width }: { x: number; y: number, width: number }) {
+ this.doc = doc;
+ this.#x = x;
+ this.#y = y;
+ this.#scale = width / this.#defaultWidth;
+ this.renderedHeight = this.#defaultHeight * this.#scale;
+ }
+
+ scale(number: number) {
+ return number * this.#scale;
+ }
+
+ tx(x: number) {
+ return this.#x + this.scale(x);
+ }
+
+ ty(y: number) {
+ return this.#y + this.scale(y);
+ }
+
+ circle({
+ x, y, size, fill, thickness,
+ }: FillOpts & PositionOpts & SizeOpts & ThicknessOpts) {
+ this.withLineWidth(thickness, () => {
+ this.withDrawColor(0, () => {
+ this.doc.circle(this.tx(x), this.ty(y), this.scale(size / 2), fill ? 'F' : 'S');
+ });
+ });
+ }
+
+ line({
+ x1, y1, x2, y2, thickness,
+ }: { x1: number, y1: number, x2: number, y2: number } & ThicknessOpts) {
+ this.withLineWidth(thickness, () => {
+ this.withDrawColor(0, () => {
+ this.doc.line(this.tx(x1), this.ty(y1), this.tx(x2), this.ty(y2));
+ });
+ });
+ }
+
+ rect({
+ x, y, width, height, fill, thickness, radius,
+ }: DimensionOpts & FillOpts & PositionOpts & RadiusOpts & ThicknessOpts) {
+ this.withLineWidth(thickness, () => {
+ this.withDrawColor(0, () => {
+ this.doc.roundedRect(
+ this.tx(x),
+ this.ty(y),
+ this.scale(width),
+ this.scale(height),
+ this.scale(radius),
+ this.scale(radius),
+ fill ? 'F' : 'S',
+ );
+ });
+ });
+ }
+
+ text(text: string, { fontSize, x, y }: { fontSize: number, x: number, y: number }) {
+ this.withFontSize(fontSize, () => {
+ const textDimensions = this.doc.getTextDimensions(text);
+ this.doc.text(text, this.tx(x) - (textDimensions.w / 2), this.ty(y));
+ });
+ }
+
+ withDrawColor(drawColor: number, callback: () => void) {
+ const previousDrawColor = this.doc.getDrawColor();
+ this.doc.setDrawColor(drawColor);
+ callback();
+ this.doc.setDrawColor(previousDrawColor);
+ }
+
+ withFillColor(fillColor: string, callback: () => void) {
+ const previousFillColor = this.doc.getFillColor();
+ this.doc.setFillColor(fillColor);
+ callback();
+ this.doc.setFillColor(previousFillColor);
+ }
+
+ withFontSize(fontSize: number, callback: () => void) {
+ const previousFontSize = this.doc.getFontSize();
+ this.doc.setFontSize(this.scale(fontSize));
+ callback();
+ this.doc.setFontSize(previousFontSize);
+ }
+
+ withLineWidth(lineWidth: number, callback: () => void) {
+ const previousLineWidth = this.doc.getLineWidth();
+ this.doc.setLineWidth(this.scale(lineWidth));
+ callback();
+ this.doc.setLineWidth(previousLineWidth);
+ }
+}
+
+export default JsPDFRenderer;
diff --git a/src/chord_diagram/renderer.ts b/src/chord_diagram/renderer.ts
new file mode 100644
index 00000000..f24bbcab
--- /dev/null
+++ b/src/chord_diagram/renderer.ts
@@ -0,0 +1,24 @@
+export interface RadiusOpts { radius: number }
+export interface FillOpts { fill?: boolean }
+interface HeightOpts { height: number }
+export interface PositionOpts { x: number, y: number }
+export interface SizeOpts { size: number }
+export interface ThicknessOpts { thickness: number }
+interface WidthOpts { width: number }
+export type DimensionOpts = WidthOpts & HeightOpts;
+
+export interface Renderer {
+ circle({
+ x, y, size, fill, thickness,
+ }: FillOpts & PositionOpts & SizeOpts & ThicknessOpts): void
+
+ line({
+ x1, y1, x2, y2, thickness,
+ }: { x1: number, y1: number, x2: number, y2: number } & ThicknessOpts): void
+
+ rect({
+ x, y, width, height, fill, thickness, radius,
+ }: DimensionOpts & FillOpts & PositionOpts & RadiusOpts & ThicknessOpts): void
+
+ text(text: string, { fontSize, x, y } : { fontSize: number, x: number, y: number }): void;
+}
diff --git a/src/chord_diagram/svg_renderer.ts b/src/chord_diagram/svg_renderer.ts
new file mode 100644
index 00000000..920350f9
--- /dev/null
+++ b/src/chord_diagram/svg_renderer.ts
@@ -0,0 +1,99 @@
+import {
+ DimensionOpts, FillOpts, PositionOpts, RadiusOpts, Renderer, SizeOpts, ThicknessOpts,
+} from './renderer';
+
+type ViewBox = [number, number, number, number];
+
+class SVGRenderer implements Renderer {
+ content = '';
+
+ viewBox: ViewBox;
+
+ constructor({ viewBox }: { viewBox: ViewBox }) {
+ this.viewBox = viewBox;
+ }
+
+ output() {
+ return this.svgWrapper(this.content);
+ }
+
+ svgWrapper(innerHTML: string) {
+ return `
+
+ `;
+ }
+
+ circle({
+ x, y, size, fill, thickness,
+ }: FillOpts & PositionOpts & SizeOpts & ThicknessOpts) {
+ this.content += `
+
+ `;
+ }
+
+ line({
+ x1, y1, x2, y2, thickness,
+ }: {
+ x1: number, y1: number, x2: number, y2: number,
+ } & ThicknessOpts): void {
+ this.content += `
+
+ `;
+ }
+
+ rect({
+ x, y, width, height, fill, thickness, radius,
+ }: DimensionOpts & FillOpts & PositionOpts & RadiusOpts & ThicknessOpts): void {
+ this.content += `
+
+ `;
+ }
+
+ text(text: string, { fontSize, x, y } : { fontSize: number, x: number, y: number }): void {
+ this.content += `
+ ${text}
+ `;
+ }
+}
+
+export default SVGRenderer;
diff --git a/src/formatter/pdf_formatter.ts b/src/formatter/pdf_formatter.ts
index 9df1f98c..096a81d0 100644
--- a/src/formatter/pdf_formatter.ts
+++ b/src/formatter/pdf_formatter.ts
@@ -50,6 +50,8 @@ import { Performance } from 'perf_hooks';
import { Blob } from 'buffer';
import Configuration, { defaultConfiguration } from './configuration';
import { getCapos } from '../helpers';
+import JsPDFRenderer from '../chord_diagram/js_pdf_renderer';
+import ChordDiagram from '../chord_diagram/chord_diagram';
declare const performance: Performance;
class PdfFormatter extends Formatter {
@@ -93,11 +95,12 @@ class PdfFormatter extends Formatter {
this.configuration = configuration;
this.pdfConfiguration = pdfConfiguration;
this.doc = this.setupDoc(docConstructor);
- this.margins = this.pdfConfiguration.layout.global.margins;
+
this.y = this.margins.top + this.pdfConfiguration.layout.header.height;
this.x = this.margins.left;
this.currentColumn = 1;
- this.formatParagraphs();
+ // this.formatParagraphs();
+ this.renderChordDiagram();
this.recordFormattingTime();
// Must render the footer and header after all formatting
@@ -110,6 +113,33 @@ class PdfFormatter extends Formatter {
}
}
+ renderChordDiagram() {
+ const chordDiagram = new ChordDiagram({
+ barres: [
+ { from: 3, to: 6, fret: 1 },
+ { from: 1, to: 5, fret: 5 },
+ ],
+ chord: 'Bm7',
+ markers: [
+ { string: 2, fret: 1, finger: 3 },
+ { string: 3, fret: 2, finger: 4 },
+ { string: 4, fret: 3, finger: 2 },
+ { string: 5, fret: 4, finger: 1 },
+ { string: 6, fret: 5, finger: 5 },
+ ],
+ fretCount: 5,
+ stringCount: 6,
+ openStrings: [6],
+ unusedStrings: [1, 2, 3, 4, 5],
+ });
+
+ const renderer = new JsPDFRenderer(this.doc, { x: 50, y: 100, width: 140 });
+ console.log(`rendered height: ${renderer.renderedHeight}`);
+ chordDiagram.render(renderer);
+ this.doc.setDrawColor(0);
+ this.doc.rect(50, 100, 140, renderer.renderedHeight, 'S');
+ }
+
// Save the formatted document as a PDF file
save(): void {
this.doc.save(`${this.song.title || 'untitled'}.pdf`);