1 /**
  2  * The MIT License (MIT)
  3  *
  4  * Copyright (c) 2016 DeNA Co., Ltd.
  5  *
  6  * Permission is hereby granted, free of charge, to any person obtaining a copy
  7  * of this software and associated documentation files (the "Software"), to deal
  8  * in the Software without restriction, including without limitation the rights
  9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10  * copies of the Software, and to permit persons to whom the Software is
 11  * furnished to do so, subject to the following conditions:
 12  *
 13  * The above copyright notice and this permission notice shall be included in
 14  * all copies or substantial portions of the Software.
 15  *
 16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 22  * SOFTWARE.
 23  */
 24 
 25 /// <reference path="base.js"/>
 26 /// <reference path="object.js"/>
 27 /// <reference path="renderer.js"/>
 28 /// <reference path="shadow.js"/>
 29 /// <reference path="rectangle.js"/>
 30 /// <reference path="counter.js"/>
 31 /// <reference path="event.js"/>
 32 /// <reference path="user_agent.js"/>
 33 /// <reference path="ticker.js"/>
 34 
 35 /**
 36  * A class that implements the createjs.Renderer interface with Canvas 2D.
 37  * @param {HTMLCanvasElement} canvas
 38  * @param {createjs.BoundingBox} scissor
 39  * @extends {createjs.Renderer}
 40  * @implements {EventListener}
 41  * @constructor
 42  */
 43 createjs.CanvasRenderer = function(canvas, scissor) {
 44   /// <param type="HTMLCanvasElement" name="canvas"/>
 45   /// <param type="createjs.BoundingBox" name="scissor"/>
 46   createjs.Renderer.call(this, canvas, canvas.width, canvas.height);
 47 
 48   /**
 49    * The 2D rendering context attached to the output <canvas> element.
 50    * @type {CanvasRenderingContext2D}
 51    * @private
 52    */
 53   this.context_ = createjs.getRenderingContext2D(canvas);
 54 
 55   // Write the context used by this renderer.
 56   canvas.setAttribute('dena-context', '2d');
 57 
 58   // Listen keyup events to enable debugging features or disable them.
 59   if (createjs.DEBUG) {
 60     document.addEventListener('keyup', this, false);
 61   }
 62 
 63   // Create the clip specified by an application. (This renderer does NOT delete
 64   // this clip.)
 65   if (scissor) {
 66     this.context_.beginPath();
 67     this.context_.rect(scissor.getLeft(), scissor.getTop(),
 68                        scissor.getWidth(), scissor.getHeight());
 69     this.context_.clip();
 70   }
 71 };
 72 createjs.inherits('CanvasRenderer', createjs.CanvasRenderer, createjs.Renderer);
 73 
 74 /**
 75  * The current alpha value of the output <canvas> element.
 76  * @type {number}
 77  * @private
 78  */
 79 createjs.CanvasRenderer.prototype.alpha_ = 1;
 80 
 81 /**
 82  * The current composition value of the output <canvas> element.
 83  * @type {number}
 84  * @private
 85  */
 86 createjs.CanvasRenderer.prototype.compositeOperation_ =
 87     createjs.Renderer.Composition.SOURCE_OVER;
 88 
 89 /**
 90  * The scissor rectangle currently used by this renderer.
 91  * @type {createjs.BoundingBox}
 92  * @private
 93  */
 94 createjs.CanvasRenderer.prototype.scissor_ = null;
 95 
 96 /**
 97  * The scissor rectangle currently used by this renderer.
 98  * @type {createjs.CanvasRenderer}
 99  * @private
100  */
101 createjs.CanvasRenderer.prototype.mask_ = null;
102 
103 /**
104  * Saves the current rendering context.
105  * @private
106  */
107 createjs.CanvasRenderer.prototype.saveContext_ = function() {
108   this.context_.save();
109 };
110 
111 /**
112  * Restores the rendering context.
113  * @private
114  */
115 createjs.CanvasRenderer.prototype.restoreContext_ = function() {
116   this.context_.restore();
117   this.alpha_ = -1;
118   this.compositeOperation_ = -1;
119 };
120 
121 /**
122  * Updates the scissor rectangle.
123  * @param {createjs.BoundingBox} scissor
124  * @private
125  */
126 createjs.CanvasRenderer.prototype.updateScissor_ = function(scissor) {
127   /// <param type="createjs.CanvasRenderer" name="renderer"/>
128   /// <param type="createjs.BoundingBox" name="scissor"/>
129   if (this.scissor_) {
130     if (this.scissor_.isEqual(scissor)) {
131       return;
132     }
133     this.restoreContext_();
134   }
135   this.scissor_ = scissor;
136   this.saveContext_();
137   this.setTransformation(1, 0, 0, 1, 0, 0);
138   this.context_.beginPath();
139   this.context_.rect(scissor.getLeft(), scissor.getTop(),
140                      scissor.getWidth(), scissor.getHeight());
141   this.context_.clip();
142 };
143 
144 /**
145  * Destroys the scissor rectangle.
146  * @private
147  */
148 createjs.CanvasRenderer.prototype.destroyScissor_ = function() {
149   /// <param type="createjs.CanvasRenderer" name="renderer"/>
150   if (this.scissor_) {
151     this.restoreContext_();
152     this.scissor_ = null;
153   }
154 };
155 
156 if (createjs.DEBUG) {
157   /**
158    * Whether to show a red rectangle around a painted objects in a rendering
159    * cycle.
160    * @type {boolean}
161    * @private
162    */
163   createjs.CanvasRenderer.prototype.showPainted_ = true;
164 
165   /**
166    * @type {Array.<createjs.Renderer.RenderObject>}
167    * @private
168    */
169   createjs.CanvasRenderer.prototype.painted_ = null;
170 
171   /**
172    * Draws debug information on this renderer.
173    * @param {Array.<createjs.Renderer.RenderObject>} painted
174    * @private
175    */
176   createjs.CanvasRenderer.prototype.drawDebug_ = function(painted) {
177     /// <param type="Array" elementType="createjs.Renderer.RenderObject"
178     ///        name="painted"/>
179     var context = this.context_;
180     if (!createjs.UserAgent.isIPhone()) {
181       context.fillStyle = '#f00';
182       var HEIGHT = 24;
183       context.font = HEIGHT + 'px arial';
184       var text = createjs.Counter.paintedObjects + '/' +
185                  createjs.Counter.visibleObjects + '/' +
186                  createjs.Counter.totalObjects + ' ' +
187                  createjs.Counter.updatedTweens + '/' +
188                  createjs.Counter.runningTweens + ' ' +
189                  createjs.Counter.cachedRenderers + '/' +
190                  createjs.Counter.totalRenderers;
191       context.fillText(text, 0, HEIGHT, 10000);
192     }
193     if (this.showPainted_) {
194       context.strokeStyle = '#f00';
195       context.setTransform(1, 0, 0, 1, 0, 0);
196       for (var i = 0; i < painted.length; ++i) {
197         var object = painted[i];
198         var box = object.getRenderBox();
199         context.strokeRect(
200             box.getLeft(), box.getTop(), box.getWidth(), box.getHeight());
201       }
202     }
203   };
204 
205   /** @override */
206   createjs.CanvasRenderer.prototype.addDirtyObject = function(object) {
207     this.painted_.push(object);
208   };
209 
210   /** @override */
211   createjs.CanvasRenderer.prototype.handleEvent = function(event) {
212     createjs.assert(event.type == 'keyup');
213 
214     /// <var type="KeyboardEvent" name="keyEvent"/>
215     var keyEvent = /** @type {KeyboardEvent} */ (event);
216     var keyCode = keyEvent.keyCode;
217     if (keyCode == createjs.Event.KeyCodes.Q) {
218       this.showPainted_ = !this.showPainted_;
219     }
220   };
221 }
222 
223 /**
224  * Clears the invalidated rectangles.
225  * @protected
226  */
227 createjs.CanvasRenderer.prototype.clearScreen = function() {
228   // There is a bug on stock browsers of Android 4.1.x and 4.2.x where the
229   // clearRect() method crashes or does not clear the target canvas. To work
230   // around this bug, this method calls the clearRect() method with a size
231   // bigger than the canvas size.
232   this.context_.setTransform(1, 0, 0, 1, 0, 0);
233   this.context_.clearRect(0, 0, this.getWidth() + 1, this.getHeight() + 1);
234 };
235 
236 /**
237  * Returns a renderer used for composing a render object with a mask.
238  * @return {createjs.CanvasRenderer}
239  * @protected
240  */
241 createjs.CanvasRenderer.prototype.getMask = function() {
242   /// <returns type="createjs.CanvasRenderer"/>
243   if (!this.mask_) {
244     var canvas = createjs.createCanvas();
245     canvas.width = this.getWidth();
246     canvas.height = this.getHeight();
247     this.mask_ = new createjs.CanvasRenderer(canvas, null);
248   } else {
249     this.mask_.clearScreen();
250   }
251   return this.mask_;
252 };
253 
254 /**
255  * Deletes the renderer used for composing a render object with a mask.
256  * @protected
257  * @const
258  */
259 createjs.CanvasRenderer.prototype.destroyMask = function() {
260   if (this.mask_) {
261     this.mask_.destroy();
262     this.mask_ = null;
263   }
264 };
265 
266 /**
267  * Copies the image of a mask renderer, a renderer used for composing a render
268  * object with a mask object, to this renderer.
269  * @param {createjs.CanvasRenderer} mask
270  * @param {number} composition
271  * @protected
272  * @const
273  */
274 createjs.CanvasRenderer.prototype.drawMask = function(mask, composition) {
275   /// <param type="createjs.CanvasRenderer" name="mask"/>
276   /// <param type="number" name="composition"/>
277   this.setTransformation(1, 0, 0, 1, 0, 0);
278   this.setAlpha(1);
279   this.setComposition(createjs.Renderer.Composition.SOURCE_OVER);
280   this.drawCanvas(
281       mask.getCanvas(), 0, 0, this.getWidth(), this.getHeight());
282 };
283 
284 /** @override */
285 createjs.CanvasRenderer.prototype.destroy = function() {
286   this.destroyMask();
287   this.context_ = null;
288   this.resetCanvas();
289 };
290 
291 /** @override */
292 createjs.CanvasRenderer.prototype.setTransformation =
293     function(a, b, c, d, tx, ty) {
294   /// <param type="number" name="a"/>
295   /// <param type="number" name="b"/>
296   /// <param type="number" name="c"/>
297   /// <param type="number" name="d"/>
298   /// <param type="number" name="tx"/>
299   /// <param type="number" name="ty"/>
300   this.context_.setTransform(a, b, c, d, tx, ty);
301 };
302 
303 /** @override */
304 createjs.CanvasRenderer.prototype.setAlpha = function(alpha) {
305   /// <param type="number" name="alpha"/>
306   if (this.alpha_ != alpha) {
307     this.alpha_ = alpha;
308     this.context_.globalAlpha = alpha;
309   }
310 };
311 
312 /** @override */
313 createjs.CanvasRenderer.prototype.setComposition = function(operation) {
314   /// <param type="number" name="operation"/>
315   if (this.compositeOperation_ != operation) {
316     this.compositeOperation_ = operation;
317     this.context_.globalCompositeOperation =
318         createjs.Renderer.getCompositionName(operation);
319   }
320 };
321 
322 /** @override */
323 createjs.CanvasRenderer.prototype.drawCanvas =
324     function(canvas, x, y, width, height) {
325   /// <param type="HTMLCanvasElement" name="canvas"/>
326   /// <param type="number" name="x"/>
327   /// <param type="number" name="y"/>
328   /// <param type="number" name="width"/>
329   /// <param type="number" name="height"/>
330   this.context_.drawImage(canvas, x, y, width, height);
331 };
332 
333 /** @override */
334 createjs.CanvasRenderer.prototype.drawVideo =
335     function (video, x, y, width, height) {
336   /// <param type="HTMLVideoElement" name="video"/>
337   /// <param type="number" name="x"/>
338   /// <param type="number" name="y"/>
339   /// <param type="number" name="width"/>
340   /// <param type="number" name="height"/>
341   this.context_.drawImage(video, x, y, width, height);
342 };
343 
344 /** @override */
345 createjs.CanvasRenderer.prototype.drawPartial =
346     function(image, srcX, srcY, srcWidth, srcHeight, x, y, width, height) {
347   /// <param type="HTMLImageElement" name="image"/>
348   /// <param type="number" name="srcX"/>
349   /// <param type="number" name="srcY"/>
350   /// <param type="number" name="srcWidth"/>
351   /// <param type="number" name="srcHeight"/>
352   /// <param type="number" name="x"/>
353   /// <param type="number" name="y"/>
354   /// <param type="number" name="width"/>
355   /// <param type="number" name="height"/>
356   this.context_.drawImage(
357       image, srcX, srcY, srcWidth, srcHeight, x, y, width, height);
358 };
359 
360 /** @override */
361 createjs.CanvasRenderer.prototype.addObject = function(object) {
362   /// <param type="createjs.Renderer.RenderObject" name="object"/>
363   // Skip rendering the specified object if it is not in the output <canvas>.
364   // Even when this renderer skips rendering an object, it still needs to
365   // call its 'beginPaintObject()' method and to update properties of this
366   // renderer so it can render succeeding objects.
367   var box = object.getRenderBox();
368   if (box.maxX <= 0 || box.maxY <= 0 ||
369       this.getWidth() <= box.minX || this.getHeight() <= box.minY) {
370     object.beginPaintObject(this);
371     return;
372   }
373   if (createjs.DEBUG) {
374     if (createjs.CanvasRenderer.showPainted_) {
375       this.addDirtyObject(object);
376     }
377     ++createjs.Counter.paintedObjects;
378   }
379   // Draw a masked object. The current code just show render objects
380   // with 'compose' clips.
381   var scissor = object.getClip();
382   if (!scissor || !scissor.getMethod() || scissor.isShow()) {
383     this.destroyScissor_();
384   } else if (!scissor.getShape()) {
385     this.updateScissor_(scissor.getBox());
386   } else if (scissor.isCompose()) {
387     var mask = this.getMask();
388     object.beginPaintObject(mask);
389     object.paintObject(mask);
390     var shape = scissor.getShape();
391     shape.beginPaintObject(mask);
392     shape.paintObject(mask);
393     this.drawMask(mask, scissor.getComposition());
394     return;
395   } else {
396     return;
397   }
398   object.beginPaintObject(this);
399   object.paintObject(this);
400 };
401 
402 /** @override */
403 createjs.CanvasRenderer.prototype.begin = function() {
404   if (createjs.DEBUG) {
405     this.painted_ = [];
406   }
407   this.clearScreen();
408   // Some Android 4.1.x browsers (e.g. DoCoMo L-05E) has an issue that calling
409   // the CanvasRenderingContext2D.prototype.clearRect() method in a
410   // setInterval() callback does not clear a <canvas> element:
411   //   <https://code.google.com/p/android/issues/detail?id=39247>.
412   // This renderer triggers DOM reflow to work around this issue when a game
413   // needs it, i.e. it calls the createjs.Config.setUseAndroidWorkarounds()
414   // method with its parameters '2d' and 1. (DOM reflow is a very slow operation
415   // and this renderer does not trigger it by default.)
416   this.updateCanvas('2d');
417 };
418 
419 /** @override */
420 createjs.CanvasRenderer.prototype.paint = function(time) {
421   /// <param type="number" name="time"/>
422   // Render each layer and copy it to this renderer. Passing null to the
423   // endPaint() method prevents it from copying its result to this renderer.
424   // (This is used only by debug builds to hide layers specified by a user.)
425   this.destroyScissor_();
426   if (createjs.DEBUG) {
427     this.drawDebug_(this.painted_);
428   }
429 };
430