FretboardJs





5.00/5 (8投票s)
SVG 吉他指板库
引言
目标是创建一个用于JavaScript的吉他指板框架。FretboardJs在SVG元素中渲染。
使用者向指板添加音符,并使用全面的和弦和音阶库来创建复杂的指板应用程序,同时忽略有关指板逻辑的细节。
可以在示例代码中找到使用FretboardJs的示例应用程序。此应用程序利用和弦库来渲染和弦族。吉他手输入和弦名称,应用程序显示和弦族,展示了各种位置和指法。
使用库
让我们看看如何使用FretboardJs库。
首先:包含fretboard.version.js文件的链接,并定义具有以下结构的SVG:
<svg id="svg" xmlns="http://www.w3.org/2000/svg">
<g id="resources">
<radialGradient id="fingering-dot-gradient" cx="60%" cy="40%" r="50%" fx="50%" fy="50%">
<stop offset="0%" style="stop-color:rgb(255,0,0); stop-opacity:1"></stop>
<stop offset="100%" style="stop-color:rgb(80,0,0);stop-opacity:1"></stop>
</radialGradient>
<linearGradient id="fretboard-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#2b2b2b"></stop>
<stop offset="10%" style="stop-color:#191919"></stop>
<stop offset="50%" style="stop-color:black"></stop>
<stop offset="90%" style="stop-color:#191919"></stop>
<stop offset="100%" style="stop-color:#2b2b2b"></stop>
</linearGradient>
<linearGradient id="head-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#3d3d3d"></stop>
<stop offset="10%" style="stop-color:#191919"></stop>
<stop offset="50%" style="stop-color:black"></stop>
<stop offset="90%" style="stop-color:#191919"></stop>
<stop offset="100%" style="stop-color:#3d3d3d"></stop>
</linearGradient>
<pattern id="pearl-inlay" patternUnits="userSpaceOnUse" width="100" height="100">
<image xlink:href="/images/pearl-inlay.jpg" x="0" y="0" width="100" height="100"></image>
</pattern>
</g>
<g id="fretboard-layer">
</g>
<g id="fingering-layer">
</g>
</svg>
这里有一些重要的id
标签
- fingering-dot-gradient: 指板点的径向渐变。
- fretboard-gradient: 指板弦颈的线性渐变。
- head-gradient: 琴头的线性渐变。
- pearl-inlay: 第3、5、7、9和12品指板镶嵌元素的图像图案。
- fretboard-layer: 所有静态指板弦颈元素(琴弦、品丝、镶嵌)将放置在其中的SVG组。
- fingering-layer: 所有点将渲染在其中的SVG组。
资源组定义了渲染代码使用的一些渐变色(将其放在标记中,方便用户编辑这些值。例如,pearl-inlay图像可以简单地替换为纯色或渐变,或其他图像。或者,弦颈渐变可以直接在标记中轻松修改。
接下来,一旦上述标记到位并且框架已加载,使用者代码只需执行以下语句
Fretboard.Neck.Draw(svg);
这将在SVG元素内渲染整个指板。结果将如下所示
指法使用Finger
对象定义
new Finger(fret, string, finger)
我们可以通过以下方式向指板添加音符或“点”
Fretboard.Neck.AddDot(Finger);
因此,任何任意音符都可以通过以下方式渲染
Fretboard.Neck.AddDot(new Finger(3, 5, 2));
这会在客户端生成
和弦呢?
Chord
对象定义为命名指法集合,如下所示:
new Chord(name, [ new Finger(...), new Finger(...), new Finger(...), new Finger(...) ])
例如,名为'AØ'的指法集合如下所示:
new Chord('AØ', [ new Finger(5, 6, 2), new Finger(5, 4, 3), new Finger(5, 3, 4), new Finger(4, 2, 1), ])
然而,在Fretboard.Chords
命名空间中存在一个丰富的和弦库(参见下方关于Chord
库的讨论)。例如,Fretboard.Chords.Dominant7th
对象是代表属七和弦指法的Chord
对象数组。同样,对于Fretboard.Chords.MajorTriad
以及其他许多。
一旦我们有了和弦实例,我们就可以调用DrawChord方法,如下所示
Fretboard.Neck.AddChord(Fretboard.Chords.Dominant7th[0]);
结果是
就这样,很简单,对吧?
要了解使用FretboardJs可以构建的应用程序类型,请参阅演示应用程序“和弦探索器”,它利用FretboardJs库探索和弦族的各种指法位置。输入文本字段允许吉他手输入和弦名称并迭代各种指法。和弦是从上述和弦库中选择的。
实现
本节讨论FretboardJs
的实现细节。
定义全局命名空间Fretboard
后,定义了两个指定整体指板行为的对象:Fretboard.Metrics
和Fretboard.
Neck
。
指标
Fretboard.Metrics
提供了诸如指板宽度、第n品长度、琴弦位置、点位置等值的度量。
校准
Fretboard.Metric
对象在初始化时进行校准,此时直接从SVG的父元素检索SVG元素的可用宽度。最大可用宽度用于将SVG元素本身的宽度设置为父元素宽度的95%。此Width
成为指板的“长度”,然后用于确定弦颈的宽度或“高度”,并将SVG元素的高度设置为此Height
值。高度任意选择为宽度的22.75%。(术语宽度/高度/长度/宽度可能存在混淆,因此我们放弃了弦颈特定术语如弦颈长度或弦颈宽度之间的区别,现在只称SVG元素的宽度和高度。)
然后,Calibrate
函数将琴枕的位置设置为宽度(Width)的3%,并将其存储在私有变量barPosition
中。
由于SVG元素使用了父元素宽度的95%,将SVG元素的左右边距设置为2.5%将创建完全响应式渲染。
function Calibrate(svg) { var fretboardApp = svg.parentNode; self.Width = Math.round(.95 * fretboardApp.clientWidth); self.Height = Math.round(0.2275 * self.Width); svg.setAttribute('width', self.Width + "px"); svg.setAttribute('height', self.Height + "px"); barPosition = self.Width * 0.0375; }
品位位置
品位使用Fretboard.Metric.FretPosition(n)
函数进行定位。该函数使用标准的鲁特琴制造者品位宽度函数,返回给定品位的x轴位置。
function FretPosition(n) { var length = self.Width * 1.85; var position = barPosition + length - (length / Math.pow(2, (n / 12))); return position; }
手指位置
Fretboard.Metric.FingerPosition(n)
返回给定品位n
的x轴距离。此方法返回第n品和第(n-1)品之间的中点。例如,第一品手指位置是第0品和第一品之间的中点。
function FingerPosition(n) { if (n < 0 || n > highestFret) { throw "Argument out of range: N-th Fret must be between 0 and 9 inclusive. n = " + n; } var p = FretPosition(n - 1); var q = FretPosition(n); var position = p + (q - p) / 2; return position; }
琴弦位置
最后,Fretboard.Metric.
StringPosition(n)
返回第n根琴弦的y轴位置。
function StringPosition(n) { if (n < 1 || n > 7) { throw "Argument out of range: N-th String must be between 1 and 6 inclusive. n = " + n; } var result = 0.086 * self.Height + (n - 1) * 0.165 * self.Height; return result; }
弦颈
Fretboard.Neck
负责绘制SVG元素,包括琴弦、品丝、镶嵌等,并负责绘制指法。
DrawFretboard(svg)
方法接受一个SVG元素,通过调用Calibration
函数来配置Metrics
对象,然后继续渲染弦颈、镶嵌、品丝、琴弦和琴枕(或琴头)。
从SVG元素中检索两个重要的SVG组“fretboard-layer”和“fingering-layer”,并将它们存储在私有变量fretboardLayer
和fingeringLayer
中。
function DrawFretboard(svg) { app = svg.parentNode; fretboardLayer = svg.getElementById('fretboard-layer'); fingeringLayer = svg.getElementById('fingering-layer'); Fretboard.Metrics.Calibrate(svg); AddNeckDetail(3); AddNeckDetail(5); AddNeckDetail(7); AddNeckDetail(9); AddNeckDetail(12); AddFrets(); AddStrings(); DrawNut(); }
绘制弦颈
DrawNeck
现在在fretboardLayer
组(在DrawFretboard
方法中初始化的私有变量)中创建一个矩形。矩形的左上角位置设置为贴靠琴枕,并距离SVG边框边缘向下3像素。矩形延伸到SVG元素的整个宽度,直到距离底部SVG边框上方3像素。背景填充被指定为ID值'fretboard-gradient',该值在SVG的标记中定义为线性渐变。
function DrawNeck() { var shape = document.createElementNS(Fretboard.NS, "rect"); shape.x.baseVal.value = Fretboard.Metrics.BarPosition; shape.y.baseVal.value = 3; shape.width.baseVal.value = width; shape.height.baseVal.value = height - 6; shape.setAttribute("height", height - 6); shape.style.stroke = 'black'; shape.style.strokeWidth = 2; shape.style.fill = 'url(#fretboard-gradient)'; fretboardLayer.appendChild(shape); return shape; }
此时可能会问:为什么要在JavaScript中定义这样的指板矩形或其他元素,如琴弦、品丝等,而不是在标记中?那样做的问题是,像指法这样的程序化元素的度量将与标记元素区分开来。
因此,与上面的代码一样,其他指板组件也以同样的方式渲染到fretboardLayer
组中。
添加品丝
每个品丝使用DrawFret
方法绘制,该方法使用Fretboard.Metrics.FretPosition
函数来获取每个品丝,并在给定x轴距离处绘制一条白色垂直线。此方法为从Fretboard.Metrics.HighestFret
到最高品位的所有品丝调用。
function DrawFret(x1, y1, x2, y2) { var shape = document.createElementNS(Fretboard.NS, "line"); shape.x1.baseVal.value = x1; shape.x2.baseVal.value = x2; shape.y1.baseVal.value = y1; shape.y2.baseVal.value = y2; shape.style.stroke = 'white'; shape.style.strokeWidth = width * 0.0035; fretboardLayer.appendChild(shape); return shape; } function AddFrets() { for (var i = 1; i <= Fretboard.Metrics.HighestFret; i++) { var position = Fretboard.Metrics.FretPosition(i); DrawFret(position, 2, position, height - 2); } }
添加琴弦
重复相同的过程来添加琴弦。AddStrings
函数为6根琴弦中的每一根检索y轴上的第n根琴弦位置,并绘制一条从琴枕到指板右端的线。
function DrawString(x1, y1, x2, y2, guage) { var shape = document.createElementNS(Fretboard.NS, "line"); shape.x1.baseVal.value = x1; shape.x2.baseVal.value = x2; shape.y1.baseVal.value = y1; shape.y2.baseVal.value = y2; shape.style.stroke = '#cbcbcb'; shape.style.strokeWidth = guage; fretboardLayer.appendChild(shape); return shape; } function AddStrings() { for (var i = 1; i < 7; i++) { var position = Fretboard.Metrics.StringPosition(i); DrawString(Fretboard.Metrics.BarPosition, position, width, position, Fretboard.Metrics.StringGague(i)); } }
添加点
通过添加点将指法添加到指板。点添加到fingeringLayer
SVG组。
AddDot
函数接受一个Finger
对象和一个可选的ghost
参数。如果ghost
为true,则点将呈现半透明的淡白色。Finger
对象指定点的Fret
(品位)、String
(琴弦)和Finger
(手指)。
该函数现在使用SVGcircle
元素创建点,并且根据ghost
是true还是false,用半透明的白色填充点,或者填充标记中定义的具有ID值'fingering-dot-gradient'的径向渐变。
接下来,函数确定Fret
是否为0,在这种情况下,填充是透明的,并且circle
被赋予了宽的蓝色描边宽度。
然后将dot
附加到fingeringLayer
SVG组。
最后,如果Fret
不是0,则创建一个显示手指值(Finger)的SVG文本元素,并将其居中放置在圆点中。
function AddDot(finger, ghost) { var dot = document.createElementNS(Fretboard.NS, "circle"); dot.Finger = finger; dot.setAttributeNS(null, "cx", Fretboard.Metrics.FingerPosition(finger.Fret)); dot.setAttributeNS(null, "cy", Fretboard.Metrics.StringPosition(finger.String)); dot.setAttributeNS(null, "r", .017 * width); if (ghost) { dot.setAttributeNS(null, "fill", "white"); dot.setAttributeNS(null, "opacity", ".1"); } else { dot.setAttributeNS(null, "fill", "url(#fingering-dot-gradient)"); } if (finger.Fret == 0) { dot.setAttributeNS(null, "cx", .011 * width + width * 0.004); dot.setAttributeNS(null, "r", .008 * width); dot.setAttributeNS(null, "fill", "transparent"); dot.style.stroke = '#0090ff'; dot.style.strokeWidth = width * 0.004; } fingeringLayer.appendChild(dot); if (!ghost && !!finger.Fret && !!finger.Finger) { var text = document.createElementNS(Fretboard.NS, "text"); text.setAttribute('x', Fretboard.Metrics.FingerPosition(finger.Fret) - width * 0.006); text.setAttribute('y', Fretboard.Metrics.StringPosition(finger.String) + width * 0.007); text.textContent = finger.Finger; text.setAttributeNS(null, "fill", "white"); text.style.fontSize = width * .0225 + 'px'; text.style.fontWeight = 'lighter'; fingeringLayer.appendChild(text); fingeringText.push(text); } dot.addEventListener('click', OnClickDot); return dot; }
清除指法
可以通过调用Fretboard.Neck.EraseFingerings
函数来清除指板上的所有指法,该函数只需移除fingeringLayer
SVG组的所有内部内容。
function EraseFingerings() { fingeringLayer.textContent = ''; }
添加和弦和音阶
AddChord
和AddScale
函数分别接受Chord
和Scale
对象,并为给定Chord
或Scale
中定义的每个指法调用AddDot
函数。
以下是AddChord
函数的示例。(AddScale
函数相同。)这些函数还接受一个ghost
参数,并将该值传递给AddDot
函数。
function AddChord(chord, ghost) { chord.Fingering.forEach(function (a) { AddDot(a, ghost); }); }
和弦、音阶和指法
Chord
、Scales
和Finger
对象定义在global
作用域中。不幸的是,这是原始设计的错误,但将来会得到纠正。
手指
Finger
对象完全指定了指板上任何音符的指法。大多数FretboardJs组件都依赖于Finger对象。Finger对象包含以下属性:
品位
字符串
手指
度数
此Finger
对象还包含可选信息,指定使用的手指以及音符在音阶或和弦中的上下文(如果存在)。
function Finger(fret, string, finger, degree) { var Finger = finger || 0; var Fret = fret; var String = string; var Degree = degree || 0; var self = { Degree: Degree, Finger: Finger, Fret: Fret, String: String, }; return self; }
和弦
Chord
对象是Finger
对象的命名数组。
包含的和弦库基于标准吉他调弦,但可以为其他调弦重新定义。和弦命名是任意的,并为任何指法/命名组合留下了可能性。
这给出了Chord
对象的本质结构如下
function Chord(def) { var Name = def.name; var Fingering = def.fingering; var self = { Fingering: Fingering, Name: Name, }; return self; }
然而,Chord
对象定义了许多函数。
例如,Transpose
函数,它将Chord
的副本调整给定的半音数。
function Transpose(n) { var result = self.Copy(); result.Fingering.forEach(function (a) { a.Fret += n; }); return result; }
提高或降低和弦度数
包含的和弦库仅针对标准调弦定义。然而,Chord对象上的函数可用于系统地修改和弦:
function Flaten(degree) { var result = self.Copy(); result.Fingering.forEach(function (a) { if (a.Degree == degree) { a.Fret--; } }); return result; } function Sharpen(degree) { var result = self.Copy(); result.Fingering.forEach(function (a) { if (a.Degree == degree) { a.Fret++; } }); return result; }
Scale
Scale
对象与Chord对象几乎相同,因此在此不再讨论。
和弦库
和弦库定义在命名空间Fretboard.Chords
中。
Fretboard.Chords = {};
该库由命名的Chord
组组成。例如,以下显示了包含两个和弦的MajorTriad
和弦组的定义:
Fretboard.Chords.MajorTriad = function () {
var self = [
new Chord({
name: 'E',
fingering: [
new Finger(0, 6, 1, 1),
new Finger(2, 5, 3, 5),
new Finger(2, 4, 4, 1),
new Finger(1, 3, 2, 3),
new Finger(0, 2, 1, 5),
new Finger(0, 1, 1, 1),
],
}),
new Chord({
name: 'D',
fingering: [
new Finger(0, 4, 1, 1),
new Finger(2, 3, 2, 5),
new Finger(3, 2, 4, 1),
new Finger(2, 1, 3, 3),
],
OpenOnly: true
})
];
return self;
}();
Chord
组可以根据另一个Chord
组通过算法定义,如下面的代码所示。这里,MinorTriad
Chord组是通过将MajorTriad
组中每个和弦的第3度音降低而定义的。可以定义一个验证规则来确定对给定Chord
的修改是否有效。在这种情况下,规则要求和弦的手指跨度不超过四品。
Fretboard.Chords.MinorTriad = function () { var self = []; for (var i = 0; i < Fretboard.Chords.MajorTriad.length; i++) { var chord = Fretboard.Chords.MajorTriad[i].Flaten3rd(); if (chord.Span() > 4) { continue; } self.push(chord); } return self; }();
库中的和弦组可以被覆盖。
脚本
scripts文件夹包含所有代码。此文件夹包括以下文件
- _namespace.js: 定义了名为
Fretboard
的基本命名空间。 - chord.js: 定义了
Finger
、Chord
和Notes
对象。 - chords.js: 定义了所有可用的和弦族
- metrics.js: 定义了全局度量和计算,用于确定各种坐标值,例如第n品沿x轴的位置。
- neck.js: 定义了主要的FretboardJs对象
Neck
。 - scale.js: 定义了Scale对象,用途与Chord对象类似
- scales.js: 定义了一个按组划分的音阶库
- app.js: 定义了示例应用程序“和弦探索器”。
- chord-utility.js: 以正则表达式比较列表的形式定义了一个关联表,用于返回一个与给定和弦拼写匹配的通用和弦族。
- fretboard-1.3.0.js: 打包好的库
- fretboard-1.3.0.min.js: 精简版的包
历史
- 2014年7月30日 - 初始帖子,包括对演示应用程序“和弦探索器”的初步讨论,以及FretboardJs和弦库的使用。
- 2014年8月1日 - 版本1.3,包含设计和实现讨论