API Docs for: v2.1.0
Show:

File: src\node\class.node.animatedSprite.js

/*
 * Copyright (c) 2014 Gwennael Buchet
 *
 * License/Terms of Use
 *
 * Permission is hereby granted, free of charge and for the term of intellectual property rights on the Software, to any
 * person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify
 * and propagate free of charge, anywhere in the world, all or part of the Software subject to the following mandatory conditions:
 *
 *   •    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 *  Any failure to comply with the above shall automatically terminate the license and be construed as a breach of these
 *  Terms of Use causing significant harm to Gwennael Buchet.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 *  WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
 *  OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 *  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 *  Except as contained in this notice, the name of Gwennael Buchet shall not be used in advertising or otherwise to promote
 *  the use or other dealings in this Software without prior written authorization from Gwennael Buchet.
 *
 *  These Terms of Use are subject to French law.
 */

/**
 * A CGSGNodeSprite represent an animated sprite, with all animations in the image
 *
 * @class CGSGNodeSprite
 * @extends CGSGNode
 * @module Node
 * @constructor
 * @param {Number} x Relative position on X
 * @param {Number} y Relative position on Y
 * @param {String} urlImage URL of the image. Can be null to be set later
 * @type {CGSGNodeSprite}
 * @author Gwennael Buchet (gwennael.buchet@gmail.com)
 */
