From f99f6eb135610d81564930f143ec8c24e110f294 Mon Sep 17 00:00:00 2001 From: Peter Ehrlich Date: Sat, 25 Oct 2014 12:06:27 -0700 Subject: [PATCH 01/29] WIP cloth example --- v2/cloth/circuit_pattern.png | Bin 0 -> 5782 bytes v2/cloth/index.html | 354 ++++ v2/cloth/js/Cloth.js | 252 +++ v2/cloth/js/Detector.js | 66 + v2/cloth/js/leap-plugins-0.1.9pre.js | 2491 ++++++++++++++++++++++++++ v2/cloth/js/leap.pinchEvent.js | 58 + 6 files changed, 3221 insertions(+) create mode 100644 v2/cloth/circuit_pattern.png create mode 100644 v2/cloth/index.html create mode 100644 v2/cloth/js/Cloth.js create mode 100644 v2/cloth/js/Detector.js create mode 100644 v2/cloth/js/leap-plugins-0.1.9pre.js create mode 100644 v2/cloth/js/leap.pinchEvent.js diff --git a/v2/cloth/circuit_pattern.png b/v2/cloth/circuit_pattern.png new file mode 100644 index 0000000000000000000000000000000000000000..64b96eb77145fc85ec8fca280db70e5738561959 GIT binary patch literal 5782 zcmZu#d05iv7N*8V%gLE0rzVwZswpEUN;3@v~PgQb`ekl$J5?)nv+I z)KYMAAtiCl5*L(m3cFUKxTm;eiYr*4C@9>I*(&T0p6~bYaDL}}?|ILA&f&j5x;U&{ zVYC7QfviLy{=pprSqdJPLi9cbhb*pD0gmNShkc?U5dGDf&l1SZtgpeBx-n>Hd)*#A zoli{G>*>TSgFx0;qJKEx8TWEh*i3o7F>Tq877c23$S}cc#{(+*(YLR%MB@98`m4VG zbt)seQ5O<6WLE%z=s+O*!P`DO+~Wi1Z@u|l!k_AM;)=Ipc;Ph}YsYI<%7*!x4M`x3 z5f@;y1OoX20y&@7Yq+F#;pmUV(*Ec$6RtnLKIx`QDt?+Xag<#4biT_$B8gnn#g#VI z9!akyt7A-nZO%8q_%EN1it^R{l)8s)X~{8O{63pKy>0S(+U>m3fw`>#^5q*O97&$r-4UlIF_KTEtw<>{ytEY;d+SB}0b=u{DJOK3 z_z5uDvjtod_|pSOp(B$aE&8$uvmSzh@EK=HUBHhYYP`bWRp*vIqUHLvgzG z|B>ebn8FSNyZ2z~?X(^B487+O9aZ$j9mhu)>c!uJc-Lyht}mj8Ty9`y+^EwW#kz%P z8o1{EgCJ<&Q{(!EY*KW-Z%x3&$m`b0md93?uyp6!VsL69yJAKE^>W zQnbCIbXkO8Mh$y7QGRaFx+n&9q|bq|Vh7%ay()%G)&EzEqy*>otG1Qbnk9_~)%c`5 zzO%|*&ojgX+1^wUH95w~3~I#;t$t}Sud1q%XeZ;$s$=e=qP$nvWCq7wXZ>FsjHU

+EN)t}mYE->~k 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 literal 0 HcmV?d00001 diff --git a/v2/cloth/index.html b/v2/cloth/index.html new file mode 100644 index 0000000..99ab890 --- /dev/null +++ b/v2/cloth/index.html @@ -0,0 +1,354 @@ + + + + three.js webgl - cloth simulation + + + + + + + + +

