Skip to content

Commit 8f077bd

Browse files
committed
feat(chord diagram): implement arc barre chords
Barre chords can now be drawn as arcs over the strings. The barre chord style can be configured globally or on individual barre chords.
1 parent 8094eb0 commit 8f077bd

File tree

6 files changed

+437
-109
lines changed

6 files changed

+437
-109
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ new SVGuitarChord('#some-selector')
116116
color: '#0F0',
117117
textColor: '#F00',
118118
className: 'my-barre-chord',
119+
style: 'rectangle' // use 'arc' for an arc
119120
},
120121
],
121122

@@ -203,6 +204,11 @@ new SVGuitarChord('#some-selector')
203204
*/
204205
fingerStrokeWidth: 0,
205206

207+
/**
208+
* style of barre chords. Can be either 'rectangle' (default) or 'arc'.
209+
*/
210+
barreChordStyle: 'rectangle',
211+
206212
/**
207213
* stroke color of a barre chord. Defaults to the finger color if not set
208214
*/

src/renderer/renderer.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ export enum Alignment {
66
RIGHT = 'right',
77
}
88

9+
export enum ArcDirection {
10+
UP = 'up',
11+
LEFT = 'left',
12+
}
13+
914
export interface GraphcisElement {
1015
width: number
1116
height: number
@@ -71,6 +76,18 @@ export abstract class Renderer {
7176
radius?: number,
7277
): GraphcisElement
7378

79+
abstract arc(
80+
x: number,
81+
y: number,
82+
width: number,
83+
height: number,
84+
direction: ArcDirection,
85+
strokeWidth: number,
86+
strokeColor: string,
87+
classes?: string | string[],
88+
fill?: string,
89+
): GraphcisElement
90+
7491
abstract triangle(
7592
x: number,
7693
y: number,
@@ -119,6 +136,60 @@ export abstract class Renderer {
119136
return `M${curX} ${curY} ${lines}`
120137
}
121138

139+
protected static arcBarrePath(
140+
x: number,
141+
y: number,
142+
width: number,
143+
height: number,
144+
direction: ArcDirection,
145+
): string {
146+
// arc thickness (0-1): higher means thicker
147+
const thickness = 0.35
148+
const t = Math.max(0, Math.min(1, 1 - thickness))
149+
150+
let xStart: number, yStart: number
151+
let xEnd: number, yEnd: number
152+
let cxOuter: number, cyOuter: number
153+
let cxInner: number, cyInner: number
154+
155+
switch (direction) {
156+
case ArcDirection.UP: {
157+
xStart = x
158+
yStart = y + height
159+
xEnd = x + width
160+
yEnd = y + height
161+
162+
cxOuter = x + width / 2
163+
cyOuter = y - height
164+
165+
cxInner = cxOuter
166+
cyInner = yStart - (height * 2) * t
167+
break
168+
}
169+
170+
case ArcDirection.LEFT: {
171+
xStart = x + width
172+
yStart = y
173+
xEnd = x + width
174+
yEnd = y + height
175+
176+
cxOuter = x - width
177+
cyOuter = y + height / 2
178+
179+
cxInner = xStart - (width * 2) * t
180+
cyInner = cyOuter
181+
break
182+
}
183+
}
184+
185+
return [
186+
`M ${xStart} ${yStart}`,
187+
`Q ${cxOuter} ${cyOuter} ${xEnd} ${yEnd}`,
188+
`Q ${cxInner} ${cyInner} ${xStart} ${yStart}`,
189+
`Z`,
190+
].join(' ')
191+
}
192+
122193
protected static toClassName(classes?: string | string[]): string {
123194
if (!classes) {
124195
return ''

src/renderer/roughjs/roughjs-renderer.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ that there is no SVG implementation for JSDOM. If that changes at some point thi
55
tested just like the svg.js implementation
66
*/
77

8-
import { QuerySelector } from '@svgdotjs/svg.js'
8+
import { Array, DOMRect, QuerySelector } from '@svgdotjs/svg.js'
99
import { RoughSVG } from 'roughjs/bin/svg'
1010
import rough from 'roughjs'
1111
import { Options } from 'roughjs/bin/core'
1212
import defs from './defs'
13-
import { Alignment, GraphcisElement, Renderer } from '../renderer'
13+
import { Alignment, ArcDirection, GraphcisElement, Renderer } from '../renderer'
1414

1515
/**
1616
* Currently the font is hard-coded to 'Patrick Hand' when using the handdrawn chord diagram style.
@@ -223,6 +223,32 @@ export class RoughJsRenderer extends Renderer {
223223
return RoughJsRenderer.boxToElement(rect.getBBox(), () => rect.remove())
224224
}
225225

226+
227+
arc(x: number,
228+
y: number,
229+
width: number,
230+
height: number,
231+
direction: ArcDirection,
232+
strokeWidth: number,
233+
strokeColor: string,
234+
classes?: string | string[],
235+
fill?: string,
236+
): GraphcisElement {
237+
const path = Renderer.arcBarrePath(x, y, width, height, direction)
238+
239+
const arc = this.rc.path(path, {
240+
fill: fill || 'none',
241+
fillWeight: 2.5,
242+
stroke: strokeColor || fill || 'none',
243+
roughness: 1.5,
244+
})
245+
246+
arc.classList.add(...RoughJsRenderer.toClassArray(classes))
247+
this.svgNode.appendChild(arc)
248+
249+
return RoughJsRenderer.boxToElement(arc.getBBox(), () => arc.remove())
250+
}
251+
226252
triangle(
227253
x: number,
228254
y: number,
@@ -337,11 +363,12 @@ export class RoughJsRenderer extends Renderer {
337363
}
338364

339365
private static boxToElement(box: DOMRect, remove: () => void): GraphcisElement {
366+
340367
return {
341-
width: box.width,
342-
height: box.height,
343-
x: box.x,
344-
y: box.y,
368+
width: box.width ?? 0,
369+
height: box.height ?? 0,
370+
x: box.x ?? 0,
371+
y: box.y ?? 0,
345372
remove,
346373
}
347374
}
@@ -370,6 +397,7 @@ export class RoughJsRenderer extends Renderer {
370397

371398
return Renderer.toClassName(classes).split(' ')
372399
}
400+
373401
}
374402

375403
export default RoughJsRenderer

src/renderer/svgjs/svg-js-renderer.ts

Lines changed: 87 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Box, Container, QuerySelector, SVG } from '@svgdotjs/svg.js'
2-
import { Alignment, GraphcisElement, Renderer } from '../renderer'
2+
import { Alignment, ArcDirection, GraphcisElement, Renderer } from '../renderer'
33
import { constants } from '../../constants'
44
import { isNode } from '../../utils/is-node'
55

@@ -10,7 +10,7 @@ export class SvgJsRenderer extends Renderer {
1010
super(container)
1111

1212
// initialize the SVG
13-
const { width } = constants
13+
const {width} = constants
1414
const height = 0
1515

1616
/*
@@ -42,7 +42,7 @@ export class SvgJsRenderer extends Renderer {
4242
strokeWidth: number,
4343
color: string,
4444
): void {
45-
this.svg.line(fromX, fromY, toX, toY).stroke({ color, width: strokeWidth })
45+
this.svg.line(fromX, fromY, toX, toY).stroke({color, width: strokeWidth})
4646
}
4747

4848
size(width: number, height: number): void {
@@ -77,30 +77,30 @@ export class SvgJsRenderer extends Renderer {
7777
if (plain) {
7878
// create a text element centered at x,y. No SVG.js magic.
7979
element = this.svg
80-
.plain(text)
81-
.attr({
82-
x,
83-
y,
84-
})
85-
.font({
86-
family: fontFamily,
87-
size: fontSize,
88-
anchor: alignment,
89-
'dominant-baseline': 'central',
90-
})
91-
.fill(color)
92-
.addClass(Renderer.toClassName(classes))
80+
.plain(text)
81+
.attr({
82+
x,
83+
y,
84+
})
85+
.font({
86+
family: fontFamily,
87+
size: fontSize,
88+
anchor: alignment,
89+
'dominant-baseline': 'central',
90+
})
91+
.fill(color)
92+
.addClass(Renderer.toClassName(classes))
9393
} else {
9494
element = this.svg
95-
.text(text)
96-
.move(x, y)
97-
.font({
98-
family: fontFamily,
99-
size: fontSize,
100-
anchor: alignment,
101-
})
102-
.fill(color)
103-
.addClass(Renderer.toClassName(classes))
95+
.text(text)
96+
.move(x, y)
97+
.font({
98+
family: fontFamily,
99+
size: fontSize,
100+
anchor: alignment,
101+
})
102+
.fill(color)
103+
.addClass(Renderer.toClassName(classes))
104104
}
105105

106106
return SvgJsRenderer.boxToElement(element.bbox(), element.remove.bind(element))
@@ -116,14 +116,14 @@ export class SvgJsRenderer extends Renderer {
116116
classes?: string | string[],
117117
): GraphcisElement {
118118
const element = this.svg
119-
.circle(diameter)
120-
.move(x, y)
121-
.fill(fill || 'none')
122-
.stroke({
123-
color: strokeColor,
124-
width: strokeWidth,
125-
})
126-
.addClass(Renderer.toClassName(classes))
119+
.circle(diameter)
120+
.move(x, y)
121+
.fill(fill || 'none')
122+
.stroke({
123+
color: strokeColor,
124+
width: strokeWidth,
125+
})
126+
.addClass(Renderer.toClassName(classes))
127127

128128
return SvgJsRenderer.boxToElement(element.bbox(), element.remove.bind(element))
129129
}
@@ -140,15 +140,15 @@ export class SvgJsRenderer extends Renderer {
140140
radius?: number,
141141
): GraphcisElement {
142142
const element = this.svg
143-
.rect(width, height)
144-
.move(x, y)
145-
.fill(fill || 'none')
146-
.stroke({
147-
width: strokeWidth,
148-
color: strokeColor,
149-
})
150-
.radius(radius || 0)
151-
.addClass(Renderer.toClassName(classes))
143+
.rect(width, height)
144+
.move(x, y)
145+
.fill(fill || 'none')
146+
.stroke({
147+
width: strokeWidth,
148+
color: strokeColor,
149+
})
150+
.radius(radius || 0)
151+
.addClass(Renderer.toClassName(classes))
152152

153153
return SvgJsRenderer.boxToElement(element.bbox(), element.remove.bind(element))
154154
}
@@ -163,14 +163,14 @@ export class SvgJsRenderer extends Renderer {
163163
fill?: string | undefined,
164164
): GraphcisElement {
165165
const element = this.svg
166-
.path(Renderer.trianglePath(x, y, size))
167-
.move(x, y)
168-
.fill(fill || 'none')
169-
.stroke({
170-
width: strokeWidth,
171-
color: strokeColor,
172-
})
173-
.addClass(Renderer.toClassName(classes))
166+
.path(Renderer.trianglePath(x, y, size))
167+
.move(x, y)
168+
.fill(fill || 'none')
169+
.stroke({
170+
width: strokeWidth,
171+
color: strokeColor,
172+
})
173+
.addClass(Renderer.toClassName(classes))
174174

175175
return SvgJsRenderer.boxToElement(element.bbox(), element.remove.bind(element))
176176
}
@@ -198,14 +198,14 @@ export class SvgJsRenderer extends Renderer {
198198
classes?: string | string[],
199199
) {
200200
const element = this.svg
201-
.path(Renderer.ngonPath(x, y, size, edges))
202-
.move(x, y)
203-
.fill(fill || 'none')
204-
.stroke({
205-
width: strokeWidth,
206-
color: strokeColor,
207-
})
208-
.addClass(Renderer.toClassName(classes))
201+
.path(Renderer.ngonPath(x, y, size, edges))
202+
.move(x, y)
203+
.fill(fill || 'none')
204+
.stroke({
205+
width: strokeWidth,
206+
color: strokeColor,
207+
})
208+
.addClass(Renderer.toClassName(classes))
209209

210210
return SvgJsRenderer.boxToElement(element.bbox(), element.remove.bind(element))
211211
}
@@ -219,6 +219,35 @@ export class SvgJsRenderer extends Renderer {
219219
remove,
220220
}
221221
}
222+
223+
arc(x: number,
224+
y: number,
225+
width: number,
226+
height: number,
227+
direction: ArcDirection,
228+
strokeWidth: number,
229+
strokeColor: string,
230+
classes?: string | string[],
231+
fill?: string,
232+
): GraphcisElement {
233+
const path = Renderer.arcBarrePath(x, y, width, height, direction)
234+
235+
const element = this.svg
236+
.path(path)
237+
.stroke({
238+
width: strokeWidth,
239+
color: strokeColor,
240+
linecap: 'round',
241+
})
242+
.fill({
243+
color: fill,
244+
})
245+
.addClass(Renderer.toClassName(classes))
246+
247+
// this.rect(x, y, width, height, strokeWidth, strokeColor, classes, 'rgba(0, 0, 0, 0.2)') // TODO: remove rectangle
248+
249+
return SvgJsRenderer.boxToElement(element.bbox(), element.remove.bind(element))
250+
}
222251
}
223252

224253
export default SvgJsRenderer

0 commit comments

Comments
 (0)