├── .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 | [![LeapJS Logo](https://cloud.githubusercontent.com/assets/407497/2652512/eedfb992-bfae-11e3-8323-f202845e3fd8.png)](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 | [![Build Status](https://travis-ci.org/leapmotion/leapjs.svg)](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 | [![Analytics](https://ga-beacon.appspot.com/UA-31536531-10/LeapJS/README.md?pixel)](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 | 283 | 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 | 90 | 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 | 11 | 12 |
13 |     
14 |
15 | 16 | -------------------------------------------------------------------------------- /examples/optimizeHMDScreentop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | HMD/Screentop Optimization - Leap 4 | 5 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 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 | "" + 101 | ""; 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 | * ![Palm Vectors](images/Leap_Palm_Vectors.png) 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 | * ![Hand Ball](images/Leap_Hand_Ball.png) 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 | * ![Interaction Box](images/Leap_InteractionBox.png) 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 | * ![Finger](images/Leap_Finger_Model.png) 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 | * ![Touch Distance](images/Leap_Touch_Plane.png) 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 |
17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |

{{script}} v{{protocol}}

31 | frame: {{frameId}} 32 | 33 | 38 | 39 | 40 | --------------------------------------------------------------------------------