UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature

682 lines (564 loc) 19.3 kB
// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010. import { Vex } from './vex'; import { Element } from './element'; import { Flow } from './tables'; import { Barline } from './stavebarline'; import { StaveModifier } from './stavemodifier'; import { Repetition } from './staverepetition'; import { StaveSection } from './stavesection'; import { StaveTempo } from './stavetempo'; import { StaveText } from './stavetext'; import { BoundingBox } from './boundingbox'; import { Clef } from './clef'; import { KeySignature } from './keysignature'; import { TimeSignature } from './timesignature'; import { Volta } from './stavevolta'; export class Stave extends Element { constructor(x, y, width, options) { super(); this.setAttribute('type', 'Stave'); this.x = x; this.y = y; this.width = width; this.formatted = false; this.start_x = x + 5; this.end_x = x + width; this.modifiers = []; // stave modifiers (clef, key, time, barlines, coda, segno, etc.) this.measure = 0; this.clef = 'treble'; this.font = { family: 'sans-serif', size: 8, weight: '', }; this.options = { vertical_bar_width: 10, // Width around vertical bar end-marker glyph_spacing_px: 10, num_lines: 5, fill_style: '#999999', left_bar: true, // draw vertical bar on left right_bar: true, // draw vertical bar on right spacing_between_lines_px: 10, // in pixels space_above_staff_ln: 4, // in staff lines space_below_staff_ln: 4, // in staff lines top_text_position: 1, // in staff lines }; this.bounds = { x: this.x, y: this.y, w: this.width, h: 0 }; Vex.Merge(this.options, options); this.resetLines(); const BARTYPE = Barline.type; // beg bar this.addModifier(new Barline(this.options.left_bar ? BARTYPE.SINGLE : BARTYPE.NONE)); // end bar this.addEndModifier(new Barline(this.options.right_bar ? BARTYPE.SINGLE : BARTYPE.NONE)); } space(spacing) { return this.options.spacing_between_lines_px * spacing; } resetLines() { this.options.line_config = []; for (let i = 0; i < this.options.num_lines; i++) { this.options.line_config.push({ visible: true }); } this.height = (this.options.num_lines + this.options.space_above_staff_ln) * this.options.spacing_between_lines_px; this.options.bottom_text_position = this.options.num_lines; } getOptions() { return this.options; } setNoteStartX(x) { if (!this.formatted) this.format(); this.start_x = x; return this; } getNoteStartX() { if (!this.formatted) this.format(); return this.start_x; } getNoteEndX() { if (!this.formatted) this.format(); return this.end_x; } getTieStartX() { return this.start_x; } getTieEndX() { return this.x + this.width; } getX() { return this.x; } getNumLines() { return this.options.num_lines; } setNumLines(lines) { this.options.num_lines = parseInt(lines, 10); this.resetLines(); return this; } setY(y) { this.y = y; return this; } getTopLineTopY() { return this.getYForLine(0) - (Flow.STAVE_LINE_THICKNESS / 2); } getBottomLineBottomY() { return this.getYForLine(this.getNumLines() - 1) + (Flow.STAVE_LINE_THICKNESS / 2); } setX(x) { const shift = x - this.x; this.formatted = false; this.x = x; this.start_x += shift; this.end_x += shift; for (let i = 0; i < this.modifiers.length; i++) { const mod = this.modifiers[i]; if (mod.x !== undefined) { mod.x += shift; } } return this; } setWidth(width) { this.formatted = false; this.width = width; this.end_x = this.x + width; // reset the x position of the end barline (TODO(0xfe): This makes no sense) // this.modifiers[1].setX(this.end_x); return this; } getWidth() { return this.width; } getStyle() { return Object.assign({ fillStyle: this.options.fill_style, strokeStyle: this.options.fill_style, // yes, this is correct for legacy compatibility lineWidth: Flow.STAVE_LINE_THICKNESS, }, this.style || {}); } setMeasure(measure) { this.measure = measure; return this; } /** * Gets the pixels to shift from the beginning of the stave * following the modifier at the provided index * @param {Number} index The index from which to determine the shift * @return {Number} The amount of pixels shifted */ getModifierXShift(index = 0) { if (typeof index !== 'number') { throw new Vex.RERR('InvalidIndex', 'Must be of number type'); } if (!this.formatted) this.format(); if (this.getModifiers(StaveModifier.Position.BEGIN).length === 1) { return 0; } let start_x = this.start_x - this.x; const begBarline = this.modifiers[0]; if (begBarline.getType() === Barline.type.REPEAT_BEGIN && start_x > begBarline.getWidth()) { start_x -= begBarline.getWidth(); } return start_x; } // Coda & Segno Symbol functions setRepetitionTypeLeft(type, y) { this.modifiers.push(new Repetition(type, this.x, y)); return this; } setRepetitionTypeRight(type, y) { this.modifiers.push(new Repetition(type, this.x, y)); return this; } // Volta functions setVoltaType(type, number_t, y) { this.modifiers.push(new Volta(type, number_t, this.x, y)); return this; } // Section functions setSection(section, y) { this.modifiers.push(new StaveSection(section, this.x, y)); return this; } // Tempo functions setTempo(tempo, y) { this.modifiers.push(new StaveTempo(tempo, this.x, y)); return this; } // Text functions setText(text, position, options) { this.modifiers.push(new StaveText(text, position, options)); return this; } getHeight() { return this.height; } getSpacingBetweenLines() { return this.options.spacing_between_lines_px; } getBoundingBox() { return new BoundingBox(this.x, this.y, this.width, this.getBottomY() - this.y); } getBottomY() { const options = this.options; const spacing = options.spacing_between_lines_px; const score_bottom = this.getYForLine(options.num_lines) + (options.space_below_staff_ln * spacing); return score_bottom; } getBottomLineY() { return this.getYForLine(this.options.num_lines); } // This returns the y for the *center* of a staff line getYForLine(line) { const options = this.options; const spacing = options.spacing_between_lines_px; const headroom = options.space_above_staff_ln; const y = this.y + (line * spacing) + (headroom * spacing); return y; } getLineForY(y) { // Does the reverse of getYForLine - somewhat dumb and just calls // getYForLine until the right value is reaches const options = this.options; const spacing = options.spacing_between_lines_px; const headroom = options.space_above_staff_ln; return ((y - this.y) / spacing) - headroom; } getYForTopText(line) { const l = line || 0; return this.getYForLine(-l - this.options.top_text_position); } getYForBottomText(line) { const l = line || 0; return this.getYForLine(this.options.bottom_text_position + l); } getYForNote(line) { const options = this.options; const spacing = options.spacing_between_lines_px; const headroom = options.space_above_staff_ln; const y = this.y + (headroom * spacing) + (5 * spacing) - (line * spacing); return y; } getYForGlyphs() { return this.getYForLine(3); } addModifier(modifier, position) { if (position !== undefined) { modifier.setPosition(position); } modifier.setStave(this); this.formatted = false; this.modifiers.push(modifier); return this; } addEndModifier(modifier) { this.addModifier(modifier, StaveModifier.Position.END); return this; } // Bar Line functions setBegBarType(type) { // Only valid bar types at beginning of stave is none, single or begin repeat const { SINGLE, REPEAT_BEGIN, NONE } = Barline.type; if (type === SINGLE || type === REPEAT_BEGIN || type === NONE) { this.modifiers[0].setType(type); this.formatted = false; } return this; } setEndBarType(type) { // Repeat end not valid at end of stave if (type !== Barline.type.REPEAT_BEGIN) { this.modifiers[1].setType(type); this.formatted = false; } return this; } setClef(clefSpec, size, annotation, position) { if (position === undefined) { position = StaveModifier.Position.BEGIN; } this.clef = clefSpec; const clefs = this.getModifiers(position, Clef.CATEGORY); if (clefs.length === 0) { this.addClef(clefSpec, size, annotation, position); } else { clefs[0].setType(clefSpec, size, annotation); } return this; } setEndClef(clefSpec, size, annotation) { this.setClef(clefSpec, size, annotation, StaveModifier.Position.END); return this; } setKeySignature(keySpec, cancelKeySpec, position) { if (position === undefined) { position = StaveModifier.Position.BEGIN; } const keySignatures = this.getModifiers(position, KeySignature.CATEGORY); if (keySignatures.length === 0) { this.addKeySignature(keySpec, cancelKeySpec, position); } else { keySignatures[0].setKeySig(keySpec, cancelKeySpec); } return this; } setEndKeySignature(keySpec, cancelKeySpec) { this.setKeySignature(keySpec, cancelKeySpec, StaveModifier.Position.END); return this; } setTimeSignature(timeSpec, customPadding, position) { if (position === undefined) { position = StaveModifier.Position.BEGIN; } const timeSignatures = this.getModifiers(position, TimeSignature.CATEGORY); if (timeSignatures.length === 0) { this.addTimeSignature(timeSpec, customPadding, position); } else { timeSignatures[0].setTimeSig(timeSpec); } return this; } setEndTimeSignature(timeSpec, customPadding) { this.setTimeSignature(timeSpec, customPadding, StaveModifier.Position.END); return this; } addKeySignature(keySpec, cancelKeySpec, position) { this.addModifier(new KeySignature(keySpec, cancelKeySpec), position); return this; } addClef(clef, size, annotation, position) { if (position === undefined || position === StaveModifier.Position.BEGIN) { this.clef = clef; } this.addModifier(new Clef(clef, size, annotation), position); return this; } addEndClef(clef, size, annotation) { this.addClef(clef, size, annotation, StaveModifier.Position.END); return this; } addTimeSignature(timeSpec, customPadding, position) { this.addModifier(new TimeSignature(timeSpec, customPadding), position); return this; } addEndTimeSignature(timeSpec, customPadding) { this.addTimeSignature(timeSpec, customPadding, StaveModifier.Position.END); return this; } // Deprecated addTrebleGlyph() { this.addClef('treble'); return this; } getModifiers(position, category) { if (position === undefined) return this.modifiers; return this.modifiers.filter(modifier => position === modifier.getPosition() && (category === undefined || category === modifier.getCategory()) ); } sortByCategory(items, order) { for (let i = items.length - 1; i >= 0; i--) { for (let j = 0; j < i; j++) { if (order[items[j].getCategory()] > order[items[j + 1].getCategory()]) { const temp = items[j]; items[j] = items[j + 1]; items[j + 1] = temp; } } } } format() { const begBarline = this.modifiers[0]; const endBarline = this.modifiers[1]; const begModifiers = this.getModifiers(StaveModifier.Position.BEGIN); const endModifiers = this.getModifiers(StaveModifier.Position.END); this.sortByCategory(begModifiers, { barlines: 0, clefs: 1, keysignatures: 2, timesignatures: 3, }); this.sortByCategory(endModifiers, { timesignatures: 0, keysignatures: 1, barlines: 2, clefs: 3, }); if (begModifiers.length > 1 && begBarline.getType() === Barline.type.REPEAT_BEGIN) { begModifiers.push(begModifiers.splice(0, 1)[0]); begModifiers.splice(0, 0, new Barline(Barline.type.SINGLE)); } if (endModifiers.indexOf(endBarline) > 0) { endModifiers.splice(0, 0, new Barline(Barline.type.NONE)); } let width; let padding; let modifier; let offset = 0; let x = this.x; for (let i = 0; i < begModifiers.length; i++) { modifier = begModifiers[i]; padding = modifier.getPadding(i + offset); width = modifier.getWidth(); x += padding; modifier.setX(x); x += width; if (padding + width === 0) offset--; } this.start_x = x; x = this.x + this.width; const widths = { left: 0, right: 0, paddingRight: 0, paddingLeft: 0, }; let lastBarlineIdx = 0; for (let i = 0; i < endModifiers.length; i++) { modifier = endModifiers[i]; lastBarlineIdx = (modifier.getCategory() === 'barlines') ? i : lastBarlineIdx; widths.right = 0; widths.left = 0; widths.paddingRight = 0; widths.paddingLeft = 0; const layoutMetrics = modifier.getLayoutMetrics(); if (layoutMetrics) { if (i !== 0) { widths.right = layoutMetrics.xMax || 0; widths.paddingRight = layoutMetrics.paddingRight || 0; } widths.left = (-layoutMetrics.xMin) || 0; widths.paddingLeft = layoutMetrics.paddingLeft || 0; if (i === endModifiers.length - 1) { widths.paddingLeft = 0; } } else { widths.paddingRight = modifier.getPadding(i - lastBarlineIdx); if (i !== 0) { widths.right = modifier.getWidth(); } if (i === 0) { widths.left = modifier.getWidth(); } } x -= widths.paddingRight; x -= widths.right; modifier.setX(x); x -= widths.left; x -= widths.paddingLeft; } this.end_x = endModifiers.length === 1 ? this.x + this.width : x; this.formatted = true; } /** * All drawing functions below need the context to be set. */ draw() { this.checkContext(); this.setRendered(); if (!this.formatted) this.format(); const num_lines = this.options.num_lines; const width = this.width; const x = this.x; let y; // Render lines for (let line = 0; line < num_lines; line++) { y = this.getYForLine(line); this.applyStyle(); if (this.options.line_config[line].visible) { this.context.beginPath(); this.context.moveTo(x, y); this.context.lineTo(x + width, y); this.context.stroke(); } this.restoreStyle(); } // Draw the modifiers (bar lines, coda, segno, repeat brackets, etc.) for (let i = 0; i < this.modifiers.length; i++) { // Only draw modifier if it has a draw function if (typeof this.modifiers[i].draw === 'function') { this.modifiers[i].draw(this, this.getModifierXShift(i)); } } // Render measure numbers if (this.measure > 0) { this.context.save(); this.context.setFont(this.font.family, this.font.size, this.font.weight); const text_width = this.context.measureText('' + this.measure).width; y = this.getYForTopText(0) + 3; this.context.fillText('' + this.measure, this.x - text_width / 2, y); this.context.restore(); } return this; } // Draw Simple barlines for backward compatability // Do not delete - draws the beginning bar of the stave drawVertical(x, isDouble) { this.drawVerticalFixed(this.x + x, isDouble); } drawVerticalFixed(x, isDouble) { this.checkContext(); const top_line = this.getYForLine(0); const bottom_line = this.getYForLine(this.options.num_lines - 1); if (isDouble) { this.context.fillRect(x - 3, top_line, 1, bottom_line - top_line + 1); } this.context.fillRect(x, top_line, 1, bottom_line - top_line + 1); } drawVerticalBar(x) { this.drawVerticalBarFixed(this.x + x, false); } drawVerticalBarFixed(x) { this.checkContext(); const top_line = this.getYForLine(0); const bottom_line = this.getYForLine(this.options.num_lines - 1); this.context.fillRect(x, top_line, 1, bottom_line - top_line + 1); } /** * Get the current configuration for the Stave. * @return {Array} An array of configuration objects. */ getConfigForLines() { return this.options.line_config; } /** * Configure properties of the lines in the Stave * @param line_number The index of the line to configure. * @param line_config An configuration object for the specified line. * @throws Vex.RERR "StaveConfigError" When the specified line number is out of * range of the number of lines specified in the constructor. */ setConfigForLine(line_number, line_config) { if (line_number >= this.options.num_lines || line_number < 0) { throw new Vex.RERR( 'StaveConfigError', 'The line number must be within the range of the number of lines in the Stave.' ); } if (line_config.visible === undefined) { throw new Vex.RERR( 'StaveConfigError', "The line configuration object is missing the 'visible' property." ); } if (typeof (line_config.visible) !== 'boolean') { throw new Vex.RERR( 'StaveConfigError', "The line configuration objects 'visible' property must be true or false." ); } this.options.line_config[line_number] = line_config; return this; } /** * Set the staff line configuration array for all of the lines at once. * @param lines_configuration An array of line configuration objects. These objects * are of the same format as the single one passed in to setLineConfiguration(). * The caller can set null for any line config entry if it is desired that the default be used * @throws Vex.RERR "StaveConfigError" When the lines_configuration array does not have * exactly the same number of elements as the num_lines configuration object set in * the constructor. */ setConfigForLines(lines_configuration) { if (lines_configuration.length !== this.options.num_lines) { throw new Vex.RERR( 'StaveConfigError', 'The length of the lines configuration array must match the number of lines in the Stave' ); } // Make sure the defaults are present in case an incomplete set of // configuration options were supplied. // eslint-disable-next-line for (const line_config in lines_configuration) { // Allow 'null' to be used if the caller just wants the default for a particular node. if (!lines_configuration[line_config]) { lines_configuration[line_config] = this.options.line_config[line_config]; } Vex.Merge(this.options.line_config[line_config], lines_configuration[line_config]); } this.options.line_config = lines_configuration; return this; } }