UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature.

303 lines (302 loc) 11.6 kB
import { Font } from './font.js'; import { Glyph } from './glyph.js'; import { Modifier } from './modifier.js'; import { Stem } from './stem.js'; import { StemmableNote } from './stemmablenote.js'; import { Tables } from './tables.js'; import { isDot } from './typeguard.js'; import { defined, RuntimeError } from './util.js'; function getUnusedStringGroups(num_lines, strings_used) { const stem_through = []; let group = []; for (let string = 1; string <= num_lines; string++) { const is_used = strings_used.indexOf(string) > -1; if (!is_used) { group.push(string); } else { stem_through.push(group); group = []; } } if (group.length > 0) stem_through.push(group); return stem_through; } function getPartialStemLines(stem_y, unused_strings, stave, stem_direction) { const up_stem = stem_direction !== 1; const down_stem = stem_direction !== -1; const line_spacing = stave.getSpacingBetweenLines(); const total_lines = stave.getNumLines(); const stem_lines = []; unused_strings.forEach((strings) => { const containsLastString = strings.indexOf(total_lines) > -1; const containsFirstString = strings.indexOf(1) > -1; if ((up_stem && containsFirstString) || (down_stem && containsLastString)) { return; } if (strings.length === 1) { strings.push(strings[0]); } const line_ys = []; strings.forEach((string, index, strings) => { const isTopBound = string === 1; const isBottomBound = string === total_lines; let y = stave.getYForLine(string - 1); if (index === 0 && !isTopBound) { y -= line_spacing / 2 - 1; } else if (index === strings.length - 1 && !isBottomBound) { y += line_spacing / 2 - 1; } line_ys.push(y); if (stem_direction === 1 && isTopBound) { line_ys.push(stem_y - 2); } else if (stem_direction === -1 && isBottomBound) { line_ys.push(stem_y + 2); } }); stem_lines.push(line_ys.sort((a, b) => a - b)); }); return stem_lines; } export class TabNote extends StemmableNote { static get CATEGORY() { return "TabNote"; } constructor(noteStruct, draw_stem = false) { super(noteStruct); this.glyphPropsArr = []; this.greatestString = () => { return this.positions.map((x) => x.str).reduce((a, b) => (a > b ? a : b)); }; this.leastString = () => { return this.positions.map((x) => x.str).reduce((a, b) => (a < b ? a : b)); }; this.ghost = false; this.positions = noteStruct.positions || []; this.render_options = Object.assign(Object.assign({}, this.render_options), { glyph_font_scale: Tables.TABLATURE_FONT_SCALE, draw_stem, draw_dots: draw_stem, draw_stem_through_stave: false, y_shift: 0, scale: 1.0, font: `${Font.SIZE}pt ${Font.SANS_SERIF}` }); this.glyphProps = Tables.getGlyphProps(this.duration, this.noteType); defined(this.glyphProps, 'BadArguments', `No glyph found for duration '${this.duration}' and type '${this.noteType}'`); this.buildStem(); if (noteStruct.stem_direction) { this.setStemDirection(noteStruct.stem_direction); } else { this.setStemDirection(Stem.UP); } this.ghost = false; this.updateWidth(); } reset() { super.reset(); if (this.stave) this.setStave(this.stave); return this; } setGhost(ghost) { this.ghost = ghost; this.updateWidth(); return this; } hasStem() { if (this.render_options.draw_stem) return true; return false; } getStemExtension() { const glyphProps = this.getGlyphProps(); if (this.stem_extension_override != null) { return this.stem_extension_override; } if (glyphProps) { return this.getStemDirection() === Stem.UP ? glyphProps.tabnote_stem_up_extension : glyphProps.tabnote_stem_down_extension; } return 0; } updateWidth() { this.glyphPropsArr = []; this.width = 0; for (let i = 0; i < this.positions.length; ++i) { let fret = this.positions[i].fret; if (this.ghost) fret = '(' + fret + ')'; const glyphProps = Tables.tabToGlyphProps(fret.toString(), this.render_options.scale); this.glyphPropsArr.push(glyphProps); this.width = Math.max(glyphProps.getWidth(), this.width); } this.glyphProps.getWidth = () => this.width; } setStave(stave) { super.setStave(stave); const ctx = stave.getContext(); this.setContext(ctx); if (ctx) { this.width = 0; for (let i = 0; i < this.glyphPropsArr.length; ++i) { const glyphProps = this.glyphPropsArr[i]; const text = '' + glyphProps.text; if (text.toUpperCase() !== 'X') { ctx.save(); ctx.setFont(this.render_options.font); glyphProps.width = ctx.measureText(text).width; ctx.restore(); glyphProps.getWidth = () => glyphProps.width; } this.width = Math.max(glyphProps.getWidth(), this.width); } this.glyphProps.getWidth = () => this.width; } const ys = this.positions.map(({ str: line }) => stave.getYForLine(Number(line) - 1)); this.setYs(ys); if (this.stem) { this.stem.setYBounds(this.getStemY(), this.getStemY()); } return this; } getPositions() { return this.positions; } getModifierStartXY(position, index) { if (!this.preFormatted) { throw new RuntimeError('UnformattedNote', "Can't call GetModifierStartXY on an unformatted note"); } if (this.ys.length === 0) { throw new RuntimeError('NoYValues', 'No Y-Values calculated for this note.'); } let x = 0; if (position === Modifier.Position.LEFT) { x = -1 * 2; } else if (position === Modifier.Position.RIGHT) { x = this.width + 2; } else if (position === Modifier.Position.BELOW || position === Modifier.Position.ABOVE) { const note_glyph_width = this.glyphProps.getWidth(); x = note_glyph_width / 2; } return { x: this.getAbsoluteX() + x, y: this.ys[index], }; } getLineForRest() { return Number(this.positions[0].str); } preFormat() { if (this.preFormatted) return; if (this.modifierContext) this.modifierContext.preFormat(); this.preFormatted = true; } getStemX() { return this.getCenterGlyphX(); } getStemY() { const num_lines = this.checkStave().getNumLines(); const stemUpLine = -0.5; const stemDownLine = num_lines - 0.5; const stemStartLine = Stem.UP === this.stem_direction ? stemUpLine : stemDownLine; return this.checkStave().getYForLine(stemStartLine); } getStemExtents() { return this.checkStem().getExtents(); } drawFlag() { var _a; const { beam, glyphProps, render_options: { draw_stem }, } = this; const context = this.checkContext(); const shouldDrawFlag = beam == undefined && draw_stem; if (glyphProps.flag && shouldDrawFlag) { const flag_x = this.getStemX(); const flag_y = this.getStemDirection() === Stem.DOWN ? this.getStemY() - this.checkStem().getHeight() - (this.glyphProps ? this.glyphProps.stem_down_extension : 0) : this.getStemY() - this.checkStem().getHeight() + (this.glyphProps ? this.glyphProps.stem_up_extension : 0); (_a = this.flag) === null || _a === void 0 ? void 0 : _a.render(context, flag_x, flag_y); } } drawModifiers() { this.modifiers.forEach((modifier) => { if (isDot(modifier) && !this.render_options.draw_dots) { return; } modifier.setContext(this.getContext()); modifier.drawWithStyle(); }); } drawStemThrough() { const stemX = this.getStemX(); const stemY = this.getStemY(); const ctx = this.checkContext(); const drawStem = this.render_options.draw_stem; const stemThrough = this.render_options.draw_stem_through_stave; if (drawStem && stemThrough) { const numLines = this.checkStave().getNumLines(); const stringsUsed = this.positions.map((position) => Number(position.str)); const unusedStrings = getUnusedStringGroups(numLines, stringsUsed); const stemLines = getPartialStemLines(stemY, unusedStrings, this.checkStave(), this.getStemDirection()); ctx.save(); ctx.setLineWidth(Stem.WIDTH); stemLines.forEach((bounds) => { if (bounds.length === 0) return; ctx.beginPath(); ctx.moveTo(stemX, bounds[0]); ctx.lineTo(stemX, bounds[bounds.length - 1]); ctx.stroke(); ctx.closePath(); }); ctx.restore(); } } drawPositions() { var _a; const ctx = this.checkContext(); const x = this.getAbsoluteX(); const ys = this.ys; for (let i = 0; i < this.positions.length; ++i) { const y = ys[i] + this.render_options.y_shift; const glyphProps = this.glyphPropsArr[i]; const note_glyph_width = this.glyphProps.getWidth(); const tab_x = x + note_glyph_width / 2 - glyphProps.getWidth() / 2; ctx.clearRect(tab_x - 2, y - 3, glyphProps.getWidth() + 4, 6); if (glyphProps.code) { Glyph.renderGlyph(ctx, tab_x, y, this.render_options.glyph_font_scale * this.render_options.scale, glyphProps.code); } else { ctx.save(); ctx.setFont(this.render_options.font); const text = (_a = glyphProps.text) !== null && _a !== void 0 ? _a : ''; ctx.fillText(text, tab_x, y + 5 * this.render_options.scale); ctx.restore(); } } } draw() { const ctx = this.checkContext(); if (this.ys.length === 0) { throw new RuntimeError('NoYValues', "Can't draw note without Y values."); } this.setRendered(); const render_stem = this.beam == undefined && this.render_options.draw_stem; this.applyStyle(); ctx.openGroup('tabnote', this.getAttribute('id'), { pointerBBox: true }); this.drawPositions(); this.drawStemThrough(); if (this.stem && render_stem) { const stem_x = this.getStemX(); this.stem.setNoteHeadXBounds(stem_x, stem_x); this.stem.setContext(ctx).draw(); } this.drawFlag(); this.drawModifiers(); ctx.closeGroup(); this.restoreStyle(); } }