1 /**
  2  * The Render Engine
  3  * HTMLElementContext
  4  *
  5  * @fileoverview A render context which wraps a specified HTML node.
  6  *
  7  * @author: Brett Fattori (brettf@renderengine.com)
  8  * @author: $Author: bfattori $
  9  * @version: $Revision: 1555 $
 10  *
 11  * Copyright (c) 2011 Brett Fattori (brettf@renderengine.com)
 12  *
 13  * Permission is hereby granted, free of charge, to any person obtaining a copy
 14  * of this software and associated documentation files (the "Software"), to deal
 15  * in the Software without restriction, including without limitation the rights
 16  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 17  * copies of the Software, and to permit persons to whom the Software is
 18  * furnished to do so, subject to the following conditions:
 19  *
 20  * The above copyright notice and this permission notice shall be included in
 21  * all copies or substantial portions of the Software.
 22  *
 23  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 24  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 25  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 26  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 27  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 28  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 29  * THE SOFTWARE.
 30  *
 31  */
 32 
 33 // The class this file defines and its required classes
 34 R.Engine.define({
 35     "class":"R.rendercontexts.HTMLElementContext",
 36     "requires":[
 37         "R.rendercontexts.RenderContext2D",
 38         "R.math.Math2D"
 39     ]
 40 });
 41 
 42 /**
 43  * @class A wrapper for any HTML element to convert it into a targetable render context.
 44  *        The {@link R.rendercontexts.DocumentContext} and {@link R.rendercontexts.HTMLDivContext} use this as their base
 45  *        class.
 46  *
 47  * @extends R.rendercontexts.RenderContext2D
 48  * @constructor
 49  * @description Create an instance of an HTML element rendering context.  This context
 50  * represents any HTML element.
 51  * @param name {String} The name of the context
 52  * @param element {Number} The element which is the surface of the context.
 53  */
 54 R.rendercontexts.HTMLElementContext = function () {
 55     return R.rendercontexts.RenderContext2D.extend(/** @scope R.rendercontexts.HTMLElementContext.prototype */{
 56 
 57         transformStack:null,
 58         cursorPos:null,
 59         jQObj:null,
 60         hasTxfm:false,
 61         has3dTxfm:false,
 62         txfmBrowser:null,
 63         txfmOrigin:null,
 64         txfm:null,
 65 
 66         tmpP1:null,
 67         tmpP2:null,
 68         tmpP3:null,
 69 
 70         /** @private */
 71         constructor:function (name, element) {
 72             this.base(name || "HTMLElementContext", element);
 73             element.id = this.getId();
 74             this.cursorPos = R.math.Point2D.create(0, 0);
 75             this.txfm = [];
 76             this.transformStack = [];
 77             this.pushTransform();
 78             this.jQObj = null;
 79             this.setViewport(R.math.Rectangle2D.create(0, 0, this.jQ().width(), this.jQ().height()));
 80             this.checkTransformSupport();
 81 
 82             // Temporary points to use in calculations
 83             this.tmpP1 = R.math.Point2D.create(0, 0);
 84             this.tmpP2 = R.math.Point2D.create(0, 0);
 85             this.tmpP3 = R.math.Point2D.create(0, 0);
 86         },
 87 
 88         /**
 89          * Destroy the context and any objects within the context.
 90          */
 91         destroy:function () {
 92 
 93             // If the objects in the context are elements in
 94             // the DOM, remove them from the DOM
 95             var objs = this.getObjects();
 96             for (var o in objs) {
 97                 var e = objs[o].getElement();
 98                 if (e && e.nodeName && e != document.body) {
 99 
100                     this.getSurface().removeChild(e);
101                 }
102             }
103             this.cursorPos.destroy();
104             this.getViewport().destroy();
105             this.txfm = null;
106 
107             this.tmpP1.destroy();
108             this.tmpP2.destroy();
109             this.tmpP3.destroy();
110 
111             this.base();
112         },
113 
114         /**
115          * Check the browser and version to see if it supports transformations.
116          * @private
117          */
118         checkTransformSupport:function () {
119             var version = parseFloat(R.engine.Support.sysInfo().version);
120             switch (R.engine.Support.sysInfo().browser) {
121                 case "safari":
122                 case "safarimobile":
123                     if (version >= 3) {
124                         // Support for webkit transforms
125                         this.hasTxfm = true;
126                         this.has3dTxfm = true;
127                         this.txfmBrowser = "webkitTransform";
128                         this.txfmOrigin = "webkitTransformOrigin";
129                     }
130                     break;
131                 case "chrome":
132                     // Support for webkit transforms
133                     this.hasTxfm = true;
134                     this.has3dTxfm = true;
135                     this.txfmBrowser = "webkitTransform";
136                     this.txfmOrigin = "webkitTransformOrigin";
137                     break;
138                 case "firefox":
139                     if (version >= 3.5) {
140                         // Support for gecko transforms
141                         this.hasTxfm = true;
142                         this.has3dTxfm = false;
143                         this.txfmBrowser = "MozTransform";
144                         this.txfmOrigin = "MozTransformOrigin";
145                     }
146                     break;
147                 case "opera":
148                     if (version >= 10.5) {
149                         // Support for opera transforms
150                         this.hasTxfm = true;
151                         this.has3dTxfm = false;
152                         this.txfmBrowser = "OTransform";
153                         this.txfmOrigin = "OTransformOrigin";
154                     }
155                     break;
156                 case "msie":
157                     if (version >= 9.0) {
158                         // Support for Internet Explorer transforms
159                         this.hasTxfm = true;
160                         this.has3dTxfm = false;
161                         this.txfmBrowser = "msTransform";
162                         this.txfmOrigin = "msTransformOrigin";
163                     }
164                     break;
165                 default:
166                     this.hasTxfm = false;
167                     this.has3dTxfm = false;
168                     break;
169             }
170         },
171 
172         /**
173          * Add an object to the context, or creates an element to represent the object.  Objects
174          * added to the <tt>HTMLElementContext</tt> need a DOM representation, otherwise one
175          * will be created for the object being added.
176          *
177          * @param obj {HTMLElement} The element, or <tt>null</tt>
178          */
179         add:function (obj) {
180             if (!obj.getElement()) {
181                 // Create an element for the object
182                 obj.setElement($("<div>").css("position", "absolute"));
183             }
184 
185             // Look to see if the element is already a child of the element
186             // we're appending to.  This will occur when someone adds a HTMLElementContext
187             // to the default context, when the element which represents the HTMLElementContext
188             // already exists in the DOM.
189             if (this.jQ().find(obj.getElement()).length == 0) {
190                 this.jQ().append(obj.getElement());
191             }
192 
193             var pos = $(obj.getElement()).position();
194             obj.setObjectDataModel("DOMPosition", R.math.Point2D.create(pos.left, pos.top));
195 
196             this.base(obj);
197         },
198 
199         /**
200          * Remove an object from the context.
201          * @param obj {HTMLElement} The object to remove
202          */
203         remove:function (obj) {
204             if (obj.jQ().length) {
205                 obj.jQ().remove();
206             }
207             this.base(obj);
208         },
209 
210         /**
211          * Serializes the current transformation state to an object.
212          * @return {Object}
213          * @private
214          */
215         serializeTransform:function () {
216             return {
217                 pos:R.clone(this.cursorPos),
218                 txfm:this.txfm,
219                 stroke:this.getLineStyle(),
220                 sWidth:this.getLineWidth(),
221                 fill:this.getFillStyle()
222             };
223         },
224 
225         /**
226          * Deserializes a transformation state from an object.
227          * @param transform {Object} The object which contains the current transformation
228          * @private
229          */
230         deserializeTransform:function (transform) {
231             this.txfm = transform.txfm;
232             this.cursorPos.set(transform.pos);
233             transform.pos.destroy();
234             this.setLineStyle(transform.stroke);
235             this.setLineWidth(transform.sWidth);
236             this.setFillStyle(transform.fill);
237         },
238 
239         /**
240          * Push a transform state onto the stack.
241          */
242         pushTransform:function () {
243             this.base();
244             this.transformStack.push(this.serializeTransform());
245         },
246 
247         /**
248          * Pop a transform state off the stack.
249          */
250         popTransform:function () {
251             this.base();
252             this.deserializeTransform(this.transformStack.pop());
253         },
254 
255         //================================================================
256         // Drawing functions
257 
258         /**
259          * Set the background color of the context.
260          *
261          * @param color {String} An HTML color
262          */
263         setBackgroundColor:function (color) {
264             this.base(color);
265             this.jQ().css("background-color", color);
266         },
267 
268         /**
269          * Set the current transform position (translation).
270          *
271          * @param point {R.math.Point2D} The translation
272          */
273         setPosition:function (point) {
274             this.cursorPos.add(point);
275             if (this.hasTxfm) {
276                 this.txfm[0] = "translate" + (this.has3dTxfm ? "3d" : "") + "(" + this.cursorPos.x + "px," + this.cursorPos.y + "px" + (this.has3dTxfm ? ",0" : "") + ")";
277             }
278             this.base(this.cursorPos);
279         },
280 
281         /**
282          * Set the rotation angle of the current transform
283          *
284          * @param angle {Number} An angle in degrees
285          */
286         setRotation:function (angle) {
287             if (this.hasTxfm) {
288                 angle = Math.floor(angle % 360);
289                 this.txfm[1] = "rotate" + (this.has3dTxfm ? "3d(0,0,1," : "(") + angle + "deg)";
290             }
291             this.base(angle);
292         },
293 
294         /**
295          * Set the scale of the current transform.  Specifying
296          * only the first parameter implies a uniform scale.
297          *
298          * @param scaleX {Number} The X scaling factor, with 1 being 100%
299          * @param scaleY {Number} The Y scaling factor
300          */
301         setScale:function (scaleX, scaleY) {
302             scaleX = scaleX || 1;
303             scaleY = scaleY || scaleX;
304             if (this.hasTxfm) {
305                 this.txfm[2] = "scale" + (this.has3dTxfm ? "3d" : "") + "(" + scaleX + "," + scaleY + (this.has3dTxfm ? ",1" : "") + ")";
306             }
307             this.base(scaleX, scaleY);
308         },
309 
310         /**
311          * Set the width of the context drawing area.
312          *
313          * @param width {Number} The width in pixels
314          */
315         setWidth:function (width) {
316             this.base(width);
317             this.jQ().width(width);
318         },
319 
320         /**
321          * Set the height of the context drawing area
322          *
323          * @param height {Number} The height in pixels
324          */
325         setHeight:function (height) {
326             this.base(height);
327             this.jQ().height(height);
328         },
329 
330         _onlyChanged:function (ref, css) {
331             if (!ref)
332                 return css;
333 
334             var modifiedCSS = {}, jq = ref.jQ();
335             if (!jq)
336                 return css;
337 
338             for (var attr in css) {
339                 var oldValue = jq.css(attr);
340                 if (oldValue !== css[attr]) {
341                     modifiedCSS[attr] = css[attr];
342                 }
343             }
344             return modifiedCSS;
345         },
346 
347         /**
348          * Merge in the CSS transformations object, if the browser supports it.
349          * @param css {Object} CSS properties to merge with
350          * @return {Object}
351          * @private
352          */
353         _mergeTransform:function (ref, css) {
354             if (this.hasTxfm && this.txfm[0]) {
355                 css[this.txfmBrowser] = this.txfm[0] + " " +
356                     (ref && ref.getRotation() != 0 ? this.txfm[1] + " " : "") +
357                     (ref && ref.getScale().len() != 1 ? this.txfm[2] : "");
358             }
359             else {
360                 css.top = css.top || this.cursorPos.y;
361                 css.left = css.left || this.cursorPos.x;
362             }
363 
364             return css;
365         },
366 
367         /**
368          * Create an element and append it to the render context
369          * @param element {String} Element type
370          * @return {jQuery}
371          * @private
372          */
373         _createElement:function (element) {
374             var e = $(element).css({
375                 position:"absolute",
376                 display:"block",
377                 left:0,
378                 top:0
379             });
380             this.jQ().append(e);
381             return e;
382         },
383 
384         /**
385          * Draw an un-filled rectangle on the context.  Unless <tt>ref</tt> is provided, a div element
386          * will be added to the render context.
387          *
388          * @param rect {R.math.Rectangle2D} The rectangle to draw
389          * @param ref {R.engine.GameObject} A reference game object
390          * @return {HTMLElement} The element added to the DOM
391          */
392         drawRectangle:function (rect, ref) {
393             var rD = rect.getDims(),
394                 obj = ref && ref.jQ() ? ref.jQ() : this._createElement("<div>");
395 
396             obj.css(this._mergeTransform(ref, {
397                 borderWidth:this.getLineWidth(),
398                 borderColor:this.getLineStyle(),
399                 left:rD.l,
400                 top:rD.t,
401                 width:rD.w,
402                 height:rD.h,
403                 position:"absolute"
404             }));
405 
406             return obj;
407         },
408 
409         /**
410          * Draw a filled rectangle on the context.  Unless <tt>ref</tt> is provided, a div element
411          * will be added to the render context.
412          *
413          * @param rect {R.math.Rectangle2D} The rectangle to draw
414          * @param ref {R.engine.GameObject} A reference game object
415          * @return {HTMLElement} The element added to the DOM
416          */
417         drawFilledRectangle:function (rect, ref) {
418             var rD = rect.getDims(),
419                 obj = ref && ref.jQ() ? ref.jQ() : this._createElement("<div>");
420 
421             obj.css(this._mergeTransform(ref, {
422                 borderWidth:this.getLineWidth(),
423                 borderColor:this.getLineStyle(),
424                 backgroundColor:this.getFillStyle(),
425                 left:rD.l,
426                 top:rD.t,
427                 width:rD.w,
428                 height:rD.h,
429                 position:"absolute"
430             }));
431 
432             return obj;
433         },
434 
435         /**
436          * Draw a point on the context.  Unless <tt>ref</tt> is provided, a new image
437          * will be added to the render context.
438          *
439          * @param point {R.math.Point2D} The position to draw the point
440          * @param ref {R.engine.GameObject} A reference game object
441          * @return {HTMLElement} The element added to the DOM
442          */
443         drawPoint:function (point, ref) {
444             return this.drawFilledRectangle(R.math.Rectangle2D.create(point.x, point.y, 1, 1), ref);
445         },
446 
447         /**
448          * Draw a sprite on the context.  Unless <tt>ref</tt> is provided, a new image
449          * will be added to the render context.
450          *
451          * @param sprite {R.resources.types.Sprite} The sprite to draw
452          * @param time {Number} The current world time
453          * @param dt {Number} The delta between the world time and the last time the world was updated
454          *          in milliseconds.
455          * @param ref {R.math.HostObject} A reference game object
456          * @return {HTMLElement} The element added to the DOM
457          */
458         drawSprite:function (sprite, time, dt, ref) {
459             var f = sprite.getFrame(time, dt);
460 
461             // The reference object is a host object it
462             // will give us a reference to the HTML element which we can then
463             // just modify the displayed image for.  If no ref was provided,
464             // create a new image.
465             var obj = ref && ref.jQ() ? ref.jQ() : this._createElement("<div>");
466 
467             var css = this._mergeTransform(ref, {
468                 width:f.w,
469                 height:f.h,
470                 backgroundPosition:-f.x + "px " + -f.y + "px",
471                 backgroundImage:'url:(' + sprite.getSourceImage().src + ')'
472             });
473             obj.css(css);
474             this.base(sprite, time, dt);
475             f.destroy();
476 
477             return obj;
478         },
479 
480         /**
481          * Draw an image on the context.  Unless <tt>ref</tt> is provided, a new image
482          * will be added to the render context.
483          *
484          * @param rect {R.math.Rectangle2D} The rectangle that specifies the position and
485          *             dimensions of the image rectangle.
486          * @param image {HTMLImage} The image to draw onto the context
487          * @param [srcRect] {R.math.Rectangle2D} <i>[optional]</i> The source rectangle within the image, if
488          *                <tt>null</tt> the entire image is used
489          * @param [ref] {R.engine.GameObject} A reference game object
490          * @return {HTMLElement} The element added to the DOM
491          */
492         drawImage:function (rect, image, srcRect, ref) {
493             srcRect = (srcRect.__RECTANGLE2D ? srcRect : null);
494             var sD = srcRect ? srcRect : rect;
495             ref = (!srcRect.__RECTANGLE2D ? srcRect : ref);
496 
497             // The reference object is an object that should
498             // have a reference to an HTML element which we can
499             // just modify the displayed image for.
500             // If no ref is provided, create a new element.
501             var obj = ref && ref.jQ() ? ref.jQ() : this._createElement("<div>");
502 
503             var css = this._mergeTransform(ref, {
504                 backgroundImage:"url(" + image.src + ")",
505                 backgroundPosition:-sD.x + "px " + -sD.y + "px",
506                 left:rect.x,
507                 top:rect.y,
508                 width:sD.w,
509                 height:sD.h
510             });
511             obj.css(this._onlyChanged(ref, css));
512             this.base(rect, image, srcRect, ref);
513 
514             return obj;
515         },
516 
517         /**
518          * Draw text on the context.  Unless <tt>ref</tt> is provided, a span element
519          * will be added to the render context.
520          *
521          * @param point {R.math.Point2D} The top-left position to draw the image.
522          * @param text {String} The text to draw
523          * @param ref {R.engine.GameObject} A reference game object
524          * @return {HTMLElement} The element added to the DOM
525          */
526         drawText:function (point, text, ref) {
527             this.base(point, text);
528 
529             // The reference object is a host object it
530             // will give us a reference to the HTML element which we can then
531             // just modify the displayed text for.  If no ref was provided,
532             // create a new image.
533             var obj = ref && ref.jQ() ? ref.jQ() : this._createElement("<span>");
534 
535             var css = this._mergeTransform(ref, {
536                 font:this.getNormalizedFont(),
537                 color:this.getFillStyle(),
538                 left:point.x,
539                 top:point.y,
540                 position:"absolute"
541             });
542             obj.css(css).text(text);
543 
544             return obj;
545         },
546 
547         /**
548          * Draw an element on the context.
549          * @param ref {R.engine.GameObject} A reference game object
550          * @param [el] {HTMLElement} A DOM element to draw
551          */
552         drawElement:function (ref, el, pos) {
553             if (ref && ref.getElement()) {
554                 // TODO: Can probably save cycles by checking for changes in the
555                 //			transformations before blindly applying them
556                 el = el || ref.getElement();
557                 var css = {}, i;
558                 if (this.hasTxfm && ref.getOrigin) {
559                     if (ref.getOrigin().isZero()) {
560                         el.style[this.txfmOrigin] = "top left";
561                     }
562                     else {
563                         var o = ref.getOrigin();
564                         el.style[this.txfmOrigin] = o.x + "px " + o.y + "px";
565                     }
566                 }
567                 css = this._mergeTransform(ref, css);
568                 for (i in css) {
569                     el.style[i] = css[i];
570                 }
571             } else {
572                 el.css({
573                     top:pos.y,
574                     left:pos.x
575                 })
576             }
577         }
578 
579     }, /** @scope R.rendercontexts.HTMLElementContext.prototype */ {
580 
581         /**
582          * Get the class name of this object
583          * @return {String} The string "R.rendercontexts.HTMLElementContext"
584          */
585         getClassName:function () {
586             return "R.rendercontexts.HTMLElementContext";
587         }
588     });
589 
590 };
591