diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/renders/main.js b/scripts/renders/main.js
new file mode 100644
index 0000000..7ee1a1c
--- /dev/null
+++ b/scripts/renders/main.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app; // Control application life
+const BrowserWindow = electron.BrowserWindow; // Native browser window
+
+// Keep a global reference of the window object
+let mainWindow = null;
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () =>
+{
+ app.quit();
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+app.on('ready', () =>
+{
+ mainWindow = new BrowserWindow({
+ width: 675,
+ height: 400,
+ useContentSize: true,
+ resizable: false,
+ textAreasAreResizable: false,
+ });
+
+ mainWindow.loadURL(`file://${__dirname}/index.html`);
+ mainWindow.on('closed', () =>
+ {
+ mainWindow = null;
+ });
+});
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/renders/main.js b/scripts/renders/main.js
new file mode 100644
index 0000000..7ee1a1c
--- /dev/null
+++ b/scripts/renders/main.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app; // Control application life
+const BrowserWindow = electron.BrowserWindow; // Native browser window
+
+// Keep a global reference of the window object
+let mainWindow = null;
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () =>
+{
+ app.quit();
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+app.on('ready', () =>
+{
+ mainWindow = new BrowserWindow({
+ width: 675,
+ height: 400,
+ useContentSize: true,
+ resizable: false,
+ textAreasAreResizable: false,
+ });
+
+ mainWindow.loadURL(`file://${__dirname}/index.html`);
+ mainWindow.on('closed', () =>
+ {
+ mainWindow = null;
+ });
+});
diff --git a/scripts/renders/package.json b/scripts/renders/package.json
new file mode 100644
index 0000000..72753f3
--- /dev/null
+++ b/scripts/renders/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "renders-app",
+ "private": true,
+ "main": "main.js",
+ "dependencies": {
+ "chai": "^3.5.0",
+ "sinon": "^1.17.6",
+ "sinon-chai": "^2.8.0"
+ }
+}
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/renders/main.js b/scripts/renders/main.js
new file mode 100644
index 0000000..7ee1a1c
--- /dev/null
+++ b/scripts/renders/main.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app; // Control application life
+const BrowserWindow = electron.BrowserWindow; // Native browser window
+
+// Keep a global reference of the window object
+let mainWindow = null;
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () =>
+{
+ app.quit();
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+app.on('ready', () =>
+{
+ mainWindow = new BrowserWindow({
+ width: 675,
+ height: 400,
+ useContentSize: true,
+ resizable: false,
+ textAreasAreResizable: false,
+ });
+
+ mainWindow.loadURL(`file://${__dirname}/index.html`);
+ mainWindow.on('closed', () =>
+ {
+ mainWindow = null;
+ });
+});
diff --git a/scripts/renders/package.json b/scripts/renders/package.json
new file mode 100644
index 0000000..72753f3
--- /dev/null
+++ b/scripts/renders/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "renders-app",
+ "private": true,
+ "main": "main.js",
+ "dependencies": {
+ "chai": "^3.5.0",
+ "sinon": "^1.17.6",
+ "sinon-chai": "^2.8.0"
+ }
+}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
index 09620cb..e03220d 100644
--- a/test/.eslintrc.json
+++ b/test/.eslintrc.json
@@ -11,6 +11,7 @@
"globals": {
"sinon": false,
"expect": false,
+ "assert": false,
"PIXI": false
},
"rules": {
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/renders/main.js b/scripts/renders/main.js
new file mode 100644
index 0000000..7ee1a1c
--- /dev/null
+++ b/scripts/renders/main.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app; // Control application life
+const BrowserWindow = electron.BrowserWindow; // Native browser window
+
+// Keep a global reference of the window object
+let mainWindow = null;
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () =>
+{
+ app.quit();
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+app.on('ready', () =>
+{
+ mainWindow = new BrowserWindow({
+ width: 675,
+ height: 400,
+ useContentSize: true,
+ resizable: false,
+ textAreasAreResizable: false,
+ });
+
+ mainWindow.loadURL(`file://${__dirname}/index.html`);
+ mainWindow.on('closed', () =>
+ {
+ mainWindow = null;
+ });
+});
diff --git a/scripts/renders/package.json b/scripts/renders/package.json
new file mode 100644
index 0000000..72753f3
--- /dev/null
+++ b/scripts/renders/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "renders-app",
+ "private": true,
+ "main": "main.js",
+ "dependencies": {
+ "chai": "^3.5.0",
+ "sinon": "^1.17.6",
+ "sinon-chai": "^2.8.0"
+ }
+}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
index 09620cb..e03220d 100644
--- a/test/.eslintrc.json
+++ b/test/.eslintrc.json
@@ -11,6 +11,7 @@
"globals": {
"sinon": false,
"expect": false,
+ "assert": false,
"PIXI": false
},
"rules": {
diff --git a/test/index.js b/test/index.js
index 1d245bc..7424430 100755
--- a/test/index.js
+++ b/test/index.js
@@ -3,6 +3,8 @@
/* eslint-disable global-require */
require('../bin/pixi');
+PIXI.utils.skipHello(); // hide banner
+
describe('PIXI', function ()
{
it('should exist as a global object', function ()
@@ -11,4 +13,5 @@
});
require('./core');
require('./interaction');
+ require('./renders');
});
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/renders/main.js b/scripts/renders/main.js
new file mode 100644
index 0000000..7ee1a1c
--- /dev/null
+++ b/scripts/renders/main.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app; // Control application life
+const BrowserWindow = electron.BrowserWindow; // Native browser window
+
+// Keep a global reference of the window object
+let mainWindow = null;
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () =>
+{
+ app.quit();
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+app.on('ready', () =>
+{
+ mainWindow = new BrowserWindow({
+ width: 675,
+ height: 400,
+ useContentSize: true,
+ resizable: false,
+ textAreasAreResizable: false,
+ });
+
+ mainWindow.loadURL(`file://${__dirname}/index.html`);
+ mainWindow.on('closed', () =>
+ {
+ mainWindow = null;
+ });
+});
diff --git a/scripts/renders/package.json b/scripts/renders/package.json
new file mode 100644
index 0000000..72753f3
--- /dev/null
+++ b/scripts/renders/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "renders-app",
+ "private": true,
+ "main": "main.js",
+ "dependencies": {
+ "chai": "^3.5.0",
+ "sinon": "^1.17.6",
+ "sinon-chai": "^2.8.0"
+ }
+}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
index 09620cb..e03220d 100644
--- a/test/.eslintrc.json
+++ b/test/.eslintrc.json
@@ -11,6 +11,7 @@
"globals": {
"sinon": false,
"expect": false,
+ "assert": false,
"PIXI": false
},
"rules": {
diff --git a/test/index.js b/test/index.js
index 1d245bc..7424430 100755
--- a/test/index.js
+++ b/test/index.js
@@ -3,6 +3,8 @@
/* eslint-disable global-require */
require('../bin/pixi');
+PIXI.utils.skipHello(); // hide banner
+
describe('PIXI', function ()
{
it('should exist as a global object', function ()
@@ -11,4 +13,5 @@
});
require('./core');
require('./interaction');
+ require('./renders');
});
diff --git a/test/renders/index.js b/test/renders/index.js
new file mode 100644
index 0000000..cf9cbe2
--- /dev/null
+++ b/test/renders/index.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const Renderer = require('./lib/Renderer');
+const path = require('path');
+
+describe('renders', function ()
+{
+ before(function ()
+ {
+ this.renderer = new Renderer();
+ this.validate = function (id, done)
+ {
+ const code = path.join(__dirname, 'tests', `${id}.js`);
+ const solution = path.join(__dirname, 'solutions', `${id}.json`);
+
+ this.renderer.compare(code, solution, (err, success) =>
+ {
+ if (err)
+ {
+ assert(false, err.message);
+ }
+ assert(success, 'Render not successful');
+ done();
+ });
+ };
+ });
+
+ beforeEach(function ()
+ {
+ this.renderer.clear();
+ });
+
+ after(function ()
+ {
+ this.renderer.destroy();
+ this.renderer = null;
+ this.validate = null;
+ });
+
+ it('should draw a rectangle', function (done)
+ {
+ this.validate('graphics-rect', done);
+ });
+
+ it('should draw a sprite', function (done)
+ {
+ this.validate('sprite-new', done);
+ });
+});
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/renders/main.js b/scripts/renders/main.js
new file mode 100644
index 0000000..7ee1a1c
--- /dev/null
+++ b/scripts/renders/main.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app; // Control application life
+const BrowserWindow = electron.BrowserWindow; // Native browser window
+
+// Keep a global reference of the window object
+let mainWindow = null;
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () =>
+{
+ app.quit();
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+app.on('ready', () =>
+{
+ mainWindow = new BrowserWindow({
+ width: 675,
+ height: 400,
+ useContentSize: true,
+ resizable: false,
+ textAreasAreResizable: false,
+ });
+
+ mainWindow.loadURL(`file://${__dirname}/index.html`);
+ mainWindow.on('closed', () =>
+ {
+ mainWindow = null;
+ });
+});
diff --git a/scripts/renders/package.json b/scripts/renders/package.json
new file mode 100644
index 0000000..72753f3
--- /dev/null
+++ b/scripts/renders/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "renders-app",
+ "private": true,
+ "main": "main.js",
+ "dependencies": {
+ "chai": "^3.5.0",
+ "sinon": "^1.17.6",
+ "sinon-chai": "^2.8.0"
+ }
+}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
index 09620cb..e03220d 100644
--- a/test/.eslintrc.json
+++ b/test/.eslintrc.json
@@ -11,6 +11,7 @@
"globals": {
"sinon": false,
"expect": false,
+ "assert": false,
"PIXI": false
},
"rules": {
diff --git a/test/index.js b/test/index.js
index 1d245bc..7424430 100755
--- a/test/index.js
+++ b/test/index.js
@@ -3,6 +3,8 @@
/* eslint-disable global-require */
require('../bin/pixi');
+PIXI.utils.skipHello(); // hide banner
+
describe('PIXI', function ()
{
it('should exist as a global object', function ()
@@ -11,4 +13,5 @@
});
require('./core');
require('./interaction');
+ require('./renders');
});
diff --git a/test/renders/index.js b/test/renders/index.js
new file mode 100644
index 0000000..cf9cbe2
--- /dev/null
+++ b/test/renders/index.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const Renderer = require('./lib/Renderer');
+const path = require('path');
+
+describe('renders', function ()
+{
+ before(function ()
+ {
+ this.renderer = new Renderer();
+ this.validate = function (id, done)
+ {
+ const code = path.join(__dirname, 'tests', `${id}.js`);
+ const solution = path.join(__dirname, 'solutions', `${id}.json`);
+
+ this.renderer.compare(code, solution, (err, success) =>
+ {
+ if (err)
+ {
+ assert(false, err.message);
+ }
+ assert(success, 'Render not successful');
+ done();
+ });
+ };
+ });
+
+ beforeEach(function ()
+ {
+ this.renderer.clear();
+ });
+
+ after(function ()
+ {
+ this.renderer.destroy();
+ this.renderer = null;
+ this.validate = null;
+ });
+
+ it('should draw a rectangle', function (done)
+ {
+ this.validate('graphics-rect', done);
+ });
+
+ it('should draw a sprite', function (done)
+ {
+ this.validate('sprite-new', done);
+ });
+});
diff --git a/test/renders/lib/ImageDiff.js b/test/renders/lib/ImageDiff.js
new file mode 100644
index 0000000..6bf2b54
--- /dev/null
+++ b/test/renders/lib/ImageDiff.js
@@ -0,0 +1,77 @@
+'use strict';
+
+/**
+ * Compare images
+ * @class ImageDiff
+ */
+class ImageDiff
+{
+ /**
+ * @constructor
+ * @param {int} width Width of the canvas
+ * @param {int} height Height of the canvas
+ * @param {Number} tolerance Tolerance for difference
+ */
+ constructor(width, height, tolerance)
+ {
+ this.width = width;
+ this.height = height;
+ this.tolerance = tolerance;
+
+ const canvas = document.createElement('canvas');
+
+ canvas.width = width;
+ canvas.height = height;
+ this.context = canvas.getContext('2d', {
+ antialias: false,
+ preserveDrawingBuffer: true,
+ });
+ }
+
+ /**
+ * Compare two base64 images
+ * @method compare
+ * @param {string} src1 Canvas source
+ * @param {string} src2 Canvas source
+ * @return {Boolean} If images are within tolerance
+ */
+ compare(src1, src2)
+ {
+ const a = this.getImageData(src1);
+ const b = this.getImageData(src2);
+ const len = a.length;
+ const tolerance = this.tolerance;
+
+ const diff = a.filter(function (val, i)
+ {
+ return Math.abs(val - b[i]) / 255 > tolerance;
+ });
+
+ if (diff.length / len > tolerance)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get an array of pixels
+ * @method getImageData
+ * @param {string} src Source of the image
+ * @return {Uint8ClampedArray} Data for image of pixels
+ */
+ getImageData(src)
+ {
+ const image = new Image();
+
+ image.src = src;
+ this.context.clearRect(0, 0, this.width, this.height);
+ this.context.drawImage(image, 0, 0, this.width, this.height);
+ const imageData = this.context.getImageData(0, 0, this.width, this.height);
+
+ return imageData.data;
+ }
+}
+
+module.exports = ImageDiff;
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/renders/main.js b/scripts/renders/main.js
new file mode 100644
index 0000000..7ee1a1c
--- /dev/null
+++ b/scripts/renders/main.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app; // Control application life
+const BrowserWindow = electron.BrowserWindow; // Native browser window
+
+// Keep a global reference of the window object
+let mainWindow = null;
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () =>
+{
+ app.quit();
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+app.on('ready', () =>
+{
+ mainWindow = new BrowserWindow({
+ width: 675,
+ height: 400,
+ useContentSize: true,
+ resizable: false,
+ textAreasAreResizable: false,
+ });
+
+ mainWindow.loadURL(`file://${__dirname}/index.html`);
+ mainWindow.on('closed', () =>
+ {
+ mainWindow = null;
+ });
+});
diff --git a/scripts/renders/package.json b/scripts/renders/package.json
new file mode 100644
index 0000000..72753f3
--- /dev/null
+++ b/scripts/renders/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "renders-app",
+ "private": true,
+ "main": "main.js",
+ "dependencies": {
+ "chai": "^3.5.0",
+ "sinon": "^1.17.6",
+ "sinon-chai": "^2.8.0"
+ }
+}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
index 09620cb..e03220d 100644
--- a/test/.eslintrc.json
+++ b/test/.eslintrc.json
@@ -11,6 +11,7 @@
"globals": {
"sinon": false,
"expect": false,
+ "assert": false,
"PIXI": false
},
"rules": {
diff --git a/test/index.js b/test/index.js
index 1d245bc..7424430 100755
--- a/test/index.js
+++ b/test/index.js
@@ -3,6 +3,8 @@
/* eslint-disable global-require */
require('../bin/pixi');
+PIXI.utils.skipHello(); // hide banner
+
describe('PIXI', function ()
{
it('should exist as a global object', function ()
@@ -11,4 +13,5 @@
});
require('./core');
require('./interaction');
+ require('./renders');
});
diff --git a/test/renders/index.js b/test/renders/index.js
new file mode 100644
index 0000000..cf9cbe2
--- /dev/null
+++ b/test/renders/index.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const Renderer = require('./lib/Renderer');
+const path = require('path');
+
+describe('renders', function ()
+{
+ before(function ()
+ {
+ this.renderer = new Renderer();
+ this.validate = function (id, done)
+ {
+ const code = path.join(__dirname, 'tests', `${id}.js`);
+ const solution = path.join(__dirname, 'solutions', `${id}.json`);
+
+ this.renderer.compare(code, solution, (err, success) =>
+ {
+ if (err)
+ {
+ assert(false, err.message);
+ }
+ assert(success, 'Render not successful');
+ done();
+ });
+ };
+ });
+
+ beforeEach(function ()
+ {
+ this.renderer.clear();
+ });
+
+ after(function ()
+ {
+ this.renderer.destroy();
+ this.renderer = null;
+ this.validate = null;
+ });
+
+ it('should draw a rectangle', function (done)
+ {
+ this.validate('graphics-rect', done);
+ });
+
+ it('should draw a sprite', function (done)
+ {
+ this.validate('sprite-new', done);
+ });
+});
diff --git a/test/renders/lib/ImageDiff.js b/test/renders/lib/ImageDiff.js
new file mode 100644
index 0000000..6bf2b54
--- /dev/null
+++ b/test/renders/lib/ImageDiff.js
@@ -0,0 +1,77 @@
+'use strict';
+
+/**
+ * Compare images
+ * @class ImageDiff
+ */
+class ImageDiff
+{
+ /**
+ * @constructor
+ * @param {int} width Width of the canvas
+ * @param {int} height Height of the canvas
+ * @param {Number} tolerance Tolerance for difference
+ */
+ constructor(width, height, tolerance)
+ {
+ this.width = width;
+ this.height = height;
+ this.tolerance = tolerance;
+
+ const canvas = document.createElement('canvas');
+
+ canvas.width = width;
+ canvas.height = height;
+ this.context = canvas.getContext('2d', {
+ antialias: false,
+ preserveDrawingBuffer: true,
+ });
+ }
+
+ /**
+ * Compare two base64 images
+ * @method compare
+ * @param {string} src1 Canvas source
+ * @param {string} src2 Canvas source
+ * @return {Boolean} If images are within tolerance
+ */
+ compare(src1, src2)
+ {
+ const a = this.getImageData(src1);
+ const b = this.getImageData(src2);
+ const len = a.length;
+ const tolerance = this.tolerance;
+
+ const diff = a.filter(function (val, i)
+ {
+ return Math.abs(val - b[i]) / 255 > tolerance;
+ });
+
+ if (diff.length / len > tolerance)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get an array of pixels
+ * @method getImageData
+ * @param {string} src Source of the image
+ * @return {Uint8ClampedArray} Data for image of pixels
+ */
+ getImageData(src)
+ {
+ const image = new Image();
+
+ image.src = src;
+ this.context.clearRect(0, 0, this.width, this.height);
+ this.context.drawImage(image, 0, 0, this.width, this.height);
+ const imageData = this.context.getImageData(0, 0, this.width, this.height);
+
+ return imageData.data;
+ }
+}
+
+module.exports = ImageDiff;
diff --git a/test/renders/lib/Renderer.js b/test/renders/lib/Renderer.js
new file mode 100644
index 0000000..d60217f
--- /dev/null
+++ b/test/renders/lib/Renderer.js
@@ -0,0 +1,302 @@
+'use strict';
+
+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(function (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} solution.webgl Solution for webgl
+ * @param {Array} 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;
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/renders/main.js b/scripts/renders/main.js
new file mode 100644
index 0000000..7ee1a1c
--- /dev/null
+++ b/scripts/renders/main.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app; // Control application life
+const BrowserWindow = electron.BrowserWindow; // Native browser window
+
+// Keep a global reference of the window object
+let mainWindow = null;
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () =>
+{
+ app.quit();
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+app.on('ready', () =>
+{
+ mainWindow = new BrowserWindow({
+ width: 675,
+ height: 400,
+ useContentSize: true,
+ resizable: false,
+ textAreasAreResizable: false,
+ });
+
+ mainWindow.loadURL(`file://${__dirname}/index.html`);
+ mainWindow.on('closed', () =>
+ {
+ mainWindow = null;
+ });
+});
diff --git a/scripts/renders/package.json b/scripts/renders/package.json
new file mode 100644
index 0000000..72753f3
--- /dev/null
+++ b/scripts/renders/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "renders-app",
+ "private": true,
+ "main": "main.js",
+ "dependencies": {
+ "chai": "^3.5.0",
+ "sinon": "^1.17.6",
+ "sinon-chai": "^2.8.0"
+ }
+}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
index 09620cb..e03220d 100644
--- a/test/.eslintrc.json
+++ b/test/.eslintrc.json
@@ -11,6 +11,7 @@
"globals": {
"sinon": false,
"expect": false,
+ "assert": false,
"PIXI": false
},
"rules": {
diff --git a/test/index.js b/test/index.js
index 1d245bc..7424430 100755
--- a/test/index.js
+++ b/test/index.js
@@ -3,6 +3,8 @@
/* eslint-disable global-require */
require('../bin/pixi');
+PIXI.utils.skipHello(); // hide banner
+
describe('PIXI', function ()
{
it('should exist as a global object', function ()
@@ -11,4 +13,5 @@
});
require('./core');
require('./interaction');
+ require('./renders');
});
diff --git a/test/renders/index.js b/test/renders/index.js
new file mode 100644
index 0000000..cf9cbe2
--- /dev/null
+++ b/test/renders/index.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const Renderer = require('./lib/Renderer');
+const path = require('path');
+
+describe('renders', function ()
+{
+ before(function ()
+ {
+ this.renderer = new Renderer();
+ this.validate = function (id, done)
+ {
+ const code = path.join(__dirname, 'tests', `${id}.js`);
+ const solution = path.join(__dirname, 'solutions', `${id}.json`);
+
+ this.renderer.compare(code, solution, (err, success) =>
+ {
+ if (err)
+ {
+ assert(false, err.message);
+ }
+ assert(success, 'Render not successful');
+ done();
+ });
+ };
+ });
+
+ beforeEach(function ()
+ {
+ this.renderer.clear();
+ });
+
+ after(function ()
+ {
+ this.renderer.destroy();
+ this.renderer = null;
+ this.validate = null;
+ });
+
+ it('should draw a rectangle', function (done)
+ {
+ this.validate('graphics-rect', done);
+ });
+
+ it('should draw a sprite', function (done)
+ {
+ this.validate('sprite-new', done);
+ });
+});
diff --git a/test/renders/lib/ImageDiff.js b/test/renders/lib/ImageDiff.js
new file mode 100644
index 0000000..6bf2b54
--- /dev/null
+++ b/test/renders/lib/ImageDiff.js
@@ -0,0 +1,77 @@
+'use strict';
+
+/**
+ * Compare images
+ * @class ImageDiff
+ */
+class ImageDiff
+{
+ /**
+ * @constructor
+ * @param {int} width Width of the canvas
+ * @param {int} height Height of the canvas
+ * @param {Number} tolerance Tolerance for difference
+ */
+ constructor(width, height, tolerance)
+ {
+ this.width = width;
+ this.height = height;
+ this.tolerance = tolerance;
+
+ const canvas = document.createElement('canvas');
+
+ canvas.width = width;
+ canvas.height = height;
+ this.context = canvas.getContext('2d', {
+ antialias: false,
+ preserveDrawingBuffer: true,
+ });
+ }
+
+ /**
+ * Compare two base64 images
+ * @method compare
+ * @param {string} src1 Canvas source
+ * @param {string} src2 Canvas source
+ * @return {Boolean} If images are within tolerance
+ */
+ compare(src1, src2)
+ {
+ const a = this.getImageData(src1);
+ const b = this.getImageData(src2);
+ const len = a.length;
+ const tolerance = this.tolerance;
+
+ const diff = a.filter(function (val, i)
+ {
+ return Math.abs(val - b[i]) / 255 > tolerance;
+ });
+
+ if (diff.length / len > tolerance)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get an array of pixels
+ * @method getImageData
+ * @param {string} src Source of the image
+ * @return {Uint8ClampedArray} Data for image of pixels
+ */
+ getImageData(src)
+ {
+ const image = new Image();
+
+ image.src = src;
+ this.context.clearRect(0, 0, this.width, this.height);
+ this.context.drawImage(image, 0, 0, this.width, this.height);
+ const imageData = this.context.getImageData(0, 0, this.width, this.height);
+
+ return imageData.data;
+ }
+}
+
+module.exports = ImageDiff;
diff --git a/test/renders/lib/Renderer.js b/test/renders/lib/Renderer.js
new file mode 100644
index 0000000..d60217f
--- /dev/null
+++ b/test/renders/lib/Renderer.js
@@ -0,0 +1,302 @@
+'use strict';
+
+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(function (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} solution.webgl Solution for webgl
+ * @param {Array} 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;
diff --git a/test/renders/solutions/graphics-rect.json b/test/renders/solutions/graphics-rect.json
new file mode 100644
index 0000000..d51ebd2
--- /dev/null
+++ b/test/renders/solutions/graphics-rect.json
@@ -0,0 +1,10 @@
+{
+ "webgl": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYR+3WsREAIAxCUZnJ/UdwJtxACopY/NTG417BRba9BkcEQACB/wWOupbY75rJPUAABBBAAAEEEBgX6K6BuJ3vgfhF94AACCBwAThbn6FVGGGJAAAAAElFTkSuQmCC",
+ "hash": "243969710ac251e9d5080cd55dedc517"
+ },
+ "canvas": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYR+3WsREAIAxCUZnJ/UdwJtxACopY/NTG417BRba9BkcEQACB/wWOupbY75rJPUAABBBAAAEEEBgX6K6BuJ3vgfhF94AACCBwAThbn6FVGGGJAAAAAElFTkSuQmCC",
+ "hash": "243969710ac251e9d5080cd55dedc517"
+ }
+}
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/renders/main.js b/scripts/renders/main.js
new file mode 100644
index 0000000..7ee1a1c
--- /dev/null
+++ b/scripts/renders/main.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app; // Control application life
+const BrowserWindow = electron.BrowserWindow; // Native browser window
+
+// Keep a global reference of the window object
+let mainWindow = null;
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () =>
+{
+ app.quit();
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+app.on('ready', () =>
+{
+ mainWindow = new BrowserWindow({
+ width: 675,
+ height: 400,
+ useContentSize: true,
+ resizable: false,
+ textAreasAreResizable: false,
+ });
+
+ mainWindow.loadURL(`file://${__dirname}/index.html`);
+ mainWindow.on('closed', () =>
+ {
+ mainWindow = null;
+ });
+});
diff --git a/scripts/renders/package.json b/scripts/renders/package.json
new file mode 100644
index 0000000..72753f3
--- /dev/null
+++ b/scripts/renders/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "renders-app",
+ "private": true,
+ "main": "main.js",
+ "dependencies": {
+ "chai": "^3.5.0",
+ "sinon": "^1.17.6",
+ "sinon-chai": "^2.8.0"
+ }
+}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
index 09620cb..e03220d 100644
--- a/test/.eslintrc.json
+++ b/test/.eslintrc.json
@@ -11,6 +11,7 @@
"globals": {
"sinon": false,
"expect": false,
+ "assert": false,
"PIXI": false
},
"rules": {
diff --git a/test/index.js b/test/index.js
index 1d245bc..7424430 100755
--- a/test/index.js
+++ b/test/index.js
@@ -3,6 +3,8 @@
/* eslint-disable global-require */
require('../bin/pixi');
+PIXI.utils.skipHello(); // hide banner
+
describe('PIXI', function ()
{
it('should exist as a global object', function ()
@@ -11,4 +13,5 @@
});
require('./core');
require('./interaction');
+ require('./renders');
});
diff --git a/test/renders/index.js b/test/renders/index.js
new file mode 100644
index 0000000..cf9cbe2
--- /dev/null
+++ b/test/renders/index.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const Renderer = require('./lib/Renderer');
+const path = require('path');
+
+describe('renders', function ()
+{
+ before(function ()
+ {
+ this.renderer = new Renderer();
+ this.validate = function (id, done)
+ {
+ const code = path.join(__dirname, 'tests', `${id}.js`);
+ const solution = path.join(__dirname, 'solutions', `${id}.json`);
+
+ this.renderer.compare(code, solution, (err, success) =>
+ {
+ if (err)
+ {
+ assert(false, err.message);
+ }
+ assert(success, 'Render not successful');
+ done();
+ });
+ };
+ });
+
+ beforeEach(function ()
+ {
+ this.renderer.clear();
+ });
+
+ after(function ()
+ {
+ this.renderer.destroy();
+ this.renderer = null;
+ this.validate = null;
+ });
+
+ it('should draw a rectangle', function (done)
+ {
+ this.validate('graphics-rect', done);
+ });
+
+ it('should draw a sprite', function (done)
+ {
+ this.validate('sprite-new', done);
+ });
+});
diff --git a/test/renders/lib/ImageDiff.js b/test/renders/lib/ImageDiff.js
new file mode 100644
index 0000000..6bf2b54
--- /dev/null
+++ b/test/renders/lib/ImageDiff.js
@@ -0,0 +1,77 @@
+'use strict';
+
+/**
+ * Compare images
+ * @class ImageDiff
+ */
+class ImageDiff
+{
+ /**
+ * @constructor
+ * @param {int} width Width of the canvas
+ * @param {int} height Height of the canvas
+ * @param {Number} tolerance Tolerance for difference
+ */
+ constructor(width, height, tolerance)
+ {
+ this.width = width;
+ this.height = height;
+ this.tolerance = tolerance;
+
+ const canvas = document.createElement('canvas');
+
+ canvas.width = width;
+ canvas.height = height;
+ this.context = canvas.getContext('2d', {
+ antialias: false,
+ preserveDrawingBuffer: true,
+ });
+ }
+
+ /**
+ * Compare two base64 images
+ * @method compare
+ * @param {string} src1 Canvas source
+ * @param {string} src2 Canvas source
+ * @return {Boolean} If images are within tolerance
+ */
+ compare(src1, src2)
+ {
+ const a = this.getImageData(src1);
+ const b = this.getImageData(src2);
+ const len = a.length;
+ const tolerance = this.tolerance;
+
+ const diff = a.filter(function (val, i)
+ {
+ return Math.abs(val - b[i]) / 255 > tolerance;
+ });
+
+ if (diff.length / len > tolerance)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get an array of pixels
+ * @method getImageData
+ * @param {string} src Source of the image
+ * @return {Uint8ClampedArray} Data for image of pixels
+ */
+ getImageData(src)
+ {
+ const image = new Image();
+
+ image.src = src;
+ this.context.clearRect(0, 0, this.width, this.height);
+ this.context.drawImage(image, 0, 0, this.width, this.height);
+ const imageData = this.context.getImageData(0, 0, this.width, this.height);
+
+ return imageData.data;
+ }
+}
+
+module.exports = ImageDiff;
diff --git a/test/renders/lib/Renderer.js b/test/renders/lib/Renderer.js
new file mode 100644
index 0000000..d60217f
--- /dev/null
+++ b/test/renders/lib/Renderer.js
@@ -0,0 +1,302 @@
+'use strict';
+
+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(function (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} solution.webgl Solution for webgl
+ * @param {Array} 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;
diff --git a/test/renders/solutions/graphics-rect.json b/test/renders/solutions/graphics-rect.json
new file mode 100644
index 0000000..d51ebd2
--- /dev/null
+++ b/test/renders/solutions/graphics-rect.json
@@ -0,0 +1,10 @@
+{
+ "webgl": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYR+3WsREAIAxCUZnJ/UdwJtxACopY/NTG417BRba9BkcEQACB/wWOupbY75rJPUAABBBAAAEEEBgX6K6BuJ3vgfhF94AACCBwAThbn6FVGGGJAAAAAElFTkSuQmCC",
+ "hash": "243969710ac251e9d5080cd55dedc517"
+ },
+ "canvas": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYR+3WsREAIAxCUZnJ/UdwJtxACopY/NTG417BRba9BkcEQACB/wWOupbY75rJPUAABBBAAAEEEBgX6K6BuJ3vgfhF94AACCBwAThbn6FVGGGJAAAAAElFTkSuQmCC",
+ "hash": "243969710ac251e9d5080cd55dedc517"
+ }
+}
\ No newline at end of file
diff --git a/test/renders/solutions/sprite-new.json b/test/renders/solutions/sprite-new.json
new file mode 100644
index 0000000..c5396a6
--- /dev/null
+++ b/test/renders/solutions/sprite-new.json
@@ -0,0 +1,10 @@
+{
+ "webgl": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAASUlEQVRYR+3XsQ0AMAwCwTBtZvK0pI6S1sLFewALnWiQba/giQDjBLTV2gjXXbmnAwRAAAEEEEAAAQQQQACBuEDrKPg8ZxvGBQ6XssehFyMftwAAAABJRU5ErkJggg==",
+ "hash": "bd5ca4fe3ec53c421044c78d86592b86"
+ },
+ "canvas": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAASUlEQVRYR+3XsQ0AMAwCwTBtZvK0pI6S1sLFewALnWiQba/giQDjBLTV2gjXXbmnAwRAAAEEEEAAAQQQQACBuEDrKPg8ZxvGBQ6XssehFyMftwAAAABJRU5ErkJggg==",
+ "hash": "bd5ca4fe3ec53c421044c78d86592b86"
+ }
+}
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/renders/main.js b/scripts/renders/main.js
new file mode 100644
index 0000000..7ee1a1c
--- /dev/null
+++ b/scripts/renders/main.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app; // Control application life
+const BrowserWindow = electron.BrowserWindow; // Native browser window
+
+// Keep a global reference of the window object
+let mainWindow = null;
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () =>
+{
+ app.quit();
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+app.on('ready', () =>
+{
+ mainWindow = new BrowserWindow({
+ width: 675,
+ height: 400,
+ useContentSize: true,
+ resizable: false,
+ textAreasAreResizable: false,
+ });
+
+ mainWindow.loadURL(`file://${__dirname}/index.html`);
+ mainWindow.on('closed', () =>
+ {
+ mainWindow = null;
+ });
+});
diff --git a/scripts/renders/package.json b/scripts/renders/package.json
new file mode 100644
index 0000000..72753f3
--- /dev/null
+++ b/scripts/renders/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "renders-app",
+ "private": true,
+ "main": "main.js",
+ "dependencies": {
+ "chai": "^3.5.0",
+ "sinon": "^1.17.6",
+ "sinon-chai": "^2.8.0"
+ }
+}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
index 09620cb..e03220d 100644
--- a/test/.eslintrc.json
+++ b/test/.eslintrc.json
@@ -11,6 +11,7 @@
"globals": {
"sinon": false,
"expect": false,
+ "assert": false,
"PIXI": false
},
"rules": {
diff --git a/test/index.js b/test/index.js
index 1d245bc..7424430 100755
--- a/test/index.js
+++ b/test/index.js
@@ -3,6 +3,8 @@
/* eslint-disable global-require */
require('../bin/pixi');
+PIXI.utils.skipHello(); // hide banner
+
describe('PIXI', function ()
{
it('should exist as a global object', function ()
@@ -11,4 +13,5 @@
});
require('./core');
require('./interaction');
+ require('./renders');
});
diff --git a/test/renders/index.js b/test/renders/index.js
new file mode 100644
index 0000000..cf9cbe2
--- /dev/null
+++ b/test/renders/index.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const Renderer = require('./lib/Renderer');
+const path = require('path');
+
+describe('renders', function ()
+{
+ before(function ()
+ {
+ this.renderer = new Renderer();
+ this.validate = function (id, done)
+ {
+ const code = path.join(__dirname, 'tests', `${id}.js`);
+ const solution = path.join(__dirname, 'solutions', `${id}.json`);
+
+ this.renderer.compare(code, solution, (err, success) =>
+ {
+ if (err)
+ {
+ assert(false, err.message);
+ }
+ assert(success, 'Render not successful');
+ done();
+ });
+ };
+ });
+
+ beforeEach(function ()
+ {
+ this.renderer.clear();
+ });
+
+ after(function ()
+ {
+ this.renderer.destroy();
+ this.renderer = null;
+ this.validate = null;
+ });
+
+ it('should draw a rectangle', function (done)
+ {
+ this.validate('graphics-rect', done);
+ });
+
+ it('should draw a sprite', function (done)
+ {
+ this.validate('sprite-new', done);
+ });
+});
diff --git a/test/renders/lib/ImageDiff.js b/test/renders/lib/ImageDiff.js
new file mode 100644
index 0000000..6bf2b54
--- /dev/null
+++ b/test/renders/lib/ImageDiff.js
@@ -0,0 +1,77 @@
+'use strict';
+
+/**
+ * Compare images
+ * @class ImageDiff
+ */
+class ImageDiff
+{
+ /**
+ * @constructor
+ * @param {int} width Width of the canvas
+ * @param {int} height Height of the canvas
+ * @param {Number} tolerance Tolerance for difference
+ */
+ constructor(width, height, tolerance)
+ {
+ this.width = width;
+ this.height = height;
+ this.tolerance = tolerance;
+
+ const canvas = document.createElement('canvas');
+
+ canvas.width = width;
+ canvas.height = height;
+ this.context = canvas.getContext('2d', {
+ antialias: false,
+ preserveDrawingBuffer: true,
+ });
+ }
+
+ /**
+ * Compare two base64 images
+ * @method compare
+ * @param {string} src1 Canvas source
+ * @param {string} src2 Canvas source
+ * @return {Boolean} If images are within tolerance
+ */
+ compare(src1, src2)
+ {
+ const a = this.getImageData(src1);
+ const b = this.getImageData(src2);
+ const len = a.length;
+ const tolerance = this.tolerance;
+
+ const diff = a.filter(function (val, i)
+ {
+ return Math.abs(val - b[i]) / 255 > tolerance;
+ });
+
+ if (diff.length / len > tolerance)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get an array of pixels
+ * @method getImageData
+ * @param {string} src Source of the image
+ * @return {Uint8ClampedArray} Data for image of pixels
+ */
+ getImageData(src)
+ {
+ const image = new Image();
+
+ image.src = src;
+ this.context.clearRect(0, 0, this.width, this.height);
+ this.context.drawImage(image, 0, 0, this.width, this.height);
+ const imageData = this.context.getImageData(0, 0, this.width, this.height);
+
+ return imageData.data;
+ }
+}
+
+module.exports = ImageDiff;
diff --git a/test/renders/lib/Renderer.js b/test/renders/lib/Renderer.js
new file mode 100644
index 0000000..d60217f
--- /dev/null
+++ b/test/renders/lib/Renderer.js
@@ -0,0 +1,302 @@
+'use strict';
+
+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(function (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} solution.webgl Solution for webgl
+ * @param {Array} 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;
diff --git a/test/renders/solutions/graphics-rect.json b/test/renders/solutions/graphics-rect.json
new file mode 100644
index 0000000..d51ebd2
--- /dev/null
+++ b/test/renders/solutions/graphics-rect.json
@@ -0,0 +1,10 @@
+{
+ "webgl": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYR+3WsREAIAxCUZnJ/UdwJtxACopY/NTG417BRba9BkcEQACB/wWOupbY75rJPUAABBBAAAEEEBgX6K6BuJ3vgfhF94AACCBwAThbn6FVGGGJAAAAAElFTkSuQmCC",
+ "hash": "243969710ac251e9d5080cd55dedc517"
+ },
+ "canvas": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYR+3WsREAIAxCUZnJ/UdwJtxACopY/NTG417BRba9BkcEQACB/wWOupbY75rJPUAABBBAAAEEEBgX6K6BuJ3vgfhF94AACCBwAThbn6FVGGGJAAAAAElFTkSuQmCC",
+ "hash": "243969710ac251e9d5080cd55dedc517"
+ }
+}
\ No newline at end of file
diff --git a/test/renders/solutions/sprite-new.json b/test/renders/solutions/sprite-new.json
new file mode 100644
index 0000000..c5396a6
--- /dev/null
+++ b/test/renders/solutions/sprite-new.json
@@ -0,0 +1,10 @@
+{
+ "webgl": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAASUlEQVRYR+3XsQ0AMAwCwTBtZvK0pI6S1sLFewALnWiQba/giQDjBLTV2gjXXbmnAwRAAAEEEEAAAQQQQACBuEDrKPg8ZxvGBQ6XssehFyMftwAAAABJRU5ErkJggg==",
+ "hash": "bd5ca4fe3ec53c421044c78d86592b86"
+ },
+ "canvas": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAASUlEQVRYR+3XsQ0AMAwCwTBtZvK0pI6S1sLFewALnWiQba/giQDjBLTV2gjXXbmnAwRAAAEEEEAAAQQQQACBuEDrKPg8ZxvGBQ6XssehFyMftwAAAABJRU5ErkJggg==",
+ "hash": "bd5ca4fe3ec53c421044c78d86592b86"
+ }
+}
\ No newline at end of file
diff --git a/test/renders/tests/assets/bitmap-1.png b/test/renders/tests/assets/bitmap-1.png
new file mode 100644
index 0000000..1284fb8
--- /dev/null
+++ b/test/renders/tests/assets/bitmap-1.png
Binary files differ
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/renders/main.js b/scripts/renders/main.js
new file mode 100644
index 0000000..7ee1a1c
--- /dev/null
+++ b/scripts/renders/main.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app; // Control application life
+const BrowserWindow = electron.BrowserWindow; // Native browser window
+
+// Keep a global reference of the window object
+let mainWindow = null;
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () =>
+{
+ app.quit();
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+app.on('ready', () =>
+{
+ mainWindow = new BrowserWindow({
+ width: 675,
+ height: 400,
+ useContentSize: true,
+ resizable: false,
+ textAreasAreResizable: false,
+ });
+
+ mainWindow.loadURL(`file://${__dirname}/index.html`);
+ mainWindow.on('closed', () =>
+ {
+ mainWindow = null;
+ });
+});
diff --git a/scripts/renders/package.json b/scripts/renders/package.json
new file mode 100644
index 0000000..72753f3
--- /dev/null
+++ b/scripts/renders/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "renders-app",
+ "private": true,
+ "main": "main.js",
+ "dependencies": {
+ "chai": "^3.5.0",
+ "sinon": "^1.17.6",
+ "sinon-chai": "^2.8.0"
+ }
+}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
index 09620cb..e03220d 100644
--- a/test/.eslintrc.json
+++ b/test/.eslintrc.json
@@ -11,6 +11,7 @@
"globals": {
"sinon": false,
"expect": false,
+ "assert": false,
"PIXI": false
},
"rules": {
diff --git a/test/index.js b/test/index.js
index 1d245bc..7424430 100755
--- a/test/index.js
+++ b/test/index.js
@@ -3,6 +3,8 @@
/* eslint-disable global-require */
require('../bin/pixi');
+PIXI.utils.skipHello(); // hide banner
+
describe('PIXI', function ()
{
it('should exist as a global object', function ()
@@ -11,4 +13,5 @@
});
require('./core');
require('./interaction');
+ require('./renders');
});
diff --git a/test/renders/index.js b/test/renders/index.js
new file mode 100644
index 0000000..cf9cbe2
--- /dev/null
+++ b/test/renders/index.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const Renderer = require('./lib/Renderer');
+const path = require('path');
+
+describe('renders', function ()
+{
+ before(function ()
+ {
+ this.renderer = new Renderer();
+ this.validate = function (id, done)
+ {
+ const code = path.join(__dirname, 'tests', `${id}.js`);
+ const solution = path.join(__dirname, 'solutions', `${id}.json`);
+
+ this.renderer.compare(code, solution, (err, success) =>
+ {
+ if (err)
+ {
+ assert(false, err.message);
+ }
+ assert(success, 'Render not successful');
+ done();
+ });
+ };
+ });
+
+ beforeEach(function ()
+ {
+ this.renderer.clear();
+ });
+
+ after(function ()
+ {
+ this.renderer.destroy();
+ this.renderer = null;
+ this.validate = null;
+ });
+
+ it('should draw a rectangle', function (done)
+ {
+ this.validate('graphics-rect', done);
+ });
+
+ it('should draw a sprite', function (done)
+ {
+ this.validate('sprite-new', done);
+ });
+});
diff --git a/test/renders/lib/ImageDiff.js b/test/renders/lib/ImageDiff.js
new file mode 100644
index 0000000..6bf2b54
--- /dev/null
+++ b/test/renders/lib/ImageDiff.js
@@ -0,0 +1,77 @@
+'use strict';
+
+/**
+ * Compare images
+ * @class ImageDiff
+ */
+class ImageDiff
+{
+ /**
+ * @constructor
+ * @param {int} width Width of the canvas
+ * @param {int} height Height of the canvas
+ * @param {Number} tolerance Tolerance for difference
+ */
+ constructor(width, height, tolerance)
+ {
+ this.width = width;
+ this.height = height;
+ this.tolerance = tolerance;
+
+ const canvas = document.createElement('canvas');
+
+ canvas.width = width;
+ canvas.height = height;
+ this.context = canvas.getContext('2d', {
+ antialias: false,
+ preserveDrawingBuffer: true,
+ });
+ }
+
+ /**
+ * Compare two base64 images
+ * @method compare
+ * @param {string} src1 Canvas source
+ * @param {string} src2 Canvas source
+ * @return {Boolean} If images are within tolerance
+ */
+ compare(src1, src2)
+ {
+ const a = this.getImageData(src1);
+ const b = this.getImageData(src2);
+ const len = a.length;
+ const tolerance = this.tolerance;
+
+ const diff = a.filter(function (val, i)
+ {
+ return Math.abs(val - b[i]) / 255 > tolerance;
+ });
+
+ if (diff.length / len > tolerance)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get an array of pixels
+ * @method getImageData
+ * @param {string} src Source of the image
+ * @return {Uint8ClampedArray} Data for image of pixels
+ */
+ getImageData(src)
+ {
+ const image = new Image();
+
+ image.src = src;
+ this.context.clearRect(0, 0, this.width, this.height);
+ this.context.drawImage(image, 0, 0, this.width, this.height);
+ const imageData = this.context.getImageData(0, 0, this.width, this.height);
+
+ return imageData.data;
+ }
+}
+
+module.exports = ImageDiff;
diff --git a/test/renders/lib/Renderer.js b/test/renders/lib/Renderer.js
new file mode 100644
index 0000000..d60217f
--- /dev/null
+++ b/test/renders/lib/Renderer.js
@@ -0,0 +1,302 @@
+'use strict';
+
+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(function (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} solution.webgl Solution for webgl
+ * @param {Array} 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;
diff --git a/test/renders/solutions/graphics-rect.json b/test/renders/solutions/graphics-rect.json
new file mode 100644
index 0000000..d51ebd2
--- /dev/null
+++ b/test/renders/solutions/graphics-rect.json
@@ -0,0 +1,10 @@
+{
+ "webgl": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYR+3WsREAIAxCUZnJ/UdwJtxACopY/NTG417BRba9BkcEQACB/wWOupbY75rJPUAABBBAAAEEEBgX6K6BuJ3vgfhF94AACCBwAThbn6FVGGGJAAAAAElFTkSuQmCC",
+ "hash": "243969710ac251e9d5080cd55dedc517"
+ },
+ "canvas": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYR+3WsREAIAxCUZnJ/UdwJtxACopY/NTG417BRba9BkcEQACB/wWOupbY75rJPUAABBBAAAEEEBgX6K6BuJ3vgfhF94AACCBwAThbn6FVGGGJAAAAAElFTkSuQmCC",
+ "hash": "243969710ac251e9d5080cd55dedc517"
+ }
+}
\ No newline at end of file
diff --git a/test/renders/solutions/sprite-new.json b/test/renders/solutions/sprite-new.json
new file mode 100644
index 0000000..c5396a6
--- /dev/null
+++ b/test/renders/solutions/sprite-new.json
@@ -0,0 +1,10 @@
+{
+ "webgl": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAASUlEQVRYR+3XsQ0AMAwCwTBtZvK0pI6S1sLFewALnWiQba/giQDjBLTV2gjXXbmnAwRAAAEEEEAAAQQQQACBuEDrKPg8ZxvGBQ6XssehFyMftwAAAABJRU5ErkJggg==",
+ "hash": "bd5ca4fe3ec53c421044c78d86592b86"
+ },
+ "canvas": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAASUlEQVRYR+3XsQ0AMAwCwTBtZvK0pI6S1sLFewALnWiQba/giQDjBLTV2gjXXbmnAwRAAAEEEEAAAQQQQACBuEDrKPg8ZxvGBQ6XssehFyMftwAAAABJRU5ErkJggg==",
+ "hash": "bd5ca4fe3ec53c421044c78d86592b86"
+ }
+}
\ No newline at end of file
diff --git a/test/renders/tests/assets/bitmap-1.png b/test/renders/tests/assets/bitmap-1.png
new file mode 100644
index 0000000..1284fb8
--- /dev/null
+++ b/test/renders/tests/assets/bitmap-1.png
Binary files differ
diff --git a/test/renders/tests/graphics-rect.js b/test/renders/tests/graphics-rect.js
new file mode 100644
index 0000000..1143c96
--- /dev/null
+++ b/test/renders/tests/graphics-rect.js
@@ -0,0 +1,22 @@
+'use strict';
+
+module.exports = function ()
+{
+ const graphic = new PIXI.Graphics()
+ .beginFill(0xFFCC00, 1)
+ .drawRect(8, 8, 16, 16);
+
+ expect(graphic.width).to.equal(16);
+ expect(graphic.height).to.equal(16);
+ expect(graphic.x).to.equal(0);
+ expect(graphic.y).to.equal(0);
+
+ const bounds = graphic.getBounds();
+
+ expect(bounds.x).to.equal(8);
+ expect(bounds.y).to.equal(8);
+ expect(bounds.width).to.equal(16);
+ expect(bounds.height).to.equal(16);
+
+ this.stage.addChild(graphic);
+};
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ef2050a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+scripts/renders/node_modules/*
\ No newline at end of file
diff --git a/package.json b/package.json
index 94140a4..8cc41a6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,8 @@
"watch:lint": "watch \"eslint scripts src test || exit 0\" src",
"test": "floss --path test/index.js",
"test:debug": "npm test -- --debug",
+ "prerenders": "npm --prefix scripts/renders i scripts/renders",
+ "renders": "electron scripts/renders",
"precoverage": "rimraf coverage && npm run build -- --noExternal",
"coverage": "npm test -- -c bin/pixi.js -s -h",
"lint": "eslint scripts src test --max-warnings 0",
@@ -66,6 +68,7 @@
"floss": "^1.2.0",
"gh-pages": "^0.11.0",
"jaguarjs-jsdoc": "^1.0.1",
+ "js-md5": "^0.4.1",
"jsdoc": "^3.4.2",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
diff --git a/scripts/renders/.eslintrc.json b/scripts/renders/.eslintrc.json
new file mode 100644
index 0000000..5a103d2
--- /dev/null
+++ b/scripts/renders/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "globals": {
+ "document": false,
+ "PIXI": false
+ }
+}
\ No newline at end of file
diff --git a/scripts/renders/client.css b/scripts/renders/client.css
new file mode 100755
index 0000000..b544283
--- /dev/null
+++ b/scripts/renders/client.css
@@ -0,0 +1,145 @@
+body {
+ background:#ccc;
+ font-family:Arial, sans-serif;
+ margin: 0 0 0 0;
+ padding: 20px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ overflow: hidden;
+ box-sizing: border-box;
+ transition:background-color 0.3s;
+}
+body, body * {
+ user-select:none;
+ -webkit-user-select:none;
+ cursor:default;
+}
+body.dragging {
+ background-color:#ddd;
+}
+body.dragging #solution {
+ background-color: #fff;
+}
+#frame {
+ margin:0 auto;
+ max-width:640px;
+ width:100%;
+}
+.hide {
+ display:none;
+}
+#solution {
+ border:1px solid #bbb;
+ padding:10px;
+ border-radius:6px;
+ box-sizing:border-box;
+ transition:background-color 0.3s;
+ font-size:10px;
+ font-family: monospace;
+ background:#ddd;
+ color:#666;
+ height:240px;
+ width:403px;
+ margin-bottom:20px;
+ resize:none;
+ white-space:pre;
+ line-height: 1.4;
+ overflow-x:hidden;
+ overflow-y: auto;
+ user-select:initial;
+ -webkit-user-select:initial;
+}
+#solution:focus, textarea:focus {
+ outline:0;
+ border-color:#4993EF;
+}
+#solution.copied {
+ position:relative;
+ border-color:#4993EF;
+}
+#solution.copied:before {
+ display:block;
+ position:absolute;
+ top:0;
+ right:10px;
+ padding:8px 16px;
+ color:#fff;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ font-size:8px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ font-family: Arial, sans-serif;
+ content: 'Copied To Clipboard';
+ background-color:#4993EF;
+}
+label {
+ display:inline-block;
+ padding:15px 0 10px;
+ font-size: 10px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ border-radius: 6px;
+ background-color: rgba(0,0,0,0.1);
+ padding: 4px 8px;
+ color: #fff;
+ margin-bottom:10px;
+}
+button {
+ padding:10px 20px;
+ border: 1px solid #3267A9;
+ background-color: #4993EF;
+ color: #fff;
+ font-size:14px;
+ border-radius:6px;
+ cursor:pointer;
+ display:block;
+ width:100%;
+ transition:background 0.3s;
+ box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.5);
+ outline:0;
+}
+button:hover{
+ background-color: #5CA5FF;
+}
+button:active {
+ background-color: #3267A9;
+ box-shadow: none;
+}
+button.disabled {
+ pointer-events:none;
+ background-color:#999;
+ border-color:#777;
+ box-shadow: none;
+ color:#ddd;
+}
+.cols {
+ display:flex;
+ margin-top:20px;
+}
+.cols > .preview {
+ width:100px;
+ margin-right:20px;
+}
+.cols > .output {
+ width:100%;
+}
+.full {
+ width:100%;
+}
+.center {
+ text-align:center;
+}
+.view {
+ image-rendering: optimizeSpeed;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+ zoom:3;
+}
\ No newline at end of file
diff --git a/scripts/renders/client.js b/scripts/renders/client.js
new file mode 100755
index 0000000..16c9565
--- /dev/null
+++ b/scripts/renders/client.js
@@ -0,0 +1,135 @@
+'use strict';
+
+require('../../bin/pixi');
+PIXI.utils.skipHello();
+
+const fs = require('fs');
+const path = require('path');
+const electron = require('electron');
+const remote = electron.remote;
+const clipboard = electron.clipboard;
+const dialog = remote.dialog;
+const Renderer = require('../../test/renders/lib/Renderer');
+const Droppable = require('./droppable');
+const $ = document.querySelector.bind(document);
+
+// Feature parity with floss: chai, sinon, sinon-chai
+const chai = require('chai');
+const sinon = require('sinon');
+const sinonChai = require('sinon-chai');
+
+global.chai = chai;
+global.sinon = sinon;
+global.should = chai.should;
+global.assert = chai.assert;
+global.expect = chai.expect;
+global.chai.use(sinonChai);
+
+// Create the renderer to convert the json into
+// a JSON solution which can be used by testing
+const renderer = new Renderer(
+ $('#view-webgl'),
+ $('#view-canvas')
+);
+
+// Clicking on the solution copies it to the clipboard
+const solution = $('#solution');
+
+// Current filename
+let currentName;
+let currentPath;
+
+// drag-n-drop
+new Droppable($('body'), (err, file) => // eslint-disable-line no-new
+{
+ if (err)
+ {
+ dialog.showErrorBox('Drop Error', err.message);
+ }
+ else if (!(/\.js$/).test(file))
+ {
+ dialog.showErrorBox('Invalid Filetype', 'The specified file must be a ".js" file.');
+ }
+ else
+ {
+ open(file);
+ }
+});
+
+// Click on the chooser button to browser for file
+$('#choose').addEventListener('click', () =>
+{
+ dialog.showOpenDialog({
+ filters: [{
+ name: 'JavaScript',
+ extensions: ['js'],
+ }],
+ },
+ (fileNames) =>
+ {
+ if (fileNames)
+ {
+ open(fileNames[0]);
+ }
+ });
+});
+
+const save = $('#save');
+
+save.addEventListener('click', () =>
+{
+ dialog.showSaveDialog({
+ title: 'Save Solution',
+ defaultPath: path.join(currentPath, `${currentName}.json`),
+ filters: [{
+ name: 'JSON',
+ extensions: ['json'],
+ }],
+ }, (fileName) =>
+ {
+ if (fileName)
+ {
+ fs.writeFileSync(fileName, solution.innerHTML);
+ }
+ });
+});
+
+let copiedId = null;
+
+solution.addEventListener('click', () =>
+{
+ if (solution.innerHTML)
+ {
+ clipboard.writeText(solution.innerHTML);
+ solution.className = 'copied';
+ if (copiedId)
+ {
+ clearTimeout(copiedId);
+ copiedId = null;
+ }
+ copiedId = setTimeout(() =>
+ {
+ solution.className = '';
+ copiedId = null;
+ }, 3000);
+ }
+});
+
+function open(file)
+{
+ save.className = 'disabled';
+ renderer.run(file, (err, result) =>
+ {
+ if (err)
+ {
+ dialog.showErrorBox('Invalid Output Format', err.message);
+ }
+ else
+ {
+ solution.innerHTML = JSON.stringify(result, null, ' ');
+ save.className = '';
+ currentName = path.basename(file, '.js');
+ currentPath = path.dirname(file);
+ }
+ });
+}
diff --git a/scripts/renders/droppable.js b/scripts/renders/droppable.js
new file mode 100644
index 0000000..b5db16b
--- /dev/null
+++ b/scripts/renders/droppable.js
@@ -0,0 +1,67 @@
+'use strict';
+
+/**
+ * Add drag-n-drop handling
+ * @class Droppable
+ */
+class Droppable
+{
+ /**
+ * @constructor
+ * @param {HTMLElement} node Element to listen on
+ * @param {Function} callback Callback when complete
+ */
+ constructor(node, callback)
+ {
+ node.addEventListener('dragenter', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = Droppable.CLASS_NAME;
+ });
+
+ node.addEventListener('dragover', (ev) =>
+ {
+ ev.preventDefault();
+ if (node.className !== Droppable.CLASS_NAME)
+ {
+ node.className = Droppable.CLASS_NAME;
+ }
+ });
+
+ node.addEventListener('dragleave', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ });
+
+ node.addEventListener('drop', (ev) =>
+ {
+ ev.preventDefault();
+ node.className = '';
+ const fileList = ev.dataTransfer.files;
+
+ if (fileList.length > 1)
+ {
+ callback(new Error('Only one file at a time.'));
+ }
+ else
+ {
+ const file = fileList[0];
+
+ callback(null, file.path);
+ }
+ });
+ }
+
+ /**
+ * The name of the class to add to the HTML node
+ * @static
+ * @property {String} CLASS_NAME
+ */
+ static get CLASS_NAME()
+ {
+ return 'dragging';
+ }
+}
+
+module.exports = Droppable;
diff --git a/scripts/renders/index.html b/scripts/renders/index.html
new file mode 100755
index 0000000..bc95d9a
--- /dev/null
+++ b/scripts/renders/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Pixi Tests Tool
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/scripts/renders/main.js b/scripts/renders/main.js
new file mode 100644
index 0000000..7ee1a1c
--- /dev/null
+++ b/scripts/renders/main.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const electron = require('electron');
+const app = electron.app; // Control application life
+const BrowserWindow = electron.BrowserWindow; // Native browser window
+
+// Keep a global reference of the window object
+let mainWindow = null;
+
+// Quit when all windows are closed.
+app.on('window-all-closed', () =>
+{
+ app.quit();
+});
+
+// This method will be called when Electron has finished
+// initialization and is ready to create browser windows.
+app.on('ready', () =>
+{
+ mainWindow = new BrowserWindow({
+ width: 675,
+ height: 400,
+ useContentSize: true,
+ resizable: false,
+ textAreasAreResizable: false,
+ });
+
+ mainWindow.loadURL(`file://${__dirname}/index.html`);
+ mainWindow.on('closed', () =>
+ {
+ mainWindow = null;
+ });
+});
diff --git a/scripts/renders/package.json b/scripts/renders/package.json
new file mode 100644
index 0000000..72753f3
--- /dev/null
+++ b/scripts/renders/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "renders-app",
+ "private": true,
+ "main": "main.js",
+ "dependencies": {
+ "chai": "^3.5.0",
+ "sinon": "^1.17.6",
+ "sinon-chai": "^2.8.0"
+ }
+}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
index 09620cb..e03220d 100644
--- a/test/.eslintrc.json
+++ b/test/.eslintrc.json
@@ -11,6 +11,7 @@
"globals": {
"sinon": false,
"expect": false,
+ "assert": false,
"PIXI": false
},
"rules": {
diff --git a/test/index.js b/test/index.js
index 1d245bc..7424430 100755
--- a/test/index.js
+++ b/test/index.js
@@ -3,6 +3,8 @@
/* eslint-disable global-require */
require('../bin/pixi');
+PIXI.utils.skipHello(); // hide banner
+
describe('PIXI', function ()
{
it('should exist as a global object', function ()
@@ -11,4 +13,5 @@
});
require('./core');
require('./interaction');
+ require('./renders');
});
diff --git a/test/renders/index.js b/test/renders/index.js
new file mode 100644
index 0000000..cf9cbe2
--- /dev/null
+++ b/test/renders/index.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const Renderer = require('./lib/Renderer');
+const path = require('path');
+
+describe('renders', function ()
+{
+ before(function ()
+ {
+ this.renderer = new Renderer();
+ this.validate = function (id, done)
+ {
+ const code = path.join(__dirname, 'tests', `${id}.js`);
+ const solution = path.join(__dirname, 'solutions', `${id}.json`);
+
+ this.renderer.compare(code, solution, (err, success) =>
+ {
+ if (err)
+ {
+ assert(false, err.message);
+ }
+ assert(success, 'Render not successful');
+ done();
+ });
+ };
+ });
+
+ beforeEach(function ()
+ {
+ this.renderer.clear();
+ });
+
+ after(function ()
+ {
+ this.renderer.destroy();
+ this.renderer = null;
+ this.validate = null;
+ });
+
+ it('should draw a rectangle', function (done)
+ {
+ this.validate('graphics-rect', done);
+ });
+
+ it('should draw a sprite', function (done)
+ {
+ this.validate('sprite-new', done);
+ });
+});
diff --git a/test/renders/lib/ImageDiff.js b/test/renders/lib/ImageDiff.js
new file mode 100644
index 0000000..6bf2b54
--- /dev/null
+++ b/test/renders/lib/ImageDiff.js
@@ -0,0 +1,77 @@
+'use strict';
+
+/**
+ * Compare images
+ * @class ImageDiff
+ */
+class ImageDiff
+{
+ /**
+ * @constructor
+ * @param {int} width Width of the canvas
+ * @param {int} height Height of the canvas
+ * @param {Number} tolerance Tolerance for difference
+ */
+ constructor(width, height, tolerance)
+ {
+ this.width = width;
+ this.height = height;
+ this.tolerance = tolerance;
+
+ const canvas = document.createElement('canvas');
+
+ canvas.width = width;
+ canvas.height = height;
+ this.context = canvas.getContext('2d', {
+ antialias: false,
+ preserveDrawingBuffer: true,
+ });
+ }
+
+ /**
+ * Compare two base64 images
+ * @method compare
+ * @param {string} src1 Canvas source
+ * @param {string} src2 Canvas source
+ * @return {Boolean} If images are within tolerance
+ */
+ compare(src1, src2)
+ {
+ const a = this.getImageData(src1);
+ const b = this.getImageData(src2);
+ const len = a.length;
+ const tolerance = this.tolerance;
+
+ const diff = a.filter(function (val, i)
+ {
+ return Math.abs(val - b[i]) / 255 > tolerance;
+ });
+
+ if (diff.length / len > tolerance)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get an array of pixels
+ * @method getImageData
+ * @param {string} src Source of the image
+ * @return {Uint8ClampedArray} Data for image of pixels
+ */
+ getImageData(src)
+ {
+ const image = new Image();
+
+ image.src = src;
+ this.context.clearRect(0, 0, this.width, this.height);
+ this.context.drawImage(image, 0, 0, this.width, this.height);
+ const imageData = this.context.getImageData(0, 0, this.width, this.height);
+
+ return imageData.data;
+ }
+}
+
+module.exports = ImageDiff;
diff --git a/test/renders/lib/Renderer.js b/test/renders/lib/Renderer.js
new file mode 100644
index 0000000..d60217f
--- /dev/null
+++ b/test/renders/lib/Renderer.js
@@ -0,0 +1,302 @@
+'use strict';
+
+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(function (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} solution.webgl Solution for webgl
+ * @param {Array} 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;
diff --git a/test/renders/solutions/graphics-rect.json b/test/renders/solutions/graphics-rect.json
new file mode 100644
index 0000000..d51ebd2
--- /dev/null
+++ b/test/renders/solutions/graphics-rect.json
@@ -0,0 +1,10 @@
+{
+ "webgl": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYR+3WsREAIAxCUZnJ/UdwJtxACopY/NTG417BRba9BkcEQACB/wWOupbY75rJPUAABBBAAAEEEBgX6K6BuJ3vgfhF94AACCBwAThbn6FVGGGJAAAAAElFTkSuQmCC",
+ "hash": "243969710ac251e9d5080cd55dedc517"
+ },
+ "canvas": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAATklEQVRYR+3WsREAIAxCUZnJ/UdwJtxACopY/NTG417BRba9BkcEQACB/wWOupbY75rJPUAABBBAAAEEEBgX6K6BuJ3vgfhF94AACCBwAThbn6FVGGGJAAAAAElFTkSuQmCC",
+ "hash": "243969710ac251e9d5080cd55dedc517"
+ }
+}
\ No newline at end of file
diff --git a/test/renders/solutions/sprite-new.json b/test/renders/solutions/sprite-new.json
new file mode 100644
index 0000000..c5396a6
--- /dev/null
+++ b/test/renders/solutions/sprite-new.json
@@ -0,0 +1,10 @@
+{
+ "webgl": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAASUlEQVRYR+3XsQ0AMAwCwTBtZvK0pI6S1sLFewALnWiQba/giQDjBLTV2gjXXbmnAwRAAAEEEEAAAQQQQACBuEDrKPg8ZxvGBQ6XssehFyMftwAAAABJRU5ErkJggg==",
+ "hash": "bd5ca4fe3ec53c421044c78d86592b86"
+ },
+ "canvas": {
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAASUlEQVRYR+3XsQ0AMAwCwTBtZvK0pI6S1sLFewALnWiQba/giQDjBLTV2gjXXbmnAwRAAAEEEEAAAQQQQACBuEDrKPg8ZxvGBQ6XssehFyMftwAAAABJRU5ErkJggg==",
+ "hash": "bd5ca4fe3ec53c421044c78d86592b86"
+ }
+}
\ No newline at end of file
diff --git a/test/renders/tests/assets/bitmap-1.png b/test/renders/tests/assets/bitmap-1.png
new file mode 100644
index 0000000..1284fb8
--- /dev/null
+++ b/test/renders/tests/assets/bitmap-1.png
Binary files differ
diff --git a/test/renders/tests/graphics-rect.js b/test/renders/tests/graphics-rect.js
new file mode 100644
index 0000000..1143c96
--- /dev/null
+++ b/test/renders/tests/graphics-rect.js
@@ -0,0 +1,22 @@
+'use strict';
+
+module.exports = function ()
+{
+ const graphic = new PIXI.Graphics()
+ .beginFill(0xFFCC00, 1)
+ .drawRect(8, 8, 16, 16);
+
+ expect(graphic.width).to.equal(16);
+ expect(graphic.height).to.equal(16);
+ expect(graphic.x).to.equal(0);
+ expect(graphic.y).to.equal(0);
+
+ const bounds = graphic.getBounds();
+
+ expect(bounds.x).to.equal(8);
+ expect(bounds.y).to.equal(8);
+ expect(bounds.width).to.equal(16);
+ expect(bounds.height).to.equal(16);
+
+ this.stage.addChild(graphic);
+};
diff --git a/test/renders/tests/sprite-new.js b/test/renders/tests/sprite-new.js
new file mode 100644
index 0000000..b25ae83
--- /dev/null
+++ b/test/renders/tests/sprite-new.js
@@ -0,0 +1,30 @@
+'use strict';
+
+module.exports.async = function (done)
+{
+ const url = `file://${__dirname}/assets/bitmap-1.png`;
+ const loader = new PIXI.loaders.Loader();
+
+ loader.add('bitmap', url);
+ loader.once('complete', (loader, resources) =>
+ {
+ expect(resources.bitmap).to.be.ok;
+ expect(resources.bitmap.texture).to.be.ok;
+ expect(resources.bitmap.url).to.equal(url);
+
+ const sprite = new PIXI.Sprite(resources.bitmap.texture);
+
+ expect(sprite.width).to.equal(24);
+ expect(sprite.height).to.equal(24);
+
+ sprite.x = (32 - sprite.width) / 2;
+ sprite.y = (32 - sprite.height) / 2;
+
+ expect(sprite.x).to.equal(4);
+ expect(sprite.y).to.equal(4);
+
+ this.stage.addChild(sprite);
+ done();
+ });
+ loader.load();
+};