/**
* Library for an HTML5 WYSIWYG editor to build ChordPro chord define tags.
* @module ugsChordBuilder
* @namespace ugsChordBuilder
* @main ugsChordBuilder
*/
var ugsChordBuilder = window.ugsChordBuilder || {};
/**
* Entities (data containers) shared between the class libraries. Private
* JSON objects used internally by a class are not included here.
* @class entities
* @namespace ugsChordBuilder
* @static
* @singleton
*/
ugsChordBuilder.entities = {
/**
* @class entities.BoundingBox
* @constructor
* @param {Position} pos Position (JSON) object
* @param {JSON} dimensions JSON Object of form: {width: {int}, height: {int}}
*/
BoundingBox: function(pos, dimensions) {
/**
* @property x
* @type {int}
*/
this.x = pos ? pos.x : 0;
/**
* @property y
* @type {int}
*/
this.y = pos ? pos.y : 0;
/**
* @property width
* @type {int}
*/
this.width = dimensions ? dimensions.width : 1;
/**
* @property height
* @type {int}
*/
this.height = dimensions ? dimensions.height : 1;
},
/**
* Describes a fingering Dot on the fretboard
* @class entities.Dot
* @constructor
* @param {int} string
* @param {int} fret
* @param {int} finger
*/
Dot: function(string, fret, finger) {
/**
* String number, on sporano (GCEA), G is 0th string, and so on
* @property string
* @type {int}
*/
this.string = string;
/**
* @property fret
* @type {int}
*/
this.fret = fret ? fret : 0;
/**
* @property finger
* @type {int}
*/
this.finger = finger ? finger : 0;
},
/**
* @class entities.Position
* @constructor
* @param {int} x
* @param {int} y
*/
Position: function(x, y) {
/**
* @property x
* @type {int}
*/
this.x = x ? x : 0;
/**
* @property y
* @type {int}
*/
this.y = y ? y : 0;
}
};
/**
* "Properties, Options, Preferences" such as fretboard size and colors; dot attributes, the cursors, fonts etc.
* @class settings
* @namespace ugsChordBuilder
* @static
* @final
* @singleton
*/
ugsChordBuilder.settings = (function() {
// "Revealing Module Pattern"
// dependencies:
var ents = ugsChordBuilder.entities;
//'Geneva, "Lucida Sans", "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, sans-serif';
/**
* San-serif font stack used when drawing text on Canvas.
* @property {String} FONT_STACK
* @final
* @constant
*/
var FONT_STACK = 'Arial, "Helvetica Neue", Helvetica, Verdana, sans-serif';
/**
* Fretboard upper left hand corner position (pseudo-constants)
* @method anchorPos
* @type {Position}
* @static
*/
var anchorPos = {
x: 75,
y: 75
};
var cursor = {
fillColor: 'rgba(220, 216, 73, 0.35)', // 'rgba(245, 127, 18, 0.3)',
strokeWidth: 1,
strokeColor: '#AAB444', // '#F57F12',
radius: 9,
imageUri: '/img/editor/hand-cursor.png'
};
var fretBoard = {
numFrets: 5,
maxFret: 16,
stringNames: ['G', 'C', 'E', 'A'],
strokeWidth: 4,
strokeColor: '#8F8569',
fretSpace: 35,
stringSpace: 30
};
var dot = {
fillColor: '#F68014',
radius: 11,
strokeWidth: 2,
strokeColor: '#D56333',
fontWeight: 'bold',
fontFamily: FONT_STACK,
fontSize: 16,
fontColor: '#ffffff'
};
var fretLabel = {
fontFamily: FONT_STACK,
fontSize: 28, // Pixels
color: '#6A6A63',
lightColor: '#EAEAE8' //D6D6D6' //A4A4A3'
};
var stringLabel = {
fontFamily: FONT_STACK,
fontSize: 34, // Pixels
color: '#DCD849' // #AAB444'//
};
var chord = {
nameMaxLength: 20
};
/**
* Dimensions of a single target
* @method targetDimensions
* @return {JSON} {width: ?, height: ? }
*/
var targetDimensions = function() {
return {
height: fretBoard.fretSpace,
width: fretBoard.stringSpace
};
};
/**
* Top left-hand corner where Targets begin positioning
* @method targetAnchorPos
* @return {postion}
*/
var targetAnchorPos = function() {
var dimensions = targetDimensions();
return new ents.Position(
anchorPos.x - 0.5 * dimensions.width,
anchorPos.y - dimensions.height - 0.2 * fretBoard.strokeWidth
);
};
/**
* re-centers the fretboard's anchor position
* @method centerFretboard
* @param {element} canvas
* @return {void}
*/
var centerAnchor = function(canvas) {
anchorPos.x = (0.5 * canvas.width) - (0.5 * (fretBoard.stringNames.length - 1) * fretBoard.stringSpace) - fretBoard.strokeWidth;
anchorPos.y = (0.5 * canvas.height) - (0.5 * fretBoard.numFrets * fretBoard.stringSpace);
};
return {
// Properties
anchorPos: anchorPos,
cursor: cursor,
fretBoard: fretBoard,
dot: dot,
fretLabel: fretLabel,
stringLabel: stringLabel,
chord: chord,
// Methods
targetDimensions: targetDimensions,
targetAnchorPos: targetAnchorPos,
centerAnchor: centerAnchor
};
}());
/**
* Tracks curor position relative to fretboard's hot (clickable) regions
* @class tracking
* @namespace ugsChordBuilder
* @static
* @singleton
*/
ugsChordBuilder.tracking = (function() {
// dependencies:
var ents = ugsChordBuilder.entities,
settings = ugsChordBuilder.settings;
/**
* attach public members to this object
* @property _public
* @type JsonObject
*/
var _public = {};
var targetBox = null;
var getTarget = function() {
if (targetBox) {
return targetBox;
}
var dimensions = settings.targetDimensions();
dimensions.width = dimensions.width * settings.fretBoard.stringNames.length;
dimensions.height = dimensions.height * (settings.fretBoard.numFrets + 1);
targetBox = new ents.BoundingBox(settings.targetAnchorPos(), dimensions);
return targetBox;
};
/**
* Returns TRUE if the two objects overlap
* @method collision
* @param {BoundingBox} object1
* @param {BoundingBox} object2
* @return {bool}
*/
var collision = function(object1, object2) {
return (object1.x < object2.x + object2.width) && (object1.x + object1.width > object2.x) && (object1.y < object2.y + object2.height) && (object1.y + object1.height > object2.y);
};
/**
* Converts position (x,y) to the fret
* @method toDot
* @param {position} pos
* @return {dot}
*/
_public.toDot = function(pos) {
var cursorBox = new ents.BoundingBox(pos);
var box = getTarget();
if (!collision(cursorBox, box)) {
return null;
}
var dimensions = settings.targetDimensions();
return new ents.Dot(
Math.floor((pos.x - box.x) / dimensions.width),
Math.floor((pos.y - box.y) / dimensions.height)
);
};
// ---------------------------------------
// return public interface
// ---------------------------------------
return _public;
}());
/**
* Did I overlook this or was it deliberate? Either case, the "fret" in the dot object is
* merely the fret in the visible diagram -- that is, a value between 0 and maxFrets, not
* the actual fret on the instrument... beware.
*
* Unless otherwise stated all "dot" parames are of type ugsChordBuilder.entities.dot
* @class fretDots
* @namespace ugsChordBuilder
* @static
* @singleton
*/
ugsChordBuilder.fretDots = (function() {
// dependencies:
var ents = ugsChordBuilder.entities,
anchor_pos = ugsChordBuilder.settings.anchorPos,
opts_board = ugsChordBuilder.settings.fretBoard,
opts_dot = ugsChordBuilder.settings.dot;
/**
* attach public members to this object
* @property _public
* @type JsonObject
*/
var _public = {};
// locals
var _dots = [];
_public.getDots = function() {
return _dots.slice();
};
_public.slide = function(numSteps) {
if (!inRange(numSteps)) {
return false;
}
for (var i = 0; i < _dots.length; i++) {
_dots[i].fret = _dots[i].fret + numSteps;
}
return true;
};
var inRange = function(numSteps) {
for (var i = 0; i < _dots.length; i++) {
if ((_dots[i].fret + numSteps < 1) || (_dots[i].fret + numSteps > opts_board.numFrets)) {
return false;
}
}
return true;
};
_public.toggleDot = function(dot) {
if (dot.fret == 0) {
clearColumn(dot.string);
return;
}
var index = find(dot);
if (index < 0) {
_dots.push(dot);
}
else {
_dots.splice(index, 1);
}
};
_public.toggleFinger = function(dot, finger) {
var index = find(dot);
if (index < 0) {
return false;
}
_dots[index].finger = _dots[index].finger == finger ? 0 : finger;
return true;
};
/**
* Clears all saved dots.
* @method reset
*/
_public.reset = function() {
for (var i = 0; i < opts_board.stringNames.length; i++) {
clearColumn(i);
}
};
/**
* Returns index of Dot within _dots or -1 if not found.
* @method find
* @param {entities.dot} dot
* @return {int}
*/
var find = function(dot) {
for (var i = _dots.length - 1; i >= 0; i--) {
if (_dots[i].string == dot.string && _dots[i].fret == dot.fret) {
return i;
}
}
return -1;
};
/**
* Clears all dots for a particular string.
* @method clearColumn
* @param string {int}
*/
var clearColumn = function(string) {
for (var i = _dots.length - 1; i >= 0; i--) {
if (_dots[i].string == string) {
_dots.splice(i, 1);
}
}
};
var getPosition = function(dot) {
return new ents.Position(
anchor_pos.x + 0.47 * opts_board.strokeWidth + dot.string * opts_board.stringSpace,
anchor_pos.y + 0.47 * opts_board.strokeWidth + (dot.fret - 0.5) * opts_board.fretSpace
);
};
var drawDot = function(context, pos) {
context.beginPath();
context.arc(pos.x, pos.y, opts_dot.radius, 0, 2 * Math.PI, false);
context.fillStyle = opts_dot.fillColor;
context.fill();
context.lineWidth = opts_dot.strokeWidth;
context.strokeStyle = opts_dot.strokeColor;
context.stroke();
};
var addLabel = function(context, pos, text) {
context.font = opts_dot.fontWeight + ' ' + opts_dot.fontSize + 'px ' + opts_dot.fontFamily;
context.textAlign = 'center';
context.fillStyle = opts_dot.fontColor;
context.fillText(text, pos.x, pos.y + 0.3 * opts_dot.fontSize);
};
_public.draw = function(context) {
for (var i = _dots.length - 1; i >= 0; i--) {
var pos = getPosition(_dots[i]);
drawDot(context, pos);
if (_dots[i].finger > 0) {
addLabel(context, pos, _dots[i].finger);
}
}
};
// ---------------------------------------
// return public interface
// ---------------------------------------
return _public;
}());
/**
* Plots cursor moving across its own canvas context.
* @class cursorCanvas
* @namespace ugsChordBuilder
* @static
* @singleton
*/
ugsChordBuilder.cursorCanvas = (function() {
// dependencies
var opts_cursor = ugsChordBuilder.settings.cursor,
opts_dot = ugsChordBuilder.settings.dot;
/**
* attach public members to this object
* @property _public
* @type JsonObject
*/
var _public = {};
var _context = null;
var _handImage = null;
var _imgOk = false;
var _dotCursor = true;
var _finger = 1;
var _lastPos = {
x: 0,
y: 0
};
_public.init = function(ctx) {
_context = ctx;
loadImage();
};
var erase = function(pos) {
var radius = opts_cursor.radius + opts_cursor.strokeWidth;
// Need to allow for dot, image, and the finger number -- magic number for now:
_context.clearRect(pos.x - radius, pos.y - radius, radius + 50, radius + 60);
/*
if (_imgOk) {
_context.clearRect(pos.x - radius, pos.y - radius, radius + _handImage.width, radius + _handImage.height);
} else {
_context.clearRect(pos.x - radius, pos.y - radius, 2 * radius, 2 * radius);
}
*/
};
var drawHandCursor = function(pos) {
_context.drawImage(_handImage, pos.x, pos.y);
_context.font = opts_dot.fontWeight + ' ' + opts_dot.fontSize + 'px ' + opts_dot.fontFamily;
_context.textAlign = 'left';
_context.fillStyle = 'black'; //opts_dot.fontColor;
_context.fillText(_finger, pos.x + 0.8 * _handImage.width, pos.y + _handImage.height);
// not centering pos.x - 0.5 * _handImage.width, pos.y - 0.5 * _handImage.height);
};
var loadImage = function() {
_handImage = new Image();
_handImage.onload = function() {
_imgOk = true;
};
_handImage.src = opts_cursor.imageUri;
};
var drawDotCursor = function(pos) {
_context.beginPath();
_context.arc(pos.x, pos.y, opts_cursor.radius, 0, 2 * Math.PI, false);
_context.fillStyle = opts_cursor.fillColor;
_context.fill();
_context.lineWidth = opts_cursor.strokeWidth;
_context.strokeStyle = opts_cursor.strokeColor;
_context.stroke();
};
_public.setCursor = function(isDot, finger) {
_dotCursor = isDot;
_finger = finger;
};
_public.draw = function(pos) {
erase(_lastPos);
if (!_imgOk || _dotCursor) {
drawDotCursor(pos);
}
else {
drawHandCursor(pos);
}
_lastPos = pos;
};
// ---------------------------------------
// return public interface
// ---------------------------------------
return _public;
}());
/**
* Plots chord diagram (fretboard with fret labels) on its canvas context.
* @class chordCanvas
* @namespace ugsChordBuilder
* @static
* @singleton
*/
ugsChordBuilder.chordCanvas = (function() {
// dependencies
var ents = ugsChordBuilder.entities,
center_anchor = ugsChordBuilder.settings.centerAnchor,
anchor_pos = ugsChordBuilder.settings.anchorPos,
opt_fLabel = ugsChordBuilder.settings.fretLabel,
opt_sLabel = ugsChordBuilder.settings.stringLabel,
opts_board = ugsChordBuilder.settings.fretBoard;
/**
* attach public members to this object
* @property _public
* @type JsonObject
*/
var _public = {};
var _context = null,
_canvas = null;
_public.init = function(ctx, ele) {
_context = ctx;
_canvas = ele;
center_anchor(_canvas);
};
var erase = function() {
_context.clearRect(0, 0, _canvas.width, _canvas.height);
};
var addLabel = function(text, color, pos) {
_context.font = opt_fLabel.fontSize + 'px ' + opt_fLabel.fontFamily;
_context.textAlign = 'right';
_context.fillStyle = color;
_context.fillText(text, pos.x, pos.y);
};
var addLabels = function(startingFret) {
var pos = new ents.Position(
anchor_pos.x - 0.3 * opt_fLabel.fontSize,
anchor_pos.y + opt_fLabel.fontSize
);
var color = startingFret > 1 ? opt_fLabel.color : opt_fLabel.lightColor;
for (var i = 0; i < opts_board.numFrets; i++) {
addLabel(startingFret + i, color, pos);
pos.y += opts_board.fretSpace;
color = opt_fLabel.lightColor;
}
};
var addStringName = function(text, pos) {
_context.font = opt_sLabel.fontSize + 'px ' + opt_sLabel.fontFamily;
_context.textAlign = 'center';
_context.fillStyle = opt_sLabel.color;
_context.fillText(text, pos.x, pos.y);
};
var addStringNames = function() {
var pos = new ents.Position(
anchor_pos.x + 0.5 * opts_board.strokeWidth,
anchor_pos.y - 0.25 * opt_fLabel.fontSize
);
for (var i = 0; i < opts_board.stringNames.length; i++) {
addStringName(opts_board.stringNames[i], pos);
pos.x += opts_board.stringSpace;
}
};
var drawFretboard = function() {
var i, x, y;
// width offset, a "subpixel" adjustment
var offset = opts_board.strokeWidth / 2;
// locals
var stringHeight = opts_board.numFrets * opts_board.fretSpace;
var fretWidth = (opts_board.stringNames.length - 1) * opts_board.stringSpace;
// build shape
_context.beginPath();
// add "C" & "E" strings
for (i = 1; i < (opts_board.stringNames.length - 1); i++) {
x = anchor_pos.x + i * opts_board.stringSpace + offset;
_context.moveTo(x, anchor_pos.y + offset);
_context.lineTo(x, anchor_pos.y + stringHeight + offset);
}
// add frets
for (i = 1; i < opts_board.numFrets; i++) {
y = anchor_pos.y + i * opts_board.fretSpace + offset;
_context.moveTo(anchor_pos.x + offset, y);
_context.lineTo(anchor_pos.x + fretWidth + offset, y);
}
//
_context.rect(anchor_pos.x + offset, anchor_pos.y + offset, fretWidth, stringHeight);
// stroke shape
_context.strokeStyle = opts_board.strokeColor;
_context.lineWidth = opts_board.strokeWidth;
_context.stroke();
_context.closePath();
};
_public.draw = function(pos, startingFret) {
erase();
// ugsChordBuilder.debugTargets.drawTargets(_context);
addLabels(startingFret);
addStringNames();
drawFretboard();
ugsChordBuilder.fretDots.draw(_context);
};
// ---------------------------------------
// return public interface
// ---------------------------------------
return _public;
}());
/**
*
* @class export
* @namespace ugsChordBuilder
* @static
* @singleton
*/
ugsChordBuilder.export = (function() {
// dependencies
var opts_board = ugsChordBuilder.settings.fretBoard,
opts_chord = ugsChordBuilder.settings.chord;
/**
* attach public members to this object
* @property _public
* @type JsonObject
*/
var _public = {};
var _fretOffset = null;
/**
* Class for "reorganized" dots, think of this as a necklace where the
* thread, the instrument string, has zero or more beads, or dots -- fret plus finger
* @class StringDots
* @constructor
* @private
* @param {int} string
* @param {dot_Array} dots
*/
var StringDots = function(string, dots) {
this.string = string;
this.dots = dots ? dots : [];
//this.fingers = fingers ? fingers : [];
};
var getStringDots = function() {
// initialize empty string array
var stringNumber,
aryStringDots = [];
for (stringNumber = 1; stringNumber <= opts_board.stringNames.length; stringNumber++) {
aryStringDots.push(new StringDots(stringNumber));
}
// add dots
var dots = ugsChordBuilder.fretDots.getDots();
for (stringNumber = aryStringDots.length - 1; stringNumber >= 0; stringNumber--) {
for (var i = dots.length - 1; i >= 0; i--) {
if (aryStringDots[stringNumber].string == dots[i].string + 1) {
aryStringDots[stringNumber].dots.push(dots[i]);
}
}
}
return aryStringDots;
};
/**
* Returns the minimum & maximum fret found withing array (of dots)
* @method getMinMax
* @param {dot_array} ary
* @return {JSON}
*/
var getMinMax = function(ary) {
var max = 0;
var min = 900;
for (var i = ary.length - 1; i >= 0; i--) {
if (ary[i].fret > max) {
max = ary[i].fret;
}
if (ary[i].fret < min) {
min = ary[i].fret;
}
}
return {
max: max,
min: (min < 900) ? min : max
};
};
/**
* Handles the offset, translates from fret (on the diagram's N frets) to the insturment's complete fretbaord
* @method fretNumber
* @param {int} fret
* @return {int}
*/
var fretNumber = function(fret) {
return fret > 0 ? _fretOffset + fret : 0;
};
/**
* Not too surprisingly this finds "fret" within dots and returns finger. If there isn't a dot
* for fret returns zed.
* @method getFinger
* @param {array} dots
* @param {int} fret
* @return {int}
*/
var getFinger = function(dots, fret) {
for (var i = 0; i < dots.length; i++) {
if (dots[i].fret == fret) {
return dots[i].finger;
}
}
return 0;
};
/**
* Returns an array of ints, one for each string, with the HIGHEST REAL fret appearing on that string.
* Default is zed per string.
* @method getPrimaryFrets
* @param {int} startingFret
* @return {int}
*/
_public.getPrimaryFrets = function(startingFret) {
_fretOffset = startingFret - 1;
var dotsPerString = getStringDots();
var primaries = [];
for (var i = 0; i < dotsPerString.length; i++) {
var minMax = getMinMax(dotsPerString[i].dots);
primaries.push(fretNumber(minMax.max));
}
return primaries;
};
/**
* Returns complete ChordPro definition statement
* @method getDefinition
* @param {string} chordName
* @param {int} startingFret
* @return {string}
*/
_public.getDefinition = function(chordName, startingFret) {
chordName = scrub(chordName);
var name = (chordName && chordName.length > 0) ? chordName : 'CHORDNAME';
var fretsStr = '';
var fingersString = '';
var addsString = '';
_fretOffset = startingFret - 1;
var dotsPerString = getStringDots();
for (var i = 0; i < dotsPerString.length; i++) {
var minMax = getMinMax(dotsPerString[i].dots);
fretsStr += fretNumber(minMax.max) + ' ';
fingersString += getFinger(dotsPerString[i].dots, minMax.max) + ' ';
if (minMax.max != minMax.min) {
addsString += ' add: string ' + dotsPerString[i].string + ' fret ' + fretNumber(minMax.min) + ' finger ' + getFinger(dotsPerString[i].dots, minMax.min);
}
}
// no double spaces, no space before the closing "}"
return ('{define: ' + name + ' frets ' + fretsStr + ' fingers ' + fingersString + addsString + '}').replace(/\s+/g, ' ').replace(' }', '}');
};
/**
* Returns "highlighted" (HTML-ified) ChordPro definition statement.
* @method getDefinition
* @param {string} chordName
* @param {int} startingFret
* @return {string}
*/
_public.getDefinitionHtml = function(chordName, startingFret) {
chordName = scrub(chordName);
// Keep regexs simple by a couple cheats:
// First, using 'X07MX001' as my CSS clasname prefix to avoid collisions.
// We temporarily remove the name, then put it back in the very last step.
var html = _public.getDefinition(chordName, startingFret);
html = html.replace(' ' + chordName, ' ' + 'X07Myq791wso01');
html = html.replace(/\b(\d+)\b/g, '<span class="chordPro-X07MX001number">$1</span>');
html = html.replace(/\b(frets?|fingers?|string)\b/g, '<span class="chordPro-X07MX001attribute">$1</span>');
html = html.replace(/\b(define|add)\b/g, '<span class="chordPro-X07MX001keyword">$1</span>');
return html
.replace('X07Myq791wso01', '<span class="chordPro-string">' + chordName + '</span>')
.replace(/X07MX001/g, '')
.replace(/ +/g, ' ');
};
/**
* Returns "safe" version of chord name, removing disallowed characters and reserved names (such as "add:")
* @method scrub
* @param {string} chordName
* @return {string}
*/
var scrub = function(name) {
// paranoia protection: no reserved words (makes life easier for parsing)
var disallow = /^(frets|fingers|add:)$/i;
// trim leading & trailing spaces, internal spaces get smushed into single dash
var cleaned = name.replace(/^\s*(.*?)\s*$/, '$1').replace(/\s+/g, '-');
if (disallow.test(cleaned)) {
cleaned = '';
}
return cleaned.substring(0, opts_chord.nameMaxLength);
};
// ---------------------------------------
// return public interface
// ---------------------------------------
return _public;
}());