Source: scene/sprite.js

Object.assign(pc, function () {
    'use strict';

    /**
     * @enum pc.SPRITE_RENDERMODE
     * @name pc.SPRITE_RENDERMODE_SIMPLE
     * @description This mode renders a sprite as a simple quad.
     */
    pc.SPRITE_RENDERMODE_SIMPLE = 0;

    /**
     * @enum pc.SPRITE_RENDERMODE
     * @name pc.SPRITE_RENDERMODE_SLICED
     * @description This mode renders a sprite using 9-slicing in 'sliced' mode. Sliced mode stretches the
     * top and bottom regions of the sprite horizontally, the left and right regions vertically and the middle region
     * both horizontally and vertically.
     */
    pc.SPRITE_RENDERMODE_SLICED = 1;

    /**
     * @enum pc.SPRITE_RENDERMODE
     * @name pc.SPRITE_RENDERMODE_TILED
     * @description This mode renders a sprite using 9-slicing in 'tiled' mode. Tiled mode tiles the
     * top and bottom regions of the sprite horizontally, the left and right regions vertically and the middle region
     * both horizontally and vertically.
     */
    pc.SPRITE_RENDERMODE_TILED = 2;

    // normals are the same for every mesh
    var spriteNormals = [
        0, 0, 1,
        0, 0, 1,
        0, 0, 1,
        0, 0, 1
    ];

    // indices are the same for every mesh
    var spriteIndices = [
        0, 1, 3,
        2, 3, 1
    ];


    /**
     * @constructor
     * @name pc.Sprite
     * @classdesc A pc.Sprite is contains references to one or more frames of a {@link pc.TextureAtlas}. It can be used
     * by the {@link pc.SpriteComponent} or the {@link pc.ElementComponent} to render a single frame or a sprite animation.
     * @param {pc.GraphicsDevice} device The graphics device of the application.
     * @param {Object} options Options for creating the pc.Sprite.
     * @param {Number} [options.pixelsPerUnit] The number of pixels that map to one PlayCanvas unit.
     * @param {pc.SPRITE_RENDERMODE} [options.renderMode] The rendering mode of the Sprite.
     * @param {pc.TextureAtlas} [options.atlas] The texture atlas.
     * @property {String[]} [options.frameKeys] The keys of the frames in the sprite atlas that this sprite is using.
     * @property {Number} pixelsPerUnit The number of pixels that map to one PlayCanvas unit.
     * @property {pc.TextureAtlas} atlas The texture atlas.
     * @property {pc.SPRITE_RENDERMODE} renderMode The rendering mode of the Sprite.
     * @property {String[]} frameKeys The keys of the frames in the sprite atlas that this sprite is using.
     * @property {pc.Mesh[]} meshes An array that contains a mesh for each frame.
     */
    var Sprite = function (device, options) {
        this._device = device;
        this._pixelsPerUnit = options && options.pixelsPerUnit !== undefined ? options.pixelsPerUnit : 1;
        this._renderMode = options && options.renderMode !== undefined ? options.renderMode : pc.SPRITE_RENDERMODE_SIMPLE;
        this._atlas = options && options.atlas !== undefined ? options.atlas : null;
        this._frameKeys = options && options.frameKeys !== undefined ? options.frameKeys : null;
        this._meshes = [];

        // set to true to update multiple
        // properties without re-creating meshes
        this._updatingProperties = false;
        // if true, endUpdate() will re-create meshes when it's called
        this._meshesDirty = false;

        pc.events.attach(this);

        if (this._atlas && this._frameKeys) {
            this._createMeshes();
        }
    };

    Sprite.prototype._createMeshes = function () {
        var i, len;

        // destroy old meshes
        for (i = 0, len = this._meshes.length; i < len; i++) {
            var mesh = this._meshes[i];
            if (!mesh) continue;

            mesh.vertexBuffer.destroy();
            for (var j = 0, len2 = mesh.indexBuffer.length; j < len2; j++) {
                mesh.indexBuffer[j].destroy();
            }
        }

        // clear meshes array
        var count = this._frameKeys.length;
        this._meshes = new Array(count);

        // get function to create meshes
        var createMeshFunc = (this.renderMode === pc.SPRITE_RENDERMODE_SLICED || this._renderMode === pc.SPRITE_RENDERMODE_TILED ? this._create9SliceMesh : this._createSimpleMesh);

        // create a mesh for each frame in the sprite
        for (i = 0; i < count; i++) {
            var frame = this._atlas.frames[this._frameKeys[i]];
            this._meshes[i] = frame ? createMeshFunc.call(this, frame) : null;
        }

        this.fire('set:meshes');
    };

    Sprite.prototype._createSimpleMesh = function (frame) {
        var rect = frame.rect;
        var texWidth = this._atlas.texture.width;
        var texHeight = this._atlas.texture.height;

        var w = rect.z / this._pixelsPerUnit;
        var h = rect.w / this._pixelsPerUnit;
        var hp = frame.pivot.x;
        var vp = frame.pivot.y;

        // positions based on pivot and size of frame
        var positions = [
            -hp * w,      -vp * h,      0,
            (1 - hp) * w, -vp * h,      0,
            (1 - hp) * w, (1 - vp) * h, 0,
            -hp * w,      (1 - vp) * h, 0
        ];

        // uvs based on frame rect
        // uvs
        var lu = rect.x / texWidth;
        var bv = rect.y / texHeight;
        var ru = (rect.x + rect.z) / texWidth;
        var tv = (rect.y + rect.w) / texHeight;

        var uvs = [
            lu, bv,
            ru, bv,
            ru, tv,
            lu, tv
        ];

        var mesh = pc.createMesh(this._device, positions, {
            uvs: uvs,
            normals: spriteNormals,
            indices: spriteIndices
        });

        return mesh;
    };

    Sprite.prototype._create9SliceMesh = function () {
        // Check the supplied options and provide defaults for unspecified ones
        var he = pc.Vec2.ONE;
        var ws = 3;
        var ls = 3;

        // Variable declarations
        var i, j;
        var x, y, z, u, v;
        var positions = [];
        var normals = [];
        var uvs = [];
        var indices = [];

        // Generate plane as follows (assigned UVs denoted at corners):
        // (0,1)x---------x(1,1)
        //      |         |
        //      |         |
        //      |    O--X |length
        //      |    |    |
        //      |    Z    |
        // (0,0)x---------x(1,0)
        // width
        var vcounter = 0;
        for (i = 0; i <= ws; i++) {
            u = (i === 0 || i === ws) ? 0 : 1;

            for (j = 0; j <= ls; j++) {

                x = -he.x + 2.0 * he.x * (i <= 1 ? 0 : 3) / ws;
                y = 0.0;
                z = -(-he.y + 2.0 * he.y * (j <= 1 ? 0 : 3) / ls);

                v = (j === 0 || j === ls) ? 0 : 1;

                positions.push(-x, y, z);
                normals.push(0.0, 1.0, 0.0);
                uvs.push(u, v);

                if ((i < ws) && (j < ls)) {
                    indices.push(vcounter + ls + 1, vcounter + 1, vcounter);
                    indices.push(vcounter + ls + 1, vcounter + ls + 2, vcounter + 1);
                }

                vcounter++;
            }
        }

        var options = {
            normals: normals, // crashes without normals on mac?
            uvs: uvs,
            indices: indices
        };

        return pc.createMesh(this._device, positions, options);
    };

    Sprite.prototype._onSetFrames = function (frames) {
        if (this._updatingProperties) {
            this._meshesDirty = true;
        } else {
            this._createMeshes();
        }
    };

    Sprite.prototype._onFrameChanged = function (frameKey, frame) {
        var idx = this._frameKeys.indexOf(frameKey);
        if (idx < 0) return;

        if (frame) {
            // only re-create frame for simple render mode, since
            // 9-sliced meshes don't need frame info to create their mesh
            if (this.renderMode === pc.SPRITE_RENDERMODE_SIMPLE) {
                this._meshes[idx] = this._createSimpleMesh(frame);
            }
        } else {
            this._meshes[idx] = null;
        }

        this.fire('set:meshes');
    };

    Sprite.prototype._onFrameRemoved = function (frameKey) {
        var idx = this._frameKeys.indexOf(frameKey);
        if (idx < 0) return;

        this._meshes[idx] = null;
        this.fire('set:meshes');
    };

    Sprite.prototype.startUpdate = function () {
        this._updatingProperties = true;
        this._meshesDirty = false;
    };

    Sprite.prototype.endUpdate = function () {
        this._updatingProperties = false;
        if (this._meshesDirty && this._atlas && this._frameKeys) {
            this._createMeshes();

        }
        this._meshesDirty = false;
    };

    /**
     * @function
     * @name pc.Sprite#destroy
     * @description Free up the meshes created by the sprite.
     */
    Sprite.prototype.destroy = function () {
        var i;
        var len;

        // destroy old meshes
        for (i = 0, len = this._meshes.length; i < len; i++) {
            var mesh = this._meshes[i];
            if (!mesh) continue;

            mesh.vertexBuffer.destroy();
            for (var j = 0, len2 = mesh.indexBuffer.length; j < len2; j++) {
                mesh.indexBuffer[j].destroy();
            }
        }
        this._meshes.length = 0;
    };

    Object.defineProperty(Sprite.prototype, 'frameKeys', {
        get: function () {
            return this._frameKeys;
        },
        set: function (value) {
            this._frameKeys = value;

            if (this._atlas && this._frameKeys) {
                if (this._updatingProperties) {
                    this._meshesDirty = true;
                } else {
                    this._createMeshes();
                }
            }

            this.fire('set:frameKeys', value);
        }
    });

    Object.defineProperty(Sprite.prototype, 'atlas', {
        get: function () {
            return this._atlas;
        },
        set: function (value) {
            if (value === this._atlas) return;

            if (this._atlas) {
                this._atlas.off('set:frames', this._onSetFrames, this);
                this._atlas.off('set:frame', this._onFrameChanged, this);
                this._atlas.off('remove:frame', this._onFrameRemoved, this);
            }

            this._atlas = value;
            if (this._atlas && this._frameKeys) {
                this._atlas.on('set:frames', this._onSetFrames, this);
                this._atlas.on('set:frame', this._onFrameChanged, this);
                this._atlas.on('remove:frame', this._onFrameRemoved, this);

                if (this._updatingProperties) {
                    this._meshesDirty = true;
                } else {
                    this._createMeshes();
                }
            }

            this.fire('set:atlas', value);
        }
    });

    Object.defineProperty(Sprite.prototype, 'pixelsPerUnit', {
        get: function () {
            return this._pixelsPerUnit;
        },
        set: function (value) {
            if (this._pixelsPerUnit === value) return;

            this._pixelsPerUnit = value;
            this.fire('set:pixelsPerUnit', value);

            // simple mode uses pixelsPerUnit to create the mesh so re-create those meshes
            if (this._atlas && this._frameKeys && this.renderMode === pc.SPRITE_RENDERMODE_SIMPLE) {
                if (this._updatingProperties) {
                    this._meshesDirty = true;
                } else {
                    this._createMeshes();
                }
            }

        }
    });

    Object.defineProperty(Sprite.prototype, 'renderMode', {
        get: function () {
            return this._renderMode;
        },
        set: function (value) {
            if (this._renderMode === value)
                return;

            var prev = this._renderMode;
            this._renderMode = value;
            this.fire('set:renderMode', value);

            // re-create the meshes if we're going from simple to 9-sliced or vice versa
            if (prev === pc.SPRITE_RENDERMODE_SIMPLE || value === pc.SPRITE_RENDERMODE_SIMPLE) {
                if (this._atlas && this._frameKeys) {
                    if (this._updatingProperties) {
                        this._meshesDirty = true;
                    } else {
                        this._createMeshes();
                    }
                }
            }
        }
    });

    Object.defineProperty(Sprite.prototype, 'meshes', {
        get: function () {
            return this._meshes;
        }
    });

    return {
        Sprite: Sprite
    };
}());