vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
347 lines (294 loc) • 11.9 kB
text/typescript
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
// Author: Cyril Silverman
// MIT License
import { Glyph } from './glyph';
import { Modifier } from './modifier';
import { ModifierContextState } from './modifiercontext';
import { Stem } from './stem';
import { StemmableNote } from './stemmablenote';
import { Tables } from './tables';
import { TickContext } from './tickcontext';
import { Category, isTabNote } from './typeguard';
import { defined, log, RuntimeError } from './util';
// eslint-disable-next-line
function L(...args: any[]) {
if (Ornament.DEBUG) log('Vex.Flow.Ornament', args);
}
export interface OrnamentMetrics {
xOffset: number;
yOffset: number;
stemUpYOffset: number;
reportedWidth: number;
}
/**
* Ornament implements ornaments as modifiers that can be
* attached to notes. The complete list of ornaments is available in
* `tables.ts` under `Vex.Flow.ornamentCodes`.
*
* See `tests/ornament_tests.ts` for usage examples.
*/
export class Ornament extends Modifier {
/** To enable logging for this class. Set `Vex.Flow.Ornament.DEBUG` to `true`. */
static DEBUG: boolean = false;
/** Ornaments category string. */
static get CATEGORY(): string {
return Category.Ornament;
}
static get minPadding(): number {
const musicFont = Tables.currentMusicFont();
return musicFont.lookupMetric('noteHead.minPadding');
}
protected ornament: {
code: string;
};
protected stemUpYOffset: number;
protected ornamentAlignWithNoteHead: string[] | boolean;
protected type: string;
protected delayed: boolean;
protected reportedWidth: number;
protected adjustForStemDirection: boolean;
public render_options: {
accidentalUpperPadding: number;
accidentalLowerPadding: number;
font_scale: number;
};
protected glyph: Glyph;
protected accidentalUpper?: Glyph;
protected accidentalLower?: Glyph;
protected delayXShift?: number;
/** Arrange ornaments inside `ModifierContext` */
static format(ornaments: Ornament[], state: ModifierContextState): boolean {
if (!ornaments || ornaments.length === 0) return false;
let width = 0; // width is used by ornaments, which are always centered on the note head
let right_shift = state.right_shift; // jazz ornaments calculate r/l shift separately
let left_shift = state.left_shift;
let yOffset = 0;
for (let i = 0; i < ornaments.length; ++i) {
const ornament = ornaments[i];
const increment = 2;
if (Ornament.ornamentRelease.indexOf(ornament.type) >= 0) {
ornament.x_shift += right_shift + 2;
}
if (Ornament.ornamentAttack.indexOf(ornament.type) >= 0) {
ornament.x_shift -= left_shift + 2;
}
if (ornament.reportedWidth && ornament.x_shift < 0) {
left_shift += ornament.reportedWidth;
} else if (ornament.reportedWidth && ornament.x_shift >= 0) {
right_shift += ornament.reportedWidth + Ornament.minPadding;
} else {
width = Math.max(ornament.getWidth(), width);
}
// articulations above/below the line can be stacked.
if (Ornament.ornamentArticulation.indexOf(ornament.type) >= 0) {
// Unfortunately we don't know the stem direction. So we base it
// on the line number, but also allow it to be overridden.
const ornamentNote = defined(ornament.note, 'NoAttachedNote');
if (ornamentNote.getLineNumber() >= 3 || ornament.getPosition() === Modifier.Position.ABOVE) {
state.top_text_line += increment;
ornament.y_shift += yOffset;
yOffset -= ornament.glyph.bbox.getH();
} else {
state.text_line += increment;
ornament.y_shift += yOffset;
yOffset += ornament.glyph.bbox.getH();
}
} else {
if (ornament.getPosition() === Modifier.Position.ABOVE) {
ornament.setTextLine(state.top_text_line);
state.top_text_line += increment;
} else {
ornament.setTextLine(state.text_line);
state.text_line += increment;
}
}
}
// Note: 'legit' ornaments don't consider other modifiers when calculating their
// X position, but jazz ornaments sometimes need to.
state.left_shift = left_shift + width / 2;
state.right_shift = right_shift + width / 2;
return true;
}
/**
* ornamentNoteTransition means the jazz ornament represents an effect from one note to another,
* these are generally on the top of the staff.
*/
static get ornamentNoteTransition(): string[] {
return ['flip', 'jazzTurn', 'smear'];
}
/**
* ornamentAttack indicates something that happens in the attach, placed before the note and
* any accidentals
*/
static get ornamentAttack(): string[] {
return ['scoop'];
}
/**
* The ornament is aligned based on the note head, but without regard to whether the
* stem goes up or down.
*/
static get ornamentAlignWithNoteHead(): string[] {
return ['doit', 'fall', 'fallLong', 'doitLong', 'bend', 'plungerClosed', 'plungerOpen', 'scoop'];
}
/**
* An ornament that happens on the release of the note, generally placed after the
* note and overlapping the next beat/measure..
*/
static get ornamentRelease(): string[] {
return ['doit', 'fall', 'fallLong', 'doitLong', 'jazzTurn', 'smear', 'flip'];
}
/** ornamentArticulation goes above/below the note based on space availablity */
static get ornamentArticulation(): string[] {
return ['bend', 'plungerClosed', 'plungerOpen'];
}
/**
* Legacy ornaments have hard-coded metrics. If additional ornament types are
* added, get their metrics here.
*/
getMetrics(): OrnamentMetrics {
const ornamentMetrics = Tables.currentMusicFont().getMetrics().ornament;
if (!ornamentMetrics) throw new RuntimeError('BadMetrics', `ornament missing`);
return ornamentMetrics[this.ornament.code];
}
/**
* Create a new ornament of type `type`, which is an entry in
* `Vex.Flow.ornamentCodes` in `tables.ts`.
*/
constructor(type: string) {
super();
this.type = type;
this.delayed = false;
this.render_options = {
font_scale: Tables.NOTATION_FONT_SCALE,
accidentalLowerPadding: 3,
accidentalUpperPadding: 3,
};
this.ornament = Tables.ornamentCodes(this.type);
// new ornaments have their origin at the origin, and have more specific
// metrics. Legacy ornaments do some
// x scaling, and have hard-coded metrics
const metrics = this.getMetrics();
// some jazz ornaments are above or below depending on stem direction.
this.adjustForStemDirection = false;
// some jazz ornaments like falls are supposed to overlap with future bars
// and so we report a different width than they actually take up.
this.reportedWidth = metrics && metrics.reportedWidth ? metrics.reportedWidth : 0;
this.stemUpYOffset = metrics && metrics.stemUpYOffset ? metrics.stemUpYOffset : 0;
this.ornamentAlignWithNoteHead = Ornament.ornamentAlignWithNoteHead.indexOf(this.type) >= 0;
if (!this.ornament) {
throw new RuntimeError('ArgumentError', `Ornament not found: '${this.type}'`);
}
this.x_shift = metrics ? metrics.xOffset : 0;
this.y_shift = metrics ? metrics.yOffset : 0;
this.glyph = new Glyph(this.ornament.code, this.render_options.font_scale, {
category: `ornament.${this.ornament.code}`,
});
// Is this a jazz ornament that goes between this note and the next note.
if (Ornament.ornamentNoteTransition.indexOf(this.type) >= 0) {
this.delayed = true;
}
// Legacy ornaments need this. I don't know why, but horizontal spacing issues
// happen if I don't set it.
if (!metrics) {
this.glyph.setOrigin(0.5, 1.0); // FIXME: SMuFL won't require a vertical origin shift
}
}
/** Set whether the ornament is to be delayed. */
setDelayed(delayed: boolean): this {
this.delayed = delayed;
return this;
}
/** Set the upper accidental for the ornament. */
setUpperAccidental(accid: string): this {
const scale = this.render_options.font_scale / 1.3;
this.accidentalUpper = new Glyph(Tables.accidentalCodes(accid).code, scale);
this.accidentalUpper.setOrigin(0.5, 1.0);
return this;
}
/** Set the lower accidental for the ornament. */
setLowerAccidental(accid: string): this {
const scale = this.render_options.font_scale / 1.3;
this.accidentalLower = new Glyph(Tables.accidentalCodes(accid).code, scale);
this.accidentalLower.setOrigin(0.5, 1.0);
return this;
}
/** Render ornament in position next to note. */
draw(): void {
const ctx = this.checkContext();
const note = this.checkAttachedNote() as StemmableNote;
this.setRendered();
const stemDir = note.getStemDirection();
const stave = note.checkStave();
this.applyStyle();
ctx.openGroup('ornament', this.getAttribute('id'));
// Get stem extents
const stemExtents = note.checkStem().getExtents();
let y = stemDir === Stem.DOWN ? stemExtents.baseY : stemExtents.topY;
// TabNotes don't have stems attached to them. Tab stems are rendered outside the stave.
if (isTabNote(note)) {
if (note.hasStem()) {
if (stemDir === Stem.DOWN) {
y = stave.getYForTopText(this.text_line);
}
} else {
// Without a stem
y = stave.getYForTopText(this.text_line);
}
}
const isPlacedOnNoteheadSide = stemDir === Stem.DOWN;
const spacing = stave.getSpacingBetweenLines();
let lineSpacing = 1;
// Beamed stems are longer than quarter note stems, adjust accordingly
if (!isPlacedOnNoteheadSide && note.hasBeam()) {
lineSpacing += 0.5;
}
const totalSpacing = spacing * (this.text_line + lineSpacing);
const glyphYBetweenLines = y - totalSpacing;
// Get initial coordinates for the modifier position
const start = note.getModifierStartXY(this.position, this.index);
let glyphX = start.x;
// If the ornament is aligned with the note head, don't consider the stave y
// but use the 'natural' modifier y
let glyphY = this.ornamentAlignWithNoteHead
? start.y
: Math.min(stave.getYForTopText(this.text_line), glyphYBetweenLines);
glyphY += this.y_shift;
// Ajdust x position if ornament is delayed
if (this.delayed) {
let delayXShift = 0;
const startX = glyphX - (stave.getX() - 10);
if (this.delayXShift !== undefined) {
delayXShift = this.delayXShift;
} else {
delayXShift += this.glyph.getMetrics().width / 2;
const nextContext = TickContext.getNextContext(note.getTickContext());
if (nextContext) {
delayXShift += (nextContext.getX() - startX) * 0.5;
} else {
delayXShift += (stave.getX() + stave.getWidth() - startX) * 0.5;
}
this.delayXShift = delayXShift;
}
glyphX += delayXShift;
}
L('Rendering ornament: ', this.ornament, glyphX, glyphY);
if (this.accidentalLower) {
this.accidentalLower.render(ctx, glyphX, glyphY);
glyphY -= this.accidentalLower.getMetrics().height;
glyphY -= this.render_options.accidentalLowerPadding;
}
if (this.stemUpYOffset && note.hasStem() && note.getStemDirection() === 1) {
glyphY += this.stemUpYOffset;
}
if (note.getLineNumber() < 5 && Ornament.ornamentNoteTransition.indexOf(this.type) >= 0) {
glyphY = note.checkStave().getBoundingBox().getY() + 40;
}
this.glyph.render(ctx, glyphX + this.x_shift, glyphY);
if (this.accidentalUpper) {
glyphY -= this.glyph.getMetrics().height + this.render_options.accidentalUpperPadding;
this.accidentalUpper.render(ctx, glyphX, glyphY);
}
ctx.closeGroup();
this.restoreStyle();
}
}