Learn Vexflow By Example

vexflow 是一个 html 渲染五线谱的库,它有两套 API,一套是 low-level 的,一套是 high-level 的,考虑到我的需求实际上并非是去渲染乐谱,这里去学习它的 low-level API,下面全部用 vexflow 去指代它 low-level 的 API。

学习这些概念及其相关操作、相互关系就是学习 vexflow,这里学习一下vexflow的如下概念:

  1. Renderer
  2. Context
  3. Stave
  4. StaveNote
  5. Formatter

Renderer, Context

参考:https://github.com/0xfe/vexflow/wiki/Understanding-Renderer-&-Context

使用 vexflow 时,一般会有这样的样板代码,它引用一个 div 或 canvas 元素设置相应空间,并获取 context,其用于渲染五线谱。

1
2
3
4
5
6
7
8
9
10
const { Renderer, Stave } = Vex.Flow;
const div = document.getElementById("some-div")
const renderer = new Renderer(div, Renderer.Backends.SVG);
renderer.resize(500, 200);
const context = renderer.getContext();
// vexflow 的接口都是 fluent 的,也就是说可以链式调用
// 比如这里直接初始化 renderer 并获取 context,反正 renderer 之后也不会再用了
// const context = new Renderer(div, Renderer.Backends.SVG)
// .resize(500, 250)
// .getContext();

Renderer 对应渲染五线谱的空间,其可调整渲染方式:svg 或 canvas。Renderer 只有两个方法——调整大小或获取 context。

context 提供了大量进行图形绘制的方法,它抽象了底层细节,使同一套接口能适用于 SVG 和 canvas。但 context 的方法太过底层,一般没有需要直接使用 context,而是利用 Stave,Note,Voice 等类的接口进行绘制。比如 Stave 可能会这样调用stave.setContext(context).draw(),这暗示了它会利用 context 的方法进行绘制。

Renderer 使用 svg 作为渲染方式时需要注意,其会在引用的 div 下新建一个 svg 元素,而如果这个元素下面原来就存在其它 Renderer,其不会受影响。这导致在 React 的 StrictMode 下会渲染重复的元素,因此需要在清理函数中删除 div 下所有子元素。canvas 似乎无此影响,但尚不知是否有其它 bug。这里只使用 svg。

上面的代码会得到下面的结果,这里加上一个 border 方便查看:

1
<div id="some-div" style="border:1px solid red; max-width: 500px; background-color: white;"></div>

白如纸,得加点东西上去。

五线谱

Stave 即五线谱,StaveNote,Voice 等实体需要依赖 Stave 进行绘制(它们都继承一个 Element 类)。

创建五线谱是 trival 的:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 模板代码
const div = document.getElementById("stave-example0")
const context = new Renderer(div, Renderer.Backends.SVG)
.resize(500, 200)
.getContext();

// 创建 Stave,其在画布上的 x,y 轴分别偏移 10,40,长度位 400
const stave = new Stave(10, 40, 400)
.addClef("treble") // 添加调号,treble 是 G 谱号
.addTimeSignature("4/4") // 拍号,4/4 拍
.addKeySignature('G') // 调号,G 调
.setContext(context) // 注入 context
.draw(); // 然后进行绘制

Stave 的构造器接受三个参数——x,y 轴的偏移量和长度,其中 y 轴偏移量即使设置为 0,其也不会真正地挨着顶部,这是为上加线/间的音符预留的空间。

addClef方法去设置谱号,可选项颇多,但常用的顶多 treble 汗 bass。

addTimeSignature设置拍号,语法如6/8,表示八分音符为一拍,一小节六拍。

addKeySignature设置调号,调号按大调来,比如 G 表示 G 大调,在 F 上有升号。

