src/VexFlowPatch/src/stavenote.js
// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
//
// ## Description
// This file implements notes for standard notation. This consists of one or
// more `NoteHeads`, an optional stem, and an optional flag.
//
// *Throughout these comments, a "note" refers to the entire `StaveNote`,
// and a "key" refers to a specific pitch/notehead within a note.*
//
// See `tests/stavenote_tests.js` for usage examples.
import { Vex } from './vex';
import { Flow } from './tables';
import { BoundingBox } from './boundingbox';
import { Stem } from './stem';
import { NoteHead } from './notehead';
import { StemmableNote } from './stemmablenote';
import { Modifier } from './modifier';
import { Dot } from './dot';
// To enable logging for this class. Set `Vex.Flow.StaveNote.DEBUG` to `true`.
function L(...args) { if (StaveNote.DEBUG) Vex.L('Vex.Flow.StaveNote', args); }
const getStemAdjustment = (note) => Stem.WIDTH / (2 * -note.getStemDirection());
const isInnerNoteIndex = (note, index) =>
index === (note.getStemDirection() === Stem.UP ? note.keyProps.length - 1 : 0);
// Helper methods for rest positioning in ModifierContext.
function shiftRestVertical(rest, note, dir) {
if (rest.note.shiftRestVerticalDisabled) {
return;
}
const delta = (note.isrest ? 0.0 : 1.0) * dir;
rest.line += delta;
rest.maxLine += delta;
rest.minLine += delta;
rest.note.setKeyLine(0, rest.note.getKeyLine(0) + (delta));
}
// Called from formatNotes :: center a rest between two notes
function centerRest(rest, noteU, noteL) {
const delta = rest.line - Vex.MidLine(noteU.minLine, noteL.maxLine);
rest.note.setKeyLine(0, rest.note.getKeyLine(0) - delta);
rest.line -= delta;
rest.maxLine -= delta;
rest.minLine -= delta;
}
export class StaveNote extends StemmableNote {
static get CATEGORY() { return 'stavenotes'; }
static get STEM_UP() { return Stem.UP; }
static get STEM_DOWN() { return Stem.DOWN; }
static get DEFAULT_LEDGER_LINE_OFFSET() { return 3; }
// ## Static Methods
//
// Format notes inside a ModifierContext.
static format(notes, state) {
if (!notes || notes.length < 2) return false;
// FIXME: VexFlow will soon require that a stave be set before formatting.
// Which, according to the below condition, means that following branch will
// always be taken and the rest of this function is dead code.
//
// Problematically, `Formatter#formatByY` was not designed to work for more
// than 2 voices (although, doesn't throw on this condition, just tries
// to power through).
//
// Based on the above:
// * 2 voices can be formatted *with or without* a stave being set but
// the output will be different
// * 3 voices can only be formatted *without* a stave
// if this is enabled, notes are sometimes not staggered correctly, see setXShift lines below
// if (notes[0].getStave()) {
// return StaveNote.formatByY(notes, state); //
// }
const notesList = [];
const stagger_same_whole_notes = notes[0].stagger_same_whole_notes;
// whether to stagger whole notes on the same line but different voice (show 2 instead of 1).
// controlled by EngravingRules.StaggerSameWholeNotes
for (let i = 0; i < notes.length; i++) {
const props = notes[i].getKeyProps();
const line = props[0].line;
let minL = props[props.length - 1].line;
const stemDirection = notes[i].getStemDirection();
const stemMax = notes[i].getStemLength() / 10;
const stemMin = notes[i].getStemMinumumLength() / 10;
let maxL;
if (notes[i].isRest()) {
maxL = line + notes[i].glyph.line_above;
minL = line - notes[i].glyph.line_below;
} else {
maxL = stemDirection === 1
? props[props.length - 1].line + stemMax
: props[props.length - 1].line;
minL = stemDirection === 1
? props[0].line
: props[0].line - stemMax;
}
notesList.push({
line: props[0].line, // note/rest base line
maxLine: maxL, // note/rest upper bounds line
minLine: minL, // note/rest lower bounds line
isrest: notes[i].isRest(),
stemDirection,
stemMax, // Maximum (default) note stem length;
stemMin, // minimum note stem length
voice_shift: notes[i].getVoiceShiftWidth(),
is_displaced: notes[i].isDisplaced(), // note manually displaced
note: notes[i],
});
}
const voices = notesList.length;
let noteU = notesList[0];
const noteM = voices > 2 ? notesList[1] : null;
let noteL = voices > 2 ? notesList[2] : notesList[1];
// for two voice backward compatibility, ensure upper voice is stems up
// for three voices, the voices must be in order (upper, middle, lower)
if (voices === 2 && noteU.stemDirection === -1 && noteL.stemDirection === 1 &&
!noteU.isrest && !noteL.isRest // no need to switch positions if one is a rest
) {
noteU = notesList[1];
noteL = notesList[0];
}
const voiceXShift = Math.max(noteU.voice_shift, noteL.voice_shift);
let xShift = 0;
let stemDelta;
// Test for two voice note intersection
if (voices === 2) {
const lineSpacing = noteU.stemDirection === noteL.stemDirection ? 0.0 : 0.5;
// if top voice is a middle voice, check stem intersection with lower voice
if (noteU.stemDirection === noteL.stemDirection &&
noteU.minLine <= noteL.maxLine) {
if (!noteU.isrest) {
stemDelta = Math.abs(noteU.line - (noteL.maxLine + 0.5));
stemDelta = Math.max(stemDelta, noteU.stemMin);
noteU.minLine = noteU.line - stemDelta;
noteU.note.setStemLength(stemDelta * 10);
}
}
if (noteU.minLine <= noteL.maxLine + lineSpacing) {
if (noteU.isrest) {
// shift rest up
shiftRestVertical(noteU, noteL, 1);
if (noteU.note.hasLedgerLinedRest) {
noteU.note.shiftRestVerticalDisabled = true; // don't shift again on re-render
}
} else if (noteL.isrest) {
// shift rest down
shiftRestVertical(noteL, noteU, -1);
if (noteL.note.hasLedgerLinedRest) {
noteL.note.shiftRestVerticalDisabled = true; // don't shift again on re-render
}
} else {
xShift = voiceXShift;
//Vexflowpatch: Instead of shifting notes, remove the appropriate flag.
//If we are sharing a line, switch one notes stem direction.
//If we are sharing a line and in the same voice, only then offset one note
const lineDiff = Math.abs(noteU.line - noteL.line);
//if (noteU.note.glyph.stem && noteL.note.glyph.stem) { // skip this condition: whole notes also relevant
//If we have different dot values, must offset
//Or If we have a non-filled in mixed with a filled in notehead, must offset
let halfNoteCount = 0;
let wholeNoteCount = 0;
if (noteU.note.duration === "h") {
halfNoteCount++;
} else if (noteU.note.duration === "w") {
wholeNoteCount++;
}
if (noteL.note.duration === "h") {
halfNoteCount++;
} else if (noteL.note.duration === "w") {
wholeNoteCount++;
}
// only stagger/x-shift if one of the notes is whole or half note and the other isn't. (or dots different)
let staggerConditions = halfNoteCount === 1 || wholeNoteCount === 1 || noteU.note.dots !== noteL.note.dots;
if (stagger_same_whole_notes) { // controlled by EngravingRules.StaggerSameWholeNotes. see declaration above
staggerConditions = staggerConditions || wholeNoteCount === 2;
// should be ||=, but appveyor says syntax error, doesn't know the operator.
}
if (lineDiff === 0 && staggerConditions) {
noteL.note.setXShift(xShift);
if (noteU.note.dots > 0) {
let foundDots = 0;
for (const modifier of noteU.note.modifiers) {
if (modifier instanceof Dot) {
foundDots++;
//offset dot(s) above the shifted note
//lines + 1 to negative pixels
modifier.setYShift(-10 * (noteL.maxLine - noteU.line + 1));
if (foundDots === noteU.note.dots) {
break;
}
}
}
}
} else if (lineDiff < 1 && lineDiff > 0) {//if the notes are quite close but not on the same line, shift
noteL.note.setXShift(xShift);
} else if (noteU.note.voice !== noteL.note.voice) {//If we are not in the same voice
if (noteU.stemDirection === noteL.stemDirection) {
if (noteU.line > noteL.line) {
//noteU is above noteL
if (noteU.stemDirection === 1) {
noteL.note.renderFlag = false;
} else {
noteU.note.renderFlag = false;
}
} else if (noteL.line > noteU.line) {
//note L is above noteU
if (noteL.stemDirection === 1) {
noteU.note.renderFlag = false;
} else {
noteL.note.renderFlag = false;
}
} else {
//same line, swap stem direction for one note
if (noteL.stemDirection === 1) {
noteL.stemDirection = -1;
noteL.note.setStemDirection(-1);
}
}
}
}
//Very close whole notes
// } else if ((!noteU.note.glyph.stem && !noteL.note.glyph.stem && lineDiff < 1.5)) {
// noteL.note.setXShift(xShift);
// }
}
}
// format complete
return true;
}
// Check middle voice stem intersection with lower voice
if (noteM !== null && noteM.minLine < noteL.maxLine + 0.5) {
if (!noteM.isrest) {
stemDelta = Math.abs(noteM.line - (noteL.maxLine + 0.5));
stemDelta = Math.max(stemDelta, noteM.stemMin);
noteM.minLine = noteM.line - stemDelta;
noteM.note.setStemLength(stemDelta * 10);
}
}
// For three voices, test if rests can be repositioned
//
// Special case 1 :: middle voice rest between two notes
//
if (noteM.isrest && !noteU.isrest && !noteL.isrest) {
if (noteU.minLine <= noteM.maxLine || noteM.minLine <= noteL.maxLine) {
const restHeight = noteM.maxLine - noteM.minLine;
const space = noteU.minLine - noteL.maxLine;
if (restHeight < space) {
// center middle voice rest between the upper and lower voices
centerRest(noteM, noteU, noteL);
} else {
xShift = voiceXShift + 3; // shift middle rest right
noteM.note.setXShift(xShift);
}
// format complete
return true;
}
}
// Special case 2 :: all voices are rests
if (noteU.isrest && noteM.isrest && noteL.isrest) {
// Shift upper voice rest up
shiftRestVertical(noteU, noteM, 1);
// Shift lower voice rest down
shiftRestVertical(noteL, noteM, -1);
// format complete
return true;
}
// Test if any other rests can be repositioned
if (noteM.isrest && noteU.isrest && noteM.minLine <= noteL.maxLine) {
// Shift middle voice rest up
shiftRestVertical(noteM, noteL, 1);
}
if (noteM.isrest && noteL.isrest && noteU.minLine <= noteM.maxLine) {
// Shift middle voice rest down
shiftRestVertical(noteM, noteU, -1);
}
if (noteU.isrest && noteU.minLine <= noteM.maxLine) {
// shift upper voice rest up;
shiftRestVertical(noteU, noteM, 1);
}
if (noteL.isrest && noteM.minLine <= noteL.maxLine) {
// shift lower voice rest down
shiftRestVertical(noteL, noteM, -1);
}
// If middle voice intersects upper or lower voice
if ((!noteU.isrest && !noteM.isrest && noteU.minLine <= noteM.maxLine + 0.5) ||
(!noteM.isrest && !noteL.isrest && noteM.minLine <= noteL.maxLine)) {
xShift = voiceXShift + 3; // shift middle note right
noteM.note.setXShift(xShift);
}
return true;
}
static formatByY(notes, state) {
// NOTE: this function does not support more than two voices per stave
// use with care.
let hasStave = true;
for (let i = 0; i < notes.length; i++) {
hasStave = hasStave && notes[i].getStave() != null;
}
if (!hasStave) {
throw new Vex.RERR(
'Stave Missing',
'All notes must have a stave - Vex.Flow.ModifierContext.formatMultiVoice!'
);
}
let xShift = 0;
for (let i = 0; i < notes.length - 1; i++) {
let topNote = notes[i];
let bottomNote = notes[i + 1];
//Vexflowpatch: The stem direction doesn't really determine which note is on top.
//Pick the actual note that is on top via the line number
if (topNote.maxLine < bottomNote.maxLine) {
topNote = notes[i + 1];
bottomNote = notes[i];
}
const topKeys = topNote.getKeyProps();
const bottomKeys = bottomNote.getKeyProps();
const HALF_NOTEHEAD_HEIGHT = 0.5;
// `keyProps` and `stave.getYForLine` have different notions of a `line`
// so we have to convert the keyProps value by subtracting 5.
// See https://github.com/0xfe/vexflow/wiki/Development-Gotchas
//
// We also extend the y for each note by a half notehead because the
// notehead's origin is centered
const topNoteBottomY = topNote
.getStave()
.getYForLine(5 - topKeys[0].line + HALF_NOTEHEAD_HEIGHT);
const bottomNoteTopY = bottomNote
.getStave()
.getYForLine(5 - bottomKeys[bottomKeys.length - 1].line - HALF_NOTEHEAD_HEIGHT);
const areNotesColliding = bottomNoteTopY - topNoteBottomY < 0;
if (areNotesColliding) {
//Vexflowpatch: Only shift if we are in the same voice. This is mostly taken care of format() above.
if (topNote.voice === bottomNote.voice) {
xShift = topNote.getVoiceShiftWidth() + 2;
bottomNote.setXShift(xShift);
}
}
}
state.right_shift += xShift;
}
static postFormat(notes) {
if (!notes) return false;
notes.forEach(note => note.postFormat());
return true;
}
constructor(noteStruct) {
super(noteStruct);
this.setAttribute('type', 'StaveNote');
this.keys = noteStruct.keys;
this.clef = noteStruct.clef;
this.octave_shift = noteStruct.octave_shift;
this.beam = null;
// Pull note rendering properties
this.glyph = Flow.getGlyphProps(this.duration, this.noteType);
if (!this.glyph) {
throw new Vex.RuntimeError(
'BadArguments',
`Invalid note initialization data (No glyph found): ${JSON.stringify(noteStruct)}`
);
}
// if true, displace note to right
this.displaced = false;
this.dot_shiftY = 0;
//VexflowPatch: We seem to init with a dot count, and also call addDot, so this is to count the added dots vs. the inited dots.
this.addDotsCount = 0;
// per-pitch properties
this.keyProps = [];
// for displaced ledger lines
this.use_default_head_x = false;
// VexFlowPatch: add optional padding to the right (e.g. for large lyrics)
this.paddingRight = 0;
// Drawing
this.note_heads = [];
this.modifiers = [];
Vex.Merge(this.render_options, {
// font size for note heads and rests
glyph_font_scale: noteStruct.glyph_font_scale || Flow.DEFAULT_NOTATION_FONT_SCALE,
// number of stroke px to the left and right of head
stroke_px: noteStruct.stroke_px || StaveNote.DEFAULT_LEDGER_LINE_OFFSET,
});
this.calculateKeyProps();
this.buildStem();
// Set the stem direction
if (noteStruct.auto_stem) {
this.autoStem();
} else {
this.setStemDirection(noteStruct.stem_direction);
}
this.reset();
this.buildFlag();
}
reset() {
super.reset();
// Save prior noteHead styles & reapply them after making new noteheads.
const noteHeadStyles = this.note_heads.map(noteHead => noteHead.getStyle());
// VexFlowPatch: save and restore noteheads (e.g. slash noteheads)
const note_types = [];
this.note_heads.forEach(head => note_types.push(head.note_type));
this.buildNoteHeads();
this.note_heads.forEach((noteHead, index) => {
noteHead.setStyle(noteHeadStyles[index]);
if (note_types[index]) {
noteHead.note_type = note_types[index];
}
});
if (this.stave) {
this.note_heads.forEach(head => head.setStave(this.stave));
}
this.calcExtraPx();
}
setBeam(beam) {
this.beam = beam;
this.calcExtraPx();
return this;
}
getCategory() { return StaveNote.CATEGORY; }
// Builds a `Stem` for the note
buildStem() {
this.setStem(new Stem({ hide: !!this.isRest(), }));
this.stem.id = Vex.Prefix(`${this.getAttribute("id")}-stem`);
}
// Builds a `NoteHead` for each key in the note
buildNoteHeads() {
this.note_heads = [];
const stemDirection = this.getStemDirection();
const keys = this.getKeys();
let lastLine = null;
let lineDiff = null;
let displaced = false;
// Draw notes from bottom to top.
// For down-stem notes, we draw from top to bottom.
let start;
let end;
let step;
if (stemDirection === Stem.UP) {
start = 0;
end = keys.length;
step = 1;
} else if (stemDirection === Stem.DOWN) {
start = keys.length - 1;
end = -1;
step = -1;
}
for (let i = start; i !== end; i += step) {
const noteProps = this.keyProps[i];
const line = noteProps.line;
// Keep track of last line with a note head, so that consecutive heads
// are correctly displaced.
if (lastLine === null) {
lastLine = line;
} else {
lineDiff = Math.abs(lastLine - line);
if (lineDiff === 0 || lineDiff === 0.5) {
displaced = !displaced;
} else {
displaced = false;
this.use_default_head_x = true;
}
}
lastLine = line;
const notehead = new NoteHead({
duration: this.duration,
note_type: this.noteType,
displaced,
stem_direction: stemDirection,
custom_glyph_code: noteProps.code,
glyph_font_scale: this.render_options.glyph_font_scale,
x_shift: noteProps.shift_right,
stem_up_x_offset: noteProps.stem_up_x_offset,
stem_down_x_offset: noteProps.stem_down_x_offset,
// VexFlowPatch: add option to shift notehead up or down (instead of stem in the variables above)
stem_up_y_shift: noteProps.stem_up_y_shift,
stem_down_y_shift: noteProps.stem_down_y_shift,
line: noteProps.line,
});
if (notehead.isLedgerLinedRest) {
this.hasLedgerLinedRest = true;
}
this.note_heads[i] = notehead;
}
}
// Automatically sets the stem direction based on the keys in the note
autoStem() {
// Figure out optimal stem direction based on given notes
this.minLine = this.keyProps[0].line;
this.maxLine = this.keyProps[this.keyProps.length - 1].line;
const MIDDLE_LINE = 3;
const decider = (this.minLine + this.maxLine) / 2;
const stemDirection = decider < MIDDLE_LINE ? Stem.UP : Stem.DOWN;
this.setStemDirection(stemDirection);
}
// Calculates and stores the properties for each key in the note
calculateKeyProps() {
let lastLine = null;
for (let i = 0; i < this.keys.length; ++i) {
const key = this.keys[i];
// All rests use the same position on the line.
// if (this.glyph.rest) key = this.glyph.position;
if (this.glyph.rest) this.glyph.position = key;
const options = { octave_shift: this.octave_shift || 0 };
const props = Flow.keyProperties(key, this.clef, options);
if (!props) {
throw new Vex.RuntimeError('BadArguments', `Invalid key for note properties: ${key}`);
}
// Override line placement for default rests
if (props.key === 'R') {
if (this.duration === '1' || this.duration === 'w') {
props.line = 4;
} else {
props.line = 3;
}
}
// Calculate displacement of this note
const line = props.line;
if (lastLine === null) {
lastLine = line;
} else {
if (Math.abs(lastLine - line) === 0.5) {
this.displaced = true;
props.displaced = true;
// Have to mark the previous note as
// displaced as well, for modifier placement
if (this.keyProps.length > 0) {
this.keyProps[i - 1].displaced = true;
}
}
}
lastLine = line;
this.keyProps.push(props);
}
// Sort the notes from lowest line to highest line
lastLine = -Infinity;
this.keyProps.forEach(key => {
if (key.line < lastLine) {
Vex.W(
'Unsorted keys in note will be sorted. ' +
'See https://github.com/0xfe/vexflow/issues/104 for details.'
);
}
lastLine = key.line;
});
this.keyProps.sort((a, b) => a.line - b.line);
}
// Get the `BoundingBox` for the entire note
getBoundingBox() {
if (!this.preFormatted) {
throw new Vex.RERR('UnformattedNote', "Can't call getBoundingBox on an unformatted note.");
}
const { width: w, modLeftPx, extraLeftPx } = this.getMetrics();
// VexFlowPatch: also subtract paddingRight (newly added in VexFlowPatch) to not shift note bbox
const x = this.getAbsoluteX() - modLeftPx - extraLeftPx - this.paddingRight;
let minY = 0;
let maxY = 0;
const halfLineSpacing = this.getStave().getSpacingBetweenLines() / 2;
const lineSpacing = halfLineSpacing * 2;
if (this.isRest()) {
const y = this.ys[0];
const frac = Flow.durationToFraction(this.duration);
if (frac.equals(1) || frac.equals(2)) {
minY = y - halfLineSpacing;
maxY = y + halfLineSpacing;
} else {
minY = y - (this.glyph.line_above * lineSpacing);
maxY = y + (this.glyph.line_below * lineSpacing);
}
} else if (this.glyph.stem) {
const ys = this.getStemExtents();
ys.baseY += halfLineSpacing * this.stem_direction;
minY = Math.min(ys.topY, ys.baseY);
maxY = Math.max(ys.topY, ys.baseY);
} else {
minY = null;
maxY = null;
for (let i = 0; i < this.ys.length; ++i) {
const yy = this.ys[i];
if (i === 0) {
minY = yy;
maxY = yy;
} else {
minY = Math.min(yy, minY);
maxY = Math.max(yy, maxY);
}
}
minY -= halfLineSpacing;
maxY += halfLineSpacing;
}
return new BoundingBox(x, minY, w, maxY - minY);
}
// Gets the line number of the top or bottom note in the chord.
// If `isTopNote` is `true` then get the top note
getLineNumber(isTopNote) {
if (!this.keyProps.length) {
throw new Vex.RERR(
'NoKeyProps', "Can't get bottom note line, because note is not initialized properly."
);
}
let resultLine = this.keyProps[0].line;
// No precondition assumed for sortedness of keyProps array
for (let i = 0; i < this.keyProps.length; i++) {
const thisLine = this.keyProps[i].line;
if (isTopNote) {
if (thisLine > resultLine) resultLine = thisLine;
} else {
if (thisLine < resultLine) resultLine = thisLine;
}
}
return resultLine;
}
// Determine if current note is a rest
isRest() { return this.glyph.rest; }
// Determine if the current note is a chord
isChord() { return !this.isRest() && this.keys.length > 1; }
// Determine if the `StaveNote` has a stem
hasStem() { return this.glyph.stem; }
hasFlag() {
return super.hasFlag() && !this.isRest() && this.renderFlag;
}
getStemX() {
if (this.noteType === 'r') {
return this.getCenterGlyphX();
} else {
// We adjust the origin of the stem because we want the stem left-aligned
// with the notehead if stemmed-down, and right-aligned if stemmed-up
return super.getStemX() + getStemAdjustment(this);
}
}
// Get the `y` coordinate for text placed on the top/bottom of a
// note at a desired `text_line`
getYForTopText(textLine) {
const extents = this.getStemExtents();
return Math.min(
this.stave.getYForTopText(textLine),
extents.topY - (this.render_options.annotation_spacing * (textLine + 1))
);
}
getYForBottomText(textLine) {
const extents = this.getStemExtents();
return Math.max(
this.stave.getYForTopText(textLine),
extents.baseY + (this.render_options.annotation_spacing * (textLine))
);
}
// Sets the current note to the provided `stave`. This applies
// `y` values to the `NoteHeads`.
setStave(stave) {
super.setStave(stave);
const ys = this.note_heads.map(notehead => {
notehead.setStave(stave);
return notehead.getY();
});
this.setYs(ys);
if (this.stem) {
const { y_top, y_bottom } = this.getNoteHeadBounds();
this.stem.setYBounds(y_top, y_bottom);
}
return this;
}
// Get the pitches in the note
getKeys() { return this.keys; }
// Get the properties for all the keys in the note
getKeyProps() {
return this.keyProps;
}
// Check if note is shifted to the right
isDisplaced() {
return this.displaced;
}
// Sets whether shift note to the right. `displaced` is a `boolean`
setNoteDisplaced(displaced) {
this.displaced = displaced;
return this;
}
// Get the starting `x` coordinate for a `StaveTie`
getTieRightX() {
let tieStartX = this.getAbsoluteX();
tieStartX += this.getGlyphWidth() + this.x_shift + this.extraRightPx;
if (this.modifierContext) tieStartX += this.modifierContext.getExtraRightPx();
return tieStartX;
}
// Get the ending `x` coordinate for a `StaveTie`
getTieLeftX() {
let tieEndX = this.getAbsoluteX();
tieEndX += this.x_shift - this.extraLeftPx;
return tieEndX;
}
// Get the stave line on which to place a rest
getLineForRest() {
let restLine = this.keyProps[0].line;
if (this.keyProps.length > 1) {
const lastLine = this.keyProps[this.keyProps.length - 1].line;
const top = Math.max(restLine, lastLine);
const bot = Math.min(restLine, lastLine);
restLine = Vex.MidLine(top, bot);
}
return restLine;
}
// Get the default `x` and `y` coordinates for the provided `position`
// and key `index`
getModifierStartXY(position, index, options) {
options = options || {};
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.');
}
const { ABOVE, BELOW, LEFT, RIGHT } = Modifier.Position;
let x = 0;
if (position === LEFT) {
// extra_left_px
// FIXME: What are these magic numbers?
x = -1 * 2;
} else if (position === RIGHT) {
// extra_right_px
// FIXME: What is this magical +2?
x = this.getGlyphWidth() + this.x_shift + 2;
if (this.stem_direction === Stem.UP && this.hasFlag() &&
(options.forceFlagRight || isInnerNoteIndex(this, index))) {
x += this.flag.getMetrics().width;
}
} else if (position === BELOW || position === ABOVE) {
x = this.getGlyphWidth() / 2;
}
return {
x: this.getAbsoluteX() + x,
y: this.ys[index],
};
}
// Sets the style of the complete StaveNote, including all keys
// and the stem.
setStyle(style) {
super.setStyle(style);
this.note_heads.forEach(notehead => notehead.setStyle(style));
if (this.stem){
this.stem.setStyle(style);
}
}
setStemStyle(style) {
if (this.stem){
const stem = this.getStem();
stem.setStyle(style);
}
}
getStemStyle() { return this.stem.getStyle(); }
setLedgerLineStyle(style) { this.ledgerLineStyle = style; }
getLedgerLineStyle() { return this.ledgerLineStyle; }
setFlagStyle(style) { this.flagStyle = style; }
getFlagStyle() { return this.flagStyle; }
// Sets the notehead at `index` to the provided coloring `style`.
//
// `style` is an `object` with the following properties: `shadowColor`,
// `shadowBlur`, `fillStyle`, `strokeStyle`
setKeyStyle(index, style) {
this.note_heads[index].setStyle(style);
return this;
}
setKeyLine(index, line) {
this.keyProps[index].line = line;
this.reset();
return this;
}
getKeyLine(index) {
return this.keyProps[index].line;
}
// Add self to modifier context. `mContext` is the `ModifierContext`
// to be added to.
addToModifierContext(mContext) {
this.setModifierContext(mContext);
for (let i = 0; i < this.modifiers.length; ++i) {
this.modifierContext.addModifier(this.modifiers[i]);
}
this.modifierContext.addModifier(this);
this.setPreFormatted(false);
return this;
}
// Generic function to add modifiers to a note
//
// Parameters:
// * `index`: The index of the key that we're modifying
// * `modifier`: The modifier to add
addModifier(index, modifier) {
modifier.setNote(this);
modifier.setIndex(index);
this.modifiers.push(modifier);
this.setPreFormatted(false);
return this;
}
// Helper function to add an accidental to a key
addAccidental(index, accidental) {
return this.addModifier(index, accidental);
}
// Helper function to add an articulation to a key
addArticulation(index, articulation) {
return this.addModifier(index, articulation);
}
// Helper function to add an annotation to a key
addAnnotation(index, annotation) {
return this.addModifier(index, annotation);
}
// Helper function to add a dot on a specific key
addDot(index) {
const dot = new Dot();
dot.setDotShiftY(this.glyph.dot_shiftY);
this.addDotsCount++;
return this.addModifier(index, dot);
}
// Convenience method to add dot to all keys in note
addDotToAll() {
for (let i = 0; i < this.keys.length; ++i) {
this.addDot(i);
}
return this;
}
// Get all accidentals in the `ModifierContext`
getAccidentals() {
return this.modifierContext.getModifiers('accidentals');
}
// Get all dots in the `ModifierContext`
getDots() {
return this.modifierContext.getModifiers('dots');
}
// Get the width of the note if it is displaced. Used for `Voice`
// formatting
getVoiceShiftWidth() {
// TODO: may need to accomodate for dot here.
return this.getGlyphWidth() * (this.displaced ? 2 : 1);
}
// Calculates and sets the extra pixels to the left or right
// if the note is displaced.
calcExtraPx() {
this.setExtraLeftPx(
this.displaced && this.stem_direction === Stem.DOWN
? this.getGlyphWidth()
: 0
);
// For upstems with flags, the extra space is unnecessary, since it's taken
// up by the flag.
this.setExtraRightPx(
!this.hasFlag() && this.displaced && this.stem_direction === Stem.UP
? this.getGlyphWidth()
: 0
);
}
// Pre-render formatting
preFormat() {
if (this.preFormatted) return;
if (this.modifierContext) this.modifierContext.preFormat();
// VexFlowPatch: add optional padding to the right (e.g. for large lyrics), default 0.
let width = this.getGlyphWidth() + this.extraLeftPx + this.extraRightPx + this.paddingRight;
// For upward flagged notes, the width of the flag needs to be added
if (this.renderFlag && this.glyph.flag && this.beam === null && this.stem_direction === Stem.UP) {
width += this.getGlyphWidth();
}
this.setWidth(width);
this.setPreFormatted(true);
}
/**
* @typedef {Object} noteHeadBounds
* @property {number} y_top the highest notehead bound
* @property {number} y_bottom the lowest notehead bound
* @property {number|Null} displaced_x the starting x for displaced noteheads
* @property {number|Null} non_displaced_x the starting x for non-displaced noteheads
* @property {number} highest_line the highest notehead line in traditional music line
* numbering (bottom line = 1, top line = 5)
* @property {number} lowest_line the lowest notehead line
* @property {number|false} highest_displaced_line the highest staff line number
* for a displaced notehead
* @property {number|false} lowest_displaced_line
* @property {number} highest_non_displaced_line
* @property {number} lowest_non_displaced_line
*/
/**
* Get the staff line and y value for the highest & lowest noteheads
* @returns {noteHeadBounds}
*/
getNoteHeadBounds() {
// Top and bottom Y values for stem.
let yTop = null;
let yBottom = null;
let nonDisplacedX = null;
let displacedX = null;
let highestLine = this.stave.getNumLines();
let lowestLine = 1;
let highestDisplacedLine = false;
let lowestDisplacedLine = false;
let highestNonDisplacedLine = highestLine;
let lowestNonDisplacedLine = lowestLine;
this.note_heads.forEach(notehead => {
const line = notehead.getLine();
const y = notehead.getY();
if (yTop === null || y < yTop) {
yTop = y;
}
if (yBottom === null || y > yBottom) {
yBottom = y;
}
if (displacedX === null && notehead.isDisplaced()) {
displacedX = notehead.getAbsoluteX();
}
if (nonDisplacedX === null && !notehead.isDisplaced()) {
nonDisplacedX = notehead.getAbsoluteX();
}
highestLine = line > highestLine ? line : highestLine;
lowestLine = line < lowestLine ? line : lowestLine;
if (notehead.isDisplaced()) {
highestDisplacedLine = (highestDisplacedLine === false) ?
line : Math.max(line, highestDisplacedLine);
lowestDisplacedLine = (lowestDisplacedLine === false) ?
line : Math.min(line, lowestDisplacedLine);
} else {
highestNonDisplacedLine = Math.max(line, highestNonDisplacedLine);
lowestNonDisplacedLine = Math.min(line, lowestNonDisplacedLine);
}
}, this);
return {
y_top: yTop,
y_bottom: yBottom,
displaced_x: displacedX,
non_displaced_x: nonDisplacedX,
highest_line: highestLine,
lowest_line: lowestLine,
highest_displaced_line: highestDisplacedLine,
lowest_displaced_line: lowestDisplacedLine,
highest_non_displaced_line: highestNonDisplacedLine,
lowest_non_displaced_line: lowestNonDisplacedLine,
};
}
// Get the starting `x` coordinate for the noteheads
getNoteHeadBeginX() {
return this.getAbsoluteX() + this.x_shift;
}
// Get the ending `x` coordinate for the noteheads
getNoteHeadEndX() {
const xBegin = this.getNoteHeadBeginX();
return xBegin + this.getGlyphWidth();
}
// Draw the ledger lines between the stave and the highest/lowest keys
drawLedgerLines() {
const {
stave, glyph,
render_options: { stroke_px },
context: ctx,
} = this;
const width = glyph.getWidth() + (stroke_px * 2);
const doubleWidth = 2 * (glyph.getWidth() + stroke_px) - (Stem.WIDTH / 2);
if (this.isRest()) return;
if (!ctx) {
throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
}
const {
highest_line,
lowest_line,
highest_displaced_line,
highest_non_displaced_line,
lowest_displaced_line,
lowest_non_displaced_line,
displaced_x,
non_displaced_x,
} = this.getNoteHeadBounds();
const min_x = Math.min(displaced_x, non_displaced_x);
const drawLedgerLine = (y, normal, displaced) => {
let x;
if (displaced && normal) x = min_x - stroke_px;
else if (normal) x = non_displaced_x - stroke_px;
else x = displaced_x - stroke_px;
const ledgerWidth = (normal && displaced) ? doubleWidth : width;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + ledgerWidth, y);
ctx.stroke();
};
const style = { ...stave.getStyle() || {}, ...this.getLedgerLineStyle() || {} };
this.applyStyle(ctx, style);
// Draw ledger lines below the staff:
for (let line = 6; line <= highest_line; ++line) {
const normal = (non_displaced_x !== null) && (line <= highest_non_displaced_line);
const displaced = (displaced_x !== null) && (line <= highest_displaced_line);
drawLedgerLine(stave.getYForNote(line), normal, displaced);
}
// Draw ledger lines above the staff:
for (let line = 0; line >= lowest_line; --line) {
const normal = (non_displaced_x !== null) && (line >= lowest_non_displaced_line);
const displaced = (displaced_x !== null) && (line >= lowest_displaced_line);
drawLedgerLine(stave.getYForNote(line), normal, displaced);
}
this.restoreStyle(ctx, style);
}
// Draw all key modifiers
drawModifiers() {
if (!this.context) {
throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
}
const ctx = this.context;
ctx.openGroup('modifiers');
for (let i = 0; i < this.modifiers.length; i++) {
const modifier = this.modifiers[i];
const notehead = this.note_heads[modifier.getIndex()];
const noteheadStyle = notehead.getStyle();
notehead.applyStyle(ctx, noteheadStyle);
modifier.setContext(ctx);
modifier.drawWithStyle();
notehead.restoreStyle(ctx, noteheadStyle);
}
ctx.closeGroup();
}
// Draw the flag for the note
drawFlag() {
const { stem, beam, context: ctx } = this;
if (!ctx) {
throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
}
const shouldRenderFlag = beam === null && this.renderFlag;
const glyph = this.getGlyph();
if (glyph.flag && shouldRenderFlag) {
const { y_top, y_bottom } = this.getNoteHeadBounds();
const noteStemHeight = stem.getHeight();
const flagX = this.getStemX();
// FIXME: What's with the magic +/- 2
const flagY = this.getStemDirection() === Stem.DOWN
// Down stems have flags on the left
? y_top - noteStemHeight + 2
// Up stems have flags on the eft.
: y_bottom - noteStemHeight - 2;
// Draw the Flag
ctx.openGroup('flag', null, { pointerBBox: true });
this.applyStyle(ctx, this.getFlagStyle() || false);
this.flag.render(ctx, flagX, flagY);
this.restoreStyle(ctx, this.getFlagStyle() || false);
ctx.closeGroup();
}
}
// Draw the NoteHeads
drawNoteHeads() {
this.note_heads.forEach(notehead => {
this.context.openGroup('notehead', null, { pointerBBox: true });
notehead.setContext(this.context).draw();
this.context.closeGroup();
});
}
drawStem(stemStruct) {
// GCR TODO: I can't find any context in which this is called with the stemStruct
// argument in the codebase or tests. Nor can I find a case where super.drawStem
// is called at all. Perhaps these should be removed?
if (!this.context) {
throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
}
if (stemStruct) {
this.setStem(new Stem(stemStruct));
}
// seems to not get called here, see this.stem.id above
this.stem.id = Vex.Prefix(`${this.getAttribute("id")}-stem`);
if (this.stem) {
this.context.openGroup('stem', null, { pointerBBox: true });
this.stem.setContext(this.context).draw();
this.context.closeGroup();
}
}
// Draws all the `StaveNote` parts. This is the main drawing method.
draw() {
if (!this.context) {
throw new Vex.RERR('NoCanvasContext', "Can't draw without a canvas context.");
}
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 xBegin = this.getNoteHeadBeginX();
const shouldRenderStem = this.hasStem() && !this.beam;
// Format note head x positions
this.note_heads.forEach(notehead => notehead.setX(xBegin));
if(this.stem) {
// Format stem x positions
const stemX = this.getStemX();
this.stem.setNoteHeadXBounds(stemX, stemX);
}
L('Rendering ', this.isChord() ? 'chord :' : 'note :', this.keys);
// Draw each part of the note
this.drawLedgerLines();
// Apply the overall style -- may be contradicted by local settings:
this.applyStyle();
this.setAttribute('el', this.context.openGroup('stavenote', this.getAttribute('id')));
this.context.openGroup('note', null, { pointerBBox: true });
if (shouldRenderStem) this.drawStem();
this.drawNoteHeads();
this.drawFlag();
this.context.closeGroup();
this.drawModifiers();
this.context.closeGroup();
this.restoreStyle();
this.setRendered();
}
}