PIXIAnimation.js

/*global platypus */
import {AnimatedSprite, BaseTexture, Container, Point, Rectangle, Sprite, Texture} from 'pixi.js';
import {arrayCache, greenSlice} from './utils/array.js';
import Data from './Data.js';

export default (function () {
    var MAX_KEY_LENGTH_PER_IMAGE = 128,
        animationCache = {},
        baseTextureCache = {},
        doNothing = function () {},
        emptyFrame = Texture.EMPTY,
        regex = /[\[\]{},-]/g,
        getBaseTextures = function (images) {
            var i = 0,
                bts = arrayCache.setUp(),
                asset = null,
                assetCache = platypus.assetCache,
                btCache = baseTextureCache,
                path = null;
            
            for (i = 0; i < images.length; i++) {
                path = images[i];
                if (typeof path === 'string') {
                    if (!btCache[path]) {
                        asset = assetCache.get(path);
                        if (!asset) {
                            platypus.debug.warn('"' + path + '" is not a loaded asset.');
                            break;
                        }
                        btCache[path] = new BaseTexture(asset);
                    }
                    bts.push(btCache[path]);
                } else {
                    bts.push(new BaseTexture(path));
                }
            }
            
            return bts;
        },
        getTexturesCacheId = function (spriteSheet) {
            var i = 0;
            
            if (spriteSheet.id) {
                return spriteSheet.id;
            }
            
            for (i = 0; i < spriteSheet.images.length; i++) {
                if (typeof spriteSheet.images[i] !== 'string') {
                    return '';
                }
            }
            
            spriteSheet.id = JSON.stringify(spriteSheet).replace(regex, '');

            return spriteSheet.id;
        },
        getDefaultAnimation = function (length, textures) {
            var frames = arrayCache.setUp(),
                i = 0;
            
            for (i = 0; i < length; i++) {
                frames.push(textures[i] || emptyFrame);
            }
            return Data.setUp(
                "id", "default",
                "frames", frames,
                "next", "default",
                "speed", 1
            );
        },
        standardizeAnimations = function (def, textures) {
            var animation = '',
                anims = Data.setUp(),
                i = 0,
                frames = null,
                key = '';
            
            for (key in def) {
                if (def.hasOwnProperty(key)) {
                    animation = def[key];
                    frames = greenSlice(animation.frames);
                    i = frames.length;
                    while (i--) {
                        frames[i] = textures[frames[i]] || emptyFrame;
                    }
                    anims[key] = Data.setUp(
                        "id", key,
                        "frames", frames,
                        "next", animation.next,
                        "speed", animation.speed
                    );
                }
            }

            if (!anims.default) {
                // Set up a default animation that plays through all frames
                anims.default = getDefaultAnimation(textures.length, textures);
            }
            
            return anims;
        },
        getAnimations = function (spriteSheet) {
            var i = 0,
                anims    = null,
                frame    = null,
                frames   = spriteSheet.frames,
                images   = spriteSheet.images,
                textures = arrayCache.setUp(),
                bases    = getBaseTextures(images);

            // Set up texture for each frame
            for (i = 0; i < frames.length; i++) {
                frame = frames[i];
                textures.push(new Texture(bases[frame[4]], new Rectangle(frame[0], frame[1], frame[2], frame[3]), null, null, 0, new Point((frame[5] || 0) / frame[2], (frame[6] || 0) / frame[3])));
            }

            // Set up animations
            anims = standardizeAnimations(spriteSheet.animations, textures);

            // Set up a default animation that plays through all frames
            if (!anims.default) {
                anims.default = getDefaultAnimation(textures.length, textures);
            }
            
            arrayCache.recycle(bases);
            
            return Data.setUp(
                "textures", textures,
                "animations", anims
            );
        },
        cacheAnimations = function (spriteSheet, cacheId) {
            var i = 0,
                anims    = null,
                frame    = null,
                frames   = spriteSheet.frames,
                images   = spriteSheet.images,
                textures = arrayCache.setUp(),
                bases    = getBaseTextures(images);

            // Set up texture for each frame
            for (i = 0; i < frames.length; i++) {
                frame = frames[i];
                textures.push(new Texture(bases[frame[4]], new Rectangle(frame[0], frame[1], frame[2], frame[3]), null, null, 0, new Point((frame[5] || 0) / frame[2], (frame[6] || 0) / frame[3])));
            }

            // Set up animations
            anims = standardizeAnimations(spriteSheet.animations, textures);

            arrayCache.recycle(bases);
            
            return Data.setUp(
                "textures", textures,
                "animations", anims,
                "viable", 1,
                "cacheId", cacheId
            );
        },
        /**
         * This class plays animation sequences of frames and mimics the syntax required for creating CreateJS Sprites, allowing CreateJS Sprite Sheet definitions to be used with PixiJS.
         *
         * @memberof platypus
         * @class PIXIAnimation
         * @param {Object} spriteSheet JSON sprite sheet definition.
         * @param {string} animation The name of the animation to start playing.
         */
        PIXIAnimation = function (spriteSheet, animation) {
            var FR = 60,
                cacheId = getTexturesCacheId(spriteSheet),
                cache = (cacheId ? animationCache[cacheId] : null),
                speed = (spriteSheet.framerate || FR) / FR;

            if (!cacheId) {
                cache = getAnimations(spriteSheet);
            } else if (!cache) {
                cache = animationCache[cacheId] = cacheAnimations(spriteSheet, cacheId);
                this.cacheId = cacheId;
            } else {
                cache.viable += 1;
                this.cacheId = cacheId;
            }

            Container.call(this); //, cache.textures[0].texture
        
            /**
            * @private
            */
            this._animations = {};
            for (const key in cache.animations) {
                if (cache.animations[key].frames.length === 1) {
                    this._animations[key] = new Sprite(cache.animations[key].frames[0]);
                } else {
                    const anim = this._animations[key] = new AnimatedSprite(cache.animations[key].frames);

                    anim.animationSpeed = speed * cache.animations[key].speed;
                    anim.onComplete = anim.onLoop = function (animation, properties) {
                        if (this.onComplete) {
                            this.onComplete(animation);
                        }
                        if (properties.next) {
                            this.gotoAndPlay(properties.next);
                        }
                    }.bind(this, key, cache.animations[key]);
                    anim.updateAnchor = true;
                }
            }
            
            this._animation = null;
        
            /**
            * The speed that the PIXIAnimation will play at. Higher is faster, lower is slower
            *
            * @member {number}
            * @default 1
            */
            this.animationSpeed = speed;

            /**
             * The currently playing animation name.
             *
             * @property currentAnimation
             * @default ""
             * @type String
             */
            this.currentAnimation = null;
        
            /**
            * Indicates if the PIXIAnimation is currently playing
            *
            * @member {boolean}
            * @readonly
            */
            this.playing = false;
            
            this._visible = true;
            
            this._updating = false;

            /*
            * Updates the object transform for rendering
            * @private
            */
            this.update = doNothing;

            // Set up initial playthrough.
            this.gotoAndPlay(animation);
        },
        prototype = PIXIAnimation.prototype = Object.create(Container.prototype);
    
    PIXIAnimation.prototype.constructor = PIXIAnimation;
    
    Object.defineProperties(prototype, {
        /**
        * The visibility of the sprite.
        *
        * @property visible
        * @memberof platypus.PIXIAnimation.prototype
        */
        visible: {
            get: function () {
                return this._visible;
            },
            set: function (value) {
                this._visible = value;
            }
        },
        
        /**
        * The PIXIAnimations paused state. If paused, the animation doesn't update.
        *
        * @property paused
        * @memberof platypus.PIXIAnimation.prototype
        */
        paused: {
            get: function () {
                return !this.playing;
            },
            set: function (value) {
                if ((value && this.playing) || (!value && !this.playing)) {
                    this.playing = !value;
                }
            }
        }
    
    });
    
    /**
    * Stops the PIXIAnimation
    *
    * @method platypus.PIXIAnimation#stop
    */
    prototype.stop = function () {
        this.paused = true;
    };
    
    /**
    * Plays the PIXIAnimation
    *
    * @method platypus.PIXIAnimation#play
    */
    prototype.play = function () {
        this.paused = false;
    };
    
    /**
    * Stops the PIXIAnimation and goes to a specific frame
    *
    * @method platypus.PIXIAnimation#gotoAndStop
    * @param animation {number} frame index to stop at
    */
    prototype.gotoAndStop = function (animation) {
        this.stop();
        if (this._animation && this._animation.stop) {
            this._animation.stop();
        }
    
        this._animation = this._animations[animation];
        if (!this._animation) {
            this._animation = this._animations.default;
        }
        this.removeChildren();
        this.addChild(this._animation);
    };
    
    /**
    * Goes to a specific frame and begins playing the PIXIAnimation
    *
    * @method platypus.PIXIAnimation#gotoAndPlay
    * @param animation {string} The animation to begin playing.
    * @param [loop = true] {Boolean} Whether this animation should loop.
    * @param [restart = true] {Boolean} Whether to restart the animation if it's currently playing.
    */
    prototype.gotoAndPlay = function (animation, loop = true, restart = true) {
        if ((this.currentAnimation !== animation) || restart) {
            if (this._animation && this._animation.stop) {
                this._animation.stop();
            }
            this._animation = this._animations[animation];
            this.currentAnimation = animation;
            if (!this._animation) {
                this._animation = this._animations.default;
                this.currentAnimation = 'default';
            }
            this.removeChildren();
            this.addChild(this._animation);
        }

        this._animation.loop = loop;
        
        if (this._animation.play) {
            this._animation.play();
        }
        this.play();
    };
    
    /**
    * Returns whether a particular animation is available.
    *
    * @method platypus.PIXIAnimation#has
    * @param animation {string} The animation to check.
    */
    prototype.has = function (animation) {
        return !!this._animations[animation];
    };
    
    /**
     * Stops the PIXIAnimation and destroys it
     *
     * @method platypus.PIXIAnimation#destroy
     */
    prototype.destroy = function () {
        var key = '';
        
        this.stop();
        if (this._animation && this._animation.stop) {
            this._animation.stop();
        }
        Container.prototype.destroy.call(this);
        if (this.cacheId) {
            animationCache[this.cacheId].viable -= 1;
            if (animationCache[this.cacheId].viable <= 0) {
                arrayCache.recycle(animationCache[this.cacheId].textures);
                for (key in animationCache[this.cacheId].animations) {
                    if (animationCache[this.cacheId].animations.hasOwnProperty(key)) {
                        arrayCache.recycle(animationCache[this.cacheId].animations[key].frames);
                    }
                }
                delete animationCache[this.cacheId];
            }
        }
    };
    
    PIXIAnimation.EmptySpriteSheet = {
        framerate: 60,
        frames: [],
        images: [],
        animations: {},
        recycleSpriteSheet: function () {
            // We don't recycle this sprite sheet.
        }
    };
    
    /**
     * This method formats a provided value into a valid PIXIAnimation Sprite Sheet. This includes accepting the EaselJS spec, strings mapping to Platypus sprite sheets, or arrays of either.
     *
     * @method platypus.PIXIAnimation.formatSpriteSheet
     * @param spriteSheet {String|Array|Object} The value to cast to a valid Sprite Sheet.
     * @return {Object}
     */
    PIXIAnimation.formatSpriteSheet = (function () {
        var imageParts = /([\w-]+)\.(\w+)$/,
            addAnimations = function (source, destination, speedRatio, firstFrameIndex, id) {
                var key = '';
                
                for (key in source) {
                    if (source.hasOwnProperty(key)) {
                        if (destination[key]) {
                            arrayCache.recycle(destination[key].frames);
                            destination[key].recycle();
                            platypus.debug.olive('PIXIAnimation "' + id + '": Overwriting duplicate animation for "' + key + '".');
                        }
                        destination[key] = formatAnimation(key, source[key], speedRatio, firstFrameIndex);
                    }
                }
            },
            addFrameObject = function (source, destination, firstImageIndex, bases) {
                var i = 0,
                    fw = source.width,
                    fh = source.height,
                    rx = source.regX || 0,
                    ry = source.regY || 0,
                    w = 0,
                    h = 0,
                    x = 0,
                    y = 0;
                
                for (i = 0; i < bases.length; i++) {
                    
                    // Subtract the size of a frame so that margin slivers aren't returned as frames.
                    w = bases[i].realWidth - fw;
                    h = bases[i].realHeight - fh;
                    
                    for (y = 0; y <= h; y += fh) {
                        for (x = 0; x <= w; x += fw) {
                            destination.push([x, y, fw, fh, i + firstImageIndex, rx, ry]);
                        }
                    }
                }
            },
            addFrameArray = function (source, destination, firstImageIndex) {
                var frame = null,
                    i = 0;
                
                for (i = 0; i < source.length; i++) {
                    frame = source[i];
                    destination.push(arrayCache.setUp(
                        frame[0],
                        frame[1],
                        frame[2],
                        frame[3],
                        frame[4] + firstImageIndex,
                        frame[5],
                        frame[6]
                    ));
                }
            },
            createId = function (images) {
                var i = images.length,
                    id = '',
                    segment = '',
                    separator = '';

                while (i--) {
                    segment = images[i].src || images[i];
                    id += separator + segment.substring(0, MAX_KEY_LENGTH_PER_IMAGE);
                    separator = ',';
                }

                return id;
            },
            format = function (source, destination) {
                var bases = null,
                    dAnims = destination.animations,
                    dImages = destination.images,
                    dFR = destination.framerate || 60,
                    dFrames = destination.frames,
                    i = 0,
                    images = arrayCache.setUp(),
                    firstImageIndex = dImages.length,
                    firstFrameIndex = dFrames.length,
                    sAnims = source.animations,
                    sImages = source.images,
                    sFR = source.framerate || 60,
                    sFrames = source.frames;
                
                // Set up id
                if (destination.id) {
                    destination.id += ';' + (source.id || createId(source.images));
                } else {
                    destination.id = source.id || createId(source.images);
                }
                
                // Set up images array
                for (i = 0; i < sImages.length; i++) {
                    images.push(formatImages(sImages[i]));
                    dImages.push(images[i]);
                }

                // Set up frames array
                if (Array.isArray(sFrames)) {
                    addFrameArray(sFrames, dFrames, firstImageIndex);
                } else {
                    bases = getBaseTextures(images);
                    addFrameObject(sFrames, dFrames, firstImageIndex, bases);
                    arrayCache.recycle(bases);
                }
                
                // Set up animations object
                addAnimations(sAnims, dAnims, sFR / dFR, firstFrameIndex, destination.id);
                
                arrayCache.recycle(images);
                
                return destination;
            },
            formatAnimation = function (key, animation, speedRatio, firstFrameIndex) {
                var i = 0,
                    first = 0,
                    frames = arrayCache.setUp(),
                    last = 0;
                
                if (typeof animation === 'number') {
                    frames.push(animation + firstFrameIndex);
                    return Data.setUp(
                        "frames", frames,
                        "next", key,
                        "speed", speedRatio
                    );
                } else if (Array.isArray(animation)) {
                    first = animation[0] || 0;
                    last = (animation[1] || first) + 1 + firstFrameIndex;
                    first += firstFrameIndex;
                    for (i = first; i < last; i++) {
                        frames.push(i);
                    }
                    return Data.setUp(
                        "frames", frames,
                        "next", animation[2] || key,
                        "speed", (animation[3] || 1) * speedRatio
                    );
                } else {
                    for (i = 0; i < animation.frames.length; i++) {
                        frames.push(animation.frames[i] + firstFrameIndex);
                    }
                    return Data.setUp(
                        "frames", frames,
                        "next", animation.next || key,
                        "speed", (animation.speed || 1) * speedRatio
                    );
                }
            },
            formatImages = function (name) {
                var match = false;
                
                if (typeof name === 'string') {
                    match = name.match(imageParts);

                    if (match) {
                        return match[1];
                    }
                }

                return name;
            },
            recycle = function () {
                var animations = this.animations,
                    key = '';
                
                for (key in animations) {
                    if (animations.hasOwnProperty(key)) {
                        arrayCache.recycle(animations[key].frames);
                    }
                    animations[key].recycle();
                }
                
                arrayCache.recycle(this.frames, 2);
                this.frames = null;
                arrayCache.recycle(this.images);
                this.images = null;
                this.recycle();
            },
            merge = function (spriteSheets, destination) {
                var i = spriteSheets.length,
                    ss = null;
                
                while (i--) {
                    ss = spriteSheets[i];
                    if (typeof ss === 'string') {
                        ss = platypus.game.settings.spriteSheets[ss];
                    }
                    if (ss) {
                        format(ss, destination);
                    }
                }
                
                return destination;
            };
        
        return function (spriteSheet) {
            var response = PIXIAnimation.EmptySpriteSheet,
                ss = spriteSheet;
            
            if (typeof ss === 'string') {
                ss = platypus.game.settings.spriteSheets[spriteSheet];
            }
            
            if (ss) {
                response = Data.setUp(
                    "animations", Data.setUp(),
                    "framerate", 60,
                    "frames", arrayCache.setUp(),
                    "id", '',
                    "images", arrayCache.setUp(),
                    "recycleSpriteSheet", recycle
                );
                    
                if (Array.isArray(ss)) {
                    return merge(ss, response);
                } else if (ss) {
                    return format(ss, response);
                }
            }

            return response;
        };
    }());

    return PIXIAnimation;
}());