diff --git a/tools/sundae/ref-test-runner.html b/tools/sundae/ref-test-runner.html
new file mode 100755
index 0000000..01acf38
--- /dev/null
+++ b/tools/sundae/ref-test-runner.html
@@ -0,0 +1,42 @@
+
+
+
+
+ Sundae
+
+
+
+ Sundae Ref Test Runner
+ Settings
+ Run Tests:
+
+ Blur Radius:
+ Epsilon (0-1.0):
+
+
+
+
\ No newline at end of file
diff --git a/tools/sundae/resources/acorn_unlit.js b/tools/sundae/resources/acorn_unlit.js
new file mode 100644
index 0000000..25fd2a1
--- /dev/null
+++ b/tools/sundae/resources/acorn_unlit.js
@@ -0,0 +1,16 @@
+function start(cvs){
+ var acorn;
+ var ps = new PointStream();
+ ps.setup(cvs);
+ ps.pointSize(5);
+ ps.onRender = function(){
+ ps.background([1, 1, 1, 1]);
+ ps.clear();
+ ps.translate(0, 0, -25);
+ ps.render(acorn);
+ if(acorn.status === 3){
+ ps.onRender = function(){};
+ }
+ };
+ acorn = ps.load('../../clouds/acorn.asc');
+}
\ No newline at end of file
diff --git a/tools/sundae/resources/acorn_unlit.png b/tools/sundae/resources/acorn_unlit.png
new file mode 100644
index 0000000..2f004be
Binary files /dev/null and b/tools/sundae/resources/acorn_unlit.png differ
diff --git a/tools/sundae/resources/only_verts.js b/tools/sundae/resources/only_verts.js
new file mode 100644
index 0000000..39d35e7
--- /dev/null
+++ b/tools/sundae/resources/only_verts.js
@@ -0,0 +1,16 @@
+function start(cvs){
+ var pointCloud;
+ var ps = new PointStream();
+ ps.setup(cvs);
+ ps.pointSize(5);
+ ps.onRender = function(){
+ ps.background([1,1,1,1]);
+ ps.clear();
+ ps.translate(0, 0, -25);
+ ps.render(pointCloud);
+ if(pointCloud.status === 3){
+ ps.onRender = function(){};
+ }
+ };
+ pointCloud = ps.load('../../clouds/acorn_only_verts.asc');
+}
\ No newline at end of file
diff --git a/tools/sundae/resources/only_verts.png b/tools/sundae/resources/only_verts.png
new file mode 100644
index 0000000..0c9becf
Binary files /dev/null and b/tools/sundae/resources/only_verts.png differ
diff --git a/tools/sundae/resources/tests.js b/tools/sundae/resources/tests.js
new file mode 100755
index 0000000..133da82
--- /dev/null
+++ b/tools/sundae/resources/tests.js
@@ -0,0 +1,42 @@
+{
+ "testSuite": [{
+ "test":
+ [
+ {
+ "name": "user shader",
+ "dependancyURL": ["../../mjs.js",
+ "../../psapi.js",
+ "../../parsers/asc.js",
+ "resources/user_shader/user_shader.js"],
+ "referenceImageURL": "resources/user_shader/mickey_lit.png",
+ "run": {"src": "resources/user_shader/test.js", "func": "start" }
+ },
+
+ {
+ "name": "only verts",
+ "dependancyURL": ["../../mjs.js",
+ "../../psapi.js",
+ "../../parsers/asc.js"],
+ "referenceImageURL": "resources/only_verts.png",
+ "run": {"src": "resources/only_verts.js", "func": "start" }
+ },
+ {
+ "name": "1",
+ "dependancyURL": ["../../mjs.js",
+ "../../psapi.js",
+ "../../parsers/asc.js"],
+ "referenceImageURL": "resources/acorn_unlit.png",
+ "run": {"src": "resources/acorn_unlit.js", "func": "start" }
+ },
+ {
+ "name": "2",
+ "dependancyURL": ["../../mjs.js",
+ "../../psapi.js",
+ "../../parsers/asc.js"],
+ "referenceImageURL": "resources/acorn_unlit.png",
+ "run": {"src": "resources/acorn_unlit.js", "func": "start" }
+ }
+ ]
+ }]
+}
+
\ No newline at end of file
diff --git a/tools/sundae/resources/user_shader/mickey_lit.png b/tools/sundae/resources/user_shader/mickey_lit.png
new file mode 100644
index 0000000..7a7332c
Binary files /dev/null and b/tools/sundae/resources/user_shader/mickey_lit.png differ
diff --git a/tools/sundae/resources/user_shader/test.js b/tools/sundae/resources/user_shader/test.js
new file mode 100644
index 0000000..f4cdcd6
--- /dev/null
+++ b/tools/sundae/resources/user_shader/test.js
@@ -0,0 +1,20 @@
+function start(cvs){
+ var ps, pointCloud;
+ ps = new PointStream();
+ ps.setup(cvs);
+ ps.onRender = function(){
+ ps.clear();
+ ps.uniformi("reflection", false);
+ ps.uniformf("lightPos", [0, 50, 10]);
+ ps.uniformf("uReflection", [1, 1, 1, 1]);
+ ps.translate(0, 10, -80);
+ ps.render(pointCloud);
+ if(pointCloud.status === 3){
+ ps.onRender = function(){};
+ }
+ };
+ var progObj = ps.createProgram(vertShader, fragShader);
+ ps.useProgram(progObj);
+ ps.pointSize(10);
+ pointCloud = ps.load("../../clouds/mickey.asc");
+}
diff --git a/tools/sundae/resources/user_shader/user_shader.js b/tools/sundae/resources/user_shader/user_shader.js
new file mode 100644
index 0000000..4203bdc
--- /dev/null
+++ b/tools/sundae/resources/user_shader/user_shader.js
@@ -0,0 +1,62 @@
+var vertShader =
+"varying vec4 frontColor;" +
+
+"attribute vec3 ps_Vertex;" +
+"attribute vec3 ps_Normal;" +
+"attribute vec4 ps_Color;" +
+
+"uniform float ps_PointSize;" +
+"uniform vec3 ps_Attenuation;" +
+
+"uniform vec3 lightPos;" +
+"uniform bool reflection;"+
+
+"uniform mat4 ps_ModelViewMatrix;" +
+"uniform mat4 ps_ProjectionMatrix;" +
+"uniform mat4 ps_NormalMatrix;" +
+
+"void PointLight(inout vec3 col, in vec3 ecPos, in vec3 vertNormal) {" +
+" vec3 VP = lightPos - ecPos;" +
+" VP = normalize( VP );" +
+" float nDotVP = max( 0.0, dot( vertNormal, VP ));" +
+" col += vec3(1.0, 1.0, 1.0) * nDotVP;" +
+"}" +
+
+"void main(void) {" +
+" vec3 transNorm = vec3(ps_NormalMatrix * vec4(ps_Normal, 0.0));" +
+
+" vec4 ecPos4 = ps_ModelViewMatrix * vec4(ps_Vertex, 1.0);" +
+" vec3 ecPos = (vec3(ecPos4))/ecPos4.w;" +
+
+" vec3 col = vec3(0.0, 0.0, 0.0);" +
+" PointLight(col, ecPos, transNorm);" +
+
+" frontColor = ps_Color * vec4(col, 1.0);" +
+
+" if(reflection){" +
+" float l = length(ps_Vertex - vec3(0.0, -20.0, 0.0))/20.0;" +
+ // magic number that super saturates to white
+" float col = l * 5.0;" +
+" frontColor += vec4(col, col, col, 1.0);" +
+" }" +
+
+" float dist = length( ecPos4 );" +
+" float attn = ps_Attenuation[0] + " +
+" (ps_Attenuation[1] * dist) + " +
+" (ps_Attenuation[2] * dist * dist);" +
+
+" gl_PointSize = ps_PointSize * sqrt(1.0/attn);" +
+" gl_Position = ps_ProjectionMatrix * ecPos4;" +
+"}";
+
+var fragShader =
+"#ifdef GL_ES\n" +
+" precision highp float;\n" +
+"#endif\n" +
+
+"uniform vec4 uReflection;" +
+
+"varying vec4 frontColor;" +
+"void main(void){" +
+" gl_FragColor = frontColor * uReflection;" +
+"}";
diff --git a/tools/sundae/sundae.js b/tools/sundae/sundae.js
new file mode 100755
index 0000000..ab9ec9f
--- /dev/null
+++ b/tools/sundae/sundae.js
@@ -0,0 +1,314 @@
+/*!
+ * Sundae Javascript Library v0.4
+ * http://sundae.lighthouseapp.com/dashboard
+ *
+ * The MIT License
+ * Copyright (c) 2011 Carlin Desautels
+ * http://www.opensource.org/licenses/mit-license.php
+*/
+var sundae = {};
+(function (window, undef) {
+ //Enviroment variables
+ var _kernel, _kernelSize, _kernelSum;
+ var _tag = "a";
+ var _sigma = 2;
+ var _epsilon = 0.05;
+ var _w = window;
+ var _testSuite = [];
+
+ sundae.setBlurRadius = function(s){
+ if(s)
+ _sigma = s;
+ };
+ sundae.setTolerance = function(e){
+ if(e)
+ _epsilon = e;
+ };
+ sundae.setTestTag = function(t){
+ if(t)
+ _tag = t;
+ };
+ sundae.init = function(){
+ setupKernel();
+ setupBody();
+ getTests();
+ };
+ function reportResult(r,t){
+ r.innerHTML = (" [" + t + "ms]");
+ }
+ function setupTest(test){
+ var name = test.name || "default";
+ var d = createDiv(_w.document.getElementById("sundae"), name);
+ var r = createDiv(d, name + "-title");
+ var a = createCanvas(d, name + "-orig", 100, 100);
+ var b = createCanvas(d, name + "-curr", 100, 100);
+ var c = createCanvas(d, name + "-diff", 100, 100);
+ var t = 0;
+ function runTest(func, aCanvas){
+ var startTime = (new Date).getTime();
+ func(aCanvas);
+ t = (new Date).getTime() - startTime;
+ }
+ var isDone = {"orig" : false, "curr" : false};
+ var whenDone = function(who, func, aCanvas){
+ isDone[who] = true;
+ if(who == "curr" && func && aCanvas){
+ runTest(func, aCanvas);
+ reportResult(r, t, e);
+ }
+ if(isDone.curr === true && isDone.orig === true){
+ //if aPix == null error
+ //about:config
+ //security.fileuri.strict_origin_policy == false
+ compare(a, b, c);
+ }
+ };
+ injectOrig(a, test.referenceImageURL, whenDone);
+ injectCurr(b, test.run, whenDone);
+ }
+ function injectOrig(aCanvas, url, callback){
+ var ctx = aCanvas.getContext("2d");
+ var img = new Image();
+ img.onload = function(){
+ ctx.drawImage(img, 0, 0, img.width, img.height);
+ callback("orig");
+ }
+ img.src = url;
+ }
+ function injectCurr(aCanvas, run, callback){
+ var testObj = eval(run);
+ if(typeof(testObj) === "function"){
+ callback("curr", testObj, aCanvas);
+ }
+ else if(typeof(testObj) === "object"){
+ getScript(testObj.src,
+ function(){
+ callback("curr", _w[testObj.func], aCanvas);
+ }
+ );
+ }
+ else if(typeof(testObj) === "string"){
+ callback("curr", _w[testObj], aCanvas);
+ }
+ }
+ function createDiv(parent, id){
+ var d = _w.document.createElement("div");
+ d.id = id;
+ parent.appendChild(d);
+ return d;
+ }
+ function createCanvas(parent, id, h, w){
+ var c = _w.document.createElement("canvas");
+ c.id = id;
+ c.width = w;
+ c.height = h;
+ parent.appendChild(c);
+ return c;
+ }
+ function setupBody(){
+ createDiv(_w.document.body, "sundae");
+ }
+ function getTests(){
+ var setupTests = function(data){
+ var loadDeps = function(deps, test){
+ if(typeof(deps) === 'object'){
+ if(deps.length > 0){
+ getScript(deps.pop(),
+ function(){
+ loadDeps(deps, test);
+ }
+ );
+ }
+ else{
+ setupTest(test);
+ }
+ }
+ else if(typeof(deps) === 'string'){
+ getScript(deps,
+ function(){
+ setupTest(test);
+ }
+ );
+ }
+ };
+ _testSuite = data.testSuite || undef;
+ if(_testSuite){
+ for(var i = 0, sl = _testSuite.length; i < sl; i++){
+ if(_testSuite[i].test){
+ for(var j = 0, tl = _testSuite[i].test.length; j < tl; j++){
+ if(_testSuite[i].test[j].dependancyURL){
+ loadDeps(_testSuite[i].test[j].dependancyURL, _testSuite[i].test[j]);
+ }
+ else{
+ setupTest(_testSuite[i].test[j]);
+ }
+ }
+ }
+ }
+ }
+ };
+ getJSON("resources/tests.js", setupTests);
+ }
+ function getJSON(src,callback){
+ get(src,callback,true);
+ }
+ function getScript(src,callback){
+ get(src,callback);
+ }
+ function get(src, success, isJSON){
+ if(isJSON){
+ var r = new XMLHttpRequest();
+ r.open("GET", src, true);
+ r.overrideMimeType("application/json");
+ r.onload = function(){
+ try{
+ success(JSON.parse(r.responseText));
+ }
+ catch(e){
+ //Not valid JSON
+ success(eval("(" + r.responseText + ")"));
+ }
+ };
+ r.send(null);
+ }
+ else{
+ var s = _w.document.createElement('script');
+ s.type = 'text/javascript';
+ s.onload = function(){
+ success();
+ _w.document.head.removeChild(s);
+ };
+ s.src = src;
+ _w.document.head.appendChild(s);
+ }
+ }
+ //Global Utility Functions
+ function getPixels(aCanvas, isWebGL) {
+ try {
+ if (isWebGL) {
+ var context = aCanvas.getContext("experimental-webgl");
+ var data = null;
+ try{
+ // try deprecated way first
+ data = context.readPixels(0, 0, aCanvas.width, aCanvas.height, context.RGBA, context.UNSIGNED_BYTE);
+ // Chrome posts an error
+ if(context.getError()){
+ throw new Error("API has changed");
+ }
+ }
+ catch(e){
+ // if that failed, try new way
+ if(!data){
+ data = new Uint8Array(aCanvas.width * aCanvas.height * 4);
+ context.readPixels(0, 0, aCanvas.width, aCanvas.height, context.RGBA, context.UNSIGNED_BYTE, data);
+ }
+ }
+ return data;
+ }
+ else {
+ return aCanvas.getContext('2d').getImageData(0, 0, aCanvas.width, aCanvas.height).data;
+ }
+ }
+ catch (e) {
+ return null;
+ }
+ }
+ function compare(a, b, c){
+ var failed = false;
+ var valueEpsilon = _epsilon * 255;
+ //Get pixel arrays from canvases
+ var aPix = getPixels(a, false);
+ var bPix = getPixels(b, true);
+ //Blur pixel arrays
+ //aPix = blur(aPix, aPix.width, aPix.height);
+ //bPix = blur(bPix, bPix.width, bPix.height);
+ if(aPix.length === bPix.length){
+ //Compare pixel arrays
+ var cCtx = c.getContext('2d');
+ var cPix = cCtx.createImageData(c.width, c.height);
+ var len = bPix.length;
+ for (var j=0; j < len; j+=4){
+ if (Math.abs(bPix[j] - aPix[j]) < valueEpsilon &&
+ Math.abs(bPix[j + 1] - aPix[j + 1]) < valueEpsilon &&
+ Math.abs(bPix[j + 2] - aPix[j + 2]) < valueEpsilon &&
+ Math.abs(bPix[j + 3] - aPix[j + 3]) < valueEpsilon){
+ cPix.data[j] = cPix.data[j+1] = cPix.data[j+2] = cPix.data[j+3] = 0;
+ } //Pixel difference in c
+ else{
+ cPix.data[j] = 255;
+ cPix.data[j+1] = cPix.data[j+2] = 0;
+ cPix.data[j+3] = 255;
+ failed = true;
+ }
+ }
+ //Display pixel difference in _c
+ if(failed){
+ cCtx.putImageData(cPix, 0, 0);
+ }
+ else{
+ cCtx.fillStyle = "rgb(0,255,0)";
+ cCtx.fillRect (0, 0, c.width, c.height);
+ }
+ }
+ else{
+ failed = true;
+ }
+ }
+ function setupKernel() {
+ var ss = _sigma * _sigma;
+ var factor = 2 * Math.PI * ss;
+ _kernel = new Array();
+ _kernel.push(new Array());
+ var i = 0, j;
+ do {
+ var g = Math.exp(-(i * i) / (2 * ss)) / factor;
+ if (g < 1e-3) break;
+ _kernel[0].push(g);
+ ++i;
+ } while (i < 7);
+ _kernelSize = i;
+ for (j = 1; j < _kernelSize; ++j) {
+ _kernel.push(new Array());
+ for (i = 0; i < _kernelSize; ++i) {
+ var g = Math.exp(-(i * i + j * j) / (2 * ss)) / factor;
+ _kernel[j].push(g);
+ }
+ }
+ _kernelSum = 0;
+ for (j = 1 - _kernelSize; j < _kernelSize; ++j) {
+ for (i = 1 - _kernelSize; i < _kernelSize; ++i) {
+ _kernelSum += _kernel[Math.abs(j)][Math.abs(i)];
+ }
+ }
+ }
+ function blur(data, width, height) {
+ var len = data.length;
+ var newData = new Array(len);
+ for (var y = 0; y < height; ++y) {
+ for (var x = 0; x < width; ++x) {
+ var r = 0, g = 0, b = 0, a = 0;
+ for (j = 1 - _kernelSize; j < _kernelSize; ++j) {
+ if (y + j < 0 || y + j >= height) continue;
+ for (i = 1 - _kernelSize; i < _kernelSize; ++i) {
+ if (x + i < 0 || x + i >= width) continue;
+ r += data[4 * ((y + j) * width + (x + i)) + 0] * _kernel[Math.abs(j)][Math.abs(i)];
+ g += data[4 * ((y + j) * width + (x + i)) + 1] * _kernel[Math.abs(j)][Math.abs(i)];
+ b += data[4 * ((y + j) * width + (x + i)) + 2] * _kernel[Math.abs(j)][Math.abs(i)];
+ a += data[4 * ((y + j) * width + (x + i)) + 3] * _kernel[Math.abs(j)][Math.abs(i)];
+ }
+ }
+ newData[4 * (y * width + x) + 0] = r / _kernelSum;
+ newData[4 * (y * width + x) + 1] = g / _kernelSum;
+ newData[4 * (y * width + x) + 2] = b / _kernelSum;
+ newData[4 * (y * width + x) + 3] = a / _kernelSum;
+ }
+ }
+ return newData;
+ }
+ // Opera createImageData fix
+ try {
+ if (!("createImageData" in CanvasRenderingContext2D.prototype)) {
+ CanvasRenderingContext2D.prototype.createImageData = function(sw,sh) { return this.getImageData(0,0,sw,sh); }
+ }
+ } catch(e) {}
+})(window);