SOURCE

console 命令行工具 X clear

                    
>
console
var Delaunay;

(function() {
    "use strict";

    var EPSILON = 1.0 / 1048576.0;

    function supertriangle(vertices) {
        var xmin = Number.POSITIVE_INFINITY,
            ymin = Number.POSITIVE_INFINITY,
            xmax = Number.NEGATIVE_INFINITY,
            ymax = Number.NEGATIVE_INFINITY,
            i, dx, dy, dmax, xmid, ymid;

        for(i = vertices.length; i--; ) {
            if(vertices[i][0] < xmin) xmin = vertices[i][0];
            if(vertices[i][0] > xmax) xmax = vertices[i][0];
            if(vertices[i][1] < ymin) ymin = vertices[i][1];
            if(vertices[i][1] > ymax) ymax = vertices[i][1];
        }

        dx = xmax - xmin;
        dy = ymax - ymin;
        dmax = Math.max(dx, dy);
        xmid = xmin + dx * 0.5;
        ymid = ymin + dy * 0.5;

        return [
            [xmid - 20 * dmax, ymid -      dmax],
            [xmid            , ymid + 20 * dmax],
            [xmid + 20 * dmax, ymid -      dmax]
        ];
    }

    function circumcircle(vertices, i, j, k) {
        var x1 = vertices[i][0],
            y1 = vertices[i][1],
            x2 = vertices[j][0],
            y2 = vertices[j][1],
            x3 = vertices[k][0],
            y3 = vertices[k][1],
            fabsy1y2 = Math.abs(y1 - y2),
            fabsy2y3 = Math.abs(y2 - y3),
            xc, yc, m1, m2, mx1, mx2, my1, my2, dx, dy;

        /* Check for coincident points */
        if(fabsy1y2 < EPSILON && fabsy2y3 < EPSILON)
            throw new Error("Eek! Coincident points!");

        if(fabsy1y2 < EPSILON) {
            m2  = -((x3 - x2) / (y3 - y2));
            mx2 = (x2 + x3) / 2.0;
            my2 = (y2 + y3) / 2.0;
            xc  = (x2 + x1) / 2.0;
            yc  = m2 * (xc - mx2) + my2;
        }

        else if(fabsy2y3 < EPSILON) {
            m1  = -((x2 - x1) / (y2 - y1));
            mx1 = (x1 + x2) / 2.0;
            my1 = (y1 + y2) / 2.0;
            xc  = (x3 + x2) / 2.0;
            yc  = m1 * (xc - mx1) + my1;
        }

        else {
            m1  = -((x2 - x1) / (y2 - y1));
            m2  = -((x3 - x2) / (y3 - y2));
            mx1 = (x1 + x2) / 2.0;
            mx2 = (x2 + x3) / 2.0;
            my1 = (y1 + y2) / 2.0;
            my2 = (y2 + y3) / 2.0;
            xc  = (m1 * mx1 - m2 * mx2 + my2 - my1) / (m1 - m2);
            yc  = (fabsy1y2 > fabsy2y3) ?
                m1 * (xc - mx1) + my1 :
                m2 * (xc - mx2) + my2;
        }

        dx = x2 - xc;
        dy = y2 - yc;
        return {i: i, j: j, k: k, x: xc, y: yc, r: dx * dx + dy * dy};
    }

    function dedup(edges) {
        var i, j, a, b, m, n;

        for(j = edges.length; j; ) {
            b = edges[--j];
            a = edges[--j];

            for(i = j; i; ) {
                n = edges[--i];
                m = edges[--i];

                if((a === m && b === n) || (a === n && b === m)) {
                    edges.splice(j, 2);
                    edges.splice(i, 2);
                    break;
                }
            }
        }
    }

    Delaunay = {
        triangulate: function(vertices, key) {
            var n = vertices.length,
                i, j, indices, st, open, closed, edges, dx, dy, a, b, c;

            /* Bail if there aren't enough vertices to form any triangles. */
            if(n < 3)
                return [];

            /* Slice out the actual vertices from the passed objects. (Duplicate the
             * array even if we don't, though, since we need to make a supertriangle
             * later on!) */
            vertices = vertices.slice(0);

            if(key)
                for(i = n; i--; )
                    vertices[i] = vertices[i][key];

            /* Make an array of indices into the vertex array, sorted by the
             * vertices' x-position. */
            indices = new Array(n);

            for(i = n; i--; )
                indices[i] = i;

            indices.sort(function(i, j) {
                return vertices[j][0] - vertices[i][0];
            });

            /* Next, find the vertices of the supertriangle (which contains all other
             * triangles), and append them onto the end of a (copy of) the vertex
             * array. */
            st = supertriangle(vertices);
            vertices.push(st[0], st[1], st[2]);

            /* Initialize the open list (containing the supertriangle and nothing
             * else) and the closed list (which is empty since we havn't processed
             * any triangles yet). */
            open   = [circumcircle(vertices, n + 0, n + 1, n + 2)];
            closed = [];
            edges  = [];

            /* Incrementally add each vertex to the mesh. */
            for(i = indices.length; i--; edges.length = 0) {
                c = indices[i];

                /* For each open triangle, check to see if the current point is
                 * inside it's circumcircle. If it is, remove the triangle and add
                 * it's edges to an edge list. */
                for(j = open.length; j--; ) {
                    /* If this point is to the right of this triangle's circumcircle,
                     * then this triangle should never get checked again. Remove it
                     * from the open list, add it to the closed list, and skip. */
                    dx = vertices[c][0] - open[j].x;
                    if(dx > 0.0 && dx * dx > open[j].r) {
                        closed.push(open[j]);
                        open.splice(j, 1);
                        continue;
                    }

                    /* If we're outside the circumcircle, skip this triangle. */
                    dy = vertices[c][1] - open[j].y;
                    if(dx * dx + dy * dy - open[j].r > EPSILON)
                        continue;

                    /* Remove the triangle and add it's edges to the edge list. */
                    edges.push(
                        open[j].i, open[j].j,
                        open[j].j, open[j].k,
                        open[j].k, open[j].i
                    );
                    open.splice(j, 1);
                }

                /* Remove any doubled edges. */
                dedup(edges);

                /* Add a new triangle for each edge. */
                for(j = edges.length; j; ) {
                    b = edges[--j];
                    a = edges[--j];
                    open.push(circumcircle(vertices, a, b, c));
                }
            }

            /* Copy any remaining open triangles to the closed list, and then
             * remove any triangles that share a vertex with the supertriangle,
             * building a list of triplets that represent triangles. */
            for(i = open.length; i--; )
                closed.push(open[i]);
            open.length = 0;

            for(i = closed.length; i--; )
                if(closed[i].i < n && closed[i].j < n && closed[i].k < n)
                    open.push(closed[i].i, closed[i].j, closed[i].k);

            /* Yay, we're done! */
            return open;
        },
        contains: function(tri, p) {
            /* Bounding box test first, for quick rejections. */
            if((p[0] < tri[0][0] && p[0] < tri[1][0] && p[0] < tri[2][0]) ||
                (p[0] > tri[0][0] && p[0] > tri[1][0] && p[0] > tri[2][0]) ||
                (p[1] < tri[0][1] && p[1] < tri[1][1] && p[1] < tri[2][1]) ||
                (p[1] > tri[0][1] && p[1] > tri[1][1] && p[1] > tri[2][1]))
                return null;

            var a = tri[1][0] - tri[0][0],
                b = tri[2][0] - tri[0][0],
                c = tri[1][1] - tri[0][1],
                d = tri[2][1] - tri[0][1],
                i = a * d - b * c;

            /* Degenerate tri. */
            if(i === 0.0)
                return null;

            var u = (d * (p[0] - tri[0][0]) - b * (p[1] - tri[0][1])) / i,
                v = (a * (p[1] - tri[0][1]) - c * (p[0] - tri[0][0])) / i;

            /* If we're outside the tri, fail. */
            if(u < 0.0 || v < 0.0 || (u + v) > 1.0)
                return null;

            return [u, v];
        }
    };

    if(typeof module !== "undefined")
        module.exports = Delaunay;
})();

