/**
* Create a new tuplet from the specified notes. The notes must
* be part of the same line, and have the same duration (in ticks).
*
* @constructor
* @param {Array.<Vex.Flow.StaveNote>} A set of notes.
*/
Vex.Flow.Tuplet = (function() {
function Tuplet(notes, options) {
if (arguments.length > 0) this.init(notes, options);
}
Tuplet.LOCATION_TOP = 1;
Tuplet.LOCATION_BOTTOM = -1;
Tuplet.prototype = {
init: function(notes, options) {
if (!notes || notes == []) {
throw new Vex.RuntimeError("BadArguments", "No notes provided for tuplet.");
}
if (notes.length == 1) {
throw new Vex.RuntimeError("BadArguments", "Too few notes for tuplet.");
}
this.options = Vex.Merge({}, options);
this.notes = notes;
this.num_notes = 'num_notes' in this.options ?
this.options.num_notes : notes.length;
this.beats_occupied = 'beats_occupied' in this.options ?
this.options.beats_occupied : 2;
this.bracketed = (notes[0].beam == null);
this.ratioed = false;
this.point = 28;
this.y_pos = 16;
this.x_pos = 100;
this.width = 200;
this.location = Tuplet.LOCATION_TOP;
Vex.Flow.Formatter.AlignRestsToNotes(notes, true, true);
this.resolveGlyphs();
this.attach();
},
attach: function () {
for (var i = 0; i < this.notes.length; i++) {
var note = this.notes[i];
note.setTuplet(this);
}
},
detach: function () {
for (var i = 0; i < this.notes.length; i++) {
var note = this.notes[i];
note.setTuplet(null);
}
},
setContext: function(context) {
this.context = context;
return this;
},
/**
* Set whether or not the bracket is drawn.
*/
setBracketed: function(bracketed) {
this.bracketed = bracketed ? true : false;
return this;
},
/**
* Set whether or not the ratio is shown.
*/
setRatioed: function(ratioed) {
this.ratioed = ratioed ? true : false;
return this;
},
/**
* Set the tuplet to be displayed either on the top or bottom of the stave
*/
setTupletLocation: function(location) {
if (!location) location = Tuplet.LOCATION_TOP;
else if (location != Tuplet.LOCATION_TOP &&
location != Tuplet.LOCATION_BOTTOM) {
throw new Vex.RERR("BadArgument", "Invalid tuplet location: " + location);
}
this.location = location;
return this;
},
getNotes: function() {
return this.notes;
},
getNoteCount: function() {
return this.num_notes;
},
getBeatsOccupied: function() {
return this.beats_occupied;
},
setBeatsOccupied: function(beats) {
this.detach();
this.beats_occupied = beats;
this.resolveGlyphs();
this.attach();
},
resolveGlyphs: function() {
this.num_glyphs = [];
var n = this.num_notes;
while (n >= 1) {
this.num_glyphs.push(new Vex.Flow.Glyph("v" + (n % 10), this.point));
n = parseInt(n / 10, 10);
}
this.denom_glyphs = [];
n = this.beats_occupied;
while (n >= 1) {
this.denom_glyphs.push(new Vex.Flow.Glyph("v" + (n % 10), this.point));
n = parseInt(n / 10, 10);
}
},
draw: function() {
if (!this.context) throw new Vex.RERR("NoCanvasContext",
"Can't draw without a canvas context.");determine x value of left bound of tuplet
var first_note = this.notes[0];
var last_note = this.notes[this.notes.length - 1];
if (!this.bracketed) {
this.x_pos = first_note.getStemX();
this.width = last_note.getStemX() - this.x_pos;
}
else {
this.x_pos = first_note.getTieLeftX() - 5;
this.width = last_note.getTieRightX() - this.x_pos + 5;
}determine y value for tuplet
var i;
if (this.location == Tuplet.LOCATION_TOP) {
this.y_pos = first_note.getStave().getYForLine(0) - 15;this.y_pos = first_note.getStemExtents().topY - 10;
for (i=0; i<this.notes.length; ++i) {
var top_y = this.notes[i].getStemDirection() === Vex.Flow.Stem.UP ?
this.notes[i].getStemExtents().topY - 10
: this.notes[i].getStemExtents().baseY - 20;
if (top_y < this.y_pos)
this.y_pos = top_y;
}
}
else {
this.y_pos = first_note.getStave().getYForLine(4) + 20;
for (i=0; i<this.notes.length; ++i) {
var bottom_y = this.notes[i].getStemDirection() === Vex.Flow.Stem.UP ?
this.notes[i].getStemExtents().baseY + 20
: this.notes[i].getStemExtents().topY + 10;
if (bottom_y > this.y_pos)
this.y_pos = bottom_y;
}
}calculate total width of tuplet notation
var width = 0;
var glyph;
for (glyph in this.num_glyphs) {
width += this.num_glyphs[glyph].getMetrics().width;
}
if (this.ratioed) {
for (glyph in this.denom_glyphs) {
width += this.denom_glyphs[glyph].getMetrics().width;
}
width += this.point * 0.32;
}
var notation_center_x = this.x_pos + (this.width/2);
var notation_start_x = notation_center_x - (width/2);draw bracket if the tuplet is not beamed
if (this.bracketed) {
var line_width = this.width/2 - width/2 - 5;only draw the bracket if it has positive length
if (line_width > 0) {
this.context.fillRect(this.x_pos, this.y_pos,line_width, 1);
this.context.fillRect(this.x_pos + this.width / 2 + width / 2 + 5,
this.y_pos,line_width, 1);
this.context.fillRect(this.x_pos,
this.y_pos + (this.location == Tuplet.LOCATION_BOTTOM),
1, this.location * 10);
this.context.fillRect(this.x_pos + this.width,
this.y_pos + (this.location == Tuplet.LOCATION_BOTTOM),
1, this.location * 10);
}
}draw numerator glyphs
var x_offset = 0;
var size = this.num_glyphs.length;
for (glyph in this.num_glyphs) {
this.num_glyphs[size-glyph-1].render(
this.context, notation_start_x + x_offset,
this.y_pos + (this.point/3) - 2);
x_offset += this.num_glyphs[size-glyph-1].getMetrics().width;
}display colon and denominator if the ratio is to be shown
if (this.ratioed) {
var colon_x = notation_start_x + x_offset + this.point*0.16;
var colon_radius = this.point * 0.06;
this.context.beginPath();
this.context.arc(colon_x, this.y_pos - this.point*0.08,
colon_radius, 0, Math.PI*2, true);
this.context.closePath();
this.context.fill();
this.context.beginPath();
this.context.arc(colon_x, this.y_pos + this.point*0.12,
colon_radius, 0, Math.PI*2, true);
this.context.closePath();
this.context.fill();
x_offset += this.point*0.32;
size = this.denom_glyphs.length;
for (glyph in this.denom_glyphs) {
this.denom_glyphs[size-glyph-1].render(
this.context, notation_start_x + x_offset,
this.y_pos + (this.point/3) - 2);
x_offset += this.denom_glyphs[size-glyph-1].getMetrics().width;
}
}
}
};
return Tuplet;
}());