-
Notifications
You must be signed in to change notification settings - Fork 662
VexFlow Font Rendering
VexFlow comes with a pluggable font system for music glyphs, based on Standard Music Font Layout (SMuFL), developed by the W3C Music Notation Community Group.
By default, it supports the following fonts:
- Bravura (a SMuFL font by Steinberg)
- Petaluma (a SMuFL font by Steinberg)
- Gonville (by Simon Tatham)
- Custom - Glyphs submitted by Vexflow contributors (microtonal accidentals, eastern music glyphs, etc).
VexFlow resolves glyphs through a font stack.
The default font stack is: [Bravura, Gonville, Custom]
. When VexFlow tries to resolve a glyph code, it searches each font in the stack, and returns the first glyph it finds. If it can't find a glyph, an exception is thrown.
Change the font stack by calling Flow.setMusicFont(...fontNames)
:
Vex.Flow.setMusicFont("Petaluma", "Bravura", "Gonville");
Each VexFlow music font consists of a glyphs file, a metrics file and a loader file, in the src/fonts
directory.
For example, Bravura has:
Glyphs files include drawing primitives (moveTo
, lineTo
, bezierCurve
, etc.) for each glyph, indexed by glyph code (a SMuFL code.) This file is generated from a OTF font file by a script. Here's an example:
"bracketTop": {
"x_min": 0,
"x_max": 469,
"y_min": 0,
"y_max": 295,
"ha": 295,
"o": "m 0 168 l 0 0 l 180 0 b 674 390 410 43 616 150 b 675 405 675 396 675 400 b 664 425 675 416 671 422 b 628 405 651 425 635 415 b 157 179 613 389 432 199 l 12 179 b 0 168 3 179 0 177 z"
},
"bracketBottom": {
"x_min": 0,
"x_max": 469,
"y_min": -295,
"y_max": 0,
"ha": 295,
"o": "m 0 0 l 0 -168 b 12 -179 0 -177 3 -179 l 157 -179 b 628 -405 432 -199 613 -389 b 664 -425 635 -415 651 -425 b 675 -405 671 -422 675 -416 b 674 -390 675 -400 675 -396 b 180 0 616 -150 410 -43 z"
},
The metrics files consist of Vexflow-specific metrics for the font, such as stem lengths, positioning and scaling information, point-size overrides, etc. Metrics files are more free form, since they're used differently by different elements, however there are some conventions. Here's an example:
glyphs: {
coda: {
point: 20,
shiftX: -7,
shiftY: 8,
},
segno: {
shiftX: -7,
},
flag: {
shiftX: -0.75,
tabStem: {
shiftX: -1.75,
},
staveTempo: {
shiftX: -1,
}
},
clef: {
gClef: {
default: { scale: 1.1, shiftY: 1 },
small: { shiftY: 1.5 }
},
fClef: {
default: { shiftY: -0.5 }
}
},
// ...
// ...
}
The container files consist of a standard container importing the glyphs file and the metrics file which allows the generation of a separated file using the dynamic import feature of Code Splitting|webpack. Here's an example:
import { BravuraFont } from "./bravura_glyphs";
import { BravuraMetrics } from "./bravura_metrics";
const Bravura = {
fontData: BravuraFont,
metrics: BravuraMetrics,
};
export default Bravura;
Rendering a glyph consistently across different fonts, canvases, and backends involves a number of moving parts. Let's walk through what happens when the following line of code is run:
Glyph.renderGlyph(ctx, x, y, fontSize, glyphCode, { category: "flag.tabStem" });
The glyph code (e.g., noteheadBlack
, coda
) is resolved by searching the music font stack and returning the first available glyph (along with its font, metrics, etc.) This glyph consists of an unprocessed outline made up of lines and curves.
Before the final outline can be calculated, some transformations may need to be applied.
If a category
option is provided to renderGlyph(...)
(e.g., { category: 'flag.tabStem' }
above), the following variables are loaded from the glyphs.flag.tabStem
section of the font metrics file bravura_metrics.js
:
scale
shiftX
shiftY
point
If no category
is provided, then defaults are used (typically 0-positioned, and 1-scaled.)
The bounding box for the glyph is then calculated, from which the width and height are derived. An origin shift may also be calculated if requested.
Depending on the rendering backend (e.g., SVG, Canvas), styles such as color, stroke-widths, may be applied.
The glyph is ready to be drawn. Depending on the outline and the transformations, a series of moveTo
, lineTo
, bezierCurveTo
, or quadraticCurveTo
calls are sent to the backend. The glyph is rendered, and canvas styles are restored (if necessary) for whatever comes next.
If you're interested in the details, the entire glyph rendering code is available in src/glyph.ts
.
The metrics files (e.g., src/font/bravura_metrics.ts
consist of a single exported configuration object. It has no pre-defined structure, but it does have some conventions. Here's a snippet from the bravura_metrics.ts
file.
clef: {
default: {
point: 32,
width: 26,
},
small: {
point: 26,
width: 20,
},
// ...
}
You can lookup a metric from within any element with:
Tables.currentMusicFont().lookupMetric(metricPath, optionalDefault);
So to get the point-size for the small clef in the bravura_metrics.ts
excerpt above, you can call:
const clefPointSize = Tables.currentMusicFont().lookupMetric("clef.small.point", 40);
renderClef("treble", clefPointSize);
If metric is not found, the default (40
in this case) is returned. If no default is provided, an exception is thrown.
There can be some standardized metric sections used by common classes, e.g., glyphs
used by the category
option of the Glyph
class.
glyphs: {
// ...
textNote: {
point: 34,
breathMarkTick: {
point: 36,
shiftY: 9,
},
breathMarkComma: {
point: 36,
},
segno: {
point: 30,
shiftX: -7,
shiftY: 8,
},
coda: {
point: 20,
shiftX: -7,
shiftY: 8,
}
},
// ...
}
If a category
option is provided to Glyph.renderGlyph(ctx, x, y, code, point, options)
, e.g., {category: 'textNote'}
, the renderer will lookup metrics such as textNote.{code}.scale
or textNote.{code}.shiftX
to apply size and shift overrides on a glyph. If it can't find a metric for the specific code in textNote.{code}.scale
, it checks the parent for a category default (textNote.scale
).
This way you can provide default options for a category and customize them for specific SMuFL codes.
The Glyphs file (e.g. bravura_glyphs.ts
consists of glyph drawing outlines indexed by glyph code. These files are typically machine generated from OTF font files, however you can add custom glyphs by adding a new drawing outline to src/font/custom_glyphs.ts
.
The glyph structure consists of the following fields:
-
x_min
: left-most x value -
x_max
: right-most x value -
ha
: height of glyph -
o
: a long string consisting of repeated drawing commands followed by coordinates. (Below.)
The full width of the glyph must be x_max - x_min
.
-
m
- MoveTo(x,y) -
l
- LineTo(x,y) -
q
- QuadraticCurveTo(cpx, cpy, x, y) -
b
- BeizerCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
The cp* parameters are coordinates to control points for the curves. All coordinates are scaled by point_size * 72 / (Vex.Flow.Font.resolution \* 100)
.
To see the rendering code, see Vex.Flow.Glyph.renderOutline()
in src/glyph.js
. You can see how we generated the outlines for Bravura by examining tools/fonts/fontgen_smufl.js
.
The tools for glyph management are in the tools/fonts/
directory. The tools depend on the configuration files in tools/fonts/config/
to pick SMuFL codepoints and associate them with UTF codes or legacy Vexflow font codes (e.g., vf1
, vb6
).
-
tools/fonts/config/glyphnames.json
- Mappings from SMuFL code-points to UTF code points. These are used to create thesrc/fonts/xxx_glyphs.ts
files for Vexflow. -
tools/fonts/config/valid_codes.js
- List of SMuFL codes used by VexFlow, along with a mapping into legacy vexflow codes.
-
fontgen_smufl.js
- Tool to generatesrc/fonts/fontname_glyphs.js
from OTF font files based on the above configuration. This tool can be used for any SMuFL-compliant OTF music font file. -
fontgen_gonville_custom.js
- Tool to generatesrc/fonts/gonville_glyphs.ts
andsrc/fonts/custom_glyphs.ts
from files in thetools/fonts/other/
directory.
- Add the SMuFL glyph code to
config/valid_codes.js
. You can find SMuFL glyph codes from the glyph browser at https://smufl.org. If there is no standard code for your glyph, see next section on creating custom glyphs. - If there's a Gonville glyph available, then set the value of the code in
valid_codes.js
to the Gonville glyph code. If not, simply set it tonull
. - Run
fontgen_smufl.js
to generate the new Bravura and Petaluma glyph files.
$ node fontgen_smufl.js fonts/Bravura.otf ../../src/fonts/bravura_glyphs.js
$ node fontgen_smufl.js fonts/Petaluma-1.055.otf ../../src/fonts/petaluma_glyphs.js
- Run
fontgen_gonville_custom.js
to generate a new Gonville font file (if necessary.)
node fontgen_gonville_custom.js ../../src/fonts/
- Edit your element source file (e.g.,
src/accidental.ts
if this is a new accidental) and add the code. - Adjust scale or position by adding configuration to the relevant metrics file (
src/fonts/bravura_metrics.ts
.)
- Create a unique custom code (prefixed with
vex
), e.g.,vexMyNewAccidentalGlyph
, and add it totools/fonts/config/valid_codes.js
. You can set the value tonull
. - Add the glyph outline to
tools/fonts/other/custom.js
. - Regenerate
src/fonts/custom_glyphs.ts
with this command:
node fontgen_gonville_custom.js ../../src/fonts/
- Add the code to
tables.ts
(e.g., search forvexAccidentalMicrotonal1
). - Adjust scale or position by adding configuration to the relevant metrics file (
src/fonts/bravura_metrics.ts
.)
The Vexflow tests automatically run all renders using multiple font stacks, so there's not much more to do than to write tests! :-)
For this example, the new font is named "Awesome" instead of Bravura / Petaluma / Gonville. If you want to try this with a real SMuFL font, you can try this with Leland or Sebastian.
In the VexFlow repository, search for ADD_MUSIC_FONT
to see the integration points for a completely new font.
- The first integration point is in
load_all.ts
. Follow the existing examples to import your font and call the load function on that font module. For example:
import { loadAwesome } from "./load_awesome"; // <<< Added this!
import { loadBravura } from "./load_bravura";
import { loadCustom } from "./load_custom";
import { loadGonville } from "./load_gonville";
import { loadPetaluma } from "./load_petaluma";
export function loadAllMusicFonts(): void {
loadAwesome(); // <<< Added this!
loadBravura();
loadCustom();
loadGonville();
loadPetaluma();
}
- In the
src/fonts/
directory, create a new file calledload_awesome.ts
. Your loader file will look something like this:
import { Font } from "../font";
import { AwesomeFont } from "./awesome_glyphs";
import { AwesomeMetrics } from "./awesome_metrics";
export function loadAwesome() {
Font.load("Awesome", AwesomeFont, AwesomeMetrics);
}
Now we need to generate the awesome_glyphs.ts
and awesome_metrics.ts
files. These files should be placed in the src/fonts/
directory.
- Let's take a look at the glyphs file first. Open
bravura_glyphs.ts
to see what it should look like. It's a big file containing all the glyph outlines for each SMuFL code. To generate this file, we use thefontgen_smufl.js
tool.
node fontgen_smufl.js AwesomeFont.otf ../../src/fonts/awesome_glyphs.ts
- The easiest way to generate
awesome_metrics.ts
is to duplicate thebravura_metrics.ts
file and then customize it to adjust the scale and position of your glyphs.
Here are some handy external resources:
- SMuFL – Browse the glyphs and understand metadata (e.g., Clefs).
- OpenType Glyph Inspector - Upload a font file and investigate glyphs.
- Bravura – Download Bravura font files.
- OpenType – The tools use the OpenType library to parse OTF fonts and generate Vexflow glyph files.
[ VexFlow.com ] [ API ] [ GitHub Repo ] [ Contributors ]