You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

658 lines
22 KiB
Plaintext

/**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 : <a href="http://www.ampsoft.net/webdesign-l/WindowsMacFonts.html">http://www.ampsoft.net/webdesign-l/WindowsMacFonts.html</a> Arial, Verdana
* </p>
* @see /documents/specs/text.png
*
* @author Alex Gheroghiu <alex@scriptoid.com>
* @author Augustin <cdaugustin@yahoo.com>
* @author Artyom Pokatilov <artyom.pokatilov@gmail.com>
* <p/>
* Note:<br/>
* Canvas's metrics do not report updated width for rotated (context) text ...so we need to compute it
* <p/>
* Alignement note: <br/>
* It can be Center|Left|Right <a href="http://dev.w3.org/html5/2dcontext/#dom-context-2d-textalign">http://dev.w3.org/html5/2dcontext/#dom-context-2d-textalign</a>
**/
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 <alex@scriptoid.com>
*@author Janis Sejans <janis.sejans@towntech.lv>
*@author Artyom Pokatilov <artyom.pokatilov@gmail.com>
**/
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 <alex@scriptoid.com>
*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 <alex@scriptoid.com>
**/
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 <cdaugustin@yahoo.com>
**/
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 <cdaugustin@yahoo.com>
**/
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 <cdaugustin@yahoo.com>
*@author Alex <alex@scriptoid.com>
*@author Artyom <artyom.pokatilov@gmail.com>
**/
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 <artyom.pokatilov@gmail.com>
**/
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<Number>} - 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 <alex@scriptoid.com>
**/
escapeString: function (s) {
var result = new String(s);
var map = [];
map.push(['<', '&lt;']);
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 <a href="http://www.w3.org/TR/SVG/text.html">http://www.w3.org/TR/SVG/text.html</a>
*@see <a href="http://tutorials.jenkov.com/svg/text-element.html">http://tutorials.jenkov.com/svg/text-element.html</a> for rotation
*@see <a href="http://tutorials.jenkov.com/svg/tspan-element.html">http://tutorials.jenkov.com/svg/tspan-element.html</a> for tspan
*@see <a href="http://tutorials.jenkov.com/svg/svg-transformation.html">http://tutorials.jenkov.com/svg/svg-transformation.html</a> for detailed rotation
*@see <a href="http://www.w3.org/TR/SVG/coords.html#TransformAttribute">http://www.w3.org/TR/SVG/coords.html#TransformAttribute</a> for a very detailed rotation documentation
*<p/>
*@see Also read /documents/specs/text.png
*@author Alex <alex@scriptoid.com>
*
*
* Note:
* The position of the text is determined by the x and y attributes of the <text> 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:
<text x="200" y="150" fill="blue" style="stroke:none; fill:#000000;text-anchor: middle" transform="rotate(30 200,150)">
You are not a banana.
</text>
*/
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) + '<text y="' + (this.vector[0].y - height / 2) + '" ';
result += ' transform="rotate(' + angle + ' ' + this.vector[0].x + ' ,' + this.vector[0].y + ')" ';
result += ' font-family="' + this.font + '" ';
result += ' font-size="' + this.size + '" ';
result += ' font-weight="bold" ';
/**
*We will extract only the fill properties from Style, also we will not use
*Note: The outline color of the font. By default text only has fill color, not stroke.
*Adding stroke will make the font appear bold.*/
if (this.style.fillStyle != null) {
// result += ' stroke=" ' + this.style.fillStyle + '" ';
result += ' fill=" ' + this.style.fillStyle + '" ';
}
result += ' text-anchor="' + alignment + '" ';
result += '>';
INDENTATION++;
//any line of text (tspan tags)
var lines = this.str.split("\n");
for (var i = 0; i < lines.length; i++) {
var dy = parseFloat(this.size);
if (i > 0) {
dy += parseFloat(this.lineSpacing);
}
//alert('Size: ' + this.size + ' ' + (typeof this.size) + ' lineSpacing:' + this.lineSpacing + ' dy: ' + dy);
result += "\n" + repeat("\t", INDENTATION) + '<tspan x="' + (this.vector[0].x + offsetX) + '" dy="' + dy + '">' + this.escapeString(lines[i]) + '</tspan>'
} //end for
INDENTATION--;
//result += this.str;
result += "\n" + repeat("\t", INDENTATION) + '</text>';
if (this.debug) {
result += "\n" + repeat("\t", INDENTATION) + '<circle cx="' + this.vector[0].x + '" cy="' + this.vector[0].y + '" r="3" style="stroke: #FF0000; fill: yellow;" '
+ ' transform="rotate(' + angle + ' ' + this.vector[0].x + ' ,' + this.vector[0].y + ')" '
+ '/>';
result += "\n" + repeat("\t", INDENTATION) + '<circle cx="' + this.vector[0].x + '" cy="' + (this.vector[0].y - height / 2) + '" r="3" style="stroke: #FF0000; fill: green;" '
+ ' transform="rotate(' + angle + ' ' + this.vector[0].x + ' ,' + this.vector[0].y + ')" '
+ ' />';
}
return result;
}
}