Home Reference Source

src/sprite.js

import { Model } from './model';

/**
 * The class to use sprite image
 * You can add animations
 * @example
 * const image = loader.get('my-sprite'); // see Loader documentation
 * const animation = [{ frames: [9, 10, 11, 12], name: 'walk', loop: true }];
 * let sprite = new Sprite(10, 10, 20, 20, image, animation);
 *
 * sprite.play('walk');
 * sprite.render();
 */
export class Sprite extends Model {

    /**
     * @param {number} x
     * @param {number} y
     * @param {number} tileWidth - width tile
     * @param {number} tileHeight - height tile
     * @param {Image} image
     * @param {Array<Object>} animations - list of animations
     * @param {Object} [hitbox={}]
     * @param {number} [hitbox.x]
     * @param {number} [hitbox.y]
     * @param {number} [hitbox.width]
     * @param {number} [hitbox.height]
     *
     * @example
     * new Sprite(0, 0, 20, 20, image, [{ frames: [9, 10, 11, 12], name: 'walk', loop: true, flip: false }]);
     */
    constructor(x, y, tileWidth, tileHeight, image, animations, hitbox = {}) {
        super(x, y, tileWidth, tileHeight, hitbox);

        /** @type {Image} */
        this.image = image;
        /** @type {Array<Object>} */
        this.animations = animations;

        /** @type {number} */
        this.time = 1;

        /** @type {boolean} */
        this.stopped = true;

        /** @type {Object} */
        this.frame = { x: 0, y: 0 };

        /** @type {Object} */
        this.frames = {
            width: image.width / tileWidth,
            height: image.height / tileHeight,
            total: (image.width / tileWidth) * (image.height / tileHeight)
        };

        /** @type {number} */
        this.currentAnimation = 0;

        /** @type {number} */
        this.currentFrame = 0;
    }

    /**
     * @private
     */
    getNextFrame() {
        let currentAnimation = this.animations[this.currentAnimation];
        let currentFrame = currentAnimation.frames[this.currentFrame];

        if (this.frames.width - currentFrame >= 0) {
            this.frame.x = currentFrame - 1;
            this.frame.y = 0;
        } else {

            let delta = currentFrame - this.frames.width;
            this.frame.y = 1;

            while (delta > this.frames.width) {
                delta = delta - this.frames.width;
                this.frame.y++;
            }

            this.frame.x = delta - 1;
        }

        this.currentFrame++;

        if (this.currentFrame >= currentAnimation.frames.length) {
            this.currentFrame = 0;

            if (!currentAnimation.loop) {
                this.stopped = true;
            }
        }
    }

    /**
     * @param {number} dt - Delta between two frames
     */
    step(dt) {
        this.time += dt;

        if (this.time >= 0.25 && !this.stopped) {
            this.getNextFrame();
            this.time = 0;
        }
    }

    /**
     * Play animation
     * @param {String} animation
     */
    play(animation) {
        let currentAnimation = this.animations.filter(anim => anim.name === animation);

        if (!!currentAnimation) {
            let index = this.animations.indexOf(currentAnimation[0]);

            if (this.currentAnimation != index) {
                this.time = 1;
                this.currentFrame = 0;
            }

            this.currentAnimation = index;
            this.stopped = false;
        }
    }

    /**
     * Stop animation
     */
    stop() {
        this.stopped = true;
    }

    /**
     * Reset animation
     */
    reset() {
        this.frame = { x: 0, y: 0 };
    }

    /**
     * Render the sprite
     * @param {RenderingContext} [ctx=null]
     * @param {Drawer} [drawer=null]
     */
    render(ctx = null, drawer = null) {
        ctx = ctx || this.parent.ctx;
        drawer = drawer || this.parent;

        let currentAnimation = this.animations[this.currentAnimation];
        ctx.save();

        if (!!currentAnimation && !!currentAnimation.flip) {
            ctx.translate((this.x * 2) + this.width, 1);
            ctx.scale(-1, 1);
        }

        ctx.drawImage(
            this.image,     // image
            this.frame.x * this.width,  // pos x
            this.frame.y * this.height, // pos y
            this.width,     // frame width
            this.height,    // frame height
            this.x,         // destination x
            this.y,         // destination y
            this.width,     // destination frame width
            this.height     // destination frame height
        );
        ctx.restore();
    }

    /**
     * @return {Object}
     * @property {number} x
     * @property {number} y
     * @property {number} width
     * @property {number} height
     * @property {Object} hitbox
     * @property {Image} image
     * @property {Object} animations
     * @property {number} time
     * @property {boolean} stopped
     * @property {Object} frame
     * @property {number} currentAnimation
     * @property {number} currentFrame
     */
    serialize() {
        return Object.assign(super.serialize(), {
            image: this.image,
            animations: this.animations,
            time: this.time,
            stopped: this.stopped,
            frame: this.frame,
            currentAnimation: this.currentAnimation,
            currentFrame: this.currentFrame
        });
    }

    /**
     * @param {Object} data
     * @param {number} data.x
     * @param {number} data.y
     * @param {number} data.width
     * @param {number} data.height
     * @param {Object} data.hitbox
     * @param {boolean} data.collision
     * @param {Image} data.image
     * @param {Object} data.animations
     * @param {number} data.time
     * @param {boolean} data.stopped
     * @param {Object} data.frame
     * @param {number} data.currentAnimation
     * @param {number} data.currentFrame
     * @return {Sprite}
     */
    static deserialize({
        x,
        y,
        width,
        height,
        hitbox,
        collision,
        image,
        animations,
        time,
        stopped,
        frame,
        currentAnimation,
        currentFrame
    }) {
        const sprite = new Sprite(x, y, width, height, image, animations, hitbox);

        sprite.collision = collision;
        sprite.time = time;
        sprite.stopped = stopped;
        sprite.frame = frame;
        sprite.currentAnimation = currentAnimation;
        sprite.currentFrame = currentFrame;

        return sprite;
    }
}