1 /**
  2  * @fileOverview A jaws.Text object with word-wrapping functionality.
  3  * @class jaws.Text
  4  * @property {integer}    x             Horizontal position  (0 = furthest left)
  5  * @property {integer}    y             Vertical position    (0 = top)
  6  * @property {number}     alpha         Transparency: 0 (fully transparent) to 1 (no transparency)
  7  * @property {number}     angle         Angle in degrees (0-360)
  8  * @property {string}     anchor        String stating how to anchor the sprite to canvas; @see Sprite#anchor
  9  * @property {string}     text          The actual text to be displayed 
 10  * @property {string}     fontFace      A valid font-family
 11  * @property {number}     fontSize      The size of the text in pixels
 12  * @property {string}     textAlign     "start", "end", "left", "right", or "center"
 13  * @property {string}     textBaseline  "top", "bottom", "hanging", "middle", "alphabetic", or "ideographic"
 14  * @property {number}     width         The width of the rect() containing the text
 15  * @property {number}     height        The height of the rect() containing the text
 16  * @property {string}     style         The style to draw the text: "normal", "bold" or italic"
 17  * @property {boolean}    wordWrap      If word-wrapping should be attempted
 18  * @property {string}     shadowColor   The color of the shadow for the text
 19  * @property {number}     shadowBlur    The amount of shadow blur (length away from text)
 20  * @property {number}     shadowOffsetX The start of the shadow from initial x
 21  * @property {number}     shadowOffsetY The start of the shadow from initial y
 22  * @example
 23  *  var text = new Text({text: "Hello world!", x: 10, y: 10}) 
 24  */
 25 
 26 var jaws = (function(jaws) {
 27 
 28   /**
 29    * jaws.Text constructor
 30    * @constructor
 31    * @param {object} options An object-literal collection of constructor values
 32    */
 33   jaws.Text = function(options) {
 34     if (!(this instanceof arguments.callee))
 35       return new arguments.callee(options);
 36 
 37     this.set(options);
 38 
 39     if (options.context) {
 40       this.context = options.context;
 41     }
 42    
 43     if (!options.context) { // Defaults to jaws.context
 44       if (jaws.context)
 45         this.context = jaws.context;
 46     }
 47   };
 48 
 49   /**
 50    * The default values of jaws.Text properties
 51    */
 52   jaws.Text.prototype.default_options = {
 53     x: 0,
 54     y: 0,
 55     alpha: 1,
 56     angle: 0,
 57     anchor_x: 0,
 58     anchor_y: 0,
 59     anchor: "top_left",
 60     damping: 1,
 61     style: "normal",
 62     fontFace: "serif",
 63     fontSize: 12,
 64     color: "black",
 65     textAlign: "start",
 66     textBaseline: "alphabetic",
 67     text: "",
 68     wordWrap: false,
 69     width: function(){ return jaws.width; },
 70     height: function() { return jaws.height; },
 71     shadowColor: null,
 72     shadowBlur: null,
 73     shadowOffsetX: null,
 74     shadowOffsetY: null,
 75     _constructor: null,
 76   };
 77 
 78   /**
 79    * Overrides constructor values with defaults
 80    * @this {jaws.Text}
 81    * @param {object} options An object-literal collection of constructor values
 82    * @returns {this}
 83    * @see jaws.parseOptions
 84    */
 85   jaws.Text.prototype.set = function(options) {
 86 
 87     jaws.parseOptions(this, options, this.default_options);
 88 
 89     if (this.anchor)
 90       this.setAnchor(this.anchor);
 91 
 92     this.cacheOffsets();
 93 
 94     return this;
 95   };
 96 
 97   /**
 98    * Returns a new instance based on the current jaws.Text object
 99    * @private
100    * @this {jaws.Text}
101    * @returns {object} The newly cloned object
102    */
103   jaws.Text.prototype.clone = function() {
104     var constructor = this._constructor ? eval(this._constructor) : this.constructor;
105     var new_sprite = new constructor(this.attributes());
106     new_sprite._constructor = this._constructor || this.constructor.name;
107     return new_sprite;
108   };
109 
110   /**
111    * Rotate sprite by value degrees
112    * @this {jaws.Text}
113    * @param {number} value The amount of the rotation
114    * @returns {this} Current function scope
115    */
116   jaws.Text.prototype.rotate = function(value) {
117     this.angle += value;
118     return this;
119   };
120 
121   /**
122    * Forces a rotation-angle on sprite
123    * @this {jaws.Text}
124    * @param {number} value The amount of the rotation
125    * @returns {this} Current function instance
126    */
127   jaws.Text.prototype.rotateTo = function(value) {
128     this.angle = value;
129     return this;
130   };
131 
132   /**
133    * Move object to position x, y
134    * @this {jaws.Text}
135    * @param {number} x The x position to move to
136    * @param {number} y The y position to move to
137    * @returns {this} Current function instance
138    */
139   jaws.Text.prototype.moveTo = function(x, y) {
140     this.x = x;
141     this.y = y;
142     return this;
143   };
144 
145   /**
146    * Modify x and/or y by a fixed amount
147    * @this {jaws.Text}
148    * @param {type} x The additional amount to move x
149    * @param {type} y The additional amount to move y
150    * @returns {this} Current function instance
151    */
152   jaws.Text.prototype.move = function(x, y) {
153     if (x)
154       this.x += x;
155     if (y)
156       this.y += y;
157     return this;
158   };
159 
160   /**
161    * Sets x
162    * @param {number} value The new x value
163    * @returns {this} The current function instance
164    */
165   jaws.Text.prototype.setX = function(value) {
166     this.x = value;
167     return this;
168   };
169 
170   /**
171    * Sets y
172    * @param {number} value The new y value
173    * @returns {this} The current function instance
174    */
175   jaws.Text.prototype.setY = function(value) {
176     this.y = value;
177     return this;
178   };
179 
180   /**
181    * Position sprites top on the y-axis
182    * @param {number} value
183    * @returns {this} The current function instance
184    */
185   jaws.Text.prototype.setTop = function(value) {
186     this.y = value + this.top_offset;
187     return this;
188   };
189 
190   /**
191    * Position sprites bottom on the y-axis
192    * @param {number} value
193    * @returns {this} The current function instance
194    */
195   jaws.Text.prototype.setBottom = function(value) {
196     this.y = value - this.bottom_offset;
197     return this;
198   };
199 
200   /**
201    * Position sprites left side on the x-axis
202    * @param {number} value
203    * @returns {this} The current function instance
204    */
205   jaws.Text.prototype.setLeft = function(value) {
206     this.x = value + this.left_offset;
207     return this;
208   };
209 
210   /**
211    * Position sprites right side on the x-axis
212    * @param {number} value
213    * @returns {this} The current function instance
214    */
215   jaws.Text.prototype.setRight = function(value) {
216     this.x = value - this.right_offset;
217     return this;
218   };
219 
220   /**
221    * Set new width.
222    * @param {number} value The new width
223    * @returns {this}
224    */
225   jaws.Text.prototype.setWidth = function(value) {
226     this.width = value;
227     this.cacheOffsets();
228     return this;
229   };
230 
231   /**
232    * Set new height. 
233    * @param {number} value The new height
234    * @returns {this}
235    */
236   jaws.Text.prototype.setHeight = function(value) {
237     this.height = value;
238     this.cacheOffsets();
239     return this;
240   };
241 
242   /**
243    * Resize sprite by adding width or height
244    * @param {number} width
245    * @param {number} height
246    * @returns {this}
247    */
248   jaws.Text.prototype.resize = function(width, height) {
249     this.width += width;
250     this.height += height;
251     this.cacheOffsets();
252     return this;
253   };
254 
255   /**
256    * Resize sprite to exact width/height
257    * @this {jaws.Text}
258    * @param {number} width
259    * @param {number} height
260    * @returns {this}
261    */
262   jaws.Text.prototype.resizeTo = function(width, height) {
263     this.width = width;
264     this.height = height;
265     this.cacheOffsets();
266     return this;
267   };
268 
269   /**
270    * The anchor could be describe as "the part of the text will be placed at x/y"
271    * or "when rotating, what point of the of the text will it rotate round"
272    * @param {string} value
273    * @returns {this} The current function instance
274    * @example
275    * For example, a topdown shooter could use setAnchor("center") --> Place middle of the ship on x/y
276    * .. and a sidescroller would probably use setAnchor("center_bottom") --> Place "feet" at x/y
277    */
278   jaws.Text.prototype.setAnchor = function(value) {
279     var anchors = {
280       top_left: [0, 0],
281       left_top: [0, 0],
282       center_left: [0, 0.5],
283       left_center: [0, 0.5],
284       bottom_left: [0, 1],
285       left_bottom: [0, 1],
286       top_center: [0.5, 0],
287       center_top: [0.5, 0],
288       center_center: [0.5, 0.5],
289       center: [0.5, 0.5],
290       bottom_center: [0.5, 1],
291       center_bottom: [0.5, 1],
292       top_right: [1, 0],
293       right_top: [1, 0],
294       center_right: [1, 0.5],
295       right_center: [1, 0.5],
296       bottom_right: [1, 1],
297       right_bottom: [1, 1]
298     };
299 
300     if (anchors.hasOwnProperty(value)) {
301       this.anchor_x = anchors[value][0];
302       this.anchor_y = anchors[value][1];
303       this.cacheOffsets();
304     }
305     return this;
306   };
307 
308   /**
309    * Save the object's dimensions
310    * @private
311    * @returns {this} The current function instance
312    */
313   jaws.Text.prototype.cacheOffsets = function() {
314 
315     this.left_offset = this.width * this.anchor_x;
316     this.top_offset = this.height * this.anchor_y;
317     this.right_offset = this.width * (1.0 - this.anchor_x);
318     this.bottom_offset = this.height * (1.0 - this.anchor_y);
319 
320     if (this.cached_rect)
321       this.cached_rect.resizeTo(this.width, this.height);
322     return this;
323   };
324 
325   /**
326    * Returns a jaws.Rect() perfectly surrouning text.
327    * @returns {jaws.Rect}
328    */
329   jaws.Text.prototype.rect = function() {
330     if (!this.cached_rect && this.width)
331       this.cached_rect = new jaws.Rect(this.x, this.y, this.width, this.height);
332     if (this.cached_rect)
333       this.cached_rect.moveTo(this.x - this.left_offset, this.y - this.top_offset);
334     return this.cached_rect;
335   };
336 
337   /**
338    * Draw sprite on active canvas or update its DOM-properties
339    * @this {jaws.Text}
340    * @returns {this} The current function instance
341    */
342   jaws.Text.prototype.draw = function() {
343     this.context.save();
344     if (this.angle !== 0) {
345       this.context.rotate(this.angle * Math.PI / 180);
346     }
347     this.context.globalAlpha = this.alpha;
348     this.context.translate(-this.left_offset, -this.top_offset); // Needs to be separate from above translate call cause of flipped
349     this.context.fillStyle = this.color;
350     this.context.font = this.style + " " + this.fontSize + "px " + this.fontFace;
351     this.context.textBaseline = this.textBaseline;
352     this.context.textAlign = this.textAlign;
353     if (this.shadowColor)
354       this.context.shadowColor = this.shadowColor;
355     if (this.shadowBlur)
356       this.context.shadowBlur = this.shadowBlur;
357     if (this.shadowOffsetX)
358       this.context.shadowOffsetX = this.shadowOffsetX;
359     if (this.shadowOffsetY)
360       this.context.shadowOffsetY = this.shadowOffsetY;
361     var oldY = this.y;
362     var oldX = this.x;
363     if (this.wordWrap)
364     {
365       var words = this.text.split(' ');
366       var nextLine = '';
367 
368       for (var n = 0; n < words.length; n++)
369       {
370         var testLine = nextLine + words[n] + ' ';
371         var measurement = this.context.measureText(testLine);
372         if (this.y < oldY + this.height)
373         {
374           if (measurement.width > this.width)
375           {
376             this.context.fillText(nextLine, this.x, this.y);
377             nextLine = words[n] + ' ';
378             this.y += this.fontSize;
379           }
380           else {
381             nextLine = testLine;
382           }
383           this.context.fillText(nextLine, this.x, this.y);
384         }
385       }
386     }
387     else
388     {
389       if (this.context.measureText(this.text).width < this.width)
390       {
391         this.context.fillText(this.text, this.x, this.y);
392       }
393       else
394       {
395         var words = this.text.split(' ');
396         var nextLine = ' ';
397         for (var n = 0; n < words.length; n++)
398         {
399           var testLine = nextLine + words[n] + ' ';
400           if (this.context.measureText(testLine).width < Math.abs(this.width - this.x))
401           {
402             this.context.fillText(testLine, this.x, this.y);
403             nextLine = words[n] + ' ';
404             nextLine = testLine;
405           }
406         }
407       }
408     }
409     this.y = oldY;
410     this.x = oldX;
411     this.context.restore();
412     return this;
413   };
414 
415   /** 
416    * Returns sprite as a canvas context.
417    * (For certain browsers, a canvas context is faster to work with then a pure image.)
418    * @public
419    * @this {jaws.Text}
420    */
421   jaws.Text.prototype.asCanvasContext = function() {
422     var canvas = document.createElement("canvas");
423     canvas.width = this.width;
424     canvas.height = this.height;
425 
426     var context = canvas.getContext("2d");
427     context.mozImageSmoothingEnabled = jaws.context.mozImageSmoothingEnabled;
428 
429     this.context.fillStyle = this.color;
430     this.context.font = this.style + this.fontSize + "px " + this.fontFace;
431     this.context.textBaseline = this.textBaseline;
432     this.context.textAlign = this.textAlign;
433     if (this.shadowColor)
434       this.context.shadowColor = this.shadowColor;
435     if (this.shadowBlur)
436       this.context.shadowBlur = this.shadowBlur;
437     if (this.shadowOffsetX)
438       this.context.shadowOffsetX = this.shadowOffsetX;
439     if (this.shadowOffsetY)
440       this.context.shadowOffsetY = this.shadowOffsetY;
441     var oldY = this.y;
442     var oldX = this.x;
443     if (this.wordWrap)
444     {
445       var words = this.text.split(' ');
446       var nextLine = '';
447 
448       for (var n = 0; n < words.length; n++)
449       {
450         var testLine = nextLine + words[n] + ' ';
451         var measurement = this.context.measureText(testLine);
452         if (this.y < oldY + this.height)
453         {
454           if (measurement.width > this.width)
455           {
456             this.context.fillText(nextLine, this.x, this.y);
457             nextLine = words[n] + ' ';
458             this.y += this.fontSize;
459           }
460           else {
461             nextLine = testLine;
462           }
463           this.context.fillText(nextLine, this.x, this.y);
464         }
465       }
466     }
467     else
468     {
469       if (this.context.measureText(this.text).width < this.width)
470       {
471         this.context.fillText(this.text, this.x, this.y);
472       }
473       else
474       {
475         var words = this.text.split(' ');
476         var nextLine = ' ';
477         for (var n = 0; n < words.length; n++)
478         {
479           var testLine = nextLine + words[n] + ' ';
480           if (this.context.measureText(testLine).width < Math.abs(this.width - this.x))
481           {
482             this.context.fillText(testLine, this.x, this.y);
483             nextLine = words[n] + ' ';
484             nextLine = testLine;
485           }
486         }
487       }
488     }
489     this.y = oldY;
490     this.x = oldX;
491     return context;
492   };
493 
494   /** 
495    * Returns text as a canvas
496    * @this {jaws.Text}
497    */
498   jaws.Text.prototype.asCanvas = function() {
499     var canvas = document.createElement("canvas");
500     canvas.width = this.width;
501     canvas.height = this.height;
502 
503     var context = canvas.getContext("2d");
504     context.mozImageSmoothingEnabled = jaws.context.mozImageSmoothingEnabled;
505 
506     this.context.fillStyle = this.color;
507     this.context.font = this.style + this.fontSize + "px " + this.fontFace;
508     this.context.textBaseline = this.textBaseline;
509     this.context.textAlign = this.textAlign;
510     if (this.shadowColor)
511       this.context.shadowColor = this.shadowColor;
512     if (this.shadowBlur)
513       this.context.shadowBlur = this.shadowBlur;
514     if (this.shadowOffsetX)
515       this.context.shadowOffsetX = this.shadowOffsetX;
516     if (this.shadowOffsetY)
517       this.context.shadowOffsetY = this.shadowOffsetY;
518     var oldY = this.y;
519     var oldX = this.x;
520     if (this.wordWrap)
521     {
522       var words = this.text.split(' ');
523       var nextLine = '';
524 
525       for (var n = 0; n < words.length; n++)
526       {
527         var testLine = nextLine + words[n] + ' ';
528         var measurement = context.measureText(testLine);
529         if (this.y < oldY + this.height)
530         {
531           if (measurement.width > this.width)
532           {
533             context.fillText(nextLine, this.x, this.y);
534             nextLine = words[n] + ' ';
535             this.y += this.fontSize;
536           }
537           else {
538             nextLine = testLine;
539           }
540           context.fillText(nextLine, this.x, this.y);
541         }
542       }
543     }
544     else
545     {
546       if (context.measureText(this.text).width < this.width)
547       {
548         this.context.fillText(this.text, this.x, this.y);
549       }
550       else
551       {
552         var words = this.text.split(' ');
553         var nextLine = ' ';
554         for (var n = 0; n < words.length; n++)
555         {
556           var testLine = nextLine + words[n] + ' ';
557           if (context.measureText(testLine).width < Math.abs(this.width - this.x))
558           {
559             context.fillText(testLine, this.x, this.y);
560             nextLine = words[n] + ' ';
561             nextLine = testLine;
562           }
563         }
564       }
565     }
566     this.y = oldY;
567     this.x = oldX;
568     return canvas;
569   };
570 
571   /**
572    * Returns Text's properties as a String 
573    * @returns {string}
574    */
575   jaws.Text.prototype.toString = function() {
576     return "[Text " + this.x.toFixed(2) + ", " + this.y.toFixed(2) + ", " + this.width + ", " + this.height + "]";
577   };
578 
579   /**
580    * Returns Text's properties as a pure object
581    * @returns {object}
582    */
583   jaws.Text.prototype.attributes = function() {
584     var object = this.options;                  // Start with all creation time properties
585     object["_constructor"] = this._constructor || "jaws.Text";
586     object["x"] = parseFloat(this.x.toFixed(2));
587     object["y"] = parseFloat(this.y.toFixed(2));
588     object["text"] = this.text;
589     object["alpha"] = this.alpha;
590     object["angle"] = parseFloat(this.angle.toFixed(2));
591     object["anchor_x"] = this.anchor_x;
592     object["anchor_y"] = this.anchor_y;
593     object["style"] = this.style;
594     object["fontSize"] = this.fontSize;
595     object["fontFace"] = this.fontFace;
596     object["color"] = this.color;
597     object["textAlign"] = this.textAlign;
598     object["textBaseline"] = this.textBaseline;
599     object["wordWrap"] = this.wordWrap;
600     object["width"] = this.width;
601     object["height"] = this.height;
602     return object;
603   };
604 
605   /**
606    * Returns a JSON-string representing the properties of the Text.
607    * @returns {string}
608    */
609   jaws.Text.prototype.toJSON = function() {
610     return JSON.stringify(this.attributes());
611   };
612 
613   return jaws;
614 })(jaws || {});
615 
616 // Support CommonJS require()
617 if (typeof module !== "undefined" && ('exports' in module)) {
618   module.exports = jaws.Text;
619 }
620