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