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="event_dispatcher.js"/>
 27 /// <reference path="rectangle.js"/>
 28 /// <reference path="config.js"/>
 29 /// <reference path="image_factory.js"/>
 30 
 31 /**
 32  * A class that represents a sprite animation. This class reads a sprite sheet
 33  * defined in <http://createjs.com/docs/easeljs/classes/SpriteSheet.html> and
 34  * creates an animation timeline. (A sprite sheet cannot write a complicated
 35  * animation consisting of multiple CreateJS objects and games mostly use the
 36  * createjs.MovieClip class.)
 37  * @param {Object} data
 38  * @implements {EventListener}
 39  * @extends {createjs.EventDispatcher}
 40  * @constructor
 41  */
 42 createjs.SpriteSheet = function(data) {
 43   /// <param type="Object" name="data"/>
 44   createjs.EventDispatcher.call(this);
 45 
 46   if (data) {
 47     this.parseData_(data);
 48   }
 49 };
 50 createjs.inherits('SpriteSheet',
 51                   createjs.SpriteSheet,
 52                   createjs.EventDispatcher);
 53 
 54 /**
 55  * Indicates whether all images are finished loading, i.e. this sprite sheet
 56  * is ready to play.
 57  * @type {boolean}
 58  */
 59 createjs.SpriteSheet.prototype['complete'] = true;
 60 
 61 /**
 62  * The default frame-rate.
 63  * @type {number}
 64  */
 65 createjs.SpriteSheet.prototype.framerate = 0;
 66 
 67 /**
 68  * The list of animation names.
 69  * @type {Array.<string>}
 70  * @private
 71  */
 72 createjs.SpriteSheet.prototype.animations_ = null;
 73 
 74 /**
 75  * The list of animation frames (i.e. a pair of an <image> element and a source 
 76  * rectangle) generated from a 'frames' parameter.
 77  * @type {Array.<createjs.SpriteSheet.Frame>}
 78  * @private
 79  */
 80 createjs.SpriteSheet.prototype.frames_ = null;
 81 
 82 /**
 83  * The list of <image> elements used by this animation.
 84  * @type {Array.<HTMLImageElement>}
 85  * @private
 86  */
 87 createjs.SpriteSheet.prototype.images_ = null;
 88 
 89 /**
 90  * The mapping table from an animation name to an animation data.
 91  * @type {Object.<string,createjs.SpriteSheet.Animation>}
 92  * @private
 93  */
 94 createjs.SpriteSheet.prototype.data_ = null;
 95 
 96 /**
 97  * The number of <image> elements being loaded now.
 98  * @type {number}
 99  * @private
100  */
101 createjs.SpriteSheet.prototype.loadCount_ = 0;
102 
103 /**
104  * The value of a 'frames' parameter.
105  * @type {Object}
106  * @private
107  */
108 createjs.SpriteSheet.prototype.frameData_ = null;
109 
110 /**
111  * The number of animation frames.
112  * @type {number}
113  * @private
114  */
115 createjs.SpriteSheet.prototype.numFrames_ = 0;
116 
117 /**
118  * A class representing a frame used by the createjs.SpriteSheet object.
119  * @param {HTMLImageElement} image
120  * @param {createjs.Rectangle} rectangle
121  * @param {number} regX
122  * @param {number} regY
123  * @param {number} width
124  * @param {number} height
125  * @constructor
126  */
127 createjs.SpriteSheet.Frame =
128     function(image, rectangle, regX, regY, width, height) {
129   /// <param type="HTMLImageElement" name="image"/>
130   /// <param type="createjs.Rectangle" name="rectangle"/>
131   /// <param type="number" name="regX"/>
132   /// <param type="number" name="regY"/>
133   /// <param type="number" name="width"/>
134   /// <param type="number" name="height"/>
135 
136   /**
137    * The source image of this frame.
138    * @const {HTMLImageElement}
139    */
140   this.image = image;
141 
142   /**
143    * The source rectangle of this frame.
144    * @const {createjs.Rectangle}
145    */
146   this.rect = rectangle;
147 
148   /**
149    * The x position of the destination of this frame.
150    * @const {number}
151    */
152   this.regX = regX;
153 
154   /**
155    * The y position of the destination of this frame.
156    * @const {number}
157    */
158   this.regY = regY;
159 };
160 
161 /**
162  * Creates a createjs.SpriteSheet.Frame object.
163  * @param {HTMLImageElement} image
164  * @param {number} x
165  * @param {number} y
166  * @param {number} width
167  * @param {number} height
168  * @param {number} regX
169  * @param {number} regY
170  * @return {createjs.SpriteSheet.Frame}
171  * @private
172  */
173 createjs.SpriteSheet.Frame.create_ =
174     function(image, x, y, width, height, regX, regY) {
175   /// <param type="HTMLImageElement" name="image"/>
176   /// <param type="number" name="x"/>
177   /// <param type="number" name="y"/>
178   /// <param type="number" name="width"/>
179   /// <param type="number" name="height"/>
180   /// <param type="number" name="regX"/>
181   /// <param type="number" name="regY"/>
182   var rectangle = new createjs.Rectangle(x, y, width, height);
183   return new createjs.SpriteSheet.Frame(
184       image, rectangle, regX, regY, width, height);
185 };
186 
187 /**
188  * A class representing a frame used by the createjs.SpriteSheet object.
189  * @param {string} name
190  * @constructor
191  */
192 createjs.SpriteSheet.Animation = function(name) {
193   /// <param type="string" name="name"/>
194   /**
195    * @const {string}
196    * @private
197    */
198   this.name_ = name;
199 
200   /**
201    * @type {Array.<number>}
202    * @private
203    */
204   this.frames_ = null;
205 
206   /**
207    * @type {number}
208    * @private
209    */
210   this.speed_ = 1;
211 
212   /**
213    * @type {string}
214    * @private
215    */
216   this.next_ = '';
217 };
218 
219 /**
220  * Returns the name of this animation.
221  * @return {string}
222  * @const
223  */
224 createjs.SpriteSheet.Animation.prototype.getName = function() {
225   /// <returns type="string"/>
226   return this.name_;
227 };
228 
229 /**
230  * Returns the speed of this animation.
231  * @return {number}
232  * @const
233  */
234 createjs.SpriteSheet.Animation.prototype.getSpeed = function() {
235   /// <returns type="number"/>
236   return this.speed_;
237 };
238 
239 /**
240  * Changes the speed of this animation.
241  * @param {number} speed
242  * @const
243  */
244 createjs.SpriteSheet.Animation.prototype.setSpeed = function(speed) {
245   /// <returns type="number"/>
246   this.speed_ = speed;
247 };
248 
249 /**
250  * Returns the next animation.
251  * @return {string}
252  * @const
253  */
254 createjs.SpriteSheet.Animation.prototype.getNext = function() {
255   /// <returns type="string"/>
256   return this.next_;
257 };
258 
259 /**
260  * Sets the next animation.
261  * @param {string} next
262  * @const
263  */
264 createjs.SpriteSheet.Animation.prototype.setNext = function(next) {
265   /// <returns type="string"/>
266   this.next_ = next;
267 };
268 
269 /**
270  * Returns the frames of which this animation consists.
271  * @return {number}
272  * @const
273  */
274 createjs.SpriteSheet.Animation.prototype.getFrameLength = function() {
275   /// <returns type="number"/>
276   return this.frames_.length;
277 };
278 
279 /**
280  * Returns the frames of which this animation consists.
281  * @return {number}
282  * @const
283  */
284 createjs.SpriteSheet.Animation.prototype.getFrame = function(frame) {
285   /// <returns type="number"/>
286   return this.frames_[frame];
287 };
288 
289 /**
290  * Parses an 'animation' section of a SpriteSheet object.
291  * @param {string} key
292  * @param {*} value
293  * @private
294  * @const
295  */
296 createjs.SpriteSheet.Animation.prototype.parse_ = function(key, value) {
297   /// <signature>
298   ///   <param type="string" name="key"/>
299   ///   <param type="number" name="value"/>
300   /// </signature>
301   /// <signature>
302   ///   <param type="string" name="key"/>
303   ///   <param type="Array" elementType="number" name="value"/>
304   /// </signature>
305   /// <signature>
306   ///   <param type="string" name="key"/>
307   ///   <param type="Object" name="value"/>
308   /// </signature>
309   if (createjs.isNumber(value)) {
310     // This animation is a single-frame animation, which consists only of a
311     // frame number as listed below.
312     //   'animations': {
313     //     'stand': 7
314     //   }
315     this.frames_ = [createjs.getNumber(value)];
316   } else if (createjs.isArray(value)) {
317     // This animation is a simple animation, which is an array consisting of up
318     // to four parameters (a start frame, an end frame, an animation name, and a
319     // speed) as listed below.
320     //   'animations': {
321     //     'run': [0, 8],
322     //     'jump': [9, 12, 'run', 2],
323     //   }
324     var parameters = createjs.getArray(value);
325     if (parameters.length == 1) {
326       this.frames_ = [createjs.getNumber(parameters[0])];
327     } else {
328       this.frames_ = [];
329       var start = createjs.getNumber(parameters[0]);
330       var end = createjs.getNumber(parameters[1]);
331       for (var i = start; i <= end; ++i) {
332         this.frames_.push(i);
333       }
334       if (parameters[2]) {
335         this.next_ = key;
336       }
337       this.speed_ = createjs.castNumber(parameters[3]) || 1;
338     }
339   } else {
340     // This animation is a complex animation, which is an Object consisting of
341     // up to three parameters ('frame', 'next', and 'speed') as listed below.
342     //   'animations': {
343     //     'run': {
344     //       'frames: [1, 2, 4, 4, 2, 1]
345     //     },
346     //     'jump': {
347     //       'frames': [1, 4, 5, 6, 1],
348     //       'next': 'run',
349     //       'speed': 2
350     //     },
351     //     'stand': {
352     //       'frames': [7],
353     //     }
354     //   }
355     this.speed_ = createjs.castNumber(value['speed']) || 1;
356     this.next_ = createjs.castString(value['next']) || '';
357     var frames = value['frames'];
358     if (createjs.isNumber(frames)) {
359       this.frames_ = [createjs.getNumber(frames)];
360     } else {
361       var parameters = createjs.getArray(frames);
362       this.frames_ = parameters.slice(0);
363     }
364   }
365   if (this.frames_.length < 2 && this.next_ == key) {
366     this.next_ = '';
367   }
368   if (!this.speed_) {
369     this.speed_ = 1;
370   }
371 };
372 
373 // Adds getters and setters so games can access animation properties.
374 Object.defineProperties(createjs.SpriteSheet.Animation.prototype, {
375   'name': {
376     get: createjs.SpriteSheet.Animation.prototype.getName
377   },
378   'speed': {
379     get: createjs.SpriteSheet.Animation.prototype.getSpeed,
380     set: createjs.SpriteSheet.Animation.prototype.setSpeed
381   },
382   'next': {
383     get: createjs.SpriteSheet.Animation.prototype.getNext,
384     set: createjs.SpriteSheet.Animation.prototype.setNext
385   }
386 });
387 
388 /**
389  * Retrieves an image or creates one. The input parameter for this method is
390  * one of a string, an HTMLImageElement object, or an HTMLCanvasElement object.
391  * If the parameter is a string, this method creates a new HTMLImageElement and
392  * loads an image from the string. Otherwise, this method just changes its type
393  * to HTMLImageElement to avoid a warning.
394  * @param {*} image
395  * @return {HTMLImageElement}
396  * @private
397  * @const
398  */
399 createjs.SpriteSheet.prototype.getImage_ = function(image) {
400   /// <signature>
401   ///   <param type="string" name="path"/>
402   ///   <returns type="HTMLImageElement"/>
403   /// </signature>
404   /// <signature>
405   ///   <param type="HTMLImageElement" name="image"/>
406   ///   <returns type="HTMLImageElement"/>
407   /// </signature>
408   if (createjs.isString(image)) {
409     var path = createjs.getString(image);
410     return createjs.ImageFactory.get(
411         path, path, this, createjs.DEFAULT_TEXTURE);
412   }
413   return /** @type {HTMLImageElement} */ (image);
414 };
415 
416 /**
417  * Generates frames from images. This method divides each image of this object
418  * into partial images (whose size is specified in the frameData_ property) and
419  * adds these partial images to the frame_ property.
420  * @private
421  * @const
422  */
423 createjs.SpriteSheet.prototype.calculateFrames_ = function() {
424   if (this.frames_ || !this.frameData_) {
425     return;
426   }
427   var data = this.frameData_;
428   var width = data['width'] || 0;
429   var height = data['height'] || 0;
430   var regX = data['regX'] || 0;
431   var regY = data['regY'] || 0;
432   var count = data['count'] || 0;
433   this.frameData_ = null;
434   createjs.assert(width > 0 && height > 0);
435 
436   this.frames_ = [];
437   var images = this.images_;
438   var length = images.length;
439   for (var i = 0; i < length; ++i) {
440     var image = images[i];
441     var cols = createjs.floor(image.width / width);
442     var rows = createjs.floor(image.height / height);
443     var frames = cols * rows;
444     if (count > 0) {
445       frames = createjs.min(count, frames);
446       count -= frames;
447     }
448     for (var j = 0; j < frames; ++j) {
449       var x = (j % cols) * width;
450       var y = createjs.floor(j / cols) * height;
451       this.frames_.push(createjs.SpriteSheet.Frame.create_(
452           image, x, y, width, height, regX, regY));
453     }
454   }
455 };
456 
457 /**
458  * Parses the specified SpriteSheet object and initializes this object.
459  * @param {Object} data
460  * @private
461  * @const
462  */
463 createjs.SpriteSheet.prototype.parseData_ = function(data) {
464   /// <param type="Object" name="data"/>
465   this.framerate = data['framerate'] || 0;
466 
467   // Parse an 'images' parameter. An 'images' parameter is an array of either
468   // <image> elements or URLs.
469   if (data['images']) {
470     this.images_ = [];
471     var images = createjs.castArray(data['images']);
472     var length = images.length;
473     for (var i = 0; i < length; ++i) {
474       var image = this.getImage_(images[i]);
475       this.images_.push(image);
476       if (!image.complete) {
477         ++this.loadCount_;
478         this['complete'] = false;
479       }
480     }
481   }
482 
483   // Parse a 'frames' parameter. An 'frames' parameter is either an array of
484   // frame rectangles or an object (representing consecutive frames).
485   if (data['frames']) {
486     if (createjs.isArray(data['frames'])) {
487       this.frames_ = [];
488       var frames = createjs.getArray(data['frames']);
489       var length = frames.length;
490       for (var i = 0; i < length; ++i) {
491         var parameters = createjs.getArray(frames[i]);
492         var x = parameters[0];
493         var y = parameters[1];
494         var width = parameters[2];
495         var height = parameters[3];
496         var image = this.images_[parameters[4] ? parameters[4] : 0];
497         var regX = parameters[5] || 0;
498         var regY = parameters[6] || 0;
499         this.frames_.push(createjs.SpriteSheet.Frame.create_(
500             image, x, y, width, height, regX, regY));
501       }
502     } else {
503       // Save the 'frames' parameter and its count to parse this parameter when
504       // the hosting browser finishes loading all images used by this sprite
505       // sheet. (This parameter depends on the image sizes.)
506       this.frameData_ = data['frames'];
507       this.numFrames_ = this.frameData_['count'] || 0;
508       if (!this.loadCount_) {
509         this.calculateFrames_();
510       }
511     }
512   }
513 
514   // Parse an 'animations' parameter. An 'animations' parameter is an object.
515   // Each value is one of a number, an array, or an object, as described in the
516   // parse_() method.
517   this.animations_ = [];
518   var animations = data['animations'];
519   if (animations) {
520     this.data_ = {};
521     for (var key in animations) {
522       var animation = new createjs.SpriteSheet.Animation(key);
523       animation.parse_(key, animations[key]);
524       this.animations_.push(key);
525       this.data_[key] = animation;
526     }
527   }
528 };
529 
530 /**
531  * Returns whether this sprite sheet is ready to play.
532  * @return {boolean}
533  * @const
534  */
535 createjs.SpriteSheet.prototype.isComplete = function() {
536   /// <returns type="boolean"/>
537   return this['complete'];
538 };
539 
540 /**
541  * Returns the total number of frames in this sprite sheet.
542  * @return {number}
543  * @const
544  */
545 createjs.SpriteSheet.prototype.getFrameLength = function() {
546   /// <returns type="number"/>
547   return this.frames_ ? this.frames_.length : this.numFrames_;
548 };
549 
550 /**
551  * Returns the total number of frames in the specified animation, or in the
552  * whole sprite sheet if the animation param is omitted.
553  * @param {string} animation
554  * @return {number}
555  * @const
556  */
557 createjs.SpriteSheet.prototype.getNumFrames = function(animation) {
558   /// <param type="string" name="animation"/>
559   /// <returns type="number"/>
560   if (!animation) {
561     return this.getFrameLength();
562   }
563   var data = this.data_[animation];
564   if (!data) {
565     return 0;
566   }
567   return data.getFrameLength();
568 };
569 
570 /**
571  * Returns all available animation names.
572  * @return {Array.<string>}
573  * @const
574  */
575 createjs.SpriteSheet.prototype.getAnimations = function() {
576   /// <returns type="Array" elementType="string"/>
577   return this.animations_.slice(0);
578 };
579 
580 /**
581  * Returns an animation.
582  * @param {string} name
583  * @return {createjs.SpriteSheet.Animation}
584  * @const
585  */
586 createjs.SpriteSheet.prototype.getAnimation = function(name) {
587   /// <param type="string" name="name"/>
588   /// <returns type="createjs.SpriteSheet.Animation"/>
589   return this.data_[name];
590 };
591 
592 /**
593  * Returns an animation frame.
594  * @param {number} index
595  * @return {createjs.SpriteSheet.Frame}
596  * @const
597  */
598 createjs.SpriteSheet.prototype.getFrame = function(index) {
599   /// <param type="number" name="frameIndex"/>
600   /// <returns type="createjs.SpriteSheet.Frame"/>
601   return this.frames_ ? this.frames_[index] : null;
602 };
603 
604 /**
605  * Returns a bounding box of the specified frame.
606  * @param {number} index
607  * @param {createjs.Rectangle=} opt_rectangle
608  * @return {createjs.Rectangle}
609  * @const
610  */
611 createjs.SpriteSheet.prototype.getFrameBounds = function(index, opt_rectangle) {
612   /// <param type="number" name="index"/>
613   /// <param type="createjs.Rectangle" optional="true" name="opt_rectangle"/>
614   /// <returns type="createjs.Rectangle"/>
615   var frame = this.getFrame(index);
616   if (!frame) {
617     return null;
618   }
619   var rectangle = opt_rectangle || new createjs.Rectangle(0, 0, 0, 0);
620   return rectangle.initialize(
621       -frame.regX, -frame.regY, frame.rect.width, frame.rect.height);
622 };
623 
624 /** @override */
625 createjs.SpriteSheet.prototype.handleEvent = function(event) {
626   /// <param type="Event" name="event"/>
627   var type = event.type;
628   var image = /** @type{HTMLImageElement} */ (event.target);
629   createjs.ImageFactory.removeListeners(image, this);
630   if (type == 'load') {
631     if (--this.loadCount_ == 0) {
632       this.calculateFrames_();
633       this['complete'] = true;
634       this.dispatchNotification('complete');
635     }
636   }
637 };
638 
639 // Export the createjs.SpriteSheet object to the global namespace.
640 createjs.exportObject('createjs.SpriteSheet', createjs.SpriteSheet, {
641   // createjs.SpriteSheet methods
642   'getNumFrames': createjs.SpriteSheet.prototype.getNumFrames,
643   'getAnimations': createjs.SpriteSheet.prototype.getAnimations,
644   'getAnimation': createjs.SpriteSheet.prototype.getAnimation,
645   'getFrame': createjs.SpriteSheet.prototype.getFrame,
646   'getFrameBounds': createjs.SpriteSheet.prototype.getFrameBounds
647 });
648