Newer
Older
pixi.js / tools / renders / src / Renderer.js
@Matt Karl Matt Karl on 31 Oct 2017 7 KB Next Restructure for v5 (#4387)
/* global PIXI */
const md5 = require('js-md5');
const path = require('path');
const ImageDiff = require('./ImageDiff');

/**
 * Class to create and validate solutions.
 * @class Renderer
 */
class Renderer
{
    /**
     * @constructor
     * @param {HTMLCanvasElement} [viewWebGL] Optional canvas element for webgl
     * @param {HTMLCanvasElement} [viewContext2d] Optional canvas element for context2d
     * @param {HTMLElement} [parentNode] container, defaults to `document.body`
     */
    constructor(viewWebGL, viewContext2d, parentNode)
    {
        viewWebGL = viewWebGL || document.createElement('canvas');
        viewContext2d = viewContext2d || document.createElement('canvas');

        /**
         * The container node to add the canvas elements.
         * @name parentNode
         * @type {HTMLElement}
         */
        this.parentNode = parentNode || document.body;

        if (!viewWebGL.parentNode)
        {
            this.parentNode.appendChild(viewWebGL);
        }
        if (!viewContext2d.parentNode)
        {
            this.parentNode.appendChild(viewContext2d);
        }

        /**
         * The container for the display objects.
         * @name stage
         * @type {PIXI.Container}
         */
        this.stage = new PIXI.Container();

        /**
         * If the current browser supports WebGL.
         * @name hasWebGL
         * @type {Boolean}
         */
        this.hasWebGL = PIXI.utils.isWebGLSupported();

        if (this.hasWebGL)
        {
            /**
             * The WebGL PIXI renderer.
             * @name webgl
             * @type {PIXI.WebGLRenderer}
             */
            this.webgl = new PIXI.WebGLRenderer(Renderer.WIDTH, Renderer.HEIGHT, {
                view: viewWebGL,
                backgroundColor: 0xffffff,
                antialias: false,
                preserveDrawingBuffer: true,
            });
        }

        /**
         * The Canvas PIXI renderer.
         * @name webgl
         * @type {PIXI.WebGLRenderer}
         */
        this.canvas = new PIXI.CanvasRenderer(Renderer.WIDTH, Renderer.HEIGHT, {
            view: viewContext2d,
            backgroundColor: 0xffffff,
            antialias: false,
            preserveDrawingBuffer: true,
            roundPixels: true,
        });
        this.canvas.smoothProperty = null;
        this.render();

        /**
         * Library for comparing images diffs.
         * @name imagediff
         * @type {ImageDiff}
         */
        this.imagediff = new ImageDiff(
            Renderer.WIDTH,
            Renderer.HEIGHT,
            Renderer.TOLERANCE
        );
    }

    /**
     * Rerender the stage
     * @method render
     */
    render()
    {
        if (this.hasWebGL)
        {
            this.webgl.render(this.stage);
        }
        this.canvas.render(this.stage);
    }

    /**
     * Clear the stage
     * @method clear
     */
    clear()
    {
        this.stage.children.forEach((child) =>
        {
            child.destroy(true);
        });
        this.render();
    }

    /**
     * Run the solution renderer
     * @method run
     * @param {String} filePath Fully resolved path to javascript.
     * @param {Function} callback Takes error and result as arguments.
     */
    run(filePath, callback)
    {
        delete require.cache[path.resolve(filePath)];
        let data;
        const code = require(filePath); // eslint-disable-line global-require

        if (typeof code !== 'function' && !code.async)
        {
            callback(new Error('Invalid JS format, make sure file is '
                + 'CommonJS compatible, e.g. "module.exports = function(){};"'));
        }
        else
        {
            this.clear();
            const done = () =>
            {
                if (!this.stage.children.length)
                {
                    callback(new Error('Stage has no children, make sure to '
                        + 'add children in your test, e.g. "module.exports = function(){};"'));
                }
                else
                {
                    // Generate the result
                    const result = {
                        webgl: {},
                        canvas: {},
                    };

                    this.render();
                    if (this.hasWebGL)
                    {
                        data = this.webgl.view.toDataURL();
                        result.webgl.image = data;
                        result.webgl.hash = md5(data);
                    }
                    data = this.canvas.view.toDataURL();
                    result.canvas.image = data;
                    result.canvas.hash = md5(data);

                    callback(null, result);
                }
            };

            // Support for asynchronous tests
            if (code.async)
            {
                code.async.call(this, done);
            }
            else
            {
                // Just run the test synchronously
                code.call(this);
                done();
            }
        }
    }

    /**
     * Compare a file with a solution.
     * @method compare
     * @param {String} filePath The file to load.
     * @param {String} solutionPath The path to the solution file.
     * @param {Array<String>} solution.webgl Solution for webgl
     * @param {Array<String>} solution.canvas Solution for canvas
     * @param {Function} callback Complete callback, takes err as an error and success boolean as args.
     */
    compare(filePath, solutionPath, callback)
    {
        this.run(filePath, (err, result) =>
        {
            const solution = require(solutionPath); // eslint-disable-line global-require

            if (!solution.webgl || !solution.canvas)
            {
                callback(new Error('Invalid solution'));
            }
            else if (err)
            {
                callback(err);
            }
            else if (this.hasWebGL && !this.compareResult(solution.webgl, result.webgl))
            {
                callback(new Error('WebGL results do not match.'));
            }
            else if (!this.compareResult(solution.canvas, result.canvas))
            {
                callback(new Error('Canvas results do not match.'));
            }
            else
            {
                callback(null, true);
            }
        });
    }

    /**
     * Compare two arrays of frames
     * @method compareResult
     * @private
     * @param {Array} a First result to compare
     * @param {Array} b Second result to compare
     * @return {Boolean} If we're equal
     */
    compareResult(a, b)
    {
        if (a === b)
        {
            return true;
        }
        if (a === null || b === null)
        {
            return false;
        }
        if (a.hash !== b.hash)
        {
            if (!this.imagediff.compare(a.image, b.image))
            {
                return false;
            }
        }

        return true;
    }

    /**
     * Destroy and don't use after this.
     * @method destroy
     */
    destroy()
    {
        this.clear();
        this.stage.destroy(true);
        this.parentNode = null;
        this.canvas.destroy(true);
        this.canvas = null;

        if (this.hasWebGL)
        {
            this.webgl.destroy(true);
            this.webgl = null;
        }
    }
}

/**
 * The width of the render.
 * @static
 * @type {int}
 * @name WIDTH
 * @default 32
 */
Renderer.WIDTH = 32;

/**
 * The height of the render.
 * @static
 * @type {int}
 * @name HEIGHT
 * @default 32
 */
Renderer.HEIGHT = 32;

/**
 * The tolerance when comparing image solutions.
 * for instance 0.01 would mean any difference greater
 * than 1% would be consider not the same.
 * @static
 * @type {Number}
 * @name TOLERANCE
 * @default 0.01
 */
Renderer.TOLERANCE = 0.01;

module.exports = Renderer;