var subset = function(ob, string){ output = {}; var propList = string.split(','); for(var i = 0; i < propList.length; i += 1){ output[propList[i]] = ob[propList[i]]; } return output; } var get_type = function(input){ if(input===null)return "[object Null]"; // special case return Object.prototype.toString.call(input); } var debug = false; var displayDebug = function(input,ownProperty){ if(get_type(input).match(/Number/i)){ var output = input + '
\n'; }else{ var output = input.constructor.name + '
\n'; } for(var i in input){ if(ownProperty === undefined || input.hasOwnProperty(i)){ output += i.toString() + ':' + get_type(input[i]) + ' - ' + input[i] + '
\n'; } } if(!debug){ debug = document.createElement('pre'); debug.style.display='block'; debug.style.position='fixed'; debug.style.top=0; debug.style.left=0; debug.style.zIndex=9001; debug.style.fontFamily='monospace'; debug.style.fontSize='10px'; debug.style.lineHeight='7px'; debug.style.color='hsl(' + (Math.random() * 360) + ',100%,50%)'; document.body.appendChild(debug); } debug.innerHTML += output; } var clearDebug = function(){ debug.innerHTML = ''; } var NPos2d = NPos2d || { 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){ //Works for 2D, 3D, and nD! Please, please feed in bounds generated like the line below. //var bounds = get2DBounds(pointList); //d stands for dimention for(var d = 0; d < point.length; d += 1){ //dimentional 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 = get2DBounds(pointList); //dimentional value check if( point.x < bounds[0].x || point.x > bounds[1].x || point.y < bounds[0].y || point.y > bounds[1].y ){ return false; } return true; }, }; NPos2d.Scene = function(args){ var t = this; if(t===window){throw 'You must use the `new` keyword when calling a Constructor Method!';} var args = args || {}; t.debug = args.debug || false; // mostly for mobile window resolution testing for now t.mpos = new Vec2(); t.mpos.down = false; // for consistency, so it doesn't start out as `undefined` t.camera = new NPos2d.Camera(); t.frameRate = args.frameRate || 30; t.pixelScale = args.pixelScale || 1; t.globalCompositeOperation = args.globalCompositeOperation || 'source-over'; t.backgroundColor = args.backgroundColor || 'transparent'; var isMobile = (/iphone|ipad|ipod|android|blackberry|mini|windows\sce|palm/i.test(navigator.userAgent.toLowerCase())); if(isMobile){ 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); }; } t.canvasId = args.canvasId || 'canvas'; t.canvas = document.createElement('canvas'); t.canvas.id = t.canvasId; document.body.appendChild(t.canvas); 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; t.canvas.style.zIndex=-10; if(t.pixelscale !== 1){ t.canvas.style.imageRendering = '-moz-crisp-edges'; t.canvas.style.imageRendering = '-webkit-optimize-contrast'; } t.lineWidth = args.lineWidth || undefined; t.checkWindow(); t.resize(); //t.canvas.style.width= t.w + 'px'; //t.canvas.style.height= t.h + 'px'; t.canvas.style.backgroundColor='#000'; t.c = t.canvas.getContext('2d'); t.mouseHandler = function(e){ //console.dir(e); if(e.target instanceof HTMLInputElement){ //Repairs all input element interaction? }else{ e.preventDefault(); } if((e.type === 'touchstart' || e.type === 'touchmove') && e.touches && e.touches.length){ //t.mpos.x = e.touches[0].screenX - t.cx; //t.mpos.y = e.touches[0].screenY - t.cy; t.mpos.x=Math.ceil((e.touches[0].screenX / t.pixelScale) - t.cx); t.mpos.y=Math.ceil((e.touches[0].screenY / t.pixelScale) - t.cy); t.mpos.down = true; }else{ //t.mpos.x = e.pageX - t.cx; //t.mpos.y = e.pageY - t.cy; t.mpos.x=Math.ceil((e.pageX / t.pixelScale) - t.cx); t.mpos.y=Math.ceil((e.pageY / t.pixelScale) - t.cy); } } t.clickHandler = function(e){ //console.log(e); if(e.target instanceof HTMLInputElement){ //Repairs all input element interaction? }else{ e.preventDefault(); } if(e.type === 'mousedown'){ t.mpos.down = true; }else{ t.mpos.down = false; t.mpos.hitting = undefined; //in case I want to store the object it's hitting, I clear that on release. } } window.addEventListener('touchstart',t.mouseHandler,false); window.addEventListener('touchmove',t.mouseHandler,false); window.addEventListener('touchend',t.clickHandler,false); window.addEventListener('mousemove',t.mouseHandler,false); window.addEventListener('mousedown',t.clickHandler,false); window.addEventListener('mouseup',t.clickHandler,false); //console.log(window.innerHeight, window.outerHeight); t.rQ = [];//RenderQueue t.cro = 0;//CurrentlyRenderingObject t.start(); t.globalize(); return this; } NPos2d.Scene.prototype={ 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 = NPos2d.pi; window.tau = NPos2d.tau; window.deg = NPos2d.deg; window.sin = NPos2d.sin; window.cos = NPos2d.cos; window.square = NPos2d.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; t.canvas.style.width = t.w*t.pixelScale + 'px'; t.canvas.style.height = t.h*t.pixelScale + 'px'; t.lw=t.w; t.lh=t.h; //Normally, this function would end here, //but both FireFox and "Web" for Android 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 meta = document.getElementById('vp'); if(!meta){ var meta = document.createElement('meta'); meta.setAttribute('name','viewport'); meta.setAttribute('id','vp'); } if(meta && meta.parentNode === document.head){ document.head.removeChild(meta); } //var oldSize = subset(window,'innerHeight,innerWidth,outerWidth,outerHeight'); meta.setAttribute('content','width=' + t.w + ', user-scalable=0, target-densityDpi=device-dpi'); document.head.appendChild(meta); document.body.style.height = t.h.toString() + 'px'; window.scrollTo(0,1); //window.scrollTo(0,0); //displayDebug(oldSize); //displayDebug(document.body.style); }, update:function(){ var t = this; t.checkWindow(); if(t.w !== t.lw || t.h !== t.lh){t.resize();} if(t.debug){ var newSize = subset(window,'innerHeight,innerWidth,outerWidth,outerHeight'); newSize.bodyHeight = document.body.style.height; clearDebug(); displayDebug(newSize); } 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(); //c.strokeStyle = '#fff'; t.c.translate(t.cx,t.cy); t.c.globalCompositeOperation = t.globalCompositeOperation; //t.rQ.sort(t.sortByObjectZDepth); for(t.cro = 0; t.cro < t.rQ.length; t.cro += 1){ t.rQ[t.cro].update(t); } t.c.restore(); }, start: function () { var t = this; t.interval = setInterval(function () {t.update();}, 1000 / t.frameRate); }, stop: function () { clearInterval(this.interval); }, sortByObjectZDepth:function(a,b){return a.pos[2] - b.pos[2];}, //-------------------------------- //This is where all of the math happens //-------------------------------- project3Dto2D:function(p3){ //return {x:p3[0],y:p3[1]}; Orthographic! var scale = this.camera.fov/(this.camera.fov + -p3[2]), p2 = {}; p2.x = (p3[0] * scale); p2.y = (p3[1] * scale); p2.scale = scale; p2.color = p3[3] || false; return p2; }, getRelativeAngle3d:function(p3){ //DO NOT try to optomize out the use of sqrt in this function!!! var topAngle = Math.atan2(p3[0], p3[1]); var sideAngle = tau - Math.atan2(p3[2], Math.sqrt(NPos2d.square(p3[0]) + NPos2d.square(p3[1]))); return [sideAngle,0,-topAngle]; }, pointAt:function(o,endPos){ var posDiff = [ endPos[0] - o.pos[0], endPos[1] - o.pos[1], endPos[2] - o.pos[2] ]; o.rot = this.getRelativeAngle3d(posDiff); }, rotatePoint:function(x,y,rad){ var length = Math.sqrt((x * x) + (y * y)); var currentRad = Math.atan2(x,y); x = Math.sin(currentRad - rad) * length; y = Math.cos(currentRad - rad) * length; var output = [x,y]; return output; }, totalRotationCalculations:0, getP3Rotated:function(p3,rot,order){ var t = this; //return p3; var x = p3[0], y = p3[1], z = p3[2]; var xr = rot[0], yr = rot[1], zr = rot[2]; //Alright, here's something interesting. //The order you rotate the dimentions is IMPORTANT to rotation animation! //Here's my quick, no math approach to applying that. for(var r = 0; r < order.length; r += 1){ if(order[r] === 0){ //x... if(xr !== 0){ var zy = t.rotatePoint(z,y,xr); z = zy[0]; y = zy[1]; t.totalRotationCalculations += 1; } }else if(order[r] === 1){ //y... if(yr !== 0){ var xz = t.rotatePoint(x,z,yr); x = xz[0]; z = xz[1]; t.totalRotationCalculations += 1; } }else if(order[r] === 2){ //z... if(zr !== 0){ var xy = t.rotatePoint(x,y,zr); x = xy[0]; y = xy[1]; t.totalRotationCalculations += 1; } }else{ throw 'up'; } } return [x,y,z]; }, getP3Scaled:function(p3,scale){ //return p3; return [p3[0]*scale[0], p3[1]*scale[1], p3[2]*scale[2]];; }, //I used to use a function in here named nGetOffsets that would do the same thing, looping through dimentions. //It was TERRIBLY inefficient at this task, so I replaced it in favor of nDimention specific versions. getP3Offset:function(p3,offset){ //an efficient hack to quickly add an offset to a 3D point return [p3[0]+offset[0], p3[1]+offset[1], p3[2]+offset[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! 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 dimention 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]; }, get2DBounds:function(pointList){ //Works for 2D, 3D, and nD! var p = pointList[0]; var min = new Vec2(p); var max = new Vec2(p); for(var i = 1; i < pointList.length; i += 1){ var p = new Vec2(pointList[i]); if(p.x < min.x){min.x = p.x;} else if(p.x > max.x){max.x = p.x;} if(p.y < min.y){min.y = p.y;} else if(p.y > max.y){max.y = p.y;} } return [min,max]; }, makeBBSquareFromTwoPoints:function(bbMinOffset,bbMaxOffset){ return [ new Vec2(bbMinOffset[0],bbMinOffset[1]), new Vec2(bbMaxOffset[0],bbMinOffset[1]), new Vec2(bbMaxOffset[0],bbMaxOffset[1]), new Vec2(bbMinOffset[0],bbMaxOffset[1]), ]; }, lineRenderLoop:function(o){ var t = this, c = t.c; var computedPointList = []; for(var i = 0; i < o.shape.points.length; i += 1){ //to make sure I'm not messing with the original array... var point = o.transformedPointCache[i]; point = o.pos.clone().add(point).add(t.camera.pos); computedPointList[i] = point; } for(var i = 0; i < o.transformedLineCache.length; i += 1){ //offset the points by the object's position var p0 = computedPointList[o.transformedLineCache[i][0]]; var p1 = computedPointList[o.transformedLineCache[i][1]]; // min max var screenBounds = [new Vec2(-t.cx, -t.cy),new Vec2(t.cx, t.cy)]; var p0InBounds = NPos2d.pointIn2dBounds(p0,screenBounds); var p1InBounds = NPos2d.pointIn2dBounds(p1,screenBounds); //If the line is completely off screen, do not bother rendering it. if(p0InBounds || p1InBounds){ c.beginPath(); c.moveTo(p0.x,p0.y); c.lineTo(p1.x,p1.y); c.strokeStyle= o.transformedLineCache[i][2] || o.shape.color || o.color || '#fff'; c.lineWidth= o.lineWidth || o.scene.lineWidth || 2; c.lineCap='round'; c.stroke(); } } }, drawLines:function(o){ var t = this; //I see no reason to check whether the rotation/scale is different between processing each point, //so I'll just do that once per frame and have a loop just for rotating the points. if(o.lastRotString !== o.rot.toString() || o.lastScaleString !== o.scale.toString()){ //console.log(o.lastRotString); o.transformedPointCache = []; for(var i = 0; i < o.shape.points.length; i += 1){ //to make sure I'm not messing with the original array... var point = new Vec2(o.shape.points[i]).scale(o.scale); point.color = o.shape.points[i][2]; //point = t.getP3Rotated(point, o.rot, o.rotOrder); //point[3] = o.shape.points[i][3] || false;//Point Color Preservation - no need to offset or rotate it o.transformedPointCache[i] = point; } o.transformedLineCache = o.shape.lines; o.boundingBox = t.get2DBounds(o.transformedPointCache); o.lastScaleString = o.scale.toString(); o.lastRotString = o.rot.toString(); } if(o.renderAlways){ t.lineRenderLoop(o); return; } var bbMinOffset = o.pos.clone().add(o.boundingBox[0]).add(t.camera.pos); var bbMaxOffset = o.pos.clone().add(o.boundingBox[1]).add(t.camera.pos); //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... var bbSquare = t.makeBBSquareFromTwoPoints(bbMinOffset,bbMaxOffset); var 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(var i = 0; i < bbSquare.length && bbOffscreen; i += 1){ bbp = bbSquare[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; //I see no reason to check whether the rotation/scale is different between processing each point, //so I'll just do that once per frame and have a loop just for rotating the points. if(o.lastRotString !== t.getP3String(o.rot) || o.lastScaleString !== t.getP3String(o.scale)){ //console.log(o.lastRotString); o.transformedPointCache = []; for(var i = 0; i < o.shape.points.length; i += 1){ //to make sure I'm not messing with the original array... var point = [o.shape.points[i][0],o.shape.points[i][1],o.shape.points[i][2]]; point = t.getP3Scaled(point, o.scale); point = t.getP3Rotated(point, o.rot, o.rotOrder); point[3] = o.shape.points[i][3] || false;//Point Color Preservation - no need to offset or rotate it o.transformedPointCache[i] = point; } //Now with Z-Depth sorting for each point on an object! if(o.transformedPointCache.length > 1){ o.transformedPointCache.sort(function(a,b) { return a[2] - b[2]; }); } //end z-sorting for the points o.boundingBox = t.get2DBounds(o.transformedPointCache); o.lastScaleString = t.getP3String(o.scale); o.lastRotString = t.getP3String(o.rot); } if(o.renderAlways){ t.pointRenderLoop(o); return; } var bbMinOffset = t.getP3Offset(t.getP3Offset(o.boundingBox[0], o.pos), t.camera.pos); var bbMaxOffset = t.getP3Offset(t.getP3Offset(o.boundingBox[1], o.pos), t.camera.pos); //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 square... let's start from the top left, spiraling down clockwise var bbSquare = t.makeBBSquareFromTwoPoints(bbMinOffset,bbMaxOffset); var 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 4 points to determine if it's on screen - or even off screen. for(var i = 0; i < bbSquare.length && bbOffscreen; i += 1){ bbp = bbSquare[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); } } }, drawCircle:function(vec,radius,color,fill,lineWidth){ var t = this, c = t.c; var radius = radius || 8; var pos = vec.clone().sub(t.camera.pos); c.beginPath(); c.strokeStyle = color || '#0f0'; c.lineCap = 'round'; c.lineJoin = 'round'; c.lineWidth= lineWidth || t.lineWidth || t.scene.lineWidth || 2; c.arc(pos.x,pos.y, radius, 0, tau, false); if(fill){ c.fillStyle= color || '#0f0'; c.fill(); }else{ c.stroke(); } }, pointRenderLoop:function(o){ var t = this, c = t.c; var computedPointList = []; for(var i = 0; i < o.shape.points.length; i += 1){ //to make sure I'm not messing with the original array... var point = [o.transformedPointCache[i][0],o.transformedPointCache[i][1],o.transformedPointCache[i][2]]; point = t.getP3Offset(point, o.pos); point = t.getP3Offset(point, t.camera.pos); point[3] = o.transformedPointCache[i][3] || false;//Point Color Preservation - no need to offset or rotate it computedPointList[i] = point; } for(var i = 0; i < o.transformedPointCache.length; i += 1){ //offset the points by the object's position var p3a = computedPointList[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){ var p0 = t.project3Dto2D(p3a); // min max var screenBounds = [[-t.cx, -t.cy],[t.cx, t.cy]]; var p0InBounds = NPos2d.pointIn2dBounds(p0,screenBounds); //If the line is completely off screen, do not bother rendering it. if(p0InBounds){ //console.log(p0.color); c.moveTo(p0.x,p0.y); c.beginPath(); c.arc(p0.x,p0.y,(p0.scale * o.pointScale),0,tau,false); if(o.pointStyle === 'fill'){ c.fillStyle= p0.color || o.shape.color || '#fff'; c.fill(); }else if(o.pointStyle === 'stroke'){ c.strokeStyle= p0.color || o.shape.color || '#fff'; c.lineWidth= o.lineWidth || o.scene.lineWidth || 2; c.lineCap='round'; c.stroke(); } } } } }, add:function(o){ //I may rename this to addChild in the future. Hmm... o.scene = this; if(o.onAdd !== undefined){o.onAdd();} this.rQ.push(o); }, remove:function(o){ var t = this; for(var i = 0; i < t.rQ.length; i += 1){ if(t.rQ[i] === o){ t.rQ.splice(i,1); if(o.onRemove !== undefined){o.onRemove();} o.scene = false; //I FOUND THE BLINKING FOR REAL THIS TIME!!! //console.log(cro,i); //If the object being removed from the render queue is positioned earlier than //the object that's currently being rendered, subtract 1 from the 'render state' //to compensate for the object being taken out, so on cro +=1 in the global //'update' loop, we don't skip a beat for that same render pass. Oh yeah! if(i <= t.cro ){ t.cro -= 1; } } } }, wrap:function(pos){ var t = this; if(pos.x > t.cx) {pos.x -= t.cx * 2;} if(pos.x < -t.cx){pos.x += t.cx * 2;} if(pos.y > t.cy) {pos.y -= t.cy * 2;} if(pos.y < -t.cy){pos.y += t.cy * 2;} }, wrapByRadius:function(pos, radius){ var t = this; if(pos.x > t.cx + radius) {pos.x -= (t.cx + radius) * 2;} if(pos.x < -t.cx - radius){pos.x += (t.cx + radius) * 2;} if(pos.y > t.cy + radius) {pos.y -= (t.cy + radius) * 2;} if(pos.y < -t.cy - radius){pos.y += (t.cy + radius) * 2;} } }; NPos2d.destroyFunc = function(){ var t = this; if(t.scene){ t.scene.remove(t); } } NPos2d.Camera = function(args){ var t = this; if(t===window){throw 'You must use the `new` keyword when calling a Constructor Method!';} var args = args || {}; t.pos = args.pos || new Vec2(); t.rot = args.rot || 0;//Totally not implemented yet. } NPos2d.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 NPos2d.Geom.square = { color:'#999', points:[ [ -10, 10], [ 10, 10], [ 10,-10], [ -10,-10], ], lines:[[0,1],[1,2],[2,3],[3,0]], }; NPos2d.blessWith2DBase = function(o,args){ o.pos = args.pos || new Vec2(); o.rot = args.rot || 0; o.scale = args.scale || o.scale || new Vec2(1,1); o.lastScaleString = false; o.lastRotString = false; o.transformedPointCache = []; o.transformedLineCache = []; o.boundingBox = [new Vec2(),new Vec2()]; o.shape = args.shape || o.shape; 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 || o.lineWidth || undefined; o.scene = false; //An object should know which scene it's in, if it would like to be destroyed. if(o.render === undefined){ if(o.renderStyle === 'lines'){ o.render = function(){ o.scene.drawLines(o); } }else if(o.renderStyle === 'points'){ o.render = function(){ o.scene.drawPoints(o); } }else if(o.renderStyle === 'both'){ o.render = function(){ o.scene.drawLines(o); o.scene.drawPoints(o); } }else{ throw 'Invalid renderStyle specified: ' + o.renderStyle; } } o.destroy = NPos2d.destroyFunc; } NPos2d.Ob2D = function(args){ if(this === window){throw 'JIM TYPE ERROR';} if(arguments.length > 1){throw 'ob2D expects only one param, an object with the named arguments.';} var args = args || {}; NPos2d.blessWith2DBase(this,args); return this; } NPos2d.Ob2D.prototype = { shape: NPos2d.Geom.square, update:function(s){ this.render(); } };