vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
285 lines (235 loc) • 9.56 kB
text/typescript
// VexFlow - Music Engraving for HTML5
// Copyright Mohit Muthanna 2010
//
// This class implements multiple measure rests.
import { Element } from './element';
import { Glyph } from './glyph';
import { NoteHead } from './notehead';
import { RenderContext } from './rendercontext';
import { Stave } from './stave';
import { StaveModifierPosition } from './stavemodifier';
import { Tables } from './tables';
import { TimeSignature } from './timesignature';
import { Category, isBarline } from './typeguard';
import { defined } from './util';
export interface MultimeasureRestRenderOptions {
/** Extracted by Factory.MultiMeasureRest() and passed to the MultiMeasureRest constructor. */
number_of_measures: number;
/** Use rest symbols. Defaults to `false`, which renders a thick horizontal line with serifs at both ends. */
use_symbols?: boolean;
/** Horizontal spacing between rest symbol glyphs (if `use_symbols` is `true`).*/
symbol_spacing?: number;
/** Show the number of measures at the top. Defaults to `true`. */
show_number?: boolean;
/** Vertical position of the "number of measures" text (measured in stave lines). Defaults to -0.5, which is above the stave. 6.5 is below the stave. */
number_line?: number;
/** Font size of the "number of measures" text. */
number_glyph_point?: number;
/** Left padding from `stave.getX()`. */
padding_left?: number;
/** Right padding from `stave.getX() + stave.getWidth()` */
padding_right?: number;
/** Vertical position of the rest line or symbols, expressed as stave lines. Default: 2. The top stave line is 1, and the bottom stave line is 5. */
line?: number;
/** Defaults to the number of vertical pixels between stave lines. Used for serif height or 2-bar / 4-bar symbol height. */
spacing_between_lines_px?: number;
/** Size of the semibreve (1-bar) rest symbol. Other symbols are scaled accordingly. */
semibreve_rest_glyph_scale?: number;
/** Thickness of the rest line. Used when `use_symbols` is false. Defaults to half the space between stave lines. */
line_thickness?: number;
/** Thickness of the rest line's serif. Used when `use_symbols` is false. */
serif_thickness?: number;
}
let semibreve_rest: { glyph_font_scale: number; glyph_code: string; width: number } | undefined;
function get_semibreve_rest() {
if (!semibreve_rest) {
const noteHead = new NoteHead({ duration: 'w', note_type: 'r' });
semibreve_rest = {
glyph_font_scale: noteHead.render_options.glyph_font_scale,
glyph_code: noteHead.glyph_code,
width: noteHead.getWidth(),
};
}
return semibreve_rest;
}
export class MultiMeasureRest extends Element {
static get CATEGORY(): string {
return Category.MultiMeasureRest;
}
public render_options: Required<MultimeasureRestRenderOptions>;
protected xs = { left: NaN, right: NaN };
protected number_of_measures: number;
protected stave?: Stave;
private hasPaddingLeft = false;
private hasPaddingRight = false;
private hasLineThickness = false;
private hasSymbolSpacing = false;
/**
*
* @param number_of_measures Number of measures.
* @param options The options object.
*/
constructor(number_of_measures: number, options: MultimeasureRestRenderOptions) {
super();
this.number_of_measures = number_of_measures;
// Keep track of whether these four options were provided.
this.hasPaddingLeft = typeof options.padding_left === 'number';
this.hasPaddingRight = typeof options.padding_right === 'number';
this.hasLineThickness = typeof options.line_thickness === 'number';
this.hasSymbolSpacing = typeof options.symbol_spacing === 'number';
const musicFont = Tables.currentMusicFont();
this.render_options = {
use_symbols: false,
show_number: true,
number_line: -0.5,
number_glyph_point: musicFont.lookupMetric('digits.point') ?? Tables.NOTATION_FONT_SCALE, // same as TimeSignature.
line: 2,
spacing_between_lines_px: Tables.STAVE_LINE_DISTANCE, // same as Stave.
serif_thickness: 2,
semibreve_rest_glyph_scale: Tables.NOTATION_FONT_SCALE, // same as NoteHead.
padding_left: 0,
padding_right: 0,
line_thickness: 5,
symbol_spacing: 0,
...options,
};
const fontLineShift = musicFont.lookupMetric('digits.shiftLine', 0);
this.render_options.number_line += fontLineShift;
}
getXs(): { left: number; right: number } {
return this.xs;
}
setStave(stave: Stave): this {
this.stave = stave;
return this;
}
getStave(): Stave | undefined {
return this.stave;
}
checkStave(): Stave {
return defined(this.stave, 'NoStave', 'No stave attached to instance.');
}
drawLine(stave: Stave, ctx: RenderContext, left: number, right: number, spacingBetweenLines: number): void {
const options = this.render_options;
const y = stave.getYForLine(options.line);
const padding = (right - left) * 0.1;
left += padding;
right -= padding;
let lineThicknessHalf;
if (this.hasLineThickness) {
lineThicknessHalf = options.line_thickness * 0.5;
} else {
lineThicknessHalf = spacingBetweenLines * 0.25;
}
const serifThickness = options.serif_thickness;
const top = y - spacingBetweenLines;
const bot = y + spacingBetweenLines;
const leftIndented = left + serifThickness;
const rightIndented = right - serifThickness;
const lineTop = y - lineThicknessHalf;
const lineBottom = y + lineThicknessHalf;
ctx.save();
ctx.beginPath();
ctx.moveTo(left, top);
ctx.lineTo(leftIndented, top);
ctx.lineTo(leftIndented, lineTop);
ctx.lineTo(rightIndented, lineTop);
ctx.lineTo(rightIndented, top);
ctx.lineTo(right, top);
ctx.lineTo(right, bot);
ctx.lineTo(rightIndented, bot);
ctx.lineTo(rightIndented, lineBottom);
ctx.lineTo(leftIndented, lineBottom);
ctx.lineTo(leftIndented, bot);
ctx.lineTo(left, bot);
ctx.closePath();
ctx.fill();
}
drawSymbols(stave: Stave, ctx: RenderContext, left: number, right: number, spacingBetweenLines: number): void {
const n4 = Math.floor(this.number_of_measures / 4);
const n = this.number_of_measures % 4;
const n2 = Math.floor(n / 2);
const n1 = n % 2;
const options = this.render_options;
// FIXME: TODO: invalidate semibreve_rest at the appropriate time
// (e.g., if the system font settings are changed).
semibreve_rest = undefined;
const rest = get_semibreve_rest();
const rest_scale = options.semibreve_rest_glyph_scale;
const rest_width = rest.width * (rest_scale / rest.glyph_font_scale);
const glyphs = {
2: {
width: rest_width * 0.5,
height: spacingBetweenLines,
},
1: {
width: rest_width,
},
};
/* 10: normal spacingBetweenLines */
const spacing = this.hasSymbolSpacing ? options.symbol_spacing : 10;
const width = n4 * glyphs[2].width + n2 * glyphs[2].width + n1 * glyphs[1].width + (n4 + n2 + n1 - 1) * spacing;
let x = left + (right - left) * 0.5 - width * 0.5;
const line = options.line;
const yTop = stave.getYForLine(line - 1);
const yMiddle = stave.getYForLine(line);
const yBottom = stave.getYForLine(line + 1);
ctx.save();
ctx.setStrokeStyle('none');
ctx.setLineWidth(0);
for (let i = 0; i < n4; ++i) {
ctx.fillRect(x, yMiddle - glyphs[2].height, glyphs[2].width, glyphs[2].height);
ctx.fillRect(x, yBottom - glyphs[2].height, glyphs[2].width, glyphs[2].height);
x += glyphs[2].width + spacing;
}
for (let i = 0; i < n2; ++i) {
ctx.fillRect(x, yMiddle - glyphs[2].height, glyphs[2].width, glyphs[2].height);
x += glyphs[2].width + spacing;
}
for (let i = 0; i < n1; ++i) {
Glyph.renderGlyph(ctx, x, yTop, rest_scale, rest.glyph_code);
x += glyphs[1].width + spacing;
}
ctx.restore();
}
draw(): void {
const ctx = this.checkContext();
this.setRendered();
const stave = this.checkStave();
let left = stave.getNoteStartX();
let right = stave.getNoteEndX();
// FIXME: getNoteStartX() returns x + 5(barline width)
// getNoteEndX() returns x + width(no barline width)
// See Stave constructor. How do we fix this?
// Here, we subtract the barline width.
const begModifiers = stave.getModifiers(StaveModifierPosition.BEGIN);
if (begModifiers.length === 1 && isBarline(begModifiers[0])) {
left -= begModifiers[0].getWidth();
}
const options = this.render_options;
if (this.hasPaddingLeft) {
left = stave.getX() + options.padding_left;
}
if (this.hasPaddingRight) {
right = stave.getX() + stave.getWidth() - options.padding_right;
}
this.xs.left = left;
this.xs.right = right;
const spacingBetweenLines = options.spacing_between_lines_px;
if (options.use_symbols) {
this.drawSymbols(stave, ctx, left, right, spacingBetweenLines);
} else {
this.drawLine(stave, ctx, left, right, spacingBetweenLines);
}
if (options.show_number) {
const timeSpec = '/' + this.number_of_measures;
const timeSig = new TimeSignature(timeSpec, 0, false);
timeSig.point = options.number_glyph_point;
timeSig.setTimeSig(timeSpec);
timeSig.setStave(stave);
timeSig.setX(left + (right - left) * 0.5 - timeSig.getInfo().glyph.getMetrics().width * 0.5);
timeSig.bottomLine = options.number_line;
timeSig.setContext(ctx).draw();
}
}
}