├── .coveralls.yml ├── .eslintrc.json ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .gitmodules ├── .ignore ├── .jsdocrc.json ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── index.js └── index.js.map ├── docs ├── ReturnTypes.html ├── core_index.js.html ├── core_src_Gesture.js.html ├── core_src_Input.js.html ├── core_src_Point2D.js.html ├── core_src_PointerData.js.html ├── core_src_Region.js.html ├── core_src_Smoothable.js.html ├── core_src_State.js.html ├── core_src_constants.js.html ├── core_src_utils.js.html ├── fonts │ ├── Montserrat │ │ ├── Montserrat-Bold.eot │ │ ├── Montserrat-Bold.ttf │ │ ├── Montserrat-Bold.woff │ │ ├── Montserrat-Bold.woff2 │ │ ├── Montserrat-Regular.eot │ │ ├── Montserrat-Regular.ttf │ │ ├── Montserrat-Regular.woff │ │ └── Montserrat-Regular.woff2 │ └── Source-Sans-Pro │ │ ├── sourcesanspro-light-webfont.eot │ │ ├── sourcesanspro-light-webfont.svg │ │ ├── sourcesanspro-light-webfont.ttf │ │ ├── sourcesanspro-light-webfont.woff │ │ ├── sourcesanspro-light-webfont.woff2 │ │ ├── sourcesanspro-regular-webfont.eot │ │ ├── sourcesanspro-regular-webfont.svg │ │ ├── sourcesanspro-regular-webfont.ttf │ │ ├── sourcesanspro-regular-webfont.woff │ │ └── sourcesanspro-regular-webfont.woff2 ├── index.html ├── index.js.html ├── scripts │ ├── collapse.js │ ├── commonNav.js │ ├── linenumber.js │ ├── nav.js │ ├── polyfill.js │ ├── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js │ └── search.js ├── src_Pan.js.html ├── src_Pinch.js.html ├── src_Pivotable.js.html ├── src_Press.js.html ├── src_Pull.js.html ├── src_Rotate.js.html ├── src_Swipe.js.html ├── src_Swivel.js.html ├── src_Tap.js.html ├── src_Track.js.html ├── styles │ ├── jsdoc.css │ └── prettify.css ├── westures-core.Gesture.html ├── westures-core.Input.html ├── westures-core.Point2D.html ├── westures-core.PointerData.html ├── westures-core.Region.html ├── westures-core.Smoothable.html ├── westures-core.State.html ├── westures-core.html ├── westures.Pan.html ├── westures.Pinch.html ├── westures.Pivotable.html ├── westures.Press.html ├── westures.Pull.html ├── westures.Rotate.html ├── westures.Swipe.html ├── westures.Swivel.html ├── westures.Tap.html ├── westures.Track.html └── westures.html ├── index.js ├── package-lock.json ├── package.json ├── src ├── Pan.js ├── Pinch.js ├── Pivotable.js ├── Press.js ├── Pull.js ├── Rotate.js ├── Swipe.js ├── Swivel.js ├── Tap.js └── Track.js └── test ├── Pan.test.js ├── Pinch.test.js ├── Pivotable.test.js ├── Press.test.js ├── Pull.test.js ├── Rotate.test.js ├── Swipe.test.js ├── Swivel.test.js ├── Tap.test.js ├── Track.test.js └── Westures.test.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: 1SMnpKOVOGBAwQSd2qb7yDrzxEQNIcEsy 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "ecmaVersion": 2018 11 | }, 12 | "rules": { 13 | "accessor-pairs": "error", 14 | "array-bracket-newline": "off", 15 | "array-bracket-spacing": [ 16 | "error", 17 | "never" 18 | ], 19 | "array-callback-return": "error", 20 | "array-element-newline": "off", 21 | "arrow-body-style": "off", 22 | "arrow-parens": "off", 23 | "arrow-spacing": [ 24 | "error", 25 | { 26 | "after": true, 27 | "before": true 28 | } 29 | ], 30 | "block-scoped-var": "error", 31 | "block-spacing": [ 32 | "error", 33 | "always" 34 | ], 35 | "brace-style": [ 36 | "error", 37 | "1tbs", 38 | { 39 | "allowSingleLine": true 40 | } 41 | ], 42 | "callback-return": "error", 43 | "camelcase": "off", 44 | "capitalized-comments": "off", 45 | "class-methods-use-this": "off", 46 | "comma-dangle": ["error", "always-multiline"], 47 | "comma-spacing": "error", 48 | "comma-style": [ 49 | "error", 50 | "last" 51 | ], 52 | "complexity": "error", 53 | "computed-property-spacing": [ 54 | "error", 55 | "never" 56 | ], 57 | "consistent-return": "error", 58 | "consistent-this": "error", 59 | "curly": "off", 60 | "default-case": "error", 61 | "dot-location": [ 62 | "error", 63 | "property" 64 | ], 65 | "dot-notation": "error", 66 | "eol-last": "error", 67 | "eqeqeq": "off", 68 | "func-call-spacing": "error", 69 | "func-name-matching": "error", 70 | "func-names": "error", 71 | "func-style": [ 72 | "error", 73 | "declaration", 74 | { 75 | "allowArrowFunctions": true 76 | } 77 | ], 78 | "function-paren-newline": "error", 79 | "generator-star-spacing": "off", 80 | "global-require": "error", 81 | "guard-for-in": "error", 82 | "handle-callback-err": "error", 83 | "id-blacklist": "error", 84 | "id-length": "off", 85 | "id-match": "error", 86 | "implicit-arrow-linebreak": [ 87 | "error", 88 | "beside" 89 | ], 90 | "indent": [ 91 | "error", 92 | 2 93 | ], 94 | "indent-legacy": "off", 95 | "init-declarations": "error", 96 | "jsx-quotes": "error", 97 | "key-spacing": [ 98 | "error", 99 | { 100 | "beforeColon": false, 101 | "afterColon": true, 102 | "mode": "minimum", 103 | "align": "value" 104 | } 105 | ], 106 | "keyword-spacing": [ 107 | "error", 108 | { 109 | "after": true, 110 | "before": true 111 | } 112 | ], 113 | "line-comment-position": "off", 114 | "linebreak-style": [ 115 | "error", 116 | "unix" 117 | ], 118 | "lines-around-comment": "off", 119 | "lines-around-directive": "error", 120 | "lines-between-class-members": [ 121 | "error", 122 | "always", 123 | { 124 | "exceptAfterSingleLine": true 125 | } 126 | ], 127 | "max-classes-per-file": "error", 128 | "max-depth": "error", 129 | "max-len": "error", 130 | "max-lines": "off", 131 | "max-lines-per-function": "off", 132 | "max-nested-callbacks": "error", 133 | "max-params": "off", 134 | "max-statements": "off", 135 | "max-statements-per-line": "error", 136 | "multiline-comment-style": "off", 137 | "new-parens": "error", 138 | "newline-after-var": "off", 139 | "newline-before-return": "off", 140 | "newline-per-chained-call": "error", 141 | "no-alert": "error", 142 | "no-array-constructor": "error", 143 | "no-async-promise-executor": "error", 144 | "no-await-in-loop": "error", 145 | "no-bitwise": "error", 146 | "no-buffer-constructor": "error", 147 | "no-caller": "error", 148 | "no-catch-shadow": "error", 149 | "no-console": [ 150 | "error", 151 | { 152 | "allow": [ 153 | "info", 154 | "warn", 155 | "error" 156 | ] 157 | } 158 | ], 159 | "no-confusing-arrow": "error", 160 | "no-continue": "error", 161 | "no-div-regex": "error", 162 | "no-duplicate-imports": "error", 163 | "no-else-return": "error", 164 | "no-empty-function": "off", 165 | "no-eq-null": "off", 166 | "no-eval": "error", 167 | "no-extend-native": "error", 168 | "no-extra-bind": "error", 169 | "no-extra-label": "error", 170 | "no-extra-parens": "off", 171 | "no-floating-decimal": "error", 172 | "no-implicit-coercion": "error", 173 | "no-implicit-globals": "error", 174 | "no-implied-eval": "error", 175 | "no-inline-comments": "off", 176 | "no-invalid-this": "off", 177 | "no-iterator": "error", 178 | "no-label-var": "error", 179 | "no-labels": "error", 180 | "no-lone-blocks": "error", 181 | "no-lonely-if": "off", 182 | "no-loop-func": "error", 183 | "no-magic-numbers": "off", 184 | "no-misleading-character-class": "error", 185 | "no-mixed-operators": "off", 186 | "no-mixed-requires": "error", 187 | "no-multi-assign": "error", 188 | "no-multi-spaces": "off", 189 | "no-multi-str": "error", 190 | "no-multiple-empty-lines": "error", 191 | "no-native-reassign": "error", 192 | "no-negated-condition": "error", 193 | "no-negated-in-lhs": "error", 194 | "no-nested-ternary": "error", 195 | "no-new": "off", 196 | "no-new-func": "error", 197 | "no-new-object": "error", 198 | "no-new-require": "error", 199 | "no-new-wrappers": "error", 200 | "no-octal-escape": "error", 201 | "no-param-reassign": "off", 202 | "no-path-concat": "error", 203 | "no-plusplus": "off", 204 | "no-process-env": "error", 205 | "no-process-exit": "error", 206 | "no-proto": "error", 207 | "no-prototype-builtins": "off", 208 | "no-restricted-globals": "error", 209 | "no-restricted-imports": "error", 210 | "no-restricted-modules": "error", 211 | "no-restricted-properties": "error", 212 | "no-restricted-syntax": "error", 213 | "no-return-assign": "error", 214 | "no-return-await": "error", 215 | "no-script-url": "error", 216 | "no-self-compare": "error", 217 | "no-sequences": "error", 218 | "no-shadow": "off", 219 | "no-shadow-restricted-names": "error", 220 | "no-spaced-func": "off", 221 | "no-sync": "error", 222 | "no-tabs": "error", 223 | "no-template-curly-in-string": "error", 224 | "no-ternary": "off", 225 | "no-throw-literal": "off", 226 | "no-trailing-spaces": "error", 227 | "no-undef": "error", 228 | "no-undef-init": "error", 229 | "no-undefined": "error", 230 | "no-underscore-dangle": "error", 231 | "no-unmodified-loop-condition": "error", 232 | "no-unneeded-ternary": "error", 233 | "no-use-before-define": [ 234 | "error", 235 | { 236 | "functions": true, 237 | "classes": true 238 | } 239 | ], 240 | "no-useless-call": "error", 241 | "no-useless-catch": "error", 242 | "no-useless-computed-key": "error", 243 | "no-useless-concat": "error", 244 | "no-useless-constructor": "error", 245 | "no-useless-rename": "error", 246 | "no-useless-return": "error", 247 | "no-var": "error", 248 | "no-void": "error", 249 | "no-warning-comments": "off", 250 | "no-whitespace-before-property": "error", 251 | "no-with": "error", 252 | "nonblock-statement-body-position": "error", 253 | "object-curly-newline": "error", 254 | "object-curly-spacing": [ 255 | "error", 256 | "always" 257 | ], 258 | "object-shorthand": "off", 259 | "one-var": "off", 260 | "one-var-declaration-per-line": "error", 261 | "operator-assignment": [ 262 | "error", 263 | "always" 264 | ], 265 | "operator-linebreak": "off", 266 | "padded-blocks": [ 267 | "error", 268 | "never" 269 | ], 270 | "padding-line-between-statements": "error", 271 | "prefer-const": "error", 272 | "prefer-destructuring": "off", 273 | "prefer-numeric-literals": "error", 274 | "prefer-object-spread": "error", 275 | "prefer-promise-reject-errors": "error", 276 | "prefer-reflect": "off", 277 | "prefer-rest-params": "error", 278 | "prefer-spread": "error", 279 | "prefer-template": "error", 280 | "quote-props": "off", 281 | "quotes": [ 282 | "error", 283 | "single" 284 | ], 285 | "radix": "error", 286 | "require-atomic-updates": "error", 287 | "require-await": "error", 288 | "require-jsdoc": "off", 289 | "require-unicode-regexp": "error", 290 | "rest-spread-spacing": [ 291 | "error", 292 | "never" 293 | ], 294 | "semi": [ 295 | "error", 296 | "always" 297 | ], 298 | "semi-spacing": [ 299 | "error", 300 | { 301 | "after": true, 302 | "before": false 303 | } 304 | ], 305 | "semi-style": [ 306 | "error", 307 | "last" 308 | ], 309 | "sort-imports": "error", 310 | "sort-keys": "off", 311 | "sort-vars": "error", 312 | "space-before-blocks": "error", 313 | "space-before-function-paren": [ 314 | "error", 315 | "never" 316 | ], 317 | "space-in-parens": "error", 318 | "space-infix-ops": "error", 319 | "space-unary-ops": [ 320 | "error", 321 | { 322 | "nonwords": false, 323 | "words": true 324 | } 325 | ], 326 | "spaced-comment": [ 327 | "error", 328 | "always" 329 | ], 330 | "strict": "error", 331 | "switch-colon-spacing": "error", 332 | "symbol-description": "error", 333 | "template-curly-spacing": [ 334 | "error", 335 | "never" 336 | ], 337 | "template-tag-spacing": "error", 338 | "unicode-bom": [ 339 | "error", 340 | "never" 341 | ], 342 | "valid-jsdoc": "off", 343 | "vars-on-top": "error", 344 | "wrap-iife": "error", 345 | "wrap-regex": "error", 346 | "yield-star-spacing": "error", 347 | "yoda": [ 348 | "error", 349 | "never" 350 | ] 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, 2 | # cache/restore them, build the source code and run tests across different 3 | # versions of node 4 | # For more information see: 5 | # https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 6 | --- 7 | name: Node.js CI 8 | 9 | on: 10 | push: 11 | branches: 12 | - master 13 | pull_request: null 14 | 15 | jobs: 16 | test: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | node-version: [18.x, 20.x, 22.x] 23 | # See supported Node.js release schedule at: 24 | # https://nodejs.org/en/about/releases/ 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | with: 29 | submodules: true 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | cache: 'npm' 35 | - run: npm clean-install 36 | - run: npm run build --if-present 37 | - run: npm test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | ._* 4 | bower_components/ 5 | .cache/ 6 | .idea/ 7 | playground/ 8 | yarn-error.log 9 | examples/ 10 | dist/index.html 11 | *.swp 12 | tags 13 | edit.sh 14 | usage.sh 15 | coverage/ 16 | .parcel-cache/ 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "core"] 2 | path = core 3 | url = https://github.com/mvanderkamp/westures-core 4 | -------------------------------------------------------------------------------- /.ignore: -------------------------------------------------------------------------------- 1 | *.svg 2 | docs 3 | dist 4 | coverage 5 | node_modules 6 | bundle* 7 | package-lock.json 8 | 9 | -------------------------------------------------------------------------------- /.jsdocrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdown": { 3 | "idInHeadings": true 4 | }, 5 | "opts": { 6 | "destination": "docs/", 7 | "encoding": "utf8", 8 | "mainpagetitle": "Home | Westures Documentation", 9 | "package": "package.json", 10 | "readme": "README.md", 11 | "recurse": true, 12 | "template": "node_modules/docdash", 13 | "verbose": true 14 | }, 15 | "plugins": [], 16 | "recurseDepth": 10, 17 | "source": { 18 | "include": [ 19 | "core", 20 | "src", 21 | "index.js" 22 | ], 23 | "includePattern": ".js$", 24 | "exclude": [ 25 | "core/dist", 26 | "core/docs", 27 | "core/node_modules", 28 | "core/test" 29 | ] 30 | }, 31 | "sourceType": "module", 32 | "tags": { 33 | "allowUnknownTags": true, 34 | "dictionaries": ["jsdoc"] 35 | }, 36 | "docdash": { 37 | "static": false, 38 | "sort": true, 39 | "meta": { 40 | "title": "Westures Documentation", 41 | "description": "Reference documentation for the Westures multitouch gesture library for JavaScript", 42 | "keyword": "javascript, multitouch, gesture" 43 | }, 44 | "search": true, 45 | "collapse": true, 46 | "typedefs": true, 47 | "private": false, 48 | "removeQuotes": "trim", 49 | "ShortenTypes": true, 50 | "scopeInOutputPath": false, 51 | "nameInOutputPath": false, 52 | "versionInOutputPath": false 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # local script files 2 | edit.sh 3 | usage.sh 4 | 5 | # tag files 6 | tags 7 | 8 | # documentation 9 | docs 10 | 11 | # test related 12 | test 13 | coverage 14 | 15 | # development assist files 16 | Makefile 17 | arkit.* 18 | .*.json 19 | .cache 20 | .ignore 21 | .gitmodules 22 | 23 | # unneeded core-related files 24 | core/dist 25 | core/*.yml 26 | core/package-lock.json 27 | core/CHANGELOG.md 28 | core/README.md 29 | 30 | # build and development artifacts 31 | .parcel-cache/ 32 | .travis.yml 33 | .coveralls.yml 34 | .github 35 | 36 | # Shouldn't have to exclude this, but here we are 37 | node_modules 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.0 4 | 5 | - Switch to docdash for documentation. 6 | - Update westures-core to 1.3.0. This introces a "headless" mode which allows westures to be run from a server. 7 | 8 | ## 1.0.0 9 | 10 | - Official first release! The engine is no longer considered to be in beta. 11 | - Refactor Smoothable to be a data type, not a mixin. 12 | - Remove the Binding class and integrate with the Gesture class. It was more of 13 | a hindrance than a help on its own. 14 | - Provide automatic detection of enabled and disabled gestures, including using 15 | keys to enable and disable, in a simple way such that gestures don't need to 16 | check if their enabled inside their hooks. 17 | - 'cancel' phase is now properly called. 18 | - Region class now takes an optional 'options' object instead of lots of 19 | arguments. 20 | - Remove the 'getProgressOfGesture' method from the Input class. Gestures should 21 | track their progress internally, on their own instance! 22 | - Remove the 'radius' property from the outgoing data. It didn't seem useful and 23 | was just cluttering the output. 24 | - Use Sets for tracking Gestures inside the Region instead of Arrays. (Faster 25 | access operations). 26 | - Update the Press gesture to allow multiple presses, one after the other, by 27 | adding successive inputs. Effectively makes it a multi-touch press! Single 28 | touch press is still possible using the min/maxInputs options! 29 | - Use a Pivotable base gesture type for Swivel and Pull. 30 | - Improved documentation by showing all of westures-core 31 | - Change pivotCenter -> dynamicPivot, default to false 32 | - Clean up Rotate implementation to reduce reliance on side effects 33 | - Switch to using pointer events by default, combined with setting touch-action: 34 | none on the gesture elements (not the region itself). 35 | - Provide options on the Region for choosing whether to prefer pointer events 36 | over mouse/touch events (preferPointer) and what to set the touch-action 37 | property to on gesture elements (touchAction). 38 | - Default to using the window as the region if no element provided. 39 | - Add mouseleave to the CANCEL_EVENTS 40 | - Include the core engine as a git submodule instead of relying on the npm package. 41 | - Keeps the core code out of node_modules which simplifies a lot of things... 42 | - Store all options in a consistent Gesture.options object 43 | 44 | ## 0.7.8 45 | 46 | - Add a Pull gesture. Pull is to Pinch as Swivel is to Rotate. In other words, 47 | the data is calculated relative to a fixed point, rather than relative to the 48 | other input points. 49 | 50 | ## 0.7.7 51 | 52 | - Simplify the Swivel class a bit. 53 | 54 | ## 0.7.6 55 | 56 | - Update dev support packages and switch to parcel-bundler for the distributable 57 | instead of browserify. This bundle is now found in the dist/ folder, along 58 | with a source map. Support for the old 'bundle.js' and 'bundle.min.js' is 59 | approximately maintained by providing two copies of 'dist/index.js' under 60 | those names. They will be removed in subsequent releases. 61 | 62 | ## 0.7.5 63 | 64 | - Improvements to the Press gesture. No longer fails as touches are added, 65 | supports layering of Presses. For example, you can have presses that respond 66 | to 1,2,3,... however many touches all attached to the same element, and they 67 | will each fire in turn as touches are added (but not as they are removed!). 68 | - [POSSIBLE BREAKING] The 'numInputs' option was renamed to 'minInputs'. 69 | - Remove confusing and unnceessary console.warn() statements from core engine. 70 | 71 | ## 0.7.4 72 | 73 | - Add a check that ensures smoothing will only ever be applied on devices that 74 | need it. That is, devices with 'coarse' pointers. 75 | 76 | ## 0.7.3 77 | 78 | - [POSSIBLE BREAKING] But only for those who have implemented their own 79 | Smoothable gesture with a non-zero identity value (e.g. Rotate has an identity 80 | of 0, as that represents no change, and Pinch has an identity of 1, as that 81 | represents no change). Such gestures will now need to declare their own 82 | identity value *after* calling super() in the constructor. 83 | - The smoothing algorithm used by the Smoothable mixin has been 84 | simplified. There is no delay to emits, as analysis of the data 85 | revealed this really only occurred for the first emit. Instead a 86 | simple rolling average is maintained. 87 | - Additionally, note that `this.smooth(data, field)` must be called instead 88 | of `this.emit(data, field)` 89 | - Add an experimental Press gesture. 90 | 91 | ## 0.7.2 92 | 93 | - Fix bug in gestures that used the Smoothable mixin which prevented their 94 | default 'smoothing' setting from being used. 95 | - Turn on smoothing by default in Pan. 96 | 97 | ## 0.7.1 98 | 99 | - Add `babelify` transform for `bundle.js`. Should add Edge support, and at the 100 | very least opens up the possibility of expanding browser support a bit. 101 | 102 | ## 0.7.0 103 | 104 | - Use new Smoothable mixin from `westures-core` for Pan, Pinch, Rotate, and 105 | Swivel. Set smoothing as enabled by default, except in Pan (may enable 106 | later...) 107 | - Place the `angularMinus` function into its own file, so that it can be used by 108 | both Rotate and Swivel. 109 | - Make Swivel multitouch-capable. 110 | - Change names of emitted data properties to be more idiomatic. 'rotation' for 111 | Rotate and Swivel instead of delta, 'scale' for Pinch and 'translation' for 112 | Pan instead of change. 113 | - Point2D#midpoint was renamed to Point2D#centroid 114 | - centroid and radius were added to base data for emits. 115 | - Preference is now given to data from the gesture over base data in the case of 116 | property name collisions. 117 | 118 | ## 0.6.3 119 | 120 | - Switch to simple average for Pinch and Rotate smoothing 121 | - This makes the smoothing more general, ensures a 60fps update rate is 122 | maintained, and generally has a nicer feel to it. 123 | - Downside is that there will be a bit of drift, but that's why this setting 124 | is optional! 125 | 126 | ## 0.6.2 127 | 128 | - Add optional smoothing to Pinch and Rotate (on by default). 129 | 130 | ## 0.6.1 131 | 132 | - Treat 'touchcancel' and 'pointercancel' the same way as 'blur'. 133 | - This is an unfortunate hack, necessitated by an apparent bug in Chrome, 134 | wherein only the _first_ (primary?) pointer will receive a 'cancel' event. 135 | The other's are cancelled, but no event is emitted. Their IDs are reused 136 | for subsequent pointers, making gesture state recoverable, but this is 137 | still not a good situation. 138 | - The downside is that this workaround means that if any single pointer is 139 | cancelled, _all_ of the pointers are treated as cancelled. I don't have 140 | enough in depth knowledge to say for sure, but I suspect that this doesn't 141 | have to be the case. If I have time soon I'll post a ticket to Chrome, at 142 | the very least to find out if this is actually a bug (my read of the spec 143 | tells me that it is). 144 | - The upside is that this should be pretty fail-safe, when combined with the 145 | 'blur' listener on the window. 146 | 147 | ## 0.6.0 148 | 149 | - Fix Tap bug preventing rapid taps. 150 | - 'ended' list wasn't being cleared on an emit, preventing further emits if 151 | taps came in rapid succession. 152 | - Expand default deadzone radius of Swivel. 153 | - Fix bugs in Swipe: 154 | - Make sure swipe state is reset on 'start' and after 'end' phases. 155 | - Prevent delayed emits if the user stops suddenly and doesn't move again 156 | before releasing the pointer. 157 | - [BREAKING CHANGE] Use inner fields instead of input progress. 158 | - Breaking because you can't reuse some of the Gesture objects the way you 159 | could previously. 160 | - Slightly more efficient, therefore preferable overall. 161 | - Rotate still uses input progress so that angle changes can be tracked on a 162 | per-input basis, which is more responsive than anything else I've tried so 163 | far. 164 | 165 | ## 0.5.4 166 | 167 | - Add 'cancel' phase support for touchcancel and pointercancel. 168 | - For most gestures, will probably be the same as 'end', but it must be 169 | different for gestures that emit on 'end'. 170 | - Add a 'blur' listener to window to reset the state when the window loses 171 | focus. 172 | - Fix Swivel bug in Edge: Edge doesn't provide 'x' and 'y' fields with 173 | 'getBoundingClientRect', so use 'left' and 'top' instead. 174 | - Make Swipes work for multitouch. 175 | 176 | ## 0.5.3 177 | 178 | - Fix buggy Swivel results caused by >1 active inputs. 179 | - Fix bugs in Swipe: 180 | - Erroneous acceptance of >1 inputs. 181 | - Velocity going to infinity in some cases (division by 0) 182 | - Direction calculation could produce errors, switched to using a more 183 | common mathematical approach. 184 | - Normalize Swipe a bit by taking average velocity instead of max. 185 | 186 | ## 0.5.2 187 | 188 | - Fix bug in Swivel when using the `pivotCenter` option. Initial angle wasn't 189 | being set correctly, causing jumps when initiating a Swivel. 190 | 191 | ## 0.5.0 192 | 193 | - Rename Region#bind() -> Region#addGesture() and Region#unbind() -> 194 | Region#removeGestures(). 195 | - I was not happy with the way that the 'bind' naming clashes with the 196 | 'bind' function on the Function prototype. 197 | - Simplified "unbind" function. It now returns null, as the Bindings should not 198 | be exposed to the end user. 199 | - Sped up Binding selection in the Region's `arbitrate` function, while 200 | simultaneously fixing a critical bug! 201 | - Only the bindings associated with elements on the composed path of the 202 | first input to touch the surface will be accessed. 203 | - In other words, this batch of bindings is cached instead of being 204 | recalculated on every input event. 205 | - Previously, if the user started one input in one bound element, then 206 | another input in another bound element, the bindings for both elements 207 | would think they have full control, leading to some potentially weird 208 | behaviour. 209 | - If you want this behaviour, you'll now have to simulate it by creating a 210 | separate region for each binding. 211 | - Removed Region#getBindingsByInitialPos 212 | - Removed State#someInputWasInitiallyInside 213 | - Improved test coverage a bit 214 | 215 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright for portions of project Westures are held by ZingChart, 2016-2018 as 4 | part of project ZingTouch. All other copyright for project Westures are held by 5 | Michael van der Kamp, 2018. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # westures 2 | 3 | [![Node.js CI](https://github.com/mvanderkamp/westures/actions/workflows/node.js.yml/badge.svg)](https://github.com/mvanderkamp/westures/actions/workflows/node.js.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/mvanderkamp/westures/badge.svg?branch=master)](https://coveralls.io/github/mvanderkamp/westures?branch=master) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/fc7d7ace5a3018dc4071/maintainability)](https://codeclimate.com/github/mvanderkamp/westures/maintainability) 6 | 7 | Westures is a robust multitouch gesture engine for JavaScript. Each gesture is 8 | capable of working seamlessly as touch points are added and removed, with no 9 | limit on the number of touch points, and with each touch point contributing to 10 | the gesture. 11 | 12 | Visit this page for an example of the system in action: [Westures Example]( 13 | https://mvanderkamp.github.io/westures-example/). 14 | 15 | The library achieves its goals without using any dependencies except for its 16 | own core, yet maintains usability across the main modern browsers. 17 | Transpilation may be necessary for this last point to be achieved, as the 18 | library is written using many of the newer features of the JavaScript language. 19 | A transpiled bundle is provided, but the browser target list is arbitrary and 20 | likely includes some bloat. In most cases you will be better off performing 21 | bundling, transpilation, and minification yourself. 22 | 23 | This module includes 24 | [westures-core](https://mvanderkamp.github.io/westures-core/) 25 | as well as a base set of gestures. 26 | 27 | Westures is a fork of [ZingTouch](https://github.com/zingchart/zingtouch). 28 | 29 | ## Quick Example 30 | 31 | ```javascript 32 | // Import the module. 33 | const wes = require('westures'); 34 | 35 | // Declare a region. The default is the window object, but other elements like 36 | // the document body work too. 37 | const region = new wes.Region(); 38 | 39 | // Combine an element and a handler into a Gesture. 40 | const pan = new wes.Pan(document.querySelector('#pannable'), (data) => { 41 | console.log(data.translation.x, data.translation.y); 42 | }) 43 | 44 | // And add the gesture to the region. 45 | region.addGesture(pan) 46 | ``` 47 | 48 | ## Table of Contents 49 | 50 | - [Overview](#overview) 51 | - [Basic Usage](#basic-usage) 52 | - [Implementing Custom Gestures](#implementing-custom-gestures) 53 | - [What's Changed](#changes) 54 | - [Nomenclature and Origins](#nomenclature-and-origins) 55 | - [Issues](#Issues) 56 | - [Links](#links) 57 | 58 | ## Overview 59 | 60 | There are nine gestures defined in this module: 61 | 62 | Name | # of Inputs | Emit Phase | Recognized Input Behaviour 63 | ------ | ----------- | ---------- | ----------------- 64 | Pan | 1+ | Move | Sliding around the screen 65 | Pinch | 2+ | Move | Moving together or apart 66 | Press | 1+ | Move | Held down without moving 67 | Pull | 1+ | Move | Moving away from or toward a fixed point 68 | Rotate | 2+ | Move | Rotating around each other 69 | Swipe | 1+ | End | Moving quickly then released 70 | Swivel | 1+ | Move | Rotating around a fixed pivot point 71 | Tap | 1+ | End | Quickly pressing and releasing 72 | Track | 1+ | All | Track locations of all active pointers 73 | 74 | See the [documentation](https://mvanderkamp.github.io/westures/) for more 75 | information about each gesture. 76 | 77 | Note that all x,y positions are obtained from the corresponding `clientX` and 78 | `clientY` properties of the input event. 79 | 80 | ## Basic Usage 81 | 82 | - [Declaring a Region](#declaring-a-region) 83 | - [Instantiating a Gesture](#instantiating-a-gesture) 84 | - [Adding a Gesture to a Region](#adding-a-gesture-to-a-region) 85 | 86 | ### Importing the module 87 | 88 | ```javascript 89 | const wes = require('westures'); 90 | ``` 91 | 92 | ### Declaring a Region 93 | 94 | First, decide what region should listen for events. This could be the 95 | interactable element itself, or a larger region (possibly containing many 96 | interactable elements). Behaviour may differ slightly based on the approach you 97 | take, as a `Region` will perform locking operations on its interactable 98 | elements and their bound gestures so as to limit interference between elements 99 | during gestures, and no such locking occurs between Regions. 100 | 101 | If you have lots of interactable elements on your page, you may find it 102 | convenient to use smaller elements as regions. Test it out in case, and see what 103 | works better for you. 104 | 105 | By default, the window object is used. 106 | 107 | ```javascript 108 | const region = new wes.Region(); 109 | ``` 110 | 111 | ### Instantiating a Gesture 112 | 113 | When you instantiate a gesture, you need to provide a handler as well as an 114 | Element. The gesture will only be recognized when the first pointer to interact 115 | with the region was inside the given Element. Therefore unless you want to try 116 | something fancy the gesture element should probably be contained inside the 117 | region element. It could even be the region element. 118 | 119 | Now for an example. Suppose you have a div (id 'pannable', although this is 120 | irrelevant from Westures' perspective) within which you want to detect a Pan 121 | gesture. First we need to find the element. 122 | 123 | ```javascript 124 | const pannable = document.querySelector('#pannable'); 125 | ``` 126 | 127 | And we also need a handler. This function will be called whenever a gesture 128 | hook returns non-null data. For `Pan`, this is just the move phase, but the 129 | handler doesn't need to know that. The data returned by the hook will be 130 | available inside the handler. 131 | 132 | ```javascript 133 | function panLogger(data) { 134 | console.log(data.translation.x, data.translation.y); 135 | } 136 | ``` 137 | 138 | Now we're ready to combine the element and its handler into a gesture. 139 | 140 | ```javascript 141 | pan = new wes.Pan(pannable, panLogger); 142 | ``` 143 | 144 | We're not quite done though, as none of this will actually work until you add 145 | the gesture to the region. 146 | 147 | ### Adding a Gesture to a Region 148 | 149 | Simple: 150 | 151 | ```javascript 152 | region.addGesture(pan); 153 | ``` 154 | 155 | Now the `panLogger` function will be called whenever a `pan` gesture is 156 | detected on the `#pannable` element inside the region. 157 | 158 | ## Implementing Custom Gestures 159 | 160 | The technique used by Westures (originally conceived for ZingTouch) is to 161 | filter all user inputs through four key lifecycle phases: `start`, `move`, 162 | `end`, and `cancel`. Gestures are defined by how they respond to these phases. 163 | To respond to the phases, a gesture extends the `Gesture` class provided by 164 | this module and overrides the method (a.k.a. "hook") corresponding to the name 165 | of the phase. 166 | 167 | The hook, when called, will receive the current `State` object of the region. 168 | To maintain responsiveness, the functionality within a hook should be short and 169 | as efficient as possible. 170 | 171 | For example, a simple way to implement a `Tap` gesture would be as follows: 172 | 173 | ```javascript 174 | const { Gesture } = require('westures'); 175 | 176 | const TIMEOUT = 100; 177 | 178 | class Tap extends Gesture { 179 | constructor() { 180 | super('tap'); 181 | this.startTime = null; 182 | } 183 | 184 | start(state) { 185 | this.startTime = Date.now(); 186 | } 187 | 188 | end(state) { 189 | if (Date.now() - this.startTime <= TIMEOUT) { 190 | return state.getInputsInPhase('end')[0].current.point; 191 | } 192 | return null; 193 | } 194 | } 195 | ``` 196 | 197 | There are problems with this example, and it should probably not be used as an 198 | actual Tap gesture, it is merely to illustrate the basic idea. 199 | 200 | The default hooks for all Gestures simply return null. Data will only be 201 | forwarded to bound handlers when a non-null value is returned by a hook. 202 | Returned values should be packed inside an object. For example, instead of just 203 | `return 42;`, a custom hook should do `return { value: 42 };` 204 | 205 | If your Gesture subclass needs to track any kind of complex state, remember that 206 | it may be necessary to reset the state in the `cancel` phase. 207 | 208 | For information about what data is accessible via the State object, see the full 209 | documentation [here](https://mvanderkamp.github.io/westures-core/State.html). 210 | Note that his documentation was generated with `jsdoc`. 211 | 212 | ### Data Passed to Handlers 213 | 214 | As you can see from above, it is the gesture which decides when data gets passed 215 | to handlers, and for the most part what that data will be. Note though that a 216 | few properties will get added to the outgoing data object before the handler is 217 | called. Those properties are: 218 | 219 | Name | Type | Value 220 | -------- | -------- | ----- 221 | centroid | Point2D | The centroid of the input points. 222 | event | Event | The input event which caused the gesture to be recognized 223 | phase | String | `'start'`, `'move'`, `'end'`, or `'cancel'` 224 | type | String | The name of the gesture as specified by its designer. 225 | target | Element | The Element that is associated with the recognized gesture. 226 | 227 | If data properties returned by a hook have a name collision with one of these 228 | properties, the value from the hook gets precedent and the default is 229 | overwritten. 230 | 231 | ## Changes 232 | 233 | See the [changelog]( 234 | https://github.com/mvanderkamp/westures/blob/master/CHANGELOG.md) for the most 235 | recent updates. 236 | 237 | ## Nomenclature and Origins 238 | 239 | In my last year of univerisity, I was working on an API for building 240 | multi-device interfaces called "WAMS" (Workspaces Across Multiple Surfaces), 241 | which included the goal of supporting multi-device gestures. 242 | 243 | After an extensive search I found that none of the available multitouch 244 | libraries for JavaScript provided the fidelity I needed, and concluded that I 245 | would need to write my own, or at least fork an existing one. ZingTouch proved 246 | to the be the most approachable, so I decided it would make a good starting 247 | point. 248 | 249 | The name "westures" is a mash-up of "WAMS" and "gestures". 250 | 251 | ## Issues 252 | 253 | If you find any issues, please let me know! 254 | 255 | ## Links 256 | 257 | ### westures 258 | 259 | - [npm](https://www.npmjs.com/package/westures) 260 | - [github](https://github.com/mvanderkamp/westures) 261 | - [documentation](https://mvanderkamp.github.io/westures/) 262 | 263 | ### westures-core 264 | 265 | - [npm](https://www.npmjs.com/package/westures-core) 266 | - [github](https://github.com/mvanderkamp/westures-core) 267 | - [documentation](https://mvanderkamp.github.io/westures-core/) 268 | 269 | -------------------------------------------------------------------------------- /docs/core_index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | core/index.js - Westures Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 43 | 44 |
45 | 46 |

core/index.js

47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 |
/**
 57 |  * The global API interface for westures-core. Exposes all classes, constants,
 58 |  * and routines used by the package. Use responsibly.
 59 |  *
 60 |  * @namespace westures-core
 61 |  */
 62 | 
 63 | 'use strict';
 64 | 
 65 | const Gesture = require('./src/Gesture.js');
 66 | const Input = require('./src/Input.js');
 67 | const Point2D = require('./src/Point2D.js');
 68 | const PointerData = require('./src/PointerData.js');
 69 | const Region = require('./src/Region.js');
 70 | const Smoothable = require('./src/Smoothable.js');
 71 | const State = require('./src/State.js');
 72 | const constants = require('./src/constants.js');
 73 | const utils = require('./src/utils.js');
 74 | 
 75 | module.exports = {
 76 |   Gesture,
 77 |   Input,
 78 |   Point2D,
 79 |   PointerData,
 80 |   Region,
 81 |   Smoothable,
 82 |   State,
 83 |   ...constants,
 84 |   ...utils,
 85 | };
 86 | 
 87 | 
88 |
89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 |
97 | 98 |
99 | 100 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /docs/core_src_PointerData.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | core/src/PointerData.js - Westures Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 43 | 44 |
45 | 46 |

core/src/PointerData.js

47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 |
'use strict';
 57 | 
 58 | const Point2D   = require('./Point2D.js');
 59 | const { PHASE } = require('./constants.js');
 60 | 
 61 | /**
 62 |  * @private
 63 |  * @inner
 64 |  * @memberof westures-core.PointerData
 65 |  *
 66 |  * @return {Event} The Event object which corresponds to the given identifier.
 67 |  *    Contains clientX, clientY values.
 68 |  */
 69 | function getEventObject(event, identifier) {
 70 |   if (event.changedTouches) {
 71 |     return Array.from(event.changedTouches).find(touch => {
 72 |       return touch.identifier === identifier;
 73 |     });
 74 |   }
 75 |   return event;
 76 | }
 77 | 
 78 | /**
 79 |  * Low-level storage of pointer data based on incoming data from an interaction
 80 |  * event.
 81 |  *
 82 |  * @memberof westures-core
 83 |  *
 84 |  * @param {Event} event - The event object being wrapped.
 85 |  * @param {number} identifier - The index of touch if applicable
 86 |  */
 87 | class PointerData {
 88 |   constructor(event, identifier) {
 89 |     const { clientX, clientY } = getEventObject(event, identifier);
 90 | 
 91 |     /**
 92 |      * The original event object.
 93 |      *
 94 |      * @type {Event}
 95 |      */
 96 |     this.event = event;
 97 | 
 98 |     /**
 99 |      * The type or 'phase' of this batch of pointer data. 'start' or 'move' or
100 |      * 'end' or 'cancel'
101 |      *
102 |      * @type {string}
103 |      */
104 |     this.type = PHASE[event.type];
105 | 
106 |     /**
107 |      * The timestamp of the event in milliseconds elapsed since January 1, 1970,
108 |      * 00:00:00 UTC.
109 |      *
110 |      * @type {number}
111 |      */
112 |     this.time = Date.now();
113 | 
114 |     /**
115 |      * The (x,y) coordinate of the event, wrapped in a Point2D.
116 |      *
117 |      * @type {westures-core.Point2D}
118 |      */
119 |     this.point = new Point2D(clientX, clientY);
120 |   }
121 | }
122 | 
123 | module.exports = PointerData;
124 | 
125 | 
126 |
127 |
128 | 129 | 130 | 131 | 132 | 133 | 134 |
135 | 136 |
137 | 138 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Montserrat/Montserrat-Bold.eot -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Montserrat/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Montserrat/Montserrat-Bold.woff -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Montserrat/Montserrat-Bold.woff2 -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Montserrat/Montserrat-Regular.eot -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Montserrat/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Montserrat/Montserrat-Regular.woff -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Montserrat/Montserrat-Regular.woff2 -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvanderkamp/westures/a72af8cf5f89269da2bca96e52c3113732d70dea/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 -------------------------------------------------------------------------------- /docs/index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | index.js - Westures Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 43 | 44 |
45 | 46 |

index.js

47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 |
/**
 57 |  * The API interface for Westures. Defines a number of gestures on top of the
 58 |  * engine provided by {@link
 59 |  * https://mvanderkamp.github.io/westures-core/index.html|westures-core}.
 60 |  *
 61 |  * @namespace westures
 62 |  */
 63 | 
 64 | 'use strict';
 65 | 
 66 | const core = require('./core');
 67 | 
 68 | const Pan     = require('./src/Pan.js');
 69 | const Pinch   = require('./src/Pinch.js');
 70 | const Press   = require('./src/Press.js');
 71 | const Pull    = require('./src/Pull.js');
 72 | const Rotate  = require('./src/Rotate.js');
 73 | const Swipe   = require('./src/Swipe.js');
 74 | const Swivel  = require('./src/Swivel.js');
 75 | const Tap     = require('./src/Tap.js');
 76 | const Track   = require('./src/Track.js');
 77 | 
 78 | module.exports = {
 79 |   Pan,
 80 |   Pinch,
 81 |   Press,
 82 |   Pull,
 83 |   Rotate,
 84 |   Swipe,
 85 |   Swivel,
 86 |   Tap,
 87 |   Track,
 88 |   ...core,
 89 | };
 90 | 
 91 | /**
 92 |  * Here are the return "types" of the gestures that are included in this
 93 |  * package.
 94 |  *
 95 |  * @namespace ReturnTypes
 96 |  */
 97 | 
 98 | /**
 99 |  * The base data that is included for all emitted gestures.
100 |  *
101 |  * @typedef {Object} BaseData
102 |  *
103 |  * @property {westures-core.Point2D} centroid - The centroid of the input
104 |  * points.
105 |  * @property {Event} event - The input event which caused the gesture to be
106 |  * recognized.
107 |  * @property {string} phase - 'start', 'move', 'end', or 'cancel'.
108 |  * @property {string} type - The name of the gesture as specified by its
109 |  * designer.
110 |  * @property {Element} target - The bound target of the gesture.
111 |  *
112 |  * @memberof ReturnTypes
113 |  */
114 | 
115 | 
116 |
117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 |
125 | 126 |
127 | 128 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /docs/scripts/collapse.js: -------------------------------------------------------------------------------- 1 | function hideAllButCurrent(){ 2 | //by default all submenut items are hidden 3 | //but we need to rehide them for search 4 | document.querySelectorAll("nav > ul").forEach(function(parent) { 5 | if (parent.className.indexOf("collapse_top") !== -1) { 6 | parent.style.display = "none"; 7 | } 8 | }); 9 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(parent) { 10 | parent.style.display = "none"; 11 | }); 12 | document.querySelectorAll("nav > h3").forEach(function(section) { 13 | if (section.className.indexOf("collapsed_header") !== -1) { 14 | section.addEventListener("click", function(){ 15 | if (section.nextSibling.style.display === "none") { 16 | section.nextSibling.style.display = "block"; 17 | } else { 18 | section.nextSibling.style.display = "none"; 19 | } 20 | }); 21 | } 22 | }); 23 | 24 | //only current page (if it exists) should be opened 25 | var file = window.location.pathname.split("/").pop().replace(/\.html/, ''); 26 | document.querySelectorAll("nav > ul > li > a").forEach(function(parent) { 27 | var href = parent.attributes.href.value.replace(/\.html/, ''); 28 | if (file === href) { 29 | if (parent.parentNode.parentNode.className.indexOf("collapse_top") !== -1) { 30 | parent.parentNode.parentNode.style.display = "block"; 31 | } 32 | parent.parentNode.querySelectorAll("ul li").forEach(function(elem) { 33 | elem.style.display = "block"; 34 | }); 35 | } 36 | }); 37 | } 38 | 39 | hideAllButCurrent(); -------------------------------------------------------------------------------- /docs/scripts/commonNav.js: -------------------------------------------------------------------------------- 1 | if (typeof fetch === 'function') { 2 | const init = () => { 3 | if (typeof scrollToNavItem !== 'function') return false 4 | scrollToNavItem() 5 | // hideAllButCurrent not always loaded 6 | if (typeof hideAllButCurrent === 'function') hideAllButCurrent() 7 | return true 8 | } 9 | fetch('./nav.inc.html') 10 | .then(response => response.ok ? response.text() : `${response.url} => ${response.status} ${response.statusText}`) 11 | .then(body => { 12 | document.querySelector('nav').innerHTML += body 13 | // nav.js should be quicker to load than nav.inc.html, a fallback just in case 14 | return init() 15 | }) 16 | .then(done => { 17 | if (done) return 18 | let i = 0 19 | ;(function waitUntilNavJs () { 20 | if (init()) return 21 | if (i++ < 100) return setTimeout(waitUntilNavJs, 300) 22 | console.error(Error('nav.js not loaded after 30s waiting for it')) 23 | })() 24 | }) 25 | .catch(error => console.error(error)) 26 | } else { 27 | console.error(Error('Browser too old to display commonNav (remove commonNav docdash option)')) 28 | } 29 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = 'line' + lineNumber; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/nav.js: -------------------------------------------------------------------------------- 1 | function scrollToNavItem() { 2 | var path = window.location.href.split('/').pop().replace(/\.html/, ''); 3 | document.querySelectorAll('nav a').forEach(function(link) { 4 | var href = link.attributes.href.value.replace(/\.html/, ''); 5 | if (path === href) { 6 | link.scrollIntoView({block: 'center'}); 7 | return; 8 | } 9 | }) 10 | } 11 | 12 | scrollToNavItem(); 13 | -------------------------------------------------------------------------------- /docs/scripts/polyfill.js: -------------------------------------------------------------------------------- 1 | //IE Fix, src: https://www.reddit.com/r/programminghorror/comments/6abmcr/nodelist_lacks_foreach_in_internet_explorer/ 2 | if (typeof(NodeList.prototype.forEach)!==typeof(alert)){ 3 | NodeList.prototype.forEach=Array.prototype.forEach; 4 | } -------------------------------------------------------------------------------- /docs/scripts/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 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 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /docs/scripts/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= 3 | [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), 9 | l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, 11 | q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, 12 | "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), 13 | a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} 14 | for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ 21 | I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), 22 | ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", 23 | /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), 24 | ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", 25 | hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= 26 | !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p ul > li:not(.level-hide)").forEach(function(elem) { 16 | elem.style.display = "block"; 17 | }); 18 | 19 | if (typeof hideAllButCurrent === "function"){ 20 | //let's do what ever collapse wants to do 21 | hideAllButCurrent(); 22 | } else { 23 | //menu by default should be opened 24 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { 25 | elem.style.display = "block"; 26 | }); 27 | } 28 | } else { 29 | //we are searching 30 | document.documentElement.setAttribute(searchAttr, ''); 31 | 32 | //show all parents 33 | document.querySelectorAll("nav > ul > li").forEach(function(elem) { 34 | elem.style.display = "block"; 35 | }); 36 | document.querySelectorAll("nav > ul").forEach(function(elem) { 37 | elem.style.display = "block"; 38 | }); 39 | //hide all results 40 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { 41 | elem.style.display = "none"; 42 | }); 43 | //show results matching filter 44 | document.querySelectorAll("nav > ul > li > ul a").forEach(function(elem) { 45 | if (!contains(elem.parentNode, search)) { 46 | return; 47 | } 48 | elem.parentNode.style.display = "block"; 49 | }); 50 | //hide parents without children 51 | document.querySelectorAll("nav > ul > li").forEach(function(parent) { 52 | var countSearchA = 0; 53 | parent.querySelectorAll("a").forEach(function(elem) { 54 | if (contains(elem, search)) { 55 | countSearchA++; 56 | } 57 | }); 58 | 59 | var countUl = 0; 60 | var countUlVisible = 0; 61 | parent.querySelectorAll("ul").forEach(function(ulP) { 62 | // count all elements that match the search 63 | if (contains(ulP, search)) { 64 | countUl++; 65 | } 66 | 67 | // count all visible elements 68 | var children = ulP.children 69 | for (i=0; i ul.collapse_top").forEach(function(parent) { 86 | var countVisible = 0; 87 | parent.querySelectorAll("li").forEach(function(elem) { 88 | if (elem.style.display !== "none") { 89 | countVisible++; 90 | } 91 | }); 92 | 93 | if (countVisible == 0) { 94 | //has no child at all and does not contain text 95 | parent.style.display = "none"; 96 | } 97 | }); 98 | } 99 | }); -------------------------------------------------------------------------------- /docs/styles/prettify.css: -------------------------------------------------------------------------------- 1 | .pln { 2 | color: #ddd; 3 | } 4 | 5 | /* string content */ 6 | .str { 7 | color: #61ce3c; 8 | } 9 | 10 | /* a keyword */ 11 | .kwd { 12 | color: #fbde2d; 13 | } 14 | 15 | /* a comment */ 16 | .com { 17 | color: #aeaeae; 18 | } 19 | 20 | /* a type name */ 21 | .typ { 22 | color: #8da6ce; 23 | } 24 | 25 | /* a literal value */ 26 | .lit { 27 | color: #fbde2d; 28 | } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #ddd; 33 | } 34 | 35 | /* lisp open bracket */ 36 | .opn { 37 | color: #000000; 38 | } 39 | 40 | /* lisp close bracket */ 41 | .clo { 42 | color: #000000; 43 | } 44 | 45 | /* a markup tag name */ 46 | .tag { 47 | color: #8da6ce; 48 | } 49 | 50 | /* a markup attribute name */ 51 | .atn { 52 | color: #fbde2d; 53 | } 54 | 55 | /* a markup attribute value */ 56 | .atv { 57 | color: #ddd; 58 | } 59 | 60 | /* a declaration */ 61 | .dec { 62 | color: #EF5050; 63 | } 64 | 65 | /* a variable name */ 66 | .var { 67 | color: #c82829; 68 | } 69 | 70 | /* a function name */ 71 | .fun { 72 | color: #4271ae; 73 | } 74 | 75 | /* Specify class=linenums on a pre to get line numbering */ 76 | ol.linenums { 77 | margin-top: 0; 78 | margin-bottom: 0; 79 | padding-bottom: 2px; 80 | } 81 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The API interface for Westures. Defines a number of gestures on top of the 3 | * engine provided by {@link 4 | * https://mvanderkamp.github.io/westures-core/index.html|westures-core}. 5 | * 6 | * @namespace westures 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const core = require('./core'); 12 | 13 | const Pan = require('./src/Pan.js'); 14 | const Pinch = require('./src/Pinch.js'); 15 | const Press = require('./src/Press.js'); 16 | const Pull = require('./src/Pull.js'); 17 | const Rotate = require('./src/Rotate.js'); 18 | const Swipe = require('./src/Swipe.js'); 19 | const Swivel = require('./src/Swivel.js'); 20 | const Tap = require('./src/Tap.js'); 21 | const Track = require('./src/Track.js'); 22 | 23 | module.exports = { 24 | Pan, 25 | Pinch, 26 | Press, 27 | Pull, 28 | Rotate, 29 | Swipe, 30 | Swivel, 31 | Tap, 32 | Track, 33 | ...core, 34 | }; 35 | 36 | /** 37 | * Here are the return "types" of the gestures that are included in this 38 | * package. 39 | * 40 | * @namespace ReturnTypes 41 | */ 42 | 43 | /** 44 | * The base data that is included for all emitted gestures. 45 | * 46 | * @typedef {Object} BaseData 47 | * 48 | * @property {westures-core.Point2D} centroid - The centroid of the input 49 | * points. 50 | * @property {Event} event - The input event which caused the gesture to be 51 | * recognized. 52 | * @property {string} phase - 'start', 'move', 'end', or 'cancel'. 53 | * @property {string} type - The name of the gesture as specified by its 54 | * designer. 55 | * @property {Element} target - The bound target of the gesture. 56 | * 57 | * @memberof ReturnTypes 58 | */ 59 | 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "westures", 3 | "version": "1.1.1", 4 | "description": "Robust n-pointer multitouch gesture detection library for JavaScript", 5 | "author": "Michael van der Kamp ", 6 | "keywords": [ 7 | "multitouch", 8 | "gesture", 9 | "library", 10 | "tap", 11 | "pan", 12 | "pinch", 13 | "rotate", 14 | "press", 15 | "swipe", 16 | "swivel", 17 | "track", 18 | "simultaneous", 19 | "gestures", 20 | "touch", 21 | "mouse", 22 | "pointer" 23 | ], 24 | "source": "index.js", 25 | "main": "dist/index.js", 26 | "directories": { 27 | "test": "test" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/mvanderkamp/westures" 32 | }, 33 | "homepage": "https://mvanderkamp.github.io/westures/", 34 | "license": "MIT", 35 | "scripts": { 36 | "build": "parcel build", 37 | "build:debug": "parcel build --no-optimize", 38 | "coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls", 39 | "docs": "jsdoc -c .jsdocrc.json", 40 | "lint": "eslint src test index.js", 41 | "lint:fix": "eslint --fix src test index.js", 42 | "test": "jest", 43 | "test:debug": "node inspect $(npm bin)/jest --runInBand" 44 | }, 45 | "jest": { 46 | "modulePaths": [ 47 | "", 48 | "/src/" 49 | ], 50 | "setupFilesAfterEnv": [ 51 | "./core/test/setup.js" 52 | ], 53 | "testEnvironment": "jsdom", 54 | "testPathIgnorePatterns": [ 55 | "/node_modules/", 56 | "/dist/" 57 | ], 58 | "coveragePathIgnorePatterns": [ 59 | "/node_modules/", 60 | "/dist/", 61 | "/test/" 62 | ], 63 | "fakeTimers": { 64 | "enableGlobally": true, 65 | "legacyFakeTimers": true 66 | } 67 | }, 68 | "devDependencies": { 69 | "@babel/core": "^7.21.8", 70 | "@babel/preset-env": "^7.21.5", 71 | "coveralls-next": "^4.2.1", 72 | "docdash": "^2.0.1", 73 | "eslint": "^8.41.0", 74 | "jest": "^29.5.0", 75 | "jest-environment-jsdom": "^29.5.0", 76 | "jsdoc": "^4.0.2", 77 | "parcel": "^2.8.3", 78 | "underscore": "^1.13.6" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Pan.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Contains the Pan class. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const { Gesture, Point2D, Smoothable } = require('../core'); 8 | 9 | /** 10 | * Data returned when a Pan is recognized. 11 | * 12 | * @typedef {Object} PanData 13 | * @mixes ReturnTypes.BaseData 14 | * 15 | * @property {westures-core.Point2D} translation - The change vector from the 16 | * last emit. 17 | * 18 | * @memberof ReturnTypes 19 | */ 20 | 21 | /** 22 | * A Pan is defined as a normal movement in any direction. 23 | * 24 | * @extends westures-core.Gesture 25 | * @see {ReturnTypes.PanData} 26 | * @see {westures-core.Smoothable} 27 | * @memberof westures 28 | * 29 | * @param {Element} element - The element with which to associate the gesture. 30 | * @param {Function} handler - The function handler to execute when a gesture 31 | * is recognized on the associated element. 32 | * @param {object} [options] - Gesture customization options. 33 | * @param {westures-core.STATE_KEYS[]} [options.enableKeys=[]] - List of keys 34 | * which will enable the gesture. The gesture will not be recognized unless one 35 | * of these keys is pressed while the interaction occurs. If not specified or an 36 | * empty list, the gesture is treated as though the enable key is always down. 37 | * @param {westures-core.STATE_KEYS[]} [options.disableKeys=[]] - List of keys 38 | * which will disable the gesture. The gesture will not be recognized if one of 39 | * these keys is pressed while the interaction occurs. If not specified or an 40 | * empty list, the gesture is treated as though the disable key is never down. 41 | * @param {number} [options.minInputs=1] - The minimum number of pointers that 42 | * must be active for the gesture to be recognized. Uses >=. 43 | * @param {number} [options.maxInputs=Number.MAX_VALUE] - The maximum number of 44 | * pointers that may be active for the gesture to be recognized. Uses <=. 45 | * @param {boolean} [options.applySmoothing=true] - Whether to apply inertial 46 | * smoothing for systems with coarse pointers. 47 | */ 48 | class Pan extends Gesture { 49 | constructor(element, handler, options = {}) { 50 | super('pan', element, handler, options); 51 | 52 | /** 53 | * The previous point location. 54 | * 55 | * @type {westures-core.Point2D} 56 | */ 57 | this.previous = null; 58 | 59 | /* 60 | * The outgoing data, with optional inertial smoothing. 61 | * 62 | * @override 63 | * @type {westures-core.Smoothable} 64 | */ 65 | this.outgoing = new Smoothable({ ...options, identity: new Point2D() }); 66 | this.outgoing.average = (a, b) => Point2D.centroid([a, b]); 67 | } 68 | 69 | /** 70 | * Resets the gesture's progress by saving the current centroid of the active 71 | * inputs. To be called whenever the number of inputs changes. 72 | * 73 | * @param {State} state 74 | */ 75 | restart(state) { 76 | this.previous = state.centroid; 77 | this.outgoing.restart(); 78 | } 79 | 80 | start(state) { 81 | this.restart(state); 82 | } 83 | 84 | move(state) { 85 | const translation = state.centroid.minus(this.previous); 86 | this.previous = state.centroid; 87 | return { translation: this.outgoing.next(translation) }; 88 | } 89 | 90 | end(state) { 91 | this.restart(state); 92 | } 93 | 94 | cancel(state) { 95 | this.restart(state); 96 | } 97 | } 98 | 99 | module.exports = Pan; 100 | 101 | -------------------------------------------------------------------------------- /src/Pinch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Contains the abstract Pinch class. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const { Gesture, Smoothable } = require('../core'); 8 | 9 | /** 10 | * Data returned when a Pinch is recognized. 11 | * 12 | * @typedef {Object} PinchData 13 | * @mixes ReturnTypes.BaseData 14 | * 15 | * @property {number} distance - The average distance from an active input to 16 | * the centroid. 17 | * @property {number} scale - The proportional change in distance since last 18 | * emit. 19 | * 20 | * @memberof ReturnTypes 21 | */ 22 | 23 | /** 24 | * A Pinch is defined as two or more inputs moving either together or apart. 25 | * 26 | * @extends westures-core.Gesture 27 | * @see {ReturnTypes.PinchData} 28 | * @memberof westures 29 | * 30 | * @param {Element} element - The element to which to associate the gesture. 31 | * @param {Function} handler - The function handler to execute when a gesture 32 | * is recognized on the associated element. 33 | * @param {object} [options] - Gesture customization options. 34 | * @param {westures-core.STATE_KEYS[]} [options.enableKeys=[]] - List of keys 35 | * which will enable the gesture. The gesture will not be recognized unless one 36 | * of these keys is pressed while the interaction occurs. If not specified or an 37 | * empty list, the gesture is treated as though the enable key is always down. 38 | * @param {westures-core.STATE_KEYS[]} [options.disableKeys=[]] - List of keys 39 | * which will disable the gesture. The gesture will not be recognized if one of 40 | * these keys is pressed while the interaction occurs. If not specified or an 41 | * empty list, the gesture is treated as though the disable key is never down. 42 | * @param {number} [options.minInputs=2] - The minimum number of pointers that 43 | * must be active for the gesture to be recognized. Uses >=. 44 | * @param {number} [options.maxInputs=Number.MAX_VALUE] - The maximum number of 45 | * pointers that may be active for the gesture to be recognized. Uses <=. 46 | * @param {boolean} [options.applySmoothing=true] - Whether to apply inertial 47 | * smoothing for systems with coarse pointers. 48 | */ 49 | class Pinch extends Gesture { 50 | constructor(element, handler, options = {}) { 51 | options = { ...Pinch.DEFAULTS, ...options }; 52 | super('pinch', element, handler, options); 53 | 54 | /** 55 | * The previous distance. 56 | * 57 | * @type {number} 58 | */ 59 | this.previous = 0; 60 | 61 | /* 62 | * The outgoing data, with optional inertial smoothing. 63 | * 64 | * @override 65 | * @type {westures-core.Smoothable} 66 | */ 67 | this.outgoing = new Smoothable({ ...options, identity: 1 }); 68 | } 69 | 70 | /** 71 | * Initializes the gesture progress. 72 | * 73 | * @param {State} state - current input state. 74 | */ 75 | restart(state) { 76 | this.previous = state.centroid.averageDistanceTo(state.activePoints); 77 | this.outgoing.restart(); 78 | } 79 | 80 | start(state) { 81 | this.restart(state); 82 | } 83 | 84 | move(state) { 85 | const distance = state.centroid.averageDistanceTo(state.activePoints); 86 | const scale = distance / this.previous; 87 | this.previous = distance; 88 | return { distance, scale: this.outgoing.next(scale) }; 89 | } 90 | 91 | end(state) { 92 | this.restart(state); 93 | } 94 | 95 | cancel(state) { 96 | this.restart(state); 97 | } 98 | } 99 | 100 | Pinch.DEFAULTS = Object.freeze({ 101 | minInputs: 2, 102 | }); 103 | 104 | module.exports = Pinch; 105 | 106 | -------------------------------------------------------------------------------- /src/Pivotable.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Contains the Rotate class. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const { Gesture, Point2D, Smoothable } = require('../core'); 8 | 9 | /** 10 | * Data returned when a Pivotable is recognized. 11 | * 12 | * @typedef {Object} SwivelData 13 | * @mixes ReturnTypes.BaseData 14 | * 15 | * @property {number} rotation - In radians, the change in angle since last 16 | * emit. 17 | * @property {westures-core.Point2D} pivot - The pivot point. 18 | * 19 | * @memberof ReturnTypes 20 | */ 21 | 22 | /** 23 | * A Pivotable is a single input rotating around a fixed point. The fixed point 24 | * is determined by the input's location at its 'start' phase. 25 | * 26 | * @extends westures.Gesture 27 | * @see {ReturnTypes.SwivelData} 28 | * @memberof westures 29 | * 30 | * @param {Element} element - The element to which to associate the gesture. 31 | * @param {Function} handler - The function handler to execute when a gesture 32 | * is recognized on the associated element. 33 | * @param {object} [options] - Gesture customization options. 34 | * @param {westures-core.STATE_KEYS[]} [options.enableKeys=[]] - List of keys 35 | * which will enable the gesture. The gesture will not be recognized unless one 36 | * of these keys is pressed while the interaction occurs. If not specified or an 37 | * empty list, the gesture is treated as though the enable key is always down. 38 | * @param {westures-core.STATE_KEYS[]} [options.disableKeys=[]] - List of keys 39 | * which will disable the gesture. The gesture will not be recognized if one of 40 | * these keys is pressed while the interaction occurs. If not specified or an 41 | * empty list, the gesture is treated as though the disable key is never down. 42 | * @param {number} [options.minInputs=1] - The minimum number of pointers that 43 | * must be active for the gesture to be recognized. Uses >=. 44 | * @param {number} [options.maxInputs=Number.MAX_VALUE] - The maximum number of 45 | * pointers that may be active for the gesture to be recognized. Uses <=. 46 | * @param {boolean} [options.applySmoothing=true] - Whether to apply inertial 47 | * smoothing for systems with coarse pointers. 48 | * @param {number} [options.deadzoneRadius=15] - The radius in pixels around the 49 | * start point in which to do nothing. 50 | * @param {Element} [options.dynamicPivot=false] - Normally the center point of 51 | * the gesture's element is used as the pivot. If this option is set, the 52 | * initial contact point with the element is used as the pivot instead. 53 | */ 54 | class Pivotable extends Gesture { 55 | constructor(type = 'pivotable', element, handler, options = {}) { 56 | super(type, element, handler, { ...Pivotable.DEFAULTS, ...options }); 57 | 58 | /** 59 | * The pivot point of the pivotable. 60 | * 61 | * @type {westures-core.Point2D} 62 | */ 63 | this.pivot = null; 64 | 65 | /** 66 | * The previous data. 67 | * 68 | * @type {number} 69 | */ 70 | this.previous = 0; 71 | 72 | /** 73 | * The outgoing data. 74 | * 75 | * @type {westures-core.Smoothable} 76 | */ 77 | this.outgoing = new Smoothable(options); 78 | } 79 | 80 | /** 81 | * Determine the center point of the given element's bounding client 82 | * rectangle. 83 | * 84 | * @static 85 | * 86 | * @param {Element} element - The DOM element to analyze. 87 | * @return {westures-core.Point2D} - The center of the element's bounding 88 | * client rectangle. 89 | */ 90 | static getClientCenter(element) { 91 | const rect = element.getBoundingClientRect(); 92 | return new Point2D( 93 | rect.left + (rect.width / 2), 94 | rect.top + (rect.height / 2), 95 | ); 96 | } 97 | 98 | /** 99 | * Updates the previous data. It will be called during the 'start' and 'end' 100 | * phases, and should also be called during the 'move' phase implemented by 101 | * the subclass. 102 | * 103 | * @abstract 104 | * @param {State} state - the current input state. 105 | */ 106 | updatePrevious() { 107 | throw 'Gestures which extend Pivotable must implement updatePrevious()'; 108 | } 109 | 110 | /** 111 | * Restart the given progress object using the given input object. 112 | * 113 | * @param {State} state - current input state. 114 | */ 115 | restart(state) { 116 | if (this.options.dynamicPivot) { 117 | this.pivot = state.centroid; 118 | this.previous = 0; 119 | } else { 120 | this.pivot = Pivotable.getClientCenter(this.element); 121 | this.updatePrevious(state); 122 | } 123 | this.outgoing.restart(); 124 | } 125 | 126 | start(state) { 127 | this.restart(state); 128 | } 129 | 130 | end(state) { 131 | if (state.active.length > 0) { 132 | this.restart(state); 133 | } else { 134 | this.outgoing.restart(); 135 | } 136 | } 137 | 138 | cancel() { 139 | this.outgoing.restart(); 140 | } 141 | } 142 | 143 | Pivotable.DEFAULTS = Object.freeze({ 144 | deadzoneRadius: 15, 145 | dynamicPivot: false, 146 | }); 147 | 148 | module.exports = Pivotable; 149 | -------------------------------------------------------------------------------- /src/Press.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Contains the Press class. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const { Gesture, Point2D, MOVE } = require('../core'); 8 | 9 | /** 10 | * Data returned when a Press is recognized. 11 | * 12 | * @typedef {Object} PressData 13 | * 14 | * @property {westures-core.Point2D} centroid - The current centroid of the 15 | * input points. 16 | * @property {westures-core.Point2D} initial - The initial centroid of the input 17 | * points. 18 | * @property {number} distance - The total movement since initial contact. 19 | * 20 | * @memberof ReturnTypes 21 | */ 22 | 23 | /** 24 | * A Press is defined as one or more input points being held down without 25 | * moving. Press gestures may be stacked by pressing with additional pointers 26 | * beyond the minimum, so long as none of the points move or are lifted, a Press 27 | * will be recognized for each additional pointer. 28 | * 29 | * @extends westures-core.Gesture 30 | * @see {ReturnTypes.PressData} 31 | * @memberof westures 32 | * 33 | * @param {Element} element - The element to which to associate the gesture. 34 | * @param {Function} handler - The function handler to execute when a gesture 35 | * is recognized on the associated element. 36 | * @param {object} [options] - Gesture customization options. 37 | * @param {westures-core.STATE_KEYS[]} [options.enableKeys=[]] - List of keys 38 | * which will enable the gesture. The gesture will not be recognized unless one 39 | * of these keys is pressed while the interaction occurs. If not specified or an 40 | * empty list, the gesture is treated as though the enable key is always down. 41 | * @param {westures-core.STATE_KEYS[]} [options.disableKeys=[]] - List of keys 42 | * which will disable the gesture. The gesture will not be recognized if one of 43 | * these keys is pressed while the interaction occurs. If not specified or an 44 | * empty list, the gesture is treated as though the disable key is never down. 45 | * @param {number} [options.minInputs=1] - The minimum number of pointers that 46 | * must be active for the gesture to be recognized. Uses >=. 47 | * @param {number} [options.maxInputs=Number.MAX_VALUE] - The maximum number of 48 | * pointers that may be active for the gesture to be recognized. Uses <=. 49 | * @param {number} [options.delay=1000] - The delay before emitting, during 50 | * which time the number of inputs must not go below minInputs. 51 | * @param {number} [options.tolerance=10] - The tolerance in pixels a user can 52 | * move and still allow the gesture to emit. 53 | */ 54 | class Press extends Gesture { 55 | constructor(element, handler, options = {}) { 56 | super('press', element, handler, { ...Press.DEFAULTS, ...options }); 57 | } 58 | 59 | start(state) { 60 | const initial = state.centroid; 61 | const originalInputs = Array.from(state.active); 62 | setTimeout(() => { 63 | const inputs = state.active.filter(i => originalInputs.includes(i)); 64 | if (inputs.length === originalInputs.length) { 65 | const centroid = Point2D.centroid(inputs.map(i => i.current.point)); 66 | const distance = initial.distanceTo(centroid); 67 | if (distance <= this.options.tolerance) { 68 | this.recognize(MOVE, state, { centroid, distance, initial }); 69 | } 70 | } 71 | }, this.options.delay); 72 | } 73 | } 74 | 75 | Press.DEFAULTS = Object.freeze({ 76 | delay: 1000, 77 | tolerance: 10, 78 | }); 79 | 80 | module.exports = Press; 81 | 82 | -------------------------------------------------------------------------------- /src/Pull.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Contains the abstract Pull class. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const { Smoothable } = require('../core'); 8 | const Pivotable = require('./Pivotable.js'); 9 | 10 | /** 11 | * Data returned when a Pull is recognized. 12 | * 13 | * @typedef {Object} PullData 14 | * @mixes ReturnTypes.BaseData 15 | * 16 | * @property {number} distance - The average distance from an active input to 17 | * the centroid. 18 | * @property {number} scale - The proportional change in distance since last 19 | * emit. 20 | * @property {westures-core.Point2D} pivot - The pivot point. 21 | * 22 | * @memberof ReturnTypes 23 | */ 24 | 25 | /** 26 | * A Pull is defined as a single input moving away from or towards a pivot 27 | * point. 28 | * 29 | * @extends westures-core.Gesture 30 | * @see {ReturnTypes.PullData} 31 | * @memberof westures 32 | * 33 | * @param {Element} element - The element to which to associate the gesture. 34 | * @param {Function} handler - The function handler to execute when a gesture 35 | * is recognized on the associated element. 36 | * @param {object} [options] - Gesture customization options. 37 | * @param {westures-core.STATE_KEYS[]} [options.enableKeys=[]] - List of keys 38 | * which will enable the gesture. The gesture will not be recognized unless one 39 | * of these keys is pressed while the interaction occurs. If not specified or an 40 | * empty list, the gesture is treated as though the enable key is always down. 41 | * @param {westures-core.STATE_KEYS[]} [options.disableKeys=[]] - List of keys 42 | * which will disable the gesture. The gesture will not be recognized if one of 43 | * these keys is pressed while the interaction occurs. If not specified or an 44 | * empty list, the gesture is treated as though the disable key is never down. 45 | * @param {number} [options.minInputs=1] - The minimum number of pointers that 46 | * must be active for the gesture to be recognized. Uses >=. 47 | * @param {number} [options.maxInputs=Number.MAX_VALUE] - The maximum number of 48 | * pointers that may be active for the gesture to be recognized. Uses <=. 49 | * @param {boolean} [options.applySmoothing=true] - Whether to apply inertial 50 | * smoothing for systems with coarse pointers. 51 | * @param {number} [options.deadzoneRadius=15] - The radius in pixels around the 52 | * start point in which to do nothing. 53 | * @param {Element} [options.dynamicPivot=false] - Normally the center point of 54 | * the gesture's element is used as the pivot. If this option is set, the 55 | * initial contact point with the element is used as the pivot instead. 56 | */ 57 | class Pull extends Pivotable { 58 | constructor(element, handler, options = {}) { 59 | super('pull', element, handler, options); 60 | 61 | /* 62 | * The outgoing data, with optional inertial smoothing. 63 | * 64 | * @override 65 | * @type {westures-core.Smoothable} 66 | */ 67 | this.outgoing = new Smoothable({ ...options, identity: 1 }); 68 | } 69 | 70 | updatePrevious(state) { 71 | this.previous = this.pivot.distanceTo(state.centroid); 72 | } 73 | 74 | move(state) { 75 | const pivot = this.pivot; 76 | const distance = pivot.distanceTo(state.centroid); 77 | const scale = distance / this.previous; 78 | const { deadzoneRadius } = this.options; 79 | 80 | let rv = null; 81 | if (distance > deadzoneRadius && this.previous > deadzoneRadius) { 82 | rv = { distance, scale: this.outgoing.next(scale), pivot }; 83 | } 84 | 85 | /* 86 | * Updating the previous distance regardless of emit prevents sudden changes 87 | * when the user exits the deadzone circle. 88 | */ 89 | this.previous = distance; 90 | 91 | return rv; 92 | } 93 | } 94 | 95 | module.exports = Pull; 96 | 97 | -------------------------------------------------------------------------------- /src/Rotate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Contains the Rotate class. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const { angularDifference, Gesture, Smoothable } = require('../core'); 8 | 9 | /** 10 | * Data returned when a Rotate is recognized. 11 | * 12 | * @typedef {Object} RotateData 13 | * @mixes ReturnTypes.BaseData 14 | * 15 | * @property {number} rotation - In radians, the change in angle since last 16 | * emit. 17 | * 18 | * @memberof ReturnTypes 19 | */ 20 | 21 | /** 22 | * A Rotate is defined as two inputs moving with a changing angle between them. 23 | * 24 | * @extends westures-core.Gesture 25 | * @see {ReturnTypes.RotateData} 26 | * @memberof westures 27 | * 28 | * @param {Element} element - The element to which to associate the gesture. 29 | * @param {Function} handler - The function handler to execute when a gesture 30 | * is recognized on the associated element. 31 | * @param {object} [options] - Gesture customization options. 32 | * @param {westures-core.STATE_KEYS[]} [options.enableKeys=[]] - List of keys 33 | * which will enable the gesture. The gesture will not be recognized unless one 34 | * of these keys is pressed while the interaction occurs. If not specified or an 35 | * empty list, the gesture is treated as though the enable key is always down. 36 | * @param {westures-core.STATE_KEYS[]} [options.disableKeys=[]] - List of keys 37 | * which will disable the gesture. The gesture will not be recognized if one of 38 | * these keys is pressed while the interaction occurs. If not specified or an 39 | * empty list, the gesture is treated as though the disable key is never down. 40 | * @param {number} [options.minInputs=2] - The minimum number of pointers that 41 | * must be active for the gesture to be recognized. Uses >=. 42 | * @param {number} [options.maxInputs=Number.MAX_VALUE] - The maximum number of 43 | * pointers that may be active for the gesture to be recognized. Uses <=. 44 | * @param {boolean} [options.applySmoothing=true] - Whether to apply inertial 45 | * smoothing for systems with coarse pointers. 46 | */ 47 | class Rotate extends Gesture { 48 | constructor(element, handler, options = {}) { 49 | options = { ...Rotate.DEFAULTS, ...options }; 50 | super('rotate', element, handler, options); 51 | 52 | /** 53 | * Track the previous angles for each input. 54 | * 55 | * @type {number[]} 56 | */ 57 | this.previousAngles = []; 58 | 59 | /* 60 | * The outgoing data, with optional inertial smoothing. 61 | * 62 | * @override 63 | * @type {westures-core.Smoothable} 64 | */ 65 | this.outgoing = new Smoothable(options); 66 | } 67 | 68 | /** 69 | * Restart the gesture for a new number of inputs. 70 | * 71 | * @param {State} state - current input state. 72 | */ 73 | restart(state) { 74 | this.previousAngles = state.centroid.anglesTo(state.activePoints); 75 | this.outgoing.restart(); 76 | } 77 | 78 | start(state) { 79 | this.restart(state); 80 | } 81 | 82 | move(state) { 83 | const stagedAngles = state.centroid.anglesTo(state.activePoints); 84 | const angle = stagedAngles.reduce((total, current, index) => { 85 | return total + angularDifference(current, this.previousAngles[index]); 86 | }, 0); 87 | this.previousAngles = stagedAngles; 88 | const rotation = angle / state.activePoints.length; 89 | return { rotation: this.outgoing.next(rotation) }; 90 | } 91 | 92 | end(state) { 93 | this.restart(state); 94 | } 95 | 96 | cancel() { 97 | this.outgoing.restart(); 98 | } 99 | } 100 | 101 | Rotate.DEFAULTS = Object.freeze({ 102 | minInputs: 2, 103 | }); 104 | 105 | module.exports = Rotate; 106 | 107 | -------------------------------------------------------------------------------- /src/Swipe.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Contains the Swipe class. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const { Gesture } = require('../core'); 8 | 9 | const PROGRESS_STACK_SIZE = 7; 10 | const MS_THRESHOLD = 300; 11 | 12 | /** 13 | * Data returned when a Swipe is recognized. 14 | * 15 | * @typedef {Object} SwipeData 16 | * @mixes ReturnTypes.BaseData 17 | * 18 | * @property {number} velocity - The velocity of the swipe. 19 | * @property {number} direction - In radians, the direction of the swipe. 20 | * @property {westures-core.Point2D} point - The point at which the swipe ended. 21 | * @property {number} time - The epoch time, in ms, when the swipe ended. 22 | * 23 | * @memberof ReturnTypes 24 | */ 25 | 26 | /** 27 | * A swipe is defined as input(s) moving in the same direction in an relatively 28 | * increasing velocity and leaving the screen at some point before it drops 29 | * below it's escape velocity. 30 | * 31 | * @extends westures-core.Gesture 32 | * @see {ReturnTypes.SwipeData} 33 | * @memberof westures 34 | * 35 | * @param {Element} element - The element to which to associate the gesture. 36 | * @param {Function} handler - The function handler to execute when a gesture 37 | * is recognized on the associated element. 38 | * @param {object} [options] - Gesture customization options. 39 | * @param {westures-core.STATE_KEYS[]} [options.enableKeys=[]] - List of keys 40 | * which will enable the gesture. The gesture will not be recognized unless one 41 | * of these keys is pressed while the interaction occurs. If not specified or an 42 | * empty list, the gesture is treated as though the enable key is always down. 43 | * @param {westures-core.STATE_KEYS[]} [options.disableKeys=[]] - List of keys 44 | * which will disable the gesture. The gesture will not be recognized if one of 45 | * these keys is pressed while the interaction occurs. If not specified or an 46 | * empty list, the gesture is treated as though the disable key is never down. 47 | * @param {number} [options.minInputs=1] - The minimum number of pointers that 48 | * must be active for the gesture to be recognized. Uses >=. 49 | * @param {number} [options.maxInputs=Number.MAX_VALUE] - The maximum number of 50 | * pointers that may be active for the gesture to be recognized. Uses <=. 51 | */ 52 | class Swipe extends Gesture { 53 | constructor(element, handler, options = {}) { 54 | super('swipe', element, handler, options); 55 | 56 | /** 57 | * Moves list. 58 | * 59 | * @type {object[]} 60 | */ 61 | this.moves = []; 62 | 63 | /** 64 | * Data to emit when all points have ended. 65 | * 66 | * @type {ReturnTypes.SwipeData} 67 | */ 68 | this.saved = null; 69 | } 70 | 71 | /** 72 | * Restart the swipe state for a new numper of inputs. 73 | */ 74 | restart() { 75 | this.moves = []; 76 | this.saved = null; 77 | } 78 | 79 | start() { 80 | this.restart(); 81 | } 82 | 83 | move(state) { 84 | this.moves.push({ 85 | time: Date.now(), 86 | point: state.centroid, 87 | }); 88 | 89 | if (this.moves.length > PROGRESS_STACK_SIZE) { 90 | this.moves.splice(0, this.moves.length - PROGRESS_STACK_SIZE); 91 | } 92 | } 93 | 94 | end(state) { 95 | const result = this.getResult(); 96 | this.moves = []; 97 | 98 | if (state.active.length > 0) { 99 | this.saved = result; 100 | return null; 101 | } 102 | 103 | this.saved = null; 104 | return Swipe.validate(result); 105 | } 106 | 107 | cancel() { 108 | this.restart(); 109 | } 110 | 111 | /** 112 | * Get the swipe result. 113 | * 114 | * @returns {?ReturnTypes.SwipeData} 115 | */ 116 | getResult() { 117 | if (this.moves.length < PROGRESS_STACK_SIZE) { 118 | return this.saved; 119 | } 120 | const vlim = PROGRESS_STACK_SIZE - 1; 121 | const { point, time } = this.moves[vlim]; 122 | const velocity = Swipe.calc_velocity(this.moves, vlim); 123 | const direction = Swipe.calc_angle(this.moves, vlim); 124 | const centroid = point; 125 | return { point, velocity, direction, time, centroid }; 126 | } 127 | 128 | /** 129 | * Validates that an emit should occur with the given data. 130 | * 131 | * @static 132 | * @param {?ReturnTypes.SwipeData} data 133 | * @returns {?ReturnTypes.SwipeData} 134 | */ 135 | static validate(data) { 136 | if (data == null) return null; 137 | return (Date.now() - data.time > MS_THRESHOLD) ? null : data; 138 | } 139 | 140 | /** 141 | * Calculates the angle of movement along a series of moves. 142 | * 143 | * @static 144 | * @see {@link https://en.wikipedia.org/wiki/Mean_of_circular_quantities} 145 | * 146 | * @param {{time: number, point: westures-core.Point2D}} moves - The moves 147 | * list to process. 148 | * @param {number} vlim - The number of moves to process. 149 | * 150 | * @return {number} The angle of the movement. 151 | */ 152 | static calc_angle(moves, vlim) { 153 | const point = moves[vlim].point; 154 | let sin = 0; 155 | let cos = 0; 156 | for (let i = 0; i < vlim; ++i) { 157 | const angle = moves[i].point.angleTo(point); 158 | sin += Math.sin(angle); 159 | cos += Math.cos(angle); 160 | } 161 | sin /= vlim; 162 | cos /= vlim; 163 | return Math.atan2(sin, cos); 164 | } 165 | 166 | /** 167 | * Local helper function for calculating the velocity between two timestamped 168 | * points. 169 | * 170 | * @static 171 | * @param {object} start 172 | * @param {westures-core.Point2D} start.point 173 | * @param {number} start.time 174 | * @param {object} end 175 | * @param {westures-core.Point2D} end.point 176 | * @param {number} end.time 177 | * 178 | * @return {number} velocity from start to end point. 179 | */ 180 | static velocity(start, end) { 181 | const distance = end.point.distanceTo(start.point); 182 | const time = end.time - start.time + 1; 183 | return distance / time; 184 | } 185 | 186 | /** 187 | * Calculates the veloctiy of movement through a series of moves. 188 | * 189 | * @static 190 | * @param {{time: number, point: westures-core.Point2D}} moves - The moves 191 | * list to process. 192 | * @param {number} vlim - The number of moves to process. 193 | * 194 | * @return {number} The velocity of the moves. 195 | */ 196 | static calc_velocity(moves, vlim) { 197 | let max = 0; 198 | for (let i = 0; i < vlim; ++i) { 199 | const current = Swipe.velocity(moves[i], moves[i + 1]); 200 | if (current > max) max = current; 201 | } 202 | return max; 203 | } 204 | } 205 | 206 | module.exports = Swipe; 207 | 208 | -------------------------------------------------------------------------------- /src/Swivel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Contains the Rotate class. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const { angularDifference, Smoothable } = require('../core'); 8 | const Pivotable = require('./Pivotable.js'); 9 | 10 | /** 11 | * Data returned when a Swivel is recognized. 12 | * 13 | * @typedef {Object} SwivelData 14 | * @mixes ReturnTypes.BaseData 15 | * 16 | * @property {number} rotation - In radians, the change in angle since last 17 | * emit. 18 | * @property {westures-core.Point2D} pivot - The pivot point. 19 | * 20 | * @memberof ReturnTypes 21 | */ 22 | 23 | /** 24 | * A Swivel is a single input rotating around a fixed point. The fixed point is 25 | * determined by the input's location at its 'start' phase. 26 | * 27 | * @extends westures-core.Gesture 28 | * @see {ReturnTypes.SwivelData} 29 | * @memberof westures 30 | * 31 | * @param {Element} element - The element to which to associate the gesture. 32 | * @param {Function} handler - The function handler to execute when a gesture 33 | * is recognized on the associated element. 34 | * @param {object} [options] - Gesture customization options. 35 | * @param {westures-core.STATE_KEYS[]} [options.enableKeys=[]] - List of keys 36 | * which will enable the gesture. The gesture will not be recognized unless one 37 | * of these keys is pressed while the interaction occurs. If not specified or an 38 | * empty list, the gesture is treated as though the enable key is always down. 39 | * @param {westures-core.STATE_KEYS[]} [options.disableKeys=[]] - List of keys 40 | * which will disable the gesture. The gesture will not be recognized if one of 41 | * these keys is pressed while the interaction occurs. If not specified or an 42 | * empty list, the gesture is treated as though the disable key is never down. 43 | * @param {number} [options.minInputs=1] - The minimum number of pointers that 44 | * must be active for the gesture to be recognized. Uses >=. 45 | * @param {number} [options.maxInputs=Number.MAX_VALUE] - The maximum number of 46 | * pointers that may be active for the gesture to be recognized. Uses <=. 47 | * @param {boolean} [options.applySmoothing=true] - Whether to apply inertial 48 | * smoothing for systems with coarse pointers. 49 | * @param {number} [options.deadzoneRadius=15] - The radius in pixels around the 50 | * start point in which to do nothing. 51 | * @param {Element} [options.dynamicPivot=false] - Normally the center point of 52 | * the gesture's element is used as the pivot. If this option is set, the 53 | * initial contact point with the element is used as the pivot instead. 54 | */ 55 | class Swivel extends Pivotable { 56 | constructor(element, handler, options = {}) { 57 | super('swivel', element, handler, options); 58 | 59 | /* 60 | * The outgoing data, with optional inertial smoothing. 61 | * 62 | * @override 63 | * @type {westures-core.Smoothable} 64 | */ 65 | this.outgoing = new Smoothable(options); 66 | } 67 | 68 | updatePrevious(state) { 69 | this.previous = this.pivot.angleTo(state.centroid); 70 | } 71 | 72 | move(state) { 73 | const pivot = this.pivot; 74 | const angle = pivot.angleTo(state.centroid); 75 | const rotation = angularDifference(angle, this.previous); 76 | 77 | let rv = null; 78 | if (pivot.distanceTo(state.centroid) > this.options.deadzoneRadius) { 79 | rv = { rotation: this.outgoing.next(rotation), pivot }; 80 | } 81 | 82 | /* 83 | * Updating the previous angle regardless of emit prevents sudden flips when 84 | * the user exits the deadzone circle. 85 | */ 86 | this.previous = angle; 87 | 88 | return rv; 89 | } 90 | } 91 | 92 | module.exports = Swivel; 93 | 94 | -------------------------------------------------------------------------------- /src/Tap.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Contains the Tap class. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const { Gesture, Point2D } = require('../core'); 8 | 9 | /** 10 | * Data returned when a Tap is recognized. 11 | * 12 | * @typedef {Object} TapData 13 | * @mixes ReturnTypes.BaseData 14 | * 15 | * @property {number} x - x coordinate of tap point. 16 | * @property {number} y - y coordinate of tap point. 17 | * 18 | * @memberof ReturnTypes 19 | */ 20 | 21 | /** 22 | * A Tap is defined as a touchstart to touchend event in quick succession. 23 | * 24 | * @extends westures-core.Gesture 25 | * @see {ReturnTypes.TapData} 26 | * @memberof westures 27 | * 28 | * @param {Element} element - The element to which to associate the gesture. 29 | * @param {Function} handler - The function handler to execute when a gesture 30 | * is recognized on the associated element. 31 | * @param {object} [options] - Gesture customization options. 32 | * @param {westures-core.STATE_KEYS[]} [options.enableKeys=[]] - List of keys 33 | * which will enable the gesture. The gesture will not be recognized unless one 34 | * of these keys is pressed while the interaction occurs. If not specified or an 35 | * empty list, the gesture is treated as though the enable key is always down. 36 | * @param {westures-core.STATE_KEYS[]} [options.disableKeys=[]] - List of keys 37 | * which will disable the gesture. The gesture will not be recognized if one of 38 | * these keys is pressed while the interaction occurs. If not specified or an 39 | * empty list, the gesture is treated as though the disable key is never down. 40 | * @param {number} [options.minInputs=1] - The minimum number of pointers that 41 | * must be active for the gesture to be recognized. Uses >=. 42 | * @param {number} [options.maxInputs=Number.MAX_VALUE] - The maximum number of 43 | * pointers that may be active for the gesture to be recognized. Uses <=. 44 | * @param {number} [options.minDelay=0] - The minimum delay between a touchstart 45 | * and touchend can be configured in milliseconds. 46 | * @param {number} [options.maxDelay=300] - The maximum delay between a 47 | * touchstart and touchend can be configured in milliseconds. 48 | * @param {number} [options.maxRetain=300] - The maximum time after a tap ends 49 | * before it is discarded can be configured in milliseconds. Useful for 50 | * multi-tap gestures, to allow things like slow "double clicks". 51 | * @param {number} [options.numTaps=1] - Number of taps to require. 52 | * @param {number} [options.tolerance=10] - The tolerance in pixels an input can 53 | * move before it will no longer be considered part of a tap. 54 | */ 55 | class Tap extends Gesture { 56 | constructor(element, handler, options = {}) { 57 | super('tap', element, handler, { ...Tap.DEFAULTS, ...options }); 58 | 59 | /** 60 | * An array of inputs that have ended recently. 61 | * 62 | * @type {Input[]} 63 | */ 64 | this.taps = []; 65 | } 66 | 67 | end(state) { 68 | const now = Date.now(); 69 | const { minDelay, maxDelay, maxRetain, numTaps, tolerance } = this.options; 70 | 71 | // Save the recently ended inputs as taps. 72 | this.taps = this.taps.concat(state.getInputsInPhase('end')) 73 | .filter(input => { 74 | const elapsed = input.elapsedTime; 75 | const tdiff = now - input.current.time; 76 | return ( 77 | elapsed <= maxDelay 78 | && elapsed >= minDelay 79 | && tdiff <= maxRetain 80 | ); 81 | }); 82 | 83 | // Validate the list of taps. 84 | if (this.taps.length !== numTaps || 85 | this.taps.some(i => i.totalDistance() > tolerance)) { 86 | return null; 87 | } 88 | 89 | const centroid = Point2D.centroid(this.taps.map(i => i.current.point)); 90 | this.taps = []; // Critical! Used taps need to be cleared! 91 | return { centroid, ...centroid }; 92 | } 93 | } 94 | 95 | Tap.DEFAULTS = Object.freeze({ 96 | minDelay: 0, 97 | maxDelay: 300, 98 | maxRetain: 300, 99 | numTaps: 1, 100 | tolerance: 10, 101 | }); 102 | 103 | module.exports = Tap; 104 | 105 | -------------------------------------------------------------------------------- /src/Track.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Contains the Track class. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const { Gesture } = require('../core'); 8 | 9 | /** 10 | * Data returned when a Track is recognized. 11 | * 12 | * @typedef {Object} TrackData 13 | * @mixes ReturnTypes.BaseData 14 | * 15 | * @property {westures-core.Point2D[]} active - Points currently in 'start' or 16 | * 'move' phase. 17 | * 18 | * @memberof ReturnTypes 19 | */ 20 | 21 | /** 22 | * A Track gesture forwards a list of active points and their centroid on each 23 | * of the selected phases. 24 | * 25 | * @extends westures-core.Gesture 26 | * @see {ReturnTypes.TrackData} 27 | * @memberof westures 28 | * 29 | * @param {Element} element - The element to which to associate the gesture. 30 | * @param {Function} handler - The function handler to execute when a gesture 31 | * is recognized on the associated element. 32 | * @param {object} [options] - Gesture customization options. 33 | * @param {westures-core.STATE_KEYS[]} [options.enableKeys=[]] - List of keys 34 | * which will enable the gesture. The gesture will not be recognized unless one 35 | * of these keys is pressed while the interaction occurs. If not specified or an 36 | * empty list, the gesture is treated as though the enable key is always down. 37 | * @param {westures-core.STATE_KEYS[]} [options.disableKeys=[]] - List of keys 38 | * which will disable the gesture. The gesture will not be recognized if one of 39 | * these keys is pressed while the interaction occurs. If not specified or an 40 | * empty list, the gesture is treated as though the disable key is never down. 41 | * @param {number} [options.minInputs=1] - The minimum number of pointers that 42 | * must be active for the gesture to be recognized. Uses >=. 43 | * @param {number} [options.maxInputs=Number.MAX_VALUE] - The maximum number of 44 | * pointers that may be active for the gesture to be recognized. Uses <=. 45 | * @param {string[]} [options.phases=[]] Phases to recognize. Entries can be any 46 | * or all of 'start', 'move', 'end', and 'cancel'. 47 | */ 48 | class Track extends Gesture { 49 | constructor(element, handler, options = {}) { 50 | super('track', element, handler, { ...Track.DEFAULTS, ...options }); 51 | } 52 | 53 | /** 54 | * Filters out the state's data, down to what should be emitted. 55 | 56 | * @param {State} state - current input state. 57 | * @return {ReturnTypes.TrackData} 58 | */ 59 | data({ activePoints }) { 60 | return { active: activePoints }; 61 | } 62 | 63 | tracks(phase) { 64 | return this.options.phases.includes(phase); 65 | } 66 | 67 | start(state) { 68 | return this.tracks('start') ? this.data(state) : null; 69 | } 70 | 71 | move(state) { 72 | return this.tracks('move') ? this.data(state) : null; 73 | } 74 | 75 | end(state) { 76 | return this.tracks('end') ? this.data(state) : null; 77 | } 78 | 79 | cancel(state) { 80 | return this.tracks('cancel') ? this.data(state) : null; 81 | } 82 | } 83 | 84 | Track.DEFAULTS = Object.freeze({ 85 | phases: Object.freeze([]), 86 | }); 87 | 88 | module.exports = Track; 89 | 90 | -------------------------------------------------------------------------------- /test/Pan.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tests Pan class 3 | */ 4 | 5 | /* global expect, describe, test, jest, beforeEach, beforeAll */ 6 | 7 | 'use strict'; 8 | 9 | const Pan = require('src/Pan.js'); 10 | const { Point2D } = require('../core'); 11 | 12 | let element = null; 13 | let handler = null; 14 | 15 | beforeAll(() => { 16 | element = document.createElement('div'); 17 | document.body.appendChild(element); 18 | }); 19 | 20 | beforeEach(() => { 21 | handler = jest.fn(); 22 | }); 23 | 24 | describe('Pan', () => { 25 | describe('constructor', () => { 26 | test('Returns a Pan object', () => { 27 | expect(new Pan()).toBeInstanceOf(Pan); 28 | }); 29 | }); 30 | 31 | describe('phase hooks', () => { 32 | let pan = null; 33 | let state = null; 34 | const options = { applySmoothing: false }; 35 | 36 | beforeAll(() => { 37 | state = { 38 | centroid: new Point2D(42, 117), 39 | }; 40 | }); 41 | 42 | beforeEach(() => { 43 | pan = new Pan(element, handler, options); 44 | }); 45 | 46 | describe('start(state)', () => { 47 | test('Returns null', () => { 48 | expect(pan.start(state)).toBeFalsy(); 49 | }); 50 | }); 51 | 52 | describe('move(state)', () => { 53 | test('Returns the change vector', () => { 54 | const change = new Point2D(12, -13); 55 | const move_state = { 56 | centroid: state.centroid.plus(change), 57 | }; 58 | 59 | expect(pan.start(state)).toBeFalsy(); 60 | expect(pan.move(move_state).translation).toMatchObject(change); 61 | }); 62 | 63 | test('Returns a smoothed change vector, if using smoothing', () => { 64 | const change = new Point2D(12, -13); 65 | const move_state = { 66 | centroid: state.centroid.plus(change), 67 | }; 68 | const options = { applySmoothing: true }; 69 | const expected = Point2D.centroid([change, new Point2D(0, 0)]); 70 | 71 | pan = new Pan(element, handler, options); 72 | expect(pan.start(state)).toBeFalsy(); 73 | expect(pan.move(move_state).translation).toMatchObject(expected); 74 | }); 75 | }); 76 | 77 | describe('end(state)', () => { 78 | test('Returns null', () => { 79 | expect(pan.end(state)).toBeFalsy(); 80 | }); 81 | }); 82 | 83 | describe('cancel(state)', () => { 84 | test('Returns null', () => { 85 | expect(pan.cancel(state)).toBeFalsy(); 86 | }); 87 | }); 88 | }); 89 | 90 | describe('other prototype methods', () => { 91 | let pan = null; 92 | beforeEach(() => { 93 | pan = new Pan(element, handler); 94 | }); 95 | 96 | describe('restart(state)', () => { 97 | const state = { 98 | centroid: new Point2D(42, 117), 99 | }; 100 | 101 | test('Resets the previous value', () => { 102 | pan.restart(state); 103 | expect(pan.previous).toBe(state.centroid); 104 | }); 105 | }); 106 | }); 107 | }); 108 | 109 | -------------------------------------------------------------------------------- /test/Pinch.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tests Pinch class 3 | */ 4 | 5 | /* global expect, describe, test, jest, beforeEach, beforeAll */ 6 | 7 | 'use strict'; 8 | 9 | const Pinch = require('src/Pinch.js'); 10 | const { Point2D } = require('../core'); 11 | 12 | let element = null; 13 | let handler = null; 14 | 15 | beforeAll(() => { 16 | element = document.createElement('div'); 17 | document.body.appendChild(element); 18 | }); 19 | 20 | beforeEach(() => { 21 | handler = jest.fn(); 22 | }); 23 | 24 | describe('Pinch', () => { 25 | describe('constructor', () => { 26 | test('Returns a Pinch object', () => { 27 | expect(new Pinch()).toBeInstanceOf(Pinch); 28 | }); 29 | }); 30 | 31 | describe('phase hooks', () => { 32 | let pinch = null; 33 | let state = null; 34 | let point1 = null; 35 | let point2 = null; 36 | let point3 = null; 37 | let distance = null; 38 | let move_state = null; 39 | let move_distance = null; 40 | 41 | beforeAll(() => { 42 | point1 = new Point2D(5, 5); 43 | point2 = new Point2D(2, 1); 44 | point3 = new Point2D(4, 4); 45 | 46 | let activePoints = [point1, point2]; 47 | let centroid = Point2D.centroid(activePoints); 48 | state = { centroid, activePoints }; 49 | distance = centroid.averageDistanceTo(activePoints); 50 | 51 | activePoints = [point1, point3]; 52 | centroid = Point2D.centroid(activePoints); 53 | move_state = { activePoints, centroid }; 54 | move_distance = centroid.averageDistanceTo(activePoints); 55 | }); 56 | 57 | beforeEach(() => { 58 | pinch = new Pinch(element, handler, { applySmoothing: false }); 59 | }); 60 | 61 | describe('start(state)', () => { 62 | test('Returns null', () => { 63 | expect(pinch.start(state)).toBeFalsy(); 64 | }); 65 | }); 66 | 67 | describe('move(state)', () => { 68 | test('Returns the distance ratio from the previous emit', () => { 69 | const expected = move_distance / distance; 70 | 71 | expect(pinch.start(state)).toBeFalsy(); 72 | expect(pinch.move(move_state).scale).toBeCloseTo(expected); 73 | }); 74 | 75 | test('Returns a smoothed ratio when applySmoothing is true', () => { 76 | pinch = new Pinch(element, handler, { applySmoothing: true }); 77 | const expected = (1 + move_distance / distance) / 2; 78 | 79 | expect(pinch.start(state)).toBeFalsy(); 80 | expect(pinch.move(move_state).scale).toBeCloseTo(expected); 81 | }); 82 | }); 83 | 84 | describe('end(state)', () => { 85 | test('Returns null', () => { 86 | expect(pinch.end(state)).toBeFalsy(); 87 | }); 88 | }); 89 | 90 | describe('cancel(state)', () => { 91 | test('Returns null', () => { 92 | expect(pinch.cancel(state)).toBeFalsy(); 93 | }); 94 | }); 95 | }); 96 | 97 | describe('other prototype methods', () => { 98 | let point = null; 99 | let state = null; 100 | let pinch = null; 101 | 102 | beforeAll(() => { 103 | point = new Point2D(42, 117); 104 | state = { 105 | centroid: point, 106 | activePoints: [point], 107 | }; 108 | }); 109 | 110 | beforeEach(() => { 111 | pinch = new Pinch(element, handler); 112 | }); 113 | 114 | describe('restart(state)', () => { 115 | test( 116 | 'Resets the previous value as average distance to the active points', 117 | () => { 118 | pinch.restart(state); 119 | expect(pinch.previous).toBe(0); 120 | 121 | state.activePoints = [new Point2D(45, 121)]; 122 | const expected = state.centroid.averageDistanceTo(state.activePoints); 123 | pinch.restart(state); 124 | expect(pinch.previous).toBe(expected); 125 | }, 126 | ); 127 | }); 128 | }); 129 | }); 130 | 131 | -------------------------------------------------------------------------------- /test/Pivotable.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tests Pivotable class 3 | */ 4 | 5 | /* global expect, describe, test, jest, beforeEach, beforeAll */ 6 | 7 | 'use strict'; 8 | 9 | const Pivotable = require('src/Pivotable.js'); 10 | const { Point2D } = require('../core'); 11 | 12 | let element = null; 13 | let handler = null; 14 | 15 | beforeAll(() => { 16 | element = document.createElement('div'); 17 | element.getBoundingClientRect = jest.fn(); 18 | element.getBoundingClientRect.mockReturnValue({ 19 | left: 0, 20 | top: 0, 21 | width: 50, 22 | height: 80, 23 | }); 24 | document.body.appendChild(element); 25 | }); 26 | 27 | beforeEach(() => { 28 | handler = jest.fn(); 29 | }); 30 | 31 | describe('Pivotable', () => { 32 | describe('constructor', () => { 33 | test('Returns a Pivotable object', () => { 34 | expect(new Pivotable()).toBeInstanceOf(Pivotable); 35 | }); 36 | }); 37 | 38 | describe('updatePrevious()', () => { 39 | test('throws: is abstract interface', () => { 40 | expect(() => new Pivotable().updatePrevious()).toThrow(); 41 | }); 42 | }); 43 | 44 | describe('phase hooks', () => { 45 | let pivotable = null; 46 | let state = null; 47 | 48 | beforeEach(() => { 49 | const options = { applySmoothing: false }; 50 | pivotable = new Pivotable('testing', element, handler, options); 51 | pivotable.updatePrevious = () => {}; 52 | state = { 53 | active: [], 54 | centroid: new Point2D(42, 117), 55 | }; 56 | }); 57 | 58 | describe('start(state)', () => { 59 | test('returns null', () => { 60 | expect(pivotable.start(state)).toBeFalsy(); 61 | }); 62 | }); 63 | 64 | describe('move(state)', () => { 65 | test('returns null', () => { 66 | expect(pivotable.move(state)).toBeFalsy(); 67 | }); 68 | }); 69 | 70 | describe('end(state)', () => { 71 | test('returns null', () => { 72 | expect(pivotable.end(state)).toBeFalsy(); 73 | state.active.push('hello'); 74 | expect(pivotable.end(state)).toBeFalsy(); 75 | }); 76 | }); 77 | 78 | describe('cancel(state)', () => { 79 | test('returns null', () => { 80 | expect(pivotable.cancel(state)).toBeFalsy(); 81 | }); 82 | }); 83 | }); 84 | 85 | describe('other prototype methods', () => { 86 | let pivotable = null; 87 | let state = null; 88 | 89 | beforeEach(() => { 90 | const options = { applySmoothing: false }; 91 | pivotable = new Pivotable('testing', element, handler, options); 92 | pivotable.updatePrevious = () => {}; 93 | state = { 94 | active: [], 95 | centroid: new Point2D(42, 117), 96 | }; 97 | }); 98 | 99 | describe('updatePrevious(state)', () => { 100 | test('is a NOP in the Pivotable base class', () => { 101 | const previous = pivotable.previous; 102 | expect(pivotable.updatePrevious(state)).toBeFalsy(); 103 | expect(pivotable.previous).toBe(previous); 104 | }); 105 | }); 106 | 107 | describe('restart(state)', () => { 108 | test('by default, sets the pivot to the center of the element', () => { 109 | expect(pivotable.restart(state)).toBeFalsy(); 110 | expect(pivotable.pivot) 111 | .toMatchObject(Pivotable.getClientCenter(pivotable.element)); 112 | }); 113 | 114 | test('if using dynamicPivot, sets the pivot to the centroid', () => { 115 | const options = { applySmoothing: false, dynamicPivot: true }; 116 | pivotable = new Pivotable('testing', element, handler, options); 117 | expect(pivotable.restart(state)).toBeFalsy(); 118 | expect(pivotable.pivot).toBe(state.centroid); 119 | }); 120 | }); 121 | }); 122 | }); 123 | 124 | -------------------------------------------------------------------------------- /test/Press.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tests Press class 3 | */ 4 | 5 | /* global expect, describe, test, jest, beforeEach, beforeAll */ 6 | 7 | 'use strict'; 8 | 9 | const Press = require('src/Press.js'); 10 | const { Point2D, Input } = require('core'); 11 | const MouseEvent = require('core/test/MouseEvent.js'); 12 | 13 | let element = null; 14 | let handler = null; 15 | 16 | beforeAll(() => { 17 | element = document.createElement('div'); 18 | element.getBoundingClientRect = jest.fn(); 19 | element.getBoundingClientRect.mockReturnValue({ 20 | left: 0, 21 | top: 0, 22 | width: 50, 23 | height: 80, 24 | }); 25 | document.body.appendChild(element); 26 | }); 27 | 28 | beforeEach(() => { 29 | handler = jest.fn(); 30 | }); 31 | 32 | describe('Press', () => { 33 | describe('constructor', () => { 34 | test('Returns a Press object', () => { 35 | expect(new Press()).toBeInstanceOf(Press); 36 | }); 37 | }); 38 | 39 | describe('phase hooks', () => { 40 | let press = null; 41 | let state = null; 42 | let event = null; 43 | let input = null; 44 | const options = { applySmoothing: false }; 45 | 46 | beforeEach(() => { 47 | event = new MouseEvent(0, element, MouseEvent.start, 42, 117); 48 | input = new Input(event, event.id); 49 | state = { 50 | centroid: new Point2D(42, 117), 51 | active: [input], 52 | }; 53 | setTimeout.mockClear(); 54 | press = new Press(element, handler, options); 55 | }); 56 | 57 | describe.each(['move', 'end', 'cancel'])('%s(state)', (phase) => { 58 | test('Returns null', () => { 59 | expect(press[phase](state)).toBeFalsy(); 60 | }); 61 | 62 | test('Timers are not set', () => { 63 | press[phase](state); 64 | expect(setTimeout).not.toHaveBeenCalled(); 65 | }); 66 | 67 | test('Handler is not called', () => { 68 | press[phase](state); 69 | expect(press.handler).not.toHaveBeenCalled(); 70 | }); 71 | }); 72 | 73 | describe('start(state)', () => { 74 | test('Returns null', () => { 75 | expect(press.start(state)).toBeFalsy(); 76 | }); 77 | 78 | test('Timer is set', () => { 79 | press.start(state); 80 | expect(setTimeout).toHaveBeenCalled(); 81 | }); 82 | 83 | test('Handler is called if delay is reached', () => { 84 | press.start(state); 85 | jest.runAllTimers(); 86 | expect(press.handler).toHaveBeenCalled(); 87 | }); 88 | 89 | test('Handler is still called if additional pointers added', () => { 90 | press.start(state); 91 | state.active.push(new Input(new MouseEvent( 92 | 0, element, MouseEvent.start, 93 | 107, 222, 94 | ))); 95 | jest.runAllTimers(); 96 | expect(press.handler).toHaveBeenCalled(); 97 | }); 98 | 99 | test('Handler is not called if pointer removed before timeout', () => { 100 | press.start(state); 101 | state.active = []; 102 | jest.runAllTimers(); 103 | expect(press.handler).not.toHaveBeenCalled(); 104 | }); 105 | 106 | test('Handler is not called if pointer moves beyond threshold', () => { 107 | press.start(state); 108 | input.update(new MouseEvent(0, element, MouseEvent.move, -123, -524)); 109 | jest.runAllTimers(); 110 | expect(press.handler).not.toHaveBeenCalled(); 111 | }); 112 | 113 | test('Handler is not called if pointer replaced with new pointer', () => { 114 | // Event though it's in exactly the same spot... 115 | press.start(state); 116 | state.active = [new Input(event, event.id)]; 117 | jest.runAllTimers(); 118 | expect(press.handler).not.toHaveBeenCalled(); 119 | }); 120 | }); 121 | }); 122 | }); 123 | 124 | -------------------------------------------------------------------------------- /test/Pull.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tests Pull class 3 | */ 4 | 5 | /* global expect, describe, test, beforeEach, jest */ 6 | 7 | 'use strict'; 8 | 9 | const Pull = require('src/Pull.js'); 10 | const { Point2D } = require('core'); 11 | 12 | describe('Pull', () => { 13 | let centroid = null; 14 | let element = null; 15 | let expected_distance = null; 16 | let handler = null; 17 | let pivot = null; 18 | let state = null; 19 | 20 | beforeEach(() => { 21 | // diff x, y components to get 3rd point of 3x4x5 triangle 22 | pivot = new Point2D(0, 0); 23 | centroid = new Point2D(3, 4); 24 | 25 | element = document.createElement('div'); 26 | expected_distance = 5; 27 | handler = jest.fn(); 28 | state = { centroid }; 29 | }); 30 | 31 | describe('constructor', () => { 32 | test('Returns a Pull object', () => { 33 | expect(new Pull()).toBeInstanceOf(Pull); 34 | }); 35 | }); 36 | 37 | describe('updatePrevious()', () => { 38 | test('stores distance to pivot in previous', () => { 39 | const pull = new Pull(element, handler); 40 | pull.pivot = pivot; 41 | expect(() => pull.updatePrevious(state)) 42 | .not.toThrow(); 43 | expect(pull.previous).toBe(expected_distance); 44 | }); 45 | }); 46 | 47 | describe('move(state)', () => { 48 | test('returns null if new point within deadzoneRadius', () => { 49 | const pull = new Pull(element, handler, { 50 | deadzoneRadius: expected_distance + 1, 51 | }); 52 | pull.start({ centroid: new Point2D(6, 8) }); 53 | expect(pull.move(state)).toBeFalsy(); 54 | }); 55 | 56 | test('returns null if previous point within deadzoneRadius', () => { 57 | const pull = new Pull(element, handler, { 58 | deadzoneRadius: expected_distance + 1, 59 | }); 60 | pull.start({ centroid }); 61 | expect(pull.move({ centroid: new Point2D(42, 1337) })).toBeFalsy(); 62 | }); 63 | 64 | test('returns distance, scale, and pivot on regocnition', () => { 65 | const pull = new Pull(element, handler, { 66 | deadzoneRadius: expected_distance / 2, 67 | }); 68 | pull.start({ centroid: new Point2D(6, 8) }); 69 | const result = pull.move({ centroid }); 70 | expect(result).toMatchObject({ 71 | distance: expected_distance, 72 | pivot: pivot, 73 | 74 | // scale here goes from a dist of 10 to a dist of 5, which looks like it 75 | // should be 0.5, but remember there is smoothing applied, so it gets 76 | // softened to 0.75. 77 | scale: 0.75, 78 | }); 79 | }); 80 | }); 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /test/Rotate.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tests Rotate class 3 | */ 4 | 5 | /* global expect, describe, test, jest, beforeEach, beforeAll */ 6 | 7 | 'use strict'; 8 | 9 | const Rotate = require('src/Rotate.js'); 10 | const { Point2D, angularDifference } = require('../core'); 11 | const _ = require('underscore'); 12 | 13 | let element = null; 14 | let handler = null; 15 | 16 | beforeAll(() => { 17 | element = document.createElement('div'); 18 | document.body.appendChild(element); 19 | }); 20 | 21 | beforeEach(() => { 22 | handler = jest.fn(); 23 | }); 24 | 25 | describe('Rotate', () => { 26 | describe('constructor', () => { 27 | test('Returns a Rotate object', () => { 28 | expect(new Rotate()).toBeInstanceOf(Rotate); 29 | }); 30 | }); 31 | 32 | describe('phase hooks', () => { 33 | let rotate = null; 34 | let state = null; 35 | let point1 = null; 36 | let point2 = null; 37 | let point3 = null; 38 | let angles = null; 39 | let move_state = null; 40 | let move_angles = null; 41 | 42 | beforeAll(() => { 43 | point1 = new Point2D(5, 5); 44 | point2 = new Point2D(2, 1); 45 | point3 = new Point2D(4, 4); 46 | 47 | let activePoints = [point1, point2]; 48 | let centroid = Point2D.centroid(activePoints); 49 | state = { centroid, activePoints }; 50 | angles = centroid.anglesTo(activePoints); 51 | 52 | activePoints = [point1, point3]; 53 | centroid = Point2D.centroid(activePoints); 54 | move_state = { activePoints, centroid }; 55 | move_angles = centroid.anglesTo(activePoints); 56 | }); 57 | 58 | beforeEach(() => { 59 | rotate = new Rotate(element, handler, { applySmoothing: false }); 60 | }); 61 | 62 | describe('start(state)', () => { 63 | test('Returns null', () => { 64 | expect(rotate.start(state)).toBeFalsy(); 65 | }); 66 | }); 67 | 68 | describe('move(state)', () => { 69 | test('Returns the change in angle from the previous emit', () => { 70 | const deltas = _.zip(angles, move_angles) 71 | .map(([angle, move_angle]) => { 72 | return angularDifference(move_angle, angle); 73 | }); 74 | const sum = deltas.reduce((total, current) => total + current); 75 | const expected = sum / angles.length; 76 | 77 | expect(rotate.start(state)).toBeFalsy(); 78 | expect(rotate.move(move_state).rotation).toBeCloseTo(expected); 79 | }); 80 | 81 | test('Returns a smoothed ratio when applySmoothing is true', () => { 82 | rotate = new Rotate(element, handler, { applySmoothing: true }); 83 | const deltas = _.zip(angles, move_angles) 84 | .map(([angle, move_angle]) => { 85 | return angularDifference(move_angle, angle); 86 | }); 87 | const sum = deltas.reduce((total, current) => total + current); 88 | const expected = (sum / angles.length) / 2; 89 | 90 | expect(rotate.start(state)).toBeFalsy(); 91 | expect(rotate.move(move_state).rotation).toBeCloseTo(expected); 92 | }); 93 | }); 94 | 95 | describe('end(state)', () => { 96 | test('Returns null', () => { 97 | expect(rotate.end(state)).toBeFalsy(); 98 | }); 99 | }); 100 | 101 | describe('cancel(state)', () => { 102 | test('Returns null', () => { 103 | expect(rotate.cancel(state)).toBeFalsy(); 104 | }); 105 | }); 106 | }); 107 | }); 108 | 109 | -------------------------------------------------------------------------------- /test/Swipe.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tests Swipe class 3 | */ 4 | 5 | /* global expect, describe, test, jest, beforeEach, beforeAll */ 6 | 7 | 'use strict'; 8 | 9 | const Swipe = require('src/Swipe.js'); 10 | const { Point2D } = require('core'); 11 | 12 | let element = null; 13 | let handler = null; 14 | let last_time = 0; 15 | 16 | beforeAll(() => { 17 | element = document.createElement('div'); 18 | document.body.appendChild(element); 19 | 20 | // increase time by 10 every time this is called 21 | jest.spyOn(global.Date, 'now') 22 | .mockImplementation(() => { 23 | last_time += 10; 24 | return last_time; 25 | }); 26 | }); 27 | 28 | beforeEach(() => { 29 | handler = jest.fn(); 30 | }); 31 | 32 | describe('Swipe', () => { 33 | describe('constructor', () => { 34 | test('Returns a Swipe object', () => { 35 | expect(new Swipe()).toBeInstanceOf(Swipe); 36 | }); 37 | }); 38 | 39 | describe('phase hooks', () => { 40 | let swipe = null; 41 | let state = null; 42 | let point = null; 43 | 44 | beforeAll(() => { 45 | point = new Point2D(0, 0); 46 | state = { active: [point], centroid: point }; 47 | }); 48 | 49 | beforeEach(() => { 50 | swipe = new Swipe(element, handler); 51 | }); 52 | 53 | describe('start(state)', () => { 54 | test('Returns null', () => { 55 | expect(swipe.start(state)).toBeFalsy(); 56 | }); 57 | }); 58 | 59 | describe('move(state)', () => { 60 | test('Returns null', () => { 61 | expect(swipe.start(state)).toBeFalsy(); 62 | expect(swipe.move(state)).toBeFalsy(); 63 | }); 64 | 65 | test('Only keeps up to PROGRESS_STACK_SIZE moves', () => { 66 | expect(swipe.start(state)).toBeFalsy(); 67 | // at first the moves list grows 68 | for (let i = 0; i < 7; ++i) { 69 | point = new Point2D(i, i); 70 | state = { active: [point], centroid: point }; 71 | expect(swipe.move(state)).toBeFalsy(); 72 | expect(swipe.moves.length).toBe(i + 1); 73 | expect(swipe.moves[0].point).toMatchObject(new Point2D(0, 0)); 74 | expect(swipe.moves[swipe.moves.length - 1].point).toBe(point); 75 | } 76 | // from now on moves length is limited to 7 77 | for (let i = 7; i < 12; ++i) { 78 | point = new Point2D(i, i); 79 | state = { active: [point], centroid: point }; 80 | expect(swipe.move(state)).toBeFalsy(); 81 | expect(swipe.moves.length).toBe(7); 82 | // items are removed from the front 83 | expect(swipe.moves[0].point).toMatchObject(new Point2D(i - 6, i - 6)); 84 | expect(swipe.moves[6].point).toBe(point); 85 | } 86 | }); 87 | }); 88 | 89 | describe('end(state)', () => { 90 | test('returns null if less than 7 moves', () => { 91 | expect(swipe.start(state)).toBeFalsy(); 92 | for (let i = 0; i < 6; ++i) { 93 | point = new Point2D(i, i); 94 | state = { active: [point], centroid: point }; 95 | expect(swipe.move(state)).toBeFalsy(); 96 | } 97 | state = { 98 | active: [], 99 | centroid: null, 100 | }; 101 | expect(swipe.end(state)).toBeFalsy(); 102 | }); 103 | 104 | test('returns null if there are still active points', () => { 105 | expect(swipe.start(state)).toBeFalsy(); 106 | const stable_point = new Point2D(0, 0); 107 | state = { active: [point, stable_point], centroid: point }; 108 | expect(swipe.start(state)).toBeFalsy(); 109 | for (let i = 0; i < 10; ++i) { 110 | point = new Point2D(i, i); 111 | state = { active: [point, stable_point], centroid: point }; 112 | expect(swipe.move(state)).toBeFalsy(); 113 | } 114 | state = { 115 | active: [stable_point], 116 | centroid: stable_point, 117 | }; 118 | expect(swipe.end(state)).toBeFalsy(); 119 | }); 120 | 121 | test('returns SwipeData when valid', () => { 122 | expect(swipe.start(state)).toBeFalsy(); 123 | for (let i = 1; i < 10; ++i) { 124 | point = new Point2D(i, i); 125 | state = { active: [point], centroid: point }; 126 | expect(swipe.move(state)).toBeFalsy(); 127 | } 128 | state = { 129 | active: [], 130 | centroid: null, 131 | }; 132 | const expected = { 133 | point: point, 134 | centroid: point, 135 | time: last_time, 136 | direction: Math.atan2(1, 1), // proceeding perfectly at 45 degrees 137 | velocity: Math.sqrt(2) / 11, 138 | }; 139 | expect(swipe.end(state)).toMatchObject(expected); 140 | }); 141 | 142 | test('Uses cached result if last point does not swipe', () => { 143 | // More specifically, if a swipe would have been detected if not for a 144 | // final touch point hanging around too long, but that touch point is 145 | // released before resulting in a new swipe and without holding around 146 | // too long, the saved result is used. This is to make sure that 147 | // multi-touch swipes work even if not all the points are removed at 148 | // once. 149 | expect(swipe.start(state)).toBeFalsy(); 150 | const stable_point = new Point2D(0, 0); 151 | state = { active: [point, stable_point], centroid: point }; 152 | expect(swipe.start(state)).toBeFalsy(); 153 | for (let i = 1; i < 10; ++i) { 154 | point = new Point2D(i, i); 155 | // we'll cheat a little bit and pretend stable_point doesn't affect 156 | // the centroid, so that the math will be easier to predict 157 | state = { active: [point, stable_point], centroid: point }; 158 | expect(swipe.move(state)).toBeFalsy(); 159 | } 160 | state = { 161 | active: [stable_point], 162 | centroid: stable_point, 163 | }; 164 | expect(swipe.end(state)).toBeFalsy(); 165 | 166 | // Now this end should work 167 | state = { 168 | active: [], 169 | centroid: null, 170 | }; 171 | const expected = { 172 | point: point, 173 | centroid: point, 174 | time: last_time, 175 | direction: Math.atan2(1, 1), // proceeding perfectly at 45 degrees 176 | velocity: Math.sqrt(2) / 11, 177 | }; 178 | expect(swipe.end(state)).toMatchObject(expected); 179 | }); 180 | }); 181 | 182 | describe('cancel(state)', () => { 183 | test('Returns null', () => { 184 | expect(swipe.cancel(state)).toBeFalsy(); 185 | }); 186 | }); 187 | }); 188 | 189 | describe('static methods', () => { 190 | describe('validate(data)', () => { 191 | test('null if data is null', () => { 192 | expect(Swipe.validate(null)).toBeNull(); 193 | }); 194 | 195 | test('null if too old', () => { 196 | const data = { time: Date.now() - 1000 }; 197 | expect(Swipe.validate(data)).toBeNull(); 198 | }); 199 | 200 | test('returns data back if valid', () => { 201 | const data = { time: Date.now(), foo: 'bar' }; 202 | expect(Swipe.validate(data)).toBe(data); 203 | }); 204 | }); 205 | 206 | describe('velocity(start, end)', () => { 207 | test('Calculates dx/dt', () => { 208 | const start = { 209 | point: new Point2D(0, 0), 210 | time: 0, 211 | }; 212 | const end = { 213 | point: new Point2D(3, 4), 214 | time: 9, // will get +1 to avoid div by zero 215 | }; 216 | expect(Swipe.velocity(start, end)).toBeCloseTo(0.5); 217 | }); 218 | }); 219 | 220 | describe('calc_velocity(moves, vlim)', () => { 221 | test('Given vlim of 0, reports 0', () => { 222 | const moves = [ 223 | { point: new Point2D(3, 4), time: 0 }, 224 | ]; 225 | expect(Swipe.calc_velocity(moves, 0)).toBe(0); 226 | }); 227 | 228 | test('Given vlim of 1, reports velocity of that move', () => { 229 | const moves = [ 230 | { point: new Point2D(0, 0), time: 0 }, 231 | { point: new Point2D(3, 4), time: 9 }, 232 | ]; 233 | const vlim = 1; 234 | expect(Swipe.calc_velocity(moves, vlim)).toBeCloseTo(0.5); 235 | }); 236 | 237 | test('Given vlim >1, reports max velocity', () => { 238 | let moves = [ 239 | { point: new Point2D(0, 0), time: 0 }, 240 | { point: new Point2D(3, 4), time: 9 }, // 0.5 241 | { point: new Point2D(6, 8), time: 28 }, // 0.25 242 | ]; 243 | const vlim = 2; 244 | expect(Swipe.calc_velocity(moves, vlim)).toBeCloseTo(0.5); 245 | moves = [ 246 | { point: new Point2D(0, 0), time: 0 }, 247 | { point: new Point2D(3, 4), time: 9 }, // 0.5 248 | { point: new Point2D(6, 8), time: 13 }, // 1 249 | ]; 250 | expect(Swipe.calc_velocity(moves, vlim)).toBeCloseTo(1); 251 | }); 252 | }); 253 | 254 | describe('calc_angle(moves, vlim)', () => { 255 | test('Given vlim of 0, reports NaN (div by zero)', () => { 256 | const moves = [ 257 | { point: new Point2D(3, 4) }, 258 | ]; 259 | expect(Swipe.calc_angle(moves, 0)).toBeNaN(); 260 | }); 261 | 262 | test('Given vlim of 1, calculates angle to second point', () => { 263 | const moves = [ 264 | { point: new Point2D(0, 0) }, 265 | { point: new Point2D(3, 4) }, 266 | ]; 267 | expect(Swipe.calc_angle(moves, 1)).toBeCloseTo(Math.atan2(4, 3)); 268 | }); 269 | 270 | test('Given vlim >1, calculates circular mean to last point', () => { 271 | const moves = [ 272 | { point: new Point2D(0, 0) }, 273 | { point: new Point2D(1, 2) }, 274 | { point: new Point2D(3, 4) }, 275 | ]; 276 | const vlim = 2; 277 | const expected = (Math.atan2(4, 3) + Math.atan2(2, 2)) / vlim; 278 | expect(Swipe.calc_angle(moves, vlim)).toBeCloseTo(expected); 279 | }); 280 | }); 281 | }); 282 | }); 283 | -------------------------------------------------------------------------------- /test/Swivel.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tests Swivel class 3 | */ 4 | 5 | /* global expect, describe, test, jest, beforeEach, beforeAll */ 6 | 7 | 'use strict'; 8 | 9 | const Swivel = require('src/Swivel.js'); 10 | const { Point2D } = require('../core'); 11 | 12 | let element = null; 13 | let handler = null; 14 | 15 | beforeAll(() => { 16 | element = document.createElement('div'); 17 | element.getBoundingClientRect = jest.fn(); 18 | element.getBoundingClientRect.mockReturnValue({ 19 | left: 0, 20 | top: 0, 21 | width: 50, 22 | height: 80, 23 | }); 24 | document.body.appendChild(element); 25 | }); 26 | 27 | beforeEach(() => { 28 | handler = jest.fn(); 29 | }); 30 | 31 | describe('Swivel', () => { 32 | describe('constructor', () => { 33 | test('Returns a Swivel object', () => { 34 | expect(new Swivel()).toBeInstanceOf(Swivel); 35 | }); 36 | }); 37 | 38 | describe('phase hooks', () => { 39 | let swivel = null; 40 | let state = null; 41 | 42 | beforeEach(() => { 43 | const options = { applySmoothing: false }; 44 | swivel = new Swivel(element, handler, options); 45 | state = { 46 | active: [], 47 | centroid: new Point2D(42, 117), 48 | }; 49 | }); 50 | 51 | describe('start(state)', () => { 52 | test('returns null', () => { 53 | expect(swivel.start(state)).toBeFalsy(); 54 | }); 55 | }); 56 | 57 | describe('move(state)', () => { 58 | test('returns 0 if move somehow shows no change', () => { 59 | expect(swivel.start(state)).toBeFalsy(); 60 | const expected = { 61 | 'pivot': { 62 | 'x': 25, 63 | 'y': 40, 64 | }, 65 | 'rotation': 0, 66 | }; 67 | // Passing the same state back in! 68 | expect(swivel.move(state)).toMatchObject(expected); 69 | }); 70 | 71 | test('returns null if the point is in the deadzone', () => { 72 | expect(swivel.start(state)).toBeFalsy(); 73 | state = { 74 | centroid: { 'x': 26, 'y': 39 }, 75 | }; 76 | expect(swivel.move(state)).toBeFalsy(); 77 | }); 78 | 79 | test('updates previous if the point is in the deadzone', () => { 80 | // This is to avoid sudden flips when exiting the deadzone 81 | expect(swivel.start(state)).toBeFalsy(); 82 | const previous = swivel.previous; 83 | state = { 84 | centroid: { 'x': 26, 'y': 39 }, 85 | }; 86 | expect(swivel.move(state)).toBeFalsy(); 87 | expect(swivel.previous).not.toEqual(previous); 88 | }); 89 | 90 | test('Reports change in angle from previous', () => { 91 | expect(swivel.start(state)).toBeFalsy(); 92 | const previous = swivel.previous; 93 | state = { 94 | centroid: { 95 | 'x': state.centroid.x + 2, 96 | 'y': state.centroid.y - 2, 97 | }, 98 | }; 99 | const expected = { 100 | 'pivot': { 101 | 'x': 25, 102 | 'y': 40, 103 | }, 104 | 'rotation': -0.03082001817365554, 105 | }; 106 | expect(swivel.move(state)).toMatchObject(expected); 107 | expect(swivel.previous).not.toEqual(previous); 108 | }); 109 | }); 110 | 111 | describe('end(state)', () => { 112 | test('returns null', () => { 113 | expect(swivel.end(state)).toBeFalsy(); 114 | }); 115 | }); 116 | 117 | describe('cancel(state)', () => { 118 | test('returns null', () => { 119 | expect(swivel.cancel(state)).toBeFalsy(); 120 | }); 121 | }); 122 | }); 123 | 124 | describe('other prototype methods', () => { 125 | }); 126 | }); 127 | 128 | -------------------------------------------------------------------------------- /test/Tap.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tests Tap class 3 | */ 4 | 5 | /* global expect, describe, test, beforeEach, beforeAll, afterEach, jest */ 6 | 7 | 'use strict'; 8 | 9 | const Tap = require('src/Tap.js'); 10 | const { State, Point2D } = require('core'); 11 | const PointerEvent = require('core/test/PointerEvent.js'); 12 | 13 | describe('Tap', () => { 14 | describe('constructor', () => { 15 | test('Returns a Tap object', () => { 16 | expect(new Tap()).toBeInstanceOf(Tap); 17 | }); 18 | 19 | test('Accepts delays and number of taps as options parameters', () => { 20 | const tap = new Tap(null, null, { 21 | maxDelay: 2000, 22 | minDelay: 500, 23 | maxRetain: 1200, 24 | numTaps: 2, 25 | tolerance: 42, 26 | }); 27 | expect(tap.options.maxRetain).toEqual(1200); 28 | expect(tap.options.maxDelay).toEqual(2000); 29 | expect(tap.options.minDelay).toEqual(500); 30 | expect(tap.options.numTaps).toEqual(2); 31 | expect(tap.options.tolerance).toEqual(42); 32 | }); 33 | }); 34 | 35 | describe('end(state)', () => { 36 | const start_time = 1000; 37 | const element = document.createElement('div'); 38 | document.body.appendChild(element); 39 | 40 | let handler = null; 41 | let state = null; 42 | let real_Date_now = null; 43 | 44 | function add_event(identifier, phase, x, y, time_offset = 0) { 45 | Date.now = () => start_time + time_offset; 46 | const event = new PointerEvent( 47 | identifier, element, 48 | PointerEvent[phase], 49 | x, y, 50 | ); 51 | state.updateAllInputs(event); 52 | } 53 | 54 | beforeAll(() => { 55 | // Need to mock this out so tests will be deterministic (not reliant on 56 | // how fast the test happens to run, etc). 57 | real_Date_now = Date.now; 58 | }); 59 | 60 | afterEach(() => { 61 | Date.now = real_Date_now; 62 | state = null; // Just to make sure there's no cross-contamination 63 | }); 64 | 65 | beforeEach(() => { 66 | Date.now = () => start_time; 67 | handler = jest.fn(); 68 | state = new State(element); 69 | }); 70 | 71 | describe('Return Value', () => { 72 | test('Returns the ended point position for a single tap', () => { 73 | add_event(0, 'start', 0, 0); 74 | add_event(0, 'end', 1, 2); 75 | const tap = new Tap(element, handler, { numTaps: 1 }); 76 | expect(tap.end(state)).toMatchObject({ x: 1, y: 2 }); 77 | }); 78 | 79 | test('Return object includes centroid point object', () => { 80 | add_event(0, 'start', 0, 0); 81 | add_event(0, 'end', 1, 2); 82 | const tap = new Tap(element, handler, { numTaps: 1 }); 83 | expect(tap.end(state)).toMatchObject({ 84 | centroid: new Point2D(1, 2), 85 | }); 86 | }); 87 | 88 | test('Returns centroid of end points', () => { 89 | add_event(0, 'start', 0, 0); 90 | add_event(1, 'start', 0, 0); 91 | add_event(0, 'end', 10, 100); 92 | add_event(1, 'end', 20, 200); 93 | const tap = new Tap(element, handler, { 94 | numTaps: 2, 95 | tolerance: 500, 96 | }); 97 | expect(tap.end(state)).toMatchObject({ x: 15, y: 150 }); 98 | }); 99 | }); 100 | 101 | describe('Accepts', () => { 102 | test('Correct number of taps', () => { 103 | add_event(0, 'start', 0, 0); 104 | add_event(0, 'end', 3, 4); 105 | const tap = new Tap(element, handler, { numTaps: 1 }); 106 | expect(tap.end(state)).toMatchObject({ x: 3, y: 4 }); 107 | }); 108 | 109 | test('Input travel is inside tolerance', () => { 110 | add_event(0, 'start', 0, 0); 111 | add_event(0, 'end', 3, 4); 112 | const tap = new Tap(element, handler, { 113 | numTaps: 1, 114 | tolerance: 100, // expected distance is 5 115 | }); 116 | expect(tap.end(state)).toMatchObject({ x: 3, y: 4 }); 117 | }); 118 | 119 | test('Input travel tolerance bound is inclusive', () => { 120 | add_event(0, 'start', 0, 0); 121 | add_event(0, 'end', 3, 4); 122 | const tap = new Tap(element, handler, { 123 | numTaps: 1, 124 | tolerance: 5, // expected distance is 5 125 | }); 126 | expect(tap.end(state)).toMatchObject({ x: 3, y: 4 }); 127 | }); 128 | 129 | test('Tap time is between minDelay and maxDelay', () => { 130 | add_event(0, 'start', 0, 0); 131 | add_event(0, 'end', 3, 4, 150); 132 | const tap = new Tap(element, handler, { 133 | numTaps: 1, 134 | minDelay: 100, 135 | maxDelay: 200, 136 | }); 137 | expect(tap.end(state)).toMatchObject({ x: 3, y: 4 }); 138 | }); 139 | 140 | test('minDelay and maxDelay bounds are inclusive', () => { 141 | add_event(0, 'start', 0, 0); 142 | add_event(0, 'end', 3, 4, 150); 143 | const tap = new Tap(element, handler, { 144 | numTaps: 1, 145 | minDelay: 150, 146 | maxDelay: 150, 147 | }); 148 | expect(tap.end(state)).toMatchObject({ x: 3, y: 4 }); 149 | }); 150 | }); 151 | 152 | describe('Rejects', () => { 153 | test('Too few taps', () => { 154 | add_event(0, 'start', 0, 0); 155 | add_event(0, 'end', 3, 4); 156 | const tap = new Tap(element, handler, { numTaps: 2 }); 157 | expect(tap.end(state)).toBeNull(); 158 | }); 159 | 160 | test('Too many taps', () => { 161 | add_event(0, 'start', 0, 0); 162 | add_event(1, 'start', 0, 0); 163 | add_event(0, 'end', 10, 100); 164 | add_event(1, 'end', 20, 200); 165 | const tap = new Tap(element, handler, { numTaps: 1 }); 166 | expect(tap.end(state)).toBeNull(); 167 | }); 168 | 169 | test('Input travel is outside tolerance', () => { 170 | add_event(0, 'start', 0, 0); 171 | add_event(0, 'end', 3, 4); 172 | const tap = new Tap(element, handler, { 173 | numTaps: 1, 174 | tolerance: 4.9999, // expected distance is 5 175 | }); 176 | expect(tap.end(state)).toBeNull(); 177 | }); 178 | 179 | test('Tap time is less than minDelay', () => { 180 | add_event(0, 'start', 0, 0); 181 | add_event(0, 'end', 3, 4, 150); 182 | const tap = new Tap(element, handler, { 183 | numTaps: 1, 184 | minDelay: 151, 185 | }); 186 | expect(tap.end(state)).toBeNull(); 187 | }); 188 | 189 | test('Tap time is greater than maxDelay', () => { 190 | add_event(0, 'start', 0, 0); 191 | add_event(0, 'end', 3, 4, 150); 192 | const tap = new Tap(element, handler, { 193 | numTaps: 1, 194 | maxDelay: 149, 195 | }); 196 | expect(tap.end(state)).toBeNull(); 197 | }); 198 | }); 199 | 200 | describe('Miscellaneous', () => { 201 | test('Keeps track of ended inputs', () => { 202 | // The state will have its ended inputs cleared, we need to make sure 203 | // that doesn't affect Taps. 204 | const tap = new Tap(element, handler, { 205 | numTaps: 2, 206 | maxDelay: 250, 207 | }); 208 | 209 | add_event(0, 'start', 0, 0); 210 | add_event(1, 'start', 0, 0, 100); 211 | add_event(0, 'end', 2, 4, 150); 212 | 213 | expect(tap.end(state)).toBeNull(); 214 | state.clearEndedInputs(); 215 | 216 | add_event(1, 'end', 4, 6, 200); 217 | expect(tap.end(state)).toMatchObject({ x: 3, y: 5 }); 218 | }); 219 | 220 | test('Inputs can only be used once for a tap', () => { 221 | const tap = new Tap(element, handler, { 222 | numTaps: 2, 223 | maxDelay: 250, 224 | }); 225 | 226 | add_event(0, 'start', 0, 0); 227 | add_event(1, 'start', 0, 0, 100); 228 | add_event(0, 'end', 2, 4, 150); 229 | add_event(1, 'end', 4, 6, 200); 230 | 231 | expect(tap.end(state)).toMatchObject({ x: 3, y: 5 }); 232 | state.clearEndedInputs(); 233 | 234 | add_event(2, 'start', 0, 0, 250); 235 | add_event(2, 'end', 0, 0, 300); 236 | 237 | expect(tap.end(state)).toBeNull(); 238 | }); 239 | 240 | test('Inputs that took too long are discarded', () => { 241 | const tap = new Tap(element, handler, { 242 | numTaps: 2, 243 | maxDelay: 250, 244 | maxRetain: 200, 245 | tolerance: 10, 246 | }); 247 | 248 | add_event(0, 'start', 0, 0); 249 | add_event(1, 'start', 0, 0, 100); 250 | add_event(0, 'end', 200, 400, 150); 251 | 252 | expect(tap.end(state)).toBeNull(); 253 | state.clearEndedInputs(); 254 | 255 | add_event(1, 'end', 4, 6, 200); 256 | 257 | // Should fail because input 0 traveled too far 258 | expect(tap.end(state)).toBeNull(); 259 | state.clearEndedInputs(); 260 | 261 | add_event(2, 'start', 0, 0, 250); 262 | add_event(2, 'end', 0, 0, 375); 263 | 264 | // input 0 should now be cleared 265 | expect(tap.end(state)).toMatchObject({ x: 2, y: 3 }); 266 | }); 267 | 268 | test('Inputs that were too short are ignored', () => { 269 | const tap = new Tap(element, handler, { 270 | numTaps: 2, 271 | minDelay: 250, 272 | tolerance: 10, 273 | }); 274 | 275 | add_event(0, 'start', 0, 0); 276 | add_event(1, 'start', 0, 0, 200); 277 | add_event(2, 'start', 0, 0, 200); 278 | add_event(0, 'end', 4, 6, 300); 279 | 280 | // Should fail because of insufficient ended taps 281 | expect(tap.end(state)).toBeNull(); 282 | expect(tap.taps.length).toBe(1); 283 | state.clearEndedInputs(); 284 | 285 | add_event(1, 'end', 1, 2, 400); 286 | 287 | // Should fail because input 1 didn't take long enough 288 | expect(tap.end(state)).toBeNull(); 289 | expect(tap.taps.length).toBe(1); 290 | state.clearEndedInputs(); 291 | 292 | add_event(2, 'end', 0, 0, 500); 293 | 294 | // input 1 should now be cleared, input 2 should be recognized 295 | expect(tap.end(state)).toMatchObject({ x: 2, y: 3 }); 296 | }); 297 | }); 298 | }); 299 | }); 300 | 301 | -------------------------------------------------------------------------------- /test/Track.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tests Track class 3 | */ 4 | 5 | /* global expect, describe, test, jest, beforeAll, beforeEach */ 6 | 7 | 'use strict'; 8 | 9 | const Track = require('src/Track.js'); 10 | const { Point2D, PHASES } = require('../core'); 11 | 12 | describe('Track', () => { 13 | describe('constructor', () => { 14 | test('Does not throw an exception', () => { 15 | expect(() => new Track()).not.toThrow(); 16 | }); 17 | 18 | test('Creates a Track object', () => { 19 | expect(new Track()).toBeInstanceOf(Track); 20 | }); 21 | 22 | test('Created object has the type "track"', () => { 23 | expect(new Track().type).toBe('track'); 24 | }); 25 | 26 | test('Can be passed an element and a handler function', () => { 27 | const element = document.createElement('div'); 28 | const handler = jest.fn(); 29 | let track = null; 30 | expect(() => { 31 | track = new Track(element, handler); 32 | }).not.toThrow(); 33 | expect(track.element).toBe(element); 34 | expect(track.handler).toBe(handler); 35 | }); 36 | 37 | test('Can also be passed an options object with "phases"', () => { 38 | const element = document.createElement('div'); 39 | const handler = jest.fn(); 40 | const options = { 41 | phases: ['start'], 42 | }; 43 | expect(() => new Track(element, handler, options)).not.toThrow(); 44 | }); 45 | }); 46 | 47 | describe('phase hooks', () => { 48 | let element = null; 49 | let expected_data = null; 50 | let handler = null; 51 | let options = null; 52 | let state = null; 53 | 54 | beforeAll(() => { 55 | const points = [new Point2D(42, 117)]; 56 | state = { activePoints: points }; 57 | expected_data = { active: points }; 58 | element = document.createElement('div'); 59 | }); 60 | 61 | beforeEach(() => { 62 | handler = jest.fn(); 63 | }); 64 | 65 | describe.each(PHASES)('%s(state)', (phase) => { 66 | test('Returns null if not included in "phases" option', () => { 67 | const track = new Track(element, handler); 68 | expect(track[phase](state)).toBeFalsy(); 69 | }); 70 | 71 | test('If specified in "phases", returns active points', () => { 72 | options = { phases: [phase] }; 73 | const track = new Track(element, handler, options); 74 | expect(track[phase](state)).toMatchObject(expected_data); 75 | }); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/Westures.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Test file for the westures index. 3 | */ 4 | 5 | /* global expect, describe, test */ 6 | 7 | 'use strict'; 8 | 9 | const westures = require('index.js'); 10 | const core = require('core/index.js'); 11 | 12 | 13 | describe('westures', () => { 14 | test('has constructors for all of the gestures', () => { 15 | const gestures = [ 16 | 'Gesture', 17 | 'Pan', 18 | 'Pinch', 19 | 'Press', 20 | 'Pull', 21 | 'Rotate', 22 | 'Swipe', 23 | 'Swivel', 24 | 'Tap', 25 | 'Track', 26 | ]; 27 | expect(Object.keys(westures)).toEqual(expect.arrayContaining(gestures)); 28 | }); 29 | 30 | test('includes all of westures-core', () => { 31 | const coreKeys = Object.keys(core); 32 | expect(Object.keys(westures)).toEqual(expect.arrayContaining(coreKeys)); 33 | }); 34 | }); 35 | 36 | --------------------------------------------------------------------------------