vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
1,060 lines (1,024 loc) • 1.88 MB
JavaScript
/*!
* VexFlow 4.2.1 2023-06-21T20:24:17.916Z 6c05deea44902a09a017507a8ddc810d2e2a9922
* Copyright (c) 2010 Mohit Muthanna Cheppudira <mohit@muthanna.com>
* https://www.vexflow.com https://github.com/0xfe/vexflow
*/
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["Vex"] = factory();
else
root["Vex"] = factory();
})((typeof window !== 'undefined' ? window : typeof globalThis !== 'undefined' ? globalThis : this), () => {
return /******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./src/version.ts":
/*!************************!*\
!*** ./src/version.ts ***!
\************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "DATE": () => (/* binding */ DATE),
/* harmony export */ "ID": () => (/* binding */ ID),
/* harmony export */ "VERSION": () => (/* binding */ VERSION)
/* harmony export */ });
const VERSION = '4.2.1';
const ID = '6c05deea44902a09a017507a8ddc810d2e2a9922';
const DATE = '2023-06-21T20:24:17.916Z';
/***/ }),
/***/ "./src/accidental.ts":
/*!***************************!*\
!*** ./src/accidental.ts ***!
\***************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "Accidental": () => (/* binding */ Accidental)
/* harmony export */ });
/* harmony import */ var _fraction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./fraction */ "./src/fraction.ts");
/* harmony import */ var _glyph__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./glyph */ "./src/glyph.ts");
/* harmony import */ var _modifier__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./modifier */ "./src/modifier.ts");
/* harmony import */ var _music__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./music */ "./src/music.ts");
/* harmony import */ var _tables__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./tables */ "./src/tables.ts");
/* harmony import */ var _typeguard__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./typeguard */ "./src/typeguard.ts");
/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./util */ "./src/util.ts");
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
// MIT License
// @author Mohit Cheppudira
// @author Greg Ristow (modifications)
// eslint-disable-next-line
function L(...args) {
if (Accidental.DEBUG)
(0,_util__WEBPACK_IMPORTED_MODULE_6__.log)('Vex.Flow.Accidental', args);
}
/**
* An `Accidental` inherits from `Modifier`, and is formatted within a
* `ModifierContext`. Accidentals are modifiers that can be attached to
* notes. Support is included for both western and microtonal accidentals.
*
* See `tests/accidental_tests.ts` for usage examples.
*/
class Accidental extends _modifier__WEBPACK_IMPORTED_MODULE_2__.Modifier {
/** Accidentals category string. */
static get CATEGORY() {
return _typeguard__WEBPACK_IMPORTED_MODULE_5__.Category.Accidental;
}
/** Arrange accidentals inside a ModifierContext. */
static format(accidentals, state) {
// If there are no accidentals, no need to format their positions.
if (!accidentals || accidentals.length === 0)
return;
const musicFont = _tables__WEBPACK_IMPORTED_MODULE_4__.Tables.currentMusicFont();
const noteheadAccidentalPadding = musicFont.lookupMetric('accidental.noteheadAccidentalPadding');
const leftShift = state.left_shift + noteheadAccidentalPadding;
const accidentalSpacing = musicFont.lookupMetric('accidental.accidentalSpacing');
const additionalPadding = musicFont.lookupMetric('accidental.leftPadding'); // padding to the left of all accidentals
const accList = [];
let prevNote = undefined;
let shiftL = 0;
// First determine the accidentals' Y positions from the note.keys
for (let i = 0; i < accidentals.length; ++i) {
const acc = accidentals[i];
const note = acc.getNote();
const stave = note.getStave();
const index = acc.checkIndex();
const props = note.getKeyProps()[index];
if (note !== prevNote) {
// Iterate through all notes to get the displaced pixels
for (let n = 0; n < note.keys.length; ++n) {
shiftL = Math.max(note.getLeftDisplacedHeadPx() - note.getXShift(), shiftL);
}
prevNote = note;
}
if (stave) {
const lineSpace = stave.getSpacingBetweenLines();
const y = stave.getYForLine(props.line);
const accLine = Math.round((y / lineSpace) * 2) / 2;
accList.push({ y, line: accLine, shift: shiftL, acc, lineSpace });
}
else {
accList.push({ line: props.line, shift: shiftL, acc });
}
}
// Sort accidentals by line number.
accList.sort((a, b) => b.line - a.line);
// FIXME: Confusing name. Each object in this array has a property called `line`.
// So if this is a list of lines, you end up with: `line.line` which is very awkward.
const lineList = [];
// amount by which all accidentals must be shifted right or left for
// stem flipping, notehead shifting concerns.
let accShift = 0;
let previousLine = undefined;
// Create an array of unique line numbers (lineList) from accList
for (let i = 0; i < accList.length; i++) {
const acc = accList[i];
// if this is the first line, or a new line, add a lineList
if (previousLine === undefined || previousLine !== acc.line) {
lineList.push({
line: acc.line,
flatLine: true,
dblSharpLine: true,
numAcc: 0,
width: 0,
column: 0,
});
}
// if this accidental is not a flat, the accidental needs 3.0 lines lower
// clearance instead of 2.5 lines for b or bb.
// FIXME: Naming could use work. acc.acc is very awkward
if (acc.acc.type !== 'b' && acc.acc.type !== 'bb') {
lineList[lineList.length - 1].flatLine = false;
}
// if this accidental is not a double sharp, the accidental needs 3.0 lines above
if (acc.acc.type !== '##') {
lineList[lineList.length - 1].dblSharpLine = false;
}
// Track how many accidentals are on this line:
lineList[lineList.length - 1].numAcc++;
// Track the total x_offset needed for this line which will be needed
// for formatting lines w/ multiple accidentals:
// width = accidental width + universal spacing between accidentals
lineList[lineList.length - 1].width += acc.acc.getWidth() + accidentalSpacing;
// if this accShift is larger, use it to keep first column accidentals in the same line
accShift = acc.shift > accShift ? acc.shift : accShift;
previousLine = acc.line;
}
// ### Place Accidentals in Columns
//
// Default to a classic triangular layout (middle accidental farthest left),
// but follow exceptions as outlined in G. Read's _Music Notation_ and
// Elaine Gould's _Behind Bars_.
//
// Additionally, this implements different vertical collision rules for
// flats (only need 2.5 lines clearance below) and double sharps (only
// need 2.5 lines of clearance above or below).
//
// Classic layouts and exception patterns are found in the 'tables.js'
// in 'Tables.accidentalColumnsTable'
//
// Beyond 6 vertical accidentals, default to the parallel ascending lines approach,
// using as few columns as possible for the verticle structure.
//
// TODO (?): Allow column to be specified for an accidental at run-time?
let totalColumns = 0;
// establish the boundaries for a group of notes with clashing accidentals:
for (let i = 0; i < lineList.length; i++) {
let noFurtherConflicts = false;
const groupStart = i;
let groupEnd = i;
while (groupEnd + 1 < lineList.length && !noFurtherConflicts) {
// if this note conflicts with the next:
if (this.checkCollision(lineList[groupEnd], lineList[groupEnd + 1])) {
// include the next note in the group:
groupEnd++;
}
else {
noFurtherConflicts = true;
}
}
// Gets an a line from the `lineList`, relative to the current group
const getGroupLine = (index) => lineList[groupStart + index];
const getGroupLines = (indexes) => indexes.map(getGroupLine);
const lineDifference = (indexA, indexB) => {
const [a, b] = getGroupLines([indexA, indexB]).map((item) => item.line);
return a - b;
};
const notColliding = (...indexPairs) => indexPairs.map(getGroupLines).every(([line1, line2]) => !this.checkCollision(line1, line2));
// Set columns for the lines in this group:
const groupLength = groupEnd - groupStart + 1;
// Set the accidental column for each line of the group
let endCase = this.checkCollision(lineList[groupStart], lineList[groupEnd]) ? 'a' : 'b';
switch (groupLength) {
case 3:
if (endCase === 'a' && lineDifference(1, 2) === 0.5 && lineDifference(0, 1) !== 0.5) {
endCase = 'second_on_bottom';
}
break;
case 4:
if (notColliding([0, 2], [1, 3])) {
endCase = 'spaced_out_tetrachord';
}
break;
case 5:
if (endCase === 'b' && notColliding([1, 3])) {
endCase = 'spaced_out_pentachord';
if (notColliding([0, 2], [2, 4])) {
endCase = 'very_spaced_out_pentachord';
}
}
break;
case 6:
if (notColliding([0, 3], [1, 4], [2, 5])) {
endCase = 'spaced_out_hexachord';
}
if (notColliding([0, 2], [2, 4], [1, 3], [3, 5])) {
endCase = 'very_spaced_out_hexachord';
}
break;
default:
break;
}
let groupMember;
let column;
// If the group contains seven members or more, use ascending parallel lines
// of accidentals, using as few columns as possible while avoiding collisions.
if (groupLength >= 7) {
// First, determine how many columns to use:
let patternLength = 2;
let collisionDetected = true;
while (collisionDetected === true) {
collisionDetected = false;
for (let line = 0; line + patternLength < lineList.length; line++) {
if (this.checkCollision(lineList[line], lineList[line + patternLength])) {
collisionDetected = true;
patternLength++;
break;
}
}
}
// Then, assign a column to each line of accidentals
for (groupMember = i; groupMember <= groupEnd; groupMember++) {
column = ((groupMember - i) % patternLength) + 1;
lineList[groupMember].column = column;
totalColumns = totalColumns > column ? totalColumns : column;
}
}
else {
// If the group contains fewer than seven members, use the layouts from
// the Tables.accidentalColumnsTable (See: tables.ts).
for (groupMember = i; groupMember <= groupEnd; groupMember++) {
column = _tables__WEBPACK_IMPORTED_MODULE_4__.Tables.accidentalColumnsTable[groupLength][endCase][groupMember - i];
lineList[groupMember].column = column;
totalColumns = totalColumns > column ? totalColumns : column;
}
}
// Increment i to the last note that was set, so that if a lower set of notes
// does not conflict at all with this group, it can have its own classic shape.
i = groupEnd;
}
// ### Convert Columns to x_offsets
//
// This keeps columns aligned, even if they have different accidentals within them
// which sometimes results in a larger x_offset than is an accidental might need
// to preserve the symmetry of the accidental shape.
//
// Neither A.C. Vinci nor G. Read address this, and it typically only happens in
// music with complex chord clusters.
//
// TODO (?): Optionally allow closer compression of accidentals, instead of forcing
// parallel columns.
// track each column's max width, which will be used as initial shift of later columns:
const columnWidths = [];
const columnXOffsets = [];
for (let i = 0; i <= totalColumns; i++) {
columnWidths[i] = 0;
columnXOffsets[i] = 0;
}
columnWidths[0] = accShift + leftShift;
columnXOffsets[0] = accShift + leftShift;
// Fill columnWidths with widest needed x-space;
// this is what keeps the columns parallel.
lineList.forEach((line) => {
if (line.width > columnWidths[line.column])
columnWidths[line.column] = line.width;
});
for (let i = 1; i < columnWidths.length; i++) {
// this column's offset = this column's width + previous column's offset
columnXOffsets[i] = columnWidths[i] + columnXOffsets[i - 1];
}
const totalShift = columnXOffsets[columnXOffsets.length - 1];
// Set the xShift for each accidental according to column offsets:
let accCount = 0;
lineList.forEach((line) => {
let lineWidth = 0;
const lastAccOnLine = accCount + line.numAcc;
// handle all of the accidentals on a given line:
for (accCount; accCount < lastAccOnLine; accCount++) {
const xShift = columnXOffsets[line.column - 1] + lineWidth;
accList[accCount].acc.setXShift(xShift);
// keep track of the width of accidentals we've added so far, so that when
// we loop, we add space for them.
lineWidth += accList[accCount].acc.getWidth() + accidentalSpacing;
L('Line, accCount, shift: ', line.line, accCount, xShift);
}
});
// update the overall layout with the full width of the accidental shapes:
state.left_shift += totalShift + additionalPadding;
}
/** Helper function to determine whether two lines of accidentals collide vertically */
static checkCollision(line1, line2) {
let clearance = line2.line - line1.line;
let clearanceRequired = 3;
// But less clearance is required for certain accidentals: b, bb and ##.
if (clearance > 0) {
// then line 2 is on top
clearanceRequired = line2.flatLine || line2.dblSharpLine ? 2.5 : 3.0;
if (line1.dblSharpLine)
clearance -= 0.5;
}
else {
// line 1 is on top
clearanceRequired = line1.flatLine || line1.dblSharpLine ? 2.5 : 3.0;
if (line2.dblSharpLine)
clearance -= 0.5;
}
const collision = Math.abs(clearance) < clearanceRequired;
L('Line_1, Line_2, Collision: ', line1.line, line2.line, collision);
return collision;
}
/**
* Use this method to automatically apply accidentals to a set of `voices`.
* The accidentals will be remembered between all the voices provided.
* Optionally, you can also provide an initial `keySignature`.
*/
static applyAccidentals(voices, keySignature) {
const tickPositions = [];
const tickNoteMap = {};
// Sort the tickables in each voice by their tick position in the voice.
voices.forEach((voice) => {
const tickPosition = new _fraction__WEBPACK_IMPORTED_MODULE_0__.Fraction(0, 1);
const tickable = voice.getTickables();
tickable.forEach((t) => {
if (t.shouldIgnoreTicks())
return;
const notesAtPosition = tickNoteMap[tickPosition.value()];
if (!notesAtPosition) {
tickPositions.push(tickPosition.value());
tickNoteMap[tickPosition.value()] = [t];
}
else {
notesAtPosition.push(t);
}
tickPosition.add(t.getTicks());
});
});
const music = new _music__WEBPACK_IMPORTED_MODULE_3__.Music();
// Default key signature is C major.
if (!keySignature)
keySignature = 'C';
// Get the scale map, which represents the current state of each pitch.
const scaleMapKey = music.createScaleMap(keySignature);
const scaleMap = {};
tickPositions.forEach((tickPos) => {
const tickables = tickNoteMap[tickPos];
// Array to store all pitches that modified accidental states
// at this tick position
const modifiedPitches = [];
const processNote = (t) => {
// Only StaveNote implements .addModifier(), which is used below.
if (!(0,_typeguard__WEBPACK_IMPORTED_MODULE_5__.isStaveNote)(t) || t.isRest() || t.shouldIgnoreTicks()) {
return;
}
// Go through each key and determine if an accidental should be applied.
const staveNote = t;
staveNote.keys.forEach((keyString, keyIndex) => {
const key = music.getNoteParts(keyString.split('/')[0]);
const octave = keyString.split('/')[1];
// Force a natural for every key without an accidental
const accidentalString = key.accidental || 'n';
const pitch = key.root + accidentalString;
// Determine if the current pitch has the same accidental
// as the scale state
if (!scaleMap[key.root + octave])
scaleMap[key.root + octave] = scaleMapKey[key.root];
const sameAccidental = scaleMap[key.root + octave] === pitch;
// Determine if an identical pitch in the chord already
// modified the accidental state
const previouslyModified = modifiedPitches.indexOf(keyString) > -1;
// Remove accidentals
staveNote.getModifiers().forEach((modifier, index) => {
if ((0,_typeguard__WEBPACK_IMPORTED_MODULE_5__.isAccidental)(modifier) && modifier.type == accidentalString && modifier.getIndex() == keyIndex) {
staveNote.getModifiers().splice(index, 1);
}
});
// Add the accidental to the StaveNote
if (!sameAccidental || (sameAccidental && previouslyModified)) {
// Modify the scale map so that the root pitch has an
// updated state
scaleMap[key.root + octave] = pitch;
// Create the accidental
const accidental = new Accidental(accidentalString);
// Attach the accidental to the StaveNote
staveNote.addModifier(accidental, keyIndex);
// Add the pitch to list of pitches that modified accidentals
modifiedPitches.push(keyString);
}
});
// process grace notes
staveNote.getModifiers().forEach((modifier) => {
if ((0,_typeguard__WEBPACK_IMPORTED_MODULE_5__.isGraceNoteGroup)(modifier)) {
modifier.getGraceNotes().forEach(processNote);
}
});
};
tickables.forEach(processNote);
});
}
/**
* Create accidental.
* @param type value from `Vex.Flow.accidentalCodes.accidentals` table in `tables.ts`.
* For example: `#`, `##`, `b`, `n`, etc.
*/
constructor(type) {
super();
L('New accidental: ', type);
this.type = type;
this.position = _modifier__WEBPACK_IMPORTED_MODULE_2__.Modifier.Position.LEFT;
this.render_options = {
// Font size for glyphs
font_scale: _tables__WEBPACK_IMPORTED_MODULE_4__.Tables.NOTATION_FONT_SCALE,
// Padding between accidental and parentheses on each side
parenLeftPadding: 2,
parenRightPadding: 2,
};
this.accidental = _tables__WEBPACK_IMPORTED_MODULE_4__.Tables.accidentalCodes(this.type);
(0,_util__WEBPACK_IMPORTED_MODULE_6__.defined)(this.accidental, 'ArgumentError', `Unknown accidental type: ${type}`);
// Cautionary accidentals have parentheses around them
this.cautionary = false;
this.reset();
}
reset() {
const fontScale = this.render_options.font_scale;
this.glyph = new _glyph__WEBPACK_IMPORTED_MODULE_1__.Glyph(this.accidental.code, fontScale);
this.glyph.setOriginX(1.0);
if (this.cautionary) {
this.parenLeft = new _glyph__WEBPACK_IMPORTED_MODULE_1__.Glyph(_tables__WEBPACK_IMPORTED_MODULE_4__.Tables.accidentalCodes('{').code, fontScale);
this.parenRight = new _glyph__WEBPACK_IMPORTED_MODULE_1__.Glyph(_tables__WEBPACK_IMPORTED_MODULE_4__.Tables.accidentalCodes('}').code, fontScale);
this.parenLeft.setOriginX(1.0);
this.parenRight.setOriginX(1.0);
}
}
/** Get width in pixels. */
getWidth() {
if (this.cautionary) {
const parenLeft = (0,_util__WEBPACK_IMPORTED_MODULE_6__.defined)(this.parenLeft);
const parenRight = (0,_util__WEBPACK_IMPORTED_MODULE_6__.defined)(this.parenRight);
const parenWidth = parenLeft.getMetrics().width +
parenRight.getMetrics().width +
this.render_options.parenLeftPadding +
this.render_options.parenRightPadding;
return this.glyph.getMetrics().width + parenWidth;
}
else {
return this.glyph.getMetrics().width;
}
}
/** Attach this accidental to `note`, which must be a `StaveNote`. */
setNote(note) {
(0,_util__WEBPACK_IMPORTED_MODULE_6__.defined)(note, 'ArgumentError', `Bad note value: ${note}`);
this.note = note;
// Accidentals attached to grace notes are rendered smaller.
if ((0,_typeguard__WEBPACK_IMPORTED_MODULE_5__.isGraceNote)(note)) {
this.render_options.font_scale = 25;
this.reset();
}
return this;
}
/** If called, draws parenthesis around accidental. */
setAsCautionary() {
this.cautionary = true;
this.render_options.font_scale = 28;
this.reset();
return this;
}
/** Render accidental onto canvas. */
draw() {
const { type, position, index, cautionary, x_shift, y_shift, glyph, render_options: { parenLeftPadding, parenRightPadding }, } = this;
const ctx = this.checkContext();
const note = this.checkAttachedNote();
this.setRendered();
// Figure out the start `x` and `y` coordinates for note and index.
const start = note.getModifierStartXY(position, index);
let accX = start.x + x_shift;
const accY = start.y + y_shift;
L('Rendering: ', type, accX, accY);
if (!cautionary) {
glyph.render(ctx, accX, accY);
}
else {
const parenLeft = (0,_util__WEBPACK_IMPORTED_MODULE_6__.defined)(this.parenLeft);
const parenRight = (0,_util__WEBPACK_IMPORTED_MODULE_6__.defined)(this.parenRight);
// Render the accidental in parentheses.
parenRight.render(ctx, accX, accY);
accX -= parenRight.getMetrics().width;
accX -= parenRightPadding;
accX -= this.accidental.parenRightPaddingAdjustment;
glyph.render(ctx, accX, accY);
accX -= glyph.getMetrics().width;
accX -= parenLeftPadding;
parenLeft.render(ctx, accX, accY);
}
}
}
/** To enable logging for this class. Set `Vex.Flow.Accidental.DEBUG` to `true`. */
Accidental.DEBUG = false;
/***/ }),
/***/ "./src/annotation.ts":
/*!***************************!*\
!*** ./src/annotation.ts ***!
\***************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "Annotation": () => (/* binding */ Annotation),
/* harmony export */ "AnnotationHorizontalJustify": () => (/* binding */ AnnotationHorizontalJustify),
/* harmony export */ "AnnotationVerticalJustify": () => (/* binding */ AnnotationVerticalJustify)
/* harmony export */ });
/* harmony import */ var _element__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./element */ "./src/element.ts");
/* harmony import */ var _modifier__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./modifier */ "./src/modifier.ts");
/* harmony import */ var _stem__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./stem */ "./src/stem.ts");
/* harmony import */ var _tables__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./tables */ "./src/tables.ts");
/* harmony import */ var _textformatter__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./textformatter */ "./src/textformatter.ts");
/* harmony import */ var _typeguard__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./typeguard */ "./src/typeguard.ts");
/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./util */ "./src/util.ts");
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
// MIT License
// eslint-disable-next-line
function L(...args) {
if (Annotation.DEBUG)
(0,_util__WEBPACK_IMPORTED_MODULE_6__.log)('Vex.Flow.Annotation', args);
}
var AnnotationHorizontalJustify;
(function (AnnotationHorizontalJustify) {
AnnotationHorizontalJustify[AnnotationHorizontalJustify["LEFT"] = 1] = "LEFT";
AnnotationHorizontalJustify[AnnotationHorizontalJustify["CENTER"] = 2] = "CENTER";
AnnotationHorizontalJustify[AnnotationHorizontalJustify["RIGHT"] = 3] = "RIGHT";
AnnotationHorizontalJustify[AnnotationHorizontalJustify["CENTER_STEM"] = 4] = "CENTER_STEM";
})(AnnotationHorizontalJustify || (AnnotationHorizontalJustify = {}));
var AnnotationVerticalJustify;
(function (AnnotationVerticalJustify) {
AnnotationVerticalJustify[AnnotationVerticalJustify["TOP"] = 1] = "TOP";
AnnotationVerticalJustify[AnnotationVerticalJustify["CENTER"] = 2] = "CENTER";
AnnotationVerticalJustify[AnnotationVerticalJustify["BOTTOM"] = 3] = "BOTTOM";
AnnotationVerticalJustify[AnnotationVerticalJustify["CENTER_STEM"] = 4] = "CENTER_STEM";
})(AnnotationVerticalJustify || (AnnotationVerticalJustify = {}));
/**
* Annotations are modifiers that can be attached to
* notes.
*
* See `tests/annotation_tests.ts` for usage examples.
*/
class Annotation extends _modifier__WEBPACK_IMPORTED_MODULE_1__.Modifier {
/** Annotations category string. */
static get CATEGORY() {
return _typeguard__WEBPACK_IMPORTED_MODULE_5__.Category.Annotation;
}
// Use the same padding for annotations as note head so the
// words don't run into each other.
static get minAnnotationPadding() {
const musicFont = _tables__WEBPACK_IMPORTED_MODULE_3__.Tables.currentMusicFont();
return musicFont.lookupMetric('noteHead.minPadding');
}
/** Arrange annotations within a `ModifierContext` */
static format(annotations, state) {
if (!annotations || annotations.length === 0)
return false;
let leftWidth = 0;
let rightWidth = 0;
let maxLeftGlyphWidth = 0;
let maxRightGlyphWidth = 0;
for (let i = 0; i < annotations.length; ++i) {
const annotation = annotations[i];
const textFormatter = _textformatter__WEBPACK_IMPORTED_MODULE_4__.TextFormatter.create(annotation.textFont);
// Text height is expressed in fractional stave spaces.
const textLines = (2 + textFormatter.getYForStringInPx(annotation.text).height) / _tables__WEBPACK_IMPORTED_MODULE_3__.Tables.STAVE_LINE_DISTANCE;
let verticalSpaceNeeded = textLines;
const note = annotation.checkAttachedNote();
const glyphWidth = note.getGlyphProps().getWidth();
// Get the text width from the font metrics.
const textWidth = textFormatter.getWidthForTextInPx(annotation.text);
if (annotation.horizontalJustification === AnnotationHorizontalJustify.LEFT) {
maxLeftGlyphWidth = Math.max(glyphWidth, maxLeftGlyphWidth);
leftWidth = Math.max(leftWidth, textWidth) + Annotation.minAnnotationPadding;
}
else if (annotation.horizontalJustification === AnnotationHorizontalJustify.RIGHT) {
maxRightGlyphWidth = Math.max(glyphWidth, maxRightGlyphWidth);
rightWidth = Math.max(rightWidth, textWidth);
}
else {
leftWidth = Math.max(leftWidth, textWidth / 2) + Annotation.minAnnotationPadding;
rightWidth = Math.max(rightWidth, textWidth / 2);
maxLeftGlyphWidth = Math.max(glyphWidth / 2, maxLeftGlyphWidth);
maxRightGlyphWidth = Math.max(glyphWidth / 2, maxRightGlyphWidth);
}
const stave = note.getStave();
const stemDirection = note.hasStem() ? note.getStemDirection() : _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.UP;
let stemHeight = 0;
let lines = 5;
if ((0,_typeguard__WEBPACK_IMPORTED_MODULE_5__.isTabNote)(note)) {
if (note.render_options.draw_stem) {
const stem = note.getStem();
if (stem) {
stemHeight = Math.abs(stem.getHeight()) / _tables__WEBPACK_IMPORTED_MODULE_3__.Tables.STAVE_LINE_DISTANCE;
}
}
else {
stemHeight = 0;
}
}
else if ((0,_typeguard__WEBPACK_IMPORTED_MODULE_5__.isStemmableNote)(note)) {
const stem = note.getStem();
if (stem && note.getNoteType() === 'n') {
stemHeight = Math.abs(stem.getHeight()) / _tables__WEBPACK_IMPORTED_MODULE_3__.Tables.STAVE_LINE_DISTANCE;
}
}
if (stave) {
lines = stave.getNumLines();
}
if (annotation.verticalJustification === this.VerticalJustify.TOP) {
let noteLine = note.getLineNumber(true);
if ((0,_typeguard__WEBPACK_IMPORTED_MODULE_5__.isTabNote)(note)) {
noteLine = lines - (note.leastString() - 0.5);
}
if (stemDirection === _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.UP) {
noteLine += stemHeight;
}
const curTop = noteLine + state.top_text_line + 0.5;
if (curTop < lines) {
annotation.setTextLine(lines - noteLine);
verticalSpaceNeeded += lines - noteLine;
state.top_text_line = verticalSpaceNeeded;
}
else {
annotation.setTextLine(state.top_text_line);
state.top_text_line += verticalSpaceNeeded;
}
}
else if (annotation.verticalJustification === this.VerticalJustify.BOTTOM) {
let noteLine = lines - note.getLineNumber();
if ((0,_typeguard__WEBPACK_IMPORTED_MODULE_5__.isTabNote)(note)) {
noteLine = note.greatestString() - 1;
}
if (stemDirection === _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.DOWN) {
noteLine += stemHeight;
}
const curBottom = noteLine + state.text_line + 1;
if (curBottom < lines) {
annotation.setTextLine(lines - curBottom);
verticalSpaceNeeded += lines - curBottom;
state.text_line = verticalSpaceNeeded;
}
else {
annotation.setTextLine(state.text_line);
state.text_line += verticalSpaceNeeded;
}
}
else {
annotation.setTextLine(state.text_line);
}
}
const rightOverlap = Math.min(Math.max(rightWidth - maxRightGlyphWidth, 0), Math.max(rightWidth - state.right_shift, 0));
const leftOverlap = Math.min(Math.max(leftWidth - maxLeftGlyphWidth, 0), Math.max(leftWidth - state.left_shift, 0));
state.left_shift += leftOverlap;
state.right_shift += rightOverlap;
return true;
}
/**
* Annotations inherit from `Modifier` and is positioned correctly when
* in a `ModifierContext`.
* Create a new `Annotation` with the string `text`.
*/
constructor(text) {
super();
this.text = text;
this.horizontalJustification = AnnotationHorizontalJustify.CENTER;
// warning: the default in the constructor is TOP, but in the factory the default is BOTTOM.
// this is to support legacy application that may expect this.
this.verticalJustification = AnnotationVerticalJustify.TOP;
this.resetFont();
// The default width is calculated from the text.
this.setWidth(_tables__WEBPACK_IMPORTED_MODULE_3__.Tables.textWidth(text));
}
/**
* Set vertical position of text (above or below stave).
* @param just value in `AnnotationVerticalJustify`.
*/
setVerticalJustification(just) {
this.verticalJustification = typeof just === 'string' ? Annotation.VerticalJustifyString[just] : just;
return this;
}
/**
* Get horizontal justification.
*/
getJustification() {
return this.horizontalJustification;
}
/**
* Set horizontal justification.
* @param justification value in `Annotation.Justify`.
*/
setJustification(just) {
this.horizontalJustification = typeof just === 'string' ? Annotation.HorizontalJustifyString[just] : just;
return this;
}
/** Render text beside the note. */
draw() {
const ctx = this.checkContext();
const note = this.checkAttachedNote();
const stemDirection = note.hasStem() ? note.getStemDirection() : _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.UP;
const textFormatter = _textformatter__WEBPACK_IMPORTED_MODULE_4__.TextFormatter.create(this.textFont);
const start = note.getModifierStartXY(_modifier__WEBPACK_IMPORTED_MODULE_1__.ModifierPosition.ABOVE, this.index);
this.setRendered();
// We're changing context parameters. Save current state.
ctx.save();
// Apply style might not save context, if this.style is undefined, so we
// still need to save context state just before this, since we will be
// changing ctx parameters below.
this.applyStyle();
ctx.openGroup('annotation', this.getAttribute('id'));
ctx.setFont(this.textFont);
const text_width = textFormatter.getWidthForTextInPx(this.text);
const text_height = textFormatter.getYForStringInPx(this.text).height;
let x;
let y;
if (this.horizontalJustification === AnnotationHorizontalJustify.LEFT) {
x = start.x;
}
else if (this.horizontalJustification === AnnotationHorizontalJustify.RIGHT) {
x = start.x - text_width;
}
else if (this.horizontalJustification === AnnotationHorizontalJustify.CENTER) {
x = start.x - text_width / 2;
} /* CENTER_STEM */
else {
x = note.getStemX() - text_width / 2;
}
let stem_ext = {};
let spacing = 0;
const has_stem = note.hasStem();
const stave = note.checkStave();
// The position of the text varies based on whether or not the note
// has a stem.
if (has_stem) {
stem_ext = note.checkStem().getExtents();
spacing = stave.getSpacingBetweenLines();
}
if (this.verticalJustification === AnnotationVerticalJustify.BOTTOM) {
// Use the largest (lowest) Y value
const ys = note.getYs();
y = ys.reduce((a, b) => (a > b ? a : b));
y += (this.text_line + 1) * _tables__WEBPACK_IMPORTED_MODULE_3__.Tables.STAVE_LINE_DISTANCE + text_height;
if (has_stem && stemDirection === _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.DOWN) {
y = Math.max(y, stem_ext.topY + text_height + spacing * this.text_line);
}
}
else if (this.verticalJustification === AnnotationVerticalJustify.CENTER) {
const yt = note.getYForTopText(this.text_line) - 1;
const yb = stave.getYForBottomText(this.text_line);
y = yt + (yb - yt) / 2 + text_height / 2;
}
else if (this.verticalJustification === AnnotationVerticalJustify.TOP) {
const topY = Math.min(...note.getYs());
y = topY - (this.text_line + 1) * _tables__WEBPACK_IMPORTED_MODULE_3__.Tables.STAVE_LINE_DISTANCE;
if (has_stem && stemDirection === _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.UP) {
// If the stem is above the stave already, go with default line width vs. actual
// since the lines between don't really matter.
spacing = stem_ext.topY < stave.getTopLineTopY() ? _tables__WEBPACK_IMPORTED_MODULE_3__.Tables.STAVE_LINE_DISTANCE : spacing;
y = Math.min(y, stem_ext.topY - spacing * (this.text_line + 1));
}
} /* CENTER_STEM */
else {
const extents = note.getStemExtents();
y = extents.topY + (extents.baseY - extents.topY) / 2 + text_height / 2;
}
L('Rendering annotation: ', this.text, x, y);
ctx.fillText(this.text, x, y);
ctx.closeGroup();
this.restoreStyle();
ctx.restore();
}
}
/** To enable logging for this class. Set `Vex.Flow.Annotation.DEBUG` to `true`. */
Annotation.DEBUG = false;
Annotation.TEXT_FONT = Object.assign({}, _element__WEBPACK_IMPORTED_MODULE_0__.Element.TEXT_FONT);
/** Text annotations can be positioned and justified relative to the note. */
Annotation.HorizontalJustify = AnnotationHorizontalJustify;
Annotation.HorizontalJustifyString = {
left: AnnotationHorizontalJustify.LEFT,
right: AnnotationHorizontalJustify.RIGHT,
center: AnnotationHorizontalJustify.CENTER,
centerStem: AnnotationHorizontalJustify.CENTER_STEM,
};
Annotation.VerticalJustify = AnnotationVerticalJustify;
Annotation.VerticalJustifyString = {
above: AnnotationVerticalJustify.TOP,
top: AnnotationVerticalJustify.TOP,
below: AnnotationVerticalJustify.BOTTOM,
bottom: AnnotationVerticalJustify.BOTTOM,
center: AnnotationVerticalJustify.CENTER,
centerStem: AnnotationVerticalJustify.CENTER_STEM,
};
/***/ }),
/***/ "./src/articulation.ts":
/*!*****************************!*\
!*** ./src/articulation.ts ***!
\*****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "Articulation": () => (/* binding */ Articulation)
/* harmony export */ });
/* harmony import */ var _glyph__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./glyph */ "./src/glyph.ts");
/* harmony import */ var _modifier__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./modifier */ "./src/modifier.ts");
/* harmony import */ var _stem__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./stem */ "./src/stem.ts");
/* harmony import */ var _tables__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./tables */ "./src/tables.ts");
/* harmony import */ var _typeguard__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./typeguard */ "./src/typeguard.ts");
/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./util */ "./src/util.ts");
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
// Author: Larry Kuhns.
// MIT License
// eslint-disable-next-line
function L(...args) {
if (Articulation.DEBUG)
(0,_util__WEBPACK_IMPORTED_MODULE_5__.log)('Vex.Flow.Articulation', args);
}
const { ABOVE, BELOW } = _modifier__WEBPACK_IMPORTED_MODULE_1__.Modifier.Position;
function roundToNearestHalf(mathFn, value) {
return mathFn(value / 0.5) * 0.5;
}
// This includes both staff and ledger lines
function isWithinLines(line, position) {
return position === ABOVE ? line <= 5 : line >= 1;
}
function getRoundingFunction(line, position) {
if (isWithinLines(line, position)) {
if (position === ABOVE) {
return Math.ceil;
}
else {
return Math.floor;
}
}
else {
return Math.round;
}
}
function snapLineToStaff(canSitBetweenLines, line, position, offsetDirection) {
// Initially, snap to nearest staff line or space
const snappedLine = roundToNearestHalf(getRoundingFunction(line, position), line);
const canSnapToStaffSpace = canSitBetweenLines && isWithinLines(snappedLine, position);
const onStaffLine = snappedLine % 1 === 0;
if (canSnapToStaffSpace && onStaffLine) {
const HALF_STAFF_SPACE = 0.5;
return snappedLine + HALF_STAFF_SPACE * -offsetDirection;
}
else {
return snappedLine;
}
}
// Helper function for checking if a Note object is either a StaveNote or a GraceNote.
const isStaveOrGraceNote = (note) => (0,_typeguard__WEBPACK_IMPORTED_MODULE_4__.isStaveNote)(note) || (0,_typeguard__WEBPACK_IMPORTED_MODULE_4__.isGraceNote)(note);
function getTopY(note, textLine) {
const stemDirection = note.getStemDirection();
const { topY: stemTipY, baseY: stemBaseY } = note.getStemExtents();
if (isStaveOrGraceNote(note)) {
if (note.hasStem()) {
if (stemDirection === _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.UP) {
return stemTipY;
}
else {
return stemBaseY;
}
}
else {
return Math.min(...note.getYs());
}
}
else if ((0,_typeguard__WEBPACK_IMPORTED_MODULE_4__.isTabNote)(note)) {
if (note.hasStem()) {
if (stemDirection === _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.UP) {
return stemTipY;
}
else {
return note.checkStave().getYForTopText(textLine);
}
}
else {
return note.checkStave().getYForTopText(textLine);
}
}
else {
throw new _util__WEBPACK_IMPORTED_MODULE_5__.RuntimeError('UnknownCategory', 'Only can get the top and bottom ys of stavenotes and tabnotes');
}
}
function getBottomY(note, textLine) {
const stemDirection = note.getStemDirection();
const { topY: stemTipY, baseY: stemBaseY } = note.getStemExtents();
if (isStaveOrGraceNote(note)) {
if (note.hasStem()) {
if (stemDirection === _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.UP) {
return stemBaseY;
}
else {
return stemTipY;
}
}
else {
return Math.max(...note.getYs());
}
}
else if ((0,_typeguard__WEBPACK_IMPORTED_MODULE_4__.isTabNote)(note)) {
if (note.hasStem()) {
if (stemDirection === _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.UP) {
return note.checkStave().getYForBottomText(textLine);
}
else {
return stemTipY;
}
}
else {
return note.checkStave().getYForBottomText(textLine);
}
}
else {
throw new _util__WEBPACK_IMPORTED_MODULE_5__.RuntimeError('UnknownCategory', 'Only can get the top and bottom ys of stavenotes and tabnotes');
}
}
/**
* Get the initial offset of the articulation from the y value of the starting position.
* This is required because the top/bottom text positions already have spacing applied to
* provide a "visually pleasant" default position. However the y values provided from
* the stavenote's top/bottom do *not* have any pre-applied spacing. This function
* normalizes this asymmetry.
* @param note
* @param position
* @returns
*/
function getInitialOffset(note, position) {
const isOnStemTip = (position === ABOVE && note.getStemDirection() === _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.UP) ||
(position === BELOW && note.getStemDirection() === _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.DOWN);
if (isStaveOrGraceNote(note)) {
if (note.hasStem() && isOnStemTip) {
return 0.5;
}
else {
// this amount is larger than the stem-tip offset because we start from
// the center of the notehead
return 1;
}
}
else {
if (note.hasStem() && isOnStemTip) {
return 1;
}
else {
return 0;
}
}
}
/**
* Articulations and Accents are modifiers that can be
* attached to notes. The complete list of articulations is available in
* `tables.ts` under `Vex.Flow.articulationCodes`.
*
* See `tests/articulation_tests.ts` for usage examples.
*/
class Articulation extends _modifier__WEBPACK_IMPORTED_MODULE_1__.Modifier {
/** Articulations category string. */
static get CATEGORY() {
return _typeguard__WEBPACK_IMPORTED_MODULE_4__.Category.Articulation;
}
/**
* FIXME:
* Most of the complex formatting logic (ie: snapping to space) is
* actually done in .render(). But that logic belongs in this method.
*
* Unfortunately, this isn't possible because, by this point, stem lengths
* have not yet been finalized. Finalized stem lengths are required to determine the
* initial position of any stem-side articulation.
*
* This indicates that all objects should have their stave set before being
* formatted. It can't be an optional if you want accurate vertical positioning.
* Consistently positioned articulations that play nice with other modifiers
* won't be possible until we stop relying on render-time formatting.
*
* Ideally, when this function has completed, the vertical articulation positions
* should be ready to render without further adjustment. But the current state
* is far from this ideal.
*/
static format(articulations, state) {
if (!articulations || articulations.length === 0)
return false;
const margin = 0.5;
let maxGlyphWidth = 0;
const getIncrement = (articulation, line, position) => roundToNearestHalf(getRoundingFunction(line, position), (0,_util__WEBPACK_IMPORTED_MODULE_5__.defined)(articulation.glyph.getMetrics().height) / 10 + margin);
articulations.forEach((articulation) => {
const note = articulation.checkAttachedNote();
maxGlyphWidth = Math.max(note.getGlyphProps().getWidth(), maxGlyphWidth);
let lines = 5;
const stemDirection = note.hasStem() ? note.getStemDirection() : _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.UP;
let stemHeight = 0;
// Decide if we need to consider beam direction in placement.
if ((0,_typeguard__WEBPACK_IMPORTED_MODULE_4__.isStemmableNote)(note)) {
const stem = note.getStem();
if (stem) {
stemHeight = Math.abs(stem.getHeight()) / _tables__WEBPACK_IMPORTED_MODULE_3__.Tables.STAVE_LINE_DISTANCE;
}
}
const stave = note.getStave();
if (stave