├── .gitattributes
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTORS.txt
├── Gruntfile.js
├── LICENSE
├── Makefile
├── README.md
├── bower.json
├── examples
├── css-visualizer.html
├── dialog.html
├── dumper.html
├── iframe.html
├── images
│ └── dot-product.png
├── lib
│ ├── leap-plugins-0.1.6.js
│ └── three.js
├── loop.html
├── math.html
├── node.js
├── optimizeHMD.html
├── optimizeHMDScreentop.html
├── plugins.html
├── roll-pitch-yaw.html
├── threejs-bones-arrows.html
└── threejs-bones.html
├── images
├── README.md
├── logo-128.png
└── logo-large.png
├── integration_test
├── protocol_versions.js
└── reconnection.js
├── leap-1.1.1.js
├── leap-1.1.1.min.js
├── lib
├── bone.js
├── circular_buffer.js
├── connection
│ ├── base.js
│ ├── browser.js
│ └── node.js
├── controller.js
├── dialog.js
├── finger.js
├── frame.js
├── hand.js
├── index.js
├── interaction_box.js
├── pipeline.js
├── pointable.js
├── protocol.js
├── ui.js
├── ui
│ ├── cursor.js
│ └── region.js
└── version.js
├── package-lock.json
├── package.json
├── stress
└── punisher.js
├── template
└── entry.js
└── test
├── assertUtil.js
├── bone.js
├── circular_buffer.js
├── connection.js
├── controller.js
├── dialog.js
├── finger.js
├── frame.js
├── hand.js
├── helpers
├── browser.html
├── common.js
└── node.js
├── interaction_box.js
├── lib
├── protocol.coffee
└── protocol.js
├── pointable.js
└── protocol.html
/.gitattributes:
--------------------------------------------------------------------------------
1 | # lets treat only leap.min.js as a binary files, because diffing it is hard
2 |
3 | leap.min.js binary
4 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: leapjs - Build Library and Run Unit Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | name: Build Library and Run Unit Tests
8 | runs-on: ubuntu-latest
9 | steps:
10 |
11 | # Checkout
12 | - name: Checkout repository
13 | uses: actions/checkout@v2
14 |
15 | # Install Node
16 | - name: Use Node.js
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: '12.x'
20 |
21 | # Install Dependencies
22 | - name: Install NPM Dependencies
23 | run: npm ci
24 |
25 | # Build and Run Tests
26 | - name: Build Library and Run Tests
27 | run: ./node_modules/.bin/grunt test
28 |
29 | # Commit All Build Artifacts
30 | - name: Commit Build Artifacts
31 | run: |
32 | git config --local user.email "action@github.com"
33 | git config --local user.name "GitHub Action"
34 | set +e
35 | git add *
36 | git diff-index --quiet HEAD || git commit -m "Build Leap.js"
37 |
38 | # Push Build Artifacts
39 | - name: Push Changes to branch
40 | id: push-build
41 | uses: ad-m/github-push-action@master
42 | continue-on-error: true
43 | with:
44 | github_token: ${{ secrets.GITHUB_TOKEN }}
45 | branch: ${{ github.ref }}
46 |
47 | # Reenable Errors
48 | - name: Clean Up
49 | if: steps.push-build.outcome == 'success' && steps.push-build.conclusion == 'success'
50 | run: set -e
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .*.swp
3 | *.bak
4 | node_modules
5 | *.idea
6 | docs
7 | /bin/
8 |
9 | /lib/dtrace/
10 |
11 | /share/
12 |
13 | /ChangeLog
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "12"
4 | before_install: npm install -g grunt-cli
5 | script: grunt test
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.0.0
4 |
5 | - Upgraded Node to v12
6 | - Upgraded and removed deprecated and vulnerable depends/packages
7 | - Fixed compatiility with v3 of wss (Websockets) package
8 | - Removed phantomjs and related mocha components and migrated to mocha-chrome
9 | - Added ultraleap to bower keywords
10 |
11 |
12 | ## 0.6.4
13 | - Expose EventEmitter and _ on Leap
14 | - Fixes #190 Cannot pass in Leap.loop options without callback
15 | - Fix #194: Add frameEnd event dumper example
16 | - loopWhileDisconnected now defaults to true
17 | - FrameEnd Looping now begins even before (or without) controller connection
18 |
19 |
20 | ## 0.6.3
21 | - Add Optimize HMD flag for Controller
22 |
23 |
24 | ## 0.6.2
25 | - Add support for Secure Websocket connection over https:// pages for Leap Software >= 2.1.0
26 | - Add events fore beforeFrameCreated and afterFrameCreated (allowing low-level frame access)
27 |
28 | ## 0.6.1
29 | - Add arm bone for Leap Software >= 2.0.3
30 | - Fix issue where initial frame can be invalid
31 | - Allow Leap.loop to be called with no arguments
32 |
33 |
34 | ## 0.6.0
35 | - Bones API: finger.metacarpal, proximal, intermediate, distal, each with .prevJoin and .nextJoint
36 | - Fix issue where hand options could not be used with loop + loop options
37 | - console.warn plugin duplication, rather than throwing an error.
38 | - Allow prototypical extension of Fingers in plugins
39 |
40 |
41 | ## 0.6.0-beta2
42 | - Added convenience `hand` event for controllers. Add `hand` and `frame` as callback options for Leap.loop.
43 | - Merge in LeapJS 0.5.0 - device events (see below)
44 | - Fingers on hands will now always be ordered correctly.
45 | - Upgrade gl-matrix to 2.2.1
46 |
47 |
48 | ## 0.6.0-beta1
49 | - Hand:
50 | - type ('left' or 'right'),
51 | - grabStrength (number 0-1) - 1 being fully closed
52 | - pinchStrength (number 0-1) - 1 being fully closed, between the thumb and any finger
53 | - confidence (number 0-1) (a measure of hand data accuracy, 1 being good)
54 | - Finger:
55 | - mcp, pip, dip, and tip positions (all array vec3) - joint positions of the finger (see source/docs4)
56 | - extended (boolean) - True for a straight finger, false if the finger is curled
57 | - Moved to own class
58 | - Add convenience method controller.connected()
59 |
60 | ## 0.5.0
61 | - [feature] Support protocol v5: better device info when connected to clients with Leap Service version 1.2.0 or above.
62 | - [feature] Added streamingStarted, streamingStopped, deviceStreaming, and deviceStopped events
63 | - [feature] Device events now include device info with the following fields:
64 | - attached [boolean]
65 | - streaming [boolean]
66 | - id [string]
67 | - type [string], on of: "peripheral", "keyboard", "laptop", "unknown", or "unrecognized".
68 | - [feature] Add convenience methods `controller.connected()` and `controller.streaming()`
69 | - [bugfix] Focus state is now cleared after disconnection
70 | - [behavior change] Leap.loop no longer uses all plugins by default.
71 | - [feature] serviceVersion will now be available when connected to Leap Service v1.3.0 or above
72 | - [bugfix] Fix issue where any call to disconnect would automatically reconnect
73 | - [bugfix] Send focus state more quickly when connecting
74 | - [bugfix] Fix issue where focus changes while disconnected would cause focus not to be updated upon reconnection
75 |
76 | ## 0.4.3
77 | - Added a new controller option `loopWhileDisconnected` which defaults to true (legacy behavior) and can be set
78 | to false. This is an optimization which allows the loop to be turned off when the leap is not connected.
79 | -- When set to false, Animation Frames are no longer be sent after disconnect
80 | -- When set to false, Animation Frames are no longer sent if new device frame data is not available
81 | - suppressAnimationLoop, when set to false in node, will cause node to try and use the animation loop, and fail.
82 | - Fixed #159 where setBackground would not apply when reconnecting a controller.
83 |
84 | ## 0.4.2 - 2014-03-12
85 | - Allow controller.use to be called idempotently
86 | - Fix issue where requestAnimationFrame would not be used in Chrome
87 | - Exclude node code from browser js
88 | - The plugin pipeline now runs on animationFrames rather than deviceFrames when in the browser.
89 |
90 | ##0.4.1 - 2014-02-11
91 | - Allow controller.use to accept a factory directly (node support)
92 |
93 | ##0.4.0 - 2014-01-30
94 | - Started changelog
95 | - Added plugins
96 | - Allow Controller method chaining
97 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.txt:
--------------------------------------------------------------------------------
1 | Leap Motion Individual Contributor License Agreement, v1.0
2 |
3 | In order to clarify the intellectual property license granted with
4 | Contributions from any person or entity, Leap Motion, Inc. ("Leap
5 | Motion") must have a Contributor License Agreement ("CLA") on file that
6 | has been signed by each Contributor, indicating agreement to the license
7 | terms below. This license is for your protection as a Contributor as
8 | well as the protection of Leap Motion; it does not change your rights to
9 | use your own Contributions for any other purpose.
10 |
11 | If you wish to submit a pull request, please read the terms of this agreement
12 | and indicate your acceptance of it by adding your name, email, and github account name
13 | to the bottom of this file. Pull requests submitted without accepting this agreement
14 | will not be reviewed.
15 |
16 | By adding your information to this file, you accept and agree to the following
17 | terms and conditions for Your present and future Contributions submitted to
18 | Leap Motion. Except for the license granted herein to Leap Motion and recipients of
19 | software distributed by Leap Motion, You reserve all right, title, and interest
20 | in and to Your Contributions.
21 |
22 | 1. Definitions.
23 |
24 | "You" (or "Your") shall mean the copyright owner or legal entity
25 | authorized by the copyright owner that is making this Agreement with
26 | Leap Motion.
27 |
28 | For legal entities, the entity making a Contribution and all other
29 | entities that control, are controlled by, or are under common control
30 | with that entity are considered to be a single Contributor.
31 |
32 | For the purposes of this definition, "control" means (i) the power,
33 | direct or indirect, to cause the direction or management of such entity,
34 | whether by contract or otherwise, or (ii) ownership of fifty percent
35 | (50%) or more of the outstanding shares, or (iii) beneficial ownership
36 | of such entity.
37 |
38 | “Contribution" shall mean any original work of authorship, including
39 | any modifications or additions to an existing work, that is
40 | intentionally submitted by You to Leap Motion for inclusion in, or
41 | documentation of, any of the products owned or managed by Leap Motion
42 | (the "Work"). For the purposes of this definition, "submitted" means any
43 | form of electronic, verbal, or written communication sent to Leap Motion
44 | or its representatives, including but not limited to communication on
45 | electronic mailing lists, source code control systems, and issue
46 | tracking systems that are managed by, or on behalf of, Leap Motion for
47 | the purpose of discussing and improving the Work, but excluding
48 | communication that is conspicuously marked or otherwise designated in
49 | writing by You as "Not a Contribution."
50 |
51 | 2. Grant of Copyright License.
52 |
53 | Subject to the terms and conditions of this Agreement, You hereby grant
54 | to Leap Motion and to recipients of software distributed by Leap Motion
55 | a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
56 | irrevocable copyright license to reproduce, prepare derivative works of,
57 | publicly display, publicly perform, sublicense, and distribute Your
58 | Contributions and such derivative works.
59 |
60 | 3. Grant of Patent License.
61 |
62 | Subject to the terms and conditions of this Agreement, You hereby grant
63 | to Leap Motion and to recipients of software distributed by Leap Motion
64 | a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
65 | irrevocable (except as stated in this section) patent license to make,
66 | have made, use, offer to sell, import, and otherwise transfer the Work,
67 | where such license applies only to those patent claims licensable by You
68 | that are necessarily infringed by Your Contribution(s) alone or by
69 | combination of Your Contribution(s) with the Work to which such
70 | Contribution(s) was submitted. If any entity institutes patent
71 | litigation against You or any other entity (including a cross-claim or
72 | counterclaim in a lawsuit) alleging that your Contribution, or the Work
73 | to which you have contributed, constitutes direct or contributory patent
74 | infringement, then any patent licenses granted to that entity under this
75 | Agreement for that Contribution or Work shall terminate as of the date
76 | such litigation is filed.
77 |
78 | 4. You represent that you are legally entitled to grant the above
79 | license.
80 |
81 | If your employer(s) has rights to intellectual property that you create
82 | that includes your Contributions, you represent that you have received
83 | permission to make Contributions on behalf of that employer, that your
84 | employer has waived such rights for your Contributions to Leap Motion,
85 | or that your employer has executed a separate Corporate CLA with Leap
86 | Motion.
87 |
88 | 5. You represent that each of Your Contributions is Your original
89 | creation (see section 7 for submissions on behalf of others).
90 |
91 | You represent that Your Contribution submissions include complete
92 | details of any third-party license or other restriction (including, but
93 | not limited to, related patents and trademarks) of which you are
94 | personally aware and which are associated with any part of Your
95 | Contributions.
96 |
97 | 6. You are not expected to provide support for Your Contributions,
98 | except to the extent You desire to provide support. You may provide
99 | support for free, for a fee, or not at all. Unless required by
100 | applicable law or agreed to in writing, You provide Your Contributions
101 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
102 | either express or implied, including, without limitation, any warranties
103 | or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS
104 | FOR A PARTICULAR PURPOSE.
105 |
106 | 7. Should You wish to submit work that is not Your original creation,
107 | You may submit it to Leap Motion separately from any Contribution,
108 | identifying the complete details of its source and of any license or
109 | other restriction (including, but not limited to, related patents,
110 | trademarks, and license agreements) of which you are personally aware,
111 | and conspicuously marking the work as "Submitted on behalf of a
112 | third-party: [[]named here]".
113 |
114 | 8. You agree to notify Leap Motion of any facts or circumstances of
115 | which you become aware that would make these representations inaccurate
116 | in any respect.
117 |
118 | ======================================================
119 |
120 | 1. Jared Deckard
121 | Github account: deckar01
122 | Email: jared.deckard@gmail.com
123 |
124 | 2. Stu Kabakoff
125 | Github account: sakabako
126 | Email: sakabako@gmail.com
127 |
128 | 3. Yoshihiro Iwanaga
129 | Github account: iwanaga
130 | Email: iwanaga.blackie@gmail.com
131 |
132 | 4. ARJUNKUMAR KRISHNAMOORTHY
133 | Github account: tk120404
134 | Email: Arjunkumartk@gmail.com
135 |
136 | 5. Rob Witoff
137 | Github account: witoff
138 | Email: leap@pspct.com
139 |
140 | 6. Richard Pearson
141 | Github account: catdevnull
142 | Email: github@catdevnull.co.uk
143 |
144 | 7. Ben Nortier
145 | Github account: bjnortier
146 | Email: bjnortier@gmail.com
147 |
148 | 8. Andrew Kennedy
149 | Github account: akenn
150 | Email: andrew@akenn.org
151 |
152 | 9. Victor Norgren
153 | Github account: logotype
154 | Email: victor@logotype.se
155 |
156 | 10. Carl Goldberg
157 | Github account: ceg1236
158 | Email: carlgoldberg1236@gmail.com
159 |
160 | 11. Ashley Wright
161 | Github account: AshleyWright
162 | Email: ashley.wright.77.aw@gmail.com
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt){
2 | var filename = "leap-<%= pkg.version %>";
3 | var banner = "/*! \
4 | \n * LeapJS v<%= pkg.version %> \
5 | \n * http://github.com/leapmotion/leapjs/ \
6 | \n * \
7 | \n * Copyright 2021 Ultraleap, Inc. and other contributors \
8 | \n * Released under the Apache-2.0 license \
9 | \n * http://github.com/leapmotion/leapjs/blob/master/LICENSE \
10 | \n */";
11 |
12 | grunt.initConfig({
13 | pkg: grunt.file.readJSON("package.json"),
14 | // This updates the version.js to match pkg.version
15 | 'string-replace': {
16 | build: {
17 | files: {
18 | 'lib/': 'lib/version.js',
19 | './': 'bower.json',
20 | 'examples/': 'examples/*.html',
21 | 'test/': 'test/helpers/browser.html'
22 | },
23 | options:{
24 | replacements: [
25 | // version.js
26 | {
27 | pattern: /(full:\s)'.*'/,
28 | replacement: "$1'<%= pkg.version %>'"
29 | },
30 | {
31 | pattern: /(major:\s)\d/,
32 | replacement: "$1<%= pkg.version.split('.')[0] %>"
33 | },
34 | {
35 | pattern: /(minor:\s)\d/,
36 | replacement: "$1<%= pkg.version.split('.')[1] %>"
37 | },
38 | {
39 | pattern: /(dot:\s)\d.*/,
40 | replacement: "$1<%= pkg.version.split('.')[2][0] %>"
41 | },
42 | // bower.json
43 | {
44 | pattern: /"version": ".*"/,
45 | replacement: '"version": "<%= pkg.version %>"'
46 | },
47 | // examples
48 | {
49 | pattern: /leap.*\.js/,
50 | replacement: filename + '.js'
51 | }
52 | ]
53 | }
54 | }
55 | },
56 | clean: {
57 | build: {
58 | src: ['./leap-*.js']
59 | }
60 | },
61 | browserify: {
62 | build: {
63 | options: {
64 | ignore: ['lib/connection/node.js']
65 | },
66 | src: 'template/entry.js',
67 | dest: filename + '.js'
68 | }
69 | },
70 | uglify: {
71 | build: {
72 | src: filename + '.js',
73 | dest: filename + '.min.js'
74 | }
75 | },
76 | usebanner: {
77 | build: {
78 | options: {
79 | banner: banner
80 | },
81 | src: [filename + '.js', filename + '.min.js']
82 | }
83 | },
84 | // run with `grunt watch` or `grunt test watch`
85 | watch: {
86 | options: {
87 | atBegin: true
88 | },
89 | files: 'lib/**/*',
90 | tasks: ['default'],
91 | test: {
92 | files: ['lib/*', 'test/*'],
93 | tasks: ['test']
94 | }
95 |
96 | },
97 | exec: {
98 | 'test-browser': '"./node_modules/.bin/mocha-headless-chrome" -r dot -f test/helpers/browser.html',
99 | // -i -g stands for inverse grep. Tests tagged browser-only will be excluded.
100 | 'test-node': '"./node_modules/.bin/mocha" lib/index.js test/helpers/node.js test/*.js -R dot -i -g browser-only',
101 | 'test-integration': 'node integration_test/reconnection.js && node integration_test/protocol_versions.js'
102 | }
103 | });
104 |
105 | require('load-grunt-tasks')(grunt);
106 |
107 | grunt.registerTask('default', [
108 | 'string-replace',
109 | 'clean',
110 | 'browserify',
111 | 'uglify',
112 | 'usebanner'
113 | ]);
114 |
115 |
116 | grunt.registerTask('test', [
117 | 'default',
118 | 'test-only'
119 | ]);
120 |
121 | grunt.registerTask('test-only', [
122 | 'exec:test-node',
123 | 'exec:test-browser',
124 | 'exec:test-integration'
125 | ]);
126 | };
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BROWSERIFY_ARGS=--ignore=./connection/node template/entry.js
2 |
3 | build: compile compress
4 |
5 | compile:
6 | ./node_modules/.bin/browserify ${BROWSERIFY_ARGS} -o leap.js
7 |
8 | compress:
9 | ./node_modules/.bin/uglifyjs ./leap.js -o leap.min.js
10 |
11 | docs:
12 | ./node_modules/jsdoc/jsdoc lib README.md -d docs
13 |
14 | test: build test-only
15 |
16 | test-only: test-node test-browser test-integration
17 |
18 | test-browser:
19 | "./node_modules/.bin/mocha-headless-chrome" -r dot -f test/helpers/browser.html
20 |
21 | test-node:
22 | ./node_modules/.bin/mocha lib/index.js test/helpers/node.js test/*.js -R dot
23 |
24 | test-integration:
25 | node integration_test/reconnection.js && node integration_test/protocol_versions.js
26 |
27 | stress: stress/punisher.js
28 | node stress/punisher.js
29 |
30 | watch: watch-test
31 |
32 | watch-build:
33 | ./node_modules/.bin/nodemon --watch lib --exec "make" build
34 |
35 | watch-test:
36 | ./node_modules/.bin/nodemon --watch lib --exec "make" test
37 |
38 | open-in-browsers: build
39 | open -a /Applications/Firefox.app test/helpers/browser.html
40 | open -a /Applications/Safari.app test/helpers/browser.html
41 | open -a /Applications/Google\ Chrome.app test/helpers/browser.html
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://developer.leapmotion.com/leapjs/)
3 |
4 | Welcome to the [Leap Motion](https://www.leapmotion.com) JavaScript framework.
5 |
6 | You can now use LeapJS with **Ultraleap Gemini V5** and **Hyperion V6**! Visit the [Ultraleap Tracking WebSocket](https://github.com/ultraleap/UltraleapTrackingWebSocket) repository to grab the code and build it from source.
7 |
8 | [](https://travis-ci.org/leapmotion/leapjs)
9 |
10 | ```javascript
11 | Leap.loop(function(frame){
12 | console.log(frame.hands.length);
13 | });
14 | ```
15 |
16 | Learn more in the [Getting Started Guide](https://developer.leapmotion.com/leapjs/getting-started), and the [API Reference](https://developer.leapmotion.com/documentation/javascript/api/Leap_Classes.html).
17 |
18 | ### Installation
19 |
20 | **Browser**: Download the latest `leap.js` [from our CDN](https://developer.leapmotion.com/leapjs/welcome).
21 |
22 | **Bower**: `bower install leapjs`
23 |
24 | **Node**: `npm install leapjs`
25 |
26 | ### Examples
27 |
28 | Visit [developer.leapmotion.com/gallery/category/javascript](https://developer.leapmotion.com/gallery/category/javascript) for the latest examples.
29 |
30 | Some more basic examples have also been included in the [examples/](https://github.com/leapmotion/leapjs/tree/master/examples) directory.
31 |
32 | ### Plugins
33 |
34 | [Plugins](http://developer.leapmotion.com/leapjs/plugins) are used to modularly extend Leap Webapps with external libraries.
35 | Here we use the `screenPosition` plugin to get the position of the hand as an on-screen cursor.
36 |
37 | ```javascript
38 | Leap.loop({
39 |
40 | hand: function(hand){
41 | console.log( hand.screenPosition() );
42 | }
43 |
44 | }).use('screenPosition');
45 | ```
46 |
47 |
48 | ### Misc
49 |
50 | LeapJS includes the vector math library [GL-Matrix](http://glmatrix.net/) for your use and convenience. For example, we can easily compute a dot product. See [the example](https://github.com/leapmotion/leapjs/blob/master/examples/math.html) and [the gl matrix docs](http://glmatrix.net/docs/2.2.0/) for more info.
51 |
52 | ```javascript
53 | var dot = Leap.vec3.dot(hand.direction, hand.indexFinger.direction);
54 | ```
55 |
56 | Also visit the wiki for [how to make plugins](https://github.com/leapmotion/leapjs/wiki/Plugins),
57 | [protocol guide](https://github.com/leapmotion/leapjs/wiki/Protocol), and [other stuff](https://github.com/leapmotion/leapjs/wiki).
58 |
59 |
60 |
61 | ### Contributing
62 |
63 | Add your name, email, and github account to the CONTRIBUTORS.txt list, thereby agreeing to the terms and conditions of the Contributor License Agreement.
64 |
65 | Open a Pull Request. If your information is not in the CONTRIBUTORS file, your pull request will not be reviewed.
66 |
67 | [](https://github.com/leapmotion/LeapJS)
68 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "leapjs",
3 | "version": "1.1.1",
4 | "homepage": "https://github.com/leapmotion/leapjs",
5 | "description": "JavaScript client for the Leap Motion Controller",
6 | "main": "leap-1.1.1.js",
7 | "keywords": [
8 | "leap",
9 | "leapmotion",
10 | "ultraleap"
11 | ],
12 | "ignore": [
13 | "**/.*",
14 | "node_modules",
15 | "bower_components",
16 | "test",
17 | "tests"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/examples/css-visualizer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | DOM Visualizer - Leap
4 |
5 |
113 |
279 |
280 |
281 |
282 |
Show Hands
283 |
hide Hands
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
--------------------------------------------------------------------------------
/examples/dialog.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Dialog - Leap
4 |
5 |
14 |
15 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/examples/dumper.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Dumper - Leap
4 |
5 |
87 |
88 |
89 | Disconnect
90 | Connect
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/examples/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | iFrame - Leap
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/examples/images/dot-product.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leapmotion/leapjs/f990d44fb51f7ac8868f951dd71db2e0d4a976a1/examples/images/dot-product.png
--------------------------------------------------------------------------------
/examples/loop.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Loop - Leap
5 |
6 |
7 |
8 |
29 |
30 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/examples/math.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Math - Leap
5 |
6 |
7 |
8 |
26 |
27 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | This page demonstrates the usage of basic vector Math with the Leap. In this example, we will compute the dot-product,
43 | of hand direction and index finger direction. This is a measure of how close two vectors are.
44 |
45 |
46 |
47 | Find out more in the GL-Matrix Documentation and
48 | Wikipedia .
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | hand.direction · hand.indexFinger.direction =
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/examples/node.js:
--------------------------------------------------------------------------------
1 | require('../template/entry');
2 |
3 | var controller = new Leap.Controller()
4 | controller.on("frame", function(frame) {
5 | console.log("Frame: " + frame.id + " @ " + frame.timestamp);
6 | });
7 |
8 | var frameCount = 0;
9 | controller.on("frame", function(frame) {
10 | frameCount++;
11 | });
12 |
13 | setInterval(function() {
14 | var time = frameCount/2;
15 | console.log("received " + frameCount + " frames @ " + time + "fps");
16 | frameCount = 0;
17 | }, 2000);
18 |
19 | controller.on('ready', function() {
20 | console.log("ready");
21 | });
22 | controller.on('connect', function() {
23 | console.log("connect");
24 | });
25 | controller.on('disconnect', function() {
26 | console.log("disconnect");
27 | });
28 | controller.on('focus', function() {
29 | console.log("focus");
30 | });
31 | controller.on('blur', function() {
32 | console.log("blur");
33 | });
34 | controller.on('deviceConnected', function() {
35 | console.log("deviceConnected");
36 | });
37 | controller.on('deviceDisconnected', function() {
38 | console.log("deviceDisconnected");
39 | });
40 |
41 | controller.connect();
42 | console.log("\nWaiting for device to connect...");
43 |
--------------------------------------------------------------------------------
/examples/optimizeHMD.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | HMD Optimization - Leap
4 |
5 |
8 |
9 |
10 | controller.setOptimizeHMD(true);
11 | controller.setOptimizeHMD(false);
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/optimizeHMDScreentop.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | HMD/Screentop Optimization - Leap
4 |
5 |
8 |
9 |
10 | controller.setOptimizeHMD(true);
11 | controller.setOptimizeHMD(false);
12 |
13 |
14 | controller.setOptimizeScreentop(true);
15 | controller.setOptimizeScreentop(false);
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/examples/plugins.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Plugins - Leap
4 |
5 |
42 |
43 |
44 | Wave your hand..
45 |
46 |
--------------------------------------------------------------------------------
/examples/roll-pitch-yaw.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Dumper - Leap
4 |
5 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/examples/threejs-bones-arrows.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Bone Hands - Leap
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Move Hand over Leap to see bones and bone basis.
13 |
14 |
15 | x, y, z are in order: r, g, b; also basis[0], basis[1], basis[2].
16 |
17 |
18 |
19 |
20 |
166 |
--------------------------------------------------------------------------------
/examples/threejs-bones.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Bone Hands - Leap
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Move Hand over Leap to see bones and bone basis.
13 |
14 |
15 | Even without the leap, you should see a rotating blue rectangle to know that your WebGL is working.
16 |
17 |
18 |
19 |
20 |
236 |
--------------------------------------------------------------------------------
/images/README.md:
--------------------------------------------------------------------------------
1 | You can download and use these logo images on your own projects.
2 |
3 | These images are made available under Creative Commons Attribution-ShareAlike 3.0 Unported License
4 |
5 | http://creativecommons.org/licenses/by-sa/3.0/
--------------------------------------------------------------------------------
/images/logo-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leapmotion/leapjs/f990d44fb51f7ac8868f951dd71db2e0d4a976a1/images/logo-128.png
--------------------------------------------------------------------------------
/images/logo-large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leapmotion/leapjs/f990d44fb51f7ac8868f951dd71db2e0d4a976a1/images/logo-large.png
--------------------------------------------------------------------------------
/integration_test/protocol_versions.js:
--------------------------------------------------------------------------------
1 | var WebSocketServer = require('ws').Server,
2 | Leap = require('../lib');
3 |
4 |
5 | setTimeout(function() {console.log('Failure, timing out.'); process.exit(1);}, 30000);
6 |
7 | downgradeProtocol = function(){
8 | var controller = new Leap.Controller({port: 9494})
9 | controller.connect();
10 |
11 | // Once a controller tried to connect, it will always be trying to reconnect. For now we just use two ports.
12 | var passed = false;
13 |
14 | var expected = ['/v6.json', '/v5.json', '/v4.json', '/v3.json', '/v2.json', '/v1.json'];
15 |
16 | var wss = new WebSocketServer({port: 9494})
17 | // https://github.com/websockets/ws/issues/1114
18 | // https://github.com/websockets/ws/pull/1099
19 | wss.on('connection', function(ws, req) {
20 | ws.upgradeReq = req
21 | if (passed) return;
22 | console.log("connected to socket with "+ws.upgradeReq.url)
23 | if (expected.length == 0) {
24 | if (!passed){
25 | passed = true;
26 | // no testing framework here, we manually advance to the next case
27 | console.log('PASSED downgradeProtocol');
28 | saveGoodProtocol();
29 | }
30 | } else if (ws.upgradeReq.url == expected[0]) {
31 | expected.shift();
32 | // for some reason, the response gets eaten without this.
33 | setTimeout(function(){ws.close(1001);}, 100)
34 | } else {
35 | console.log("FAILED downgradeProtocol, expected: "+JSON.stringify(expected[0])+" , got ws.upgradeReq.url:"+ws.upgradeReq.url);
36 | process.exit(1);
37 | }
38 | });
39 | }
40 |
41 | saveGoodProtocol = function(){
42 | var controller = new Leap.Controller({port: 9495});
43 | controller.connect();
44 |
45 | var wss = new WebSocketServer({port: 9495})
46 | var origUrl;
47 | wss.on('connection', function(ws, req) {
48 | ws.upgradeReq = req
49 | console.log("connected to socket with "+ws.upgradeReq.url)
50 |
51 | ws.send('{"version": 4}');
52 |
53 | if (origUrl){
54 | if (origUrl === ws.upgradeReq.url){
55 | console.log('PASSED saveGoodProtocol');
56 | wss.close();
57 | process.exit(0);
58 | }else{
59 | console.log('FAILED saveGoodProtocol');
60 | wss.close();
61 | process.exit(1);
62 | }
63 | }
64 | origUrl = ws.upgradeReq.url;
65 | ws.close();
66 | });
67 | }
68 |
69 |
70 | disconnectAfterConnect = function () {
71 | var controller = new Leap.Controller({port: 9496})
72 | controller.on('ready', function(){
73 | console.log('ready - disconnecting');
74 | controller.disconnect();
75 | setTimeout(function(){
76 | console.log('PASSED disconnectAfterConnect');
77 | downgradeProtocol()
78 | }, 2000);
79 | });
80 | controller.connect();
81 |
82 | var timesConnected = 0;
83 |
84 | var wss = new WebSocketServer({port: 9496})
85 |
86 |
87 | wss.on('connection', function (ws, req) {
88 | ws.upgradeReq = req
89 | timesConnected++;
90 | console.log("connected to socket with "+ws.upgradeReq.url)
91 |
92 | if (timesConnected > 1) {
93 | console.log('FAILED disconnectAfterConnect');
94 | wss.close();
95 | process.exit(1);
96 | }
97 |
98 | ws.send('{"version": 4}');
99 | });
100 | }
101 |
102 | disconnectAfterConnect();
103 |
--------------------------------------------------------------------------------
/integration_test/reconnection.js:
--------------------------------------------------------------------------------
1 | var WebSocketServer = require('ws').Server,
2 | exec = require('child_process').exec,
3 | Leap = require('../lib');
4 |
5 | var controller = new Leap.Controller({port: 9494});
6 | controller.connect();
7 |
8 | setTimeout(function() {process.exit(1);}, 30000);
9 |
10 | var testServer = function(cb) {
11 | var wss = new WebSocketServer({port: 9494})
12 | wss.on('connection', function(ws) {
13 | setTimeout(function() {
14 | wss.close();
15 | cb();
16 | }, 100)
17 |
18 | });
19 | }
20 |
21 | var counter = function() {
22 | console.log("re-connecting");
23 | counter.count++;
24 | if (counter.count >= 6) {
25 | console.log("test looks good");
26 | process.exit(0);
27 | } else {
28 | setTimeout(function() { testServer(counter) }, 500);
29 | }
30 | }
31 | counter.count = 0
32 |
33 | testServer(counter);
--------------------------------------------------------------------------------
/lib/bone.js:
--------------------------------------------------------------------------------
1 | var Pointable = require('./pointable')
2 | , glMatrix = require("gl-matrix")
3 | , vec3 = glMatrix.vec3
4 | , mat3 = glMatrix.mat3
5 | , mat4 = glMatrix.mat4;
6 |
7 |
8 | var Bone = module.exports = function(finger, data) {
9 | this.finger = finger;
10 |
11 | this._center = null, this._matrix = null;
12 |
13 | /**
14 | * An integer code for the name of this bone.
15 | *
16 | * * 0 -- metacarpal
17 | * * 1 -- proximal
18 | * * 2 -- medial
19 | * * 3 -- distal
20 | * * 4 -- arm
21 | *
22 | * @member type
23 | * @type {number}
24 | * @memberof Leap.Bone.prototype
25 | */
26 | this.type = data.type;
27 |
28 | /**
29 | * The position of the previous, or base joint of the bone closer to the wrist.
30 | * @type {vector3}
31 | */
32 | this.prevJoint = data.prevJoint;
33 |
34 | /**
35 | * The position of the next joint, or the end of the bone closer to the finger tip.
36 | * @type {vector3}
37 | */
38 | this.nextJoint = data.nextJoint;
39 |
40 | /**
41 | * The estimated width of the pointable in millimeters.
42 | *
43 | * The reported width is the average width of the visible portion of the
44 | * pointable from the hand to the tip. If the width isn't known,
45 | * then a value of 0 is returned.
46 | *
47 | * Bone objects representing fingers do not have a width property.
48 | *
49 | * @member width
50 | * @type {number}
51 | * @memberof Leap.Pointable.prototype
52 | */
53 | this.width = data.width;
54 |
55 | var displacement = new Array(3);
56 | vec3.sub(displacement, data.nextJoint, data.prevJoint);
57 |
58 | this.length = vec3.length(displacement);
59 |
60 |
61 | /**
62 | *
63 | * These fully-specify the orientation of the bone.
64 | * See examples/threejs-bones.html for more info
65 | * Three vec3s:
66 | * x (red): The rotation axis of the finger, pointing outwards. (In general, away from the thumb )
67 | * y (green): The "up" vector, orienting the top of the finger
68 | * z (blue): The roll axis of the bone.
69 | *
70 | * Most up vectors will be pointing the same direction, except for the thumb, which is more rightwards.
71 | *
72 | * The thumb has one fewer bones than the fingers, but there are the same number of joints & joint-bases provided
73 | * the first two appear in the same position, but only the second (proximal) rotates.
74 | *
75 | * Normalized.
76 | */
77 | this.basis = data.basis;
78 | };
79 |
80 | Bone.prototype.left = function(){
81 |
82 | if (this._left) return this._left;
83 |
84 | this._left = mat3.determinant(this.basis[0].concat(this.basis[1]).concat(this.basis[2])) < 0;
85 |
86 | return this._left;
87 |
88 | };
89 |
90 |
91 | /**
92 | * The Affine transformation matrix describing the orientation of the bone, in global Leap-space.
93 | * It contains a 3x3 rotation matrix (in the "top left"), and center coordinates in the fourth column.
94 | *
95 | * Unlike the basis, the right and left hands have the same coordinate system.
96 | *
97 | */
98 | Bone.prototype.matrix = function(){
99 |
100 | if (this._matrix) return this._matrix;
101 |
102 | var b = this.basis,
103 | t = this._matrix = mat4.create();
104 |
105 | // open transform mat4 from rotation mat3
106 | t[0] = b[0][0], t[1] = b[0][1], t[2] = b[0][2];
107 | t[4] = b[1][0], t[5] = b[1][1], t[6] = b[1][2];
108 | t[8] = b[2][0], t[9] = b[2][1], t[10] = b[2][2];
109 |
110 | t[3] = this.center()[0];
111 | t[7] = this.center()[1];
112 | t[11] = this.center()[2];
113 |
114 | if ( this.left() ) {
115 | // flip the basis to be right-handed
116 | t[0] *= -1;
117 | t[1] *= -1;
118 | t[2] *= -1;
119 | }
120 |
121 | return this._matrix;
122 | };
123 |
124 | /**
125 | * Helper method to linearly interpolate between the two ends of the bone.
126 | *
127 | * when t = 0, the position of prevJoint will be returned
128 | * when t = 1, the position of nextJoint will be returned
129 | */
130 | Bone.prototype.lerp = function(out, t){
131 |
132 | vec3.lerp(out, this.prevJoint, this.nextJoint, t);
133 |
134 | };
135 |
136 | /**
137 | *
138 | * The center position of the bone
139 | * Returns a vec3 array.
140 | *
141 | */
142 | Bone.prototype.center = function(){
143 |
144 | if (this._center) return this._center;
145 |
146 | var center = vec3.create();
147 | this.lerp(center, 0.5);
148 | this._center = center;
149 | return center;
150 |
151 | };
152 |
153 | // The negative of the z-basis
154 | Bone.prototype.direction = function(){
155 |
156 | return [
157 | this.basis[2][0] * -1,
158 | this.basis[2][1] * -1,
159 | this.basis[2][2] * -1
160 | ];
161 |
162 | };
163 |
--------------------------------------------------------------------------------
/lib/circular_buffer.js:
--------------------------------------------------------------------------------
1 | var CircularBuffer = module.exports = function(size) {
2 | this.pos = 0;
3 | this._buf = [];
4 | this.size = size;
5 | }
6 |
7 | CircularBuffer.prototype.get = function(i) {
8 | if (i == undefined) i = 0;
9 | if (i >= this.size) return undefined;
10 | if (i >= this._buf.length) return undefined;
11 | return this._buf[(this.pos - i - 1) % this.size];
12 | }
13 |
14 | CircularBuffer.prototype.push = function(o) {
15 | this._buf[this.pos % this.size] = o;
16 | return this.pos++;
17 | }
18 |
--------------------------------------------------------------------------------
/lib/connection/base.js:
--------------------------------------------------------------------------------
1 | var chooseProtocol = require('../protocol').chooseProtocol
2 | , EventEmitter = require('events').EventEmitter;
3 |
4 | var BaseConnection = module.exports = function(opts) {
5 | this.opts = Object.assign({
6 | host : '127.0.0.1',
7 | scheme: this.getScheme(),
8 | port: this.getPort(),
9 | background: false,
10 | optimizeHMD: false,
11 | requestProtocolVersion: BaseConnection.defaultProtocolVersion
12 | }, opts || {});
13 | this.host = this.opts.host;
14 | this.port = this.opts.port;
15 | this.scheme = this.opts.scheme;
16 | this.protocolVersionVerified = false;
17 | this.background = null;
18 | this.optimizeHMD = null;
19 | this.on('ready', function() {
20 | this.setBackground(this.opts.background);
21 | this.setOptimizeHMD(this.opts.optimizeHMD);
22 |
23 | if (this.opts.optimizeHMD){
24 | console.log("Optimized for head mounted display usage.");
25 | }else {
26 | console.log("Optimized for desktop usage.");
27 | }
28 |
29 | });
30 | };
31 |
32 | // The latest available:
33 | BaseConnection.defaultProtocolVersion = 6;
34 |
35 | BaseConnection.prototype.getUrl = function() {
36 | return this.scheme + "//" + this.host + ":" + this.port + "/v" + this.opts.requestProtocolVersion + ".json";
37 | }
38 |
39 |
40 | BaseConnection.prototype.getScheme = function(){
41 | return 'ws:'
42 | }
43 |
44 | BaseConnection.prototype.getPort = function(){
45 | return 6437
46 | }
47 |
48 |
49 | BaseConnection.prototype.setBackground = function(state) {
50 | this.opts.background = state;
51 | if (this.protocol && this.protocol.sendBackground && this.background !== this.opts.background) {
52 | this.background = this.opts.background;
53 | this.protocol.sendBackground(this, this.opts.background);
54 | }
55 | }
56 |
57 | BaseConnection.prototype.setOptimizeHMD = function(state) {
58 | this.opts.optimizeHMD = state;
59 | if (this.protocol && this.protocol.sendOptimizeHMD && this.optimizeHMD !== this.opts.optimizeHMD) {
60 | this.optimizeHMD = this.opts.optimizeHMD;
61 | this.protocol.sendOptimizeHMD(this, this.opts.optimizeHMD);
62 | }
63 | }
64 |
65 | BaseConnection.prototype.handleOpen = function() {
66 | if (!this.connected) {
67 | this.connected = true;
68 | this.emit('connect');
69 | }
70 | }
71 |
72 | BaseConnection.prototype.handleClose = function(code, reason) {
73 | if (!this.connected) return;
74 | this.disconnect();
75 |
76 | // 1001 - an active connection is closed
77 | // 1006 - cannot connect
78 | if (code === 1001 && this.opts.requestProtocolVersion > 1) {
79 | if (this.protocolVersionVerified) {
80 | this.protocolVersionVerified = false;
81 | }else{
82 | this.opts.requestProtocolVersion--;
83 | }
84 | }
85 | this.startReconnection();
86 | }
87 |
88 | BaseConnection.prototype.startReconnection = function() {
89 | var connection = this;
90 | if(!this.reconnectionTimer){
91 | (this.reconnectionTimer = setInterval(function() { connection.reconnect() }, 500));
92 | }
93 | }
94 |
95 | BaseConnection.prototype.stopReconnection = function() {
96 | this.reconnectionTimer = clearInterval(this.reconnectionTimer);
97 | }
98 |
99 | // By default, disconnect will prevent auto-reconnection.
100 | // Pass in true to allow the reconnection loop not be interrupted continue
101 | BaseConnection.prototype.disconnect = function(allowReconnect) {
102 | if (!allowReconnect) this.stopReconnection();
103 | if (!this.socket) return;
104 | this.socket.close();
105 | delete this.socket;
106 | delete this.protocol;
107 | delete this.background; // This is not persisted when reconnecting to the web socket server
108 | delete this.optimizeHMD;
109 | delete this.focusedState;
110 | if (this.connected) {
111 | this.connected = false;
112 | this.emit('disconnect');
113 | }
114 | return true;
115 | }
116 |
117 | BaseConnection.prototype.reconnect = function() {
118 | if (this.connected) {
119 | this.stopReconnection();
120 | } else {
121 | this.disconnect(true);
122 | this.connect();
123 | }
124 | }
125 |
126 | BaseConnection.prototype.handleData = function(data) {
127 | var message = JSON.parse(data);
128 |
129 | var messageEvent;
130 | if (this.protocol === undefined) {
131 | messageEvent = this.protocol = chooseProtocol(message);
132 | this.protocolVersionVerified = true;
133 | this.emit('ready');
134 | } else {
135 | messageEvent = this.protocol(message);
136 | }
137 | this.emit(messageEvent.type, messageEvent);
138 | }
139 |
140 | BaseConnection.prototype.connect = function() {
141 | if (this.socket) return;
142 | this.socket = this.setupSocket();
143 | return true;
144 | }
145 |
146 | BaseConnection.prototype.send = function(data) {
147 | this.socket.send(data);
148 | }
149 |
150 | BaseConnection.prototype.reportFocus = function(state) {
151 | if (!this.connected || this.focusedState === state) return;
152 | this.focusedState = state;
153 | this.emit(this.focusedState ? 'focus' : 'blur');
154 | if (this.protocol && this.protocol.sendFocused) {
155 | this.protocol.sendFocused(this, this.focusedState);
156 | }
157 | }
158 |
159 | Object.assign(BaseConnection.prototype, EventEmitter.prototype);
--------------------------------------------------------------------------------
/lib/connection/browser.js:
--------------------------------------------------------------------------------
1 | var BaseConnection = module.exports = require('./base');
2 |
3 |
4 | var BrowserConnection = module.exports = function(opts) {
5 | BaseConnection.call(this, opts);
6 | var connection = this;
7 | this.on('ready', function() { connection.startFocusLoop(); })
8 | this.on('disconnect', function() { connection.stopFocusLoop(); })
9 | }
10 |
11 | Object.assign(BrowserConnection.prototype, BaseConnection.prototype);
12 |
13 | BrowserConnection.__proto__ = BaseConnection;
14 |
15 | BrowserConnection.prototype.useSecure = function(){
16 | return location.protocol === 'https:'
17 | }
18 |
19 | BrowserConnection.prototype.getScheme = function(){
20 | return this.useSecure() ? 'wss:' : 'ws:'
21 | }
22 |
23 | BrowserConnection.prototype.getPort = function(){
24 | return this.useSecure() ? 6436 : 6437
25 | }
26 |
27 | BrowserConnection.prototype.setupSocket = function() {
28 | var connection = this;
29 | var socket = new WebSocket(this.getUrl());
30 | socket.onopen = function() { connection.handleOpen(); };
31 | socket.onclose = function(data) { connection.handleClose(data['code'], data['reason']); };
32 | socket.onmessage = function(message) { connection.handleData(message.data) };
33 | socket.onerror = function(error) {
34 |
35 | // attempt to degrade to ws: after one failed attempt for older Leap Service installations.
36 | if (connection.useSecure() && connection.scheme === 'wss:'){
37 | connection.scheme = 'ws:';
38 | connection.port = 6437;
39 | connection.disconnect();
40 | connection.connect();
41 | }
42 |
43 | };
44 | return socket;
45 | }
46 |
47 | BrowserConnection.prototype.startFocusLoop = function() {
48 | if (this.focusDetectorTimer) return;
49 | var connection = this;
50 | var propertyName = null;
51 | if (typeof document.hidden !== "undefined") {
52 | propertyName = "hidden";
53 | } else if (typeof document.mozHidden !== "undefined") {
54 | propertyName = "mozHidden";
55 | } else if (typeof document.msHidden !== "undefined") {
56 | propertyName = "msHidden";
57 | } else if (typeof document.webkitHidden !== "undefined") {
58 | propertyName = "webkitHidden";
59 | } else {
60 | propertyName = undefined;
61 | }
62 |
63 | if (connection.windowVisible === undefined) {
64 | connection.windowVisible = propertyName === undefined ? true : document[propertyName] === false;
65 | }
66 |
67 | var focusListener = window.addEventListener('focus', function(e) {
68 | connection.windowVisible = true;
69 | updateFocusState();
70 | });
71 |
72 | var blurListener = window.addEventListener('blur', function(e) {
73 | connection.windowVisible = false;
74 | updateFocusState();
75 | });
76 |
77 | this.on('disconnect', function() {
78 | window.removeEventListener('focus', focusListener);
79 | window.removeEventListener('blur', blurListener);
80 | });
81 |
82 | var updateFocusState = function() {
83 | var isVisible = propertyName === undefined ? true : document[propertyName] === false;
84 | connection.reportFocus(isVisible && connection.windowVisible);
85 | }
86 |
87 | // save 100ms when resuming focus
88 | updateFocusState();
89 |
90 | this.focusDetectorTimer = setInterval(updateFocusState, 100);
91 | }
92 |
93 | BrowserConnection.prototype.stopFocusLoop = function() {
94 | if (!this.focusDetectorTimer) return;
95 | clearTimeout(this.focusDetectorTimer);
96 | delete this.focusDetectorTimer;
97 | }
98 |
--------------------------------------------------------------------------------
/lib/connection/node.js:
--------------------------------------------------------------------------------
1 | var WebSocket = require('ws')
2 | , BaseConnection = require('./base');
3 |
4 | var NodeConnection = module.exports = function(opts) {
5 | BaseConnection.call(this, opts);
6 | var connection = this;
7 | this.on('ready', function() { connection.reportFocus(true); });
8 | }
9 |
10 | Object.assign(NodeConnection.prototype, BaseConnection.prototype);
11 |
12 | NodeConnection.__proto__ = BaseConnection;
13 |
14 | NodeConnection.prototype.setupSocket = function() {
15 | var connection = this;
16 | var socket = new WebSocket(this.getUrl());
17 | socket.on('open', function() { connection.handleOpen(); });
18 | socket.on('message', function(m) { connection.handleData(m); });
19 | socket.on('close', function(code, reason) { connection.handleClose(code, reason); });
20 | socket.on('error', function() { connection.startReconnection(); });
21 | return socket;
22 | }
23 |
--------------------------------------------------------------------------------
/lib/dialog.js:
--------------------------------------------------------------------------------
1 | var Dialog = module.exports = function(message, options){
2 | this.options = (options || {});
3 | this.message = message;
4 |
5 | this.createElement();
6 | };
7 |
8 | Dialog.prototype.createElement = function(){
9 | this.element = document.createElement('div');
10 | this.element.className = "leapjs-dialog";
11 | this.element.style.position = "fixed";
12 | this.element.style.top = '8px';
13 | this.element.style.left = 0;
14 | this.element.style.right = 0;
15 | this.element.style.textAlign = 'center';
16 | this.element.style.zIndex = 1000;
17 |
18 | var dialog = document.createElement('div');
19 | this.element.appendChild(dialog);
20 | dialog.style.className = "leapjs-dialog";
21 | dialog.style.display = "inline-block";
22 | dialog.style.margin = "auto";
23 | dialog.style.padding = "8px";
24 | dialog.style.color = "#222";
25 | dialog.style.background = "#eee";
26 | dialog.style.borderRadius = "4px";
27 | dialog.style.border = "1px solid #999";
28 | dialog.style.textAlign = "left";
29 | dialog.style.cursor = "pointer";
30 | dialog.style.whiteSpace = "nowrap";
31 | dialog.style.transition = "box-shadow 1s linear";
32 | dialog.innerHTML = this.message;
33 |
34 |
35 | if (this.options.onclick){
36 | dialog.addEventListener('click', this.options.onclick);
37 | }
38 |
39 | if (this.options.onmouseover){
40 | dialog.addEventListener('mouseover', this.options.onmouseover);
41 | }
42 |
43 | if (this.options.onmouseout){
44 | dialog.addEventListener('mouseout', this.options.onmouseout);
45 | }
46 |
47 | if (this.options.onmousemove){
48 | dialog.addEventListener('mousemove', this.options.onmousemove);
49 | }
50 | };
51 |
52 | Dialog.prototype.show = function(){
53 | document.body.appendChild(this.element);
54 | return this;
55 | };
56 |
57 | Dialog.prototype.hide = function(){
58 | document.body.removeChild(this.element);
59 | return this;
60 | };
61 |
62 |
63 |
64 |
65 | // Shows a DOM dialog box with links to developer.leapmotion.com to upgrade
66 | // This will work whether or not the Leap is plugged in,
67 | // As long as it is called after a call to .connect() and the 'ready' event has fired.
68 | Dialog.warnOutOfDate = function(params){
69 | params || (params = {});
70 |
71 | var url = "http://developer.leapmotion.com?";
72 |
73 | params.returnTo = window.location.href;
74 |
75 | for (var key in params){
76 | url += key + '=' + encodeURIComponent(params[key]) + '&';
77 | }
78 |
79 | var dialog,
80 | onclick = function(event){
81 |
82 | if (event.target.id != 'leapjs-decline-upgrade'){
83 |
84 | var popup = window.open(url,
85 | '_blank',
86 | 'height=800,width=1000,location=1,menubar=1,resizable=1,status=1,toolbar=1,scrollbars=1'
87 | );
88 |
89 | if (window.focus) {popup.focus()}
90 |
91 | }
92 |
93 | dialog.hide();
94 |
95 | return true;
96 | },
97 |
98 |
99 | message = "This site requires Leap Motion Tracking V2." +
100 | "Upgrade " +
101 | "Not Now ";
102 |
103 | dialog = new Dialog(message, {
104 | onclick: onclick,
105 | onmousemove: function(e){
106 | if (e.target == document.getElementById('leapjs-decline-upgrade')){
107 | document.getElementById('leapjs-decline-upgrade').style.color = '#000';
108 | document.getElementById('leapjs-decline-upgrade').style.boxShadow = '0px 0px 2px #5daa00';
109 |
110 | document.getElementById('leapjs-accept-upgrade').style.color = '#444';
111 | document.getElementById('leapjs-accept-upgrade').style.boxShadow = 'none';
112 | }else{
113 | document.getElementById('leapjs-accept-upgrade').style.color = '#000';
114 | document.getElementById('leapjs-accept-upgrade').style.boxShadow = '0px 0px 2px #5daa00';
115 |
116 | document.getElementById('leapjs-decline-upgrade').style.color = '#444';
117 | document.getElementById('leapjs-decline-upgrade').style.boxShadow = 'none';
118 | }
119 | },
120 | onmouseout: function(){
121 | document.getElementById('leapjs-decline-upgrade').style.color = '#444';
122 | document.getElementById('leapjs-decline-upgrade').style.boxShadow = 'none';
123 | document.getElementById('leapjs-accept-upgrade').style.color = '#444';
124 | document.getElementById('leapjs-accept-upgrade').style.boxShadow = 'none';
125 | }
126 | }
127 | );
128 |
129 | return dialog.show();
130 | };
131 |
132 |
133 | // Tracks whether we've warned for lack of bones API. This will be shown only for early private-beta members.
134 | Dialog.hasWarnedBones = false;
135 |
136 | Dialog.warnBones = function(){
137 | if (this.hasWarnedBones) return;
138 | this.hasWarnedBones = true;
139 |
140 | console.warn("Your Leap Service is out of date");
141 |
142 | if ( !(typeof(process) !== 'undefined' && process.versions && process.versions.node) ){
143 | this.warnOutOfDate({reason: 'bones'});
144 | }
145 |
146 | }
--------------------------------------------------------------------------------
/lib/finger.js:
--------------------------------------------------------------------------------
1 | var Pointable = require('./pointable')
2 | , Bone = require('./bone')
3 | , Dialog = require('./dialog');
4 |
5 | /**
6 | * Constructs a Finger object.
7 | *
8 | * An uninitialized finger is considered invalid.
9 | * Get valid Finger objects from a Frame or a Hand object.
10 | *
11 | * @class Finger
12 | * @memberof Leap
13 | * @classdesc
14 | * The Finger class reports the physical characteristics of a finger.
15 | *
16 | * Both fingers are classified as Pointable objects. Use the
17 | * Pointable.tool property to determine whether a Pointable object represents a
18 | * tool or finger. The Leap classifies a detected entity as a tool when it is
19 | * thinner, straighter, and longer than a typical finger.
20 | *
21 | * Note that Finger objects can be invalid, which means that they do not
22 | * contain valid tracking data and do not correspond to a physical entity.
23 | * Invalid Finger objects can be the result of asking for a Finger object
24 | * using an ID from an earlier frame when no Finger objects with that ID
25 | * exist in the current frame. A Finger object created from the Finger
26 | * constructor is also invalid. Test for validity with the Pointable.valid
27 | * property.
28 | */
29 | var Finger = module.exports = function(data) {
30 | Pointable.call(this, data); // use pointable as super-constructor
31 |
32 | /**
33 | * The position of the distal interphalangeal joint of the finger.
34 | * This joint is closest to the tip.
35 | *
36 | * The distal interphalangeal joint is located between the most extreme segment
37 | * of the finger (the distal phalanx) and the middle segment (the medial
38 | * phalanx).
39 | *
40 | * @member dipPosition
41 | * @type {number[]}
42 | * @memberof Leap.Finger.prototype
43 | */
44 | this.dipPosition = data.dipPosition;
45 |
46 | /**
47 | * The position of the proximal interphalangeal joint of the finger. This joint is the middle
48 | * joint of a finger.
49 | *
50 | * The proximal interphalangeal joint is located between the two finger segments
51 | * closest to the hand (the proximal and the medial phalanges). On a thumb,
52 | * which lacks an medial phalanx, this joint index identifies the knuckle joint
53 | * between the proximal phalanx and the metacarpal bone.
54 | *
55 | * @member pipPosition
56 | * @type {number[]}
57 | * @memberof Leap.Finger.prototype
58 | */
59 | this.pipPosition = data.pipPosition;
60 |
61 | /**
62 | * The position of the metacarpopophalangeal joint, or knuckle, of the finger.
63 | *
64 | * The metacarpopophalangeal joint is located at the base of a finger between
65 | * the metacarpal bone and the first phalanx. The common name for this joint is
66 | * the knuckle.
67 | *
68 | * On a thumb, which has one less phalanx than a finger, this joint index
69 | * identifies the thumb joint near the base of the hand, between the carpal
70 | * and metacarpal bones.
71 | *
72 | * @member mcpPosition
73 | * @type {number[]}
74 | * @memberof Leap.Finger.prototype
75 | */
76 | this.mcpPosition = data.mcpPosition;
77 |
78 | /**
79 | * The position of the Carpometacarpal joint
80 | *
81 | * This is at the distal end of the wrist, and has no common name.
82 | *
83 | */
84 | this.carpPosition = data.carpPosition;
85 |
86 | /**
87 | * Whether or not this finger is in an extended posture.
88 | *
89 | * A finger is considered extended if it is extended straight from the hand as if
90 | * pointing. A finger is not extended when it is bent down and curled towards the
91 | * palm.
92 | * @member extended
93 | * @type {Boolean}
94 | * @memberof Leap.Finger.prototype
95 | */
96 | this.extended = data.extended;
97 |
98 | /**
99 | * An integer code for the name of this finger.
100 | *
101 | * * 0 -- thumb
102 | * * 1 -- index finger
103 | * * 2 -- middle finger
104 | * * 3 -- ring finger
105 | * * 4 -- pinky
106 | *
107 | * @member type
108 | * @type {number}
109 | * @memberof Leap.Finger.prototype
110 | */
111 | this.type = data.type;
112 |
113 | this.finger = true;
114 |
115 | /**
116 | * The joint positions of this finger as an array in the order base to tip.
117 | *
118 | * @member positions
119 | * @type {array[]}
120 | * @memberof Leap.Finger.prototype
121 | */
122 | this.positions = [this.carpPosition, this.mcpPosition, this.pipPosition, this.dipPosition, this.tipPosition];
123 |
124 | if (data.bases){
125 | this.addBones(data);
126 | } else {
127 | Dialog.warnBones();
128 | }
129 |
130 | };
131 |
132 | Object.assign(Finger.prototype, Pointable.prototype);
133 |
134 |
135 | Finger.prototype.addBones = function(data){
136 | /**
137 | * Four bones per finger, from wrist outwards:
138 | * metacarpal, proximal, medial, and distal.
139 | *
140 | * See http://en.wikipedia.org/wiki/Interphalangeal_articulations_of_hand
141 | */
142 | this.metacarpal = new Bone(this, {
143 | type: 0,
144 | width: this.width,
145 | prevJoint: this.carpPosition,
146 | nextJoint: this.mcpPosition,
147 | basis: data.bases[0]
148 | });
149 |
150 | this.proximal = new Bone(this, {
151 | type: 1,
152 | width: this.width,
153 | prevJoint: this.mcpPosition,
154 | nextJoint: this.pipPosition,
155 | basis: data.bases[1]
156 | });
157 |
158 | this.medial = new Bone(this, {
159 | type: 2,
160 | width: this.width,
161 | prevJoint: this.pipPosition,
162 | nextJoint: this.dipPosition,
163 | basis: data.bases[2]
164 | });
165 |
166 | /**
167 | * Note that the `distal.nextJoint` position is slightly different from the `finger.tipPosition`.
168 | * The former is at the very end of the bone, where the latter is the center of a sphere positioned at
169 | * the tip of the finger. The btipPosition "bone tip position" is a few mm closer to the wrist than
170 | * the tipPosition.
171 | * @type {Bone}
172 | */
173 | this.distal = new Bone(this, {
174 | type: 3,
175 | width: this.width,
176 | prevJoint: this.dipPosition,
177 | nextJoint: data.btipPosition,
178 | basis: data.bases[3]
179 | });
180 |
181 | this.bones = [this.metacarpal, this.proximal, this.medial, this.distal];
182 | };
183 |
184 | Finger.prototype.toString = function() {
185 | return "Finger [ id:" + this.id + " " + this.length + "mmx | width:" + this.width + "mm | direction:" + this.direction + ' ]';
186 | };
187 |
188 | Finger.Invalid = { valid: false };
189 |
--------------------------------------------------------------------------------
/lib/frame.js:
--------------------------------------------------------------------------------
1 | var Hand = require("./hand")
2 | , Pointable = require("./pointable")
3 | , glMatrix = require("gl-matrix")
4 | , mat3 = glMatrix.mat3
5 | , vec3 = glMatrix.vec3
6 | , InteractionBox = require("./interaction_box")
7 | , Finger = require('./finger');
8 |
9 | /**
10 | * Constructs a Frame object.
11 | *
12 | * Frame instances created with this constructor are invalid.
13 | * Get valid Frame objects by calling the
14 | * [Controller.frame]{@link Leap.Controller#frame}() function.
15 | *
16 | * @class Frame
17 | * @memberof Leap
18 | * @classdesc
19 | * The Frame class represents a set of hand and finger tracking data detected
20 | * in a single frame.
21 | *
22 | * The Leap detects hands, fingers within the tracking area, reporting
23 | * their positions, orientations and motions in frames at the Leap frame rate.
24 | *
25 | * Access Frame objects using the [Controller.frame]{@link Leap.Controller#frame}() function.
26 | */
27 | var Frame = module.exports = function(data) {
28 | /**
29 | * Reports whether this Frame instance is valid.
30 | *
31 | * A valid Frame is one generated by the Controller object that contains
32 | * tracking data for all detected entities. An invalid Frame contains no
33 | * actual tracking data, but you can call its functions without risk of a
34 | * undefined object exception. The invalid Frame mechanism makes it more
35 | * convenient to track individual data across the frame history. For example,
36 | * you can invoke:
37 | *
38 | * ```javascript
39 | * var finger = controller.frame(n).finger(fingerID);
40 | * ```
41 | *
42 | * for an arbitrary Frame history value, "n", without first checking whether
43 | * frame(n) returned a null object. (You should still check that the
44 | * returned Finger instance is valid.)
45 | *
46 | * @member valid
47 | * @memberof Leap.Frame.prototype
48 | * @type {Boolean}
49 | */
50 | this.valid = true;
51 | /**
52 | * A unique ID for this Frame. Consecutive frames processed by the Leap
53 | * have consecutive increasing values.
54 | * @member id
55 | * @memberof Leap.Frame.prototype
56 | * @type {String}
57 | */
58 | this.id = data.id;
59 | /**
60 | * The frame capture time in microseconds elapsed since the Leap started.
61 | * @member timestamp
62 | * @memberof Leap.Frame.prototype
63 | * @type {number}
64 | */
65 | this.timestamp = data.timestamp;
66 | /**
67 | * The list of Hand objects detected in this frame, given in arbitrary order.
68 | * The list can be empty if no hands are detected.
69 | *
70 | * @member hands[]
71 | * @memberof Leap.Frame.prototype
72 | * @type {Leap.Hand}
73 | */
74 | this.hands = [];
75 | this.handsMap = {};
76 | /**
77 | * The list of Pointable objects (fingers) detected in this frame,
78 | * given in arbitrary order. The list can be empty if no fingers are
79 | * detected.
80 | *
81 | * @member pointables[]
82 | * @memberof Leap.Frame.prototype
83 | * @type {Leap.Pointable}
84 | */
85 | this.pointables = [];
86 | /**
87 | * The list of Finger objects detected in this frame, given in arbitrary order.
88 | * The list can be empty if no fingers are detected.
89 | * @member fingers[]
90 | * @memberof Leap.Frame.prototype
91 | * @type {Leap.Pointable}
92 | */
93 | this.fingers = [];
94 |
95 | /**
96 | * The InteractionBox associated with the current frame.
97 | *
98 | * @member interactionBox
99 | * @memberof Leap.Frame.prototype
100 | * @type {Leap.InteractionBox}
101 | */
102 | if (data.interactionBox) {
103 | this.interactionBox = new InteractionBox(data.interactionBox);
104 | }
105 | this.pointablesMap = {};
106 | this._translation = data.t;
107 | function flattenDeep(arr) {
108 | return Array.isArray(arr)
109 | ? arr.reduce(function (a, b) { return a.concat(flattenDeep(b)) }, [])
110 | : [arr];
111 | }
112 | this._rotation = flattenDeep(data.r);
113 | this._scaleFactor = data.s;
114 | this.data = data;
115 | this.type = 'frame'; // used by event emitting
116 | this.currentFrameRate = data.currentFrameRate;
117 |
118 | this.postprocessData(data);
119 | };
120 |
121 | Frame.prototype.postprocessData = function(data){
122 | if (!data) {
123 | data = this.data;
124 | }
125 |
126 | for (var handIdx = 0, handCount = data.hands.length; handIdx != handCount; handIdx++) {
127 | var hand = new Hand(data.hands[handIdx]);
128 | hand.frame = this;
129 | this.hands.push(hand);
130 | this.handsMap[hand.id] = hand;
131 | }
132 |
133 | var sortBy = function(key) {
134 | return function (a, b) { return (a[key] > b[key]) ? 1 : ((b[key] > a[key]) ? -1 : 0) };
135 | };
136 |
137 | data.pointables.sort(sortBy("id"));
138 |
139 | for (var pointableIdx = 0, pointableCount = data.pointables.length; pointableIdx != pointableCount; pointableIdx++) {
140 | var pointableData = data.pointables[pointableIdx];
141 | var pointable = pointableData.dipPosition ? new Finger(pointableData) : new Pointable(pointableData);
142 | pointable.frame = this;
143 | this.addPointable(pointable);
144 | }
145 | };
146 |
147 | /**
148 | * Adds data from a pointable element into the pointablesMap;
149 | * also adds the pointable to the frame.handsMap hand to which it belongs,
150 | * and to the hand's fingers map.
151 | *
152 | * @param pointable {Object} a Pointable
153 | */
154 | Frame.prototype.addPointable = function (pointable) {
155 | this.pointables.push(pointable);
156 | this.pointablesMap[pointable.id] = pointable;
157 | (this.fingers).push(pointable);
158 | if (pointable.handId !== undefined && this.handsMap.hasOwnProperty(pointable.handId)) {
159 | var hand = this.handsMap[pointable.handId];
160 | hand.pointables.push(pointable);
161 | (hand.fingers).push(pointable);
162 | switch (pointable.type){
163 | case 0:
164 | hand.thumb = pointable;
165 | break;
166 | case 1:
167 | hand.indexFinger = pointable;
168 | break;
169 | case 2:
170 | hand.middleFinger = pointable;
171 | break;
172 | case 3:
173 | hand.ringFinger = pointable;
174 | break;
175 | case 4:
176 | hand.pinky = pointable;
177 | break;
178 | }
179 | }
180 | };
181 |
182 | /**
183 | * The Pointable object with the specified ID in this frame.
184 | *
185 | * Use the Frame pointable() function to retrieve the Pointable object from
186 | * this frame using an ID value obtained from a previous frame.
187 | * This function always returns a Pointable object, but if no finger
188 | * with the specified ID is present, an invalid Pointable object is returned.
189 | *
190 | * Note that ID values persist across frames, but only until tracking of a
191 | * particular object is lost. If tracking of a finger is lost and subsequently
192 | * regained, the new Pointable object representing that finger may have
193 | * a different ID than that representing the finger in an earlier frame.
194 | *
195 | * @method pointable
196 | * @memberof Leap.Frame.prototype
197 | * @param {String} id The ID value of a Pointable object from a previous frame.
198 | * @returns {Leap.Pointable} The Pointable object with
199 | * the matching ID if one exists in this frame;
200 | * otherwise, an invalid Pointable object is returned.
201 | */
202 | Frame.prototype.pointable = function(id) {
203 | return this.pointablesMap[id] || Pointable.Invalid;
204 | };
205 |
206 | /**
207 | * The finger with the specified ID in this frame.
208 | *
209 | * Use the Frame finger() function to retrieve the finger from
210 | * this frame using an ID value obtained from a previous frame.
211 | * This function always returns a Finger object, but if no finger
212 | * with the specified ID is present, an invalid Pointable object is returned.
213 | *
214 | * Note that ID values persist across frames, but only until tracking of a
215 | * particular object is lost. If tracking of a finger is lost and subsequently
216 | * regained, the new Pointable object representing that physical finger may have
217 | * a different ID than that representing the finger in an earlier frame.
218 | *
219 | * @method finger
220 | * @memberof Leap.Frame.prototype
221 | * @param {String} id The ID value of a finger from a previous frame.
222 | * @returns {Leap.Pointable} The finger with the
223 | * matching ID if one exists in this frame; otherwise, an invalid Pointable
224 | * object is returned.
225 | */
226 | Frame.prototype.finger = function(id) {
227 | return this.pointable(id);
228 | };
229 |
230 | /**
231 | * The Hand object with the specified ID in this frame.
232 | *
233 | * Use the Frame hand() function to retrieve the Hand object from
234 | * this frame using an ID value obtained from a previous frame.
235 | * This function always returns a Hand object, but if no hand
236 | * with the specified ID is present, an invalid Hand object is returned.
237 | *
238 | * Note that ID values persist across frames, but only until tracking of a
239 | * particular object is lost. If tracking of a hand is lost and subsequently
240 | * regained, the new Hand object representing that physical hand may have
241 | * a different ID than that representing the physical hand in an earlier frame.
242 | *
243 | * @method hand
244 | * @memberof Leap.Frame.prototype
245 | * @param {String} id The ID value of a Hand object from a previous frame.
246 | * @returns {Leap.Hand} The Hand object with the matching
247 | * ID if one exists in this frame; otherwise, an invalid Hand object is returned.
248 | */
249 | Frame.prototype.hand = function(id) {
250 | return this.handsMap[id] || Hand.Invalid;
251 | };
252 |
253 | /**
254 | * The angle of rotation around the rotation axis derived from the overall
255 | * rotational motion between the current frame and the specified frame.
256 | *
257 | * The returned angle is expressed in radians measured clockwise around
258 | * the rotation axis (using the right-hand rule) between the start and end frames.
259 | * The value is always between 0 and pi radians (0 and 180 degrees).
260 | *
261 | * The Leap derives frame rotation from the relative change in position and
262 | * orientation of all objects detected in the field of view.
263 | *
264 | * If either this frame or sinceFrame is an invalid Frame object, then the
265 | * angle of rotation is zero.
266 | *
267 | * @method rotationAngle
268 | * @memberof Leap.Frame.prototype
269 | * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
270 | * @param {number[]} [axis] The axis to measure rotation around.
271 | * @returns {number} A positive value containing the heuristically determined
272 | * rotational change between the current frame and that specified in the sinceFrame parameter.
273 | */
274 | Frame.prototype.rotationAngle = function(sinceFrame, axis) {
275 | if (!this.valid || !sinceFrame.valid) return 0.0;
276 |
277 | var rot = this.rotationMatrix(sinceFrame);
278 | var cs = (rot[0] + rot[4] + rot[8] - 1.0)*0.5;
279 | var angle = Math.acos(cs);
280 | angle = isNaN(angle) ? 0.0 : angle;
281 |
282 | if (axis !== undefined) {
283 | var rotAxis = this.rotationAxis(sinceFrame);
284 | angle *= vec3.dot(rotAxis, vec3.normalize(vec3.create(), axis));
285 | }
286 |
287 | return angle;
288 | };
289 |
290 | /**
291 | * The axis of rotation derived from the overall rotational motion between
292 | * the current frame and the specified frame.
293 | *
294 | * The returned direction vector is normalized.
295 | *
296 | * The Leap derives frame rotation from the relative change in position and
297 | * orientation of all objects detected in the field of view.
298 | *
299 | * If either this frame or sinceFrame is an invalid Frame object, or if no
300 | * rotation is detected between the two frames, a zero vector is returned.
301 | *
302 | * @method rotationAxis
303 | * @memberof Leap.Frame.prototype
304 | * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
305 | * @returns {number[]} A normalized direction vector representing the axis of the heuristically determined
306 | * rotational change between the current frame and that specified in the sinceFrame parameter.
307 | */
308 | Frame.prototype.rotationAxis = function(sinceFrame) {
309 | if (!this.valid || !sinceFrame.valid) return vec3.create();
310 | return vec3.normalize(vec3.create(), [
311 | this._rotation[7] - sinceFrame._rotation[5],
312 | this._rotation[2] - sinceFrame._rotation[6],
313 | this._rotation[3] - sinceFrame._rotation[1]
314 | ]);
315 | }
316 |
317 | /**
318 | * The transform matrix expressing the rotation derived from the overall
319 | * rotational motion between the current frame and the specified frame.
320 | *
321 | * The Leap derives frame rotation from the relative change in position and
322 | * orientation of all objects detected in the field of view.
323 | *
324 | * If either this frame or sinceFrame is an invalid Frame object, then
325 | * this method returns an identity matrix.
326 | *
327 | * @method rotationMatrix
328 | * @memberof Leap.Frame.prototype
329 | * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
330 | * @returns {number[]} A transformation matrix containing the heuristically determined
331 | * rotational change between the current frame and that specified in the sinceFrame parameter.
332 | */
333 | Frame.prototype.rotationMatrix = function(sinceFrame) {
334 | if (!this.valid || !sinceFrame.valid) return mat3.create();
335 | var transpose = mat3.transpose(mat3.create(), this._rotation)
336 | return mat3.multiply(mat3.create(), sinceFrame._rotation, transpose);
337 | }
338 |
339 | /**
340 | * The scale factor derived from the overall motion between the current frame and the specified frame.
341 | *
342 | * The scale factor is always positive. A value of 1.0 indicates no scaling took place.
343 | * Values between 0.0 and 1.0 indicate contraction and values greater than 1.0 indicate expansion.
344 | *
345 | * The Leap derives scaling from the relative inward or outward motion of all
346 | * objects detected in the field of view (independent of translation and rotation).
347 | *
348 | * If either this frame or sinceFrame is an invalid Frame object, then this method returns 1.0.
349 | *
350 | * @method scaleFactor
351 | * @memberof Leap.Frame.prototype
352 | * @param {Leap.Frame} sinceFrame The starting frame for computing the relative scaling.
353 | * @returns {number} A positive value representing the heuristically determined
354 | * scaling change ratio between the current frame and that specified in the sinceFrame parameter.
355 | */
356 | Frame.prototype.scaleFactor = function(sinceFrame) {
357 | if (!this.valid || !sinceFrame.valid) return 1.0;
358 | return Math.exp(this._scaleFactor - sinceFrame._scaleFactor);
359 | }
360 |
361 | /**
362 | * The change of position derived from the overall linear motion between the
363 | * current frame and the specified frame.
364 | *
365 | * The returned translation vector provides the magnitude and direction of the
366 | * movement in millimeters.
367 | *
368 | * The Leap derives frame translation from the linear motion of all objects
369 | * detected in the field of view.
370 | *
371 | * If either this frame or sinceFrame is an invalid Frame object, then this
372 | * method returns a zero vector.
373 | *
374 | * @method translation
375 | * @memberof Leap.Frame.prototype
376 | * @param {Leap.Frame} sinceFrame The starting frame for computing the relative translation.
377 | * @returns {number[]} A vector representing the heuristically determined change in
378 | * position of all objects between the current frame and that specified in the sinceFrame parameter.
379 | */
380 | Frame.prototype.translation = function(sinceFrame) {
381 | if (!this.valid || !sinceFrame.valid) return vec3.create();
382 | return vec3.subtract(vec3.create(), this._translation, sinceFrame._translation);
383 | }
384 |
385 | /**
386 | * A string containing a brief, human readable description of the Frame object.
387 | *
388 | * @method toString
389 | * @memberof Leap.Frame.prototype
390 | * @returns {String} A brief description of this frame.
391 | */
392 | Frame.prototype.toString = function() {
393 | var str = "Frame [ id:"+this.id+" | timestamp:"+this.timestamp+" | Hand count:("+this.hands.length+") | Pointable count:("+this.pointables.length+")";
394 | str += " ]";
395 | return str;
396 | }
397 |
398 | /**
399 | * Returns a JSON-formatted string containing the hands, pointables
400 | * in this frame.
401 | *
402 | * @method dump
403 | * @memberof Leap.Frame.prototype
404 | * @returns {String} A JSON-formatted string.
405 | */
406 | Frame.prototype.dump = function() {
407 | var out = '';
408 | out += "Frame Info: ";
409 | out += this.toString();
410 | out += " Hands: "
411 | for (var handIdx = 0, handCount = this.hands.length; handIdx != handCount; handIdx++) {
412 | out += " "+ this.hands[handIdx].toString() + " ";
413 | }
414 | out += " Pointables: ";
415 | for (var pointableIdx = 0, pointableCount = this.pointables.length; pointableIdx != pointableCount; pointableIdx++) {
416 | out += " "+ this.pointables[pointableIdx].toString() + " ";
417 | }
418 | out += " Raw JSON: ";
419 | out += JSON.stringify(this.data);
420 | return out;
421 | }
422 |
423 | /**
424 | * An invalid Frame object.
425 | *
426 | * You can use this invalid Frame in comparisons testing
427 | * whether a given Frame instance is valid or invalid. (You can also check the
428 | * [Frame.valid]{@link Leap.Frame#valid} property.)
429 | *
430 | * @static
431 | * @type {Leap.Frame}
432 | * @name Invalid
433 | * @memberof Leap.Frame
434 | */
435 | Frame.Invalid = {
436 | valid: false,
437 | hands: [],
438 | fingers: [],
439 | pointables: [],
440 | pointable: function() { return Pointable.Invalid },
441 | finger: function() { return Pointable.Invalid },
442 | hand: function() { return Hand.Invalid },
443 | toString: function() { return "invalid frame" },
444 | dump: function() { return this.toString() },
445 | rotationAngle: function() { return 0.0; },
446 | rotationMatrix: function() { return mat3.create(); },
447 | rotationAxis: function() { return vec3.create(); },
448 | scaleFactor: function() { return 1.0; },
449 | translation: function() { return vec3.create(); }
450 | };
451 |
--------------------------------------------------------------------------------
/lib/hand.js:
--------------------------------------------------------------------------------
1 | var Pointable = require("./pointable")
2 | , Bone = require('./bone')
3 | , glMatrix = require("gl-matrix")
4 | , mat3 = glMatrix.mat3
5 | , vec3 = glMatrix.vec3;
6 |
7 | /**
8 | * Constructs a Hand object.
9 | *
10 | * An uninitialized hand is considered invalid.
11 | * Get valid Hand objects from a Frame object.
12 | * @class Hand
13 | * @memberof Leap
14 | * @classdesc
15 | * The Hand class reports the physical characteristics of a detected hand.
16 | *
17 | * Hand tracking data includes a palm position and velocity; vectors for
18 | * the palm normal and direction to the fingers; properties of a sphere fit
19 | * to the hand; and lists of the attached fingers.
20 | *
21 | * Note that Hand objects can be invalid, which means that they do not contain
22 | * valid tracking data and do not correspond to a physical entity. Invalid Hand
23 | * objects can be the result of asking for a Hand object using an ID from an
24 | * earlier frame when no Hand objects with that ID exist in the current frame.
25 | * A Hand object created from the Hand constructor is also invalid.
26 | * Test for validity with the [Hand.valid]{@link Leap.Hand#valid} property.
27 | */
28 | var Hand = module.exports = function(data) {
29 | /**
30 | * A unique ID assigned to this Hand object, whose value remains the same
31 | * across consecutive frames while the tracked hand remains visible. If
32 | * tracking is lost (for example, when a hand is occluded by another hand
33 | * or when it is withdrawn from or reaches the edge of the Leap field of view),
34 | * the Leap may assign a new ID when it detects the hand in a future frame.
35 | *
36 | * Use the ID value with the {@link Frame.hand}() function to find this
37 | * Hand object in future frames.
38 | *
39 | * @member id
40 | * @memberof Leap.Hand.prototype
41 | * @type {String}
42 | */
43 | this.id = data.id;
44 | /**
45 | * The center position of the palm in millimeters from the Leap origin.
46 | * @member palmPosition
47 | * @memberof Leap.Hand.prototype
48 | * @type {number[]}
49 | */
50 | this.palmPosition = data.palmPosition;
51 | /**
52 | * The direction from the palm position toward the fingers.
53 | *
54 | * The direction is expressed as a unit vector pointing in the same
55 | * direction as the directed line from the palm position to the fingers.
56 | *
57 | * @member direction
58 | * @memberof Leap.Hand.prototype
59 | * @type {number[]}
60 | */
61 | this.direction = data.direction;
62 | /**
63 | * The rate of change of the palm position in millimeters/second.
64 | *
65 | * @member palmVeclocity
66 | * @memberof Leap.Hand.prototype
67 | * @type {number[]}
68 | */
69 | this.palmVelocity = data.palmVelocity;
70 | /**
71 | * The normal vector to the palm. If your hand is flat, this vector will
72 | * point downward, or "out" of the front surface of your palm.
73 | *
74 | * 
75 | *
76 | * The direction is expressed as a unit vector pointing in the same
77 | * direction as the palm normal (that is, a vector orthogonal to the palm).
78 | * @member palmNormal
79 | * @memberof Leap.Hand.prototype
80 | * @type {number[]}
81 | */
82 | this.palmNormal = data.palmNormal;
83 | /**
84 | * The center of a sphere fit to the curvature of this hand.
85 | *
86 | * This sphere is placed roughly as if the hand were holding a ball.
87 | *
88 | * 
89 | * @member sphereCenter
90 | * @memberof Leap.Hand.prototype
91 | * @type {number[]}
92 | */
93 | this.sphereCenter = data.sphereCenter;
94 | /**
95 | * The radius of a sphere fit to the curvature of this hand, in millimeters.
96 | *
97 | * This sphere is placed roughly as if the hand were holding a ball. Thus the
98 | * size of the sphere decreases as the fingers are curled into a fist.
99 | *
100 | * @member sphereRadius
101 | * @memberof Leap.Hand.prototype
102 | * @type {number}
103 | */
104 | this.sphereRadius = data.sphereRadius;
105 | /**
106 | * Reports whether this is a valid Hand object.
107 | *
108 | * @member valid
109 | * @memberof Leap.Hand.prototype
110 | * @type {boolean}
111 | */
112 | this.valid = true;
113 | /**
114 | * The list of Pointable objects (fingers) detected in this frame
115 | * that are associated with this hand, given in arbitrary order. The list
116 | * can be empty if no fingers or tools associated with this hand are detected.
117 | *
118 | * Use the {@link Pointable} tool property to determine
119 | * whether or not an item in the list represents a tool or finger.
120 | * You can also get only the fingers using the Hand.fingers[] list.
121 | *
122 | * @member pointables[]
123 | * @memberof Leap.Hand.prototype
124 | * @type {Leap.Pointable[]}
125 | */
126 | this.pointables = [];
127 | /**
128 | * The list of fingers detected in this frame that are attached to
129 | * this hand, given in arbitrary order.
130 | *
131 | * The list can be empty if no fingers attached to this hand are detected.
132 | *
133 | * @member fingers[]
134 | * @memberof Leap.Hand.prototype
135 | * @type {Leap.Pointable[]}
136 | */
137 | this.fingers = [];
138 |
139 | if (data.armBasis){
140 | this.arm = new Bone(this, {
141 | type: 4,
142 | width: data.armWidth,
143 | prevJoint: data.elbow,
144 | nextJoint: data.wrist,
145 | basis: data.armBasis
146 | });
147 | }else{
148 | this.arm = null;
149 | }
150 |
151 | this._translation = data.t;
152 | function flattenDeep(arr) {
153 | return Array.isArray(arr)
154 | ? arr.reduce(function (a, b) { return a.concat(flattenDeep(b)) }, [])
155 | : [arr];
156 | }
157 | this._rotation = flattenDeep(data.r);
158 | this._scaleFactor = data.s;
159 |
160 | /**
161 | * Time the hand has been visible in seconds.
162 | *
163 | * @member timeVisible
164 | * @memberof Leap.Hand.prototype
165 | * @type {number}
166 | */
167 | this.timeVisible = data.timeVisible;
168 |
169 | /**
170 | * The palm position with stabalization
171 | * @member stabilizedPalmPosition
172 | * @memberof Leap.Hand.prototype
173 | * @type {number[]}
174 | */
175 | this.stabilizedPalmPosition = data.stabilizedPalmPosition;
176 |
177 | /**
178 | * Reports whether this is a left or a right hand.
179 | *
180 | * @member type
181 | * @type {String}
182 | * @memberof Leap.Hand.prototype
183 | */
184 | this.type = data.type;
185 | this.grabStrength = data.grabStrength;
186 | this.pinchStrength = data.pinchStrength;
187 | this.confidence = data.confidence;
188 | }
189 |
190 | /**
191 | * The finger with the specified ID attached to this hand.
192 | *
193 | * Use this function to retrieve a Pointable object representing a finger
194 | * attached to this hand using an ID value obtained from a previous frame.
195 | * This function always returns a Pointable object, but if no finger
196 | * with the specified ID is present, an invalid Pointable object is returned.
197 | *
198 | * Note that the ID values assigned to fingers persist across frames, but only
199 | * until tracking of a particular finger is lost. If tracking of a finger is
200 | * lost and subsequently regained, the new Finger object representing that
201 | * finger may have a different ID than that representing the finger in an
202 | * earlier frame.
203 | *
204 | * @method finger
205 | * @memberof Leap.Hand.prototype
206 | * @param {String} id The ID value of a finger from a previous frame.
207 | * @returns {Leap.Pointable} The Finger object with
208 | * the matching ID if one exists for this hand in this frame; otherwise, an
209 | * invalid Finger object is returned.
210 | */
211 | Hand.prototype.finger = function(id) {
212 | var finger = this.frame.finger(id);
213 | return (finger && (finger.handId == this.id)) ? finger : Pointable.Invalid;
214 | }
215 |
216 | /**
217 | * The angle of rotation around the rotation axis derived from the change in
218 | * orientation of this hand, and any associated fingers, between the
219 | * current frame and the specified frame.
220 | *
221 | * The returned angle is expressed in radians measured clockwise around the
222 | * rotation axis (using the right-hand rule) between the start and end frames.
223 | * The value is always between 0 and pi radians (0 and 180 degrees).
224 | *
225 | * If a corresponding Hand object is not found in sinceFrame, or if either
226 | * this frame or sinceFrame are invalid Frame objects, then the angle of rotation is zero.
227 | *
228 | * @method rotationAngle
229 | * @memberof Leap.Hand.prototype
230 | * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
231 | * @param {numnber[]} [axis] The axis to measure rotation around.
232 | * @returns {number} A positive value representing the heuristically determined
233 | * rotational change of the hand between the current frame and that specified in
234 | * the sinceFrame parameter.
235 | */
236 | Hand.prototype.rotationAngle = function(sinceFrame, axis) {
237 | if (!this.valid || !sinceFrame.valid) return 0.0;
238 | var sinceHand = sinceFrame.hand(this.id);
239 | if(!sinceHand.valid) return 0.0;
240 | var rot = this.rotationMatrix(sinceFrame);
241 | var cs = (rot[0] + rot[4] + rot[8] - 1.0)*0.5
242 | var angle = Math.acos(cs);
243 | angle = isNaN(angle) ? 0.0 : angle;
244 | if (axis !== undefined) {
245 | var rotAxis = this.rotationAxis(sinceFrame);
246 | angle *= vec3.dot(rotAxis, vec3.normalize(vec3.create(), axis));
247 | }
248 | return angle;
249 | }
250 |
251 | /**
252 | * The axis of rotation derived from the change in orientation of this hand, and
253 | * any associated fingers, between the current frame and the specified frame.
254 | *
255 | * The returned direction vector is normalized.
256 | *
257 | * If a corresponding Hand object is not found in sinceFrame, or if either
258 | * this frame or sinceFrame are invalid Frame objects, then this method returns a zero vector.
259 | *
260 | * @method rotationAxis
261 | * @memberof Leap.Hand.prototype
262 | * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
263 | * @returns {number[]} A normalized direction Vector representing the axis of the heuristically determined
264 | * rotational change of the hand between the current frame and that specified in the sinceFrame parameter.
265 | */
266 | Hand.prototype.rotationAxis = function(sinceFrame) {
267 | if (!this.valid || !sinceFrame.valid) return vec3.create();
268 | var sinceHand = sinceFrame.hand(this.id);
269 | if (!sinceHand.valid) return vec3.create();
270 | return vec3.normalize(vec3.create(), [
271 | this._rotation[7] - sinceHand._rotation[5],
272 | this._rotation[2] - sinceHand._rotation[6],
273 | this._rotation[3] - sinceHand._rotation[1]
274 | ]);
275 | }
276 |
277 | /**
278 | * The transform matrix expressing the rotation derived from the change in
279 | * orientation of this hand, and any associated fingers, between
280 | * the current frame and the specified frame.
281 | *
282 | * If a corresponding Hand object is not found in sinceFrame, or if either
283 | * this frame or sinceFrame are invalid Frame objects, then this method returns
284 | * an identity matrix.
285 | *
286 | * @method rotationMatrix
287 | * @memberof Leap.Hand.prototype
288 | * @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
289 | * @returns {number[]} A transformation Matrix containing the heuristically determined
290 | * rotational change of the hand between the current frame and that specified in the sinceFrame parameter.
291 | */
292 | Hand.prototype.rotationMatrix = function(sinceFrame) {
293 | if (!this.valid || !sinceFrame.valid) return mat3.create();
294 | var sinceHand = sinceFrame.hand(this.id);
295 | if(!sinceHand.valid) return mat3.create();
296 | var transpose = mat3.transpose(mat3.create(), this._rotation);
297 | var m = mat3.multiply(mat3.create(), sinceHand._rotation, transpose);
298 | return m;
299 | }
300 |
301 | /**
302 | * The scale factor derived from the hand's motion between the current frame and the specified frame.
303 | *
304 | * The scale factor is always positive. A value of 1.0 indicates no scaling took place.
305 | * Values between 0.0 and 1.0 indicate contraction and values greater than 1.0 indicate expansion.
306 | *
307 | * The Leap derives scaling from the relative inward or outward motion of a hand
308 | * and its associated fingers (independent of translation and rotation).
309 | *
310 | * If a corresponding Hand object is not found in sinceFrame, or if either this frame or sinceFrame
311 | * are invalid Frame objects, then this method returns 1.0.
312 | *
313 | * @method scaleFactor
314 | * @memberof Leap.Hand.prototype
315 | * @param {Leap.Frame} sinceFrame The starting frame for computing the relative scaling.
316 | * @returns {number} A positive value representing the heuristically determined
317 | * scaling change ratio of the hand between the current frame and that specified in the sinceFrame parameter.
318 | */
319 | Hand.prototype.scaleFactor = function(sinceFrame) {
320 | if (!this.valid || !sinceFrame.valid) return 1.0;
321 | var sinceHand = sinceFrame.hand(this.id);
322 | if(!sinceHand.valid) return 1.0;
323 |
324 | return Math.exp(this._scaleFactor - sinceHand._scaleFactor);
325 | }
326 |
327 | /**
328 | * The change of position of this hand between the current frame and the specified frame
329 | *
330 | * The returned translation vector provides the magnitude and direction of the
331 | * movement in millimeters.
332 | *
333 | * If a corresponding Hand object is not found in sinceFrame, or if either this frame or
334 | * sinceFrame are invalid Frame objects, then this method returns a zero vector.
335 | *
336 | * @method translation
337 | * @memberof Leap.Hand.prototype
338 | * @param {Leap.Frame} sinceFrame The starting frame for computing the relative translation.
339 | * @returns {number[]} A Vector representing the heuristically determined change in hand
340 | * position between the current frame and that specified in the sinceFrame parameter.
341 | */
342 | Hand.prototype.translation = function(sinceFrame) {
343 | if (!this.valid || !sinceFrame.valid) return vec3.create();
344 | var sinceHand = sinceFrame.hand(this.id);
345 | if(!sinceHand.valid) return vec3.create();
346 | return [
347 | this._translation[0] - sinceHand._translation[0],
348 | this._translation[1] - sinceHand._translation[1],
349 | this._translation[2] - sinceHand._translation[2]
350 | ];
351 | }
352 |
353 | /**
354 | * A string containing a brief, human readable description of the Hand object.
355 | * @method toString
356 | * @memberof Leap.Hand.prototype
357 | * @returns {String} A description of the Hand as a string.
358 | */
359 | Hand.prototype.toString = function() {
360 | return "Hand (" + this.type + ") [ id: "+ this.id + " | palm velocity:"+this.palmVelocity+" | sphere center:"+this.sphereCenter+" ] ";
361 | }
362 |
363 | /**
364 | * The pitch angle in radians.
365 | *
366 | * Pitch is the angle between the negative z-axis and the projection of
367 | * the vector onto the y-z plane. In other words, pitch represents rotation
368 | * around the x-axis.
369 | * If the vector points upward, the returned angle is between 0 and pi radians
370 | * (180 degrees); if it points downward, the angle is between 0 and -pi radians.
371 | *
372 | * @method pitch
373 | * @memberof Leap.Hand.prototype
374 | * @returns {number} The angle of this vector above or below the horizon (x-z plane).
375 | *
376 | */
377 | Hand.prototype.pitch = function() {
378 | return Math.atan2(this.direction[1], -this.direction[2]);
379 | }
380 |
381 | /**
382 | * The yaw angle in radians.
383 | *
384 | * Yaw is the angle between the negative z-axis and the projection of
385 | * the vector onto the x-z plane. In other words, yaw represents rotation
386 | * around the y-axis. If the vector points to the right of the negative z-axis,
387 | * then the returned angle is between 0 and pi radians (180 degrees);
388 | * if it points to the left, the angle is between 0 and -pi radians.
389 | *
390 | * @method yaw
391 | * @memberof Leap.Hand.prototype
392 | * @returns {number} The angle of this vector to the right or left of the y-axis.
393 | *
394 | */
395 | Hand.prototype.yaw = function() {
396 | return Math.atan2(this.direction[0], -this.direction[2]);
397 | }
398 |
399 | /**
400 | * The roll angle in radians.
401 | *
402 | * Roll is the angle between the y-axis and the projection of
403 | * the vector onto the x-y plane. In other words, roll represents rotation
404 | * around the z-axis. If the vector points to the left of the y-axis,
405 | * then the returned angle is between 0 and pi radians (180 degrees);
406 | * if it points to the right, the angle is between 0 and -pi radians.
407 | *
408 | * @method roll
409 | * @memberof Leap.Hand.prototype
410 | * @returns {number} The angle of this vector to the right or left of the y-axis.
411 | *
412 | */
413 | Hand.prototype.roll = function() {
414 | return Math.atan2(this.palmNormal[0], -this.palmNormal[1]);
415 | }
416 |
417 | /**
418 | * An invalid Hand object.
419 | *
420 | * You can use an invalid Hand object in comparisons testing
421 | * whether a given Hand instance is valid or invalid. (You can also use the
422 | * Hand valid property.)
423 | *
424 | * @static
425 | * @type {Leap.Hand}
426 | * @name Invalid
427 | * @memberof Leap.Hand
428 | */
429 | Hand.Invalid = {
430 | valid: false,
431 | fingers: [],
432 | pointables: [],
433 | left: false,
434 | pointable: function() { return Pointable.Invalid },
435 | finger: function() { return Pointable.Invalid },
436 | toString: function() { return "invalid frame" },
437 | dump: function() { return this.toString(); },
438 | rotationAngle: function() { return 0.0; },
439 | rotationMatrix: function() { return mat3.create(); },
440 | rotationAxis: function() { return vec3.create(); },
441 | scaleFactor: function() { return 1.0; },
442 | translation: function() { return vec3.create(); }
443 | };
444 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Leap is the global namespace of the Leap API.
3 | * @namespace Leap
4 | */
5 | module.exports = {
6 | Controller: require("./controller"),
7 | Frame: require("./frame"),
8 | Hand: require("./hand"),
9 | Pointable: require("./pointable"),
10 | Finger: require("./finger"),
11 | InteractionBox: require("./interaction_box"),
12 | CircularBuffer: require("./circular_buffer"),
13 | UI: require("./ui"),
14 | JSONProtocol: require("./protocol").JSONProtocol,
15 | glMatrix: require("gl-matrix"),
16 | mat3: require("gl-matrix").mat3,
17 | vec3: require("gl-matrix").vec3,
18 | loopController: undefined,
19 | version: require('./version.js'),
20 |
21 | /**
22 | * Expose utility libraries for convenience
23 | * Use carefully - they may be subject to upgrade or removal in different versions of LeapJS.
24 | */
25 | EventEmitter: require('events').EventEmitter,
26 |
27 | /**
28 | * The Leap.loop() function passes a frame of Leap data to your
29 | * callback function and then calls window.requestAnimationFrame() after
30 | * executing your callback function.
31 | *
32 | * Leap.loop() sets up the Leap controller and WebSocket connection for you.
33 | * You do not need to create your own controller when using this method.
34 | *
35 | * Your callback function is called on an interval determined by the client
36 | * browser. Typically, this is on an interval of 60 frames/second. The most
37 | * recent frame of Leap data is passed to your callback function. If the Leap
38 | * is producing frames at a slower rate than the browser frame rate, the same
39 | * frame of Leap data can be passed to your function in successive animation
40 | * updates.
41 | *
42 | * As an alternative, you can create your own Controller object and use a
43 | * {@link Controller#onFrame onFrame} callback to process the data at
44 | * the frame rate of the Leap device. See {@link Controller} for an
45 | * example.
46 | *
47 | * @method Leap.loop
48 | * @param {function} callback A function called when the browser is ready to
49 | * draw to the screen. The most recent {@link Frame} object is passed to
50 | * your callback function.
51 | *
52 | * ```javascript
53 | * Leap.loop( function( frame ) {
54 | * // ... your code here
55 | * })
56 | * ```
57 | */
58 | loop: function(opts, callback) {
59 | if (opts && callback === undefined && ( ({}).toString.call(opts) === '[object Function]' ) ) {
60 | callback = opts;
61 | opts = {};
62 | }
63 |
64 | if (this.loopController) {
65 | if (opts){
66 | this.loopController.setupFrameEvents(opts);
67 | }
68 | }else{
69 | this.loopController = new this.Controller(opts);
70 | }
71 |
72 | this.loopController.loop(callback);
73 | return this.loopController;
74 | },
75 |
76 | /*
77 | * Convenience method for Leap.Controller.plugin
78 | */
79 | plugin: function(name, options){
80 | this.Controller.plugin(name, options)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/lib/interaction_box.js:
--------------------------------------------------------------------------------
1 | var glMatrix = require("gl-matrix")
2 | , vec3 = glMatrix.vec3;
3 |
4 | /**
5 | * Constructs a InteractionBox object.
6 | *
7 | * @class InteractionBox
8 | * @memberof Leap
9 | * @classdesc
10 | * The InteractionBox class represents a box-shaped region completely within
11 | * the field of view of the Leap Motion controller.
12 | *
13 | * The interaction box is an axis-aligned rectangular prism and provides
14 | * normalized coordinates for hands, fingers, and tools within this box.
15 | * The InteractionBox class can make it easier to map positions in the
16 | * Leap Motion coordinate system to 2D or 3D coordinate systems used
17 | * for application drawing.
18 | *
19 | * 
20 | *
21 | * The InteractionBox region is defined by a center and dimensions along the x, y, and z axes.
22 | */
23 | var InteractionBox = module.exports = function(data) {
24 | /**
25 | * Indicates whether this is a valid InteractionBox object.
26 | *
27 | * @member valid
28 | * @type {Boolean}
29 | * @memberof Leap.InteractionBox.prototype
30 | */
31 | this.valid = true;
32 | /**
33 | * The center of the InteractionBox in device coordinates (millimeters).
34 | * This point is equidistant from all sides of the box.
35 | *
36 | * @member center
37 | * @type {number[]}
38 | * @memberof Leap.InteractionBox.prototype
39 | */
40 | this.center = data.center;
41 |
42 | this.size = data.size;
43 | /**
44 | * The width of the InteractionBox in millimeters, measured along the x-axis.
45 | *
46 | * @member width
47 | * @type {number}
48 | * @memberof Leap.InteractionBox.prototype
49 | */
50 | this.width = data.size[0];
51 | /**
52 | * The height of the InteractionBox in millimeters, measured along the y-axis.
53 | *
54 | * @member height
55 | * @type {number}
56 | * @memberof Leap.InteractionBox.prototype
57 | */
58 | this.height = data.size[1];
59 | /**
60 | * The depth of the InteractionBox in millimeters, measured along the z-axis.
61 | *
62 | * @member depth
63 | * @type {number}
64 | * @memberof Leap.InteractionBox.prototype
65 | */
66 | this.depth = data.size[2];
67 | }
68 |
69 | /**
70 | * Converts a position defined by normalized InteractionBox coordinates
71 | * into device coordinates in millimeters.
72 | *
73 | * This function performs the inverse of normalizePoint().
74 | *
75 | * @method denormalizePoint
76 | * @memberof Leap.InteractionBox.prototype
77 | * @param {number[]} normalizedPosition The input position in InteractionBox coordinates.
78 | * @returns {number[]} The corresponding denormalized position in device coordinates.
79 | */
80 | InteractionBox.prototype.denormalizePoint = function(normalizedPosition) {
81 | return vec3.fromValues(
82 | (normalizedPosition[0] - 0.5) * this.size[0] + this.center[0],
83 | (normalizedPosition[1] - 0.5) * this.size[1] + this.center[1],
84 | (normalizedPosition[2] - 0.5) * this.size[2] + this.center[2]
85 | );
86 | }
87 |
88 | /**
89 | * Normalizes the coordinates of a point using the interaction box.
90 | *
91 | * Coordinates from the Leap Motion frame of reference (millimeters) are
92 | * converted to a range of [0..1] such that the minimum value of the
93 | * InteractionBox maps to 0 and the maximum value of the InteractionBox maps to 1.
94 | *
95 | * @method normalizePoint
96 | * @memberof Leap.InteractionBox.prototype
97 | * @param {number[]} position The input position in device coordinates.
98 | * @param {Boolean} clamp Whether or not to limit the output value to the range [0,1]
99 | * when the input position is outside the InteractionBox. Defaults to true.
100 | * @returns {number[]} The normalized position.
101 | */
102 | InteractionBox.prototype.normalizePoint = function(position, clamp) {
103 | var vec = vec3.fromValues(
104 | ((position[0] - this.center[0]) / this.size[0]) + 0.5,
105 | ((position[1] - this.center[1]) / this.size[1]) + 0.5,
106 | ((position[2] - this.center[2]) / this.size[2]) + 0.5
107 | );
108 |
109 | if (clamp) {
110 | vec[0] = Math.min(Math.max(vec[0], 0), 1);
111 | vec[1] = Math.min(Math.max(vec[1], 0), 1);
112 | vec[2] = Math.min(Math.max(vec[2], 0), 1);
113 | }
114 | return vec;
115 | }
116 |
117 | /**
118 | * Writes a brief, human readable description of the InteractionBox object.
119 | *
120 | * @method toString
121 | * @memberof Leap.InteractionBox.prototype
122 | * @returns {String} A description of the InteractionBox object as a string.
123 | */
124 | InteractionBox.prototype.toString = function() {
125 | return "InteractionBox [ width:" + this.width + " | height:" + this.height + " | depth:" + this.depth + " ]";
126 | }
127 |
128 | /**
129 | * An invalid InteractionBox object.
130 | *
131 | * You can use this InteractionBox instance in comparisons testing
132 | * whether a given InteractionBox instance is valid or invalid. (You can also use the
133 | * InteractionBox.valid property.)
134 | *
135 | * @static
136 | * @type {Leap.InteractionBox}
137 | * @name Invalid
138 | * @memberof Leap.InteractionBox
139 | */
140 | InteractionBox.Invalid = { valid: false };
141 |
--------------------------------------------------------------------------------
/lib/pipeline.js:
--------------------------------------------------------------------------------
1 | var Pipeline = module.exports = function (controller) {
2 | this.steps = [];
3 | this.controller = controller;
4 | }
5 |
6 | Pipeline.prototype.addStep = function (step) {
7 | this.steps.push(step);
8 | }
9 |
10 | Pipeline.prototype.run = function (frame) {
11 | var stepsLength = this.steps.length;
12 | for (var i = 0; i != stepsLength; i++) {
13 | if (!frame) break;
14 | frame = this.steps[i](frame);
15 | }
16 | return frame;
17 | }
18 |
19 | Pipeline.prototype.removeStep = function(step){
20 | var index = this.steps.indexOf(step);
21 | if (index === -1) throw "Step not found in pipeline";
22 | this.steps.splice(index, 1);
23 | }
24 |
25 | /*
26 | * Wraps a plugin callback method in method which can be run inside the pipeline.
27 | * This wrapper method loops the callback over objects within the frame as is appropriate,
28 | * calling the callback for each in turn.
29 | *
30 | * @method createStepFunction
31 | * @memberOf Leap.Controller.prototype
32 | * @param {Controller} The controller on which the callback is called.
33 | * @param {String} type What frame object the callback is run for and receives.
34 | * Can be one of 'frame', 'finger', 'hand', 'pointable'
35 | * @param {function} callback The method which will be run inside the pipeline loop. Receives one argument, such as a hand.
36 | * @private
37 | */
38 | Pipeline.prototype.addWrappedStep = function (type, callback) {
39 | var controller = this.controller,
40 | step = function (frame) {
41 | var dependencies, i, len;
42 | dependencies = (type == 'frame') ? [frame] : (frame[type + 's'] || []);
43 |
44 | for (i = 0, len = dependencies.length; i < len; i++) {
45 | callback.call(controller, dependencies[i]);
46 | }
47 |
48 | return frame;
49 | };
50 |
51 | this.addStep(step);
52 | return step;
53 | };
--------------------------------------------------------------------------------
/lib/pointable.js:
--------------------------------------------------------------------------------
1 | var glMatrix = require("gl-matrix")
2 | , vec3 = glMatrix.vec3;
3 |
4 | /**
5 | * Constructs a Pointable object.
6 | *
7 | * An uninitialized pointable is considered invalid.
8 | * Get valid Pointable objects from a Frame or a Hand object.
9 | *
10 | * @class Pointable
11 | * @memberof Leap
12 | * @classdesc
13 | * The Pointable class reports the physical characteristics of a detected
14 | * finger or tool.
15 | *
16 | * Both fingers and tools are classified as Pointable objects. Use the
17 | * Pointable.tool property to determine whether a Pointable object represents a
18 | * tool or finger. The Leap classifies a detected entity as a tool when it is
19 | * thinner, straighter, and longer than a typical finger.
20 | *
21 | * Note that Pointable objects can be invalid, which means that they do not
22 | * contain valid tracking data and do not correspond to a physical entity.
23 | * Invalid Pointable objects can be the result of asking for a Pointable object
24 | * using an ID from an earlier frame when no Pointable objects with that ID
25 | * exist in the current frame. A Pointable object created from the Pointable
26 | * constructor is also invalid. Test for validity with the Pointable.valid
27 | * property.
28 | */
29 | var Pointable = module.exports = function(data) {
30 | /**
31 | * Indicates whether this is a valid Pointable object.
32 | *
33 | * @member valid
34 | * @type {Boolean}
35 | * @memberof Leap.Pointable.prototype
36 | */
37 | this.valid = true;
38 | /**
39 | * A unique ID assigned to this Pointable object, whose value remains the
40 | * same across consecutive frames while the tracked finger or tool remains
41 | * visible. If tracking is lost (for example, when a finger is occluded by
42 | * another finger or when it is withdrawn from the Leap field of view), the
43 | * Leap may assign a new ID when it detects the entity in a future frame.
44 | *
45 | * Use the ID value with the pointable() functions defined for the
46 | * {@link Frame} and {@link Frame.Hand} classes to find this
47 | * Pointable object in future frames.
48 | *
49 | * @member id
50 | * @type {String}
51 | * @memberof Leap.Pointable.prototype
52 | */
53 | this.id = data.id;
54 | this.handId = data.handId;
55 | /**
56 | * The estimated length of the finger or tool in millimeters.
57 | *
58 | * The reported length is the visible length of the finger or tool from the
59 | * hand to tip. If the length isn't known, then a value of 0 is returned.
60 | *
61 | * @member length
62 | * @type {number}
63 | * @memberof Leap.Pointable.prototype
64 | */
65 | this.length = data.length;
66 | /**
67 | * Whether or not the Pointable is believed to be a tool.
68 | * Tools are generally longer, thinner, and straighter than fingers.
69 | *
70 | * If tool is false, then this Pointable must be a finger.
71 | *
72 | * @member tool
73 | * @type {Boolean}
74 | * @memberof Leap.Pointable.prototype
75 | */
76 | this.tool = data.tool;
77 | /**
78 | * The estimated width of the tool in millimeters.
79 | *
80 | * The reported width is the average width of the visible portion of the
81 | * tool from the hand to the tip. If the width isn't known,
82 | * then a value of 0 is returned.
83 | *
84 | * Pointable objects representing fingers do not have a width property.
85 | *
86 | * @member width
87 | * @type {number}
88 | * @memberof Leap.Pointable.prototype
89 | */
90 | this.width = data.width;
91 | /**
92 | * The direction in which this finger or tool is pointing.
93 | *
94 | * The direction is expressed as a unit vector pointing in the same
95 | * direction as the tip.
96 | *
97 | * 
98 | * @member direction
99 | * @type {number[]}
100 | * @memberof Leap.Pointable.prototype
101 | */
102 | this.direction = data.direction;
103 | /**
104 | * The tip position in millimeters from the Leap origin.
105 | * Stabilized
106 | *
107 | * @member stabilizedTipPosition
108 | * @type {number[]}
109 | * @memberof Leap.Pointable.prototype
110 | */
111 | this.stabilizedTipPosition = data.stabilizedTipPosition;
112 | /**
113 | * The tip position in millimeters from the Leap origin.
114 | *
115 | * @member tipPosition
116 | * @type {number[]}
117 | * @memberof Leap.Pointable.prototype
118 | */
119 | this.tipPosition = data.tipPosition;
120 | /**
121 | * The rate of change of the tip position in millimeters/second.
122 | *
123 | * @member tipVelocity
124 | * @type {number[]}
125 | * @memberof Leap.Pointable.prototype
126 | */
127 | this.tipVelocity = data.tipVelocity;
128 | /**
129 | * The current touch zone of this Pointable object.
130 | *
131 | * The Leap Motion software computes the touch zone based on a floating touch
132 | * plane that adapts to the user's finger movement and hand posture. The Leap
133 | * Motion software interprets purposeful movements toward this plane as potential touch
134 | * points. When a Pointable moves close to the adaptive touch plane, it enters the
135 | * "hovering" zone. When a Pointable reaches or passes through the plane, it enters
136 | * the "touching" zone.
137 | *
138 | * The possible states include:
139 | *
140 | * * "none" -- The Pointable is outside the hovering zone.
141 | * * "hovering" -- The Pointable is close to, but not touching the touch plane.
142 | * * "touching" -- The Pointable has penetrated the touch plane.
143 | *
144 | * The touchDistance value provides a normalized indication of the distance to
145 | * the touch plane when the Pointable is in the hovering or touching zones.
146 | *
147 | * @member touchZone
148 | * @type {String}
149 | * @memberof Leap.Pointable.prototype
150 | */
151 | this.touchZone = data.touchZone;
152 | /**
153 | * A value proportional to the distance between this Pointable object and the
154 | * adaptive touch plane.
155 | *
156 | * 
157 | *
158 | * The touch distance is a value in the range [-1, 1]. The value 1.0 indicates the
159 | * Pointable is at the far edge of the hovering zone. The value 0 indicates the
160 | * Pointable is just entering the touching zone. A value of -1.0 indicates the
161 | * Pointable is firmly within the touching zone. Values in between are
162 | * proportional to the distance from the plane. Thus, the touchDistance of 0.5
163 | * indicates that the Pointable is halfway into the hovering zone.
164 | *
165 | * You can use the touchDistance value to modulate visual feedback given to the
166 | * user as their fingers close in on a touch target, such as a button.
167 | *
168 | * @member touchDistance
169 | * @type {number}
170 | * @memberof Leap.Pointable.prototype
171 | */
172 | this.touchDistance = data.touchDistance;
173 |
174 | /**
175 | * How long the pointable has been visible in seconds.
176 | *
177 | * @member timeVisible
178 | * @type {number}
179 | * @memberof Leap.Pointable.prototype
180 | */
181 | this.timeVisible = data.timeVisible;
182 | }
183 |
184 | /**
185 | * A string containing a brief, human readable description of the Pointable
186 | * object.
187 | *
188 | * @method toString
189 | * @memberof Leap.Pointable.prototype
190 | * @returns {String} A description of the Pointable object as a string.
191 | */
192 | Pointable.prototype.toString = function() {
193 | return "Pointable [ id:" + this.id + " " + this.length + "mmx | width:" + this.width + "mm | direction:" + this.direction + ' ]';
194 | }
195 |
196 | /**
197 | * Returns the hand which the pointable is attached to.
198 | */
199 | Pointable.prototype.hand = function(){
200 | return this.frame.hand(this.handId);
201 | }
202 |
203 | /**
204 | * An invalid Pointable object.
205 | *
206 | * You can use this Pointable instance in comparisons testing
207 | * whether a given Pointable instance is valid or invalid. (You can also use the
208 | * Pointable.valid property.)
209 |
210 | * @static
211 | * @type {Leap.Pointable}
212 | * @name Invalid
213 | * @memberof Leap.Pointable
214 | */
215 | Pointable.Invalid = { valid: false };
216 |
--------------------------------------------------------------------------------
/lib/protocol.js:
--------------------------------------------------------------------------------
1 | var Frame = require('./frame')
2 | , EventEmitter = require('events').EventEmitter;
3 |
4 | var Event = function(data) {
5 | this.type = data.type;
6 | this.state = data.state;
7 | };
8 |
9 | exports.chooseProtocol = function(header) {
10 | var protocol;
11 | switch(header.version) {
12 | case 1:
13 | case 2:
14 | case 3:
15 | case 4:
16 | case 5:
17 | case 6:
18 | protocol = JSONProtocol(header);
19 | protocol.sendBackground = function(connection, state) {
20 | connection.send(protocol.encode({background: state}));
21 | }
22 | protocol.sendFocused = function(connection, state) {
23 | connection.send(protocol.encode({focused: state}));
24 | }
25 | protocol.sendOptimizeHMD = function(connection, state) {
26 | connection.send(protocol.encode({optimizeHMD: state}));
27 | }
28 | break;
29 | default:
30 | throw "unrecognized version";
31 | }
32 | return protocol;
33 | }
34 |
35 | var JSONProtocol = exports.JSONProtocol = function(header) {
36 |
37 | var protocol = function(frameData) {
38 |
39 | if (frameData.event) {
40 |
41 | return new Event(frameData.event);
42 |
43 | } else {
44 |
45 | protocol.emit('beforeFrameCreated', frameData);
46 |
47 | var frame = new Frame(frameData);
48 |
49 | protocol.emit('afterFrameCreated', frame, frameData);
50 |
51 | return frame;
52 |
53 | }
54 |
55 | };
56 |
57 | protocol.encode = function(message) {
58 | return JSON.stringify(message);
59 | };
60 | protocol.version = header.version;
61 | protocol.serviceVersion = header.serviceVersion;
62 | protocol.versionLong = 'Version ' + header.version;
63 | protocol.type = 'protocol';
64 |
65 | Object.assign(protocol, EventEmitter.prototype);
66 |
67 | return protocol;
68 | };
69 |
70 |
71 |
--------------------------------------------------------------------------------
/lib/ui.js:
--------------------------------------------------------------------------------
1 | exports.UI = {
2 | Region: require("./ui/region"),
3 | Cursor: require("./ui/cursor")
4 | };
--------------------------------------------------------------------------------
/lib/ui/cursor.js:
--------------------------------------------------------------------------------
1 | var Cursor = module.exports = function() {
2 | return function(frame) {
3 | var pointable = frame.pointables.sort(function(a, b) { return a.z - b.z })[0]
4 | if (pointable && pointable.valid) {
5 | frame.cursorPosition = pointable.tipPosition
6 | }
7 | return frame
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/lib/ui/region.js:
--------------------------------------------------------------------------------
1 | var EventEmitter = require('events').EventEmitter;
2 |
3 | var Region = module.exports = function(start, end) {
4 | this.start = new Vector(start)
5 | this.end = new Vector(end)
6 | this.enteredFrame = null
7 | }
8 |
9 | Region.prototype.hasPointables = function(frame) {
10 | for (var i = 0; i != frame.pointables.length; i++) {
11 | var position = frame.pointables[i].tipPosition
12 | if (position.x >= this.start.x && position.x <= this.end.x && position.y >= this.start.y && position.y <= this.end.y && position.z >= this.start.z && position.z <= this.end.z) {
13 | return true
14 | }
15 | }
16 | return false
17 | }
18 |
19 | Region.prototype.listener = function(opts) {
20 | var region = this
21 | if (opts && opts.nearThreshold) this.setupNearRegion(opts.nearThreshold)
22 | return function(frame) {
23 | return region.updatePosition(frame)
24 | }
25 | }
26 |
27 | Region.prototype.clipper = function() {
28 | var region = this
29 | return function(frame) {
30 | region.updatePosition(frame)
31 | return region.enteredFrame ? frame : null
32 | }
33 | }
34 |
35 | Region.prototype.setupNearRegion = function(distance) {
36 | var nearRegion = this.nearRegion = new Region(
37 | [this.start.x - distance, this.start.y - distance, this.start.z - distance],
38 | [this.end.x + distance, this.end.y + distance, this.end.z + distance]
39 | )
40 | var region = this
41 | nearRegion.on("enter", function(frame) {
42 | region.emit("near", frame)
43 | })
44 | nearRegion.on("exit", function(frame) {
45 | region.emit("far", frame)
46 | })
47 | region.on('exit', function(frame) {
48 | region.emit("near", frame)
49 | })
50 | }
51 |
52 | Region.prototype.updatePosition = function(frame) {
53 | if (this.nearRegion) this.nearRegion.updatePosition(frame)
54 | if (this.hasPointables(frame) && this.enteredFrame == null) {
55 | this.enteredFrame = frame
56 | this.emit("enter", this.enteredFrame)
57 | } else if (!this.hasPointables(frame) && this.enteredFrame != null) {
58 | this.enteredFrame = null
59 | this.emit("exit", this.enteredFrame)
60 | }
61 | return frame
62 | }
63 |
64 | Region.prototype.normalize = function(position) {
65 | return new Vector([
66 | (position.x - this.start.x) / (this.end.x - this.start.x),
67 | (position.y - this.start.y) / (this.end.y - this.start.y),
68 | (position.z - this.start.z) / (this.end.z - this.start.z)
69 | ])
70 | }
71 |
72 | Region.prototype.mapToXY = function(position, width, height) {
73 | var normalized = this.normalize(position)
74 | var x = normalized.x, y = normalized.y
75 | if (x > 1) x = 1
76 | else if (x < -1) x = -1
77 | if (y > 1) y = 1
78 | else if (y < -1) y = -1
79 | return [
80 | (x + 1) / 2 * width,
81 | (1 - y) / 2 * height,
82 | normalized.z
83 | ]
84 | }
85 |
86 | Object.assign(Region.prototype, EventEmitter.prototype)
--------------------------------------------------------------------------------
/lib/version.js:
--------------------------------------------------------------------------------
1 | // This file is automatically updated from package.json by grunt.
2 | module.exports = {
3 | full: '1.1.1',
4 | major: 1,
5 | minor: 1,
6 | dot: 1
7 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "leapjs",
3 | "version": "1.1.1",
4 | "description": "JavaScript client for the Leap Motion Controller",
5 | "main": "lib",
6 | "engines": {
7 | "node": "~12.0.0"
8 | },
9 | "scripts": {
10 | "test": "make test-node test-integration"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "http://github.com/leapmotion/leapjs.git"
15 | },
16 | "license": "Apache-2.0",
17 | "dependencies": {
18 | "gl-matrix": "^3.3.0",
19 | "ws": "^7.4.6"
20 | },
21 | "devDependencies": {
22 | "browserify": "^16.5.0",
23 | "chai": "^4.2.0",
24 | "grunt": "^1.3.0",
25 | "grunt-banner": "^0.6.0",
26 | "grunt-browserify": "^6.0.0",
27 | "grunt-contrib-clean": "^2.0.0",
28 | "grunt-contrib-uglify": "^4.0.1",
29 | "grunt-contrib-watch": "^1.1.0",
30 | "grunt-exec": "^3.0.0",
31 | "grunt-string-replace": "^1.3.1",
32 | "jsdoc": "^3.6.6",
33 | "load-grunt-tasks": "^5.1.0",
34 | "mocha": "^8.2.1",
35 | "mocha-headless-chrome": "^3.1.0",
36 | "nodemon": "^2.0.6",
37 | "uglify-js": "^3.11.5"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/stress/punisher.js:
--------------------------------------------------------------------------------
1 | var Leap = require('../lib');
2 |
3 | setTimeout(function() {
4 | process.exit(0);
5 | }, Math.floor(Math.random() * 5000));
6 |
7 | var controller = new Leap.Controller();
8 |
9 | controller.on('connect', function() {
10 | console.log("connected")
11 | });
12 |
13 | controller.on('ready', function() {
14 | console.log("ready")
15 | setInterval(function() {
16 | controller.connection.sendHeartbeat();
17 | }, 1);
18 | })
19 |
20 | controller.loop(function(frame) {
21 | console.log('.');
22 | });
23 |
--------------------------------------------------------------------------------
/template/entry.js:
--------------------------------------------------------------------------------
1 | if (typeof (window) !== 'undefined'){
2 | if (typeof (window.requestAnimationFrame) !== 'function') {
3 | window.requestAnimationFrame = (
4 | window.webkitRequestAnimationFrame ||
5 | window.mozRequestAnimationFrame ||
6 | window.oRequestAnimationFrame ||
7 | window.msRequestAnimationFrame ||
8 | function (callback) { setTimeout(callback, 1000 / 60); }
9 | );
10 | }
11 | window.Leap = require("../lib/index");
12 | } else {
13 | Leap = require("../lib/index");
14 | }
15 |
--------------------------------------------------------------------------------
/test/assertUtil.js:
--------------------------------------------------------------------------------
1 | (function (window) {
2 |
3 | if (typeof module != 'undefined') {
4 | var assert = require('assert');
5 | var util = require('util');
6 | } else {
7 | var assert = window.assert;
8 | }
9 |
10 | function ok(test, message) {
11 | if (assert && assert.ok) {
12 | assert.ok(test, message);
13 | } else {
14 | assert.strictEqual(!!test, message);
15 | }
16 | }
17 |
18 | function isObject(o){
19 | return typeof o == 'object';
20 | }
21 |
22 | var _DEFAULT_RANGE = 1e-6;
23 |
24 | var _DEBUG = 0;
25 |
26 | function f(v1, v2, v3) {
27 | var args = arguments || [v1, v2, v3];
28 | if (typeof util !== 'undefined') {
29 | return util.format.apply(util, args);
30 | } else {
31 | try {
32 | return args.join(',');// very sloppy hack of util.format
33 | } catch (er) {
34 | return args[0];
35 | }
36 | }
37 | }
38 |
39 | var lib = {
40 |
41 | /**
42 | * A "Constant function" -- returns the default threshold, so you can see but not alter it.
43 | *
44 | * @returns {number}
45 | * @constructor
46 | */
47 | DEFAULT_RANGE: function () {
48 | return _DEFAULT_RANGE;
49 | },
50 |
51 | /**
52 | * validates that a number >= 0 has been passed.
53 | *
54 | * @param threshold {number}
55 | * @param comment {string}
56 | */
57 | validThreshold: function (threshold, comment) {
58 | if (!comment) comment = 'threshold violation';
59 | ok(!Number.isNaN(threshold), "matrix3CloseTo: third argument must be a number");
60 | ok(threshold >= 0, 'matrix3CloseTo: threshold must be >= 0 -- is ' + threshold);
61 | },
62 |
63 | /**
64 | * compares one vector to another.
65 | * Uses the properties of the first parameter , and compares them to the second one.
66 | *
67 | * A few assumptions made:
68 | * 1) we don't care if vecB has extra properties
69 | * 2) nobody has/should add extra properties to either vecA, vecB
70 | * 3) these properties are numeric.
71 | *
72 | * @param vecA {Object|array}
73 | * @param vecB {Object|array}
74 | * @param threshold {float}
75 | * @param comment {string}
76 | */
77 | vectorCloseTo: function (vecA, vecB, threshold, comment) {
78 | try {
79 | if (!comment) comment = 'vectorCloseTo';
80 | lib.validThreshold(threshold, 'vectorCloseTo: ' + comment);
81 |
82 | ok(vecA && isObject(vecA), f('vectorCloseTo bad argument 1: %s', vecA));
83 | ok(vecB && isObject(vecB), f('vectorCloseTo bad argument 2: %s', vecB));
84 |
85 | if (_DEBUG) console.log('veca: %s', f(vecA));
86 | if (!threshold) threshold = lib.DEFAULT_RANGE();
87 | for (let dim = 0; dim < 3; dim++){
88 | lib.closeTo(vecA[dim], vecB[dim], threshold, comment, dim);
89 | }
90 | } catch (e) {
91 | console.log('vct error: %s', e, f(vecA), f(vecB));
92 | throw e;
93 | }
94 | },
95 |
96 | /**
97 | * compares one matrix to another. Same assumptions as vectorCloseTo.
98 | * While both functions are identical, this makes it easier to keep discrete tests
99 | * if vector and/or matrix definitions are changed in the future.
100 | *
101 | * @param matA {Object} : a Leap Matrix
102 | * @param matB {Object} : a Leap Matrix
103 | * @param threshold {number} a number
104 | * @param comment : {string}
105 | */
106 | matrix3CloseTo: function (matA, matB, threshold, comment) {
107 |
108 | try {
109 | if (!comment) comment = 'matrix3CloseTo';
110 | lib.validThreshold(threshold, 'matrix3CloseTo: ' + comment);
111 |
112 | if (!(typeof threshold == 'number')) throw new Error("matrix3CloseTo: third argument must be a number");
113 | if (threshold < 0) throw new Error('matrix3CloseTo: threshold must be >= 0 -- is ' + threshold);
114 |
115 | ok(matA && isObject(matA), f('vectorCloseTo bad argument 1: %s', matA));
116 | ok(matB && isObject(matB), f('vectorCloseTo bad argument 2: %s', matB));
117 |
118 | if (!threshold) threshold = lib.DEFAULT_RANGE();
119 |
120 | for (let dim = 0; dim < 9; dim++){
121 | lib.closeTo(matB[dim], matA[dim], threshold, dim + ': ' + comment, dim);
122 | }
123 | } catch (e) {
124 | console.log('mct error: %s -- %s, %s', e, f(matA), f(matB));
125 | throw e;
126 | }
127 | },
128 | closeTo: function (a, b, threshold, comment, dim) {
129 | // Inline Definition of Number.IsNaN()
130 | ok(!(typeof a === 'number' && a !== a), 'bad number a: ' + comment);
131 | ok(!(typeof b === 'number' && b !== b), 'bad number b: ' + comment);
132 | var dd = Math.abs(a - b);
133 | if (_DEBUG) console.log('dim: %s, dd: %s, threshold: %s ', dim, dd, threshold);
134 | ok(dd <= threshold, comment);
135 | }
136 | };
137 |
138 | if (typeof module == 'undefined') {
139 | window.assertUtil = lib;
140 | } else {
141 | module.exports = lib;
142 | }
143 | })(this);
--------------------------------------------------------------------------------
/test/bone.js:
--------------------------------------------------------------------------------
1 | describe('Bone', function(){
2 | describe("properties", function() {
3 | var frame = fakeActualFrame();
4 | var bone = frame.hands[0].fingers[0].bones[0];
5 |
6 | it('should have nextJoint', function() { assert.property(bone, 'nextJoint') });
7 | it('should have prevJoint', function() { assert.property(bone, 'prevJoint') });
8 | it('should have basis', function() { assert.property(bone, 'basis') });
9 | it('should have length', function() { assert.property(bone, 'length') });
10 | it('should have width', function() { assert.property(bone, 'width') });
11 | it('should have direction', function() { assert.property(bone, 'direction') });
12 | it('should have matrix', function() { assert(bone.matrix().length == 16) });
13 | it('should have finger', function() { assert.property(bone, 'finger') });
14 |
15 | it('center() should return a vec3', function(){ assert(bone.center().length == 3) });
16 |
17 |
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/test/circular_buffer.js:
--------------------------------------------------------------------------------
1 | describe('CircularBuffer', function(){
2 | describe('#push', function(){
3 | it('should allow pushing elements', function(){
4 | var buf = new Leap.CircularBuffer(10);
5 | buf.push(1)
6 | buf.push(2)
7 | buf.push(3)
8 | assert.equal(3, buf.get())
9 | assert.equal(2, buf.get(1))
10 | assert.equal(1, buf.get(2))
11 | assert.equal(undefined, buf.get(3))
12 | })
13 | })
14 |
15 | describe('overflowing', function(){
16 | it('should return elements after its overflowed', function(){
17 | var buf = new Leap.CircularBuffer(10);
18 | for (var i = 0; i != 20; i++) {
19 | buf.push(i)
20 | }
21 | assert.equal(19, buf.get())
22 | assert.equal(18, buf.get(1))
23 | assert.equal(undefined, buf.get(10))
24 | })
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/test/connection.js:
--------------------------------------------------------------------------------
1 | describe('Connection', function(){
2 | describe("class methods", function(){
3 | it('should return defaultProtocolVersion', function(){
4 | var controller = fakeController();
5 | assert(controller.connectionType.defaultProtocolVersion, 'should have default protocol version');
6 | });
7 | });
8 |
9 | describe('#new', function(){
10 | it('should return a connection that pumps events', function(done) {
11 | var controller = fakeController()
12 | var connection = controller.connection
13 | controller.on('ready', function() {
14 | connection.handleData(JSON.stringify(fakeFrame({id:123})))
15 | })
16 | controller.on('frame', function() {
17 | assert.equal(123, controller.lastFrame.id)
18 | connection.disconnect()
19 | done()
20 | })
21 | connection.connect()
22 | })
23 | })
24 |
25 | describe('#connect', function(){
26 | it('should fire a "connect" event', function(done){
27 | var controller = fakeController()
28 | var connection = controller.connection
29 | connection.on('ready', function() {
30 | connection.disconnect()
31 | done()
32 | })
33 | connection.connect()
34 | })
35 | })
36 |
37 | describe('#disconnect', function(){
38 | it('should fire a "disconnect" event', function(done){
39 | var controller = fakeController()
40 | var connection = controller.connection
41 | connection.on('disconnect', function() {
42 | assert.isUndefined(connection.socket);
43 | assert.isUndefined(connection.protocol);
44 | done();
45 | });
46 | connection.on('ready', function() {
47 | assert.isDefined(connection.socket);
48 | assert.isDefined(connection.protocol);
49 | connection.disconnect()
50 | })
51 | connection.connect()
52 | })
53 | })
54 |
55 | describe('background in protocol 4', function(){
56 | it('should send background true', function(done){
57 | var controller = fakeController({version: 4});
58 | var connection = controller.connection;
59 | connection.setBackground(true);
60 | controller.on('ready', function() {
61 | setTimeout(function() {
62 | assert.include(connection.socket.messages, JSON.stringify({"background":true}));
63 | connection.disconnect();
64 | done();
65 | }, 100);
66 | })
67 | controller.connection.connect()
68 | })
69 |
70 | it('should send background false', function(done){
71 | var controller = fakeController({version: 4});
72 | var connection = controller.connection;
73 | controller.on('ready', function() {
74 | controller.setBackground(false);
75 | setTimeout(function() {
76 | assert.include(connection.socket.messages, JSON.stringify({"background":false}));
77 | connection.disconnect();
78 | done();
79 | }, 100);
80 | })
81 | controller.connection.connect()
82 | })
83 |
84 | it('should send focused true', function(done){
85 | var controller = fakeController({version: 4});
86 | var connection = controller.connection;
87 | controller.on('ready', function() {
88 | controller.setBackground(false);
89 | setTimeout(function() {
90 | assert.include(connection.socket.messages, JSON.stringify({"focused":true}));
91 | connection.disconnect();
92 | done();
93 | }, 100);
94 | })
95 | controller.connection.connect()
96 | })
97 | })
98 |
99 |
100 | describe('HMD in protocol 6', function(){
101 |
102 | it('should send optimizeHMD true', function(done){
103 | var controller = fakeController({version: 6});
104 | var connection = controller.connection;
105 | connection.setOptimizeHMD(true);
106 | controller.on('ready', function() {
107 | setTimeout(function() {
108 | assert.include(connection.socket.messages, JSON.stringify({"optimizeHMD":true}));
109 | connection.disconnect();
110 | done();
111 | }, 100);
112 | });
113 | controller.connection.connect()
114 | });
115 |
116 | it('should send optimizeHMD false', function(done){
117 | var controller = fakeController({version: 4});
118 | var connection = controller.connection;
119 | controller.on('ready', function() {
120 | controller.setOptimizeHMD(false);
121 | setTimeout(function() {
122 | assert.include(connection.socket.messages, JSON.stringify({"optimizeHMD":false}));
123 | connection.disconnect();
124 | done();
125 | }, 100);
126 | });
127 | controller.connection.connect()
128 | })
129 |
130 | })
131 | });
132 |
--------------------------------------------------------------------------------
/test/dialog.js:
--------------------------------------------------------------------------------
1 | describe('Controller', function(){
2 |
3 | it ("should show & hide [browser-only]", function(){
4 | var dialog = new Dialog("Test Message");
5 |
6 | dialog.show();
7 | dialog.hide();
8 |
9 | assert(true);
10 | });
11 |
12 | });
13 |
--------------------------------------------------------------------------------
/test/finger.js:
--------------------------------------------------------------------------------
1 | describe('Finger', function(){
2 | describe("properties", function() {
3 |
4 | var frame = fakeActualFrame();
5 | var pointable = frame.hands[0].fingers[0];
6 |
7 | it('should have id', function() { assert.property(pointable, 'id') })
8 | it('should have tool', function() { assert.equal(false, pointable.tool) })
9 | it('should have width', function() { assert.property(pointable, 'width') })
10 | it('should be valid', function() { assert(pointable.valid) })
11 | it('should be a valid', function() { assert(pointable.finger) })
12 | it('should have an array of 5 positions', function() { assert.equal(5, pointable.positions.length) })
13 | it('should have direction', function() { assert.property(pointable, 'direction') })
14 | it('should have stabilizedTipPosition', function() { assert.property(pointable, 'stabilizedTipPosition') })
15 | it('should have tipPosition', function() { assert.property(pointable, 'tipPosition') })
16 | it('should have tipVelocity', function() { assert.property(pointable, 'tipVelocity') })
17 | it('should have touchZone', function() { assert.property(pointable, 'touchZone') })
18 | it('should have touchDistance', function() { assert.property(pointable, 'touchDistance') })
19 | it('should have timeVisible', function() { assert.property(pointable, 'timeVisible') })
20 | it('should have mcpPosition', function() { assert.property(pointable, 'mcpPosition') })
21 | it('should have pipPosition', function() { assert.property(pointable, 'pipPosition') })
22 | it('should have dipPosition', function() { assert.property(pointable, 'dipPosition') })
23 | it('should have positions', function() { assert.property(pointable, 'positions') })
24 | it('should have four bones', function() { assert(pointable.bones.length == 4) })
25 | it('should match Finger in #toString', function() { assert.match(pointable.toString(), /^Finger/); })
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/test/frame.js:
--------------------------------------------------------------------------------
1 | describe('Frame', function(){
2 | describe('#finger()', function(){
3 | it('should return a valid finger when requested', function(){
4 | var data = fakeFrame({fingers: 2})
5 | var frame = createFrame(data);
6 | assert.equal(2, frame.fingers.length);
7 | assert(frame.finger(0).valid);
8 | assert(frame.finger(1).valid);
9 | })
10 |
11 | it('should return an invalid finger when an invalid finger is requested', function(){
12 | var data = fakeFrame()
13 | var frame = createFrame(data);
14 | assert.equal(0, frame.fingers.length);
15 | assert.equal(false, frame.finger(0).valid);
16 | })
17 | })
18 |
19 | describe('#fingers', function(){
20 | it('should return a list of fingers', function(){
21 | var data = fakeFrame({fingers: 2})
22 | var frame = createFrame(data);
23 | assert.equal(2, frame.fingers.length);
24 | assert(frame.fingers[0].valid);
25 | assert(frame.fingers[1].valid);
26 | })
27 | })
28 |
29 | describe('#hand()', function(){
30 | it('should return a valid hand when requested', function(){
31 | var data = fakeFrame({hands: 1})
32 | var frame = createFrame(data);
33 | assert.equal(1, frame.hands.length);
34 | assert(frame.hand(0).valid);
35 | })
36 |
37 | it('should return an invalid hand when an invalid hand is requested', function(){
38 | var data = fakeFrame()
39 | var frame = createFrame(data);
40 | assert.equal(0, frame.hands.length);
41 | assert.equal(false, frame.hand(0).valid);
42 | })
43 | })
44 |
45 | describe('#hands', function(){
46 | it('should return a list of valid hands', function(){
47 | var data = fakeFrame({hands:1})
48 | var frame = createFrame(data);
49 | assert.equal(1, frame.hands.length);
50 | assert(frame.hands[0].valid);
51 | })
52 | })
53 |
54 | describe('#translation()', function(){
55 | it('should return the translation', function(){
56 | var data1 = fakeFrame({translation: [1, 2, 3]})
57 | var frame1 = createFrame(data1);
58 | var data2 = fakeFrame({translation: [3, 1, 5]})
59 | var frame2 = createFrame(data2);
60 | var t = frame1.translation(frame2);
61 | assert.deepEqual([-2, 1, -2], [t[0], t[1], t[2]]);
62 | })
63 | })
64 |
65 | describe('#rotationAxis()', function(){
66 | it('should return the rotationAxis', function(){
67 | var data1 = fakeFrame({rotation: [[0,1,2], [2,3,4], [2,3,4]]})
68 | var frame1 = createFrame(data1);
69 | var data2 = fakeFrame({rotation: [[0,4,5], [1,3,7], [5,4,2]]})
70 | var frame2 = createFrame(data2);
71 | var result = frame1.rotationAxis(frame2);
72 | assert.closeTo(-0.7427813, result[0], 0.0001)
73 | assert.closeTo(-0.55708, result[1], 0.0001)
74 | assert.closeTo(-0.37139, result[2], 0.0001)
75 | })
76 | })
77 |
78 | describe('#rotationAngle()', function(){
79 | it('should return the rotationAngle', function(){
80 | var data1 = fakeFrame({rotation: [[1.0,0.0,0.0], [0.0,1.0,0.0], [0.0,0.0,1.0]]})
81 | var frame1 = createFrame(data1);
82 | var data2 = fakeFrame({rotation: [[1.0,0.0,0.0], [0.0,1.0,0.0], [0.5,0.0,0.5]]})
83 | var frame2 = createFrame(data2);
84 | var result = frame1.rotationAngle(frame2);
85 | assert.closeTo(0.72273, result, 0.0001);
86 | })
87 | })
88 |
89 | describe('Invalid', function() {
90 | it('should be invalid', function() { assert(!Leap.Frame.Invalid.valid)})
91 | it('should have empty fingers', function() { assert.equal(0, Leap.Frame.Invalid.fingers.length)})
92 | it('should have empty pointables', function() { assert.equal(0, Leap.Frame.Invalid.pointables.length)})
93 | it('should return an invalid #pointable', function() { assert(!Leap.Frame.Invalid.pointable().valid)})
94 | it('should return an invalid #finger', function() { assert(!Leap.Frame.Invalid.finger().valid)})
95 | it('should return 0.0 from #rotationAngle', function() { assert.equal(0.0, Leap.Frame.Invalid.rotationAngle())})
96 | it('should return an identity matrix from #rotationMatrix', function() { assertUtil.matrix3CloseTo(Leap.mat3.create(), Leap.Frame.Invalid.rotationMatrix(), assertUtil.DEFAULT_RANGE())})
97 | it('should return a null vector from #rotationAxis', function() { assertUtil.vectorCloseTo (Leap.vec3.create(), Leap.Frame.Invalid.rotationAxis (), assertUtil.DEFAULT_RANGE())})
98 | it('should return 1.0 from #scaleFactor', function() { assert.equal(1.0, Leap.Frame.Invalid.scaleFactor())})
99 | it('should return a null vector from #translation', function() { assertUtil.vectorCloseTo(Leap.vec3.create(), Leap.Frame.Invalid.translation (), assertUtil.DEFAULT_RANGE())})
100 | it('should return a null vector from #translation', function() { assert.equal(1.0, Leap.Frame.Invalid.scaleFactor())})
101 | });
102 |
103 | describe('End to End', function(){
104 | var frame = fakeActualFrame();
105 |
106 | it('should have five pointables', function(){
107 | assert.equal(frame.pointables.length, 5, 'five pointables found');
108 | });
109 |
110 | it('should make a valid frame', function(){
111 | assert.ok(frame.valid, 'frame is valid');
112 | });
113 |
114 | frame.pointables.forEach(function (pointable) {
115 | var stp = pointable.stabilizedTipPosition;
116 | var match = undefined;
117 | frame.pointables.forEach(function(p) {
118 | if(p.stabilizedTipPosition[0] == stp[0] &&
119 | p.stabilizedTipPosition[1] == stp[1] &&
120 | p.stabilizedTipPosition[2] == stp[2]){
121 | match = p;
122 | }
123 | });
124 |
125 | assert.ok(match, 'found a match for stp ' + stp.join(','));
126 | assert.ok(match.id, 'match has an ID');
127 | var foundPointable = frame.pointable(match.id);
128 | assert.ok(foundPointable, 'found the pointable by id in frame');
129 | assert.equal(foundPointable.stabilizedTipPosition[0], stp[0], 'stp match')
130 |
131 | var hand = frame.hand(match.handId);
132 |
133 | assert.ok(hand, 'frame has pointables hand');
134 |
135 | var hand_pointable = hand.finger(match.id);
136 |
137 | assert.ok(hand_pointable, 'found finger in its hand; also , is finger');
138 | });
139 | });
140 | })
141 |
--------------------------------------------------------------------------------
/test/hand.js:
--------------------------------------------------------------------------------
1 | describe('Hand', function(){
2 | describe("an instance", function() {
3 | var data = fakeFrame({fingers: 5, hands:1})
4 | var frame = createFrame(data);
5 | var hand = frame.hands[0];
6 |
7 | describe("properties", function() {
8 | it('should have palmPosition', function() { assert(hand.palmPosition) })
9 | it('should have direction', function() { assert(hand.direction) })
10 | it('should have palmVelocity', function() { assert(hand.palmVelocity) })
11 | it('should have palmNormal', function() { assert(hand.palmNormal) })
12 | it('should have sphereCenter', function() { assert(hand.sphereCenter) })
13 | it('should have 5 pointables', function() { assert.equal(5, hand.pointables.length) })
14 | it('should have 5 fingers', function() { assert.equal(5, hand.fingers.length) })
15 | it('should have timeVisible', function() { assert(hand.timeVisible) })
16 | it('should have stabilizedPalmPosition', function() { assert(hand.stabilizedPalmPosition) })
17 | it('should have type', function() { assert(hand.type) })
18 | it('should have grabStrength', function() { assert(hand.grabStrength) })
19 | it('should have pinchStrength', function() { assert(hand.pinchStrength) })
20 | it('should have confidence', function() { assert(hand.confidence) })
21 | it('should have a thumb', function() { assert.equal(hand.thumb.type, 0) })
22 | it('should have a indexFinger', function() { assert.equal(hand.indexFinger.type, 1) })
23 | it('should have a middleFinger', function() { assert.equal(hand.middleFinger.type, 2) })
24 | it('should have a ringFinger', function() { assert.equal(hand.ringFinger.type, 3) })
25 | it('should have a pinky', function() { assert.equal(hand.pinky.type, 4) })
26 | });
27 |
28 | describe("#finger", function() {
29 | it('should return a finger by id', function() {
30 | assert.equal(1, hand.finger(1).id)
31 | })
32 |
33 |
34 | it('should return fingers ordered by id', function(){
35 | var hand = fakeActualFrame().hands[0];
36 | for (var i = 1; i < hand.fingers.length; i++){
37 | assert(hand.fingers[i].id > hand.fingers[i-1].id,
38 | "Fingers to be in sequential order. Got '" + hand.fingers[i].id + ', ' + hand.fingers[i-1].id + "'");
39 | }
40 | });
41 | })
42 |
43 | describe("#rotationAngle", function() {
44 | it('should work', function() {
45 | var frame1 = createFrame(data);
46 | frame1.hand(0)._rotation = [1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0];
47 | var frame2 = createFrame(data);
48 | frame2.hand(0)._rotation = [1.0,0.0,0.0,0.0,1.0,0.0,0.5,0.0,0.5];
49 | var result = frame2.hand(0).rotationAngle(frame1);
50 | assert.closeTo(0.72273, result, 0.0001)
51 | })
52 | })
53 | });
54 |
55 | describe('#translation()', function(){
56 | it('should return the translation', function(){
57 | var data1 = fakeFrame({handData: [fakeHand({translation: [1, 2, 3]})]})
58 | var frame1 = createFrame(data1);
59 | var data2 = fakeFrame({handData: [fakeHand({translation: [3, 1, 5]})]})
60 | var frame2 = createFrame(data2);
61 | assert.deepEqual([-2, 1, -2], frame1.hand(0).translation(frame2))
62 | })
63 | })
64 |
65 | describe('#rotationAxis()', function(){
66 | it('should return the rotationAxis', function(){
67 | var data1 = fakeFrame({handData: [fakeHand({rotation: [[0,1,2], [2,3,4], [2,3,4]]})]})
68 | var frame1 = createFrame(data1);
69 | var data2 = fakeFrame({handData: [fakeHand({rotation: [[0,4,5], [1,3,7], [5,4,2]]})]})
70 | var frame2 = createFrame(data2);
71 | var result = frame1.hand(0).rotationAxis(frame2);
72 | assertUtil.vectorCloseTo([-0.74278, -0.55708,-0.37139], result, 0.0001, 'rotation axis');
73 | })
74 |
75 | it('should return a null vector if the hand is invalid', function(){
76 | var data1 = fakeFrame({handData: [fakeHand({rotation: [[0,4,5], [1,3,7], [5,4,2]]})]})
77 | var frame1 = createFrame(data1);
78 | var data2 = fakeFrame({handData: []})
79 | var frame2 = createFrame(data2);
80 | var result = frame1.hand(0).rotationAxis(frame2);
81 | assert(!frame2.hand(0).valid);
82 | assertUtil.vectorCloseTo(result, Leap.vec3.create(), 0.0001, 'result is zero vector');
83 | });
84 | })
85 |
86 | describe('#rotationAngle()', function(){
87 | it('should return the rotationAngle', function(){
88 | var data1 = fakeFrame({handData: [fakeHand({rotation: [[1,0,0], [0,1,0], [0,0,1]]})]})
89 | var frame1 = createFrame(data1);
90 | var data2 = fakeFrame({handData: [fakeHand({rotation: [[1,0,0], [0,1,0], [0.5,0,0.5]]})]})
91 | var frame2 = createFrame(data2);
92 | var result = frame1.hand(0).rotationAngle(frame2);
93 | assert.closeTo(0.72273, result, 0.0001)
94 | })
95 | })
96 |
97 | describe('Invalid', function() {
98 | it('should be invalid', function() { assert(!Leap.Hand.Invalid.valid)})
99 | it('should have empty fingers', function() { assert.equal(0, Leap.Hand.Invalid.fingers.length)})
100 | it('should have empty pointables', function() { assert.equal(0, Leap.Hand.Invalid.pointables.length)})
101 | it('should return an invalid #pointable', function() { assert(!Leap.Hand.Invalid.pointable().valid)})
102 | it('should return an invalid #finger', function() { assert(!Leap.Hand.Invalid.finger().valid)})
103 | it('should return 0.0 from #rotationAngle', function() { assert.equal(0.0, Leap.Hand.Invalid.rotationAngle())})
104 | it('should return an identity matrix from #rotationMatrix', function() { assertUtil.matrix3CloseTo(Leap.mat3.create(), Leap.Hand.Invalid.rotationMatrix(), assertUtil.DEFAULT_RANGE())})
105 | it('should return a null vector from #rotationAxis', function() { assertUtil.vectorCloseTo(Leap.vec3.create(), Leap.Hand.Invalid.rotationAxis(), assertUtil.DEFAULT_RANGE())})
106 | it('should return 1.0 from #scaleFactor', function() { assert.equal(1.0, Leap.Hand.Invalid.scaleFactor())})
107 | it('should return a null vector from #translation', function() { assertUtil.vectorCloseTo(Leap.vec3.create(),Leap.Hand.Invalid.rotationAxis(), assertUtil.DEFAULT_RANGE())})
108 | })
109 | });
110 |
--------------------------------------------------------------------------------
/test/helpers/browser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Mocha Tests
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/test/helpers/common.js:
--------------------------------------------------------------------------------
1 | if (typeof(exports) === 'undefined') {
2 | exports = {};
3 | }
4 |
5 | if (typeof(window) !== 'undefined') {
6 | window.requestAnimationFrame = function(callback) { setTimeout(callback, 1000 / 60); };
7 | }
8 |
9 | var fingerId = 0
10 | , handId = 0
11 | , frameId =0;
12 |
13 | var fakeController = exports.fakeController = function(opts) {
14 | opts = Object.assign({frameEventName: "deviceFrame", version: 6}, opts || {});
15 | var controller = new Leap.Controller(opts);
16 | var connection = controller.connection;
17 |
18 | connection.setupSocket = function() {
19 | setTimeout(function() { connection.handleOpen() }, 1)
20 | var socket = {
21 | messages: [],
22 | send: function(message) {
23 | socket.messages.push(message);
24 | },
25 | close: function() { }
26 | };
27 | connection.on('connect', function() {
28 | connection.handleData(JSON.stringify({version: opts.version}))
29 | });
30 | return socket;
31 | }
32 | return controller;
33 | }
34 |
35 | var fakePluginFactory = exports.fakePluginFactory = function (returning) {
36 | returning || (returning = {
37 | frame: function (frame) {}
38 | })
39 | return function(){
40 | return returning
41 | }
42 | }
43 |
44 | var createFrame = exports.createFrame = function(data) {
45 | return Leap.JSONProtocol(5)(data);
46 | }
47 |
48 | function times(n, callback) {
49 | var returnArray = [];
50 | for (var i = 0; i < n; i++) {
51 | returnArray.push(callback(i));
52 | }
53 | return returnArray;
54 | }
55 |
56 | var fakeFrame = exports.fakeFrame = function(opts) {
57 | if (opts === undefined) opts = {};
58 |
59 | handId = 0
60 | fingerId = 0
61 |
62 | var fingers = opts.pointableData || times((opts.fingers || 0), function(n) { return fakeFinger(n) });
63 |
64 | var frame = {
65 | id: opts.id || ++frameId,
66 | valid: true,
67 | timestamp: frameId,
68 | fingers: fingers,
69 | pointables: fingers.slice(),
70 | hands: opts.handData || times((opts.hands || 0), function() { return fakeHand() }),
71 | r: opts.rotation || [[0,1,2], [2,3,4], [2,3,4]],
72 | t: opts.translation || [1, 2, 3],
73 | interactionBox: {center: [1,2,3], size: [1,2,3]},
74 | currentFrameRate: 10
75 | };
76 | for (var i = 0; i != frame.pointables.length; i++) {
77 | (frame.fingers).push(frame.pointables[i]);
78 | }
79 |
80 | return frame;
81 | };
82 |
83 | var fakeHand = exports.fakeHand = function(opts) {
84 | if (opts === undefined) opts = {};
85 | handId++
86 | return {
87 | id: handId - 1,
88 | valid: true,
89 | palmPosition: [1,2,3],
90 | direction: [1,2,3],
91 | palmVelocity: [1,2,3],
92 | palmNormal: [1,2,3],
93 | sphereCenter:[1,2,3],
94 | r: (opts && opts.rotation) || [[0,1,2], [2,3,4], [2,3,4]],
95 | t: (opts && opts.translation) || [1, 2, 3],
96 | timeVisible: 10,
97 | stabilizedPalmPosition: [1,2,3],
98 | type: 'left',
99 | grabStrength: 0.5,
100 | pinchStrength: 0.5,
101 | confidence: 0.74094,
102 | fingers: times((opts.hands || 5), function() { return fakeFinger() })
103 | }
104 | }
105 |
106 | var fakeFinger = exports.fakeFinger = function (type) {
107 | fingerId++
108 | var finger = {
109 | id: fingerId - 1,
110 | handId: 0,
111 | length: 5,
112 | tool: false,
113 | width: 5,
114 | direction: [10, 10, 10],
115 | tipPosition: [10, 10, 10],
116 | stabilizedTipPosition: [10, 10, 10],
117 | tipVelocity: [10, 10, 10],
118 | touchZone: "none",
119 | touchDistance: 5,
120 | timeVisible: 10
121 | };
122 | finger.type = type;
123 | finger.carpPosition = [10.1, 10.1, 10.1];
124 | finger.mcpPosition = [11, 11, 11];
125 | finger.pipPosition = [12, 12, 12];
126 | finger.dipPosition = [13, 13, 13];
127 | finger.btipPosition = [14, 14, 14];
128 | finger.extended = true;
129 | finger.bases = [
130 | [
131 | [
132 | 0.346327,
133 | 0.789873,
134 | 0.50612
135 | ],
136 | [
137 | -0.937462,
138 | 0.271288,
139 | 0.218102
140 | ],
141 | [
142 | 0.0349685,
143 | -0.550003,
144 | 0.83443
145 | ]
146 | ],
147 | [
148 | [
149 | 0.330638,
150 | 0.889895,
151 | 0.31427
152 | ],
153 | [
154 | -0.908056,
155 | 0.209249,
156 | 0.362834
157 | ],
158 | [
159 | 0.257124,
160 | -0.405342,
161 | 0.87726
162 | ]
163 | ],
164 | [
165 | [
166 | 0.330638,
167 | 0.889895,
168 | 0.31427
169 | ],
170 | [
171 | -0.856128,
172 | 0.14269,
173 | 0.496673
174 | ],
175 | [
176 | 0.397144,
177 | -0.433274,
178 | 0.809043
179 | ]
180 | ],
181 | [
182 | [
183 | 0.330638,
184 | 0.889895,
185 | 0.31427
186 | ],
187 | [
188 | -0.782763,
189 | 0.0725576,
190 | 0.618075
191 | ],
192 | [
193 | 0.527219,
194 | -0.450358,
195 | 0.720567
196 | ]
197 | ]
198 | ];
199 | return finger;
200 | }
201 |
202 |
203 | var fakeActualFrame = exports.fakeActualFrame = function(){
204 | // var frameDump = '{"currentFrameRate":110.046,"hands":[{"confidence":0.789415,"direction":[0.158462,0.627121,-0.762633],"grabStrength":0,"id":410,"palmNormal":[0.116439,-0.778872,-0.61628],"palmPosition":[60.9126,76.8613,36.0687],"palmVelocity":[3.25498,-2.77319,3.25398],"pinchStrength":0,"r":[[0.113258,-0.577876,0.808227],[-0.0837691,0.805011,0.587315],[-0.990028,-0.134223,0.0427661]],"s":1.15718,"sphereCenter":[54.5696,91.5943,0.920909],"sphereRadius":68.9144,"stabilizedPalmPosition":[58.3818,81.7843,32.9256],"t":[57.5602,63.7134,52.0596],"timeVisible":56257.5,"type":"right"}],"id":197377,"interactionBox":{"center":[0,200,0],"size":[235.247,235.247,147.751]},"pointables":[{"dipPosition":[-11.5826,65.0593,14.9045],"direction":[-0.545695,0.302478,-0.781489],"extended":true,"handId":410,"id":4100,"length":51.57,"mcpPosition":[28.3806,43.8226,70.3412],"pipPosition":[5.64499,55.5101,39.5761],"stabilizedTipPosition":[-22.9433,71.2217,-1.08015],"timeVisible":56257.5,"tipPosition":[-22.0341,71.31,-0.960369],"tipVelocity":[5.04347,3.46689,1.45025],"tool":false,"touchDistance":-0.177438,"touchZone":"touching","type":0,"width":18.75},{"dipPosition":[48.808,97.8943,-42.5311],"direction":[0.189785,-0.136463,-0.972296],"extended":true,"handId":410,"id":4101,"length":63.65,"mcpPosition":[38.0774,87.8413,21.1445],"pipPosition":[43.9495,101.388,-17.6403],"stabilizedTipPosition":[49.5451,90.7304,-59.3122],"timeVisible":56257.5,"tipPosition":[52.2109,88.0403,-56.3373],"tipVelocity":[9.16327,10.2824,4.49866],"tool":false,"touchDistance":0.209659,"touchZone":"hovering","type":1,"width":17.91},{"dipPosition":[82.27,98.519,-43.1687],"direction":[0.240974,-0.22168,-0.944875],"extended":true,"handId":410,"id":4102,"length":66.045,"mcpPosition":[62.4541,91.6378,22.3131],"pipPosition":[75.9251,104.356,-18.2901],"stabilizedTipPosition":[82.458,91.7338,-59.7407],"timeVisible":56257.5,"tipPosition":[84.3069,86.8553,-55.9189],"tipVelocity":[1.8102,-25.5031,14.1264],"tool":false,"touchDistance":0.210768,"touchZone":"hovering","type":2,"width":17.59},{"dipPosition":[104.219,100.709,-31.6167],"direction":[0.287859,-0.0803306,-0.954298],"extended":true,"handId":410,"id":4104,"length":63.635,"mcpPosition":[82.2655,89.5215,29.2434],"pipPosition":[96.8355,102.77,-7.13894],"stabilizedTipPosition":[104.576,92.5282,-48.7994],"timeVisible":56257.5,"tipPosition":[107.239,92.6262,-46.6113],"tipVelocity":[12.115,-4.25882,7.09828],"tool":false,"touchDistance":0.167466,"touchZone":"hovering","type":3,"width":16.738},{"dipPosition":[126.282,87.2965,-5.24043],"direction":[0.464892,-0.0265298,-0.88497],"extended":true,"handId":410,"id":4103,"length":50.07,"mcpPosition":[99.059,79.3988,35.2855],"pipPosition":[117.863,87.7769,10.7864],"stabilizedTipPosition":[129.399,82.0949,-21.6027],"timeVisible":56257.5,"tipPosition":[130.909,82.3187,-19.6813],"tipVelocity":[12.3715,7.4338,7.42059],"tool":false,"touchDistance":-0.0523789,"touchZone":"touching","type":4,"width":14.868}],"r":[[0.113258,-0.577876,0.808227],[-0.0837691,0.805011,0.587315],[-0.990028,-0.134223,0.0427661]],"s":1.15718,"t":[57.5602,63.7134,52.0596],"timestamp":56257460689}';
205 | var frameDump = '{ "currentFrameRate": 110.747, "hands": [ { "confidence": 1, "direction": [ 0.490721, 0.425143, -0.760557 ], "grabStrength": 0, "id": 1, "palmNormal": [ 0.141838, -0.900216, -0.411695 ], "palmPosition": [ 47.033, 113.621, 102.153 ], "palmVelocity": [ -3.30303, 5.60355, 1.47788 ], "palmWidth": 92.4988, "pinchStrength": 0, "r": [ [ 0.584034, 0.313747, -0.748643 ], [ 0.0202196, 0.916373, 0.399814 ], [ 0.811477, -0.248642, 0.528849 ] ], "s": 1.42457, "sphereCenter": [ 64.9106, 183.062, 85.421 ], "sphereRadius": 81.7923, "stabilizedPalmPosition": [ 43.7677, 112.357, 87.4045 ], "t": [ 47.0678, 113.56, 102.138 ], "timeVisible": 810.321, "type": "right" } ], "id": 13319, "interactionBox": { "center": [ 0, 200, 0 ], "size": [ 235.247, 235.247, 147.751 ] }, "pointables": [ { "bases": [ [ [ 0.314831, 0.85025, 0.421848 ], [ -0.94682, 0.312444, 0.0768821 ], [ -0.0664349, -0.423619, 0.903401 ] ], [ [ 0.321292, 0.905114, 0.278459 ], [ -0.916178, 0.222714, 0.333191 ], [ 0.239559, -0.362169, 0.900802 ] ], [ [ 0.321292, 0.905114, 0.278459 ], [ -0.842096, 0.138569, 0.521222 ], [ 0.43318, -0.401954, 0.806715 ] ], [ [ 0.321292, 0.905114, 0.278459 ], [ -0.727658, 0.0477828, 0.684274 ], [ 0.606041, -0.422475, 0.673966 ] ] ], "btipPosition": [ -40.5204, 127.235, 43.4704 ], "carpPosition": [ 1.06923, 84.8729, 133.178 ], "dipPosition": [ -26.1017, 117.183, 59.5051 ], "direction": [ -0.43318, 0.401954, -0.806715 ], "extended": true, "handId": 1, "id": 10, "length": 52.9806, "mcpPosition": [ 1.06923, 84.8729, 133.178 ], "pipPosition": [ -11.0873, 103.251, 87.4667 ], "stabilizedTipPosition": [ -42.7656, 121.635, 37.8675 ], "timeVisible": 810.321, "tipPosition": [ -37.2041, 124.923, 47.1584 ], "tipVelocity": [ -4.03195, 5.29145, 3.86774 ], "tool": false, "touchDistance": 0.332609, "touchZone": "hovering", "type": 0, "width": 20.5858 }, { "bases": [ [ [ 0.929883, -0.0214331, 0.36723 ], [ -0.0899203, 0.954772, 0.283416 ], [ -0.356696, -0.296565, 0.885899 ] ], [ [ 0.906197, -0.0396238, 0.420996 ], [ -0.207543, 0.825755, 0.524457 ], [ -0.368421, -0.562636, 0.740073 ] ], [ [ 0.906197, -0.0396238, 0.420996 ], [ -0.0713341, 0.967006, 0.244561 ], [ -0.416796, -0.251652, 0.873471 ] ], [ [ 0.906197, -0.0396238, 0.420996 ], [ 0.0732099, 0.995267, -0.063911 ], [ -0.416471, 0.088737, 0.904808 ] ] ], "btipPosition": [ 70.8792, 158.015, 1.44856 ], "carpPosition": [ 10.6366, 106.62, 137.205 ], "dipPosition": [ 63.6455, 159.556, 17.1641 ], "direction": [ 0.416796, 0.251652, -0.873471 ], "extended": true, "handId": 1, "id": 11, "length": 59.7826, "mcpPosition": [ 37.3137, 128.8, 70.9488 ], "pipPosition": [ 53.4044, 153.373, 38.6263 ], "stabilizedTipPosition": [ 65.6654, 153.213, -14.0434 ], "timeVisible": 810.321, "tipPosition": [ 69.2154, 158.369, 5.06314 ], "tipVelocity": [ -4.20065, 9.68335, 2.59506 ], "tool": false, "touchDistance": 0.3253, "touchZone": "hovering", "type": 1, "width": 19.6635 }, { "bases": [ [ [ 0.874756, -0.223752, 0.42981 ], [ 0.0607727, 0.93066, 0.360802 ], [ -0.480737, -0.289493, 0.827699 ] ], [ [ 0.784201, -0.268147, 0.559576 ], [ -0.109474, 0.82787, 0.550133 ], [ -0.610773, -0.492674, 0.619862 ] ], [ [ 0.784201, -0.268147, 0.559576 ], [ 0.0996896, 0.944536, 0.312912 ], [ -0.612446, -0.189602, 0.767438 ] ], [ [ 0.784201, -0.268147, 0.559576 ], [ 0.297525, 0.95387, 0.0401336 ], [ -0.544524, 0.135015, 0.827807 ] ] ], "btipPosition": [ 114.34, 155.759, 14.6355 ], "carpPosition": [ 22.2091, 108.184, 141.712 ], "dipPosition": [ 103.938, 158.338, 30.4496 ], "direction": [ 0.612446, 0.189602, -0.767438 ], "extended": true, "handId": 1, "id": 12, "length": 68.1175, "mcpPosition": [ 56.3054, 128.716, 83.0076 ], "pipPosition": [ 86.233, 152.857, 52.6346 ], "stabilizedTipPosition": [ 107.84, 151.212, 1.45647 ], "timeVisible": 810.321, "tipPosition": [ 111.947, 156.352, 18.2727 ], "tipVelocity": [ -4.66062, 9.58164, 2.1671 ], "tool": false, "touchDistance": 0.333287, "touchZone": "hovering", "type": 2, "width": 19.3122 }, { "bases": [ [ [ 0.78739, -0.333431, 0.518498 ], [ 0.107759, 0.902593, 0.416789 ], [ -0.606963, -0.272303, 0.746624 ] ], [ [ 0.767862, -0.341851, 0.54178 ], [ -0.0158866, 0.835297, 0.549569 ], [ -0.640418, -0.4306, 0.635962 ] ], [ [ 0.767862, -0.341851, 0.54178 ], [ 0.147855, 0.917457, 0.36934 ], [ -0.623319, -0.203498, 0.755024 ] ], [ [ 0.767862, -0.341851, 0.54178 ], [ 0.301831, 0.939024, 0.164719 ], [ -0.565054, 0.0370446, 0.824222 ] ] ], "btipPosition": [ 129.561, 148.422, 33.3462 ], "carpPosition": [ 33.5367, 106.497, 146.693 ], "dipPosition": [ 118.829, 149.125, 49.0013 ], "direction": [ 0.623319, 0.203498, -0.755024 ], "extended": true, "handId": 1, "id": 13, "length": 65.4968, "mcpPosition": [ 72.1873, 123.837, 99.1495 ], "pipPosition": [ 101.275, 143.395, 70.2638 ], "stabilizedTipPosition": [ 123.254, 144.207, 20.7744 ], "timeVisible": 810.321, "tipPosition": [ 127.093, 148.584, 36.9468 ], "tipVelocity": [ -5.69924, 8.89887, 2.42685 ], "tool": false, "touchDistance": 0.331146, "touchZone": "hovering", "type": 3, "width": 18.3768 }, { "bases": [ [ [ 0.683148, -0.507227, 0.525386 ], [ 0.174865, 0.812118, 0.556675 ], [ -0.709035, -0.288419, 0.643493 ] ], [ [ 0.513798, -0.557566, 0.652022 ], [ 0.044047, 0.776152, 0.629005 ], [ -0.85678, -0.294462, 0.423345 ] ], [ [ 0.513798, -0.557566, 0.652022 ], [ 0.251492, 0.824509, 0.506888 ], [ -0.820222, -0.0964597, 0.563855 ] ], [ [ 0.513798, -0.557566, 0.652022 ], [ 0.443777, 0.823161, 0.354214 ], [ -0.734217, 0.107357, 0.670373 ] ] ], "btipPosition": [ 145.6, 125.131, 75.3324 ], "carpPosition": [ 43.833, 97.5082, 151.439 ], "dipPosition": [ 132.734, 127.012, 87.079 ], "direction": [ 0.820222, 0.0964597, -0.563855 ], "extended": true, "handId": 1, "id": 14, "length": 51.3483, "mcpPosition": [ 85.6283, 114.51, 113.508 ], "pipPosition": [ 116.426, 125.094, 98.2902 ], "stabilizedTipPosition": [ 138.574, 119.388, 60.1701 ], "timeVisible": 810.321, "tipPosition": [ 142.641, 125.564, 78.0341 ], "tipVelocity": [ -5.60761, 10.7884, 0.949619 ], "tool": false, "touchDistance": 0.331132, "touchZone": "hovering", "type": 4, "width": 16.3237 } ], "r": [ [ 0.584034, 0.313747, -0.748643 ], [ 0.0202196, 0.916373, 0.399814 ], [ 0.811477, -0.248642, 0.528849 ] ], "s": 1.42457, "t": [ 47.0678, 113.56, 102.138 ], "timestamp": 810321011 }';
206 | return new Leap.Frame(JSON.parse(frameDump));
207 | }
--------------------------------------------------------------------------------
/test/helpers/node.js:
--------------------------------------------------------------------------------
1 | assert = require('chai').assert;
2 |
3 | Leap = require('../../lib');
4 |
5 | var common = require('./common');
6 | assertUtil = require('./../assertUtil');
7 |
8 | fakeHand = common.fakeHand;
9 | fakeFrame = common.fakeFrame;
10 | fakeActualFrame = common.fakeActualFrame;
11 | fakeFinger = common.fakeFinger;
12 | fakeController = common.fakeController;
13 | createFrame = common.createFrame;
14 | fakePluginFactory = common.fakePluginFactory;
15 |
--------------------------------------------------------------------------------
/test/interaction_box.js:
--------------------------------------------------------------------------------
1 | describe('InteractionBox', function(){
2 | describe('#new', function(){
3 | it('should return a new interaction box', function() {
4 | var box = new Leap.InteractionBox({center:[2,3,4], size:[4,5,6]});
5 | assert.equal(4, box.width);
6 | assert.equal(5, box.height);
7 | assert.equal(6, box.depth);
8 | })
9 | })
10 |
11 | describe('#normalizePoint', function(){
12 | it('should normalize a point', function() {
13 | var box = new Leap.InteractionBox({center:[2,3,4], size:[4,5,6]});
14 | var point = box.normalizePoint([3,4,5])
15 | assert.closeTo(0.75, point[0], 0.1);
16 | assert.closeTo(0.70, point[1], 0.1);
17 | assert.closeTo(0.66, point[2], 0.1);
18 | })
19 |
20 | it('should normalize a point but clamp', function() {
21 | var box = new Leap.InteractionBox({center:[2,3,4], size:[4,5,6]});
22 | var point = box.normalizePoint([10,-20,20], true)
23 | assert.equal(1, point[0]);
24 | assert.equal(0, point[1]);
25 | assert.equal(1, point[2]);
26 | })
27 | })
28 |
29 | describe('#denormalizePoint', function(){
30 | it('should denormalize a point', function() {
31 | var box = new Leap.InteractionBox({center:[2,3,4], size:[4,5,6]});
32 | var point = box.denormalizePoint([0.75,0.70,0.66])
33 | assert.closeTo(3, point[0], 0.1);
34 | assert.closeTo(4, point[1], 0.1);
35 | assert.closeTo(5, point[2], 0.1);
36 | })
37 |
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/test/lib/protocol.coffee:
--------------------------------------------------------------------------------
1 | angular.
2 | module('ProtocolTest', []).
3 | controller 'ProtocolController', ($scope)->
4 | $scope.protocol = 4
5 | $scope.script = "//js.leapmotion.com/leap-0.4.1.min.js"
6 | $scope.scripts = [
7 | "//js.leapmotion.com/0.2.0-beta1/leap.min.js"
8 | "//js.leapmotion.com/0.2.0-beta2/leap.min.js"
9 | "//js.leapmotion.com/0.2.0-beta3/leap.min.js"
10 | "//js.leapmotion.com/0.2.0-beta4/leap.min.js"
11 | "//js.leapmotion.com/0.2.0-beta5/leap.min.js"
12 | "//js.leapmotion.com/0.2.0-beta6/leap.min.js"
13 | "//js.leapmotion.com/0.2.0/leap.min.js"
14 | "//js.leapmotion.com/0.2.1/leap.min.js"
15 | "//js.leapmotion.com/0.2.2/leap.min.js"
16 | "//js.leapmotion.com/0.3.0-beta1/leap.min.js"
17 | "//js.leapmotion.com/0.3.0-beta2/leap.min.js"
18 | "//js.leapmotion.com/0.3.0-beta3/leap.min.js"
19 | "//js.leapmotion.com/0.3.0/leap.min.js"
20 | "//js.leapmotion.com/leap-0.4.0.min.js"
21 | "//js.leapmotion.com/leap-0.4.1.min.js"
22 | ]
23 | $scope.log = []
24 |
25 | $scope.safeApply = (fn) ->
26 | phase = this.$root.$$phase;
27 | if(phase == '$apply' || phase == '$digest')
28 | fn() if fn && (typeof(fn) == 'function')
29 | else
30 | this.$apply(fn)
31 |
32 | log = (message, options = {})->
33 | $scope.log.unshift {
34 | message: message
35 | date: Date.now()
36 | class: options.class
37 | }
38 | $scope.safeApply()
39 |
40 |
41 | $scope.$watch 'protocol', (newVal, oldVal)->
42 | reconnect() if window.Leap
43 |
44 |
45 | $scope.saveCustomScriptURL = ->
46 | console.log 'save custom url'
47 | $scope.showCustomScriptInput = false
48 | if $scope.customScriptURL.length > 0
49 | $scope.scripts.push $scope.customScriptURL
50 | $scope.script = $scope.customScriptURL
51 |
52 | $scope.$watch 'script', (newVal, oldVal)->
53 | if newVal == 'other'
54 | $scope.showCustomScriptInput = true
55 | return
56 |
57 |
58 | script = document.createElement('script')
59 | document.body.appendChild script
60 | script.type = 'text/javascript'
61 | script.src = newVal
62 | script.onload = reconnect
63 | script.onerror = (e)->
64 | log("Error loading #{newVal}", class: 'italic')
65 |
66 | controller = undefined
67 |
68 | reconnect = ->
69 | if controller
70 | controller.disconnect()
71 | # for whatever reason, blur and focus events still trigger after d/c
72 | controller.removeAllListeners('blur')
73 | controller.removeAllListeners('focus')
74 | controller = null
75 |
76 | log "connected #{$scope.script}, protocol v#{$scope.protocol}", {class: 'italic'}
77 | window.controller = controller = new Leap.Controller()
78 | controller.connection.opts.requestProtocolVersion = $scope.protocol
79 | controller.on 'connect', () ->
80 | log('connect')
81 |
82 | controller.on 'deviceConnected', () ->
83 | log('deviceConnected')
84 |
85 | controller.on 'deviceDisconnected', () ->
86 | log('deviceDisconnected')
87 |
88 | controller.on 'ready', () ->
89 | log('ready')
90 |
91 | controller.on 'focus', () ->
92 | log('focus')
93 |
94 | controller.on 'blur', () ->
95 | log('blur')
96 |
97 | controller.on 'deviceStreaming', (e) ->
98 | log("deviceStreaming")
99 |
100 | controller.on 'streamingStarted', (e) ->
101 | log("streamingStarted")
102 |
103 | controller.on 'streamingStopped', (e) ->
104 | log("streamingStopped")
105 |
106 | controller.on 'deviceStopped', (e) ->
107 | log("deviceStopped")
108 |
109 | controller.on 'deviceAttached', (e) ->
110 | log("deviceAttached")
111 |
112 | controller.on 'deviceRemoved', (e) ->
113 | log("deviceRemoved")
114 |
115 | controller.on 'frame', (frame) ->
116 | $scope.frameId = frame.id
117 | $scope.safeApply()
118 |
119 | controller.connect()
120 |
121 |
122 | window.onerror = (message, url, linenumber)->
123 | # this will be unhelpful for remote scripts due to CORS.
124 | # See http://blog.meldium.com/home/2013/9/30/so-youre-thinking-of-tracking-your-js-errors
125 | log(message, class: 'italic')
126 |
--------------------------------------------------------------------------------
/test/lib/protocol.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.6.3
2 | (function() {
3 | angular.module('ProtocolTest', []).controller('ProtocolController', function($scope) {
4 | var controller, log, reconnect;
5 | $scope.protocol = 4;
6 | $scope.script = "//js.leapmotion.com/leap-0.4.1.min.js";
7 | $scope.scripts = ["//js.leapmotion.com/0.2.0-beta1/leap.min.js", "//js.leapmotion.com/0.2.0-beta2/leap.min.js", "//js.leapmotion.com/0.2.0-beta3/leap.min.js", "//js.leapmotion.com/0.2.0-beta4/leap.min.js", "//js.leapmotion.com/0.2.0-beta5/leap.min.js", "//js.leapmotion.com/0.2.0-beta6/leap.min.js", "//js.leapmotion.com/0.2.0/leap.min.js", "//js.leapmotion.com/0.2.1/leap.min.js", "//js.leapmotion.com/0.2.2/leap.min.js", "//js.leapmotion.com/0.3.0-beta1/leap.min.js", "//js.leapmotion.com/0.3.0-beta2/leap.min.js", "//js.leapmotion.com/0.3.0-beta3/leap.min.js", "//js.leapmotion.com/0.3.0/leap.min.js", "//js.leapmotion.com/leap-0.4.0.min.js", "//js.leapmotion.com/leap-0.4.1.min.js"];
8 | $scope.log = [];
9 | $scope.safeApply = function(fn) {
10 | var phase;
11 | phase = this.$root.$$phase;
12 | if (phase === '$apply' || phase === '$digest') {
13 | if (fn && (typeof fn === 'function')) {
14 | return fn();
15 | }
16 | } else {
17 | return this.$apply(fn);
18 | }
19 | };
20 | log = function(message, options) {
21 | if (options == null) {
22 | options = {};
23 | }
24 | $scope.log.unshift({
25 | message: message,
26 | date: Date.now(),
27 | "class": options["class"]
28 | });
29 | return $scope.safeApply();
30 | };
31 | $scope.$watch('protocol', function(newVal, oldVal) {
32 | if (window.Leap) {
33 | return reconnect();
34 | }
35 | });
36 | $scope.saveCustomScriptURL = function() {
37 | console.log('save custom url');
38 | $scope.showCustomScriptInput = false;
39 | if ($scope.customScriptURL.length > 0) {
40 | $scope.scripts.push($scope.customScriptURL);
41 | return $scope.script = $scope.customScriptURL;
42 | }
43 | };
44 | $scope.$watch('script', function(newVal, oldVal) {
45 | var script;
46 | if (newVal === 'other') {
47 | $scope.showCustomScriptInput = true;
48 | return;
49 | }
50 | script = document.createElement('script');
51 | document.body.appendChild(script);
52 | script.type = 'text/javascript';
53 | script.src = newVal;
54 | script.onload = reconnect;
55 | return script.onerror = function(e) {
56 | return log("Error loading " + newVal, {
57 | "class": 'italic'
58 | });
59 | };
60 | });
61 | controller = void 0;
62 | reconnect = function() {
63 | if (controller) {
64 | controller.disconnect();
65 | controller.removeAllListeners('blur');
66 | controller.removeAllListeners('focus');
67 | controller = null;
68 | }
69 | log("connected " + $scope.script + ", protocol v" + $scope.protocol, {
70 | "class": 'italic'
71 | });
72 | window.controller = controller = new Leap.Controller();
73 | controller.connection.opts.requestProtocolVersion = $scope.protocol;
74 | controller.on('connect', function() {
75 | return log('connect');
76 | });
77 | controller.on('deviceConnected', function() {
78 | return log('deviceConnected');
79 | });
80 | controller.on('deviceDisconnected', function() {
81 | return log('deviceDisconnected');
82 | });
83 | controller.on('ready', function() {
84 | return log('ready');
85 | });
86 | controller.on('focus', function() {
87 | return log('focus');
88 | });
89 | controller.on('blur', function() {
90 | return log('blur');
91 | });
92 | controller.on('deviceStreaming', function(e) {
93 | return log("deviceStreaming");
94 | });
95 | controller.on('streamingStarted', function(e) {
96 | return log("streamingStarted");
97 | });
98 | controller.on('streamingStopped', function(e) {
99 | return log("streamingStopped");
100 | });
101 | controller.on('deviceStopped', function(e) {
102 | return log("deviceStopped");
103 | });
104 | controller.on('deviceAttached', function(e) {
105 | return log("deviceAttached");
106 | });
107 | controller.on('deviceRemoved', function(e) {
108 | return log("deviceRemoved");
109 | });
110 | controller.on('frame', function(frame) {
111 | $scope.frameId = frame.id;
112 | return $scope.safeApply();
113 | });
114 | return controller.connect();
115 | };
116 | return window.onerror = function(message, url, linenumber) {
117 | return log(message, {
118 | "class": 'italic'
119 | });
120 | };
121 | });
122 |
123 | }).call(this);
124 |
--------------------------------------------------------------------------------
/test/pointable.js:
--------------------------------------------------------------------------------
1 | describe('Pointable', function(){
2 | describe("properties", function() {
3 | var data = fakeFrame({fingers: 5, hands:1})
4 | var frame = createFrame(data);
5 | var pointable = frame.hands[0].pointables[0];
6 |
7 | it('should have id', function() { assert.property(pointable, 'id') })
8 | it('should have tool', function() { assert.equal(false, pointable.tool) })
9 | it('should have width', function() { assert.property(pointable, 'width') })
10 | it('should be valid', function() { assert(pointable.valid) })
11 | it('should have direction', function() { assert.property(pointable, 'direction') })
12 | it('should have stabilizedTipPosition', function() { assert.property(pointable, 'stabilizedTipPosition') })
13 | it('should have tipPosition', function() { assert.property(pointable, 'tipPosition') })
14 | it('should have tipVelocity', function() { assert.property(pointable, 'tipVelocity') })
15 | it('should have touchZone', function() { assert.property(pointable, 'touchZone') })
16 | it('should have touchDistance', function() { assert.property(pointable, 'touchDistance') })
17 | it('should have timeVisible', function() { assert.property(pointable, 'timeVisible') })
18 | //it('should not have mcpPosition', function() { assert.notProperty(pointable, 'mcpPosition') })
19 | it('should match Pointable in #toString', function() { assert.match(pointable.toString(), /^Finger/); })
20 | it('should respond to hand()', function() { assert.equal(pointable.hand(), frame.hands[0]) })
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/test/protocol.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
29 |
30 | {{script}} v{{protocol}}
31 | frame: {{frameId}}
32 |
33 |
34 |
35 | {{entry.date | date:'medium'}} - {{entry.message}}
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------