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.
Vex.Flow.StaveLine = (function() {
function StaveLine(notes) {
if (arguments.length > 0) this.init(notes);
}Text Positioning
StaveLine.TextVerticalPosition = {
TOP: 1,
BOTTOM: 2
};
StaveLine.TextJustification = {
LEFT: 1,
CENTER: 2,
RIGHT: 3
}; StaveLine.prototype = {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]
}
init: function(notes) {
this.notes = notes;
this.context = null;
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 rendering context
setContext: function(context) { this.context = context; return this; },Set the font for the StaveLine text
setFont: function(font) { this.font = font; return this; },The the annotation for the StaveLine
setText: function(text) { this.text = text; return this; },Set the notes for the StaveLine
setNotes: function(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: function() {
if (!this.context) {
throw new Vex.RERR("NoContext","No context to apply the styling to");
}
var render_options = this.render_options;
var ctx = this.context;
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: function() {
if (!this.context) {
throw new Vex.RERR("NoContext","No context to apply the styling to");
}
var ctx = this.context;
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: function() {
if (!this.context) {
throw new Vex.RERR("NoContext", "No context to render StaveLine.");
}
var ctx = this.context;
var first_note = this.first_note;
var last_note = this.last_note;
var render_options = this.render_options;
ctx.save();
this.applyLineStyle();Cycle through each set of indices and draw lines
var start_position;
var end_position;
this.first_indices.forEach(function(first_index, i) {
var 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);
var 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
var notehead_width = first_note.getGlyph().head_width;
var 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
var 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);
}, this);
ctx.restore();Determine the x coordinate where to start the text
var text_width = ctx.measureText(this.text).width;
var justification = render_options.text_justification;
var x = 0;
if (justification === StaveLine.TextJustification.LEFT) {
x = start_position.x;
} else if (justification === StaveLine.TextJustification.CENTER) {
var delta_x = (end_position.x - start_position.x);
var 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
var y;
var 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();
}Draw the text
ctx.save();
this.applyFontStyle();
ctx.fillText(this.text, x, y);
ctx.restore();
return this;
}
};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) {
var both_arrows = config.draw_start_arrow && config.draw_end_arrow;
var x1 = point1.x;
var y1 = point1.y;
var x2 = point2.x;
var 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.
var distance = Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1));
var ratio = (distance - config.arrowhead_length/3) / distance;
var end_x, end_y, start_x, 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
var line_angle = Math.atan2(y2 - y1, x2 - x1);h is the line length of a side of the arrow head
var h = Math.abs(config.arrowhead_length / Math.cos(config.arrowhead_angle));
var angle1, angle2;
var top_x, top_y;
var bottom_x, 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);
}
}
return StaveLine;
}());