/**
 * StyleFix 1.0.3 & PrefixFree 1.0.7
 * @author Lea Verou
 * MIT license
 */(function(){function t(e,t){return[].slice.call((t||document).querySelectorAll(e))}if(!window.addEventListener)return;var e=window.StyleFix={link:function(t){try{if(t.rel!=="stylesheet"||t.hasAttribute("data-noprefix"))return}catch(n){return}var r=t.href||t.getAttribute("data-href"),i=r.replace(/[^\/]+$/,""),s=(/^[a-z]{3,10}:/.exec(i)||[""])[0],o=(/^[a-z]{3,10}:\/\/[^\/]+/.exec(i)||[""])[0],u=/^([^?]*)\??/.exec(r)[1],a=t.parentNode,f=new XMLHttpRequest,l;f.onreadystatechange=function(){f.readyState===4&&l()};l=function(){var n=f.responseText;if(n&&t.parentNode&&(!f.status||f.status<400||f.status>600)){n=e.fix(n,!0,t);if(i){n=n.replace(/url\(\s*?((?:"|')?)(.+?)\1\s*?\)/gi,function(e,t,n){return/^([a-z]{3,10}:|#)/i.test(n)?e:/^\/\//.test(n)?'url("'+s+n+'")':/^\//.test(n)?'url("'+o+n+'")':/^\?/.test(n)?'url("'+u+n+'")':'url("'+i+n+'")'});var r=i.replace(/([\\\^\$*+[\]?{}.=!:(|)])/g,"\\$1");n=n.replace(RegExp("\\b(behavior:\\s*?url\\('?\"?)"+r,"gi"),"$1")}var l=document.createElement("style");l.textContent=n;l.media=t.media;l.disabled=t.disabled;l.setAttribute("data-href",t.getAttribute("href"));a.insertBefore(l,t);a.removeChild(t);l.media=t.media}};try{f.open("GET",r);f.send(null)}catch(n){if(typeof XDomainRequest!="undefined"){f=new XDomainRequest;f.onerror=f.onprogress=function(){};f.onload=l;f.open("GET",r);f.send(null)}}t.setAttribute("data-inprogress","")},styleElement:function(t){if(t.hasAttribute("data-noprefix"))return;var n=t.disabled;t.textContent=e.fix(t.textContent,!0,t);t.disabled=n},styleAttribute:function(t){var n=t.getAttribute("style");n=e.fix(n,!1,t);t.setAttribute("style",n)},process:function(){t('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link);t("style").forEach(StyleFix.styleElement);t("[style]").forEach(StyleFix.styleAttribute)},register:function(t,n){(e.fixers=e.fixers||[]).splice(n===undefined?e.fixers.length:n,0,t)},fix:function(t,n,r){for(var i=0;i<e.fixers.length;i++)t=e.fixers[i](t,n,r)||t;return t},camelCase:function(e){return e.replace(/-([a-z])/g,function(e,t){return t.toUpperCase()}).replace("-","")},deCamelCase:function(e){return e.replace(/[A-Z]/g,function(e){return"-"+e.toLowerCase()})}};(function(){setTimeout(function(){t('link[rel="stylesheet"]').forEach(StyleFix.link)},10);document.addEventListener("DOMContentLoaded",StyleFix.process,!1)})()})();(function(e){function t(e,t,r,i,s){e=n[e];if(e.length){var o=RegExp(t+"("+e.join("|")+")"+r,"gi");s=s.replace(o,i)}return s}if(!window.StyleFix||!window.getComputedStyle)return;var n=window.PrefixFree={prefixCSS:function(e,r,i){var s=n.prefix;n.functions.indexOf("linear-gradient")>-1&&(e=e.replace(/(\s|:|,)(repeating-)?linear-gradient\(\s*(-?\d*\.?\d*)deg/ig,function(e,t,n,r){return t+(n||"")+"linear-gradient("+(90-r)+"deg"}));e=t("functions","(\\s|:|,)","\\s*\\(","$1"+s+"$2(",e);e=t("keywords","(\\s|:)","(\\s|;|\\}|$)","$1"+s+"$2$3",e);e=t("properties","(^|\\{|\\s|;)","\\s*:","$1"+s+"$2:",e);if(n.properties.length){var o=RegExp("\\b("+n.properties.join("|")+")(?!:)","gi");e=t("valueProperties","\\b",":(.+?);",function(e){return e.replace(o,s+"$1")},e)}if(r){e=t("selectors","","\\b",n.prefixSelector,e);e=t("atrules","@","\\b","@"+s+"$1",e)}e=e.replace(RegExp("-"+s,"g"),"-");e=e.replace(/-\*-(?=[a-z]+)/gi,n.prefix);return e},property:function(e){return(n.properties.indexOf(e)?n.prefix:"")+e},value:function(e,r){e=t("functions","(^|\\s|,)","\\s*\\(","$1"+n.prefix+"$2(",e);e=t("keywords","(^|\\s)","(\\s|$)","$1"+n.prefix+"$2$3",e);return e},prefixSelector:function(e){return e.replace(/^:{1,2}/,function(e){return e+n.prefix})},prefixProperty:function(e,t){var r=n.prefix+e;return t?StyleFix.camelCase(r):r}};(function(){var e={},t=[],r={},i=getComputedStyle(document.documentElement,null),s=document.createElement("div").style,o=function(n){if(n.charAt(0)==="-"){t.push(n);var r=n.split("-"),i=r[1];e[i]=++e[i]||1;while(r.length>3){r.pop();var s=r.join("-");u(s)&&t.indexOf(s)===-1&&t.push(s)}}},u=function(e){return StyleFix.camelCase(e)in s};if(i.length>0)for(var a=0;a<i.length;a++)o(i[a]);else for(var f in i)o(StyleFix.deCamelCase(f));var l={uses:0};for(var c in e){var h=e[c];l.uses<h&&(l={prefix:c,uses:h})}n.prefix="-"+l.prefix+"-";n.Prefix=StyleFix.camelCase(n.prefix);n.properties=[];for(var a=0;a<t.length;a++){var f=t[a];if(f.indexOf(n.prefix)===0){var p=f.slice(n.prefix.length);u(p)||n.properties.push(p)}}n.Prefix=="Ms"&&!("transform"in s)&&!("MsTransform"in s)&&"msTransform"in s&&n.properties.push("transform","transform-origin");n.properties.sort()})();(function(){function i(e,t){r[t]="";r[t]=e;return!!r[t]}var e={"linear-gradient":{property:"backgroundImage",params:"red, teal"},calc:{property:"width",params:"1px + 5%"},element:{property:"backgroundImage",params:"#foo"},"cross-fade":{property:"backgroundImage",params:"url(a.png), url(b.png), 50%"}};e["repeating-linear-gradient"]=e["repeating-radial-gradient"]=e["radial-gradient"]=e["linear-gradient"];var t={initial:"color","zoom-in":"cursor","zoom-out":"cursor",box:"display",flexbox:"display","inline-flexbox":"display",flex:"display","inline-flex":"display",grid:"display","inline-grid":"display","min-content":"width"};n.functions=[];n.keywords=[];var r=document.createElement("div").style;for(var s in e){var o=e[s],u=o.property,a=s+"("+o.params+")";!i(a,u)&&i(n.prefix+a,u)&&n.functions.push(s)}for(var f in t){var u=t[f];!i(f,u)&&i(n.prefix+f,u)&&n.keywords.push(f)}})();(function(){function s(e){i.textContent=e+"{}";return!!i.sheet.cssRules.length}var t={":read-only":null,":read-write":null,":any-link":null,"::selection":null},r={keyframes:"name",viewport:null,document:'regexp(".")'};n.selectors=[];n.atrules=[];var i=e.appendChild(document.createElement("style"));for(var o in t){var u=o+(t[o]?"("+t[o]+")":"");!s(u)&&s(n.prefixSelector(u))&&n.selectors.push(o)}for(var a in r){var u=a+" "+(r[a]||"");!s("@"+u)&&s("@"+n.prefix+u)&&n.atrules.push(a)}e.removeChild(i)})();n.valueProperties=["transition","transition-property"];e.className+=" "+n.prefix;StyleFix.register(n.prefixCSS)})(document.documentElement);
/**
 * Defines the Flat Surface Shader namespace for all the awesomeness to exist upon.
 * @author Matthew Wagerfield
 */
FSS = {
    FRONT  : 0,
    BACK   : 1,
    DOUBLE : 2,
    SVGNS  : 'http://www.w3.org/2000/svg'
};

/**
 * @class Array
 * @author Matthew Wagerfield
 */
FSS.Array = typeof Float32Array === 'function' ? Float32Array : Array;

/**
 * @class Utils
 * @author Matthew Wagerfield
 */
FSS.Utils = {
    isNumber: function(value) {
        return !isNaN(parseFloat(value)) && isFinite(value);
    }
};

/**
 * Request Animation Frame Polyfill.
 * @author Paul Irish
 * @see https://gist.github.com/paulirish/1579671
 */
(function() {

    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];

    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame  = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
    }

    if (!window.requestAnimationFrame) {
        window.requestAnimationFrame = function(callback, element) {
            var currentTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currentTime - lastTime));
            var id = window.setTimeout(function() {
                callback(currentTime + timeToCall);
            }, timeToCall);
            lastTime = currentTime + timeToCall;
            return id;
        };
    }

    if (!window.cancelAnimationFrame) {
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
    }

}());

/**
 * @object Math Augmentation
 * @author Matthew Wagerfield
 */
Math.PIM2 = Math.PI*2;
Math.PID2 = Math.PI/2;
Math.randomInRange = function(min, max) {
    return min + (max - min) * Math.random();
};
Math.clamp = function(value, min, max) {
    value = Math.max(value, min);
    value = Math.min(value, max);
    return value;
};

/**
 * @object Vector3
 * @author Matthew Wagerfield
 */
FSS.Vector3 = {
    create: function(x, y, z) {
        var vector = new FSS.Array(3);
        this.set(vector, x, y, z);
        return vector;
    },
    clone: function(a) {
        var vector = this.create();
        this.copy(vector, a);
        return vector;
    },
    set: function(target, x, y, z) {
        target[0] = x || 0;
        target[1] = y || 0;
        target[2] = z || 0;
        return this;
    },
    setX: function(target, x) {
        target[0] = x || 0;
        return this;
    },
    setY: function(target, y) {
        target[1] = y || 0;
        return this;
    },
    setZ: function(target, z) {
        target[2] = z || 0;
        return this;
    },
    copy: function(target, a) {
        target[0] = a[0];
        target[1] = a[1];
        target[2] = a[2];
        return this;
    },
    add: function(target, a) {
        target[0] += a[0];
        target[1] += a[1];
        target[2] += a[2];
        return this;
    },
    addVectors: function(target, a, b) {
        target[0] = a[0] + b[0];
        target[1] = a[1] + b[1];
        target[2] = a[2] + b[2];
        return this;
    },
    addScalar: function(target, s) {
        target[0] += s;
        target[1] += s;
        target[2] += s;
        return this;
    },
    subtract: function(target, a) {
        target[0] -= a[0];
        target[1] -= a[1];
        target[2] -= a[2];
        return this;
    },
    subtractVectors: function(target, a, b) {
        target[0] = a[0] - b[0];
        target[1] = a[1] - b[1];
        target[2] = a[2] - b[2];
        return this;
    },
    subtractScalar: function(target, s) {
        target[0] -= s;
        target[1] -= s;
        target[2] -= s;
        return this;
    },
    multiply: function(target, a) {
        target[0] *= a[0];
        target[1] *= a[1];
        target[2] *= a[2];
        return this;
    },
    multiplyVectors: function(target, a, b) {
        target[0] = a[0] * b[0];
        target[1] = a[1] * b[1];
        target[2] = a[2] * b[2];
        return this;
    },
    multiplyScalar: function(target, s) {
        target[0] *= s;
        target[1] *= s;
        target[2] *= s;
        return this;
    },
    divide: function(target, a) {
        target[0] /= a[0];
        target[1] /= a[1];
        target[2] /= a[2];
        return this;
    },
    divideVectors: function(target, a, b) {
        target[0] = a[0] / b[0];
        target[1] = a[1] / b[1];
        target[2] = a[2] / b[2];
        return this;
    },
    divideScalar: function(target, s) {
        if (s !== 0) {
            target[0] /= s;
            target[1] /= s;
            target[2] /= s;
        } else {
            target[0] = 0;
            target[1] = 0;
            target[2] = 0;
        }
        return this;
    },
    cross: function(target, a) {
        var x = target[0];
        var y = target[1];
        var z = target[2];
        target[0] = y*a[2] - z*a[1];
        target[1] = z*a[0] - x*a[2];
        target[2] = x*a[1] - y*a[0];
        return this;
    },
    crossVectors: function(target, a, b) {
        target[0] = a[1]*b[2] - a[2]*b[1];
        target[1] = a[2]*b[0] - a[0]*b[2];
        target[2] = a[0]*b[1] - a[1]*b[0];
        return this;
    },
    min: function(target, value) {
        if (target[0] < value) { target[0] = value; }
        if (target[1] < value) { target[1] = value; }
        if (target[2] < value) { target[2] = value; }
        return this;
    },
    max: function(target, value) {
        if (target[0] > value) { target[0] = value; }
        if (target[1] > value) { target[1] = value; }
        if (target[2] > value) { target[2] = value; }
        return this;
    },
    clamp: function(target, min, max) {
        this.min(target, min);
        this.max(target, max);
        return this;
    },
    limit: function(target, min, max) {
        var length = this.length(target);
        if (min !== null && length < min) {
            this.setLength(target, min);
        } else if (max !== null && length > max) {
            this.setLength(target, max);
        }
        return this;
    },
    dot: function(a, b) {
        return a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
    },
    normalise: function(target) {
        return this.divideScalar(target, this.length(target));
    },
    negate: function(target) {
        return this.multiplyScalar(target, -1);
    },
    distanceSquared: function(a, b) {
        var dx = a[0] - b[0];
        var dy = a[1] - b[1];
        var dz = a[2] - b[2];
        return dx*dx + dy*dy + dz*dz;
    },
    distance: function(a, b) {
        return Math.sqrt(this.distanceSquared(a, b));
    },
    lengthSquared: function(a) {
        return a[0]*a[0] + a[1]*a[1] + a[2]*a[2];
    },
    length: function(a) {
        return Math.sqrt(this.lengthSquared(a));
    },
    setLength: function(target, l) {
        var length = this.length(target);
        if (length !== 0 && l !== length) {
            this.multiplyScalar(target, l / length);
        }
        return this;
    }
};

/**
 * @object Vector4
 * @author Matthew Wagerfield
 */
FSS.Vector4 = {
    create: function(x, y, z, w) {
        var vector = new FSS.Array(4);
        this.set(vector, x, y, z);
        return vector;
    },
    set: function(target, x, y, z, w) {
        target[0] = x || 0;
        target[1] = y || 0;
        target[2] = z || 0;
        target[3] = w || 0;
        return this;
    },
    setX: function(target, x) {
        target[0] = x || 0;
        return this;
    },
    setY: function(target, y) {
        target[1] = y || 0;
        return this;
    },
    setZ: function(target, z) {
        target[2] = z || 0;
        return this;
    },
    setW: function(target, w) {
        target[3] = w || 0;
        return this;
    },
    add: function(target, a) {
        target[0] += a[0];
        target[1] += a[1];
        target[2] += a[2];
        target[3] += a[3];
        return this;
    },
    multiplyVectors: function(target, a, b) {
        target[0] = a[0] * b[0];
        target[1] = a[1] * b[1];
        target[2] = a[2] * b[2];
        target[3] = a[3] * b[3];
        return this;
    },
    multiplyScalar: function(target, s) {
        target[0] *= s;
        target[1] *= s;
        target[2] *= s;
        target[3] *= s;
        return this;
    },
    min: function(target, value) {
        if (target[0] < value) { target[0] = value; }
        if (target[1] < value) { target[1] = value; }
        if (target[2] < value) { target[2] = value; }
        if (target[3] < value) { target[3] = value; }
        return this;
    },
    max: function(target, value) {
        if (target[0] > value) { target[0] = value; }
        if (target[1] > value) { target[1] = value; }
        if (target[2] > value) { target[2] = value; }
        if (target[3] > value) { target[3] = value; }
        return this;
    },
    clamp: function(target, min, max) {
        this.min(target, min);
        this.max(target, max);
        return this;
    }
};

/**
 * @class Color
 * @author Matthew Wagerfield
 */
FSS.Color = function(hex, opacity) {
    this.rgba = FSS.Vector4.create();
    this.hex = hex || '#000000';
    this.opacity = FSS.Utils.isNumber(opacity) ? opacity : 1;
    this.set(this.hex, this.opacity);
};

FSS.Color.prototype = {
    set: function(hex, opacity) {
        hex = hex.replace('#', '');
        var size = hex.length / 3;
        this.rgba[0] = parseInt(hex.substring(size*0, size*1), 16) / 255;
        this.rgba[1] = parseInt(hex.substring(size*1, size*2), 16) / 255;
        this.rgba[2] = parseInt(hex.substring(size*2, size*3), 16) / 255;
        this.rgba[3] = FSS.Utils.isNumber(opacity) ? opacity : this.rgba[3];
        return this;
    },
    hexify: function(channel) {
        var hex = Math.ceil(channel*255).toString(16);
        if (hex.length === 1) { hex = '0' + hex; }
        return hex;
    },
    format: function() {
        var r = this.hexify(this.rgba[0]);
        var g = this.hexify(this.rgba[1]);
        var b = this.hexify(this.rgba[2]);
        this.hex = '#' + r + g + b;
        return this.hex;
    }
};

/**
 * @class Object
 * @author Matthew Wagerfield
 */
FSS.Object = function() {
    this.position = FSS.Vector3.create();
};

FSS.Object.prototype = {
    setPosition: function(x, y, z) {
        FSS.Vector3.set(this.position, x, y, z);
        return this;
    }
};

/**
 * @class Light
 * @author Matthew Wagerfield
 */
FSS.Light = function(ambient, diffuse) {
    FSS.Object.call(this);
    this.ambient = new FSS.Color(ambient || '#FFFFFF');
    this.diffuse = new FSS.Color(diffuse || '#FFFFFF');
    this.ray = FSS.Vector3.create();
};

FSS.Light.prototype = Object.create(FSS.Object.prototype);

/**
 * @class Vertex
 * @author Matthew Wagerfield
 */
FSS.Vertex = function(x, y, z) {
    this.position = FSS.Vector3.create(x, y, z);
};

FSS.Vertex.prototype = {
    setPosition: function(x, y, z) {
        FSS.Vector3.set(this.position, x, y, z);
        return this;
    }
};

/**
 * @class Triangle
 * @author Matthew Wagerfield
 */
FSS.Triangle = function(a, b, c) {
    this.a = a || new FSS.Vertex();
    this.b = b || new FSS.Vertex();
    this.c = c || new FSS.Vertex();
    this.vertices = [this.a, this.b, this.c];
    this.u = FSS.Vector3.create();
    this.v = FSS.Vector3.create();
    this.centroid = FSS.Vector3.create();
    this.normal = FSS.Vector3.create();
    this.color = new FSS.Color();
    this.polygon = document.createElementNS(FSS.SVGNS, 'polygon');
    this.polygon.setAttributeNS(null, 'stroke-linejoin', 'round');
    this.polygon.setAttributeNS(null, 'stroke-miterlimit', '1');
    this.polygon.setAttributeNS(null, 'stroke-width', '1');
    this.computeCentroid();
    this.computeNormal();
};

FSS.Triangle.prototype = {
    computeCentroid: function() {
        this.centroid[0] = this.a.position[0] + this.b.position[0] + this.c.position[0];
        this.centroid[1] = this.a.position[1] + this.b.position[1] + this.c.position[1];
        this.centroid[2] = this.a.position[2] + this.b.position[2] + this.c.position[2];
        FSS.Vector3.divideScalar(this.centroid, 3);
        return this;
    },
    computeNormal: function() {
        FSS.Vector3.subtractVectors(this.u, this.b.position, this.a.position);
        FSS.Vector3.subtractVectors(this.v, this.c.position, this.a.position);
        FSS.Vector3.crossVectors(this.normal, this.u, this.v);
        FSS.Vector3.normalise(this.normal);
        return this;
    }
};

/**
 * @class Geometry
 * @author Matthew Wagerfield
 */
FSS.Geometry = function() {
    this.vertices = [];
    this.triangles = [];
    this.dirty = false;
};

FSS.Geometry.prototype = {
    update: function() {
        if (this.dirty) {
            var t,triangle;
            for (t = this.triangles.length - 1; t >= 0; t--) {
                triangle = this.triangles[t];
                triangle.computeCentroid();
                triangle.computeNormal();
            }
            this.dirty = false;
        }
        return this;
    }
};

/**
 * @class Plane
 * @author Matthew Wagerfield, modified by Maksim Surguy to implement Delaunay triangulation
 */
FSS.Plane = function(width, height, howmany) {
    FSS.Geometry.call(this);
    this.width = width || 100;
    this.height = height || 100;

    // Cache Variables
    var x, y, vertices = new Array(howmany);
    offsetX = this.width * -0.5,
        offsetY = this.height * 0.5;

    for(i = vertices.length; i--; ) {
        x =  offsetX + Math.random()*width;
        y =  offsetY - Math.random()*height;

        vertices[i] = [x, y];
    }

    // Generate additional points on the perimeter so that there are no holes in the pattern
    vertices.push([offsetX, offsetY]);
    vertices.push([offsetX + width/2, offsetY]);
    vertices.push([offsetX + width, offsetY]);
    vertices.push([offsetX + width, offsetY - height/2]);
    vertices.push([offsetX + width, offsetY - height]);
    vertices.push([offsetX + width/2, offsetY - height]);
    vertices.push([offsetX, offsetY - height]);
    vertices.push([offsetX, offsetY - height/2]);

    // Generate additional randomly placed points on the perimeter
    for (var i = 6; i >= 0; i--) {
        vertices.push([ offsetX + Math.random()*width, offsetY]);
        vertices.push([ offsetX, offsetY - Math.random()*height]);
        vertices.push([ offsetX + width, offsetY - Math.random()*height]);
        vertices.push([ offsetX + Math.random()*width, offsetY-height]);
    }

    // Create an array of triangulated coordinates from our vertices
    var triangles = Delaunay.triangulate(vertices);

    for(i = triangles.length; i; ) {
        --i;
        var p1 = [Math.ceil(vertices[triangles[i]][0]), Math.ceil(vertices[triangles[i]][1])];
        --i;
        var p2 = [Math.ceil(vertices[triangles[i]][0]), Math.ceil(vertices[triangles[i]][1])];
        --i;
        var p3 = [Math.ceil(vertices[triangles[i]][0]), Math.ceil(vertices[triangles[i]][1])];

        t1 = new FSS.Triangle(new FSS.Vertex(p1[0],p1[1]), new FSS.Vertex(p2[0],p2[1]), new FSS.Vertex(p3[0],p3[1]));
        this.triangles.push(t1);
    }
};

FSS.Plane.prototype = Object.create(FSS.Geometry.prototype);

/**
 * @class Material
 * @author Matthew Wagerfield
 */
FSS.Material = function(ambient, diffuse) {
    this.ambient = new FSS.Color(ambient || '#444444');
    this.diffuse = new FSS.Color(diffuse || '#FFFFFF');
    this.slave = new FSS.Color();
};

/**
 * @class Mesh
 * @author Matthew Wagerfield
 */
FSS.Mesh = function(geometry, material) {
    FSS.Object.call(this);
    this.geometry = geometry || new FSS.Geometry();
    this.material = material || new FSS.Material();
    this.side = FSS.FRONT;
    this.visible = true;
};

FSS.Mesh.prototype = Object.create(FSS.Object.prototype);

FSS.Mesh.prototype.update = function(lights, calculate) {
    var t,triangle, l,light, illuminance;

    // Update Geometry
    this.geometry.update();

    // Calculate the triangle colors
    if (calculate) {

        // Iterate through Triangles
        for (t = this.geometry.triangles.length - 1; t >= 0; t--) {
            triangle = this.geometry.triangles[t];

            // Reset Triangle Color
            FSS.Vector4.set(triangle.color.rgba);

            // Iterate through Lights
            for (l = lights.length - 1; l >= 0; l--) {
                light = lights[l];

                // Calculate Illuminance
                FSS.Vector3.subtractVectors(light.ray, light.position, triangle.centroid);
                FSS.Vector3.normalise(light.ray);
                illuminance = FSS.Vector3.dot(triangle.normal, light.ray);
                if (this.side === FSS.FRONT) {
                    illuminance = Math.max(illuminance, 0);
                } else if (this.side === FSS.BACK) {
                    illuminance = Math.abs(Math.min(illuminance, 0));
                } else if (this.side === FSS.DOUBLE) {
                    illuminance = Math.max(Math.abs(illuminance), 0);
                }

                // Calculate Ambient Light
                FSS.Vector4.multiplyVectors(this.material.slave.rgba, this.material.ambient.rgba, light.ambient.rgba);
                FSS.Vector4.add(triangle.color.rgba, this.material.slave.rgba);

                // Calculate Diffuse Light
                FSS.Vector4.multiplyVectors(this.material.slave.rgba, this.material.diffuse.rgba, light.diffuse.rgba);
                FSS.Vector4.multiplyScalar(this.material.slave.rgba, illuminance);
                FSS.Vector4.add(triangle.color.rgba, this.material.slave.rgba);
            }

            // Clamp & Format Color
            FSS.Vector4.clamp(triangle.color.rgba, 0, 1);
        }
    }
    return this;
};

/**
 * @class Scene
 * @author Matthew Wagerfield
 */
FSS.Scene = function() {
    this.meshes = [];
    this.lights = [];
};

FSS.Scene.prototype = {
    add: function(object) {
        if (object instanceof FSS.Mesh && !~this.meshes.indexOf(object)) {
            this.meshes.push(object);
        } else if (object instanceof FSS.Light && !~this.lights.indexOf(object)) {
            this.lights.push(object);
        }
        return this;
    },
    remove: function(object) {
        if (object instanceof FSS.Mesh && ~this.meshes.indexOf(object)) {
            this.meshes.splice(this.meshes.indexOf(object), 1);
        } else if (object instanceof FSS.Light && ~this.lights.indexOf(object)) {
            this.lights.splice(this.lights.indexOf(object), 1);
        }
        return this;
    }
};

/**
 * @class Renderer
 * @author Matthew Wagerfield
 */
FSS.Renderer = function() {
    this.width = 0;
    this.height = 0;
    this.halfWidth = 0;
    this.halfHeight = 0;
};

FSS.Renderer.prototype = {
    setSize: function(width, height) {
        if (this.width === width && this.height === height) return;
        this.width = width;
        this.height = height;
        this.halfWidth = this.width * 0.5;
        this.halfHeight = this.height * 0.5;
        return this;
    },
    clear: function() {
        return this;
    },
    render: function(scene) {
        return this;
    }
};

/**
 * @class Canvas Renderer
 * @author Matthew Wagerfield
 */
FSS.CanvasRenderer = function() {
    FSS.Renderer.call(this);
    this.element = document.createElement('canvas');
    this.element.style.display = 'block';
    this.context = this.element.getContext('2d');
    this.setSize(this.element.width, this.element.height);
};

FSS.CanvasRenderer.prototype = Object.create(FSS.Renderer.prototype);

FSS.CanvasRenderer.prototype.setSize = function(width, height) {
    FSS.Renderer.prototype.setSize.call(this, width, height);
    this.element.width = width;
    this.element.height = height;
    this.context.setTransform(1, 0, 0, -1, this.halfWidth, this.halfHeight);
    return this;
};

FSS.CanvasRenderer.prototype.clear = function() {
    FSS.Renderer.prototype.clear.call(this);
    this.context.clearRect(-this.halfWidth, -this.halfHeight, this.width, this.height);
    return this;
};

FSS.CanvasRenderer.prototype.render = function(scene) {
    FSS.Renderer.prototype.render.call(this, scene);
    var m,mesh, t,triangle, color;

    // Clear Context
    this.clear();

    // Configure Context
    this.context.lineJoin = 'round';
    this.context.lineWidth = 1;

    // Update Meshes
    for (m = scene.meshes.length - 1; m >= 0; m--) {
        mesh = scene.meshes[m];
        if (mesh.visible) {
            mesh.update(scene.lights, true);

            // Render Triangles
            for (t = mesh.geometry.triangles.length - 1; t >= 0; t--) {
                triangle = mesh.geometry.triangles[t];
                color = triangle.color.format();
                this.context.beginPath();
                this.context.moveTo(triangle.a.position[0], triangle.a.position[1]);
                this.context.lineTo(triangle.b.position[0], triangle.b.position[1]);
                this.context.lineTo(triangle.c.position[0], triangle.c.position[1]);
                this.context.closePath();
                this.context.strokeStyle = color;
                this.context.fillStyle = color;
                this.context.stroke();
                this.context.fill();
            }
        }
    }
    return this;
};

/**
 * @class WebGL Renderer
 * @author Matthew Wagerfield
 */
FSS.WebGLRenderer = function() {
    FSS.Renderer.call(this);
    this.element = document.createElement('canvas');
    this.element.style.display = 'block';

    // Set initial vertex and light count
    this.vertices = null;
    this.lights = null;

    // Create parameters object
    var parameters = {
        preserveDrawingBuffer: false,
        premultipliedAlpha: true,
        antialias: true,
        stencil: true,
        alpha: true
    };

    // Create and configure the gl context
    this.gl = this.getContext(this.element, parameters);

    // Set the internal support flag
    this.unsupported = !this.gl;

    // Setup renderer
    if (this.unsupported) {
        return 'WebGL is not supported by your browser.';
    } else {
        this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
        this.gl.enable(this.gl.DEPTH_TEST);
        this.setSize(this.element.width, this.element.height);
    }
};

FSS.WebGLRenderer.prototype = Object.create(FSS.Renderer.prototype);

FSS.WebGLRenderer.prototype.getContext = function(canvas, parameters) {
    var context = false;
    try {
        if (!(context = canvas.getContext('experimental-webgl', parameters))) {
            throw 'Error creating WebGL context.';
        }
    } catch (error) {
        console.error(error);
    }
    return context;
};

FSS.WebGLRenderer.prototype.setSize = function(width, height) {
    FSS.Renderer.prototype.setSize.call(this, width, height);
    if (this.unsupported) return;

    // Set the size of the canvas element
    this.element.width = width;
    this.element.height = height;

    // Set the size of the gl viewport
    this.gl.viewport(0, 0, width, height);
    return this;
};

FSS.WebGLRenderer.prototype.clear = function() {
    FSS.Renderer.prototype.clear.call(this);
    if (this.unsupported) return;
    this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
    return this;
};

FSS.WebGLRenderer.prototype.render = function(scene) {
    FSS.Renderer.prototype.render.call(this, scene);
    if (this.unsupported) return;
    var m,mesh, t,tl,triangle, l,light,
        attribute, uniform, buffer, data, location,
        update = false, lights = scene.lights.length,
        index, v,vl,vetex,vertices = 0;

    // Clear context
    this.clear();

    // Build the shader program
    if (this.lights !== lights) {
        this.lights = lights;
        if (this.lights > 0) {
            this.buildProgram(lights);
        } else {
            return;
        }
    }

    // Update program
    if (!!this.program) {

        // Increment vertex counter
        for (m = scene.meshes.length - 1; m >= 0; m--) {
            mesh = scene.meshes[m];
            if (mesh.geometry.dirty) update = true;
            mesh.update(scene.lights, false);
            vertices += mesh.geometry.triangles.length*3;
        }

        // Compare vertex counter
        if (update || this.vertices !== vertices) {
            this.vertices = vertices;

            // Build buffers
            for (attribute in this.program.attributes) {
                buffer = this.program.attributes[attribute];
                buffer.data = new FSS.Array(vertices*buffer.size);

                // Reset vertex index
                index = 0;

                // Update attribute buffer data
                for (m = scene.meshes.length - 1; m >= 0; m--) {
                    mesh = scene.meshes[m];

                    for (t = 0, tl = mesh.geometry.triangles.length; t < tl; t++) {
                        triangle = mesh.geometry.triangles[t];

                        for (v = 0, vl = triangle.vertices.length; v < vl; v++) {
                            vertex = triangle.vertices[v];
                            switch (attribute) {
                                case 'side':
                                    this.setBufferData(index, buffer, mesh.side);
                                    break;
                                case 'position':
                                    this.setBufferData(index, buffer, vertex.position);
                                    break;
                                case 'centroid':
                                    this.setBufferData(index, buffer, triangle.centroid);
                                    break;
                                case 'normal':
                                    this.setBufferData(index, buffer, triangle.normal);
                                    break;
                                case 'ambient':
                                    this.setBufferData(index, buffer, mesh.material.ambient.rgba);
                                    break;
                                case 'diffuse':
                                    this.setBufferData(index, buffer, mesh.material.diffuse.rgba);
                                    break;
                            }
                            index++;
                        }
                    }
                }

                // Upload attribute buffer data
                this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer.buffer);
                this.gl.bufferData(this.gl.ARRAY_BUFFER, buffer.data, this.gl.DYNAMIC_DRAW);
                this.gl.enableVertexAttribArray(buffer.location);
                this.gl.vertexAttribPointer(buffer.location, buffer.size, this.gl.FLOAT, false, 0, 0);
            }
        }

        // Build uniform buffers
        this.setBufferData(0, this.program.uniforms.resolution, [this.width, this.height, this.width]);
        for (l = lights-1; l >= 0; l--) {
            light = scene.lights[l];
            this.setBufferData(l, this.program.uniforms.lightPosition, light.position);
            this.setBufferData(l, this.program.uniforms.lightAmbient, light.ambient.rgba);
            this.setBufferData(l, this.program.uniforms.lightDiffuse, light.diffuse.rgba);
        }

        // Update uniforms
        for (uniform in this.program.uniforms) {
            buffer = this.program.uniforms[uniform];
            location = buffer.location;
            data = buffer.data;
            switch (buffer.structure) {
                case '3f':
                    this.gl.uniform3f(location, data[0], data[1], data[2]);
                    break;
                case '3fv':
                    this.gl.uniform3fv(location, data);
                    break;
                case '4fv':
                    this.gl.uniform4fv(location, data);
                    break;
            }
        }
    }

    // Draw those lovely triangles
    this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertices);
    return this;
};

FSS.WebGLRenderer.prototype.setBufferData = function(index, buffer, value) {
    if (FSS.Utils.isNumber(value)) {
        buffer.data[index*buffer.size] = value;
    } else {
        for (var i = value.length - 1; i >= 0; i--) {
            buffer.data[index*buffer.size+i] = value[i];
        }
    }
};

/**
 * Concepts taken from three.js WebGLRenderer
 * @see https://github.com/mrdoob/three.js/blob/master/src/renderers/WebGLRenderer.js
 */
FSS.WebGLRenderer.prototype.buildProgram = function(lights) {
    if (this.unsupported) return;

    // Create shader source
    var vs = FSS.WebGLRenderer.VS(lights);
    var fs = FSS.WebGLRenderer.FS(lights);

    // Derive the shader fingerprint
    var code = vs + fs;

    // Check if the program has already been compiled
    if (!!this.program && this.program.code === code) return;

    // Create the program and shaders
    var program = this.gl.createProgram();
    var vertexShader = this.buildShader(this.gl.VERTEX_SHADER, vs);
    var fragmentShader = this.buildShader(this.gl.FRAGMENT_SHADER, fs);

    // Attach an link the shader
    this.gl.attachShader(program, vertexShader);
    this.gl.attachShader(program, fragmentShader);
    this.gl.linkProgram(program);

    // Add error handling
    if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
        var error = this.gl.getError();
        var status = this.gl.getProgramParameter(program, this.gl.VALIDATE_STATUS);
        console.error('Could not initialise shader.\nVALIDATE_STATUS: '+status+'\nERROR: '+error);
        return null;
    }

    // Delete the shader
    this.gl.deleteShader(fragmentShader);
    this.gl.deleteShader(vertexShader);

    // Set the program code
    program.code = code;

    // Add the program attributes
    program.attributes = {
        side:     this.buildBuffer(program, 'attribute', 'aSide',     1, 'f' ),
        position: this.buildBuffer(program, 'attribute', 'aPosition', 3, 'v3'),
        centroid: this.buildBuffer(program, 'attribute', 'aCentroid', 3, 'v3'),
        normal:   this.buildBuffer(program, 'attribute', 'aNormal',   3, 'v3'),
        ambient:  this.buildBuffer(program, 'attribute', 'aAmbient',  4, 'v4'),
        diffuse:  this.buildBuffer(program, 'attribute', 'aDiffuse',  4, 'v4')
    };

    // Add the program uniforms
    program.uniforms = {
        resolution:    this.buildBuffer(program, 'uniform', 'uResolution',    3, '3f',  1     ),
        lightPosition: this.buildBuffer(program, 'uniform', 'uLightPosition', 3, '3fv', lights),
        lightAmbient:  this.buildBuffer(program, 'uniform', 'uLightAmbient',  4, '4fv', lights),
        lightDiffuse:  this.buildBuffer(program, 'uniform', 'uLightDiffuse',  4, '4fv', lights)
    };

    // Set the renderer program
    this.program = program;

    // Enable program
    this.gl.useProgram(this.program);

    // Return the program
    return program;
};

FSS.WebGLRenderer.prototype.buildShader = function(type, source) {
    if (this.unsupported) return;

    // Create and compile shader
    var shader = this.gl.createShader(type);
    this.gl.shaderSource(shader, source);
    this.gl.compileShader(shader);

    // Add error handling
    if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
        console.error(this.gl.getShaderInfoLog(shader));
        return null;
    }

    // Return the shader
    return shader;
};

FSS.WebGLRenderer.prototype.buildBuffer = function(program, type, identifier, size, structure, count) {
    var buffer = {buffer:this.gl.createBuffer(), size:size, structure:structure, data:null};

    // Set the location
    switch (type) {
        case 'attribute':
            buffer.location = this.gl.getAttribLocation(program, identifier);
            break;
        case 'uniform':
            buffer.location = this.gl.getUniformLocation(program, identifier);
            break;
    }

    // Create the buffer if count is provided
    if (!!count) {
        buffer.data = new FSS.Array(count*size);
    }

    // Return the buffer
    return buffer;
};

FSS.WebGLRenderer.VS = function(lights) {
    var shader = [

        // Precision
        'precision mediump float;',

        // Lights
        '#define LIGHTS ' + lights,

        // Attributes
        'attribute float aSide;',
        'attribute vec3 aPosition;',
        'attribute vec3 aCentroid;',
        'attribute vec3 aNormal;',
        'attribute vec4 aAmbient;',
        'attribute vec4 aDiffuse;',

        // Uniforms
        'uniform vec3 uResolution;',
        'uniform vec3 uLightPosition[LIGHTS];',
        'uniform vec4 uLightAmbient[LIGHTS];',
        'uniform vec4 uLightDiffuse[LIGHTS];',

        // Varyings
        'varying vec4 vColor;',

        // Main
        'void main() {',

        // Create color
        'vColor = vec4(0.0);',

        // Calculate the vertex position
        'vec3 position = aPosition / uResolution * 2.0;',

        // Iterate through lights
        'for (int i = 0; i < LIGHTS; i++) {',
        'vec3 lightPosition = uLightPosition[i];',
        'vec4 lightAmbient = uLightAmbient[i];',
        'vec4 lightDiffuse = uLightDiffuse[i];',

        // Calculate illuminance
        'vec3 ray = normalize(lightPosition - aCentroid);',
        'float illuminance = dot(aNormal, ray);',
        'if (aSide == 0.0) {',
        'illuminance = max(illuminance, 0.0);',
        '} else if (aSide == 1.0) {',
        'illuminance = abs(min(illuminance, 0.0));',
        '} else if (aSide == 2.0) {',
        'illuminance = max(abs(illuminance), 0.0);',
        '}',

        // Calculate ambient light
        'vColor += aAmbient * lightAmbient;',

        // Calculate diffuse light
        'vColor += aDiffuse * lightDiffuse * illuminance;',
        '}',

        // Clamp color
        'vColor = clamp(vColor, 0.0, 1.0);',

        // Set gl_Position
        'gl_Position = vec4(position, 1.0);',

        '}'

        // Return the shader
    ].join('\n');
    return shader;
};

FSS.WebGLRenderer.FS = function(lights) {
    var shader = [

        // Precision
        'precision mediump float;',

        // Varyings
        'varying vec4 vColor;',

        // Main
        'void main() {',

        // Set gl_FragColor
        'gl_FragColor = vColor;',

        '}'

        // Return the shader
    ].join('\n');
    return shader;
};

/**
 * @class SVG Renderer
 * @author Matthew Wagerfield
 */
FSS.SVGRenderer = function() {
    FSS.Renderer.call(this);
    this.element = document.createElementNS(FSS.SVGNS, 'svg');
    this.element.setAttribute('xmlns', FSS.SVGNS);
    this.element.setAttribute('version', '1.1');
    this.element.style.display = 'block';
    this.setSize(300, 150);
};

FSS.SVGRenderer.prototype = Object.create(FSS.Renderer.prototype);

FSS.SVGRenderer.prototype.setSize = function(width, height) {
    FSS.Renderer.prototype.setSize.call(this, width, height);
    this.element.setAttribute('width', width);
    this.element.setAttribute('height', height);
    return this;
};

FSS.SVGRenderer.prototype.clear = function() {
    FSS.Renderer.prototype.clear.call(this);
    for (var i = this.element.childNodes.length - 1; i >= 0; i--) {
        this.element.removeChild(this.element.childNodes[i]);
    }
    return this;
};

FSS.SVGRenderer.prototype.render = function(scene) {
    FSS.Renderer.prototype.render.call(this, scene);
    var m,mesh, t,triangle, points, style;

    // Update Meshes
    for (m = scene.meshes.length - 1; m >= 0; m--) {
        mesh = scene.meshes[m];
        if (mesh.visible) {
            mesh.update(scene.lights, true);

            // Render Triangles
            for (t = mesh.geometry.triangles.length - 1; t >= 0; t--) {
                triangle = mesh.geometry.triangles[t];
                if (triangle.polygon.parentNode !== this.element) {
                    this.element.appendChild(triangle.polygon);
                }
                points  = this.formatPoint(triangle.a)+' ';
                points += this.formatPoint(triangle.b)+' ';
                points += this.formatPoint(triangle.c);
                style = this.formatStyle(triangle.color.format());
                triangle.polygon.setAttributeNS(null, 'points', points);
                triangle.polygon.setAttributeNS(null, 'style', style);
            }
        }
    }
    return this;
};

FSS.SVGRenderer.prototype.formatPoint = function(vertex) {
    return (this.halfWidth+vertex.position[0])+','+(this.halfHeight-vertex.position[1]);
};

FSS.SVGRenderer.prototype.formatStyle = function(color) {
    var style = 'fill:'+color+';';
    style += 'stroke:'+color+';';
    return style;
};

(function(){

    //------------------------------
    // Mesh Properties
    //------------------------------
    var MESH = {
        width: 1,
        height: 1,
        slices: 40,
        ambient: '#02b9e3',
        diffuse: '#aaaaaa'
    };

    //------------------------------
    // Light Properties
    //------------------------------
    var LIGHT = {
        count: 2,
        xPos : 100,
        yPos : 0,
        zOffset: 100,
        ambient: '#666666',
        diffuse: '#666666',
        pickedup : true,
        proxy : false,
        currIndex : 0
    };

    //------------------------------
    // Render Properties
    //------------------------------
    var WEBGL = 'webgl';
    var CANVAS = 'canvas';
    var SVG = 'svg';
    var RENDER = {
        renderer: CANVAS
    };

    //------------------------------
    // Global Properties
    //------------------------------
    var center = FSS.Vector3.create();
    var container = document.getElementById('container');
    var controls = document.getElementById('controls');
    var output = document.getElementById('output');
    var renderer, scene, mesh, geometry, material;
    var webglRenderer, canvasRenderer, svgRenderer;

    //------------------------------
    // Methods
    //------------------------------
    function initialise() {
        createRenderer();
        createScene();
        createMesh();
        addLight();
        setLightPosition(container.offsetWidth / 2, 0);
      LIGHT.count++;              addLight();

        setLightPosition(0, 0);
        addEventListeners();
        //addControls();
        resize(container.offsetWidth, container.offsetHeight);
        animate();
    }

    function createRenderer() {
        webglRenderer = new FSS.WebGLRenderer();
        canvasRenderer = new FSS.CanvasRenderer();
        svgRenderer = new FSS.SVGRenderer();
        setRenderer(RENDER.renderer);
    }

    function setRenderer(index) {
        if (renderer) {
            output.removeChild(renderer.element);
        }
        switch(index) {
            case WEBGL:
                renderer = webglRenderer;
                break;
            case CANVAS:
                renderer = canvasRenderer;
                break;
            case SVG:
                renderer = svgRenderer;
                break;
        }
        renderer.setSize(container.offsetWidth, container.offsetHeight);
        output.appendChild(renderer.element);
    }

    function createScene() {
        scene = new FSS.Scene();
    }

    function createMesh() {
        scene.remove(mesh);
        renderer.clear();
        geometry = new FSS.Plane(MESH.width * renderer.width, MESH.height * renderer.height, MESH.slices);
        material = new FSS.Material(MESH.ambient, MESH.diffuse);
        mesh = new FSS.Mesh(geometry, material);
        scene.add(mesh);
    }

    // Add a single light
    function addLight() {
        renderer.clear();
        var light = new FSS.Light(LIGHT.ambient, LIGHT.diffuse);
        light.ambientHex = light.ambient.format();
        light.diffuseHex = light.diffuse.format();
        light.setPosition(LIGHT.xPos, LIGHT.yPos, LIGHT.zOffset);
        scene.add(light);
        LIGHT.proxy = light;
        LIGHT.currIndex++;
    }

    // Remove lights
    function trimLights(value) {
        LIGHT.proxy = scene.lights[value];
        for (l = value; l >= scene.lights.length - 1; l--) {
            light = scene.lights[l];
            scene.remove(light);
        }
        renderer.clear();
    }

    // Resize canvas
    function resize(width, height) {
        renderer.setSize(width, height);
        FSS.Vector3.set(center, renderer.halfWidth, renderer.halfHeight);
        createMesh();
    }

    function animate() {
        render();
        requestAnimationFrame(animate);
    }

    function render() {
        renderer.render(scene);
    }

    function addEventListeners() {
        window.addEventListener('resize', onWindowResize);
        container.addEventListener('mousemove', onMouseMove);
    }

    function toggleEl(id) {
        var e = document.getElementById(id);
        if(e.style.display == 'block')
            e.style.display = 'none';
        else
            e.style.display = 'block';
    }


    //------------------------------
    // Callbacks
    //------------------------------

    function onWindowResize(event) {
        resize(container.offsetWidth, container.offsetHeight);
        render();
    }

    function onMouseMove(event) {
        if(!LIGHT.pickedup){
            setLightPosition(event.x, event.y);
        }
    }

    function setLightPosition(x, y) {
        LIGHT.xPos = x - renderer.width/2;
        LIGHT.yPos = renderer.height/2 - y;
        LIGHT.proxy.setPosition(LIGHT.xPos, LIGHT.yPos, LIGHT.proxy.position[2]);
    }

    // Let there be light!
    initialise();
})();
<div id="container">
  <div id="output"></div>
</div>
#container,#output {
  width:100%;
  height:53px;
}