vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
267 lines (223 loc) • 7.89 kB
text/typescript
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
//
// ## Description
// `StemmableNote` is an abstract interface for notes with optional stems.
// Examples of stemmable notes are `StaveNote` and `TabNote`
import { Glyph, GlyphProps } from './glyph';
import { Note, NoteStruct } from './note';
import { Stem, StemOptions } from './stem';
import { Tables } from './tables';
import { Category } from './typeguard';
import { RuntimeError } from './util';
export abstract class StemmableNote extends Note {
static get CATEGORY(): string {
return Category.StemmableNote;
}
stem_direction?: number;
stem?: Stem;
protected flag?: Glyph;
protected stem_extension_override?: number;
constructor(noteStruct: NoteStruct) {
super(noteStruct);
}
// Get and set the note's `Stem`
getStem(): Stem | undefined {
return this.stem;
}
checkStem(): Stem {
if (!this.stem) {
throw new RuntimeError('NoStem', 'No stem attached to instance');
}
return this.stem;
}
setStem(stem: Stem): this {
this.stem = stem;
this.addChildElement(stem);
return this;
}
// Builds and sets a new stem
buildStem(): this {
const stem = new Stem();
this.setStem(stem);
return this;
}
buildFlag(category = 'flag'): void {
const { glyphProps } = this;
if (this.hasFlag()) {
const flagCode =
this.getStemDirection() === Stem.DOWN ? glyphProps.code_flag_downstem : glyphProps.code_flag_upstem;
if (flagCode) this.flag = new Glyph(flagCode, this.render_options.glyph_font_scale, { category });
}
}
// Get the custom glyph associated with the outer note head on the base of the stem.
getBaseCustomNoteHeadGlyphProps(): GlyphProps {
if (this.getStemDirection() === Stem.DOWN) {
return this.customGlyphs[this.customGlyphs.length - 1];
} else {
return this.customGlyphs[0];
}
}
// Get the full length of stem
getStemLength(): number {
return Stem.HEIGHT + this.getStemExtension();
}
// Get the number of beams for this duration
getBeamCount(): number {
const glyphProps = this.getGlyphProps();
if (glyphProps) {
return glyphProps.beam_count;
} else {
return 0;
}
}
// Get the minimum length of stem
getStemMinimumLength(): number {
const frac = Tables.durationToFraction(this.duration);
let length = frac.value() <= 1 ? 0 : 20;
// if note is flagged, cannot shorten beam
switch (this.duration) {
case '8':
if (this.beam == undefined) length = 35;
break;
case '16':
length = this.beam == undefined ? 35 : 25;
break;
case '32':
length = this.beam == undefined ? 45 : 35;
break;
case '64':
length = this.beam == undefined ? 50 : 40;
break;
case '128':
length = this.beam == undefined ? 55 : 45;
break;
default:
break;
}
return length;
}
// Get/set the direction of the stem
getStemDirection(): number {
if (!this.stem_direction) throw new RuntimeError('NoStem', 'No stem attached to this note.');
return this.stem_direction;
}
setStemDirection(direction?: number): this {
if (!direction) direction = Stem.UP;
if (direction !== Stem.UP && direction !== Stem.DOWN) {
throw new RuntimeError('BadArgument', `Invalid stem direction: ${direction}`);
}
this.stem_direction = direction;
// Reset and reformat everything. Flag has to be built before calling getStemExtension.
this.reset();
if (this.hasFlag()) {
this.buildFlag();
}
this.beam = undefined;
if (this.stem) {
this.stem.setDirection(direction);
this.stem.setExtension(this.getStemExtension());
// Lookup the base custom notehead (closest to the base of the stem) to extend or shorten
// the stem appropriately. If there's no custom note head, lookup the standard notehead.
const glyphProps = this.getBaseCustomNoteHeadGlyphProps() || this.getGlyphProps();
// Get the font-specific customizations for the note heads.
const offsets = Tables.currentMusicFont().lookupMetric(`stem.noteHead.${glyphProps.code_head}`, {
offsetYBaseStemUp: 0,
offsetYTopStemUp: 0,
offsetYBaseStemDown: 0,
offsetYTopStemDown: 0,
});
// Configure the stem to use these offsets.
this.stem.setOptions({
stem_up_y_offset: offsets.offsetYTopStemUp, // glyph.stem_up_y_offset,
stem_down_y_offset: offsets.offsetYTopStemDown, // glyph.stem_down_y_offset,
stem_up_y_base_offset: offsets.offsetYBaseStemUp, // glyph.stem_up_y_base_offset,
stem_down_y_base_offset: offsets.offsetYBaseStemDown, // glyph.stem_down_y_base_offset,
});
}
if (this.preFormatted) {
this.preFormat();
}
return this;
}
// Get the `x` coordinate of the stem
getStemX(): number {
const x_begin = this.getAbsoluteX() + this.x_shift;
const x_end = this.getAbsoluteX() + this.x_shift + this.getGlyphWidth();
const stem_x = this.stem_direction === Stem.DOWN ? x_begin : x_end;
return stem_x;
}
// Get the `x` coordinate for the center of the glyph.
// Used for `TabNote` stems and stemlets over rests
getCenterGlyphX(): number {
return this.getAbsoluteX() + this.x_shift + this.getGlyphWidth() / 2;
}
// Get the stem extension for the current duration
getStemExtension(): number {
const glyphProps = this.getGlyphProps();
if (this.stem_extension_override != undefined) {
return this.stem_extension_override;
}
// Use stem_beam_extension with beams
if (this.beam) {
return glyphProps.stem_beam_extension;
}
if (glyphProps) {
return this.getStemDirection() === Stem.UP ? glyphProps.stem_up_extension : glyphProps.stem_down_extension;
}
return 0;
}
// Set the stem length to a specific. Will override the default length.
setStemLength(height: number): this {
this.stem_extension_override = height - Stem.HEIGHT;
return this;
}
// Get the top and bottom `y` values of the stem.
getStemExtents(): { topY: number; baseY: number } {
if (!this.stem) throw new RuntimeError('NoStem', 'No stem attached to this note.');
return this.stem.getExtents();
}
/** Gets the `y` value for the top modifiers at a specific `textLine`. */
getYForTopText(textLine: number): number {
const stave = this.checkStave();
if (this.hasStem()) {
const extents = this.getStemExtents();
if (!extents) throw new RuntimeError('InvalidState', 'Stem does not have extents.');
return Math.min(
stave.getYForTopText(textLine),
extents.topY - this.render_options.annotation_spacing * (textLine + 1)
);
} else {
return stave.getYForTopText(textLine);
}
}
/** Gets the `y` value for the bottom modifiers at a specific `textLine`. */
getYForBottomText(textLine: number): number {
const stave = this.checkStave();
if (this.hasStem()) {
const extents = this.getStemExtents();
if (!extents) throw new RuntimeError('InvalidState', 'Stem does not have extents.');
return Math.max(
stave.getYForTopText(textLine),
extents.baseY + this.render_options.annotation_spacing * textLine
);
} else {
return stave.getYForBottomText(textLine);
}
}
hasFlag(): boolean {
return Tables.getGlyphProps(this.duration).flag == true && !this.beam;
}
/** Post formats the note. */
postFormat(): this {
this.beam?.postFormat();
this.postFormatted = true;
return this;
}
/** Renders the stem onto the canvas. */
drawStem(stemOptions: StemOptions): void {
this.checkContext();
this.setRendered();
this.setStem(new Stem(stemOptions));
this.stem?.setContext(this.getContext()).draw();
}
}