diff --git a/saku/index.css b/saku/index.css
new file mode 100644
index 0000000..cd5a378
--- /dev/null
+++ b/saku/index.css
@@ -0,0 +1,41 @@
+@import url('https://fonts.googleapis.com/css?family=Raleway');
+@import url('https://fonts.googleapis.com/css?family=Josefin+Sans');
+
+html {
+ font-family: 'Raleway';
+ /*background-color: #15171B;*/
+}
+
+body {
+ margin: 0;
+}
+
+#hexagon {
+ height: 300;
+}
+
+#hexagon polygon {
+ cursor: pointer;
+ fill: rgba(0,0,0,0);
+ stroke-width: 2px;
+ stroke: #D0790E;
+
+ transition: fill 0.2s ease;
+}
+
+#hexagon text {
+ fill: #D0790E;
+ pointer-events: none;
+}
+
+.hex {
+ background-color: red;
+}
+
+#hexagon polygon:hover {
+ fill: rgba(255,255,255,0.05);
+}
+
+#glCanvas {
+
+}
\ No newline at end of file
diff --git a/saku/index.html b/saku/index.html
new file mode 100644
index 0000000..d6939eb
--- /dev/null
+++ b/saku/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Foxnet
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/saku/index.js b/saku/index.js
new file mode 100644
index 0000000..aa6f263
--- /dev/null
+++ b/saku/index.js
@@ -0,0 +1,353 @@
+//temp scaling 1 = 1 centimeter
+// 1 centimeter = 100px
+
+Saku = {
+ Scene: function(canvas, pixelScale, globalScale) {
+ //invalid
+ this.canvas = canvas;
+ this.globalScale = "centimeter";
+ this.pixelScale = pixelScale || 1000;
+ this.camera = undefined;
+ this.objects = [];
+ this.addObject = function(obj) {
+ var invalid = [varType(obj) !== "Object", varType(obj)];
+ if(varType(obj) !== "Object") throw TypeError("Expected Object as argument; Got " + invalid[1] + ".");
+
+ this.objects.push(obj);
+ }
+ this.setCamera = function(cam) {
+ //invalid
+ this.camera = cam;
+ }
+ },
+ Camera: function(pos, rot, foc, size) {
+ //invalid
+ this.position = pos || [0,0,0];
+ this.rotation = rot || [0,0];
+ this.focal = foc || 0.35;
+ this.size = size || 0.32;
+ },
+ Face: function(vertices, options) {
+ if(vertices === undefined) throw TypeError("Not enough arguments; Expected Array as argument.");
+ var invalid = [[varType(vertices) !== "Array", varType(vertices)]];
+ invalid[1] = vertices.every(function(element) { return varType(element) !== "Array"; });
+ invalid[2] = invalid[1] || vertices.every(function(element) { return element.every(function(number) { return isNaN(number); }); });
+ if(invalid[0][0]) throw TypeError("Expected Array as argument; Got " + invalid[0][1]);
+ if(invalid[1]) throw TypeError("Expected Arrays as 1st dimensional element.");
+ if(invalid[2]) throw TypeError("Expected Numbers as 2nd dimensional element.");
+
+ this.name = options.name || "Unnamed Object Face";
+ this.vertices = vertices;
+ this.origin = options.origin || getOrigin(vertices);
+
+ this.translateX = function(value) {
+ this.vertices.forEach(function(ele, index, arr) {
+ arr[index][0] = ele[0] + value;
+ });
+ }
+
+ this.translateY = function(value) {
+ this.vertices.forEach(function(ele, index, arr) {
+ arr[index][1] = ele[1] + value;
+ });
+ }
+
+ this.translateZ = function(value) {
+ this.vertices.forEach(function(ele, index, arr) {
+ arr[index][2] = ele[2] + value;
+ });
+ }
+
+ this.translate = function(value) {
+ this.vertices.forEach(function(ele, index, arr) {
+ arr[index][0] = ele[0] + value;
+ arr[index][1] = ele[1] + value;
+ arr[index][2] = ele[2] + value;
+ });
+ }
+ },
+ Model: function(faces, connections, name) {
+ this.name = options.name || "Unnamed Object Model";
+ this.faces = faces;
+ this.connections = connections;
+ }
+}
+
+var ref = ["Triangle", "Square", "Pentagon", "Hexagon", "Heptagon", "Octagon", "Nonagon", "Decagon"];
+
+Saku["Polygon"] = function(sides, name, size) {
+ var defArray = [];
+ var offset = !(sides%2)*Math.PI/sides;
+ for(var i = 0; i < sides; i++) {
+ defArray.push([
+ Math.sin(i*-2*Math.PI/sides+offset),
+ 0,
+ Math.cos(i*-2*Math.PI/sides+offset)
+ ]);
+ }
+
+ defArray = Rnd(defArray,5);
+ Saku.Face.call(this, defArray, {name: (name || (sides > 10) ? sides+"-gon" : ref[sides-3])});
+ this.prototype = Object.create(Saku.Face.prototype);
+}
+
+ref.forEach(function(ele, index) {
+ Saku[ele] = function(name) {
+ Saku.Polygon.call(this, index+3, name);
+ this.prototype = Object.create(Saku.Face.prototype);
+ }
+});
+
+/*Saku["Cube"] = function(name) {
+ var defArray = [
+ [-0.5, -0.5, 0.5],
+ [0.5, -0.5, 0.5],
+ []
+ ]
+}*/
+
+function arrayOperation(item, operator, amount) {
+ var operators = {
+ "+": function(x,y) {return x+y},
+ "-": function(x,y) {return x-y},
+ "*": function(x,y) {return x*y},
+ "/": function(x,y) {return x/y},
+ "^": function(x,y) {return Math.pow(x,y)},
+ "log": function(x,y) {return Math.log(x) / Math.log(y || 10)}
+ }
+ var type = varType(item);
+ if(type === "Array") {
+ var arr = [];
+ for(var i = 0; i < item.length; i++) {
+ arr[i] = arrayOperation(item[i], operator, amount);
+ }
+ return arr;
+ } else if(type === "Number") {
+ return operators[operator](item, amount);
+ } else {
+ throw new TypeError("Expected Numbers, got " + type + ".");
+ }
+}
+
+function varType(variable) {
+ var type = typeof variable;
+ if(type === "object") {
+ return (variable.constructor === Array) ? "Array" : "Object";
+ } else {
+ return type[0].toUpperCase() + type.slice(1);
+ }
+}
+
+function getOrigin(vert) {
+ var origin = [0,0,0];
+ for(var i = 0; i < vert[0].length; i++) {
+ for(var j = 0; j < vert.length; j++) origin[i] += vert[j][i];
+ origin[i] /= Rnd(vert.length,3);
+ }
+ return origin;
+}
+
+function updateFrame(scene) {
+ var ctx = document.getElementById(scene.canvas).getContext("2d");
+ ctx.clearRect(0,0,canvas.width,canvas.height)
+ for(var i = 0; i < scene.objects.length; i++) {
+ var object = scene.objects[i];
+ object = projectObj(object, scene);
+
+ ctx.beginPath();
+ ctx.moveTo(object[0][0],object[0][1]);
+ for(var j = 1; j < object.length; j++) {
+ ctx.lineTo(object[j][0],object[j][1]);
+ }
+ ctx.fill();
+ }
+}
+
+function projectObj(object, scene) {
+ var log = [];
+ var newShape = [];
+ var canvas = document.getElementById(scene.canvas);
+ var cP = scene.camera.position;
+ var cR = scene.camera.rotation;
+ var cF = scene.camera.focal;
+ // Camera direction vector
+ var cV = [
+ Rnd(cF*Math.cos(toRad(cR[0]))*Math.sin(toRad(cR[1])),3), // 0 Degrees Z points straight to Y.
+ Rnd(cF*Math.cos(toRad(cR[0]))*Math.cos(toRad(cR[1])),3),
+ Rnd(cF*Math.sin(toRad(cR[0])),3) // 0 Degrees X points straight to Y.
+ ];
+
+ if(verbose) {
+ log = {
+ "Object Name": object.name,
+ "Camera Position": vecToObj(cP),
+ "Camera Rotation": {
+ "X": cR[0],
+ "Z": cR[1]
+ },
+ "Camera Focal": cF,
+ "Camera Vector": vecToObj(cV),
+ "Object Origin": vecToObj(object.origin),
+ "Object Vertice Values": {},
+ "Canvas Points": {}
+ }
+ }
+
+ // Perspective mapping
+ for(var i = 0; i < object.vertices.length; i++) { // Each point in 3D
+ var x = object.vertices[i][0];
+ var y = object.vertices[i][1];
+ var z = object.vertices[i][2];
+ var pV = [x-cP[0],y-cP[1],z-cP[2]]; // Point direction vector
+ // Restricting to X and Z dimensions and comparing to Y.
+ var distPX = mag(dim("XY",pV));
+ var distPZ = mag(dim("ZY",pV));
+ var distCX = mag(dim("XY",cV));
+ var distCZ = mag(dim("ZY",cV));
+
+ /* Adjacent and Opposite calculated with math simplifications
+ cos(arccosx) = x
+ sin(arccosx) = sqrt(1-x^2) or Pythagorean theorem
+ */
+
+ var adjX = Rnd(dot(dim("XY",cV),dim("XY",pV)) / distCX,5);
+ var adjZ = Rnd(dot(dim("ZY",cV),dim("ZY",pV)) / distCZ,5);
+
+ var oppX = Math.sqrt(Math.pow(distPX,2) - Math.pow(adjX,2));
+ var oppZ = Math.sqrt(Math.pow(distPZ,2) - Math.pow(adjZ,2));
+
+ var projOppX = Rnd(distCX*oppX/adjX,5); // Represents X in projective plane.
+ var projOppZ = Rnd(distCZ*oppZ/adjZ,5); // Represents Y in projective plane.
+ // If the dot product of a and (b rotated -pi/2) is greater than 0, b is on the right of a.
+ var aheadX = dot([cV[0],cV[1]],[-pV[1],pV[0]]);
+ var aheadZ = dot([cV[2],cV[1]],[-pV[1],pV[2]]);
+
+ if(aheadX < 0) projOppX *= -1;
+ if(aheadZ > 0) projOppZ *= -1;
+
+ projOppX += scene.camera.size/2;
+ projOppZ += (canvas.height/canvas.width)*scene.camera.size/2;
+
+ var canvasPointX = Rnd(projOppX * canvas.width/scene.camera.size,3);
+ var canvasPointY = Rnd(projOppZ * canvas.width/scene.camera.size,3);
+
+ if(verbose) {
+ var num = "Point " + (i+1).toString();
+ log["Object Vertice Values"][num] = {
+ "Point Position": vecToObj(object.vertices[i]),
+ "Position Vector": vecToObj(pV),
+ "DistancePXY": distPX,
+ "DistancePZY": distPZ,
+ "DistanceCXY": distCX,
+ "DistanceCZY": distCZ,
+ "AdjacentXY": adjX,
+ "AdjacentZY": adjZ,
+ "OppositeXY": oppX,
+ "OppositeZY": oppZ,
+ "ProjectedOppXY": projOppX,
+ "ProjectedOppZY": projOppZ,
+ "AheadXY": aheadX,
+ "AheadZY": aheadZ
+ };
+ log["Canvas Points"][num] = vecToObj([canvasPointX, canvasPointY]);
+ }
+ newShape.push([canvasPointX, canvasPointY]);
+ }
+ if(verbose) console.log(log);
+ return newShape;
+}
+
+function toRad(deg) {
+ return deg/180*Math.PI;
+}
+
+function Rnd(item,fig) {
+ if(varType(item) === "Array") {
+ var arr = [];
+ for(var i = 0; i < item.length; i++) {
+ arr[i] = Rnd(item[i],fig);
+ }
+ return arr;
+ } else if(varType(item) === "Number") {
+ return Math.round(item*Math.pow(10,fig))/Math.pow(10,fig);
+ } else {
+ throw new TypeError("Expected Integers, got " + varType(item) + ".");
+ }
+}
+
+function dot(vecOne, vecTwo) {
+ if(vecOne.length !== vecTwo.length) {
+ throw new SizeMismatch('VectorDimMismatch', [vecOne,vecTwo]);
+ return;
+ }
+ var final = 0;
+ for(var i = 0; i < vecOne.length; i++) {
+ final += vecOne[i]*vecTwo[i];
+ }
+ return final;
+}
+
+function mag(vec) {
+ var rad = 0;
+ for(var i = 0; i < vec.length; i++) {
+ rad += Math.pow(vec[i],2);
+ }
+ return Math.sqrt(rad);
+}
+
+function dim(dimensions, vector) {
+ var newVec = [];
+ var ref = {
+ "x": 0,
+ "y": 1,
+ "z": 2
+ };
+
+ if(dimensions.constructor === Array) {
+ for(var i = 0; i < dimensions.length; i++) {
+ newVec.push(vector[dimensions[i]]);
+ }
+ } else if(dimensions.constructor === String) {
+ for(var i = 0; i < dimensions.length; i++) {
+ newVec.push(vector[ref[dimensions[i].toLowerCase()]]);
+ }
+ }
+ return newVec;
+}
+
+function SizeMismatch(message, obj) {
+ this.mesage = message;
+ this.name = "SizeMismatch";
+ this.matrix = obj;
+}
+
+function vecToObj(vec) {
+ var obj = {};
+ var ref = ["X","Y","Z"];
+ vec.forEach(function(part, index) {
+ obj[ref[index]] = part;
+ });
+ return obj;
+}
+
+var canvas = document.getElementById("glCanvas");
+canvas.width = window.innerWidth;
+canvas.height = window.innerHeight;
+
+drawObjects = [];
+verbose = false; // [[Position],[Rotation(x,z)],Focal];
+
+scene = new Saku.Scene("glCanvas");
+var polygon = new Saku.Polygon(3);
+var camera = new Saku.Camera();
+scene.setCamera(camera);
+
+scene.addObject(polygon);
+polygon.translateY(10);
+
+setInterval(function() { updateFrame(scene); }, 500);
+
+function verb() {
+ verbose = true;
+ setTimeout(function() { verbose = false; }, 600);
+}