var NPos3d = NPos3d || {
addFunc: function (o) {
var t = this, len, i;
if(o.parent){ //If the object already has a parent, remove it from that one first.
o.parent.remove(o);
}
if(t.children === undefined){
t.children = [];
} else {
//It is never a good idea to allow an item to be a child of a parent more than once.
//Trust me.
len = t.children.length;
for (i = 0; i < len; i += 1) {
if (t.children[i] === o) {
return false;
}
}
}
if(t.childrenToBeAdded === undefined){
t.childrenToBeAdded = [];
} else {
//Check here too, in case it was added multiple times per frame.
len = t.childrenToBeAdded.length;
for (i = 0; i < len; i += 1) {
if (t.childrenToBeAdded[i] === o) {
return false;
}
}
}
t.childrenToBeAdded.push(o);
return true;
},
removeFunc: function (o) {
if (o.onRemove !== undefined) {
o.onRemove();
}
o.expired = true;
},
destroyFunc: function () {
if (this.onRemove !== undefined) {
this.onRemove();
}
this.expired = true;
},
updateMatricesFunc: function(viewMatrix) {
var t = this,
m = NPos3d.Maths,
p = t.parent;
//localScale: ,
//localRotation: ,
//localComposite: ,
//globalComposite
//START updating the object's local matrices
//scale
m.mat4P3Scale(m.__mat4Identity, t.scale, t.matrices.localScale);
//rotate
m.eulerToMat4(t.rot, t.rotOrder, t.matrices.localRotation);
//composite matrix starts out as scale, no need to multiply
m.mat4Set(t.matrices.localComposite, t.matrices.localScale);
m.mat4Mul(t.matrices.localComposite, t.matrices.localRotation, t.matrices.localComposite);
//no need to multiply the local composite, adding 3 keys will be faster
//this is also why we don't need a local localPosition matrix.
m.mat4P3Translate(t.matrices.localComposite, t.pos, t.matrices.localComposite);
//END updating the object's local matrices
//Multiply the localComposite by the patent's globalComposite to get this object's globalComposite
if(p && !p.isScene){
m.mat4Mul(t.matrices.localComposite, p.matrices.globalComposite, t.matrices.globalComposite);
} else if(viewMatrix != undefined) {
m.mat4Mul(t.matrices.localComposite, p.viewMatrix, t.matrices.globalComposite);
} else {
m.mat4Set(t.matrices.globalComposite, t.matrices.localComposite);
}
m.p3Mat4Mul([0,0,0], t.matrices.globalComposite, t.gPos); //because it rocks to be able to read a global position
m.p3Mat4Mul(t.scale, t.matrices.globalComposite, t.gScale); //Would this even work?
},
recursivelyUpdateMatrices: function rUM(o) {
if(o.parent && !o.parent.isScene) {
rUM(o.parent);
}
o.updateMatrices();
},
transformPoints: function(o, outPoints) {
var m = NPos3d.Maths, i;
for (i = 0; i < o.shape.points.length; i += 1) {
outPoints[i] = m.p3Mat4Mul(o.shape.points[i], o.matrices.globalComposite, outPoints[i]);
}
},
getTransformedPointsFunc: function(){
var t = this;
var transformedPoints = [];
if(t.shape && t.shape.points && t.shape.points.length){
NPos3d.recursivelyUpdateMatrices(t);
NPos3d.transformPoints(t, transformedPoints);
}
return transformedPoints;
},
getWorldPositionFunc: function(){
NPos3d.recursivelyUpdateMatrices(this);
return NPos3d.Maths.p3Mat4Mul([0,0,0], this.matrices.globalComposite);
},
renderFunc: function(){
//This function should be assigned to objects in the scene which will be rendered;
//Example: myObject.render = NPos3d.renderFunc;
var t = this; //should be referring to the object being rendered
t.updateMatrices(t, t.scene.viewMatrix);
if(!t.shape || !t.shape.points || !t.shape.points.length){
t.transformedPointCache.length = 0;
} else {
t.scene.updateTransformedPointCache(t);
//if there are no points, there is nothing to render with these methods!
if(t.transformedPointCache.length > 0){
if (t.renderStyle === 'both' || t.renderStyle === 'lines') {
if(
//I can't render a line if I don't have at least 2 points and 1 line.
t.transformedPointCache.length > 1 &&
t.shape.lines !== undefined &&
typeof t.shape.lines.length === 'number' &&
t.shape.lines.length > 0
) {
t.scene.drawLines(t);
}
}
if (t.renderStyle === 'both' || t.renderStyle === 'points') {
t.scene.drawPoints(t);
}
if(t.renderStyle !== 'both' && t.renderStyle !== 'lines' && t.renderStyle !== 'points') {
throw 'Invalid renderStyle specified: ' + t.renderStyle;
}
}
}
if(t.postRender){
t.postRender();
}
}
};
//Here lies almost anything related to trig/calc
NPos3d.Maths = {
pi: Math.PI,
tau: (Math.PI * 2),
deg: (Math.PI / 180),
sin: Math.sin,
cos: Math.cos,
square: function (num) {return num * num;},
//--------------------------------
//Some basic boundary / collission testing maths.
//--------------------------------
//I'm sure this function causes lag. Please use the 2D and 3D speciffic versions instead.
pointInNBounds: function (point, bounds) {
var d;
//Works for 2D, 3D, and nD! Please, please feed in bounds generated like the line below.
//var bounds = nGetBounds(pointList);
//d stands for dimension
for (d = 0; d < point.length; d += 1) {
//dimensional value check
if (point[d] < bounds[0][d] || point[d] > bounds[1][d]) {
return false;
}
}
return true;
},
pointIn2dBounds: function (point, bounds) {
//Works for 2D! Please, please feed in bounds generated like the line below.
//var bounds = nGetBounds(pointList);
//dimensional value check
if (
point[0] < bounds[0][0] || point[0] > bounds[1][0] ||
point[1] < bounds[0][1] || point[1] > bounds[1][1]
) {
return false;
}
return true;
},
pointIn3dBounds: function (point, bounds) {
//Works for 3D! Please, please feed in bounds generated like the line below.
//var bounds = nGetBounds(pointList);
//dimensional value check
if (
point[0] < bounds[0][0] || point[0] > bounds[1][0] ||
point[1] < bounds[0][1] || point[1] > bounds[1][1] ||
point[2] < bounds[0][2] || point[2] > bounds[1][2]
) {
return false;
}
return true;
},
//--------------------------------
//This is where all of the 3D and math happens
//--------------------------------
getSquareVecLength2D: function (x,y) {
return NPos3d.Maths.square(x) + NPos3d.Maths.square(y);
},
getVecLength2D: function (x,y) {
return Math.sqrt(NPos3d.Maths.getSquareVecLength2D(x,y));
},
getRelativeAngle3D: function (p3) { //DO NOT try to optimize out the use of Math.sqrt in this function!!!
var topAngle = Math.atan2(p3[1], p3[0]),
length = NPos3d.Maths.getVecLength2D(p3[0], p3[1]),
sideAngle = -Math.atan2(p3[2], length);
return [ 0, sideAngle, topAngle];
},
p3Add: function (a, b, outputPoint) {
var o = outputPoint || [];
o[0] = a[0] + b[0];
o[1] = a[1] + b[1];
o[2] = a[2] + b[2];
o[3] = a[3]; //preserve point color
return o;
},
p3Sub: function (a, b, outputPoint) {
var o = outputPoint || [];
o[0] = a[0] - b[0];
o[1] = a[1] - b[1];
o[2] = a[2] - b[2];
o[3] = a[3]; //preserve point color
return o;
},
pointAt: function (o, endPos) {
var m = NPos3d.Maths,
posDiff = m.p3Sub(endPos, o.gPos);
//works only for this rotOrder at the moment
if(o.rotOrder !== [2,1,0]){
o.rotOrder = [2,1,0];
}
o.rot = m.getRelativeAngle3D(posDiff);
},
__mat4Identity: [
1,0,0,0,
0,1,0,0,
0,0,1,0,
0,0,0,1
],
makeMat4: function() {
return this.__mat4Identity.slice();
},
mat3ToMat4Translation: [
[0,1,2],
[4,5,6],
[8,9,10]
],
rotOrders: {
/* i, j, k, parity */
'0,1,2':[0, 1, 2, 0], /* XYZ */
'0,2,1':[0, 2, 1, 1], /* XZY */
'1,0,2':[1, 0, 2, 1], /* YXZ */
'1,2,0':[1, 2, 0, 0], /* YZX */
'2,0,1':[2, 0, 1, 0], /* ZXY */
'2,1,0':[2, 1, 0, 1] /* ZYX */
},
//The below function mostly converted from Blender source (so much love for team Blender),
//plus a little redundancy reduction if no rotation or same as last rotation
//Original function was named: eulO_to_mat3 - Construct 3x3 matrix from Euler angles (in radians).
//http://projects.blender.org/scm/viewvc.php/trunk/blender/source/blender/blenlib/intern/math_rotation.c?view=markup&root=bf-blender
eulerToMat4: function(euler, order, outputMatrix) {
var m = this,
o = outputMatrix || m.makeMat4(),
M = m.mat3ToMat4Translation,
eulerString = euler.toString(),
orderString = order.toString(),
R = m.rotOrders[orderString],
i = R[0],
j = R[1],
k = R[2],
parity = R[3],
ti, tj, th, ci, cj, ch, si, sj, sh, cc, cs, sc, ss;
if(o.euler !== eulerString && o.rotOrder !== orderString) {
o.euler = eulerString;
o.order = orderString;
if(eulerString === '0,0,0'){ //if no rotation, do no work and just return an identity matrix
o[00] = 1,
o[01] = 0,
o[02] = 0,
o[04] = 0,
o[05] = 1,
o[06] = 0,
o[08] = 0,
o[09] = 0,
o[10] = 1;
} else {
//below here is all of the Blender magic
//ti, th, tj are all inverted for NPos3d purposes
if (parity) {
ti = euler[i];
tj = euler[j];
th = euler[k];
}
else {
ti = -euler[i];
tj = -euler[j];
th = -euler[k];
}
ci = m.cos(ti);
cj = m.cos(tj);
ch = m.cos(th);
si = m.sin(ti);
sj = m.sin(tj);
sh = m.sin(th);
cc = ci * ch;
cs = ci * sh;
sc = si * ch;
ss = si * sh;
o[M[i][i]] = cj * ch;
o[M[j][i]] = sj * sc - cs;
o[M[k][i]] = sj * cc + ss;
o[M[i][j]] = cj * sh;
o[M[j][j]] = sj * ss + cc;
o[M[k][j]] = sj * cs - sc;
o[M[i][k]] = -sj;
o[M[j][k]] = cj * si;
o[M[k][k]] = cj * ci;
}
}
return o;
},
mat4Set: function(a, b) { // essentially `a = b`
a[00] = b[00], a[01] = b[01], a[02] = b[02], a[03] = b[03],
a[04] = b[04], a[05] = b[05], a[06] = b[06], a[07] = b[07],
a[08] = b[08], a[09] = b[09], a[10] = b[10], a[11] = b[11],
a[12] = b[12], a[13] = b[13], a[14] = b[14], a[15] = b[15],
a.euler = b.euler, a.order = b.order;
return a;
},
mat4Mul: function(a, b, outputMatrix) {
var a00=a[00],a01=a[01],a02=a[02],a03=a[03],a04=a[04],a05=a[05],a06=a[06],a07=a[07],
a08=a[08],a09=a[09],a10=a[10],a11=a[11],a12=a[12],a13=a[13],a14=a[14],a15=a[15],
b00=b[00],b01=b[01],b02=b[02],b03=b[03],b04=b[04],b05=b[05],b06=b[06],b07=b[07],
b08=b[08],b09=b[09],b10=b[10],b11=b[11],b12=b[12],b13=b[13],b14=b[14],b15=b[15],
o = outputMatrix || [];
//performance testing this approach versus`o[00] = b[01] * a[01] ...`
//on 10 million points indicated that this is a lot faster
o[00] = b00 * a00 + b01 * a04 + b02 * a08 + b03 * a12,
o[01] = b00 * a01 + b01 * a05 + b02 * a09 + b03 * a13,
o[02] = b00 * a02 + b01 * a06 + b02 * a10 + b03 * a14,
o[03] = b00 * a03 + b01 * a07 + b02 * a11 + b03 * a15,
o[04] = b04 * a00 + b05 * a04 + b06 * a08 + b07 * a12,
o[05] = b04 * a01 + b05 * a05 + b06 * a09 + b07 * a13,
o[06] = b04 * a02 + b05 * a06 + b06 * a10 + b07 * a14,
o[07] = b04 * a03 + b05 * a07 + b06 * a11 + b07 * a15,
o[08] = b08 * a00 + b09 * a04 + b10 * a08 + b11 * a12,
o[09] = b08 * a01 + b09 * a05 + b10 * a09 + b11 * a13,
o[10] = b08 * a02 + b09 * a06 + b10 * a10 + b11 * a14,
o[11] = b08 * a03 + b09 * a07 + b10 * a11 + b11 * a15,
o[12] = b12 * a00 + b13 * a04 + b14 * a08 + b15 * a12,
o[13] = b12 * a01 + b13 * a05 + b14 * a09 + b15 * a13,
o[14] = b12 * a02 + b13 * a06 + b14 * a10 + b15 * a14,
o[15] = b12 * a03 + b13 * a07 + b14 * a11 + b15 * a15;
return o;
},
mat4P3Translate: function (m, v, outputMatrix) {
var o = outputMatrix || this.makeMat4(),
aX = m[03],
aY = m[07],
aZ = m[11],
x = v[0],
y = v[1],
z = v[2];
o[03] = aX + x,
o[07] = aY + y,
o[11] = aZ + z;
return o;
},
mat4P3Scale: function (m, v, outputMatrix) { //scales both rotation and translation
var o = outputMatrix || this.makeMat4(),
x = v[0],
y = v[1],
z = v[2],
a00 = m[00],
a01 = m[01],
a02 = m[02],
a03 = m[03],
a04 = m[04],
a05 = m[05],
a06 = m[06],
a07 = m[07],
a08 = m[08],
a09 = m[09],
a10 = m[10],
a11 = m[11];
o[00] = a00 * x,
o[01] = a01 * x,
o[02] = a02 * x,
o[03] = a03 * x,
o[04] = a04 * y,
o[05] = a05 * y,
o[06] = a06 * y,
o[07] = a07 * y,
o[08] = a08 * z,
o[09] = a09 * z,
o[10] = a10 * z,
o[11] = a11 * z;
return o;
},
p3Mat4Mul: function(v, m, outputPoint) {
var o = outputPoint || [],
x = v[0],
y = v[1],
z = v[2],
color = v[3] || false; //Point Color Preservation - no need to offset or rotate it
o[0] = (x * m[00]) + (y * m[01]) + (z * m[02]) + m[03];
o[1] = (x * m[04]) + (y * m[05]) + (z * m[06]) + m[07];
o[2] = (x * m[08]) + (y * m[09]) + (z * m[10]) + m[11];
//o[3] = (x * m[12]) + (y * m[13]) + (z * m[14]) + (w * m[15]);
o[3] = color;
return o;
},
__matrix: false,
//Probably performance hazardous but Human friendly - use only sparingly.
p3Rotate: function (p3, rot, order){
var m = NPos3d.Maths;
if(!m.__matrix){
m.__matrix = m.makeMat4();
}
m.eulerToMat4(rot, order, m.__matrix);
return m.p3Mat4Mul(p3, m.__matrix);
},
getP3Scaled: function (p3,scale) {
//return p3;
return [p3[0]*scale[0], p3[1]*scale[1], p3[2]*scale[2]];
},
getP2Offset: function (p2,offset) {
//an efficient hack to quickly add an offset to a 2D point
return [p2[0]+offset[0], p2[1]+offset[1]];
},
getP3String: function (p3) {
return 'x: '+p3[0]+' y: '+p3[1]+' z: '+p3[2];
},
nGetBounds: function (pointList) {
//Works for 2D, 3D, and nD!
if(pointList.length < 1){return [[0,0,0],[0,0,0]];} //assume 3D if empty
var min = [];
var max = [];
var p = pointList[0];
for (var d = 0; d < p.length; d += 1) {
min[d] = p[d]; max[d] = p[d];
}
for (var i = 1; i < pointList.length; i += 1) {
var p = pointList[i];
//d stands for dimension
for (var d = 0; d < p.length; d += 1) {
if (p[d] < min[d]) {min[d] = p[d];}
else if (p[d] > max[d]) {max[d] = p[d];}
}
}
return [min,max];
},
makeBBCubeFromTwoPoints: function (bbMinOffset,bbMaxOffset) {
return [
[bbMinOffset[0],bbMinOffset[1],bbMaxOffset[2]],
[bbMaxOffset[0],bbMinOffset[1],bbMaxOffset[2]],
[bbMaxOffset[0],bbMaxOffset[1],bbMaxOffset[2]],
[bbMinOffset[0],bbMaxOffset[1],bbMaxOffset[2]],
[bbMinOffset[0],bbMinOffset[1],bbMinOffset[2]],
[bbMaxOffset[0],bbMinOffset[1],bbMinOffset[2]],
[bbMaxOffset[0],bbMaxOffset[1],bbMinOffset[2]],
[bbMinOffset[0],bbMaxOffset[1],bbMinOffset[2]]
];
}
};
NPos3d.Utils = {
subset: function (ob, string) {
var output = {}, propList = string.split(','), i;
for (i = 0; i < propList.length; i += 1) {
output[propList[i]] = ob[propList[i]];
}
return output;
},
initVal: function () { //A function designed to compensate for lack of function (value = default)
var i;
if (arguments.length < 1) {
throw 'ur doin it wrong. initVal function requires > 1 arguments';
}
for (i = 0; i < arguments.length; i += 1) {
if (arguments[i] !== undefined && arguments[i] !== null) {
return arguments[i];
}
}
return arguments[i];
},
get_type: function (input) {
if (input === null) {
return "[object Null]"; // special case
}
return Object.prototype.toString.call(input);
},
displayDebug: function (input) {
var u = NPos3d.Utils, output = [], keyName;
if (u.get_type(input).match(/Number/i)) {
output.push(input + '
');
} else {
output.push(input.constructor.name + '
');
}
for (keyName in input) {
if (input.hasOwnProperty(keyName)) {
output.push(keyName.toString() + ': ' + u.get_type(input[keyName]) + ' - ' + input[keyName] + '
');
}
}
if (!u.display) {
u.display = document.createElement('pre');
u.display.style.display = 'block';
u.display.style.position = 'fixed';
u.display.style.top = 0;
u.display.style.left = 0;
u.display.style.zIndex = 9001;
u.display.style.fontFamily = 'monospace';
u.display.style.fontSize = '10px';
u.display.style.lineHeight = '7px';
u.display.style.whiteSpace = 'pre-wrap';
u.display.style.color = 'hsl(' + Math.round(Math.random() * 360) + ',100%,50%)';
}
document.body.appendChild(u.display);
u.display.innerHTML += output.join("\n");
},
clearDebug: function () {
var u = NPos3d.Utils;
if(u.display){
u.display.innerHTML = '';
if(u.display.parentNode === document.body){
document.body.removeChild(u.display);
}
}
}
};
NPos3d.Scene = function (args) {
var t = this, type = 'Scene';
if(t.type !== type){throw 'You must use the `new` keyword when invoking the ' + type + ' constructor.';}
args = args || {};
t.debugViewport = args.debugViewport || false;
t.mpos = {x: 0,y: 0};
t.frameRate = args.frameRate || 30;
t.lastFrameRate = t.frameRate;
t.pixelScale = args.pixelScale || 1;
t.globalCompositeOperation = args.globalCompositeOperation || 'source-over';
t.backgroundColor = args.backgroundColor || 'transparent';
t.strokeStyle = args.strokeStyle || '#fff';
t.fillStyle = args.fillStyle || '#fff';
t.lineWidth = args.lineWidth || undefined;
t.fullScreen = !!(args.fullScreen === undefined || args.fullScreen === true);
t.forceRealPixels = !!(args.forceRealPixels === undefined || args.forceRealPixels === true);
t.oldAndroid = /android 2/i.test(navigator.userAgent);
t.mobileSafari = /iphone|ipad|ipod/i.test(navigator.userAgent);
t.isMobile = t.oldAndroid || t.mobileSafari || /android|blackberry|mini|windows\sce|palm/i.test(navigator.userAgent);
t.isChrome = /Chrome/i.test(navigator.userAgent);
t.newChromeMobile = t.isMobile && t.isChrome && parseInt(navigator.userAgent.match(/Chrome\/(\d*)/i)[1]) > 18;
t.mobileFireFox = t.isMobile && /firefox/i.test(navigator.userAgent);
t.useOuterWidth = t.oldAndroid || t.mobileFireFox;
t.canvasId = args.canvasId || 'canvas';
t.existingCanvas = args.canvas !== undefined;
t.canvas = args.canvas || document.createElement('canvas');
t.canvas.style.backgroundColor = args.canvasStyleColor || '#000';
t.canvas.id = t.canvasId;
t.c = t.canvas.getContext('2d');
if (args.canvas === undefined) {
document.body.appendChild(t.canvas);
}
if (t.fullScreen) {
t.canvas.parentNode.style.margin = 0;
t.canvas.parentNode.style.padding = 0;
t.canvas.style.display = 'block';
t.canvas.style.position = 'fixed';
t.canvas.style.top = 0;
t.canvas.style.left = 0;
if(args.zIndex !== undefined){
t.canvas.style.zIndex = args.zIndex;
}
if(
window.orientation !== undefined &&
window.outerWidth === 0
) { //it's broken because it's an iOS device
t.checkWindow = function () {
var scaleMultiplier = t.forceRealPixels ? window.devicePixelRatio : 1,
actualWidth = window.screen.width;
if(window.orientation !== 0){
actualWidth = window.screen.height;
}
t.w = Math.ceil(scaleMultiplier * actualWidth / t.pixelScale);
t.h = Math.ceil(window.innerHeight / t.pixelScale);
};
} else if (t.useOuterWidth) {
t.checkWindow = function () {
t.w = Math.ceil(window.outerWidth / t.pixelScale);
t.h = Math.ceil(window.outerHeight / t.pixelScale);
};
} else {
t.checkWindow = function () {
t.w = Math.ceil(window.innerWidth / t.pixelScale);
t.h = Math.ceil(window.innerHeight / t.pixelScale);
};
}
} else {
t.checkWindow = function () {
t.w = Math.ceil(t.canvas.width / t.pixelScale);
t.h = Math.ceil(t.canvas.height / t.pixelScale);
};
if (t.canvas.width == '') { t.canvas.width = args.width || 512; }
if (t.canvas.height == '') { t.canvas.height = args.height || 384; }
}
if (t.pixelScale !== 1) {
t.canvas.style.imageRendering = '-moz-crisp-edges';
t.canvas.style.imageRendering = '-webkit-optimize-contrast';
//reference: http://stackoverflow.com/questions/10525107/html5-canvas-image-scaling-issue
t.c.imageSmoothingEnabled = false;
t.c.mozImageSmoothingEnabled = false;
t.c.webkitImageSmoothingEnabled = false;
}
if (!t.isMobile && !t.existingCanvas) {
t.canvas.style.position = 'fixed';
}
//console.log(isMobile);
t.checkWindow();
t.resize();
t.camera = args.camera || new NPos3d.Camera({scene: t});
t.canvas.style.backgroundColor = t.canvasStyleColor;
t.cursorPosition = args.canvas !== undefined ? 'absolute' : 'relative';
t.mouseHandler = function (e) {
var canvasOffsetX = 0,
canvasOffsetY = 0,
pointX = 0,
pointY = 0;
if(e.target === t.canvas || e.target === window){
e.preventDefault();
}
if (!t.fullScreen) {
var offset = t.canvas.getBoundingClientRect();
canvasOffsetX = offset.left;
canvasOffsetY = offset.top;
}
if (e.touches && e.touches.length) {
pointX = e.touches[0].clientX;
pointY = e.touches[0].clientY;
} else {
pointX = e.clientX;
pointY = e.clientY;
}
if(t.cursorPosition === 'absolute'){
pointX -= canvasOffsetX;
pointY -= canvasOffsetY;
}
t.mpos.x = Math.ceil((pointX / t.pixelScale) - t.cx);
t.mpos.y = Math.ceil((pointY / t.pixelScale) - t.cy);
//console.log(t.mpos.x, t.mpos.y);
};
window.addEventListener('mousemove',t.mouseHandler,false);
window.addEventListener('touchstart',t.mouseHandler,false);
window.addEventListener('touchmove',t.mouseHandler,false);
//window.addEventListener('touchend',t.mouseHandler,false);
t.children = [];
t.renderInstructionList = [];
t.add(t.camera);
t.start();
t.globalize();
return this;
};
NPos3d.Scene.prototype = {
type: 'Scene',
isScene: true,
globalize: function () {
//Because it's a pain to have to reference too much. I'll unpack my tools so I can get to work.
window.pi = NPos3d.Maths.pi;
window.tau = NPos3d.Maths.tau;
window.deg = NPos3d.Maths.deg;
window.sin = NPos3d.Maths.sin;
window.cos = NPos3d.Maths.cos;
window.square = NPos3d.Maths.square;
},
resize: function () {
var t = this;
t.cx = Math.floor(t.w/2);
t.cy = Math.floor(t.h/2);
t.mpos.x = 0;
t.mpos.y = 0;
t.canvas.width = t.w;
t.canvas.height = t.h;
if (t.pixelScale !== 1) {
t.canvas.style.width = Math.ceil(t.w * t.pixelScale) + 'px';
t.canvas.style.height = Math.ceil(t.h * t.pixelScale) + 'px';
} else {
t.canvas.style.width = t.w + 'px';
t.canvas.style.height = t.h + 'px';
}
t.lw = t.w;
t.lh = t.h;
if(t.isMobile ){
t.handleViewportShenanigans();
}
},
handleViewportShenanigans: function(){
//Normally, something like this should never exist under any reasonable circumstances,
//but mobile browsers refuse to allow me to display pages pixel-per-pixel in any sane way.
//This does 3 things -
// 1: Make the canvas very, very large, which kills performance
// 2: Make the render output SUCK
// 3: HULK SMASH!!!
var t = this,
meta = document.getElementById('vp'),
viewportContentString;
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('name','viewport');
meta.setAttribute('id','vp');
}
if (meta && meta.parentNode === document.head) {
document.head.removeChild(meta);
}
if(t.forceRealPixels){
viewportContentString = t.getViewportForceRealPixelsString();
} else {
viewportContentString = t.getCommonViewportWidth() + t.commonViewportProperties;
}
meta.setAttribute('content', viewportContentString);
document.head.appendChild(meta);
},
getCommonViewportWidth: function(){
var t = this,
result = 'width=device-width';
if(t.mobileSafari){
result = 'width=' + t.w;
}
return result;
},
commonViewportProperties: ', initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no',
getViewportForceRealPixelsString: function(){
var t = this,
result = '',
ratio, repairedRatioString;
if(t.mobileFireFox){
result = 'width=' + t.w + ', user-scalable=no, target-densityDpi=device-dpi';
} else if(t.mobileSafari || t.newChromeMobile) {
ratio = 1 / (window.devicePixelRatio || 1);
repairedRatioString = ', initial-scale=' + ratio + ', minimum-scale=' + ratio + ', maximum-scale=' + ratio + ', user-scalable=no';
result = t.getCommonViewportWidth() + repairedRatioString;
} else {
result = 'width=device-width, target-densityDpi=device-dpi' + t.commonViewportProperties;
}
return result;
},
updateRecursively: function updateRecursively(o){
if(!o.isScene){
o.update();
}
if(o.children){
o.children.forEach(updateRecursively);
}
},
renderRecursively: function renderRecursively(o){
if(!o.isScene && o.render){
o.render();
}
if(o.children){
o.children.forEach(renderRecursively);
}
},
removeExpiredChildrenRecursively: function rECR(o){
var len, i, child;
if(o.children){
len = o.children.length;
for (i = len - 1; i >= 0; i -= 1) {
child = o.children[i];
rECR(child);
if(child.expired){
o.children.splice(i,1);
child.expired = false;
child.parent = false;
child.scene = false;
}
}
}
},
addNewChildrenRecursively: function addNewChildrenRecursively(o){
var i, child, newChild, scene = false;
if(o.children !== undefined && o.children.length !== undefined && o.children.length > 0){
for (i = 0; i < o.children.length; i += 1) {
child = o.children[i];
addNewChildrenRecursively(child);
}
}
if (o.isScene === true) {
scene = o;
} else {
scene = o.scene;
}
//This bit is nifty: thanks to scene = false at init,
//if a node is parented to another node with no scene,
//then all child nodes have their scene removed as well.
if(o.childrenToBeAdded !== undefined && o.childrenToBeAdded.length !== undefined && o.childrenToBeAdded.length > 0){
for (i = 0; i < o.childrenToBeAdded.length; i += 1) {
newChild = o.childrenToBeAdded[i];
if(newChild.expired) {
newChild.expired = false;
}
else {
o.children.push(newChild);
newChild.parent = o;
newChild.scene = scene;
if(newChild.onAdd !== undefined){
newChild.onAdd();
}
}
}
delete o.childrenToBeAdded;
}
},
render: function(){
var t = this, i, len, instruction;
if(t.renderInstructionList !== undefined && t.renderInstructionList.length > 0){
len = t.renderInstructionList.length;
for(i = 0; i < len; i += 1){
instruction = t.renderInstructionList[i];
instruction.method(t.c,instruction.args);
}
}
},
update: function () {
var t = this, i, len = t.children.length, child, u = NPos3d.Utils;
try{
t.checkWindow();
if (t.w !== t.lw || t.h !== t.lh) {t.resize();}
if (t.debugViewport) {
var newSize = u.subset(window,'innerHeight,innerWidth,outerWidth,outerHeight,devicePixelRatio');
newSize.screenSizeWidth = window.screen.width;
newSize.screenSizeHeight = window.screen.height;
newSize.documentElementClientWidth = document.documentElement.clientWidth;
newSize.documentElementClientHeight = document.documentElement.clientHeight;
newSize.devicePixelRatio = window.devicePixelRatio || 1;
newSize.navigator = navigator.userAgent;
u.clearDebug();
u.displayDebug(newSize);
}
t.renderInstructionList = []; //the render methods on each object are supposed to populate this array
t.addNewChildrenRecursively(t);
t.updateRecursively(t);
t.viewMatrix = t.inverseMatrix(t.camera);
t.renderRecursively(t);
t.removeExpiredChildrenRecursively(t);
if(t.backgroundColor === 'transparent'){
t.c.clearRect(0,0,t.w,t.h);
}else{
t.c.fillStyle = t.backgroundColor;
t.c.fillRect(0,0,t.w,t.h);
}
t.c.save();
t.c.globalCompositeOperation = t.globalCompositeOperation;
t.c.translate(t.cx, t.cy);
t.renderInstructionList.sort(t.sortRenderInstructionByZDepth);
t.render();
t.c.restore();
} catch(e){
t.stop();
console.log(e, e.stack, t);
}
},
start: function () {
var t = this;
t.interval = setInterval(
function () {
t.update();
if(t.frameRate !== t.lastFrameRate){ //automatically updates frameRate if updated mid-usage
t.stop();
t.lastFrameRate = t.frameRate;
t.start();
}
},
1000 / t.frameRate
);
},
stop: function () {
clearInterval(this.interval);
},
sortRenderInstructionByZDepth: function (a,b) {return a.z - b.z;},
recurseForInheritedProperties:function rfip(o, propName){
if(o[propName] !== undefined){
return o[propName];
}
if(o.parent){
return rfip(o.parent, propName);
}
},
drawLine: function(c,o){
c.beginPath();
c.moveTo(o.a.x,o.a.y);
c.lineTo(o.b.x,o.b.y);
if(c.strokeStyle !== o.color){c.strokeStyle = o.color;}
if(c.lineWidth !== o.lineWidth){c.lineWidth = o.lineWidth;}
if(c.lineCap !== 'round'){c.lineCap = 'round';}
c.stroke();
},
drawCircle: function(c,o){
var scale = o.pos.scale * o.pointScale;
if(scale >= 0){
c.moveTo(o.pos.x, o.pos.y);
c.beginPath();
c.arc(o.pos.x,o.pos.y,scale,0,tau,false);
if (o.pointStyle === 'fill') {
if(c.fillStyle !== o.color){c.fillStyle = o.color}
c.fill();
}else if (o.pointStyle === 'stroke') {
if(c.strokeStyle !== o.color){c.strokeStyle = o.color;}
if(c.lineWidth !== o.lineWidth){c.lineWidth = o.lineWidth;}
if(c.lineCap !== 'round'){c.lineCap = 'round';}
c.stroke();
}
}
},
inverseMatrix: function(o) {
var m = NPos3d.Maths;
var resultMatrix = m.makeMat4();
do {
var rotationMatrix = m.makeMat4();
m.eulerToMat4(
o.rot.map(function(n){
return -n
}),
o.rotOrder.slice().reverse(),
rotationMatrix
);
m.mat4P3Translate(resultMatrix, [-o.pos[0], -o.pos[1], -o.pos[2]], resultMatrix);
m.mat4Mul(resultMatrix, rotationMatrix, resultMatrix);
m.mat4P3Scale(resultMatrix, o.scale.map(function(n){
return -n
}));
o = o.parent;
} while(o && !o.isScene);
return resultMatrix;
},
updateTransformedPointCache: function (o){
var t = this, m = NPos3d.Maths, i, currentGlobalCompositeMatrixString;
if(o.transformedPointCache.length !== o.shape.points.length){
o.transformedPointCache.length = 0; //empty the array, keep the object reference
o.lastGlobalCompositeMatrixString = false;
}
currentGlobalCompositeMatrixString = o.matrices.globalComposite.toString();
if (!o.lastGlobalCompositeMatrixString || o.lastGlobalCompositeMatrixString !== currentGlobalCompositeMatrixString) {
NPos3d.transformPoints(o, o.transformedPointCache);
o.boundingBox = m.nGetBounds(o.transformedPointCache);
o.lastGlobalCompositeMatrixString = currentGlobalCompositeMatrixString;
}
},
lineRenderLoop: function (o) {
var t = this, m = NPos3d.Maths,
i, p3a, p3b;
for (i = 0; i < o.shape.lines.length; i += 1) {
//offset the points by the object's position
p3a = o.transformedPointCache[o.shape.lines[i][0]];
p3b = o.transformedPointCache[o.shape.lines[i][1]];
//if the depths of the first and second point in the line are not behind the camera...
//and the depths of the first and second point in the line are closer than the far plane...
if (
p3a[2] < t.camera.clipNear &&
p3b[2] < t.camera.clipNear &&
p3a[2] > t.camera.clipFar &&
p3b[2] > t.camera.clipFar
) {
var p0 = t.camera.project3Dto2D(p3a);
var p1 = t.camera.project3Dto2D(p3b);
// min max
var screenBounds = [[-t.cx, -t.cy],[t.cx, t.cy]];
var p0InBounds = m.pointIn2dBounds([p0.x,p0.y],screenBounds);
var p1InBounds = m.pointIn2dBounds([p1.x,p1.y],screenBounds);
//If the line is completely off screen, do not bother rendering it.
if (p0InBounds || p1InBounds) {
t.renderInstructionList.push({
method: t.drawLine,
args: {
a: p0,
b: p1,
color: o.shape.lines[i][2] || o.shape.color || o.color || t.strokeStyle,
lineWidth: o.lineWidth || o.parent.lineWidth || t.lineWidth || 1
},
z: Math.max(p3a[2], p3b[2])
});
}
}
}
},
drawLines: function (o) {
var t = this, m = NPos3d.Maths, i, bbCube, bbMinOffset, bbMaxOffset, bbOffScreen, bbp;
if (o.renderAlways) {
t.lineRenderLoop(o);
return;
}
bbMinOffset = o.boundingBox[0];
bbMaxOffset = o.boundingBox[1];
//Checking to see if any part of the bounding box is in front on the camera and closer than the far plane before bothering to do anything else...
if (bbMaxOffset[2] > t.camera.clipFar && bbMinOffset[2] < t.camera.clipNear && bbMaxOffset[2] > t.camera.clipFar && bbMaxOffset[2] < t.camera.clipNear) {
//Alright. It's in front and not behind. Now is the bounding box even partially on screen?
//8 points determine the cube... let's start from the top left, spiraling down clockwise
bbCube = m.makeBBCubeFromTwoPoints(bbMinOffset,bbMaxOffset);
bbOffScreen = true;
//At some point in the future if I wanted to get really crazy, I could probably determine which order
//to sort the array above to orient the point closest to the center of the screen nearest the first of the list,
//so I don't bother checking all 8 points to determine if it's on screen - or even off screen.
for (i = 0; i < bbCube.length && bbOffScreen; i += 1) {
bbp = t.camera.project3Dto2D(bbCube[i]);
if (bbp.x < t.cx && bbp.x > -t.cx && bbp.y < t.cy && bbp.y > -t.cy) {
bbOffScreen = false;
}
}
if (!bbOffScreen) {
t.lineRenderLoop(o);
}
}
},
drawPoints: function (o) {
var t = this, m = NPos3d.Maths, i, bbMinOffset, bbMaxOffset, bbCube, bbOffScreen, bbp;
if (o.renderAlways) {
t.pointRenderLoop(o);
return;
}
bbMinOffset = o.boundingBox[0];
bbMaxOffset = o.boundingBox[1];
//Checking to see if any part of the bounding box is in front on the camera and closer than the far plane before bothering to do anything else...
if (
bbMaxOffset[2] > t.camera.clipFar &&
bbMinOffset[2] < t.camera.clipNear &&
bbMaxOffset[2] > t.camera.clipFar &&
bbMaxOffset[2] < t.camera.clipNear
) {
//Alright. It's in front and not behind. Now is the bounding box even partially on screen?
//8 points determine the cube... let's start from the top left, spiraling down clockwise
bbCube = m.makeBBCubeFromTwoPoints(bbMinOffset,bbMaxOffset);
bbOffScreen = true;
//At some point in the future if I wanted to get really crazy, I could probably determine which order
//to sort the array above to orient the point closest to the center of the screen nearest the first of the list,
//so I don't bother checking all 8 points to determine if it's on screen - or even off screen.
for (i = 0; i < bbCube.length && bbOffScreen; i += 1) {
bbp = t.camera.project3Dto2D(bbCube[i]);
if (bbp.x < t.cx && bbp.x > -t.cx && bbp.y < t.cy && bbp.y > -t.cy) {
bbOffScreen = false;
}
}
if (!bbOffScreen) {
t.pointRenderLoop(o);
}
}
},
pointRenderLoop: function (o) {
var t = this, m = NPos3d.Maths,
i, p3a, p0, screenBounds, circleArgs;
for (i = 0; i < o.transformedPointCache.length; i += 1) {
//offset the points by the object's position
p3a = o.transformedPointCache[i];
//if the depth of the point is not behind the camera...
//and the depth of the point is closer than the far plane...
if (p3a[2] < t.camera.clipNear && p3a[2] > t.camera.clipFar) {
p0 = t.camera.project3Dto2D(p3a);
// min max
screenBounds = [[-t.cx, -t.cy],[t.cx, t.cy]];
var p0InBounds = m.pointIn2dBounds([p0.x,p0.y],screenBounds);
//If the line is completely off screen, do not bother rendering it.
if (p0InBounds) {
//console.log(p0.color);
circleArgs = {
pos: p0,
pointScale: o.pointScale,
pointStyle: o.pointStyle
};
if (o.pointStyle === 'fill') {
circleArgs.color = p0.color || o.shape.color || o.color || t.fillStyle;
}else if (o.pointStyle === 'stroke') {
circleArgs.color = p0.color || o.shape.color || o.color || t.strokeStyle;
circleArgs.lineWidth = o.lineWidth || o.scene.lineWidth || 1;
}
t.renderInstructionList.push({
method: t.drawCircle,
args: circleArgs,
z: p3a[2]
});
}
}
}
},
add: NPos3d.addFunc,
remove: NPos3d.removeFunc
};
NPos3d.Camera = function (args) {
var t = this, type = 'Camera';
args = args || {};
if(t.type !== type){throw 'You must use the `new` keyword when invoking the ' + type + ' constructor.';}
if(!args.scene){throw 'You must provide a `scene` property when invoking the ' + type + ' constructor.';}
NPos3d.blessWith3DBase(t, args);
//Field Of View; Important!
t.clipNear = args.clipNear || -0.01;
t.clipFar = args.clipFar || -9001;
t.frustumMultiplier = args.frustumMultiplier || 0.75;
};
NPos3d.Camera.prototype = {
type: 'Camera',
update: function(){
var t = this;
t.pos[2] = Math.max(t.scene.w, t.scene.h) * t.frustumMultiplier;
// RECIPROCAL width / height of the frustum at ONE unit away from the camera
// this arranges it so that it is exactly the right number of pixels where z=0, given where the camera is now
},
project3Dto2D: function (p3) {
var t = this,
canvasDim = Math.max(t.scene.w, t.scene.h),
scale = 1 / -p3[2],
p2 = {
x: (p3[0] * canvasDim * t.frustumMultiplier * scale),
y: (p3[1] * canvasDim * t.frustumMultiplier * scale),
scale: canvasDim * t.frustumMultiplier * scale,
color: p3[3] || false
};
return p2;
}
};
NPos3d.Geom = {
//The only reason this isn't with the rest of the shapes is because I need to use it inside the prototype of ob3D
cube: {
points: [
[ 10, 10, 10],
[ 10, 10,-10],
[ 10,-10, 10],
[ 10,-10,-10],
[-10, 10, 10],
[-10, 10,-10],
[-10,-10, 10],
[-10,-10,-10]
],
lines: [[0,1],[2,3],[4,5],[6,7],[3,1],[2,0],[7,5],[6,4],[5,1],[7,3],[4,0],[6,2]]
},
axies: {
points: [
[ 4, 0, 0,'#f00'],
[32, 0, 0,'#f00'],
[22, 6,-6,'#f00'],
[22,-6, 6,'#f00'],
[ 0, 4, 0,'#0f0'],
[ 0,32, 0,'#0f0'],
[ 6,22,-6,'#0f0'],
[-6,22, 6,'#0f0'],
[ 0, 0, 4,'#00f'],
[ 0, 0,32,'#00f'],
[-6, 6,22,'#00f'],
[ 6,-6,22,'#00f']
],
lines: [
[0,1,'#f00'],
[1,2,'#f00'],
[1,3,'#f00'],
[4,5,'#0f0'],
[5,6,'#0f0'],
[5,7,'#0f0'],
[8,9,'#00f'],
[9,10,'#00f'],
[9,11,'#00f']
]
}
};
NPos3d.blessWith3DBase = function (o,args) {
var m = NPos3d.Maths;
o.pos = args.pos || [0,0,0];
o.rot = args.rot || [0,0,0];
o.rotOrder = args.rotOrder || o.rotOrder || [0,1,2];
o.scale = args.scale || o.scale || [1,1,1];
o.gPos = o.pos.slice(); //global position
o.gScale = o.scale.slice(); //global scale
o.matrices = {
localScale: m.makeMat4(),
localRotation: m.makeMat4(),
localComposite: m.makeMat4(),
globalComposite: m.makeMat4()
};
o.lastGlobalCompositeMatrixString = false;
o.transformedPointCache = [];
o.boundingBox = [[0,0,0],[0,0,0]];
o.shape = args.shape || o.shape;
o.color = args.color || o.color ||undefined;
o.renderAlways = args.renderAlways || o.renderAlways || false;
o.renderStyle = args.renderStyle || o.renderStyle || 'lines';//points, both
o.pointScale = args.pointScale || o.pointScale || 2;
o.pointStyle = args.pointStyle || o.pointStyle || 'fill';//stroke
o.lineWidth = args.lineWidth || undefined;
o.scene = args.scene;
o.expired = false;
o.add = NPos3d.addFunc;
o.remove = NPos3d.removeFunc;
o.destroy = NPos3d.destroyFunc;
o.render = NPos3d.renderFunc;
o.getTransformedPoints = NPos3d.getTransformedPointsFunc;
o.getWorldPosition = NPos3d.getWorldPositionFunc;
o.updateMatrices = NPos3d.updateMatricesFunc;
};
NPos3d.Ob3D = function (args) {
var t = this, type = 'Ob3D';
if(t.type !== type){throw 'You must use the `new` keyword when invoking the ' + type + ' constructor.';}
args = args || {};
NPos3d.blessWith3DBase(t,args);
return this;
};
NPos3d.Ob3D.prototype = {
type: 'Ob3D',
shape: NPos3d.Geom.cube,
update: function () {}
};