vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
249 lines (217 loc) • 6.75 kB
text/typescript
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
// MIT License
import { Font, FontInfo, FontStyle, FontWeight } from './font';
import { Glyph } from './glyph';
import { Note, NoteStruct } from './note';
import { Tables } from './tables';
import { Category } from './typeguard';
import { RuntimeError } from './util';
export enum TextJustification {
LEFT = 1,
CENTER = 2,
RIGHT = 3,
}
export interface TextNoteStruct extends NoteStruct {
text?: string;
glyph?: string;
ignore_ticks?: boolean;
smooth?: boolean;
font?: FontInfo;
subscript?: string;
superscript?: string;
}
/**
* `TextNote` is a notation element that is positioned in time. Generally
* meant for objects that sit above/below the staff and inline with each other.
* `TextNote` has to be assigned to a `Stave` before rendering by means of `setStave`.
* Examples of this would be such as dynamics, lyrics, chord changes, etc.
*/
export class TextNote extends Note {
static get CATEGORY(): string {
return Category.TextNote;
}
static TEXT_FONT: Required<FontInfo> = {
family: Font.SANS_SERIF,
size: 12,
weight: FontWeight.NORMAL,
style: FontStyle.NORMAL,
};
static readonly Justification = TextJustification;
/** Glyph data. */
static get GLYPHS(): Record<string, { code: string }> {
return {
segno: {
code: 'segno',
},
tr: {
code: 'ornamentTrill',
},
mordent: {
code: 'ornamentMordent',
},
mordent_upper: {
code: 'ornamentShortTrill',
},
mordent_lower: {
code: 'ornamentMordent',
},
f: {
code: 'dynamicForte',
},
p: {
code: 'dynamicPiano',
},
m: {
code: 'dynamicMezzo',
},
s: {
code: 'dynamicSforzando',
},
z: {
code: 'dynamicZ',
},
coda: {
code: 'coda',
},
pedal_open: {
code: 'keyboardPedalPed',
},
pedal_close: {
code: 'keyboardPedalUp',
},
caesura_straight: {
code: 'caesura',
},
caesura_curved: {
code: 'caesuraCurved',
},
breath: {
code: 'breathMarkComma',
},
tick: {
code: 'breathMarkTick',
},
turn: {
code: 'ornamentTurn',
},
turn_inverted: {
code: 'ornamentTurnSlash',
},
};
}
protected text: string;
protected glyph?: Glyph;
protected superscript?: string;
protected subscript?: string;
protected smooth: boolean;
protected justification: TextJustification;
protected line: number;
constructor(noteStruct: TextNoteStruct) {
super(noteStruct);
this.text = noteStruct.text || '';
this.superscript = noteStruct.superscript;
this.subscript = noteStruct.subscript;
this.setFont(noteStruct.font);
this.line = noteStruct.line || 0;
this.smooth = noteStruct.smooth || false;
this.ignore_ticks = noteStruct.ignore_ticks || false;
this.justification = TextJustification.LEFT;
// Determine and set initial note width. Note that the text width is
// an approximation and isn't very accurate. The only way to accurately
// measure the length of text is with `CanvasRenderingContext2D.measureText()`.
if (noteStruct.glyph) {
const struct = TextNote.GLYPHS[noteStruct.glyph];
if (!struct) throw new RuntimeError('Invalid glyph type: ' + noteStruct.glyph);
this.glyph = new Glyph(struct.code, Tables.NOTATION_FONT_SCALE, { category: 'textNote' });
this.setWidth(this.glyph.getMetrics().width);
} else {
this.glyph = undefined;
}
}
/** Set the horizontal justification of the TextNote. */
setJustification(just: TextJustification): this {
this.justification = just;
return this;
}
/** Set the Stave line on which the note should be placed. */
setLine(line: number): this {
this.line = line;
return this;
}
/** Return the Stave line on which the TextNote is placed. */
getLine(): number {
return this.line;
}
/** Return the unformatted text of this TextNote. */
getText(): string {
return this.text;
}
/** Pre-render formatting. */
preFormat(): void {
if (this.preFormatted) return;
const tickContext = this.checkTickContext(`Can't preformat without a TickContext.`);
if (this.smooth) {
this.setWidth(0);
} else {
if (this.glyph) {
// Width already set.
} else {
const ctx = this.checkContext();
ctx.setFont(this.textFont);
this.setWidth(ctx.measureText(this.text).width);
}
}
if (this.justification === TextJustification.CENTER) {
this.leftDisplacedHeadPx = this.width / 2;
} else if (this.justification === TextJustification.RIGHT) {
this.leftDisplacedHeadPx = this.width;
}
// We reposition to the center of the note head
this.rightDisplacedHeadPx = tickContext.getMetrics().glyphPx / 2;
this.preFormatted = true;
}
/**
* Renders the TextNote.
* `TextNote` has to be assigned to a `Stave` before rendering by means of `setStave`.
*/
draw(): void {
const ctx = this.checkContext();
const stave = this.checkStave();
const tickContext = this.checkTickContext(`Can't draw without a TickContext.`);
this.setRendered();
// Reposition to center of note head
let x = this.getAbsoluteX() + tickContext.getMetrics().glyphPx / 2;
// Align based on tick-context width.
const width = this.getWidth();
if (this.justification === TextJustification.CENTER) {
x -= width / 2;
} else if (this.justification === TextJustification.RIGHT) {
x -= width;
}
let y;
if (this.glyph) {
y = stave.getYForLine(this.line + -3);
this.glyph.render(ctx, x, y);
} else {
y = stave.getYForLine(this.line + -3);
this.applyStyle(ctx);
ctx.setFont(this.textFont);
ctx.fillText(this.text, x, y);
const height = ctx.measureText(this.text).height;
// We called this.setFont(...) in the constructor, so we know this.textFont is available.
// eslint-disable-next-line
const { family, size, weight, style } = this.textFont!;
// Scale the font size by 1/1.3.
const smallerFontSize = Font.scaleSize(size, 0.769231);
if (this.superscript) {
ctx.setFont(family, smallerFontSize, weight, style);
ctx.fillText(this.superscript, x + this.width + 2, y - height / 2.2);
}
if (this.subscript) {
ctx.setFont(family, smallerFontSize, weight, style);
ctx.fillText(this.subscript, x + this.width + 2, y + height / 2.2 - 1);
}
this.restoreStyle(ctx);
}
}
}