vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
299 lines (253 loc) • 8.64 kB
text/typescript
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
// MIT License
import { BoundingBox } from './boundingbox';
import { ElementStyle } from './element';
import { Glyph } from './glyph';
import { Note, NoteStruct } from './note';
import { RenderContext } from './rendercontext';
import { Stave } from './stave';
import { Stem } from './stem';
import { Tables } from './tables';
import { Category } from './typeguard';
import { defined, log } from './util';
// eslint-disable-next-line
function L(...args: any[]) {
if (NoteHead.DEBUG) log('Vex.Flow.NoteHead', args);
}
export interface NoteHeadMetrics {
minPadding?: number;
displacedShiftX?: number;
}
export interface NoteHeadStruct extends NoteStruct {
line?: number;
glyph_font_scale?: number;
slashed?: boolean;
style?: ElementStyle;
stem_down_x_offset?: number;
stem_up_x_offset?: number;
custom_glyph_code?: string;
x_shift?: number;
stem_direction?: number;
displaced?: boolean;
note_type?: string;
x?: number;
y?: number;
index?: number;
}
/**
* Draw slashnote head manually. No glyph exists for this.
* @param ctx the Canvas context
* @param duration the duration of the note. ex: "4"
* @param x the x coordinate to draw at
* @param y the y coordinate to draw at
* @param stem_direction the direction of the stem
*/
function drawSlashNoteHead(
ctx: RenderContext,
duration: string,
x: number,
y: number,
stem_direction: number,
staveSpace: number
) {
const width = Tables.SLASH_NOTEHEAD_WIDTH;
ctx.save();
ctx.setLineWidth(Tables.STEM_WIDTH);
let fill = false;
if (Tables.durationToNumber(duration) > 2) {
fill = true;
}
if (!fill) x -= (Tables.STEM_WIDTH / 2) * stem_direction;
ctx.beginPath();
ctx.moveTo(x, y + staveSpace);
ctx.lineTo(x, y + 1);
ctx.lineTo(x + width, y - staveSpace);
ctx.lineTo(x + width, y);
ctx.lineTo(x, y + staveSpace);
ctx.closePath();
if (fill) {
ctx.fill();
} else {
ctx.stroke();
}
if (Tables.durationToFraction(duration).equals(0.5)) {
const breve_lines = [-3, -1, width + 1, width + 3];
for (let i = 0; i < breve_lines.length; i++) {
ctx.beginPath();
ctx.moveTo(x + breve_lines[i], y - 10);
ctx.lineTo(x + breve_lines[i], y + 11);
ctx.stroke();
}
}
ctx.restore();
}
/**
* `NoteHeads` are typically not manipulated
* directly, but used internally in `StaveNote`.
*
* See `tests/notehead_tests.ts` for usage examples.
*/
export class NoteHead extends Note {
/** To enable logging for this class. Set `Vex.Flow.NoteHead.DEBUG` to `true`. */
static DEBUG: boolean = false;
static get CATEGORY(): string {
return Category.NoteHead;
}
glyph_code: string;
protected custom_glyph: boolean = false;
protected stem_up_x_offset: number = 0;
protected stem_down_x_offset: number = 0;
protected displaced: boolean;
protected stem_direction: number;
protected x: number;
protected y: number;
protected line: number;
protected index?: number;
protected slashed: boolean;
constructor(noteStruct: NoteHeadStruct) {
super(noteStruct);
this.index = noteStruct.index;
this.x = noteStruct.x || 0;
this.y = noteStruct.y || 0;
if (noteStruct.note_type) this.noteType = noteStruct.note_type;
this.displaced = noteStruct.displaced || false;
this.stem_direction = noteStruct.stem_direction || Stem.UP;
this.line = noteStruct.line || 0;
// Get glyph code based on duration and note type. This could be
// regular notes, rests, or other custom codes.
this.glyphProps = Tables.getGlyphProps(this.duration, this.noteType);
defined(
this.glyphProps,
'BadArguments',
`No glyph found for duration '${this.duration}' and type '${this.noteType}'`
);
// Swap out the glyph with ledger lines
if ((this.line > 5 || this.line < 0) && this.glyphProps.ledger_code_head) {
this.glyphProps.code_head = this.glyphProps.ledger_code_head;
}
this.glyph_code = this.glyphProps.code_head;
this.x_shift = noteStruct.x_shift || 0;
if (noteStruct.custom_glyph_code) {
this.custom_glyph = true;
this.glyph_code = noteStruct.custom_glyph_code;
this.stem_up_x_offset = noteStruct.stem_up_x_offset || 0;
this.stem_down_x_offset = noteStruct.stem_down_x_offset || 0;
}
this.setStyle(noteStruct.style);
this.slashed = noteStruct.slashed || false;
this.render_options = {
...this.render_options,
// font size for note heads
glyph_font_scale: noteStruct.glyph_font_scale || Tables.NOTATION_FONT_SCALE,
};
this.setWidth(
this.custom_glyph &&
!this.glyph_code.startsWith('noteheadSlashed') &&
!this.glyph_code.startsWith('noteheadCircled')
? Glyph.getWidth(this.glyph_code, this.render_options.glyph_font_scale)
: this.glyphProps.getWidth(this.render_options.glyph_font_scale)
);
}
/** Get the width of the notehead. */
getWidth(): number {
return this.width;
}
/** Determine if the notehead is displaced. */
isDisplaced(): boolean {
return this.displaced === true;
}
/** Set the X coordinate. */
setX(x: number): this {
this.x = x;
return this;
}
/** Get the Y coordinate. */
getY(): number {
return this.y;
}
/** Set the Y coordinate. */
setY(y: number): this {
this.y = y;
return this;
}
/** Get the stave line the notehead is placed on. */
getLine(): number {
return this.line;
}
/** Set the stave line the notehead is placed on. */
setLine(line: number): this {
this.line = line;
return this;
}
/** Get the canvas `x` coordinate position of the notehead. */
getAbsoluteX(): number {
// If the note has not been preformatted, then get the static x value
// Otherwise, it's been formatted and we should use it's x value relative
// to its tick context
const x = !this.preFormatted ? this.x : super.getAbsoluteX();
// For a more natural displaced notehead, we adjust the displacement amount
// by half the stem width in order to maintain a slight overlap with the stem
const displacementStemAdjustment = Stem.WIDTH / 2;
const musicFont = Tables.currentMusicFont();
const fontShift = musicFont.lookupMetric('notehead.shiftX', 0) * this.stem_direction;
const displacedFontShift = musicFont.lookupMetric('noteHead.displacedShiftX', 0) * this.stem_direction;
return (
x +
fontShift +
(this.displaced ? (this.width - displacementStemAdjustment) * this.stem_direction + displacedFontShift : 0)
);
}
/** Get the `BoundingBox` for the `NoteHead`. */
getBoundingBox(): BoundingBox {
const spacing = this.checkStave().getSpacingBetweenLines();
const half_spacing = spacing / 2;
const min_y = this.y - half_spacing;
return new BoundingBox(this.getAbsoluteX(), min_y, this.width, spacing);
}
/** Set notehead to a provided `stave`. */
setStave(stave: Stave): this {
const line = this.getLine();
this.stave = stave;
if (this.stave) {
this.setY(this.stave.getYForNote(line));
this.setContext(this.stave.getContext());
}
return this;
}
/** Pre-render formatting. */
preFormat(): this {
if (this.preFormatted) return this;
const width = this.getWidth() + this.leftDisplacedHeadPx + this.rightDisplacedHeadPx;
this.setWidth(width);
this.preFormatted = true;
return this;
}
/** Draw the notehead. */
draw(): void {
const ctx = this.checkContext();
this.setRendered();
let head_x = this.getAbsoluteX();
if (this.custom_glyph) {
// head_x += this.x_shift;
head_x +=
this.stem_direction === Stem.UP
? this.stem_up_x_offset +
(this.glyphProps.stem ? this.glyphProps.getWidth(this.render_options.glyph_font_scale) - this.width : 0)
: this.stem_down_x_offset;
}
const y = this.y;
L("Drawing note head '", this.noteType, this.duration, "' at", head_x, y);
// Begin and end positions for head.
const stem_direction = this.stem_direction;
const glyph_font_scale = this.render_options.glyph_font_scale;
const categorySuffix = `${this.glyph_code}Stem${stem_direction === Stem.UP ? 'Up' : 'Down'}`;
if (this.noteType === 's') {
const staveSpace = this.checkStave().getSpacingBetweenLines();
drawSlashNoteHead(ctx, this.duration, head_x, y, stem_direction, staveSpace);
} else {
Glyph.renderGlyph(ctx, head_x, y, glyph_font_scale, this.glyph_code, {
category: `noteHead.${categorySuffix}`,
});
}
}
}