/**
* Tablature renderer -- reads tab data and draws canvas elements.
* Creates "packed" versions of the tabs, including a "key line" that's comprised
* only of '-' and '*' -- the asterisks denoting where a dot will eventually be placed.
* @class tabs
* @constructor
* @namespace ukeGeeks
*/
ukeGeeks.tabs = function() {
/**
* alias for external Settings dependencies (helps with complression, too)
* @property tab_settings
* @private
* @type {JSON}
*/
var tab_settings = ukeGeeks.settings.tabs;
// TODO: use ukeGeeks.settings.tuning for NUM_STRINGS and LAST_STRING_NAME??
/**
* (Constant) Number of Strings (dashed lines of tablature notation) expected. (For now
* a constant -- ukueleles "always" have four). Making a variable to help support port
* for other instruments.
* @property NUM_STRINGS
* @private
* @type int
*/
var NUM_STRINGS= 4;
/**
* (Constant) Last String Name (Note), as above, on Ukulele is a "G". Here for other instruments.
* @property LAST_STRING_NAME
* @private
* @type string
*/
var LAST_STRING_NAME= 'G';
/* PUBLIC METHODS
---------------------------------------------- */
/**
* Again this is a constructor replacement
* @method init
* @public
* @return {void}
*/
var init= function() {};
/**
* Races through all <pre> tags within h, any with the CSS class of "ugsTabs" will be replaced with the canvas element.
* @method replace
* @public
* @param h {DOM-element}
* @return {void}
*/
var replace= function(h) {
var tabBlocks = h.getElementsByTagName('pre');
for (var i in tabBlocks) {
if (tabBlocks[i].className == 'ugsTabs') {
var s = tabBlocks[i].innerHTML;
tabBlocks[i].innerHTML = '';
loadBlocks(s, tabBlocks[i]);
}
}
};
/**
*
* @method loadBlocks
* @param text {string} Block of text that contains one or more tablature blocks
* @param outElement {string or DOM} Either: (string) the Id to a DOM element, or DOM element handle where the canvas/converted text will be placed.
* @return {void}
*/
var loadBlocks= function(text, outElement) {
var lines = text.split('\n');
var tab = [];
for (var i in lines) {
var s = ukeGeeks.toolsLite.trim(lines[i]);
if (s.length > 0) {
tab.push(s);
}
if (tab.length == NUM_STRINGS) {
redraw(tab, outElement);
tab = [];
}
}
};
/**
*
* @method redraw
* @param inTabs {string or array} Block of text or four element array containing tablbature to be parsed
* @param outElement {string or DOM} Either: (string) the Id to a DOM element, or DOM element handle where the canvas/converted text will be placed.
* @return {void}
*/
var redraw= function(inTabs, outElement) {
// validate inTabs input...
// TODO: instead of this if it's text pop the entire processing back to loadBlocks!
inTabs = (typeof(inTabs) == 'string') ? (inTabs.split('\n')) : inTabs;
if (inTabs.length < NUM_STRINGS) {
return;
}
// read tabs
var tabInfo = readTabs(inTabs);
var labelOffset = (tabInfo.hasLabels) ? tab_settings.labelWidth : 0;
var tabs = tabInfo.tabs;
// how much space?
var height = ((NUM_STRINGS - 1) * tab_settings.lineSpacing) + (2 * tab_settings.dotRadius) + tab_settings.bottomPadding;
// prep canvas
outElement = (typeof(outElement) == 'string') ? document.getElementById(outElement) : outElement;
var ctx = ukeGeeks.canvasTools.addCanvas(outElement, getWidth(tabs, labelOffset, false), height);
var pos = {
x: tab_settings.dotRadius + labelOffset,
y: 1 + tab_settings.dotRadius
};
var lineWidth = getWidth(tabs, labelOffset, true);
drawStaff(ctx, pos, lineWidth, tab_settings);
drawNotes(ctx, pos, tabs, tab_settings, lineWidth);
if (tabInfo.hasLabels) {
drawLabels(ctx, pos, tab_settings);
}
};
/**
* This is insanely long, insanely kludgy, but, insanely, it works. This will read break a block of text into
* four lines (the ukulele strings), then find which frets are used by each. Then, the hard part, pack un-needed
* dashes. Once it's done that a 2-dimentional array (strings X frets) is created and returned.
* @method readTabs
* @private
* @param ukeStrings {array<string>} Block of tablbabure to be parsed
* @return {2-dimentional array}
*/
var readTabs= function(ukeStrings) {
var hasLabels = ukeStrings[NUM_STRINGS - 1][0] == LAST_STRING_NAME;
if (hasLabels) {
stripStringLabels(ukeStrings);
}
var frets = getFretNumbers(ukeStrings);
var symbols = getSymbols(ukeStrings);
var minLength = getMinLineLength(ukeStrings);
var guide = getGuideLine(symbols, minLength);
return {
tabs: getPackedLines(frets, symbols, guide, minLength),
hasLabels: hasLabels
};
};
/**
* @method getWidth
* @private
* @param tabs {2Darray}
* @param labelOffset {int}
* @param isTruncate {bool} If TRUE returns the length of the line, allowing for a terminating "|" character, othwrwise, it's for canvas width
* @return {int}
*/
var getWidth= function(tabs, labelOffset, isTruncate) {
if (!isTruncate) {
return (tab_settings.noteSpacing * tabs[0].length) + labelOffset + tab_settings.dotRadius;
}
var len = tabs[0].length;
var plusDot = tab_settings.dotRadius;
if (tabs[0][len - 1] == '|') {
// TODO: too much??? retest
len -= 1;
plusDot = 0;
}
return tab_settings.noteSpacing * len + labelOffset + plusDot;
};
/**
* Processes ukeStrings stripping the first character from each line
* @method stripStringLabels
* @private
* @param ukeStrings {array<string>}
* @return {void}
*/
var stripStringLabels= function(ukeStrings) {
for (var i = 0; i < NUM_STRINGS; i++) {
ukeStrings[i] = ukeStrings[i].substr(1);
}
};
/**
* Finds the frets in used for each line. In other words, ignoring
* spacers ("-" or "|") this returns arrays of numbers, the frets
* in use, for each line.
* @method getFretNumbers
* @private
* @param ukeStrings {array<string>}
* @return {void}
*/
var getFretNumbers= function(ukeStrings) {
// first, get the frets
var reInts = /([0-9]+)/g;
var frets = [];
for (var i = 0; i < NUM_STRINGS; i++) {
frets[i] = ukeStrings[i].match(reInts);
}
return frets;
};
/**
* Returns array of the strings with placeholders instead of the numbers.
* This helps us pack because "12" and "7" now occupy the same space horizontally.
* @method getSymbols
* @private
* @param ukeStrings {array<string>}
* @return {void}
*/
var getSymbols= function(ukeStrings) {
// convert to symbols
var reDoubles = /([0-9]{2})/g;
var reSingle = /([0-9])/g;
var symbols = [];
// TODO: verify why using NUM_STRINGS instead of ukeStrings.length (appears in other methods, again, do you recall why?)
for (var i = 0; i < NUM_STRINGS; i++) {
symbols[i] = ukeStrings[i].replace(reDoubles, '-*');
symbols[i] = symbols[i].replace(reSingle, '*');
}
return symbols;
};
/**
* Run through all of the strings (array) and return the length of the shortest one.
* would prefer the max length, but then I'd need to pad the shorter ones and ... well, it's complicated.
* this gets a TODO: get max!
* @method getMinLineLength
* @private
* @param ukeStrings {array<string>}
* @return {void}
*/
var getMinLineLength = function(ukeStrings){
var minLength = 0;
var line;
var re = /-+$/gi;
for (var i = 0; i < ukeStrings.length; i++) {
line = ukeStrings[i].trim().replace(re, '');
if (line.length > minLength){
minLength = line.length;
}
}
return minLength;
};
/**
* OK, having created symbolic representations for the lines in earlier steps
* here we go through and "merge" them into a single, master "guide" -- saying
* "somewhere on this beat you'll pluck (or not) one note". This normalized
* guide will be the master for the next step.
* @method getGuideLine
* @private
* @param symbols {undefined}
* @param minLength {int}
* @return {void}
*/
var getGuideLine= function(symbols, minLength) {
// Build a master pattern "guide" and eliminate double dashes
var guide = '';
for (var i = 0; i < minLength; i++) {
if (symbols[0][i] == '|') {
guide += '|';
}
else {
// TODO: assumes 4 strings, use NUM_STRINGS
guide += ((symbols[0][i] == '*') || (symbols[1][i] == '*') || (symbols[2][i] == '*') || (symbols[3][i] == '*')) ? '*' : '-';
}
}
var reDash = /--/g;
guide = guide.replace(reDash, '- ');
reDash = / -/g;
var lastGuide = guide;
while (true) {
guide = guide.replace(reDash, ' ');
if (guide == lastGuide) {
break;
}
lastGuide = guide;
}
return guide;
};
/**
* Using the packed "guide" line we loop over the strings, rebuilding each string
* with either a space, measure marker, or the note -- as an integer! Now the frets
* are the same regardless of whether they are single or double digit numbers:
* a "12" occupies no more horizontal space than a "5".
* @method getPackedLines
* @private
* @param frets {undefined}
* @param symbols {undefined}
* @param guide {undefined}
* @param minLength {int}
* @return {void}
*/
var getPackedLines= function(frets, symbols, guide, minLength) {
// pack it!
var packed = [],
chrNote = '', // a temp variable to hold the 'note'
guideIdx, // loop index for guide string
stringIdx, // loop index for instrument's strings (uke's 4)
lineIdx, // index to single line within packed array (along a string)
fretCount; // fret marker counter
for (stringIdx = 0; stringIdx < NUM_STRINGS; stringIdx++) {
packed.push([]);
}
for (stringIdx = 0; stringIdx < NUM_STRINGS; stringIdx++) { // loop over lines
lineIdx = 0;
fretCount = 0;
for (guideIdx = 0; guideIdx < minLength; guideIdx++) { // loop over guide
if (guide[guideIdx] != ' ') {
if (symbols[stringIdx][guideIdx] == '*') {
chrNote = frets[stringIdx][fretCount];
fretCount++;
}
else {
chrNote = ((guide[guideIdx] == '|')) ? '|' : '-';
}
packed[stringIdx][lineIdx] = chrNote;
lineIdx++;
}
}
}
return packed;
};
/**
* Create the staff -- really the four tablature strings
* @method drawStaff
* @private
* @param ctx {canvasContext} Handle to active canvas context
* @param pos {xyPos} JSON (x,y) position
* @param length {int} Length in pixels
* @param settings {settingsObj}
* @return {voie}
*/
var drawStaff= function(ctx, pos, length, settings) {
var offset = settings.lineWidth / 2;
var x = pos.x + offset;
var y = pos.y + offset;
ctx.beginPath();
for (var i = 0; i < NUM_STRINGS; i++) {
ctx.moveTo(x, y);
ctx.lineTo(x + length, y);
y += settings.lineSpacing;
}
ctx.strokeStyle = settings.lineColor;
ctx.lineWidth = settings.lineWidth;
ctx.stroke();
ctx.closePath();
};
/**
* Loop over the normalized tabs emitting the dots/fingers on the passed in canvase
* @method drawNotes
* @private
* @param ctx {canvasContext} Handle to active canvas context
* @param pos {xyPos} JSON (x,y) position
* @param tabs {array} Array of normalized string data -- space (character) or int (fret number)
* @param settings {settingsObj}
* @param lineWidth {int} Length in pixels (used only when line ends with a measure mark)
* @return {void}
*/
var drawNotes= function(ctx, pos, tabs, settings, lineWidth) {
var c;
var center = {
x: 0,
y: pos.y
};
for (var strIdx in tabs) {
if (strIdx > 3) {
return;
}
center.x = pos.x;
for (var chrIdx in tabs[strIdx]) {
c = tabs[strIdx][chrIdx];
// (c != '-'){
if (c == '|') {
var jnum = parseInt(chrIdx, 10);
var heavy = (((jnum + 1) < (tabs[strIdx].length - 1)) && (tabs[strIdx][jnum + 1] == '|')) || ((jnum == (tabs[strIdx].length - 1)) && (tabs[strIdx][jnum - 1] == '|'));
drawMeasure(ctx, {
x: (chrIdx == tabs[strIdx].length - 1) ? pos.x + lineWidth : center.x,
y: pos.y
}, settings, heavy);
}
else if (!isNaN(c)) {
ukeGeeks.canvasTools.drawDot(ctx, center, settings.dotRadius, settings.dotColor);
ukeGeeks.canvasTools.drawText(ctx, {
x: center.x,
y: (center.y + 0.5 * settings.dotRadius)
}, c, settings.textFont, settings.textColor);
}
center.x += settings.noteSpacing;
}
center.y += settings.lineSpacing;
}
};
/**
* Draws a vertical "measure" demarcation line
* @method drawMeasure
* @private
* @param ctx {canvasContext} Handle to active canvas context
* @param pos {xyPos} JSON (x,y) position
* @param settings {settingsObj}
* @param heavy {bool} if TRUE hevy line
* @return {void}
*/
var drawMeasure= function(ctx, pos, settings, heavy) {
var offset = settings.lineWidth / 2;
ctx.beginPath();
ctx.moveTo(pos.x + offset, pos.y);
ctx.lineTo(pos.x + offset, pos.y + (NUM_STRINGS - 1) * settings.lineSpacing);
ctx.strokeStyle = settings.lineColor;
ctx.lineWidth = (heavy ? 4.5 : 1) * settings.lineWidth;
ctx.stroke();
ctx.closePath();
};
/**
* Adds the string letters on the left-side of the canvas, before the tablature string lines
* @method drawLabels
* @private
* @param ctx {canvasContext} Handle to active canvas context
* @param pos {xyPos} JSON (x,y) position
* @param settings {settingsObj}
* @return {void}
*/
var drawLabels= function(ctx, pos, settings) {
// ['A','E','C','G'];
var labels = ukeGeeks.settings.tuning.slice(0).reverse();
for (var i = 0; i < NUM_STRINGS; i++) {
ukeGeeks.canvasTools.drawText(ctx, {
x: 1,
y: (pos.y + (i + 0.3) * settings.lineSpacing)
}, labels[i], settings.labelFont, settings.lineColor, 'left');
}
};
/* return our public interface */
return {
init: init,
replace: replace
};
};