leapjs
Version:
JavaScript client for the Leap Motion Controller
1,481 lines (1,371 loc) • 222 kB
JavaScript
;(function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){
/*!
* LeapJS v0.4.3
* http://github.com/leapmotion/leapjs/
*
* Copyright 2013 LeapMotion, Inc. and other contributors
* Released under the BSD-2-Clause license
* http://github.com/leapmotion/leapjs/blob/master/LICENSE.txt
*/
},{}],2:[function(require,module,exports){
var CircularBuffer = module.exports = function(size) {
this.pos = 0;
this._buf = [];
this.size = size;
}
CircularBuffer.prototype.get = function(i) {
if (i == undefined) i = 0;
if (i >= this.size) return undefined;
if (i >= this._buf.length) return undefined;
return this._buf[(this.pos - i - 1) % this.size];
}
CircularBuffer.prototype.push = function(o) {
this._buf[this.pos % this.size] = o;
return this.pos++;
}
},{}],3:[function(require,module,exports){
var chooseProtocol = require('../protocol').chooseProtocol
, EventEmitter = require('events').EventEmitter
, _ = require('underscore');
var BaseConnection = module.exports = function(opts) {
this.opts = _.defaults(opts || {}, {
host : '127.0.0.1',
enableGestures: false,
port: 6437,
background: false,
requestProtocolVersion: 4
});
this.host = this.opts.host;
this.port = this.opts.port;
this.protocolVersionVerified = false;
this.on('ready', function() {
this.enableGestures(this.opts.enableGestures);
this.setBackground(this.opts.background);
});
}
BaseConnection.prototype.getUrl = function() {
return "ws://" + this.host + ":" + this.port + "/v" + this.opts.requestProtocolVersion + ".json";
}
BaseConnection.prototype.setBackground = function(state) {
this.opts.background = state;
if (this.protocol && this.protocol.sendBackground && this.background !== this.opts.background) {
this.background = this.opts.background;
this.protocol.sendBackground(this, this.opts.background);
}
}
BaseConnection.prototype.handleOpen = function() {
if (!this.connected) {
this.connected = true;
this.emit('connect');
}
}
BaseConnection.prototype.enableGestures = function(enabled) {
this.gesturesEnabled = enabled ? true : false;
this.send(this.protocol.encode({"enableGestures": this.gesturesEnabled}));
}
BaseConnection.prototype.handleClose = function(code, reason) {
if (!this.connected) return;
this.disconnect();
// 1001 - an active connection is closed
// 1006 - cannot connect
if (code === 1001 && this.opts.requestProtocolVersion > 1) {
if (this.protocolVersionVerified) {
this.protocolVersionVerified = false;
}else{
this.opts.requestProtocolVersion--;
}
}
this.startReconnection();
}
BaseConnection.prototype.startReconnection = function() {
var connection = this;
this.reconnectionTimer = setInterval(function() { connection.reconnect() }, 1000);
}
BaseConnection.prototype.disconnect = function() {
if (!this.socket) return;
this.socket.close();
delete this.socket;
delete this.protocol;
delete this.background; // This is not persisted when reconnecting to the web socket server
if (this.connected) {
this.connected = false;
this.emit('disconnect');
}
return true;
}
BaseConnection.prototype.reconnect = function() {
if (this.connected) {
clearInterval(this.reconnectionTimer);
} else {
this.disconnect();
this.connect();
}
}
BaseConnection.prototype.handleData = function(data) {
var message = JSON.parse(data);
var messageEvent;
if (this.protocol === undefined) {
messageEvent = this.protocol = chooseProtocol(message);
this.protocolVersionVerified = true;
this.emit('ready');
} else {
messageEvent = this.protocol(message);
}
this.emit(messageEvent.type, messageEvent);
}
BaseConnection.prototype.connect = function() {
if (this.socket) return;
this.socket = this.setupSocket();
return true;
}
BaseConnection.prototype.send = function(data) {
this.socket.send(data);
}
BaseConnection.prototype.reportFocus = function(state) {
if (this.focusedState === state) return;
this.focusedState = state;
this.emit(this.focusedState ? 'focus' : 'blur');
if (this.protocol && this.protocol.sendFocused) {
this.protocol.sendFocused(this, this.focusedState);
}
}
_.extend(BaseConnection.prototype, EventEmitter.prototype);
},{"../protocol":13,"events":19,"underscore":22}],4:[function(require,module,exports){
var BaseConnection = module.exports = require('./base')
, _ = require('underscore');
var BrowserConnection = module.exports = function(opts) {
BaseConnection.call(this, opts);
var connection = this;
this.on('ready', function() { connection.startFocusLoop(); })
this.on('disconnect', function() { connection.stopFocusLoop(); })
}
_.extend(BrowserConnection.prototype, BaseConnection.prototype);
BrowserConnection.prototype.setupSocket = function() {
var connection = this;
var socket = new WebSocket(this.getUrl());
socket.onopen = function() { connection.handleOpen(); };
socket.onclose = function(data) { connection.handleClose(data['code'], data['reason']); };
socket.onmessage = function(message) { connection.handleData(message.data) };
return socket;
}
BrowserConnection.prototype.startFocusLoop = function() {
if (this.focusDetectorTimer) return;
var connection = this;
var propertyName = null;
if (typeof document.hidden !== "undefined") {
propertyName = "hidden";
} else if (typeof document.mozHidden !== "undefined") {
propertyName = "mozHidden";
} else if (typeof document.msHidden !== "undefined") {
propertyName = "msHidden";
} else if (typeof document.webkitHidden !== "undefined") {
propertyName = "webkitHidden";
} else {
propertyName = undefined;
}
if (connection.windowVisible === undefined) {
connection.windowVisible = propertyName === undefined ? true : document[propertyName] === false;
}
var focusListener = window.addEventListener('focus', function(e) {
connection.windowVisible = true;
updateFocusState();
});
var blurListener = window.addEventListener('blur', function(e) {
connection.windowVisible = false;
updateFocusState();
});
this.on('disconnect', function() {
window.removeEventListener('focus', focusListener);
window.removeEventListener('blur', blurListener);
});
var updateFocusState = function() {
var isVisible = propertyName === undefined ? true : document[propertyName] === false;
connection.reportFocus(isVisible && connection.windowVisible);
}
this.focusDetectorTimer = setInterval(updateFocusState, 100);
}
BrowserConnection.prototype.stopFocusLoop = function() {
if (!this.focusDetectorTimer) return;
clearTimeout(this.focusDetectorTimer);
delete this.focusDetectorTimer;
}
},{"./base":3,"underscore":22}],5:[function(require,module,exports){
var process=require("__browserify_process");var Frame = require('./frame')
, Hand = require('./hand')
, Pointable = require('./pointable')
, CircularBuffer = require("./circular_buffer")
, Pipeline = require("./pipeline")
, EventEmitter = require('events').EventEmitter
, gestureListener = require('./gesture').gestureListener
, _ = require('underscore');
/**
* Constructs a Controller object.
*
* When creating a Controller object, you may optionally pass in options
* to set the host , set the port, enable gestures, or select the frame event type.
*
* ```javascript
* var controller = new Leap.Controller({
* host: '127.0.0.1',
* port: 6437,
* enableGestures: true,
* frameEventName: 'animationFrame'
* });
* ```
*
* @class Controller
* @memberof Leap
* @classdesc
* The Controller class is your main interface to the Leap Motion Controller.
*
* Create an instance of this Controller class to access frames of tracking data
* and configuration information. Frame data can be polled at any time using the
* [Controller.frame]{@link Leap.Controller#frame}() function. Call frame() or frame(0) to get the most recent
* frame. Set the history parameter to a positive integer to access previous frames.
* A controller stores up to 60 frames in its frame history.
*
* Polling is an appropriate strategy for applications which already have an
* intrinsic update loop, such as a game.
*/
var Controller = module.exports = function(opts) {
var inNode = (typeof(process) !== 'undefined' && process.versions && process.versions.node),
controller = this;
opts = _.defaults(opts || {}, {
inNode: inNode
});
this.inNode = opts.inNode;
opts = _.defaults(opts || {}, {
frameEventName: this.useAnimationLoop() ? 'animationFrame' : 'deviceFrame',
suppressAnimationLoop: !this.useAnimationLoop(),
loopWhileDisconnected: false,
useAllPlugins: false
});
this.animationFrameRequested = false;
this.onAnimationFrame = function() {
controller.emit('animationFrame', controller.lastConnectionFrame);
if (controller.loopWhileDisconnected && (controller.connection.focusedState || controller.connection.opts.background) ){
window.requestAnimationFrame(controller.onAnimationFrame);
}else{
controller.animationFrameRequested = false;
}
}
this.suppressAnimationLoop = opts.suppressAnimationLoop;
this.loopWhileDisconnected = opts.loopWhileDisconnected;
this.frameEventName = opts.frameEventName;
this.useAllPlugins = opts.useAllPlugins;
this.history = new CircularBuffer(200);
this.lastFrame = Frame.Invalid;
this.lastValidFrame = Frame.Invalid;
this.lastConnectionFrame = Frame.Invalid;
this.accumulatedGestures = [];
if (opts.connectionType === undefined) {
this.connectionType = (this.inBrowser() ? require('./connection/browser') : require('./connection/node'));
} else {
this.connectionType = opts.connectionType;
}
this.connection = new this.connectionType(opts);
this.plugins = {};
this._pluginPipelineSteps = {};
this._pluginExtendedMethods = {};
if (opts.useAllPlugins) this.useRegisteredPlugins();
this.setupConnectionEvents();
}
Controller.prototype.gesture = function(type, cb) {
var creator = gestureListener(this, type);
if (cb !== undefined) {
creator.stop(cb);
}
return creator;
}
/*
* @returns the controller
*/
Controller.prototype.setBackground = function(state) {
this.connection.setBackground(state);
return this;
}
Controller.prototype.inBrowser = function() {
return !this.inNode;
}
Controller.prototype.useAnimationLoop = function() {
return this.inBrowser() && !this.inBackgroundPage();
}
Controller.prototype.inBackgroundPage = function(){
// http://developer.chrome.com/extensions/extension#method-getBackgroundPage
return (typeof(chrome) !== "undefined") &&
chrome.extension &&
chrome.extension.getBackgroundPage &&
(chrome.extension.getBackgroundPage() === window)
}
/*
* @returns the controller
*/
Controller.prototype.connect = function() {
this.connection.connect();
return this;
}
Controller.prototype.runAnimationLoop = function(){
if (!this.suppressAnimationLoop && !this.animationFrameRequested) {
this.animationFrameRequested = true;
window.requestAnimationFrame(this.onAnimationFrame);
}
}
/*
* @returns the controller
*/
Controller.prototype.disconnect = function() {
this.connection.disconnect();
return this;
}
/**
* Returns a frame of tracking data from the Leap.
*
* Use the optional history parameter to specify which frame to retrieve.
* Call frame() or frame(0) to access the most recent frame; call frame(1) to
* access the previous frame, and so on. If you use a history value greater
* than the number of stored frames, then the controller returns an invalid frame.
*
* @method frame
* @memberof Leap.Controller.prototype
* @param {number} history The age of the frame to return, counting backwards from
* the most recent frame (0) into the past and up to the maximum age (59).
* @returns {Leap.Frame} The specified frame; or, if no history
* parameter is specified, the newest frame. If a frame is not available at
* the specified history position, an invalid Frame is returned.
*/
Controller.prototype.frame = function(num) {
return this.history.get(num) || Frame.Invalid;
}
Controller.prototype.loop = function(callback) {
switch (callback.length) {
case 1:
this.on(this.frameEventName, callback);
break;
case 2:
var controller = this;
var scheduler = null;
var immediateRunnerCallback = function(frame) {
callback(frame, function() {
if (controller.lastFrame != frame) {
immediateRunnerCallback(controller.lastFrame);
} else {
controller.once(controller.frameEventName, immediateRunnerCallback);
}
});
}
this.once(this.frameEventName, immediateRunnerCallback);
break;
}
return this.connect();
}
Controller.prototype.addStep = function(step) {
if (!this.pipeline) this.pipeline = new Pipeline(this);
this.pipeline.addStep(step);
}
// this is run on every deviceFrame
Controller.prototype.processFrame = function(frame) {
if (frame.gestures) {
this.accumulatedGestures = this.accumulatedGestures.concat(frame.gestures);
}
// lastConnectionFrame is used by the animation loop
this.lastConnectionFrame = frame;
this.runAnimationLoop();
this.emit('deviceFrame', frame);
}
// on a this.deviceEventName (usually 'animationFrame' in browsers), this emits a 'frame'
Controller.prototype.processFinishedFrame = function(frame) {
this.lastFrame = frame;
if (frame.valid) {
this.lastValidFrame = frame;
}
frame.controller = this;
frame.historyIdx = this.history.push(frame);
if (frame.gestures) {
frame.gestures = this.accumulatedGestures;
this.accumulatedGestures = [];
for (var gestureIdx = 0; gestureIdx != frame.gestures.length; gestureIdx++) {
this.emit("gesture", frame.gestures[gestureIdx], frame);
}
}
if (this.pipeline) {
frame = this.pipeline.run(frame);
if (!frame) frame = Frame.Invalid;
}
this.emit('frame', frame);
}
Controller.prototype.setupConnectionEvents = function() {
var controller = this;
this.connection.on('frame', function(frame) {
controller.processFrame(frame);
});
this.on(this.frameEventName, function(frame) {
controller.processFinishedFrame(frame);
});
// Delegate connection events
this.connection.on('disconnect', function() { controller.emit('disconnect'); });
this.connection.on('ready', function() { controller.emit('ready'); });
this.connection.on('connect', function() { controller.emit('connect'); });
this.connection.on('focus', function() { controller.emit('focus'); controller.runAnimationLoop(); });
this.connection.on('blur', function() { controller.emit('blur') });
this.connection.on('protocol', function(protocol) { controller.emit('protocol', protocol); });
this.connection.on('deviceConnect', function(evt) { controller.emit(evt.state ? 'deviceConnected' : 'deviceDisconnected'); });
}
Controller._pluginFactories = {};
/*
* Registers a plugin, making is accessible to controller.use later on.
*
* @member plugin
* @memberof Leap.Controller.prototype
* @param {String} name The name of the plugin (usually camelCase).
* @param {function} factory A factory method which will return an instance of a plugin.
* The factory receives an optional hash of options, passed in via controller.use.
*
* Valid keys for the object include frame, hand, finger, tool, and pointable. The value
* of each key can be either a function or an object. If given a function, that function
* will be called once for every instance of the object, with that instance injected as an
* argument. This allows decoration of objects with additional data:
*
* ```javascript
* Leap.Controller.plugin('testPlugin', function(options){
* return {
* frame: function(frame){
* frame.foo = 'bar';
* }
* }
* });
* ```
*
* When hand is used, the callback is called for every hand in `frame.hands`. Note that
* hand objects are recreated with every new frame, so that data saved on the hand will not
* persist.
*
* ```javascript
* Leap.Controller.plugin('testPlugin', function(){
* return {
* hand: function(hand){
* console.log('testPlugin running on hand ' + hand.id);
* }
* }
* });
* ```
*
* A factory can return an object to add custom functionality to Frames, Hands, or Pointables.
* The methods are added directly to the object's prototype. Finger and Tool cannot be used here, Pointable
* must be used instead.
* This is encouraged for calculations which may not be necessary on every frame.
* Memoization is also encouraged, for cases where the method may be called many times per frame by the application.
*
* ```javascript
* // This plugin allows hand.usefulData() to be called later.
* Leap.Controller.plugin('testPlugin', function(){
* return {
* hand: {
* usefulData: function(){
* console.log('usefulData on hand', this.id);
* // memoize the results on to the hand, preventing repeat work:
* this.x || this.x = someExpensiveCalculation();
* return this.x;
* }
* }
* }
* });
*
* Note that the factory pattern allows encapsulation for every plugin instance.
*
* ```javascript
* Leap.Controller.plugin('testPlugin', function(options){
* options || options = {}
* options.center || options.center = [0,0,0]
*
* privatePrintingMethod = function(){
* console.log('privatePrintingMethod - options', options);
* }
*
* return {
* pointable: {
* publicPrintingMethod: function(){
* privatePrintingMethod();
* }
* }
* }
* });
*
*/
Controller.plugin = function(pluginName, factory) {
if (this._pluginFactories[pluginName]) {
throw "Plugin \"" + pluginName + "\" already registered";
}
return this._pluginFactories[pluginName] = factory;
};
/*
* Returns a list of registered plugins.
* @returns {Array} Plugin Factories.
*/
Controller.plugins = function() {
return _.keys(this._pluginFactories);
};
/*
* Begin using a registered plugin. The plugin's functionality will be added to all frames
* returned by the controller (and/or added to the objects within the frame).
* - The order of plugin execution inside the loop will match the order in which use is called by the application.
* - The plugin be run for both deviceFrames and animationFrames.
*
* If called a second time, the options will be merged with those of the already instantiated plugin.
*
* @method use
* @memberOf Leap.Controller.prototype
* @param pluginName
* @param {Hash} Options to be passed to the plugin's factory.
* @returns the controller
*/
Controller.prototype.use = function(pluginName, options) {
var functionOrHash, pluginFactory, key, pluginInstance, klass;
pluginFactory = (typeof pluginName == 'function') ? pluginName : Controller._pluginFactories[pluginName];
if (!pluginFactory) {
throw 'Leap Plugin ' + pluginName + ' not found.';
}
options || (options = {});
if (this.plugins[pluginName]){
_.extend(this.plugins[pluginName], options)
return this;
}
this.plugins[pluginName] = options;
pluginInstance = pluginFactory.call(this, options);
for (key in pluginInstance) {
functionOrHash = pluginInstance[key];
if (typeof functionOrHash === 'function') {
if (!this.pipeline) this.pipeline = new Pipeline(this);
if (!this._pluginPipelineSteps[pluginName]) this._pluginPipelineSteps[pluginName] = [];
this._pluginPipelineSteps[pluginName].push( this.pipeline.addWrappedStep(key, functionOrHash) );
} else {
if (!this._pluginExtendedMethods[pluginName]) this._pluginExtendedMethods[pluginName] = [];
switch (key) {
case 'frame':
klass = Frame
break;
case 'hand':
klass = Hand
break;
case 'pointable':
klass = Pointable
break;
default:
throw pluginName + ' specifies invalid object type "' + key + '" for prototypical extension'
}
_.extend(klass.prototype, functionOrHash);
_.extend(klass.Invalid, functionOrHash);
this._pluginExtendedMethods[pluginName].push([klass, functionOrHash])
}
}
return this;
};
/*
* Stop using a used plugin. This will remove any of the plugin's pipeline methods (those called on every frame)
* and remove any methods which extend frame-object prototypes.
*
* @method stopUsing
* @memberOf Leap.Controller.prototype
* @param pluginName
* @returns the controller
*/
Controller.prototype.stopUsing = function (pluginName) {
var steps = this._pluginPipelineSteps[pluginName],
extMethodHashes = this._pluginExtendedMethods[pluginName],
i = 0, klass, extMethodHash;
if (!this.plugins[pluginName]) return;
if (steps) {
for (i = 0; i < steps.length; i++) {
this.pipeline.removeStep(steps[i]);
}
}
if (extMethodHashes){
for (i = 0; i < extMethodHashes.length; i++){
klass = extMethodHashes[i][0]
extMethodHash = extMethodHashes[i][1]
for (var methodName in extMethodHash) {
delete klass.prototype[methodName]
delete klass.Invalid[methodName]
}
}
}
delete this.plugins[pluginName]
return this;
}
Controller.prototype.useRegisteredPlugins = function(){
for (var plugin in Controller._pluginFactories){
this.use(plugin);
}
}
_.extend(Controller.prototype, EventEmitter.prototype);
},{"./circular_buffer":2,"./connection/browser":4,"./connection/node":18,"./frame":6,"./gesture":7,"./hand":8,"./pipeline":11,"./pointable":12,"__browserify_process":20,"events":19,"underscore":22}],6:[function(require,module,exports){
var Hand = require("./hand")
, Pointable = require("./pointable")
, createGesture = require("./gesture").createGesture
, glMatrix = require("gl-matrix")
, mat3 = glMatrix.mat3
, vec3 = glMatrix.vec3
, InteractionBox = require("./interaction_box")
, _ = require("underscore");
/**
* Constructs a Frame object.
*
* Frame instances created with this constructor are invalid.
* Get valid Frame objects by calling the
* [Controller.frame]{@link Leap.Controller#frame}() function.
*<C-D-Space>
* @class Frame
* @memberof Leap
* @classdesc
* The Frame class represents a set of hand and finger tracking data detected
* in a single frame.
*
* The Leap detects hands, fingers and tools within the tracking area, reporting
* their positions, orientations and motions in frames at the Leap frame rate.
*
* Access Frame objects using the [Controller.frame]{@link Leap.Controller#frame}() function.
*/
var Frame = module.exports = function(data) {
/**
* Reports whether this Frame instance is valid.
*
* A valid Frame is one generated by the Controller object that contains
* tracking data for all detected entities. An invalid Frame contains no
* actual tracking data, but you can call its functions without risk of a
* undefined object exception. The invalid Frame mechanism makes it more
* convenient to track individual data across the frame history. For example,
* you can invoke:
*
* ```javascript
* var finger = controller.frame(n).finger(fingerID);
* ```
*
* for an arbitrary Frame history value, "n", without first checking whether
* frame(n) returned a null object. (You should still check that the
* returned Finger instance is valid.)
*
* @member valid
* @memberof Leap.Frame.prototype
* @type {Boolean}
*/
this.valid = true;
/**
* A unique ID for this Frame. Consecutive frames processed by the Leap
* have consecutive increasing values.
* @member id
* @memberof Leap.Frame.prototype
* @type {String}
*/
this.id = data.id;
/**
* The frame capture time in microseconds elapsed since the Leap started.
* @member timestamp
* @memberof Leap.Frame.prototype
* @type {number}
*/
this.timestamp = data.timestamp;
/**
* The list of Hand objects detected in this frame, given in arbitrary order.
* The list can be empty if no hands are detected.
*
* @member hands[]
* @memberof Leap.Frame.prototype
* @type {Leap.Hand}
*/
this.hands = [];
this.handsMap = {};
/**
* The list of Pointable objects (fingers and tools) detected in this frame,
* given in arbitrary order. The list can be empty if no fingers or tools are
* detected.
*
* @member pointables[]
* @memberof Leap.Frame.prototype
* @type {Leap.Pointable}
*/
this.pointables = [];
/**
* The list of Tool objects detected in this frame, given in arbitrary order.
* The list can be empty if no tools are detected.
*
* @member tools[]
* @memberof Leap.Frame.prototype
* @type {Leap.Pointable}
*/
this.tools = [];
/**
* The list of Finger objects detected in this frame, given in arbitrary order.
* The list can be empty if no fingers are detected.
* @member fingers[]
* @memberof Leap.Frame.prototype
* @type {Leap.Pointable}
*/
this.fingers = [];
/**
* The InteractionBox associated with the current frame.
*
* @member interactionBox
* @memberof Leap.Frame.prototype
* @type {Leap.InteractionBox}
*/
if (data.interactionBox) {
this.interactionBox = new InteractionBox(data.interactionBox);
}
this.gestures = [];
this.pointablesMap = {};
this._translation = data.t;
this._rotation = _.flatten(data.r);
this._scaleFactor = data.s;
this.data = data;
this.type = 'frame'; // used by event emitting
this.currentFrameRate = data.currentFrameRate;
var handMap = {};
for (var handIdx = 0, handCount = data.hands.length; handIdx != handCount; handIdx++) {
var hand = new Hand(data.hands[handIdx]);
hand.frame = this;
this.hands.push(hand);
this.handsMap[hand.id] = hand;
handMap[hand.id] = handIdx;
}
for (var pointableIdx = 0, pointableCount = data.pointables.length; pointableIdx != pointableCount; pointableIdx++) {
var pointable = new Pointable(data.pointables[pointableIdx]);
pointable.frame = this;
this.pointables.push(pointable);
this.pointablesMap[pointable.id] = pointable;
(pointable.tool ? this.tools : this.fingers).push(pointable);
if (pointable.handId !== undefined && handMap.hasOwnProperty(pointable.handId)) {
var hand = this.hands[handMap[pointable.handId]];
hand.pointables.push(pointable);
(pointable.tool ? hand.tools : hand.fingers).push(pointable);
}
}
if (data.gestures) {
/**
* The list of Gesture objects detected in this frame, given in arbitrary order.
* The list can be empty if no gestures are detected.
*
* Circle and swipe gestures are updated every frame. Tap gestures
* only appear in the list for a single frame.
* @member gestures[]
* @memberof Leap.Frame.prototype
* @type {Leap.Gesture}
*/
for (var gestureIdx = 0, gestureCount = data.gestures.length; gestureIdx != gestureCount; gestureIdx++) {
this.gestures.push(createGesture(data.gestures[gestureIdx]));
}
}
}
/**
* The tool with the specified ID in this frame.
*
* Use the Frame tool() function to retrieve a tool from
* this frame using an ID value obtained from a previous frame.
* This function always returns a Pointable object, but if no tool
* with the specified ID is present, an invalid Pointable object is returned.
*
* Note that ID values persist across frames, but only until tracking of a
* particular object is lost. If tracking of a tool is lost and subsequently
* regained, the new Pointable object representing that tool may have a
* different ID than that representing the tool in an earlier frame.
*
* @method tool
* @memberof Leap.Frame.prototype
* @param {String} id The ID value of a Tool object from a previous frame.
* @returns {Leap.Pointable} The tool with the
* matching ID if one exists in this frame; otherwise, an invalid Pointable object
* is returned.
*/
Frame.prototype.tool = function(id) {
var pointable = this.pointable(id);
return pointable.tool ? pointable : Pointable.Invalid;
}
/**
* The Pointable object with the specified ID in this frame.
*
* Use the Frame pointable() function to retrieve the Pointable object from
* this frame using an ID value obtained from a previous frame.
* This function always returns a Pointable object, but if no finger or tool
* with the specified ID is present, an invalid Pointable object is returned.
*
* Note that ID values persist across frames, but only until tracking of a
* particular object is lost. If tracking of a finger or tool is lost and subsequently
* regained, the new Pointable object representing that finger or tool may have
* a different ID than that representing the finger or tool in an earlier frame.
*
* @method pointable
* @memberof Leap.Frame.prototype
* @param {String} id The ID value of a Pointable object from a previous frame.
* @returns {Leap.Pointable} The Pointable object with
* the matching ID if one exists in this frame;
* otherwise, an invalid Pointable object is returned.
*/
Frame.prototype.pointable = function(id) {
return this.pointablesMap[id] || Pointable.Invalid;
}
/**
* The finger with the specified ID in this frame.
*
* Use the Frame finger() function to retrieve the finger from
* this frame using an ID value obtained from a previous frame.
* This function always returns a Finger object, but if no finger
* with the specified ID is present, an invalid Pointable object is returned.
*
* Note that ID values persist across frames, but only until tracking of a
* particular object is lost. If tracking of a finger is lost and subsequently
* regained, the new Pointable object representing that physical finger may have
* a different ID than that representing the finger in an earlier frame.
*
* @method finger
* @memberof Leap.Frame.prototype
* @param {String} id The ID value of a finger from a previous frame.
* @returns {Leap.Pointable} The finger with the
* matching ID if one exists in this frame; otherwise, an invalid Pointable
* object is returned.
*/
Frame.prototype.finger = function(id) {
var pointable = this.pointable(id);
return !pointable.tool ? pointable : Pointable.Invalid;
}
/**
* The Hand object with the specified ID in this frame.
*
* Use the Frame hand() function to retrieve the Hand object from
* this frame using an ID value obtained from a previous frame.
* This function always returns a Hand object, but if no hand
* with the specified ID is present, an invalid Hand object is returned.
*
* Note that ID values persist across frames, but only until tracking of a
* particular object is lost. If tracking of a hand is lost and subsequently
* regained, the new Hand object representing that physical hand may have
* a different ID than that representing the physical hand in an earlier frame.
*
* @method hand
* @memberof Leap.Frame.prototype
* @param {String} id The ID value of a Hand object from a previous frame.
* @returns {Leap.Hand} The Hand object with the matching
* ID if one exists in this frame; otherwise, an invalid Hand object is returned.
*/
Frame.prototype.hand = function(id) {
return this.handsMap[id] || Hand.Invalid;
}
/**
* The angle of rotation around the rotation axis derived from the overall
* rotational motion between the current frame and the specified frame.
*
* The returned angle is expressed in radians measured clockwise around
* the rotation axis (using the right-hand rule) between the start and end frames.
* The value is always between 0 and pi radians (0 and 180 degrees).
*
* The Leap derives frame rotation from the relative change in position and
* orientation of all objects detected in the field of view.
*
* If either this frame or sinceFrame is an invalid Frame object, then the
* angle of rotation is zero.
*
* @method rotationAngle
* @memberof Leap.Frame.prototype
* @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
* @param {number[]} [axis] The axis to measure rotation around.
* @returns {number} A positive value containing the heuristically determined
* rotational change between the current frame and that specified in the sinceFrame parameter.
*/
Frame.prototype.rotationAngle = function(sinceFrame, axis) {
if (!this.valid || !sinceFrame.valid) return 0.0;
var rot = this.rotationMatrix(sinceFrame);
var cs = (rot[0] + rot[4] + rot[8] - 1.0)*0.5
var angle = Math.acos(cs);
angle = isNaN(angle) ? 0.0 : angle;
if (axis !== undefined) {
var rotAxis = this.rotationAxis(sinceFrame);
angle *= vec3.dot(rotAxis, vec3.normalize(vec3.create(), axis));
}
return angle;
}
/**
* The axis of rotation derived from the overall rotational motion between
* the current frame and the specified frame.
*
* The returned direction vector is normalized.
*
* The Leap derives frame rotation from the relative change in position and
* orientation of all objects detected in the field of view.
*
* If either this frame or sinceFrame is an invalid Frame object, or if no
* rotation is detected between the two frames, a zero vector is returned.
*
* @method rotationAxis
* @memberof Leap.Frame.prototype
* @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
* @returns {number[]} A normalized direction vector representing the axis of the heuristically determined
* rotational change between the current frame and that specified in the sinceFrame parameter.
*/
Frame.prototype.rotationAxis = function(sinceFrame) {
if (!this.valid || !sinceFrame.valid) return vec3.create();
return vec3.normalize(vec3.create(), [
this._rotation[7] - sinceFrame._rotation[5],
this._rotation[2] - sinceFrame._rotation[6],
this._rotation[3] - sinceFrame._rotation[1]
]);
}
/**
* The transform matrix expressing the rotation derived from the overall
* rotational motion between the current frame and the specified frame.
*
* The Leap derives frame rotation from the relative change in position and
* orientation of all objects detected in the field of view.
*
* If either this frame or sinceFrame is an invalid Frame object, then
* this method returns an identity matrix.
*
* @method rotationMatrix
* @memberof Leap.Frame.prototype
* @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
* @returns {number[]} A transformation matrix containing the heuristically determined
* rotational change between the current frame and that specified in the sinceFrame parameter.
*/
Frame.prototype.rotationMatrix = function(sinceFrame) {
if (!this.valid || !sinceFrame.valid) return mat3.create();
var transpose = mat3.transpose(mat3.create(), this._rotation)
return mat3.multiply(mat3.create(), sinceFrame._rotation, transpose);
}
/**
* The scale factor derived from the overall motion between the current frame and the specified frame.
*
* The scale factor is always positive. A value of 1.0 indicates no scaling took place.
* Values between 0.0 and 1.0 indicate contraction and values greater than 1.0 indicate expansion.
*
* The Leap derives scaling from the relative inward or outward motion of all
* objects detected in the field of view (independent of translation and rotation).
*
* If either this frame or sinceFrame is an invalid Frame object, then this method returns 1.0.
*
* @method scaleFactor
* @memberof Leap.Frame.prototype
* @param {Leap.Frame} sinceFrame The starting frame for computing the relative scaling.
* @returns {number} A positive value representing the heuristically determined
* scaling change ratio between the current frame and that specified in the sinceFrame parameter.
*/
Frame.prototype.scaleFactor = function(sinceFrame) {
if (!this.valid || !sinceFrame.valid) return 1.0;
return Math.exp(this._scaleFactor - sinceFrame._scaleFactor);
}
/**
* The change of position derived from the overall linear motion between the
* current frame and the specified frame.
*
* The returned translation vector provides the magnitude and direction of the
* movement in millimeters.
*
* The Leap derives frame translation from the linear motion of all objects
* detected in the field of view.
*
* If either this frame or sinceFrame is an invalid Frame object, then this
* method returns a zero vector.
*
* @method translation
* @memberof Leap.Frame.prototype
* @param {Leap.Frame} sinceFrame The starting frame for computing the relative translation.
* @returns {number[]} A vector representing the heuristically determined change in
* position of all objects between the current frame and that specified in the sinceFrame parameter.
*/
Frame.prototype.translation = function(sinceFrame) {
if (!this.valid || !sinceFrame.valid) return vec3.create();
return vec3.subtract(vec3.create(), this._translation, sinceFrame._translation);
}
/**
* A string containing a brief, human readable description of the Frame object.
*
* @method toString
* @memberof Leap.Frame.prototype
* @returns {String} A brief description of this frame.
*/
Frame.prototype.toString = function() {
var str = "Frame [ id:"+this.id+" | timestamp:"+this.timestamp+" | Hand count:("+this.hands.length+") | Pointable count:("+this.pointables.length+")";
if (this.gestures) str += " | Gesture count:("+this.gestures.length+")";
str += " ]";
return str;
}
/**
* Returns a JSON-formatted string containing the hands, pointables and gestures
* in this frame.
*
* @method dump
* @memberof Leap.Frame.prototype
* @returns {String} A JSON-formatted string.
*/
Frame.prototype.dump = function() {
var out = '';
out += "Frame Info:<br/>";
out += this.toString();
out += "<br/><br/>Hands:<br/>"
for (var handIdx = 0, handCount = this.hands.length; handIdx != handCount; handIdx++) {
out += " "+ this.hands[handIdx].toString() + "<br/>";
}
out += "<br/><br/>Pointables:<br/>";
for (var pointableIdx = 0, pointableCount = this.pointables.length; pointableIdx != pointableCount; pointableIdx++) {
out += " "+ this.pointables[pointableIdx].toString() + "<br/>";
}
if (this.gestures) {
out += "<br/><br/>Gestures:<br/>";
for (var gestureIdx = 0, gestureCount = this.gestures.length; gestureIdx != gestureCount; gestureIdx++) {
out += " "+ this.gestures[gestureIdx].toString() + "<br/>";
}
}
out += "<br/><br/>Raw JSON:<br/>";
out += JSON.stringify(this.data);
return out;
}
/**
* An invalid Frame object.
*
* You can use this invalid Frame in comparisons testing
* whether a given Frame instance is valid or invalid. (You can also check the
* [Frame.valid]{@link Leap.Frame#valid} property.)
*
* @static
* @type {Leap.Frame}
* @name Invalid
* @memberof Leap.Frame
*/
Frame.Invalid = {
valid: false,
hands: [],
fingers: [],
tools: [],
gestures: [],
pointables: [],
pointable: function() { return Pointable.Invalid },
finger: function() { return Pointable.Invalid },
hand: function() { return Hand.Invalid },
toString: function() { return "invalid frame" },
dump: function() { return this.toString() },
rotationAngle: function() { return 0.0; },
rotationMatrix: function() { return mat3.create(); },
rotationAxis: function() { return vec3.create(); },
scaleFactor: function() { return 1.0; },
translation: function() { return vec3.create(); }
};
},{"./gesture":7,"./hand":8,"./interaction_box":10,"./pointable":12,"gl-matrix":21,"underscore":22}],7:[function(require,module,exports){
var glMatrix = require("gl-matrix")
, vec3 = glMatrix.vec3
, EventEmitter = require('events').EventEmitter
, _ = require('underscore');
/**
* Constructs a new Gesture object.
*
* An uninitialized Gesture object is considered invalid. Get valid instances
* of the Gesture class, which will be one of the Gesture subclasses, from a
* Frame object.
*
* @class Gesture
* @abstract
* @memberof Leap
* @classdesc
* The Gesture class represents a recognized movement by the user.
*
* The Leap watches the activity within its field of view for certain movement
* patterns typical of a user gesture or command. For example, a movement from side to
* side with the hand can indicate a swipe gesture, while a finger poking forward
* can indicate a screen tap gesture.
*
* When the Leap recognizes a gesture, it assigns an ID and adds a
* Gesture object to the frame gesture list. For continuous gestures, which
* occur over many frames, the Leap updates the gesture by adding
* a Gesture object having the same ID and updated properties in each
* subsequent frame.
*
* **Important:** Recognition for each type of gesture must be enabled;
* otherwise **no gestures are recognized or reported**.
*
* Subclasses of Gesture define the properties for the specific movement patterns
* recognized by the Leap.
*
* The Gesture subclasses for include:
*
* * CircleGesture -- A circular movement by a finger.
* * SwipeGesture -- A straight line movement by the hand with fingers extended.
* * ScreenTapGesture -- A forward tapping movement by a finger.
* * KeyTapGesture -- A downward tapping movement by a finger.
*
* Circle and swipe gestures are continuous and these objects can have a
* state of start, update, and stop.
*
* The screen tap gesture is a discrete gesture. The Leap only creates a single
* ScreenTapGesture object appears for each tap and it always has a stop state.
*
* Get valid Gesture instances from a Frame object. You can get a list of gestures
* from the Frame gestures array. You can also use the Frame gesture() method
* to find a gesture in the current frame using an ID value obtained in a
* previous frame.
*
* Gesture objects can be invalid. For example, when you get a gesture by ID
* using Frame.gesture(), and there is no gesture with that ID in the current
* frame, then gesture() returns an Invalid Gesture object (rather than a null
* value). Always check object validity in situations where a gesture might be
* invalid.
*/
var createGesture = exports.createGesture = function(data) {
var gesture;
switch (data.type) {
case 'circle':
gesture = new CircleGesture(data);
break;
case 'swipe':
gesture = new SwipeGesture(data);
break;
case 'screenTap':
gesture = new ScreenTapGesture(data);
break;
case 'keyTap':
gesture = new KeyTapGesture(data);
break;
default:
throw "unkown gesture type";
}
/**
* The gesture ID.
*
* All Gesture objects belonging to the same recognized movement share the
* same ID value. Use the ID value with the Frame::gesture() method to
* find updates related to this Gesture object in subsequent frames.
*
* @member id
* @memberof Leap.Gesture.prototype
* @type {number}
*/
gesture.id = data.id;
/**
* The list of hands associated with this Gesture, if any.
*
* If no hands are related to this gesture, the list is empty.
*
* @member handIds
* @memberof Leap.Gesture.prototype
* @type {Array}
*/
gesture.handIds = data.handIds;
/**
* The list of fingers and tools associated with this Gesture, if any.
*
* If no Pointable objects are related to this gesture, the list is empty.
*
* @member pointableIds
* @memberof Leap.Gesture.prototype
* @type {Array}
*/
gesture.pointableIds = data.pointableIds;
/**
* The elapsed duration of the recognized movement up to the
* frame containing this Gesture object, in microseconds.
*
* The duration reported for the first Gesture in the sequence (with the
* start state) will typically be a small positive number since
* the movement must progress far enough for the Leap to recognize it as
* an intentional gesture.
*
* @member duration
* @memberof Leap.Gesture.prototype
* @type {number}
*/
gesture.duration = data.duration;
/**
* The gesture ID.
*
* Recognized movements occur over time and have a beginning, a middle,
* and an end. The 'state()' attribute reports where in that sequence this
* Gesture object falls.
*
* Possible values for the state field are:
*
* * start
* * update
* * stop
*
* @member state
* @memberof Leap.Gesture.prototype
* @type {String}
*/
gesture.state = data.state;
/**
* The gesture type.
*
* Possible values for the type field are:
*
* * circle
* * swipe
* * screenTap
* * keyTap
*
* @member type
* @memberof Leap.Gesture.prototype
* @type {String}
*/
gesture.type = data.type;
return gesture;
}
/*
* Returns a builder object, which uses method chaining for gesture callback binding.
*/
var gestureListener = exports.gestureListener = function(controller, type) {
var handlers = {};
var gestureMap = {};
controller.on('gesture', function(gesture, frame) {
if (gesture.type == type) {
if (gesture.state == "start" || gesture.state == "stop") {
if (gestureMap[gesture.id] === undefined) {
var gestureTracker = new Gesture(gesture, frame);
gestureMap[gesture.id] = gestureTracker;
_.each(handlers, function(cb, name) {
gestureTracker.on(name, cb);
});
}
}
gestureMap[gesture.id].update(gesture, frame);
if (gesture.state == "stop") {
delete gestureMap[gesture.id];
}
}
});
var builder = {
start: function(cb) {
handlers['start'] = cb;
return builder;
},
stop: function(cb) {
handlers['stop'] = cb;
return builder;
},
complete: function(cb) {
handlers['stop'] = cb;
return builder;
},
update: function(cb) {
handlers['update'] = cb;
return builder;
}
}
return builder;
}
var Gesture = exports.Gesture = function(gesture, frame) {
this.gestures = [gesture];
this.frames = [frame];
}
Gesture.prototype.update = function(gesture, frame) {
this.lastGesture = gesture;
this.lastFrame = frame;
this.gestures.push(gesture);
this.frames.push(frame);
this.emit(gesture.state, this);
}
Gesture.prototype.translation = function() {
return vec3.subtract(vec3.create(), this.lastGesture.startPosition, this.lastGesture.position);
}
_.extend(Gesture.prototype, EventEmitter.prototype);
/**
* Constructs a new CircleGesture object.
*
* An uninitialized CircleGesture object is considered invalid. Get valid instances
* of the CircleGesture class from a Frame object.
*
* @class CircleGesture
* @memberof Leap
* @augments Leap.Gesture
* @classdesc
* The CircleGesture classes represents a circular finger movement.
*
* A circle movement is recognized when the tip of a finger draws a circle
* within the Leap field of view.
*
* 
*
* Circle gestures are continuous. The CircleGesture objects for the gesture have
* three possible states:
*
* * start -- The circle gesture has just started. The movement has
* progressed far enough for the recognizer to classify it as a circle.
* * update -- The circle gesture is continuing.
* * stop -- The circle gesture is finished.
*/
var CircleGesture = function(data) {
/**
* The center point of the circle within the Leap frame of reference.
*
* @member center
* @memberof Leap.CircleGesture.prototype
* @type {number[]}
*/
this.center = data.center;
/**
* The normal vector for the circle being traced.
*
* If you draw the circle clockwise, the normal vector points in the same
* general direction as the pointable object drawing the circle. If you draw
* the circle counterclockwise, the normal points back toward the
* pointable. If the angle between the normal and the pointable object
* drawing the circle is less than 90 degrees, then the circle is clockwise.
*
* ```javascript
* var clockwiseness;
* if (circle.pointable.direction.angleTo(circle.normal) <= PI/4) {
* clockwiseness = "clockwise";
* }
* else
* {
* clockwiseness = "counterclockwise";
* }
* ```
*
* @member normal
* @memberof Leap.CircleGesture.prototype
* @type {number[]}
*/
this.normal = data.normal;
/**
* The number of times the finger tip has traversed the circle.
*
* Progress is reported as a positive number of the number. For example,
* a progress value of .5 indicates that the finger has gone halfway
* around, while a value of 3 indicates that the finger has gone around
* the the circle three times.
*
* Progress starts where the circle gesture began. Since the circle
* must be partially formed before the Leap can recognize it, progress
* will be greater than zero when a circle gesture first appears in the
* frame.
*
* @member progress
* @memberof Leap.CircleGesture.prototype
* @type {number}
*/
this.progress = data.progress;
/**
* The radius of the circle in mm.
*
* @member radius
* @memberof Leap.CircleGesture.prototype
* @type {number}
*/
this.radius = data.radius;
}
CircleGesture.prototype.toString = function() {
return "CircleGesture ["+JSON.stringify(this)+"]";
}
/**
* Constructs a new SwipeGesture object.
*
* An uninitialized SwipeGesture object is considered invalid. Get valid instances
* of the SwipeGesture class from a Frame object.
*
* @class SwipeGesture
* @memberof Leap
* @augments Leap.Gesture
* @classdesc
* The SwipeGesture class represents a swiping motion of a finger or tool.
*
* 
*
* Swipe gestures are continuous.
*/
var SwipeGesture = function(data) {
/**
* The starting position within the Leap frame of
* reference, in mm.