はじめに
一素人が SMuFL について調べたメモをここにまとめる.
コード例などは HTML+JavaScript で挙げるが、別の環境でもフォントの扱い方などの違いを考慮すれば同じような計算で同じ結果を得られるはずである.
この記事では実験用のフォントファイルとして Bravura の otf ファイル (ver.1.392) を使用した.
ソースコードの内容に関する説明があまりない点、この記事の内容には多くの誤りが含まれるだろうことを先に断っておく.
SMuFL とは
SMuFL の公式サイト によれば、
SMuFL is a specification that provides a standard way of mapping the thousands of musical symbols required by conventional music notation into the Private Use Area in Unicode’s Basic Multilingual Plane for a single (format-independent) font.
[中略]
The goal of SMuFL is to establish a new standard glyph mapping for musical symbols that is optimised for modern font formats and that can be adopted by a variety of software vendors and font designers, for the benefit of all users of music notation software.
和訳(DeepL):
SMuFLは、従来の記譜法で必要とされる何千もの音楽記号を、単一の(フォーマットに依存しない)フォントに対して、ユニコードの基本多言語平面の私的使用領域にマッピングする標準的な方法を提供する仕様です。
[中略]
SMuFLの目標は、楽譜作成ソフトウェアのすべてのユーザーのために、現代のフォントフォーマットに最適化され、さまざまなソフトウェアベンダーやフォントデザイナーが採用できる、音楽記号の新しい標準グリフマッピングを確立することです。
とのことで、つまりは数々の音楽記号を収めるフォントデータに関する仕様である.
ここに含まれる記号をいい感じに配置することができれば、綺麗な楽譜も描画できるはずだ.
ただし中身はただのフォントであるため、「いい感じに配置する」には相応の努力を要することを覚悟しなければならない.
これは SMuFL の仕様に従って配置の計算をするというだけでなく、一般的な浄書規則に従って音符の配置、符幹の長さ、符幹の向き、改行位置、小節の幅などあらゆることに気を配る必要があるという意味である.
この記事ではそこまで頑張らない.
なお、SMuFL の仕様は ここ にあるので参照されたし.
初めの一歩
まず Hello, world! の代わりとして、ト音記号の描画を行う. (と言ってもフォントファイルを読み込んで表示するだけだが)
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>SMuFL: Engraving Demo</title>
<style>
@font-face {
font-family: 'Bravura';
src: url('file:../fonts/Bravura.otf');
}
.bravura {
font-family: 'Bravura';
}
span {
font-size: 100px;
}
</style>
</head>
<body>
<div id="text">
<p>U+E050: <span></span></p>
<p>U+E050: <span class='bravura'></span></p>
</div>
</body>
</html>
結果:
ここではローカルに置いたフォントファイルを @font-face
で読み込み、.bravura
クラスで使用している.
音楽記号は Unicode の私用領域にあるため、SMuFL フォントを指定せずに書くと当然だが豆腐が出る(上段).
ト音記号の文字コードは U+E050
なので(記号と文字コードの関係は仕様書の第4章 Glyph tables で確認できる)、これをフォント指定して書き込むとト音記号が表示される.
HTML canvas
楽譜をテキストだけで描画するのは不可能なので canvas 要素を用い位置指定の上で描画するようにする.
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>SMuFL: Engraving Demo</title>
<script type='text/javascript'>
window.onload = function () {
// フォントファイルの準備
new FontFace('Bravura', 'url(file:../fonts/Bravura.otf)').load().then(font => {
document.fonts.add(font);
// フォントファイルが準備できたので描画を行う
const ctx = document.getElementById('main-canvas').getContext('2d');
ctx.font = '100px Bravura';
ctx.fillText('\uE050', 100, 200);// x,y = 100,200
// 描画位置に赤い十字線を引く
// 縦線 x = 100
ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.moveTo(100, 75);
ctx.lineTo(100, 300);
// 横線 y = 200
ctx.moveTo( 50, 200);
ctx.lineTo(200, 200);
ctx.stroke();
});// FontFace.load
};// function window.onload
</script>
</head>
<body>
<div>
<canvas id='main-canvas' width='1000px' height='500px'></canvas>
</div>
</body>
</html>
結果:
ついでに赤い線を描いているが、fillText
ではこの横線がいわゆるベースライン、縦線がグリフの左端になっている(グリフによっては必ずしも左端とは限らない).
描画位置の高さがこうなるのは HTML+JavaScript 的に、ctx.textBaseline
のデフォルト値が 'alphabetic'
であることによる(参考).
JavaScript ではこれを設定可能であるし、使用する言語/環境にもよってはテキストの描画処理で指定する座標がテキストのAABBの左上である場合もある.
この記事の内容を参考にして何かに移植するなどの際にはこの辺の仕様を確認されたし.
フォントファイルの読み込みは、以下の方法でも実現できる:
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>SMuFL: Engraving Demo</title>
<style>
@font-face {
font-family: 'Bravura';
src: url('../fonts/Bravura.otf');
}
</style>
<script type='text/javascript'>
window.onload = function () {
// フォントファイルの準備
document.fonts.load('10px Bravura').then(() => {
// フォントファイルが準備できたので描画を行う
const ctx = document.getElementById('main-canvas').getContext('2d');
ctx.font = '100px Bravura';
ctx.fillText('\uE050', 100, 200);// x,y = 100,200
// 描画位置に赤い十字線を引く
// 縦線 x = 100
ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.moveTo(100, 75);
ctx.lineTo(100, 300);
// 横線 y = 200
ctx.moveTo( 50, 200);
ctx.lineTo(200, 200);
ctx.stroke();
});// FontFace.load
};// function window.onload
</script>
</head>
<body>
<div>
<canvas id='main-canvas' width='1000px' height='500px'></canvas>
</div>
</body>
</html>
どちらがよいかは好みだと思う.
SMuFL の設定ファイル
SMuFL フォントではフォントファイルに加えて、{font name}_metadata.json
のようなファイルが配布されるはずだ.
これに関する仕様はドキュメントの第3章4節 Metadata for SMuFL-compliant fonts に記述がある.
使用する線の太さや、描画位置に関する情報が書かれており、その単位は staff space
である.
staff space
は五線の隙間ひとつ分であって、単純に フォントサイズ / 4
で求められる.
この記事では、面倒なので、json ファイルを JavaScript ファイルにすることでこれを読み込む.
JavaScript できちんとやる場合は Fetch API などを使うべきだろう.
(面倒でなければ、ローカルサーバーなどを用意して Fetch API によるローカルファイルの読み込みを利用したり、input[type=file] タグを利用して都度ファイルを指定してやるなどによって json ファイルの書き換えが不要となる)
{
"fontName":"Bravura",
"fontVersion":1.392,
[...中略...]
}
const bravuraMetadata = {
"fontName":"Bravura",
"fontVersion":1.392,
[...中略...]
};
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>SMuFL: Engraving Demo</title>
<script src='../fonts/bravura_metadata.js'></script>
<script type='text/javascript'>
window.onload = function () {
// フォントファイルの準備
new FontFace('Bravura', 'url(file:../fonts/Bravura.otf)').load().then(font => {
document.fonts.add(font);
// フォントファイルが準備できた
const ctx = document.getElementById('main-canvas').getContext('2d');
const fontSize = 100;
const staffSpace = fontSize / 4;
ctx.font = `${fontSize}px Bravura`;
// 五線を描画する
const staffLeft = 100;// 五線の左端
const staffWidth = 500;// 五線の幅
const firstLineY = 200;// 第一線(一番下の線)の高さ
ctx.strokeStyle = bravuraMetadata.engravingDefaults.staffLineThickness * staffSpace;
for (let i = 0; i < 5; ++i) {
ctx.beginPath();
ctx.moveTo(staffLeft, firstLineY - staffSpace * i);// canvas だと Y 軸は下向き
ctx.lineTo(staffLeft + staffWidth, firstLineY - staffSpace * i);
ctx.stroke();
}// loop for i in [0..5)
// ト音記号
ctx.fillText('\uE050', 150, firstLineY - staffSpace * 1);// 第二線の位置に描画したい
// ハ音記号
ctx.fillText('\uE05C', 300, firstLineY - staffSpace * 2);// 第三線の位置に描画したい
// ヘ音記号
ctx.fillText('\uE062', 450, firstLineY - staffSpace * 3);// 第四線の位置に描画したい
});// FontFace.load
};// function window.onload
</script>
</head>
<body>
<div>
<canvas id='main-canvas' width='1000px' height='500px'></canvas>
</div>
</body>
</html>
結果:
ここでは五線の太さしか使用していないが、metadata ファイルには連桁、スラー、符幹などの太さ、グリフの BB(Bounding box)、符幹や符尾の描画位置に関する設定など実に多くの項目が存在する.
SMuFL を使用し楽譜描画するには、それらを理解し正しく計算することで描画位置を決定しなければならない.
各種記号の描画
以下では楽譜の描画に最低限必要と思われる各種記号単体の描画処理を示す.
処理は概ね以下の順序で行っている.
- 楽譜要素のモデル表現
- 五線上の垂直位置の導出と、記号の描画要素への変換
- 記号を x = 0, firstLineY = 0 に描画する場合における大きさと、各部の描画位置の導出
- x, firstLineY が指定された場合における各部の描画位置の導出
- 描画
ここで言う楽譜要素のモデルは、基本的に昔書いた 音楽に関連する計算式まとめ の内容である.
また、この記事では描画処理の中で、AABB を青い四角で描いている.
共通処理
ここでは、この記事で使用している共通処理を示す.
utility-functions.js
あると便利な関数などを定義している.
ソースコード
const Fs = Object.freeze({// Functions
floorDivMod: (x, y) => {
const q = Math.floor(x / y);
const r = x - q * y;
return { q: q, r: r };
},
roundDivMod: (x, y) => {
const q = Math.round(x / y);
const r = x - q * y;
return { q: q, r: r };
},
power: (base, exp) => {// 繰り返し二乗法による累乗 base^exp
if(exp < 0) throw new Error(`negative exp[${exp}] is not allowed.`);
let result = 1;
while(exp != 0) {
if((exp&1) === 1) result *= base;
base *= base;
exp >>= 1;
}
return result;
},
gcd: (a, b) => {// 最大公約数
a = Math.abs(a);
b = Math.abs(b);
while(b != 0) {
const c = a % b;
a = b;
b = c;
}
return a;
},
naturalToneToFifths: (naturalTone) => {
// NaturalTone を C=0, D=1, ..., B=6 とし、
// 五度圏上で C を 0 とする.
// このとき、naturalTone が五度圏上で何番になるかを求める.
return Fs.floorDivMod(naturalTone * 2 + 1, 7).r - 1;
},
log2: (n) => {
return Math.floor(Math.log2(n));
},
});// const Fs
class Rational {// 分数
constructor(num, den = 1) {
if(!(Number.isInteger(num) && Number.isInteger(den))) {
throw new Error(`num, and den must be integers. but found: ${num}, ${den}`);
} else if(den == 0) {
throw new Error(`denominator cannot be zero`);
}
if(num == 0) {
this.num = 0;
this.den = 1;
return;
}
if(den < 0) {
// denominator must be positive
num *= -1;
den *= -1;
}
const gcd = Fs.gcd(num, den);
this.num = num / gcd;
this.den = den / gcd;
}
add(r) {
if(typeof(r) === 'number') {
r = {
num: r,
den: 1
};
}
const n = this.num * r.den + r.num * this.den;
const d = this.den * r.den;
return new Rational(n, d);
}
sub(r) {
if(typeof(r) === 'number') {
r = {
num: r,
den: 1
};
}
const n = this.num * r.den - r.num * this.den;
const d = this.den * r.den;
return new Rational(n, d);
}
mul(r) {
if(typeof(r) === 'number') {
r = {
num: r,
den: 1
};
}
const n = this.num * r.num;
const d = this.den * r.den;
return new Rational(n, d);
}
div(r) {
if(typeof(r) === 'number') {
r = {
num: r,
den: 1
};
}
const n = this.num * r.den;
const d = this.den * r.num;
return new Rational(n, d);
}
toString() {
if(this.den === 1) return this.num.toString();
else return `${this.num}/${this.den}`;
}
}// class Rational
smufl-characters.js
文字の名前とその文字コードをペアにして定義する.
内容は公式ドキュメントの第4章 Glyph Tables に従っている.
ソースコード
class SMuFLCharacter {
constructor(name, code) {
this.name = name;
this.code = code;
}// SMuFLCharacter.constructor
}// class SMuFLCharacter
const SMuFL = Object.freeze({
Barline: Object.freeze({
barlineSingle : Object.freeze(new SMuFLCharacter('barlineSingle' , '\uE030')),
barlineDouble : Object.freeze(new SMuFLCharacter('barlineDouble' , '\uE031')),
barlineFinal : Object.freeze(new SMuFLCharacter('barlineFinal' , '\uE032')),
barlineReverseFinal: Object.freeze(new SMuFLCharacter('barlineReverseFinal', '\uE033')),
barlineHeavy : Object.freeze(new SMuFLCharacter('barlineHeavy' , '\uE034')),
barlineHeavyHeavy : Object.freeze(new SMuFLCharacter('barlineHeavyHeavy' , '\uE035')),
barlineDashed : Object.freeze(new SMuFLCharacter('barlineDashed' , '\uE036')),
barlineDotted : Object.freeze(new SMuFLCharacter('barlineDotted' , '\uE037')),
barlineShort : Object.freeze(new SMuFLCharacter('barlineShort' , '\uE038')),
barlineTick : Object.freeze(new SMuFLCharacter('barlineTick' , '\uE039')),
}), Repeat: Object.freeze({
repeatLeft : Object.freeze(new SMuFLCharacter('repeatLeft' , '\uE040')),
repeatRight : Object.freeze(new SMuFLCharacter('repeatRight' , '\uE041')),
repeatRightLeft: Object.freeze(new SMuFLCharacter('repeatRightLeft', '\uE042')),
repeatDots : Object.freeze(new SMuFLCharacter('repeatDots' , '\uE043')),
repeatDot : Object.freeze(new SMuFLCharacter('repeatDot' , '\uE044')),
dalSegno : Object.freeze(new SMuFLCharacter('dalSegno' , '\uE045')),
daCapo : Object.freeze(new SMuFLCharacter('daCapo' , '\uE046')),
segno : Object.freeze(new SMuFLCharacter('segno' , '\uE047')),
coda : Object.freeze(new SMuFLCharacter('coda' , '\uE048')),
codaSquare : Object.freeze(new SMuFLCharacter('codaSquare' , '\uE049')),
}), Clef: Object.freeze({
gClef : Object.freeze(new SMuFLCharacter('gClef' , '\uE050')),
gClef15mb : Object.freeze(new SMuFLCharacter('gClef15mb' , '\uE051')),
gClef8vb : Object.freeze(new SMuFLCharacter('gClef8vb' , '\uE052')),
gClef8va : Object.freeze(new SMuFLCharacter('gClef8va' , '\uE053')),
gClef15ma : Object.freeze(new SMuFLCharacter('gClef15ma' , '\uE054')),
gClef8vbOld : Object.freeze(new SMuFLCharacter('gClef8vbOld' , '\uE055')),
gClef8vbCClef : Object.freeze(new SMuFLCharacter('gClef8vbCClef' , '\uE056')),
gClef8vbParens : Object.freeze(new SMuFLCharacter('gClef8vbParens' , '\uE057')),
gClefLigatedNumberBelow : Object.freeze(new SMuFLCharacter('gClefLigatedNumberBelow' , '\uE058')),
gClefLigatedNumberAbove : Object.freeze(new SMuFLCharacter('gClefLigatedNumberAbove' , '\uE059')),
gClefArrowUp : Object.freeze(new SMuFLCharacter('gClefArrowUp' , '\uE05A')),
gClefArrowDown : Object.freeze(new SMuFLCharacter('gClefArrowDown' , '\uE05B')),
cClef : Object.freeze(new SMuFLCharacter('cClef' , '\uE05C')),
cClef8vb : Object.freeze(new SMuFLCharacter('cClef8vb' , '\uE05D')),
cClefArrowUp : Object.freeze(new SMuFLCharacter('cClefArrowUp' , '\uE05E')),
cClefArrowDown : Object.freeze(new SMuFLCharacter('cClefArrowDown' , '\uE05F')),
cClefSquare : Object.freeze(new SMuFLCharacter('cClefSquare' , '\uE060')),
cClefCombining : Object.freeze(new SMuFLCharacter('cClefCombining' , '\uE061')),
fClef : Object.freeze(new SMuFLCharacter('fClef' , '\uE062')),
fClef15mb : Object.freeze(new SMuFLCharacter('fClef15mb' , '\uE063')),
fClef8vb : Object.freeze(new SMuFLCharacter('fClef8vb' , '\uE064')),
fClef8va : Object.freeze(new SMuFLCharacter('fClef8va' , '\uE065')),
fClef15ma : Object.freeze(new SMuFLCharacter('fClef15ma' , '\uE066')),
fClefArrowUp : Object.freeze(new SMuFLCharacter('fClefArrowUp' , '\uE067')),
fClefArrowDown : Object.freeze(new SMuFLCharacter('fClefArrowDown' , '\uE068')),
unpitchedPercussionClef1 : Object.freeze(new SMuFLCharacter('unpitchedPercussionClef1' , '\uE069')),
unpitchedPercussionClef2 : Object.freeze(new SMuFLCharacter('unpitchedPercussionClef2' , '\uE06A')),
semipitchedPercussionClef1: Object.freeze(new SMuFLCharacter('semipitchedPercussionClef1', '\uE06B')),
semipitchedPercussionClef2: Object.freeze(new SMuFLCharacter('semipitchedPercussionClef2', '\uE06C')),
'6stringTabClef' : Object.freeze(new SMuFLCharacter('6stringTabClef' , '\uE06D')),
'4stringTabClef' : Object.freeze(new SMuFLCharacter('4stringTabClef' , '\uE06E')),
schaefferClef : Object.freeze(new SMuFLCharacter('schaefferClef' , '\uE06F')),
schaefferPreviousClef : Object.freeze(new SMuFLCharacter('schaefferPreviousClef' , '\uE070')),
schaefferGClefToFClef : Object.freeze(new SMuFLCharacter('schaefferGClefToFClef' , '\uE071')),
schaefferFClefToGClef : Object.freeze(new SMuFLCharacter('schaefferFClefToGClef' , '\uE072')),
gClefReversed : Object.freeze(new SMuFLCharacter('gClefReversed' , '\uE073')),
gClefTurned : Object.freeze(new SMuFLCharacter('gClefTurned' , '\uE074')),
cClefReversed : Object.freeze(new SMuFLCharacter('cClefReversed' , '\uE075')),
fClefReversed : Object.freeze(new SMuFLCharacter('fClefReversed' , '\uE076')),
fClefTurned : Object.freeze(new SMuFLCharacter('fClefTurned' , '\uE077')),
bridgeClef : Object.freeze(new SMuFLCharacter('bridgeClef' , '\uE078')),
accdnDiatonicClef : Object.freeze(new SMuFLCharacter('accdnDiatonicClef' , '\uE079')),
gClefChange : Object.freeze(new SMuFLCharacter('gClefChange' , '\uE07A')),
cClefChange : Object.freeze(new SMuFLCharacter('cClefChange' , '\uE07B')),
fClefChange : Object.freeze(new SMuFLCharacter('fClefChange' , '\uE07C')),
clef8 : Object.freeze(new SMuFLCharacter('clef8' , '\uE07D')),
clef15 : Object.freeze(new SMuFLCharacter('clef15' , '\uE07E')),
clefChangeCombining : Object.freeze(new SMuFLCharacter('clefChangeCombining' , '\uE07F')),
}), TimeSignature: Object.freeze({
timeSig0 : Object.freeze(new SMuFLCharacter('timeSig0' , '\uE080')),
timeSig1 : Object.freeze(new SMuFLCharacter('timeSig1' , '\uE081')),
timeSig2 : Object.freeze(new SMuFLCharacter('timeSig2' , '\uE082')),
timeSig3 : Object.freeze(new SMuFLCharacter('timeSig3' , '\uE083')),
timeSig4 : Object.freeze(new SMuFLCharacter('timeSig4' , '\uE084')),
timeSig5 : Object.freeze(new SMuFLCharacter('timeSig5' , '\uE085')),
timeSig6 : Object.freeze(new SMuFLCharacter('timeSig6' , '\uE086')),
timeSig7 : Object.freeze(new SMuFLCharacter('timeSig7' , '\uE087')),
timeSig8 : Object.freeze(new SMuFLCharacter('timeSig8' , '\uE088')),
timeSig9 : Object.freeze(new SMuFLCharacter('timeSig9' , '\uE089')),
timeSigCommon : Object.freeze(new SMuFLCharacter('timeSigCommon' , '\uE08A')),
timeSigCutCommon : Object.freeze(new SMuFLCharacter('timeSigCutCommon' , '\uE08B')),
timeSigPlus : Object.freeze(new SMuFLCharacter('timeSigPlus' , '\uE08C')),
timeSigPlusSmall : Object.freeze(new SMuFLCharacter('timeSigPlusSmall' , '\uE08D')),
timeSigFractionalSlash : Object.freeze(new SMuFLCharacter('timeSigFractionalSlash' , '\uE08E')),
timeSigEquals : Object.freeze(new SMuFLCharacter('timeSigEquals' , '\uE08F')),
timeSigMinus : Object.freeze(new SMuFLCharacter('timeSigMinus' , '\uE090')),
timeSigMultiply : Object.freeze(new SMuFLCharacter('timeSigMultiply' , '\uE091')),
timeSigParensLeftSmall : Object.freeze(new SMuFLCharacter('timeSigParensLeftSmall' , '\uE092')),
timeSigParensRightSmall : Object.freeze(new SMuFLCharacter('timeSigParensRightSmall' , '\uE093')),
timeSigParensLeft : Object.freeze(new SMuFLCharacter('timeSigParensLeft' , '\uE094')),
timeSigParensRight : Object.freeze(new SMuFLCharacter('timeSigParensRight' , '\uE095')),
timeSigComma : Object.freeze(new SMuFLCharacter('timeSigComma' , '\uE096')),
timeSigFractionQuarter : Object.freeze(new SMuFLCharacter('timeSigFractionQuarter' , '\uE097')),
timeSigFractionHalf : Object.freeze(new SMuFLCharacter('timeSigFractionHalf' , '\uE098')),
timeSigFractionThreeQuarters: Object.freeze(new SMuFLCharacter('timeSigFractionThreeQuarters', '\uE099')),
timeSigFractionOneThird : Object.freeze(new SMuFLCharacter('timeSigFractionOneThird' , '\uE09A')),
timeSigFractionTwoThirds : Object.freeze(new SMuFLCharacter('timeSigFractionTwoThirds' , '\uE09B')),
timeSigX : Object.freeze(new SMuFLCharacter('timeSigX' , '\uE09C')),
timeSigOpenPenderecki : Object.freeze(new SMuFLCharacter('timeSigOpenPenderecki' , '\uE09D')),
timeSigCombNumerator : Object.freeze(new SMuFLCharacter('timeSigCombNumerator' , '\uE09E')),
timeSigCombDenominator : Object.freeze(new SMuFLCharacter('timeSigCombDenominator' , '\uE09F')),
}), Notehead: Object.freeze({
noteheadDoubleWhole : Object.freeze(new SMuFLCharacter('noteheadDoubleWhole' , '\uE0A0')),
noteheadDoubleWholeSquare : Object.freeze(new SMuFLCharacter('noteheadDoubleWholeSquare' , '\uE0A1')),
noteheadWhole : Object.freeze(new SMuFLCharacter('noteheadWhole' , '\uE0A2')),
noteheadHalf : Object.freeze(new SMuFLCharacter('noteheadHalf' , '\uE0A3')),
noteheadBlack : Object.freeze(new SMuFLCharacter('noteheadBlack' , '\uE0A4')),
noteheadNull : Object.freeze(new SMuFLCharacter('noteheadNull' , '\uE0A5')),
noteheadXDoubleWhole : Object.freeze(new SMuFLCharacter('noteheadXDoubleWhole' , '\uE0A6')),
noteheadXWhole : Object.freeze(new SMuFLCharacter('noteheadXWhole' , '\uE0A7')),
noteheadXHalf : Object.freeze(new SMuFLCharacter('noteheadXHalf' , '\uE0A8')),
noteheadXBlack : Object.freeze(new SMuFLCharacter('noteheadXBlack' , '\uE0A9')),
noteheadXOrnate : Object.freeze(new SMuFLCharacter('noteheadXOrnate' , '\uE0AA')),
noteheadXOrnateEllipse : Object.freeze(new SMuFLCharacter('noteheadXOrnateEllipse' , '\uE0AB')),
noteheadPlusDoubleWhole : Object.freeze(new SMuFLCharacter('noteheadPlusDoubleWhole' , '\uE0AC')),
noteheadPlusWhole : Object.freeze(new SMuFLCharacter('noteheadPlusWhole' , '\uE0AD')),
noteheadPlusHalf : Object.freeze(new SMuFLCharacter('noteheadPlusHalf' , '\uE0AE')),
noteheadPlusBlack : Object.freeze(new SMuFLCharacter('noteheadPlusBlack' , '\uE0AF')),
noteheadCircleXDoubleWhole : Object.freeze(new SMuFLCharacter('noteheadCircleXDoubleWhole' , '\uE0B0')),
noteheadCircleXWhole : Object.freeze(new SMuFLCharacter('noteheadCircleXWhole' , '\uE0B1')),
noteheadCircleXHalf : Object.freeze(new SMuFLCharacter('noteheadCircleXHalf' , '\uE0B2')),
noteheadCircleX : Object.freeze(new SMuFLCharacter('noteheadCircleX' , '\uE0B3')),
noteheadDoubleWholeWithX : Object.freeze(new SMuFLCharacter('noteheadDoubleWholeWithX' , '\uE0B4')),
noteheadWholeWithX : Object.freeze(new SMuFLCharacter('noteheadWholeWithX' , '\uE0B5')),
noteheadHalfWithX : Object.freeze(new SMuFLCharacter('noteheadHalfWithX' , '\uE0B6')),
noteheadVoidWithX : Object.freeze(new SMuFLCharacter('noteheadVoidWithX' , '\uE0B7')),
noteheadSquareWhite : Object.freeze(new SMuFLCharacter('noteheadSquareWhite' , '\uE0B8')),
noteheadSquareBlack : Object.freeze(new SMuFLCharacter('noteheadSquareBlack' , '\uE0B9')),
noteheadTriangleUpDoubleWhole : Object.freeze(new SMuFLCharacter('noteheadTriangleUpDoubleWhole' , '\uE0BA')),
noteheadTriangleUpWhole : Object.freeze(new SMuFLCharacter('noteheadTriangleUpWhole' , '\uE0BB')),
noteheadTriangleUpHalf : Object.freeze(new SMuFLCharacter('noteheadTriangleUpHalf' , '\uE0BC')),
noteheadTriangleUpWhite : Object.freeze(new SMuFLCharacter('noteheadTriangleUpWhite' , '\uE0BD')),
noteheadTriangleUpBlack : Object.freeze(new SMuFLCharacter('noteheadTriangleUpBlack' , '\uE0BE')),
noteheadTriangleLeftWhite : Object.freeze(new SMuFLCharacter('noteheadTriangleLeftWhite' , '\uE0BF')),
noteheadTriangleLeftBlack : Object.freeze(new SMuFLCharacter('noteheadTriangleLeftBlack' , '\uE0C0')),
noteheadTriangleRightWhite : Object.freeze(new SMuFLCharacter('noteheadTriangleRightWhite' , '\uE0C1')),
noteheadTriangleRightBlack : Object.freeze(new SMuFLCharacter('noteheadTriangleRightBlack' , '\uE0C2')),
noteheadTriangleDownDoubleWhole : Object.freeze(new SMuFLCharacter('noteheadTriangleDownDoubleWhole' , '\uE0C3')),
noteheadTriangleDownWhole : Object.freeze(new SMuFLCharacter('noteheadTriangleDownWhole' , '\uE0C4')),
noteheadTriangleDownHalf : Object.freeze(new SMuFLCharacter('noteheadTriangleDownHalf' , '\uE0C5')),
noteheadTriangleDownWhite : Object.freeze(new SMuFLCharacter('noteheadTriangleDownWhite' , '\uE0C6')),
noteheadTriangleDownBlack : Object.freeze(new SMuFLCharacter('noteheadTriangleDownBlack' , '\uE0C7')),
noteheadTriangleUpRightWhite : Object.freeze(new SMuFLCharacter('noteheadTriangleUpRightWhite' , '\uE0C8')),
noteheadTriangleUpRightBlack : Object.freeze(new SMuFLCharacter('noteheadTriangleUpRightBlack' , '\uE0C9')),
noteheadMoonWhite : Object.freeze(new SMuFLCharacter('noteheadMoonWhite' , '\uE0CA')),
noteheadMoonBlack : Object.freeze(new SMuFLCharacter('noteheadMoonBlack' , '\uE0CB')),
noteheadTriangleRoundDownWhite : Object.freeze(new SMuFLCharacter('noteheadTriangleRoundDownWhite' , '\uE0CC')),
noteheadTriangleRoundDownBlack : Object.freeze(new SMuFLCharacter('noteheadTriangleRoundDownBlack' , '\uE0CD')),
noteheadParenthesis : Object.freeze(new SMuFLCharacter('noteheadParenthesis' , '\uE0CE')),
noteheadSlashedBlack1 : Object.freeze(new SMuFLCharacter('noteheadSlashedBlack1' , '\uE0CF')),
noteheadSlashedBlack2 : Object.freeze(new SMuFLCharacter('noteheadSlashedBlack2' , '\uE0D0')),
noteheadSlashedHalf1 : Object.freeze(new SMuFLCharacter('noteheadSlashedHalf1' , '\uE0D1')),
noteheadSlashedHalf2 : Object.freeze(new SMuFLCharacter('noteheadSlashedHalf2' , '\uE0D2')),
noteheadSlashedWhole1 : Object.freeze(new SMuFLCharacter('noteheadSlashedWhole1' , '\uE0D3')),
noteheadSlashedWhole2 : Object.freeze(new SMuFLCharacter('noteheadSlashedWhole2' , '\uE0D4')),
noteheadSlashedDoubleWhole1 : Object.freeze(new SMuFLCharacter('noteheadSlashedDoubleWhole1' , '\uE0D5')),
noteheadSlashedDoubleWhole2 : Object.freeze(new SMuFLCharacter('noteheadSlashedDoubleWhole2' , '\uE0D6')),
noteheadDiamondDoubleWhole : Object.freeze(new SMuFLCharacter('noteheadDiamondDoubleWhole' , '\uE0D7')),
noteheadDiamondWhole : Object.freeze(new SMuFLCharacter('noteheadDiamondWhole' , '\uE0D8')),
noteheadDiamondHalf : Object.freeze(new SMuFLCharacter('noteheadDiamondHalf' , '\uE0D9')),
noteheadDiamondHalfWide : Object.freeze(new SMuFLCharacter('noteheadDiamondHalfWide' , '\uE0DA')),
noteheadDiamondBlack : Object.freeze(new SMuFLCharacter('noteheadDiamondBlack' , '\uE0DB')),
noteheadDiamondBlackWide : Object.freeze(new SMuFLCharacter('noteheadDiamondBlackWide' , '\uE0DC')),
noteheadDiamondWhite : Object.freeze(new SMuFLCharacter('noteheadDiamondWhite' , '\uE0DD')),
noteheadDiamondWhiteWide : Object.freeze(new SMuFLCharacter('noteheadDiamondWhiteWide' , '\uE0DE')),
noteheadDiamondDoubleWholeOld : Object.freeze(new SMuFLCharacter('noteheadDiamondDoubleWholeOld' , '\uE0DF')),
noteheadDiamondWholeOld : Object.freeze(new SMuFLCharacter('noteheadDiamondWholeOld' , '\uE0E0')),
noteheadDiamondHalfOld : Object.freeze(new SMuFLCharacter('noteheadDiamondHalfOld' , '\uE0E1')),
noteheadDiamondBlackOld : Object.freeze(new SMuFLCharacter('noteheadDiamondBlackOld' , '\uE0E2')),
noteheadDiamondHalfFilled : Object.freeze(new SMuFLCharacter('noteheadDiamondHalfFilled' , '\uE0E3')),
noteheadCircledBlack : Object.freeze(new SMuFLCharacter('noteheadCircledBlack' , '\uE0E4')),
noteheadCircledHalf : Object.freeze(new SMuFLCharacter('noteheadCircledHalf' , '\uE0E5')),
noteheadCircledWhole : Object.freeze(new SMuFLCharacter('noteheadCircledWhole' , '\uE0E6')),
noteheadCircledDoubleWhole : Object.freeze(new SMuFLCharacter('noteheadCircledDoubleWhole' , '\uE0E7')),
noteheadCircledBlackLarge : Object.freeze(new SMuFLCharacter('noteheadCircledBlackLarge' , '\uE0E8')),
noteheadCircledHalfLarge : Object.freeze(new SMuFLCharacter('noteheadCircledHalfLarge' , '\uE0E9')),
noteheadCircledWholeLarge : Object.freeze(new SMuFLCharacter('noteheadCircledWholeLarge' , '\uE0EA')),
noteheadCircledDoubleWholeLarge : Object.freeze(new SMuFLCharacter('noteheadCircledDoubleWholeLarge' , '\uE0EB')),
noteheadCircledXLarge : Object.freeze(new SMuFLCharacter('noteheadCircledXLarge' , '\uE0EC')),
noteheadLargeArrowUpDoubleWhole : Object.freeze(new SMuFLCharacter('noteheadLargeArrowUpDoubleWhole' , '\uE0ED')),
noteheadLargeArrowUpWhole : Object.freeze(new SMuFLCharacter('noteheadLargeArrowUpWhole' , '\uE0EE')),
noteheadLargeArrowUpHalf : Object.freeze(new SMuFLCharacter('noteheadLargeArrowUpHalf' , '\uE0EF')),
noteheadLargeArrowUpBlack : Object.freeze(new SMuFLCharacter('noteheadLargeArrowUpBlack' , '\uE0F0')),
noteheadLargeArrowDownDoubleWhole: Object.freeze(new SMuFLCharacter('noteheadLargeArrowDownDoubleWhole', '\uE0F1')),
noteheadLargeArrowDownWhole : Object.freeze(new SMuFLCharacter('noteheadLargeArrowDownWhole' , '\uE0F2')),
noteheadLargeArrowDownHalf : Object.freeze(new SMuFLCharacter('noteheadLargeArrowDownHalf' , '\uE0F3')),
noteheadLargeArrowDownBlack : Object.freeze(new SMuFLCharacter('noteheadLargeArrowDownBlack' , '\uE0F4')),
noteheadParenthesisLeft : Object.freeze(new SMuFLCharacter('noteheadParenthesisLeft' , '\uE0F5')),
noteheadParenthesisRight : Object.freeze(new SMuFLCharacter('noteheadParenthesisRight' , '\uE0F6')),
noteheadCircleSlash : Object.freeze(new SMuFLCharacter('noteheadCircleSlash' , '\uE0F7')),
noteheadHeavyX : Object.freeze(new SMuFLCharacter('noteheadHeavyX' , '\uE0F8')),
noteheadHeavyXHat : Object.freeze(new SMuFLCharacter('noteheadHeavyXHat' , '\uE0F9')),
noteheadWholeFilled : Object.freeze(new SMuFLCharacter('noteheadWholeFilled' , '\uE0FA')),
noteheadHalfFilled : Object.freeze(new SMuFLCharacter('noteheadHalfFilled' , '\uE0FB')),
noteheadDiamondOpen : Object.freeze(new SMuFLCharacter('noteheadDiamondOpen' , '\uE0FC')),
}), IndividualNote: Object.freeze({
augmentationDot: Object.freeze(new SMuFLCharacter('augmentationDot', '\uE1E7')),
}), Stem: Object.freeze({
stem : Object.freeze(new SMuFLCharacter('stem' , '\uE210')),
stemSprechgesang : Object.freeze(new SMuFLCharacter('stemSprechgesang' , '\uE211')),
stemSwished : Object.freeze(new SMuFLCharacter('stemSwished' , '\uE212')),
stemPendereckiTremolo : Object.freeze(new SMuFLCharacter('stemPendereckiTremolo' , '\uE213')),
stemSulPonticello : Object.freeze(new SMuFLCharacter('stemSulPonticello' , '\uE214')),
stemBowOnBridge : Object.freeze(new SMuFLCharacter('stemBowOnBridge' , '\uE215')),
stemBowOnTailpiece : Object.freeze(new SMuFLCharacter('stemBowOnTailpiece' , '\uE216')),
stemBuzzRoll : Object.freeze(new SMuFLCharacter('stemBuzzRoll' , '\uE217')),
stemDamp : Object.freeze(new SMuFLCharacter('stemDamp' , '\uE218')),
stemVibratoPulse : Object.freeze(new SMuFLCharacter('stemVibratoPulse' , '\uE219')),
stemMultiphonicsBlack : Object.freeze(new SMuFLCharacter('stemMultiphonicsBlack' , '\uE21A')),
stemMultiphonicsWhite : Object.freeze(new SMuFLCharacter('stemMultiphonicsWhite' , '\uE21B')),
stemMultiphonicsBlackWhite: Object.freeze(new SMuFLCharacter('stemMultiphonicsBlackWhite', '\uE21C')),
stemSussurando : Object.freeze(new SMuFLCharacter('stemSussurando' , '\uE21D')),
stemRimShot : Object.freeze(new SMuFLCharacter('stemRimShot' , '\uE21E')),
stemHarpStringNoise : Object.freeze(new SMuFLCharacter('stemHarpStringNoise' , '\uE21F')),
}), Flag: Object.freeze({
flag8thUp : Object.freeze(new SMuFLCharacter('flag8thUp' , '\uE240')),
flag8thDown : Object.freeze(new SMuFLCharacter('flag8thDown' , '\uE241')),
flag16thUp : Object.freeze(new SMuFLCharacter('flag16thUp' , '\uE242')),
flag16thDown : Object.freeze(new SMuFLCharacter('flag16thDown' , '\uE243')),
flag32ndUp : Object.freeze(new SMuFLCharacter('flag32ndUp' , '\uE244')),
flag32ndDown : Object.freeze(new SMuFLCharacter('flag32ndDown' , '\uE245')),
flag64thUp : Object.freeze(new SMuFLCharacter('flag64thUp' , '\uE246')),
flag64thDown : Object.freeze(new SMuFLCharacter('flag64thDown' , '\uE247')),
flag128thUp : Object.freeze(new SMuFLCharacter('flag128thUp' , '\uE248')),
flag128thDown : Object.freeze(new SMuFLCharacter('flag128thDown' , '\uE249')),
flag256thUp : Object.freeze(new SMuFLCharacter('flag256thUp' , '\uE24A')),
flag256thDown : Object.freeze(new SMuFLCharacter('flag256thDown' , '\uE24B')),
flag512thUp : Object.freeze(new SMuFLCharacter('flag512thUp' , '\uE24C')),
flag512thDown : Object.freeze(new SMuFLCharacter('flag512thDown' , '\uE24D')),
flag1024thUp : Object.freeze(new SMuFLCharacter('flag1024thUp' , '\uE24E')),
flag1024thDown : Object.freeze(new SMuFLCharacter('flag1024thDown' , '\uE24F')),
flagInternalUp : Object.freeze(new SMuFLCharacter('flagInternalUp' , '\uE250')),
flagInternalDown: Object.freeze(new SMuFLCharacter('flagInternalDown', '\uE251')),
}), StandardAccidental: Object.freeze({
accidentalFlat : Object.freeze(new SMuFLCharacter('accidentalFlat' , '\uE260')),
accidentalNatural : Object.freeze(new SMuFLCharacter('accidentalNatural' , '\uE261')),
accidentalSharp : Object.freeze(new SMuFLCharacter('accidentalSharp' , '\uE262')),
accidentalDoubleSharp : Object.freeze(new SMuFLCharacter('accidentalDoubleSharp' , '\uE263')),
accidentalDoubleFlat : Object.freeze(new SMuFLCharacter('accidentalDoubleFlat' , '\uE264')),
accidentalTripleSharp : Object.freeze(new SMuFLCharacter('accidentalTripleSharp' , '\uE265')),
accidentalTripleFlat : Object.freeze(new SMuFLCharacter('accidentalTripleFlat' , '\uE266')),
accidentalNaturalFlat : Object.freeze(new SMuFLCharacter('accidentalNaturalFlat' , '\uE267')),
accidentalNaturalSharp: Object.freeze(new SMuFLCharacter('accidentalNaturalSharp', '\uE268')),
accidentalSharpSharp : Object.freeze(new SMuFLCharacter('accidentalSharpSharp' , '\uE269')),
accidentalParensLeft : Object.freeze(new SMuFLCharacter('accidentalParensLeft' , '\uE26A')),
accidentalParensRight : Object.freeze(new SMuFLCharacter('accidentalParensRight' , '\uE26B')),
accidentalBracketLeft : Object.freeze(new SMuFLCharacter('accidentalBracketLeft' , '\uE26C')),
accidentalBracketRight: Object.freeze(new SMuFLCharacter('accidentalBracketRight', '\uE26D')),
}), Rest: Object.freeze({
restMaxima : Object.freeze(new SMuFLCharacter('restMaxima' , '\uE4E0')),
restLonga : Object.freeze(new SMuFLCharacter('restLonga' , '\uE4E1')),
restDoubleWhole : Object.freeze(new SMuFLCharacter('restDoubleWhole' , '\uE4E2')),
restWhole : Object.freeze(new SMuFLCharacter('restWhole' , '\uE4E3')),
restHalf : Object.freeze(new SMuFLCharacter('restHalf' , '\uE4E4')),
restQuarter : Object.freeze(new SMuFLCharacter('restQuarter' , '\uE4E5')),
rest8th : Object.freeze(new SMuFLCharacter('rest8th' , '\uE4E6')),
rest16th : Object.freeze(new SMuFLCharacter('rest16th' , '\uE4E7')),
rest32nd : Object.freeze(new SMuFLCharacter('rest32nd' , '\uE4E8')),
rest64th : Object.freeze(new SMuFLCharacter('rest64th' , '\uE4E9')),
rest128th : Object.freeze(new SMuFLCharacter('rest128th' , '\uE4EA')),
rest256th : Object.freeze(new SMuFLCharacter('rest256th' , '\uE4EB')),
rest512th : Object.freeze(new SMuFLCharacter('rest512th' , '\uE4EC')),
rest1024th : Object.freeze(new SMuFLCharacter('rest1024th' , '\uE4ED')),
restHBar : Object.freeze(new SMuFLCharacter('restHBar' , '\uE4EE')),
restHBarLeft : Object.freeze(new SMuFLCharacter('restHBarLeft' , '\uE4EF')),
restHBarMiddle : Object.freeze(new SMuFLCharacter('restHBarMiddle' , '\uE4F0')),
restHBarRight : Object.freeze(new SMuFLCharacter('restHBarRight' , '\uE4F1')),
restQuarterOld : Object.freeze(new SMuFLCharacter('restQuarterOld' , '\uE4F2')),
restDoubleWholeLegerLine: Object.freeze(new SMuFLCharacter('restDoubleWholeLegerLine', '\uE4F3')),
restWholeLegerLine : Object.freeze(new SMuFLCharacter('restWholeLegerLine' , '\uE4F4')),
restHalfLegerLine : Object.freeze(new SMuFLCharacter('restHalfLegerLine' , '\uE4F5')),
restQuarterZ : Object.freeze(new SMuFLCharacter('restQuarterZ' , '\uE4F6')),
}),
});// const SMuFL
plane-geometry.js
平面上の点、ベクトル、大きさ
ソースコード
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}// Point.constructor
vectorTo(p) {
const x = p.x - this.x;
const y = p.y - this.y;
return new Vector(x, y);
}// Point.vectorTo
translate(v) {
const x = this.x + v.x;
const y = this.y + v.y;
return new Point(x, y);
}// Point.translate
scale(s) {
const x = this.x * s;
const y = this.y * s;
return new Point(x, y);
}// Point.scale
asVector() {
return new Vector(this.x, this.y);
}// Point.asVector
}// class Point
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}// Vector.constructor
get norm() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}// get Vector.norm
normalise() {
return this.scale(1 / this.norm);;
}// Vector.normalise
add(v) {
const x = this.x + v.x;
const y = this.y + v.y;
return new Vector(x, y);
}// Vector.add
rotateByDegree(degree) {
const rad = degree / 180 * Math.PI;
const sin = Math.sin(rad);
const cos = Math.cos(rad);
const x = cos * this.x - sin * this.y;
const y = sin * this.x + cos * this.y;
return new Vector(x, y);
}// Vector.rotateByDegree
scale(s) {
const x = this.x * s;
const y = this.y * s;
return new Vector(x, y);
}// Vector.scale
crossProduct(v) {
return this.x*v.y - this.y*v.x;
}// Vector.crossProduct
dotProduct(v) {
return this.x*v.x + this.y*v.y;
}// Vector.dotProduct
}// class Vector
class Size {
constructor(width, height) {
this.width = width;
this.height = height;
}// Size.constructor
stretch(s) {
const w = this.width * s;
const h = this.height * s;
return new Size(w, h);
}// Size.stretch
}// class Size
class AABB {
constructor(leftTop, size) {
this.leftTop = leftTop;
this.size = size;
}// AABB.constructor
get width() { return this.size.width; }
get height() { return this.size.height; }
get left() { return this.leftTop.x; }
get top() { return this.leftTop.y; }
get right() { return this.leftTop.x + this.width; }
get bottom() { return this.leftTop.y + this.height; }
or(aabb) {
const left = Math.min(this.left, aabb.left);
const top = Math.min(this.top, aabb.top);
const right = Math.max(this.right, aabb.right);
const bottom = Math.max(this.bottom, aabb.bottom);
return new AABB(new Point(left, top), new Size(right - left, bottom - top));
}// AABB.or
and(aabb) {
const left = Math.min(this.left, aabb.left);
const top = Math.min(this.top, aabb.top);
const right = Math.max(this.right, aabb.right);
const bottom = Math.max(this.bottom, aabb.bottom);
return new AABB(new Point(left, top), new Size(right - left, bottom - top));
}// AABB.and
translate(v) {
return new AABB(this.leftTop.translate(v), this.size);
}// AABB.and
scale(s) {
return new AABB(this.leftTop.scale(s), this.size.stretch(s));
}// AABB.scale
stretch(s) {
return new AABB(this.leftTop, this.size.stretch(s));
}// AABB.stretch
}// class AABB
function intersection(a, b, c, d) {
// 直線 a-b、c-d の交点を求める
const vab = a.vectorTo(b);
const vac = a.vectorTo(c);
const vcd = c.vectorTo(d);
const s = vac.crossProduct(vcd) / vab.crossProduct(vcd);
if(!isFinite(s)) {
// s が有限の値でない <=> 分母が 0 であった <=> 二直線が平行
// => 交点が0個、または無限個となるので null を返す
return null;
}
return a.translate(vab.scale(s));
}
stave-position.js
五線上の垂直位置を表現するクラス.
第一線の index を 0 として、第一間を 1、第二線を 2、...、下第一間を -1、下第一線を -2、... とする形で定めている.
const StavePositionPrefix = Object.freeze({
upper : 'upper',
center: 'center',
lower : 'lower'
});// const StavePositionPrefix
class StavePosition {
constructor({ prefix, position, onLine, index }) {
if(index != undefined) {
// constructor(index)
onLine = (index & 1) !== 1;
const b = onLine? 0 : 1;
const a = (index - b) / 2;
if(index > 8) {
prefix = StavePositionPrefix.upper;
position = onLine? a-4 : a-3;
} else if(index < 0) {
prefix = StavePositionPrefix.lower;
position = -a;
} else {
prefix = StavePositionPrefix.center;
position = a+1;
}
} else {
// constructor(prefix, position, onLine)
const a = prefix === StavePositionPrefix.upper ? onLine? position+4 : position+3
: prefix === StavePositionPrefix.center? position-1
: /* prefix === 'lower' ? */ -position;
const b = onLine? 0 : 1;
index = a * 2 + b;
}
if(prefix === undefined || position === undefined || onLine === undefined || index === undefined) {
throw new Error(`Invalid state exception!; { prefix: ${prefix}, position: ${position}, onLine: ${onLine}, index: ${index} }`);
}
this.prefix = prefix;
this.position = position;
this.onLine = onLine;
this.index = index;
}
}// class StavePosition
音部記号
定義されたグリフひとつをポン置きするだけ.
ソースコード
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>SMuFL: Engraving Demo</title>
<style>
@font-face {
font-family: 'Bravura';
src: url('../fonts/Bravura.otf');
}
</style>
<script src='../fonts/bravura_metadata.js'></script>
<script src='./util/utility-functions.js'></script>
<script src='./util/smufl-characters.js'></script>
<script src='./util/plane-geometry.js'></script>
<script src='./util/stave-position.js'></script>
<script type='text/javascript'>
function smuflBBox2aabb(metadata, characterName) {
// (x, y) = (0, 0)、staffSpace = 1 (fontSize = 4) であるときの AABB を取得する
const bb = metadata.glyphBBoxes[characterName];
const sw = bb.bBoxSW;
const ne = bb.bBoxNE;
const left = sw[0];
const top = -ne[1];
const right = ne[0];
const bottom = -sw[1];
return new AABB(new Point(left, top), new Size(right - left, bottom - top));
}// function smuflBBox2aabb
const ClefType = Object.freeze({
G: 'G',
C: 'C',
F: 'F'
// Percussion も定義した方がいいかも?
});// const ClefType
class Clef {
constructor(clefType, lineAt) {
this.clefType = clefType;
this.lineAt = lineAt;
this.stavePosition = new StavePosition({
prefix: StavePositionPrefix.center,
position: lineAt,
onLine: true
});
}// Clef.constructor
}// class Clef
window.onload = function () {
function drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata) {
const staffSpace = fontSize / 4;
const staffLineThickness = metadata.engravingDefaults.staffLineThickness * staffSpace;
ctx.strokeStyle = staffLineThickness;
ctx.beginPath();
for(let i = 0; i < 5; ++i) {
ctx.moveTo(x, firstLineY - i * staffSpace);
ctx.lineTo(x + staffWidth, firstLineY - i * staffSpace);
}
ctx.stroke();
}// function drawStave
function drawClef(ctx, clef, fontSize, x, firstLineY, metadata) {
// metadata が不要な計算
const { clefType, lineAt, stavePosition } = clef;
const smuflCharacter = {
[ClefType.G]: SMuFL.Clef.gClef,
[ClefType.C]: SMuFL.Clef.cClef,
[ClefType.F]: SMuFL.Clef.fClef,
}[clefType];
// metadata を用いて、x = 0, firstLineY = 0, staffSpace = 1 として描画した場合の大きさなどを計算する
const position = new Point(0, -stavePosition.index/2);
const aabb = smuflBBox2aabb(metadata, smuflCharacter.name).translate(position.asVector());
// (x, firstLineY) と staffSpace を指定された場合の大きさなどを計算する
const staffSpace = fontSize / 4;
const basePosition = new Point(x, firstLineY);
const finalPosition = basePosition.translate(position.asVector().scale(staffSpace));
const finalAABB = aabb.scale(staffSpace).translate(basePosition);
// 実際に描画する
ctx.fillText(smuflCharacter.code, finalPosition.x, finalPosition.y);
ctx.save();
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.rect(finalAABB.left, finalAABB.top, finalAABB.width, finalAABB.height);
ctx.stroke();
ctx.restore();
return finalAABB;
}// function drawClef
// フォントファイルの準備
document.fonts.load('10px Bravura').then(() => {
const canvas = document.getElementById('main-canvas');
const ctx = canvas.getContext('2d');
const firstLineY = 115;
const fontSize = 45;
const staffSpace = fontSize / 4;
const metadata = bravuraMetadata;
const fontName = 'Bravura';
ctx.font = `${fontSize}px ${fontName}`;
const padding = 0.4 * staffSpace;// 記号間のスペースを適当に決める
const initialX = 10;
let x = initialX;
const staffWidth = 1240 / 2;
drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata);
// ト音記号
for(let i = 1; i <= 5; ++i) {
const clef = new Clef(ClefType.G, i);
const clefAABB = drawClef(ctx, clef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
}
// ハ音記号
for(let i = 1; i <= 5; ++i) {
const clef = new Clef(ClefType.C, i);
const clefAABB = drawClef(ctx, clef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
}
// ヘ音記号
for(let i = 1; i <= 5; ++i) {
const clef = new Clef(ClefType.F, i);
const clefAABB = drawClef(ctx, clef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
}
});// document.fonts.load
};// function window.onload
</script>
</head>
<body>
<div>
<canvas id='main-canvas' width='640px' height='180px' style='border:solid'></canvas>
</div>
</body>
</html>
結果:
調号
調号を描画するには、現在の音部記号が必要である.
音部記号がなければ五線上の位置と音高が対応付けられないためである.
トレブル記号(第二線のト音記号)が書かれているとき、ト長調では第五線にだけシャープを書くが、同じ F の音である第一間には何も書かない.
同じ音でもどこに記号を置くかはおそらく伝統的に決まっているものであって、なにか規則性があるものではないように感じた.
以下で示す処理では、五線の第一線が何の音であるかを求め、それに応じて記号の位置を決定している.
第一線の音が決まったとき、記号をどこに書くべきかについては MuseScore を参考にした.
調号(複数の臨時記号)の描画に際しては、公式ドキュメント第3章第6節 Bounding box cut-outs に従って適当に間を詰めてやる必要がある.
ソースコード
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>SMuFL: Engraving Demo</title>
<style>
@font-face {
font-family: 'Bravura';
src: url('../fonts/Bravura.otf');
}
</style>
<script src='../fonts/bravura_metadata.js'></script>
<script src='./util/utility-functions.js'></script>
<script src='./util/smufl-characters.js'></script>
<script src='./util/plane-geometry.js'></script>
<script src='./util/stave-position.js'></script>
<script type='text/javascript'>
function smuflBBox2aabb(metadata, characterName) {
// (x, y) = (0, 0)、staffSpace = 1 (fontSize = 4) であるときの AABB を取得する
const bb = metadata.glyphBBoxes[characterName];
const sw = bb.bBoxSW;
const ne = bb.bBoxNE;
const left = sw[0];
const top = -ne[1];
const right = ne[0];
const bottom = -sw[1];
return new AABB(new Point(left, top), new Size(right - left, bottom - top));
}// function smuflBBox2aabb
const ClefType = Object.freeze({
[省略]
});// const ClefType
class Clef {
[省略]
}// class Clef
const NaturalTone = Object.freeze({
C: 0,
D: 1,
E: 2,
F: 3,
G: 4,
A: 5,
B: 6,
});// const NaturalTone
const NaturalToneArray = Object.freeze([
'C',
'D',
'E',
'F',
'G',
'A',
'B',
]);// const NaturalToneArray
class Tone {
constructor({ naturalTone, alter, fifths }) {
if(fifths == undefined) {
fifths = Tone.toFifths(naturalTone, alter);
} else {
naturalTone = Fs.floorDivMod(fifths * 4, 7).r;
alter = Fs.floorDivMod(fifths + 1, 7).q;
}
this.naturalTone = naturalTone;
this.fifths = fifths;
this.alter = alter;
}
toString() {
const name = NaturalToneArray[this.naturalTone];
let alter = '';
if(this.alter < 0) {
alter = 'b'.repeat(-this.alter);
} else if(this.alter > 0) {
alter = 'x'.repeat(this.alter/2);
if(this.alter&1) alter += '#'
}
return name + alter;
}
static toFifths(naturalTone, alter) {
const x = Fs.naturalToneToFifths(naturalTone, 7);
const y = alter * 7;
return x + y;
}
}// class Tone
const KeyMode = Object.freeze({
ionian : 0,
dorian : 1,
phrigian : 2,
lydian : 3,
mixolydian: 4,
aeolian : 5,
locrian : 6,
major : 0,
minor : 5,
});// const KeyMode
const KeyModeArray = Object.freeze([
'ionian' ,
'dorian' ,
'phrigian' ,
'lydian' ,
'mixolydian',
'aeolian' ,
'locrian' ,
]);// const KeyModeArray
class KeySignature {
constructor(tonic, mode) {
const modeFifths = Fs.naturalToneToFifths(mode);
if(tonic.fifths < modeFifths-7 || modeFifths+7 < tonic.fifths) {
throw new Error(`strange key: tonic=${tonic}, mode=${mode}`);
}
this.tonic = tonic;
this.mode = mode;
const s = tonic.fifths - modeFifths - 1;
const alters = new Array(7);
for(let i = 0; i < 7; ++i) {
const fifths = i + s;
const t = new Tone({ fifths: fifths });
alters[t.naturalTone] = t.alter;
}// loop for i in [0..7)
this.alters = Object.freeze(alters);
}
toString() {
return `${this.tonic} ${KeyModeArray[this.mode]}`;
}
parallelKey(newMode) {
return new KeySignature(this.tonic, newMode);
}
relativeKey(newMode) {
const oldModeFifths = Tone.toFifths(this.mode, 0);
const newModeFifths = Tone.toFifths(newMode, 0);
const newFifths = this.tonic.fifths - oldModeFifths + newModeFifths;
return new KeySignature(new Tone({ fifths: newFifths }), newMode);
}
}// KeySignature
class Pitch {
constructor(tone, octave) {
this.tone = tone;
this.octave = octave;
this.diatonicNumber = tone.naturalTone + (octave + 1) * 7;
}
static fromDiatonicNumber(diatonicNumber) {
const fdm = Fs.floorDivMod(diatonicNumber, 7);
const tone = new Tone({ naturalTone: fdm.r, alter: 0 });
const octave = fdm.q - 1;
return new Pitch(tone, octave);
}
}// class Pitch
window.onload = function () {
function drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata) {
const staffSpace = fontSize / 4;
const staffLineThickness = metadata.engravingDefaults.staffLineThickness * staffSpace;
ctx.strokeStyle = staffLineThickness;
ctx.beginPath();
for(let i = 0; i < 5; ++i) {
ctx.moveTo(x, firstLineY - i * staffSpace);
ctx.lineTo(x + staffWidth, firstLineY - i * staffSpace);
}
ctx.stroke();
}// function drawStave
function drawClef(ctx, clef, fontSize, x, firstLineY, metadata) {
[省略]
}// function drawClef
function drawKeySignature(ctx, keySignature, currentClef, fontSize, x, firstLineY, metadata) {
const stavePositionIndexDifferencePerfectFifth = 4;
// 現在の音部記号から、第一線の音を特定する
const basePosition = new StavePosition({prefix:'center', position:currentClef.lineAt, onLine:true});
let basePitch = null;
if(currentClef.clefType === ClefType.F) {
basePitch = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 3);
} else if(currentClef.clefType === ClefType.C) {
basePitch = new Pitch(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), 4);
} else if(currentClef.clefType === ClefType.G) {
basePitch = new Pitch(new Tone({ naturalTone: NaturalTone.G, alter: 0 }), 4);
} else {
throw new Error('Unknown clef type: ' + currentClef.type);
}
// basePosition corresponds basePitch
const pitchAtFirstLine = Pitch.fromDiatonicNumber(basePitch.diatonicNumber - basePosition.index);
// metadata が不要な計算
// キャンセルのためにナチュラルを置く場合や、シャープとフラットが入り混じる変な調号は考えない
const { tonic, mode } = keySignature;
const tonicInMajor = keySignature.relativeKey(KeyMode.major).tonic;
const tonicFifths = tonicInMajor.fifths;
const smuflCharacterAndPositions = [];
if(tonicFifths < 0) {
const smuflCharacter = SMuFL.StandardAccidental.accidentalFlat;
const minIndex = [0,2,1,0,-1,2,1][pitchAtFirstLine.tone.naturalTone];
let stavePositionIndex = [6,5,4,3,2,8,7][pitchAtFirstLine.tone.naturalTone];
for(let i = 0; i > tonicFifths; --i) {
smuflCharacterAndPositions.push({
smuflCharacter: smuflCharacter,
stavePosition: new StavePosition({index: stavePositionIndex}),
});
stavePositionIndex = Fs.floorDivMod(stavePositionIndex-stavePositionIndexDifferencePerfectFifth-minIndex, 7).r + minIndex;
}// loop for i in (tonicFifths..0]
} else if(tonicFifths > 0) {
const smuflCharacter = SMuFL.StandardAccidental.accidentalSharp;
const minIndex = [3,2,3,2,1,2,3][pitchAtFirstLine.tone.naturalTone];
let stavePositionIndex = [3,2,8,7,6,5,4][pitchAtFirstLine.tone.naturalTone];
for(let i = 0; i < tonicFifths; ++i) {
smuflCharacterAndPositions.push({
smuflCharacter: smuflCharacter,
stavePosition: new StavePosition({index: stavePositionIndex}),
});
stavePositionIndex = Fs.floorDivMod(stavePositionIndex+stavePositionIndexDifferencePerfectFifth-minIndex, 7).r + minIndex;
}// loop for i in [0..tonicFifths)
} else {
// 調号で描画する記号が存在しないので、大きさ 0 として終了する
const finalPosition = new Point(x, firstLineY);
return new AABB(finalPosition, new Size(0, 0));
}
// metadata を用いて、x = 0, firstLineY = 0, staffSpace = 1 として描画した場合の大きさなどを計算する
let left = 0;
let top = 0;
let right = 0;
let bottom = 0;
const positions = new Array();
let tx = 0;
let ty = -smuflCharacterAndPositions[0].stavePosition.index / 2;
let aabb = smuflBBox2aabb(metadata, smuflCharacterAndPositions[0].smuflCharacter.name).translate(new Vector(tx, ty));
positions.push(new Point(tx, ty));
if(Math.abs(tonicFifths) > 0) for(let { smuflCharacter, stavePosition } of smuflCharacterAndPositions.slice(1)) {
const modifierName = smuflCharacter.name;
const pos = stavePosition.index / 2;
const newAABB = smuflBBox2aabb(metadata, smuflCharacter.name);
let { cutOutSE, cutOutNE, cutOutSW, cutOutNW } = metadata.glyphsWithAnchors[modifierName];
if(cutOutSE === undefined) cutOutSE = [newAABB.right, newAABB.bottom];// 例えばフラットは右上にしか欠き込みがないため、NE 以外は undefined となっている.
if(cutOutNE === undefined) cutOutNE = [newAABB.right, newAABB.top ];// 欠き込みがない場合は、bb の頂点を使用する.
if(cutOutSW === undefined) cutOutSW = [newAABB.left , newAABB.bottom];// 計算しないようにすると分岐が面倒そうだし.
if(cutOutNW === undefined) cutOutNW = [newAABB.left , newAABB.top ];
const lastX = tx;
const lastY = ty;
tx = lastX + newAABB.right;
ty = -pos;
const lastCutOutSE = {
depth: newAABB.right - cutOutSE[0],
y: lastY - cutOutSE[1]
};
const lastCutOutNE = {
depth: newAABB.right - cutOutNE[0],
y: lastY - cutOutNE[1]
};
const curCutOutSW = {
depth: cutOutSW[0] - newAABB.left,
y: ty - cutOutSW[1]
};
const curCutOutNW = {
depth: cutOutNW[0] - newAABB.left,
y: ty - cutOutNW[1]
};
if(lastCutOutSE.y >= curCutOutNW.y) {
// 前の要素の右下が、今の要素の左上より高い
tx -= Math.min(lastCutOutSE.depth, curCutOutNW.depth);
} else if(lastCutOutNE.y <= curCutOutSW.y) {
// 前の要素の右上が、今の要素の左下より低い
tx -= Math.min(lastCutOutNE.depth, curCutOutSW.depth);
}
aabb = aabb.or(newAABB.translate(new Vector(tx, ty)));
positions.push(new Point(tx, ty));
}// loop for { smuflCharacter, stavePosition } in smuflCharacterAndPositions
// (x, firstLineY) と staffSpace を指定された場合の大きさなどを計算する
const staffSpace = fontSize / 4;
const finalPosition = new Point(x, firstLineY);
const finalAABB = aabb.scale(staffSpace).translate(finalPosition.asVector());
// 実際に描画する
for(let i in positions) {
const { smuflCharacter } = smuflCharacterAndPositions[i];
const pos = positions[i];
const fp = pos.scale(staffSpace).translate(finalPosition.asVector());
ctx.fillText(smuflCharacter.code, fp.x, fp.y);
}// loop for i in [0 .. upperSmuflCharacters.length)
ctx.save();
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.rect(finalAABB.left, finalAABB.top, finalAABB.width, finalAABB.height);
ctx.stroke();
ctx.restore();
return finalAABB;
}// function drawKeySignature
// フォントファイルの準備
document.fonts.load('10px Bravura').then(() => {
const canvas = document.getElementById('main-canvas-');
const ctx = canvas.getContext('2d');
const firstLineY = 115;
const fontSize = 45;
const staffSpace = fontSize / 4;
const metadata = bravuraMetadata;
const fontName = 'Bravura';
ctx.font = `${fontSize}px ${fontName}`;
const padding = 0.4 * staffSpace;// 記号間のスペースを適当に決める
const initialX = 10;
let x = initialX;
const staffWidth = 1240 / 2;
drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata);
// draw key signature
x = initialX;
{
const currentClef = new Clef(ClefType.G, 2);// 調号の描画位置を決定するには、音部記号が必要
const clefAABB = drawClef(ctx, currentClef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
for(let key of [new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: 1 }), KeyMode.ionian), new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: -1 }), KeyMode.ionian)]) {
const keySignatureAABB = drawKeySignature(ctx, key, currentClef, fontSize, x + padding, firstLineY, metadata);
x = keySignatureAABB.right + padding;
}// loop for timeSignature
} {
const currentClef = new Clef(ClefType.F, 4);// 調号の描画位置を決定するには、音部記号が必要
const clefAABB = drawClef(ctx, currentClef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
for(let key of [new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: 1 }), KeyMode.ionian), new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: -1 }), KeyMode.ionian)]) {
const keySignatureAABB = drawKeySignature(ctx, key, currentClef, fontSize, x + padding, firstLineY, metadata);
x = keySignatureAABB.right + padding;
}// loop for timeSignature
} {
const currentClef = new Clef(ClefType.C, 3);// 調号の描画位置を決定するには、音部記号が必要
const clefAABB = drawClef(ctx, currentClef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
for(let key of [new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: 1 }), KeyMode.ionian), new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: -1 }), KeyMode.ionian)]) {
const keySignatureAABB = drawKeySignature(ctx, key, currentClef, fontSize, x + padding, firstLineY, metadata);
x = keySignatureAABB.right + padding;
}// loop for timeSignature
}
});// document.fonts.load
};// function window.onload
</script>
</head>
<body>
<div>
<canvas id='main-canvas-' width='640px' height='180px' style='border:solid'></canvas>
</div>
</body>
</html>
結果:
拍子記号
4/4 拍子を C みたいな記号で書く場合もあるが、ここでは上下二段に数字を各方法だけを考慮している.
注意点があるとすれば、上下を中央揃えにすることくらいだろうか.
ソースコード
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>SMuFL: Engraving Demo</title>
<style>
@font-face {
font-family: 'Bravura';
src: url('../fonts/Bravura.otf');
}
</style>
<script src='../fonts/bravura_metadata.js'></script>
<script src='./util/utility-functions.js'></script>
<script src='./util/smufl-characters.js'></script>
<script src='./util/plane-geometry.js'></script>
<script src='./util/stave-position.js'></script>
<script type='text/javascript'>
function smuflBBox2aabb(metadata, characterName) {
// (x, y) = (0, 0)、staffSpace = 1 (fontSize = 4) であるときの AABB を取得する
const bb = metadata.glyphBBoxes[characterName];
const sw = bb.bBoxSW;
const ne = bb.bBoxNE;
const left = sw[0];
const top = -ne[1];
const right = ne[0];
const bottom = -sw[1];
return new AABB(new Point(left, top), new Size(right - left, bottom - top));
}// function smuflBBox2aabb
class TimeSignature {
constructor(beats, beatType) {
this.beats = beats;
this.beatType = beatType;
}// TimeSignature.constructor
}// class TimeSignature
window.onload = function () {
function drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata) {
const staffSpace = fontSize / 4;
const staffLineThickness = metadata.engravingDefaults.staffLineThickness * staffSpace;
ctx.strokeStyle = staffLineThickness;
ctx.beginPath();
for(let i = 0; i < 5; ++i) {
ctx.moveTo(x, firstLineY - i * staffSpace);
ctx.lineTo(x + staffWidth, firstLineY - i * staffSpace);
}
ctx.stroke();
}// function drawStave
function drawTimeSignature(ctx, timeSignature, fontSize, x, firstLineY, metadata) {
function integer2symbols(int, symbols) {
// int を digit 進位取り記法に倣った記号列に変換する
// 渡された int = 0 であるとき空の配列を返してしまうが、そんな入力は考慮しない
const digit = symbols.length;
const digits = [];
while(int != 0) {
digits.push(symbols[int % digit]);
int = Fs.floorDivMod(int, digit).q;
}// while int != 0
return digits.reverse();
}// function integer2symbols
// metadata が不要な計算
const { beats, beatType } = timeSignature;
const digitGlyphs = [
SMuFL.TimeSignature.timeSig0,
SMuFL.TimeSignature.timeSig1,
SMuFL.TimeSignature.timeSig2,
SMuFL.TimeSignature.timeSig3,
SMuFL.TimeSignature.timeSig4,
SMuFL.TimeSignature.timeSig5,
SMuFL.TimeSignature.timeSig6,
SMuFL.TimeSignature.timeSig7,
SMuFL.TimeSignature.timeSig8,
SMuFL.TimeSignature.timeSig9,
];
const upperSmuflCharacters = integer2symbols(beats, digitGlyphs);
const lowerSmuflCharacters = integer2symbols(beatType, digitGlyphs);
// metadata を用いて、x = 0, firstLineY = 0, staffSpace = 1 として描画した場合の描画位置を計算する
// -- 上側の配置を計算する
let upperAABB = null;
let upperXs = new Array();
let tx = 0;
let ty = -3;
for(let i = 0; i < upperSmuflCharacters.length; ++i) {
const ch = upperSmuflCharacters[i];
const newAABB = smuflBBox2aabb(metadata, ch.name).translate(new Vector(tx, ty));
if(!upperAABB) {
upperAABB = newAABB;
} else {
upperAABB = upperAABB.or(newAABB);
}
upperXs.push(tx);
tx = newAABB.right;
}// loop for ch in symbol.upperSmuflCharacters
// -- 下側の配置を計算する
let lowerAABB = null;
let lowerXs = new Array();
tx = 0;
ty = -1;
for(let i = 0; i < lowerSmuflCharacters.length; ++i) {
const ch = lowerSmuflCharacters[i];
const newAABB = smuflBBox2aabb(metadata, ch.name).translate(new Vector(tx, ty));
if(!lowerAABB) {
lowerAABB = newAABB;
} else {
lowerAABB = lowerAABB.or(newAABB);
}
lowerXs.push(tx);
tx = newAABB.right;
}// loop for ch in symbol.lowerSmuflCharacters
// -- 上側と下側を中央揃えにする
const bigHead = upperAABB.width > lowerAABB.width;
let maxWidth = lowerAABB.width;
let minWidth = upperAABB.width;
if(bigHead) {
maxWidth = upperAABB.width;
minWidth = lowerAABB.width;
}
const diff = (maxWidth - minWidth) / 2;
if(bigHead) {
lowerXs = lowerXs.map(x => x + diff);
} else {
upperXs = upperXs.map(x => x + diff);
}
const aabb = lowerAABB.or(upperAABB);// 拍子記号全体の AABB
// (x, firstLineY) と staffSpace を指定された場合の大きさなどを計算する
const staffSpace = fontSize / 4;
const finalPosition = new Point(x, firstLineY);
const finalAABB = new AABB(new Point(x + aabb.left * staffSpace, firstLineY + aabb.top * staffSpace), new Size(aabb.width * staffSpace, aabb.height * staffSpace));
// 実際に描画する
for(let i in upperSmuflCharacters) {
const ch = upperSmuflCharacters[i];
const cx = upperXs[i];// current x/y
const cy = -3;
const fp = finalPosition.translate(new Vector(cx, cy).scale(staffSpace));// final position
ctx.fillText(ch.code, fp.x, fp.y);
}// loop for i in [0 .. upperSmuflCharacters.length)
for(let i in lowerSmuflCharacters) {
const ch = lowerSmuflCharacters[i];
const cx = lowerXs[i];// current x/y
const cy = -1;
const fp = finalPosition.translate(new Vector(cx, cy).scale(staffSpace));// final position
ctx.fillText(ch.code, fp.x, fp.y);
}// loop for i in [0 .. lowerSmuflCharacters.length)
ctx.save();
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.rect(finalAABB.left, finalAABB.top, finalAABB.width, finalAABB.height);
ctx.stroke();
ctx.restore();
return finalAABB;
}// function drawTimeSignature
// フォントファイルの準備
document.fonts.load('10px Bravura').then(() => {
const canvas = document.getElementById('main-canvas');
const ctx = canvas.getContext('2d');
const firstLineY = 115;
const fontSize = 45;
const staffSpace = fontSize / 4;
const metadata = bravuraMetadata;
const fontName = 'Bravura';
ctx.font = `${fontSize}px ${fontName}`;
const padding = 0.4 * staffSpace;// 記号間のスペースを適当に決める
const initialX = 10;
let x = initialX;
const staffWidth = 1240 / 2;
drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata);
// draw time signature
x = initialX;
for(let timeSignature of [new TimeSignature(3, 4), new TimeSignature(9, 16), new TimeSignature(35, 128), new TimeSignature(1356789, 2048)]) {
const timeSignatureAABB = drawTimeSignature(ctx, timeSignature, fontSize, x + padding, firstLineY, metadata);
x = timeSignatureAABB.right + padding;
}// loop for timeSignature
});// document.fonts.load
};// function window.onload
</script>
</head>
<body>
<div>
<canvas id='main-canvas' width='640px' height='180px' style='border:solid'></canvas>
</div>
</body>
</html>
結果:
小節線
小節線については engravingDefaults にも各種パラメータが定義されているため線を引いたり丸を書いたりしてもよいかもしれない.
が、ここではグリフの描画だけ行っている.
ソースコード
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>SMuFL: Engraving Demo</title>
<style>
@font-face {
font-family: 'Bravura';
src: url('../fonts/Bravura.otf');
}
</style>
<script src='../fonts/bravura_metadata.js'></script>
<script src='./util/utility-functions.js'></script>
<script src='./util/smufl-characters.js'></script>
<script src='./util/plane-geometry.js'></script>
<script src='./util/stave-position.js'></script>
<script type='text/javascript'>
function smuflBBox2aabb(metadata, characterName) {
// (x, y) = (0, 0)、staffSpace = 1 (fontSize = 4) であるときの AABB を取得する
const bb = metadata.glyphBBoxes[characterName];
const sw = bb.bBoxSW;
const ne = bb.bBoxNE;
const left = sw[0];
const top = -ne[1];
const right = ne[0];
const bottom = -sw[1];
return new AABB(new Point(left, top), new Size(right - left, bottom - top));
}// function smuflBBox2aabb
// 左辺の dashed から tick までは musicxml の仕様に倣った名前
const BarlineType = Object.freeze({
dashed : 'dashed',
dotted : 'dotted',
heavy : 'heavy',
heavyHeavy : 'heavy-heavy',
heavyLight : 'heavy-light',
lightHeavy : 'light-heavy',
lightLight : 'light-light',
none : 'none',
regular : 'regular',
short : 'short',
tick : 'tick',
beginRepeat: 'begin-repeat',
endRepeat : 'end-repeat',
endBeginRepeat: 'end-begin-repeat'
});// const BarlineType
class Barline {
constructor(barlineType) {
this.barlineType = barlineType;
}// Barline.constructor
}// class Barline
window.onload = function () {
const canvas = document.getElementById('main-canvas-');
const ctx = canvas.getContext('2d');
function drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata) {
const staffSpace = fontSize / 4;
const staffLineThickness = metadata.engravingDefaults.staffLineThickness * staffSpace;
ctx.strokeStyle = staffLineThickness;
ctx.beginPath();
for(let i = 0; i < 5; ++i) {
ctx.moveTo(x, firstLineY - i * staffSpace);
ctx.lineTo(x + staffWidth, firstLineY - i * staffSpace);
}
ctx.stroke();
}// function drawStave
function drawBarline(ctx, barline, fontSize, x, firstLineY, metadata) {
// metadata が不要な計算
const smuflCharacter = {
[BarlineType.dashed ]: SMuFL.Barline.barlineDashed,
[BarlineType.dotted ]: SMuFL.Barline.barlineDotted,
[BarlineType.heavy ]: SMuFL.Barline.barlineHeavy,
[BarlineType.heavyHeavy ]: SMuFL.Barline.barlineHeavyHeavy,
[BarlineType.heavyLight ]: SMuFL.Barline.barlineReverseFinal,
[BarlineType.lightHeavy ]: SMuFL.Barline.barlineFinal,
[BarlineType.lightLight ]: SMuFL.Barline.barlineDouble,
[BarlineType.none ]: null,
[BarlineType.regular ]: SMuFL.Barline.barlineSingle,
[BarlineType.short ]: SMuFL.Barline.barlineShort,
[BarlineType.tick ]: SMuFL.Barline.barlineTick,
[BarlineType.beginRepeat ]: SMuFL.Repeat.repeatLeft,
[BarlineType.endRepeat ]: SMuFL.Repeat.repeatRight,
[BarlineType.endBeginRepeat]: SMuFL.Repeat.repeatRightLeft,
}[barline.barlineType];
if(!smuflCharacter) {
return new AABB(new Point(x, firstLineY), new Size(0, 0));
}
const stavePosition = new StavePosition({index:0});
// metadata を用いて、x = 0, firstLineY = 0, staffSpace = 1 として描画した場合の大きさなどを計算する
const aabb = smuflBBox2aabb(metadata, smuflCharacter.name);
// (x, firstLineY) と staffSpace を指定された場合の大きさなどを計算する
const staffSpace = fontSize / 4;
const finalPosition = new Point(x, firstLineY);
const finalAABB = aabb.scale(staffSpace).translate(finalPosition.asVector());
// 実際に描画する
ctx.fillText(smuflCharacter.code, finalPosition.x, finalPosition.y);
ctx.save();
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.rect(finalAABB.left, finalAABB.top, finalAABB.width, finalAABB.height);
ctx.stroke();
ctx.restore();
return finalAABB;
}// function drawBarline
// フォントファイルの準備
document.fonts.load('10px Bravura').then(() => {
const firstLineY = 115;
const fontSize = 45;
const staffSpace = fontSize / 4;
const metadata = bravuraMetadata;
const fontName = 'Bravura';
ctx.font = `${fontSize}px ${fontName}`;
const padding = 0.4 * staffSpace;// 記号間のスペースを適当に決める
const initialX = 10;
let x = initialX;
const staffWidth = 1240 / 2;
drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata);
// draw barline
x = initialX;
{
for(let type of [BarlineType.dashed, BarlineType.dotted, BarlineType.heavy, BarlineType.heavyHeavy, BarlineType.heavyLight,
BarlineType.lightHeavy, BarlineType.lightLight, BarlineType.none, BarlineType.regular, BarlineType.short, BarlineType.tick,
BarlineType.beginRepeat, BarlineType.endRepeat, BarlineType.endBeginRepeat]) {
const barline = new Barline(type);
const barlineAABB = drawBarline(ctx, barline, fontSize, x + padding, firstLineY, metadata);
x = barlineAABB.right + padding;
}
}
});// document.fonts.load
};// function window.onload
</script>
</head>
<body>
<div>
<canvas id='main-canvas-' width='640px' height='180px' style='border:solid'></canvas>
</div>
</body>
</html>
結果:
休符の描画
付点を考慮する必要はあるものの、基本的にはグリフをひとつ描画するだけである.
ソースコード
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>SMuFL: Engraving Demo</title>
<style>
@font-face {
font-family: 'Bravura';
src: url('../fonts/Bravura.otf');
}
</style>
<script src='../fonts/bravura_metadata.js'></script>
<script src='./util/utility-functions.js'></script>
<script src='./util/smufl-characters.js'></script>
<script src='./util/plane-geometry.js'></script>
<script src='./util/stave-position.js'></script>
<script type='text/javascript'>
function smuflBBox2aabb(metadata, characterName) {
// (x, y) = (0, 0)、staffSpace = 1 (fontSize = 4) であるときの AABB を取得する
const bb = metadata.glyphBBoxes[characterName];
const sw = bb.bBoxSW;
const ne = bb.bBoxNE;
const left = sw[0];
const top = -ne[1];
const right = ne[0];
const bottom = -sw[1];
return new AABB(new Point(left, top), new Size(right - left, bottom - top));
}// function smuflBBox2aabb
const NoteValueType = Object.freeze({
maxima : -3,
longa : -2,
breve : -1,
semibreve : 0,// 全音符
minim : 1,// 二分音符
crotchet : 2,// 四分音符
quaver : 3,// 八分音符
semiquaver : 4,
demisemiquaver : 5,
hemidemisemiquaver: 6,
});// const NoteValueType
const NoteValueTypeArray = Object.freeze({
'-3': 'maxima' ,
'-2': 'longa' ,
'-1': 'breve' ,
0 : 'semibreve' ,// 全音符
1 : 'minim' ,// 二分音符
2 : 'crotchet' ,// 四分音符
3 : 'quaver' ,// 八分音符
4 : 'semiquaver' ,
5 : 'demisemiquaver' ,
6 : 'hemidemisemiquaver',
});// const NoteValueTypeArray
class Tuplet {
constructor(n) {
this.actual = n;
this.normal = 1 << Fs.log2(n);
}
toString() {
return `${this.actual}:${KeyModeArray[this.normal]}`;
}
}// class Tuplet
class NoteValue {
constructor(type, dots, tuplet) {
if(!tuplet) tuplet = new Tuplet(1);
const baseLength = type>= 0? new Rational(1, 1 << type)
: /* else */ new Rational(1 << -typeAsInt);
const d = 1 << dots;
const n = (d << 1) - 1;
const ratioByDots = new Rational(n, d);
const ratioByTuplet = new Rational(tuplet.actual, tuplet.normal);
this.type = type;
this.dots = dots;
this.tuplet = tuplet;
this.length = baseLength.mul(ratioByDots).mul(ratioByTuplet);
}
toString() {
return `NoteValue[type=${NoteValueTypeArray[this.type]},dots=${this.dots},tuplet=${this.tuplet},length=${this.length}]`;
}
}// class NoteValue
class Rest {
constructor(noteValue) {
this.noteValue = noteValue;
}// Rest.constructor
}// class Rest
window.onload = function () {
const canvas = document.getElementById('main-canvas-');
const ctx = canvas.getContext('2d');
function drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata) {
const staffSpace = fontSize / 4;
const staffLineThickness = metadata.engravingDefaults.staffLineThickness * staffSpace;
ctx.strokeStyle = staffLineThickness;
ctx.beginPath();
for(let i = 0; i < 5; ++i) {
ctx.moveTo(x, firstLineY - i * staffSpace);
ctx.lineTo(x + staffWidth, firstLineY - i * staffSpace);
}
ctx.stroke();
}// function drawStave
function drawRest(ctx, rest, fontSize, x, firstLineY, metadata) {
// metadata が不要な計算
const { noteValue } = rest;// tuplet はとりあえず考慮しないでやってみる
const { dots } = noteValue;
let stavePosition = new StavePosition({ index: 4 });// なぜだか知らないが、Bravura の restWhole は描画位置が異なるので、その場合 switch 文内で設定する. 他のフォントでもそうなのだろうか?
let smuflCharacter = null;
switch(noteValue.type) {
case NoteValueType.maxima : smuflCharacter = SMuFL.Rest.restMaxima ; break;
case NoteValueType.longa : smuflCharacter = SMuFL.Rest.restLonga ; break;
case NoteValueType.breve : smuflCharacter = SMuFL.Rest.restDoubleWhole; break;
case NoteValueType.semibreve : smuflCharacter = SMuFL.Rest.restWhole ; stavePosition = new StavePosition({ index: 6 }); break;
case NoteValueType.minim : smuflCharacter = SMuFL.Rest.restHalf ; break;
case NoteValueType.crotchet : smuflCharacter = SMuFL.Rest.restQuarter ; break;
case NoteValueType.quaver : smuflCharacter = SMuFL.Rest.rest8th ; break;
case NoteValueType.semiquaver : smuflCharacter = SMuFL.Rest.rest16th ; break;
case NoteValueType.demisemiquaver : smuflCharacter = SMuFL.Rest.rest32nd ; break;
case NoteValueType.hemidemisemiquaver: smuflCharacter = SMuFL.Rest.rest64th ; break;
default : throw new Error('Unknown noteValue.type: ', noteValue.type);
}
// metadata を用いて、x = 0, firstLineY = 0, staffSpace = 1 として描画した場合の大きさなどを計算する
const position = new Point(0, -stavePosition.index/2);
let aabb = smuflBBox2aabb(metadata, smuflCharacter.name).translate(position.asVector());
const dotXs = [];
const dotY = new StavePosition({index: 5});// 第三間
const dotPadding = 0.2;// 付点の左パディング
for(let i = 0; i < dots; ++i) {
const tx = aabb.right + dotPadding;
const ty = -dotY.index / 2;
const newAABB = smuflBBox2aabb(metadata, SMuFL.IndividualNote.augmentationDot.name).translate(new Vector(tx, ty));
aabb = aabb.or(newAABB);
dotXs.push(tx);
}
// (x, firstLineY) と staffSpace を指定された場合の大きさなどを計算する
const staffSpace = fontSize / 4;
const basePosition = new Point(x, firstLineY);
const finalPosition = position.scale(staffSpace).translate(basePosition.asVector());
const finalAABB = aabb.scale(staffSpace).translate(basePosition.asVector());
const finalDotY = firstLineY - dotY.index / 2 * staffSpace;
const finalDotXs = [];
for(let i = 0; i < dots; ++i) {
const dotX = dotXs[i];
finalDotXs.push(finalPosition.x + dotX * staffSpace);
}
// 実際に描画する
ctx.fillText(smuflCharacter.code, finalPosition.x, finalPosition.y);
for(let i = 0; i < dots; ++i) {
ctx.fillText(SMuFL.IndividualNote.augmentationDot.code, finalDotXs[i], finalDotY);
}
ctx.save();
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.rect(finalAABB.left, finalAABB.top, finalAABB.width, finalAABB.height);
ctx.stroke();
ctx.restore();
return finalAABB;
}// function drawRest
// フォントファイルの準備
document.fonts.load('10px Bravura').then(() => {
const firstLineY = 115;
const fontSize = 45;
const staffSpace = fontSize / 4;
const metadata = bravuraMetadata;
const fontName = 'Bravura';
ctx.font = `${fontSize}px ${fontName}`;
const padding = 0.4 * staffSpace;// 記号間のスペースを適当に決める
const initialX = 10;
let x = initialX;
const staffWidth = 1240 / 2;
drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata);
// draw rest
x = initialX;
for(let noteValueType of [NoteValueType.semibreve, NoteValueType.minim, NoteValueType.crotchet, NoteValueType.quaver, NoteValueType.semiquaver, NoteValueType.demisemiquaver, NoteValueType.hemidemisemiquaver]) {
const rest = new Rest(new NoteValue(noteValueType, 0));
const restAABB = drawRest(ctx, rest, fontSize, x + padding, firstLineY, metadata);
x = restAABB.right + padding;
}
for(let dots = 0; dots < 4; ++dots) {
const rest = new Rest(new NoteValue(NoteValueType.crotchet, dots));
const restAABB = drawRest(ctx, rest, fontSize, x + padding, firstLineY, metadata);
x = restAABB.right + padding;
}
for(let dots = 0; dots < 4; ++dots) {
const rest = new Rest(new NoteValue(NoteValueType.quaver, dots));
const restAABB = drawRest(ctx, rest, fontSize, x + padding, firstLineY, metadata);
x = restAABB.right + padding;
}
for(let dots = 0; dots < 4; ++dots) {
const rest = new Rest(new NoteValue(NoteValueType.semiquaver, dots));
const restAABB = drawRest(ctx, rest, fontSize, x + padding, firstLineY, metadata);
x = restAABB.right + padding;
}
});// document.fonts.load
};// function window.onload
</script>
</head>
<body>
<div>
<canvas id='main-canvas-' width='640px' height='180px' style='border:solid'></canvas>
</div>
</body>
</html>
結果:
音符(含:臨時記号)の描画
単体の音符
音符の描画には以下の要素が関係する:
- 臨時記号
- 符頭
- 符幹
- 符尾
- 付点
- 加線
臨時記号をここに置くかどうかは書き方によるだろうが、楽譜の内部表現としてはどのような臨時記号を表示するかではなく、その音の高さを保持するのが自然だと思われる.
音の高さが与えられているとき、表示される臨時記号の決定には現在の調号と、現在の小節内に現れた臨時記号の情報が必要である.
音の高さが与えられているとき、音符をどの高さに描くかを決定するには、現在の音部記号の情報が必要である.
参考: ドキュメントの第3章5節 Example of glyph registration for notes with flags
ソースコード
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>SMuFL: Engraving Demo</title>
<style>
@font-face {
font-family: 'Bravura';
src: url('../fonts/Bravura.otf');
}
</style>
<script src='../fonts/bravura_metadata.js'></script>
<script src='./util/utility-functions.js'></script>
<script src='./util/smufl-characters.js'></script>
<script src='./util/plane-geometry.js'></script>
<script src='./util/stave-position.js'></script>
<script type='text/javascript'>
function smuflBBox2aabb(metadata, characterName) {
// (x, y) = (0, 0)、staffSpace = 1 (fontSize = 4) であるときの AABB を取得する
const bb = metadata.glyphBBoxes[characterName];
const sw = bb.bBoxSW;
const ne = bb.bBoxNE;
const left = sw[0];
const top = -ne[1];
const right = ne[0];
const bottom = -sw[1];
return new AABB(new Point(left, top), new Size(right - left, bottom - top));
}// function smuflBBox2aabb
const ClefType = Object.freeze({
G: 'G',
C: 'C',
F: 'F'
// Percussion も定義した方がいいかも?
});// const ClefType
class Clef {
constructor(clefType, lineAt) {
this.clefType = clefType;
this.lineAt = lineAt;
this.stavePosition = new StavePosition({
prefix: StavePositionPrefix.center,
position: lineAt,
onLine: true
});
}// Clef.constructor
}// class Clef
const NaturalTone = Object.freeze({
C: 0,
D: 1,
E: 2,
F: 3,
G: 4,
A: 5,
B: 6,
});// const NaturalTone
const NaturalToneArray = Object.freeze([
'C',
'D',
'E',
'F',
'G',
'A',
'B',
]);// const NaturalToneArray
class Tone {
constructor({ naturalTone, alter, fifths }) {
if(fifths == undefined) {
fifths = Tone.toFifths(naturalTone, alter);
} else {
naturalTone = Fs.floorDivMod(fifths * 4, 7).r;
alter = Fs.floorDivMod(fifths + 1, 7).q;
}
this.naturalTone = naturalTone;
this.fifths = fifths;
this.alter = alter;
}
toString() {
const name = NaturalToneArray[this.naturalTone];
let alter = '';
if(this.alter < 0) {
alter = 'b'.repeat(-this.alter);
} else if(this.alter > 0) {
alter = 'x'.repeat(this.alter/2);
if(this.alter&1) alter += '#'
}
return name + alter;
}
static toFifths(naturalTone, alter) {
const x = Fs.naturalToneToFifths(naturalTone, 7);
const y = alter * 7;
return x + y;
}
}// class Tone
const KeyMode = Object.freeze({
ionian : 0,
dorian : 1,
phrigian : 2,
lydian : 3,
mixolydian: 4,
aeolian : 5,
locrian : 6,
major : 0,
minor : 5,
});// const KeyMode
const KeyModeArray = Object.freeze([
'ionian' ,
'dorian' ,
'phrigian' ,
'lydian' ,
'mixolydian',
'aeolian' ,
'locrian' ,
]);// const KeyModeArray
class KeySignature {
constructor(tonic, mode) {
const modeFifths = Fs.naturalToneToFifths(mode);
if(tonic.fifths < modeFifths-7 || modeFifths+7 < tonic.fifths) {
throw new Error(`strange key: tonic=${tonic}, mode=${mode}`);
}
this.tonic = tonic;
this.mode = mode;
const s = tonic.fifths - modeFifths - 1;
const alters = new Array(7);
for(let i = 0; i < 7; ++i) {
const fifths = i + s;
const t = new Tone({ fifths: fifths });
alters[t.naturalTone] = t.alter;
}// loop for i in [0..7)
this.alters = Object.freeze(alters);
}
toString() {
return `${this.tonic} ${KeyModeArray[this.mode]}`;
}
parallelKey(newMode) {
return new KeySignature(this.tonic, newMode);
}
relativeKey(newMode) {
const oldModeFifths = Tone.toFifths(this.mode, 0);
const newModeFifths = Tone.toFifths(newMode, 0);
const newFifths = this.tonic.fifths - oldModeFifths + newModeFifths;
return new KeySignature(new Tone({ fifths: newFifths }), newMode);
}
}// KeySignature
class Pitch {
constructor(tone, octave) {
this.tone = tone;
this.octave = octave;
this.diatonicNumber = tone.naturalTone + (octave + 1) * 7;// noteNumber に倣って、幹音を表現する値を定義する
}
static fromDiatonicNumber(diatonicNumber) {
const fdm = Fs.floorDivMod(diatonicNumber, 7);
const tone = new Tone({ naturalTone: fdm.r, alter: 0 });
const octave = fdm.q - 1;
return new Pitch(tone, octave);
}
}// class Pitch
const NoteValueType = Object.freeze({
maxima : -3,
longa : -2,
breve : -1,
semibreve : 0,// 全音符
minim : 1,// 二分音符
crotchet : 2,// 四分音符
quaver : 3,// 八分音符
semiquaver : 4,
demisemiquaver : 5,
hemidemisemiquaver: 6,
});// const NoteValueType
const NoteValueTypeArray = Object.freeze({
'-3': 'maxima' ,
'-2': 'longa' ,
'-1': 'breve' ,
0 : 'semibreve' ,// 全音符
1 : 'minim' ,// 二分音符
2 : 'crotchet' ,// 四分音符
3 : 'quaver' ,// 八分音符
4 : 'semiquaver' ,
5 : 'demisemiquaver' ,
6 : 'hemidemisemiquaver',
});// const NoteValueTypeArray
class Tuplet {
constructor(n) {
this.actual = n;
this.normal = 1 << Fs.log2(n);
}
toString() {
return `${this.actual}:${KeyModeArray[this.normal]}`;
}
}// class Tuplet
class NoteValue {
constructor(type, dots, tuplet) {
if(!tuplet) tuplet = new Tuplet(1);
const baseLength = type>= 0? new Rational(1, 1 << type)
: /* else */ new Rational(1 << -typeAsInt);
const d = 1 << dots;
const n = (d << 1) - 1;
const ratioByDots = new Rational(n, d);
const ratioByTuplet = new Rational(tuplet.actual, tuplet.normal);
this.type = type;
this.dots = dots;
this.tuplet = tuplet;
this.length = baseLength.mul(ratioByDots).mul(ratioByTuplet);
}
toString() {
return `NoteValue[type=${NoteValueTypeArray[this.type]},dots=${this.dots},tuplet=${this.tuplet},length=${this.length}]`;
}
}// class NoteValue
class Note {
constructor(pitch, noteValue) {
this.pitch = pitch;
this.noteValue = noteValue;
}// Note.constructor
}// class Note
window.onload = function () {
function drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata) {
[省略]
}// function drawStave
function drawClef(ctx, clef, fontSize, x, firstLineY, metadata) {
[省略]
}// function drawClef
function drawKeySignature(ctx, keySignature, currentClef, fontSize, x, firstLineY, metadata) {
[省略]
}// function drawKeySignature
function drawNote(ctx, note, currentClef, currentKey, currentAccidentals, fontSize, x, firstLineY, metadata) {
// 現在の音部記号から、第一線の音を特定する
const basePosition = new StavePosition({prefix:'center', position:currentClef.lineAt, onLine:true});
let basePitch = null;
if(currentClef.clefType === ClefType.F) {
basePitch = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 3);
} else if(currentClef.clefType === ClefType.C) {
basePitch = new Pitch(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), 4);
} else if(currentClef.clefType === ClefType.G) {
basePitch = new Pitch(new Tone({ naturalTone: NaturalTone.G, alter: 0 }), 4);
} else {
throw new Error('Unknown clef type: ' + currentClef.type);
}
// basePosition corresponds basePitch
const pitchAtFirstLine = Pitch.fromDiatonicNumber(basePitch.diatonicNumber - basePosition.index);
// metadata が不要な計算
const { alters } = currentKey;
const { pitch, noteValue } = note;
const stavePosition = new StavePosition({ index: pitch.diatonicNumber - pitchAtFirstLine.diatonicNumber });
const stemUp = stavePosition.index < 4;// 符頭が第三線以下の場合、符幹は上向きとする. できれば同じ連桁に含まれる音符や、隣接する音符を考慮するべきではある.
const flagCount = Math.max(noteValue.type - NoteValueType.crotchet, 0);
let flag = null;
if(flagCount > 0) {
if(stemUp) {
switch(flagCount) {
case 1: flag = SMuFL.Flag.flag8thUp ; break;
case 2: flag = SMuFL.Flag.flag16thUp ; break;
case 3: flag = SMuFL.Flag.flag32ndUp ; break;
case 4: flag = SMuFL.Flag.flag64thUp ; break;
case 5: flag = SMuFL.Flag.flag128thUp ; break;
case 6: flag = SMuFL.Flag.flag256thUp ; break;
case 7: flag = SMuFL.Flag.flag512thUp ; break;
case 8: flag = SMuFL.Flag.flag1024thUp; break;
}
} else {
switch(flagCount) {
case 1: flag = SMuFL.Flag.flag8thDown ; break;
case 2: flag = SMuFL.Flag.flag16thDown ; break;
case 3: flag = SMuFL.Flag.flag32ndDown ; break;
case 4: flag = SMuFL.Flag.flag64thDown ; break;
case 5: flag = SMuFL.Flag.flag128thDown ; break;
case 6: flag = SMuFL.Flag.flag256thDown ; break;
case 7: flag = SMuFL.Flag.flag512thDown ; break;
case 8: flag = SMuFL.Flag.flag1024thDown; break;
}
}
}
let notehead = SMuFL.Notehead.noteheadNull;
if(noteValue.type == NoteValueType.breve) {
notehead = SMuFL.Notehead.noteheadDoubleWhole;
} else if(noteValue.type == NoteValueType.semibreve) {
notehead = SMuFL.Notehead.noteheadWhole;
} else if(noteValue.type == NoteValueType.minim) {
notehead = SMuFL.Notehead.noteheadHalf;
} else if(noteValue.type >= NoteValueType.crotchet) {
notehead = SMuFL.Notehead.noteheadBlack;
}
let accidental = null;
const getAccidentalGlyph = (alter) => {
switch(alter) {
case 3: return SMuFL.StandardAccidental.accidentalTripleSharp;
case 2: return SMuFL.StandardAccidental.accidentalDoubleSharp;
case 1: return SMuFL.StandardAccidental.accidentalSharp;
case 0: return SMuFL.StandardAccidental.accidentalNatural;
case -1: return SMuFL.StandardAccidental.accidentalFlat;
case -2: return SMuFL.StandardAccidental.accidentalDoubleFlat;
case -3: return SMuFL.StandardAccidental.accidentalTripleFlat;
default: throw new Error('Not supported alter: ' + alter);
}
};
if(currentAccidentals[pitch.tone.naturalTone]?.[pitch.octave] !== undefined) {
console.log(`[DEBUG] currentAccidentals[pitch.tone.naturalTone][pitch.octave] !== undefined`, currentAccidentals[pitch.tone.naturalTone][pitch.octave]);
// 現在の位置に臨時記号が存在する場合
if(currentAccidentals[pitch.tone.naturalTone][pitch.octave] !== pitch.tone.alter) {
console.log(`[DEBUG] currentAccidentals[pitch.tone.naturalTone][pitch.octave] = ${currentAccidentals[pitch.tone.naturalTone][pitch.octave]} !== pitch.tone.alter = ${pitch.tone.alter}`);
// で、かつ存在する臨時記号とこの音符が必要とする記号が相異なる場合
// 臨時記号を設定する
accidental = getAccidentalGlyph(pitch.tone.alter);
currentAccidentals[pitch.tone.naturalTone][pitch.octave] = pitch.tone.alter;
} else {
console.log(`[DEBUG] currentAccidentals[pitch.tone.naturalTone][pitch.octave] = ${currentAccidentals[pitch.tone.naturalTone][pitch.octave]} === pitch.tone.alter = ${pitch.tone.alter}`);
}
} else {
// 現在の位置に臨時記号が存在しない場合
if(currentKey.alters[pitch.tone.naturalTone] !== pitch.tone.alter) {
// で、かつ調号の記号とこの音符が必要とする記号が相異なる場合
// 臨時記号を設定する
accidental = getAccidentalGlyph(pitch.tone.alter);
if(currentAccidentals[pitch.tone.naturalTone] === undefined) {
currentAccidentals[pitch.tone.naturalTone] = { };
}
currentAccidentals[pitch.tone.naturalTone][pitch.octave] = pitch.tone.alter;
}
}
const hasStem = (noteValue.type - NoteValueType.minim) >= 0;
const { dots } = noteValue;
const ledgerCount = stavePosition.index <= -2? Math.floor(-stavePosition.index / 2):// 下第一線以下である場合の加線
10 <= stavePosition.index? Math.floor((stavePosition.index - 8) / 2):// 上第一線以上である場合の加線
0;// 五線に収まる場合は加線不要
const hasUpperLedger = 10 <= stavePosition.index;// 加線がある場合、五線の上側か?
// metadata を用いて、x = 0, firstLineY = 0, staffSpace = 1 として描画した場合の大きさなどを計算する
let tx = 0;// (tx,ty) は最終的に符頭の描画位置となる
const ty = -stavePosition.index/2;
let aabb = new AABB(new Point(tx, ty), new Size(0, 0));
// -- 臨時記号
let accidentalPosition = null;
if(accidental !== null) {
accidentalPosition = new Point(tx, ty);
const padding = 0.4;// 臨時記号と符頭の間の広さ
const accidentalAABB = smuflBBox2aabb(metadata, accidental.name).translate(new Vector(tx, ty));
aabb = aabb.or(accidentalAABB);
tx = aabb.right + padding;
}
// -- 符頭
const noteheadPosition = new Point(tx, ty);
const anchors = metadata.glyphsWithAnchors[notehead.name];
const noteheadAABB = smuflBBox2aabb(metadata, notehead.name).translate(new Vector(tx, ty));
aabb = aabb.or(noteheadAABB);
// -- 符幹
let stemRectangle = null;// Rectangle 型は用意していないので、AABB 型を使用する
if(hasStem) {
const { stemThickness } = metadata.engravingDefaults;
if(stemUp) {
// 符幹が上向きのとき
const { stemUpSE } = anchors;
const stemTop = Math.min(ty - 3.5, -2);// オクターブ上まで伸ばす. 低くとも第三線(y = -2)であるようにする.
const stemBottom = ty - stemUpSE[1];
const stemRight = tx + stemUpSE[0];
const stemLeft = stemRight - stemThickness;
const width = stemRight - stemLeft;
const height = stemBottom - stemTop;
stemRectangle = new AABB(new Point(stemLeft, stemTop), new Size(width, height));
} else {
// 符幹が下向きのとき
const { stemDownNW } = anchors;
const stemTop = ty - stemDownNW[1];
const stemBottom = Math.max(ty + 3.5, -2);// オクターブ下まで伸ばす. 高くとも第三線(y = -2)であるようにする.
const stemLeft = tx + stemDownNW[0];
const stemRight = stemLeft + stemThickness;
const width = stemRight - stemLeft;
const height = stemBottom - stemTop;
stemRectangle = new AABB(new Point(stemLeft, stemTop), new Size(width, height));
}
aabb = aabb.or(stemRectangle);
}
// -- 符尾
let flagPosition = null;
if(flag !== null) {
/* Bravura ではなぜか stemUpNW/stemDownSW が必要ない
*/
if(stemUp) {
// 符幹が上向きのとき
const { stemUpNW } = metadata.glyphsWithAnchors[flag.name];// いちおう取得だけしてみる
flagPosition = new Point(stemRectangle.left, stemRectangle.top);
} else {
// 符幹が下向きのとき
const { stemDownSW } = metadata.glyphsWithAnchors[flag.name];
flagPosition = new Point(stemRectangle.left, stemRectangle.bottom);
}
const flagAABB = smuflBBox2aabb(metadata, flag.name).translate(flagPosition.asVector());
aabb = aabb.or(flagAABB);
}
// -- 付点
const dotXs = [];
const dotY = stavePosition.onLine? new StavePosition({index: stavePosition.index + 1}) : stavePosition;// 音符の位置、またはその上の線間
const dotPadding = 0.2;// 付点の左パディング
let dotsAABB = new AABB(new Point(noteheadAABB.right, -dotY.index / 2), new Size(0, 0));// 付点は符頭の右端から考え始める. 符尾の右端から始めるべきなのだろうか?
for(let i = 0; i < dots; ++i) {
const tx = dotsAABB.right + dotPadding;
const ty = -dotY.index / 2;
dotsAABB = smuflBBox2aabb(metadata, SMuFL.IndividualNote.augmentationDot.name).translate(new Vector(tx, ty));
dotXs.push(tx);
}
aabb = aabb.or(dotsAABB);
// -- 加線
const { legerLineThickness, legerLineExtension } = metadata.engravingDefaults;
const ledgerLeftX = noteheadAABB.left - legerLineExtension / 2;
const ledgerStavePositions = [];
const stavePositionPrefix = hasUpperLedger? StavePositionPrefix.upper : StavePositionPrefix.lower;
const ledgerSize = new Size(noteheadAABB.width + legerLineExtension, legerLineThickness);
for(let i = 1; i <= ledgerCount; ++i) {
const stavePosition = new StavePosition({ prefix: stavePositionPrefix, position: i, onLine: true });
ledgerStavePositions.push(stavePosition);
const ledgerCentreY = -stavePosition.index / 2;
const ledgerAABB = new AABB(new Point(ledgerLeftX, ledgerCentreY - legerLineThickness/2), ledgerSize);
aabb = aabb.or(ledgerAABB);
}// loop for i in [1 .. ledgerCount]
// (x, firstLineY) と staffSpace を指定された場合の大きさなどを計算する
const staffSpace = fontSize / 4;
const originPosition = new Point(x, firstLineY);
const finalAABB = aabb.scale(staffSpace).translate(originPosition.asVector());
// -- 臨時記号
let finalAccidentalPosition = null;
if(accidental !== null) {
finalAccidentalPosition = accidentalPosition.scale(staffSpace).translate(originPosition.asVector());
}
// -- 符頭
const finalNoteheadPosition = noteheadPosition.scale(staffSpace).translate(originPosition.asVector());
// -- 符幹
let finalStemRectangle = null;
if(hasStem) {
finalStemRectangle = stemRectangle.scale(staffSpace).translate(originPosition.asVector());
}
// -- 符尾
let finalFlagPosition = null;
if(flag !== null) {
finalFlagPosition = flagPosition.scale(staffSpace).translate(originPosition.asVector());
}
// -- 付点
const finalDotXs = [];
const finalDotY = firstLineY - dotY.index / 2 * staffSpace;
for(let i = 0; i < dots; ++i) {
const dotX = dotXs[i];
finalDotXs.push(x + dotX * staffSpace);
}
// -- 加線
const finalLedgerLeftX = x + ledgerLeftX * staffSpace;
const finalLedgerTopYs = [];
const finalLedgerSize = new Size(ledgerSize.width * staffSpace, ledgerSize.height * staffSpace);
for(let ledgerStavePosition of ledgerStavePositions) {
const pos = -ledgerStavePosition.index / 2;
finalLedgerTopYs.push(firstLineY + (pos - legerLineThickness/2) * staffSpace);
}// loop for i in [1 .. ledgerCount]
// 実際に描画する
// -- 臨時記号
if(accidental !== null) {
ctx.fillText(accidental.code, finalAccidentalPosition.x, finalAccidentalPosition.y);
}
// -- 符頭
ctx.fillText(notehead.code, finalNoteheadPosition.x, finalNoteheadPosition.y);
// -- 符幹
if(hasStem) {
ctx.beginPath();
ctx.rect(finalStemRectangle.left, finalStemRectangle.top, finalStemRectangle.width, finalStemRectangle.height);
ctx.fill();
}
// -- 符尾
if(flag !== null) {
ctx.fillText(flag.code, finalFlagPosition.x, finalFlagPosition.y);
}
// -- 付点
if(dots > 0) {
for(let i = 0; i < dots; ++i) {
ctx.fillText(SMuFL.IndividualNote.augmentationDot.code, finalDotXs[i], finalDotY);
}
}
// -- 加線
for(let ledgerTopY of finalLedgerTopYs) {
ctx.beginPath();
ctx.rect(finalLedgerLeftX, ledgerTopY, finalLedgerSize.width, finalLedgerSize.height);
ctx.fill();
}// loop for i in [1 .. ledgerCount]
ctx.save();
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.rect(finalAABB.left, finalAABB.top, finalAABB.width, finalAABB.height);
ctx.stroke();
ctx.restore();
return finalAABB;
}// function drawNote
// フォントファイルの準備
document.fonts.load('10px Bravura').then(() => {
const canvas0 = document.getElementById('main-canvas-0');
const ctx0 = canvas0.getContext('2d');
const canvas1 = document.getElementById('main-canvas-1');
const ctx1 = canvas1.getContext('2d');
const canvas2 = document.getElementById('main-canvas-2');
const ctx2 = canvas2.getContext('2d');
const firstLineY = 115;
const fontSize = 45;
const staffSpace = fontSize / 4;
const metadata = bravuraMetadata;
const fontName = 'Bravura';
ctx0.font = `${fontSize}px ${fontName}`;
ctx1.font = `${fontSize}px ${fontName}`;
ctx2.font = `${fontSize}px ${fontName}`;
const padding = 0.4 * staffSpace;// 記号間のスペースを適当に決める
const initialX = 10;
let x = initialX;
const staffWidth = 1240 / 2;
drawStave(ctx0, staffWidth, fontSize, x, firstLineY, metadata);
drawStave(ctx1, staffWidth, fontSize, x, firstLineY, metadata);
drawStave(ctx2, staffWidth, fontSize, x, firstLineY, metadata);
// draw note (色々な音価)
x = initialX;
{
const currentClef = new Clef(ClefType.G, 2);// 調号/音符の描画位置を決定するには、音部記号が必要
const clefAABB = drawClef(ctx0, currentClef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
const currentKey = new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), KeyMode.ionian);// 音符に付く臨時記号を決定するには、調号が必要
const keySignatureAABB = drawKeySignature(ctx0, currentKey, currentClef, fontSize, x + padding, firstLineY, metadata);
x = keySignatureAABB.right + padding;
const currentAccidentals = { };// 音符に付く臨時記号を決定するには、同じ小節内で現れた臨時記号の情報が必要 ; Dictionary<NaturalTone, Dictionary<int/*octave*/, int/*alter*/>>
// Dictionary<int/*diatonicNumber*/, int/*alter*/> の方がスマートだろうか?
let diatonicNumber = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4).diatonicNumber;// E4 (トレブル記号の場合第一線)
for(let noteValueType of [NoteValueType.semibreve, NoteValueType.minim, NoteValueType.crotchet, NoteValueType.quaver, NoteValueType.semiquaver, NoteValueType.demisemiquaver, NoteValueType.hemidemisemiquaver]) {
const pitch = Pitch.fromDiatonicNumber(diatonicNumber);
const note = new Note(pitch, new NoteValue(noteValueType, 0));
const noteAABB = drawNote(ctx0, note, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = noteAABB.right + padding;
++diatonicNumber;
}
for(let dots = 0; dots <= 3; ++dots) {
const pitch = Pitch.fromDiatonicNumber(diatonicNumber);
const note = new Note(pitch, new NoteValue(NoteValueType.crotchet, dots));
const noteAABB = drawNote(ctx0, note, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = noteAABB.right + padding;
++diatonicNumber;
}
}
// draw note (たくさんの加線)
x = initialX;
{
const currentClef = new Clef(ClefType.F, 4);// 調号/音符の描画位置を決定するには、音部記号が必要
const clefAABB = drawClef(ctx1, currentClef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
const currentKey = new KeySignature(new Tone({ naturalTone: NaturalTone.A, alter: 0 }), KeyMode.aeolian);// 音符に付く臨時記号を決定するには、調号が必要
const keySignatureAABB = drawKeySignature(ctx1, currentKey, currentClef, fontSize, x + padding, firstLineY, metadata);
x = keySignatureAABB.right + padding;
const currentAccidentals = { };// 音符に付く臨時記号を決定するには、同じ小節内で現れた臨時記号の情報が必要 ; Dictionary<NaturalTone, Dictionary<int/*octave*/, int/*alter*/>>
let diatonicNumber = new Pitch(new Tone({ naturalTone: NaturalTone.A, alter: 0 }), 3).diatonicNumber;// A3 (バス記号の場合第五線)
for(let i = 0; i < 7; ++i) {
const pitch = Pitch.fromDiatonicNumber(diatonicNumber);
const note = new Note(pitch, new NoteValue(NoteValueType.crotchet, 1));
const noteAABB = drawNote(ctx1, note, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = noteAABB.right + padding;
++diatonicNumber;
}
diatonicNumber = new Pitch(new Tone({ naturalTone: NaturalTone.G, alter: 0 }), 2).diatonicNumber;// G2 (バス記号の場合第一線)
for(let i = 0; i < 7; ++i) {
const pitch = Pitch.fromDiatonicNumber(diatonicNumber);
const note = new Note(pitch, new NoteValue(NoteValueType.crotchet, 1));
const noteAABB = drawNote(ctx1, note, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = noteAABB.right + padding;
--diatonicNumber;
}
}
// draw note (色々な臨時記号)
x = initialX;
{
const currentClef = new Clef(ClefType.G, 2);// 調号/音符の描画位置を決定するには、音部記号が必要
const clefAABB = drawClef(ctx2, currentClef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
const currentKey = new KeySignature(new Tone({ naturalTone: NaturalTone.A, alter: -1 }), KeyMode.ionian);// 音符に付く臨時記号を決定するには、調号が必要
const keySignatureAABB = drawKeySignature(ctx2, currentKey, currentClef, fontSize, x + padding, firstLineY, metadata);
x = keySignatureAABB.right + padding;
const currentAccidentals = { };// 音符に付く臨時記号を決定するには、同じ小節内で現れた臨時記号の情報が必要 ; Dictionary<NaturalTone, Dictionary<int/*octave*/, int/*alter*/>>
{
const pitch = new Pitch(new Tone({ naturalTone: NaturalTone.B, alter: -1 }), 4);// 調号でフラットが付いているので、この音を描画する際には臨時記号を書かなくてよい
const note = new Note(pitch, new NoteValue(NoteValueType.crotchet, 0));
const noteAABB = drawNote(ctx2, note, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = noteAABB.right + padding;
}
for(let alter = -3; alter <= 3; ++alter) {
const pitch = new Pitch(new Tone({ naturalTone: NaturalTone.B, alter: alter }), 4);
const note = new Note(pitch, new NoteValue(NoteValueType.crotchet, 0));
const noteAABB = drawNote(ctx2, note, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = noteAABB.right + padding;
}
for(let i = 0; i < 3; ++i) {// 同じ臨時記号を持つ音符が同じ小節内にあるなら、最初のものにだけ臨時記号を書く
const pitch = new Pitch(new Tone({ naturalTone: NaturalTone.B, alter: 1 }), 4);
const note = new Note(pitch, new NoteValue(NoteValueType.crotchet, 0));
const noteAABB = drawNote(ctx2, note, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = noteAABB.right + padding;
}
}
});// document.fonts.load
};// function window.onload
</script>
</head>
<body>
<div>
<p>canvas-0: 音符 a (色々な音価)</p>
<canvas id='main-canvas-0' width='640px' height='180px' style='border:solid'></canvas>
</div>
<div>
<p>canvas-1: 音符 b (たくさんの加線)</p>
<canvas id='main-canvas-1' width='640px' height='180px' style='border:solid'></canvas>
</div>
<div>
<p>canvas-2: 音符 c (色々な臨時記号)</p>
<canvas id='main-canvas-2' width='640px' height='180px' style='border:solid'></canvas>
</div>
</body>
</html>
結果:
タイ/スラー
タイ/スラーについては、engravingDefaults 内で端点と中央の太さがそれぞれ定義されている.
曲線を描くということで真っ先に挙がる候補はベジェ曲線だと思うのだが、太さが途中で変化するとなると難しそうに感じてしまう.
曲線上の3点(端点 p0 = B(0), p1 = B(1) と曲線上の点 q = B(t))と、媒介変数 t を決定すれば制御点(c0)を求められるため、
端点(B(0), B(1))と、中央(B(1/2))を決定し、それぞれ指定された太さだけずらした曲線を用いて描画するものとする.
元とする曲線の決定方法は二種類実装してみた. この例ではあまり違いがないような気もする.
参考: ベジェ曲線入門
drawNote 関数の返り値として、全体のAABBだけでなく、五線上の垂直位置と符頭のAABBも追加している.
ソースコード
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>SMuFL: Engraving Demo</title>
<style>
@font-face {
font-family: 'Bravura';
src: url('../fonts/Bravura.otf');
}
</style>
<script src='../fonts/bravura_metadata.js'></script>
<script src='./util/utility-functions.js'></script>
<script src='./util/smufl-characters.js'></script>
<script src='./util/plane-geometry.js'></script>
<script src='./util/stave-position.js'></script>
<script type='text/javascript'>
function smuflBBox2aabb(metadata, characterName) {
// (x, y) = (0, 0)、staffSpace = 1 (fontSize = 4) であるときの AABB を取得する
const bb = metadata.glyphBBoxes[characterName];
const sw = bb.bBoxSW;
const ne = bb.bBoxNE;
const left = sw[0];
const top = -ne[1];
const right = ne[0];
const bottom = -sw[1];
return new AABB(new Point(left, top), new Size(right - left, bottom - top));
}// function smuflBBox2aabb
const ClefType = Object.freeze({
[省略]
});// const ClefType
class Clef {
[省略]
}// class Clef
const NaturalTone = Object.freeze({
[省略]
});// const NaturalTone
const NaturalToneArray = Object.freeze([
[省略]
]);// const NaturalToneArray
class Tone {
[省略]
}// class Tone
const KeyMode = Object.freeze({
[省略]
});// const KeyMode
const KeyModeArray = Object.freeze([
[省略]
]);// const KeyModeArray
class KeySignature {
[省略]
}// KeySignature
class Pitch {
[省略]
}// class Pitch
const NoteValueType = Object.freeze({
[省略]
});// const NoteValueType
const NoteValueTypeArray = Object.freeze({
[省略]
});// const NoteValueTypeArray
class Tuplet {
[省略]
}// class Tuplet
class NoteValue {
[省略]
}// class NoteValue
class Note {
[省略]
}// class Note
window.onload = function () {
function drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata) {
[省略]
}// function drawStave
function drawClef(ctx, clef, fontSize, x, firstLineY, metadata) {
[省略]
}// function drawClef
function drawKeySignature(ctx, keySignature, currentClef, fontSize, x, firstLineY, metadata) {
[省略]
}// function drawKeySignature
function drawNote(ctx, note, currentClef, currentKey, currentAccidentals, fontSize, x, firstLineY, metadata) {
[省略]
// 単体で描画する例とは返り値を変えている点に注意
return { aabb: finalAABB, noteheadAABB: finalNoteheadPosition, stavePosition: stavePosition };
}// function drawNote
function getTangentAndPointOnQuadraticBezier(p0, c0, p1, t) {
// result.q := B(t)
// result.tangent := B(t) における接線ベクトル(正規化済み)
const v0 = p0.vectorTo(c0);
const v1 = c0.vectorTo(p1);
const a = p0.translate(v0.scale(t));
const b = c0.translate(v1.scale(t));
const v2 = a.vectorTo(b);
return {
q: a.translate(v2.scale(t)),
tangent: v2.normalise(),
};
}// function getTangentAndPointOnQuadraticBezier
function getControlPointForQuadraticBezierThroughThreePoints(p0, q, p1, t) {
const s = 1 - t;
const numx = q.x - (t*t * p0.x + s*s * p1.x);// numerator for x
const numy = q.y - (t*t * p0.y + s*s * p1.y);// numerator for y
const den = 2 * t * s;// denominator
const x = numx / den;
const y = numy / den;
return new Point(x, y);
}// function getControlPointForQuadraticBezierThroughThreePoints
function getRootOfLinearEquation(a, b) {
// solve ax + b = 0
return -b / a;
}
function getAABB(p0, c0, p1) {
// ベジェ曲線の aabb を求める
const rx = getRootOfLinearEquation(p0.x - 2*c0.x + p1.x, c0.x - p0.x);
const ry = getRootOfLinearEquation(p0.y - 2*c0.y + p1.y, c0.y - p0.y);
const v0 = p0.vectorTo(c0);
const v1 = c0.vectorTo(p1);
let left = Math.min(p0.x, p1.x);
let top = Math.min(p0.y, p1.y);
let right = Math.max(p0.x, p1.x);
let bottom = Math.max(p0.y, p1.y);
if(0 <= rx && rx <= 1) {
const a = p0.translate(v0.scale(rx));
const b = c0.translate(v1.scale(rx));
const v2 = a.vectorTo(b);
const q = a.translate(v2.scale(rx));
const qx = (1-rx)*(1-rx)*p0.x + 2*rx*(1-rx)*c0.x + rx*rx*p1.x;
left = Math.min(qx, left );
right = Math.max(qx, right );
}
if(0 <= ry && ry <= 1) {
const a = p0.translate(v0.scale(ry));
const b = c0.translate(v1.scale(ry));
const v2 = a.vectorTo(b);
const q = a.translate(v2.scale(ry));
const qy = (1-ry)*(1-ry)*p0.y + 2*ry*(1-ry)*c0.y + ry*ry*p1.y;
top = Math.min(qy, top );
bottom = Math.max(qy, bottom);
}
return new AABB(new Point(left, top), new Size(right - left, bottom - top));
}// function getAABB
function drawGradualOffsetQuadraticBezier(ctx, p0, c0, p1, edgeThickness, centreThickness) {
const t = 1/2;
const { tangent, q } = getTangentAndPointOnQuadraticBezier(p0, c0, p1, t);
const v0 = p0.vectorTo(c0).normalise();
const v1 = c0.vectorTo(p1).normalise();
const tangent0 = tangent.rotateByDegree(90);
const v00 = v0.rotateByDegree(90);
const v10 = v1.rotateByDegree(90);
const p00 = p0.translate(v00.scale(edgeThickness));
const q0 = q .translate(tangent0.scale(centreThickness));
const p10 = p1.translate(v10.scale(edgeThickness));
const c00 = getControlPointForQuadraticBezierThroughThreePoints(p00, q0, p10, t);
const tangent1 = tangent0.scale(-1);
const v01 = v10.scale(-1);
const v11 = v00.scale(-1);
const p01 = p1.translate(v01.scale(edgeThickness));
const q1 = q .translate(tangent1.scale(centreThickness));
const p11 = p0.translate(v11.scale(edgeThickness));
const c01 = getControlPointForQuadraticBezierThroughThreePoints(p01, q1, p11, t);
ctx.beginPath();
ctx.moveTo(p00.x, p00.y);
ctx.quadraticCurveTo(c00.x, c00.y, p10.x, p10.y);
ctx.lineTo(p01.x, p01.y);
ctx.quadraticCurveTo(c01.x, c01.y, p11.x, p11.y);
ctx.lineTo(p00.x, p00.y);
ctx.fill();
const aabb0 = getAABB(p00, c00, p10);
const aabb1 = getAABB(p01, c01, p11);
return aabb0.or(aabb1);
}// function gradualOffsetQuadraticBezier
function drawTieSlur0(ctx, noteInfo0, noteInfo1, downCurve, fontSize, metadata) {
// noteInfoX は、drawNote の返り値
// downCurve は、タイ/スラーが下向きであるかを表す真理値
// 制御点 c0 を求める関数
function getC0(p0, q, p1, t) {
const s = 1 - t;
const numx = q.x - (t*t * p0.x + s*s * p1.x);// numerator for x
const numy = q.y - (t*t * p0.y + s*s * p1.y);// numerator for y
const den = 2 * t * s;// denominator
const x = numx / den;
const y = numy / den;
return new Point(x, y);
}// function getC0
const staffSpace = fontSize / 4;
const p0x = noteInfo0.aabb.left + noteInfo0.aabb.width / 2;
const p1x = noteInfo1.aabb.left + noteInfo1.aabb.width / 2;
const p0y = downCurve? noteInfo0.aabb.bottom + staffSpace/2 : noteInfo0.aabb.top - staffSpace/2;
const p1y = downCurve? noteInfo1.aabb.bottom + staffSpace/2 : noteInfo1.aabb.top - staffSpace/2;
const p0 = new Point(p0x, p0y);// 曲線の端点(noteInfo0 側)
const p1 = new Point(p1x, p1y);// 曲線の端点(noteInfo1 側)
const qx = (p0x + p1x) / 2;
const qy = downCurve? (Math.max(p0y, p1y) + staffSpace/2) : (Math.min(p0y, p1y) - staffSpace/2);// q: 曲線上の点
const q = new Point(qx, qy);// 曲線上の点
const c0 = getC0(p0, q, p1, 1/2);
const isTie = noteInfo0.stavePosition.index === noteInfo1.stavePosition.index;// 同音スラーなどのことは考慮しない
const { tieEndpointThickness , tieMidpointThickness } = metadata.engravingDefaults;
const { slurEndpointThickness, slurMidpointThickness } = metadata.engravingDefaults;
const endPointThickness = isTie? tieEndpointThickness : slurEndpointThickness;
const midpointThickness = isTie? tieMidpointThickness : slurMidpointThickness;
const edgeThickness = endPointThickness * staffSpace;
const centreThickness = midpointThickness * staffSpace;
const aabb = drawGradualOffsetQuadraticBezier(ctx, p0, c0, p1, edgeThickness, centreThickness);
// ついでに AABB も描く
ctx.save();
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.rect(aabb.left, aabb.top, aabb.width, aabb.height);
ctx.stroke();
ctx.restore();
return aabb;
}// function drawTieSlur0
function drawTieSlur1(ctx, noteInfo0, noteInfo1, downCurve, fontSize, metadata) {
// noteInfoX は、drawNote の返り値
// downCurve は、タイ/スラーが下向きであるかを表す真理値
// 制御点 c0 を求める関数
function getC0(p0, p1, qy) {
const c0x = (p0.x + p1.x) / 2;
const k = p0.y * p1.y - p0.y * qy - p1.y * qy + qy * qy;
let c0y;
if(downCurve) {
c0y = qy + Math.sqrt(k);
} else /* if(!downCurve) */ {
c0y = qy - Math.sqrt(k);
}
return new Point(c0x, c0y);
}// function getC0
const staffSpace = fontSize / 4;
const p0x = noteInfo0.aabb.left + noteInfo0.aabb.width / 2;
const p1x = noteInfo1.aabb.left + noteInfo1.aabb.width / 2;
const p0y = downCurve? noteInfo0.aabb.bottom + staffSpace/2 : noteInfo0.aabb.top - staffSpace/2;
const p1y = downCurve? noteInfo1.aabb.bottom + staffSpace/2 : noteInfo1.aabb.top - staffSpace/2;
const p0 = new Point(p0x, p0y);// 曲線の端点(noteInfo0 側)
const p1 = new Point(p1x, p1y);// 曲線の端点(noteInfo1 側)
const qy = downCurve? (Math.max(p0y, p1y) + staffSpace/2) : (Math.min(p0y, p1y) - staffSpace/2);// q: 曲線上の点
const c0 = getC0(p0, p1, qy);
const isTie = noteInfo0.stavePosition.index === noteInfo1.stavePosition.index;// 同音スラーなどのことは考慮しない
const { tieEndpointThickness , tieMidpointThickness } = metadata.engravingDefaults;
const { slurEndpointThickness, slurMidpointThickness } = metadata.engravingDefaults;
const endPointThickness = isTie? tieEndpointThickness : slurEndpointThickness;
const midpointThickness = isTie? tieMidpointThickness : slurMidpointThickness;
const edgeThickness = endPointThickness * staffSpace;
const centreThickness = midpointThickness * staffSpace;
const aabb = drawGradualOffsetQuadraticBezier(ctx, p0, c0, p1, edgeThickness, centreThickness);
// ついでに AABB も描く
ctx.save();
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.rect(aabb.left, aabb.top, aabb.width, aabb.height);
ctx.stroke();
ctx.restore();
return aabb;
}// function drawTieSlur1
// フォントファイルの準備
document.fonts.load('10px Bravura').then(() => {
const canvas0 = document.getElementById('main-canvas-0');
const ctx0 = canvas0.getContext('2d');
const canvas1 = document.getElementById('main-canvas-1');
const ctx1 = canvas1.getContext('2d');
const firstLineY = 115;
const fontSize = 45;
const staffSpace = fontSize / 4;
const metadata = bravuraMetadata;
const fontName = 'Bravura';
ctx0.font = `${fontSize}px ${fontName}`;
ctx1.font = `${fontSize}px ${fontName}`;
const padding = 0.4 * staffSpace;// 記号間のスペースを適当に決める
const paddingBetweenNotes = 2 * staffSpace;// 記号間のスペースを適当に決める
const initialX = 10;
let x = initialX;
const staffWidth = 1240 / 2;
drawStave(ctx0, staffWidth, fontSize, x, firstLineY, metadata);
drawStave(ctx1, staffWidth, fontSize, x, firstLineY, metadata);
// draw tie/slur (方法 0)
x = initialX;
{
const currentClef = new Clef(ClefType.G, 2);// 調号/音符の描画位置を決定するには、音部記号が必要
const clefAABB = drawClef(ctx0, currentClef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
const currentKey = new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), KeyMode.ionian);// 音符に付く臨時記号を決定するには、調号が必要
const keySignatureAABB = drawKeySignature(ctx0, currentKey, currentClef, fontSize, x + padding, firstLineY, metadata);
x = keySignatureAABB.right + padding;
const currentAccidentals = { };// 音符に付く臨時記号を決定するには、同じ小節内で現れた臨時記号の情報が必要 ; Dictionary<NaturalTone, Dictionary<int/*octave*/, int/*alter*/>>
// Dictionary<int/*diatonicNumber*/, int/*alter*/> の方がスマートだろうか?
// 下向きの曲線
{// タイ
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.crotchet, 0));
const result0 = drawNote(ctx0, note0, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = result0.aabb.right + paddingBetweenNotes;
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.crotchet, 0));
const result1 = drawNote(ctx0, note1, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
drawTieSlur0(ctx0, result0, result1, true, fontSize, metadata);
x = result1.aabb.right + paddingBetweenNotes;
}
{// スラー
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.crotchet, 0));
const result0 = drawNote(ctx0, note0, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = result0.aabb.right + paddingBetweenNotes;
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.A, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.crotchet, 0));
const result1 = drawNote(ctx0, note1, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
drawTieSlur0(ctx0, result0, result1, true, fontSize, metadata);
x = result1.aabb.right + paddingBetweenNotes;
}
{// スラー
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.A, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.crotchet, 0));
const result0 = drawNote(ctx0, note0, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = result0.aabb.right + paddingBetweenNotes;
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.crotchet, 0));
const result1 = drawNote(ctx0, note1, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
drawTieSlur0(ctx0, result0, result1, true, fontSize, metadata);
x = result1.aabb.right + paddingBetweenNotes;
}
// 上向きの曲線
{// タイ
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 5);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.crotchet, 0));
const result0 = drawNote(ctx0, note0, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = result0.aabb.right + paddingBetweenNotes;
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 5);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.crotchet, 0));
const result1 = drawNote(ctx0, note1, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
drawTieSlur0(ctx0, result0, result1, false, fontSize, metadata);
x = result1.aabb.right + paddingBetweenNotes;
}
{// スラー
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 5);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.crotchet, 0));
const result0 = drawNote(ctx0, note0, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = result0.aabb.right + paddingBetweenNotes;
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), 5);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.crotchet, 0));
const result1 = drawNote(ctx0, note1, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
drawTieSlur0(ctx0, result0, result1, false, fontSize, metadata);
x = result1.aabb.right + paddingBetweenNotes;
}
{// スラー
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), 5);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.crotchet, 0));
const result0 = drawNote(ctx0, note0, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = result0.aabb.right + paddingBetweenNotes;
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 5);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.crotchet, 0));
const result1 = drawNote(ctx0, note1, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
drawTieSlur0(ctx0, result0, result1, false, fontSize, metadata);
x = result1.aabb.right + paddingBetweenNotes;
}
}
// draw tie/slur (方法 1)
x = initialX;
{
const currentClef = new Clef(ClefType.G, 2);// 調号/音符の描画位置を決定するには、音部記号が必要
const clefAABB = drawClef(ctx1, currentClef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
const currentKey = new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), KeyMode.ionian);// 音符に付く臨時記号を決定するには、調号が必要
const keySignatureAABB = drawKeySignature(ctx1, currentKey, currentClef, fontSize, x + padding, firstLineY, metadata);
x = keySignatureAABB.right + padding;
const currentAccidentals = { };// 音符に付く臨時記号を決定するには、同じ小節内で現れた臨時記号の情報が必要 ; Dictionary<NaturalTone, Dictionary<int/*octave*/, int/*alter*/>>
// Dictionary<int/*diatonicNumber*/, int/*alter*/> の方がスマートだろうか?
// 下向きの曲線
{// タイ
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.crotchet, 0));
const result0 = drawNote(ctx1, note0, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = result0.aabb.right + paddingBetweenNotes;
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.crotchet, 0));
const result1 = drawNote(ctx1, note1, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
drawTieSlur1(ctx1, result0, result1, true, fontSize, metadata);
x = result1.aabb.right + paddingBetweenNotes;
}
{// スラー
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.crotchet, 0));
const result0 = drawNote(ctx1, note0, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = result0.aabb.right + paddingBetweenNotes;
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.A, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.crotchet, 0));
const result1 = drawNote(ctx1, note1, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
drawTieSlur1(ctx1, result0, result1, true, fontSize, metadata);
x = result1.aabb.right + paddingBetweenNotes;
}
{// スラー
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.A, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.crotchet, 0));
const result0 = drawNote(ctx1, note0, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = result0.aabb.right + paddingBetweenNotes;
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.crotchet, 0));
const result1 = drawNote(ctx1, note1, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
drawTieSlur1(ctx1, result0, result1, true, fontSize, metadata);
x = result1.aabb.right + paddingBetweenNotes;
}
// 上向きの曲線
{// タイ
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 5);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.crotchet, 0));
const result0 = drawNote(ctx1, note0, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = result0.aabb.right + paddingBetweenNotes;
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 5);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.crotchet, 0));
const result1 = drawNote(ctx1, note1, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
drawTieSlur1(ctx1, result0, result1, false, fontSize, metadata);
x = result1.aabb.right + paddingBetweenNotes;
}
{// スラー
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 5);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.crotchet, 0));
const result0 = drawNote(ctx1, note0, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = result0.aabb.right + paddingBetweenNotes;
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), 5);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.crotchet, 0));
const result1 = drawNote(ctx1, note1, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
drawTieSlur1(ctx1, result0, result1, false, fontSize, metadata);
x = result1.aabb.right + paddingBetweenNotes;
}
{// スラー
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), 5);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.crotchet, 0));
const result0 = drawNote(ctx1, note0, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
x = result0.aabb.right + paddingBetweenNotes;
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 5);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.crotchet, 0));
const result1 = drawNote(ctx1, note1, currentClef, currentKey, currentAccidentals, fontSize, x + padding, firstLineY, metadata);
drawTieSlur1(ctx1, result0, result1, false, fontSize, metadata);
x = result1.aabb.right + paddingBetweenNotes;
}
}
});// document.fonts.load
};// function window.onload
</script>
</head>
<body>
<div>
<p>canvas-0: タイ/スラー (方法 0)</p>
<canvas id='main-canvas-0' width='640px' height='180px' style='border:solid'></canvas>
</div>
<div>
<p>canvas-1: タイ/スラー (方法 1)</p>
<canvas id='main-canvas-1' width='640px' height='180px' style='border:solid'></canvas>
</div>
</body>
</html>
結果:
連桁
連桁を描画するためには、両端にある音符の符幹がどこまで伸びているのか(y 座標)と、それぞれの符幹の x 座標が決定されていなければならない.
その都合、単体で描画していたときには一息で行っていた処理をいくつかに分割し、途中で横棒のための計算処理を挟み込むようにしている.
従って、上で挙げたソースコード例とは大分違う書き方になってしまった.
ソースコード
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<title>SMuFL: Engraving Demo</title>
<style>
@font-face {
font-family: 'Bravura';
src: url('../fonts/Bravura.otf');
}
</style>
<script src='../fonts/bravura_metadata.js'></script>
<script src='./util/utility-functions.js'></script>
<script src='./util/smufl-characters.js'></script>
<script src='./util/plane-geometry.js'></script>
<script src='./util/stave-position.js'></script>
<script type='text/javascript'>
function smuflBBox2aabb(metadata, characterName) {
[省略]
}// function smuflBBox2aabb
const ClefType = Object.freeze({
[省略]
});// const ClefType
class Clef {
[省略]
}// class Clef
const NaturalTone = Object.freeze({
[省略]
});// const NaturalTone
const NaturalToneArray = Object.freeze([
[省略]
]);// const NaturalToneArray
class Tone {
[省略]
}// class Tone
const KeyMode = Object.freeze({
[省略]
});// const KeyMode
const KeyModeArray = Object.freeze([
[省略]
]);// const KeyModeArray
class KeySignature {
[省略]
}// KeySignature
class Pitch {
[省略]
}// class Pitch
const NoteValueType = Object.freeze({
[省略]
});// const NoteValueType
const NoteValueTypeArray = Object.freeze({
[省略]
});// const NoteValueTypeArray
class Tuplet {
[省略]
}// class Tuplet
class NoteValue {
[省略]
}// class NoteValue
class Note {
[省略]
}// class Note
window.onload = function () {
function drawStave(ctx, staffWidth, fontSize, x, firstLineY, metadata) {
[省略]
}// function drawStave
function drawClef(ctx, clef, fontSize, x, firstLineY, metadata) {
[省略]
}// function drawClef
function drawKeySignature(ctx, keySignature, currentClef, fontSize, x, firstLineY, metadata) {
[省略]
}// function drawKeySignature
// -- ここから -----------------------------------------
// 連桁でまとめられた音符の描画用処理
// ----------------------------------------- ここから --
function notes2graphicalNotes(notes, currentClef, currentKey, currentAccidentals) {
// 現在の音部記号から、第一線の音を特定する
const basePosition = new StavePosition({prefix:'center', position:currentClef.lineAt, onLine:true});
let basePitch = null;
if(currentClef.clefType === ClefType.F) {
basePitch = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 3);
} else if(currentClef.clefType === ClefType.C) {
basePitch = new Pitch(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), 4);
} else if(currentClef.clefType === ClefType.G) {
basePitch = new Pitch(new Tone({ naturalTone: NaturalTone.G, alter: 0 }), 4);
} else {
throw new Error('Unknown clef type: ' + currentClef.type);
}
// basePosition corresponds basePitch
const pitchAtFirstLine = Pitch.fromDiatonicNumber(basePitch.diatonicNumber - basePosition.index);
// metadata が不要な計算
const { alters } = currentKey;
const graphicalNotes = [];
for(let note of notes) {
const { pitch, noteValue } = note;
// -- 垂直位置
const stavePosition = new StavePosition({ index: pitch.diatonicNumber - pitchAtFirstLine.diatonicNumber });
// -- 臨時記号
let accidental = null;
const getAccidentalGlyph = (alter) => {
switch(alter) {
case 3: return SMuFL.StandardAccidental.accidentalTripleSharp;
case 2: return SMuFL.StandardAccidental.accidentalDoubleSharp;
case 1: return SMuFL.StandardAccidental.accidentalSharp;
case 0: return SMuFL.StandardAccidental.accidentalNatural;
case -1: return SMuFL.StandardAccidental.accidentalFlat;
case -2: return SMuFL.StandardAccidental.accidentalDoubleFlat;
case -3: return SMuFL.StandardAccidental.accidentalTripleFlat;
default: throw new Error('Not supported alter: ' + alter);
}
};
if(currentAccidentals[pitch.tone.naturalTone]?.[pitch.octave] !== undefined) {
// 現在の位置に臨時記号が存在する場合
if(currentAccidentals[pitch.tone.naturalTone][pitch.octave] !== pitch.tone.alter) {
// で、かつ存在する臨時記号とこの音符が必要とする記号が相異なる場合
// 臨時記号を設定する
accidental = getAccidentalGlyph(pitch.tone.alter);
currentAccidentals[pitch.tone.naturalTone][pitch.octave] = pitch.tone.alter;
}
} else {
// 現在の位置に臨時記号が存在しない場合
if(currentKey.alters[pitch.tone.naturalTone] !== pitch.tone.alter) {
// で、かつ調号の記号とこの音符が必要とする記号が相異なる場合
// 臨時記号を設定する
accidental = getAccidentalGlyph(pitch.tone.alter);
if(currentAccidentals[pitch.tone.naturalTone] === undefined) {
currentAccidentals[pitch.tone.naturalTone] = { };
}
currentAccidentals[pitch.tone.naturalTone][pitch.octave] = pitch.tone.alter;
}
}
// -- 符頭
let notehead = SMuFL.Notehead.noteheadNull;
if(noteValue.type == NoteValueType.breve) {
notehead = SMuFL.Notehead.noteheadDoubleWhole;
} else if(noteValue.type == NoteValueType.semibreve) {
notehead = SMuFL.Notehead.noteheadWhole;
} else if(noteValue.type == NoteValueType.minim) {
notehead = SMuFL.Notehead.noteheadHalf;
} else if(noteValue.type >= NoteValueType.crotchet) {
notehead = SMuFL.Notehead.noteheadBlack;
}
// -- 符幹 ; 今回は連桁が書かれることを前提としているため、hasStem は固定値. stemUp は後で計算する.
const hasStem = true;
// -- 符尾 ; 今回は連桁を描くため、flag のためのグリフは求めない
const flagCount = Math.max(noteValue.type - NoteValueType.crotchet, 0);
// -- 付点
const { dots } = noteValue;
// -- 加線
const ledgerCount = stavePosition.index <= -2? Math.floor(-stavePosition.index / 2):// 下第一線以下である場合の加線
10 <= stavePosition.index? Math.floor((stavePosition.index - 8) / 2):// 上第一線以上である場合の加線
0;// 五線に収まる場合は加線不要
const hasUpperLedger = 10 <= stavePosition.index;// 加線がある場合、五線の上側か?
graphicalNotes.push({
note: note,
stavePosition: stavePosition,
accidental: accidental,
notehead: notehead,
hasStem: hasStem,
flagCount: flagCount,
dots: dots,
ledgerCount: ledgerCount,
hasUpperLedger: hasUpperLedger,
});
}// loop for note in notes
const thirdLineIndex = 4;// 第三線の index. new StavePosition({prefix: StavePositionPrefix.center, position: 3, onLine: true}).index
const furthestNotePositionIndexFromThirdLine = graphicalNotes[graphicalNotes.map(val => {
return Math.abs(val.stavePosition.index - thirdLineIndex);
}).reduce((acc, val, idx) => acc.val < val? { val: val, idx: idx } : acc, { val: -1, idx: 0 }).idx].stavePosition.index;
const stemUp = furthestNotePositionIndexFromThirdLine < thirdLineIndex;// 第三線から最も遠い音符によって決定する. 符頭が第三線以下の場合、符幹は上向きとする.
// すべて第三線にある場合や、同じ距離で第三線の上と下にそれぞれ音符がある場合も考慮すべきではあるが、いまはこれでよしとする.
// graphicalBeamGroup みたいな扱いで値を返す
return {
graphicalNotes: graphicalNotes,
stemUp: stemUp,
};
}// function notes2graphicalNotes
function graphicalNotes2layoutNotes(graphicalNotes, stemUp, fontSize, metadata) {
const staffSpace = fontSize / 4;
const layoutNotes = new Array();
for(let graphicalNote of graphicalNotes) {
const {
note,
stavePosition,
accidental,
notehead,
hasStem,
flagCount,
dots,
ledgerCount,
hasUpperLedger,
} = graphicalNote;
// metadata を用いて、x = 0, firstLineY = 0, staffSpace = 1 (fontSize = 4) で描画した場合の大きさなどを計算する
let tx = 0;// (tx,ty) は最終的に符頭の描画位置となる
const ty = -stavePosition.index/2;
let aabb = new AABB(new Point(tx, ty), new Size(0, 0));
// -- 臨時記号
let accidentalPosition = null;
if(accidental !== null) {
accidentalPosition = new Point(tx, ty);
const padding = 0.4;// 臨時記号と符頭の間の広さ
const accidentalAABB = smuflBBox2aabb(metadata, accidental.name).translate(new Vector(tx, ty));
aabb = aabb.or(accidentalAABB);
tx = aabb.right + padding;
}
// -- 符頭
const noteheadPosition = new Point(tx, ty);
const anchors = metadata.glyphsWithAnchors[notehead.name];
const noteheadAABB = smuflBBox2aabb(metadata, notehead.name).translate(new Vector(tx, ty));
aabb = aabb.or(noteheadAABB);
// -- 符幹
let stemRectangle = null;// Rectangle 型は用意していないので、AABB 型を使用する
if(hasStem) {// 今回は連桁なので、hasStem = true で固定されている
const { stemThickness } = metadata.engravingDefaults;
if(stemUp) {
// 符幹が上向きのとき
const { stemUpSE } = anchors;
const stemTop = Math.min(ty - 3.5, -2);// オクターブ上まで伸ばす. 低くとも第三線(y = -2)であるようにする.
const stemBottom = ty - stemUpSE[1];
const stemRight = tx + stemUpSE[0];
const stemLeft = stemRight - stemThickness;
const width = stemRight - stemLeft;
const height = stemBottom - stemTop;
stemRectangle = new AABB(new Point(stemLeft, stemTop), new Size(width, height));
} else {
// 符幹が下向きのとき
const { stemDownNW } = anchors;
const stemTop = ty - stemDownNW[1];
const stemBottom = Math.max(ty + 3.5, -2);// オクターブ下まで伸ばす. 高くとも第三線(y = -2)であるようにする.
const stemLeft = tx + stemDownNW[0];
const stemRight = stemLeft + stemThickness;
const width = stemRight - stemLeft;
const height = stemBottom - stemTop;
stemRectangle = new AABB(new Point(stemLeft, stemTop), new Size(width, height));
}
aabb = aabb.or(stemRectangle);
}
// -- 符尾 ; 今回は連桁を描くため、符尾については考えないけれど、連桁のために用意しなければならない高さを求める
const { beamThickness, beamSpacing } = metadata.engravingDefaults;
const stemLengthThatBeamsConsume = beamThickness * flagCount + beamSpacing * (flagCount - 1);
// -- 付点
const dotXs = [];
const dotY = stavePosition.onLine? new StavePosition({index: stavePosition.index + 1}) : stavePosition;// 音符の位置、またはその上の線間
const dotPadding = 0.2;// 付点の左パディング
let dotsAABB = new AABB(new Point(noteheadAABB.right, -dotY.index / 2), new Size(0, 0));// 付点は符頭の右端から考え始める. 符尾の右端から始めるべきなのだろうか?
for(let i = 0; i < dots; ++i) {
const tx = dotsAABB.right + dotPadding;
const ty = -dotY.index / 2;
dotsAABB = smuflBBox2aabb(metadata, SMuFL.IndividualNote.augmentationDot.name).translate(new Vector(tx, ty));
dotXs.push(tx);
}
aabb = aabb.or(dotsAABB);
// -- 加線
const { legerLineThickness, legerLineExtension } = metadata.engravingDefaults;
const ledgerLeftX = noteheadAABB.left - legerLineExtension / 2;
const ledgerStavePositions = [];
const stavePositionPrefix = hasUpperLedger? StavePositionPrefix.upper : StavePositionPrefix.lower;
const ledgerSize = new Size(noteheadAABB.width + legerLineExtension, legerLineThickness);
const ledgerAABBs = [];
for(let i = 1; i <= ledgerCount; ++i) {
const stavePosition = new StavePosition({ prefix: stavePositionPrefix, position: i, onLine: true });
ledgerStavePositions.push(stavePosition);
const ledgerCentreY = -stavePosition.index / 2;
const ledgerAABB = new AABB(new Point(ledgerLeftX, ledgerCentreY - legerLineThickness/2), ledgerSize);
aabb = aabb.or(ledgerAABB);
ledgerAABBs.push(ledgerAABB);
}// loop for i in [1 .. ledgerCount]
// staffSpace に合わせてスケールを調整する
layoutNotes.push({
graphicalNote: graphicalNote,
aabb: aabb.scale(staffSpace),
// -- 臨時記号
accidentalPosition: accidentalPosition?.scale(staffSpace),
// -- 符頭
noteheadPosition: noteheadPosition.scale(staffSpace),
noteheadAABB: noteheadAABB.scale(staffSpace),
// -- 符幹
stemRectangle: stemRectangle.scale(staffSpace),
// -- 符尾 ; 今回は連桁を描くため、符尾については考えないけれど、連桁のために用意しなければならない高さを用意する
stemLengthThatBeamsConsume: stemLengthThatBeamsConsume * staffSpace,
// -- 付点
dotXs: dotXs.map(x => x * staffSpace),
dotY: dotY * staffSpace,
// -- 加線
ledgerAABBs: ledgerAABBs.map(bb => bb.scale(staffSpace)),
});
}// loop for graphicalNote in graphicalNotes
return layoutNotes;
}// function graphicalNotes2layoutNotes
function getBeamInfo(flagCounts) {
const n = flagCounts.length;
// -- 初期値を使用することはないのだけれれど、何となく false や 0 で埋めておく
const isBegins = new Array(n).fill(false);
const edgeCounts = new Array(n).fill(0);
const isForwardHooks = new Array(n).fill(false);
const hookCounts = new Array(n).fill(0);
const continueCounts = new Array(n).fill(0);
let prevFlagCount = 0;
let curFlagCount = flagCounts[0];
let nextFlagCount = flagCounts[1];// n が 2 未満であるケースは想定しない
let cur = 0;
let maxBeamLevel = Math.max(curFlagCount, nextFlagCount);
while(true) {
const isBegin = prevFlagCount === 0 || (prevFlagCount < curFlagCount && curFlagCount <= nextFlagCount);
const isForwardHook = isBegin;
const continueCount = Math.min(prevFlagCount, Math.min(curFlagCount, nextFlagCount));
const edgeCount = Math.min(curFlagCount, isBegin? nextFlagCount : prevFlagCount) - continueCount;
const hookCount = curFlagCount - edgeCount - continueCount;
// isForwardHook は isBegin と同じにしているけれど、これで正しいのかよくわからない
isBegins[cur] = isBegin;
edgeCounts[cur] = edgeCount;
isForwardHooks[cur] = isForwardHook;
hookCounts[cur] = hookCount;
continueCounts[cur] = continueCount;
if(++cur === n) break;
prevFlagCount = curFlagCount;
curFlagCount = nextFlagCount;
nextFlagCount = cur+1 < n? flagCounts[cur + 1] : 0;
maxBeamLevel = Math.max(nextFlagCount, maxBeamLevel);
}
// beams/hooks が計算の最終的な出力
const beams = new Array(n);// beams[beginStemIndex] = [ { beamLevel, endStemIndex } ]
const hooks = new Array(n);// hooks[stemIndex] = [ { beamLevel, isForwardHook } ]
const tmp = new Array(n);// tmp[beamLevel] = beginStemIndex
for(let i = 0; i < n; ++i) {
beams[i] = new Array();
hooks[i] = new Array();
for(let j = 0; j < edgeCounts[i]; ++j) {
const beamLevel = j + continueCounts[i];
if(isBegins[i]) {
tmp[beamLevel] = i;
} else {
const beginStemIndex = tmp[beamLevel];
beams[beginStemIndex].push({ beamLevel: beamLevel, endStemIndex: i });
}
}
for(let j = 0; j < hookCounts[i]; ++j) {
const beamLevel = j + continueCounts[i] + edgeCounts[i];
hooks[i].push({ beamLevel: beamLevel, isForwardHook: isForwardHooks[i] });
}
}
return {
beams: beams,
hooks: hooks,
};
}// function getBeamInfo
function adjustStemLength(layoutNotes, stemUp, xs, firstLineY, fontSize) {
const staffSpace = fontSize / 4;
const n = layoutNotes.length;
const flagCounts = layoutNotes.map(ln => ln.graphicalNote).map(gn => gn.flagCount);
const stemLengthsThatBeamsConsume = layoutNotes.map(ln => ln.stemLengthThatBeamsConsume);
// 連桁が水平なのか、広がっていくのか、すぼまっていくのかを決定する
let ascending = true;
let descending = true;
let prevStavePosition = layoutNotes[0].graphicalNote.stavePosition;
for(let curStavePosition of layoutNotes.slice(1).map(ln => ln.graphicalNote).map(gn => gn.stavePosition)) {
if(prevStavePosition.index < curStavePosition.index) descending = false;
else if(prevStavePosition.index > curStavePosition.index) ascending = false;
prevStavePosition = curStavePosition;
}
const ASCENDING_BEAM = 0;
const DESCENDING_BEAM = 1;
const HORIZONTAL_BEAM = 2;
let beamDirection = HORIZONTAL_BEAM;
if(ascending ^ descending) {
if(ascending) beamDirection = ASCENDING_BEAM;
else beamDirection = DESCENDING_BEAM;
}
const requiredStemHeightsForBeams = new Array(n);// 連桁部分が消費する高さ
const minimumStemLength = 2.75;// 連桁部分を除いて必要な最小の符幹長さ(staffSpace = 1) ;; どこかに定数定義した方がいいかも
const minScaledStemLength = minimumStemLength * staffSpace;// 連桁部分を除いて必要な最小の符幹長さ
// 条件分けして、符幹の長さを調整していく
let defaultLeftEdgeRimY;// 左端の、連桁の外側の y 座標
let defaultRightEdgeRimY;// 左端の、連桁の外側の y 座標
if(stemUp) {
if(beamDirection === HORIZONTAL_BEAM) {
// 一番低い音の位置
const furthestNotePositionIndex = layoutNotes.map(ln => ln.graphicalNote).map(gn => gn.stavePosition).map(sp => sp.index).reduce((acc, val) => Math.min(acc, val));
const defaultBeamRimPositionIndex = furthestNotePositionIndex + 7;// デフォルトでは、一番長い符幹の長さが1オクターブ分(index 7つ分)とする
const defaultBeamRimY = (-defaultBeamRimPositionIndex/2 * staffSpace) + firstLineY;
defaultLeftEdgeRimY = defaultRightEdgeRimY = defaultBeamRimY;
} else if(beamDirection === DESCENDING_BEAM) {
const leftestNoteStavePosition = layoutNotes[0].graphicalNote.stavePosition;
const rightestNoteStavePosition = layoutNotes[n-1].graphicalNote.stavePosition;
const tooSteep = Math.abs(leftestNoteStavePosition.index - rightestNoteStavePosition.index) > 3;// 3度より大きく離れた傾きは急すぎるものとする
const defaultBeamLeftEndStavePositionIndex = leftestNoteStavePosition.index;
const defaultBeamRightEndStavePositionIndex = tooSteep? (leftestNoteStavePosition.index - 3) : rightestNoteStavePosition.index;// 急すぎる場合低い方を、高い方-3 まで引き上げる
defaultLeftEdgeRimY = (-defaultBeamLeftEndStavePositionIndex/2 * staffSpace) + firstLineY;
defaultRightEdgeRimY = (-defaultBeamRightEndStavePositionIndex/2 * staffSpace) + firstLineY;
} else /* if(beamDirection === ASCENDING_BEAM) */ {
const leftestNoteStavePosition = layoutNotes[0].graphicalNote.stavePosition;
const rightestNoteStavePosition = layoutNotes[n-1].graphicalNote.stavePosition;
const tooSteep = Math.abs(leftestNoteStavePosition.index - rightestNoteStavePosition.index) > 3;// 3度より大きく離れた傾きは急すぎるものとする
const defaultBeamLeftEndStavePositionIndex = tooSteep? (rightestNoteStavePosition.index - 3) : leftestNoteStavePosition.index;// 急すぎる場合低い方を、高い方-3 まで引き上げる
const defaultBeamRightEndStavePositionIndex = rightestNoteStavePosition.index;
defaultLeftEdgeRimY = (-defaultBeamLeftEndStavePositionIndex/2 * staffSpace) + firstLineY;
defaultRightEdgeRimY = (-defaultBeamRightEndStavePositionIndex/2 * staffSpace) + firstLineY;
}// end switch with beamDirection
} else /* if(stemDown) */ {
if(beamDirection === HORIZONTAL_BEAM) {
// 一番高い音の位置
const furthestNotePositionIndex = layoutNotes.map(ln => ln.graphicalNote).map(gn => gn.stavePosition).map(sp => sp.index).reduce((acc, val) => Math.max(acc, val));
const defaultBeamRimPositionIndex = furthestNotePositionIndex - 7;// デフォルトでは、一番長い符幹の長さが1オクターブ分(index 7つ分)とする
const defaultBeamRimY = (-defaultBeamRimPositionIndex/2 * staffSpace) + firstLineY;
defaultLeftEdgeRimY = defaultRightEdgeRimY = defaultBeamRimY;
} else if(beamDirection === DESCENDING_BEAM) {
const leftestNoteStavePosition = layoutNotes[0].graphicalNote.stavePosition;
const rightestNoteStavePosition = layoutNotes[n-1].graphicalNote.stavePosition;
const tooSteep = Math.abs(leftestNoteStavePosition.index - rightestNoteStavePosition.index) > 3;// 3度より大きく離れた傾きは急すぎるものとする
const defaultBeamLeftEndStavePositionIndex = tooSteep? (rightestNoteStavePosition.index + 3) : leftestNoteStavePosition.index;// 急すぎる場合高い方を、低い方+3 まで押し下げる
const defaultBeamRightEndStavePositionIndex = rightestNoteStavePosition.index;
defaultLeftEdgeRimY = (-defaultBeamLeftEndStavePositionIndex/2 * staffSpace) + firstLineY;
defaultRightEdgeRimY = (-defaultBeamRightEndStavePositionIndex/2 * staffSpace) + firstLineY;
} else /* if(beamDirection === ASCENDING_BEAM) */ {
const leftestNoteStavePosition = layoutNotes[0].graphicalNote.stavePosition;
const rightestNoteStavePosition = layoutNotes[n-1].graphicalNote.stavePosition;
const tooSteep = Math.abs(leftestNoteStavePosition.index - rightestNoteStavePosition.index) > 3;// 3度より大きく離れた傾きは急すぎるものとする
const defaultBeamLeftEndStavePositionIndex = leftestNoteStavePosition.index;
const defaultBeamRightEndStavePositionIndex = tooSteep? (leftestNoteStavePosition.index + 3) : rightestNoteStavePosition.index;// 急すぎる場合高い方を、低い方+3 まで押し下げる
defaultLeftEdgeRimY = (-defaultBeamLeftEndStavePositionIndex/2 * staffSpace) + firstLineY;
defaultRightEdgeRimY = (-defaultBeamRightEndStavePositionIndex/2 * staffSpace) + firstLineY;
}// end switch with beamDirection
}// end if whether stemUp, or not
// layoutNotes[*].stemRectangle は x = 0, firstLineY = 0 に fontSize の指定ありで描画する場合の図形なので、x 座標だけ考慮した図形をここで得る
const movedStemRectangles = layoutNotes.map(ln => ln.stemRectangle).map((rect, index) => rect.translate(new Vector(xs[index], 0)));
const defaultBeamLeftRim = new Point(movedStemRectangles[0].left, defaultLeftEdgeRimY);
const defaultBeamRightRim = new Point(movedStemRectangles[n-1].right, defaultRightEdgeRimY);
const beamWidth = defaultBeamRightRim.x - defaultBeamLeftRim.x;
const beamHeight = defaultBeamRightRim.y - defaultBeamLeftRim.y;
const defaultStemLengths = movedStemRectangles.map(stem => {
if(stemUp) {
const r = (stem.left - defaultBeamLeftRim.x) / beamWidth;// 連桁の左端からどれだけ進んだ所か
const h = beamHeight * r;
const defaultStemEndY = defaultBeamLeftRim.y + h;
const defaultStemHeight = stem.bottom - defaultStemEndY;
return defaultStemHeight;
} else /* if(stemDown) */ {
const r = (stem.right - defaultBeamLeftRim.x) / beamWidth;
const h = beamHeight * r;
const defaultStemEndY = defaultBeamLeftRim.y + h;
const defaultStemHeight = defaultStemEndY - stem.top;
return defaultStemHeight;
}
});
// 追加すべき長さ = 「必要な最小長さ - 単純に計算した場合の長さ」の最大値
const lengthToAdd = layoutNotes.map(ln => ln.stemLengthThatBeamsConsume)
.map((stemLengthThatBeamsConsume, index) => {
// console.log('[adjustStemLength] stemLengthThatBeamsConsume@'+index+': ', stemLengthThatBeamsConsume);
const defaultLength = defaultStemLengths[index];
return (stemLengthThatBeamsConsume + minScaledStemLength) - defaultLength;
}).reduce((acc, val) => Math.max(acc, val));
const finalLengths = defaultStemLengths.map(len => len + lengthToAdd);
// 符幹の長さが決定したので、layoutNotes[*].stemRectangle を更新する
for(let i = 0; i < n; ++i) {
const ln = layoutNotes[i];
const { stemRectangle } = ln;
let left, top, right, bottom;
if(stemUp) {
({ left, right, bottom } = stemRectangle);
top = bottom - finalLengths[i];
} else /* if(stemDown) */ {
({ left, top, right } = stemRectangle);
bottom = top + finalLengths[i];
}
ln.stemRectangle = new AABB(new Point(left, top), new Size(right - left, bottom - top));
}// loop for i in [0..n)
}// function adjustStemLength
// drawNote とは以下の点が異なる
// * note と x がそれぞれ複数になっている
function drawBeamedNotes(ctx, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata) {
// 描画される記号をグリフや基本的な図形の集まりに変換する
const { graphicalNotes, stemUp } = notes2graphicalNotes(notes, currentClef, currentKey, currentAccidentals);
// 記号を単体で書く場合の大きさなどを求める
const layoutNotes = graphicalNotes2layoutNotes(graphicalNotes, stemUp, fontSize, metadata);
// 本来は記号の大きさを求めた後、水平位置や改行位置を計算する必要がある.
// 大変なので、今回は xs を引数で受け取ることで代わりとする.
// 記号の大きさと位置が決定したので、連桁でまとめられた音符について符幹の長さを計算し調整する
// 必要に応じて layoutNotes[*].stemRectangle を書き換える
adjustStemLength(layoutNotes, stemUp, xs, firstLineY, fontSize);
// 連桁の描画に必要な情報を計算する
const beamInfo = getBeamInfo(layoutNotes.map(ln => ln.graphicalNote).map(gn => gn.flagCount));
// 実際に描画する
const n = layoutNotes.length;
const staffSpace = fontSize / 4;
// -- 音符の描画
for(let i = 0; i < n; ++i) {
const {// x = 0, firstLineY = 0 で、fontSize が指定された場合の位置や大きさ
graphicalNote,
aabb,
// -- 臨時記号
accidentalPosition,
// -- 符頭
noteheadPosition,
noteheadAABB,
// -- 符幹
stemRectangle,
// -- 符尾 ; 今回は連桁を描くため、符尾は描かない
// -- 付点
dotXs,
dotY,
// -- 加線
ledgerAABBs,
} = layoutNotes[i];
const {
note,
stavePosition,
accidental,
notehead,
hasStem,
flagCount,
dots,
ledgerCount,
hasUpperLedger,
} = graphicalNote;
const origin = new Vector(xs[i], firstLineY);
// -- 臨時記号
if(accidental !== null) {
const finalAccidentalPosition = accidentalPosition.translate(origin);
ctx.fillText(accidental.code, finalAccidentalPosition.x, finalAccidentalPosition.y);
}
// -- 符頭
const finalNoteheadPosition = noteheadPosition.translate(origin);
ctx.fillText(notehead.code, finalNoteheadPosition.x, finalNoteheadPosition.y);
// -- 符幹
if(hasStem) {
const finalStemRectangle = stemRectangle.translate(origin);
ctx.beginPath();
ctx.rect(finalStemRectangle.left, finalStemRectangle.top, finalStemRectangle.width, finalStemRectangle.height);
ctx.fill();
}
// -- 符尾 ;; 今回は描かない
// -- 付点
if(dots > 0) {
const finalDotXs = dotXs.map(dotX => dotX + origin.x);
const finalDotY = dotY + origin.y;
for(let i = 0; i < dots; ++i) {
ctx.fillText(SMuFL.IndividualNote.augmentationDot.code, finalDotXs[i], finalDotY);
}
}
// -- 加線
for(let ledgerAABB of ledgerAABBs.map(bb => bb.translate(origin))) {
ctx.beginPath();
ctx.rect(ledgerAABB.left, ledgerAABB.top, ledgerAABB.width, ledgerAABB.height);
ctx.fill();
}
const finalAABB = aabb.translate(origin);
ctx.save();
ctx.strokeStyle = 'blue';
ctx.beginPath();
ctx.rect(finalAABB.left, finalAABB.top, finalAABB.width, finalAABB.height);
ctx.stroke();
ctx.restore();
}
// -- 連桁の描画
const { beams, hooks } = beamInfo;
const finalStemRectangles = layoutNotes.map((ln, idx) =>
ln.stemRectangle.translate(new Vector(xs[idx], firstLineY))
);
let { beamThickness, beamSpacing } = metadata.engravingDefaults;
beamThickness *= staffSpace;
beamSpacing *= staffSpace;
const beamVector = finalStemRectangles[0].leftTop.vectorTo(finalStemRectangles[n - 1].leftTop).normalise();
const beamSlant = beamVector.y / beamVector.x;
const ascending = beamSlant < 0;
const descending = beamSlant > 0;
const calculateBeamY = (stem, isLeft, nthBeamY) => {
let rimY = (stemUp ? stem.top : stem.bottom);
if ((stemUp && isLeft ? descending : ascending) || (!stemUp && isLeft ? ascending : descending)) {
rimY -= stem.width * beamSlant;
}
return rimY - (stemUp ? 0 : beamThickness) + nthBeamY;
};
const drawBeam = (leftX, leftY, rightX, rightY) => {
ctx.moveTo(leftX, leftY);
ctx.lineTo(rightX, rightY);
ctx.lineTo(rightX, rightY + beamThickness);
ctx.lineTo(leftX, leftY + beamThickness);
ctx.closePath();
};
ctx.save();
ctx.beginPath();
for (let beginStemIndex in beams) {
const leftStem = finalStemRectangles[beginStemIndex];
const beamLeft = leftStem.left;
for (let { beamLevel, endStemIndex } of beams[beginStemIndex]) {
const rightStem = finalStemRectangles[endStemIndex];
const beamRight = rightStem.right;
const nthBeamY = beamLevel * (beamThickness + beamSpacing) * (stemUp ? 1 : -1);
const beamLeftTopY = calculateBeamY(leftStem, true, nthBeamY);
const beamRightTopY = calculateBeamY(rightStem, false, nthBeamY);
drawBeam(beamLeft, beamLeftTopY, beamRight, beamRightTopY);
}
}
const hookWidth = 1 * staffSpace;
for (let stemIndex in hooks) {
const stem = finalStemRectangles[stemIndex];
const beamWidth = stem.width + hookWidth;
for (let { beamLevel, isForwardHook } of hooks[stemIndex]) {
const beamLeft = isForwardHook ? stem.left : stem.left - hookWidth;
const beamRight = isForwardHook ? stem.right + hookWidth : stem.right;
const nthBeamY = beamLevel * (beamThickness + beamSpacing) * (stemUp ? 1 : -1);
const beamLeftTopY = calculateBeamY(stem, !isForwardHook, nthBeamY);
const beamRightTopY = beamLeftTopY + beamSlant * beamWidth;
drawBeam(beamLeft, beamLeftTopY, beamRight, beamRightTopY);
}
}
ctx.fill();
ctx.restore();
}// function drawBeamedNotes
// フォントファイルの準備
document.fonts.load('10px Bravura').then(() => {
const canvas0 = document.getElementById('main-canvas-0');
const ctx0 = canvas0.getContext('2d');
const canvas1 = document.getElementById('main-canvas-1');
const ctx1 = canvas1.getContext('2d');
const canvas2 = document.getElementById('main-canvas-2');
const ctx2 = canvas2.getContext('2d');
const canvas3 = document.getElementById('main-canvas-3');
const ctx3 = canvas3.getContext('2d');
const firstLineY = 115;
const fontSize = 45;
const staffSpace = fontSize / 4;
const metadata = bravuraMetadata;
const fontName = 'Bravura';
ctx0.font = `${fontSize}px ${fontName}`;
ctx1.font = `${fontSize}px ${fontName}`;
ctx2.font = `${fontSize}px ${fontName}`;
ctx3.font = `${fontSize}px ${fontName}`;
const padding = 0.4 * staffSpace;// 記号間のスペースを適当に決める
const paddingBetweenNotes = 2 * staffSpace;// 記号間のスペースを適当に決める
const initialX = 10;
let x = initialX;
const staffWidth = 1240 / 2;
drawStave(ctx0, staffWidth, fontSize, x, firstLineY, metadata);
drawStave(ctx1, staffWidth, fontSize, x, firstLineY, metadata);
drawStave(ctx2, staffWidth, fontSize, x, firstLineY, metadata);
drawStave(ctx3, staffWidth, fontSize, x, firstLineY, metadata);
// draw beams (stemUp)
x = initialX;
{
const currentClef = new Clef(ClefType.G, 2);// 調号/音符の描画位置を決定するには、音部記号が必要
const clefAABB = drawClef(ctx0, currentClef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
const currentKey = new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), KeyMode.ionian);// 音符に付く臨時記号を決定するには、調号が必要
const keySignatureAABB = drawKeySignature(ctx0, currentKey, currentClef, fontSize, x + padding, firstLineY, metadata);
x = keySignatureAABB.right + padding;
const currentAccidentals = { };// 音符に付く臨時記号を決定するには、同じ小節内で現れた臨時記号の情報が必要 ; Dictionary<NaturalTone, Dictionary<int/*octave*/, int/*alter*/>>
// Dictionary<int/*diatonicNumber*/, int/*alter*/> の方がスマートだろうか?
{// stemUp, horizontal-beam
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.A, alter: 0 }), 3);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.quaver, 0));
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.A, alter: 0 }), 3);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.quaver, 0));
const notes = [note0, note1];
const xs = [x, x + 75];
drawBeamedNotes(ctx0, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata);
x += 150;
}
{// stemUp, ascending-beam
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.A, alter: 0 }), 3);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.quaver, 0));
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.G, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.quaver, 0));
const notes = [note0, note1];
const xs = [x, x + 75];
drawBeamedNotes(ctx0, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata);
x += 150;
}
{// stemUp, descending-beam
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.G, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.quaver, 0));
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.quaver, 0));
const notes = [note0, note1];
const xs = [x, x + 75];
drawBeamedNotes(ctx0, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata);
x += 150;
}
}
// draw beams (stemDown)
x = initialX;
{
const currentClef = new Clef(ClefType.G, 2);// 調号/音符の描画位置を決定するには、音部記号が必要
const clefAABB = drawClef(ctx1, currentClef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
const currentKey = new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), KeyMode.ionian);// 音符に付く臨時記号を決定するには、調号が必要
const keySignatureAABB = drawKeySignature(ctx1, currentKey, currentClef, fontSize, x + padding, firstLineY, metadata);
x = keySignatureAABB.right + padding;
const currentAccidentals = { };// 音符に付く臨時記号を決定するには、同じ小節内で現れた臨時記号の情報が必要 ; Dictionary<NaturalTone, Dictionary<int/*octave*/, int/*alter*/>>
// Dictionary<int/*diatonicNumber*/, int/*alter*/> の方がスマートだろうか?
{// stemUp, horizontal-beam
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 5);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.quaver, 0));
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 5);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.quaver, 0));
const notes = [note0, note1];
const xs = [x, x + 75];
drawBeamedNotes(ctx1, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata);
x += 150;
}
{// stemUp, ascending-beam
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.D, alter: 0 }), 5);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.quaver, 0));
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 5);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.quaver, 0));
const notes = [note0, note1];
const xs = [x, x + 75];
drawBeamedNotes(ctx1, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata);
x += 150;
}
{// stemUp, descending-beam
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.F, alter: 0 }), 5);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.quaver, 0));
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.D, alter: 0 }), 5);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.quaver, 0));
const notes = [note0, note1];
const xs = [x, x + 75];
drawBeamedNotes(ctx1, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata);
x += 150;
}
}
// draw beams (stemUp, complicated)
x = initialX;
{
const currentClef = new Clef(ClefType.G, 2);// 調号/音符の描画位置を決定するには、音部記号が必要
const clefAABB = drawClef(ctx2, currentClef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
const currentKey = new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), KeyMode.ionian);// 音符に付く臨時記号を決定するには、調号が必要
const keySignatureAABB = drawKeySignature(ctx2, currentKey, currentClef, fontSize, x + padding, firstLineY, metadata);
x = keySignatureAABB.right + padding;
const currentAccidentals = { };// 音符に付く臨時記号を決定するには、同じ小節内で現れた臨時記号の情報が必要 ; Dictionary<NaturalTone, Dictionary<int/*octave*/, int/*alter*/>>
// Dictionary<int/*diatonicNumber*/, int/*alter*/> の方がスマートだろうか?
{// stemUp, horizontal-beam
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.demisemiquaver, 0));
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.G, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.semiquaver, 0));
const pitch2 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note2 = new Note(pitch2, new NoteValue(NoteValueType.demisemiquaver, 0));
const notes = [note0, note1, note2];
const xs = [x, x + 75, x + 150];
drawBeamedNotes(ctx2, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata);
x += 200;
}
{// stemUp, ascending-beam
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.demisemiquaver, 0));
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.G, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.semiquaver, 0));
const pitch2 = new Pitch(new Tone({ naturalTone: NaturalTone.B, alter: 0 }), 4);
const note2 = new Note(pitch2, new NoteValue(NoteValueType.demisemiquaver, 0));
const notes = [note0, note1, note2];
const xs = [x, x + 75, x + 150];
drawBeamedNotes(ctx2, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata);
x += 200;
}
{// stemUp, descending-beam
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.B, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.demisemiquaver, 0));
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.G, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.semiquaver, 0));
const pitch2 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note2 = new Note(pitch2, new NoteValue(NoteValueType.demisemiquaver, 0));
const notes = [note0, note1, note2];
const xs = [x, x + 75, x + 150];
drawBeamedNotes(ctx2, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata);
x += 200;
}
}
// draw beams (stemUp, complicated)
x = initialX;
{
const currentClef = new Clef(ClefType.G, 2);// 調号/音符の描画位置を決定するには、音部記号が必要
const clefAABB = drawClef(ctx3, currentClef, fontSize, x + padding, firstLineY, metadata);
x = clefAABB.right + padding;
const currentKey = new KeySignature(new Tone({ naturalTone: NaturalTone.C, alter: 0 }), KeyMode.ionian);// 音符に付く臨時記号を決定するには、調号が必要
const keySignatureAABB = drawKeySignature(ctx3, currentKey, currentClef, fontSize, x + padding, firstLineY, metadata);
x = keySignatureAABB.right + padding;
const currentAccidentals = { };// 音符に付く臨時記号を決定するには、同じ小節内で現れた臨時記号の情報が必要 ; Dictionary<NaturalTone, Dictionary<int/*octave*/, int/*alter*/>>
// Dictionary<int/*diatonicNumber*/, int/*alter*/> の方がスマートだろうか?
{// stemUp, horizontal-beam
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.demisemiquaver, 0));
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.G, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.demisemiquaver, 0));
const pitch2 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note2 = new Note(pitch2, new NoteValue(NoteValueType.demisemiquaver, 0));
const notes = [note0, note1, note2];
const xs = [x, x + 75, x + 150];
drawBeamedNotes(ctx3, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata);
x += 200;
}
{// stemUp, ascending-beam
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.demisemiquaver, 0));
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.G, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.demisemiquaver, 0));
const pitch2 = new Pitch(new Tone({ naturalTone: NaturalTone.B, alter: 0 }), 4);
const note2 = new Note(pitch2, new NoteValue(NoteValueType.demisemiquaver, 0));
const notes = [note0, note1, note2];
const xs = [x, x + 75, x + 150];
drawBeamedNotes(ctx3, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata);
x += 200;
}
{// stemUp, descending-beam
const pitch0 = new Pitch(new Tone({ naturalTone: NaturalTone.B, alter: 0 }), 4);
const note0 = new Note(pitch0, new NoteValue(NoteValueType.demisemiquaver, 0));
const pitch1 = new Pitch(new Tone({ naturalTone: NaturalTone.G, alter: 0 }), 4);
const note1 = new Note(pitch1, new NoteValue(NoteValueType.demisemiquaver, 0));
const pitch2 = new Pitch(new Tone({ naturalTone: NaturalTone.E, alter: 0 }), 4);
const note2 = new Note(pitch2, new NoteValue(NoteValueType.demisemiquaver, 0));
const notes = [note0, note1, note2];
const xs = [x, x + 75, x + 150];
drawBeamedNotes(ctx3, notes, currentClef, currentKey, currentAccidentals, fontSize, xs, firstLineY, metadata);
x += 200;
}
}
});// document.fonts.load
};// function window.onload
</script>
</head>
<body>
<div>
<canvas id='main-canvas-0' width='640px' height='180px' style='border:solid'></canvas>
</div>
<div>
<canvas id='main-canvas-1' width='640px' height='180px' style='border:solid'></canvas>
</div>
<div>
<canvas id='main-canvas-2' width='640px' height='180px' style='border:solid'></canvas>
</div>
<div>
<canvas id='main-canvas-3' width='640px' height='180px' style='border:solid'></canvas>
</div>
</body>
</html>
結果:
おわりに
ソースコード部分がめちゃくちゃ長くなったせいか Qiita のエディタが重いなんてもんじゃない.
この記事では単声の楽譜について最低限必要と思われる記号の書き方に終始しているが、ここから好きな楽譜を自動配置で描画するには、少なくとも以下の作業が必要である:
- 楽譜モデルの内部表現を構築できるインターフェース(ファクトリークラス、楽譜ファイルの読み込み処理)
- この記事の例では大体 drawXxx 関数内で完結させていたが、これを例えば以下のように分割する
-
- 楽譜のモデルデータから SMuFLCharacter などとその大きさへの変換
-
- 要素の大きさ(幅)を参照し、各記号の水平位置や改行位置を決定する
-
- タイやスラー、連桁に関する計算
-
- 描画
- テンポや発想記号、繰り返しの括弧、アーティキュレーション、強弱記号、リハーサルマーク、歌詞など諸々の記号類のための処理
改めて abcjs や vexflow などのライブラリは凄いなと思った.
以上、とりとめのない記事をここまで見てくださった方はありがとうございました.