UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature.

1,044 lines (1,010 loc) 2.91 MB
/*! * 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