VexFlow - Copyright (c) Mohit Muthanna 2010.
This file implements StaveLine
which are simply lines that connect
two notes. This object is highly configurable, see the render_options
.
A simple line is often used for notating glissando articulations, but you
can format a StaveLine
with arrows or colors for more pedagogical
purposes, such as diagrams.
import { Vex } from './vex';
import { Element } from './element';
import { Flow } from './tables';
Attribution: Arrow rendering implementations based off of Patrick Horgan’s article, “Drawing lines and arcs with arrow heads on HTML5 Canvas”
Draw an arrow head that connects between 3 coordinates
function drawArrowHead(ctx, x0, y0, x1, y1, x2, y2) {
all cases do this.
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x0, y0);
ctx.closePath();
ctx.fill();
}
Helper function to draw a line with arrow heads
function drawArrowLine(ctx, point1, point2, config) {
const both_arrows = config.draw_start_arrow && config.draw_end_arrow;
const x1 = point1.x;
const y1 = point1.y;
const x2 = point2.x;
const y2 = point2.y;
For ends with arrow we actually want to stop before we get to the arrow so that wide lines won’t put a flat end on the arrow.
const distance = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
const ratio = (distance - config.arrowhead_length / 3) / distance;
let end_x;
let end_y;
let start_x;
let start_y;
if (config.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 (config.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 (config.color) {
ctx.setStrokeStyle(config.color);
ctx.setFillStyle(config.color);
}
Draw the shaft of the arrow
ctx.beginPath();
ctx.moveTo(start_x, start_y);
ctx.lineTo(end_x, end_y);
ctx.stroke();
ctx.closePath();
calculate the angle of the line
const line_angle = Math.atan2(y2 - y1, x2 - x1);
h is the line length of a side of the arrow head
const h = Math.abs(config.arrowhead_length / Math.cos(config.arrowhead_angle));
let angle1;
let angle2;
let top_x;
let top_y;
let bottom_x;
let bottom_y;
if (config.draw_end_arrow || both_arrows) {
angle1 = line_angle + Math.PI + config.arrowhead_angle;
top_x = x2 + Math.cos(angle1) * h;
top_y = y2 + Math.sin(angle1) * h;
angle2 = line_angle + Math.PI - config.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 (config.draw_start_arrow || both_arrows) {
angle1 = line_angle + config.arrowhead_angle;
top_x = x1 + Math.cos(angle1) * h;
top_y = y1 + Math.sin(angle1) * h;
angle2 = line_angle - config.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);
}
}
export class StaveLine extends Element {
Text Positioning
static get TextVerticalPosition() {
return {
TOP: 1,
BOTTOM: 2,
};
}
static get TextJustification() {
return {
LEFT: 1,
CENTER: 2,
RIGHT: 3,
};
}
Initialize the StaveLine with the given notes
.
notes
is a struct that has:
{
first_note: Note,
last_note: Note,
first_indices: [n1, n2, n3],
last_indices: [n1, n2, n3]
}
constructor(notes) {
super();
this.attrs.type = 'StaveLine';
this.notes = notes;
this.text = '';
this.font = {
family: 'Arial',
size: 10,
weight: '',
};
this.render_options = {
Space to add to the left or the right
padding_left: 4,
padding_right: 3,
The width of the line in pixels
line_width: 1,
An array of line/space lengths. Unsupported with Raphael (SVG)
line_dash: null,
Can draw rounded line end, instead of a square. Unsupported with Raphael (SVG)
rounded_end: true,
The color of the line and arrowheads
color: null,
Flags to draw arrows on each end of the line
draw_start_arrow: false,
draw_end_arrow: false,
The length of the arrowhead sides
arrowhead_length: 10,
The angle of the arrowhead
arrowhead_angle: Math.PI / 8,
The position of the text
text_position_vertical: StaveLine.TextVerticalPosition.TOP,
text_justification: StaveLine.TextJustification.CENTER,
};
this.setNotes(notes);
}
Set the font for the StaveLine
text
setFont(font) { this.font = font; return this; }
The the annotation for the StaveLine
setText(text) { this.text = text; return this; }
Set the notes for the StaveLine
setNotes(notes) {
if (!notes.first_note && !notes.last_note) {
throw new Vex.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 Vex.RuntimeError(
'BadArguments', 'Connected notes must have similar index sizes'
);
}
Success. Lets grab ‘em 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;
}
Apply the style of the StaveLine
to the context
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');
}
}
Apply the text styling to the context
applyFontStyle() {
const ctx = this.checkContext();
if (this.font) {
ctx.setFont(this.font.family, this.font.size, this.font.weight);
}
if (this.render_options.color) {
ctx.setStrokeStyle(this.render_options.color);
ctx.setFillStyle(this.render_options.color);
}
}
Renders the StaveLine
on the context
draw() {
const ctx = this.checkContext();
const first_note = this.first_note;
const last_note = this.last_note;
const render_options = this.render_options;
ctx.save();
this.applyLineStyle();
Cycle through each set of indices and draw lines
let start_position;
let end_position;
this.first_indices.forEach((first_index, i) => {
const last_index = this.last_indices[i];
Get initial coordinates for the start/end of the line
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;
Adjust x
coordinates for modifiers
start_position.x += first_note.getMetrics().modRightPx + render_options.padding_left;
end_position.x -= last_note.getMetrics().modLeftPx + render_options.padding_right;
Adjust first x
coordinates for displacements
const notehead_width = first_note.getGlyph().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;
}
Adjust last x
coordinates for displacements
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;
}
Adjust y position better if it’s not coming from the center of the note
start_position.y += upwards_slope ? -3 : 1;
end_position.y += upwards_slope ? 2 : 0;
drawArrowLine(ctx, start_position, end_position, this.render_options);
});
ctx.restore();
Determine the x coordinate where to start the text
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;
}
Determine the y value to start the text
let y;
const vertical_position = render_options.text_position_vertical;
if (vertical_position === StaveLine.TextVerticalPosition.TOP) {
y = first_note.getStave().getYForTopText();
} else if (vertical_position === StaveLine.TextVerticalPosition.BOTTOM) {
y = first_note.getStave().getYForBottomText(Flow.TEXT_HEIGHT_OFFSET_HACK);
}
Draw the text
ctx.save();
this.applyFontStyle();
ctx.fillText(this.text, x, y);
ctx.restore();
return this;
}
}