/**A simple text class to render text on a HTML 5 canvas * Right now all the texts are horizontaly and verticaly centered so the (x,y) is the center of the text * * @this {Text} * @constructor * @param {String} string - the text to display * @param {Number} x - the x pos * @param {Number} y - the y pos * @param {String} font - the font we are using for this text * @param {Number} size - the size of the font * * @param {Boolean} outsideCanvas - set this on true if you want to use the Text outside of canvas (ex: Export to SVG) * @param {String} align - the alignment we are using for this text * @see list of web safe fonts : http://www.ampsoft.net/webdesign-l/WindowsMacFonts.html Arial, Verdana *

* @see /documents/specs/text.png * * @author Alex Gheroghiu * @author Augustin * @author Artyom Pokatilov *

* Note:
* Canvas's metrics do not report updated width for rotated (context) text ...so we need to compute it *

* Alignement note:
* It can be Center|Left|Right http://dev.w3.org/html5/2dcontext/#dom-context-2d-textalign **/ function Text(string, x, y, font, size, outsideCanvas, align) { /**Text used to display*/ this.str = string; /**Font used to draw text*/ this.font = font; /**Size of the text*/ this.size = size; //TODO:Builder set this as String which is bad habit /**Line spacing. It should be a percent of the font size so it will grow with the font*/ this.lineSpacing = 1 / 4 * size; /**Horizontal alignment of the text, can be: left, center, right*/ this.align = align || Text.ALIGN_CENTER; /**Sets if text is underlined*/ this.underlined = false; /**Sets text fontweight*/ this.fontWeight = ""; /**Vertical alignment of the text - for now always middle*/ // this.valign = Text.VALIGN_MIDDLE; /**We will keep the initial point (as base line) and another point just above it - similar to a vector. *So when the text is transformed we will only transform the vector and get the new angle (if needed) *from it*/ this.vector = [new Point(x, y), new Point(x, y - 20)]; /**Style of the text*/ this.style = new Style(); if (!outsideCanvas) { this.bounds = this.getNormalBounds(); } /*JSON object type used for JSON deserialization*/ this.oType = 'Text'; } /**Creates a new {Text} out of JSON parsed object *@param {JSONObject} o - the JSON parsed object *@return {Text} a newly constructed Text *@author Alex Gheorghiu *@author Janis Sejans *@author Artyom Pokatilov **/ Text.load = function (o) { //TODO: update var newText = new Text(o.str, o.x, o.y, o.font, o.size, false, o.align); //fake Text (if we do not use it we got errors - endless loop) //x loaded by contructor //y loaded by contructor //size loaded by contructor //font loaded by contructor //newText.lineSpacing = o.lineSpacing; //automatic computed from text size //align loaded by contructor newText.underlined = o.underlined; newText.fontWeight = o.fontWeight == undefined ? "" : o.fontWeight; newText.vector = Point.loadArray(o.vector); newText.style = Style.load(o.style); return newText; } /**Left alignment*/ Text.ALIGN_LEFT = "left"; /**Center alignment*/ Text.ALIGN_CENTER = "center"; /**Right alignment*/ Text.ALIGN_RIGHT = "right"; /**An {Array} with horizontal alignments*/ Text.ALIGNMENTS = [{ Value: Text.ALIGN_LEFT, Text: 'Left' }, { Value: Text.ALIGN_CENTER, Text: 'Center' }, { Value: Text.ALIGN_RIGHT, Text: 'Right' }]; /* There is no point in vertical alignment for a single text. A text is vertically aligned to something external to it. (The only exception is Chinese where they write from top to bottom and in that case vertical alignment is similar to horizontal alignment in Latin alphabet). */ /**Top alignment*/ //Text.VALIGN_TOP = "top"; /**Middle alignment*/ //Text.VALIGN_MIDDLE = "middle"; /**Bottom alignment*/ //Text.VALIGN_BOTTOM = "bottom"; /**An {Array} of vertical alignments*/ //Text.VALIGNMENTS = [{ // Value: Text.VALIGN_TOP, // Text:'Top' //},{ // Value: Text.VALIGN_MIDDLE, // Text:'Middle' //},{ // Value: Text.VALIGN_BOTTOM, // Text:'Bottom' //}]; /**An {Array} of fonts*/ Text.FONTS = [{ Value: "arial", Text: "Arial" }, { Value: "arial narrow", Text: "Arial Narrow" }, { Value: "courier new", Text: "Courier New" }, { Value: "tahoma", Text: "Tahoma" }]; /**space between 2 caracters*/ Text.SPACE_BETWEEN_CHARACTERS = 2; /**The default size of the created font*/ Text.DEFAULT_SIZE = 10; /**Proportion between size of text and thickness of underline*/ Text.UNDERLINE_THICKNESS_DIVIDER = 16; Text.prototype = { constructor: Text, getTextSize: function () { return this.size; }, //we need to transform the connectionpoints when we change the size of the text //only used by the builder, for text figures (not figures with text) setTextSize: function (size) { var oldBounds = this.getNormalBounds().getBounds(); var oldSize = this.size; this.size = size; var newBounds = this.getNormalBounds().getBounds(); }, getTextStr: function () { return this.str; }, setTextStr: function (str) { var oldBounds = this.getNormalBounds().getBounds(); this.str = str; var newBounds = this.getNormalBounds().getBounds(); }, /** *Get a refence to a context (main, in our case) *Use this method when you need access to metrics. *@author Alex Gheorghiu *TODO:later will move this or use external functions **/ _getContext: function () { //WE SHOULD NOT KEEP ANY REFERENCE TO A CONTEXT - serialization pain return document.getElementById("a").getContext("2d"); }, /**Transform the Text *Upon transformation the vector is tranformed but the text remains the same. *Later we are gonna use the vector to determine the angle of the text *@param {Matrix} matrix - the transformation matrix *@author Alex Gheorghiu **/ transform: function (matrix) { this.vector[0].transform(matrix); this.vector[1].transform(matrix); }, /** *Get the angle around the compas between the vector and North direction *@return {Number} - the angle in radians *@see /documentation/specs/angle_around_compass.jpg *@author alex@scriptoid.com **/ getAngle: function () { return Util.getAngle(this.vector[0], this.vector[1]); }, /**Returns the width of the text in normal space (no rotation) *We need to know the width of each line and then we return the maximum of all widths *@author Augustin **/ getNormalWidth: function () { var linesText = this.str.split("\n"); var linesWidth = []; var maxWidth = 0; //store lines width this._getContext().save(); this._getContext().font = this.size + "px " + this.font; for (var i in linesText) { var metrics = this._getContext().measureText(linesText[i]); linesWidth[i] = metrics.width; } this._getContext().restore(); //find maximum width for (i = 0; i < linesWidth.length; i++) { if (maxWidth < linesWidth[i]) { maxWidth = linesWidth[i]; } } return maxWidth; }, /**Approximates the height of the text in normal space (no rotation) *It is based on the size of the font and the line spacing used. *@author Augustin **/ getNormalHeight: function () { var lines = this.str.split("\n"); var nrLines = lines.length; var totalHeight = 0; if (nrLines > 0) { totalHeight = nrLines * this.size //height added by lines of text + (nrLines - 1) * this.lineSpacing; //height added by inter line spaces } return totalHeight; }, /**Paints the text *@author Augustin *@author Alex *@author Artyom **/ paint: function (context) { context.save(); var lines = this.str.split("\n"); // var noLinesTxt = 0; // var txtSizeHeight = this.size; // update lineSpacing because it could be changed // in dynamic way and we do not watch it // TODO: reorganize by deleting lineSpacing at all or by adding get/set methods this.lineSpacing = 1 / 4 * this.size; // var txtSpaceLines = this.lineSpacing; // var txtOffsetY = txtSizeHeight + txtSpaceLines; //X - offset var offsetX = 0; if (this.align == Text.ALIGN_LEFT) { offsetX = -this.getNormalWidth() / 2; } else if (this.align == Text.ALIGN_RIGHT) { offsetX = this.getNormalWidth() / 2; } //Y - offset var offsetY = 0.5 * this.size; // switch(this.valign) { // case Text.VALIGN_TOP: // offsetY = -this.getNormalHeight(); // break; // // case Text.VALIGN_BOTTOM: // offsetY = this.getNormalHeight(); // break; // // case Text.VALIGN_MIDDLE: // offsetY = 0.5 * this.size; // break; // } var angle = Util.getAngle(this.vector[0], this.vector[1]); //alert("Angle is + " + angle + ' point 0: ' + this.vector[0] + ' point 1: ' + this.vector[1]); //visual debug :D context.translate(this.vector[0].x, this.vector[0].y); context.rotate(angle); context.translate(-this.vector[0].x, -this.vector[0].y); //paint lines context.fillStyle = this.style.fillStyle; context.font = this.fontWeight + " " + this.size + "px " + this.font; context.textAlign = this.align; context.textBaseline = "middle"; // if (this.valign == Text.VALIGN_MIDDLE) { // context.textBaseline = "middle"; // } for (var i = 0; i < lines.length; i++) { // x and y starting coordinates of text lines var lineStartX = this.vector[0].x + offsetX; var lineStartY = (this.vector[0].y - this.getNormalHeight() / 2 + i * this.size + i * this.lineSpacing) + offsetY; context.fillText( lines[i], lineStartX, lineStartY ); if (this.underlined) { this.paintUnderline( context, lines[i], lineStartX, lineStartY ); } //context.fillText(lines[i], this.vector[0].x, txtOffsetY * noLinesTxt); //context.fillText(linesText[i], -this.vector[0].x, txtOffsetY * noLinesTxt); // noLinesTxt = noLinesTxt + 1; } context.restore(); }, /**Paints underline for the text. * There is no native method of canvas context for now, so we're implementing it by ourselves. * Taken and refactored from http://scriptstock.wordpress.com/2012/06/12/html5-canvas-text-underline-workaround/ * @argument {CanvasRenderingContext2D} context - the Canvas's 2D context * @argument {String} text - the text to be underlined * @argument {Number} x - the X coordinates of the text * @argument {Number} y - the Y coordinated of the the text * @author Artyom **/ paintUnderline: function (context, text, x, y) { // text width var width = context.measureText(text).width; // if text align differs from "left" - add offset to X axis // fillText method of canvas context make this automatically switch (this.align) { case "center": x -= (width / 2); break; case "right": x -= width; break; } // add offset to Y axis equal to half of text size y += this.size / 2; context.save(); context.beginPath(); context.strokeStyle = this.style.fillStyle; // color the same as text context.lineWidth = this.size / Text.UNDERLINE_THICKNESS_DIVIDER; // thickness taken in proportion of text size context.moveTo(x, y); context.lineTo(x + width, y); context.stroke(); context.restore(); }, /**Text should not add it's bounds to any figure...so the figure should *ignore any bounds reported by text *@return {Array} - returns [minX, minY, maxX, maxY] - bounds, where * all points are in the bounds. **/ getBounds: function () { var angle = Util.getAngle(this.vector[0], this.vector[1]); var nBounds = this.getNormalBounds(); /*if(this.align == Text.ALIGN_LEFT){ nBounds.transform(Matrix.translationMatrix(this.getNormalWidth()/2,0)); } if(this.align == Text.ALIGN_RIGHT){ nBounds.transform(Matrix.translationMatrix(-this.getNormalWidth()/2,0)); }*/ nBounds.transform(Matrix.translationMatrix(-this.vector[0].x, -this.vector[0].y)); nBounds.transform(Matrix.rotationMatrix(angle)); nBounds.transform(Matrix.translationMatrix(this.vector[0].x, this.vector[0].y)); return nBounds.getBounds(); }, /**Returns the bounds the text might have if in normal space (not rotated) *We will keep it as a Polygon *@return {Polygon} - a 4 points Polygon **/ getNormalBounds: function () { var lines = this.str.split("\n"); var poly = new Polygon(); poly.addPoint(new Point(this.vector[0].x - this.getNormalWidth() / 2, this.vector[0].y - this.getNormalHeight() / 2)); poly.addPoint(new Point(this.vector[0].x + this.getNormalWidth() / 2, this.vector[0].y - this.getNormalHeight() / 2)); poly.addPoint(new Point(this.vector[0].x + this.getNormalWidth() / 2, this.vector[0].y + this.getNormalHeight() / 2)); poly.addPoint(new Point(this.vector[0].x - this.getNormalWidth() / 2, this.vector[0].y + this.getNormalHeight() / 2)); return poly; }, getPoints: function () { return []; }, contains: function (x, y) { var angle = Util.getAngle(this.vector[0], this.vector[1]); var nBounds = this.getNormalBounds(); nBounds.transform(Matrix.translationMatrix(-this.vector[0].x, -this.vector[0].y)); nBounds.transform(Matrix.rotationMatrix(angle)); nBounds.transform(Matrix.translationMatrix(this.vector[0].x, this.vector[0].y)); // check if (x,y) is inside or on a borders of nBounds return nBounds.contains(x, y, true); }, near: function (x, y, radius) { var angle = Util.getAngle(this.vector[0], this.vector[1]); var nBounds = this.getNormalBounds(); nBounds.transform(Matrix.translationMatrix(-this.vector[0].x, -this.vector[0].y)); nBounds.transform(Matrix.rotationMatrix(angle)); nBounds.transform(Matrix.translationMatrix(this.vector[0].x, this.vector[0].y)); return nBounds.near(x, y, radius); }, equals: function (anotherText) { if (!anotherText instanceof Text) { return false; } if ( this.str != anotherText.str || this.font != anotherText.font || this.size != anotherText.size || this.lineSpacing != anotherText.lineSpacing || this.size != anotherText.size) { return false; } for (var i = 0; i < this.vector.length; i++) { if (!this.vector[i].equals(anotherText.vector[i])) { return false; } } if (!this.style.equals(anotherText.style)) { return false; } //TODO: compare styles too this.style = new Style(); return true; }, /**Creates a clone of current text*/ clone: function () { var cText = new Text(this.str, this.x, this.y, this.font, this.size, this.outsideCanvas); cText.align = this.align; // cText.valign = this.valign; cText.vector = Point.cloneArray(this.vector); cText.style = this.style.clone(); if (!cText.outsideCanvas) { cText.bounds = this.bounds.clone(); //It's a Polygon (so we can clone it) } return cText; /* var newText = {}; for (i in this) { if (i == 'vector'){ newText[i] = Point.cloneArray(this[i]); continue; } if (this[i] && typeof this[i] == "object") { newText[i] = this[i].clone(); } else newText[i] = this[i] } return newText; */ }, toString: function () { return 'Text: ' + this.str + ' x:' + this.vector[0].x + ' y:' + this.vector[0].y; }, /**There are characters that must be escaped when exported to SVG *Ex: < (less then) will cause SVG parser to fail *@author Alex **/ escapeString: function (s) { var result = new String(s); var map = []; map.push(['<', '<']); for (var i = 0; i < map.length; i++) { result = result.replace(map[i][0], map[i][1]); } return result; }, /** *Convert text to SVG representation *@see http://www.w3.org/TR/SVG/text.html *@see http://tutorials.jenkov.com/svg/text-element.html for rotation *@see http://tutorials.jenkov.com/svg/tspan-element.html for tspan *@see http://tutorials.jenkov.com/svg/svg-transformation.html for detailed rotation *@see http://www.w3.org/TR/SVG/coords.html#TransformAttribute for a very detailed rotation documentation *

*@see Also read /documents/specs/text.png *@author Alex * * * Note: * The position of the text is determined by the x and y attributes of the element. * The x-attribute determines where to locate the left edge of the text (the start of the text). * The y-attribute determines where to locate the bottom of the text (not the top). * Thus, there is a difference between the y-position of a text and the y-position of lines, * rectangles, or other shapes. **/ toSVG: function () { /*Example: You are not a banana. */ var angle = this.getAngle() * 180 / Math.PI; var height = this.getNormalHeight(); //X - offset var offsetX = 0; var alignment = 'middle'; if (this.align == Text.ALIGN_LEFT) { offsetX = -this.getNormalWidth() / 2; alignment = 'start'; } else if (this.align == Text.ALIGN_RIGHT) { offsetX = this.getNormalWidth() / 2; alignment = 'end'; } //svg alignment // if(this.align) //general text tag var result = "\n" + repeat("\t", INDENTATION) + ' 0) { dy += parseFloat(this.lineSpacing); } //alert('Size: ' + this.size + ' ' + (typeof this.size) + ' lineSpacing:' + this.lineSpacing + ' dy: ' + dy); result += "\n" + repeat("\t", INDENTATION) + '' + this.escapeString(lines[i]) + '' } //end for INDENTATION--; //result += this.str; result += "\n" + repeat("\t", INDENTATION) + ''; if (this.debug) { result += "\n" + repeat("\t", INDENTATION) + ''; result += "\n" + repeat("\t", INDENTATION) + ''; } return result; } }