1 /**
  2  * The Render Engine
  3  * Sprite
  4  *
  5  * @fileoverview A class for working with sprites.
  6  *
  7  * @author: Brett Fattori (brettf@renderengine.com)
  8  * @author: $Author: bfattori $
  9  * @version: $Revision: 1556 $
 10  *
 11  * Copyright (c) 2011 Brett Fattori (brettf@renderengine.com)
 12  *
 13  * Permission is hereby granted, free of charge, to any person obtaining a copy
 14  * of this software and associated documentation files (the "Software"), to deal
 15  * in the Software without restriction, including without limitation the rights
 16  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 17  * copies of the Software, and to permit persons to whom the Software is
 18  * furnished to do so, subject to the following conditions:
 19  *
 20  * The above copyright notice and this permission notice shall be included in
 21  * all copies or substantial portions of the Software.
 22  *
 23  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 24  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 25  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 26  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 27  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 28  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 29  * THE SOFTWARE.
 30  *
 31  */
 32 
 33 // The class this file defines and its required classes
 34 R.Engine.define({
 35     "class":"R.resources.types.Sprite",
 36     "requires":[
 37         "R.engine.PooledObject",
 38         "R.math.Rectangle2D"
 39     ]
 40 });
 41 
 42 /**
 43  * @class A 2D sprite object.  Sprites are either a single frame, or an animation composed of
 44  *        multiple frames run at a specified frame speed.  Animations can be run once, loop
 45  *        continuously, or toggle back and forth through the frames.  It is possible to start
 46  *        and stop animations, and also modify the speed at which each frame is played.
 47  *        <p/>
 48  *        In addition to the normal controls for an animation, a developer can also respond
 49  *        to events triggered on the sprite.  Linking to the events is done through
 50  *        {@link R.engine.BaseObject#addEvent}. The following are the events, and their
 51  *        descriptions:
 52  *        <ul>
 53  *           <li><tt>finished</tt> - A "run once" animation has played and completed</li>
 54  *           <li><tt>loopRestarted</tt> - A looping animation has begun a new cycle</li>
 55  *           <li><tt>toggled</tt> - A toggle animation has changed animation direction</li>
 56  *        </ul>
 57  *
 58  * @constructor
 59  * @param name {String} The name of the sprite within the resource
 60  * @param spriteObj {Object} Passed in by a {@link SpriteLoader}.  An array which defines the
 61  *                  sprite frame, and parameters.
 62  * @param spriteResource {Object} The sprite resource loaded by the {@link SpriteLoader}
 63  * @extends R.engine.BaseObject
 64  */
 65 R.resources.types.Sprite = function () {
 66     return R.engine.BaseObject.extend(/** @scope R.resources.types.Sprite.prototype */{
 67 
 68         // The type of sprite: Single or Animation
 69         type:-1,
 70 
 71         // Animation mode: loop or toggle
 72         mode:-1,
 73 
 74         // Animation frame count
 75         count:-1,
 76 
 77         // Animation speed
 78         speed:-1,
 79 
 80         // The rect which defines the sprite frame
 81         frame:null,
 82 
 83         // The image map that contains the sprite(s)
 84         image:null,
 85 
 86         // The bounding box for the sprite
 87         bbox:null,
 88 
 89         lastTime:null,
 90         sync:false,
 91         finished:false,
 92         toggleDir:null,
 93         frameNum:0,
 94         playing:false,
 95         resource:null,
 96         loader:null,
 97 
 98         /** @private */
 99         constructor:function (name, spriteObj, spriteResource, fileVersion, spriteLoader) {
100             this.base(name);
101             this.finished = false;
102             this.frameNum = 0;
103             this.playing = true;
104 
105             if (fileVersion == 1) {
106                 this.type = (spriteObj["a"] ? R.resources.types.Sprite.TYPE_ANIMATION : R.resources.types.Sprite.TYPE_SINGLE);
107             } else if (fileVersion == 2) {
108                 this.type = (spriteObj.length == 4 ? R.resources.types.Sprite.TYPE_SINGLE : R.resources.types.Sprite.TYPE_ANIMATION);
109             }
110 
111             var s;
112             if (fileVersion == 1) {
113                 s = (this.type == R.resources.types.Sprite.TYPE_ANIMATION ? spriteObj["a"] : spriteObj["f"]);
114             } else if (fileVersion == 2) {
115                 s = spriteObj;
116             }
117 
118             if (this.type == R.resources.types.Sprite.TYPE_ANIMATION) {
119                 switch (s[R.resources.types.Sprite.INDEX_TYPE]) {
120                     case "loop" :
121                         this.mode = R.resources.types.Sprite.MODE_LOOP;
122                         break;
123                     case "toggle" :
124                         this.mode = R.resources.types.Sprite.MODE_TOGGLE;
125                         break;
126                     case "once" :
127                         this.mode = R.resources.types.Sprite.MODE_ONCE;
128                         break;
129                 }
130                 if (s.length - 1 == R.resources.types.Sprite.INDEX_SYNC) {
131                     this.sync = true;
132                     this.lastTime = null;
133                     this.toggleDir = -1;	// Trust me
134                 } else {
135                     this.sync = false;
136                 }
137                 this.count = s[R.resources.types.Sprite.INDEX_COUNT];
138                 this.speed = s[R.resources.types.Sprite.INDEX_SPEED];
139             } else {
140                 this.count = 1;
141                 this.speed = -1;
142             }
143 
144             this.resource = spriteResource;
145             this.loader = spriteLoader;
146             this.image = spriteResource.image;
147             this.frame = R.math.Rectangle2D.create(s[R.resources.types.Sprite.INDEX_LEFT], s[R.resources.types.Sprite.INDEX_TOP], s[R.resources.types.Sprite.INDEX_WIDTH], s[R.resources.types.Sprite.INDEX_HEIGHT]);
148             this.bbox = R.math.Rectangle2D.create(0, 0, s[R.resources.types.Sprite.INDEX_WIDTH], s[R.resources.types.Sprite.INDEX_HEIGHT]);
149         },
150 
151         /**
152          * Destroy the sprite instance
153          */
154         destroy:function () {
155             this.bbox.destroy();
156             this.frame.destroy();
157             this.base();
158         },
159 
160         /**
161          * Release the sprite back into the pool for reuse
162          */
163         release:function () {
164             this.base();
165             this.mode = -1;
166             this.type = -1;
167             this.count = -1;
168             this.speed = -1;
169             this.frame = null;
170             this.image = null;
171             this.bbox = null;
172             this.resource = null;
173             this.loader = null;
174         },
175 
176         /**
177          * Get the resource this sprite originated from
178          * @return {Object}
179          */
180         getSpriteResource:function () {
181             return this.resource;
182         },
183 
184         /**
185          * Get the sprite loader this sprite originated from
186          * @return {R.resources.loaders.SpriteLoader}
187          */
188         getSpriteLoader:function () {
189             return this.loader;
190         },
191 
192         /**
193          * Returns <tt>true</tt> if the sprite is an animation.
194          * @return {Boolean} <tt>true</tt> if the sprite is an animation
195          */
196         isAnimation:function () {
197             return (this.type == R.resources.types.Sprite.TYPE_ANIMATION);
198         },
199 
200         /**
201          * Returns <tt>true</tt> if the sprite is an animation and loops.
202          * @return {Boolean} <tt>true</tt> if the sprite is an animation and loops
203          */
204         isLoop:function () {
205             return (this.isAnimation() && this.mode == R.resources.types.Sprite.MODE_LOOP);
206         },
207 
208         /**
209          * Returns <tt>true</tt> if the sprite is an animation and toggles.
210          * @return {Boolean} <tt>true</tt> if the sprite is an animation and toggles
211          */
212         isToggle:function () {
213             return (this.isAnimation() && this.mode == R.resources.types.Sprite.MODE_TOGGLE);
214         },
215 
216         /**
217          * Returns <tt>true</tt> if the sprite is an animation and plays once.
218          * @return {Boolean} <tt>true</tt> if the sprite is an animation and plays once
219          */
220         isOnce:function () {
221             return (this.isAnimation() && this.mode == R.resources.types.Sprite.MODE_ONCE);
222         },
223 
224         /**
225          * Get the number of frames in the sprite.
226          * @return {Number}
227          */
228         getFrameCount:function () {
229             return this.count;
230         },
231 
232         /**
233          * Get the frame speed of the animation in milliseconds, or -1 if it's a single frame.
234          * @return {Number}
235          */
236         getFrameSpeed:function () {
237             return this.speed;
238         },
239 
240         /**
241          * For animated sprites, play the animation if it is stopped.
242          */
243         play:function () {
244             this.playing = true;
245         },
246 
247         /**
248          * For animated sprites, stop the animation if it is playing.
249          */
250         stop:function () {
251             this.playing = false;
252         },
253 
254         /**
255          * For animated sprites, reset the animation to frame zero.
256          */
257         reset:function () {
258             this.frameNum = 0;
259         },
260 
261         /**
262          * For animated sprites, go to a particular frame number.
263          * @param frameNum {Number} The frame number to jump to
264          */
265         gotoFrame:function (frameNum) {
266             this.frameNum = (frameNum < 0 ? 0 : (frameNum >= this.count ? this.count - 1 : frameNum));
267         },
268 
269         /**
270          * Get the bounding box for the sprite.
271          * @return {R.math.Rectangle2D} The bounding box which contains the entire sprite
272          */
273         getBoundingBox:function () {
274             return this.bbox;
275         },
276 
277         /**
278          * Gets the frame of the sprite. The frame is the rectangle defining what
279          * portion of the image map the sprite frame occupies, given the specified time.
280          *
281          * @param time {Number} Current world time
282          * @param dt {Number} The delta between the world time and the last time the world was updated
283          *          in milliseconds.
284          * @return {R.math.Rectangle2D} A rectangle which defines the frame of the sprite in
285          *         the source image map.
286          */
287         getFrame:function (time, dt) {
288             if (!this.isAnimation()) {
289                 return R.math.Rectangle2D.create(this.frame);
290             } else {
291                 var f = R.math.Rectangle2D.create(this.frame);
292                 var fn = this.calcFrameNumber(time, dt);
293                 return f.offset(f.w * fn, 0);
294             }
295         },
296 
297         /**
298          * Calculate the frame number for the type of animation.
299          * @param time {Number} The current world time
300          * @param dt {Number} The delta between the world time and the last time the world was updated
301          *          in milliseconds.
302          * @private
303          */
304         calcFrameNumber:function (time, dt) {
305             if (!this.playing) {
306                 return this.frameNum;
307             }
308 
309             if (this.sync) {
310                 // Synchronized animations
311 
312                 if (this.lastTime === null) {
313                     // Note the time when the first frame is requested and just return frame zero
314                     this.lastTime = time;
315                     return 0;
316                 }
317 
318                 // How much time has elapsed since the last frame update?
319                 if (dt > this.speed) {
320                     // Engine is lagging, skip to correct frame
321                     this.frameNum += (Math.floor(dt / this.speed) * this.toggleDir);
322                 } else {
323                     this.frameNum += (time - this.lastTime > this.speed ? this.toggleDir : 0);
324                 }
325 
326                 // Modify the frame number for the animation mode
327                 if (this.isOnce()) {
328                     // Play animation once from beginning to end
329                     if (this.frameNum >= this.count) {
330                         this.frameNum = this.count - 1;
331                         if (!this.finished) {
332                             // Call event when finished
333                             this.finished = true;
334                             this.triggerEvent("finished");
335                         }
336                     }
337                 } else if (this.isLoop()) {
338                     if (this.frameNum > this.count - 1) {
339                         // Call event when loop restarts
340                         this.frameNum = 0;
341                         this.triggerEvent("loopRestarted");
342                     }
343                 } else {
344                     if (this.frameNum == this.count - 1 || this.frameNum == 0) {
345                         // Call event when animation toggles
346                         this.toggleDir *= -1;
347                         this.frameNum += this.toggleDir;
348                         this.triggerEvent("toggled");
349                     }
350                 }
351 
352                 // Remember the last time a frame was requested
353                 this.lastTime = time;
354 
355             } else {
356                 // Unsynchronized animations
357                 var lastFrame = this.frameNum;
358                 if (this.isLoop()) {
359                     this.frameNum = Math.floor(time / this.speed) % this.count;
360                     if (this.frameNum < lastFrame) {
361                         this.triggerEvent("loopRestarted");
362                     }
363                 } else if (this.isOnce() && !this.finished) {
364                     this.frameNum = Math.floor(time / this.speed) % this.count;
365                     if (this.frameNum < lastFrame) {
366                         this.finished = true;
367                         this.frameNum = this.count - 1;
368                         this.triggerEvent("finished");
369                     }
370                 } else if (this.isToggle()) {
371                     this.frameNum = Math.floor(time / this.speed) % (this.count * 2);
372                     if (this.frameNum > this.count - 1) {
373                         this.frameNum = this.count - (this.frameNum - (this.count - 1));
374                         this.triggerEvent("toggled");
375                     }
376                 }
377             }
378 
379             return this.frameNum;
380         },
381 
382         /**
383          * Set the speed, in milliseconds, that an animation runs at.  If the sprite is
384          * not an animation, this has no effect.
385          *
386          * @param speed {Number} The number of milliseconds per frame of an animation
387          */
388         setSpeed:function (speed) {
389             if (speed >= 0) {
390                 this.speed = speed;
391             }
392         },
393 
394         /**
395          * Get the number of milliseconds each frame is displayed for an animation
396          * @return {Number} The milliseconds per frame
397          */
398         getSpeed:function () {
399             return this.speed;
400         },
401 
402         /**
403          * The source image loaded by the {@link SpriteLoader} when the sprite was
404          * created.
405          * @return {HTMLImage} The source image the sprite is contained within
406          */
407         getSourceImage:function () {
408             return this.image;
409         },
410 
411         onSpriteLoopRestart:function () {
412         }
413 
414     }, /** @scope R.resources.types.Sprite.prototype */{
415         /**
416          * Gets the class name of this object.
417          * @return {String} The string "R.resources.types.Sprite"
418          */
419         getClassName:function () {
420             return "R.resources.types.Sprite";
421         },
422 
423         /** The sprite animation loops
424          * @type {Number}
425          */
426         MODE_LOOP:0,
427 
428         /** The sprite animation toggles - Plays from the first to the last frame
429          *  then plays backwards to the first frame and repeats.
430          * @type {Number}
431          */
432         MODE_TOGGLE:1,
433 
434         /** The sprite animation plays once from the beginning then stops at the last frame
435          * @type {Number}
436          */
437         MODE_ONCE:2,
438 
439         /** The sprite is a single frame
440          * @type {Number}
441          */
442         TYPE_SINGLE:0,
443 
444         /** The sprite is an animation
445          * @type {Number}
446          */
447         TYPE_ANIMATION:1,
448 
449         /** The field in the sprite definition file for the left pixel of the sprite frame
450          * @private
451          */
452         INDEX_LEFT:0,
453 
454         /** The field in the sprite definition file for the top pixel of the sprite frame
455          * @private
456          */
457         INDEX_TOP:1,
458 
459         /** The field in the sprite definition file for the width of the sprite frame
460          * @private
461          */
462         INDEX_WIDTH:2,
463 
464         /** The field in the sprite definition file for the height of the sprite frame
465          * @private
466          */
467         INDEX_HEIGHT:3,
468 
469         /** The field in the sprite definition file for the count of frames in the sprite
470          * @private
471          */
472         INDEX_COUNT:4,
473 
474         /** The field in the sprite definition file for the speed in milliseconds that the sprite animates
475          * @private
476          */
477         INDEX_SPEED:5,
478 
479         /** The field in the sprite definition file for the type of sprite animation
480          * @private
481          */
482         INDEX_TYPE:6,
483 
484         /**
485          * Use synchronized frame starts and no skipping.
486          * @private
487          */
488         INDEX_SYNC:7
489 
490     });
491 };
492