1 // Copyright 2010 Scriptoid s.r.l 2 3 4 /**A simple text class to render text on a HTML 5 canvas 5 * Right now all the texts are horizontaly and verticaly centered so the (x,y) is the center of the text 6 * 7 * @this {Text} 8 * @constructor 9 * @param {String} string - the text to display 10 * @param {Number} x - the x pos 11 * @param {Number} y - the y pos 12 * @param {String} font - the font we are using for this text 13 * @param {Number} size - the size of the font 14 * 15 * @param {Boolean} outsideCanvas - set this on true if you want to use the Text outside of canvas (ex: Export to SVG) 16 * @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 17 * </p> 18 * @see /documents/specs/text.png 19 * 20 * @author Alex Gheroghiu <alex@scriptoid.com> 21 * @author Augustin <cdaugustin@yahoo.com> 22 * <p/> 23 * Note:<br/> 24 * Canvas's metrics do not report updated width for rotated (context) text ...so we need to compute it 25 * All texts will be center aligned...for now 26 * <p/> 27 * Alignement note: <br/> 28 * 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> 29 **/ 30 function Text(string, x, y, font, size, outsideCanvas){ 31 /**Text used to display*/ 32 this.str = string; 33 34 /**Font used to draw text*/ 35 this.font = font; 36 37 /**Size of the text*/ 38 this.size = size; //TODO:Builder set this as String which is bad habit 39 40 /**Line spacing. It should be a percent of the font size so it will grow with the font*/ 41 this.lineSpacing = 1 / 4 * size; 42 43 /**Horizontal alignement of the text, can be: left, center, right*/ 44 this.align = Text.ALIGN_CENTER; 45 46 /**Vertical alignement of the text - for now always middle*/ 47 this.valign = Text.VALIGN_MIDDLE; 48 49 /**We will keep the initial point (as base line) and another point just above it - similar to a vector. 50 *So when the text is transformed we will only transform the vector and get the new angle (if needed) 51 *from it*/ 52 this.vector = [new Point(x,y),new Point(x,y-20)]; 53 54 /**Style of the text*/ 55 this.style = new Style(); 56 57 if(!outsideCanvas){ 58 this.bounds = this.getNormalBounds(); 59 } 60 61 /**Used to display visual debug information*/ 62 this.debug = false; // 63 64 /*JSON object type used for JSON deserialization*/ 65 this.oType = 'Text'; 66 } 67 68 /**Creates a new {Text} out of JSON parsed object 69 *@param {JSONObject} o - the JSON parsed object 70 *@return {Text} a newly constructed Text 71 *@author Alex Gheorghiu <alex@scriptoid.com> 72 **/ 73 Text.load = function(o){ 74 //TODO: update 75 var newText = new Text(o.str, o.x, o.y, o.font, o.size); //fake Text (if we do not use it we got errors - endless loop) 76 //x loaded by contructor 77 //y loaded by contructor 78 //size loaded by contructor 79 //font loaded by contructor 80 //newText.lineSpacing = o.lineSpacing; //automatic computed from text size 81 newText.vector = Point.loadArray(o.vector); 82 newText.style = Style.load(o.style); 83 84 return newText; 85 } 86 87 /**Left alignment*/ 88 Text.ALIGN_LEFT = "left"; 89 90 /**Center alignment*/ 91 Text.ALIGN_CENTER = "center"; 92 93 /**Right alignment*/ 94 Text.ALIGN_RIGHT = "right"; 95 96 /**An {Array} with horizontal alignments*/ 97 Text.ALIGNMENTS = [{ 98 Value: Text.ALIGN_LEFT, 99 Text:'Left' 100 },{ 101 Value: Text.ALIGN_CENTER, 102 Text:'Center' 103 },{ 104 Value: Text.ALIGN_RIGHT, 105 Text:'Right' 106 }]; 107 108 /**Top alignment*/ 109 Text.VALIGN_TOP = "top"; 110 111 /**Middle alignment*/ 112 Text.VALIGN_MIDDLE = "middle"; 113 114 /**Bottom alignment*/ 115 Text.VALIGN_BOTTOM = "bottom"; 116 117 /**An {Array} of vertical alignments*/ 118 Text.VALIGNMENTS = [{ 119 Value: Text.VALIGN_TOP, 120 Text:'Top' 121 },{ 122 Value: Text.VALIGN_MIDDLE, 123 Text:'Middle' 124 },{ 125 Value: Text.VALIGN_BOTTOM, 126 Text:'Bottom' 127 }]; 128 129 /**An {Array} of fonts*/ 130 Text.FONTS = [{ 131 Value: "arial", 132 Text: "Arial" 133 },{ 134 Value: "arial narrow", 135 Text: "Arial Narrow" 136 },{ 137 Value: "courier new", 138 Text: "Courier New" 139 },{ 140 Value: "tahoma", 141 Text: "Tahoma" 142 }]; 143 144 /**space between 2 caracters*/ 145 Text.SPACE_BETWEEN_CHARACTERS = 2; 146 147 /**The default size of the created font*/ 148 Text.DEFAULT_SIZE = 10; 149 150 151 Text.prototype = { 152 153 gettextSize:function(){ 154 return this.size; 155 }, 156 157 //we need to transform the connectionpoints when we change the size of the text 158 //only used by the builder, for text figures (not figures with text) 159 settextSize:function(text, size){ 160 var oldBounds = this.getNormalBounds().getBounds(); 161 var oldSize = this.size; 162 this.size = size; 163 var newBounds = this.getNormalBounds().getBounds(); 164 // this._updateConnectionPoints(oldBounds, newBounds); 165 }, 166 167 gettext:function(){ 168 return this.str; 169 }, 170 171 settext:function(text, str){ 172 var oldBounds = this.getNormalBounds().getBounds(); 173 this.str = str; 174 var newBounds = this.getNormalBounds().getBounds(); 175 // this._updateConnectionPoints(oldBounds, newBounds); 176 177 }, 178 179 // TODO: This should never happen in Text 180 // /* 181 // *update the connection points when we change the text's size or 182 // *@param {Array} oldBounds - the bounds of the primitive prior to action 183 // *@param {Array} newBounds - the bounds of the primitive after action 184 // */ 185 // _updateConnectionPoints:function(oldBounds, newBounds){ 186 // var oldWidth = oldBounds[2] - oldBounds[0]; 187 // var oldHeight = oldBounds[3] - oldBounds[1]; 188 // 189 // var newWidth = newBounds[2] - newBounds[0]; 190 // var newHeight = newBounds[3] - newBounds[1]; 191 // 192 // var x = oldBounds[0]; 193 // var y = oldBounds[1]; 194 // 195 // var figure = stack.figureGetById(selectedFigureId); 196 // figure.transform(Matrix.translationMatrix(-x, -y)); 197 // 198 // figure.transform(Matrix.scaleMatrix(1 / oldWidth * newWidth, 1 / oldHeight * newHeight)); 199 // 200 // figure.transform(Matrix.translationMatrix(x, y)); 201 // 202 // }, 203 204 /** 205 *Get a refence to a context (main, in our case) 206 *Use this method when you need access to metrics. 207 *@author Alex Gheorghiu <alex@scriptoid.com> 208 *TODO:later will move this or use external functions 209 **/ 210 _getContext:function(){ 211 //WE SHOULD NOT KEEP ANY REFERENCE TO A CONTEXT - serialization pain 212 return document.getElementById("a").getContext("2d"); 213 }, 214 215 216 /**Transform the Text 217 *Upon transformation the vector is tranformed but the text remains the same. 218 *Later we are gonna use the vector to determine the angle of the text 219 *@param {Matrix} matrix - the transformation matrix 220 *@author Alex Gheorghiu <alex@scriptoid.com> 221 **/ 222 transform:function(matrix){ 223 this.vector[0].transform(matrix); 224 this.vector[1].transform(matrix); 225 226 }, 227 228 229 /** 230 *Get the angle around the compas between the vector and North direction 231 *@return {Number} - the angle in radians 232 *@see /documentation/specs/angle_around_compass.jpg 233 *@author alex@scriptoid.com 234 **/ 235 getAngle: function(){ 236 return Util.getAngle(this.vector[0], this.vector[1]); 237 }, 238 239 240 /**Returns the width of the text in normal space (no rotation) 241 *We need to know the width of each line and then we return the maximum of all widths 242 *@author Augustin <cdaugustin@yahoo.com> 243 **/ 244 getNormalWidth:function(){ 245 var linesText = this.str.split("\n"); 246 var linesWidth = []; 247 var maxWidth = 0; 248 249 //store lines width 250 this._getContext().save(); 251 this._getContext().font = this.size + "px " + this.font; 252 for(var i in linesText){ 253 var metrics = this._getContext().measureText(linesText[i]); 254 linesWidth[i] = metrics.width; 255 } 256 this._getContext().restore(); 257 258 259 //find maximum width 260 for(i=0; i<linesWidth.length; i++){ 261 if(maxWidth < linesWidth[i]){ 262 maxWidth = linesWidth[i]; 263 } 264 } 265 266 return maxWidth; 267 }, 268 269 270 /**Approximates the height of the text in normal space (no rotation) 271 *It is based on the size of the font and the line spacing used. 272 *@author Augustin <cdaugustin@yahoo.com> 273 **/ 274 getNormalHeight:function(){ 275 var lines = this.str.split("\n"); 276 var nrLines = lines.length; 277 var totalHeight = 0; 278 279 if (nrLines > 0){ 280 totalHeight = nrLines * this.size //height added by lines of text 281 + (nrLines - 1) * this.lineSpacing; //height added by inter line spaces 282 } 283 284 return totalHeight; 285 }, 286 287 288 /**Paints the text 289 *@author Augustin <cdaugustin@yahoo.com> 290 *@author Alex <alex@scriptoid.com> 291 **/ 292 paint:function(context){ 293 294 context.save(); 295 296 var lines = this.str.split("\n"); 297 298 var noLinesTxt = 0; 299 var txtSizeHeight = this.size; 300 var txtSpaceLines = this.lineSpacing; 301 302 var txtOffsetY = txtSizeHeight + txtSpaceLines; 303 304 //X - offset 305 var offsetX = 0; 306 if(this.align == Text.ALIGN_LEFT){ 307 offsetX = -this.getNormalWidth()/2; 308 } 309 else if(this.align == Text.ALIGN_RIGHT){ 310 offsetX = this.getNormalWidth()/2; 311 } 312 313 //Y - offset 314 var offsetY = 0; 315 if(this.valign == Text.VALIGN_TOP){ 316 offsetY = -this.getNormalHeight(); 317 } 318 else if(this.valign == Text.VALIGN_BOTTOM){ 319 offsetY = this.getNormalHeight(); 320 } 321 322 var angle = Util.getAngle(this.vector[0],this.vector[1]); 323 //alert("Angle is + " + angle + ' point 0: ' + this.vector[0] + ' point 1: ' + this.vector[1]); 324 325 326 //visual debug :D 327 if(this.debug){ 328 //paint vector 329 context.beginPath(); 330 context.moveTo(this.vector[0].x,this.vector[0].y); 331 context.lineTo(this.vector[1].x,this.vector[1].y); 332 context.closePath(); 333 context.stroke(); 334 335 //normal bounds (RED) - as if the normal bounds rotated with the text 336 var nBounds = this.getNormalBounds(); 337 nBounds.transform( Matrix.translationMatrix(-this.vector[0].x,-this.vector[0].y) ); 338 nBounds.transform(Matrix.rotationMatrix(angle)); 339 nBounds.transform(Matrix.translationMatrix(this.vector[0].x,this.vector[0].y)); 340 nBounds.style.strokeStyle = "rgb(250, 34, 35);"; 341 //alert(this.bounds); 342 nBounds.paint(context); 343 344 //text bounds (GREEN) - the actually bounds after a rotation 345 context.save(); 346 context.strokeStyle = "rgb(30, 204, 35);"; 347 var v = nBounds.getBounds(); 348 context.beginPath(); 349 context.moveTo(v[0], v[1]); 350 context.lineTo(v[2], v[1]); 351 context.lineTo(v[2], v[3]); 352 context.lineTo(v[0], v[3]); 353 context.closePath(); 354 context.stroke(); 355 context.restore(); 356 } 357 context.translate(this.vector[0].x,this.vector[0].y); 358 context.rotate(angle); 359 context.translate(-this.vector[0].x, -this.vector[0].y); 360 361 //paint lines 362 363 context.fillStyle = this.style.fillStyle; 364 context.font = this.size + "px " + this.font; 365 context.textAlign = this.align; 366 for(var i=0; i<lines.length; i++){ 367 context.fillText(lines[i], this.vector[0].x+offsetX, 368 (this.vector[0].y - this.getNormalHeight() / 2 + (i+1) * this.size + i * this.lineSpacing)+offsetY); 369 //context.fillText(lines[i], this.vector[0].x, txtOffsetY * noLinesTxt); 370 //context.fillText(linesText[i], -this.vector[0].x, txtOffsetY * noLinesTxt); 371 noLinesTxt = noLinesTxt + 1; 372 } 373 374 375 context.restore(); 376 377 }, 378 379 380 /**Text should not add it's bounds to any figure...so the figure should 381 *ignore any bounds reported by text 382 *@return {Array<Number>} - returns [minX, minY, maxX, maxY] - bounds, where 383 * all points are in the bounds. 384 **/ 385 getBounds:function(){ 386 var angle = Util.getAngle(this.vector[0],this.vector[1]); 387 var nBounds = this.getNormalBounds(); 388 /*if(this.align == Text.ALIGN_LEFT){ 389 nBounds.transform(Matrix.translationMatrix(this.getNormalWidth()/2,0)); 390 } 391 if(this.align == Text.ALIGN_RIGHT){ 392 nBounds.transform(Matrix.translationMatrix(-this.getNormalWidth()/2,0)); 393 }*/ 394 nBounds.transform(Matrix.translationMatrix(-this.vector[0].x,-this.vector[0].y) ); 395 nBounds.transform(Matrix.rotationMatrix(angle)); 396 nBounds.transform(Matrix.translationMatrix(this.vector[0].x,this.vector[0].y)); 397 398 return nBounds.getBounds(); 399 }, 400 401 402 /**Returns the bounds the text might have if in normal space (not rotated) 403 *We will keep it as a Polygon 404 *@return {Polygon} - a 4 points Polygon 405 **/ 406 getNormalBounds:function(){ 407 var lines = this.str.split("\n"); 408 409 var poly = new Polygon(); 410 poly.addPoint(new Point(this.vector[0].x - this.getNormalWidth()/2 ,this.vector[0].y - this.getNormalHeight()/2)); 411 poly.addPoint(new Point(this.vector[0].x + this.getNormalWidth()/2 ,this.vector[0].y - this.getNormalHeight()/2)); 412 poly.addPoint(new Point(this.vector[0].x + this.getNormalWidth()/2 ,this.vector[0].y + this.getNormalHeight()/2)); 413 poly.addPoint(new Point(this.vector[0].x - this.getNormalWidth()/2 ,this.vector[0].y + this.getNormalHeight()/2)); 414 415 return poly; 416 }, 417 418 419 getPoints:function(){ 420 return []; 421 }, 422 423 424 contains: function(x,y){ 425 var angle = Util.getAngle(this.vector[0],this.vector[1]); 426 var nBounds = this.getNormalBounds(); 427 nBounds.transform( Matrix.translationMatrix(-this.vector[0].x,-this.vector[0].y) ); 428 nBounds.transform(Matrix.rotationMatrix(angle)); 429 nBounds.transform(Matrix.translationMatrix(this.vector[0].x,this.vector[0].y)); 430 431 return nBounds.contains(x,y); 432 }, 433 434 435 near:function(x, y, radius){ 436 var angle = Util.getAngle(this.vector[0],this.vector[1]); 437 var nBounds = this.getNormalBounds(); 438 nBounds.transform( Matrix.translationMatrix(-this.vector[0].x,-this.vector[0].y) ); 439 nBounds.transform(Matrix.rotationMatrix(angle)); 440 nBounds.transform(Matrix.translationMatrix(this.vector[0].x,this.vector[0].y)); 441 442 return nBounds.near(x,y, radius); 443 }, 444 445 446 equals:function(anotherText){ 447 if(!anotherText instanceof Text){ 448 return false; 449 } 450 451 if( 452 this.str != anotherText.str 453 || this.font != anotherText.font 454 || this.size != anotherText.size 455 || this.lineSpacing != anotherText.lineSpacing 456 || this.size != anotherText.size){ 457 return false; 458 } 459 460 461 for(var i=0; i<this.vector.length; i++){ 462 if(!this.vector[i].equals(anotherText.vector[i])){ 463 return false; 464 } 465 } 466 467 if(!this.style.equals(anotherText.style)){ 468 return false; 469 } 470 471 //TODO: compare styles too this.style = new Style(); 472 return true; 473 }, 474 475 476 clone: function(){ 477 throw 'Text:clone - not implemented'; 478 }, 479 480 481 toString:function(){ 482 return 'Text: ' + this.str + ' x:' + this.vector[0].x + ' y:' + this.vector[0].y; 483 }, 484 485 /**There are characters that must be escaped when exported to SVG 486 *Ex: < (less then) will cause SVG parser to fail 487 *@author Alex <alex@scriptoid.com> 488 **/ 489 escapeString:function(s){ 490 var result = new String(s); 491 492 var map = []; 493 map.push(['<','<']); 494 495 for(var i = 0; i<map.length; i++){ 496 result = result.replace(map[i][0], map[i][1]); 497 } 498 499 return result; 500 }, 501 502 /** 503 *Convert text to SVG representation 504 *@see <a href="http://www.w3.org/TR/SVG/text.html">http://www.w3.org/TR/SVG/text.html</a> 505 *@see <a href="http://tutorials.jenkov.com/svg/text-element.html">http://tutorials.jenkov.com/svg/text-element.html</a> for rotation 506 *@see <a href="http://tutorials.jenkov.com/svg/tspan-element.html">http://tutorials.jenkov.com/svg/tspan-element.html</a> for tspan 507 *@see <a href="http://tutorials.jenkov.com/svg/svg-transformation.html">http://tutorials.jenkov.com/svg/svg-transformation.html</a> for detailed rotation 508 *@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 509 *<p/> 510 *@see Also read /documents/specs/text.png 511 *@author Alex <alex@scriptoid.com> 512 * 513 * 514 * Note: 515 * The position of the text is determined by the x and y attributes of the <text> element. 516 * The x-attribute determines where to locate the left edge of the text (the start of the text). 517 * The y-attribute determines where to locate the bottom of the text (not the top). 518 * Thus, there is a difference between the y-position of a text and the y-position of lines, 519 * rectangles, or other shapes. 520 **/ 521 toSVG: function(){ 522 /*Example: 523 <text x="200" y="150" fill="blue" style="stroke:none; fill:#000000;text-anchor: middle" transform="rotate(30 200,150)"> 524 You are not a banana. 525 </text> 526 */ 527 528 var angle = this.getAngle() * 180 / Math.PI; 529 var height = this.getNormalHeight(); 530 531 //X - offset 532 var offsetX = 0; 533 var alignment = 'middle'; 534 if(this.align == Text.ALIGN_LEFT){ 535 offsetX = -this.getNormalWidth()/2; 536 alignment = 'start'; 537 } 538 else if(this.align == Text.ALIGN_RIGHT){ 539 offsetX = this.getNormalWidth()/2; 540 alignment = 'end'; 541 } 542 543 //svg alignment 544 // if(this.align) 545 546 //general text tag 547 var result = '<text y="' + (this.vector[0].y - height/2) + '" '; 548 result += ' transform="rotate(' + angle + ' ' + this.vector[0].x + ' ,' + this.vector[0].y + ')" '; 549 result += ' font-family="' + this.font + '" '; 550 result += ' font-size="' + this.size + '" '; 551 552 /** 553 *We will extract only the fill properties from Style, also we will not use 554 *Note: The outline color of the font. By default text only has fill color, not stroke. 555 *Adding stroke will make the font appear bold.*/ 556 if(this.style.fillStyle != null){ 557 // result += ' stroke=" ' + this.style.fillStyle + '" '; 558 result += ' fill=" ' + this.style.fillStyle + '" '; 559 } 560 result += ' text-anchor="' + alignment + '" '; 561 result += '>'; 562 563 //any line of text (tspan tags) 564 var lines = this.str.split("\n"); 565 for(var i=0; i< lines.length; i++){ 566 var dy = parseFloat(this.size); 567 if(i > 0){ 568 dy += parseFloat(this.lineSpacing); 569 } 570 571 //alert('Size: ' + this.size + ' ' + (typeof this.size) + ' lineSpacing:' + this.lineSpacing + ' dy: ' + dy); 572 result += '<tspan x="' + (this.vector[0].x + offsetX) + '" dy="' + dy + '">' + this.escapeString(lines[i]) + '</tspan>' 573 } //end for 574 575 //result += this.str; 576 result += '</text>'; 577 578 if(this.debug){ 579 result += '<circle cx="' + this.vector[0].x + '" cy="' + this.vector[0].y + '" r="3" style="stroke: #FF0000; fill: yellow;" ' 580 + ' transform="rotate(' + angle + ' ' + this.vector[0].x + ' ,' + this.vector[0].y + ')" ' 581 + '/>'; 582 583 result += '<circle cx="' + this.vector[0].x + '" cy="' + (this.vector[0].y - height/2) + '" r="3" style="stroke: #FF0000; fill: green;" ' 584 + ' transform="rotate(' + angle + ' ' + this.vector[0].x + ' ,' + this.vector[0].y + ')" ' 585 + ' />'; 586 } 587 588 return result; 589 590 } 591 592 } 593 594 595 596