vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
295 lines (253 loc) • 9.29 kB
text/typescript
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
// MIT License
import { Element } from './element';
import { Font, FontInfo, FontStyle, FontWeight } from './font';
import { Glyph } from './glyph';
import { RenderContext } from './rendercontext';
import { StaveNote } from './stavenote';
import { Tables } from './tables';
import { Category } from './typeguard';
import { log, RuntimeError } from './util';
// eslint-disable-next-line
function L(...args: any[]) {
if (PedalMarking.DEBUG) log('Vex.Flow.PedalMarking', args);
}
/**
* Draws a pedal glyph with the provided `name` on a rendering `context`
* at the coordinates `x` and `y. Takes into account the glyph data
* coordinate shifts.
*/
function drawPedalGlyph(name: string, context: RenderContext, x: number, y: number, point: number): void {
const glyph_data = PedalMarking.GLYPHS[name];
const glyph = new Glyph(glyph_data.code, point, { category: 'pedalMarking' });
// Center the middle of the glyph with the middle of the note head (Tables.STAVE_LINE_DISTANCE / 2)
glyph.render(context, x - (glyph.getMetrics().width - Tables.STAVE_LINE_DISTANCE) / 2, y);
}
/**
* PedalMarking implements different types of pedal markings. These notation
* elements indicate to the performer when to depress and release the a pedal.
*
* In order to create "Sostenuto", and "una corda" markings, you must set
* custom text for the release/depress pedal markings.
*/
export class PedalMarking extends Element {
/** To enable logging for this class. Set `Vex.Flow.PedalMarking.DEBUG` to `true`. */
static DEBUG: boolean = false;
static get CATEGORY(): string {
return Category.PedalMarking;
}
static TEXT_FONT: Required<FontInfo> = {
family: Font.SERIF,
size: 12,
weight: FontWeight.BOLD,
style: FontStyle.ITALIC,
};
protected line: number;
protected type: number;
protected custom_depress_text: string;
protected custom_release_text: string;
public render_options: {
color: string;
bracket_height: number;
text_margin_right: number;
bracket_line_width: number;
};
protected notes: StaveNote[];
/** Glyph data */
static readonly GLYPHS: Record<string, { code: string }> = {
pedal_depress: {
code: 'keyboardPedalPed',
},
pedal_release: {
code: 'keyboardPedalUp',
},
};
/** Pedal type as number. */
static readonly type = {
TEXT: 1,
BRACKET: 2,
MIXED: 3,
};
/** Pedal type as string. */
static readonly typeString: Record<string, number> = {
text: PedalMarking.type.TEXT,
bracket: PedalMarking.type.BRACKET,
mixed: PedalMarking.type.MIXED,
};
/**
* Create a sustain pedal marking. Returns the defaults PedalMarking.
* Which uses the traditional "Ped" and "*"" markings.
*/
static createSustain(notes: StaveNote[]): PedalMarking {
const pedal = new PedalMarking(notes);
return pedal;
}
/** Create a sostenuto pedal marking */
static createSostenuto(notes: StaveNote[]): PedalMarking {
const pedal = new PedalMarking(notes);
pedal.setType(PedalMarking.type.MIXED);
pedal.setCustomText('Sost. Ped.');
return pedal;
}
/** Create an una corda pedal marking */
static createUnaCorda(notes: StaveNote[]): PedalMarking {
const pedal = new PedalMarking(notes);
pedal.setType(PedalMarking.type.TEXT);
pedal.setCustomText('una corda', 'tre corda');
return pedal;
}
constructor(notes: StaveNote[]) {
super();
this.notes = notes;
this.type = PedalMarking.type.TEXT;
this.line = 0;
// Custom text for the release/depress markings
this.custom_depress_text = '';
this.custom_release_text = '';
this.resetFont();
this.render_options = {
bracket_height: 10,
text_margin_right: 6,
bracket_line_width: 1,
color: 'black',
};
}
/** Set pedal type. */
setType(type: string | number): this {
type = typeof type === 'string' ? PedalMarking.typeString[type] : type;
if (type >= PedalMarking.type.TEXT && type <= PedalMarking.type.MIXED) {
this.type = type;
}
return this;
}
/**
* Set custom text for the `depress`/`release` pedal markings. No text is
* set if the parameter is falsy.
*/
setCustomText(depress: string, release?: string): this {
this.custom_depress_text = depress || '';
this.custom_release_text = release || '';
return this;
}
/** Set the staff line to render the markings on. */
setLine(line: number): this {
this.line = line;
return this;
}
/** Draw the bracket based pedal markings. */
drawBracketed(): void {
const ctx = this.checkContext();
let is_pedal_depressed = false;
let prev_x: number;
let prev_y: number;
// Iterate through each note
this.notes.forEach((note, index, notes) => {
// Each note triggers the opposite pedal action
is_pedal_depressed = !is_pedal_depressed;
// Get the initial coordinates for the note
const x = note.getAbsoluteX();
const y = note.checkStave().getYForBottomText(this.line + 3);
// Throw if current note is positioned before the previous note
if (x < prev_x) {
throw new RuntimeError('InvalidConfiguration', 'The notes provided must be in order of ascending x positions');
}
// Determine if the previous or next note are the same
// as the current note. We need to keep track of this for
// when adjustments are made for the release+depress action
const next_is_same = notes[index + 1] === note;
const prev_is_same = notes[index - 1] === note;
let x_shift = 0;
const point =
Tables.currentMusicFont().lookupMetric(`pedalMarking.${is_pedal_depressed ? 'down' : 'up'}.point`) ??
Tables.NOTATION_FONT_SCALE;
if (is_pedal_depressed) {
// Adjustment for release+depress
x_shift = prev_is_same ? 5 : 0;
if (this.type === PedalMarking.type.MIXED && !prev_is_same) {
// For MIXED style, start with text instead of bracket
if (this.custom_depress_text) {
// If we have custom text, use instead of the default "Ped" glyph
const text_width = ctx.measureText(this.custom_depress_text).width;
ctx.fillText(this.custom_depress_text, x - text_width / 2, y);
x_shift = text_width / 2 + this.render_options.text_margin_right;
} else {
// Render the Ped glyph in position
drawPedalGlyph('pedal_depress', ctx, x, y, point);
x_shift = 20 + this.render_options.text_margin_right;
}
} else {
// Draw start bracket
ctx.beginPath();
ctx.moveTo(x, y - this.render_options.bracket_height);
ctx.lineTo(x + x_shift, y);
ctx.stroke();
ctx.closePath();
}
} else {
// Adjustment for release+depress
x_shift = next_is_same ? -5 : 0;
// Draw end bracket
ctx.beginPath();
ctx.moveTo(prev_x, prev_y);
ctx.lineTo(x + x_shift, y);
ctx.lineTo(x, y - this.render_options.bracket_height);
ctx.stroke();
ctx.closePath();
}
// Store previous coordinates
prev_x = x + x_shift;
prev_y = y;
});
}
/**
* Draw the text based pedal markings. This defaults to the traditional
* "Ped" and "*"" symbols if no custom text has been provided.
*/
drawText(): void {
const ctx = this.checkContext();
let is_pedal_depressed = false;
// Iterate through each note, placing glyphs or custom text accordingly
this.notes.forEach((note) => {
is_pedal_depressed = !is_pedal_depressed;
const stave = note.checkStave();
const x = note.getAbsoluteX();
const y = stave.getYForBottomText(this.line + 3);
const point =
Tables.currentMusicFont().lookupMetric(`pedalMarking.${is_pedal_depressed ? 'down' : 'up'}.point`) ??
Tables.NOTATION_FONT_SCALE;
let text_width = 0;
if (is_pedal_depressed) {
if (this.custom_depress_text) {
text_width = ctx.measureText(this.custom_depress_text).width;
ctx.fillText(this.custom_depress_text, x - text_width / 2, y);
} else {
drawPedalGlyph('pedal_depress', ctx, x, y, point);
}
} else {
if (this.custom_release_text) {
text_width = ctx.measureText(this.custom_release_text).width;
ctx.fillText(this.custom_release_text, x - text_width / 2, y);
} else {
drawPedalGlyph('pedal_release', ctx, x, y, point);
}
}
});
}
/** Render the pedal marking in position on the rendering context. */
draw(): void {
const ctx = this.checkContext();
this.setRendered();
ctx.save();
ctx.setStrokeStyle(this.render_options.color);
ctx.setFillStyle(this.render_options.color);
ctx.setFont(this.textFont);
L('Rendering Pedal Marking');
if (this.type === PedalMarking.type.BRACKET || this.type === PedalMarking.type.MIXED) {
ctx.setLineWidth(this.render_options.bracket_line_width);
this.drawBracketed();
} else if (this.type === PedalMarking.type.TEXT) {
this.drawText();
}
ctx.restore();
}
}