/src/note.js
JavaScript | 429 lines | 270 code | 72 blank | 87 comment | 39 complexity | 8b811f0a2ab91e099c050267ee2cc49a MD5 | raw file
- // [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
- //
- // ## Description
- //
- // This file implements an abstract interface for notes and chords that
- // are rendered on a stave. Notes have some common properties: All of them
- // have a value (e.g., pitch, fret, etc.) and a duration (quarter, half, etc.)
- //
- // Some notes have stems, heads, dots, etc. Most notational elements that
- // surround a note are called *modifiers*, and every note has an associated
- // array of them. All notes also have a rendering context and belong to a stave.
- import { Vex } from './vex';
- import { Flow } from './tables';
- import { Tickable } from './tickable';
- export class Note extends Tickable {
- static get CATEGORY() { return 'note'; }
- // Debug helper. Displays various note metrics for the given
- // note.
- static plotMetrics(ctx, note, yPos) {
- const metrics = note.getMetrics();
- const xStart = note.getAbsoluteX() - metrics.modLeftPx - metrics.leftDisplacedHeadPx;
- const xPre1 = note.getAbsoluteX() - metrics.leftDisplacedHeadPx;
- const xAbs = note.getAbsoluteX();
- const xPost1 = note.getAbsoluteX() + metrics.notePx;
- const xPost2 = note.getAbsoluteX() + metrics.notePx + metrics.rightDisplacedHeadPx;
- const xEnd = note.getAbsoluteX()
- + metrics.notePx
- + metrics.rightDisplacedHeadPx
- + metrics.modRightPx;
- const xFreedomRight = xEnd + (note.getFormatterMetrics().freedom.right || 0);
- const xWidth = xEnd - xStart;
- ctx.save();
- ctx.setFont('Arial', 8, '');
- ctx.fillText(Math.round(xWidth) + 'px', xStart + note.getXShift(), yPos);
- const y = (yPos + 7);
- function stroke(x1, x2, color, yy = y) {
- ctx.beginPath();
- ctx.setStrokeStyle(color);
- ctx.setFillStyle(color);
- ctx.setLineWidth(3);
- ctx.moveTo(x1 + note.getXShift(), yy);
- ctx.lineTo(x2 + note.getXShift(), yy);
- ctx.stroke();
- }
- stroke(xStart, xPre1, 'red');
- stroke(xPre1, xAbs, '#999');
- stroke(xAbs, xPost1, 'green');
- stroke(xPost1, xPost2, '#999');
- stroke(xPost2, xEnd, 'red');
- stroke(xEnd, xFreedomRight, '#DD0');
- stroke(xStart - note.getXShift(), xStart, '#BBB'); // Shift
- Vex.drawDot(ctx, xAbs + note.getXShift(), y, 'blue');
- const formatterMetrics = note.getFormatterMetrics();
- if (formatterMetrics.iterations > 0) {
- const spaceDeviation = formatterMetrics.space.deviation;
- const prefix = spaceDeviation >= 0 ? '+' : '';
- ctx.setFillStyle('red');
- ctx.fillText(prefix + Math.round(spaceDeviation),
- xAbs + note.getXShift(), yPos - 10);
- }
- ctx.restore();
- }
- static parseDuration(durationString) {
- if (typeof (durationString) !== 'string') { return null; }
- const regexp = /(\d*\/?\d+|[a-z])(d*)([nrhms]|$)/;
- const result = regexp.exec(durationString);
- if (!result) { return null; }
- const duration = result[1];
- const dots = result[2].length;
- const type = result[3] || 'n';
- return { duration, dots, type };
- }
- static parseNoteStruct(noteStruct) {
- const durationString = noteStruct.duration;
- const customTypes = [];
- // Preserve backwards-compatibility
- const durationProps = Note.parseDuration(durationString);
- if (!durationProps) { return null; }
- // If specified type is invalid, return null
- let type = noteStruct.type;
- if (type && !Flow.getGlyphProps.validTypes[type]) { return null; }
- // If no type specified, check duration or custom types
- if (!type) {
- type = durationProps.type || 'n';
- // If we have keys, try and check if we've got a custom glyph
- if (noteStruct.keys !== undefined) {
- noteStruct.keys.forEach((k, i) => {
- const result = k.split('/');
- // We have a custom glyph specified after the note eg. /X2
- customTypes[i] = (result && result.length === 3) ? result[2] : type;
- });
- }
- }
- // Calculate the tick duration of the note
- let ticks = Flow.durationToTicks(durationProps.duration);
- if (ticks == null) { return null; }
- // Are there any dots?
- const dots = noteStruct.dots ? noteStruct.dots : durationProps.dots;
- if (typeof (dots) !== 'number') { return null; }
- // Add ticks as necessary depending on the numbr of dots
- let currentTicks = ticks;
- for (let i = 0; i < dots; i++) {
- if (currentTicks <= 1) return null;
- currentTicks = currentTicks / 2;
- ticks += currentTicks;
- }
- return {
- duration: durationProps.duration,
- type,
- customTypes,
- dots,
- ticks,
- };
- }
- // Every note is a tickable, i.e., it can be mutated by the `Formatter` class for
- // positioning and layout.
- // To create a new note you need to provide a `noteStruct`, which consists
- // of the following fields:
- //
- // `type`: The note type (e.g., `r` for rest, `s` for slash notes, etc.)
- // `dots`: The number of dots, which affects the duration.
- // `duration`: The time length (e.g., `q` for quarter, `h` for half, `8` for eighth etc.)
- //
- // The range of values for these parameters are available in `src/tables.js`.
- constructor(noteStruct) {
- super();
- this.setAttribute('type', 'Note');
- if (!noteStruct) {
- throw new Vex.RuntimeError(
- 'BadArguments', 'Note must have valid initialization data to identify duration and type.'
- );
- }
- // Parse `noteStruct` and get note properties.
- const initStruct = Note.parseNoteStruct(noteStruct);
- if (!initStruct) {
- throw new Vex.RuntimeError(
- 'BadArguments', `Invalid note initialization object: ${JSON.stringify(noteStruct)}`
- );
- }
- // Set note properties from parameters.
- this.duration = initStruct.duration;
- this.dots = initStruct.dots;
- this.noteType = initStruct.type;
- this.customTypes = initStruct.customTypes;
- if (noteStruct.duration_override) {
- // Custom duration
- this.setDuration(noteStruct.duration_override);
- } else {
- // Default duration
- this.setIntrinsicTicks(initStruct.ticks);
- }
- this.modifiers = [];
- // Get the glyph code for this note from the font.
- this.glyph = Flow.getGlyphProps(this.duration, this.noteType);
- this.customGlyphs = this.customTypes.map(t => Flow.getGlyphProps(this.duration, t));
- if (this.positions && (typeof (this.positions) !== 'object' || !this.positions.length)) {
- throw new Vex.RuntimeError('BadArguments', 'Note keys must be array type.');
- }
- // Note to play for audio players.
- this.playNote = null;
- // Positioning contexts used by the Formatter.
- this.tickContext = null; // The current tick context.
- this.modifierContext = null;
- this.ignore_ticks = false;
- // Positioning variables
- this.width = 0; // Width in pixels calculated after preFormat
- this.leftDisplacedHeadPx = 0; // Extra room on left for displaced note head
- this.rightDisplacedHeadPx = 0; // Extra room on right for displaced note head
- this.x_shift = 0; // X shift from tick context X
- this.voice = null; // The voice that this note is in
- this.preFormatted = false; // Is this note preFormatted?
- this.ys = []; // list of y coordinates for each note
- // we need to hold on to these for ties and beams.
- if (noteStruct.align_center) {
- this.setCenterAlignment(noteStruct.align_center);
- }
- // The render surface.
- this.stave = null;
- this.render_options = {
- annotation_spacing: 5,
- };
- }
- // Get and set the play note, which is arbitrary data that can be used by an
- // audio player.
- getPlayNote() { return this.playNote; }
- setPlayNote(note) { this.playNote = note; return this; }
- // Don't play notes by default, call them rests. This is also used by things like
- // beams and dots for positioning.
- isRest() { return false; }
- // TODO(0xfe): Why is this method here?
- addStroke(index, stroke) {
- stroke.setNote(this);
- stroke.setIndex(index);
- this.modifiers.push(stroke);
- this.setPreFormatted(false);
- return this;
- }
- // Get and set the target stave.
- getStave() { return this.stave; }
- setStave(stave) {
- this.stave = stave;
- this.setYs([stave.getYForLine(0)]); // Update Y values if the stave is changed.
- this.context = this.stave.context;
- return this;
- }
- // `Note` is not really a modifier, but is used in
- // a `ModifierContext`.
- getCategory() { return Note.CATEGORY; }
- // Set the rendering context for the note.
- setContext(context) { this.context = context; return this; }
- // Get and set spacing to the left and right of the notes.
- getLeftDisplacedHeadPx() { return this.leftDisplacedHeadPx; }
- getRightDisplacedHeadPx() { return this.rightDisplacedHeadPx; }
- setLeftDisplacedHeadPx(x) { this.leftDisplacedHeadPx = x; return this; }
- setRightDisplacedHeadPx(x) { this.rightDisplacedHeadPx = x; return this; }
- // Returns true if this note has no duration (e.g., bar notes, spacers, etc.)
- shouldIgnoreTicks() { return this.ignore_ticks; }
- // Get the stave line number for the note.
- getLineNumber() { return 0; }
- // Get the stave line number for rest.
- getLineForRest() { return 0; }
- // Get the glyph associated with this note.
- getGlyph() { return this.glyph; }
- getGlyphWidth() {
- // TODO: FIXME (multiple potential values for this.glyph)
- if (this.glyph) {
- if (this.glyph.getMetrics) {
- return this.glyph.getMetrics().width;
- } else if (this.glyph.getWidth) {
- return this.glyph.getWidth(this.render_options.glyph_font_scale);
- }
- }
- return 0;
- }
- // Set and get Y positions for this note. Each Y value is associated with
- // an individual pitch/key within the note/chord.
- setYs(ys) { this.ys = ys; return this; }
- getYs() {
- if (this.ys.length === 0) {
- throw new Vex.RERR('NoYValues', 'No Y-values calculated for this note.');
- }
- return this.ys;
- }
- // Get the Y position of the space above the stave onto which text can
- // be rendered.
- getYForTopText(text_line) {
- if (!this.stave) {
- throw new Vex.RERR('NoStave', 'No stave attached to this note.');
- }
- return this.stave.getYForTopText(text_line);
- }
- // Get a `BoundingBox` for this note.
- getBoundingBox() { return null; }
- // Returns the voice that this note belongs in.
- getVoice() {
- if (!this.voice) throw new Vex.RERR('NoVoice', 'Note has no voice.');
- return this.voice;
- }
- // Attach this note to `voice`.
- setVoice(voice) {
- this.voice = voice;
- this.preFormatted = false;
- return this;
- }
- // Get and set the `TickContext` for this note.
- getTickContext() { return this.tickContext; }
- setTickContext(tc) {
- this.tickContext = tc;
- this.preFormatted = false;
- return this;
- }
- // Accessors for the note type.
- getDuration() { return this.duration; }
- isDotted() { return (this.dots > 0); }
- hasStem() { return false; }
- getDots() { return this.dots; }
- getNoteType() { return this.noteType; }
- setBeam() { return this; } // ignore parameters
- // Attach this note to a modifier context.
- setModifierContext(mc) { this.modifierContext = mc; return this; }
- // Attach a modifier to this note.
- addModifier(modifier, index = 0) {
- modifier.setNote(this);
- modifier.setIndex(index);
- this.modifiers.push(modifier);
- this.setPreFormatted(false);
- return this;
- }
- // Get the coordinates for where modifiers begin.
- getModifierStartXY() {
- if (!this.preFormatted) {
- throw new Vex.RERR('UnformattedNote', "Can't call GetModifierStartXY on an unformatted note");
- }
- return {
- x: this.getAbsoluteX(),
- y: this.ys[0],
- };
- }
- // Get bounds and metrics for this note.
- //
- // Returns a struct with fields:
- // `width`: The total width of the note (including modifiers.)
- // `notePx`: The width of the note head only.
- // `left_shift`: The horizontal displacement of the note.
- // `modLeftPx`: Start `X` for left modifiers.
- // `modRightPx`: Start `X` for right modifiers.
- // `leftDisplacedHeadPx`: Extra space on left of note.
- // `rightDisplacedHeadPx`: Extra space on right of note.
- getMetrics() {
- if (!this.preFormatted) {
- throw new Vex.RERR('UnformattedNote', "Can't call getMetrics on an unformatted note.");
- }
- const modLeftPx = this.modifierContext ? this.modifierContext.state.left_shift : 0;
- const modRightPx = this.modifierContext ? this.modifierContext.state.right_shift : 0;
- const width = this.getWidth();
- const glyphWidth = this.getGlyphWidth();
- const notePx = width
- - modLeftPx // subtract left modifiers
- - modRightPx // subtract right modifiers
- - this.leftDisplacedHeadPx // subtract left displaced head
- - this.rightDisplacedHeadPx; // subtract right displaced head
- return {
- // ----------
- // NOTE: If you change this, remember to update MockTickable in the tests/ directory.
- // --------------
- width,
- glyphWidth,
- notePx,
- // Modifier spacing.
- modLeftPx,
- modRightPx,
- // Displaced note head on left or right.
- leftDisplacedHeadPx: this.leftDisplacedHeadPx,
- rightDisplacedHeadPx: this.rightDisplacedHeadPx,
- };
- }
- // Get the absolute `X` position of this note's tick context. This
- // excludes x_shift, so you'll need to factor it in if you're
- // looking for the post-formatted x-position.
- getAbsoluteX() {
- if (!this.tickContext) {
- throw new Vex.RERR('NoTickContext', 'Note needs a TickContext assigned for an X-Value');
- }
- // Position note to left edge of tick context.
- let x = this.tickContext.getX();
- if (this.stave) {
- x += this.stave.getNoteStartX() + this.musicFont.lookupMetric('stave.padding');
- }
- if (this.isCenterAligned()) {
- x += this.getCenterXShift();
- }
- return x;
- }
- setPreFormatted(value) {
- this.preFormatted = value;
- }
- }