Source: og/utils/TextureAtlas.js

/**
 * @module og/utils/TextureAtlas
 */

'use strict';

import { ImageCanvas } from '../ImageCanvas.js';
import { Rectangle } from '../Rectangle.js';
import { ImagesCacheManager } from './ImagesCacheManager.js';


/**
 * Border beetween stored images.
 * @type {number}
 * @const
 */
const BORDER_SIZE = 4;

/**
 * Texture atlas stores images in one texture. Each image has texture 
 * coordinates returned with node creation by addImage function.
 * @class
 * @param {number} [width] - Texture atlas width, if it hasn't 1024 default.
 * @param {number} [height] - Texture atlas height, if it hasn't 1024 default..
 */
class TextureAtlas {
    constructor(width, height) {

        /**
         * Atlas nodes where input images store. It can be access by image.__nodeIndex.
         * @public
         * @type {Array.<og.utils.TextureAtlasNode >}
         */
        this.nodes = [];

        /**
         * Created gl texture.
         * @public
         */
        this.texture = null;

        /**
         * Atlas canvas.
         * @public
         * @type {canvas}
         */
        this.canvas = new ImageCanvas(width || 1024, height || 1024);
        this.clearCanvas();

        this._handler = null;
        this._images = [];
        this._btree = null;
        this._imagesCacheManager = new ImagesCacheManager();
        this.borderSize = 4;
    }

    /**
     * Returns atlas javascript image object.
     * @public
     * @returns {Object}
     */
    getImage() {
        return this.canvas.getImage();
    }

    /**
     * Returns canvas object.
     * @public
     * @retuns {Object}
     */
    getCanvas() {
        return this.canvas._canvas;
    }

    /**
     * Clear atlas with black.
     * @public
     */
    clearCanvas() {
        this.canvas.fillEmpty("black");
    }

    /**
     * Sets openglobus gl handler that creates gl texture.
     * @public
     * @param {og.webgl.Handler} handler - WebGL handler.
     */
    assignHandler(handler) {
        this._handler = handler;
        this.createTexture();
    }

    /**
     * Returns image diagonal size.
     * @param {Object} image - JavaSript image object.
     * @returns {number}
     */
    getDiagonal(image) {
        var w = image.atlasWidth || image.width,
            h = image.atlasHeight || image.height;
        return Math.sqrt(w * w + h * h);
    }

    /**
     * Adds image to the atlas and returns creted node with texture coordinates of the stored image.
     * @public
     * @param {Object} image - Input javascript image object.
     * @param {boolean} [fastInsert] - If it's true atlas doesnt restore all images again 
     * and store image in the curent atlas sheme.
     * @returns {og.utils.TextureAtlasNode}
     */
    addImage(image, fastInsert) {

        if (!(image.width && image.height)) {
            return;
        }

        this._images.push(image);

        this._makeAtlas(fastInsert);

        return this.nodes[image.__nodeIndex];
    }

    /**
     * Calculate texture coordianates and stores node.
     * @private
     */
    _completeNode(nodes, node) {
        if (node) {
            var w = this.canvas.getWidth(),
                h = this.canvas.getHeight();
            var im = node.image;
            var r = node.rect;
            var bs = Math.round(this.borderSize * 0.5);
            this.canvas.drawImage(im, r.left + bs, r.top + bs, im.atlasWidth, im.atlasHeight);
            var tc = node.texCoords;

            tc[0] = (r.left + bs) / w;
            tc[1] = (r.top + bs) / h;

            tc[2] = (r.left + bs) / w;
            tc[3] = (r.bottom - bs) / h;

            tc[4] = (r.right - bs) / w;
            tc[5] = (r.bottom - bs) / h;

            tc[6] = (r.right - bs) / w;
            tc[7] = (r.bottom - bs) / h;

            tc[8] = (r.right - bs) / w;
            tc[9] = (r.top + bs) / h;

            tc[10] = (r.left + bs) / w;
            tc[11] = (r.top + bs) / h;

            nodes[im.__nodeIndex] = node;
        }
    }

    /**
     * Main atlas making function.
     * @private
     * @param {boolean} [fastInsert] - If it's true atlas doesnt restore all images again 
     * and store image in the curent atlas sheme.
     */
    _makeAtlas(fastInsert) {

        if (fastInsert && this._btree) {
            var im = this._images[this._images.length - 1];
            this._completeNode(this.nodes, this._btree.insert(im));
        } else {
            var im = this._images.slice(0);

            im.sort(function (b, a) {
                return ((a.atlasWidth || a.width) - (b.atlasWidth || b.width)) || ((a.atlasHeight || a.height) - (b.atlasHeight || b.height));
            });

            this._btree = new TextureAtlasNode(new Rectangle(0, 0, this.canvas.getWidth(), this.canvas.getHeight()));
            this._btree.atlas = this;

            this.clearCanvas();

            var newNodes = [];
            for (var i = 0; i < im.length; i++) {
                this._completeNode(newNodes, this._btree.insert(im[i]));
            }
            this.nodes = [];
            this.nodes = newNodes;
        }
    }

    /**
     * Creates atlas gl texture.
     * @public
     */
    createTexture() {
        if (this._handler) {
            this._handler.gl.deleteTexture(this.texture);
            this.texture = this._handler.createTexture_l(this.canvas._canvas);
        }
    }

    /**
     * Image handler callback. 
     * @callback Object~successCallback
     * @param {Image} img - Loaded image.
     */

    /**
     * Asynchronous function that loads and creates image to the image cache, and call success callback when it's done.
     * @public
     * @param {string} src - Image object src string.
     * @param {Object~successCallback} success - The callback that handles the image loads done.
     */
    loadImage(src, success) {
        this._imagesCacheManager.load(src, success);
    }
};

/**
 * Atlas binary tree node.
 * @class
 * @prarm {og.Rectangle} rect - Node image rectangle.
 */
class TextureAtlasNode {
    constructor(rect) {
        this.childNodes = null;
        this.image = null;
        this.rect = rect;
        this.texCoords = [];
        this.atlas = null;
    }

    /**
     * This algorithm has got from here:
     * http://www.blackpawn.com/texts/lightmaps/default.html
     */
    insert(img) {

        if (this.childNodes) {

            var newNode = this.childNodes[0].insert(img);

            if (newNode != null)
                return newNode;

            return this.childNodes[1].insert(img);

        } else {

            if (this.image != null)
                return null;

            var rc = this.rect;
            var w = (img.atlasWidth || img.width) + this.atlas.borderSize;
            var h = (img.atlasHeight || img.height) + this.atlas.borderSize;

            if (w > rc.getWidth() || h > rc.getHeight())
                return null;

            if (rc.fit(w, h)) {
                this.image = img;
                return this;
            }

            this.childNodes = new Array(2);
            this.childNodes[0] = new TextureAtlasNode();
            this.childNodes[0].atlas = this.atlas;
            this.childNodes[1] = new TextureAtlasNode();
            this.childNodes[1].atlas = this.atlas;

            var dw = rc.getWidth() - w;
            var dh = rc.getHeight() - h;

            if (dw > dh) {
                this.childNodes[0].rect = new Rectangle(rc.left, rc.top, rc.left + w, rc.bottom);
                this.childNodes[1].rect = new Rectangle(rc.left + w, rc.top, rc.right, rc.bottom);
            } else {
                this.childNodes[0].rect = new Rectangle(rc.left, rc.top, rc.right, rc.top + h);
                this.childNodes[1].rect = new Rectangle(rc.left, rc.top + h, rc.right, rc.bottom);
            }

            return this.childNodes[0].insert(img);
        }
    }
};

export { TextureAtlas };