diff --git a/v2/cloth/LARGE-3-420 oiling 9000.Tif.jpg b/v2/cloth/LARGE-3-420 oiling 9000.Tif.jpg new file mode 100644 index 0000000..ccf49d8 Binary files /dev/null and b/v2/cloth/LARGE-3-420 oiling 9000.Tif.jpg differ diff --git a/v2/cloth/circuit_pattern.png b/v2/cloth/circuit_pattern.png new file mode 100644 index 0000000..64b96eb Binary files /dev/null and b/v2/cloth/circuit_pattern.png differ diff --git a/v2/cloth/index.html b/v2/cloth/index.html new file mode 100644 index 0000000..a99f8cf --- /dev/null +++ b/v2/cloth/index.html @@ -0,0 +1,331 @@ + + + + Leap Cloth + + + + + + + + +
Simple Cloth Simulation
+ Verlet integration with Constrains relaxation
+
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/v2/cloth/js/Cloth.js b/v2/cloth/js/Cloth.js new file mode 100644 index 0000000..7dc505f --- /dev/null +++ b/v2/cloth/js/Cloth.js @@ -0,0 +1,498 @@ +/* + * Cloth Simulation using a relaxed constrains solver + */ + +// Suggested Readings + +// Advanced Character Physics by Thomas Jakobsen Character +// http://freespace.virgin.net/hugo.elias/models/m_cloth.htm +// http://en.wikipedia.org/wiki/Cloth_modeling +// http://cg.alexandra.dk/tag/spring-mass-system/ +// Real-time Cloth Animation http://www.darwin3d.com/gamedev/articles/col0599.pdf + +// Certain things could be stored to attempt to reduce GC +// such as `diff` + +var DAMPING = 0.06; +var DRAG = 1 - DAMPING; + +var colliders = []; + + +var redDots = []; +var greenDots = []; +var dotGeo = new THREE.SphereGeometry( 1, 4, 4 ); +var dotMaterial = new THREE.MeshPhongMaterial( { color: 0xff0000 } ); + +var visualize = true; +if (visualize){ + + var mesh; + for (var i = 0; i < 2500; i++){ + mesh = new THREE.Mesh( dotGeo, dotMaterial ); + redDots.push( mesh ) + } + + var dotMaterial = new THREE.MeshPhongMaterial( { color: 0x00ff00 } ); + + for (var i = 0; i < 1000; i++){ + mesh = new THREE.Mesh( dotGeo, dotMaterial ); + greenDots.push( mesh ) + } +} + + +// Takes in number of particles wide by height +// in a rather unfortunate naming convention, this.w is in index-space, and this.width is in scene space. +function Cloth(xParticleCount, yParticleCount, springLen) { + this.mesh = null; // this gets reverse-referenced later. + this.w = xParticleCount || 10; // number + this.h = yParticleCount || 10; + this.particleSpacing = springLen || 25; + + this.geometry = new THREE.ParametricGeometry( + this.particlePosition, // this sets the initial position, and is effectively unused + this.w, + this.h + ); + + this.geometry.dynamic = true; + this.geometry.computeFaceNormals(); // why ? + + this.width = this.particleSpacing * this.w; + this.height = this.particleSpacing * this.h; + + this.particles = []; + this.pinnedParticles = []; + this.constrains = []; + + // Temporary usage + this.diff3 = new THREE.Vector3; + this.rayCaster = new THREE.Raycaster; + + + // todo - for now, we assume this never changes -.- + // should base off of matrixWorld instead + // used to project particles. + this.worldNormal = new THREE.Vector3(0,0,1); + + this.addParticles(); + this.addConstraints(); + +} + + +Cloth.prototype.addParticles = function(){ + var u, v; + + // Create particles + for (v=0; v<=this.h; v++) { + for (u=0; u<=this.w; u++) { + this.particles.push( + + new Particle( + this.particlePosition(u/this.w, v/this.h) + ) + ); + + } + } + +}; + + +Cloth.prototype.addConstraints = function(){ + var u, v; + + // starting at bottom left + // can the order of these two loops be flipped without problem? + for (v=0;v collider radius * 8 (8 is a bit of a magic number here.) + // Margin is taken against the origin position of the particle. + // So far, this works at any orientation. + // assumes that the mesh is a flat plane, and not curved to the view + // that would require higher or dynamic z-margin. (Might not be too bad to implement). + // this should already do a lot when the hand is not near the mesh. + if ( Math.abs(rectPos.z) > rect.depth || + Math.abs(rectPos.x) > this.width / 2 * 1.3 || // todo - dial back these 1.3s + Math.abs(rectPos.y) > this.height / 2 * 1.3) { + + rect.reset(this); + + return + + } else { + + for (i = 0; i < rect.colliders.length; i++){ + rect.colliders[i].initRelation(this, rectPos.z); + } + + } + + + // convert from meters to index-space. + // this needs to be reflected as from-center + rectPos.divideScalar(this.particleSpacing); + rectPos.x = ~~rectPos.x; // Math.floor - todo - try round and see if it feels better/more centered, especially on lower particle-density systems. + rectPos.y = ~~rectPos.y; + + + // offset is from center, but we're indexed from bottom left + // this could be sloppy/off by one? Better do the addition before the unit change? + rectPos.x += ~~(this.w / 2); + rectPos.y += ~~(this.h / 2); + + + var halfWidth = Math.ceil( rect.halfWidth / this.particleSpacing ); // avoid 0 + var halfHeight = Math.ceil( rect.halfHeight / this.particleSpacing ); + var width = halfWidth * 2; + var height = halfHeight * 2; + + var leftBound = rectPos.x - halfWidth; + var rightBound = rectPos.x + halfWidth; + + + // check for and prevent wraparound + // possible optimization would be try and no longer necessitate these conditions. todo - by hardcoding constraints + if (rightBound > this.w) { + rightBound = this.w; + }else{ + for (i = 0; i <= height; i++) { + row = (rectPos.y - halfHeight + i) * (this.w + 1); + // Skip the three conditions in the other loop - We don't want boundaries moved in artificially + boundaryParticles.push(this.particles[rightBound + row]); + } + } + + if (leftBound < 0) { + leftBound = 0 + } else { + for (i = 0; i <= height; i++) { + row = (rectPos.y - halfHeight + i) * (this.w + 1); + + boundaryParticles.push(this.particles[leftBound + row]); + } + } + + // bottom condition: + if (rectPos.y - halfHeight >= 0){ + row = (rectPos.y - halfHeight) * (this.w + 1); + boundaryParticles = boundaryParticles.concat( + this.particles.slice(leftBound + row, rightBound + row + 1) // second arg is exclusive + ); + } + + // top condition: + if (rectPos.y + halfHeight <= this.h){ + row = (rectPos.y + halfHeight) * (this.w + 1); + boundaryParticles = boundaryParticles.concat( + this.particles.slice(leftBound + row, rightBound + row + 1) // second arg is exclusive + ); + } + + // possible opt: looks like boundary particles are a subset of nearbyParticles, which == wasted work? + for (i = 0; i <= height; i++){ + row = (rectPos.y - halfHeight + i) * (this.w + 1); + if (row < 0) continue; + + particles.push( + this.particles.slice(leftBound + row, rightBound + row + 1) // second arg is exclusive + ); + + } + + rect.nearbyParticles = particles; + rect.boundaryParticles = boundaryParticles; + +}; + +arrayDiff = function(a1, a2) { + return a1.filter(function(i) {return a2.indexOf(i) < 0;}); +}; + +// call this every animation frame +// should be broken in to smaller methods +// todo - Use Shader -- not a good idea until happy with the actual algorithms going on. +// note that currently, if finger movement is > fps, some particles may get frozen in poor position :-/ flaky +Cloth.prototype.simulate = function() { + + var i, il, j, jl, k, kl, + particle, particles = this.particles, + collider, rect, + nearbyParticles, boundaryParticles,// affectedParticles = [], noLongerAffectedParticles, + pRightwards, pDownwards, rects = []; + + + // reset dots: + if (visualize){ + for (i = 0; i < redDots.length; i++){ + redDots[i].visible = false; + } + + // reset dots: + for (i = 0; i < greenDots.length; i++){ + greenDots[i].visible = false; + } + } + + // it turns out to be quite difficult to track intersections of rectangles + // for now we just combine them in to one larger rectangle... + // not as good, but should be fine + + // for each collider + // check collisions against intersecting rectangles + // if yes, take both out, add a new. + var rectCollision = false; + for (i = 0; i < colliders.length; i++){ + collider = colliders[i]; + + for (j = 0; j < rects.length; j++){ + + if ( collider.rect.intersects(rects[j]) ) { + rectCollision = true; + + collider.megaRect = collider.rect.combineWith( + rects.splice(j,1)[0] + ); + + rects.push( + collider.megaRect + ); + } else { + + collider.megaRect = null; + + } + + } + + if (!rectCollision){ + rects.push( + collider.rect + ); + } + + } + + // for every collider, run mesh calculations on effected nodes + for ( i = 0; i < rects.length; i++){ + + rect = rects[i]; + + // this is a good optimization for a large number of colliders! (tested w/ 50 to 500). + // returns a 2d array of rows, which we use later to know where to apply constraints. + // shit - now we have to prevent loop-over + this.calcNearbyParticlesFor(rect); + nearbyParticles = rect.nearbyParticles; + boundaryParticles = rect.boundaryParticles; + + for (j=0, jl = nearbyParticles.length; j < jl; j++) { + + for (k=0, kl = nearbyParticles[j].length; k < kl; k++) { + particle = nearbyParticles[j][k]; + + if (visualize){ + redDots[(j * jl) + k].visible = true; + redDots[(j * jl) + k].position.copy(particle.position); + } + + particle.calcPosition(); + + pRightwards = nearbyParticles[j][k + 1]; + + // hopefully these conditions don't cause slowness :-/ + // we would then have to re-pre-establish them. + if (pRightwards) this.satisfyConstraint(particle, pRightwards); + + if ( nearbyParticles[j+1]) { + pDownwards = nearbyParticles[j+1][k]; + if (pDownwards) this.satisfyConstraint(particle, pDownwards); + } + + for (var l = 0; l < rect.colliders.length; l++){ + this.satisfyCollider(rect.colliders[l], particle); + } + + } + + } + + // note that right now, it is undefined what happens when a collider disappears when colliding with particles + // they could instantly reset + // it would be cooler, and more fault tolerant, if the glided back in to position. + + for (i = 0, il=boundaryParticles.length; i < il; i++){ + // this could probably be optimized out + if (!boundaryParticles[i]) continue; + + if (visualize){ + greenDots[i].visible = true; + greenDots[i].position.copy(boundaryParticles[i].position); + } + + boundaryParticles[i].fixPosition(); + } + + } + + + + // can we do anything to enhance the visuals - with a good lighting, material, etc? + + // these following lines should be optimized for idle-state operation. (!) + // - add vertex ids to particles, use for the copy and vertex normal calculations + // how to get face normals? Maybe they're layed out in an orderly fashion, as vertices are, allowing us to query them directly? + for ( i = 0, il = particles.length; i < il; i ++ ) { + this.geometry.vertices[ i ].copy( particles[ i ].position ); + } + + this.geometry.computeFaceNormals(); + this.geometry.computeVertexNormals(); + + this.geometry.normalsNeedUpdate = true; + this.geometry.verticesNeedUpdate = true; + + return this; +}; diff --git a/v2/cloth/js/Collider.js b/v2/cloth/js/Collider.js new file mode 100644 index 0000000..5f18bc1 --- /dev/null +++ b/v2/cloth/js/Collider.js @@ -0,0 +1,127 @@ +COLLIDER_RECT_SCALE_FACTOR = 8; // how many times larger is the rect than the collider + +function Collider(mesh) { + this.mesh = mesh; + this.radius = mesh.geometry.parameters.radius; + this.position = mesh.position; + this.clothRelations = {}; // when pushing through a mesh, this tracks which direction the collider came from + + this.rect = new Rect( + this.position, + this.radius * COLLIDER_RECT_SCALE_FACTOR, + this.radius * COLLIDER_RECT_SCALE_FACTOR, + [this] + ); + + // hold a reference to the combination-rect of multiple colliders, so that it can be reset on hand lost. + this.megaRect = null; + +} + +// Tracks which side of a cloth a collider is physically on +// (so that it knows whether to deform to that point, regardless of speed or interval) +// takes in a cloth and the relative distance of this collider from that cloth. +Collider.prototype.initRelation = function(cloth, zOffset){ + if ( !isNaN(this.clothRelations[cloth.mesh.id]) ) return; + + this.clothRelations[cloth.mesh.id] = Math.sign(zOffset); +}; + +Collider.prototype.resetRelation = function(cloth) { + + delete this.clothRelations[cloth.mesh.id]; + +}; + +// Returns 1 or -1 +Collider.prototype.physicalSide = function(cloth){ + + return this.clothRelations[cloth.mesh.id]; + +}; + + + +// rectangle in particle 2d world space. +function Rect(position, halfWidth, halfHeight, colliders){ + this.position = position; + this.halfWidth = halfWidth; + this.halfHeight = halfHeight; + this.depth = 40; // so-to-speak. This threshold of z-offset where simulation is running. + this.colliders = colliders; + + // possible opt would be to only add or remove items from these arrays, rather than replacing them. + this.nearbyParticles = []; + this.boundaryParticles = []; +} + + +// http://stackoverflow.com/questions/16005136/how-do-i-see-if-two-rectangles-intersect-in-javascript-or-pseudocode +// we work in actual world space +Rect.prototype.intersects = function(rect){ + + var isLeft = (this.position.x + this.halfWidth ) < (rect.position.x - rect.halfWidth ); + var isRight = (this.position.x - this.halfWidth ) > (rect.position.x + rect.halfWidth ); + var isAbove = (this.position.y - this.halfHeight) > (rect.position.y + rect.halfHeight); + var isBelow = (this.position.y + this.halfHeight) < (rect.position.y - rect.halfHeight); + + return !( isLeft || isRight || isAbove || isBelow ); +}; + +// get the minimum enclosing rectangle +Rect.prototype.combineWith = function(rect){ + + var left = Math.min(this.position.x - this.halfWidth , rect.position.x - rect.halfWidth ); + var right = Math.max(this.position.x + this.halfWidth , rect.position.x + rect.halfWidth ); + var bottom = Math.min(this.position.y - this.halfHeight, rect.position.y - rect.halfHeight); + var top = Math.max(this.position.y + this.halfHeight, rect.position.y + rect.halfHeight); + + // for now, z-indices are totally thrown away. So we ignore them when creating a position. + return new Rect( + new THREE.Vector3( + (right + left) / 2, + (bottom + top) / 2, + NaN // tossed later.. + ), + (right - left) / 2, + (top - bottom) / 2, + [].concat(rect.colliders).concat(this.colliders) + ); + +}; + + +// note: this has not been performance audited +Rect.prototype.forEachNearbyParticle = function(callback){ + + for (var i=0, il = this.nearbyParticles.length; i < il; i++) { + + // note: this is a square, and could be optimized. + for (var j = 0, jk = this.nearbyParticles[i].length; j < jk; j++) { + + callback(this.nearbyParticles[i][j]); + + } + } + +}; + +// kind of a cheap method. Should be gutted and replaced by something which animates the particles back in to place +// rather than snapping them there and ceasing simulation +Rect.prototype.reset = function(cloth){ + if (this.nearbyParticles.length === 0) return; + + this.forEachNearbyParticle(function(particle){ + + particle.fixPosition() + + }); + + this.nearbyParticles = []; + this.boundaryParticles = []; + + for (var i = 0; i < this.colliders.length; i++){ + this.colliders[i].resetRelation(cloth); + } + +}; \ No newline at end of file diff --git a/v2/cloth/js/Detector.js b/v2/cloth/js/Detector.js new file mode 100644 index 0000000..70c96de --- /dev/null +++ b/v2/cloth/js/Detector.js @@ -0,0 +1,66 @@ +/** + * @author alteredq / http://alteredqualia.com/ + * @author mr.doob / http://mrdoob.com/ + */ + +var Detector = { + + canvas: !! window.CanvasRenderingContext2D, + webgl: ( function () { try { var canvas = document.createElement( 'canvas' ); return !! window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ); } catch( e ) { return false; } } )(), + workers: !! window.Worker, + fileapi: window.File && window.FileReader && window.FileList && window.Blob, + + getWebGLErrorMessage: function () { + + var element = document.createElement( 'div' ); + element.id = 'webgl-error-message'; + element.style.fontFamily = 'monospace'; + element.style.fontSize = '13px'; + element.style.fontWeight = 'normal'; + element.style.textAlign = 'center'; + element.style.background = '#fff'; + element.style.color = '#000'; + element.style.padding = '1.5em'; + element.style.width = '400px'; + element.style.margin = '5em auto 0'; + + if ( ! this.webgl ) { + + element.innerHTML = window.WebGLRenderingContext ? [ + 'Your graphics card does not seem to support WebGL.
', + 'Find out how to get it here.' + ].join( '\n' ) : [ + 'Your browser does not seem to support WebGL.
', + 'Find out how to get it here.' + ].join( '\n' ); + + } + + return element; + + }, + + addGetWebGLMessage: function ( parameters ) { + + var parent, id, element; + + parameters = parameters || {}; + + parent = parameters.parent !== undefined ? parameters.parent : document.body; + id = parameters.id !== undefined ? parameters.id : 'oldie'; + + element = Detector.getWebGLErrorMessage(); + element.id = id; + + parent.appendChild( element ); + + } + +}; + +// browserify support +if ( typeof module === 'object' ) { + + module.exports = Detector; + +} diff --git a/v2/cloth/js/OrbitControls.js b/v2/cloth/js/OrbitControls.js new file mode 100644 index 0000000..9faea1f --- /dev/null +++ b/v2/cloth/js/OrbitControls.js @@ -0,0 +1,672 @@ +/** + * @author qiao / https://github.com/qiao + * @author mrdoob / http://mrdoob.com + * @author alteredq / http://alteredqualia.com/ + * @author WestLangley / http://github.com/WestLangley + * @author erich666 / http://erichaines.com + */ +/*global THREE, console */ + +// This set of controls performs orbiting, dollying (zooming), and panning. It maintains +// the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is +// supported. +// +// Orbit - left mouse / touch: one finger move +// Zoom - middle mouse, or mousewheel / touch: two finger spread or squish +// Pan - right mouse, or arrow keys / touch: three finter swipe +// +// This is a drop-in replacement for (most) TrackballControls used in examples. +// That is, include this js file and wherever you see: +// controls = new THREE.TrackballControls( camera ); +// controls.target.z = 150; +// Simple substitute "OrbitControls" and the control should work as-is. + +THREE.OrbitControls = function ( object, domElement ) { + + this.object = object; + this.domElement = ( domElement !== undefined ) ? domElement : document; + + // API + + // Set to false to disable this control + this.enabled = true; + + // "target" sets the location of focus, where the control orbits around + // and where it pans with respect to. + this.target = new THREE.Vector3(); + + // center is old, deprecated; use "target" instead + this.center = this.target; + + // This option actually enables dollying in and out; left as "zoom" for + // backwards compatibility + this.noZoom = false; + this.zoomSpeed = 1.0; + + // Limits to how far you can dolly in and out + this.minDistance = 0; + this.maxDistance = Infinity; + + // Set to true to disable this control + this.noRotate = false; + this.rotateSpeed = 1.0; + + // Set to true to disable this control + this.noPan = false; + this.keyPanSpeed = 7.0; // pixels moved per arrow key push + + // Set to true to automatically rotate around the target + this.autoRotate = false; + this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 + + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + this.minPolarAngle = 0; // radians + this.maxPolarAngle = Math.PI; // radians + + // How far you can orbit horizontally, upper and lower limits. + // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. + this.minAzimuthAngle = - Infinity; // radians + this.maxAzimuthAngle = Infinity; // radians + + // Set to true to disable use of the keys + this.noKeys = false; + + // The four arrow keys + this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; + + // Mouse buttons + this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; + + //////////// + // internals + + var scope = this; + + var EPS = 0.000001; + + var rotateStart = new THREE.Vector2(); + var rotateEnd = new THREE.Vector2(); + var rotateDelta = new THREE.Vector2(); + + var panStart = new THREE.Vector2(); + var panEnd = new THREE.Vector2(); + var panDelta = new THREE.Vector2(); + var panOffset = new THREE.Vector3(); + + var offset = new THREE.Vector3(); + + var dollyStart = new THREE.Vector2(); + var dollyEnd = new THREE.Vector2(); + var dollyDelta = new THREE.Vector2(); + + var theta; + var phi; + var phiDelta = 0; + var thetaDelta = 0; + var scale = 1; + var pan = new THREE.Vector3(); + + var lastPosition = new THREE.Vector3(); + var lastQuaternion = new THREE.Quaternion(); + + var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; + + var state = STATE.NONE; + + // for reset + + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + + // so camera.up is the orbit axis + + var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); + var quatInverse = quat.clone().inverse(); + + // events + + var changeEvent = { type: 'change' }; + var startEvent = { type: 'start'}; + var endEvent = { type: 'end'}; + + this.rotateLeft = function ( angle ) { + + if ( angle === undefined ) { + + angle = getAutoRotationAngle(); + + } + + thetaDelta -= angle; + + }; + + this.rotateUp = function ( angle ) { + + if ( angle === undefined ) { + + angle = getAutoRotationAngle(); + + } + + phiDelta -= angle; + + }; + + // pass in distance in world space to move left + this.panLeft = function ( distance ) { + + var te = this.object.matrix.elements; + + // get X column of matrix + panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] ); + panOffset.multiplyScalar( - distance ); + + pan.add( panOffset ); + + }; + + // pass in distance in world space to move up + this.panUp = function ( distance ) { + + var te = this.object.matrix.elements; + + // get Y column of matrix + panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] ); + panOffset.multiplyScalar( distance ); + + pan.add( panOffset ); + + }; + + // pass in x,y of change desired in pixel space, + // right and down are positive + this.pan = function ( deltaX, deltaY ) { + + var element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + if ( scope.object.fov !== undefined ) { + + // perspective + var position = scope.object.position; + var offset = position.clone().sub( scope.target ); + var targetDistance = offset.length(); + + // half of the fov is center to top of screen + targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); + + // we actually don't use screenWidth, since perspective camera is fixed to screen height + scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight ); + scope.panUp( 2 * deltaY * targetDistance / element.clientHeight ); + + } else if ( scope.object.top !== undefined ) { + + // orthographic + scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth ); + scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight ); + + } else { + + // camera neither orthographic or perspective + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); + + } + + }; + + this.dollyIn = function ( dollyScale ) { + + if ( dollyScale === undefined ) { + + dollyScale = getZoomScale(); + + } + + scale /= dollyScale; + + }; + + this.dollyOut = function ( dollyScale ) { + + if ( dollyScale === undefined ) { + + dollyScale = getZoomScale(); + + } + + scale *= dollyScale; + + }; + + this.update = function () { + + var position = this.object.position; + + offset.copy( position ).sub( this.target ); + + // rotate offset to "y-axis-is-up" space + offset.applyQuaternion( quat ); + + // angle from z-axis around y-axis + + theta = Math.atan2( offset.x, offset.z ); + + // angle from y-axis + + phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); + + if ( this.autoRotate ) { + + this.rotateLeft( getAutoRotationAngle() ); + + } + + theta += thetaDelta; + phi += phiDelta; + + // restrict theta to be between desired limits + theta = Math.max( this.minAzimuthAngle, Math.min( this.maxAzimuthAngle, theta ) ); + + // restrict phi to be between desired limits + phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); + + // restrict phi to be betwee EPS and PI-EPS + phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); + + var radius = offset.length() * scale; + + // restrict radius to be between desired limits + radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); + + // move target to panned location + this.target.add( pan ); + + offset.x = radius * Math.sin( phi ) * Math.sin( theta ); + offset.y = radius * Math.cos( phi ); + offset.z = radius * Math.sin( phi ) * Math.cos( theta ); + + // rotate offset back to "camera-up-vector-is-up" space + offset.applyQuaternion( quatInverse ); + + position.copy( this.target ).add( offset ); + + this.object.lookAt( this.target ); + + thetaDelta = 0; + phiDelta = 0; + scale = 1; + pan.set( 0, 0, 0 ); + + // update condition is: + // min(camera displacement, camera rotation in radians)^2 > EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 + + if ( lastPosition.distanceToSquared( this.object.position ) > EPS + || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS ) { + + this.dispatchEvent( changeEvent ); + + lastPosition.copy( this.object.position ); + lastQuaternion.copy (this.object.quaternion ); + + } + + }; + + + this.reset = function () { + + state = STATE.NONE; + + this.target.copy( this.target0 ); + this.object.position.copy( this.position0 ); + + this.update(); + + }; + + this.getPolarAngle = function () { + + return phi; + + }; + + this.getAzimuthalAngle = function () { + + return theta + + }; + + function getAutoRotationAngle() { + + return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; + + } + + function getZoomScale() { + + return Math.pow( 0.95, scope.zoomSpeed ); + + } + + function onMouseDown( event ) { + + if ( scope.enabled === false ) return; + event.preventDefault(); + + if ( event.button === scope.mouseButtons.ORBIT ) { + if ( scope.noRotate === true ) return; + + state = STATE.ROTATE; + + rotateStart.set( event.clientX, event.clientY ); + + } else if ( event.button === scope.mouseButtons.ZOOM ) { + if ( scope.noZoom === true ) return; + + state = STATE.DOLLY; + + dollyStart.set( event.clientX, event.clientY ); + + } else if ( event.button === scope.mouseButtons.PAN ) { + if ( scope.noPan === true ) return; + + state = STATE.PAN; + + panStart.set( event.clientX, event.clientY ); + + } + + document.addEventListener( 'mousemove', onMouseMove, false ); + document.addEventListener( 'mouseup', onMouseUp, false ); + scope.dispatchEvent( startEvent ); + + } + + function onMouseMove( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + + var element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + if ( state === STATE.ROTATE ) { + + if ( scope.noRotate === true ) return; + + rotateEnd.set( event.clientX, event.clientY ); + rotateDelta.subVectors( rotateEnd, rotateStart ); + + // rotating across whole screen goes 360 degrees around + scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); + + // rotating up and down along whole screen attempts to go 360, but limited to 180 + scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); + + rotateStart.copy( rotateEnd ); + + } else if ( state === STATE.DOLLY ) { + + if ( scope.noZoom === true ) return; + + dollyEnd.set( event.clientX, event.clientY ); + dollyDelta.subVectors( dollyEnd, dollyStart ); + + if ( dollyDelta.y > 0 ) { + + scope.dollyIn(); + + } else { + + scope.dollyOut(); + + } + + dollyStart.copy( dollyEnd ); + + } else if ( state === STATE.PAN ) { + + if ( scope.noPan === true ) return; + + panEnd.set( event.clientX, event.clientY ); + panDelta.subVectors( panEnd, panStart ); + + scope.pan( panDelta.x, panDelta.y ); + + panStart.copy( panEnd ); + + } + + scope.update(); + + } + + function onMouseUp( /* event */ ) { + + if ( scope.enabled === false ) return; + + document.removeEventListener( 'mousemove', onMouseMove, false ); + document.removeEventListener( 'mouseup', onMouseUp, false ); + scope.dispatchEvent( endEvent ); + state = STATE.NONE; + + } + + function onMouseWheel( event ) { + + if ( scope.enabled === false || scope.noZoom === true ) return; + + event.preventDefault(); + event.stopPropagation(); + + var delta = 0; + + if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9 + + delta = event.wheelDelta; + + } else if ( event.detail !== undefined ) { // Firefox + + delta = - event.detail; + + } + + if ( delta > 0 ) { + + scope.dollyOut(); + + } else { + + scope.dollyIn(); + + } + + scope.update(); + scope.dispatchEvent( startEvent ); + scope.dispatchEvent( endEvent ); + + } + + function onKeyDown( event ) { + + if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return; + + switch ( event.keyCode ) { + + case scope.keys.UP: + scope.pan( 0, scope.keyPanSpeed ); + scope.update(); + break; + + case scope.keys.BOTTOM: + scope.pan( 0, - scope.keyPanSpeed ); + scope.update(); + break; + + case scope.keys.LEFT: + scope.pan( scope.keyPanSpeed, 0 ); + scope.update(); + break; + + case scope.keys.RIGHT: + scope.pan( - scope.keyPanSpeed, 0 ); + scope.update(); + break; + + } + + } + + function touchstart( event ) { + + if ( scope.enabled === false ) return; + + switch ( event.touches.length ) { + + case 1: // one-fingered touch: rotate + + if ( scope.noRotate === true ) return; + + state = STATE.TOUCH_ROTATE; + + rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + break; + + case 2: // two-fingered touch: dolly + + if ( scope.noZoom === true ) return; + + state = STATE.TOUCH_DOLLY; + + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + var distance = Math.sqrt( dx * dx + dy * dy ); + dollyStart.set( 0, distance ); + break; + + case 3: // three-fingered touch: pan + + if ( scope.noPan === true ) return; + + state = STATE.TOUCH_PAN; + + panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + break; + + default: + + state = STATE.NONE; + + } + + scope.dispatchEvent( startEvent ); + + } + + function touchmove( event ) { + + if ( scope.enabled === false ) return; + + event.preventDefault(); + event.stopPropagation(); + + var element = scope.domElement === document ? scope.domElement.body : scope.domElement; + + switch ( event.touches.length ) { + + case 1: // one-fingered touch: rotate + + if ( scope.noRotate === true ) return; + if ( state !== STATE.TOUCH_ROTATE ) return; + + rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + rotateDelta.subVectors( rotateEnd, rotateStart ); + + // rotating across whole screen goes 360 degrees around + scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); + // rotating up and down along whole screen attempts to go 360, but limited to 180 + scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); + + rotateStart.copy( rotateEnd ); + + scope.update(); + break; + + case 2: // two-fingered touch: dolly + + if ( scope.noZoom === true ) return; + if ( state !== STATE.TOUCH_DOLLY ) return; + + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + var distance = Math.sqrt( dx * dx + dy * dy ); + + dollyEnd.set( 0, distance ); + dollyDelta.subVectors( dollyEnd, dollyStart ); + + if ( dollyDelta.y > 0 ) { + + scope.dollyOut(); + + } else { + + scope.dollyIn(); + + } + + dollyStart.copy( dollyEnd ); + + scope.update(); + break; + + case 3: // three-fingered touch: pan + + if ( scope.noPan === true ) return; + if ( state !== STATE.TOUCH_PAN ) return; + + panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + panDelta.subVectors( panEnd, panStart ); + + scope.pan( panDelta.x, panDelta.y ); + + panStart.copy( panEnd ); + + scope.update(); + break; + + default: + + state = STATE.NONE; + + } + + } + + function touchend( /* event */ ) { + + if ( scope.enabled === false ) return; + + scope.dispatchEvent( endEvent ); + state = STATE.NONE; + + } + + this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); + this.domElement.addEventListener( 'mousedown', onMouseDown, false ); + this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); + this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox + + this.domElement.addEventListener( 'touchstart', touchstart, false ); + this.domElement.addEventListener( 'touchend', touchend, false ); + this.domElement.addEventListener( 'touchmove', touchmove, false ); + + window.addEventListener( 'keydown', onKeyDown, false ); + + // force an update at start + this.update(); + +}; + +THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); diff --git a/v2/cloth/js/Particle.js b/v2/cloth/js/Particle.js new file mode 100644 index 0000000..2ab1f43 --- /dev/null +++ b/v2/cloth/js/Particle.js @@ -0,0 +1,53 @@ + +function Particle(position) { + this.position = position; + this.lastPosition = position.clone(); + this.originalPosition = position.clone(); + this.a = new THREE.Vector3(0, 0, 0); // amount similar to gravity in original. + this.tmpPos = new THREE.Vector3(); // allows pointer switching + this.tmpForce = new THREE.Vector3(); + this.diff3 = new THREE.Vector3(); +} + +// Force -> Acceleration +Particle.prototype.addForce = function(force) { + this.a.add( + this.tmpForce.copy(force) + ); +}; + + +// Performs verlet integration +// instantaneous velocity times drag plus position plus acceleration times time +Particle.prototype.calcPosition = function() { // why is this squared? And in seconds ? + + var newPos = this.tmpPos.subVectors(this.position, this.lastPosition); + newPos.multiplyScalar(DRAG).add(this.position); + + // this restoring force is pretty important, as otherwise edges will get way out of sync, + // and then snap back at infinite speed when becoming once-more fixed. + // 1/2*k*x^2 + var force = this.tmpForce.subVectors(this.originalPosition, this.position); + + newPos.add( force.normalize().multiplyScalar( force.lengthSq() * DAMPING) ); + + this.tmpPos = this.lastPosition; // as this is a reference, we set it to something which is ok to mutate later. + this.lastPosition = this.position; + this.position = newPos; + +}; + +// converts position offset in to tension. +Particle.prototype.fixPosition = function(){ + this.diff3.subVectors(this.position, this.originalPosition); + + // is this conversion of xyz to w correct? + this.position.set( + this.originalPosition.x, + this.originalPosition.y, + this.originalPosition.z + ); + + this.lastPosition.copy(this.position); + +}; diff --git a/v2/cloth/js/Stats.js b/v2/cloth/js/Stats.js new file mode 100644 index 0000000..90b2a27 --- /dev/null +++ b/v2/cloth/js/Stats.js @@ -0,0 +1,149 @@ +/** + * @author mrdoob / http://mrdoob.com/ + */ + +var Stats = function () { + + var startTime = Date.now(), prevTime = startTime; + var ms = 0, msMin = Infinity, msMax = 0; + var fps = 0, fpsMin = Infinity, fpsMax = 0; + var frames = 0, mode = 0; + + var container = document.createElement( 'div' ); + container.id = 'stats'; + container.addEventListener( 'mousedown', function ( event ) { event.preventDefault(); setMode( ++ mode % 2 ) }, false ); + container.style.cssText = 'width:80px;opacity:0.9;cursor:pointer'; + + var fpsDiv = document.createElement( 'div' ); + fpsDiv.id = 'fps'; + fpsDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#002'; + container.appendChild( fpsDiv ); + + var fpsText = document.createElement( 'div' ); + fpsText.id = 'fpsText'; + fpsText.style.cssText = 'color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; + fpsText.innerHTML = 'FPS'; + fpsDiv.appendChild( fpsText ); + + var fpsGraph = document.createElement( 'div' ); + fpsGraph.id = 'fpsGraph'; + fpsGraph.style.cssText = 'position:relative;width:74px;height:30px;background-color:#0ff'; + fpsDiv.appendChild( fpsGraph ); + + while ( fpsGraph.children.length < 74 ) { + + var bar = document.createElement( 'span' ); + bar.style.cssText = 'width:1px;height:30px;float:left;background-color:#113'; + fpsGraph.appendChild( bar ); + + } + + var msDiv = document.createElement( 'div' ); + msDiv.id = 'ms'; + msDiv.style.cssText = 'padding:0 0 3px 3px;text-align:left;background-color:#020;display:none'; + container.appendChild( msDiv ); + + var msText = document.createElement( 'div' ); + msText.id = 'msText'; + msText.style.cssText = 'color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px'; + msText.innerHTML = 'MS'; + msDiv.appendChild( msText ); + + var msGraph = document.createElement( 'div' ); + msGraph.id = 'msGraph'; + msGraph.style.cssText = 'position:relative;width:74px;height:30px;background-color:#0f0'; + msDiv.appendChild( msGraph ); + + while ( msGraph.children.length < 74 ) { + + var bar = document.createElement( 'span' ); + bar.style.cssText = 'width:1px;height:30px;float:left;background-color:#131'; + msGraph.appendChild( bar ); + + } + + var setMode = function ( value ) { + + mode = value; + + switch ( mode ) { + + case 0: + fpsDiv.style.display = 'block'; + msDiv.style.display = 'none'; + break; + case 1: + fpsDiv.style.display = 'none'; + msDiv.style.display = 'block'; + break; + } + + }; + + var updateGraph = function ( dom, value ) { + + var child = dom.appendChild( dom.firstChild ); + child.style.height = value + 'px'; + + }; + + return { + + REVISION: 12, + + domElement: container, + + setMode: setMode, + + begin: function () { + + startTime = Date.now(); + + }, + + end: function () { + + var time = Date.now(); + + ms = time - startTime; + msMin = Math.min( msMin, ms ); + msMax = Math.max( msMax, ms ); + + msText.textContent = ms + ' MS (' + msMin + '-' + msMax + ')'; + updateGraph( msGraph, Math.min( 30, 30 - ( ms / 200 ) * 30 ) ); + + frames ++; + + if ( time > prevTime + 1000 ) { + + fps = Math.round( ( frames * 1000 ) / ( time - prevTime ) ); + fpsMin = Math.min( fpsMin, fps ); + fpsMax = Math.max( fpsMax, fps ); + + fpsText.textContent = fps + ' FPS (' + fpsMin + '-' + fpsMax + ')'; + updateGraph( fpsGraph, Math.min( 30, 30 - ( fps / 100 ) * 30 ) ); + + prevTime = time; + frames = 0; + + } + + return time; + + }, + + update: function () { + + startTime = this.end(); + + } + + } + +}; + +if ( typeof module === 'object' ) { + + module.exports = Stats; + +} \ No newline at end of file diff --git a/v2/cloth/js/leap-plugins-0.1.9.js b/v2/cloth/js/leap-plugins-0.1.9.js new file mode 100644 index 0000000..9879841 --- /dev/null +++ b/v2/cloth/js/leap-plugins-0.1.9.js @@ -0,0 +1,2568 @@ +/* + * LeapJS-Plugins - v0.1.9 - 2014-10-29 + * http://github.com/leapmotion/leapjs-plugins/ + * + * Copyright 2014 LeapMotion, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +//CoffeeScript generated from main/bone-hand/leap.bone-hand.coffee +(function() { + var HandMesh, armTopAndBottomRotation, baseBoneRotation, boneColor, boneHandLost, boneRadius, boneScale, initScene, jointColor, jointRadius, jointScale, material, onHand, scope; + + scope = null; + + initScene = function(targetEl) { + var camera, directionalLight, height, render, renderer, width; + scope.scene = new THREE.Scene(); + scope.renderer = renderer = new THREE.WebGLRenderer({ + alpha: true + }); + width = window.innerWidth; + height = window.innerHeight; + renderer.setClearColor(0x000000, 0); + renderer.setSize(width, height); + renderer.domElement.className = "leap-boneHand"; + targetEl.appendChild(renderer.domElement); + directionalLight = new THREE.DirectionalLight(0xffffff, 1); + directionalLight.position.set(0, 0.5, 1); + scope.scene.add(directionalLight); + directionalLight = new THREE.DirectionalLight(0xffffff, 1); + directionalLight.position.set(0.5, -0.5, -1); + scope.scene.add(directionalLight); + directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); + directionalLight.position.set(-0.5, 0, -0.2); + scope.scene.add(directionalLight); + scope.camera = camera = new THREE.PerspectiveCamera(45, width / height, 1, 10000); + camera.position.fromArray([0, 300, 500]); + camera.lookAt(new THREE.Vector3(0, 160, 0)); + scope.scene.add(camera); + renderer.render(scope.scene, camera); + window.addEventListener('resize', function() { + width = window.innerWidth; + height = window.innerHeight; + camera.aspect = width / height; + camera.updateProjectionMatrix(); + renderer.setSize(width, height); + return renderer.render(scope.scene, camera); + }, false); + render = function() { + renderer.render(scope.scene, camera); + return window.requestAnimationFrame(render); + }; + return render(); + }; + + baseBoneRotation = (new THREE.Quaternion).setFromEuler(new THREE.Euler(Math.PI / 2, 0, 0)); + + jointColor = (new THREE.Color).setHex(0x5daa00); + + boneColor = (new THREE.Color).setHex(0xffffff); + + boneScale = 1 / 6; + + jointScale = 1 / 5; + + boneRadius = null; + + jointRadius = null; + + material = null; + + armTopAndBottomRotation = (new THREE.Quaternion).setFromEuler(new THREE.Euler(0, 0, Math.PI / 2)); + + HandMesh = (function() { + HandMesh.unusedHandMeshes = []; + + HandMesh.get = function() { + var handMesh; + if (HandMesh.unusedHandMeshes.length > 0) { + handMesh = HandMesh.unusedHandMeshes.pop(); + } else { + handMesh = HandMesh.create(); + } + handMesh.show(); + return handMesh; + }; + + HandMesh.prototype.replace = function() { + this.hide(); + return HandMesh.unusedHandMeshes.push(this); + }; + + HandMesh.create = function() { + var mesh; + mesh = new HandMesh; + mesh.setVisibility(false); + HandMesh.unusedHandMeshes.push(mesh); + return mesh; + }; + + function HandMesh() { + var boneCount, finger, i, j, mesh, _i, _j, _k, _l; + material = !isNaN(scope.opacity) ? new THREE.MeshPhongMaterial({ + transparent: true, + opacity: scope.opacity + }) : new THREE.MeshPhongMaterial(); + boneRadius = 40 * boneScale; + jointRadius = 40 * jointScale; + this.fingerMeshes = []; + for (i = _i = 0; _i < 5; i = ++_i) { + finger = []; + boneCount = i === 0 ? 3 : 4; + for (j = _j = 0; 0 <= boneCount ? _j < boneCount : _j > boneCount; j = 0 <= boneCount ? ++_j : --_j) { + mesh = new THREE.Mesh(new THREE.SphereGeometry(jointRadius, 32, 32), material.clone()); + mesh.material.color.copy(jointColor); + scope.scene.add(mesh); + finger.push(mesh); + mesh = new THREE.Mesh(new THREE.CylinderGeometry(boneRadius, boneRadius, 40, 32), material.clone()); + mesh.material.color.copy(boneColor); + scope.scene.add(mesh); + finger.push(mesh); + } + mesh = new THREE.Mesh(new THREE.SphereGeometry(jointRadius, 32, 32), material.clone()); + mesh.material.color.copy(jointColor); + scope.scene.add(mesh); + finger.push(mesh); + this.fingerMeshes.push(finger); + } + if (scope.arm) { + this.armMesh = new THREE.Object3D; + this.armBones = []; + this.armSpheres = []; + for (i = _k = 0; _k <= 3; i = ++_k) { + this.armBones.push(new THREE.Mesh(new THREE.CylinderGeometry(boneRadius, boneRadius, (i < 2 ? 1000 : 100), 32), material.clone())); + this.armBones[i].material.color.copy(boneColor); + if (i > 1) { + this.armBones[i].quaternion.multiply(armTopAndBottomRotation); + } + this.armMesh.add(this.armBones[i]); + } + this.armSpheres = []; + for (i = _l = 0; _l <= 3; i = ++_l) { + this.armSpheres.push(new THREE.Mesh(new THREE.SphereGeometry(jointRadius, 32, 32), material.clone())); + this.armSpheres[i].material.color.copy(jointColor); + this.armMesh.add(this.armSpheres[i]); + } + scope.scene.add(this.armMesh); + } + } + + HandMesh.prototype.scaleTo = function(hand) { + var armLenScale, armWidthScale, baseScale, bone, boneXOffset, finger, fingerBoneLengthScale, halfArmLength, i, j, mesh, _i, _j; + baseScale = hand.middleFinger.proximal.length / this.fingerMeshes[2][1].geometry.parameters.height; + for (i = _i = 0; _i < 5; i = ++_i) { + finger = hand.fingers[i]; + j = 0; + while (true) { + if (j === this.fingerMeshes[i].length - 1) { + mesh = this.fingerMeshes[i][j]; + mesh.scale.set(baseScale, baseScale, baseScale); + break; + } + bone = finger.bones[3 - (j / 2)]; + mesh = this.fingerMeshes[i][j]; + mesh.scale.set(baseScale, baseScale, baseScale); + j++; + mesh = this.fingerMeshes[i][j]; + fingerBoneLengthScale = bone.length / mesh.geometry.parameters.height; + mesh.scale.set(baseScale, fingerBoneLengthScale, baseScale); + j++; + } + } + if (scope.arm) { + armLenScale = hand.arm.length / (this.armBones[0].geometry.parameters.height + this.armBones[0].geometry.parameters.radiusTop); + armWidthScale = hand.arm.width / (this.armBones[2].geometry.parameters.height + this.armBones[2].geometry.parameters.radiusTop); + for (i = _j = 0; _j <= 3; i = ++_j) { + this.armBones[i].scale.set(baseScale, (i < 2 ? armLenScale : armWidthScale), baseScale); + this.armSpheres[i].scale.set(baseScale, baseScale, baseScale); + } + boneXOffset = (hand.arm.width / 2) * 0.85; + halfArmLength = hand.arm.length / 2; + this.armBones[0].position.setX(boneXOffset); + this.armBones[1].position.setX(-boneXOffset); + this.armBones[2].position.setY(halfArmLength); + this.armBones[3].position.setY(-halfArmLength); + this.armSpheres[0].position.set(-boneXOffset, halfArmLength, 0); + this.armSpheres[1].position.set(boneXOffset, halfArmLength, 0); + this.armSpheres[2].position.set(boneXOffset, -halfArmLength, 0); + this.armSpheres[3].position.set(-boneXOffset, -halfArmLength, 0); + } + return this; + }; + + HandMesh.prototype.formTo = function(hand) { + var bone, finger, i, j, mesh, _i; + for (i = _i = 0; _i < 5; i = ++_i) { + finger = hand.fingers[i]; + j = 0; + while (true) { + if (j === this.fingerMeshes[i].length - 1) { + mesh = this.fingerMeshes[i][j]; + mesh.position.fromArray(bone.prevJoint); + break; + } + bone = finger.bones[3 - (j / 2)]; + mesh = this.fingerMeshes[i][j]; + mesh.position.fromArray(bone.nextJoint); + ++j; + mesh = this.fingerMeshes[i][j]; + mesh.position.fromArray(bone.center()); + mesh.setRotationFromMatrix((new THREE.Matrix4).fromArray(bone.matrix())); + mesh.quaternion.multiply(baseBoneRotation); + ++j; + } + } + if (this.armMesh) { + this.armMesh.position.fromArray(hand.arm.center()); + this.armMesh.setRotationFromMatrix((new THREE.Matrix4).fromArray(hand.arm.matrix())); + this.armMesh.quaternion.multiply(baseBoneRotation); + } + return this; + }; + + HandMesh.prototype.setVisibility = function(visible) { + var i, j, _i, _j, _results; + for (i = _i = 0; _i < 5; i = ++_i) { + j = 0; + while (true) { + this.fingerMeshes[i][j].visible = visible; + ++j; + if (j === this.fingerMeshes[i].length) { + break; + } + } + } + if (scope.arm) { + _results = []; + for (i = _j = 0; _j <= 3; i = ++_j) { + this.armBones[i].visible = visible; + _results.push(this.armSpheres[i].visible = visible); + } + return _results; + } + }; + + HandMesh.prototype.show = function() { + return this.setVisibility(true); + }; + + HandMesh.prototype.hide = function() { + return this.setVisibility(false); + }; + + return HandMesh; + + })(); + + onHand = function(hand) { + var handMesh; + if (!scope.scene) { + return; + } + handMesh = hand.data('handMesh'); + if (!handMesh) { + handMesh = HandMesh.get().scaleTo(hand); + hand.data('handMesh', handMesh); + } + return handMesh.formTo(hand); + }; + + boneHandLost = function(hand) { + var handMesh; + handMesh = hand.data('handMesh'); + if (handMesh) { + return handMesh.replace(); + } + }; + + Leap.plugin('boneHand', function(options) { + if (options == null) { + options = {}; + } + scope = options; + scope.boneScale && (boneScale = scope.boneScale); + scope.jointScale && (jointScale = scope.jointScale); + scope.boneColor && (boneColor = scope.boneColor); + scope.jointColor && (jointColor = scope.jointColor); + this.use('handEntry'); + this.use('handHold'); + if (scope.scene === void 0) { + console.assert(scope.targetEl); + initScene(scope.targetEl); + } + if (scope.scene) { + HandMesh.create(); + HandMesh.create(); + } + this.on('handLost', boneHandLost); + return { + hand: onHand + }; + }); + +}).call(this); + +//CoffeeScript generated from main/hand-entry/leap.hand-entry.coffee +/* +Emits controller events when a hand enters of leaves the frame +"handLost" and "handFound" +Each event also includes the hand object, which will be invalid for the handLost event. +*/ + + +(function() { + var handEntry; + + handEntry = function() { + var activeHandIds; + activeHandIds = []; + if (Leap.version.major === 0 && Leap.version.minor < 5) { + console.warn("The hand entry plugin requires LeapJS 0.5.0 or newer."); + } + this.on("deviceStopped", function() { + for (var i = 0, len = activeHandIds.length; i < len; i++){ + id = activeHandIds[i]; + activeHandIds.splice(i, 1); + // this gets executed before the current frame is added to the history. + this.emit('handLost', this.lastConnectionFrame.hand(id)) + i--; + len--; + }; + }); + return { + frame: function(frame) { + var id, newValidHandIds, _i, _len, _results; + newValidHandIds = frame.hands.map(function(hand) { + return hand.id; + }); + for (var i = 0, len = activeHandIds.length; i < len; i++){ + id = activeHandIds[i]; + if( newValidHandIds.indexOf(id) == -1){ + activeHandIds.splice(i, 1); + // this gets executed before the current frame is added to the history. + this.emit('handLost', this.frame(1).hand(id)); + i--; + len--; + } + }; + _results = []; + for (_i = 0, _len = newValidHandIds.length; _i < _len; _i++) { + id = newValidHandIds[_i]; + if (activeHandIds.indexOf(id) === -1) { + activeHandIds.push(id); + _results.push(this.emit('handFound', frame.hand(id))); + } else { + _results.push(void 0); + } + } + return _results; + } + }; + }; + + if ((typeof Leap !== 'undefined') && Leap.Controller) { + Leap.Controller.plugin('handEntry', handEntry); + } else if (typeof module !== 'undefined') { + module.exports.handEntry = handEntry; + } else { + throw 'leap.js not included'; + } + +}).call(this); + +//CoffeeScript generated from main/hand-hold/leap.hand-hold.coffee +(function() { + var handHold; + + handHold = function() { + var dataFn, interFrameData; + interFrameData = {}; + dataFn = function(prefix, hashOrKey, value) { + var dict, key, _name, _results; + interFrameData[_name = prefix + this.id] || (interFrameData[_name] = []); + dict = interFrameData[prefix + this.id]; + if (value !== void 0) { + return dict[hashOrKey] = value; + } else if ({}.toString.call(hashOrKey) === '[object String]') { + return dict[hashOrKey]; + } else { + _results = []; + for (key in hashOrKey) { + value = hashOrKey[key]; + if (value === void 0) { + _results.push(delete dict[key]); + } else { + _results.push(dict[key] = value); + } + } + return _results; + } + }; + return { + hand: { + data: function(hashOrKey, value) { + return dataFn.call(this, 'h', hashOrKey, value); + }, + hold: function(object) { + if (object) { + return this.data({ + holding: object + }); + } else { + return this.hold(this.hovering()); + } + }, + holding: function() { + return this.data('holding'); + }, + release: function() { + var release; + release = this.data('holding'); + this.data({ + holding: void 0 + }); + return release; + }, + hoverFn: function(getHover) { + return this.data({ + getHover: getHover + }); + }, + hovering: function() { + var getHover; + if (getHover = this.data('getHover')) { + return this._hovering || (this._hovering = getHover.call(this)); + } + } + }, + pointable: { + data: function(hashOrKey, value) { + return dataFn.call(this, 'p', hashOrKey, value); + } + } + }; + }; + + if ((typeof Leap !== 'undefined') && Leap.Controller) { + Leap.Controller.plugin('handHold', handHold); + } else if (typeof module !== 'undefined') { + module.exports.handHold = handHold; + } else { + throw 'leap.js not included'; + } + +}).call(this); + +/* + * LeapJS Playback - v0.2.1 - 2014-05-14 + * http://github.com/leapmotion/leapjs-playback/ + * + * Copyright 2014 LeapMotion, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +;(function( window, undefined ){ + 'use strict'; + // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode + +// Copyright (c) 2013 Pieroxy +// This work is free. You can redistribute it and/or modify it +// under the terms of the WTFPL, Version 2 +// For more information see LICENSE.txt or http://www.wtfpl.net/ +// +// For more information, the home page: +// http://pieroxy.net/blog/pages/lz-string/testing.html +// +// LZ-based compression algorithm, version 1.3.3 +var LZString = { + + + // private property + _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", + _f : String.fromCharCode, + + compressToBase64 : function (input) { + if (input == null) return ""; + var output = ""; + var chr1, chr2, chr3, enc1, enc2, enc3, enc4; + var i = 0; + + input = LZString.compress(input); + + while (i < input.length*2) { + + if (i%2==0) { + chr1 = input.charCodeAt(i/2) >> 8; + chr2 = input.charCodeAt(i/2) & 255; + if (i/2+1 < input.length) + chr3 = input.charCodeAt(i/2+1) >> 8; + else + chr3 = NaN; + } else { + chr1 = input.charCodeAt((i-1)/2) & 255; + if ((i+1)/2 < input.length) { + chr2 = input.charCodeAt((i+1)/2) >> 8; + chr3 = input.charCodeAt((i+1)/2) & 255; + } else + chr2=chr3=NaN; + } + i+=3; + + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + + if (isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (isNaN(chr3)) { + enc4 = 64; + } + + output = output + + LZString._keyStr.charAt(enc1) + LZString._keyStr.charAt(enc2) + + LZString._keyStr.charAt(enc3) + LZString._keyStr.charAt(enc4); + + } + + return output; + }, + + decompressFromBase64 : function (input) { + if (input == null) return ""; + var output = "", + ol = 0, + output_, + chr1, chr2, chr3, + enc1, enc2, enc3, enc4, + i = 0, f=LZString._f; + + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); + + while (i < input.length) { + + enc1 = LZString._keyStr.indexOf(input.charAt(i++)); + enc2 = LZString._keyStr.indexOf(input.charAt(i++)); + enc3 = LZString._keyStr.indexOf(input.charAt(i++)); + enc4 = LZString._keyStr.indexOf(input.charAt(i++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + if (ol%2==0) { + output_ = chr1 << 8; + + if (enc3 != 64) { + output += f(output_ | chr2); + } + if (enc4 != 64) { + output_ = chr3 << 8; + } + } else { + output = output + f(output_ | chr1); + + if (enc3 != 64) { + output_ = chr2 << 8; + } + if (enc4 != 64) { + output += f(output_ | chr3); + } + } + ol+=3; + } + + return LZString.decompress(output); + + }, + + compressToUTF16 : function (input) { + if (input == null) return ""; + var output = "", + i,c, + current, + status = 0, + f = LZString._f; + + input = LZString.compress(input); + + for (i=0 ; i> 1)+32); + current = (c & 1) << 14; + break; + case 1: + output += f((current + (c >> 2))+32); + current = (c & 3) << 13; + break; + case 2: + output += f((current + (c >> 3))+32); + current = (c & 7) << 12; + break; + case 3: + output += f((current + (c >> 4))+32); + current = (c & 15) << 11; + break; + case 4: + output += f((current + (c >> 5))+32); + current = (c & 31) << 10; + break; + case 5: + output += f((current + (c >> 6))+32); + current = (c & 63) << 9; + break; + case 6: + output += f((current + (c >> 7))+32); + current = (c & 127) << 8; + break; + case 7: + output += f((current + (c >> 8))+32); + current = (c & 255) << 7; + break; + case 8: + output += f((current + (c >> 9))+32); + current = (c & 511) << 6; + break; + case 9: + output += f((current + (c >> 10))+32); + current = (c & 1023) << 5; + break; + case 10: + output += f((current + (c >> 11))+32); + current = (c & 2047) << 4; + break; + case 11: + output += f((current + (c >> 12))+32); + current = (c & 4095) << 3; + break; + case 12: + output += f((current + (c >> 13))+32); + current = (c & 8191) << 2; + break; + case 13: + output += f((current + (c >> 14))+32); + current = (c & 16383) << 1; + break; + case 14: + output += f((current + (c >> 15))+32, (c & 32767)+32); + status = 0; + break; + } + } + + return output + f(current + 32); + }, + + + decompressFromUTF16 : function (input) { + if (input == null) return ""; + var output = "", + current,c, + status=0, + i = 0, + f = LZString._f; + + while (i < input.length) { + c = input.charCodeAt(i) - 32; + + switch (status++) { + case 0: + current = c << 1; + break; + case 1: + output += f(current | (c >> 14)); + current = (c&16383) << 2; + break; + case 2: + output += f(current | (c >> 13)); + current = (c&8191) << 3; + break; + case 3: + output += f(current | (c >> 12)); + current = (c&4095) << 4; + break; + case 4: + output += f(current | (c >> 11)); + current = (c&2047) << 5; + break; + case 5: + output += f(current | (c >> 10)); + current = (c&1023) << 6; + break; + case 6: + output += f(current | (c >> 9)); + current = (c&511) << 7; + break; + case 7: + output += f(current | (c >> 8)); + current = (c&255) << 8; + break; + case 8: + output += f(current | (c >> 7)); + current = (c&127) << 9; + break; + case 9: + output += f(current | (c >> 6)); + current = (c&63) << 10; + break; + case 10: + output += f(current | (c >> 5)); + current = (c&31) << 11; + break; + case 11: + output += f(current | (c >> 4)); + current = (c&15) << 12; + break; + case 12: + output += f(current | (c >> 3)); + current = (c&7) << 13; + break; + case 13: + output += f(current | (c >> 2)); + current = (c&3) << 14; + break; + case 14: + output += f(current | (c >> 1)); + current = (c&1) << 15; + break; + case 15: + output += f(current | c); + status=0; + break; + } + + + i++; + } + + return LZString.decompress(output); + //return output; + + }, + + + + compress: function (uncompressed) { + if (uncompressed == null) return ""; + var i, value, + context_dictionary= {}, + context_dictionaryToCreate= {}, + context_c="", + context_wc="", + context_w="", + context_enlargeIn= 2, // Compensate for the first entry which should not count + context_dictSize= 3, + context_numBits= 2, + context_data_string="", + context_data_val=0, + context_data_position=0, + ii, + f=LZString._f; + + for (ii = 0; ii < uncompressed.length; ii += 1) { + context_c = uncompressed.charAt(ii); + if (!Object.prototype.hasOwnProperty.call(context_dictionary,context_c)) { + context_dictionary[context_c] = context_dictSize++; + context_dictionaryToCreate[context_c] = true; + } + + context_wc = context_w + context_c; + if (Object.prototype.hasOwnProperty.call(context_dictionary,context_wc)) { + context_w = context_wc; + } else { + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) { + if (context_w.charCodeAt(0)<256) { + for (i=0 ; i> 1; + } + } else { + value = 1; + for (i=0 ; i> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + delete context_dictionaryToCreate[context_w]; + } else { + value = context_dictionary[context_w]; + for (i=0 ; i> 1; + } + + + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + // Add wc to the dictionary. + context_dictionary[context_wc] = context_dictSize++; + context_w = String(context_c); + } + } + + // Output the code for w. + if (context_w !== "") { + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) { + if (context_w.charCodeAt(0)<256) { + for (i=0 ; i> 1; + } + } else { + value = 1; + for (i=0 ; i> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + delete context_dictionaryToCreate[context_w]; + } else { + value = context_dictionary[context_w]; + for (i=0 ; i> 1; + } + + + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + } + + // Mark the end of the stream + value = 2; + for (i=0 ; i> 1; + } + + // Flush the last char + while (true) { + context_data_val = (context_data_val << 1); + if (context_data_position == 15) { + context_data_string += f(context_data_val); + break; + } + else context_data_position++; + } + return context_data_string; + }, + + decompress: function (compressed) { + if (compressed == null) return ""; + if (compressed == "") return null; + var dictionary = [], + next, + enlargeIn = 4, + dictSize = 4, + numBits = 3, + entry = "", + result = "", + i, + w, + bits, resb, maxpower, power, + c, + f = LZString._f, + data = {string:compressed, val:compressed.charCodeAt(0), position:32768, index:1}; + + for (i = 0; i < 3; i += 1) { + dictionary[i] = i; + } + + bits = 0; + maxpower = Math.pow(2,2); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = 32768; + data.val = data.string.charCodeAt(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + switch (next = bits) { + case 0: + bits = 0; + maxpower = Math.pow(2,8); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = 32768; + data.val = data.string.charCodeAt(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + case 1: + bits = 0; + maxpower = Math.pow(2,16); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = 32768; + data.val = data.string.charCodeAt(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + case 2: + return ""; + } + dictionary[3] = c; + w = result = c; + while (true) { + if (data.index > data.string.length) { + return ""; + } + + bits = 0; + maxpower = Math.pow(2,numBits); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = 32768; + data.val = data.string.charCodeAt(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + switch (c = bits) { + case 0: + bits = 0; + maxpower = Math.pow(2,8); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = 32768; + data.val = data.string.charCodeAt(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + dictionary[dictSize++] = f(bits); + c = dictSize-1; + enlargeIn--; + break; + case 1: + bits = 0; + maxpower = Math.pow(2,16); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = 32768; + data.val = data.string.charCodeAt(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + dictionary[dictSize++] = f(bits); + c = dictSize-1; + enlargeIn--; + break; + case 2: + return result; + } + + if (enlargeIn == 0) { + enlargeIn = Math.pow(2, numBits); + numBits++; + } + + if (dictionary[c]) { + entry = dictionary[c]; + } else { + if (c === dictSize) { + entry = w + w.charAt(0); + } else { + return null; + } + } + result += entry; + + // Add w+entry[0] to the dictionary. + dictionary[dictSize++] = w + entry.charAt(0); + enlargeIn--; + + w = entry; + + if (enlargeIn == 0) { + enlargeIn = Math.pow(2, numBits); + numBits++; + } + + } + } +}; + +if( typeof module !== 'undefined' && module != null ) { + module.exports = LZString +} + +// https://gist.github.com/paulirish/5438650 + +// relies on Date.now() which has been supported everywhere modern for years. +// as Safari 6 doesn't have support for NavigationTiming, we use a Date.now() timestamp for relative values + +// if you want values similar to what you'd get with real perf.now, place this towards the head of the page +// but in reality, you're just getting the delta between now() calls, so it's not terribly important where it's placed + + +(function(){ + + // prepare base perf object + if (typeof window.performance === 'undefined') { + window.performance = {}; + } + + if (!window.performance.now){ + + var nowOffset = Date.now(); + + if (performance.timing && performance.timing.navigationStart){ + nowOffset = performance.timing.navigationStart + } + + + window.performance.now = function now(){ + return Date.now() - nowOffset; + } + + } + +})(); +function Recording (options){ + this.options = options || (options = {}); + this.loading = false; + this.timeBetweenLoops = options.timeBetweenLoops || 50; + + // see https://github.com/leapmotion/leapjs/blob/master/Leap_JSON.rst + this.packingStructure = [ + 'id', + 'timestamp', + // this should be replace/upgraded with a whitelist instead of a blacklist. + // leaving out r,s,y, and gestures + {hands: [[ + 'id', + 'type', + 'direction', + 'palmNormal', + 'palmPosition', + 'palmVelocity', + 'stabilizedPalmPosition', + 'pinchStrength', + 'grabStrength', + 'confidence', + 'armBasis', + 'armWidth', + 'elbow', + 'wrist' + // leaving out r, s, t, sphereCenter, sphereRadius + ]]}, + {pointables: [[ + 'id', + 'direction', + 'handId', + 'length', + 'stabilizedTipPosition', + 'tipPosition', + 'tipVelocity', + 'tool', + 'carpPosition', + 'mcpPosition', + 'pipPosition', + 'dipPosition', + 'btipPosition', + 'bases', + 'type' + // leaving out touchDistance, touchZone + ]]}, + {interactionBox: [ + 'center', 'size' + ]} + ]; + + this.setFrames(options.frames || []) +} + + +Recording.prototype = { + + setFrames: function (frames) { + this.frameData = frames; + this.frameIndex = 0; + this.frameCount = frames.length; + this.leftCropPosition = 0; + this.rightCropPosition = this.frameCount; + this.setMetaData(); + }, + + addFrame: function(frameData){ + this.frameData.push(frameData); + }, + + currentFrame: function () { + return this.frameData[this.frameIndex]; + }, + + nextFrame: function () { + var frameIndex = this.frameIndex + 1; + // || 1 to prevent `mod 0` error when finishing recording before setFrames has been called. + frameIndex = frameIndex % (this.rightCropPosition || 1); + if ((frameIndex < this.leftCropPosition)) { + frameIndex = this.leftCropPosition; + } + return this.frameData[frameIndex]; + }, + + + advanceFrame: function () { + this.frameIndex++; + + if (this.frameIndex >= this.rightCropPosition && !this.options.loop) { + this.frameIndex--; + // there is currently an issue where angular watching the right handle position + // will cause this to fire prematurely + // when switching to an earlier recording + return false + } + + + this.frameIndex = this.frameIndex % (this.rightCropPosition || 1); + + if ((this.frameIndex < this.leftCropPosition)) { + this.frameIndex = this.leftCropPosition; + } + + return true + }, + + // resets to beginning if at end + readyPlay: function(){ + this.frameIndex++; + if (this.frameIndex >= this.rightCropPosition) { + this.frameIndex = this.frameIndex % (this.rightCropPosition || 1); + + if ((this.frameIndex < this.leftCropPosition)) { + this.frameIndex = this.leftCropPosition; + } + }else{ + this.frameIndex--; + } + }, + + cloneCurrentFrame: function(){ + return JSON.parse(JSON.stringify(this.currentFrame())); + }, + + + // this method would be well-moved to its own object/class -.- + // for every point, lerp as appropriate + // note: currently hand and finger props are hard coded, but things like stabilizedPalmPosition should be optional + // should have this be set from the packingStructure or some such, but only for vec3s. + createLerpFrameData: function(t){ + // http://stackoverflow.com/a/5344074/478354 + var currentFrame = this.currentFrame(), + nextFrame = this.nextFrame(), + handProps = ['palmPosition', 'stabilizedPalmPosition', 'sphereCenter', 'direction', 'palmNormal', 'palmVelocity'], + fingerProps = ['mcpPosition', 'pipPosition', 'dipPosition', 'tipPosition', 'direction'], + frameData = this.cloneCurrentFrame(), + numHands = frameData.hands.length, + numPointables = frameData.pointables.length, + len1 = handProps.length, + len2 = fingerProps.length, + prop, hand, pointable; + + for (var i = 0; i < numHands; i++){ + hand = frameData.hands[i]; + + for (var j = 0; j < len1; j++){ + prop = handProps[j]; + + if (!currentFrame.hands[i][prop]){ + continue; + } + + if (!nextFrame.hands[i]){ + continue; + } + + Leap.vec3.lerp( + hand[prop], + currentFrame.hands[i][prop], + nextFrame.hands[i][prop], + t + ); + +// console.assert(hand[prop]); + } + + } + + for ( i = 0; i < numPointables; i++){ + pointable = frameData.pointables[i]; + + for ( j = 0; j < len2; j++){ + prop = fingerProps[j]; + + if (!currentFrame.pointables[i][prop]){ + continue; + } + + if (!nextFrame.hands[i]){ + continue; + } + + Leap.vec3.lerp( + pointable[prop], + currentFrame.pointables[i][prop], + nextFrame.pointables[i][prop], + 0 + ); +// console.assert(t >= 0 && t <= 1); +// if (t > 0) debugger; + + } + + } + + return frameData; + }, + + // returns ms + timeToNextFrame: function () { + var elapsedTime = (this.nextFrame().timestamp - this.currentFrame().timestamp) / 1000; + if (elapsedTime < 0) { + elapsedTime = this.timeBetweenLoops; //arbitrary pause at slightly less than 30 fps. + } +// console.assert(!isNaN(elapsedTime)); + return elapsedTime; + }, + + + blank: function(){ + return this.frameData.length === 0; + }, + + // sets the crop-point of the current recording to the current position. + leftCrop: function () { + this.leftCropPosition = this.frameIndex + }, + + // sets the crop-point of the current recording to the current position. + rightCrop: function () { + this.rightCropPosition = this.frameIndex + }, + + // removes every other frame from the array + // Accepts an optional `factor` integer, which is the number of frames + // discarded for every frame kept. + cullFrames: function (factor) { + console.log('cull frames', factor); + factor || (factor = 1); + for (var i = 0; i < this.frameData.length; i++) { + this.frameData.splice(i, factor); + } + this.setMetaData(); + }, + + // Returns the average frames per second of the recording + frameRate: function () { + if (this.frameData.length == 0) { + return 0 + } + return this.frameData.length / (this.frameData[this.frameData.length - 1].timestamp - this.frameData[0].timestamp) * 1000000; + }, + + // returns frames without any circular references + croppedFrameData: function () { + return this.frameData.slice(this.leftCropPosition, this.rightCropPosition); + }, + + + setMetaData: function () { + + var newMetaData = { + formatVersion: 2, + generatedBy: 'LeapJS Playback 0.2.0', + frames: this.rightCropPosition - this.leftCropPosition, + protocolVersion: this.options.requestProtocolVersion, + serviceVersion: this.options.serviceVersion, + frameRate: this.frameRate().toPrecision(2), + modified: (new Date).toString() + }; + + this.metadata || (this.metadata = {}); + + for (var key in newMetaData) { + this.metadata[key] = newMetaData[key]; + } + }, + + // returns an array + // the first item is the keys of the following items + // nested arrays are expected to have idententical siblings + packedFrameData: function(){ + var frameData = this.croppedFrameData(), + packedFrames = [], + frameDatum; + + packedFrames.push(this.packingStructure); + + for (var i = 0, len = frameData.length; i < len; i++){ + frameDatum = frameData[i]; + + packedFrames.push( + this.packArray( + this.packingStructure, + frameDatum + ) + ); + + } + + return packedFrames; + }, + + // recursive method + // creates a structure of frame data matching packing structure + // there may be an issue here where hands/pointables are wrapped in one more array than necessary + packArray: function(structure, data){ + var out = [], nameOrHash; + + for (var i = 0, len1 = structure.length; i < len1; i++){ + + // e.g., nameOrHash is either 'id' or {hand: [...]} + nameOrHash = structure[i]; + + if ( typeof nameOrHash === 'string'){ + + out.push( + data[nameOrHash] + ); + + }else if (Object.prototype.toString.call(nameOrHash) == "[object Array]") { + // nested array, such as hands or fingers + + for (var j = 0, len2 = data.length; j < len2; j++){ + out.push( + this.packArray( + nameOrHash, + data[j] + ) + ); + } + + } else { // key-value (nested object) such as interactionBox + +// console.assert(nameOrHash); + + for (var key in nameOrHash) break; + +// console.assert(key); +// console.assert(nameOrHash[key]); +// console.assert(data[key]); + + out.push(this.packArray( + nameOrHash[key], + data[key] + )); + + } + + } + + return out; + }, + + // expects the first array element to describe the following arrays + // this algorithm copies frames to a new array + // could there be merit in something which would do an in-place substitution? + unPackFrameData: function(packedFrames){ + var packingStructure = packedFrames[0]; + var frameData = [], + frameDatum; + + for (var i = 1, len = packedFrames.length; i < len; i++) { + frameDatum = packedFrames[i]; + frameData.push( + this.unPackArray( + packingStructure, + frameDatum + ) + ); + + } + + return frameData; + }, + + // data is a frame or subset of frame + // returns a frame object + // this is the structure of the array + // gets unfolded to key-value pairs + // e.g.: + // this.packingStructure = [ + // 'id', + // 'timestamp', + // {hands: [[ + // 'id', + // 'direction', + // 'palmNormal', + // 'palmPosition', + // 'palmVelocity' + // ]]}, + // {pointables: [[ + // 'direction', + // 'handId', + // 'length', + // 'stabilizedTipPosition', + // 'tipPosition', + // 'tipVelocity', + // 'tool' + // ]]}, + // {interactionBox: [ + // 'center', 'size' + // ]} + // ]; + unPackArray: function(structure, data){ + var out = {}, nameOrHash; + + for (var i = 0, len1 = structure.length; i < len1; i++){ + + // e.g., nameOrHash is either 'id' or {hand: [...]} + nameOrHash = structure[i]; + + if ( typeof nameOrHash === 'string'){ + + out[nameOrHash] = data[i]; + + }else if (Object.prototype.toString.call(nameOrHash) == "[object Array]") { + // nested array, such as hands or fingers + // nameOrHash ["id", "direction", "palmNormal", "palmPosition", "palmVelocity"] + // data [ [ 31, [vec3], [vec3], ...] ] + + var subArray = []; + + for (var j = 0, len2 = data.length; j < len2; j++){ + subArray.push( + this.unPackArray( + nameOrHash, + data[j] + ) + ); + } + return subArray; + + } else { // key-value (nested object) such as interactionBox + + for (var key in nameOrHash) break; + + out[key] = this.unPackArray( + nameOrHash[key], + data[i] + ); + + } + + } + + return out; + }, + + toHash: function () { + this.setMetaData(); + return { + metadata: this.metadata, + frames: this.packedFrameData() + } + }, + + // Returns the cropped data as JSON or compressed + // http://pieroxy.net/blog/pages/lz-string/index.html + export: function (format) { + var json = JSON.stringify(this.toHash()); + + if (format == 'json') return json; + + return LZString.compressToBase64(json); + }, + + save: function(format){ + var filename; + + filename = this.metadata.title ? this.metadata.title.replace(/\s/g, '') : 'leap-playback-recording'; + + if (this.metadata.frameRate) { + filename += "-" + (Math.round(this.metadata.frameRate)) + "fps"; + } + + if (format === 'json') { + + saveAs(new Blob([this.export('json')], { + type: "text/JSON;charset=utf-8" + }), filename + ".json"); + + } else { + + saveAs(new Blob([this.export('lz')], { + type: "application/x-gzip;charset=utf-8" + }), filename + ".json.lz"); + + } + + }, + + decompress: function (data) { + return LZString.decompressFromBase64(data); + }, + + loaded: function(){ + return !!(this.frameData && this.frameData.length) + }, + + + // optional callback once frames are loaded, will have a context of player + loadFrameData: function (callback) { + var xhr = new XMLHttpRequest(), + url = this.url, + recording = this, + contentLength = 0; + + xhr.onreadystatechange = function () { + if (xhr.readyState === xhr.DONE) { + if (xhr.status === 200 || xhr.status === 0) { + if (xhr.responseText) { + + recording.finishLoad(xhr.responseText, callback); + + } else { + console.error('Leap Playback: "' + url + '" seems to be unreachable or the file is empty.'); + } + } else { + console.error('Leap Playback: Couldn\'t load "' + url + '" (' + xhr.status + ')'); + } + } + }; + + xhr.addEventListener('progress', function(oEvent){ + + if ( recording.options.loadProgress ) { + + if (oEvent.lengthComputable) { + var percentComplete = oEvent.loaded / oEvent.total; + recording.options.loadProgress( recording, percentComplete, oEvent ); + } + + } + + }); + + this.loading = true; + + xhr.open("GET", url, true); + xhr.send(null); + }, + + finishLoad: function(responseData, callback){ + + var url = this.url; + + if (url.split('.')[url.split('.').length - 1] == 'lz') { + responseData = this.decompress(responseData); + } + + responseData = JSON.parse(responseData); + + if (responseData.metadata.formatVersion == 2) { + responseData.frames = this.unPackFrameData(responseData.frames); + } + + this.metadata = responseData.metadata; + + console.log('Recording loaded:', this.metadata); + + this.loading = false; + + if (callback) { + callback.call(this, responseData.frames); + } + + } + +}; +(function () { + var CONNECT_LEAP_ICON = ''; + var MOVE_HAND_OVER_LEAP_ICON = ''; + + + function Player(controller, options) { + var player = this; + options || (options = {}); + +// this.frameData = []; + + this.options = options; + this.recording = options.recording; + + this.controller = controller; + this.resetTimers(); + this.setupLoops(); + this.controller.connection.on('ready', function () { + player.setupProtocols(); + }); + + this.userHasControl = false; + + + if (options.recording) { + if (Object.prototype.toString.call(options.recording) == '[object String]') { + options.recording = { + url: options.recording + } + } + this.setRecording(options.recording); + } + + document.addEventListener("DOMContentLoaded", function(event) { + document.body.addEventListener('keydown', function (e) { + if (e.which === player.options.pauseHotkey) { + player.toggle(); + } + }, false); + }); + + } + + Player.prototype = { + resetTimers: function (){ + this.timeSinceLastFrame = 0; + this.lastFrameTime = null; + }, + + setupLoops: function () { + var player = this; + + // Loop with explicit frame timing + this.stepFrameLoop = function (timestamp) { + if (player.state != 'playing') return; + + player.sendFrameAt(timestamp || performance.now()); + + requestAnimationFrame(player.stepFrameLoop); + }; + + + }, + + // This is how we intercept frame data early + // By hooking in before Frame creation, we get data exactly as the frame sends it. + setupProtocols: function () { + var player = this; + // This is the original normal protocol, used while in record mode but not recording. + this.stopProtocol = this.controller.connection.protocol; + + // This consumes all frame data, making the device act as if not streaming + this.playbackProtocol = function (data) { + // The old protocol still needs to emit events, so we use it, but intercept Frames + var eventOrFrame = player.stopProtocol(data); + if (eventOrFrame instanceof Leap.Frame) { + + if (player.pauseOnHand) { + if (data.hands.length > 0) { + player.userHasControl = true; + player.controller.emit('playback.userTakeControl'); + player.setGraphic(); + player.idle(); + } else if (data.hands.length == 0) { + if (player.userHasControl && player.resumeOnHandLost) { + player.userHasControl = false; + player.controller.emit('playback.userReleaseControl'); + player.setGraphic('wave'); + } + + } + } + + // prevent the actual frame from getting through + return {type: 'playback'} + } else { + return eventOrFrame; + } + }; + + // This pushes frame data, and watches for hands to auto change state. + // Returns the eventOrFrame without modifying it. + this.recordProtocol = function (data) { + var eventOrFrame = player.stopProtocol(data); + if (eventOrFrame instanceof Leap.Frame) { + player.recordFrameHandler(data); + } + return eventOrFrame; + }; + + // Copy methods/properties from the default protocol over + for (var property in this.stopProtocol) { + if (this.stopProtocol.hasOwnProperty(property)) { + this.playbackProtocol[property] = this.stopProtocol[property] + this.recordProtocol[property] = this.stopProtocol[property] + } + } + + // todo: this is messy. Should cover all cases, not just active playback! + if (this.state == 'playing') { + this.controller.connection.protocol = this.playbackProtocol + } + }, + + + + // Adds playback = true to artificial frames + sendFrameAt: function (now) { + + if (this.lastFrameTime){ + // chrome bug, see: https://code.google.com/p/chromium/issues/detail?id=268213 + // http://jsfiddle.net/pehrlich/35pTx/ + // console.assert(this.lastFrameTime < now); + if (now < this.lastFrameTime){ + // this fix will cause an extra animation frame before the lerp frame advances. no big. + this.lastFrameTime = now; + }else{ + this.timeSinceLastFrame += (now - this.lastFrameTime); + } + } + + this.lastFrameTime = now; + +// console.assert(!isNaN(this.timeSinceLastFrame)); + + + var timeToNextFrame; + + // handle frame dropping, etc + while ( this.timeSinceLastFrame > ( timeToNextFrame = this.recording.timeToNextFrame() ) ){ + this.timeSinceLastFrame -= timeToNextFrame; + if (!this.recording.advanceFrame()){ + this.pause(); + this.controller.emit('playback.playbackFinished', this); + return + } + } + + this.sendFrame( + this.recording.createLerpFrameData(this.timeSinceLastFrame / timeToNextFrame) + ); + + }, + + sendFrame: function(frameData){ + if (!frameData) throw "Frame data not provided"; + + var frame = new Leap.Frame(frameData); + + // send a deviceFrame to the controller: + // this frame gets picked up by the controllers own animation loop. + + this.controller.processFrame(frame); + return true + }, + + sendImmediateFrame: function(frameData){ + if (!frameData) throw "Frame data not provided"; + + var frame = new Leap.Frame(frameData); + + // sends an animation frame to the controller + + this.controller.processFinishedFrame(frame); + return true + }, + + setFrameIndex: function (frameIndex) { + if (frameIndex != this.recording.frameIndex) { + this.recording.frameIndex = frameIndex % this.recording.frameCount; + this.sendFrame(this.recording.currentFrame()); + } + }, + + + // used after record + stop: function () { + this.idle(); + + delete this.recording; + + this.recording = new Recording({ + timeBetweenLoops: this.options.timeBetweenLoops, + loop: this.options.loop, + requestProtocolVersion: this.controller.connection.opts.requestProtocolVersion, + serviceVersion: this.controller.connection.protocol.serviceVersion + }); + + this.controller.emit('playback.stop', this); + }, + + // used after play + pause: function () { + // todo: we should change this idle state to paused or leave it as playback with a pause flag + // state should correspond always to protocol handler (through a setter)? + this.state = 'idle'; + this.hideOverlay(); + this.controller.emit('playback.pause', this); + }, + + idle: function () { + this.state = 'idle'; + this.controller.connection.protocol = this.stopProtocol; + }, + + toggle: function () { + if (this.state == 'idle') { + this.play(); + } else if (this.state == 'playing') { + this.pause(); + } + }, + + // switches to record mode, which will be begin capturing data when a hand enters the frame, + // and stop when a hand leaves + // Todo: replace frameData with a full fledged recording, including metadata. + record: function () { + this.clear(); + this.stop(); + this.state = 'recording'; + this.controller.connection.protocol = this.recordProtocol; + this.setGraphic('connect'); + this.controller.emit('playback.record', this) + }, + + // if there is existing frame data, sends a frame with nothing in it + clear: function () { + if (!this.recording || this.recording.blank()) return; + var finalFrame = this.recording.cloneCurrentFrame(); + finalFrame.hands = []; + finalFrame.fingers = []; + finalFrame.pointables = []; + finalFrame.tools = []; + this.sendImmediateFrame(finalFrame); + }, + + recordPending: function () { + return this.state == 'recording' && this.recording.blank() + }, + + isRecording: function () { + return this.state == 'recording' && !this.recording.blank() + }, + + finishRecording: function () { + // change to the playbackHandler which suppresses frames: + this.controller.connection.protocol = this.playbackProtocol; + this.recording.setFrames(this.recording.frameData); + this.controller.emit('playback.recordingFinished', this) + }, + + + loaded: function () { + return this.recording.loaded(); + }, + + loading: function(){ + return this.recording.loading; + }, + + + /* Plays back the provided frame data + * Params {object|boolean}: + * - frames: previously recorded frame json + * - loop: whether or not to loop playback. Defaults to true. + */ + play: function () { + if (this.state === 'playing') return; + if ( this.loading() || this.recording.blank() ) return; + + this.state = 'playing'; + this.controller.connection.protocol = this.playbackProtocol; + + var player = this; + + // prevent the normal controller response while playing + this.controller.connection.removeAllListeners('frame'); + this.controller.connection.on('frame', function (frame) { + // resume play when hands are removed: + if (player.resumeOnHandLost && player.autoPlay && player.state == 'idle' && frame.hands.length == 0) { + player.play(); + } + + // The default LeapJS callback processes the frame, which is what we do now: + player.controller.processFrame(frame); + }); + + // Kick off + this.resetTimers(); + this.recording.readyPlay(); + this.stepFrameLoop(); + + this.controller.emit('playback.play', this); + }, + + // this method replaces connection.handleData when in record mode + // It accepts the raw connection data which is used to make a frame. + recordFrameHandler: function (frameData) { + // Would be better to check controller.streaming() in showOverlay, but that method doesn't exist, yet. + this.setGraphic('wave'); + if (frameData.hands.length > 0) { + this.recording.addFrame(frameData); + this.hideOverlay(); + } else if ( !this.recording.blank() ) { + this.finishRecording(); + } + }, + + + // Accepts a hash with any of + // URL, recording, metadata + // once loaded, the recording is immediately activated + setRecording: function (options) { + var player = this; + + // otherwise, the animation loop may try and play non-existant frames: + this.pause(); + + // this is called on the context of the recording + var loadComplete = function (frames) { + + this.setFrames(frames); + + if (player.recording != this){ + console.log('recordings changed during load'); + return + } + + if (player.autoPlay) { + player.play(); + if (player.pauseOnHand && !player.controller.streaming() ) { + player.setGraphic('connect'); + } + } + + player.controller.emit('playback.recordingSet', this); + }; + + this.recording = options; + + // Here we turn the existing argument in to a recording + // this allows frames to be added to the existing object via ajax + // saving ajax requests + if (!(options instanceof Recording)){ + + this.recording.__proto__ = Recording.prototype; + Recording.call(this.recording, { + timeBetweenLoops: this.options.timeBetweenLoops, + loop: this.options.loop, + loadProgress: function(recording, percentage, oEvent){ + player.controller.emit('playback.ajax:progress', recording, percentage, oEvent); + } + }); + + } + + + if ( this.recording.loaded() ) { + + loadComplete.call(this.recording, this.recording.frameData); + + } else if (options.url) { + + this.controller.emit('playback.ajax:begin', this, this.recording); + + // called in the context of the recording + this.recording.loadFrameData(function(frames){ + loadComplete.call(this, frames); + player.controller.emit('playback.ajax:complete', player, this); + }); + + } + + + return this; + }, + + + hideOverlay: function () { + if (!this.overlay) return; + this.overlay.style.display = 'none'; + }, + + + // Accepts either "connect", "wave", or undefined. + setGraphic: function (graphicName) { + if (!this.overlay) return; + if (this.graphicName == graphicName) return; + + this.graphicName = graphicName; + switch (graphicName) { + case 'connect': + this.overlay.style.display = 'block'; + this.overlay.innerHTML = CONNECT_LEAP_ICON; + break; + case 'wave': + this.overlay.style.display = 'block'; + this.overlay.innerHTML = MOVE_HAND_OVER_LEAP_ICON; + break; + case undefined: + this.overlay.innerHTML = ''; + break; + } + } + + }; + + // will only play back if device is disconnected + // Accepts options: + // - frames: [string] URL of .json frame data + // - autoPlay: [boolean true] Whether to turn on and off playback based off of connection state + // - overlay: [boolean or DOM element] Whether or not to show the overlay: "Connect your Leap Motion Controller" + // if a DOM element is passed, that will be shown/hidden instead of the default message. + // - pauseOnHand: [boolean true] Whether to stop playback when a hand is in field of view + // - resumeOnHandLost: [boolean true] Whether to resume playback after the hand leaves the frame + // - requiredProtocolVersion: clients connected with a lower protocol number will not be able to take control of the + // - timeBetweenLoops: [number, ms] delay between looping playback + // controller with their device. This option, if set, ovverrides autoPlay + // - pauseHotkey: [number or false, default: 32 (spacebar)] - keycode for pause, bound to body + var playback = function (scope) { + var controller = this; + var autoPlay = scope.autoPlay; + if (autoPlay === undefined) autoPlay = true; + + var pauseOnHand = scope.pauseOnHand; + if (pauseOnHand === undefined) pauseOnHand = true; + + var resumeOnHandLost = scope.resumeOnHandLost; + if (resumeOnHandLost === undefined) resumeOnHandLost = true; + + var timeBetweenLoops = scope.timeBetweenLoops; + if (timeBetweenLoops === undefined) timeBetweenLoops = 50; + + var requiredProtocolVersion = scope.requiredProtocolVersion; + + var pauseHotkey = scope.pauseHotkey; + if (pauseHotkey === undefined) pauseHotkey = 32; // spacebar + + var loop = scope.loop; + if (loop === undefined) loop = true; + + var overlay = scope.overlay; + // A better fix would be to set an onload handler for this, rather than disable the overlay. + if (overlay === undefined && document.body) { + overlay = document.createElement('div'); + document.body.appendChild(overlay); + overlay.style.width = '100%'; + overlay.style.position = 'absolute'; + overlay.style.top = '0'; + overlay.style.left = '-' + window.getComputedStyle(document.body).getPropertyValue('margin'); + overlay.style.padding = '10px'; + overlay.style.textAlign = 'center'; + overlay.style.fontSize = '18px'; + overlay.style.opacity = '0.8'; + overlay.style.display = 'none'; + overlay.style.zIndex = '10'; + overlay.id = 'connect-leap'; + overlay.style.cursor = 'pointer'; + overlay.addEventListener("click", function () { + this.style.display = 'none'; + return false; + }, false); + + } + + + scope.player = new Player(this, { + recording: scope.recording, + loop: loop, + pauseHotkey: pauseHotkey, + timeBetweenLoops: timeBetweenLoops + }); + + // By doing this, we allow player methods to be accessible on the scope + // this is the controller + scope.player.overlay = overlay; + scope.player.pauseOnHand = pauseOnHand; + scope.player.resumeOnHandLost = resumeOnHandLost; + scope.player.requiredProtocolVersion = requiredProtocolVersion; + scope.player.autoPlay = autoPlay; + + var setupStreamingEvents = function () { + if (scope.player.pauseOnHand && controller.connection.opts.requestProtocolVersion < scope.requiredProtocolVersion) { + console.log('Protocol Version too old (' + controller.connection.opts.requestProtocolVersion + '), disabling device interaction.'); + scope.player.pauseOnHand = false; + return + } + + if (autoPlay) { + controller.on('streamingStarted', function () { + if (scope.player.state == 'recording') { + scope.player.pause(); + scope.player.setGraphic('wave'); + } else { + if (pauseOnHand) { + scope.player.setGraphic('wave'); + } else { + scope.player.setGraphic(); + } + } + }); + + controller.on('streamingStopped', function () { + scope.player.play(); + }); + } + controller.on('streamingStopped', function () { + scope.player.setGraphic('connect'); + }); + } + + // ready happens before streamingStarted, allowing us to check the version before responding to streamingStart/Stop + // we can't call this any earlier, or protcol version won't be available + if (!!this.connection.connected) { + setupStreamingEvents() + } else { + this.on('ready', setupStreamingEvents) + } + + return {} + } + + + if ((typeof Leap !== 'undefined') && Leap.Controller) { + Leap.Controller.plugin('playback', playback); + } else if (typeof module !== 'undefined') { + module.exports.playback = playback; + } else { + throw 'leap.js not included'; + } + +}).call(this); +}( window )); +//CoffeeScript generated from main/screen-position/leap.screen-position.coffee +/* +Adds the "screenPosition" method by default to hands and pointables. This returns a vec3 (an array of length 3) +with [x,y,z] screen coordinates indicating where the hand is, originating from the bottom left. +This method can accept an optional vec3, allowing it to convert any arbitrary vec3 of coordinates. + +Custom positioning methods can be passed in, allowing different scaling techniques, +e.g., http://msdn.microsoft.com/en-us/library/windows/hardware/gg463319.aspx (Pointer Ballistics) +Here we scale based upon the interaction box and screen size: + +options: + scale, scaleX, and scaleY. They all default to 1. + verticalOffset: in pixels. This number is added to the returned Y value. Defaults to 0. + + + +controller.use 'screenPosition', { + method: (positionVec3)-> + Arguments for Leap.vec3 are (out, a, b) + [ + Leap.vec3.subtract(positionVec3, positionVec3, @frame.interactionBox.center) + Leap.vec3.divide(positionVec3, positionVec3, @frame.interactionBox.size) + Leap.vec3.multiply(positionVec3, positionVec3, [document.body.offsetWidth, document.body.offsetHeight, 0]) + ] +} +More info on vec3 can be found, here: http://glmatrix.net/docs/2.2.0/symbols/vec3.html +*/ + + +(function() { + var screenPosition; + + screenPosition = function(options) { + var baseScale, baseVerticalOffset, position, positioningMethods; + if (options == null) { + options = {}; + } + options.positioning || (options.positioning = 'absolute'); + options.scale || (options.scale = 1); + options.scaleX || (options.scaleX = 1); + options.scaleY || (options.scaleY = 1); + options.scaleZ || (options.scaleZ = 1); + options.verticalOffset || (options.verticalOffset = 0); + baseScale = 6; + baseVerticalOffset = -100; + positioningMethods = { + absolute: function(positionVec3) { + return [(window.innerWidth / 2) + (positionVec3[0] * baseScale * options.scale * options.scaleX), window.innerHeight + baseVerticalOffset + options.verticalOffset - (positionVec3[1] * baseScale * options.scale * options.scaleY), positionVec3[2] * baseScale * options.scale * options.scaleZ]; + } + }; + position = function(vec3, memoize) { + var screenPositionVec3; + if (memoize == null) { + memoize = false; + } + screenPositionVec3 = typeof options.positioning === 'function' ? options.positioning.call(this, vec3) : positioningMethods[options.positioning].call(this, vec3); + if (memoize) { + this.screenPositionVec3 = screenPositionVec3; + } + return screenPositionVec3; + }; + return { + hand: { + screenPosition: function(vec3) { + return position.call(this, vec3 || this.palmPosition, !vec3); + } + }, + pointable: { + screenPosition: function(vec3) { + return position.call(this, vec3 || this.tipPosition, !vec3); + } + } + }; + }; + + if ((typeof Leap !== 'undefined') && Leap.Controller) { + Leap.Controller.plugin('screenPosition', screenPosition); + } else if (typeof module !== 'undefined') { + module.exports.screenPosition = screenPosition; + } else { + throw 'leap.js not included'; + } + +}).call(this); + +//CoffeeScript generated from main/transform/leap.transform.coffee +(function() { + var __slice = [].slice; + + Leap.plugin('transform', function(scope) { + var noop, transformDirections, transformMat4Implicit0, transformPositions, transformWithMatrices, _directionTransform; + if (scope == null) { + scope = {}; + } + noop = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; + _directionTransform = new THREE.Matrix4; + if (scope.vr === true) { + this.setOptimizeHMD(true); + scope.quaternion = (new THREE.Quaternion).setFromRotationMatrix((new THREE.Matrix4).set(-1, 0, 0, 0, 0, 0, -1, 0, 0, -1, 0, 0, 0, 0, 0, 1)); + scope.scale = 0.001; + scope.position = new THREE.Vector3(0, 0, -0.08); + } + scope.getTransform = function(hand) { + var matrix; + if (scope.matrix) { + matrix = typeof scope.matrix === 'function' ? scope.matrix(hand) : scope.matrix; + if (window['THREE'] && matrix instanceof THREE.Matrix4) { + return matrix.elements; + } else { + return matrix; + } + } else if (scope.position || scope.quaternion || scope.scale) { + _directionTransform.set.apply(_directionTransform, noop); + if (scope.quaternion) { + _directionTransform.makeRotationFromQuaternion(typeof scope.quaternion === 'function' ? scope.quaternion(hand) : scope.quaternion); + } + if (scope.position) { + _directionTransform.setPosition(typeof scope.position === 'function' ? scope.position(hand) : scope.position); + } + return _directionTransform.elements; + } else { + return noop; + } + }; + scope.getScale = function(hand) { + if (!isNaN(scope.scale)) { + scope.scale = new THREE.Vector3(scope.scale, scope.scale, scope.scale); + } + if (typeof scope.scale === 'function') { + return scope.scale(hand); + } else { + return scope.scale; + } + }; + transformPositions = function() { + var matrix, vec3, vec3s, _i, _len, _results; + matrix = arguments[0], vec3s = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + _results = []; + for (_i = 0, _len = vec3s.length; _i < _len; _i++) { + vec3 = vec3s[_i]; + if (vec3) { + _results.push(Leap.vec3.transformMat4(vec3, vec3, matrix)); + } else { + _results.push(void 0); + } + } + return _results; + }; + transformMat4Implicit0 = function(out, a, m) { + var x, y, z; + x = a[0]; + y = a[1]; + z = a[2]; + out[0] = m[0] * x + m[4] * y + m[8] * z; + out[1] = m[1] * x + m[5] * y + m[9] * z; + out[2] = m[2] * x + m[6] * y + m[10] * z; + return out; + }; + transformDirections = function() { + var matrix, vec3, vec3s, _i, _len, _results; + matrix = arguments[0], vec3s = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + _results = []; + for (_i = 0, _len = vec3s.length; _i < _len; _i++) { + vec3 = vec3s[_i]; + if (vec3) { + _results.push(transformMat4Implicit0(vec3, vec3, matrix)); + } else { + _results.push(void 0); + } + } + return _results; + }; + transformWithMatrices = function(hand, transform, scale) { + var finger, scalarScale, _i, _j, _len, _len1, _ref, _ref1; + transformDirections(transform, hand.direction, hand.palmNormal, hand.palmVelocity, hand.arm.basis[0], hand.arm.basis[1], hand.arm.basis[2]); + _ref = hand.fingers; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + finger = _ref[_i]; + transformDirections(transform, finger.direction, finger.metacarpal.basis[0], finger.metacarpal.basis[1], finger.metacarpal.basis[2], finger.proximal.basis[0], finger.proximal.basis[1], finger.proximal.basis[2], finger.medial.basis[0], finger.medial.basis[1], finger.medial.basis[2], finger.distal.basis[0], finger.distal.basis[1], finger.distal.basis[2]); + } + Leap.glMatrix.mat4.scale(transform, transform, scale); + transformPositions(transform, hand.palmPosition, hand.stabilizedPalmPosition, hand.sphereCenter, hand.arm.nextJoint, hand.arm.prevJoint); + _ref1 = hand.fingers; + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + finger = _ref1[_j]; + transformPositions(transform, finger.carpPosition, finger.mcpPosition, finger.pipPosition, finger.dipPosition, finger.distal.nextJoint, finger.tipPosition); + } + scalarScale = (scale[0] + scale[1] + scale[2]) / 3; + return hand.arm.width *= scalarScale; + }; + return { + hand: function(hand) { + var finger, len, _i, _len, _ref; + transformWithMatrices(hand, scope.getTransform(hand), (scope.getScale(hand) || new THREE.Vector3(1, 1, 1)).toArray()); + if (scope.effectiveParent) { + transformWithMatrices(hand, scope.effectiveParent.matrixWorld.elements, scope.effectiveParent.scale.toArray()); + } + len = null; + _ref = hand.fingers; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + finger = _ref[_i]; + len = Leap.vec3.create(); + Leap.vec3.sub(len, finger.mcpPosition, finger.carpPosition); + finger.metacarpal.length = Leap.vec3.length(len); + Leap.vec3.sub(len, finger.pipPosition, finger.mcpPosition); + finger.proximal.length = Leap.vec3.length(len); + Leap.vec3.sub(len, finger.dipPosition, finger.pipPosition); + finger.medial.length = Leap.vec3.length(len); + Leap.vec3.sub(len, finger.tipPosition, finger.dipPosition); + finger.distal.length = Leap.vec3.length(len); + } + Leap.vec3.sub(len, hand.arm.prevJoint, hand.arm.nextJoint); + return hand.arm.length = Leap.vec3.length(len); + } + }; + }); + +}).call(this); + +//CoffeeScript generated from main/version-check/leap.version-check.coffee +(function() { + var versionCheck; + + versionCheck = function(scope) { + scope.alert || (scope.alert = false); + scope.requiredProtocolVersion || (scope.requiredProtocolVersion = 6); + scope.disconnect || (scope.disconnect = true); + if ((typeof Leap !== 'undefined') && Leap.Controller) { + if (Leap.version.minor < 5 && Leap.version.dot < 4) { + console.warn("LeapJS Version Check plugin incompatible with LeapJS pre 0.4.4"); + } + } + this.on('ready', function() { + var current, message, required; + required = scope.requiredProtocolVersion; + current = this.connection.opts.requestProtocolVersion; + if (current < required) { + message = "Protocol Version too old. v" + required + " required, v" + current + " available."; + if (scope.disconnect) { + this.disconnect(); + message += " Disconnecting."; + } + console.warn(message); + if (scope.alert) { + alert("Your Leap Software version is out of date. Visit http://www.leapmotion.com/setup to update"); + } + return this.emit('versionCheck.outdated', { + required: required, + current: current, + disconnect: scope.disconnect + }); + } + }); + return {}; + }; + + if ((typeof Leap !== 'undefined') && Leap.Controller) { + Leap.Controller.plugin('versionCheck', versionCheck); + } else if (typeof module !== 'undefined') { + module.exports.versionCheck = versionCheck; + } else { + throw 'leap.js not included'; + } + +}).call(this); diff --git a/v2/cloth/js/leap-plugins-0.1.9pre.js b/v2/cloth/js/leap-plugins-0.1.9pre.js new file mode 100644 index 0000000..913c3c6 --- /dev/null +++ b/v2/cloth/js/leap-plugins-0.1.9pre.js @@ -0,0 +1,2491 @@ +/* + * LeapJS-Plugins - v0.1.8 - 2014-10-21 + * http://github.com/leapmotion/leapjs-plugins/ + * + * Copyright 2014 LeapMotion, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +//CoffeeScript generated from main/bone-hand/leap.bone-hand.coffee +(function() { + var baseBoneRotation, boneColor, boneHand, boneHandLost, boneRadius, boneScale, initScene, jointColor, jointRadius, jointScale, material, scope; + + scope = null; + + initScene = function(targetEl) { + var camera, directionalLight, height, render, renderer, width; + scope.scene = new THREE.Scene(); + scope.renderer = renderer = new THREE.WebGLRenderer({ + alpha: true + }); + width = window.innerWidth; + height = window.innerHeight; + renderer.setClearColor(0x000000, 0); + renderer.setSize(width, height); + renderer.domElement.className = "leap-boneHand"; + targetEl.appendChild(renderer.domElement); + directionalLight = directionalLight = new THREE.DirectionalLight(0xffffff, 1); + directionalLight.position.set(0, 0.5, 1); + scope.scene.add(directionalLight); + directionalLight = directionalLight = new THREE.DirectionalLight(0xffffff, 1); + directionalLight.position.set(0.5, -0.5, -1); + scope.scene.add(directionalLight); + directionalLight = directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); + directionalLight.position.set(-0.5, 0, -0.2); + scope.scene.add(directionalLight); + scope.camera = camera = new THREE.PerspectiveCamera(45, width / height, 1, 10000); + camera.position.fromArray([0, 300, 500]); + camera.lookAt(new THREE.Vector3(0, 160, 0)); + scope.scene.add(camera); + renderer.render(scope.scene, camera); + window.addEventListener('resize', function() { + width = window.innerWidth; + height = window.innerHeight; + camera.aspect = width / height; + camera.updateProjectionMatrix(); + renderer.setSize(width, height); + return renderer.render(scope.scene, camera); + }, false); + render = function() { + renderer.render(scope.scene, camera); + return window.requestAnimationFrame(render); + }; + return render(); + }; + + baseBoneRotation = (new THREE.Quaternion).setFromEuler(new THREE.Euler(Math.PI / 2, 0, 0)); + + jointColor = (new THREE.Color).setHex(0x5daa00); + + boneColor = (new THREE.Color).setHex(0xffffff); + + boneScale = 1 / 6; + + jointScale = 1 / 5; + + boneRadius = null; + + jointRadius = null; + + material = null; + + boneHand = function(hand) { + var armBones, armMesh, armTopAndBottomRotation, boneXOffset, halfArmLength, i, _i, _j; + if (!scope.scene) { + return; + } + hand.fingers.forEach(function(finger) { + var boneMeshes, jointMesh, jointMeshes; + boneMeshes = finger.data("boneMeshes"); + jointMeshes = finger.data("jointMeshes"); + if (!boneMeshes) { + boneMeshes = []; + jointMeshes = []; + material = !isNaN(scope.opacity) ? new THREE.MeshPhongMaterial({ + transparent: true, + opacity: scope.opacity + }) : new THREE.MeshPhongMaterial(); + boneRadius = hand.middleFinger.proximal.length * boneScale; + jointRadius = hand.middleFinger.proximal.length * jointScale; + if (!finger.bones) { + console.warn("error, no bones on", hand.id); + return; + } + finger.bones.forEach(function(bone) { + var boneMesh, jointMesh; + boneMesh = new THREE.Mesh(new THREE.CylinderGeometry(boneRadius, boneRadius, bone.length, 32), material.clone()); + boneMesh.material.color.copy(boneColor); + scope.scene.add(boneMesh); + boneMeshes.push(boneMesh); + jointMesh = new THREE.Mesh(new THREE.SphereGeometry(jointRadius, 32, 32), material.clone()); + jointMesh.material.color.copy(jointColor); + scope.scene.add(jointMesh); + return jointMeshes.push(jointMesh); + }); + jointMesh = new THREE.Mesh(new THREE.SphereGeometry(jointRadius, 32, 32), material.clone()); + jointMesh.material.color.copy(jointColor); + scope.scene.add(jointMesh); + jointMeshes.push(jointMesh); + finger.data("boneMeshes", boneMeshes); + finger.data("jointMeshes", jointMeshes); + } + boneMeshes.forEach(function(mesh, i) { + var bone; + bone = finger.bones[i]; + mesh.position.fromArray(bone.center()); + mesh.setRotationFromMatrix((new THREE.Matrix4).fromArray(bone.matrix())); + return mesh.quaternion.multiply(baseBoneRotation); + }); + return jointMeshes.forEach(function(mesh, i) { + var bone; + bone = finger.bones[i]; + if (bone) { + return mesh.position.fromArray(bone.prevJoint); + } else { + bone = finger.bones[i - 1]; + return mesh.position.fromArray(bone.nextJoint); + } + }); + }); + if (scope.arm) { + armMesh = hand.data('armMesh'); + if (!armMesh) { + armMesh = new THREE.Object3D; + scope.scene.add(armMesh); + hand.data('armMesh', armMesh); + boneXOffset = (hand.arm.width / 2) - (boneRadius / 2); + halfArmLength = hand.arm.length / 2; + armBones = []; + for (i = _i = 0; _i <= 3; i = ++_i) { + armBones.push(new THREE.Mesh(new THREE.CylinderGeometry(boneRadius, boneRadius, (i < 2 ? hand.arm.length : hand.arm.width), 32), material.clone())); + armBones[i].material.color.copy(boneColor); + armMesh.add(armBones[i]); + } + armBones[0].position.setX(boneXOffset); + armBones[1].position.setX(-boneXOffset); + armBones[2].position.setY(halfArmLength); + armBones[3].position.setY(-halfArmLength); + armTopAndBottomRotation = (new THREE.Quaternion).setFromEuler(new THREE.Euler(0, 0, Math.PI / 2)); + armBones[2].quaternion.multiply(armTopAndBottomRotation); + armBones[3].quaternion.multiply(armTopAndBottomRotation); + armBones = []; + for (i = _j = 0; _j <= 3; i = ++_j) { + armBones.push(new THREE.Mesh(new THREE.SphereGeometry(jointRadius, 32, 32), material.clone())); + armBones[i].material.color.copy(jointColor); + armMesh.add(armBones[i]); + } + armBones[0].position.set(-boneXOffset, halfArmLength, 0); + armBones[1].position.set(boneXOffset, halfArmLength, 0); + armBones[2].position.set(boneXOffset, -halfArmLength, 0); + armBones[3].position.set(-boneXOffset, -halfArmLength, 0); + } + armMesh.position.fromArray(hand.arm.center()); + armMesh.setRotationFromMatrix((new THREE.Matrix4).fromArray(hand.arm.matrix())); + return armMesh.quaternion.multiply(baseBoneRotation); + } + }; + + boneHandLost = function(hand) { + var armMesh; + hand.fingers.forEach(function(finger) { + var boneMeshes, jointMeshes; + boneMeshes = finger.data("boneMeshes"); + jointMeshes = finger.data("jointMeshes"); + if (!boneMeshes) { + return; + } + boneMeshes.forEach(function(mesh) { + return scope.scene.remove(mesh); + }); + jointMeshes.forEach(function(mesh) { + return scope.scene.remove(mesh); + }); + finger.data({ + boneMeshes: null + }); + return finger.data({ + jointMeshes: null + }); + }); + if (scope.arm) { + armMesh = hand.data('armMesh'); + scope.scene.remove(armMesh); + return hand.data('armMesh', null); + } + }; + + Leap.plugin('boneHand', function(options) { + if (options == null) { + options = {}; + } + scope = options; + scope.boneScale && (boneScale = scope.boneScale); + scope.jointScale && (jointScale = scope.jointScale); + scope.boneColor && (boneColor = scope.boneColor); + scope.jointColor && (jointColor = scope.jointColor); + this.use('handEntry'); + this.use('handHold'); + if (scope.scene === void 0) { + console.assert(scope.targetEl); + initScene(scope.targetEl); + } + this.on('handLost', boneHandLost); + return { + hand: boneHand + }; + }); + +}).call(this); + +//CoffeeScript generated from main/hand-entry/leap.hand-entry.coffee +/* +Emits controller events when a hand enters of leaves the frame +"handLost" and "handFound" +Each event also includes the hand object, which will be invalid for the handLost event. +*/ + + +(function() { + var handEntry; + + handEntry = function() { + var activeHandIds; + activeHandIds = []; + if (Leap.version.major === 0 && Leap.version.minor < 5) { + console.warn("The hand entry plugin requires LeapJS 0.5.0 or newer."); + } + this.on("deviceStopped", function() { + for (var i = 0, len = activeHandIds.length; i < len; i++){ + id = activeHandIds[i]; + activeHandIds.splice(i, 1); + // this gets executed before the current frame is added to the history. + this.emit('handLost', this.lastConnectionFrame.hand(id)) + i--; + len--; + }; + }); + return { + frame: function(frame) { + var id, newValidHandIds, _i, _len, _results; + newValidHandIds = frame.hands.map(function(hand) { + return hand.id; + }); + for (var i = 0, len = activeHandIds.length; i < len; i++){ + id = activeHandIds[i]; + if( newValidHandIds.indexOf(id) == -1){ + activeHandIds.splice(i, 1); + // this gets executed before the current frame is added to the history. + this.emit('handLost', this.frame(1).hand(id)); + i--; + len--; + } + }; + _results = []; + for (_i = 0, _len = newValidHandIds.length; _i < _len; _i++) { + id = newValidHandIds[_i]; + if (activeHandIds.indexOf(id) === -1) { + activeHandIds.push(id); + _results.push(this.emit('handFound', frame.hand(id))); + } else { + _results.push(void 0); + } + } + return _results; + } + }; + }; + + if ((typeof Leap !== 'undefined') && Leap.Controller) { + Leap.Controller.plugin('handEntry', handEntry); + } else if (typeof module !== 'undefined') { + module.exports.handEntry = handEntry; + } else { + throw 'leap.js not included'; + } + +}).call(this); + +//CoffeeScript generated from main/hand-hold/leap.hand-hold.coffee +(function() { + var handHold; + + handHold = function() { + var dataFn, interFrameData; + interFrameData = {}; + dataFn = function(prefix, hashOrKey, value) { + var dict, key, _name, _results; + interFrameData[_name = prefix + this.id] || (interFrameData[_name] = []); + dict = interFrameData[prefix + this.id]; + if (value !== void 0) { + return dict[hashOrKey] = value; + } else if ({}.toString.call(hashOrKey) === '[object String]') { + return dict[hashOrKey]; + } else { + _results = []; + for (key in hashOrKey) { + value = hashOrKey[key]; + if (value === void 0) { + _results.push(delete dict[key]); + } else { + _results.push(dict[key] = value); + } + } + return _results; + } + }; + return { + hand: { + data: function(hashOrKey, value) { + return dataFn.call(this, 'h', hashOrKey, value); + }, + hold: function(object) { + if (object) { + return this.data({ + holding: object + }); + } else { + return this.hold(this.hovering()); + } + }, + holding: function() { + return this.data('holding'); + }, + release: function() { + var release; + release = this.data('holding'); + this.data({ + holding: void 0 + }); + return release; + }, + hoverFn: function(getHover) { + return this.data({ + getHover: getHover + }); + }, + hovering: function() { + var getHover; + if (getHover = this.data('getHover')) { + return this._hovering || (this._hovering = getHover.call(this)); + } + } + }, + pointable: { + data: function(hashOrKey, value) { + return dataFn.call(this, 'p', hashOrKey, value); + } + } + }; + }; + + if ((typeof Leap !== 'undefined') && Leap.Controller) { + Leap.Controller.plugin('handHold', handHold); + } else if (typeof module !== 'undefined') { + module.exports.handHold = handHold; + } else { + throw 'leap.js not included'; + } + +}).call(this); + + + + + + +/* + * LeapJS Playback - v0.2.1 - 2014-05-14 + * http://github.com/leapmotion/leapjs-playback/ + * + * Copyright 2014 LeapMotion, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +;(function( window, undefined ){ + 'use strict'; + // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode + +// Copyright (c) 2013 Pieroxy +// This work is free. You can redistribute it and/or modify it +// under the terms of the WTFPL, Version 2 +// For more information see LICENSE.txt or http://www.wtfpl.net/ +// +// For more information, the home page: +// http://pieroxy.net/blog/pages/lz-string/testing.html +// +// LZ-based compression algorithm, version 1.3.3 +var LZString = { + + + // private property + _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", + _f : String.fromCharCode, + + compressToBase64 : function (input) { + if (input == null) return ""; + var output = ""; + var chr1, chr2, chr3, enc1, enc2, enc3, enc4; + var i = 0; + + input = LZString.compress(input); + + while (i < input.length*2) { + + if (i%2==0) { + chr1 = input.charCodeAt(i/2) >> 8; + chr2 = input.charCodeAt(i/2) & 255; + if (i/2+1 < input.length) + chr3 = input.charCodeAt(i/2+1) >> 8; + else + chr3 = NaN; + } else { + chr1 = input.charCodeAt((i-1)/2) & 255; + if ((i+1)/2 < input.length) { + chr2 = input.charCodeAt((i+1)/2) >> 8; + chr3 = input.charCodeAt((i+1)/2) & 255; + } else + chr2=chr3=NaN; + } + i+=3; + + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + + if (isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (isNaN(chr3)) { + enc4 = 64; + } + + output = output + + LZString._keyStr.charAt(enc1) + LZString._keyStr.charAt(enc2) + + LZString._keyStr.charAt(enc3) + LZString._keyStr.charAt(enc4); + + } + + return output; + }, + + decompressFromBase64 : function (input) { + if (input == null) return ""; + var output = "", + ol = 0, + output_, + chr1, chr2, chr3, + enc1, enc2, enc3, enc4, + i = 0, f=LZString._f; + + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); + + while (i < input.length) { + + enc1 = LZString._keyStr.indexOf(input.charAt(i++)); + enc2 = LZString._keyStr.indexOf(input.charAt(i++)); + enc3 = LZString._keyStr.indexOf(input.charAt(i++)); + enc4 = LZString._keyStr.indexOf(input.charAt(i++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + if (ol%2==0) { + output_ = chr1 << 8; + + if (enc3 != 64) { + output += f(output_ | chr2); + } + if (enc4 != 64) { + output_ = chr3 << 8; + } + } else { + output = output + f(output_ | chr1); + + if (enc3 != 64) { + output_ = chr2 << 8; + } + if (enc4 != 64) { + output += f(output_ | chr3); + } + } + ol+=3; + } + + return LZString.decompress(output); + + }, + + compressToUTF16 : function (input) { + if (input == null) return ""; + var output = "", + i,c, + current, + status = 0, + f = LZString._f; + + input = LZString.compress(input); + + for (i=0 ; i> 1)+32); + current = (c & 1) << 14; + break; + case 1: + output += f((current + (c >> 2))+32); + current = (c & 3) << 13; + break; + case 2: + output += f((current + (c >> 3))+32); + current = (c & 7) << 12; + break; + case 3: + output += f((current + (c >> 4))+32); + current = (c & 15) << 11; + break; + case 4: + output += f((current + (c >> 5))+32); + current = (c & 31) << 10; + break; + case 5: + output += f((current + (c >> 6))+32); + current = (c & 63) << 9; + break; + case 6: + output += f((current + (c >> 7))+32); + current = (c & 127) << 8; + break; + case 7: + output += f((current + (c >> 8))+32); + current = (c & 255) << 7; + break; + case 8: + output += f((current + (c >> 9))+32); + current = (c & 511) << 6; + break; + case 9: + output += f((current + (c >> 10))+32); + current = (c & 1023) << 5; + break; + case 10: + output += f((current + (c >> 11))+32); + current = (c & 2047) << 4; + break; + case 11: + output += f((current + (c >> 12))+32); + current = (c & 4095) << 3; + break; + case 12: + output += f((current + (c >> 13))+32); + current = (c & 8191) << 2; + break; + case 13: + output += f((current + (c >> 14))+32); + current = (c & 16383) << 1; + break; + case 14: + output += f((current + (c >> 15))+32, (c & 32767)+32); + status = 0; + break; + } + } + + return output + f(current + 32); + }, + + + decompressFromUTF16 : function (input) { + if (input == null) return ""; + var output = "", + current,c, + status=0, + i = 0, + f = LZString._f; + + while (i < input.length) { + c = input.charCodeAt(i) - 32; + + switch (status++) { + case 0: + current = c << 1; + break; + case 1: + output += f(current | (c >> 14)); + current = (c&16383) << 2; + break; + case 2: + output += f(current | (c >> 13)); + current = (c&8191) << 3; + break; + case 3: + output += f(current | (c >> 12)); + current = (c&4095) << 4; + break; + case 4: + output += f(current | (c >> 11)); + current = (c&2047) << 5; + break; + case 5: + output += f(current | (c >> 10)); + current = (c&1023) << 6; + break; + case 6: + output += f(current | (c >> 9)); + current = (c&511) << 7; + break; + case 7: + output += f(current | (c >> 8)); + current = (c&255) << 8; + break; + case 8: + output += f(current | (c >> 7)); + current = (c&127) << 9; + break; + case 9: + output += f(current | (c >> 6)); + current = (c&63) << 10; + break; + case 10: + output += f(current | (c >> 5)); + current = (c&31) << 11; + break; + case 11: + output += f(current | (c >> 4)); + current = (c&15) << 12; + break; + case 12: + output += f(current | (c >> 3)); + current = (c&7) << 13; + break; + case 13: + output += f(current | (c >> 2)); + current = (c&3) << 14; + break; + case 14: + output += f(current | (c >> 1)); + current = (c&1) << 15; + break; + case 15: + output += f(current | c); + status=0; + break; + } + + + i++; + } + + return LZString.decompress(output); + //return output; + + }, + + + + compress: function (uncompressed) { + if (uncompressed == null) return ""; + var i, value, + context_dictionary= {}, + context_dictionaryToCreate= {}, + context_c="", + context_wc="", + context_w="", + context_enlargeIn= 2, // Compensate for the first entry which should not count + context_dictSize= 3, + context_numBits= 2, + context_data_string="", + context_data_val=0, + context_data_position=0, + ii, + f=LZString._f; + + for (ii = 0; ii < uncompressed.length; ii += 1) { + context_c = uncompressed.charAt(ii); + if (!Object.prototype.hasOwnProperty.call(context_dictionary,context_c)) { + context_dictionary[context_c] = context_dictSize++; + context_dictionaryToCreate[context_c] = true; + } + + context_wc = context_w + context_c; + if (Object.prototype.hasOwnProperty.call(context_dictionary,context_wc)) { + context_w = context_wc; + } else { + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) { + if (context_w.charCodeAt(0)<256) { + for (i=0 ; i> 1; + } + } else { + value = 1; + for (i=0 ; i> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + delete context_dictionaryToCreate[context_w]; + } else { + value = context_dictionary[context_w]; + for (i=0 ; i> 1; + } + + + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + // Add wc to the dictionary. + context_dictionary[context_wc] = context_dictSize++; + context_w = String(context_c); + } + } + + // Output the code for w. + if (context_w !== "") { + if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate,context_w)) { + if (context_w.charCodeAt(0)<256) { + for (i=0 ; i> 1; + } + } else { + value = 1; + for (i=0 ; i> 1; + } + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + delete context_dictionaryToCreate[context_w]; + } else { + value = context_dictionary[context_w]; + for (i=0 ; i> 1; + } + + + } + context_enlargeIn--; + if (context_enlargeIn == 0) { + context_enlargeIn = Math.pow(2, context_numBits); + context_numBits++; + } + } + + // Mark the end of the stream + value = 2; + for (i=0 ; i> 1; + } + + // Flush the last char + while (true) { + context_data_val = (context_data_val << 1); + if (context_data_position == 15) { + context_data_string += f(context_data_val); + break; + } + else context_data_position++; + } + return context_data_string; + }, + + decompress: function (compressed) { + if (compressed == null) return ""; + if (compressed == "") return null; + var dictionary = [], + next, + enlargeIn = 4, + dictSize = 4, + numBits = 3, + entry = "", + result = "", + i, + w, + bits, resb, maxpower, power, + c, + f = LZString._f, + data = {string:compressed, val:compressed.charCodeAt(0), position:32768, index:1}; + + for (i = 0; i < 3; i += 1) { + dictionary[i] = i; + } + + bits = 0; + maxpower = Math.pow(2,2); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = 32768; + data.val = data.string.charCodeAt(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + switch (next = bits) { + case 0: + bits = 0; + maxpower = Math.pow(2,8); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = 32768; + data.val = data.string.charCodeAt(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + case 1: + bits = 0; + maxpower = Math.pow(2,16); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = 32768; + data.val = data.string.charCodeAt(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + c = f(bits); + break; + case 2: + return ""; + } + dictionary[3] = c; + w = result = c; + while (true) { + if (data.index > data.string.length) { + return ""; + } + + bits = 0; + maxpower = Math.pow(2,numBits); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = 32768; + data.val = data.string.charCodeAt(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + switch (c = bits) { + case 0: + bits = 0; + maxpower = Math.pow(2,8); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = 32768; + data.val = data.string.charCodeAt(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + + dictionary[dictSize++] = f(bits); + c = dictSize-1; + enlargeIn--; + break; + case 1: + bits = 0; + maxpower = Math.pow(2,16); + power=1; + while (power!=maxpower) { + resb = data.val & data.position; + data.position >>= 1; + if (data.position == 0) { + data.position = 32768; + data.val = data.string.charCodeAt(data.index++); + } + bits |= (resb>0 ? 1 : 0) * power; + power <<= 1; + } + dictionary[dictSize++] = f(bits); + c = dictSize-1; + enlargeIn--; + break; + case 2: + return result; + } + + if (enlargeIn == 0) { + enlargeIn = Math.pow(2, numBits); + numBits++; + } + + if (dictionary[c]) { + entry = dictionary[c]; + } else { + if (c === dictSize) { + entry = w + w.charAt(0); + } else { + return null; + } + } + result += entry; + + // Add w+entry[0] to the dictionary. + dictionary[dictSize++] = w + entry.charAt(0); + enlargeIn--; + + w = entry; + + if (enlargeIn == 0) { + enlargeIn = Math.pow(2, numBits); + numBits++; + } + + } + } +}; + +if( typeof module !== 'undefined' && module != null ) { + module.exports = LZString +} + +// https://gist.github.com/paulirish/5438650 + +// relies on Date.now() which has been supported everywhere modern for years. +// as Safari 6 doesn't have support for NavigationTiming, we use a Date.now() timestamp for relative values + +// if you want values similar to what you'd get with real perf.now, place this towards the head of the page +// but in reality, you're just getting the delta between now() calls, so it's not terribly important where it's placed + + +(function(){ + + // prepare base perf object + if (typeof window.performance === 'undefined') { + window.performance = {}; + } + + if (!window.performance.now){ + + var nowOffset = Date.now(); + + if (performance.timing && performance.timing.navigationStart){ + nowOffset = performance.timing.navigationStart + } + + + window.performance.now = function now(){ + return Date.now() - nowOffset; + } + + } + +})(); +function Recording (options){ + this.options = options || (options = {}); + this.loading = false; + this.timeBetweenLoops = options.timeBetweenLoops || 50; + + // see https://github.com/leapmotion/leapjs/blob/master/Leap_JSON.rst + this.packingStructure = [ + 'id', + 'timestamp', + // this should be replace/upgraded with a whitelist instead of a blacklist. + // leaving out r,s,y, and gestures + {hands: [[ + 'id', + 'type', + 'direction', + 'palmNormal', + 'palmPosition', + 'palmVelocity', + 'stabilizedPalmPosition', + 'pinchStrength', + 'grabStrength', + 'confidence' + // leaving out r, s, t, sphereCenter, sphereRadius + ]]}, + {pointables: [[ + 'id', + 'direction', + 'handId', + 'length', + 'stabilizedTipPosition', + 'tipPosition', + 'tipVelocity', + 'tool', + 'carpPosition', + 'mcpPosition', + 'pipPosition', + 'dipPosition', + 'btipPosition', + 'bases', + 'type' + // leaving out touchDistance, touchZone + ]]}, + {interactionBox: [ + 'center', 'size' + ]} + ]; + + this.setFrames(options.frames || []) +} + + +Recording.prototype = { + + setFrames: function (frames) { + this.frameData = frames; + this.frameIndex = 0; + this.frameCount = frames.length; + this.leftCropPosition = 0; + this.rightCropPosition = this.frameCount; + this.setMetaData(); + }, + + addFrame: function(frameData){ + this.frameData.push(frameData); + }, + + currentFrame: function () { + return this.frameData[this.frameIndex]; + }, + + nextFrame: function () { + var frameIndex = this.frameIndex + 1; + // || 1 to prevent `mod 0` error when finishing recording before setFrames has been called. + frameIndex = frameIndex % (this.rightCropPosition || 1); + if ((frameIndex < this.leftCropPosition)) { + frameIndex = this.leftCropPosition; + } + return this.frameData[frameIndex]; + }, + + + advanceFrame: function () { + this.frameIndex++; + + if (this.frameIndex >= this.rightCropPosition && !this.options.loop) { + this.frameIndex--; + // there is currently an issue where angular watching the right handle position + // will cause this to fire prematurely + // when switching to an earlier recording + return false + } + + + this.frameIndex = this.frameIndex % (this.rightCropPosition || 1); + + if ((this.frameIndex < this.leftCropPosition)) { + this.frameIndex = this.leftCropPosition; + } + + return true + }, + + // resets to beginning if at end + readyPlay: function(){ + this.frameIndex++; + if (this.frameIndex >= this.rightCropPosition) { + this.frameIndex = this.frameIndex % (this.rightCropPosition || 1); + + if ((this.frameIndex < this.leftCropPosition)) { + this.frameIndex = this.leftCropPosition; + } + }else{ + this.frameIndex--; + } + }, + + cloneCurrentFrame: function(){ + return JSON.parse(JSON.stringify(this.currentFrame())); + }, + + + // this method would be well-moved to its own object/class -.- + // for every point, lerp as appropriate + // note: currently hand and finger props are hard coded, but things like stabilizedPalmPosition should be optional + // should have this be set from the packingStructure or some such, but only for vec3s. + createLerpFrameData: function(t){ + // http://stackoverflow.com/a/5344074/478354 + var currentFrame = this.currentFrame(), + nextFrame = this.nextFrame(), + handProps = ['palmPosition', 'stabilizedPalmPosition', 'sphereCenter', 'direction', 'palmNormal', 'palmVelocity'], + fingerProps = ['mcpPosition', 'pipPosition', 'dipPosition', 'tipPosition', 'direction'], + frameData = this.cloneCurrentFrame(), + numHands = frameData.hands.length, + numPointables = frameData.pointables.length, + len1 = handProps.length, + len2 = fingerProps.length, + prop, hand, pointable; + + for (var i = 0; i < numHands; i++){ + hand = frameData.hands[i]; + + for (var j = 0; j < len1; j++){ + prop = handProps[j]; + + if (!currentFrame.hands[i][prop]){ + continue; + } + + if (!nextFrame.hands[i]){ + continue; + } + + Leap.vec3.lerp( + hand[prop], + currentFrame.hands[i][prop], + nextFrame.hands[i][prop], + t + ); + + console.assert(hand[prop]); + } + + } + + for ( i = 0; i < numPointables; i++){ + pointable = frameData.pointables[i]; + + for ( j = 0; j < len2; j++){ + prop = fingerProps[j]; + + if (!currentFrame.pointables[i][prop]){ + continue; + } + + if (!nextFrame.hands[i]){ + continue; + } + + Leap.vec3.lerp( + pointable[prop], + currentFrame.pointables[i][prop], + nextFrame.pointables[i][prop], + 0 + ); +// console.assert(t >= 0 && t <= 1); +// if (t > 0) debugger; + + } + + } + + return frameData; + }, + + // returns ms + timeToNextFrame: function () { + var elapsedTime = (this.nextFrame().timestamp - this.currentFrame().timestamp) / 1000; + if (elapsedTime < 0) { + elapsedTime = this.timeBetweenLoops; //arbitrary pause at slightly less than 30 fps. + } + console.assert(!isNaN(elapsedTime)); + return elapsedTime; + }, + + + blank: function(){ + return this.frameData.length === 0; + }, + + // sets the crop-point of the current recording to the current position. + leftCrop: function () { + this.leftCropPosition = this.frameIndex + }, + + // sets the crop-point of the current recording to the current position. + rightCrop: function () { + this.rightCropPosition = this.frameIndex + }, + + // removes every other frame from the array + // Accepts an optional `factor` integer, which is the number of frames + // discarded for every frame kept. + cullFrames: function (factor) { + console.log('cull frames', factor); + factor || (factor = 1); + for (var i = 0; i < this.frameData.length; i++) { + this.frameData.splice(i, factor); + } + this.setMetaData(); + }, + + // Returns the average frames per second of the recording + frameRate: function () { + if (this.frameData.length == 0) { + return 0 + } + return this.frameData.length / (this.frameData[this.frameData.length - 1].timestamp - this.frameData[0].timestamp) * 1000000; + }, + + // returns frames without any circular references + croppedFrameData: function () { + return this.frameData.slice(this.leftCropPosition, this.rightCropPosition); + }, + + + setMetaData: function () { + + var newMetaData = { + formatVersion: 2, + generatedBy: 'LeapJS Playback 0.2.0', + frames: this.rightCropPosition - this.leftCropPosition, + protocolVersion: this.options.requestProtocolVersion, + serviceVersion: this.options.serviceVersion, + frameRate: this.frameRate().toPrecision(2), + modified: (new Date).toString() + }; + + this.metadata || (this.metadata = {}); + + for (var key in newMetaData) { + this.metadata[key] = newMetaData[key]; + } + }, + + // returns an array + // the first item is the keys of the following items + // nested arrays are expected to have idententical siblings + packedFrameData: function(){ + var frameData = this.croppedFrameData(), + packedFrames = [], + frameDatum; + + packedFrames.push(this.packingStructure); + + for (var i = 0, len = frameData.length; i < len; i++){ + frameDatum = frameData[i]; + + packedFrames.push( + this.packArray( + this.packingStructure, + frameDatum + ) + ); + + } + + return packedFrames; + }, + + // recursive method + // creates a structure of frame data matching packing structure + // there may be an issue here where hands/pointables are wrapped in one more array than necessary + packArray: function(structure, data){ + var out = [], nameOrHash; + + for (var i = 0, len1 = structure.length; i < len1; i++){ + + // e.g., nameOrHash is either 'id' or {hand: [...]} + nameOrHash = structure[i]; + + if ( typeof nameOrHash === 'string'){ + + out.push( + data[nameOrHash] + ); + + }else if (Object.prototype.toString.call(nameOrHash) == "[object Array]") { + // nested array, such as hands or fingers + + for (var j = 0, len2 = data.length; j < len2; j++){ + out.push( + this.packArray( + nameOrHash, + data[j] + ) + ); + } + + } else { // key-value (nested object) such as interactionBox + + console.assert(nameOrHash); + + for (var key in nameOrHash) break; + + console.assert(key); + console.assert(nameOrHash[key]); + console.assert(data[key]); + + out.push(this.packArray( + nameOrHash[key], + data[key] + )); + + } + + } + + return out; + }, + + // expects the first array element to describe the following arrays + // this algorithm copies frames to a new array + // could there be merit in something which would do an in-place substitution? + unPackFrameData: function(packedFrames){ + var packingStructure = packedFrames[0]; + var frameData = [], + frameDatum; + + for (var i = 1, len = packedFrames.length; i < len; i++) { + frameDatum = packedFrames[i]; + frameData.push( + this.unPackArray( + packingStructure, + frameDatum + ) + ); + + } + + return frameData; + }, + + // data is a frame or subset of frame + // returns a frame object + // this is the structure of the array + // gets unfolded to key-value pairs + // e.g.: + // this.packingStructure = [ + // 'id', + // 'timestamp', + // {hands: [[ + // 'id', + // 'direction', + // 'palmNormal', + // 'palmPosition', + // 'palmVelocity' + // ]]}, + // {pointables: [[ + // 'direction', + // 'handId', + // 'length', + // 'stabilizedTipPosition', + // 'tipPosition', + // 'tipVelocity', + // 'tool' + // ]]}, + // {interactionBox: [ + // 'center', 'size' + // ]} + // ]; + unPackArray: function(structure, data){ + var out = {}, nameOrHash; + + for (var i = 0, len1 = structure.length; i < len1; i++){ + + // e.g., nameOrHash is either 'id' or {hand: [...]} + nameOrHash = structure[i]; + + if ( typeof nameOrHash === 'string'){ + + out[nameOrHash] = data[i]; + + }else if (Object.prototype.toString.call(nameOrHash) == "[object Array]") { + // nested array, such as hands or fingers + // nameOrHash ["id", "direction", "palmNormal", "palmPosition", "palmVelocity"] + // data [ [ 31, [vec3], [vec3], ...] ] + + var subArray = []; + + for (var j = 0, len2 = data.length; j < len2; j++){ + subArray.push( + this.unPackArray( + nameOrHash, + data[j] + ) + ); + } + return subArray; + + } else { // key-value (nested object) such as interactionBox + + for (var key in nameOrHash) break; + + out[key] = this.unPackArray( + nameOrHash[key], + data[i] + ); + + } + + } + + return out; + }, + + toHash: function () { + this.setMetaData(); + return { + metadata: this.metadata, + frames: this.packedFrameData() + } + }, + + // Returns the cropped data as JSON or compressed + // http://pieroxy.net/blog/pages/lz-string/index.html + export: function (format) { + var json = JSON.stringify(this.toHash()); + + if (format == 'json') return json; + + return LZString.compressToBase64(json); + }, + + save: function(format){ + var filename; + + filename = this.metadata.title ? this.metadata.title.replace(/\s/g, '') : 'leap-playback-recording'; + + if (this.metadata.frameRate) { + filename += "-" + (Math.round(this.metadata.frameRate)) + "fps"; + } + + if (format === 'json') { + + saveAs(new Blob([this.export('json')], { + type: "text/JSON;charset=utf-8" + }), filename + ".json"); + + } else { + + saveAs(new Blob([this.export('lz')], { + type: "application/x-gzip;charset=utf-8" + }), filename + ".json.lz"); + + } + + }, + + decompress: function (data) { + return LZString.decompressFromBase64(data); + }, + + loaded: function(){ + return !!(this.frameData && this.frameData.length) + }, + + + // optional callback once frames are loaded, will have a context of player + loadFrameData: function (callback) { + var xhr = new XMLHttpRequest(), + url = this.url, + recording = this, + contentLength = 0; + + xhr.onreadystatechange = function () { + if (xhr.readyState === xhr.DONE) { + if (xhr.status === 200 || xhr.status === 0) { + if (xhr.responseText) { + + recording.finishLoad(xhr.responseText, callback); + + } else { + console.error('Leap Playback: "' + url + '" seems to be unreachable or the file is empty.'); + } + } else { + console.error('Leap Playback: Couldn\'t load "' + url + '" (' + xhr.status + ')'); + } + } + }; + + xhr.addEventListener('progress', function(oEvent){ + + if ( recording.options.loadProgress ) { + + if (oEvent.lengthComputable) { + var percentComplete = oEvent.loaded / oEvent.total; + recording.options.loadProgress( recording, percentComplete, oEvent ); + } + + } + + }); + + this.loading = true; + + xhr.open("GET", url, true); + xhr.send(null); + }, + + finishLoad: function(responseData, callback){ + + var url = this.url; + + if (url.split('.')[url.split('.').length - 1] == 'lz') { + responseData = this.decompress(responseData); + } + + responseData = JSON.parse(responseData); + + if (responseData.metadata.formatVersion == 2) { + responseData.frames = this.unPackFrameData(responseData.frames); + } + + this.metadata = responseData.metadata; + + console.log('Recording loaded:', this.metadata); + + this.loading = false; + + if (callback) { + callback.call(this, responseData.frames); + } + + } + +}; +(function () { + var CONNECT_LEAP_ICON = ''; + var MOVE_HAND_OVER_LEAP_ICON = ''; + + + function Player(controller, options) { + var player = this; + options || (options = {}); + +// this.frameData = []; + + this.options = options; + this.recording = options.recording; + + this.controller = controller; + this.resetTimers(); + this.setupLoops(); + this.controller.connection.on('ready', function () { + player.setupProtocols(); + }); + + this.userHasControl = false; + + + if (options.recording) { + if (Object.prototype.toString.call(options.recording) == '[object String]') { + options.recording = { + url: options.recording + } + } + this.setRecording(options.recording); + } + + document.addEventListener("DOMContentLoaded", function(event) { + document.body.addEventListener('keydown', function (e) { + if (e.which === player.options.pauseHotkey) { + player.toggle(); + } + }, false); + }); + + } + + Player.prototype = { + resetTimers: function (){ + this.timeSinceLastFrame = 0; + this.lastFrameTime = null; + }, + + setupLoops: function () { + var player = this; + + // Loop with explicit frame timing + this.stepFrameLoop = function (timestamp) { + if (player.state != 'playing') return; + + player.sendFrameAt(timestamp || performance.now()); + + requestAnimationFrame(player.stepFrameLoop); + }; + + + }, + + // This is how we intercept frame data early + // By hooking in before Frame creation, we get data exactly as the frame sends it. + setupProtocols: function () { + var player = this; + // This is the original normal protocol, used while in record mode but not recording. + this.stopProtocol = this.controller.connection.protocol; + + // This consumes all frame data, making the device act as if not streaming + this.playbackProtocol = function (data) { + // The old protocol still needs to emit events, so we use it, but intercept Frames + var eventOrFrame = player.stopProtocol(data); + if (eventOrFrame instanceof Leap.Frame) { + + if (player.pauseOnHand) { + if (data.hands.length > 0) { + player.userHasControl = true; + player.controller.emit('playback.userTakeControl'); + player.setGraphic(); + player.idle(); + } else if (data.hands.length == 0) { + if (player.userHasControl && player.resumeOnHandLost) { + player.userHasControl = false; + player.controller.emit('playback.userReleaseControl'); + player.setGraphic('wave'); + } + + } + } + + // prevent the actual frame from getting through + return {type: 'playback'} + } else { + return eventOrFrame; + } + }; + + // This pushes frame data, and watches for hands to auto change state. + // Returns the eventOrFrame without modifying it. + this.recordProtocol = function (data) { + var eventOrFrame = player.stopProtocol(data); + if (eventOrFrame instanceof Leap.Frame) { + player.recordFrameHandler(data); + } + return eventOrFrame; + }; + + // Copy methods/properties from the default protocol over + for (var property in this.stopProtocol) { + if (this.stopProtocol.hasOwnProperty(property)) { + this.playbackProtocol[property] = this.stopProtocol[property] + this.recordProtocol[property] = this.stopProtocol[property] + } + } + + // todo: this is messy. Should cover all cases, not just active playback! + if (this.state == 'playing') { + this.controller.connection.protocol = this.playbackProtocol + } + }, + + + + // Adds playback = true to artificial frames + sendFrameAt: function (now) { + + if (this.lastFrameTime){ + // chrome bug, see: https://code.google.com/p/chromium/issues/detail?id=268213 + // http://jsfiddle.net/pehrlich/35pTx/ + // console.assert(this.lastFrameTime < now); + if (now < this.lastFrameTime){ + // this fix will cause an extra animation frame before the lerp frame advances. no big. + this.lastFrameTime = now; + }else{ + this.timeSinceLastFrame += (now - this.lastFrameTime); + } + } + + this.lastFrameTime = now; + + console.assert(!isNaN(this.timeSinceLastFrame)); + + + var timeToNextFrame; + + // handle frame dropping, etc + while ( this.timeSinceLastFrame > ( timeToNextFrame = this.recording.timeToNextFrame() ) ){ + this.timeSinceLastFrame -= timeToNextFrame; + if (!this.recording.advanceFrame()){ + this.pause(); + this.controller.emit('playback.playbackFinished', this); + return + } + } + + this.sendFrame( + this.recording.createLerpFrameData(this.timeSinceLastFrame / timeToNextFrame) + ); + + }, + + sendFrame: function(frameData){ + if (!frameData) throw "Frame data not provided"; + + var frame = new Leap.Frame(frameData); + + // send a deviceFrame to the controller: + // this frame gets picked up by the controllers own animation loop. + + this.controller.processFrame(frame); + return true + }, + + sendImmediateFrame: function(frameData){ + if (!frameData) throw "Frame data not provided"; + + var frame = new Leap.Frame(frameData); + + // sends an animation frame to the controller + + this.controller.processFinishedFrame(frame); + return true + }, + + setFrameIndex: function (frameIndex) { + if (frameIndex != this.recording.frameIndex) { + this.recording.frameIndex = frameIndex % this.recording.frameCount; + this.sendFrame(this.recording.currentFrame()); + } + }, + + + // used after record + stop: function () { + this.idle(); + + delete this.recording; + + this.recording = new Recording({ + timeBetweenLoops: this.options.timeBetweenLoops, + loop: this.options.loop, + requestProtocolVersion: this.controller.connection.opts.requestProtocolVersion, + serviceVersion: this.controller.connection.protocol.serviceVersion + }); + + this.controller.emit('playback.stop', this); + }, + + // used after play + pause: function () { + // todo: we should change this idle state to paused or leave it as playback with a pause flag + // state should correspond always to protocol handler (through a setter)? + this.state = 'idle'; + this.hideOverlay(); + this.controller.emit('playback.pause', this); + }, + + idle: function () { + this.state = 'idle'; + this.controller.connection.protocol = this.stopProtocol; + }, + + toggle: function () { + if (this.state == 'idle') { + this.play(); + } else if (this.state == 'playing') { + this.pause(); + } + }, + + // switches to record mode, which will be begin capturing data when a hand enters the frame, + // and stop when a hand leaves + // Todo: replace frameData with a full fledged recording, including metadata. + record: function () { + this.clear(); + this.stop(); + this.state = 'recording'; + this.controller.connection.protocol = this.recordProtocol; + this.setGraphic('connect'); + this.controller.emit('playback.record', this) + }, + + // if there is existing frame data, sends a frame with nothing in it + clear: function () { + if (!this.recording || this.recording.blank()) return; + var finalFrame = this.recording.cloneCurrentFrame(); + finalFrame.hands = []; + finalFrame.fingers = []; + finalFrame.pointables = []; + finalFrame.tools = []; + this.sendImmediateFrame(finalFrame); + }, + + recordPending: function () { + return this.state == 'recording' && this.recording.blank() + }, + + isRecording: function () { + return this.state == 'recording' && !this.recording.blank() + }, + + finishRecording: function () { + // change to the playbackHandler which suppresses frames: + this.controller.connection.protocol = this.playbackProtocol; + this.recording.setFrames(this.recording.frameData); + this.controller.emit('playback.recordingFinished', this) + }, + + + loaded: function () { + return this.recording.loaded(); + }, + + loading: function(){ + return this.recording.loading; + }, + + + /* Plays back the provided frame data + * Params {object|boolean}: + * - frames: previously recorded frame json + * - loop: whether or not to loop playback. Defaults to true. + */ + play: function () { + if (this.state === 'playing') return; + if ( this.loading() || this.recording.blank() ) return; + + this.state = 'playing'; + this.controller.connection.protocol = this.playbackProtocol; + + var player = this; + + // prevent the normal controller response while playing + this.controller.connection.removeAllListeners('frame'); + this.controller.connection.on('frame', function (frame) { + // resume play when hands are removed: + if (player.resumeOnHandLost && player.autoPlay && player.state == 'idle' && frame.hands.length == 0) { + player.play(); + } + + // The default LeapJS callback processes the frame, which is what we do now: + player.controller.processFrame(frame); + }); + + // Kick off + this.resetTimers(); + this.recording.readyPlay(); + this.stepFrameLoop(); + + this.controller.emit('playback.play', this); + }, + + // this method replaces connection.handleData when in record mode + // It accepts the raw connection data which is used to make a frame. + recordFrameHandler: function (frameData) { + // Would be better to check controller.streaming() in showOverlay, but that method doesn't exist, yet. + this.setGraphic('wave'); + if (frameData.hands.length > 0) { + this.recording.addFrame(frameData); + this.hideOverlay(); + } else if ( !this.recording.blank() ) { + this.finishRecording(); + } + }, + + + // Accepts a hash with any of + // URL, recording, metadata + // once loaded, the recording is immediately activated + setRecording: function (options) { + var player = this; + + // otherwise, the animation loop may try and play non-existant frames: + this.pause(); + + // this is called on the context of the recording + var loadComplete = function (frames) { + + this.setFrames(frames); + + if (player.recording != this){ + console.log('recordings changed during load'); + return + } + + if (player.autoPlay) { + player.play(); + if (player.pauseOnHand && !player.controller.streaming() ) { + player.setGraphic('connect'); + } + } + + player.controller.emit('playback.recordingSet', this); + }; + + this.recording = options; + + // Here we turn the existing argument in to a recording + // this allows frames to be added to the existing object via ajax + // saving ajax requests + if (!(options instanceof Recording)){ + + this.recording.__proto__ = Recording.prototype; + Recording.call(this.recording, { + timeBetweenLoops: this.options.timeBetweenLoops, + loop: this.options.loop, + loadProgress: function(recording, percentage, oEvent){ + player.controller.emit('playback.ajax:progress', recording, percentage, oEvent); + } + }); + + } + + + if ( this.recording.loaded() ) { + + loadComplete.call(this.recording, this.recording.frameData); + + } else if (options.url) { + + this.controller.emit('playback.ajax:begin', this, this.recording); + + // called in the context of the recording + this.recording.loadFrameData(function(frames){ + loadComplete.call(this, frames); + player.controller.emit('playback.ajax:complete', player, this); + }); + + } + + + return this; + }, + + + hideOverlay: function () { + if (!this.overlay) return; + this.overlay.style.display = 'none'; + }, + + + // Accepts either "connect", "wave", or undefined. + setGraphic: function (graphicName) { + if (!this.overlay) return; + if (this.graphicName == graphicName) return; + + this.graphicName = graphicName; + switch (graphicName) { + case 'connect': + this.overlay.style.display = 'block'; + this.overlay.innerHTML = CONNECT_LEAP_ICON; + break; + case 'wave': + this.overlay.style.display = 'block'; + this.overlay.innerHTML = MOVE_HAND_OVER_LEAP_ICON; + break; + case undefined: + this.overlay.innerHTML = ''; + break; + } + } + + }; + + // will only play back if device is disconnected + // Accepts options: + // - frames: [string] URL of .json frame data + // - autoPlay: [boolean true] Whether to turn on and off playback based off of connection state + // - overlay: [boolean or DOM element] Whether or not to show the overlay: "Connect your Leap Motion Controller" + // if a DOM element is passed, that will be shown/hidden instead of the default message. + // - pauseOnHand: [boolean true] Whether to stop playback when a hand is in field of view + // - resumeOnHandLost: [boolean true] Whether to resume playback after the hand leaves the frame + // - requiredProtocolVersion: clients connected with a lower protocol number will not be able to take control of the + // - timeBetweenLoops: [number, ms] delay between looping playback + // controller with their device. This option, if set, ovverrides autoPlay + // - pauseHotkey: [number or false, default: 32 (spacebar)] - keycode for pause, bound to body + var playback = function (scope) { + var controller = this; + var autoPlay = scope.autoPlay; + if (autoPlay === undefined) autoPlay = true; + + var pauseOnHand = scope.pauseOnHand; + if (pauseOnHand === undefined) pauseOnHand = true; + + var resumeOnHandLost = scope.resumeOnHandLost; + if (resumeOnHandLost === undefined) resumeOnHandLost = true; + + var timeBetweenLoops = scope.timeBetweenLoops; + if (timeBetweenLoops === undefined) timeBetweenLoops = 50; + + var requiredProtocolVersion = scope.requiredProtocolVersion; + + var pauseHotkey = scope.pauseHotkey; + if (pauseHotkey === undefined) pauseHotkey = 32; // spacebar + + var loop = scope.loop; + if (loop === undefined) loop = true; + + var overlay = scope.overlay; + // A better fix would be to set an onload handler for this, rather than disable the overlay. + if (overlay === undefined && document.body) { + overlay = document.createElement('div'); + document.body.appendChild(overlay); + overlay.style.width = '100%'; + overlay.style.position = 'absolute'; + overlay.style.top = '0'; + overlay.style.left = '-' + window.getComputedStyle(document.body).getPropertyValue('margin'); + overlay.style.padding = '10px'; + overlay.style.textAlign = 'center'; + overlay.style.fontSize = '18px'; + overlay.style.opacity = '0.8'; + overlay.style.display = 'none'; + overlay.style.zIndex = '10'; + overlay.id = 'connect-leap'; + overlay.style.cursor = 'pointer'; + overlay.addEventListener("click", function () { + this.style.display = 'none'; + return false; + }, false); + + } + + + scope.player = new Player(this, { + recording: scope.recording, + loop: loop, + pauseHotkey: pauseHotkey, + timeBetweenLoops: timeBetweenLoops + }); + + // By doing this, we allow player methods to be accessible on the scope + // this is the controller + scope.player.overlay = overlay; + scope.player.pauseOnHand = pauseOnHand; + scope.player.resumeOnHandLost = resumeOnHandLost; + scope.player.requiredProtocolVersion = requiredProtocolVersion; + scope.player.autoPlay = autoPlay; + + var setupStreamingEvents = function () { + if (scope.player.pauseOnHand && controller.connection.opts.requestProtocolVersion < scope.requiredProtocolVersion) { + console.log('Protocol Version too old (' + controller.connection.opts.requestProtocolVersion + '), disabling device interaction.'); + scope.player.pauseOnHand = false; + return + } + + if (autoPlay) { + controller.on('streamingStarted', function () { + if (scope.player.state == 'recording') { + scope.player.pause(); + scope.player.setGraphic('wave'); + } else { + if (pauseOnHand) { + scope.player.setGraphic('wave'); + } else { + scope.player.setGraphic(); + } + } + }); + + controller.on('streamingStopped', function () { + scope.player.play(); + }); + } + controller.on('streamingStopped', function () { + scope.player.setGraphic('connect'); + }); + } + + // ready happens before streamingStarted, allowing us to check the version before responding to streamingStart/Stop + // we can't call this any earlier, or protcol version won't be available + if (!!this.connection.connected) { + setupStreamingEvents() + } else { + this.on('ready', setupStreamingEvents) + } + + return {} + } + + + if ((typeof Leap !== 'undefined') && Leap.Controller) { + Leap.Controller.plugin('playback', playback); + } else if (typeof module !== 'undefined') { + module.exports.playback = playback; + } else { + throw 'leap.js not included'; + } + +}).call(this); +}( window )); +//CoffeeScript generated from main/screen-position/leap.screen-position.coffee +/* +Adds the "screenPosition" method by default to hands and pointables. This returns a vec3 (an array of length 3) +with [x,y,z] screen coordinates indicating where the hand is, originating from the bottom left. +This method can accept an optional vec3, allowing it to convert any arbitrary vec3 of coordinates. + +Custom positioning methods can be passed in, allowing different scaling techniques, +e.g., http://msdn.microsoft.com/en-us/library/windows/hardware/gg463319.aspx (Pointer Ballistics) +Here we scale based upon the interaction box and screen size: + +options: + scale, scaleX, and scaleY. They all default to 1. + verticalOffset: in pixels. This number is added to the returned Y value. Defaults to 0. + + + +controller.use 'screenPosition', { + method: (positionVec3)-> + Arguments for Leap.vec3 are (out, a, b) + [ + Leap.vec3.subtract(positionVec3, positionVec3, @frame.interactionBox.center) + Leap.vec3.divide(positionVec3, positionVec3, @frame.interactionBox.size) + Leap.vec3.multiply(positionVec3, positionVec3, [document.body.offsetWidth, document.body.offsetHeight, 0]) + ] +} +More info on vec3 can be found, here: http://glmatrix.net/docs/2.2.0/symbols/vec3.html +*/ + + +(function() { + var screenPosition; + + screenPosition = function(options) { + var baseScale, baseVerticalOffset, position, positioningMethods; + if (options == null) { + options = {}; + } + options.positioning || (options.positioning = 'absolute'); + options.scale || (options.scale = 1); + options.scaleX || (options.scaleX = 1); + options.scaleY || (options.scaleY = 1); + options.scaleZ || (options.scaleZ = 1); + options.verticalOffset || (options.verticalOffset = 0); + baseScale = 6; + baseVerticalOffset = -100; + positioningMethods = { + absolute: function(positionVec3) { + return [(window.innerWidth / 2) + (positionVec3[0] * baseScale * options.scale * options.scaleX), window.innerHeight + baseVerticalOffset + options.verticalOffset - (positionVec3[1] * baseScale * options.scale * options.scaleY), positionVec3[2] * baseScale * options.scale * options.scaleZ]; + } + }; + position = function(vec3, memoize) { + var screenPositionVec3; + if (memoize == null) { + memoize = false; + } + screenPositionVec3 = typeof options.positioning === 'function' ? options.positioning.call(this, vec3) : positioningMethods[options.positioning].call(this, vec3); + if (memoize) { + this.screenPositionVec3 = screenPositionVec3; + } + return screenPositionVec3; + }; + return { + hand: { + screenPosition: function(vec3) { + return position.call(this, vec3 || this.palmPosition, !vec3); + } + }, + pointable: { + screenPosition: function(vec3) { + return position.call(this, vec3 || this.tipPosition, !vec3); + } + } + }; + }; + + if ((typeof Leap !== 'undefined') && Leap.Controller) { + Leap.Controller.plugin('screenPosition', screenPosition); + } else if (typeof module !== 'undefined') { + module.exports.screenPosition = screenPosition; + } else { + throw 'leap.js not included'; + } + +}).call(this); + +//CoffeeScript generated from main/transform/leap.transform.coffee +(function() { + var __slice = [].slice; + + Leap.plugin('transform', function(scope) { + var noop, transformDirections, transformMat4Implicit0, transformPositions, transformWithMatrices, _directionTransform, _positionTransform; + if (scope == null) { + scope = {}; + } + noop = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; + _positionTransform = new THREE.Matrix4; + _directionTransform = new THREE.Matrix4; + scope.getDirectionTransform = function(hand) { + var matrix; + if (scope.transform) { + matrix = typeof scope.transform === 'function' ? scope.transform(hand) : scope.transform; + if (window['THREE'] && matrix instanceof THREE.Matrix4) { + return matrix.elements; + } else { + return matrix; + } + } else if (scope.position || scope.quaternion || scope.scale) { + _directionTransform.set.apply(_directionTransform, noop); + if (scope.quaternion) { + _directionTransform.makeRotationFromQuaternion(typeof scope.quaternion === 'function' ? scope.quaternion(hand) : scope.quaternion); + } + if (scope.position) { + _directionTransform.setPosition(typeof scope.position === 'function' ? scope.position(hand) : scope.position); + } + return _directionTransform.elements; + } else { + return noop; + } + }; + scope.getPositionTransform = function(hand) { + var matrix; + if (scope.transform) { + matrix = typeof scope.transform === 'function' ? scope.transform(hand) : scope.transform; + if (window['THREE'] && matrix instanceof THREE.Matrix4) { + return matrix.elements; + } else { + return matrix; + } + } else if (scope.position || scope.quaternion || scope.scale) { + _positionTransform.set.apply(_positionTransform, noop); + if (scope.quaternion) { + _positionTransform.makeRotationFromQuaternion(typeof scope.quaternion === 'function' ? scope.quaternion(hand) : scope.quaternion); + } + if (scope.scale) { + if (!isNaN(scope.scale)) { + scope.scale = new THREE.Vector3(scope.scale, scope.scale, scope.scale); + } + _positionTransform.scale(typeof scope.scale === 'function' ? scope.scale(hand) : scope.scale); + } + if (scope.position) { + _positionTransform.setPosition(typeof scope.position === 'function' ? scope.position(hand) : scope.position); + } + return _positionTransform.elements; + } else { + return noop; + } + }; + transformPositions = function() { + var matrix, vec3, vec3s, _i, _len, _results; + matrix = arguments[0], vec3s = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + _results = []; + for (_i = 0, _len = vec3s.length; _i < _len; _i++) { + vec3 = vec3s[_i]; + if (vec3) { + _results.push(Leap.vec3.transformMat4(vec3, vec3, matrix)); + } else { + _results.push(void 0); + } + } + return _results; + }; + transformMat4Implicit0 = function(out, a, m) { + var x, y, z; + x = a[0]; + y = a[1]; + z = a[2]; + out[0] = m[0] * x + m[4] * y + m[8] * z; + out[1] = m[1] * x + m[5] * y + m[9] * z; + out[2] = m[2] * x + m[6] * y + m[10] * z; + return out; + }; + transformDirections = function() { + var matrix, vec3, vec3s, _i, _len, _results; + matrix = arguments[0], vec3s = 2 <= arguments.length ? __slice.call(arguments, 1) : []; + _results = []; + for (_i = 0, _len = vec3s.length; _i < _len; _i++) { + vec3 = vec3s[_i]; + if (vec3) { + _results.push(transformMat4Implicit0(vec3, vec3, matrix)); + } else { + _results.push(void 0); + } + } + return _results; + }; + transformWithMatrices = function(hand, positionTransform, directionTransform) { + var finger, _i, _len, _ref, _results; + transformPositions(positionTransform, hand.palmPosition, hand.stabilizedPalmPosition, hand.sphereCenter, hand.arm.nextJoint, hand.arm.prevJoint); + transformDirections(directionTransform, hand.direction, hand.palmNormal, hand.palmVelocity, hand.arm.basis[0], hand.arm.basis[1], hand.arm.basis[2]); + _ref = hand.fingers; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + finger = _ref[_i]; + transformPositions(positionTransform, finger.carpPosition, finger.mcpPosition, finger.pipPosition, finger.dipPosition, finger.distal.nextJoint, finger.tipPosition); + _results.push(transformDirections(directionTransform, finger.direction, finger.metacarpal.basis[0], finger.metacarpal.basis[1], finger.metacarpal.basis[2], finger.proximal.basis[0], finger.proximal.basis[1], finger.proximal.basis[2], finger.medial.basis[0], finger.medial.basis[1], finger.medial.basis[2], finger.distal.basis[0], finger.distal.basis[1], finger.distal.basis[2])); + } + return _results; + }; + return { + hand: function(hand) { + var finger, len, _i, _len, _ref; + transformWithMatrices(hand, scope.getPositionTransform(hand), scope.getDirectionTransform(hand)); + if (scope.effectiveParent) { + transformWithMatrices(hand, scope.effectiveParent.matrixWorld.elements, scope.effectiveParent.matrixWorld.elements); + } + len = null; + _ref = hand.fingers; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + finger = _ref[_i]; + len = Leap.vec3.create(); + Leap.vec3.sub(len, finger.mcpPosition, finger.carpPosition); + finger.metacarpal.length = Leap.vec3.length(len); + Leap.vec3.sub(len, finger.pipPosition, finger.mcpPosition); + finger.proximal.length = Leap.vec3.length(len); + Leap.vec3.sub(len, finger.dipPosition, finger.pipPosition); + finger.medial.length = Leap.vec3.length(len); + Leap.vec3.sub(len, finger.tipPosition, finger.dipPosition); + finger.distal.length = Leap.vec3.length(len); + } + Leap.vec3.sub(len, hand.arm.prevJoint, hand.arm.nextJoint); + return hand.arm.length = Leap.vec3.length(len); + } + }; + }); + +}).call(this); + +//CoffeeScript generated from main/version-check/leap.version-check.coffee +(function() { + var versionCheck; + + versionCheck = function(scope) { + scope.alert || (scope.alert = false); + scope.requiredProtocolVersion || (scope.requiredProtocolVersion = 6); + scope.disconnect || (scope.disconnect = true); + if ((typeof Leap !== 'undefined') && Leap.Controller) { + if (Leap.version.minor < 5 && Leap.version.dot < 4) { + console.warn("LeapJS Version Check plugin incompatible with LeapJS pre 0.4.4"); + } + } + this.on('ready', function() { + var current, message, required; + required = scope.requiredProtocolVersion; + current = this.connection.opts.requestProtocolVersion; + if (current < required) { + message = "Protocol Version too old. v" + required + " required, v" + current + " available."; + if (scope.disconnect) { + this.disconnect(); + message += " Disconnecting."; + } + console.warn(message); + if (scope.alert) { + alert("Your Leap Software version is out of date. Visit http://www.leapmotion.com/setup to update"); + } + return this.emit('versionCheck.outdated', { + required: required, + current: current, + disconnect: scope.disconnect + }); + } + }); + return {}; + }; + + if ((typeof Leap !== 'undefined') && Leap.Controller) { + Leap.Controller.plugin('versionCheck', versionCheck); + } else if (typeof module !== 'undefined') { + module.exports.versionCheck = versionCheck; + } else { + throw 'leap.js not included'; + } + +}).call(this); diff --git a/v2/cloth/js/leap.pinchEvent.js b/v2/cloth/js/leap.pinchEvent.js new file mode 100644 index 0000000..b3ce3f2 --- /dev/null +++ b/v2/cloth/js/leap.pinchEvent.js @@ -0,0 +1,58 @@ +// this should get a new name, now that it includes grabEvent. +Leap.plugin('pinchEvent', function(scope){ + + this.use('handHold'); + + // no hysteresis, first time around. + scope.pinchThreshold || (scope.pinchThreshold = 0.5); + scope.grabThreshold || (scope.grabThreshold = 0.5); + + var controller = this; + + // todo - add hand lost events. + this.on('handLost', function(hand){ + + if (hand.data('pinchEvent.pinching')){ + controller.emit('unpinch', hand ); + } + + if (hand.data('pinchEvent.grabbing')){ + controller.emit('ungrab', hand ); + } + + }); + + + return { + + hand: function(hand){ + + var pinching = hand.pinchStrength > scope.pinchThreshold; + + if (hand.data('pinchEvent.pinching') != pinching){ + + controller.emit( + pinching ? 'pinch' : 'unpinch', + hand + ); + + hand.data('pinchEvent.pinching', pinching) + } + + + var grabbing = hand.grabStrength > scope.grabThreshold; + + if (hand.data('pinchEvent.grabbing') != grabbing){ + + controller.emit( + grabbing ? 'grab' : 'ungrab', + hand + ); + + hand.data('pinchEvent.grabbing', grabbing) + } + + } + + } +});