vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
828 lines (728 loc) • 27.3 kB
text/typescript
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
//
// Author: Aaron (@AaronDavidNewman)
//
// This implements chord symbols above/below a chord.
// Chord symbols are modifiers that can be attached to notes.
// They can contain multiple 'blocks' which represent text or
// glyphs with various positioning options.
//
// See `tests/chordsymbol_tests.ts` for usage examples.
import { Font, FontInfo, FontStyle, FontWeight } from './font';
import { Glyph } from './glyph';
import { Modifier } from './modifier';
import { ModifierContextState } from './modifiercontext';
import { Note } from './note';
import { StemmableNote } from './stemmablenote';
import { Tables } from './tables';
import { TextFormatter } from './textformatter';
import { Category, isStemmableNote } from './typeguard';
import { log, RuntimeError } from './util';
// To enable logging for this class. Set `Vex.Flow.ChordSymbol.DEBUG` to `true`.
// eslint-disable-next-line
function L(...args: any[]): void {
if (ChordSymbol.DEBUG) log('Vex.Flow.ChordSymbol', args);
}
export interface ChordSymbolBlock {
text: string;
symbolType: SymbolTypes;
symbolModifier: SymbolModifiers;
xShift: number;
yShift: number;
vAlign: boolean;
width: number;
glyph?: Glyph;
}
export interface ChordSymbolGlyphMetrics {
leftSideBearing: number;
advanceWidth: number;
yOffset: number;
}
export interface ChordSymbolMetrics {
global: {
superscriptOffset: number;
subscriptOffset: number;
kerningOffset: number;
lowerKerningText: string[];
upperKerningText: string[];
spacing: number;
superSubRatio: number;
};
glyphs: Record<string, ChordSymbolGlyphMetrics>;
}
export enum ChordSymbolHorizontalJustify {
LEFT = 1,
CENTER = 2,
RIGHT = 3,
CENTER_STEM = 4,
}
export enum ChordSymbolVerticalJustify {
TOP = 1,
BOTTOM = 2,
}
export enum SymbolTypes {
GLYPH = 1,
TEXT = 2,
LINE = 3,
}
export enum SymbolModifiers {
NONE = 1,
SUBSCRIPT = 2,
SUPERSCRIPT = 3,
}
/**
* ChordSymbol is a modifier that creates a chord symbol above/below a chord.
* As a modifier, it is attached to an existing note.
*/
export class ChordSymbol extends Modifier {
static DEBUG: boolean = false;
static get CATEGORY(): string {
return Category.ChordSymbol;
}
// Chord symbols can be positioned and justified relative to the note.
static readonly HorizontalJustify = ChordSymbolHorizontalJustify;
static readonly HorizontalJustifyString: Record<string, ChordSymbolHorizontalJustify> = {
left: ChordSymbolHorizontalJustify.LEFT,
right: ChordSymbolHorizontalJustify.RIGHT,
center: ChordSymbolHorizontalJustify.CENTER,
centerStem: ChordSymbolHorizontalJustify.CENTER_STEM,
};
static readonly VerticalJustify = ChordSymbolVerticalJustify;
static readonly VerticalJustifyString: Record<string, ChordSymbolVerticalJustify> = {
top: ChordSymbolVerticalJustify.TOP,
above: ChordSymbolVerticalJustify.TOP,
below: ChordSymbolVerticalJustify.BOTTOM,
bottom: ChordSymbolVerticalJustify.BOTTOM,
};
static get superSubRatio(): number {
return ChordSymbol.metrics.global.superSubRatio;
}
/** Currently unused: Globally turn off text formatting, if the built-in formatting does not work for your font. */
static set NO_TEXT_FORMAT(val: boolean) {
ChordSymbol.noFormat = val;
}
static get NO_TEXT_FORMAT(): boolean {
return ChordSymbol.noFormat;
}
static getMetricForGlyph(glyphCode: string): ChordSymbolGlyphMetrics | undefined {
if (ChordSymbol.metrics.glyphs[glyphCode]) {
return ChordSymbol.metrics.glyphs[glyphCode];
}
return undefined;
}
static get engravingFontResolution(): number {
return Tables.currentMusicFont().getResolution();
}
static get spacingBetweenBlocks(): number {
return ChordSymbol.metrics.global.spacing / ChordSymbol.engravingFontResolution;
}
static getWidthForGlyph(glyph: Glyph): number {
const metric = ChordSymbol.getMetricForGlyph(glyph.code);
if (!metric) {
return 0.65; // probably should do something here.
}
return metric.advanceWidth / ChordSymbol.engravingFontResolution;
}
static getYShiftForGlyph(glyph: Glyph): number {
const metric = ChordSymbol.getMetricForGlyph(glyph.code);
if (!metric) {
return 0;
}
return metric.yOffset / ChordSymbol.engravingFontResolution;
}
static getXShiftForGlyph(glyph: Glyph): number {
const metric = ChordSymbol.getMetricForGlyph(glyph.code);
if (!metric) {
return 0;
}
return (-1 * metric.leftSideBearing) / ChordSymbol.engravingFontResolution;
}
static get superscriptOffset(): number {
return ChordSymbol.metrics.global.superscriptOffset / ChordSymbol.engravingFontResolution;
}
static get subscriptOffset(): number {
return ChordSymbol.metrics.global.subscriptOffset / ChordSymbol.engravingFontResolution;
}
static get kerningOffset(): number {
return ChordSymbol.metrics.global.kerningOffset / ChordSymbol.engravingFontResolution;
}
// Glyph data
static readonly glyphs: Record<string, { code: string }> = {
diminished: {
code: 'csymDiminished',
},
dim: {
code: 'csymDiminished',
},
halfDiminished: {
code: 'csymHalfDiminished',
},
'+': {
code: 'csymAugmented',
},
augmented: {
code: 'csymAugmented',
},
majorSeventh: {
code: 'csymMajorSeventh',
},
minor: {
code: 'csymMinor',
},
'-': {
code: 'csymMinor',
},
'(': {
code: 'csymParensLeftTall',
},
leftParen: {
code: 'csymParensLeftTall',
},
')': {
code: 'csymParensRightTall',
},
rightParen: {
code: 'csymParensRightTall',
},
leftBracket: {
code: 'csymBracketLeftTall',
},
rightBracket: {
code: 'csymBracketRightTall',
},
leftParenTall: {
code: 'csymParensLeftVeryTall',
},
rightParenTall: {
code: 'csymParensRightVeryTall',
},
'/': {
code: 'csymDiagonalArrangementSlash',
},
over: {
code: 'csymDiagonalArrangementSlash',
},
'#': {
code: 'accidentalSharp',
},
b: {
code: 'accidentalFlat',
},
};
static readonly symbolTypes = SymbolTypes;
static readonly symbolModifiers = SymbolModifiers;
static get metrics(): ChordSymbolMetrics {
const chordSymbol = Tables.currentMusicFont().getMetrics().chordSymbol;
if (!chordSymbol) throw new RuntimeError('BadMetrics', `chordSymbol missing`);
return chordSymbol;
}
static get lowerKerningText(): string[] {
// For example, see: `bravura_metrics.ts`
// BravuraMetrics.glyphs.chordSymbol.global.lowerKerningText, which returns an array of letters.
// ['D', 'F', 'P', 'T', 'V', 'Y']
return ChordSymbol.metrics.global.lowerKerningText;
}
static get upperKerningText(): string[] {
return ChordSymbol.metrics.global.upperKerningText;
}
static isSuperscript(block: ChordSymbolBlock): boolean {
return block.symbolModifier !== undefined && block.symbolModifier === SymbolModifiers.SUPERSCRIPT;
}
static isSubscript(block: ChordSymbolBlock): boolean {
return block.symbolModifier !== undefined && block.symbolModifier === SymbolModifiers.SUBSCRIPT;
}
static get minPadding(): number {
const musicFont = Tables.currentMusicFont();
return musicFont.lookupMetric('noteHead.minPadding');
}
/**
* Estimate the width of the whole chord symbol, based on the sum of the widths of the individual blocks.
* Estimate how many lines above/below the staff we need.
*/
static format(symbols: ChordSymbol[], state: ModifierContextState): boolean {
if (!symbols || symbols.length === 0) return false;
let width = 0;
let nonSuperWidth = 0;
let leftWidth = 0;
let rightWidth = 0;
let maxLeftGlyphWidth = 0;
let maxRightGlyphWidth = 0;
for (const symbol of symbols) {
const fontSize = Font.convertSizeToPointValue(symbol.textFont?.size);
const fontAdj = Font.scaleSize(fontSize, 0.05);
const glyphAdj = fontAdj * 2;
const note: Note = symbol.checkAttachedNote();
let symbolWidth = 0;
let lineSpaces = 1;
let vAlign = false;
for (let j = 0; j < symbol.symbolBlocks.length; ++j) {
const block = symbol.symbolBlocks[j];
const sup = ChordSymbol.isSuperscript(block);
const sub = ChordSymbol.isSubscript(block);
const superSubScale = sup || sub ? ChordSymbol.superSubRatio : 1;
const adj = block.symbolType === SymbolTypes.GLYPH ? glyphAdj * superSubScale : fontAdj * superSubScale;
// If there are super/subscripts, they extend beyond the line so
// assume they take up 2 lines
if (sup || sub) {
lineSpaces = 2;
}
// If there is a symbol-specific offset, add it but consider font
// size since font and glyphs will be interspersed.
const fontSize = symbol.textFormatter.fontSizeInPixels;
const superSubFontSize = fontSize * superSubScale;
if (block.symbolType === SymbolTypes.GLYPH && block.glyph !== undefined) {
block.width = ChordSymbol.getWidthForGlyph(block.glyph) * superSubFontSize;
block.yShift += ChordSymbol.getYShiftForGlyph(block.glyph) * superSubFontSize;
block.xShift += ChordSymbol.getXShiftForGlyph(block.glyph) * superSubFontSize;
block.glyph.scale = block.glyph.scale * adj;
} else if (block.symbolType === SymbolTypes.TEXT) {
block.width = block.width * superSubFontSize;
block.yShift += symbol.getYOffsetForText(block.text) * adj;
}
if (
block.symbolType === SymbolTypes.GLYPH &&
block.glyph !== undefined &&
block.glyph.code === ChordSymbol.glyphs.over.code
) {
lineSpaces = 2;
}
block.width += ChordSymbol.spacingBetweenBlocks * fontSize * superSubScale;
// If a subscript immediately follows a superscript block, try to
// overlay them.
if (sup && j > 0) {
const prev = symbol.symbolBlocks[j - 1];
if (!ChordSymbol.isSuperscript(prev)) {
nonSuperWidth = width;
}
}
if (sub && nonSuperWidth > 0) {
vAlign = true;
// slide the symbol over so it lines up with superscript
block.xShift = block.xShift + (nonSuperWidth - width);
width = nonSuperWidth;
nonSuperWidth = 0;
// If we have vertically lined up, turn kerning off.
symbol.setEnableKerning(false);
}
if (!sup && !sub) {
nonSuperWidth = 0;
}
block.vAlign = vAlign;
width += block.width;
symbolWidth = width;
}
// make kerning adjustments after computing super/subscripts
symbol.updateKerningAdjustments();
symbol.updateOverBarAdjustments();
if (symbol.getVertical() === ChordSymbolVerticalJustify.TOP) {
symbol.setTextLine(state.top_text_line);
state.top_text_line += lineSpaces;
} else {
symbol.setTextLine(state.text_line + 1);
state.text_line += lineSpaces + 1;
}
if (symbol.getReportWidth() && isStemmableNote(note)) {
const glyphWidth = note.getGlyphProps().getWidth();
if (symbol.getHorizontal() === ChordSymbolHorizontalJustify.LEFT) {
maxLeftGlyphWidth = Math.max(glyphWidth, maxLeftGlyphWidth);
leftWidth = Math.max(leftWidth, symbolWidth) + ChordSymbol.minPadding;
} else if (symbol.getHorizontal() === ChordSymbolHorizontalJustify.RIGHT) {
maxRightGlyphWidth = Math.max(glyphWidth, maxRightGlyphWidth);
rightWidth = Math.max(rightWidth, symbolWidth);
} else {
leftWidth = Math.max(leftWidth, symbolWidth / 2) + ChordSymbol.minPadding;
rightWidth = Math.max(rightWidth, symbolWidth / 2);
maxLeftGlyphWidth = Math.max(glyphWidth / 2, maxLeftGlyphWidth);
maxRightGlyphWidth = Math.max(glyphWidth / 2, maxRightGlyphWidth);
}
}
width = 0; // reset symbol width
}
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;
}
/** Currently unused. */
protected static noFormat: boolean = false;
protected symbolBlocks: ChordSymbolBlock[] = [];
protected horizontal: number = ChordSymbolHorizontalJustify.LEFT;
protected vertical: number = ChordSymbolVerticalJustify.TOP;
protected useKerning: boolean = true;
protected reportWidth: boolean = true;
// Initialized by the constructor via this.setFont().
protected textFormatter!: TextFormatter;
constructor() {
super();
this.resetFont();
}
/**
* Default text font.
* Choose a font family that works well with the current music engraving font.
* @override `Element.TEXT_FONT`.
*/
static get TEXT_FONT(): Required<FontInfo> {
let family = 'Roboto Slab, Times, serif';
if (Tables.currentMusicFont().getName() === 'Petaluma') {
// Fixes Issue #1180
family = 'PetalumaScript, Arial, sans-serif';
}
return {
family,
size: 12,
weight: FontWeight.NORMAL,
style: FontStyle.NORMAL,
};
}
/**
* The offset is specified in `em`. Scale this value by the font size in pixels.
*/
get superscriptOffset(): number {
return ChordSymbol.superscriptOffset * this.textFormatter.fontSizeInPixels;
}
get subscriptOffset(): number {
return ChordSymbol.subscriptOffset * this.textFormatter.fontSizeInPixels;
}
setReportWidth(value: boolean): this {
this.reportWidth = value;
return this;
}
getReportWidth(): boolean {
return this.reportWidth;
}
updateOverBarAdjustments(): void {
const barIndex = this.symbolBlocks.findIndex(
({ symbolType, glyph }: ChordSymbolBlock) =>
symbolType === SymbolTypes.GLYPH && glyph !== undefined && glyph.code === 'csymDiagonalArrangementSlash'
);
if (barIndex < 0) {
return;
}
const bar = this.symbolBlocks[barIndex];
const xoff = bar.width / 4;
const yoff = 0.25 * this.textFormatter.fontSizeInPixels;
let symIndex = 0;
for (symIndex === 0; symIndex < barIndex; ++symIndex) {
const symbol = this.symbolBlocks[symIndex];
symbol.xShift = symbol.xShift + xoff;
symbol.yShift = symbol.yShift - yoff;
}
for (symIndex = barIndex + 1; symIndex < this.symbolBlocks.length; ++symIndex) {
const symbol = this.symbolBlocks[symIndex];
symbol.xShift = symbol.xShift - xoff;
symbol.yShift = symbol.yShift + yoff;
}
}
updateKerningAdjustments(): void {
let accum = 0;
for (let j = 0; j < this.symbolBlocks.length; ++j) {
const symbol = this.symbolBlocks[j];
accum += this.getKerningAdjustment(j);
symbol.xShift += accum;
}
}
/** Do some basic kerning so that letter chords like 'A' don't have the extensions hanging off to the right. */
getKerningAdjustment(j: number): number {
if (!this.useKerning) {
return 0;
}
const currSymbol = this.symbolBlocks[j];
const prevSymbol = j > 0 ? this.symbolBlocks[j - 1] : undefined;
let adjustment = 0;
// Move things into the '/' over bar
if (
currSymbol.symbolType === SymbolTypes.GLYPH &&
currSymbol.glyph !== undefined &&
currSymbol.glyph.code === ChordSymbol.glyphs.over.code
) {
adjustment += currSymbol.glyph.metrics.x_shift;
}
if (
prevSymbol !== undefined &&
prevSymbol.symbolType === SymbolTypes.GLYPH &&
prevSymbol.glyph !== undefined &&
prevSymbol.glyph.code === ChordSymbol.glyphs.over.code
) {
adjustment += prevSymbol.glyph.metrics.x_shift;
}
// For superscripts that follow a letter without much top part, move it to the left slightly
let preKernUpper = false;
let preKernLower = false;
if (prevSymbol !== undefined && prevSymbol.symbolType === SymbolTypes.TEXT) {
preKernUpper = ChordSymbol.upperKerningText.some((xx) => xx === prevSymbol.text[prevSymbol.text.length - 1]);
preKernLower = ChordSymbol.lowerKerningText.some((xx) => xx === prevSymbol.text[prevSymbol.text.length - 1]);
}
const kerningOffsetPixels = ChordSymbol.kerningOffset * this.textFormatter.fontSizeInPixels;
// TODO: adjust kern for font size.
// Where should this constant live?
if (preKernUpper && currSymbol.symbolModifier === SymbolModifiers.SUPERSCRIPT) {
adjustment += kerningOffsetPixels;
}
if (preKernLower && currSymbol.symbolType === SymbolTypes.TEXT) {
if (currSymbol.text[0] >= 'a' && currSymbol.text[0] <= 'z') {
adjustment += kerningOffsetPixels / 2;
}
if (ChordSymbol.upperKerningText.some((xx) => xx === prevSymbol?.text[prevSymbol.text.length - 1])) {
adjustment += kerningOffsetPixels / 2;
}
}
return adjustment;
}
/**
* ChordSymbol allows multiple blocks so we can mix glyphs and font text.
* Each block can have its own vertical orientation.
*/
// eslint-disable-next-line
getSymbolBlock(params: any = {}): ChordSymbolBlock {
const symbolType = params.symbolType ?? SymbolTypes.TEXT;
const symbolBlock: ChordSymbolBlock = {
text: params.text ?? '',
symbolType,
symbolModifier: params.symbolModifier ?? SymbolModifiers.NONE,
xShift: 0,
yShift: 0,
vAlign: false,
width: 0,
};
// Note: symbol widths are resolution and font-independent.
// We convert to pixel values when we know what the font is.
if (symbolType === SymbolTypes.GLYPH && typeof params.glyph === 'string') {
const glyphArgs = ChordSymbol.glyphs[params.glyph];
const glyphPoints = 20;
symbolBlock.glyph = new Glyph(glyphArgs.code, glyphPoints, { category: 'chordSymbol' });
} else if (symbolType === SymbolTypes.TEXT) {
symbolBlock.width = this.textFormatter.getWidthForTextInEm(symbolBlock.text);
} else if (symbolType === SymbolTypes.LINE) {
symbolBlock.width = params.width;
}
return symbolBlock;
}
/** Add a symbol to this chord, could be text, glyph or line. */
// eslint-disable-next-line
addSymbolBlock(parameters: any): this {
this.symbolBlocks.push(this.getSymbolBlock(parameters));
return this;
}
// ### Convenience functions for creating different types of chord symbol parts.
/** Add a text block. */
// eslint-disable-next-line
addText(text: string, parameters: any = {}): this {
const symbolType = SymbolTypes.TEXT;
return this.addSymbolBlock({ ...parameters, text, symbolType });
}
/** Add a text block with superscript modifier. */
addTextSuperscript(text: string): this {
const symbolType = SymbolTypes.TEXT;
const symbolModifier = SymbolModifiers.SUPERSCRIPT;
return this.addSymbolBlock({ text, symbolType, symbolModifier });
}
/** Add a text block with subscript modifier. */
addTextSubscript(text: string): this {
const symbolType = SymbolTypes.TEXT;
const symbolModifier = SymbolModifiers.SUBSCRIPT;
return this.addSymbolBlock({ text, symbolType, symbolModifier });
}
/** Add a glyph block with superscript modifier. */
addGlyphSuperscript(glyph: string): this {
const symbolType = SymbolTypes.GLYPH;
const symbolModifier = SymbolModifiers.SUPERSCRIPT;
return this.addSymbolBlock({ glyph, symbolType, symbolModifier });
}
/** Add a glyph block. */
// eslint-disable-next-line
addGlyph(glyph: string, params: any = {}): this {
const symbolType = SymbolTypes.GLYPH;
return this.addSymbolBlock({ ...params, glyph, symbolType });
}
/**
* Add a glyph for each character in 'text'. If the glyph is not available, use text from the font.
* e.g. `addGlyphOrText('(+5#11)')` will use text for the '5' and '11', and glyphs for everything else.
*/
// eslint-disable-next-line
addGlyphOrText(text: string, params: any = {}): this {
let str = '';
for (let i = 0; i < text.length; ++i) {
const char = text[i];
if (ChordSymbol.glyphs[char]) {
if (str.length > 0) {
this.addText(str, params);
str = '';
}
this.addGlyph(char, params);
} else {
// Collect consecutive characters with no glyphs.
str += char;
}
}
if (str.length > 0) {
this.addText(str, params);
}
return this;
}
/** Add a line of the given width, used as a continuation of the previous symbol in analysis, or lyrics, etc. */
// eslint-disable-next-line
addLine(width: number, params: any = {}): this {
const symbolType = SymbolTypes.LINE;
return this.addSymbolBlock({ ...params, symbolType, width });
}
/**
* Set the chord symbol's font family, size, weight, style (e.g., `Arial`, `10pt`, `bold`, `italic`).
*
* @param f is 1) a `FontInfo` object or
* 2) a string formatted as CSS font shorthand (e.g., 'bold 10pt Arial') or
* 3) a string representing the font family (one of `size`, `weight`, or `style` must also be provided).
* @param size a string specifying the font size and unit (e.g., '16pt'), or a number (the unit is assumed to be 'pt').
* @param weight is a string (e.g., 'bold', 'normal') or a number (100, 200, ... 900).
* @param style is a string (e.g., 'italic', 'normal').
*
* @override See: Element.
*/
setFont(f?: string | FontInfo, size?: string | number, weight?: string | number, style?: string): this {
super.setFont(f, size, weight, style);
this.textFormatter = TextFormatter.create(this.textFont);
return this;
}
setEnableKerning(val: boolean): this {
this.useKerning = val;
return this;
}
/** Set vertical position of text (above or below stave). */
setVertical(vj: ChordSymbolVerticalJustify | string | number): this {
this.vertical = typeof vj === 'string' ? ChordSymbol.VerticalJustifyString[vj] : vj;
return this;
}
getVertical(): number {
return this.vertical;
}
/** Set horizontal justification. */
setHorizontal(hj: ChordSymbolHorizontalJustify | string | number): this {
this.horizontal = typeof hj === 'string' ? ChordSymbol.HorizontalJustifyString[hj] : hj;
return this;
}
getHorizontal(): number {
return this.horizontal;
}
getWidth(): number {
let width = 0;
this.symbolBlocks.forEach((symbol) => {
width += symbol.vAlign ? 0 : symbol.width;
});
return width;
}
getYOffsetForText(text: string): number {
let acc = 0;
let i = 0;
for (i = 0; i < text.length; ++i) {
const metrics = this.textFormatter.getGlyphMetrics(text[i]);
if (metrics) {
const yMax = metrics.y_max ?? 0;
acc = yMax < acc ? yMax : acc;
}
}
const resolution = this.textFormatter.getResolution();
return i > 0 ? -1 * (acc / resolution) : 0;
}
/** Render text and glyphs above/below the note. */
draw(): void {
const ctx = this.checkContext();
const note = this.checkAttachedNote() as StemmableNote;
this.setRendered();
// We're changing context parameters. Save current state.
ctx.save();
this.applyStyle();
ctx.openGroup('chordsymbol', this.getAttribute('id'));
const start = note.getModifierStartXY(Modifier.Position.ABOVE, this.index);
ctx.setFont(this.textFont);
let y: number;
// The position of the text varies based on whether or not the note
// has a stem.
const hasStem = note.hasStem();
const stave = note.checkStave();
if (this.vertical === ChordSymbolVerticalJustify.BOTTOM) {
// HACK: We need to compensate for the text's height since its origin is bottom-right.
y = stave.getYForBottomText(this.text_line + Tables.TEXT_HEIGHT_OFFSET_HACK);
if (hasStem) {
const stem_ext = note.checkStem().getExtents();
const spacing = stave.getSpacingBetweenLines();
const stem_base = note.getStemDirection() === 1 ? stem_ext.baseY : stem_ext.topY;
y = Math.max(y, stem_base + spacing * (this.text_line + 2));
}
} else {
// (this.vertical === VerticalJustify.TOP)
const topY = Math.min(...note.getYs());
y = Math.min(stave.getYForTopText(this.text_line), topY - 10);
if (hasStem) {
const stem_ext = note.checkStem().getExtents();
const spacing = stave.getSpacingBetweenLines();
y = Math.min(y, stem_ext.topY - 5 - spacing * this.text_line);
}
}
let x = start.x;
if (this.horizontal === ChordSymbolHorizontalJustify.LEFT) {
x = start.x;
} else if (this.horizontal === ChordSymbolHorizontalJustify.RIGHT) {
x = start.x + this.getWidth();
} else if (this.horizontal === ChordSymbolHorizontalJustify.CENTER) {
x = start.x - this.getWidth() / 2;
} else {
// HorizontalJustify.CENTER_STEM
x = note.getStemX() - this.getWidth() / 2;
}
L('Rendering ChordSymbol: ', this.textFormatter, x, y);
this.symbolBlocks.forEach((symbol) => {
const isSuper = ChordSymbol.isSuperscript(symbol);
const isSub = ChordSymbol.isSubscript(symbol);
let curY = y;
L('shift was ', symbol.xShift, symbol.yShift);
L('curY pre sub ', curY);
if (isSuper) {
curY += this.superscriptOffset;
}
if (isSub) {
curY += this.subscriptOffset;
}
L('curY sup/sub ', curY);
if (symbol.symbolType === SymbolTypes.TEXT) {
if (isSuper || isSub) {
ctx.save();
if (this.textFont) {
const { family, size, weight, style } = this.textFont;
const smallerFontSize = Font.scaleSize(size, ChordSymbol.superSubRatio);
ctx.setFont(family, smallerFontSize, weight, style);
}
}
// TODO???
// We estimate the text width, fill it in with the empirical value so the formatting is even.
// const textDim = ctx.measureText(symbol.text);
// symbol.width = textDim.width;
L('Rendering Text: ', symbol.text, x + symbol.xShift, curY + symbol.yShift);
ctx.fillText(symbol.text, x + symbol.xShift, curY + symbol.yShift);
if (isSuper || isSub) {
ctx.restore();
}
} else if (symbol.symbolType === SymbolTypes.GLYPH && symbol.glyph) {
curY += symbol.yShift;
L('Rendering Glyph: ', symbol.glyph.code, x + symbol.xShift, curY);
symbol.glyph.render(ctx, x + symbol.xShift, curY);
} else if (symbol.symbolType === SymbolTypes.LINE) {
L('Rendering Line : ', symbol.width, x, curY);
ctx.beginPath();
ctx.setLineWidth(1); // ?
ctx.moveTo(x, y);
ctx.lineTo(x + symbol.width, curY);
ctx.stroke();
}
x += symbol.width;
if (symbol.vAlign) {
x += symbol.xShift;
}
});
ctx.closeGroup();
this.restoreStyle();
ctx.restore();
}
}