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