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)
+ }
+
+ }
+
+ }
+});