UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature.

347 lines (294 loc) 11.9 kB
// [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(); } }