65.9K
CodeProject 正在变化。 阅读更多。
Home

FretboardJs

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2014 年 7 月 30 日

GPL3

9分钟阅读

viewsIcon

24007

downloadIcon

39

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元素内渲染整个指板。结果将如下所示

Simple FretboardJs rendered neck

指法使用Finger对象定义

new Finger(fret, string, finger)

我们可以通过以下方式向指板添加音符或“点”

Fretboard.Neck.AddDot(Finger);

因此,任何任意音符都可以通过以下方式渲染

Fretboard.Neck.AddDot(new Finger(3, 5, 2));

这会在客户端生成

Fretboard with single note

和弦呢?

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]);

结果是

A7 chord rendered on the fretboard

就这样,很简单,对吧?

要了解使用FretboardJs可以构建的应用程序类型,请参阅演示应用程序“和弦探索器”,它利用FretboardJs库探索和弦族的各种指法位置。输入文本字段允许吉他手输入和弦名称并迭代各种指法。和弦是从上述和弦库中选择的。

实现

本节讨论FretboardJs的实现细节。

定义全局命名空间Fretboard后,定义了两个指定整体指板行为的对象:Fretboard.MetricsFretboard.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”,并将它们存储在私有变量fretboardLayerfingeringLayer中。

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 = '';
}

添加和弦和音阶

AddChordAddScale函数分别接受ChordScale对象,并为给定ChordScale中定义的每个指法调用AddDot函数。

以下是AddChord函数的示例。(AddScale函数相同。)这些函数还接受一个ghost参数,并将该值传递给AddDot函数。

function AddChord(chord, ghost) {
    chord.Fingering.forEach(function (a) {
        AddDot(a, ghost);
    });
}

和弦、音阶和指法

ChordScalesFinger对象定义在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文件夹包含所有代码。此文件夹包括以下文件

  1. _namespace.js: 定义了名为Fretboard的基本命名空间。
  2. chord.js: 定义了FingerChordNotes对象。
  3. chords.js: 定义了所有可用的和弦族
  4. metrics.js: 定义了全局度量和计算,用于确定各种坐标值,例如第n品沿x轴的位置。
  5. neck.js: 定义了主要的FretboardJs对象Neck
  6. scale.js: 定义了Scale对象,用途与Chord对象类似
  7. scales.js: 定义了一个按组划分的音阶库
  8. app.js: 定义了示例应用程序“和弦探索器”。
  9. chord-utility.js: 以正则表达式比较列表的形式定义了一个关联表,用于返回一个与给定和弦拼写匹配的通用和弦族。
  10. fretboard-1.3.0.js: 打包好的库
  11. fretboard-1.3.0.min.js: 精简版的包

历史

  • 2014年7月30日 - 初始帖子,包括对演示应用程序“和弦探索器”的初步讨论,以及FretboardJs和弦库的使用。
  • 2014年8月1日 - 版本1.3,包含设计和实现讨论
© . All rights reserved.