一个严重需要注意的地方是,Stave 的谱号,拍号,调号等属性,在业务逻辑上是没有影响的,只用于绘图!比如我设置拍号为四四拍,但完全可以在一个小节里塞上 10 个全音符;比如我设置谱号为低音谱号,绘制一个 C4,它仍旧会绘制在下加一线上!它们的配置都在它们自己身上,并不依赖 Stave 的配置。

音符,Formatter

有了五线谱,该来点音符了,StaveNote 类抽象音符,使用其的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 模板代码
const div = document.getElementById("staveNote-example0")
const context = new Renderer(div, Renderer.Backends.SVG)
.resize(500, 200)
.getContext();
// 创建 Stave,其在画布上的 x,y 轴分别偏移 10,40,长度 400
const stave = new Stave(10, 40, 400)
.addClef("treble") // 添加调号,treble 是 G 谱号
.setContext(context) // 注入 context
.draw(); // 然后进行绘制

const cNote = new StaveNote({ keys: ["C/4"], duration: "2" }) // C4,2 分音符
const restNote = new StaveNote({ keys: ['B/4'], duration: '2rdd' }) // 二分复附点休止符,B/4 用于标识休止符的位置
const cMajorTraid = new StaveNote({ keys: ['C/4', 'E/4', 'G/4'], duration: '4'}) // C 大三和弦,四分
Formatter.FormatAndDraw(context, stave, [cNote, restNote, cMajorTraid]);

音符的构造器接受一个复杂对象,常用的或许有这些配置:keys,duration,clef,auto_stem。

  • keys 即音符名集合,如 C/4 代表 C4,E/5 代表 E5;形如 G#/4 这样带上变音符的音符也是合法的,但似乎没有效果,而变音号需要使用 Modifier 去表示;key 会决定音符的位置,即使是休止符也不例外全休止符的位置为 D/5,二分休止符为 B/4,四分休止符为 B/4
  • duration 即音符时值,1 表示全音符,2 和 h(half)表示二分音符,4 和 q 表示 4 分音符,8,16,32 表示对应分数音符,加上 r 表示休止符,加上 d 表示附点,比如 2rdd 表示二分复附点休止符(附点会影响(声部的)时值的计算,但渲染上没有影响)。
  • clef 同 Stave 的 clef,影响音符位置,默认 clef 是 treble。
  • auto_stem 是布尔值,表示是否自动调整符干朝向,默认关闭。

这里,输出音符使用了 Formatter 这个类的静态方法,其中参数包括 context,stave,以及要输出的 Note;能够猜测,需要 context 的原因是需要利用其方法去绘图,需要 stave 的原因是因为需要知道五线谱的位置,这样才能计算出要绘制的音符的位置。

Modifier

想要让 vexflow 给音符渲染升降号或附点,需要使用所谓的 Modifier,Modifier 应用在 StateNote 上,具体使用见代码和注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 模板代码
const div = document.getElementById("modifier-example0")
const context = new Renderer(div, Renderer.Backends.SVG).resize(500, 200).getContext();
const stave = new Stave(10, 40, 400).addClef("treble") .setContext(context) .draw();

const notes = [
new StaveNote({keys: ['C#/4'], duration: '4dd'}) // 四分复附点 C#4,这里的升号和附点对渲染没有影响
.addModifier(new Accidental('#')) // 添加升号
.addModifier(new Dot()) // 添加附点
.addModifier(new Dot()), // 再加一个
new StaveNote({keys: ['D/4', 'F#/4', 'A/4'], duration: '4'}) // D 大三和弦,有一个 F#
.addModifier(new Accidental('#'), 1) // 给数组中下标为 1 的音符添加升号
]
Formatter.FormatAndDraw(context, stave, notes);

虽然并没有学到多少东西,但我发现目前的进度好像已经能满足我的需要了!这个框架显然是专注渲染的,至于渲染的东西是否正确或合法,它并不怎么关心(但至少使用声部Voice的时候它会保证时值正确);将来若有需要的话再进一步学习(这时候估计是去专注相关样式),现在该对其进行封装了。