/* 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.Renderer} */ this.webgl = new PIXI.Renderer(Renderer.WIDTH, Renderer.HEIGHT, { view: viewWebGL, backgroundColor: 0xffffff, antialias: false, preserveDrawingBuffer: true, }); } /** * The Canvas PIXI renderer. * @name webgl * @type {PIXI.Renderer} */ this.canvas = new PIXI.CanvasRenderer(Renderer.WIDTH, Renderer.HEIGHT, { view: viewContext2d, backgroundColor: 0xffffff, antialias: false, preserveDrawingBuffer: 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;