var CGSGNodeSprite = CGSGNode.extend(
    {
        initialize : function(x, y, urlImage) {
            this._super(x, y);

            /**
             * @property classType
             * @type {String}
             */
            this.classType = "CGSGNodeSprite";

            /**
             * array of animations
             * @property listAnimations
             * @type {Array}
             */
            this.listAnimations = [];
            /**
             * @property currentAnimation
             * @default null
             * @type {Object}
             */
            this.currentAnimation = null;
            /**
             * @property isProportionalResize
             * @default true
             * @type {Boolean}
             */
            this.isProportionalResize = true;

            /**
             * Current animated frame
             * @property _currentFrame
             * @private
             * @type {Number}
             */
            this._currentFrame = 0;
            /**
             * Whether the sprite is being animated or not
             * @property _isPlaying
             * @private
             * @readonly
             * @type {Boolean}
             */
            this._isPlaying = false;

            /**
             * Number of loops for the current animation. if -1 then it's an infinite loop.
             * @property _numberOfLoops
             * @private
             * @type {Number}
             */
            this._numberOfLoops = 1;
            /**
             * Current loop number
             * @property _currentLoop
             * @private
             * @type {Number}
             */
            this._currentLoop = 0;

            /**
             * URL of the image
             * @property _urlImage
             * @type {String}
             * @private
             */
            this._urlImage = urlImage;
            /**
             * The image object itself
             * @property _img
             * @type {Image}
             * @private
             */
            this._img = new Image();

            /**
             * @property _isLoaded
             * @type {Boolean}
             * @private
             */
            this._isLoaded = false;

            /**
             * Handler function fired when the image is loaded
             * @property onLoadEnd
             * @default null
             * @type {Function}
             */
            this.onLoadEnd = null;
            /**
             * Handler function fired after an animation loop is ended
             * @property onAnimationEnd
             * @default null
             * @type {Function}
             */
            this.onAnimationEnd = null;
            /**
             * Handler function fired before an animation loop start
             * @property onAnimationStart
             * @default null
             * @type {Function}
             */
            this.onAnimationStart = null;

            ///// INITIALIZATION //////
            //finally load the image
            if (this._urlImage !== null && this._urlImage != "") {
                this._img.onload = this._createDelegate(this, this._onImageLoaded);
                this._img.src = this._urlImage;
            }
        },

        /**
         * Used to call delegate method when the image is finally loaded
         * @private
         * @method _createDelegate
         * @param objectContext
         * @param delegateMethod
         * @return {Function}
         */
        _createDelegate : function(objectContext, delegateMethod) {
            return function() {
                return delegateMethod.call(objectContext);
            }
        },

        /**
         * fired when the image is loaded.
         * Check the dimension of the image and fired the onLoadEnd event
         * @protected
         * @method _onImageLoaded
         */
        _onImageLoaded : function() {
            this.checkDimension();
            this._isLoaded = true;
            //this._initShape();
            if (this.onLoadEnd !== null) {
                this.onLoadEnd();
            }
            this.invalidate();
        },

        /**
         * To be overrided when the image failed to load
         * @method _onImageError
         * @protected
         */
        _onImageError : function() {
            console.log("Error while loading image : " + this._urlImage);
        },

        /**
         * Check the true dimension of the image and fill the this.dimension property with it,
         * only if dimension is not already defined in the constructor
         * @method checkDimension
         */
        checkDimension : function() {
            //if no width or height are specified in the constructor
            if (this.dimension.width <= 0 && this.dimension.height <= 0) {
                this.dimension.width = this._img.width;
                this.dimension.height = this._img.height;
            }
        },

        /**
         * Set the image for this animated sprite.
         * @example
         *
         * this.pingoo = new CGSGNodeSprite(60, 60, null, this.context);
         * this.pingoo.isDraggable = true;
         * //name, speed, frames, sliceX, sliceY, width, height, framesPerLine
         * this.pingoo.addAnimation("front", 6, 4, 476, 0, 34, 34, 4);
         * this.pingoo.play("front", null);
         *
         * //now, load the image containing the sprite sheet.
         * //The affectation to the sprite will be done in the loaded handler function
         * this.spriteSheet = new Image();
         * this.spriteSheet.onload = this.onImageLoaded();
         * this.spriteSheet.src = "images/board.png";
         *
         * onImageLoaded : function () {
         *     this.pingoo.setImage(this.spriteSheet);
         *     this.numbers.setImage(this.spriteSheet);
         *   this.water.setImage(this.spriteSheet);
         * }
         *
         * @method setImage
         * @param {Image} newImage new Image object. Must be an already loaded one
         */
        setImage : function(newImage) {
            this._img = newImage;
            if (cgsgExist(this._img)) {
                this._urlImage = this._img.src;
                this._isLoaded = true;
                this.invalidate();
            }
        },

        /**
         * Go to the next frame of the current animation
         * @public
         * @method goToNextFrame
         */
        goToNextFrame : function() {
            this._currentFrame += 1.0 / this.currentAnimation.speed;
            var isEndOfLoop = false;
            if (this._currentFrame >= this.currentAnimation.frames) {
                isEndOfLoop = true;
            }

            //end of animation if the previous modulo returns to frame 0
            if (isEndOfLoop) {
                //if this is the end ("... my only friend.. la la...") of the loop, stop it
                if (this._numberOfLoops < 0 || this._currentLoop < this._numberOfLoops) {
                    this._currentLoop++;
                    this.goToFirstFrame();
                }
                else {
                    this.goToLastFrame();
                    this.stop();
                }
            }

            this.invalidate();
        },

        /**
         * Go to the previous frame of the current animation
         * @public
         * @method goToPreviousFrame
         */
        goToPreviousFrame : function() {
            this._currentFrame -= this.currentAnimation.speed;
            var isStartOfLoop = false;
            if (this._currentFrame < 0) {
                isStartOfLoop = true;
            }

            //end of animation if the previous modulo returns to frame 0
            if (isStartOfLoop) {
                //if this is the end ("... my only friend.. la la...") of the loop, stop it
                if (this._numberOfLoops <= 0 || this._currentLoop >= 0) {
                    this._currentLoop--;
                    this.goToLastFrame();
                }
                else {
                    this.goToFirstFrame();
                    this.stop();
                }
            }

            this.invalidate();
        },

        /**
         * Go to the first frame of the current loop
         * @public
         * @method goToFirstFrame
         */
        goToFirstFrame : function() {
            this._currentFrame = 0;
            this.invalidate();
        },

        /**
         * Go to the last frame of the current loop
         * @public
         * @method goToLastFrame
         */
        goToLastFrame : function() {
            this._currentFrame = this.currentAnimation.frames - 1;
            this.invalidate();
        },

        /**
         * Must be defined to allow the scene graph to render the image nodes
         * @protected
         * @param c {CanvasRenderingContext2D} The context to render on
         * @method render
         * */
        render : function(c) {
            if (this._isLoaded && this._img.src != "") {

                //compute the current slice of the current sprite
                if (cgsgExist(this.currentAnimation)) {
                    c.globalAlpha = this.globalAlpha;

                    var slice = this.currentAnimation.slices[Math.floor(this._currentFrame)];

                    c.drawImage(
                        this._img, // image
                        slice.x,
                        slice.y, // start position on the image
                        this.currentAnimation.width,
                        this.currentAnimation.height, // dimension of the sprite
                        0,
                        0,
                        // position on the screen. let it to [0,0] because the 'beforeRender' function will translate the image
                        this.dimension.width,
                        this.dimension.height
                    );

                    if (this.lineWidth > 0) {
                        //Next lines are already managed by CGSGNode.
                        //I let it here just to provide an example
                        //context.lineWidth = this.lineWidth;
                        //context.strokeStyle = this.lineColor;
                        c.stroke();
                    }

                    //go to next frame
                    if (this._isPlaying) {
                        this.goToNextFrame();
                        this.invalidate();
                    }
                }
            }
        },

        /**
         * Return position x and y in the image for the slice of the animation and frame passed in parameter.
         * @private
         * @method _getSlice
         * @param {Number} frame
         * @param {Object} animation
         * @return {Object}
         */
        _getSlice : function(frame, animation) {
            var frameX = frame % animation.framesPerLine;
            var frameY = Math.floor(frame / animation.framesPerLine);

            var x = animation.sliceX + frameX * animation.width;
            var y = animation.sliceY + frameY * animation.height;

            return {x : x, y : y};
        },

        /**
         * Add an animation for this sprite
         * @public
         * @method addAnimation
         * @param {String} name Name for this animation
         * @param {Number} speed Number of frames between 2 steps
         * @param {Number} frames Number of frame for this animation
         * @param {Number} sliceX slice position inside the image for this animation
         * @param {Number} sliceY slice position inside the image for this animation
         * @param {Number} width width of 1 frame
         * @param {Number} height height of 1 frame
         * @param {Number} framesPerLine Number of frames per line in the image
         */
        addAnimation : function(name, speed, frames, sliceX, sliceY, width, height, framesPerLine) {
            var animation = {
                name          : name,
                speed         : speed,
                frames        : frames,
                sliceX        : sliceX,
                sliceY        : sliceY,
                width         : width,
                height        : height,
                framesPerLine : framesPerLine,
                slices        : []
            };

            for (var f = 0 ; f < frames ; f++) {
                animation.slices.push(this._getSlice(f, animation));
            }

            this.listAnimations.push(animation);
            if (this.listAnimations.length == 1) {
                this.currentAnimation = animation;
            }

            this.dimension.width = width;
            this.dimension.height = height;
        },

        /**
         * Start an animation
         * @public
         * @method play
         * @param {String} animationName Name of the animation to start
         * @param {Number} loop number of animation loop. Can be null or negative to set infinite loop
         * @return {Boolean} true if the animation exists; false otherwise
         */
        play : function(animationName, loop) {
            if (loop === undefined || loop === null) {
                loop = -1;
            }

            this.currentAnimation = null;
            for (var i = 0 ; i < this.listAnimations.length ; i++) {
                if (this.listAnimations[i].name === animationName) {
                    this.currentAnimation = this.listAnimations[i];
                    this.dimension.width = this.currentAnimation.width;
                    this.dimension.height = this.currentAnimation.height;
                    this.reset();
                    this._numberOfLoops = loop;
                    this._isPlaying = true;
                    this.resizeTo(this.currentAnimation.width, this.currentAnimation.height);
                    if (this.onAnimationStart !== null) {
                        this.onAnimationStart({animationName : animationName, loop : loop});
                    }
                    return true;
                }
            }
            return false;
        },

        /**
         * Stop the current animation and stay on the current frame
         * @public
         * @method stop
         */
        stop : function() {
            this._isPlaying = false;
            if (this.onAnimationEnd !== null) {
                this.onAnimationEnd({animationName : this.currentAnimation.name, loop : this._currentLoop, frame : this._currentFrame});
            }
            this.invalidate();
        },

        /**
         * return to the first frame of the first loop of the current animation
         * @public
         * @method reset
         */
        reset : function() {
            this._currentFrame = 0;
            this._currentLoop = 1;
            this.invalidate();
        },

        /**
         * @public
         * @method copy
         * @return {CGSGNodeSprite} a copy of this node
         */
        copy : function() {
            var node = new CGSGNodeSprite(this.position.x, this.position.y, this._urlImage);
            //call the super method
            node = this._super(node);

            node.listAnimations = [];
            for (var a = 0 ; a < this.listAnimations.length ; a++) {
                node.addAnimation(this.listAnimations[a].name, this.listAnimations[a].speed,
                                  this.listAnimations[a].frames, this.listAnimations[a].sliceX,
                                  this.listAnimations[a].sliceY, this.listAnimations[a].width,
                                  this.listAnimations[a].height, this.listAnimations[a].framesPerLine);
            }

            node.currentAnimation = this.currentAnimation;
            node.isProportionalResize = this.isProportionalResize;

            node._currentFrame = this._currentFrame;
            node._isPlaying = this._isPlaying;
            node._numberOfLoops = this._numberOfLoops;
            node._currentLoop = this._currentLoop;
            node._img = this._img;
            node._isLoaded = this._isLoaded;

            node.onLoadEnd = this.onLoadEnd;
            node.onAnimationEnd = this.onAnimationEnd;
            node.onAnimationStart = this.onAnimationStart;

            if (this._urlImage !== null && this._urlImage != "") {
                node._img.onload = node._createDelegate(node, node._onImageLoaded, node.context);
                node._img.src = node._urlImage;
            }

            return node;
        }
    }
);