UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature.

217 lines (216 loc) 8.42 kB
import { Element } from './element.js'; import { Tables } from './tables.js'; import { TextJustification } from './textnote.js'; import { RuntimeError } from './util.js'; function drawArrowHead(ctx, x0, y0, x1, y1, x2, y2) { ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x0, y0); ctx.closePath(); ctx.fill(); } class StaveLine extends Element { static get CATEGORY() { return "StaveLine"; } constructor(notes) { super(); this.setNotes(notes); this.text = ''; this.resetFont(); this.render_options = { padding_left: 4, padding_right: 3, line_width: 1, line_dash: undefined, rounded_end: true, color: undefined, draw_start_arrow: false, draw_end_arrow: false, arrowhead_length: 10, arrowhead_angle: Math.PI / 8, text_position_vertical: StaveLine.TextVerticalPosition.TOP, text_justification: StaveLine.TextJustification.CENTER, }; } setText(text) { this.text = text; return this; } setNotes(notes) { if (!notes.first_note && !notes.last_note) { throw new RuntimeError('BadArguments', 'Notes needs to have either first_note or last_note set.'); } if (!notes.first_indices) notes.first_indices = [0]; if (!notes.last_indices) notes.last_indices = [0]; if (notes.first_indices.length !== notes.last_indices.length) { throw new RuntimeError('BadArguments', 'Connected notes must have same number of indices.'); } this.notes = notes; this.first_note = notes.first_note; this.first_indices = notes.first_indices; this.last_note = notes.last_note; this.last_indices = notes.last_indices; return this; } applyLineStyle() { const ctx = this.checkContext(); const render_options = this.render_options; if (render_options.line_dash) { ctx.setLineDash(render_options.line_dash); } if (render_options.line_width) { ctx.setLineWidth(render_options.line_width); } if (render_options.rounded_end) { ctx.setLineCap('round'); } else { ctx.setLineCap('square'); } } applyFontStyle() { const ctx = this.checkContext(); ctx.setFont(this.textFont); const render_options = this.render_options; const color = render_options.color; if (color) { ctx.setStrokeStyle(color); ctx.setFillStyle(color); } } drawArrowLine(ctx, pt1, pt2) { const both_arrows = this.render_options.draw_start_arrow && this.render_options.draw_end_arrow; const x1 = pt1.x; const y1 = pt1.y; const x2 = pt2.x; const y2 = pt2.y; const distance = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); const ratio = (distance - this.render_options.arrowhead_length / 3) / distance; let end_x; let end_y; let start_x; let start_y; if (this.render_options.draw_end_arrow || both_arrows) { end_x = Math.round(x1 + (x2 - x1) * ratio); end_y = Math.round(y1 + (y2 - y1) * ratio); } else { end_x = x2; end_y = y2; } if (this.render_options.draw_start_arrow || both_arrows) { start_x = x1 + (x2 - x1) * (1 - ratio); start_y = y1 + (y2 - y1) * (1 - ratio); } else { start_x = x1; start_y = y1; } if (this.render_options.color) { ctx.setStrokeStyle(this.render_options.color); ctx.setFillStyle(this.render_options.color); } ctx.beginPath(); ctx.moveTo(start_x, start_y); ctx.lineTo(end_x, end_y); ctx.stroke(); ctx.closePath(); const line_angle = Math.atan2(y2 - y1, x2 - x1); const h = Math.abs(this.render_options.arrowhead_length / Math.cos(this.render_options.arrowhead_angle)); let angle1; let angle2; let top_x; let top_y; let bottom_x; let bottom_y; if (this.render_options.draw_end_arrow || both_arrows) { angle1 = line_angle + Math.PI + this.render_options.arrowhead_angle; top_x = x2 + Math.cos(angle1) * h; top_y = y2 + Math.sin(angle1) * h; angle2 = line_angle + Math.PI - this.render_options.arrowhead_angle; bottom_x = x2 + Math.cos(angle2) * h; bottom_y = y2 + Math.sin(angle2) * h; drawArrowHead(ctx, top_x, top_y, x2, y2, bottom_x, bottom_y); } if (this.render_options.draw_start_arrow || both_arrows) { angle1 = line_angle + this.render_options.arrowhead_angle; top_x = x1 + Math.cos(angle1) * h; top_y = y1 + Math.sin(angle1) * h; angle2 = line_angle - this.render_options.arrowhead_angle; bottom_x = x1 + Math.cos(angle2) * h; bottom_y = y1 + Math.sin(angle2) * h; drawArrowHead(ctx, top_x, top_y, x1, y1, bottom_x, bottom_y); } } draw() { const ctx = this.checkContext(); this.setRendered(); const first_note = this.first_note; const last_note = this.last_note; const render_options = this.render_options; ctx.save(); this.applyLineStyle(); let start_position = { x: 0, y: 0 }; let end_position = { x: 0, y: 0 }; this.first_indices.forEach((first_index, i) => { const last_index = this.last_indices[i]; start_position = first_note.getModifierStartXY(2, first_index); end_position = last_note.getModifierStartXY(1, last_index); const upwards_slope = start_position.y > end_position.y; start_position.x += first_note.getMetrics().modRightPx + render_options.padding_left; end_position.x -= last_note.getMetrics().modLeftPx + render_options.padding_right; const notehead_width = first_note.getGlyphProps().getWidth(); const first_displaced = first_note.getKeyProps()[first_index].displaced; if (first_displaced && first_note.getStemDirection() === 1) { start_position.x += notehead_width + render_options.padding_left; } const last_displaced = last_note.getKeyProps()[last_index].displaced; if (last_displaced && last_note.getStemDirection() === -1) { end_position.x -= notehead_width + render_options.padding_right; } start_position.y += upwards_slope ? -3 : 1; end_position.y += upwards_slope ? 2 : 0; this.drawArrowLine(ctx, start_position, end_position); }); ctx.restore(); const text_width = ctx.measureText(this.text).width; const justification = render_options.text_justification; let x = 0; if (justification === StaveLine.TextJustification.LEFT) { x = start_position.x; } else if (justification === StaveLine.TextJustification.CENTER) { const delta_x = end_position.x - start_position.x; const center_x = delta_x / 2 + start_position.x; x = center_x - text_width / 2; } else if (justification === StaveLine.TextJustification.RIGHT) { x = end_position.x - text_width; } let y = 0; const vertical_position = render_options.text_position_vertical; if (vertical_position === StaveLine.TextVerticalPosition.TOP) { y = first_note.checkStave().getYForTopText(); } else if (vertical_position === StaveLine.TextVerticalPosition.BOTTOM) { y = first_note.checkStave().getYForBottomText(Tables.TEXT_HEIGHT_OFFSET_HACK); } ctx.save(); this.applyFontStyle(); ctx.fillText(this.text, x, y); ctx.restore(); return this; } } StaveLine.TEXT_FONT = Object.assign({}, Element.TEXT_FONT); StaveLine.TextVerticalPosition = { TOP: 1, BOTTOM: 2, }; StaveLine.TextJustification = TextJustification; export { StaveLine };