VexFlow - Copyright (c) Mohit Muthanna 2010.
The file implements notes for Tablature notation. This consists of one or more fret positions, and can either be drawn with or without stems.
See tests/tabnote_tests.js
for usage examples
import { Vex } from './vex';
import { Flow } from './tables';
import { Modifier } from './modifier';
import { Stem } from './stem';
import { StemmableNote } from './stemmablenote';
import { Dot } from './dot';
import { Glyph } from './glyph';
Gets the unused strings grouped together if consecutive.
Parameters:
function getUnusedStringGroups(num_lines, strings_used) {
const stem_through = [];
let group = [];
for (let string = 1; string <= num_lines; string++) {
const is_used = strings_used.indexOf(string) > -1;
if (!is_used) {
group.push(string);
} else {
stem_through.push(group);
group = [];
}
}
if (group.length > 0) stem_through.push(group);
return stem_through;
}
Gets groups of points that outline the partial stem lines between fret positions
Parameters:
y
coordinate the stem is located onfunction getPartialStemLines(stem_y, unused_strings, stave, stem_direction) {
const up_stem = stem_direction !== 1;
const down_stem = stem_direction !== -1;
const line_spacing = stave.getSpacingBetweenLines();
const total_lines = stave.getNumLines();
const stem_lines = [];
unused_strings.forEach(strings => {
const containsLastString = strings.indexOf(total_lines) > -1;
const containsFirstString = strings.indexOf(1) > -1;
if ((up_stem && containsFirstString) ||
(down_stem && containsLastString)) {
return;
}
If there’s only one string in the group, push a duplicate value. We do this because we need 2 strings to convert into upper/lower y values.
if (strings.length === 1) {
strings.push(strings[0]);
}
const line_ys = [];
Iterate through each group string and store it’s y position
strings.forEach((string, index, strings) => {
const isTopBound = string === 1;
const isBottomBound = string === total_lines;
Get the y value for the appropriate staff line, we adjust for a 0 index array, since string numbers are index 1
let y = stave.getYForLine(string - 1);
Unless the string is the first or last, add padding to each side of the line
if (index === 0 && !isTopBound) {
y -= line_spacing / 2 - 1;
} else if (index === strings.length - 1 && !isBottomBound) {
y += line_spacing / 2 - 1;
}
Store the y value
line_ys.push(y);
Store a subsequent y value connecting this group to the main stem above/below the stave if it’s the top/bottom string
if (stem_direction === 1 && isTopBound) {
line_ys.push(stem_y - 2);
} else if (stem_direction === -1 && isBottomBound) {
line_ys.push(stem_y + 2);
}
});
Add the sorted y values to the
stem_lines.push(line_ys.sort((a, b) => a - b));
});
return stem_lines;
}
export class TabNote extends StemmableNote {
static get CATEGORY() { return 'tabnotes'; }
Initialize the TabNote with a tab_struct
full of properties
and whether to draw_stem
when rendering the note
constructor(tab_struct, draw_stem) {
super(tab_struct);
this.setAttribute('type', 'TabNote');
this.ghost = false; // Renders parenthesis around notes
Note properties
The fret positions in the note. An array of { str: X, fret: X }
this.positions = tab_struct.positions;
Render Options
Vex.Merge(this.render_options, {
font size for note heads and rests
glyph_font_scale: Flow.DEFAULT_TABLATURE_FONT_SCALE,
Flag to draw a stem
draw_stem,
Flag to draw dot modifiers
draw_dots: draw_stem,
Flag to extend the main stem through the stave and fret positions
draw_stem_through_stave: false,
vertical shift from stave line
y_shift: 0,
normal glyph scale
scale: 1.0,
default tablature font
font: '10pt Arial',
});
this.glyph = Flow.durationToGlyph(this.duration, this.noteType);
if (!this.glyph) {
throw new Vex.RuntimeError(
'BadArguments',
`Invalid note initialization data (No glyph found): ${JSON.stringify(tab_struct)}`
);
}
this.buildStem();
if (tab_struct.stem_direction) {
this.setStemDirection(tab_struct.stem_direction);
} else {
this.setStemDirection(Stem.UP);
}
Renders parenthesis around notes
this.ghost = false;
this.updateWidth();
}
The ModifierContext category
getCategory() { return TabNote.CATEGORY; }
Set as ghost TabNote
, surrounds the fret positions with parenthesis.
Often used for indicating frets that are being bent to
setGhost(ghost) {
this.ghost = ghost;
this.updateWidth();
return this;
}
Determine if the note has a stem
hasStem() { return this.render_options.draw_stem; }
Get the default stem extension for the note
getStemExtension() {
const glyph = this.getGlyph();
if (this.stem_extension_override != null) {
return this.stem_extension_override;
}
if (glyph) {
return this.getStemDirection() === 1
? glyph.tabnote_stem_up_extension
: glyph.tabnote_stem_down_extension;
}
return 0;
}
Add a dot to the note
addDot() {
const dot = new Dot();
this.dots += 1;
return this.addModifier(dot, 0);
}
Calculate and store the width of the note
updateWidth() {
this.glyphs = [];
this.width = 0;
for (let i = 0; i < this.positions.length; ++i) {
let fret = this.positions[i].fret;
if (this.ghost) fret = '(' + fret + ')';
const glyph = Flow.tabToGlyph(fret, this.render_options.scale);
this.glyphs.push(glyph);
this.width = Math.max(glyph.getWidth(), this.width);
}
For some reason we associate a notehead glyph with a TabNote, and this glyph is used for certain width calculations. Of course, this is totally incorrect since a notehead is a poor approximation for the dimensions of a fret number which can have multiple digits. As a result, we must overwrite getWidth() to return the correct width
this.glyph.getWidth = () => this.width;
}
Set the stave
to the note
setStave(stave) {
super.setStave(stave);
this.context = stave.context;
Calculate the fret number width based on font used
let i;
if (this.context) {
const ctx = this.context;
this.width = 0;
for (i = 0; i < this.glyphs.length; ++i) {
const glyph = this.glyphs[i];
const text = '' + glyph.text;
if (text.toUpperCase() !== 'X') {
ctx.save();
ctx.setRawFont(this.render_options.font);
glyph.width = ctx.measureText(text).width;
ctx.restore();
glyph.getWidth = () => glyph.width;
}
this.width = Math.max(glyph.getWidth(), this.width);
}
this.glyph.getWidth = () => this.width;
}
we subtract 1 from line
because getYForLine expects a 0-based index,
while the position.str is a 1-based index
const ys = this.positions.map(({ str: line }) => stave.getYForLine(line - 1));
this.setYs(ys);
if (this.stem) {
this.stem.setYBounds(this.getStemY(), this.getStemY());
}
return this;
}
Get the fret positions for the note
getPositions() { return this.positions; }
Add self to the provided modifier context mc
addToModifierContext(mc) {
this.setModifierContext(mc);
for (let i = 0; i < this.modifiers.length; ++i) {
this.modifierContext.addModifier(this.modifiers[i]);
}
this.modifierContext.addModifier(this);
this.preFormatted = false;
return this;
}
Get the x
coordinate to the right of the note
getTieRightX() {
let tieStartX = this.getAbsoluteX();
const note_glyph_width = this.glyph.getWidth();
tieStartX += note_glyph_width / 2;
tieStartX += (-this.width / 2) + this.width + 2;
return tieStartX;
}
Get the x
coordinate to the left of the note
getTieLeftX() {
let tieEndX = this.getAbsoluteX();
const note_glyph_width = this.glyph.getWidth();
tieEndX += note_glyph_width / 2;
tieEndX -= (this.width / 2) + 2;
return tieEndX;
}
Get the default x
and y
coordinates for a modifier at a specific
position
at a fret position index
getModifierStartXY(position, index) {
if (!this.preFormatted) {
throw new Vex.RERR('UnformattedNote', "Can't call GetModifierStartXY on an unformatted note");
}
if (this.ys.length === 0) {
throw new Vex.RERR('NoYValues', 'No Y-Values calculated for this note.');
}
let x = 0;
if (position === Modifier.Position.LEFT) {
x = -1 * 2; // extra_left_px
} else if (position === Modifier.Position.RIGHT) {
x = this.width + 2; // extra_right_px
} else if (position === Modifier.Position.BELOW || position === Modifier.Position.ABOVE) {
const note_glyph_width = this.glyph.getWidth();
x = note_glyph_width / 2;
}
return {
x: this.getAbsoluteX() + x,
y: this.ys[index],
};
}
Get the default line for rest
getLineForRest() { return this.positions[0].str; }
Pre-render formatting
preFormat() {
if (this.preFormatted) return;
if (this.modifierContext) this.modifierContext.preFormat();
width is already set during init()
this.setPreFormatted(true);
}
Get the x position for the stem
getStemX() { return this.getCenterGlyphX(); }
Get the y position for the stem
getStemY() {
const num_lines = this.stave.getNumLines();
The decimal staff line amounts provide optimal spacing between the fret number and the stem
const stemUpLine = -0.5;
const stemDownLine = num_lines - 0.5;
const stemStartLine = Stem.UP === this.stem_direction ? stemUpLine : stemDownLine;
return this.stave.getYForLine(stemStartLine);
}
Get the stem extents for the tabnote
getStemExtents() {
return this.stem.getExtents();
}
Draw the fal onto the context
drawFlag() {
const {
beam, glyph, context, stem, stem_direction,
render_options: { draw_stem, glyph_font_scale },
} = this;
const shouldDrawFlag = beam == null && draw_stem;
Now it’s the flag’s turn.
if (glyph.flag && shouldDrawFlag) {
const flag_x = this.getStemX() + 1;
const flag_y = this.getStemY() - stem.getHeight();
const flag_code = stem_direction === Stem.DOWN
? glyph.code_flag_downstem // Down stems have flags on the left.
: glyph.code_flag_upstem;
Draw the Flag
Glyph.renderGlyph(context, flag_x, flag_y, glyph_font_scale, flag_code);
}
}
Render the modifiers onto the context
drawModifiers() {
Draw the modifiers
this.modifiers.forEach((modifier) => {
Only draw the dots if enabled
if (modifier.getCategory() === 'dots' && !this.render_options.draw_dots) return;
modifier.setContext(this.context);
modifier.draw();
});
}
Render the stem extension through the fret positions
drawStemThrough() {
const stem_x = this.getStemX();
const stem_y = this.getStemY();
const ctx = this.context;
const stem_through = this.render_options.draw_stem_through_stave;
const draw_stem = this.render_options.draw_stem;
if (draw_stem && stem_through) {
const total_lines = this.stave.getNumLines();
const strings_used = this.positions.map(position => position.str);
const unused_strings = getUnusedStringGroups(total_lines, strings_used);
const stem_lines = getPartialStemLines(
stem_y,
unused_strings,
this.getStave(),
this.getStemDirection()
);
ctx.save();
ctx.setLineWidth(Stem.WIDTH);
stem_lines.forEach(bounds => {
if (bounds.length === 0) return;
ctx.beginPath();
ctx.moveTo(stem_x, bounds[0]);
ctx.lineTo(stem_x, bounds[bounds.length - 1]);
ctx.stroke();
ctx.closePath();
});
ctx.restore();
}
}
Render the fret positions onto the context
drawPositions() {
const ctx = this.context;
const x = this.getAbsoluteX();
const ys = this.ys;
for (let i = 0; i < this.positions.length; ++i) {
const y = ys[i] + this.render_options.y_shift;
const glyph = this.glyphs[i];
Center the fret text beneath the notation note head
const note_glyph_width = this.glyph.getWidth();
const tab_x = x + (note_glyph_width / 2) - (glyph.getWidth() / 2);
FIXME: Magic numbers.
ctx.clearRect(tab_x - 2, y - 3, glyph.getWidth() + 4, 6);
if (glyph.code) {
Glyph.renderGlyph(ctx, tab_x, y,
this.render_options.glyph_font_scale * this.render_options.scale,
glyph.code);
} else {
ctx.save();
ctx.setRawFont(this.render_options.font);
const text = glyph.text.toString();
ctx.fillText(text, tab_x, y + 5 * this.render_options.scale);
ctx.restore();
}
}
}
The main rendering function for the entire note
draw() {
this.checkContext();
if (!this.stave) {
throw new Vex.RERR('NoStave', "Can't draw without a stave.");
}
if (this.ys.length === 0) {
throw new Vex.RERR('NoYValues', "Can't draw note without Y values.");
}
const render_stem = this.beam == null && this.render_options.draw_stem;
this.drawPositions();
this.drawStemThrough();
const stem_x = this.getStemX();
this.stem.setNoteHeadXBounds(stem_x, stem_x);
if (render_stem) {
this.context.openGroup('stem', null, { pointerBBox: true });
this.stem.setContext(this.context).draw();
this.context.closeGroup();
}
this.drawFlag();
this.drawModifiers();
}
}