1 /**
  2  * The Render Engine
  3  * CanvasContext
  4  *
  5  * @fileoverview An extension of the 2D render context which encapsulates
  6  *               the Canvas element.
  7  *
  8  * @author: Brett Fattori (brettf@renderengine.com)
  9  * @author: $Author: bfattori@gmail.com $
 10  * @version: $Revision: 1557 $
 11  *
 12  * Copyright (c) 2011 Brett Fattori (brettf@renderengine.com)
 13  *
 14  * Permission is hereby granted, free of charge, to any person obtaining a copy
 15  * of this software and associated documentation files (the "Software"), to deal
 16  * in the Software without restriction, including without limitation the rights
 17  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 18  * copies of the Software, and to permit persons to whom the Software is
 19  * furnished to do so, subject to the following conditions:
 20  *
 21  * The above copyright notice and this permission notice shall be included in
 22  * all copies or substantial portions of the Software.
 23  *
 24  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 25  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 26  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 27  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 28  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 29  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 30  * THE SOFTWARE.
 31  *
 32  */
 33 
 34 // The class this file defines and its required classes
 35 R.Engine.define({
 36     "class":"R.rendercontexts.CanvasContext",
 37     "requires":[
 38         "R.math.Math2D",
 39         "R.rendercontexts.RenderContext2D"
 40     ]
 41 });
 42 
 43 /**
 44  * @class A <tt>canvas</tt> element represented within the engine.  A canvas
 45  * is a 2D context which can render lines, images, and polygons.  Transformations
 46  * can be saved and restored, allowing for complex, stacked transformations.
 47  *
 48  * @extends R.rendercontexts.RenderContext2D
 49  * @constructor
 50  * @description Create a new instance of a canvas context.
 51  * @param name {String} The name of the context
 52  * @param width {Number} The width (in pixels) of the canvas context.
 53  * @param height {Number} The height (in pixels) of the canvas context.
 54  */
 55 R.rendercontexts.CanvasContext = function () {
 56     return R.rendercontexts.RenderContext2D.extend(/** @scope R.rendercontexts.CanvasContext.prototype */{
 57 
 58         context2D:null,
 59         divisions:-1,
 60         dirtyBins:null,
 61         firstFrame:null,
 62 
 63         /** @private */
 64         constructor:function (name, width, height) {
 65             // Make sure the browser supports the canvas and 2D context!
 66             Assert((R.engine.Support.sysInfo().support.canvas.defined &&
 67                 R.engine.Support.sysInfo().support.canvas.contexts["2D"]), "Browser does not support Canvas. Cannot construct CanvasContext!");
 68 
 69             Assert((width != null && height != null), "Width and height must be specified in CanvasContext");
 70 
 71             this.setWidth(width);
 72             this.setHeight(height);
 73             var canvas;
 74             var worldScale = this.getWorldScale();
 75             // Create the canvas element
 76             canvas = document.createElement("canvas");
 77 
 78             this.base(name || "CanvasContext", canvas);
 79             this.setViewport(R.math.Rectangle2D.create(0, 0, this.width, this.height));
 80 
 81             canvas.id = this.getId();
 82             this.setWorldScale(this.getWorldScale());
 83 
 84             // Adjust the element accordingly
 85             $(this.getElement())
 86                 .attr({
 87                     "width":this.getWidth(),
 88                     "height":this.getHeight()
 89                 });
 90 
 91             // Set the number of divisions along X and Y
 92             this.divisions = 5;
 93             this.dirtyBins = {};
 94             this.firstFrame = true;
 95         },
 96 
 97         /**
 98          * Releases the context back into the object pool.  See {@link PooledObject#release}
 99          * for more information.
100          */
101         release:function () {
102             this.base();
103             this.context2D = null;
104         },
105 
106         /**
107          * Set the number of divisions along the X and Y axis used to determine the
108          * number of dirty rectangles for the current viewport.
109          *
110          * @param divisions {Number} The number of divisions along X and Y.  Defaults to 5.
111          */
112         setDivisions:function (divisions) {
113             this.divisions = divisions;
114         },
115 
116         /**
117          * Set the scale of the world
118          * @param scaleX {Number} The scale of the world along the X axis
119          * @param scaleY {Number} The scale of the world along the y axis
120          */
121         setWorldScale:function (scaleX, scaleY) {
122             this.base(scaleX, scaleY);
123             scaleY = scaleY ? scaleY : scaleX;
124             this.setViewport(R.math.Rectangle2D.create(0, 0, this.getWidth() * (1 / scaleX), this.getHeight() * (1 / scaleY)));
125         },
126 
127         /**
128          * Gets the surface context upon which all objects are drawn.
129          * @return {Object}
130          */
131         get2DContext:function () {
132             if (this.context2D == null) {
133                 this.context2D = this.getSurface().getContext('2d');
134             }
135             return this.context2D;
136         },
137 
138         /**
139          * Push a transform state onto the stack.
140          */
141         pushTransform:function () {
142             this.base();
143             this.get2DContext().save();
144         },
145 
146         /**
147          * Pop a transform state off the stack.
148          */
149         popTransform:function () {
150             this.base();
151             this.get2DContext().restore();
152         },
153 
154 
155         //================================================================
156         // Drawing functions
157 
158         /**
159          * Reset the entire context, clearing it and preparing it for drawing.
160          */
161         reset:function (rect) {
162             if (!R.Engine.options.useDirtyRectangles) {
163                 var cRect = (rect != null ? rect : this.getViewport());
164                 var d = cRect.get();
165                 this.get2DContext().clearRect(d.x, d.y, d.w, d.h);
166             }
167         },
168 
169         /**
170          * Set up the world for the given time before any rendering is dont.
171          * @param time {Number} The render time
172          * @param dt {Number} The delta between the world time and the last time the world was updated
173          *          in milliseconds.
174          */
175         setupWorld:function (time, dt) {
176             this.setScale(this.getWorldScale());
177 
178             /* pragma:DEBUG_START */
179             if (R.Engine.getDebugMode()) {
180                 this.setLineStyle("yellow");
181                 this.setLineWidth(1);
182                 this.drawRectangle(this.getViewport());
183             }
184             /* pragma:DEBUG_END */
185 
186             this.base(time, dt);
187         },
188 
189         /**
190          * Capture the dirty rectangles for the bin
191          * @param {Object} bin
192          * @param {Object} itr
193          */
194         captureBin:function (bin, itr) {
195             var dBin = this.dirtyBins["Bin" + bin];
196             if (!dBin) {
197                 dBin = this.dirtyBins["Bin" + bin] = [];
198             }
199 
200             // Brute force method
201             itr.reset();
202             while (itr.hasNext()) {
203                 var obj = itr.next();
204                 if (obj.wasDirty && obj.wasDirty()) {
205                     var aabb = obj.getAABB();
206                     dBin.push({
207                         p:aabb.getTopLeft(),
208                         d:this.getImage(obj.getAABB())
209                     });
210                 }
211             }
212             itr.reset();
213         },
214 
215         /**
216          * Reset the bin's dirty rectangles before drawing the dirty objects
217          * in the bin.
218          * @param {Object} bin
219          */
220         resetBin:function (bin) {
221             var dBin = this.dirtyBins["Bin" + bin];
222             while (dBin && dBin.length > 0) {
223                 var r = dBin.shift();
224                 this.putImage(r.d, r.p);
225             }
226         },
227 
228         /**
229          * Render all of the objects in a single bin, grouped by z-index.
230          * @param bin {Number} The bin number being rendered
231          * @param itr {R.lang.Iterator} The iterator over all the objects in the bin
232          * @param time {Number} The current render time in milliseconds from the engine.
233          * @param dt {Number} The delta between the world time and the last time the world was updated
234          *          in milliseconds.
235          */
236         renderBin:function (bin, itr, time, dt) {
237             if (R.Engine.options.useDirtyRectangles) {
238                 if (!this.firstFrame) {
239                     this.resetBin(bin);
240                 }
241                 this.captureBin(bin, itr);
242                 this.firstFrame = false;
243             }
244             R.rendercontexts.RenderContext2D.prototype.renderBin.call(this, bin, itr, time, dt);
245         },
246 
247         /**
248          * Set the background color of the context.
249          *
250          * @param color {String} An HTML color
251          */
252         setBackgroundColor:function (color) {
253             jQuery(this.getSurface()).css("background-color", color);
254             this.base(color);
255         },
256 
257         /**
258          * Set the current transform position (translation).
259          *
260          * @param point {R.math.Point2D} The translation
261          */
262         setPosition:function (point) {
263             this.get2DContext().translate(point.x, point.y);
264             this.base(point);
265         },
266 
267         /**
268          * Set the rotation angle of the current transform
269          *
270          * @param angle {Number} An angle in degrees
271          */
272         setRotation:function (angle) {
273             this.get2DContext().rotate(R.math.Math2D.degToRad(angle));
274             this.base(angle);
275         },
276 
277         /**
278          * Set the scale of the current transform.  Specifying
279          * only the first parameter implies a uniform scale.
280          *
281          * @param scaleX {Number} The X scaling factor, with 1 being 100%
282          * @param scaleY {Number} The Y scaling factor
283          */
284         setScale:function (scaleX, scaleY) {
285             scaleX = scaleX || 1;
286             scaleY = scaleY || scaleX;
287             this.get2DContext().scale(scaleX, scaleY);
288             this.base(scaleX, scaleY);
289         },
290 
291         /**
292          * Set the transformation using a matrix.
293          *
294          * @param matrix {Matrix} The transformation matrix
295          */
296         setTransform:function (matrix) {
297         },
298 
299         /**
300          * Set the line style for the context.
301          *
302          * @param lineStyle {String} An HTML color or <tt>null</tt>
303          */
304         setLineStyle:function (lineStyle) {
305             this.get2DContext().strokeStyle = lineStyle;
306             this.base(lineStyle);
307         },
308 
309         /**
310          * Set the line width for drawing paths.
311          *
312          * @param [width=1] {Number} The width of lines in pixels
313          */
314         setLineWidth:function (width) {
315             this.get2DContext().lineWidth = width * 1.0;
316             this.base(width);
317         },
318 
319         /**
320          * Set the fill style of the context.
321          *
322          * @param fillStyle {String} An HTML color, or <tt>null</tt>.
323          */
324         setFillStyle:function (fillStyle) {
325             this.get2DContext().fillStyle = fillStyle;
326             this.base(fillStyle);
327         },
328 
329         /**
330          * Draw an un-filled rectangle on the context.
331          *
332          * @param rect {R.math.Rectangle2D} The rectangle to draw
333          */
334         drawRectangle:function (rect) {
335             var rTL = rect.getTopLeft();
336             var rDM = rect.getDims();
337             this.get2DContext().strokeRect(rTL.x, rTL.y, rDM.x, rDM.y);
338             this.base(rect);
339         },
340 
341         /**
342          * Draw a filled rectangle on the context.
343          *
344          * @param rect {R.math.Rectangle2D} The rectangle to draw
345          */
346         drawFilledRectangle:function (rect) {
347             var rTL = rect.getTopLeft();
348             var rDM = rect.getDims();
349             this.get2DContext().fillRect(rTL.x, rTL.y, rDM.x, rDM.y);
350             this.base(rect);
351         },
352 
353         /**
354          * @private
355          */
356         _arc:function (point, radiusX, startAngle, endAngle) {
357             this.startPath();
358             this.get2DContext().arc(point.x, point.y, radiusX, startAngle, endAngle, false);
359             //this.endPath();
360         },
361 
362         /**
363          * Draw an un-filled arc on the context.  Arcs are drawn in clockwise
364          * order.
365          *
366          * @param point {R.math.Point2D} The point around which the arc will be drawn
367          * @param radius {Number} The radius of the arc in pixels
368          * @param startAngle {Number} The starting angle of the arc in degrees
369          * @param endAngle {Number} The end angle of the arc in degrees
370          */
371         drawArc:function (point, radiusX, startAngle, endAngle) {
372             this._arc(point, radiusX, startAngle, endAngle);
373             this.strokePath();
374             this.base(point, radiusX, startAngle, endAngle);
375         },
376 
377         /**
378          * Draw a filled arc on the context.  Arcs are drawn in clockwise
379          * order.
380          *
381          * @param point {R.math.Point2D} The point around which the arc will be drawn
382          * @param radius {Number} The radius of the arc in pixels
383          * @param startAngle {Number} The starting angle of the arc in degrees
384          * @param endAngle {Number} The end angle of the arc in degrees
385          */
386         drawFilledArc:function (point, radiusX, startAngle, endAngle) {
387             this._arc(point, radiusX, startAngle, endAngle);
388             this.fillPath();
389             this.base(point, radiusX, startAngle, endAngle);
390         },
391 
392         /**
393          * Draw a line on the context.
394          *
395          * @param point1 {R.math.Point2D} The start of the line
396          * @param point2 {R.math.Point2D} The end of the line
397          */
398         drawLine:function (point1, point2) {
399             this.startPath();
400             this.moveTo(point1);
401             this.lineTo(point2);
402             //this.endPath();
403             this.strokePath();
404             this.base(point1, point2);
405         },
406 
407         /**
408          * Draw a point on the context.
409          *
410          * @param point {R.math.Point2D} The position to draw the point
411          */
412         drawPoint:function (point) {
413             if (R.Engine.options.pointAsArc) {
414                 this._arc(point, 1, 0, 360);
415                 this.get2DContext().fill();
416             } else {
417                 this.get2DContext().fillRect(point.x, point.y, 1.5, 1.5);
418             }
419             this.base(point);
420         },
421 
422         /**
423          * Draw a sprite on the context.
424          *
425          * @param sprite {R.resources.types.Sprite} The sprite to draw
426          * @param time {Number} The current world time
427          * @param dt {Number} The delta between the world time and the last time the world was updated
428          *          in milliseconds.
429          */
430         drawSprite:function (sprite, time, dt) {
431             var f = sprite.getFrame(time, dt);
432             this.get2DContext().drawImage(sprite.getSourceImage(), f.x, f.y, f.w, f.h, 0, 0, f.w, f.h);
433             this.base(sprite, time);
434             f.destroy();
435         },
436 
437         /**
438          * Draw an image on the context.
439          *
440          * @param rect {R.math.Rectangle2D} The rectangle that specifies the position and
441          *             dimensions of the image rectangle.
442          * @param image {Object} The image to draw onto the context
443          * @param [srcRect] {R.math.Rectangle2D} <i>[optional]</i> The source rectangle within the image, if
444          *                <tt>null</tt> the entire image is used
445          */
446         drawImage:function (rect, image, srcRect) {
447             if (srcRect) {
448                 this.get2DContext().drawImage(image,
449                     srcRect.x, srcRect.y, srcRect.w, srcRect.h, rect.x, rect.y, rect.w, rect.h);
450             } else {
451                 this.get2DContext().drawImage(image, rect.x, rect.y, rect.w, rect.h);
452             }
453             this.base(rect, image);
454         },
455 
456         /**
457          * Capture an image from the context.
458          *
459          * @param rect {R.math.Rectangle2D} The area to capture
460          * @returns {Array} Image data capture
461          */
462         getImage:function (rect) {
463             this.base();
464 
465             // Clamp the rectangle to be within the bounds of the context
466             var p = rect.getTopLeft();
467             var tl = R.math.Point2D.create((p.x < 0 ? 0 : (p.x > this.getWidth() ? this.getWidth() - 1 : p.x)),
468                 (p.y < 0 ? 0 : (p.y > this.getHeight() ? this.getHeight() - 1 : p.y)));
469             var wh = R.math.Point2D.create((rect.r > this.getWidth() ? this.getWidth() - tl.x : (rect.r < 0 ? 1 : rect.w)),
470                 (rect.b > this.getHeight() ? this.getHeight() - tl.y : (rect.b < 0 ? 1 : rect.h)));
471             var imgData = this.get2DContext().getImageData(tl.x, tl.y, wh.x, wh.y);
472             tl.destroy();
473             wh.destroy();
474             return imgData;
475         },
476 
477         /**
478          * Useful method which returns a data URL which represents the
479          * current state of the canvas context.  The URL can be passed to
480          * an image element. <i>Note: Only works in Firefox and Opera!</i>
481          *
482          * @param {String} format The mime-type of the output, or <tt>null</tt> for
483          *                 the PNG default. (unsupported)
484          * @return {String} The data URL
485          */
486         getDataURL:function (format) {
487             return this.getSurface().toDataURL();
488         },
489 
490         /**
491          * Draw an image, captured with {@link #getImage}, to
492          * the context.
493          *
494          * @param imageData {Array} Image data captured
495          * @param point {R.math.Point2D} The poisition at which to draw the image
496          */
497         putImage:function (imageData, point) {
498             var x = (point.x < 0 ? 0 : (point.x > this.getWidth() ? this.getWidth() - 1 : point.x));
499             var y = (point.y < 0 ? 0 : (point.y > this.getHeight() ? this.getHeight() - 1 : point.y));
500             if (imageData != null) {
501                 this.get2DContext().putImageData(imageData, x, y);
502             }
503         },
504 
505         /**
506          * Draw filled text on the context.
507          *
508          * @param point {R.math.Point2D} The top-left position to draw the image.
509          * @param text {String} The text to draw
510          */
511         drawText:function (point, text) {
512             this.base(point, text);
513             if (!this.get2DContext().fillText) {
514                 return;  // Unsupported by canvas
515             }
516             this.get2DContext().font = this.getNormalizedFont();
517             this.get2DContext().textBaseline = this.getFontBaseline();
518             this.get2DContext().fillText(text, point.x, point.y);
519         },
520 
521         /**
522          * Get a rectangle that will approximately enclose the text drawn by the render context.
523          * @param text {String} The text to measure
524          * @return {R.math.Rectangle2D}
525          */
526         getTextMetrics:function (text) {
527             var rect = this.base(text);
528             this.get2DContext().font = this.getNormalizedFont();
529             this.get2DContext().textBaseline = this.getFontBaseline();
530             var metrics = this.get2DContext().measureText(text);
531             // Scale the height a little to account for hanging chars
532             rect.set(0, 0, metrics.width, parseFloat(this.getFontSize()) * 1.25);
533             return rect;
534         },
535 
536         /**
537          * Draw stroked (outline) text on the context.
538          *
539          * @param point {R.math.Point2D}
540          * @param text {String} The text to draw
541          */
542         strokeText:function (point, text) {
543             if (!R.engine.Support.sysInfo().canvas.text) {
544                 return;  // Unsupported by canvas
545             }
546             this.get2DContext().font = this.getNormalizedFont();
547             this.get2DContext().textBaseline = this.getFontBaseline();
548             this.get2DContext().strokeText(text, point.x, point.y);
549         },
550 
551         /**
552          * Start a path.
553          */
554         startPath:function () {
555             this.get2DContext().beginPath();
556             this.base();
557         },
558 
559         /**
560          * End a path.
561          */
562         endPath:function () {
563             this.get2DContext().closePath();
564             this.base();
565         },
566 
567         /**
568          * Stroke a path using the current line style and width.
569          */
570         strokePath:function () {
571             this.get2DContext().stroke();
572             this.base();
573         },
574 
575         /**
576          * Fill a path using the current fill style.
577          */
578         fillPath:function () {
579             this.get2DContext().fill();
580             this.base();
581         },
582 
583         /**
584          * Move the current path to the point sepcified.
585          *
586          * @param point {R.math.Point2D} The point to move to
587          */
588         moveTo:function (point) {
589             this.get2DContext().moveTo(point.x, point.y);
590             this.base();
591         },
592 
593         /**
594          * Draw a line from the current point to the point specified.
595          *
596          * @param point {R.math.Point2D} The point to draw a line to
597          */
598         lineTo:function (point) {
599             this.get2DContext().lineTo(point.x, point.y);
600             this.base(point);
601         },
602 
603         /**
604          * Draw a quadratic curve from the current point to the specified point.
605          *
606          * @param cPoint {R.math.Point2D} The control point
607          * @param point {R.math.Point2D} The point to draw to
608          */
609         quadraticCurveTo:function (cPoint, point) {
610             this.get2DContext().quadraticCurveTo(cPoint.x, cPoint.y, point.x, point.y);
611             this.base(cPoint, point);
612         },
613 
614         /**
615          * Draw a bezier curve from the current point to the specified point.
616          *
617          * @param cPoint1 {R.math.Point2D} Control point 1
618          * @param cPoint2 {R.math.Point2D} Control point 2
619          * @param point {R.math.Point2D} The point to draw to
620          */
621         bezierCurveTo:function (cPoint1, cPoint2, point) {
622             this.get2DContext().bezierCurveTo(cPoint1.x, cPoint1.y, cPoint2.x, cPoint2.y, point.x, point.y);
623             this.base(cPoint1, cPoint2, point);
624         },
625 
626         /**
627          * Draw an arc from the current point to the specified point.
628          *
629          * @param point1 {R.math.Point2D} Arc point 1
630          * @param point2 {R.math.Point2D} Arc point 2
631          * @param radius {Number} The radius of the arc
632          */
633         arcTo:function (point1, point2, radius) {
634             this.get2DContext().arcTo(point1.x, point1.y, point2.x, point2.y, radius);
635             this.base(point1, point2, radius);
636         }
637 
638     }, /** @scope R.rendercontexts.CanvasContext.prototype */{
639         /**
640          * Get the class name of this object
641          *
642          * @return {String} "R.rendercontexts.CanvasContext"
643          */
644         getClassName:function () {
645             return "R.rendercontexts.CanvasContext";
646         }
647 
648     });
649 };
650