vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
1,044 lines (1,010 loc) • 2.91 MB
JavaScript
/*!
* VexFlow 5.0.0 2025-03-05T17:05:43.991Z 0ca6f889545c33cce851b420c24945f6eb685aeb
* Copyright (c) 2023-present VexFlow contributors (see https://github.com/vexflow/vexflow/blob/main/AUTHORS.md).
*/
(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["VexFlow"] = factory();
else
root["VexFlow"] = 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 */ });
// Gruntfile.js uses string-replace-loader to replace these values during build time.
const VERSION = '5.0.0';
const ID = '0ca6f889545c33cce851b420c24945f6eb685aeb';
const DATE = '2025-03-05T17:05:43.991Z';
/***/ }),
/***/ "./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 _metrics__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./metrics */ "./src/metrics.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");
// Copyright (c) 2023-present VexFlow contributors: https://github.com/vexflow/vexflow/graphs/contributors
// 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)('VexFlow.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 noteheadAccidentalPadding = _metrics__WEBPACK_IMPORTED_MODULE_1__.Metrics.get('Accidental.noteheadAccidentalPadding');
const leftShift = state.leftShift + noteheadAccidentalPadding;
const accidentalSpacing = _metrics__WEBPACK_IMPORTED_MODULE_1__.Metrics.get('Accidental.accidentalSpacing');
const additionalPadding = _metrics__WEBPACK_IMPORTED_MODULE_1__.Metrics.get('Accidental.leftPadding'); // padding to the left of all accidentals
const accidentalLinePositionsAndSpaceNeeds = [];
let prevNote = undefined;
let extraXSpaceNeededForLeftDisplacedNotehead = 0;
// First determine the accidentals' Y positions from the note.keys
for (let i = 0; i < accidentals.length; ++i) {
const accidental = accidentals[i];
const note = accidental.getNote();
const stave = note.getStave();
const index = accidental.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) {
// If the current extra left-space needed isn't as big as this note's,
// then we need to use this note's.
extraXSpaceNeededForLeftDisplacedNotehead = Math.max(note.getLeftDisplacedHeadPx() - note.getXShift(), extraXSpaceNeededForLeftDisplacedNotehead);
}
prevNote = note;
}
if (stave) {
const lineSpace = stave.getSpacingBetweenLines();
const y = stave.getYForLine(props.line);
const accLine = Math.round((y / lineSpace) * 2) / 2;
accidentalLinePositionsAndSpaceNeeds.push({
y,
line: accLine,
extraXSpaceNeeded: extraXSpaceNeededForLeftDisplacedNotehead,
accidental: accidental,
spacingBetweenStaveLines: lineSpace,
});
}
else {
accidentalLinePositionsAndSpaceNeeds.push({
line: props.line,
extraXSpaceNeeded: extraXSpaceNeededForLeftDisplacedNotehead,
accidental: accidental,
});
}
}
// Sort accidentals by line number.
accidentalLinePositionsAndSpaceNeeds.sort((a, b) => b.line - a.line);
const staveLineAccidentalLayoutMetrics = [];
// amount by which all accidentals must be shifted right or left for
// stem flipping, notehead shifting concerns.
let maxExtraXSpaceNeeded = 0;
// Create an array of unique line numbers (staveLineAccidentalLayoutMetrics)
// from accidentalLinePositionsAndSpaceNeeds
for (let i = 0; i < accidentalLinePositionsAndSpaceNeeds.length; i++) {
const accidentalLinePositionAndSpaceNeeds = accidentalLinePositionsAndSpaceNeeds[i];
const accidentalType = accidentalLinePositionAndSpaceNeeds.accidental.type;
const priorLineMetric = staveLineAccidentalLayoutMetrics[staveLineAccidentalLayoutMetrics.length - 1];
let currentLineMetric;
// if this is the first line, or a new line, add a staveLineAccidentalLayoutMetric
if (!priorLineMetric || (priorLineMetric === null || priorLineMetric === void 0 ? void 0 : priorLineMetric.line) !== accidentalLinePositionAndSpaceNeeds.line) {
currentLineMetric = {
line: accidentalLinePositionAndSpaceNeeds.line,
flatLine: true,
dblSharpLine: true,
numAcc: 0,
width: 0,
column: 0,
};
staveLineAccidentalLayoutMetrics.push(currentLineMetric);
}
else {
currentLineMetric = priorLineMetric;
}
// if this accidental is not a flat, the accidental needs 3.0 lines lower
// clearance instead of 2.5 lines for b or bb.
if (accidentalType !== 'b' && accidentalType !== 'bb') {
currentLineMetric.flatLine = false;
}
// if this accidental is not a double sharp, the accidental needs 3.0 lines above
if (accidentalType !== '##') {
currentLineMetric.dblSharpLine = false;
}
// Track how many accidentals are on this line:
currentLineMetric.numAcc++;
// Track the total xOffset needed for this line which will be needed
// for formatting lines w/ multiple accidentals:
// width = accidental width + universal spacing between accidentals
currentLineMetric.width += accidentalLinePositionAndSpaceNeeds.accidental.getWidth() + accidentalSpacing;
// if this extraXSpaceNeeded is the largest so far, use it as the starting point for
// all accidental columns.
maxExtraXSpaceNeeded = Math.max(accidentalLinePositionAndSpaceNeeds.extraXSpaceNeeded, maxExtraXSpaceNeeded);
}
// ### 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 vertical 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 < staveLineAccidentalLayoutMetrics.length; i++) {
let noFurtherConflicts = false;
const groupStart = i;
let groupEnd = i;
while (groupEnd + 1 < staveLineAccidentalLayoutMetrics.length && !noFurtherConflicts) {
// if this note conflicts with the next:
if (this.checkCollision(staveLineAccidentalLayoutMetrics[groupEnd], staveLineAccidentalLayoutMetrics[groupEnd + 1])) {
// include the next note in the group:
groupEnd++;
}
else {
noFurtherConflicts = true;
}
}
// Gets a line from the `lineList`, relative to the current group
const getGroupLine = (index) => staveLineAccidentalLayoutMetrics[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(staveLineAccidentalLayoutMetrics[groupStart], staveLineAccidentalLayoutMetrics[groupEnd])
? 'a'
: 'b';
switch (groupLength) {
case 3:
if (endCase === 'a' && lineDifference(1, 2) === 0.5 && lineDifference(0, 1) !== 0.5) {
endCase = 'secondOnBottom';
}
break;
case 4:
if (notColliding([0, 2], [1, 3])) {
endCase = 'spacedOutTetrachord';
}
break;
case 5:
if (endCase === 'b' && notColliding([1, 3])) {
endCase = 'spacedOutPentachord';
if (notColliding([0, 2], [2, 4])) {
endCase = 'verySpacedOutPentachord';
}
}
break;
case 6:
if (notColliding([0, 3], [1, 4], [2, 5])) {
endCase = 'spacedOutHexachord';
}
if (notColliding([0, 2], [2, 4], [1, 3], [3, 5])) {
endCase = 'verySpacedOutHexachord';
}
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 < staveLineAccidentalLayoutMetrics.length; line++) {
if (this.checkCollision(staveLineAccidentalLayoutMetrics[line], staveLineAccidentalLayoutMetrics[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;
staveLineAccidentalLayoutMetrics[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];
staveLineAccidentalLayoutMetrics[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 xOffsets
//
// This keeps columns aligned, even if they have different accidentals within them
// which sometimes results in a larger xOffset 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] = leftShift + maxExtraXSpaceNeeded;
columnXOffsets[0] = leftShift;
// Fill columnWidths with the widest needed x-space;
// this is what keeps the columns parallel.
staveLineAccidentalLayoutMetrics.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;
staveLineAccidentalLayoutMetrics.forEach((line) => {
let lineWidth = 0;
const lastAccOnLine = accCount + line.numAcc;
// handle all accidentals on a given line:
for (accCount; accCount < lastAccOnLine; accCount++) {
const xShift = columnXOffsets[line.column - 1] + lineWidth + maxExtraXSpaceNeeded;
accidentalLinePositionsAndSpaceNeeds[accCount].accidental.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 += accidentalLinePositionsAndSpaceNeeds[accCount].accidental.getWidth() + accidentalSpacing;
L('Line, accCount, shift: ', line.line, accCount, xShift);
}
});
// update the overall layout with the full width of the accidental shapes:
state.leftShift = 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('Line1, Line2, 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 `VexFlow.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;
// Cautionary accidentals have parentheses around them
this.cautionary = false;
this.reset();
}
reset() {
this.text = '';
if (!this.cautionary) {
this.text += _tables__WEBPACK_IMPORTED_MODULE_4__.Tables.accidentalCodes(this.type);
this.fontInfo.size = _metrics__WEBPACK_IMPORTED_MODULE_1__.Metrics.get('Accidental.fontSize');
}
else {
this.text += _tables__WEBPACK_IMPORTED_MODULE_4__.Tables.accidentalCodes('{');
this.text += _tables__WEBPACK_IMPORTED_MODULE_4__.Tables.accidentalCodes(this.type);
this.text += _tables__WEBPACK_IMPORTED_MODULE_4__.Tables.accidentalCodes('}');
this.fontInfo.size = _metrics__WEBPACK_IMPORTED_MODULE_1__.Metrics.get('Accidental.cautionary.fontSize');
}
// Accidentals attached to grace notes are rendered smaller.
if ((0,_typeguard__WEBPACK_IMPORTED_MODULE_5__.isGraceNote)(this.note)) {
this.fontInfo.size = _metrics__WEBPACK_IMPORTED_MODULE_1__.Metrics.get('Accidental.grace.fontSize');
}
}
/** 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;
this.reset();
return this;
}
/** If called, draws parenthesis around accidental. */
setAsCautionary() {
this.cautionary = true;
this.reset();
return this;
}
/** Render accidental onto canvas. */
draw() {
const { type, position, index } = 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);
this.x = start.x - this.width;
this.y = start.y;
L('Rendering: ', type, start.x, start.y);
this.renderText(ctx, 0, 0);
}
}
/** To enable logging for this class. Set `VexFlow.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 _font__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./font */ "./src/font.ts");
/* harmony import */ var _metrics__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./metrics */ "./src/metrics.ts");
/* harmony import */ var _modifier__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./modifier */ "./src/modifier.ts");
/* harmony import */ var _stem__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./stem */ "./src/stem.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");
// Copyright (c) 2023-present VexFlow contributors: https://github.com/vexflow/vexflow/graphs/contributors
// MIT License
// eslint-disable-next-line
function L(...args) {
if (Annotation.DEBUG)
(0,_util__WEBPACK_IMPORTED_MODULE_6__.log)('VexFlow.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_2__.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() {
return _metrics__WEBPACK_IMPORTED_MODULE_1__.Metrics.get('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];
// Text height is expressed in fractional stave spaces.
const textLines = (2 + _font__WEBPACK_IMPORTED_MODULE_0__.Font.convertSizeToPixelValue(annotation.fontInfo.size)) / _tables__WEBPACK_IMPORTED_MODULE_4__.Tables.STAVE_LINE_DISTANCE;
let verticalSpaceNeeded = textLines;
const note = annotation.checkAttachedNote();
const glyphWidth = note.getGlyphWidth();
// Get the text width from the font metrics.
const textWidth = annotation.getWidth();
if (annotation.horizontalJustification === AnnotationHorizontalJustify.RIGHT) {
maxLeftGlyphWidth = Math.max(glyphWidth, maxLeftGlyphWidth);
leftWidth = Math.max(leftWidth, textWidth) + Annotation.minAnnotationPadding;
}
else if (annotation.horizontalJustification === AnnotationHorizontalJustify.LEFT) {
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_3__.Stem.UP;
let stemHeight = 0;
let lines = 5;
if ((0,_typeguard__WEBPACK_IMPORTED_MODULE_5__.isTabNote)(note)) {
if (note.renderOptions.drawStem) {
const stem = note.getStem();
if (stem) {
stemHeight = Math.abs(stem.getHeight()) / _tables__WEBPACK_IMPORTED_MODULE_4__.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_4__.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_3__.Stem.UP) {
noteLine += stemHeight;
}
const curTop = noteLine + state.topTextLine + 0.5;
if (curTop < lines) {
annotation.setTextLine(lines - noteLine);
verticalSpaceNeeded += lines - noteLine;
state.topTextLine = verticalSpaceNeeded;
}
else {
annotation.setTextLine(state.topTextLine);
state.topTextLine += 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_3__.Stem.DOWN) {
noteLine += stemHeight;
}
const curBottom = noteLine + state.textLine + 1;
if (curBottom < lines) {
annotation.setTextLine(lines - curBottom);
verticalSpaceNeeded += lines - curBottom;
state.textLine = verticalSpaceNeeded;
}
else {
annotation.setTextLine(state.textLine);
state.textLine += verticalSpaceNeeded;
}
}
else {
annotation.setTextLine(state.textLine);
}
}
const rightOverlap = Math.min(Math.max(rightWidth - maxRightGlyphWidth, 0), Math.max(rightWidth - state.rightShift, 0));
const leftOverlap = Math.min(Math.max(leftWidth - maxLeftGlyphWidth, 0), Math.max(leftWidth - state.leftShift, 0));
state.leftShift += leftOverlap;
state.rightShift += 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;
}
/**
* 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 just 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_3__.Stem.UP;
const start = note.getModifierStartXY(_modifier__WEBPACK_IMPORTED_MODULE_2__.ModifierPosition.ABOVE, this.index);
this.setRendered();
ctx.openGroup('annotation', this.getAttribute('id'));
const textWidth = this.getWidth();
const textHeight = _font__WEBPACK_IMPORTED_MODULE_0__.Font.convertSizeToPixelValue(this.fontInfo.size);
let x;
let y;
if (this.horizontalJustification === AnnotationHorizontalJustify.LEFT) {
x = start.x;
}
else if (this.horizontalJustification === AnnotationHorizontalJustify.RIGHT) {
x = start.x - textWidth;
}
else if (this.horizontalJustification === AnnotationHorizontalJustify.CENTER) {
x = start.x - textWidth / 2;
} /* CENTER_STEM */
else {
x = note.getStemX() - textWidth / 2;
}
let stemExt = {};
let spacing = 0;
const hasStem = note.hasStem();
const stave = note.checkStave();
// The position of the text varies based on whether or not the note
// has a stem.
if (hasStem) {
stemExt = 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.textLine + 1) * _tables__WEBPACK_IMPORTED_MODULE_4__.Tables.STAVE_LINE_DISTANCE + textHeight;
if (hasStem && stemDirection === _stem__WEBPACK_IMPORTED_MODULE_3__.Stem.DOWN) {
y = Math.max(y, stemExt.topY + textHeight + spacing * this.textLine);
}
}
else if (this.verticalJustification === AnnotationVerticalJustify.CENTER) {
const yt = note.getYForTopText(this.textLine) - 1;
const yb = stave.getYForBottomText(this.textLine);
y = yt + (yb - yt) / 2 + textHeight / 2;
}
else if (this.verticalJustification === AnnotationVerticalJustify.TOP) {
const topY = Math.min(...note.getYs());
y = topY - (this.textLine + 1) * _tables__WEBPACK_IMPORTED_MODULE_4__.Tables.STAVE_LINE_DISTANCE;
if (hasStem && stemDirection === _stem__WEBPACK_IMPORTED_MODULE_3__.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 = stemExt.topY < stave.getTopLineTopY() ? _tables__WEBPACK_IMPORTED_MODULE_4__.Tables.STAVE_LINE_DISTANCE : spacing;
y = Math.min(y, stemExt.topY - spacing * (this.textLine + 1));
}
} /* CENTER_STEM */
else {
const extents = note.getStemExtents();
y = extents.topY + (extents.baseY - extents.topY) / 2 + textHeight / 2;
}
L('Rendering annotation: ', this.text, x, y);
this.x = x;
this.y = y;
this.renderText(ctx, 0, 0);
ctx.closeGroup();
}
}
/** To enable logging for this class. Set `VexFlow.Annotation.DEBUG` to `true`. */
Annotation.DEBUG = false;
/** 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 */ getBottomY: () => (/* binding */ getBottomY),
/* harmony export */ getInitialOffset: () => (/* binding */ getInitialOffset),
/* harmony export */ getTopY: () => (/* binding */ getTopY)
/* harmony export */ });
/* harmony import */ var _glyphs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./glyphs */ "./src/glyphs.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");
// Copyright (c) 2023-present VexFlow contributors: https://github.com/vexflow/vexflow/graphs/contributors
// @author Larry Kuhns.
// MIT License
// eslint-disable-next-line
function L(...args) {
if (Articulation.DEBUG)
(0,_util__WEBPACK_IMPORTED_MODULE_5__.log)('VexFlow.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 `VexFlow.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), articulation.height / 10 + margin);
articulations.forEach((articulation) => {
const note = articulation.checkAttachedNote();
maxGlyphWidth = Math.max(note.getGlyphWidth(), 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) {
lines = stave.getNumLines();
}
if (articulation.getPosition() === ABOVE) {
let noteLine = note.getLineNumber(true);
if (stemDirection === _stem__WEBPACK_IMPORTED_MODULE_2__.Stem.UP) {
noteLine += stemHeight;
}
let increment = getIncrement(articulation, state.topTextLine, ABOVE);
const curTop = noteLine + state.topTextLine + 0.5;
// If articulation must be above stave, add lines between note and stave top
if (!articulation.articulation.betweenLines && curTop < lines) {
increment += lines - curTop;
}
articulation.setTextLine(state.topTextLine);
state.topTextLine += increment;
articulation.setOrigin(0.5, 1);
}
else if (articulation.getP