UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature.

305 lines (270 loc) 9.27 kB
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010. // Author: Larry Kuhns // // This file implements the `StringNumber` class which renders string // number annotations beside notes. import { Font, FontInfo, FontStyle, FontWeight } from './font'; import { Modifier, ModifierPosition } from './modifier'; import { ModifierContextState } from './modifiercontext'; import { Note } from './note'; import { Renderer } from './renderer'; import { Stem } from './stem'; import { StemmableNote } from './stemmablenote'; import { Tables } from './tables'; import { Category, isStaveNote, isStemmableNote } from './typeguard'; import { RuntimeError } from './util'; export interface StringNumberMetrics { verticalPadding: number; stemPadding: number; leftPadding: number; rightPadding: number; } export class StringNumber extends Modifier { static get CATEGORY(): string { return Category.StringNumber; } static TEXT_FONT: Required<FontInfo> = { family: Font.SANS_SERIF, size: Font.SIZE, weight: FontWeight.BOLD, style: FontStyle.NORMAL, }; static get metrics(): StringNumberMetrics { return ( Tables.currentMusicFont().getMetrics().stringNumber ?? { verticalPadding: 0, stemPadding: 0, leftPadding: 0, rightPadding: 0, } ); } // ## Static Methods // Arrange string numbers inside a `ModifierContext` static format(nums: StringNumber[], state: ModifierContextState): boolean { const left_shift = state.left_shift; const right_shift = state.right_shift; const num_spacing = 1; if (!nums || nums.length === 0) return false; const nums_list = []; let prev_note = null; let shift_left = 0; let shift_right = 0; const modLines = 0; for (let i = 0; i < nums.length; ++i) { const num = nums[i]; const note = num.getNote(); const pos = num.getPosition(); if (!isStaveNote(note)) { throw new RuntimeError('NoStaveNote'); } const index = num.checkIndex(); const props = note.getKeyProps()[index]; const mc = note.getModifierContext(); const verticalSpaceNeeded = (num.radius * 2) / Tables.STAVE_LINE_DISTANCE + 0.5; if (mc) { if (pos === ModifierPosition.ABOVE) { num.text_line = mc.getState().top_text_line; state.top_text_line += verticalSpaceNeeded; } else if (pos === ModifierPosition.BELOW) { num.text_line = mc.getState().text_line; state.text_line += verticalSpaceNeeded; } } if (note !== prev_note) { for (let n = 0; n < note.keys.length; ++n) { if (left_shift === 0) { shift_left = Math.max(note.getLeftDisplacedHeadPx(), shift_left); } if (right_shift === 0) { shift_right = Math.max(note.getRightDisplacedHeadPx(), shift_right); } } prev_note = note; } const glyphLine = modLines === 0 ? props.line : modLines; nums_list.push({ pos, note, num, line: glyphLine, shiftL: shift_left, shiftR: shift_right, }); } // Sort string numbers by line number. nums_list.sort((a, b) => b.line - a.line); let num_shiftR = 0; let x_widthL = 0; let x_widthR = 0; let last_line = null; let last_note = null; for (let i = 0; i < nums_list.length; ++i) { let num_shift = 0; const note = nums_list[i].note; const pos = nums_list[i].pos; const num = nums_list[i].num; const line = nums_list[i].line; const shiftR = nums_list[i].shiftR; // Reset the position of the string number every line. if (line !== last_line || note !== last_note) { num_shiftR = right_shift + shiftR; } const num_width = num.getWidth() + num_spacing; if (pos === Modifier.Position.LEFT) { num.setXShift(left_shift); num_shift = shift_left + num_width; // spacing x_widthL = num_shift > x_widthL ? num_shift : x_widthL; } else if (pos === Modifier.Position.RIGHT) { num.setXShift(num_shiftR); num_shift += num_width; // spacing x_widthR = num_shift > x_widthR ? num_shift : x_widthR; } last_line = line; last_note = note; } state.left_shift += x_widthL; state.right_shift += x_widthR; return true; } protected radius: number; protected drawCircle: boolean; protected last_note?: Note; protected string_number: string; protected x_offset: number; protected y_offset: number; protected text_line: number; protected stem_offset: number; protected dashed: boolean; protected leg: number; constructor(number: string) { super(); this.string_number = number; this.position = Modifier.Position.ABOVE; // Default position above stem or note head this.x_shift = 0; this.y_shift = 0; this.text_line = 0; this.stem_offset = 0; this.x_offset = 0; // Horizontal offset from default this.y_offset = 0; // Vertical offset from default this.dashed = true; // true - draw dashed extension false - no extension this.leg = Renderer.LineEndType.NONE; // draw upward/downward leg at the of extension line this.radius = 8; this.drawCircle = true; this.setWidth(this.radius * 2 + 4); this.resetFont(); } setLineEndType(leg: number): this { if (leg >= Renderer.LineEndType.NONE && leg <= Renderer.LineEndType.DOWN) { this.leg = leg; } return this; } setStringNumber(number: string): this { this.string_number = number; return this; } setOffsetX(x: number): this { this.x_offset = x; return this; } setOffsetY(y: number): this { this.y_offset = y; return this; } setLastNote(note: Note): this { this.last_note = note; return this; } setDashed(dashed: boolean): this { this.dashed = dashed; return this; } setDrawCircle(drawCircle: boolean): this { this.drawCircle = drawCircle; return this; } draw(): void { const ctx = this.checkContext(); const note = this.checkAttachedNote(); this.setRendered(); const start = note.getModifierStartXY(this.position, this.index); const stemDirection = note.hasStem() ? note.getStemDirection() : Stem.UP; let dot_x = start.x + this.x_shift + this.x_offset; let stem_ext: Record<string, number> = {}; if (note.hasStem()) { stem_ext = (note as StemmableNote).checkStem().getExtents(); } let dot_y = start.y + this.y_shift + this.y_offset; switch (this.position) { case Modifier.Position.ABOVE: { const ys = note.getYs(); dot_y = ys.reduce((a, b) => (a < b ? a : b)); if (note.hasStem() && stemDirection == Stem.UP) { dot_y = stem_ext.topY + StringNumber.metrics.stemPadding; } dot_y -= this.radius + StringNumber.metrics.verticalPadding + this.text_line * Tables.STAVE_LINE_DISTANCE; } break; case Modifier.Position.BELOW: { const ys: number[] = note.getYs(); dot_y = ys.reduce((a, b) => (a > b ? a : b)); if (note.hasStem() && stemDirection == Stem.DOWN) { dot_y = stem_ext.topY - StringNumber.metrics.stemPadding; } dot_y += this.radius + StringNumber.metrics.verticalPadding + this.text_line * Tables.STAVE_LINE_DISTANCE; } break; case Modifier.Position.LEFT: dot_x -= this.radius / 2 + StringNumber.metrics.leftPadding; break; case Modifier.Position.RIGHT: dot_x += this.radius / 2 + StringNumber.metrics.rightPadding; break; default: throw new RuntimeError('InvalidPosition', `The position ${this.position} is invalid`); } ctx.save(); if (this.drawCircle) { ctx.beginPath(); ctx.arc(dot_x, dot_y, this.radius, 0, Math.PI * 2, false); ctx.setLineWidth(1.5); ctx.stroke(); } ctx.setFont(this.textFont); const x = dot_x - ctx.measureText(this.string_number).width / 2; ctx.fillText('' + this.string_number, x, dot_y + 4.5); const lastNote = this.last_note; if (isStemmableNote(lastNote)) { // Only StemmableNote objects have getStemX(). const end = lastNote.getStemX() - note.getX() + 5; ctx.setStrokeStyle('#000000'); ctx.setLineCap('round'); ctx.setLineWidth(0.6); if (this.dashed) { Renderer.drawDashedLine(ctx, dot_x + 10, dot_y, dot_x + end, dot_y, [3, 3]); } else { Renderer.drawDashedLine(ctx, dot_x + 10, dot_y, dot_x + end, dot_y, [3, 0]); } let len; let pattern; switch (this.leg) { case Renderer.LineEndType.UP: len = -10; pattern = this.dashed ? [3, 3] : [3, 0]; Renderer.drawDashedLine(ctx, dot_x + end, dot_y, dot_x + end, dot_y + len, pattern); break; case Renderer.LineEndType.DOWN: len = 10; pattern = this.dashed ? [3, 3] : [3, 0]; Renderer.drawDashedLine(ctx, dot_x + end, dot_y, dot_x + end, dot_y + len, pattern); break; default: break; } } ctx.restore(); } }