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="display_object.js"/>
 27 /// <reference path="rectangle.js"/>
 28 /// <reference path="composer.js"/>
 29 /// <reference path="config.js"/>
 30 /// <reference path="image_factory.js"/>
 31 
 32 /**
 33  * A class that encapsulates an <img> element or a <canvas> element into a
 34  * CreateJS object. This class should not be used for rendering <video>
 35  * elements. Mobile Safari (<= iOS 9) cannot play <video> elements inline and it
 36  * needs workarounds to render <video> elements inline on Mobile Safari. The
 37  * createjs.Video class implements these workarounds needed for encapsulating a
 38  * <video> element into a CreateJS object.
 39  * @extends {createjs.DisplayObject}
 40  * @implements {EventListener}
 41  * @constructor
 42  */
 43 createjs.Bitmap = function(value) {
 44   /// <signature>
 45   ///   <param type="HTMLImageElement" name="image"/>
 46   /// </signature>
 47   /// <signature>
 48   ///   <param type="HTMLCanvasElement" name="canvas"/>
 49   /// </signature>
 50   /// <signature>
 51   ///   <param type="string" name="source"/>
 52   /// </signature>
 53   createjs.DisplayObject.call(this);
 54 
 55   // Initialize this object only if its parameter is non-null. Flash-generated 
 56   // classes use 'derived.prototype = new createjs.Bitmap' to copy the methods
 57   // of this class and its properties.
 58   if (value != null) {
 59     var image = null;
 60     var width = 0;
 61     var height = 0;
 62     if (createjs.isString(value)) {
 63       var path = createjs.getString(value);
 64       // Retrieve an image from the ImageFactory object and copy its size only
 65       // if it is loaded.
 66       image = createjs.ImageFactory.get(
 67           path, path, this, createjs.DEFAULT_TEXTURE);
 68       if (image.complete) {
 69         width = image.width;
 70         height = image.height;
 71       }
 72     } else {
 73       image = /** @type {HTMLImageElement} */ (value);
 74       width = image.width;
 75       height = image.height;
 76     }
 77 
 78     /**
 79      * The image to be rendered. (This variable may be an HTMLCanvasElement
 80      * object or an HTMLVideoElement.)
 81      * @type {HTMLImageElement}
 82      * @private
 83      */
 84     this.image_ = image;
 85 
 86     /**
 87      * A renderer can draw the image attached to this bitmap. (The input image
 88      * may be an HTMLCanvasElement object and it cannot use the 'complete'
 89      * property.)
 90      * @type {boolean}
 91      * @private
 92      */
 93     this.ready_ = !!width;
 94 
 95     /**
 96      * The offset position of this image. This position is used for aligning
 97      * this image to the center of its nominal bounds.
 98      * @type {createjs.Point}
 99      * @private
100      */
101     this.offset_ = new createjs.Point(0, 0);
102 
103     if (!this.source_) {
104       /**
105        * The source rectangle of this image.
106        * @type {createjs.Rectangle}
107        * @private
108        */
109       this.source_ = new createjs.Rectangle(0, 0, width, height);
110     }
111 
112     // Layout this image to the center of the nominal bounds.
113     width = this.source_.width;
114     height = this.source_.height;
115     var nominalBounds =
116         /** @type {createjs.Rectangle} */ (this['nominalBounds']);
117     if (nominalBounds) {
118       // Align this image to the center of its nominal bounds.
119       var offsetX = nominalBounds.width - width;
120       if (offsetX > 0) {
121         this.offset_.x = offsetX >> 1;
122       }
123       var offsetY = nominalBounds.height - height;
124       if (offsetY > 0) {
125         this.offset_.y = offsetY >> 1;
126       }
127     }
128     // Synchronize the image size with the bounding-box size of this object.
129     this.setBoundingBox(0, 0, width, height);
130   }
131 };
132 createjs.inherits('Bitmap', createjs.Bitmap, createjs.DisplayObject);
133 
134 /**
135  * The <canvas> element used for hit-testing.
136  * @type {HTMLCanvasElement}
137  * @private
138  */
139 createjs.Bitmap.hitTestCanvas_ = null;
140 
141 /**
142  * The rendering context of the above <canvas> element, i.e. the rendering
143  * context used for hit-testing.
144  * @type {CanvasRenderingContext2D}
145  * @private
146  */
147 createjs.Bitmap.hitTestContext_ = null;
148 
149 /**
150  * An image composer used for applying a color filter to this image.
151  * @type {createjs.Composer}
152  * @private
153  */
154 createjs.Bitmap.prototype.composer_ = null;
155 
156 /**
157  * The renderer that draws this object.
158  * @type {createjs.Renderer}
159  * @private
160  */
161 createjs.Bitmap.prototype.output_ = null;
162 
163 /**
164  * The position where this bitmap was hit-tested last time.
165  * @type {createjs.Point}
166  * @private
167  */
168 createjs.Bitmap.prototype.hitTestPoint_ = null;
169 
170 /**
171  * The result of the last hit-testing.
172  * @type {createjs.DisplayObject}
173  * @private
174  */
175 createjs.Bitmap.prototype.hitTestResult_ = null;
176 
177 /**
178  * Returns the composer which applies filters to this bitmap.
179  * @return {createjs.Composer}
180  * @private
181  */
182 createjs.Bitmap.prototype.getComposer_ = function() {
183   /// <returns type="createjs.Composer"/>
184   if (!this.composer_) {
185     this.composer_ = new createjs.Composer();
186   }
187   return this.composer_;
188 };
189 
190 /**
191  * Retrieves the image of this bitmap used by the specified renderer.
192  * @return {HTMLImageElement|HTMLCanvasElement}
193  * @private
194  */
195 createjs.Bitmap.prototype.getSourceImage_ = function() {
196   /// <returns type="HTMLImageElement"/>
197   if (this.composer_) {
198     return this.composer_.getOutput();
199   }
200   return this.image_;
201 };
202 
203 /**
204  * Returns the HTMLImageElement object associated with this bitmap.
205  * @return {HTMLImageElement}
206  */
207 createjs.Bitmap.prototype.getImage = function() {
208   /// <returns type="HTMLImageElement"/>
209   return this.image_;
210 };
211 
212 /**
213  * Changes the HTMLImageElement object associated with this bitmap.
214  * @param {HTMLImageElement} image
215  */
216 createjs.Bitmap.prototype.setImage = function(image) {
217   /// <param type="HTMLImageElement" name="image"/>
218   this.handleDetach();
219   this.image_ = image;
220   this.ready_ = !!(image && image.width);
221   this.handleAttach(1);
222 };
223 
224 /**
225  * Returns the source rectangle of this bitmap.
226  * @return {createjs.Rectangle}
227  * @const
228  */
229 createjs.Bitmap.prototype.getSourceRect = function() {
230   /// <returns type="createjs.Rectangle"/>
231   return this.source_;
232 };
233 
234 /**
235  * Sets the source rectangle.
236  * @param {createjs.Rectangle} rectangle
237  * @const
238  */
239 createjs.Bitmap.prototype.setSourceRect = function(rectangle) {
240   /// <param type="createjs.Rectangle" name="rectangle"/>
241   this.setBoundingBox(0, 0, rectangle.width, rectangle.height);
242   this.source_ = rectangle;
243 };
244 
245 /** @override */
246 createjs.Bitmap.prototype.handleEvent = function(event) {
247   // Update the size of this object and invalidate it to redraw this object.
248   var type = event.type;
249   var image = /** @type {HTMLImageElement} */ (event.target);
250   createjs.ImageFactory.removeListeners(image, this);
251   this.ready_ = type == 'load';
252   if (!this.source_ || !this.ready_) {
253     this.image_ = null;
254     return;
255   }
256   var width = image.width;
257   var height = image.height;
258   this.source_.width = width;
259   this.source_.height = height;
260   // Align this image to the center of its nominal bounds.
261   var nominalBounds = /** @type {createjs.Rectangle} */ (this['nominalBounds']);
262   if (nominalBounds) {
263     var offsetX = nominalBounds.width - width;
264     if (offsetX > 0) {
265       this.offset_.x = offsetX >> 1;
266     }
267     var offsetY = nominalBounds.height - height;
268     if (offsetY > 0) {
269       this.offset_.y = offsetY >> 1;
270     }
271   }
272   this.setBoundingBox(0, 0, width, height);
273   this.handleAttach(1);
274 };
275 
276 /** @override */
277 createjs.Bitmap.prototype.handleAttach = function(flag) {
278   /// <param type="number" name="flag"/>
279   if (!flag || !this.ready_) {
280     return;
281   }
282   var alphaMapFilter = this.getAlphaMapFilter();
283   if (alphaMapFilter) {
284     var alpha = alphaMapFilter.image;
285     this.getComposer_().applyAlphaMap(this.image_, alpha);
286   }
287 };
288 
289 /** @override */
290 createjs.Bitmap.prototype.removeAllChildren = function(opt_destroy) {
291   /// <param type="boolean" optional="true" name="opt_destroy"/>
292   this.handleDetach();
293   this.image_ = null;
294   this.source_ = null;
295   this.ready_ = false;
296 };
297 
298 /** @override */
299 createjs.Bitmap.prototype.handleDetach = function() {
300   var image = this.getSourceImage_();
301   if (image && this.output_) {
302     // Detach the WebGLTexture object attached to this image.
303     this.output_.uncache(image);
304     this.output_ = null;
305   }
306   if (this.composer_) {
307     this.composer_.destroy();
308     this.composer_ = null;
309   }
310 };
311 
312 if (createjs.USE_PIXEL_TEST) {
313   /** @override */
314   createjs.Bitmap.prototype.hitTestObject = function(point, types, bubble) {
315     var object = createjs.DisplayObject.prototype.hitTestObject.call(
316         this, point, types, bubble);
317     if (object) {
318       // Return the cached result if the given point is sufficiently close to
319       // the last one. This method is often called twice with the same position
320       // when a user taps on this bitmap: one is for a 'touchdown' event, and
321       // the other is for a 'touchup' event.
322       if (this.hitTestPoint_) {
323         var dx = this.hitTestPoint_.x - point.x;
324         var dy = this.hitTestPoint_.y - point.y;
325         if (dx * dx + dy * dy <= 4) {
326           return this.hitTestResult_;
327         }
328         this.hitTestPoint_.x = point.x;
329         this.hitTestPoint_.y = point.y;
330       } else {
331         this.hitTestPoint_ = new createjs.Point(point.x, point.y);
332       }
333       var local = new createjs.Point(point.x, point.y);
334       this.getInverse().transformPoint(local);
335       if (this.source_) {
336         local.x += this.source_.x;
337         local.y += this.source_.y;
338       }
339       // Draw this bitmap to the 1x1 <canvas> element used for hit-testing and
340       // read its pixel. (This <canvas> element uses the "copy" composite
341       // operation to discard the destination pixels in drawing this bitmap,
342       // i.e. it is unnecessary to clear the <canvas> element explicitly
343       // before drawing this bitmap.)
344       var context = createjs.Bitmap.hitTestContext_;
345       if (!context) {
346         var canvas = createjs.createCanvas();
347         canvas.width = 1;
348         canvas.height = 1;
349         createjs.Bitmap.hitTestCanvas_ = canvas;
350         context = createjs.getRenderingContext2D(canvas);
351         context.globalCompositeOperation = 'copy';
352         createjs.Bitmap.hitTestContext_ = context;
353       }
354       context.drawImage(this.image_, -local.x, -local.y);
355       var pixels = context.getImageData(0, 0, 1, 1);
356       if (!pixels.data[3]) {
357         object = null;
358       }
359       this.hitTestResult_ = object;
360     }
361     return object;
362   };
363 }
364 
365 /** @override */
366 createjs.Bitmap.prototype.paintObject = function(renderer) {
367   /// <param type="createjs.Renderer" name="renderer"/>
368   var matrix = this.getColorMatrix();
369   if (matrix) {
370     renderer.setColorMatrix(matrix);
371   }
372   var image = /** @type {HTMLImageElement} */ (this.getSourceImage_());
373   var source = this.source_;
374   var x = this.offset_.x;
375   var y = this.offset_.y;
376   var width = source.width;
377   var height = source.height;
378   renderer.drawPartial(image,
379       source.x, source.y, width, height,
380       x, y, width, height);
381   if (matrix) {
382     renderer.setColorMatrix(null);
383   }
384 };
385 
386 /** @override */
387 createjs.Bitmap.prototype.isVisible = function() {
388   /// <returns type="boolean"/>
389   return this.ready_ && createjs.Bitmap.superClass_.isVisible.call(this);
390 };
391 
392 // Add a getter and a setter for applications to access internal variables.
393 Object.defineProperties(createjs.Bitmap.prototype, {
394   'image': {
395     get: createjs.Bitmap.prototype.getImage,
396     set: createjs.Bitmap.prototype.setImage
397   },
398   'sourceRect': {
399     get: createjs.Bitmap.prototype.getSourceRect,
400     set: createjs.Bitmap.prototype.setSourceRect
401   }
402 });
403 
404 // Export the createjs.Bitmap object to the global namespace.
405 createjs.exportObject('createjs.Bitmap', createjs.Bitmap, {
406 });
407