Simple Cloth Simulation
+ Verlet integration with Constrains relaxation
+ Toggle: Camera | + Wind | + Ball | + Pins +
+ + + + + + + + + + + + + + + + + diff --git a/v2/cloth/js/Cloth.js b/v2/cloth/js/Cloth.js new file mode 100644 index 0000000..be5ace1 --- /dev/null +++ b/v2/cloth/js/Cloth.js @@ -0,0 +1,252 @@ +/* + * 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 + +var DAMPING = 0.03; +var DRAG = 1 - DAMPING; +var MASS = .1; +var restDistance = 25; + + +var xSegs = 20; +var ySegs = 20; + +var clothFunction = plane(restDistance * xSegs, restDistance * ySegs); + +var cloth = new Cloth(xSegs, ySegs); + +var GRAVITY = 981 * 1.4; // +var gravity = new THREE.Vector3( 0, -GRAVITY, 0 ).multiplyScalar(MASS); + + +var TIMESTEP = 18 / 1000; +var TIMESTEP_SQ = TIMESTEP * TIMESTEP; + +var pins = []; + + +var wind = false; +var windStrength = 2; +var windForce = new THREE.Vector3(0,0,0); + +var ballPosition = new THREE.Vector3(0, -45, 0); +var ballSize = 40; + +var tmpForce = new THREE.Vector3(); + +var lastTime; + + +function plane(width, height) { + + return function(u, v) { + var x = (u-0.5) * width; + var y = (v+0.5) * height; + var z = 0; + + return new THREE.Vector3(x, y, z); + }; +} + +function Particle(x, y, z, mass) { + this.position = clothFunction(x, y); + this.previous = clothFunction(x, y); + this.original = clothFunction(x, y); + this.a = new THREE.Vector3(0, 0, 0); // acceleration + this.mass = mass; + this.invMass = 1 / mass; + this.tmp = new THREE.Vector3(); // wat + this.tmp2 = new THREE.Vector3(); +} + +// Force -> Acceleration +Particle.prototype.addForce = function(force) { + this.a.add( + this.tmp2.copy(force).multiplyScalar(this.invMass) + ); +}; + + +// Performs verlet integration +// instantaneous velocity times drag plus position plus acceleration times time +Particle.prototype.integrate = function(timesq) { + var newPos = this.tmp.subVectors(this.position, this.previous); + newPos.multiplyScalar(DRAG).add(this.position); + newPos.add(this.a.multiplyScalar(timesq)); + + this.tmp = this.previous; + this.previous = this.position; + this.position = newPos; + + this.a.set(0, 0, 0); +} + + +var diff = new THREE.Vector3(); + +// should be moved to a class method? +function satisifyConstrains(p1, p2, distance) { + diff.subVectors(p2.position, p1.position); + var currentDist = diff.length(); + if (currentDist==0) return; // prevents division by 0 + var correction = diff.multiplyScalar(1 - distance/currentDist); // not sure why this is better than a straight-up subtraction. + var correctionHalf = correction.multiplyScalar(0.5); + p1.position.add(correctionHalf); + p2.position.sub(correctionHalf); +} + + +function Cloth(w, h) { + w = w || 10; + h = h || 10; + this.w = w; + this.h = h; + + var particles = []; + var constrains = []; + + var u, v; + + // Create particles + for (v=0;v<=h;v++) { + for (u=0;u<=w;u++) { + particles.push( + new Particle(u/w, v/h, 0, MASS) + ); + } + } + + // Structural + + for (v=0;vWebGL.
', + '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/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) + } + + } + + } +}); From 4a07adfd775977a7718fcf8fa9f0a43fc49d3ee9 Mon Sep 17 00:00:00 2001 From: Peter Ehrlich Date: Sat, 25 Oct 2014 12:42:48 -0700 Subject: [PATCH 02/29] Refactor to improve object orientation --- v2/cloth/index.html | 37 +++++++++++++----- v2/cloth/js/Cloth.js | 93 ++++++++++++++++---------------------------- 2 files changed, 61 insertions(+), 69 deletions(-) diff --git a/v2/cloth/index.html b/v2/cloth/index.html index 99ab890..0f9dccd 100644 --- a/v2/cloth/index.html +++ b/v2/cloth/index.html @@ -1,7 +1,7 @@ - three.js webgl - cloth simulation + Leap Cloth