├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── usfl.js ├── usfl.js.map └── usfl.min.js ├── docs └── README.md ├── karma.conf.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── array │ ├── array.js │ ├── clone.js │ ├── index.js │ ├── move-element.js │ ├── nearest.js │ ├── random-choice.js │ ├── sort-alpha.js │ ├── sort-numbered.js │ ├── sort-numeric.js │ ├── sort-random.js │ └── unique.js ├── dom │ ├── block-scrolling.js │ ├── el-coords.js │ ├── force-redraw.js │ ├── get-page-height.js │ ├── get-scroll-percentage.js │ ├── get-scroll-remaining.js │ ├── get-scroll-top.js │ ├── get-srcset-image.js │ ├── index.js │ ├── is-element-in-viewport.js │ ├── is-page-end.js │ ├── resize.js │ ├── scroll.js │ ├── set-style.js │ └── transition-end.js ├── ease │ ├── back.js │ ├── bounce.js │ ├── circular.js │ ├── cubic.js │ ├── elastic.js │ ├── expo.js │ ├── index.js │ ├── linear.js │ ├── quad.js │ ├── quart.js │ ├── quint.js │ └── sine.js ├── events │ ├── debounce.js │ ├── delegate-events.js │ ├── emitter.js │ ├── event-bus.js │ ├── heartbeat.js │ └── index.js ├── fps │ └── index.js ├── fullscreen │ ├── api.js │ └── index.js ├── graphics │ ├── download-image.js │ ├── get-image-data-url.js │ ├── index.js │ ├── open-image.js │ ├── shape.js │ └── spritesheet-player.js ├── gui │ └── index.js ├── http │ ├── get-location.js │ ├── index.js │ ├── jsonp.js │ ├── load-script.js │ ├── url-params.js │ └── xhr.js ├── index.js ├── input │ ├── click-outside.js │ ├── index.js │ ├── key-input.js │ ├── keyboard.js │ ├── microphone.js │ ├── mouse-left-window.js │ ├── mouse-wheel.js │ ├── pointer-coords.js │ └── touch-input.js ├── linked-list │ └── index.js ├── loop │ └── index.js ├── math │ ├── angle.js │ ├── cerp.js │ ├── circle-distribution.js │ ├── clamp.js │ ├── coin-toss.js │ ├── cross-product-2d.js │ ├── degrees.js │ ├── difference.js │ ├── distance-sq.js │ ├── distance.js │ ├── dot-product-2d.js │ ├── get-circle-points.js │ ├── get-intersection-area.js │ ├── get-overlap-x.js │ ├── get-overlap-y.js │ ├── index.js │ ├── lerp.js │ ├── map.js │ ├── normalize.js │ ├── orientation.js │ ├── percent-remaining.js │ ├── perspective.js │ ├── quadratic-curve.js │ ├── radians.js │ ├── random-int.js │ ├── random-sign.js │ ├── random.js │ ├── rotate-point.js │ ├── rotate-to-deg.js │ ├── rotate-to-rad.js │ ├── round-to-nearest.js │ ├── round-to.js │ ├── size.js │ ├── smerp.js │ ├── smoothstep.js │ ├── split-value-and-unit.js │ ├── weighted-average.js │ └── weighted-distribution.js ├── media │ ├── can-play.js │ ├── cuepoints-reader.js │ ├── index.js │ ├── ios-play-video-inline.js │ ├── video-player.js │ ├── vimeo.js │ ├── youtube-basic.js │ └── youtube.js ├── object-pool │ └── index.js ├── object │ ├── clone.js │ ├── filter.js │ ├── index.js │ └── map.js ├── particle │ ├── index.js │ └── particle-group.js ├── platform │ ├── android-native.js │ ├── android-version.js │ ├── android.js │ ├── chrome-ios.js │ ├── desktop.js │ ├── device-orientation.js │ ├── firefox.js │ ├── ie-version.js │ ├── ie.js │ ├── index.js │ ├── ios-version.js │ ├── ios.js │ ├── ipad.js │ ├── iphone.js │ ├── language.js │ ├── linux.js │ ├── local-host.js │ ├── mac.js │ ├── mobile.js │ ├── mp4.js │ ├── safari-ios.js │ ├── safari.js │ ├── screen.js │ ├── webgl.js │ ├── webm.js │ ├── windows-phone.js │ └── windows.js ├── polyfill │ ├── class-list.js │ ├── console.js │ ├── index.js │ └── request-animation-frame.js ├── popup │ └── index.js ├── quad-tree │ └── index.js ├── share │ ├── email.js │ ├── facebook.js │ ├── googleplus.js │ ├── index.js │ ├── linkedin.js │ ├── pinterest.js │ ├── reddit.js │ ├── renren.js │ ├── sms.js │ ├── twitter.js │ ├── vkontakte.js │ ├── weibo.js │ └── whatsapp.js ├── storage │ └── index.js ├── string │ ├── after-first.js │ ├── after-last.js │ ├── before-first.js │ ├── before-last.js │ ├── begins-with.js │ ├── between.js │ ├── block.js │ ├── capitalize.js │ ├── count-of.js │ ├── edit-distance.js │ ├── ends-with.js │ ├── escape-html.js │ ├── escape-pattern.js │ ├── has-text.js │ ├── index.js │ ├── is-numeric.js │ ├── pad-left.js │ ├── pad-right.js │ ├── prevent-widow.js │ ├── proper-case.js │ ├── remove-extra-whitespace.js │ ├── remove.js │ ├── reverse-words.js │ ├── reverse.js │ ├── similarity.js │ ├── strip-tags.js │ ├── swap-case.js │ ├── time-code.js │ ├── to-number.js │ ├── truncate.js │ └── word-count.js ├── track │ ├── event.js │ ├── index.js │ ├── load.js │ └── pageview.js ├── tween │ └── index.js └── visibility │ ├── api.js │ └── index.js └── test ├── array.spec.js ├── bundle-nodejs.spec.js ├── bundle.spec.js ├── dom.spec.js ├── ease.spec.js ├── events.spec.js ├── fps.spec.js ├── fullscreen.spec.js ├── graphics.spec.js ├── http.spec.js ├── input.spec.js ├── linked-list.spec.js ├── loop.spec.js ├── math.spec.js ├── media.spec.js ├── object-pool.spec.js ├── object.spec.js ├── particle.spec.js ├── platform.spec.js ├── popup.spec.js ├── quad-tree.spec.js ├── share.spec.js ├── storage.spec.js ├── string.spec.js ├── track.spec.js ├── tween.spec.js └── visibility.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", { 5 | "loose": true 6 | } 7 | ] 8 | ], 9 | "plugins": [ 10 | ["external-helpers"], 11 | ["transform-runtime", { 12 | "helpers": true, 13 | "polyfill": false, 14 | "regenerator": false, 15 | "moduleName": "babel-runtime" 16 | }] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [{.travis.yml,package.json,bower.json}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "globals": { 10 | "expect": true, 11 | "it": true, 12 | "usfl": true 13 | }, 14 | "plugins": [], 15 | "parserOptions": { 16 | "ecmaVersion": 6, 17 | "jsx": true, 18 | "sourceType": "module" 19 | }, 20 | "rules": { 21 | "array-bracket-spacing": [2, "never"], 22 | "block-scoped-var": 0, 23 | "block-spacing": 2, 24 | "brace-style": [2, "1tbs"], 25 | "callback-return": [2, ["cb", "callback", "next"]], 26 | "camelcase": [2, {"properties": "never"}], 27 | "comma-dangle": [2, "never"], 28 | "comma-spacing": 2, 29 | "comma-style": [2, "last"], 30 | "consistent-return": 2, 31 | "curly": [2, "all"], 32 | "default-case": 2, 33 | "dot-location": [2, "property"], 34 | "dot-notation": [2, {"allowKeywords": true}], 35 | "eol-last": 2, 36 | "eqeqeq": 2, 37 | "func-style": [2, "declaration", {"allowArrowFunctions": true}], 38 | "guard-for-in": 2, 39 | "indent": [2, 4, {"SwitchCase": 1}], 40 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 41 | "keyword-spacing": 2, 42 | "max-len": [1, 120], 43 | "new-cap": 0, 44 | "new-parens": 2, 45 | "no-alert": 2, 46 | "no-array-constructor": 2, 47 | "no-caller": 2, 48 | "no-confusing-arrow": 2, 49 | "no-console": 0, 50 | "no-const-assign": 2, 51 | "no-constant-condition": 2, 52 | "no-delete-var": 2, 53 | "no-empty": [2, {"allowEmptyCatch": true}], 54 | "no-eval": 2, 55 | "no-extend-native": 2, 56 | "no-extra-bind": 2, 57 | "no-extra-semi": 2, 58 | "no-fallthrough": 2, 59 | "no-floating-decimal": 2, 60 | "no-implied-eval": 2, 61 | "no-invalid-this": 2, 62 | "no-iterator": 2, 63 | "no-label-var": 2, 64 | "no-labels": [2, {"allowLoop": true, "allowSwitch": true}], 65 | "no-lone-blocks": 2, 66 | "no-loop-func": 2, 67 | "no-mixed-spaces-and-tabs": [2, false], 68 | "no-multi-spaces": 2, 69 | "no-multi-str": 2, 70 | "no-native-reassign": 2, 71 | "no-nested-ternary": 2, 72 | "no-new-func": 2, 73 | "no-new-object": 2, 74 | "no-new-wrappers": 2, 75 | "no-new": 2, 76 | "no-octal-escape": 2, 77 | "no-octal": 2, 78 | "no-process-exit": 2, 79 | "no-proto": 2, 80 | "no-redeclare": 2, 81 | "no-return-assign": 2, 82 | "no-script-url": 2, 83 | "no-sequences": 2, 84 | "no-shadow-restricted-names": 2, 85 | "no-shadow": 2, 86 | "no-spaced-func": 2, 87 | "no-trailing-spaces": 2, 88 | "no-undef-init": 2, 89 | "no-undef": 2, 90 | "no-undefined": 2, 91 | "no-underscore-dangle": 0, 92 | "no-unused-expressions": 0, 93 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 94 | "no-use-before-define": 2, 95 | "no-useless-concat": 2, 96 | "no-var": 2, 97 | "no-with": 2, 98 | "object-curly-spacing": [2, "never"], 99 | "prefer-const": 2, 100 | "quotes": [2, "single"], 101 | "radix": 2, 102 | "require-jsdoc": 0, 103 | "semi-spacing": [2, {"before": false, "after": true}], 104 | "semi": 2, 105 | "space-before-blocks": 2, 106 | "space-before-function-paren": [0, "never"], 107 | "space-infix-ops": 2, 108 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 109 | "spaced-comment": [0, "always", {"exceptions": ["-"]}], 110 | "strict": [1, "global"], 111 | "valid-jsdoc": [0, {"prefer": { "return": "returns"}}], 112 | "wrap-iife": 2, 113 | "yoda": [2, "never"] 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | 3 | .* 4 | !/.babelrc 5 | !/.gitignore 6 | !/.gitattributes 7 | !/.travis.yml 8 | !/.eslintrc 9 | !/.editorconfig 10 | !/.npmignore 11 | 12 | Icon? 13 | ehthumbs.db 14 | Thumbs.db 15 | Desktop.ini 16 | 17 | # VCS 18 | 19 | .svn 20 | *.orig 21 | !.gitinclude 22 | 23 | # IDE 24 | 25 | .settings/ 26 | .project 27 | user.properties 28 | .idea? 29 | .metadata/* 30 | *.ignore 31 | *.sublime-* 32 | *~ 33 | *.swp 34 | *.swo 35 | npm-debug.log 36 | 37 | # FOLDERS 38 | 39 | /bower_components/ 40 | /node_modules/ 41 | /tmp/ 42 | /array/ 43 | /dom/ 44 | /ease/ 45 | /events/ 46 | /fps/ 47 | /fullscreen/ 48 | /graphics/ 49 | /gui/ 50 | /http/ 51 | /index.js 52 | /input/ 53 | /linked-list/ 54 | /loop/ 55 | /math/ 56 | /media/ 57 | /object-pool/ 58 | /object/ 59 | /particle/ 60 | /platform/ 61 | /polyfill/ 62 | /popup/ 63 | /quad-tree/ 64 | /share/ 65 | /storage/ 66 | /string/ 67 | /track/ 68 | /tween/ 69 | /visibility/ 70 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src/ 2 | /docs/ 3 | .babelrc 4 | .eslintrc 5 | .editorconfig 6 | .gitattributes 7 | .travis.yml 8 | karma.conf.js 9 | rollup.config.js 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | addons: 5 | firefox: "latest" 6 | before_install: 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | - sleep 3 10 | before_script: 11 | - npm i -g karma karma-cli eslint babel-eslint 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ian McGregor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # usfl 2 | 3 | [![NPM version](https://badge.fury.io/js/usfl.svg)](http://badge.fury.io/js/usfl) [![Bower version](https://badge.fury.io/bo/usfl.svg)](http://badge.fury.io/bo/usfl) [![Build Status](https://secure.travis-ci.org/ianmcgregor/usfl.png)](https://travis-ci.org/ianmcgregor/usfl) 4 | 5 | A collection of tested, reusable JS utilities and snippets. 6 | 7 | ### Installation 8 | 9 | ```shell 10 | npm install usfl --save 11 | ``` 12 | 13 | ### Usage 14 | 15 | ```javascript 16 | // individual module 17 | import randomChoice from 'usfl/array/random-choice'; 18 | const chosen = randomChoice([1, 2, 3]); 19 | 20 | // group 21 | import array from 'usfl/array'; 22 | const chosen = array.randomChoice([1, 2, 3]); 23 | 24 | // everything 25 | import usfl from 'usfl'; 26 | const chosen = usfl.array.randomChoice([1, 2, 3]); 27 | ``` 28 | 29 | ### Docs 30 | 31 | [usfl/array](docs/README.md#array) 32 | 33 | [usfl/dom](docs/README.md#dom) 34 | 35 | [usfl/events](docs/README.md#events) 36 | 37 | [usfl/fps](docs/README.md#fps) 38 | 39 | [usfl/fullscreen](docs/README.md#fullscreen) 40 | 41 | [usfl/graphics](docs/README.md#graphics) 42 | 43 | [usfl/http](docs/README.md#http) 44 | 45 | [usfl/input](docs/README.md#input) 46 | 47 | [usfl/linked-list](docs/README.md#linkedlist) 48 | 49 | [usfl/loop](docs/README.md#loop) 50 | 51 | [usfl/math](docs/README.md#math) 52 | 53 | [usfl/media](docs/README.md#media) 54 | 55 | [usfl/object-pool](docs/README.md#objectpool) 56 | 57 | [usfl/platform](docs/README.md#platform) 58 | 59 | [usfl/polyfill](docs/README.md#polyfill) 60 | 61 | [usfl/popup](docs/README.md#popup) 62 | 63 | [usfl/share](docs/README.md#share) 64 | 65 | [usfl/storage](docs/README.md#storage) 66 | 67 | [usfl/string](docs/README.md#string) 68 | 69 | [usfl/track](docs/README.md#track) 70 | 71 | [usfl/visibility](docs/README.md#visibility) 72 | 73 | 74 | ### Dev setup 75 | 76 | To install dependencies: 77 | 78 | ``` 79 | $ npm i 80 | ``` 81 | 82 | To run tests: 83 | 84 | ``` 85 | $ npm i -g karma-cli 86 | $ karma start 87 | ``` 88 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | 4 | basePath: '', 5 | 6 | browserConsoleLogOptions: { 7 | level: 'log', 8 | format: '%b %T: %m', 9 | terminal: true 10 | }, 11 | 12 | plugins: [ 13 | 'karma-mocha', 14 | 'karma-chai', 15 | 'karma-browserify', 16 | 'karma-chrome-launcher', 17 | 'karma-firefox-launcher' 18 | ], 19 | 20 | frameworks: [ 21 | 'mocha', 22 | 'chai', 23 | 'browserify' 24 | ], 25 | 26 | files: [ 27 | 'dist/usfl.js', 28 | 'test/**/*.spec.js' 29 | ], 30 | 31 | exclude: [ 32 | 'test/bundle-nodejs.spec.js' 33 | ], 34 | 35 | preprocessors: {'test/**/*.js': ['browserify']}, 36 | 37 | // test results reporter to use 38 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 39 | reporters: ['progress'], 40 | 41 | // web server port 42 | port: 9876, 43 | 44 | // enable / disable colors in the output (reporters and logs) 45 | colors: true, 46 | 47 | // level of logging 48 | /* possible values: 49 | config.LOG_DISABLE 50 | config.LOG_ERROR 51 | config.LOG_WARN 52 | config.LOG_INFO 53 | config.LOG_DEBUG*/ 54 | logLevel: config.LOG_INFO, 55 | 56 | 57 | // enable / disable watching file and executing tests whenever any file changes 58 | autoWatch: true, 59 | 60 | 61 | // Start these browsers, currently available: 62 | // - Chrome 63 | // - ChromeCanary 64 | // - Firefox 65 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 66 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 67 | // - PhantomJS 68 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 69 | browsers: [ 70 | //'iOS', 71 | 'Chrome', 72 | 'Firefox' 73 | //,'IE11 - Win7' 74 | //,'IE10 - Win7' 75 | ], 76 | 77 | // If browser does not capture in given timeout [ms], kill it 78 | captureTimeout: 60000, 79 | 80 | // Continuous Integration mode 81 | // if true, it capture browsers, run tests and exit 82 | singleRun: false 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "usfl", 3 | "version": "3.2.3", 4 | "description": "A collection of tested, reusable JS utilities and snippets.", 5 | "keywords": [ 6 | "lib", 7 | "utility", 8 | "util", 9 | "utilities", 10 | "javascript", 11 | "js" 12 | ], 13 | "author": "ianmcgregor", 14 | "license": "MIT", 15 | "main": "dist/usfl.min.js", 16 | "scripts": { 17 | "prepublish": "babel src --out-dir ./", 18 | "start": "rollup -c -w", 19 | "build": "NODE_ENV=production rollup -c && rollup -c", 20 | "test": "eslint 'src/**/*.js' && mocha 'test/bundle-nodejs.spec.js' && karma start --single-run --browsers Firefox", 21 | "lint": "eslint 'src/**/*.js'; exit 0" 22 | }, 23 | "readmeFilename": "README.md", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/ianmcgregor/usfl" 27 | }, 28 | "dependencies": { 29 | "eventemitter3": "^2.0.3" 30 | }, 31 | "devDependencies": { 32 | "babel-cli": "^6.26.0", 33 | "babel-core": "^6.26.0", 34 | "babel-eslint": "^8.0.0", 35 | "babel-plugin-external-helpers": "^6.22.0", 36 | "babel-plugin-transform-runtime": "^6.23.0", 37 | "babel-preset-es2015": "^6.24.1", 38 | "babelify": "^7.3.0", 39 | "browserify": "^14.4.0", 40 | "chai": "^4.1.2", 41 | "chalk": "^2.1.0", 42 | "karma": "^1.7.1", 43 | "karma-browserify": "^5.1.1", 44 | "karma-chai": "^0.1.0", 45 | "karma-chrome-launcher": "^2.2.0", 46 | "karma-firefox-launcher": "^1.0.1", 47 | "karma-mocha": "^1.3.0", 48 | "mocha": "^3.5.3", 49 | "rollup": "^0.50.0", 50 | "rollup-plugin-babel": "^3.0.2", 51 | "rollup-plugin-commonjs": "^8.2.1", 52 | "rollup-plugin-node-resolve": "^3.0.0", 53 | "rollup-plugin-strip": "^1.1.1", 54 | "rollup-plugin-uglify": "^2.0.1", 55 | "rollup-watch": "^4.3.1", 56 | "watchify": "^3.9.0" 57 | }, 58 | "browserify": { 59 | "transform": [ 60 | "babelify" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | import strip from 'rollup-plugin-strip'; 5 | import uglify from 'rollup-plugin-uglify'; 6 | 7 | const prod = process.env.NODE_ENV === 'production'; 8 | 9 | export default { 10 | input: 'src/index.js', 11 | name: 'usfl', 12 | output: { 13 | file: (prod ? 'dist/usfl.min.js' : 'dist/usfl.js'), 14 | format: 'umd' 15 | }, 16 | sourcemap: !prod, 17 | plugins: [ 18 | nodeResolve({ 19 | jsnext: true, 20 | main: true, 21 | preferBuiltins: false 22 | }), 23 | commonjs({ 24 | include: [ 25 | 'node_modules/core-js/**', 26 | 'node_modules/eventemitter3/**' 27 | ] 28 | }), 29 | babel({ 30 | babelrc: false, 31 | // exclude: 'node_modules/**', 32 | presets: [ 33 | ['es2015', {loose: true, modules: false}] 34 | ], 35 | plugins: [ 36 | 'external-helpers' 37 | ] 38 | }), 39 | (prod && strip({sourceMap: false})), 40 | (prod && uglify()) 41 | ] 42 | }; 43 | -------------------------------------------------------------------------------- /src/array/array.js: -------------------------------------------------------------------------------- 1 | export default function array(length, value) { 2 | const arr = []; 3 | for (let i = 0; i < length; i++) { 4 | const val = typeof value !== 'undefined' ? value : i; 5 | arr.push(val); 6 | } 7 | return arr; 8 | } 9 | -------------------------------------------------------------------------------- /src/array/clone.js: -------------------------------------------------------------------------------- 1 | export default function clone(arr) { 2 | return arr.slice(0); 3 | } 4 | -------------------------------------------------------------------------------- /src/array/index.js: -------------------------------------------------------------------------------- 1 | import array from './array'; 2 | import clone from './clone'; 3 | import moveElement from './move-element'; 4 | import nearest from './nearest'; 5 | import randomChoice from './random-choice'; 6 | import sortAlpha from './sort-alpha'; 7 | import sortNumbered from './sort-numbered'; 8 | import sortNumeric from './sort-numeric'; 9 | import sortRandom from './sort-random'; 10 | import unique from './unique'; 11 | 12 | export default { 13 | array, 14 | clone, 15 | moveElement, 16 | nearest, 17 | randomChoice, 18 | sortAlpha, 19 | sortNumbered, 20 | sortNumeric, 21 | sortRandom, 22 | unique 23 | }; 24 | -------------------------------------------------------------------------------- /src/array/move-element.js: -------------------------------------------------------------------------------- 1 | export default function moveElement(arr, from, to) { 2 | arr = arr.slice(0); 3 | const removed = arr.splice(from, 1)[0]; 4 | const insertAt = to < 0 ? arr.length + to : to; 5 | arr.splice(insertAt, 0, removed); 6 | return arr; 7 | } 8 | -------------------------------------------------------------------------------- /src/array/nearest.js: -------------------------------------------------------------------------------- 1 | export default function nearest(value, arr) { 2 | let least = Number.MAX_VALUE; 3 | return arr.reduce((result, item) => { 4 | const diff = Math.abs(item - value); 5 | if (diff < least) { 6 | least = diff; 7 | result = item; 8 | } 9 | return result; 10 | }, -1); 11 | } 12 | -------------------------------------------------------------------------------- /src/array/random-choice.js: -------------------------------------------------------------------------------- 1 | export default function randomChoice(arr) { 2 | return arr[Math.floor(Math.random() * arr.length)]; 3 | } 4 | -------------------------------------------------------------------------------- /src/array/sort-alpha.js: -------------------------------------------------------------------------------- 1 | export default function sortAlpha(a, b) { 2 | if (arguments.length === 1) { 3 | return function(x, y) { 4 | return String(x[a]).toLowerCase() > String(y[a]).toLowerCase() ? 1 : -1; 5 | }; 6 | } 7 | return String(a).toLowerCase() > String(b).toLowerCase() ? 1 : -1; 8 | } 9 | -------------------------------------------------------------------------------- /src/array/sort-numbered.js: -------------------------------------------------------------------------------- 1 | const re = /[^0-9.-]/g; 2 | 3 | function diff(a, b) { 4 | const a1 = a.replace(re, ''); 5 | const b1 = b.replace(re, ''); 6 | return Number(a1) - Number(b1); 7 | } 8 | 9 | export default function sortNumbered(a, b) { 10 | if (arguments.length === 1) { 11 | return function(x, y) { 12 | return diff(x[a], y[a]); 13 | }; 14 | } 15 | return diff(a, b); 16 | } 17 | -------------------------------------------------------------------------------- /src/array/sort-numeric.js: -------------------------------------------------------------------------------- 1 | export default function sortNumeric(a, b) { 2 | if (arguments.length === 1) { 3 | return function(x, y) { 4 | return Number(x[a]) - Number(y[a]); 5 | }; 6 | } 7 | return Number(a) - Number(b); 8 | } 9 | -------------------------------------------------------------------------------- /src/array/sort-random.js: -------------------------------------------------------------------------------- 1 | export default function sortRandom() { 2 | return Math.random() > 0.5 ? -1 : 1; 3 | } 4 | -------------------------------------------------------------------------------- /src/array/unique.js: -------------------------------------------------------------------------------- 1 | export default function uniqiue(arr) { 2 | return arr.filter((value, index, self) => self.indexOf(value) === index); 3 | } 4 | -------------------------------------------------------------------------------- /src/dom/block-scrolling.js: -------------------------------------------------------------------------------- 1 | export default function blockScrolling(value) { 2 | document.body.style.overflow = value ? 'hidden' : ''; 3 | } 4 | -------------------------------------------------------------------------------- /src/dom/el-coords.js: -------------------------------------------------------------------------------- 1 | export default function elCoords(el) { 2 | const box = el.getBoundingClientRect(); 3 | 4 | const body = document.body; 5 | const docEl = document.documentElement; 6 | 7 | const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop; 8 | const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft; 9 | 10 | const clientTop = docEl.clientTop || body.clientTop || 0; 11 | const clientLeft = docEl.clientLeft || body.clientLeft || 0; 12 | 13 | const top = box.top + scrollTop - clientTop; 14 | const left = box.left + scrollLeft - clientLeft; 15 | 16 | return { 17 | top: Math.round(top), 18 | left: Math.round(left), 19 | x: Math.round(left), 20 | y: Math.round(top) 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/dom/force-redraw.js: -------------------------------------------------------------------------------- 1 | export default function forceRedraw(el) { 2 | const display = el.style.display; 3 | el.style.display = 'none'; 4 | el.offsetHeight; 5 | el.style.display = display; 6 | } 7 | -------------------------------------------------------------------------------- /src/dom/get-page-height.js: -------------------------------------------------------------------------------- 1 | export default function getPageHeight() { 2 | const body = document.body; 3 | const doc = document.documentElement; 4 | 5 | return Math.max( 6 | body.scrollHeight || 0, 7 | body.offsetHeight || 0, 8 | body.clientHeight || 0, 9 | doc.clientHeight || 0, 10 | doc.offsetHeight || 0, 11 | doc.scrollHeight || 0 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/dom/get-scroll-percentage.js: -------------------------------------------------------------------------------- 1 | import getScrollTop from './get-scroll-top'; 2 | 3 | export default function getScrollPercentage() { 4 | return (getScrollTop() + window.innerHeight) / document.body.clientHeight; 5 | } 6 | -------------------------------------------------------------------------------- /src/dom/get-scroll-remaining.js: -------------------------------------------------------------------------------- 1 | import getScrollTop from './get-scroll-top'; 2 | 3 | export default function getScrollRemaining() { 4 | const b = document.body; 5 | return Math.abs(getScrollTop() - (b.scrollHeight - b.clientHeight)); 6 | } 7 | -------------------------------------------------------------------------------- /src/dom/get-scroll-top.js: -------------------------------------------------------------------------------- 1 | export default function getScrollTop() { 2 | return window.pageYOffset || document.documentElement.scrollTop; 3 | } 4 | -------------------------------------------------------------------------------- /src/dom/get-srcset-image.js: -------------------------------------------------------------------------------- 1 | export default function getSrcsetImage(srcset, pixelWidth) { 2 | pixelWidth = pixelWidth || window.innerWidth * (window.devicePixelRatio || 0); 3 | 4 | const set = srcset.split(',') 5 | .map((item) => { 6 | const [url, width] = item.trim().split(/\s+/); 7 | const size = parseInt(width.slice(0, -1), 10); 8 | return {url, size}; 9 | }) 10 | .sort((a, b) => b.size - a.size); 11 | 12 | if (!set.length) { 13 | return ''; 14 | } 15 | 16 | return set.reduce((value, item) => { 17 | return item.size >= pixelWidth ? item.url : value; 18 | }, set[0].url); 19 | } 20 | -------------------------------------------------------------------------------- /src/dom/index.js: -------------------------------------------------------------------------------- 1 | import blockScrolling from './block-scrolling'; 2 | import elCoords from './el-coords'; 3 | import forceRedraw from './force-redraw'; 4 | import getPageHeight from './get-page-height'; 5 | import getScrollPercentage from './get-scroll-percentage'; 6 | import getScrollRemaining from './get-scroll-remaining'; 7 | import getScrollTop from './get-scroll-top'; 8 | import getSrcsetImage from './get-srcset-image'; 9 | import isElementInViewport from './is-element-in-viewport'; 10 | import isPageEnd from './is-page-end'; 11 | import resize from './resize'; 12 | import scroll from './scroll'; 13 | import setStyle from './set-style'; 14 | import transitionEnd from './transition-end'; 15 | 16 | export default { 17 | blockScrolling, 18 | elCoords, 19 | forceRedraw, 20 | getPageHeight, 21 | getScrollPercentage, 22 | getScrollRemaining, 23 | getScrollTop, 24 | getSrcsetImage, 25 | isElementInViewport, 26 | isPageEnd, 27 | resize, 28 | scroll, 29 | setStyle, 30 | transitionEnd 31 | }; 32 | -------------------------------------------------------------------------------- /src/dom/is-element-in-viewport.js: -------------------------------------------------------------------------------- 1 | export default function isElementInViewport(el, buffer = 0) { 2 | const rect = el.getBoundingClientRect(); 3 | return ( 4 | rect.top >= 0 - buffer && 5 | rect.left >= 0 - buffer && 6 | rect.bottom <= window.innerHeight + buffer && 7 | rect.right <= window.innerWidth + buffer 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/dom/is-page-end.js: -------------------------------------------------------------------------------- 1 | import getScrollRemaining from './get-scroll-remaining'; 2 | 3 | export default function isPageEnd(buffer = 0) { 4 | return getScrollRemaining() <= buffer; 5 | } 6 | -------------------------------------------------------------------------------- /src/dom/resize.js: -------------------------------------------------------------------------------- 1 | import eventBus from '../events/event-bus'; 2 | 3 | export default function resize({ 4 | onResize = () => eventBus.emit('resize'), 5 | callNow = false, 6 | debouceDelay = 500 7 | } = {}) { 8 | 9 | let timeoutId = null; 10 | 11 | function resizeHandler() { 12 | clearTimeout(timeoutId); 13 | timeoutId = window.setTimeout(() => onResize(), debouceDelay); 14 | } 15 | 16 | function start() { 17 | window.addEventListener('resize', resizeHandler, false); 18 | } 19 | 20 | function stop() { 21 | window.removeEventListener('resize', resizeHandler); 22 | } 23 | 24 | start(); 25 | 26 | if (callNow) { 27 | resizeHandler(); 28 | } 29 | 30 | return {start, stop}; 31 | } 32 | -------------------------------------------------------------------------------- /src/dom/scroll.js: -------------------------------------------------------------------------------- 1 | import eventBus from '../events/event-bus'; 2 | 3 | export default function scroll({ 4 | onScroll = lastScrollY => eventBus.emit('scroll', lastScrollY), 5 | onScrollEnd = lastScrollY => eventBus.emit('scrollend', lastScrollY), 6 | callNow = false, 7 | endTimeout = 200 8 | } = {}) { 9 | 10 | let lastScrollY = 0; 11 | let ticking = false; 12 | let timeoutId; 13 | 14 | function update() { 15 | clearTimeout(timeoutId); 16 | timeoutId = window.setTimeout(() => onScrollEnd(lastScrollY), endTimeout); 17 | 18 | onScroll(lastScrollY); 19 | ticking = false; 20 | } 21 | 22 | function requestTick() { 23 | if (!ticking) { 24 | window.requestAnimationFrame(update); 25 | ticking = true; 26 | } 27 | } 28 | 29 | function scrollHandler() { 30 | // lastScrollY = window.scrollY; 31 | lastScrollY = window.pageYOffset || document.documentElement.scrollTop; 32 | requestTick(); 33 | } 34 | 35 | function start() { 36 | window.addEventListener('scroll', scrollHandler, false); 37 | } 38 | 39 | function stop() { 40 | window.removeEventListener('scroll', scrollHandler); 41 | } 42 | 43 | start(); 44 | 45 | if (callNow) { 46 | scrollHandler(); 47 | } 48 | 49 | return {start, stop}; 50 | } 51 | -------------------------------------------------------------------------------- /src/dom/set-style.js: -------------------------------------------------------------------------------- 1 | export default function setStyle(el, style) { 2 | Object.keys(style).forEach((prop) => { 3 | el.style[prop] = style[prop]; 4 | }); 5 | return el; 6 | } 7 | -------------------------------------------------------------------------------- /src/dom/transition-end.js: -------------------------------------------------------------------------------- 1 | export default function transitionEnd(el, cb, timeout = 1000) { 2 | 3 | let timeoutId = null; 4 | 5 | function handler() { 6 | window.clearTimeout(timeoutId); 7 | el.removeEventListener('transitionend', handler); 8 | el.removeEventListener('webkitTransitionEnd', handler); 9 | cb(); 10 | } 11 | 12 | if (typeof el.style.transition !== 'undefined') { 13 | el.addEventListener('transitionend', handler); 14 | } else if (typeof el.style.WebkitTransition !== 'undefined') { 15 | el.addEventListener('webkitTransitionEnd', handler); 16 | } 17 | 18 | timeoutId = window.setTimeout(handler, timeout); 19 | } 20 | -------------------------------------------------------------------------------- /src/ease/back.js: -------------------------------------------------------------------------------- 1 | function easeInBack(t, b, c, d, s = 1.70158) { 2 | return c * (t /= d) * t * ((s + 1) * t - s) + b; 3 | } 4 | 5 | function easeOutBack(t, b, c, d, s = 1.70158) { 6 | return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; 7 | } 8 | 9 | function easeInOutBack(t, b, c, d, s = 1.70158) { 10 | if ((t /= d / 2) < 1) { 11 | return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; 12 | } 13 | return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; 14 | } 15 | 16 | export default { 17 | easeIn: easeInBack, 18 | easeOut: easeOutBack, 19 | easeInOut: easeInOutBack 20 | }; 21 | 22 | export { 23 | easeInBack, 24 | easeOutBack, 25 | easeInOutBack 26 | }; 27 | -------------------------------------------------------------------------------- /src/ease/bounce.js: -------------------------------------------------------------------------------- 1 | function easeOutBounce(t, b, c, d) { 2 | if ((t /= d) < (1 / 2.75)) { 3 | return c * (7.5625 * t * t) + b; 4 | } else if (t < (2 / 2.75)) { 5 | return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; 6 | } else if (t < (2.5 / 2.75)) { 7 | return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; 8 | } 9 | return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; 10 | } 11 | 12 | function easeInBounce(t, b, c, d) { 13 | return c - easeOutBounce(d - t, 0, c, d) + b; 14 | } 15 | 16 | function easeInOutBounce(t, b, c, d) { 17 | if (t < d / 2) { 18 | return easeInBounce(t * 2, 0, c, d) * 0.5 + b; 19 | } 20 | return easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; 21 | } 22 | 23 | export default { 24 | easeIn: easeInBounce, 25 | easeOut: easeOutBounce, 26 | easeInOut: easeInOutBounce 27 | }; 28 | 29 | export { 30 | easeInBounce, 31 | easeOutBounce, 32 | easeInOutBounce 33 | }; 34 | -------------------------------------------------------------------------------- /src/ease/circular.js: -------------------------------------------------------------------------------- 1 | const {sqrt} = Math; 2 | 3 | function easeInCircular(t, b, c, d) { 4 | return -c * (sqrt(1 - (t /= d) * t) - 1) + b; 5 | } 6 | 7 | function easeOutCircular(t, b, c, d) { 8 | return c * sqrt(1 - (t = t / d - 1) * t) + b; 9 | } 10 | 11 | function easeInOutCircular(t, b, c, d) { 12 | if ((t /= d / 2) < 1) { 13 | return -c / 2 * (sqrt(1 - t * t) - 1) + b; 14 | } 15 | return c / 2 * (sqrt(1 - (t -= 2) * t) + 1) + b; 16 | } 17 | 18 | export default { 19 | easeIn: easeInCircular, 20 | easeOut: easeOutCircular, 21 | easeInOut: easeInOutCircular 22 | }; 23 | 24 | export { 25 | easeInCircular, 26 | easeOutCircular, 27 | easeInOutCircular 28 | }; 29 | -------------------------------------------------------------------------------- /src/ease/cubic.js: -------------------------------------------------------------------------------- 1 | function easeInCubic(t, b, c, d) { 2 | return c * (t /= d) * t * t + b; 3 | } 4 | 5 | function easeOutCubic(t, b, c, d) { 6 | return c * ((t = t / d - 1) * t * t + 1) + b; 7 | } 8 | 9 | function easeInOutCubic(t, b, c, d) { 10 | if ((t /= d / 2) < 1) { 11 | return c / 2 * t * t * t + b; 12 | } 13 | return c / 2 * ((t -= 2) * t * t + 2) + b; 14 | } 15 | 16 | export default { 17 | easeIn: easeInCubic, 18 | easeOut: easeOutCubic, 19 | easeInOut: easeInOutCubic 20 | }; 21 | 22 | export { 23 | easeInCubic, 24 | easeOutCubic, 25 | easeInOutCubic 26 | }; 27 | -------------------------------------------------------------------------------- /src/ease/elastic.js: -------------------------------------------------------------------------------- 1 | const {abs, asin, PI, pow, sin} = Math; 2 | const PI_2 = PI * 2; 3 | 4 | function easeInElastic(t, b, c, d, a, p) { 5 | let s; 6 | if (t === 0) { 7 | return b; 8 | } 9 | if ((t /= d) === 1) { 10 | return b + c; 11 | } 12 | if (!p) { 13 | p = d * 0.3; 14 | } 15 | if (!a || a < abs(c)) { 16 | a = c; 17 | s = p / 4; 18 | } else { 19 | s = p / PI_2 * asin(c / a); 20 | } 21 | return -(a * pow(2, 10 * (t -= 1)) * sin((t * d - s) * PI_2 / p)) + b; 22 | } 23 | 24 | function easeOutElastic(t, b, c, d, a, p) { 25 | let s; 26 | if (t === 0) { 27 | return b; 28 | } 29 | if ((t /= d) === 1) { 30 | return b + c; 31 | } 32 | if (!p) { 33 | p = d * 0.3; 34 | } 35 | if (!a || a < abs(c)) { 36 | a = c; 37 | s = p / 4; 38 | } else { 39 | s = p / PI_2 * asin(c / a); 40 | } 41 | return (a * pow(2, -10 * t) * sin((t * d - s) * PI_2 / p) + c + b); 42 | } 43 | 44 | function easeInOutElastic(t, b, c, d, a, p) { 45 | let s; 46 | if (t === 0) { 47 | return b; 48 | } 49 | if ((t /= d / 2) === 2) { 50 | return b + c; 51 | } 52 | if (!p) { 53 | p = d * (0.3 * 1.5); 54 | } 55 | if (!a || a < abs(c)) { 56 | a = c; 57 | s = p / 4; 58 | } else { 59 | s = p / PI_2 * asin(c / a); 60 | } 61 | if (t < 1) { 62 | return -0.5 * (a * pow(2, 10 * (t -= 1)) * sin((t * d - s) * PI_2 / p)) + b; 63 | } 64 | return a * pow(2, -10 * (t -= 1)) * sin((t * d - s) * PI_2 / p) * 0.5 + c + b; 65 | } 66 | 67 | export default { 68 | easeIn: easeInElastic, 69 | easeOut: easeOutElastic, 70 | easeInOut: easeInOutElastic 71 | }; 72 | 73 | export { 74 | easeInElastic, 75 | easeOutElastic, 76 | easeInOutElastic 77 | }; 78 | -------------------------------------------------------------------------------- /src/ease/expo.js: -------------------------------------------------------------------------------- 1 | const {pow} = Math; 2 | 3 | function easeInExpo(t, b, c, d) { 4 | return t === 0 ? b : c * pow(2, 10 * (t / d - 1)) + b; 5 | } 6 | 7 | function easeOutExpo(t, b, c, d) { 8 | return t === d ? b + c : c * (-pow(2, -10 * t / d) + 1) + b; 9 | } 10 | 11 | function easeInOutExpo(t, b, c, d) { 12 | if (t === 0) { 13 | return b; 14 | } 15 | if (t === d) { 16 | return b + c; 17 | } 18 | if ((t /= d / 2) < 1) { 19 | return c / 2 * pow(2, 10 * (t - 1)) + b; 20 | } 21 | return c / 2 * (-pow(2, -10 * --t) + 2) + b; 22 | } 23 | 24 | export default { 25 | easeIn: easeInExpo, 26 | easeOut: easeOutExpo, 27 | easeInOut: easeInOutExpo 28 | }; 29 | 30 | export { 31 | easeInExpo, 32 | easeOutExpo, 33 | easeInOutExpo 34 | }; 35 | -------------------------------------------------------------------------------- /src/ease/index.js: -------------------------------------------------------------------------------- 1 | import back, {easeInBack, easeOutBack, easeInOutBack} from './back'; 2 | import bounce, {easeInBounce, easeOutBounce, easeInOutBounce} from './bounce'; 3 | import circular, {easeInCircular, easeOutCircular, easeInOutCircular} from './circular'; 4 | import cubic, {easeInCubic, easeOutCubic, easeInOutCubic} from './cubic'; 5 | import elastic, {easeInElastic, easeOutElastic, easeInOutElastic} from './elastic'; 6 | import expo, {easeInExpo, easeOutExpo, easeInOutExpo} from './expo'; 7 | import linear, {easeLinear} from './linear'; 8 | import quad, {easeInQuad, easeOutQuad, easeInOutQuad} from './quad'; 9 | import quart, {easeInQuart, easeOutQuart, easeInOutQuart} from './quart'; 10 | import quint, {easeInQuint, easeOutQuint, easeInOutQuint} from './quint'; 11 | import sine, {easeInSine, easeOutSine, easeInOutSine} from './sine'; 12 | 13 | export default { 14 | back, 15 | bounce, 16 | circular, 17 | cubic, 18 | elastic, 19 | expo, 20 | linear, 21 | quad, 22 | quart, 23 | quint, 24 | sine, 25 | easeLinear, 26 | easeInBack, 27 | easeOutBack, 28 | easeInOutBack, 29 | easeInBounce, 30 | easeOutBounce, 31 | easeInOutBounce, 32 | easeInCircular, 33 | easeOutCircular, 34 | easeInOutCircular, 35 | easeInCubic, 36 | easeOutCubic, 37 | easeInOutCubic, 38 | easeInElastic, 39 | easeOutElastic, 40 | easeInOutElastic, 41 | easeInExpo, 42 | easeOutExpo, 43 | easeInOutExpo, 44 | easeInQuad, 45 | easeOutQuad, 46 | easeInOutQuad, 47 | easeInQuart, 48 | easeOutQuart, 49 | easeInOutQuart, 50 | easeInQuint, 51 | easeOutQuint, 52 | easeInOutQuint, 53 | easeInSine, 54 | easeOutSine, 55 | easeInOutSine 56 | }; 57 | 58 | /* 59 | TERMS OF USE - EASING EQUATIONS 60 | 61 | Open source under the BSD License. 62 | 63 | Copyright © 2001 Robert Penner 64 | All rights reserved. 65 | 66 | Redistribution and use in source and binary forms, with or without 67 | modification, are permitted provided that the following conditions are met: 68 | 69 | Redistributions of source code must retain the above copyright notice, this 70 | list of conditions and the following disclaimer. 71 | Redistributions in binary form must reproduce the above copyright notice, this 72 | list of conditions and the following disclaimer in the documentation and/or 73 | other materials provided with the distribution. 74 | Neither the name of the author nor the names of contributors may be used to 75 | endorse or promote products derived from this software without specific 76 | prior written permission. 77 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 78 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 79 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 80 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 81 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 82 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 83 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 84 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 85 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 86 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 87 | */ 88 | -------------------------------------------------------------------------------- /src/ease/linear.js: -------------------------------------------------------------------------------- 1 | function easeLinear(t, b, c, d) { 2 | return c * t / d + b; 3 | } 4 | 5 | export default { 6 | easeIn: easeLinear, 7 | easeOut: easeLinear, 8 | easeInOut: easeLinear 9 | }; 10 | 11 | export { 12 | easeLinear 13 | }; 14 | -------------------------------------------------------------------------------- /src/ease/quad.js: -------------------------------------------------------------------------------- 1 | function easeInQuad(t, b, c, d) { 2 | return c * (t /= d) * t + b; 3 | } 4 | 5 | function easeOutQuad(t, b, c, d) { 6 | return -c * (t /= d) * (t - 2) + b; 7 | } 8 | 9 | function easeInOutQuad(t, b, c, d) { 10 | if ((t /= d / 2) < 1) { 11 | return c / 2 * t * t + b; 12 | } 13 | return -c / 2 * ((--t) * (t - 2) - 1) + b; 14 | } 15 | 16 | export default { 17 | easeIn: easeInQuad, 18 | easeOut: easeOutQuad, 19 | easeInOut: easeInOutQuad 20 | }; 21 | 22 | export { 23 | easeInQuad, 24 | easeOutQuad, 25 | easeInOutQuad 26 | }; 27 | -------------------------------------------------------------------------------- /src/ease/quart.js: -------------------------------------------------------------------------------- 1 | function easeInQuart(t, b, c, d) { 2 | return c * (t /= d) * t * t * t + b; 3 | } 4 | 5 | function easeOutQuart(t, b, c, d) { 6 | return -c * ((t = t / d - 1) * t * t * t - 1) + b; 7 | } 8 | 9 | function easeInOutQuart(t, b, c, d) { 10 | if ((t /= d / 2) < 1) { 11 | return c / 2 * t * t * t * t + b; 12 | } 13 | return -c / 2 * ((t -= 2) * t * t * t - 2) + b; 14 | } 15 | 16 | export default { 17 | easeIn: easeInQuart, 18 | easeOut: easeOutQuart, 19 | easeInOut: easeInOutQuart 20 | }; 21 | 22 | export { 23 | easeInQuart, 24 | easeOutQuart, 25 | easeInOutQuart 26 | }; 27 | -------------------------------------------------------------------------------- /src/ease/quint.js: -------------------------------------------------------------------------------- 1 | function easeInQuint(t, b, c, d) { 2 | return c * (t /= d) * t * t * t * t + b; 3 | } 4 | 5 | function easeOutQuint(t, b, c, d) { 6 | return c * ((t = t / d - 1) * t * t * t * t + 1) + b; 7 | } 8 | 9 | function easeInOutQuint(t, b, c, d) { 10 | if ((t /= d / 2) < 1) { 11 | return c / 2 * t * t * t * t * t + b; 12 | } 13 | return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; 14 | } 15 | 16 | export default { 17 | easeIn: easeInQuint, 18 | easeOut: easeOutQuint, 19 | easeInOut: easeInOutQuint 20 | }; 21 | 22 | export { 23 | easeInQuint, 24 | easeOutQuint, 25 | easeInOutQuint 26 | }; 27 | -------------------------------------------------------------------------------- /src/ease/sine.js: -------------------------------------------------------------------------------- 1 | const {cos, PI, sin} = Math; 2 | const PI_D2 = PI / 2; 3 | 4 | function easeInSine(t, b, c, d) { 5 | return -c * cos(t / d * PI_D2) + c + b; 6 | } 7 | 8 | function easeOutSine(t, b, c, d) { 9 | return c * sin(t / d * PI_D2) + b; 10 | } 11 | 12 | function easeInOutSine(t, b, c, d) { 13 | return -c / 2 * (cos(PI * t / d) - 1) + b; 14 | } 15 | 16 | export default { 17 | easeIn: easeInSine, 18 | easeOut: easeOutSine, 19 | easeInOut: easeInOutSine 20 | }; 21 | 22 | export { 23 | easeInSine, 24 | easeOutSine, 25 | easeInOutSine 26 | }; 27 | -------------------------------------------------------------------------------- /src/events/debounce.js: -------------------------------------------------------------------------------- 1 | export default function debounce(handler) { 2 | let ticking = false; 3 | 4 | function update(event) { 5 | handler(event); 6 | ticking = false; 7 | } 8 | 9 | function requestTick(event) { 10 | if (!ticking) { 11 | window.requestAnimationFrame(() => update(event)); 12 | ticking = true; 13 | } 14 | } 15 | 16 | return requestTick; 17 | } 18 | -------------------------------------------------------------------------------- /src/events/delegate-events.js: -------------------------------------------------------------------------------- 1 | export default function delegateEvents(parentEl, eventType, filter, fn) { 2 | 3 | if (typeof filter === 'string') { 4 | const tagName = filter.toUpperCase(); 5 | filter = target => target.tagName === tagName; 6 | } 7 | 8 | parentEl.addEventListener(eventType, (event) => { 9 | let target = event.target; 10 | 11 | while (target !== parentEl) { 12 | if (filter(target)) { 13 | event.stopImmediatePropagation(); 14 | fn(target, event); 15 | break; 16 | } 17 | target = target.parentNode; 18 | } 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/events/emitter.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | 3 | export default class Emitter extends EventEmitter { 4 | constructor() { 5 | super(); 6 | } 7 | 8 | off (type, listener) { 9 | if (listener) { 10 | return this.removeListener(type, listener); 11 | } 12 | if (type) { 13 | return this.removeAllListeners(type); 14 | } 15 | return this.removeAllListeners(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/events/event-bus.js: -------------------------------------------------------------------------------- 1 | import Emitter from './emitter'; 2 | 3 | export default new Emitter(); 4 | -------------------------------------------------------------------------------- /src/events/heartbeat.js: -------------------------------------------------------------------------------- 1 | import Emitter from './emitter'; 2 | 3 | export default function heartbeat(interval) { 4 | let beat = null, 5 | time = 0, 6 | numTimes = 0, 7 | maxTimes = 0, 8 | running = false; 9 | 10 | function start(maxNumTimes = 0, timeOffset = 0) { 11 | maxTimes = maxNumTimes; 12 | time = timeOffset; 13 | numTimes = 0; 14 | running = true; 15 | return beat; 16 | } 17 | 18 | function stop() { 19 | running = false; 20 | return beat; 21 | } 22 | 23 | function update(dt = 1) { 24 | if (!running) { 25 | return beat; 26 | } 27 | 28 | if (maxTimes > 0 && numTimes >= maxTimes) { 29 | running = false; 30 | beat.emit('complete'); 31 | return beat; 32 | } 33 | 34 | time += dt; 35 | 36 | if (time >= interval) { 37 | time = 0; 38 | numTimes++; 39 | beat.emit('update', numTimes); 40 | } 41 | return beat; 42 | } 43 | 44 | function setInterval(value) { 45 | interval = value; 46 | return beat; 47 | } 48 | 49 | beat = Object.assign(new Emitter(), { 50 | start, 51 | stop, 52 | update, 53 | setInterval 54 | }); 55 | 56 | Object.defineProperties(beat, { 57 | interval: { 58 | get() { 59 | return interval; 60 | }, 61 | set(value) { 62 | interval = value; 63 | } 64 | } 65 | }); 66 | 67 | return beat; 68 | } 69 | -------------------------------------------------------------------------------- /src/events/index.js: -------------------------------------------------------------------------------- 1 | import debounce from './debounce'; 2 | import delegateEvents from './delegate-events'; 3 | import emitter from './emitter'; 4 | import eventBus from './event-bus'; 5 | import heartbeat from './heartbeat'; 6 | 7 | export default { 8 | debounce, 9 | delegateEvents, 10 | emitter, 11 | eventBus, 12 | heartbeat 13 | }; 14 | -------------------------------------------------------------------------------- /src/fps/index.js: -------------------------------------------------------------------------------- 1 | let time = 0; 2 | let fps = 0; 3 | let currentFps = 0; 4 | let averageFps = 0; 5 | let ticks = 0; 6 | let totalFps = 0; 7 | let lastFps = 0; 8 | let lastAverage = 0; 9 | let logMsg = null; 10 | 11 | const el = document.createElement('div'); 12 | el.setAttribute('id', 'fps'); 13 | el.style.fontFamily = 'monospace'; 14 | el.style.position = 'fixed'; 15 | el.style.left = '0'; 16 | el.style.top = '0'; 17 | el.style.padding = '2px 6px'; 18 | el.style.zIndex = '99999'; 19 | el.style.background = '#000'; 20 | el.style.color = '#fff'; 21 | el.style.fontSize = '10px'; 22 | el.style.userSelect = 'none'; 23 | 24 | function report() { 25 | lastFps = currentFps; 26 | lastAverage = averageFps; 27 | el.innerHTML = `FPS: ${currentFps}
AVE: ${averageFps}`; 28 | 29 | if (logMsg) { 30 | el.innerHTML = `${el.innerHTML}
MSG: ${logMsg}`; 31 | } 32 | } 33 | 34 | function update(now) { 35 | if (!el.parentElement) { 36 | document.body.appendChild(el); 37 | } 38 | 39 | if (typeof now === 'undefined') { 40 | now = Date.now(); 41 | } 42 | 43 | if (time === 0) { 44 | time = now; 45 | } 46 | 47 | if (now - 1000 > time) { 48 | time = now; 49 | currentFps = fps; 50 | fps = 0; 51 | 52 | if (currentFps > 1) { 53 | ticks++; 54 | totalFps += currentFps; 55 | averageFps = Math.floor(totalFps / ticks); 56 | } 57 | 58 | if (currentFps !== lastFps || averageFps !== lastAverage) { 59 | report(); 60 | } 61 | } 62 | 63 | fps++; 64 | } 65 | 66 | function auto() { 67 | window.requestAnimationFrame(auto); 68 | update(); 69 | } 70 | 71 | function log(value) { 72 | logMsg = String(value); 73 | report(); 74 | } 75 | 76 | function style(props) { 77 | Object.keys(props).forEach((prop) => { 78 | el.style[prop] = props[prop]; 79 | }); 80 | } 81 | 82 | export default { 83 | auto, 84 | el, 85 | log, 86 | style, 87 | update 88 | }; 89 | -------------------------------------------------------------------------------- /src/fullscreen/api.js: -------------------------------------------------------------------------------- 1 | let request = null, 2 | exit = null, 3 | change = null, 4 | error = null, 5 | element = null, 6 | enabled = null; 7 | 8 | const docEl = (typeof document !== 'undefined' && document.documentElement) || {}; 9 | 10 | if (typeof docEl.requestFullscreen !== 'undefined') { 11 | request = 'requestFullscreen'; 12 | exit = 'exitFullscreen'; 13 | change = 'fullscreenchange'; 14 | error = 'fullscreenerror'; 15 | element = 'fullscreenElement'; 16 | enabled = 'fullscreenEnabled'; 17 | } else if (typeof docEl.mozRequestFullScreen !== 'undefined') { 18 | request = 'mozRequestFullScreen'; 19 | exit = 'mozCancelFullScreen'; 20 | change = 'mozfullscreenchange'; 21 | error = 'mozfullscreenerror'; 22 | element = 'mozFullScreenElement'; 23 | enabled = 'mozFullScreenEnabled'; 24 | } else if (typeof docEl.msRequestFullscreen !== 'undefined') { 25 | request = 'msRequestFullscreen'; 26 | exit = 'msExitFullscreen'; 27 | change = 'MSFullscreenChange'; 28 | error = 'MSFullscreenError'; 29 | element = 'msFullscreenElement'; 30 | enabled = 'msFullscreenEnabled'; 31 | } else if (typeof docEl.webkitRequestFullscreen !== 'undefined') { 32 | request = 'webkitRequestFullscreen'; 33 | exit = 'webkitExitFullscreen'; 34 | change = 'webkitfullscreenchange'; 35 | error = 'webkitFullscreenError'; 36 | element = 'webkitFullscreenElement'; 37 | enabled = 'webkitFullscreenEnabled'; 38 | } 39 | 40 | export default { 41 | request, 42 | exit, 43 | change, 44 | error, 45 | element, 46 | enabled 47 | }; 48 | -------------------------------------------------------------------------------- /src/fullscreen/index.js: -------------------------------------------------------------------------------- 1 | import api from './api'; 2 | import Emitter from '../events/emitter'; 3 | 4 | const fullscreen = new Emitter(); 5 | 6 | if (typeof document !== 'undefined') { 7 | document.addEventListener(api.change, (event) => { 8 | fullscreen.emit('change', event); 9 | }); 10 | 11 | document.addEventListener(api.error, (event) => { 12 | fullscreen.emit('error', event); 13 | }); 14 | } 15 | 16 | Object.defineProperties(fullscreen, { 17 | request: { 18 | value: function(el) { 19 | el = el || document.documentElement; 20 | el[api.request](true); 21 | } 22 | }, 23 | exit: { 24 | value: function() { 25 | document[api.exit](); 26 | } 27 | }, 28 | toggle: { 29 | value: function(el) { 30 | if (this.isFullscreen) { 31 | this.exit(); 32 | } else { 33 | this.request(el); 34 | } 35 | } 36 | }, 37 | isSupported: { 38 | get() { 39 | return !!api.request; 40 | } 41 | }, 42 | isFullscreen: { 43 | get() { 44 | return !!document[api.element]; 45 | } 46 | }, 47 | element: { 48 | enumerable: true, 49 | get() { 50 | return document[api.element]; 51 | } 52 | }, 53 | enabled: { 54 | enumerable: true, 55 | get() { 56 | return document[api.enabled]; 57 | } 58 | } 59 | }); 60 | 61 | export default fullscreen; 62 | -------------------------------------------------------------------------------- /src/graphics/download-image.js: -------------------------------------------------------------------------------- 1 | export default function downloadImage(canvas) { 2 | window.location.href = canvas.toDataURL('image/png') 3 | .replace('image/png', 'image/octet-stream'); 4 | } 5 | -------------------------------------------------------------------------------- /src/graphics/get-image-data-url.js: -------------------------------------------------------------------------------- 1 | // convert image to localstorage friendly data URL string 2 | export default function getImageDataURL(img, width, height) { 3 | const canvas = document.createElement('canvas'); 4 | const context = canvas.getContext('2d'); 5 | canvas.width = width || img.width; 6 | canvas.height = height || img.height; 7 | context.drawImage(img, 0, 0); 8 | return canvas.toDataURL('image/png'); 9 | } 10 | -------------------------------------------------------------------------------- /src/graphics/index.js: -------------------------------------------------------------------------------- 1 | function getColour(r, g, b, a = 1) { 2 | if (typeof r === 'string') { 3 | return r; 4 | } 5 | if (typeof r === 'number') { 6 | return `rgba(${r},${b},${g},${a})`; 7 | } 8 | return null; 9 | } 10 | 11 | export default class Graphics { 12 | constructor(width, height) { 13 | if (typeof width === 'object' && width.tagName === 'CANVAS') { 14 | this.canvas = width; 15 | } else { 16 | this.canvas = document.createElement('canvas'); 17 | this.size(width, height); 18 | } 19 | this.ctx = this.canvas.getContext('2d'); 20 | } 21 | 22 | get alpha() { 23 | return this.ctx.globalAlpha; 24 | } 25 | 26 | set alpha(value) { 27 | this.ctx.globalAlpha = value; 28 | } 29 | 30 | get blendMode() { 31 | return this.ctx.globalCompositeOperation; 32 | } 33 | 34 | set blendMode(value) { 35 | this.ctx.globalCompositeOperation = value; 36 | } 37 | 38 | get context() { 39 | return this.ctx; 40 | } 41 | 42 | fill(r, g, b, a = 1) { 43 | this.ctx.fillStyle = getColour(r, g, b, a); 44 | return this; 45 | } 46 | 47 | stroke(r, g, b, a = 1) { 48 | this.ctx.strokeStyle = getColour(r, g, b, a); 49 | return this; 50 | } 51 | 52 | circle(x, y, radius) { 53 | const {ctx} = this; 54 | ctx.beginPath(); 55 | ctx.arc(x, y, radius, 0, Math.PI * 2, false); 56 | ctx.fill(); 57 | return this; 58 | } 59 | 60 | rect(x, y, width, height, angle = 0) { 61 | const {ctx} = this; 62 | if (angle !== 0) { 63 | ctx.save(); 64 | ctx.translate(x + width / 2, y + height / 2); 65 | ctx.rotate(angle); 66 | ctx.beginPath(); 67 | ctx.rect(-width / 2, -height / 2, width, height); 68 | ctx.fill(); 69 | ctx.stroke(); 70 | ctx.restore(); 71 | } else { 72 | ctx.rect(x, y, width, height); 73 | ctx.fill(); 74 | ctx.stroke(); 75 | } 76 | return this; 77 | } 78 | 79 | line(x1, y1, x2, y2) { 80 | const {ctx} = this; 81 | ctx.beginPath(); 82 | ctx.moveTo(x1, y1); 83 | ctx.lineTo(x2, y2); 84 | ctx.stroke(); 85 | return this; 86 | } 87 | 88 | lineWidth(width) { 89 | this.ctx.lineWidth = width; 90 | return this; 91 | } 92 | 93 | move(x, y) { 94 | this.ctx.moveTo(x, y); 95 | return this; 96 | } 97 | 98 | image(el, x, y, options) { 99 | const {ctx} = this; 100 | if (options) { 101 | const {alpha = 1, rotation = 0, scale = 1} = options; 102 | const offsetX = el.width / 2; 103 | const offsetY = el.height / 2; 104 | ctx.save(); 105 | ctx.translate(x + offsetX, y + offsetY); 106 | ctx.rotate(rotation); 107 | ctx.scale(scale, scale); 108 | ctx.globalAlpha = alpha; 109 | ctx.drawImage(el, -offsetX, -offsetY); 110 | ctx.restore(); 111 | } else { 112 | ctx.drawImage(el, x, y); 113 | } 114 | return this; 115 | } 116 | 117 | text(str, x, y) { 118 | this.ctx.fillText(str, x, y); 119 | return this; 120 | } 121 | 122 | setFontStyle(family, size) { 123 | this.ctx.font = `${size}px ${family}`; 124 | } 125 | 126 | getImageData(x = 0, y = 0, width, height) { 127 | const {ctx, canvas} = this; 128 | return ctx.getImageData(x, y, width || canvas.width, height || canvas.height); 129 | } 130 | 131 | getPixel(x, y) { 132 | x = Math.floor(x); 133 | y = Math.floor(y); 134 | const {data} = this.ctx.getImageData(x, y, 1, 1); 135 | return Array.prototype.slice.call(data); 136 | } 137 | 138 | setPixel(x, y, r, g, b, a) { 139 | x = Math.floor(x); 140 | y = Math.floor(y); 141 | const {width, data} = this.getImageData(); 142 | const i = (x + y * width) * 4; 143 | data[i + 0] = r; 144 | data[i + 1] = g; 145 | data[i + 2] = b; 146 | data[i + 3] = a; 147 | return this; 148 | } 149 | 150 | clearCircle(x, y, radius = 20) { 151 | const {ctx} = this; 152 | ctx.save(); 153 | ctx.globalCompositeOperation = 'destination-out'; 154 | this.circle(x, y, radius); 155 | ctx.globalCompositeOperation = 'source-over'; 156 | ctx.restore(); 157 | return this; 158 | } 159 | 160 | translateAnd(x, y, fn) { 161 | const {ctx} = this; 162 | ctx.save(); 163 | ctx.translate(x, y); 164 | fn(ctx); 165 | ctx.restore(); 166 | return this; 167 | } 168 | 169 | clear(r, g, b, a = 1) { 170 | const color = getColour(r, g, b, a); 171 | const {ctx} = this; 172 | const {width, height} = this.canvas; 173 | ctx.save(); 174 | ctx.setTransform(1, 0, 0, 1, 0, 0); 175 | if (color) { 176 | ctx.fillStyle = color; 177 | ctx.fillRect(0, 0, width, height); 178 | } else { 179 | ctx.clearRect(0, 0, width, height); 180 | } 181 | ctx.beginPath(); 182 | ctx.restore(); 183 | return this; 184 | } 185 | 186 | size(width = window.innerWidth, height = window.innerHeight) { 187 | this.canvas.width = width; 188 | this.canvas.height = height; 189 | return this; 190 | } 191 | 192 | destroy() { 193 | if (this.canvas.parentElement) { 194 | this.canvas.parentElement.removeChild(this.canvas); 195 | } 196 | this.canvas = null; 197 | this.ctx = null; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/graphics/open-image.js: -------------------------------------------------------------------------------- 1 | export default function openImage(canvas) { 2 | const {width, height} = canvas; 3 | const win = window.open('', 'image'); 4 | const src = canvas.toDataURL('image/png'); 5 | win.document.write(``); 6 | } 7 | -------------------------------------------------------------------------------- /src/graphics/shape.js: -------------------------------------------------------------------------------- 1 | function triangle(ctx, x, y, width, height, angle = 0) { 2 | if (angle !== 0) { 3 | ctx.save(); 4 | ctx.translate(x, y); 5 | ctx.rotate(angle); 6 | ctx.beginPath(); 7 | ctx.moveTo(0 - width / 2, 0 + height / 2); 8 | ctx.lineTo(0, 0 - height / 2); 9 | ctx.lineTo(0 + width / 2, 0 + height / 2); 10 | ctx.closePath(); 11 | ctx.stroke(); 12 | ctx.fill(); 13 | ctx.restore(); 14 | } else { 15 | ctx.beginPath(); 16 | ctx.moveTo(x - width / 2, y + height / 2); 17 | ctx.lineTo(x, y - height / 2); 18 | ctx.lineTo(x + width / 2, y + height / 2); 19 | ctx.closePath(); 20 | ctx.stroke(); 21 | ctx.fill(); 22 | } 23 | } 24 | 25 | function triangleABC(ctx, x1, y1, x2, y2, x3, y3) { 26 | ctx.beginPath(); 27 | ctx.moveTo(x1, y1); 28 | ctx.lineTo(x2, y2); 29 | ctx.lineTo(x3, y3); 30 | ctx.closePath(); 31 | ctx.stroke(); 32 | ctx.fill(); 33 | } 34 | 35 | function cross(ctx, radius) { 36 | ctx.beginPath(); 37 | ctx.moveTo(-radius, -radius); 38 | ctx.lineTo(radius, radius); 39 | ctx.moveTo(-radius, radius); 40 | ctx.lineTo(radius, -radius); 41 | ctx.stroke(); 42 | } 43 | 44 | export default { 45 | triangle, 46 | triangleABC, 47 | cross 48 | }; 49 | -------------------------------------------------------------------------------- /src/graphics/spritesheet-player.js: -------------------------------------------------------------------------------- 1 | import looper from '../loop'; 2 | 3 | const dpr = Math.min(2, typeof window !== 'undefined' && (window.devicePixelRatio || 1)); 4 | 5 | export default class SpritesheetPlayer { 6 | constructor({ 7 | canvas = (typeof document !== 'undefined' && document.createElement('canvas')), 8 | data = null, 9 | image, 10 | fps = 12, 11 | trim = true, 12 | delay = 0, 13 | repeatDelay = 0, 14 | scale = 1, 15 | loop = true 16 | } = {}) { 17 | this.canvas = canvas; 18 | this.data = data; 19 | this.image = image; 20 | this.trim = trim; 21 | this.interval = 1000 / fps; 22 | this.last = 0; 23 | this.currentFrame = 0; 24 | this.playing = false; 25 | this.delay = delay; 26 | this.loop = loop; 27 | this.repeatDelay = repeatDelay; 28 | this.remainingDelay = delay; 29 | this.scale = scale * dpr; 30 | 31 | this.update = this.update.bind(this); 32 | 33 | if (data) { 34 | this.init(data); 35 | } 36 | } 37 | 38 | play() { 39 | if (this.playing) { 40 | return; 41 | } 42 | this.remainingDelay = this.delay; 43 | this.elapsed = 0; 44 | looper.add(this.update); 45 | looper.start(); 46 | this.playing = true; 47 | } 48 | 49 | pause() { 50 | looper.remove(this.looper); 51 | // loop.stop(); 52 | this.playing = false; 53 | } 54 | 55 | reset() { 56 | this.currentFrame = 0; 57 | this.remainingDelay = this.delay; 58 | this.elapsed = 0; 59 | this.pause(); 60 | this.draw(0); 61 | } 62 | 63 | updateDimensions() { 64 | this.canvas.width = this.w * this.scale; 65 | this.canvas.height = this.h * this.scale; 66 | this.canvas.style.width = `${this.w * this.scale / dpr}px`; 67 | this.canvas.style.height = `${this.h * this.scale / dpr}px`; 68 | } 69 | 70 | setScale(value) { 71 | this.scale = value * dpr; 72 | this.updateDimensions(); 73 | this.reset(); 74 | } 75 | 76 | init(data) { 77 | const rawFrames = Array.isArray(data.frames) ? data.frames : Object.keys(data.frames).map(i => data.frames[i]); 78 | const trimX = this.trim ? Math.min(...rawFrames.map(f => f.spriteSourceSize.x)) : 0; 79 | const trimY = this.trim ? Math.min(...rawFrames.map(f => f.spriteSourceSize.y)) : 0; 80 | const frames = rawFrames.map(f => Object.assign({}, f.frame, { 81 | tx: f.spriteSourceSize.x - trimX, 82 | ty: f.spriteSourceSize.y - trimY 83 | })); 84 | 85 | if (this.trim) { 86 | this.w = Math.max(...frames.map(f => f.tx + f.w)); 87 | this.h = Math.max(...frames.map(f => f.ty + f.h)); 88 | } else { 89 | this.w = Math.max(...rawFrames.map(f => f.sourceSize.w)); 90 | this.h = Math.max(...rawFrames.map(f => f.sourceSize.h)); 91 | } 92 | 93 | this.frames = frames; 94 | this.updateDimensions(); 95 | this.ctx = this.canvas.getContext('2d'); 96 | 97 | if (typeof this.image === 'string') { 98 | const img = new Image(); 99 | img.onload = () => this.setImg(img); 100 | img.src = this.image; 101 | } else { 102 | this.setImg(this.image); 103 | } 104 | } 105 | 106 | setImg(img) { 107 | this.img = img; 108 | this.draw(0); 109 | } 110 | 111 | update(df, dt) { 112 | this.elapsed += dt; 113 | if (this.elapsed < this.interval) { 114 | return; 115 | } 116 | this.elapsed = 0; 117 | 118 | if (this.remainingDelay > 0) { 119 | this.remainingDelay -= this.interval; 120 | return; 121 | } 122 | 123 | if (!this.img) { 124 | return; 125 | } 126 | 127 | let currentFrame = this.currentFrame + 1; 128 | 129 | if (currentFrame === this.frames.length) { 130 | if (!this.loop) { 131 | return; 132 | } 133 | this.remainingDelay = this.repeatDelay; 134 | currentFrame = 0; 135 | } 136 | 137 | this.draw(currentFrame); 138 | 139 | this.currentFrame = currentFrame; 140 | } 141 | 142 | draw(currentFrame) { 143 | // this.ctx.fillStyle = 'red'; 144 | // this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); 145 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 146 | 147 | const frame = this.frames[currentFrame]; 148 | 149 | this.ctx.drawImage( 150 | this.img, 151 | frame.x, 152 | frame.y, 153 | frame.w, 154 | frame.h, 155 | frame.tx * this.scale, 156 | frame.ty * this.scale, 157 | frame.w * this.scale, 158 | frame.h * this.scale 159 | ); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/gui/index.js: -------------------------------------------------------------------------------- 1 | import loadScript from '../http/load-script'; 2 | import localHost from '../platform/local-host'; 3 | 4 | // example usage: 5 | // 6 | // const opts = { 7 | // friction: 0.9, 8 | // maxSpeed: 1 9 | // }; 10 | // gui(true) 11 | // .then((g) => { 12 | // g.add(opts, 'friction', 0.7, 0.999); 13 | // g.add(opts, 'maxSpeed', 0.5, 2).onChange((value) => console.log(value)); 14 | // }) 15 | // .catch((err) => console.error(err)); 16 | 17 | export default function gui(localhostOnly = false) { 18 | if (localhostOnly && !localHost()) { 19 | return new Promise(() => {}); 20 | } 21 | return new Promise((resolve, reject) => { 22 | loadScript('https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.1/dat.gui.min.js', (err, src) => { 23 | if (err) { 24 | console.error('Error loading script', src); 25 | reject(new Error('Error loading script')); 26 | return; 27 | } 28 | const g = new window.dat.GUI({autoPlace: true}); 29 | 30 | const style = document.createElement('style'); 31 | document.head.appendChild(style); 32 | const s = style.sheet; 33 | s.insertRule('.dg.ac {overflow: visible !important; z-index:10000 !important}', 0); 34 | s.insertRule('.dg * {font-size:11px !important}', 0); 35 | s.insertRule('.dg input {font:11px Lucida Grande,sans-serif !important}', 0); 36 | 37 | resolve(g); 38 | }); 39 | }); 40 | } 41 | 42 | gui.localHost = localHost; 43 | -------------------------------------------------------------------------------- /src/http/get-location.js: -------------------------------------------------------------------------------- 1 | export default function getLocation(href) { 2 | const l = document.createElement('a'); 3 | l.href = href; 4 | 5 | return { 6 | hash: l.hash, 7 | host: l.host, 8 | hostname: l.hostname, 9 | pathname: l.pathname, 10 | port: l.port, 11 | protocol: l.protocol, 12 | search: l.search 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/http/index.js: -------------------------------------------------------------------------------- 1 | import getLocation from './get-location'; 2 | import jsonp from './jsonp'; 3 | import loadScript from './load-script'; 4 | import urlParams from './url-params'; 5 | import xhr from './xhr'; 6 | 7 | export default { 8 | getLocation, 9 | jsonp, 10 | loadScript, 11 | urlParams, 12 | xhr 13 | }; 14 | -------------------------------------------------------------------------------- /src/http/jsonp.js: -------------------------------------------------------------------------------- 1 | export default function jsonp(url, timeout = 10000) { 2 | return new Promise((resolve, reject) => { 3 | const script = document.createElement('script'); 4 | const callback = `jsonp_callback_${Math.round(100000 * Math.random())}`; 5 | const separator = url.indexOf('?') >= 0 ? '&' : '?'; 6 | 7 | const timeoutId = window.setTimeout(() => { 8 | window[callback](null, 'jsonp error'); 9 | }, timeout); 10 | 11 | window[callback] = function(data, err = null) { 12 | window.clearTimeout(timeoutId); 13 | delete window[callback]; 14 | document.body.removeChild(script); 15 | if (err) { 16 | reject(err); 17 | } else { 18 | resolve(data); 19 | } 20 | }; 21 | 22 | script.src = `${url}${separator}callback=${callback}`; 23 | document.body.appendChild(script); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/http/load-script.js: -------------------------------------------------------------------------------- 1 | export default function loadScript(src, cb) { 2 | const script = document.createElement('script'); 3 | script.src = src; 4 | script.addEventListener('load', () => cb(null, src)); 5 | script.addEventListener('error', () => cb(true, src)); 6 | document.body.appendChild(script); 7 | return script; 8 | } 9 | -------------------------------------------------------------------------------- /src/http/url-params.js: -------------------------------------------------------------------------------- 1 | const plus = /\+/g; // match '+' symbol 2 | const search = /([^&=]+)=?([^&]*)/g; 3 | 4 | function decode(str) { 5 | return decodeURIComponent(str.replace(plus, ' ')); 6 | } 7 | 8 | export default function urlParams(query) { 9 | query = query || window.location.search.slice(1); 10 | 11 | const params = {}; 12 | let match = search.exec(query); 13 | while (match) { 14 | params[decode(match[1])] = decode(match[2]); 15 | match = search.exec(query); 16 | } 17 | return params; 18 | } 19 | -------------------------------------------------------------------------------- /src/http/xhr.js: -------------------------------------------------------------------------------- 1 | export default function xhr(url, type = 'json') { 2 | const p = new Promise((resolve, reject) => { 3 | const req = new XMLHttpRequest(); 4 | req.addEventListener('load', () => { 5 | let response = req.response; 6 | if (type === 'json' && typeof response === 'string') { 7 | response = JSON.parse(response); 8 | } 9 | resolve(response); 10 | }); 11 | req.addEventListener('error', () => reject(req.status)); 12 | req.open('GET', url); 13 | req.responseType = type; 14 | // req.setRequestHeader('Content-type', 'application/json; charset=utf-8'); 15 | req.send(); 16 | }); 17 | return p; 18 | } 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './polyfill'; 2 | import array from './array'; 3 | import dom from './dom'; 4 | import ease from './ease'; 5 | import events from './events'; 6 | import fullscreen from './fullscreen'; 7 | import graphics from './graphics'; 8 | import gui from './gui'; 9 | import http from './http'; 10 | import input from './input'; 11 | import linkedList from './linked-list'; 12 | import loop, {Loop} from './loop'; 13 | import math from './math'; 14 | import media from './media'; 15 | import object from './object'; 16 | import objectPool from './object-pool'; 17 | import Particle from './particle'; 18 | import ParticleGroup from './particle/particle-group'; 19 | import platform from './platform'; 20 | import popup from './popup'; 21 | import QuadTree from './quad-tree'; 22 | import share from './share'; 23 | import storage from './storage'; 24 | import string from './string'; 25 | import track from './track'; 26 | import Tween from './tween'; 27 | import visibility from './visibility'; 28 | 29 | export default { 30 | array, 31 | dom, 32 | ease, 33 | events, 34 | fullscreen, 35 | graphics, 36 | gui, 37 | http, 38 | input, 39 | linkedList, 40 | loop, 41 | Loop, 42 | math, 43 | media, 44 | object, 45 | objectPool, 46 | Particle, 47 | ParticleGroup, 48 | platform, 49 | popup, 50 | QuadTree, 51 | share, 52 | storage, 53 | string, 54 | Tween, 55 | track, 56 | visibility 57 | }; 58 | -------------------------------------------------------------------------------- /src/input/click-outside.js: -------------------------------------------------------------------------------- 1 | function getTest(el) { 2 | if (Array.isArray(el)) { 3 | return target => el.includes(target); 4 | } 5 | if (typeof el === 'function') { 6 | return target => el(target); 7 | } 8 | return target => target === el; 9 | } 10 | 11 | export default function clickOutside(el, fn) { 12 | const test = getTest(el); 13 | 14 | function onClickOutside(event) { 15 | let target = event.target; 16 | let inside = false; 17 | 18 | while (target && target !== document.body) { 19 | if (test(target)) { 20 | event.stopImmediatePropagation(); 21 | inside = true; 22 | break; 23 | } 24 | target = target.parentNode; 25 | } 26 | 27 | if (!inside) { 28 | fn(event); 29 | } 30 | } 31 | 32 | function onTouchOutside(event) { 33 | document.body.removeEventListener('click', onClickOutside); 34 | onClickOutside(event); 35 | } 36 | 37 | function destroy() { 38 | document.body.removeEventListener('click', onClickOutside); 39 | document.body.removeEventListener('touchstart', onTouchOutside); 40 | } 41 | 42 | destroy(); 43 | 44 | document.body.addEventListener('click', onClickOutside); 45 | document.body.addEventListener('touchstart', onTouchOutside); 46 | 47 | return { 48 | destroy 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/input/index.js: -------------------------------------------------------------------------------- 1 | import clickOutside from './click-outside'; 2 | import keyboard from './keyboard'; 3 | import keyInput from './key-input'; 4 | import microphone from './microphone'; 5 | import mouseLeftWindow from './mouse-left-window'; 6 | import mouseWheel from './mouse-wheel'; 7 | import pointerCoords from './pointer-coords'; 8 | import touchInput from './touch-input'; 9 | 10 | export default { 11 | clickOutside, 12 | keyboard, 13 | keyInput, 14 | microphone, 15 | mouseLeftWindow, 16 | mouseWheel, 17 | pointerCoords, 18 | touchInput 19 | }; 20 | -------------------------------------------------------------------------------- /src/input/key-input.js: -------------------------------------------------------------------------------- 1 | import array from '../array/array'; 2 | import Emitter from '../events/emitter'; 3 | import keyboard from './keyboard'; 4 | 5 | export default function keyInput() { 6 | const api = new Emitter(); 7 | const keys = array(256, false); 8 | 9 | function emitKey(keyCode) { 10 | const keyName = Object.keys(keyboard).reduce((value, key) => { 11 | return keyboard[key] === keyCode ? key : value; 12 | }, '').toLowerCase(); 13 | if (keyName) { 14 | api.emit(keyName.toLowerCase()); 15 | } 16 | } 17 | 18 | function onKeyDown(event) { 19 | event.preventDefault(); 20 | keys[event.keyCode] = true; 21 | api.emit('keydown', event.keyCode); 22 | emitKey(event.keyCode); 23 | } 24 | 25 | function onKeyUp(event) { 26 | event.preventDefault(); 27 | keys[event.keyCode] = false; 28 | api.emit('keyup', event.keyCode); 29 | } 30 | 31 | function add() { 32 | document.addEventListener('keydown', onKeyDown, false); 33 | document.addEventListener('keyup', onKeyUp, false); 34 | } 35 | 36 | function remove() { 37 | document.removeEventListener('keydown', onKeyDown); 38 | document.removeEventListener('keyup', onKeyUp); 39 | } 40 | 41 | function isDown(key) { 42 | return keys[key]; 43 | } 44 | 45 | function left() { 46 | return keys[keyboard.LEFT] || keys[keyboard.A]; 47 | } 48 | 49 | function right() { 50 | return keys[keyboard.RIGHT] || keys[keyboard.D]; 51 | } 52 | 53 | function up() { 54 | return keys[keyboard.UP] || keys[keyboard.W]; 55 | } 56 | 57 | function down() { 58 | return keys[keyboard.DOWN] || keys[keyboard.S]; 59 | } 60 | 61 | function space() { 62 | return keys[keyboard.SPACE]; 63 | } 64 | 65 | function enable(value = true) { 66 | remove(); 67 | if (value) { 68 | add(); 69 | } 70 | } 71 | 72 | add(); 73 | 74 | return Object.assign(api, { 75 | keyboard, 76 | enable, 77 | isDown, 78 | left, 79 | right, 80 | up, 81 | down, 82 | space 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /src/input/keyboard.js: -------------------------------------------------------------------------------- 1 | export default { 2 | A: 'A'.charCodeAt(0), 3 | B: 'B'.charCodeAt(0), 4 | C: 'C'.charCodeAt(0), 5 | D: 'D'.charCodeAt(0), 6 | E: 'E'.charCodeAt(0), 7 | F: 'F'.charCodeAt(0), 8 | G: 'G'.charCodeAt(0), 9 | H: 'H'.charCodeAt(0), 10 | I: 'I'.charCodeAt(0), 11 | J: 'J'.charCodeAt(0), 12 | K: 'K'.charCodeAt(0), 13 | L: 'L'.charCodeAt(0), 14 | M: 'M'.charCodeAt(0), 15 | N: 'N'.charCodeAt(0), 16 | O: 'O'.charCodeAt(0), 17 | P: 'P'.charCodeAt(0), 18 | Q: 'Q'.charCodeAt(0), 19 | R: 'R'.charCodeAt(0), 20 | S: 'S'.charCodeAt(0), 21 | T: 'T'.charCodeAt(0), 22 | U: 'U'.charCodeAt(0), 23 | V: 'V'.charCodeAt(0), 24 | W: 'W'.charCodeAt(0), 25 | X: 'X'.charCodeAt(0), 26 | Y: 'Y'.charCodeAt(0), 27 | Z: 'Z'.charCodeAt(0), 28 | ZERO: '0'.charCodeAt(0), 29 | ONE: '1'.charCodeAt(0), 30 | TWO: '2'.charCodeAt(0), 31 | THREE: '3'.charCodeAt(0), 32 | FOUR: '4'.charCodeAt(0), 33 | FIVE: '5'.charCodeAt(0), 34 | SIX: '6'.charCodeAt(0), 35 | SEVEN: '7'.charCodeAt(0), 36 | EIGHT: '8'.charCodeAt(0), 37 | NINE: '9'.charCodeAt(0), 38 | NUMPAD_0: 96, 39 | NUMPAD_1: 97, 40 | NUMPAD_2: 98, 41 | NUMPAD_3: 99, 42 | NUMPAD_4: 100, 43 | NUMPAD_5: 101, 44 | NUMPAD_6: 102, 45 | NUMPAD_7: 103, 46 | NUMPAD_8: 104, 47 | NUMPAD_9: 105, 48 | NUMPAD_MULTIPLY: 106, 49 | NUMPAD_ADD: 107, 50 | NUMPAD_ENTER: 108, 51 | NUMPAD_SUBTRACT: 109, 52 | NUMPAD_DECIMAL: 110, 53 | NUMPAD_DIVIDE: 111, 54 | F1: 112, 55 | F2: 113, 56 | F3: 114, 57 | F4: 115, 58 | F5: 116, 59 | F6: 117, 60 | F7: 118, 61 | F8: 119, 62 | F9: 120, 63 | F10: 121, 64 | F11: 122, 65 | F12: 123, 66 | F13: 124, 67 | F14: 125, 68 | F15: 126, 69 | COLON: 186, 70 | EQUALS: 187, 71 | UNDERSCORE: 189, 72 | QUESTION_MARK: 191, 73 | TILDE: 192, 74 | OPEN_BRACKET: 219, 75 | BACKWARD_SLASH: 220, 76 | CLOSED_BRACKET: 221, 77 | QUOTES: 222, 78 | BACKSPACE: 8, 79 | TAB: 9, 80 | CLEAR: 12, 81 | ENTER: 13, 82 | SHIFT: 16, 83 | CONTROL: 17, 84 | ALT: 18, 85 | CAPS_LOCK: 20, 86 | ESC: 27, 87 | SPACE: 32, 88 | PAGE_UP: 33, 89 | PAGE_DOWN: 34, 90 | END: 35, 91 | HOME: 36, 92 | LEFT: 37, 93 | UP: 38, 94 | RIGHT: 39, 95 | DOWN: 40, 96 | INSERT: 45, 97 | DELETE: 46, 98 | HELP: 47, 99 | NUM_LOCK: 144 100 | }; 101 | -------------------------------------------------------------------------------- /src/input/microphone.js: -------------------------------------------------------------------------------- 1 | import Emitter from '../events/emitter'; 2 | 3 | export default function microphone() { 4 | const mic = new Emitter(); 5 | let stream = null; 6 | 7 | const getUserMedia = (navigator.getUserMedia || 8 | navigator.webkitGetUserMedia || 9 | navigator.mozGetUserMedia || 10 | navigator.msGetUserMedia); 11 | 12 | const isSupported = !!getUserMedia; 13 | 14 | function connect() { 15 | if (!isSupported) { 16 | mic.emit('error', 'Not supported'); 17 | return; 18 | } 19 | getUserMedia({ 20 | audio: true 21 | }, (mediaStream) => { 22 | stream = mediaStream; 23 | mic.emit('connect', stream); 24 | }, (e) => { 25 | if (e.name === 'PermissionDeniedError' || e === 'PERMISSION_DENIED') { 26 | console.log('Permission denied. Undo by clicking the camera icon in the address bar'); 27 | mic.emit('denied'); 28 | } else { 29 | mic.emit('error', e.message || e); 30 | } 31 | }); 32 | } 33 | 34 | function disconnect() { 35 | if (stream) { 36 | stream.stop(); 37 | stream = null; 38 | } 39 | } 40 | 41 | function createWebAudioSource(webAudioContext, connectTo) { 42 | if (!stream) { 43 | return null; 44 | } 45 | 46 | const source = webAudioContext.createMediaStreamSource(stream); 47 | 48 | if (connectTo) { 49 | source.connect(connectTo); 50 | } 51 | 52 | // HACK: stops moz garbage collection killing the stream 53 | // see https://support.mozilla.org/en-US/questions/984179 54 | if (navigator.mozGetUserMedia) { 55 | window.hack_for_mozilla = source; 56 | } 57 | 58 | return source; 59 | } 60 | 61 | return Object.assign(mic, { 62 | connect, 63 | disconnect, 64 | createWebAudioSource, 65 | isSupported: () => isSupported, 66 | stream: () => stream 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /src/input/mouse-left-window.js: -------------------------------------------------------------------------------- 1 | export default function mouseLeftWindow(fn) { 2 | function handler(event) { 3 | const from = event.relatedTarget || event.toElement; 4 | if (!from || from.nodeName === 'HTML') { 5 | fn(event); 6 | } 7 | } 8 | 9 | document.addEventListener('mouseout', handler, false); 10 | 11 | return { 12 | destroy () { 13 | document.removeEventListener('mouseout', handler); 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/input/mouse-wheel.js: -------------------------------------------------------------------------------- 1 | import Emitter from '../events/emitter'; 2 | 3 | export default function mouseWheel(speed) { 4 | speed = speed || 2; 5 | 6 | let wheel = null; 7 | 8 | function wheelHandler(event) { 9 | const direction = (event.detail < 0 || event.wheelDelta > 0) ? 1 : -1; 10 | const delta = direction * speed; 11 | 12 | if (direction > 0) { 13 | wheel.emit('up', delta); 14 | } else { 15 | wheel.emit('down', delta); 16 | } 17 | 18 | wheel.emit('update', delta); 19 | } 20 | 21 | function add() { 22 | if ('onmousewheel' in window) { 23 | window.addEventListener('mousewheel', wheelHandler, false); 24 | } else if (window.addEventListener) { 25 | window.addEventListener('DOMMouseScroll', wheelHandler, false); 26 | } 27 | } 28 | 29 | function remove() { 30 | if ('onmousewheel' in window) { 31 | window.removeEventListener('mousewheel', wheelHandler, false); 32 | } else if (window.removeEventListener) { 33 | window.removeEventListener('DOMMouseScroll', wheelHandler, false); 34 | } 35 | } 36 | 37 | add(); 38 | 39 | wheel = Object.assign(new Emitter(), { 40 | add, 41 | remove 42 | }); 43 | 44 | return wheel; 45 | } 46 | -------------------------------------------------------------------------------- /src/input/pointer-coords.js: -------------------------------------------------------------------------------- 1 | import getPageHeight from '../dom/get-page-height'; 2 | 3 | export default function pointerCoords() { 4 | let self = null; 5 | 6 | function calculateCoords(event) { 7 | const touch = event.touches && event.touches.length; 8 | const p = touch ? event.touches[0] : event; 9 | const cX = p.clientX || 0; 10 | const cY = p.clientY || 0; 11 | const pX = window.pageXOffset; 12 | const pY = window.pageYOffset; 13 | self.event = event; 14 | self.clientX = cX; 15 | self.clientY = cY; 16 | self.x = cX + pX; 17 | self.y = cY + pY; 18 | self.percentX = self.x / window.innerWidth; 19 | self.percentY = self.y / getPageHeight(); 20 | } 21 | 22 | self = { 23 | event: null, 24 | clientX: 0, 25 | clientY: 0, 26 | x: 0, 27 | y: 0, 28 | percentX: 0, 29 | percentY: 0, 30 | isListening: false, 31 | 32 | on: function() { 33 | document.body.addEventListener('mousemove', calculateCoords); 34 | document.body.addEventListener('touchmove', calculateCoords); 35 | self.isListening = true; 36 | return this; 37 | }, 38 | off: function() { 39 | document.body.removeEventListener('mousemove', calculateCoords); 40 | document.body.removeEventListener('touchmove', calculateCoords); 41 | self.isListening = false; 42 | return this; 43 | } 44 | }; 45 | return self; 46 | } 47 | -------------------------------------------------------------------------------- /src/input/touch-input.js: -------------------------------------------------------------------------------- 1 | import Emitter from '../events/emitter'; 2 | 3 | export default function touchInput(el) { 4 | el = el || document.body; 5 | 6 | const data = { 7 | start: [-1, -1], 8 | move: [-1, -1], 9 | end: [-1, -1], 10 | position: [-1, -1], 11 | distance: [0, 0], 12 | direction: ['none', 'none'], 13 | touching: false, 14 | originalEvent: null 15 | }; 16 | 17 | let self = null; 18 | 19 | function touchHandler(event) { 20 | if (!(event && event.touches)) { 21 | return; 22 | } 23 | data.originalEvent = event; 24 | const touch = event.touches[0]; 25 | const x = touch && touch.pageX; 26 | const y = touch && touch.pageY; 27 | 28 | switch (event.type) { 29 | case 'touchstart': 30 | data.start[0] = data.move[0] = data.end[0] = data.position[0] = x; 31 | data.start[1] = data.move[1] = data.end[1] = data.position[1] = y; 32 | data.touching = true; 33 | self.emit('start', data); 34 | break; 35 | case 'touchmove': 36 | data.move[0] = data.position[0] = x; 37 | data.move[1] = data.position[1] = y; 38 | self.emit('move', data); 39 | break; 40 | case 'touchend': 41 | data.end[0] = data.position[0] = x; 42 | data.end[1] = data.position[1] = y; 43 | data.touching = false; 44 | self.emit('end', data); 45 | break; 46 | default: break; 47 | } 48 | } 49 | 50 | function listen(elem) { 51 | el = elem || el; 52 | el.addEventListener('touchstart', touchHandler); 53 | el.addEventListener('touchmove', touchHandler); 54 | el.addEventListener('touchend', touchHandler); 55 | return self; 56 | } 57 | 58 | function destroy() { 59 | self.removeAllListeners(); 60 | el.removeEventListener('touchstart', touchHandler); 61 | el.removeEventListener('touchmove', touchHandler); 62 | el.removeEventListener('touchend', touchHandler); 63 | el = null; 64 | return self; 65 | } 66 | 67 | listen(el); 68 | 69 | self = Object.assign(new Emitter(), { 70 | listen, 71 | isDown: () => data.touching, 72 | getTouch: () => data, 73 | destroy 74 | }); 75 | 76 | return self; 77 | } 78 | -------------------------------------------------------------------------------- /src/linked-list/index.js: -------------------------------------------------------------------------------- 1 | export default function linkedList(arr = []) { 2 | 3 | let first, 4 | last; 5 | 6 | /* 7 | item = { 8 | 'next': null, 9 | 'prev': null 10 | } 11 | 12 | var item = linkedList.getFirst(); 13 | while(item) { 14 | // do stuff 15 | item = item.next; 16 | } 17 | */ 18 | 19 | function remove(item) { 20 | if (item.next) { 21 | item.next.prev = item.prev; 22 | } 23 | if (item.prev) { 24 | item.prev.next = item.next; 25 | } 26 | if (item === first) { 27 | first = item.next; 28 | } 29 | if (item === last) { 30 | last = item.prev; 31 | } 32 | item.next = item.prev = null; 33 | 34 | return item; 35 | } 36 | 37 | function insertAfter(item, after) { 38 | remove(item); 39 | 40 | item.prev = after; 41 | item.next = after.next; 42 | 43 | if (!after.next) { 44 | last = item; 45 | } else { 46 | after.next.prev = item; 47 | } 48 | 49 | after.next = item; 50 | 51 | return item; 52 | } 53 | 54 | function insertBefore(item, before) { 55 | remove(item); 56 | 57 | item.prev = before.prev; 58 | item.next = before; 59 | 60 | if (!before.prev) { 61 | first = item; 62 | } else { 63 | before.prev.next = item; 64 | } 65 | 66 | before.prev = item; 67 | 68 | return item; 69 | } 70 | 71 | function add(item) { 72 | if (!first) { 73 | first = last = item; 74 | } else { 75 | let i = first; 76 | while (i.next) { 77 | i = i.next; 78 | } 79 | insertAfter(item, i); 80 | } 81 | return item; 82 | } 83 | 84 | function forEach(fn) { 85 | let item = first; 86 | while (item) { 87 | fn(item); 88 | item = item.next; 89 | } 90 | } 91 | 92 | function map(fn) { 93 | const list = linkedList(); 94 | let item = first; 95 | while (item) { 96 | list.add(fn(item)); 97 | item = item.next; 98 | } 99 | return list; 100 | } 101 | 102 | arr.forEach((item) => add(item)); 103 | 104 | return { 105 | get first () { 106 | return first; 107 | }, 108 | getFirst () { 109 | return first; 110 | }, 111 | get last () { 112 | return last; 113 | }, 114 | getLast () { 115 | return last; 116 | }, 117 | get length () { 118 | return this.getCount(); 119 | }, 120 | getCount () { 121 | let count = 0; 122 | let i = first; 123 | while (i) { 124 | count++; 125 | i = i.next; 126 | } 127 | return count; 128 | }, 129 | add, 130 | remove, 131 | insertAfter, 132 | insertBefore, 133 | forEach, 134 | map 135 | }; 136 | } 137 | -------------------------------------------------------------------------------- /src/loop/index.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | 3 | export class Loop { 4 | constructor() { 5 | this.update = this.update.bind(this); 6 | 7 | this.last = 0; 8 | this.raf = null; 9 | this.running = false; 10 | 11 | this.emitter = new EventEmitter(); 12 | } 13 | 14 | start() { 15 | if (this.running) { 16 | return; 17 | } 18 | 19 | this.last = 0; 20 | this.running = true; 21 | this.update(); 22 | } 23 | 24 | stop() { 25 | if (!this.running) { 26 | return; 27 | } 28 | 29 | this.running = false; 30 | window.cancelAnimationFrame(this.raf); 31 | } 32 | 33 | update() { 34 | if (!this.running) { 35 | return; 36 | } 37 | 38 | this.raf = window.requestAnimationFrame(this.update); 39 | 40 | const now = Date.now(); 41 | const deltaTime = now - this.last; 42 | const deltaFrames = deltaTime * 0.06; 43 | this.last = now; 44 | 45 | this.emitter.emit('update', deltaFrames, deltaTime); 46 | } 47 | 48 | add(fn) { 49 | this.emitter.on('update', fn); 50 | return this; 51 | } 52 | 53 | remove(fn) { 54 | this.emitter.removeListener('update', fn); 55 | } 56 | } 57 | 58 | export default new Loop(); 59 | -------------------------------------------------------------------------------- /src/math/angle.js: -------------------------------------------------------------------------------- 1 | export default function angle(x1, y1, x2, y2) { 2 | const dx = x2 - x1; 3 | const dy = y2 - y1; 4 | return Math.atan2(dy, dx); 5 | } 6 | -------------------------------------------------------------------------------- /src/math/cerp.js: -------------------------------------------------------------------------------- 1 | export default function cerp(from, to, weight = 0.3) { 2 | const f = (1 - Math.cos(weight * Math.PI)) / 2; 3 | return (from * (1 - f) + to * f); 4 | } 5 | -------------------------------------------------------------------------------- /src/math/circle-distribution.js: -------------------------------------------------------------------------------- 1 | export default function circleDistribution(radius, origin = {x: 0, y: 0}, p = {x: 0, y: 0}) { 2 | const r = Math.sqrt(Math.random()) * radius; 3 | const theta = Math.random() * Math.PI * 2; 4 | p.x = origin.x + Math.cos(theta) * r; 5 | p.y = origin.y + Math.sin(theta) * r; 6 | return p; 7 | } 8 | -------------------------------------------------------------------------------- /src/math/clamp.js: -------------------------------------------------------------------------------- 1 | export default function clamp(value, min, max) { 2 | if (min > max) { 3 | const a = min; 4 | min = max; 5 | max = a; 6 | } 7 | if (value < min) { 8 | return min; 9 | } 10 | if (value > max) { 11 | return max; 12 | } 13 | return value; 14 | } 15 | -------------------------------------------------------------------------------- /src/math/coin-toss.js: -------------------------------------------------------------------------------- 1 | export default function coinToss(heads = true, tails = false) { 2 | return Math.random() > 0.5 ? heads : tails; 3 | } 4 | -------------------------------------------------------------------------------- /src/math/cross-product-2d.js: -------------------------------------------------------------------------------- 1 | /* 2 | The sign tells us if a is to the left (-) or the right (+) of b 3 | */ 4 | export default function crossProduct2d(aX, aY, bX, bY) { 5 | return aX * bY - aY * bX; 6 | } 7 | -------------------------------------------------------------------------------- /src/math/degrees.js: -------------------------------------------------------------------------------- 1 | const DEG = 180 / Math.PI; 2 | 3 | export default function degrees(radians) { 4 | return radians * DEG; 5 | } 6 | -------------------------------------------------------------------------------- /src/math/difference.js: -------------------------------------------------------------------------------- 1 | export default function difference(a, b) { 2 | return Math.abs(a - b); 3 | } 4 | -------------------------------------------------------------------------------- /src/math/distance-sq.js: -------------------------------------------------------------------------------- 1 | export default function distanceSQ(x1, y1, x2, y2) { 2 | const dx = x1 - x2; 3 | const dy = y1 - y2; 4 | return dx * dx + dy * dy; 5 | } 6 | -------------------------------------------------------------------------------- /src/math/distance.js: -------------------------------------------------------------------------------- 1 | export default function distance(x1, y1, x2, y2) { 2 | const dx = x1 - x2; 3 | const dy = y1 - y2; 4 | return Math.sqrt(dx * dx + dy * dy); 5 | } 6 | -------------------------------------------------------------------------------- /src/math/dot-product-2d.js: -------------------------------------------------------------------------------- 1 | /* 2 | - If A and B are perpendicular (at 90 degrees to each other), the result 3 | of the dot product will be zero, because cos(Θ) will be zero. 4 | - If the angle between A and B are less than 90 degrees, the dot product 5 | will be positive (greater than zero), as cos(Θ) will be positive, and 6 | the vector lengths are always positive values. 7 | - If the angle between A and B are greater than 90 degrees, the dot 8 | product will be negative (less than zero), as cos(Θ) will be negative, 9 | and the vector lengths are always positive values 10 | */ 11 | export default function dotProduct2d(aX, aY, bX, bY) { 12 | return aX * bX + aY * bY; 13 | } 14 | -------------------------------------------------------------------------------- /src/math/get-circle-points.js: -------------------------------------------------------------------------------- 1 | export default function getCirclePoints(originX, originY, radius, count, start, Class) { 2 | if (typeof start === 'undefined') { 3 | start = -Math.PI / 2; 4 | } 5 | 6 | const points = [], 7 | circ = Math.PI * 2, 8 | incr = circ / count; 9 | 10 | for (let i = start; i < circ + start; i += incr) { 11 | const ob = typeof Class === 'undefined' ? {} : new Class(); 12 | ob.x = originX + radius * Math.cos(i); 13 | ob.y = originY + radius * Math.sin(i); 14 | points.push(ob); 15 | } 16 | 17 | return points; 18 | } 19 | -------------------------------------------------------------------------------- /src/math/get-intersection-area.js: -------------------------------------------------------------------------------- 1 | export default function getIntersectionArea(aX, aY, aW, aH, bX, bY, bW, bH) { 2 | const overlapX = Math.max(0, Math.min(aX + aW, bX + bW) - Math.max(aX, bX)); 3 | const overlapY = Math.max(0, Math.min(aY + aH, bY + bH) - Math.max(aY, bY)); 4 | return overlapX * overlapY; 5 | } 6 | -------------------------------------------------------------------------------- /src/math/get-overlap-x.js: -------------------------------------------------------------------------------- 1 | export default function getOverlapX(aX, aW, bX, bW) { 2 | return Math.max(0, Math.min(aX + aW, bX + bW) - Math.max(aX, bX)); 3 | } 4 | -------------------------------------------------------------------------------- /src/math/get-overlap-y.js: -------------------------------------------------------------------------------- 1 | export default function getOverlapY(aY, aH, bY, bH) { 2 | return Math.max(0, Math.min(aY + aH, bY + bH) - Math.max(aY, bY)); 3 | } 4 | -------------------------------------------------------------------------------- /src/math/index.js: -------------------------------------------------------------------------------- 1 | import angle from './angle'; 2 | import cerp from './cerp'; 3 | import circleDistribution from './circle-distribution'; 4 | import clamp from './clamp'; 5 | import coinToss from './coin-toss'; 6 | import crossProduct2d from './cross-product-2d'; 7 | import degrees from './degrees'; 8 | import difference from './difference'; 9 | import distance from './distance'; 10 | import distanceSq from './distance-sq'; 11 | import dotProduct2d from './dot-product-2d'; 12 | import getCirclePoints from './get-circle-points'; 13 | import getIntersectionArea from './get-intersection-area'; 14 | import getOverlapX from './get-overlap-x'; 15 | import getOverlapY from './get-overlap-y'; 16 | import lerp from './lerp'; 17 | import map from './map'; 18 | import normalize from './normalize'; 19 | import orientation from './orientation'; 20 | import percentRemaining from './percent-remaining'; 21 | import perspective from './perspective'; 22 | import quadraticCurve from './quadratic-curve'; 23 | import radians from './radians'; 24 | import random from './random'; 25 | import randomInt from './random-int'; 26 | import randomSign from './random-sign'; 27 | import rotatePoint from './rotate-point'; 28 | import rotateToDeg from './rotate-to-deg'; 29 | import rotateToRad from './rotate-to-rad'; 30 | import roundTo from './round-to'; 31 | import roundToNearest from './round-to-nearest'; 32 | import size from './size'; 33 | import smerp from './smerp'; 34 | import smoothstep from './smoothstep'; 35 | import splitValueAndUnit from './split-value-and-unit'; 36 | import weightedAverage from './weighted-average'; 37 | import weightedDistribution from './weighted-distribution'; 38 | 39 | export default { 40 | angle, 41 | cerp, 42 | circleDistribution, 43 | clamp, 44 | coinToss, 45 | crossProduct2d, 46 | degrees, 47 | difference, 48 | distance, 49 | distanceSq, 50 | dotProduct2d, 51 | getCirclePoints, 52 | getIntersectionArea, 53 | getOverlapX, 54 | getOverlapY, 55 | lerp, 56 | map, 57 | normalize, 58 | orientation, 59 | percentRemaining, 60 | perspective, 61 | quadraticCurve, 62 | radians, 63 | random, 64 | randomInt, 65 | randomSign, 66 | rotatePoint, 67 | rotateToDeg, 68 | rotateToRad, 69 | roundTo, 70 | roundToNearest, 71 | smerp, 72 | smoothstep, 73 | size, 74 | splitValueAndUnit, 75 | weightedAverage, 76 | weightedDistribution 77 | }; 78 | -------------------------------------------------------------------------------- /src/math/lerp.js: -------------------------------------------------------------------------------- 1 | export default function lerp(from, to, weight = 0.3) { 2 | return from + (to - from) * weight; 3 | } 4 | -------------------------------------------------------------------------------- /src/math/map.js: -------------------------------------------------------------------------------- 1 | export default function map(v, a, b, x, y, clamp = true) { 2 | // value, min expected, max expected, map min, map max, clamp 3 | // e.g. map some value between 0 to 100 to -50 to 50 4 | // map(50, 0, 100, -50, 50) // 0 5 | // map(25, 0, 100, -50, 50) // -25 6 | if (v === a) { 7 | return x; 8 | } 9 | 10 | let val = (v - a) * (y - x) / (b - a) + x; 11 | 12 | if (!clamp) { 13 | return val; 14 | } 15 | 16 | if (y > x) { 17 | if (val > y) { 18 | val = y; 19 | } 20 | if (val < x) { 21 | val = x; 22 | } 23 | } else { 24 | if (val < y) { 25 | val = y; 26 | } 27 | if (val > x) { 28 | val = x; 29 | } 30 | } 31 | 32 | return val; 33 | } 34 | -------------------------------------------------------------------------------- /src/math/normalize.js: -------------------------------------------------------------------------------- 1 | export default function normalize(value, min, max) { 2 | return (value - min) / (max - min); 3 | } 4 | -------------------------------------------------------------------------------- /src/math/orientation.js: -------------------------------------------------------------------------------- 1 | export default function orientation(x, y) { 2 | return Math.atan2(y, x); 3 | } 4 | -------------------------------------------------------------------------------- /src/math/percent-remaining.js: -------------------------------------------------------------------------------- 1 | export default function percentRemaining(value, total) { 2 | return (value % total) / total; 3 | } 4 | -------------------------------------------------------------------------------- /src/math/perspective.js: -------------------------------------------------------------------------------- 1 | // x = x * perspective 2 | // y = y * perspective 3 | // scale = perspective 4 | 5 | export default function perspective(z, focalLength = 300) { 6 | return focalLength / (focalLength + z); 7 | } 8 | -------------------------------------------------------------------------------- /src/math/quadratic-curve.js: -------------------------------------------------------------------------------- 1 | export default function quadraticCurve(fromX, fromY, cpX, cpY, toX, toY, goThroughCP = false) { 2 | const n = 20; 3 | const points = [fromX, fromY]; 4 | let xa = 0; 5 | let ya = 0; 6 | 7 | if (goThroughCP) { 8 | cpX = cpX * 2 - (fromX + toX) / 2; 9 | cpY = cpY * 2 - (fromY + toY) / 2; 10 | } 11 | 12 | for (let i = 1; i <= n; ++i) { 13 | const j = i / n; 14 | 15 | xa = fromX + ((cpX - fromX) * j); 16 | ya = fromY + ((cpY - fromY) * j); 17 | 18 | points.push(xa + (((cpX + ((toX - cpX) * j)) - xa) * j), ya + (((cpY + ((toY - cpY) * j)) - ya) * j)); 19 | } 20 | 21 | return points; 22 | } 23 | -------------------------------------------------------------------------------- /src/math/radians.js: -------------------------------------------------------------------------------- 1 | const RAD = Math.PI / 180; 2 | 3 | export default function radians(degrees) { 4 | return degrees * RAD; 5 | } 6 | -------------------------------------------------------------------------------- /src/math/random-int.js: -------------------------------------------------------------------------------- 1 | export default function randomInt(min, max) { 2 | return Math.floor(min + Math.random() * (max - min + 1)); 3 | } 4 | -------------------------------------------------------------------------------- /src/math/random-sign.js: -------------------------------------------------------------------------------- 1 | export default function randomSign() { 2 | return Math.random() > 0.5 ? -1 : 1; 3 | } 4 | -------------------------------------------------------------------------------- /src/math/random.js: -------------------------------------------------------------------------------- 1 | export default function random(min, max) { 2 | if (isNaN(max)) { 3 | max = min; 4 | min = 0; 5 | } 6 | return min + Math.random() * (max - min); 7 | } 8 | -------------------------------------------------------------------------------- /src/math/rotate-point.js: -------------------------------------------------------------------------------- 1 | export default function rotatePoint(p, theta, origin = {x: 0, y: 0}, p1 = {x: 0, y: 0}) { 2 | const sinTheta = Math.sin(theta); 3 | const cosTheta = Math.cos(theta); 4 | p1.x = (p.x - origin.x) * cosTheta - (p.y - origin.y) * sinTheta; 5 | p1.y = (p.x - origin.x) * sinTheta + (p.y - origin.y) * cosTheta; 6 | p1.x += origin.x; 7 | p1.y += origin.y; 8 | return p1; 9 | } 10 | -------------------------------------------------------------------------------- /src/math/rotate-to-deg.js: -------------------------------------------------------------------------------- 1 | export default function rotateToDeg(start, end) { 2 | let diff = (end - start) % 360; 3 | if (diff !== diff % 180) { 4 | diff = (diff < 0) ? diff + 360 : diff - 360; 5 | } 6 | return start + diff; 7 | } 8 | -------------------------------------------------------------------------------- /src/math/rotate-to-rad.js: -------------------------------------------------------------------------------- 1 | const PI2 = Math.PI * 2; 2 | 3 | export default function rotateToRAD(start, end) { 4 | let diff = (end - start) % PI2; 5 | if (diff !== diff % Math.PI) { 6 | diff = diff < 0 ? diff + PI2 : diff - PI2; 7 | } 8 | return start + diff; 9 | } 10 | -------------------------------------------------------------------------------- /src/math/round-to-nearest.js: -------------------------------------------------------------------------------- 1 | export default function roundToNearest(value, unit) { 2 | return Math.round(value / unit) * unit; 3 | } 4 | -------------------------------------------------------------------------------- /src/math/round-to.js: -------------------------------------------------------------------------------- 1 | export default function roundTo(x, places = 2) { 2 | const div = Math.pow(10, places); 3 | return Math.round(x * div) / div; 4 | } 5 | -------------------------------------------------------------------------------- /src/math/size.js: -------------------------------------------------------------------------------- 1 | function getScale(method, width, height, areaWidth, areaHeight) { 2 | switch (method) { 3 | case 'cover': 4 | return Math.max(areaWidth / width, areaHeight / height); 5 | case 'contain': 6 | return Math.min(areaWidth / width, areaHeight / height); 7 | case 'width': 8 | return areaWidth / width; 9 | case 'height': 10 | return areaHeight / height; 11 | default: break; 12 | } 13 | return 1; 14 | } 15 | 16 | export default function size(rect, areaWidth, areaHeight, method = 'cover', autoCenter = true) { 17 | const scale = getScale(method, rect.width, rect.height, areaWidth, areaHeight); 18 | const width = Math.ceil(rect.width * scale); 19 | const height = Math.ceil(rect.height * scale); 20 | 21 | let x = 0, y = 0; 22 | 23 | if (autoCenter) { 24 | x = Math.round((areaWidth - width) * 0.5); 25 | y = Math.round((areaHeight - height) * 0.5); 26 | } 27 | 28 | return { 29 | x, 30 | y, 31 | width, 32 | height, 33 | scale 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/math/smerp.js: -------------------------------------------------------------------------------- 1 | import smoothstep from './smoothstep'; 2 | 3 | export default function smerp(from, to, startTime, endTime, time) { 4 | return from + (to - from) * smoothstep(startTime, endTime, time); 5 | } 6 | -------------------------------------------------------------------------------- /src/math/smoothstep.js: -------------------------------------------------------------------------------- 1 | import clamp from './clamp'; 2 | 3 | export default function smoothstep(min, max, value) { 4 | const x = clamp((value - min) / (max - min), 0, 1); 5 | return x * x * (3 - 2 * x); 6 | } 7 | -------------------------------------------------------------------------------- /src/math/split-value-and-unit.js: -------------------------------------------------------------------------------- 1 | export default function splitValueAndUnit(prop) { 2 | const re = /(^-?\d*\.?\d*)(.*)/; 3 | const match = prop.match(re); 4 | const value = Number(match[1]); 5 | const unit = match[2]; 6 | return {value, unit}; 7 | } 8 | -------------------------------------------------------------------------------- /src/math/weighted-average.js: -------------------------------------------------------------------------------- 1 | export default function weightedAverage(from, to, weight = 10) { 2 | return ((from * (weight - 1)) + to) / weight; 3 | } 4 | -------------------------------------------------------------------------------- /src/math/weighted-distribution.js: -------------------------------------------------------------------------------- 1 | import random from './random'; 2 | 3 | // greater probability of being halfway betweeen min and max 4 | 5 | export default function weightedDistribution(min, max, weight = 5) { 6 | let total = 0; 7 | for (let i = 0; i < weight; i++) { 8 | total += random(min, max); 9 | } 10 | return total / weight; 11 | } 12 | -------------------------------------------------------------------------------- /src/media/can-play.js: -------------------------------------------------------------------------------- 1 | const el = typeof document !== 'undefined' && document.createElement('video'); 2 | 3 | const tests = [ 4 | {type: 'ogv', codec: 'video/ogg; codecs="theora"'}, 5 | {type: 'mp4', codec: 'video/mp4; codecs="avc1.42E01E"'}, // H.264 Constrained baseline profile level 3 6 | {type: 'webm', codec: 'video/webm; codecs="vp8, vorbis"'}, 7 | {type: 'vp9', codec: 'video/webm; codecs="vp9"'}, 8 | {type: 'hls', codec: 'application/x-mpegURL; codecs="avc1.42E01E"'}, 9 | 10 | {type: 'ogg', codec: 'audio/ogg; codecs="vorbis"'}, 11 | {type: 'mp3', codec: 'audio/mpeg;'}, 12 | {type: 'opus', codec: 'audio/ogg; codecs="opus"'}, 13 | {type: 'wav', codec: 'audio/wav; codecs="1"'} 14 | ]; 15 | 16 | export default tests.reduce((map, test) => { 17 | map[test.type] = !!(el && el.canPlayType && el.canPlayType(test.codec)); 18 | return map; 19 | }, {}); 20 | -------------------------------------------------------------------------------- /src/media/cuepoints-reader.js: -------------------------------------------------------------------------------- 1 | export default function cuepointsReader() { 2 | const list = []; 3 | let reader = null; 4 | let dispatch; 5 | let currentPosition = 0; 6 | let lastPosition = -1; 7 | let tolerance = 0.2; 8 | 9 | function add(position, name, data) { 10 | list.push({position, name, data}); 11 | 12 | list.sort((a, b) => a.position - b.position); 13 | 14 | return reader; 15 | } 16 | 17 | function onCuepoint(fn, thisArg) { 18 | if (fn) { 19 | dispatch = thisArg ? fn.bind(thisArg) : fn; 20 | } else { 21 | dispatch = null; 22 | } 23 | return reader; 24 | } 25 | 26 | function reset() { 27 | currentPosition = 0; 28 | lastPosition = -1; 29 | return reader; 30 | } 31 | 32 | function removeAll() { 33 | list.length = 0; 34 | return reset(); 35 | } 36 | 37 | function setTolerance(value) { 38 | tolerance = value; 39 | return reader; 40 | } 41 | 42 | function inRange(cuepointPos, currentPos, lastPos) { 43 | if (cuepointPos > currentPos) { 44 | return false; 45 | } 46 | if (cuepointPos <= lastPos) { 47 | return false; 48 | } 49 | 50 | let diff = cuepointPos - currentPos; 51 | if (diff < 0) { 52 | diff = -diff; 53 | } 54 | return diff <= tolerance; 55 | } 56 | 57 | function check(currentPos, lastPos) { 58 | if (currentPos <= lastPos) { 59 | return; 60 | } 61 | if (typeof dispatch !== 'function') { 62 | return; 63 | } 64 | 65 | list.some(item => { 66 | if (inRange(item.position, currentPos, lastPos)) { 67 | dispatch(item); 68 | return true; 69 | } 70 | return false; 71 | }); 72 | } 73 | 74 | function update(position) { 75 | currentPosition = position; 76 | check(currentPosition, lastPosition); 77 | lastPosition = currentPosition; 78 | return reader; 79 | } 80 | 81 | reader = Object.freeze({ 82 | add, 83 | onCuepoint, 84 | removeAll, 85 | reset, 86 | setTolerance, 87 | update 88 | }); 89 | 90 | return reader; 91 | } 92 | -------------------------------------------------------------------------------- /src/media/index.js: -------------------------------------------------------------------------------- 1 | import canPlay from './can-play'; 2 | import cuepointsReader from './cuepoints-reader'; 3 | import iOSPlayVideoInline from './ios-play-video-inline'; 4 | import videoPlayer from './video-player'; 5 | import vimeo from './vimeo'; 6 | import youtube from './youtube'; 7 | import youtubeBasic from './youtube-basic'; 8 | 9 | export default { 10 | canPlay, 11 | cuepointsReader, 12 | iOSPlayVideoInline, 13 | videoPlayer, 14 | vimeo, 15 | youtube, 16 | youtubeBasic 17 | }; 18 | -------------------------------------------------------------------------------- /src/media/ios-play-video-inline.js: -------------------------------------------------------------------------------- 1 | export default function iOSPlayVideoInline(el, loop = true) { 2 | const frameTime = 1 / 25; 3 | 4 | let self = null; 5 | let lastTime = 0; 6 | let playing = false; 7 | 8 | // This can (and should) be put in a css file instead of doing styleSheets[0].insertRule: 9 | const cssRule = '.iOSPlayVideoInline::-webkit-media-controls { display:none !important; }'; 10 | document.styleSheets[0].insertRule(cssRule, 0); 11 | 12 | el.removeAttribute('controls'); 13 | el.classList.add('iOSPlayVideoInline'); 14 | 15 | 16 | function seek(time) { 17 | el.currentTime = time; 18 | return self; 19 | } 20 | 21 | function pause() { 22 | playing = false; 23 | return self; 24 | } 25 | 26 | function updateFrame() { 27 | if (!playing) { 28 | return; 29 | } 30 | 31 | window.requestAnimationFrame(updateFrame); 32 | 33 | const now = Date.now(); 34 | const deltaTime = now - lastTime; 35 | 36 | if (deltaTime >= frameTime * 1000) { 37 | lastTime = now; 38 | 39 | const ended = el.currentTime + frameTime >= el.duration; 40 | 41 | if (ended && loop) { 42 | seek(0); 43 | } else if (ended) { 44 | pause(); 45 | // self.emit('ended'); 46 | } else { 47 | seek(el.currentTime + frameTime); 48 | } 49 | 50 | // self.emit('timeupdate', el.currentTime, self); 51 | } 52 | } 53 | 54 | function play() { 55 | playing = true; 56 | updateFrame(); 57 | return self; 58 | } 59 | 60 | function destroy() { 61 | // self.removeAllListeners(); 62 | pause(); 63 | window.cancelAnimationFrame(updateFrame); 64 | 65 | return self; 66 | } 67 | 68 | self = Object.create(null, { 69 | destroy: { 70 | value: destroy 71 | }, 72 | pause: { 73 | value: pause 74 | }, 75 | play: { 76 | value: play 77 | }, 78 | seek: { 79 | value: seek 80 | }, 81 | el: { 82 | get: function() { 83 | return el; 84 | } 85 | }, 86 | currentTime: { 87 | get: function() { 88 | return el.currentTime; 89 | } 90 | }, 91 | duration: { 92 | get: function() { 93 | return el.duration; 94 | } 95 | }, 96 | loop: { 97 | get: function() { 98 | return loop; 99 | }, 100 | set: function(value) { 101 | loop = value; 102 | } 103 | }, 104 | playing: { 105 | get: function() { 106 | return playing; 107 | } 108 | } 109 | }); 110 | 111 | return Object.freeze(self); 112 | } 113 | -------------------------------------------------------------------------------- /src/media/video-player.js: -------------------------------------------------------------------------------- 1 | import Emitter from '../events/emitter'; 2 | 3 | export default function videoPlayer(videoEl) { 4 | let el = videoEl || document.createElement('video'); 5 | let player = null; 6 | 7 | function metadataHandler() { 8 | player.emit('metadata', { 9 | src: el.currentSrc, 10 | width: el.videoWidth, 11 | height: el.videoHeight, 12 | duration: el.duration 13 | }); 14 | } 15 | 16 | function canplayHandler() { 17 | player.emit('ready'); 18 | } 19 | 20 | function playHandler() { 21 | player.emit('play'); 22 | } 23 | 24 | function endedHandler() { 25 | player.emit('ended'); 26 | } 27 | 28 | function errorHandler() { 29 | player.emit('error', el.error); 30 | } 31 | 32 | function timeupdateHandler() { 33 | player.emit('timeupdate', el.currentTime); 34 | } 35 | 36 | function removeEventListeners() { 37 | el.removeEventListener('loadedmetadata', metadataHandler); 38 | el.removeEventListener('canplaythrough', canplayHandler); 39 | el.removeEventListener('play', playHandler); 40 | el.removeEventListener('playing', playHandler); 41 | el.removeEventListener('error', errorHandler); 42 | el.removeEventListener('ended', endedHandler); 43 | el.removeEventListener('timeupdate', timeupdateHandler); 44 | } 45 | 46 | function addEventListeners() { 47 | removeEventListeners(); 48 | 49 | el.addEventListener('loadedmetadata', metadataHandler, false); 50 | el.addEventListener('canplaythrough', canplayHandler, false); 51 | el.addEventListener('play', playHandler, false); 52 | el.addEventListener('playing', playHandler, false); 53 | el.addEventListener('error', errorHandler, false); 54 | el.addEventListener('ended', endedHandler, false); 55 | el.addEventListener('timeupdate', timeupdateHandler, false); 56 | } 57 | 58 | function destroy() { 59 | player.off(); 60 | el.pause(); 61 | 62 | try { 63 | el.removeAttribute('src'); 64 | } catch (e) {} 65 | 66 | removeEventListeners(); 67 | 68 | if (el.parentElement) { 69 | el.parentElement.removeChild(el); 70 | } 71 | 72 | el = null; 73 | 74 | return player; 75 | } 76 | 77 | function getBlobURL(url) { 78 | url = window.URL.createObjectURL(url); 79 | function revoke() { 80 | el.removeEventListener('canplaythrough', revoke); 81 | window.URL.revokeObjectURL(url); 82 | } 83 | el.addEventListener('canplaythrough', revoke); 84 | return url; 85 | } 86 | 87 | function load(url) { 88 | if (window.Blob && url instanceof window.Blob) { 89 | url = getBlobURL(url); 90 | } 91 | addEventListeners(); 92 | 93 | el.crossOrigin = 'anonymous'; 94 | el.preload = 'auto'; 95 | el.src = url; 96 | el.load(); 97 | 98 | return player; 99 | } 100 | 101 | function play() { 102 | el.play(); 103 | 104 | return player; 105 | } 106 | 107 | function pause() { 108 | el.pause(); 109 | 110 | return player; 111 | } 112 | 113 | function seek(time) { 114 | try { 115 | el.currentTime = time; 116 | } catch (e) {} 117 | 118 | return player; 119 | } 120 | 121 | addEventListeners(); 122 | 123 | player = Object.assign(new Emitter(), { 124 | destroy, 125 | load, 126 | pause, 127 | play, 128 | seek 129 | }); 130 | 131 | Object.defineProperties(player, { 132 | el: { 133 | get() { 134 | return el; 135 | } 136 | }, 137 | currentTime: { 138 | get() { 139 | return el.currentTime; 140 | }, 141 | set(value) { 142 | el.currentTime = value; 143 | } 144 | }, 145 | duration: { 146 | get() { 147 | return el.duration; 148 | } 149 | }, 150 | volume: { 151 | get() { 152 | return el.volume; 153 | }, 154 | set(value) { 155 | el.volume = value; 156 | } 157 | } 158 | }); 159 | 160 | return player; 161 | } 162 | -------------------------------------------------------------------------------- /src/media/vimeo.js: -------------------------------------------------------------------------------- 1 | import Emitter from '../events/emitter'; 2 | 3 | // https://developer.vimeo.com/player/js-api 4 | 5 | export default function vimeo(el) { 6 | const vimeoPlayer = el.contentWindow; 7 | const re = /^https?:\/\/player.vimeo.com/; 8 | let player = null; 9 | let origin = '*'; 10 | let paused = false; 11 | 12 | function sendCommand(method, value = '') { 13 | const data = { 14 | method 15 | }; 16 | 17 | if (value) { 18 | data.value = value; 19 | } 20 | 21 | const message = JSON.stringify(data); 22 | vimeoPlayer.postMessage(message, origin); 23 | } 24 | 25 | function play() { 26 | paused = false; 27 | sendCommand('play'); 28 | } 29 | 30 | function pause() { 31 | paused = true; 32 | sendCommand('pause'); 33 | } 34 | 35 | function onReady() { 36 | sendCommand('addEventListener', 'play'); 37 | sendCommand('addEventListener', 'pause'); 38 | sendCommand('addEventListener', 'finish'); 39 | sendCommand('addEventListener', 'playProgress'); 40 | player.emit('ready'); 41 | } 42 | 43 | function onPlay() { 44 | paused = false; 45 | player.emit('play'); 46 | } 47 | 48 | function onPause() { 49 | paused = true; 50 | player.emit('pause'); 51 | } 52 | 53 | function onFinish() { 54 | player.emit('ended'); 55 | } 56 | 57 | function onPlayProgress(data) { 58 | player.emit('timeupdate', data.seconds); 59 | } 60 | 61 | function onMessage(event) { 62 | const isVimeo = re.test(event.origin); 63 | 64 | if (!isVimeo) { 65 | return; 66 | } 67 | 68 | const data = JSON.parse(event.data); 69 | 70 | if (data.player_id && el.id !== data.player_id) { 71 | return; 72 | } 73 | 74 | if (origin === '*') { 75 | origin = event.origin; 76 | } 77 | 78 | switch (data.event) { 79 | case 'ready': 80 | onReady(data.player_id); 81 | break; 82 | case 'playProgress': 83 | onPlayProgress(data.data); 84 | break; 85 | case 'play': 86 | onPlay(); 87 | break; 88 | case 'pause': 89 | onPause(); 90 | break; 91 | case 'finish': 92 | onFinish(); 93 | break; 94 | default: 95 | break; 96 | } 97 | } 98 | 99 | function destroy() { 100 | window.removeEventListener('message', onMessage); 101 | } 102 | 103 | window.addEventListener('message', onMessage); 104 | 105 | player = Object.assign(new Emitter(), { 106 | play, 107 | pause, 108 | paused: () => paused, 109 | destroy 110 | }); 111 | 112 | return player; 113 | } 114 | -------------------------------------------------------------------------------- /src/media/youtube-basic.js: -------------------------------------------------------------------------------- 1 | export default function youtubeBasic(el) { 2 | const iframe = el.contentWindow; 3 | 4 | function sendCommand(command) { 5 | iframe.postMessage(`{"event":"command","func":"${command}","args":""}`, '*'); 6 | } 7 | 8 | function play() { 9 | sendCommand('playVideo'); 10 | } 11 | 12 | function pause() { 13 | sendCommand('pauseVideo'); 14 | } 15 | 16 | return { 17 | play, 18 | pause 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/media/youtube.js: -------------------------------------------------------------------------------- 1 | // https://developers.google.com/youtube/iframe_api_reference#Events 2 | import EventEmitter from 'eventemitter3'; 3 | 4 | export const getYouTubeId = url => { 5 | const regExp = /^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; 6 | const match = url.match(regExp); 7 | if (match && match[2].length === 11) { 8 | return match[2]; 9 | } 10 | return null; 11 | }; 12 | 13 | export default function youtube(el) { 14 | let emitter = null; 15 | let player = null; 16 | let paused = false; 17 | 18 | function play() { 19 | paused = false; 20 | player.playVideo(); 21 | return emitter; 22 | } 23 | 24 | function pause() { 25 | paused = true; 26 | player.pauseVideo(); 27 | return emitter; 28 | } 29 | 30 | function onReady() { 31 | emitter.emit('ready'); 32 | } 33 | 34 | function onStateChange(event) { 35 | const {PlayerState} = window.YT; 36 | 37 | switch (event.data) { 38 | case PlayerState.CUED: 39 | case PlayerState.BUFFERING: 40 | break; 41 | case PlayerState.PLAYING: 42 | paused = false; 43 | emitter.emit('play'); 44 | break; 45 | case PlayerState.PAUSED: 46 | paused = true; 47 | emitter.emit('pause'); 48 | break; 49 | case PlayerState.ENDED: 50 | emitter.emit('ended'); 51 | break; 52 | default: break; 53 | } 54 | } 55 | 56 | function destroy() {} 57 | 58 | function createPlayer() { 59 | if (player) { 60 | return; 61 | } 62 | 63 | player = new window.YT.Player(el, { 64 | events: { 65 | onReady, 66 | onStateChange 67 | } 68 | }); 69 | } 70 | 71 | 72 | 73 | if (window.YT) { 74 | createPlayer(); 75 | } else if (window.ytPlayerCalls) { 76 | window.ytPlayerCalls.push(createPlayer); 77 | } else { 78 | window.ytPlayerCalls = [createPlayer]; 79 | window.onYouTubeIframeAPIReady = function() { 80 | window.ytPlayerCalls.forEach((call) => call()); 81 | }; 82 | const script = document.createElement('script'); 83 | script.src = 'https://www.youtube.com/iframe_api'; 84 | document.body.appendChild(script); 85 | } 86 | 87 | emitter = Object.assign(new EventEmitter(), { 88 | play, 89 | pause, 90 | paused: () => paused, 91 | destroy 92 | }); 93 | 94 | return emitter; 95 | } 96 | -------------------------------------------------------------------------------- /src/object-pool/index.js: -------------------------------------------------------------------------------- 1 | export default function objectPool(factoryFn) { 2 | 3 | let pool = []; 4 | let numCreated = 0; 5 | 6 | return { 7 | getPool () { 8 | return pool; 9 | }, 10 | get () { 11 | if ( pool.length > 0 ) { 12 | return pool.pop(); 13 | } else { 14 | numCreated++; 15 | return factoryFn(); 16 | } 17 | }, 18 | dispose (instance) { 19 | pool.push(instance); 20 | }, 21 | fill (count) { 22 | while ( pool.length < count ) { 23 | numCreated++; 24 | pool[pool.length] = factoryFn(); 25 | } 26 | }, 27 | empty () { 28 | pool = []; 29 | }, 30 | getNumCreated() { 31 | return numCreated; 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/object/clone.js: -------------------------------------------------------------------------------- 1 | export default function clone(ob) { 2 | return JSON.parse(JSON.stringify(ob)); 3 | } 4 | -------------------------------------------------------------------------------- /src/object/filter.js: -------------------------------------------------------------------------------- 1 | export default function filter(ob, predicate) { 2 | return Object.keys(ob) 3 | .filter(key => predicate(key, ob[key])) 4 | .reduce((newOb, key) => { 5 | newOb[key] = ob[key]; 6 | return newOb; 7 | }, {}); 8 | } 9 | -------------------------------------------------------------------------------- /src/object/index.js: -------------------------------------------------------------------------------- 1 | import clone from './clone'; 2 | import filter from './filter'; 3 | import map from './map'; 4 | 5 | export default { 6 | clone, 7 | filter, 8 | map 9 | }; 10 | -------------------------------------------------------------------------------- /src/object/map.js: -------------------------------------------------------------------------------- 1 | export default function map(ob, fn) { 2 | return Object.keys(ob) 3 | .reduce((newOb, key) => { 4 | newOb[key] = fn(key, ob[key]); 5 | return newOb; 6 | }, {}); 7 | } 8 | -------------------------------------------------------------------------------- /src/particle/particle-group.js: -------------------------------------------------------------------------------- 1 | import linkedList from '../linked-list'; 2 | import objectPool from '../object-pool'; 3 | import Particle from './index'; 4 | 5 | export default class ParticleGroup { 6 | constructor(factoryFn) { 7 | if (!factoryFn) { 8 | factoryFn = () => new Particle(); 9 | } 10 | this.update = this.update.bind(this); 11 | this.list = linkedList(); 12 | this.pool = objectPool(factoryFn); 13 | } 14 | 15 | create(options) { 16 | const p = this.pool.get(); 17 | p.reset(options); 18 | this.list.add(p); 19 | return p; 20 | } 21 | 22 | add(p) { 23 | this.list.add(p); 24 | } 25 | 26 | remove(p) { 27 | this.list.remove(p); 28 | this.pool.dispose(p); 29 | } 30 | 31 | forEach(fn) { 32 | let p = this.list.first; 33 | while (p) { 34 | fn(p); 35 | p = p.next; 36 | } 37 | } 38 | 39 | update(fn) { 40 | let p = this.list.first; 41 | while (p) { 42 | const next = p.next; 43 | p.update(); 44 | if (typeof fn === 'function') { 45 | fn(p); 46 | } 47 | if (!p.alive) { 48 | this.remove(p); 49 | } 50 | p = next; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/platform/android-native.js: -------------------------------------------------------------------------------- 1 | import android from './android'; 2 | 3 | //http://stackoverflow.com/questions/14403766/how-to-detect-the-stock-android-browser 4 | export default function androidNative(ua = (typeof navigator !== 'undefined' && navigator.userAgent)) { 5 | if (!android(ua)) { 6 | return false; 7 | } 8 | 9 | const isAndroidMobile = ua.indexOf('Mozilla/5.0') > -1 && ua.indexOf('AppleWebKit') > -1; 10 | 11 | const reAppleWebKit = /AppleWebKit\/([\d.]+)/; 12 | const resultAppleWebKit = reAppleWebKit.exec(ua); 13 | const appleWebKitVersion = resultAppleWebKit ? parseFloat(reAppleWebKit.exec(ua)[1]) : null; 14 | 15 | const reChrome = /Chrome\/([\d.]+)/; 16 | const resultChrome = reChrome.exec(ua); 17 | const chromeVersion = resultChrome ? parseFloat(reChrome.exec(ua)[1]) : null; 18 | 19 | return isAndroidMobile && (appleWebKitVersion && appleWebKitVersion < 537) || (chromeVersion && chromeVersion < 37); 20 | } 21 | -------------------------------------------------------------------------------- /src/platform/android-version.js: -------------------------------------------------------------------------------- 1 | import android from './android'; 2 | 3 | export default function androidVersion(ua = (typeof navigator !== 'undefined' && navigator.userAgent)) { 4 | if (!android(ua)) { 5 | return 0; 6 | } 7 | const version = ua.match(/Android (\d+(?:\.\d+)+);/)[1]; 8 | const [a, b] = version.split('.'); 9 | return parseFloat(`${a}.${b}`); 10 | } 11 | -------------------------------------------------------------------------------- /src/platform/android.js: -------------------------------------------------------------------------------- 1 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => /Android/i.test(ua); 2 | -------------------------------------------------------------------------------- /src/platform/chrome-ios.js: -------------------------------------------------------------------------------- 1 | import ios from './ios'; 2 | 3 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => ios(ua) && /CriOS/.test(ua); 4 | -------------------------------------------------------------------------------- /src/platform/desktop.js: -------------------------------------------------------------------------------- 1 | import mobile from './mobile'; 2 | 3 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => !mobile(ua); 4 | -------------------------------------------------------------------------------- /src/platform/device-orientation.js: -------------------------------------------------------------------------------- 1 | export default () => !!(typeof window !== 'undefined' && window.DeviceOrientationEvent); 2 | -------------------------------------------------------------------------------- /src/platform/firefox.js: -------------------------------------------------------------------------------- 1 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => /Firefox/.test(ua); 2 | -------------------------------------------------------------------------------- /src/platform/ie-version.js: -------------------------------------------------------------------------------- 1 | export default function ieVersion(ua = (typeof navigator !== 'undefined' && navigator.userAgent)) { 2 | let v = 0; 3 | if (/MSIE (\d+\.\d+);/.test(ua)) { 4 | v = parseInt(RegExp.$1, 10); 5 | } else if (/Trident\/(\d+\.\d+)(.*)rv:(\d+\.\d+)/.test(ua)) { 6 | v = parseInt(RegExp.$3, 10); 7 | } 8 | return v; 9 | } 10 | -------------------------------------------------------------------------------- /src/platform/ie.js: -------------------------------------------------------------------------------- 1 | import ieVersion from './ie-version'; 2 | 3 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => ieVersion(ua) > 0; 4 | -------------------------------------------------------------------------------- /src/platform/index.js: -------------------------------------------------------------------------------- 1 | import android from './android'; 2 | import androidNative from './android-native'; 3 | import androidVersion from './android-version'; 4 | import chromeIOS from './chrome-ios'; 5 | import desktop from './desktop'; 6 | import deviceOrientation from './device-orientation'; 7 | import firefox from './firefox'; 8 | import ie from './ie'; 9 | import ieVersion from './ie-version'; 10 | import ios from './ios'; 11 | import iosVersion from './ios-version'; 12 | import ipad from './ipad'; 13 | import iphone from './iphone'; 14 | import language from './language'; 15 | import linux from './linux'; 16 | import localHost from './local-host'; 17 | import mac from './mac'; 18 | import mobile from './mobile'; 19 | import mp4 from './mp4'; 20 | import safari from './safari'; 21 | import safariIOS from './safari-ios'; 22 | import screen from './screen'; 23 | import webgl from './webgl'; 24 | import webm from './webm'; 25 | import windows from './windows'; 26 | import windowsPhone from './windows-phone'; 27 | 28 | export default { 29 | android: android(), 30 | androidNative: androidNative(), 31 | androidVersion: androidVersion(), 32 | chromeIOS: chromeIOS(), 33 | desktop: desktop(), 34 | deviceOrientation: deviceOrientation(), 35 | firefox: firefox(), 36 | ie: ie(), 37 | ieVersion: ieVersion(), 38 | ios: ios(), 39 | iosVersion: iosVersion(), 40 | ipad: ipad(), 41 | iphone: iphone(), 42 | language: language(), 43 | linux: linux(), 44 | localHost: localHost(), 45 | mac: mac(), 46 | mobile: mobile(), 47 | mp4: mp4(), 48 | safari: safari(), 49 | safariIOS: safariIOS(), 50 | screen: screen, 51 | webgl: webgl(), 52 | webm: webm(), 53 | windows: windows(), 54 | windowsPhone: windowsPhone() 55 | }; 56 | -------------------------------------------------------------------------------- /src/platform/ios-version.js: -------------------------------------------------------------------------------- 1 | import ios from './ios'; 2 | 3 | export default function iosVersion(ua = (typeof navigator !== 'undefined' && navigator.userAgent)) { 4 | if (ios(ua)) { 5 | const [, b, c] = ua.match(/OS (\d+)_(\d+)/i); 6 | if (b && c) { 7 | return parseFloat(`${b}.${c}`); 8 | } 9 | } 10 | return 0; 11 | } 12 | -------------------------------------------------------------------------------- /src/platform/ios.js: -------------------------------------------------------------------------------- 1 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => /iP[ao]d|iPhone/i.test(ua); 2 | -------------------------------------------------------------------------------- /src/platform/ipad.js: -------------------------------------------------------------------------------- 1 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => /iPad/i.test(ua); 2 | -------------------------------------------------------------------------------- /src/platform/iphone.js: -------------------------------------------------------------------------------- 1 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => /iPod|iPhone/i.test(ua); 2 | -------------------------------------------------------------------------------- /src/platform/language.js: -------------------------------------------------------------------------------- 1 | export default function language() { 2 | if (typeof navigator === 'undefined') { 3 | return null; 4 | } 5 | return (navigator.languages && navigator.languages[0]) || (navigator.language || navigator.userLanguage); 6 | } 7 | -------------------------------------------------------------------------------- /src/platform/linux.js: -------------------------------------------------------------------------------- 1 | import android from './android'; 2 | 3 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => !android(ua) && /Linux/.test(ua); 4 | -------------------------------------------------------------------------------- /src/platform/local-host.js: -------------------------------------------------------------------------------- 1 | export default (href = (typeof window !== 'undefined' && window.location.href)) => ( 2 | /^(?:https?:\/\/)?(?:localhost|192\.168)/.test(href) 3 | ); 4 | -------------------------------------------------------------------------------- /src/platform/mac.js: -------------------------------------------------------------------------------- 1 | import ios from './ios'; 2 | 3 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => !ios(ua) && /Mac OS/.test(ua); 4 | -------------------------------------------------------------------------------- /src/platform/mobile.js: -------------------------------------------------------------------------------- 1 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => { 2 | return /Android|webOS|iPhone|iP[ao]d|BlackBerry|IEMobile|Opera Mini|Windows Phone|SymbianOS/i.test(ua); 3 | }; 4 | -------------------------------------------------------------------------------- /src/platform/mp4.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | const el = typeof document !== 'undefined' && document.createElement('video'); 3 | return !!(el && el.canPlayType && el.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"')); 4 | }; 5 | -------------------------------------------------------------------------------- /src/platform/safari-ios.js: -------------------------------------------------------------------------------- 1 | import ios from './ios'; 2 | 3 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => ios(ua) && /AppleWebKit/.test(ua); 4 | -------------------------------------------------------------------------------- /src/platform/safari.js: -------------------------------------------------------------------------------- 1 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => ( 2 | !/Android/.test(ua) && !/Chrome/.test(ua) && /Safari/.test(ua) 3 | ); 4 | -------------------------------------------------------------------------------- /src/platform/screen.js: -------------------------------------------------------------------------------- 1 | const hasWin = typeof window !== 'undefined'; 2 | 3 | export default { 4 | width: hasWin ? Math.max(window.outerWidth, window.screen.width) : 0, 5 | height: hasWin ? Math.max(window.outerHeight, window.screen.height) : 0, 6 | dpr: hasWin ? window.devicePixelRatio || 1 : 1, 7 | retina: hasWin ? window.devicePixelRatio > 1 : false 8 | }; 9 | -------------------------------------------------------------------------------- /src/platform/webgl.js: -------------------------------------------------------------------------------- 1 | export default function webgl() { 2 | try { 3 | const canvas = document.createElement('canvas'); 4 | const context = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); 5 | return !!(window.WebGLRenderingContext && context); 6 | } catch (e) { 7 | return false; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/platform/webm.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | const el = typeof document !== 'undefined' && document.createElement('video'); 3 | return !!(el && el.canPlayType && el.canPlayType('video/webm; codecs="vp8, vorbis"')); 4 | }; 5 | -------------------------------------------------------------------------------- /src/platform/windows-phone.js: -------------------------------------------------------------------------------- 1 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => /Windows Phone/i.test(ua); 2 | -------------------------------------------------------------------------------- /src/platform/windows.js: -------------------------------------------------------------------------------- 1 | import windowsPhone from './windows-phone'; 2 | 3 | export default (ua = (typeof navigator !== 'undefined' && navigator.userAgent)) => ( 4 | !windowsPhone(ua) && /Windows/.test(ua) 5 | ); 6 | -------------------------------------------------------------------------------- /src/polyfill/class-list.js: -------------------------------------------------------------------------------- 1 | /* 2 | * classList (partial polyfill for IE 10, IE 11 and Firefox <24) 3 | * adapted from: https://github.com/eligrey/classList.js/blob/master/classList.js 4 | */ 5 | 6 | (function() { 7 | 8 | if (typeof document === 'undefined') { 9 | return; 10 | } 11 | 12 | let testElement = document.createElement('_'); 13 | 14 | testElement.classList.add('c1', 'c2'); 15 | 16 | // Polyfill for IE 10/11 and Firefox <26, where classList.add and 17 | // classList.remove exist but support only one argument at a time. 18 | if (!testElement.classList.contains('c2')) { 19 | function createMethod(method) { 20 | const original = window.DOMTokenList.prototype[method]; 21 | 22 | window.DOMTokenList.prototype[method] = function(token) { 23 | let i; 24 | const len = arguments.length; 25 | 26 | for (i = 0; i < len; i++) { 27 | token = arguments[i]; 28 | original.call(this, token); 29 | } 30 | }; 31 | } 32 | createMethod('add'); 33 | createMethod('remove'); 34 | } 35 | 36 | testElement.classList.toggle('c3', false); 37 | 38 | // Polyfill for IE 10, IE 11 and Firefox <24, where classList.toggle does not 39 | // support the second argument. 40 | if (testElement.classList.contains('c3')) { 41 | const toggle = window.DOMTokenList.prototype.toggle; 42 | 43 | window.DOMTokenList.prototype.toggle = function(token, force) { 44 | force = !!force; 45 | if (arguments.length > 1 && this.contains(token) === force) { 46 | return force; 47 | } else { 48 | return toggle.call(this, token); 49 | } 50 | }; 51 | } 52 | 53 | testElement = null; 54 | }()); 55 | -------------------------------------------------------------------------------- /src/polyfill/console.js: -------------------------------------------------------------------------------- 1 | (function(fn) { 2 | if (typeof window === 'undefined') { 3 | return; 4 | } 5 | 6 | window.console = window.console || {}; 7 | const methods = [ 8 | 'assert', 9 | 'clear', 10 | 'count', 11 | 'debug', 12 | 'dir', 13 | 'dirxml', 14 | 'error', 15 | 'group', 16 | 'groupCollapsed', 17 | 'groupEnd', 18 | 'info', 19 | 'log', 20 | 'markTimeline', 21 | 'memory', 22 | 'profile', 23 | 'profileEnd', 24 | 'table', 25 | 'time', 26 | 'timeEnd', 27 | 'timeStamp', 28 | 'timeline', 29 | 'timelineEnd', 30 | 'trace', 31 | 'warn' 32 | ]; 33 | methods.forEach((name) => { 34 | window.console[name] = window.console[name] || fn; 35 | }); 36 | }(function() {})); 37 | -------------------------------------------------------------------------------- /src/polyfill/index.js: -------------------------------------------------------------------------------- 1 | import './class-list'; 2 | import './console'; 3 | import './request-animation-frame'; 4 | -------------------------------------------------------------------------------- /src/polyfill/request-animation-frame.js: -------------------------------------------------------------------------------- 1 | /* 2 | * requestAnimationFrame (ios6 and android < 4.4) 3 | */ 4 | (function() { 5 | if (typeof window === 'undefined') { 6 | return; 7 | } 8 | if (!window.requestAnimationFrame) { 9 | const vendors = ['ms', 'moz', 'webkit', 'o']; 10 | for (let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 11 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; 12 | window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 13 | 'CancelRequestAnimationFrame']; 14 | } 15 | } 16 | }()); 17 | -------------------------------------------------------------------------------- /src/popup/index.js: -------------------------------------------------------------------------------- 1 | export default function popup(url, width = 800, height = 600, name = '') { 2 | const left = (window.screen.width - width) / 2; 3 | const top = (window.screen.height - height) / 2; 4 | // const left = (window.screen.availWidth - width) / 2; 5 | // const top = (window.screen.availHeight - height) / 2; 6 | const defaults = 'directories=no,location=no,menubar=no,resizable=no,scrollbars=no,status=no,toolbar=no'; 7 | const params = `width=${width},height=${height},top=${top},left=${left},${defaults}`; 8 | const win = window.open(url, name, params); 9 | if (win === null || typeof win === 'undefined') { 10 | return false; 11 | } 12 | if (window.focus) { 13 | win.focus(); 14 | } 15 | return true; 16 | } 17 | -------------------------------------------------------------------------------- /src/quad-tree/index.js: -------------------------------------------------------------------------------- 1 | 2 | class Node { 3 | constructor(bounds, depth, maxDepth, maxChildren) { 4 | this._bounds = bounds; 5 | this._depth = depth; 6 | this._maxDepth = maxDepth; 7 | this._maxChildren = maxChildren; 8 | 9 | this.children = []; 10 | this.nodes = []; 11 | } 12 | 13 | insert(item) { 14 | if (this.nodes.length) { 15 | const index = this._findIndex(item); 16 | this.nodes[index].insert(item); 17 | return; 18 | } 19 | 20 | this.children.push(item); 21 | 22 | if (!(this._depth >= this._maxDepth) && this.children.length > this._maxChildren) { 23 | 24 | this.subdivide(); 25 | 26 | for (let i = 0; i < this.children.length; i++) { 27 | this.insert(this.children[i]); 28 | } 29 | 30 | this.children.length = 0; 31 | } 32 | } 33 | 34 | retrieve(item) { 35 | if (this.nodes.length) { 36 | const index = this._findIndex(item); 37 | return this.nodes[index].retrieve(item); 38 | } 39 | 40 | return this.children; 41 | } 42 | 43 | _findIndex(item) { 44 | const {x, y, width, height} = this._bounds; 45 | 46 | const right = item.x > x + width / 2; 47 | const bottom = item.y > y + height / 2; 48 | 49 | let index; 50 | 51 | if (right) { 52 | index = bottom ? Node.BR : Node.TR; 53 | } else { 54 | index = bottom ? Node.BL : Node.TL; 55 | } 56 | 57 | return index; 58 | } 59 | 60 | subdivide() { 61 | const depth = this._depth + 1; 62 | 63 | const {x, y, width, height} = this._bounds; 64 | const w = width / 2; 65 | const h = height / 2; 66 | 67 | this.nodes[Node.TL] = new Node({ 68 | x, 69 | y, 70 | width: w, 71 | height: h 72 | }, 73 | depth, this._maxDepth, this._maxChildren); 74 | 75 | this.nodes[Node.TR] = new Node({ 76 | x: x + w, 77 | y, 78 | width: w, 79 | height: h 80 | }, 81 | depth, this._maxDepth, this._maxChildren); 82 | 83 | this.nodes[Node.BL] = new Node({ 84 | x, 85 | y: y + h, 86 | width: w, 87 | height: h 88 | }, 89 | depth, this._maxDepth, this._maxChildren); 90 | 91 | this.nodes[Node.BR] = new Node({ 92 | x: x + w, 93 | y: y + h, 94 | width: w, 95 | height: h 96 | }, 97 | depth, this._maxDepth, this._maxChildren); 98 | } 99 | 100 | clear() { 101 | this.children.length = 0; 102 | 103 | while (this.nodes.length) { 104 | this.nodes.pop().clear(); 105 | } 106 | } 107 | } 108 | 109 | Node.TL = 0; 110 | Node.TR = 1; 111 | Node.BL = 2; 112 | Node.BR = 3; 113 | 114 | export default class QuadTree { 115 | constructor(bounds, maxDepth = -1, maxChildren = -1) { 116 | this.root = new Node(bounds, 0, maxDepth, maxChildren); 117 | } 118 | 119 | insert(item) { 120 | if (Array.isArray(item)) { 121 | for (let i = 0; i < item.length; i++) { 122 | this.root.insert(item[i]); 123 | } 124 | } else { 125 | this.root.insert(item); 126 | } 127 | } 128 | 129 | clear() { 130 | this.root.clear(); 131 | } 132 | 133 | retrieve(item) { 134 | return this.root.retrieve(item); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/share/email.js: -------------------------------------------------------------------------------- 1 | import popup from '../popup'; 2 | 3 | export default function email(url, subject = '', body = '') { 4 | url = encodeURIComponent(url); 5 | subject = encodeURIComponent(subject); 6 | 7 | const newlines = encodeURIComponent('\r\n\r\n'); 8 | body = body ? `${encodeURIComponent(body)}${newlines}` : ''; 9 | 10 | return popup(`mailto:?subject=${subject}&body=${body}${url}`); 11 | } 12 | -------------------------------------------------------------------------------- /src/share/facebook.js: -------------------------------------------------------------------------------- 1 | import popup from '../popup'; 2 | 3 | export default function facebook(url) { 4 | url = encodeURIComponent(url); 5 | return popup(`https://www.facebook.com/sharer/sharer.php?u=${url}`); 6 | } 7 | -------------------------------------------------------------------------------- /src/share/googleplus.js: -------------------------------------------------------------------------------- 1 | import popup from '../popup'; 2 | 3 | export default function googleplus(url) { 4 | url = encodeURIComponent(url); 5 | return popup(`https://plus.google.com/share?url=${url}`); 6 | } 7 | -------------------------------------------------------------------------------- /src/share/index.js: -------------------------------------------------------------------------------- 1 | import email from './email'; 2 | import facebook from './facebook'; 3 | import googleplus from './googleplus'; 4 | import linkedin from './linkedin'; 5 | import pinterest from './pinterest'; 6 | import reddit from './reddit'; 7 | import renren from './renren'; 8 | import sms from './sms'; 9 | import twitter from './twitter'; 10 | import vkontakte from './vkontakte'; 11 | import weibo from './weibo'; 12 | import whatsapp from './whatsapp'; 13 | 14 | export default { 15 | email, 16 | facebook, 17 | googleplus, 18 | linkedin, 19 | pinterest, 20 | reddit, 21 | renren, 22 | sms, 23 | twitter, 24 | vkontakte, 25 | weibo, 26 | whatsapp 27 | }; 28 | -------------------------------------------------------------------------------- /src/share/linkedin.js: -------------------------------------------------------------------------------- 1 | import popup from '../popup'; 2 | 3 | export default function linkedin(url, title = '') { 4 | url = encodeURIComponent(url); 5 | title = encodeURIComponent(title); 6 | return popup(`https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${title}`); 7 | } 8 | -------------------------------------------------------------------------------- /src/share/pinterest.js: -------------------------------------------------------------------------------- 1 | import popup from '../popup'; 2 | 3 | export default function pinterest(url, media, desc = '') { 4 | url = encodeURIComponent(url); 5 | media = encodeURIComponent(media); 6 | desc = encodeURIComponent(desc); 7 | return popup(`https://pinterest.com/pin/create/button/?url=${url}&media=${media}&description=${desc}`); 8 | } 9 | -------------------------------------------------------------------------------- /src/share/reddit.js: -------------------------------------------------------------------------------- 1 | import popup from '../popup'; 2 | 3 | export default function reddit(url, title = '') { 4 | url = encodeURIComponent(url); 5 | title = encodeURIComponent(title); 6 | return popup(`https://www.reddit.com/submit?url=${url}&title=${title}`); 7 | } 8 | -------------------------------------------------------------------------------- /src/share/renren.js: -------------------------------------------------------------------------------- 1 | import popup from '../popup'; 2 | 3 | export default function vkontakte(url, title = '') { 4 | url = encodeURIComponent(url); 5 | title = encodeURIComponent(title); 6 | return popup(`http://share.renren.com/share/buttonshare.do?link=${url}&title=${title}`); 7 | } 8 | -------------------------------------------------------------------------------- /src/share/sms.js: -------------------------------------------------------------------------------- 1 | export default function sms(url, body = '') { 2 | url = encodeURIComponent(url); 3 | 4 | const newlines = encodeURIComponent('\r\n\r\n'); 5 | body = body ? `${encodeURIComponent(body)}${newlines}` : ''; 6 | 7 | const ios = /iP[ao]d|iPhone/i.test(navigator.userAgent); 8 | const delim = ios ? '&' : '?'; 9 | 10 | window.location.href = `sms:${delim}body=${body}${url}`; 11 | } 12 | -------------------------------------------------------------------------------- /src/share/twitter.js: -------------------------------------------------------------------------------- 1 | import popup from '../popup'; 2 | 3 | export default function twitter(url, text = '', hashtags = '', related = '') { 4 | url = encodeURIComponent(url); 5 | text = encodeURIComponent(text); 6 | hashtags = encodeURIComponent(hashtags); 7 | related = encodeURIComponent(related); 8 | 9 | return popup(`https://twitter.com/intent/tweet?url=${url}&text=${text}&hashtags=${hashtags}&related=${related}`); 10 | } 11 | -------------------------------------------------------------------------------- /src/share/vkontakte.js: -------------------------------------------------------------------------------- 1 | import popup from '../popup'; 2 | 3 | export default function vkontakte(url, title = '', description = '', image = '') { 4 | url = encodeURIComponent(url); 5 | title = encodeURIComponent(title); 6 | description = encodeURIComponent(description); 7 | image = encodeURIComponent(image); 8 | return popup(`http://vkontakte.ru/share.php?url=${url}&title=${title}&description=${description}&image=${image}`); 9 | } 10 | -------------------------------------------------------------------------------- /src/share/weibo.js: -------------------------------------------------------------------------------- 1 | import popup from '../popup'; 2 | 3 | export default function weibo(url, title = '', image = '') { 4 | url = encodeURIComponent(url); 5 | title = encodeURIComponent(title); 6 | image = encodeURIComponent(image); 7 | 8 | const params = `url=${url}&appkey=&title=${title}&pic=${image}&ralateUid=&language=zh_cn`; 9 | return popup(`http://service.weibo.com/share/share.php?${params}`); 10 | } 11 | -------------------------------------------------------------------------------- /src/share/whatsapp.js: -------------------------------------------------------------------------------- 1 | export default function whatsapp(url, body = '') { 2 | url = encodeURIComponent(url); 3 | 4 | const newlines = encodeURIComponent('\r\n\r\n'); 5 | body = body ? `${encodeURIComponent(body)}${newlines}` : ''; 6 | 7 | window.location.href = `whatsapp://send?text=${body}${url}`; 8 | } 9 | -------------------------------------------------------------------------------- /src/storage/index.js: -------------------------------------------------------------------------------- 1 | function load(key) { 2 | let item = null; 3 | 4 | try { 5 | item = localStorage.getItem(key); 6 | } catch (err) {} 7 | 8 | return item; 9 | } 10 | 11 | function save(key, item) { 12 | try { 13 | localStorage.setItem(key, item); 14 | return true; 15 | } catch (err) { 16 | console.error('Couldn\'t save in localStorage'); 17 | } 18 | return false; 19 | } 20 | 21 | function loadJSON(key) { 22 | const item = load(key); 23 | return item ? JSON.parse(item) : null; 24 | } 25 | 26 | function saveJSON(key, item) { 27 | return save(key, JSON.stringify(item)); 28 | } 29 | 30 | function remove(key) { 31 | try { 32 | localStorage.removeItem(key); 33 | } catch (err) {} 34 | } 35 | 36 | export default {load, save, loadJSON, saveJSON, remove}; 37 | -------------------------------------------------------------------------------- /src/string/after-first.js: -------------------------------------------------------------------------------- 1 | // everything after the first occurrence of substr in str 2 | export default function afterFirst(str, substr) { 3 | let index = str.indexOf(substr); 4 | if (index === -1) { 5 | return ''; 6 | } 7 | index += substr.length; 8 | return str.slice(index); 9 | } 10 | -------------------------------------------------------------------------------- /src/string/after-last.js: -------------------------------------------------------------------------------- 1 | // everything after the last occurence of substr in str 2 | export default function afterLast(str, substr) { 3 | let index = str.lastIndexOf(substr); 4 | if (index === -1) { 5 | return ''; 6 | } 7 | index += substr.length; 8 | return str.slice(index); 9 | } 10 | -------------------------------------------------------------------------------- /src/string/before-first.js: -------------------------------------------------------------------------------- 1 | // everything before the first occurrence of substr in str 2 | export default function beforeFirst(str, substr) { 3 | const index = str.indexOf(substr); 4 | if (index === -1) { 5 | return ''; 6 | } 7 | return str.slice(0, index); 8 | } 9 | -------------------------------------------------------------------------------- /src/string/before-last.js: -------------------------------------------------------------------------------- 1 | // everything before the last occurrence of substr in the string. 2 | export default function beforeLast(str, substr) { 3 | const index = str.lastIndexOf(substr); 4 | if (index === -1) { 5 | return ''; 6 | } 7 | return str.slice(0, index); 8 | } 9 | -------------------------------------------------------------------------------- /src/string/begins-with.js: -------------------------------------------------------------------------------- 1 | // whether str begins with substr 2 | export default function beginsWith(str, substr) { 3 | return str.indexOf(substr) === 0; 4 | } 5 | -------------------------------------------------------------------------------- /src/string/between.js: -------------------------------------------------------------------------------- 1 | // everything after the first occurance of start and before the first occurrence of end 2 | export default function between(str, start, end) { 3 | let substr = ''; 4 | let startIndex = str.indexOf(start); 5 | if (startIndex !== -1) { 6 | startIndex += start.length; 7 | const endIndex = str.indexOf(end, startIndex); 8 | if (endIndex !== -1) { 9 | substr = str.slice(startIndex, endIndex); 10 | } 11 | } 12 | return substr; 13 | } 14 | -------------------------------------------------------------------------------- /src/string/block.js: -------------------------------------------------------------------------------- 1 | import escapePattern from './escape-pattern'; 2 | import truncate from './truncate'; 3 | // Utility method that intelligently breaks up your string, 4 | // allowing you to create blocks of readable text. 5 | // This method returns you the closest possible match to the delim paramater, 6 | // while keeping the text length within the len paramter. 7 | // If a match can't be found in your specified length an '...' is added to that block, 8 | // and the blocking continues untill all the text is broken apart. 9 | export default function block(str, len, delim = '.') { 10 | const arr = []; 11 | 12 | if (!str || !str.includes(delim)) { 13 | return arr; 14 | } 15 | 16 | if (delim === ' ') { 17 | str += delim; 18 | } 19 | 20 | let chrIndex = 0; 21 | const replPatt = new RegExp('[^' + escapePattern(delim) + ']+$'); 22 | 23 | while (chrIndex < str.length) { 24 | let subString = str.substr(chrIndex, len); 25 | if (!subString.includes(delim)) { 26 | arr.push(truncate(subString, subString.length)); 27 | chrIndex += subString.length; 28 | } 29 | subString = subString.replace(replPatt, ''); 30 | chrIndex += subString.length; 31 | arr.push(subString.trim()); 32 | } 33 | return arr; 34 | } 35 | -------------------------------------------------------------------------------- /src/string/capitalize.js: -------------------------------------------------------------------------------- 1 | // Capitalize the first word in a string or all words 2 | export default function capitalize(str, all = false) { 3 | const substr = str.trimLeft(); 4 | const re = all ? /^.|\b./g : /(^\w)/; 5 | return substr.replace(re, (match) => match.toUpperCase()); 6 | } 7 | -------------------------------------------------------------------------------- /src/string/count-of.js: -------------------------------------------------------------------------------- 1 | import escapePattern from './escape-pattern'; 2 | 3 | // the number of times substr appears within str 4 | export default function countOf(str, substr, caseSensitive) { 5 | const escapedStr = escapePattern(substr); 6 | const flags = (!caseSensitive) ? 'ig' : 'g'; 7 | return str.match(new RegExp(escapedStr, flags)).length; 8 | } 9 | -------------------------------------------------------------------------------- /src/string/edit-distance.js: -------------------------------------------------------------------------------- 1 | // Levenshtein distance (editDistance) is a measure of the similarity between 2 | // two strings. The distance is the number of deletions, insertions, or 3 | // substitutions required to transform source into target. 4 | export default function editDistance(source = '', target = '') { 5 | 6 | if (source === target) { 7 | return 0; 8 | } 9 | 10 | if (!source.length) { 11 | return target.length; 12 | } 13 | 14 | if (!target.length) { 15 | return source.length; 16 | } 17 | 18 | const d = []; 19 | let i, j, cost; 20 | 21 | for (i = 0; i <= source.length; i++) { 22 | d[i] = []; 23 | } 24 | for (i = 0; i <= source.length; i++) { 25 | d[i][0] = i; 26 | } 27 | for (j = 0; j <= target.length; j++) { 28 | d[0][j] = j; 29 | } 30 | 31 | for (i = 1; i <= source.length; i++) { 32 | 33 | const si = source.charAt(i - 1); 34 | for (j = 1; j <= target.length; j++) { 35 | 36 | const tj = target.charAt(j - 1); 37 | 38 | if (si === tj) { 39 | cost = 0; 40 | } else { 41 | cost = 1; 42 | } 43 | 44 | d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost); 45 | } 46 | } 47 | 48 | return d[source.length][target.length]; 49 | } 50 | -------------------------------------------------------------------------------- /src/string/ends-with.js: -------------------------------------------------------------------------------- 1 | // whether str ends with substr 2 | export default function endsWith(str, substr) { 3 | return str.lastIndexOf(substr) === str.length - substr.length; 4 | } 5 | -------------------------------------------------------------------------------- /src/string/escape-html.js: -------------------------------------------------------------------------------- 1 | // export default function escapeHtml(str) { 2 | // const div = document.createElement('div'); 3 | // div.appendChild(document.createTextNode(str)); 4 | // return div.innerHTML; 5 | // } 6 | 7 | const entityMap = { 8 | '&': '&', 9 | '<': '<', 10 | '>': '>', 11 | '"': '"', 12 | '\'': ''', 13 | '/': '/', 14 | '`': '`', 15 | '=': '=' 16 | }; 17 | 18 | export default function escapeHtml(string) { 19 | return String(string) 20 | .replace(/[&<>"'`=\/]/g, function fromEntityMap(s) { 21 | return entityMap[s]; 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/string/escape-pattern.js: -------------------------------------------------------------------------------- 1 | // regex escape pattern 2 | export default function escapePattern(pattern) { 3 | return pattern.replace(/(\]|\[|\{|\}|\(|\)|\*|\+|\?|\.|\\)/g, '\\$1'); 4 | } 5 | -------------------------------------------------------------------------------- /src/string/has-text.js: -------------------------------------------------------------------------------- 1 | import removeExtraWhitespace from './remove-extra-whitespace'; 2 | 3 | // whether str contains any text 4 | export default function hasText(str) { 5 | return !!removeExtraWhitespace(str).length; 6 | } 7 | -------------------------------------------------------------------------------- /src/string/index.js: -------------------------------------------------------------------------------- 1 | import afterFirst from './after-first'; 2 | import afterLast from './after-last'; 3 | import beforeFirst from './before-first'; 4 | import beforeLast from './before-last'; 5 | import beginsWith from './begins-with'; 6 | import between from './between'; 7 | import block from './block'; 8 | import capitalize from './capitalize'; 9 | import countOf from './count-of'; 10 | import editDistance from './edit-distance'; 11 | import endsWith from './ends-with'; 12 | import escapeHTML from './escape-html'; 13 | import escapePattern from './escape-pattern'; 14 | import hasText from './has-text'; 15 | import isNumeric from './is-numeric'; 16 | import padLeft from './pad-left'; 17 | import padRight from './pad-right'; 18 | import preventWidow from './prevent-widow'; 19 | import properCase from './proper-case'; 20 | import remove from './remove'; 21 | import removeExtraWhitespace from './remove-extra-whitespace'; 22 | import reverse from './reverse'; 23 | import reverseWords from './reverse-words'; 24 | import similarity from './similarity'; 25 | import stripTags from './strip-tags'; 26 | import swapCase from './swap-case'; 27 | import timeCode from './time-code'; 28 | import toNumber from './to-number'; 29 | import truncate from './truncate'; 30 | import wordCount from './word-count'; 31 | 32 | export default { 33 | afterFirst, 34 | afterLast, 35 | beforeFirst, 36 | beforeLast, 37 | beginsWith, 38 | between, 39 | block, 40 | capitalize, 41 | countOf, 42 | editDistance, 43 | endsWith, 44 | escapeHTML, 45 | escapePattern, 46 | hasText, 47 | isNumeric, 48 | padLeft, 49 | padRight, 50 | preventWidow, 51 | properCase, 52 | remove, 53 | removeExtraWhitespace, 54 | reverse, 55 | reverseWords, 56 | similarity, 57 | stripTags, 58 | swapCase, 59 | timeCode, 60 | toNumber, 61 | truncate, 62 | wordCount 63 | }; 64 | -------------------------------------------------------------------------------- /src/string/is-numeric.js: -------------------------------------------------------------------------------- 1 | // whether str is numeric 2 | export default function isNumeric(str) { 3 | const regx = /^[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$/; 4 | return regx.test(str); 5 | } 6 | -------------------------------------------------------------------------------- /src/string/pad-left.js: -------------------------------------------------------------------------------- 1 | // pad str with substr from the left 2 | export default function padLeft(str, substr, length) { 3 | str = String(str); 4 | while (str.length < length) { 5 | str = substr + str; 6 | } 7 | return str; 8 | } 9 | -------------------------------------------------------------------------------- /src/string/pad-right.js: -------------------------------------------------------------------------------- 1 | // pads str with substr from the right 2 | export default function padRight(str, substr, length) { 3 | str = String(str); 4 | while (str.length < length) { 5 | str += substr; 6 | } 7 | return str; 8 | } 9 | -------------------------------------------------------------------------------- /src/string/prevent-widow.js: -------------------------------------------------------------------------------- 1 | export default function preventWidow(str) { 2 | str = str.trim(); 3 | 4 | const lastSpace = str.lastIndexOf(' '); 5 | if (lastSpace > 0) { 6 | return `${str.slice(0, lastSpace)} ${str.slice(lastSpace + 1)}`; 7 | } 8 | 9 | return str; 10 | } 11 | -------------------------------------------------------------------------------- /src/string/proper-case.js: -------------------------------------------------------------------------------- 1 | import capitalize from './capitalize'; 2 | 3 | // proper case str in sentence format 4 | export default function properCase(str) { 5 | const newStr = str.toLowerCase().replace(/\b([^.?;!]+)/, capitalize); 6 | return newStr.replace(/\b[i]\b/, 'I'); 7 | } 8 | -------------------------------------------------------------------------------- /src/string/remove-extra-whitespace.js: -------------------------------------------------------------------------------- 1 | // remove extra whitespace (extra spaces, tabs, line breaks, etc) 2 | export default function removeExtraWhitespace(str) { 3 | return str.trim().replace(/\s+/g, ' '); 4 | } 5 | -------------------------------------------------------------------------------- /src/string/remove.js: -------------------------------------------------------------------------------- 1 | import escapePattern from './escape-pattern'; 2 | 3 | // remove all instances of substr in str 4 | export default function remove(str, substr, caseSensitive = false) { 5 | const escapedStr = escapePattern(substr); 6 | const flags = caseSensitive ? 'g' : 'ig'; 7 | return str.replace(new RegExp(escapedStr, flags), ''); 8 | } 9 | -------------------------------------------------------------------------------- /src/string/reverse-words.js: -------------------------------------------------------------------------------- 1 | // reverse word order 2 | export default function reverseWords(str) { 3 | return str.split(' ').reverse().join(' '); 4 | } 5 | -------------------------------------------------------------------------------- /src/string/reverse.js: -------------------------------------------------------------------------------- 1 | // reverse character order 2 | export default function reverse(str) { 3 | return str.split('').reverse().join(''); 4 | } 5 | -------------------------------------------------------------------------------- /src/string/similarity.js: -------------------------------------------------------------------------------- 1 | import editDistance from './edit-distance'; 2 | 3 | // percentage of similiarity from 0 to 1 4 | export default function similarity(a, b) { 5 | const e = editDistance(a, b); 6 | const m = Math.max(a.length, b.length); 7 | if (m === 0) { 8 | return 1; 9 | } 10 | return (1 - e / m); 11 | } 12 | -------------------------------------------------------------------------------- /src/string/strip-tags.js: -------------------------------------------------------------------------------- 1 | // remove all HTML tags from str 2 | export default function stripTags(str) { 3 | return str.replace(/<\/?[^>]+>/igm, ''); 4 | } 5 | -------------------------------------------------------------------------------- /src/string/swap-case.js: -------------------------------------------------------------------------------- 1 | 2 | // swaps the case of str 3 | export default function swapCase(str) { 4 | return str.replace(/(\w)/, function(newStr) { 5 | const lower = newStr.toLowerCase(); 6 | const upper = newStr.toUpperCase(); 7 | switch (newStr) { 8 | case lower: 9 | return upper; 10 | case upper: 11 | return lower; 12 | default: 13 | return newStr; 14 | } 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/string/time-code.js: -------------------------------------------------------------------------------- 1 | // formats seconds into HH:MM:SS 2 | export default function timeCode(seconds, delim = ':') { 3 | const h = Math.floor(seconds / 3600); 4 | const m = Math.floor((seconds % 3600) / 60); 5 | const s = Math.floor((seconds % 3600) % 60); 6 | const hr = (h < 10 ? '0' + h : h) + delim; 7 | const mn = (m < 10 ? '0' + m : m) + delim; 8 | const sc = (s < 10 ? '0' + s : s); 9 | return hr + mn + sc; 10 | } 11 | -------------------------------------------------------------------------------- /src/string/to-number.js: -------------------------------------------------------------------------------- 1 | export default function toNumber(str) { 2 | return Number(str.replace(/[^0-9.]/g, '')); 3 | } 4 | -------------------------------------------------------------------------------- /src/string/truncate.js: -------------------------------------------------------------------------------- 1 | // truncate to length with suffix 2 | export default function truncate(str, len, suffix = '...') { 3 | len -= suffix.length; 4 | let trunc = str; 5 | if (trunc.length > len) { 6 | trunc = trunc.substr(0, len); 7 | const r = /[^\s]/; 8 | if (r.test(str.charAt(len))) { 9 | trunc = trunc.replace(/\w+$|\s+$/, '').trim(); 10 | } 11 | trunc += suffix; 12 | } 13 | return trunc; 14 | } 15 | -------------------------------------------------------------------------------- /src/string/word-count.js: -------------------------------------------------------------------------------- 1 | // the number of words in a string 2 | export default function wordCount(str) { 3 | return str.match(/\b\w+\b/g).length; 4 | } 5 | -------------------------------------------------------------------------------- /src/track/event.js: -------------------------------------------------------------------------------- 1 | export default function event(category, action, label, value) { 2 | if (!window.ga) { 3 | return; 4 | } 5 | window.ga('send', 'event', category, action, label, value); 6 | } 7 | -------------------------------------------------------------------------------- /src/track/index.js: -------------------------------------------------------------------------------- 1 | import event from './event'; 2 | import pageview from './pageview'; 3 | import load from './load'; 4 | 5 | export default { 6 | event, 7 | pageview, 8 | load 9 | }; 10 | -------------------------------------------------------------------------------- /src/track/load.js: -------------------------------------------------------------------------------- 1 | export default function load(gaAccount) { 2 | console.log('Initialize Google Analytics with account Id:', gaAccount); 3 | 4 | /*eslint-disable*/ 5 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 6 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 7 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 8 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 9 | /*eslint-enable*/ 10 | 11 | window.ga('create', gaAccount, 'auto'); 12 | window.ga('send', 'pageview'); 13 | } 14 | -------------------------------------------------------------------------------- /src/track/pageview.js: -------------------------------------------------------------------------------- 1 | export default function pageview(path) { 2 | if (!window.ga) { 3 | return; 4 | } 5 | window.ga('send', 'pageview', path); 6 | } 7 | -------------------------------------------------------------------------------- /src/tween/index.js: -------------------------------------------------------------------------------- 1 | import {easeOutQuad} from '../ease/quad'; 2 | 3 | export default class Tween { 4 | constructor(ob, props, duration, options) { 5 | this.ob = ob; 6 | 7 | if (props) { 8 | this.to(props, duration, options); 9 | } 10 | } 11 | 12 | to(props, duration, options = {}) { 13 | this.duration = duration; 14 | this.ease = options.ease || easeOutQuad; 15 | this.delay = options.delay || 0; 16 | this.onUpdate = options.onUpdate; 17 | this.onComplete = options.onComplete; 18 | this.time = 0; 19 | this.complete = false; 20 | 21 | this._props = Object.keys(props); 22 | this._beginVals = {}; 23 | this._changeVals = {}; 24 | 25 | for (let i = 0; i < this._props.length; i++) { 26 | const prop = this._props[i]; 27 | this._beginVals[prop] = this.ob[prop]; 28 | this._changeVals[prop] = props[prop] - this._beginVals[prop]; 29 | } 30 | } 31 | 32 | update(dt) { 33 | if (this.time === this.duration) { 34 | return; 35 | } 36 | 37 | if (this.delay > 0) { 38 | this.delay -= dt; 39 | return; 40 | } 41 | 42 | this.time += dt; 43 | 44 | if (this.time > this.duration) { 45 | this.time = this.duration; 46 | } 47 | 48 | for (let i = 0; i < this._props.length; i++) { 49 | const prop = this._props[i]; 50 | this.ob[prop] = this.ease(this.time, this._beginVals[prop], this._changeVals[prop], this.duration); 51 | } 52 | 53 | if (this.onUpdate) { 54 | this.onUpdate(this.ob); 55 | } 56 | 57 | if (this.time === this.duration) { 58 | this.complete = true; 59 | 60 | if (this.onComplete) { 61 | this.onComplete(this.ob); 62 | } 63 | } 64 | } 65 | 66 | reset() { 67 | this.time = 0; 68 | this.complete = false; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/visibility/api.js: -------------------------------------------------------------------------------- 1 | let hidden = null; 2 | let change = null; 3 | 4 | if (typeof document !== 'undefined') { 5 | if (typeof document.hidden !== 'undefined') { 6 | hidden = 'hidden'; 7 | change = 'visibilitychange'; 8 | } else if (typeof document.mozHidden !== 'undefined') { 9 | hidden = 'mozHidden'; 10 | change = 'mozvisibilitychange'; 11 | } else if (typeof document.msHidden !== 'undefined') { 12 | hidden = 'msHidden'; 13 | change = 'msvisibilitychange'; 14 | } else if (typeof document.webkitHidden !== 'undefined') { 15 | hidden = 'webkitHidden'; 16 | change = 'webkitvisibilitychange'; 17 | } 18 | } 19 | 20 | export default { 21 | hidden, 22 | change 23 | }; 24 | -------------------------------------------------------------------------------- /src/visibility/index.js: -------------------------------------------------------------------------------- 1 | import api from './api'; 2 | import Emitter from '../events/emitter'; 3 | 4 | const visibility = new Emitter(); 5 | 6 | Object.defineProperties(visibility, { 7 | hidden: { 8 | get() { 9 | return document[api.hidden]; 10 | } 11 | } 12 | }); 13 | 14 | function onVisibilityChange() { 15 | if (document[api.hidden]) { 16 | visibility.emit('hidden'); 17 | } else { 18 | visibility.emit('shown'); 19 | } 20 | } 21 | 22 | if (api.change) { 23 | document.addEventListener(api.change, onVisibilityChange, false); 24 | } 25 | 26 | export default visibility; 27 | -------------------------------------------------------------------------------- /test/array.spec.js: -------------------------------------------------------------------------------- 1 | import array from '../src/array'; 2 | 3 | describe('array utils', () => { 4 | 5 | it('should clone array', () => { 6 | const arr = [1, 2, 3]; 7 | const cloned = array.clone(arr); 8 | arr[0] = 9; 9 | expect(cloned).to.be.instanceof(Array); 10 | expect(cloned).to.eql([1, 2, 3]); 11 | }); 12 | 13 | it('should find nearest value', () => { 14 | expect(array.nearest(2.2, [3, 2, 1, 0])).to.eql(2); 15 | expect(array.nearest(100, [300, 2000])).to.eql(300); 16 | expect(array.nearest(-10, [-9, 10])).to.eql(-9); 17 | }); 18 | 19 | it('should move element', () => { 20 | expect(array.moveElement([1, 2, 3], 0, 1)).to.eql([2, 1, 3]); 21 | expect(array.moveElement([1, 2, 3], 2, 0)).to.eql([3, 1, 2]); 22 | }); 23 | 24 | it('should return random element', () => { 25 | expect(array.randomChoice([3, 2, 1, 0])).to.be.a('number'); 26 | }); 27 | 28 | it('should return alpha ordered array', () => { 29 | expect(['thing3', 'thing2', 'thing1', 'thing0'].sort(array.sortAlpha)) 30 | .to.eql(['thing0', 'thing1', 'thing2', 'thing3']); 31 | expect([{n: 'thing3'}, {n: 'thing2'}, {n: 'thing1'}].sort(array.sortAlpha('n'))) 32 | .to.eql([{n: 'thing1'}, {n: 'thing2'}, {n: 'thing3'}]); 33 | }); 34 | 35 | it('should return numbered ordered array', () => { 36 | expect(['Item 10', 'Item 2', 'Item 1'].sort(array.sortNumbered)).to.eql(['Item 1', 'Item 2', 'Item 10']); 37 | expect(['val=20', 'val=1', 'val=0.3'].sort(array.sortNumbered)).to.eql(['val=0.3', 'val=1', 'val=20']); 38 | expect(['val=-1', 'val=-3', 'val=-2'].sort(array.sortNumbered)).to.eql(['val=-3', 'val=-2', 'val=-1']); 39 | expect([{n: 'Item 2'}, {n: 'Item 3'}, {n: 'Item 1'}].sort(array.sortNumbered('n'))) 40 | .to.eql([{n: 'Item 1'}, {n: 'Item 2'}, {n: 'Item 3'}]); 41 | }); 42 | 43 | it('should return numeric ordered array', () => { 44 | expect([3, 2, 1, 0].sort(array.sortNumeric)).to.eql([0, 1, 2, 3]); 45 | expect([20, -1, 0.3].sort(array.sortNumeric)).to.eql([-1, 0.3, 20]); 46 | expect([{n: 2}, {n: 3}, {n: 1}].sort(array.sortNumeric('n'))) 47 | .to.eql([{n: 1}, {n: 2}, {n: 3}]); 48 | }); 49 | 50 | it('should return random sorted array', () => { 51 | expect([3, 2, 1, 0].sort(array.sortRandom)).to.be.instanceof(Array); 52 | expect([3, 2, 1, 0].sort(array.sortRandom)).to.have.property('length', 4); 53 | }); 54 | 55 | it('should return unique array', () => { 56 | expect(array.unique([3, 2, 1, 0])).to.be.instanceof(Array); 57 | expect(array.unique([3, 2, 1, 0])).to.eql([3, 2, 1, 0]); 58 | expect(array.unique([3, 3, 1, 1])).to.eql([3, 1]); 59 | expect(array.unique([3, 3, 3, 3])).to.eql([3]); 60 | expect(array.unique([3, 3, 3, 2])).to.eql([3, 2]); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/bundle-nodejs.spec.js: -------------------------------------------------------------------------------- 1 | const usfl = require('../dist/usfl.js'); 2 | const {expect} = require('chai'); 3 | 4 | describe('node bundle', () => { 5 | 6 | it('should exist', () => { 7 | expect(usfl).to.exist; 8 | }); 9 | 10 | it('should have array utils', () => { 11 | expect(usfl.array).to.be.an('object'); 12 | }); 13 | 14 | it('should have dom utils', () => { 15 | expect(usfl.dom).to.be.an('object'); 16 | }); 17 | 18 | it('should have ease util', () => { 19 | expect(usfl.ease).to.be.an('object'); 20 | }); 21 | 22 | it('should have events utils', () => { 23 | expect(usfl.events).to.be.an('object'); 24 | }); 25 | 26 | it('should have fullscreen util', () => { 27 | expect(usfl.fullscreen).to.be.an('object'); 28 | }); 29 | 30 | it('should have graphics utils', () => { 31 | expect(usfl.graphics).to.be.a('function'); 32 | }); 33 | 34 | it('should have gui util', () => { 35 | expect(usfl.gui).to.be.a('function'); 36 | }); 37 | 38 | it('should have http utils', () => { 39 | expect(usfl.http).to.be.an('object'); 40 | }); 41 | 42 | it('should have imput utils', () => { 43 | expect(usfl.input).to.be.an('object'); 44 | }); 45 | 46 | it('should have linkedList utils', () => { 47 | expect(usfl.linkedList).to.be.a('function'); 48 | }); 49 | 50 | it('should have loop utils', () => { 51 | expect(usfl.Loop).to.be.a('function'); 52 | expect(usfl.loop).to.be.an('object'); 53 | }); 54 | 55 | it('should have math utils', () => { 56 | expect(usfl.math).to.be.an('object'); 57 | }); 58 | 59 | it('should have media utils', () => { 60 | expect(usfl.media).to.be.an('object'); 61 | }); 62 | 63 | it('should have object utils', () => { 64 | expect(usfl.object).to.be.an('object'); 65 | }); 66 | 67 | it('should have objectPool util', () => { 68 | expect(usfl.objectPool).to.be.a('function'); 69 | }); 70 | 71 | it('should have Particle util', () => { 72 | expect(usfl.Particle).to.be.a('function'); 73 | }); 74 | 75 | it('should have ParticleGroup util', () => { 76 | expect(usfl.ParticleGroup).to.be.a('function'); 77 | }); 78 | 79 | it('should have platform utils', () => { 80 | expect(usfl.platform).to.be.an('object'); 81 | }); 82 | 83 | it('should have popup util', () => { 84 | expect(usfl.popup).to.be.a('function'); 85 | }); 86 | 87 | it('should have QuadTree util', () => { 88 | expect(usfl.QuadTree).to.be.a('function'); 89 | }); 90 | 91 | it('should have share utils', () => { 92 | expect(usfl.share).to.be.an('object'); 93 | }); 94 | 95 | it('should have storage util', () => { 96 | expect(usfl.storage).to.be.an('object'); 97 | }); 98 | 99 | it('should have string utils', () => { 100 | expect(usfl.string).to.be.an('object'); 101 | }); 102 | 103 | it('should have tween util', () => { 104 | expect(usfl.Tween).to.be.a('function'); 105 | }); 106 | 107 | it('should have track util', () => { 108 | expect(usfl.track).to.be.an('object'); 109 | }); 110 | 111 | it('should have visibility util', () => { 112 | expect(usfl.visibility).to.be.an('object'); 113 | }); 114 | 115 | }); 116 | -------------------------------------------------------------------------------- /test/bundle.spec.js: -------------------------------------------------------------------------------- 1 | describe('bundle', () => { 2 | 3 | it('should exist', () => { 4 | expect(usfl).to.exist; 5 | }); 6 | 7 | it('should have utils', () => { 8 | expect(usfl.array).to.be.an('object'); 9 | expect(usfl.dom).to.be.an('object'); 10 | expect(usfl.ease).to.be.an('object'); 11 | expect(usfl.events).to.be.an('object'); 12 | expect(usfl.fullscreen).to.be.an('object'); 13 | expect(usfl.graphics).to.be.a('function'); 14 | expect(usfl.gui).to.be.a('function'); 15 | expect(usfl.http).to.be.an('object'); 16 | expect(usfl.input).to.be.an('object'); 17 | expect(usfl.linkedList).to.be.a('function'); 18 | expect(usfl.Loop).to.be.a('function'); 19 | expect(usfl.math).to.be.an('object'); 20 | expect(usfl.media).to.be.an('object'); 21 | expect(usfl.object).to.be.an('object'); 22 | expect(usfl.objectPool).to.be.a('function'); 23 | expect(usfl.Particle).to.be.a('function'); 24 | expect(usfl.ParticleGroup).to.be.a('function'); 25 | expect(usfl.platform).to.be.an('object'); 26 | expect(usfl.popup).to.be.a('function'); 27 | expect(usfl.QuadTree).to.be.a('function'); 28 | expect(usfl.share).to.be.an('object'); 29 | expect(usfl.storage).to.be.an('object'); 30 | expect(usfl.string).to.be.an('object'); 31 | expect(usfl.Tween).to.be.a('function'); 32 | expect(usfl.track).to.be.an('object'); 33 | expect(usfl.visibility).to.be.an('object'); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/dom.spec.js: -------------------------------------------------------------------------------- 1 | import dom from '../src/dom'; 2 | 3 | describe('dom', () => { 4 | 5 | it('should block scrolling', () => { 6 | dom.blockScrolling(true); 7 | expect(document.body.style.overflow).to.eql('hidden'); 8 | dom.blockScrolling(false); 9 | expect(document.body.style.overflow).to.eql(''); 10 | }); 11 | 12 | it('should have forceRedraw', () => { 13 | expect(dom.forceRedraw).to.be.a('function'); 14 | }); 15 | 16 | it('should get ScrollPercentage', () => { 17 | expect(dom.getScrollPercentage()).to.be.a('number'); 18 | }); 19 | 20 | it('should get PageHeight', () => { 21 | expect(dom.getPageHeight()).to.be.a('number'); 22 | }); 23 | 24 | it('should get ScrollRemaining', () => { 25 | expect(dom.getScrollRemaining()).to.be.a('number'); 26 | }); 27 | 28 | it('should get ScrollTop', () => { 29 | expect(dom.getScrollTop()).to.be.a('number'); 30 | }); 31 | 32 | it('should get SrcsetImage', () => { 33 | const srcsetA = `images/image_2048.jpg 2048w, 34 | images/image_640.jpg 640w, 35 | images/image_1536.jpg 1536w`; 36 | expect(dom.getSrcsetImage(srcsetA)).to.be.a('string'); 37 | expect(dom.getSrcsetImage(srcsetA, 2048)).to.eql('images/image_2048.jpg'); 38 | expect(dom.getSrcsetImage(srcsetA, 500)).to.eql('images/image_640.jpg'); 39 | expect(dom.getSrcsetImage(srcsetA, 1280)).to.eql('images/image_1536.jpg'); 40 | expect(dom.getSrcsetImage(srcsetA, 3000)).to.eql('images/image_2048.jpg'); 41 | }); 42 | 43 | it('should get isElementInViewport', () => { 44 | expect(dom.isElementInViewport(document.body)).to.be.a('boolean'); 45 | }); 46 | 47 | it('should get isPageEnd', () => { 48 | expect(dom.isPageEnd()).to.be.a('boolean'); 49 | }); 50 | 51 | it('should have resize', () => { 52 | expect(dom.resize).to.be.a('function'); 53 | }); 54 | 55 | it('should have scroll', () => { 56 | expect(dom.scroll).to.be.a('function'); 57 | }); 58 | 59 | it('should have transitionEnd', () => { 60 | expect(dom.transitionEnd).to.be.a('function'); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/ease.spec.js: -------------------------------------------------------------------------------- 1 | import ease from '../src/ease'; 2 | 3 | describe('Ease', () => { 4 | 5 | const types = [ 6 | 'Linear', 7 | 'Back', 8 | 'Bounce', 9 | 'Circular', 10 | 'Cubic', 11 | 'Elastic', 12 | 'Expo', 13 | 'Quad', 14 | 'Quart', 15 | 'Quint', 16 | 'Sine' 17 | ]; 18 | 19 | it('should have ease type objects', () => { 20 | for (let i = 0; i < types.length; i++) { 21 | const easeType = ease[types[i].toLowerCase()]; 22 | expect(easeType).to.be.an('object'); 23 | expect(easeType.easeIn).to.be.a('function'); 24 | expect(easeType.easeOut).to.be.a('function'); 25 | expect(easeType.easeInOut).to.be.a('function'); 26 | } 27 | }); 28 | 29 | it('should have ease type functions', () => { 30 | expect(ease.easeLinear).to.be.a('function'); 31 | for (let i = 1; i < types.length; i++) { 32 | expect(ease['easeIn' + types[i]]).to.be.a('function'); 33 | expect(ease['easeOut' + types[i]]).to.be.a('function'); 34 | expect(ease['easeInOut' + types[i]]).to.be.a('function'); 35 | } 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/events.spec.js: -------------------------------------------------------------------------------- 1 | import events from '../src/events'; 2 | 3 | describe('events', () => { 4 | 5 | it('should have debounce', () => { 6 | expect(events.debounce).to.be.a('function'); 7 | }); 8 | 9 | it('should have delegateEvents', () => { 10 | expect(events.delegateEvents).to.be.a('function'); 11 | }); 12 | 13 | it('should have emitter', () => { 14 | expect(events.emitter).to.be.a('function'); 15 | }); 16 | 17 | it('should have eventBus', () => { 18 | expect(events.eventBus).to.be.an('object'); 19 | }); 20 | 21 | it('should have heartbeat', () => { 22 | expect(events.heartbeat).to.be.a('function'); 23 | expect(events.heartbeat().start).to.be.a('function'); 24 | expect(events.heartbeat().stop).to.be.a('function'); 25 | expect(events.heartbeat().update).to.be.a('function'); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test/fps.spec.js: -------------------------------------------------------------------------------- 1 | import fps from '../src/fps'; 2 | 3 | describe('fps', () => { 4 | 5 | it('should have update fn', () => { 6 | expect(fps.update).to.be.a('function'); 7 | expect(fps.auto).to.be.a('function'); 8 | }); 9 | 10 | }); 11 | -------------------------------------------------------------------------------- /test/fullscreen.spec.js: -------------------------------------------------------------------------------- 1 | import fullscreen from '../src/fullscreen'; 2 | 3 | describe('fullscreen', function() { 4 | 5 | it('should return state booleans', function() { 6 | expect(fullscreen.isSupported).to.be.a('boolean'); 7 | expect(fullscreen.isFullscreen).to.be.a('boolean'); 8 | expect(fullscreen.enabled).to.be.a('boolean'); 9 | }); 10 | 11 | it('should have request and exit methods', function() { 12 | expect(fullscreen.request).to.be.a('function'); 13 | expect(fullscreen.exit).to.be.a('function'); 14 | expect(fullscreen.toggle).to.be.a('function'); 15 | }); 16 | 17 | it('should have emitter', function() { 18 | expect(fullscreen.on).to.be.a('function'); 19 | expect(fullscreen.off).to.be.a('function'); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /test/graphics.spec.js: -------------------------------------------------------------------------------- 1 | import Graphics from '../src/graphics'; 2 | import getImageDataURL from '../src/graphics/get-image-data-url'; 3 | import SpritesheetPlayer from '../src/graphics/spritesheet-player'; 4 | 5 | describe('graphics', () => { 6 | const gfx = new Graphics(); 7 | 8 | it('should construct successfully', () => { 9 | expect(gfx).to.exist; 10 | expect(gfx.size).to.be.a('function'); 11 | expect(gfx.size(100, 100)).to.eql(gfx); 12 | }); 13 | 14 | it('should have context', () => { 15 | expect(gfx.context).to.exist; 16 | }); 17 | 18 | const image = new Image(); 19 | image.crossOrigin = 'anonymous'; 20 | 21 | beforeEach((done) => { 22 | if (!image.src) { 23 | image.addEventListener('load', () => done()); 24 | image.src = 'http://i.imgur.com/pIUsuyE.jpg'; 25 | } else { 26 | done(); 27 | } 28 | }); 29 | 30 | it('should get image data', () => { 31 | const dataURL = getImageDataURL(image); 32 | expect(dataURL.indexOf('data:image/')).to.eql(0); 33 | }); 34 | 35 | // context: ctx, 36 | // size, 37 | // clear, 38 | // background, 39 | // fill, 40 | // stroke, 41 | // strokeWeight, 42 | // move, 43 | // line, 44 | // rect, 45 | // circle, 46 | // triangle, 47 | // triangleABC, 48 | // image, 49 | // cross, 50 | // text, 51 | // setFont, 52 | // setFontSize, 53 | // openImage, 54 | // downloadImage, 55 | // getImageData, 56 | // getPixel, 57 | // setPixel, 58 | // eachPixel 59 | 60 | }); 61 | 62 | describe('spritesheet player', () => { 63 | it('should construct', () => { 64 | const p = new SpritesheetPlayer(); 65 | expect(p).to.exist; 66 | expect(p.update).to.be.a('function'); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/http.spec.js: -------------------------------------------------------------------------------- 1 | import http from '../src/http'; 2 | 3 | describe('http', () => { 4 | 5 | it('should get location', () => { 6 | expect(http.getLocation('http://www.example.com/path').hostname).to.eql('www.example.com'); 7 | expect(http.getLocation('http://www.example.com/path').pathname).to.eql('/path'); 8 | expect(http.getLocation('http://www.example.com/path').protocol).to.eql('http:'); 9 | }); 10 | 11 | it('should have jsonp', () => { 12 | expect(http.jsonp).to.be.a('function'); 13 | }); 14 | 15 | it('should get url params', () => { 16 | expect(http.urlParams('foo=bar&hello=world')).to.eql({ 17 | foo: 'bar', 18 | hello: 'world' 19 | }); 20 | }); 21 | 22 | }); 23 | 24 | describe('http.xhr', function() { 25 | this.timeout(5000); 26 | 27 | let res = null; 28 | 29 | it('should have xhr', () => { 30 | expect(http.xhr).to.be.a('function'); 31 | }); 32 | 33 | beforeEach((done) => { 34 | http.xhr('https://ianmcgregor.co/prototypes/test/test.json') 35 | .then((response) => (res = response)) 36 | .then(() => done()) 37 | .catch((err) => console.log('err', err)); 38 | }); 39 | 40 | it('should have json response', () => { 41 | expect(res).to.be.an('object'); 42 | expect(res.name).to.eql('test'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/input.spec.js: -------------------------------------------------------------------------------- 1 | import input from '../src/input'; 2 | 3 | describe('input', () => { 4 | 5 | describe('clickOutside', () => { 6 | expect(input.clickOutside).to.be.a('function'); 7 | expect(input.clickOutside().destroy).to.be.a('function'); 8 | }); 9 | 10 | describe('key', () => { 11 | 12 | it('should have keyboard constants', () => { 13 | expect(input.keyboard.A).to.eql(65); 14 | expect(input.keyboard.UP).to.eql(38); 15 | }); 16 | 17 | it('should have key input', () => { 18 | expect(input.keyInput).to.be.a('function'); 19 | expect(input.keyInput().enable).to.be.a('function'); 20 | expect(input.keyInput().isDown).to.be.a('function'); 21 | expect(input.keyInput().isDown(input.keyboard.UP)).to.be.a('boolean'); 22 | }); 23 | 24 | }); 25 | 26 | describe('microphone', () => { 27 | expect(input.microphone).to.be.a('function'); 28 | expect(input.microphone().isSupported()).to.be.a('boolean'); 29 | }); 30 | 31 | describe('mouseLeftWindow', () => { 32 | expect(input.mouseLeftWindow).to.be.a('function'); 33 | expect(input.mouseLeftWindow(() => {}).destroy).to.be.a('function'); 34 | }); 35 | 36 | describe('mouseWheel', () => { 37 | expect(input.mouseWheel).to.be.a('function'); 38 | }); 39 | 40 | describe('pointerCoords', () => { 41 | expect(input.pointerCoords).to.be.a('function'); 42 | expect(input.pointerCoords().x).to.be.a('number'); 43 | expect(input.pointerCoords().y).to.be.a('number'); 44 | expect(input.pointerCoords().percentX).to.be.a('number'); 45 | expect(input.pointerCoords().percentY).to.be.a('number'); 46 | }); 47 | 48 | describe('touch input', () => { 49 | const touchInput = input.touchInput(); 50 | 51 | it('should construct successfully', () => { 52 | expect(touchInput).to.be.an('object'); 53 | }); 54 | 55 | it('should have api', () => { 56 | expect(touchInput.listen).to.be.a('function'); 57 | expect(touchInput.isDown).to.be.a('function'); 58 | expect(touchInput.getTouch).to.be.a('function'); 59 | expect(touchInput.destroy).to.be.a('function'); 60 | }); 61 | 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/linked-list.spec.js: -------------------------------------------------------------------------------- 1 | import linkedList from '../src/linked-list'; 2 | 3 | describe('linked list', () => { 4 | const list = linkedList(); 5 | const items = []; 6 | 7 | for (let i = 0; i < 10; i++) { 8 | items.push(list.add({ 9 | 'index': i, 10 | 'next': null, 11 | 'prev': null 12 | })); 13 | } 14 | 15 | it('should have 10 items', () => { 16 | expect(list.getCount()).to.eql(10); 17 | }); 18 | 19 | it('should have first', () => { 20 | expect(list.first).to.exist; 21 | expect(list.getFirst()).to.exist; 22 | }); 23 | 24 | it('should have last', () => { 25 | expect(list.last).to.exist; 26 | expect(list.getLast()).to.exist; 27 | }); 28 | 29 | it('should be able to iterate', () => { 30 | let item = list.getFirst(); 31 | while (item.next) { 32 | expect(item).to.exist; 33 | expect(item).to.have.property('index'); 34 | item = item.next; 35 | } 36 | expect(item).to.eql(list.getLast()); 37 | }); 38 | 39 | it('should be able to remove item', () => { 40 | list.remove(list.getFirst()); 41 | expect(list.getCount()).to.eql(9); 42 | expect(list.getFirst()).to.exist; 43 | 44 | list.remove(list.getLast()); 45 | expect(list.getCount()).to.eql(8); 46 | expect(list.getLast()).to.exist; 47 | }); 48 | 49 | it('should be able to insert', () => { 50 | const item = { 51 | 'index': 100, 52 | 'next': null, 53 | 'prev': null 54 | }; 55 | list.insertBefore(item, list.getFirst()); 56 | 57 | expect(list.getCount()).to.eql(9); 58 | expect(item).to.eql(list.getFirst()); 59 | }); 60 | 61 | it('should be have getters', () => { 62 | expect(list.length).to.eql(9); 63 | expect(list.length).to.eql(list.getCount()); 64 | expect(list.first).to.exist; 65 | expect(list.first).to.eql(list.getFirst()); 66 | expect(list.last).to.exist; 67 | expect(list.last).to.eql(list.getLast()); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/loop.spec.js: -------------------------------------------------------------------------------- 1 | import {Loop} from '../src/loop'; 2 | 3 | describe('Loop', () => { 4 | const loop = new Loop(); 5 | let updated = false; 6 | let delta = 0; 7 | let elapsed = 0; 8 | loop.add((deltaTime, elapsedTime) => { 9 | updated = true; 10 | delta = deltaTime; 11 | elapsed = elapsedTime; 12 | }); 13 | 14 | beforeEach((done) => { 15 | loop.start(); 16 | done(); 17 | }); 18 | 19 | it('should construct', () => { 20 | expect(loop).to.exist; 21 | }); 22 | 23 | it('should start', () => { 24 | loop.start(); 25 | expect(loop.running).to.be.true; 26 | }); 27 | 28 | it('should stop', () => { 29 | loop.stop(); 30 | expect(loop.running).to.be.false; 31 | }); 32 | 33 | it('should update', () => { 34 | expect(updated).to.be.true; 35 | }); 36 | 37 | it('should have dt', () => { 38 | expect(delta).to.be.a('number'); 39 | expect(elapsed).to.be.a('number'); 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /test/media.spec.js: -------------------------------------------------------------------------------- 1 | import media from '../src/media'; 2 | import mp4 from '../src/platform/mp4'; 3 | 4 | describe('media', () => { 5 | 6 | describe('cuepointsReader', () => { 7 | let name = ''; 8 | const reader = media.cuepointsReader(); 9 | reader.onCuepoint((item) => (name = item.name)); 10 | reader.add(2, 'foo', {name: 'bar'}); 11 | reader.add(3, 'bar', {name: 'foo'}); 12 | 13 | beforeEach((done) => { 14 | reader.update(2); 15 | done(); 16 | }); 17 | 18 | expect(media.cuepointsReader).to.be.a('function'); 19 | 20 | it('should have dispatched foo', () => { 21 | expect(name).to.eql('foo'); 22 | }); 23 | }); 24 | 25 | describe('video', () => { 26 | it('should have iOSPlayVideoInline', () => { 27 | expect(media.iOSPlayVideoInline).to.be.a('function'); 28 | }); 29 | 30 | it('should have videoPlayer', () => { 31 | expect(media.videoPlayer).to.be.a('function'); 32 | }); 33 | 34 | it('should have vimeo', () => { 35 | expect(media.vimeo).to.be.a('function'); 36 | }); 37 | 38 | it('should have youtube', () => { 39 | expect(media.youtube).to.be.a('function'); 40 | }); 41 | 42 | it('should have youtubeBasic', () => { 43 | expect(media.youtubeBasic).to.be.a('function'); 44 | }); 45 | }); 46 | 47 | describe('video player', function() { 48 | this.timeout(20000); // extend timeout for this test 49 | 50 | const videoPlayer = media.videoPlayer(), 51 | ext = mp4 ? 'mp4' : 'webm', 52 | file = `https://ianmcgregor.co/prototypes/video/counter.${ext}`; 53 | 54 | let ready = false; 55 | 56 | beforeEach((done) => { 57 | 58 | videoPlayer.on('ready', function() { 59 | ready = true; 60 | done(); 61 | }).on('error', function(err) { 62 | console.error(err); 63 | ready = true; 64 | done(); 65 | }); 66 | videoPlayer.load(file); 67 | }); 68 | 69 | it('should be ready', function() { 70 | expect(ready).to.be.true; 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/object-pool.spec.js: -------------------------------------------------------------------------------- 1 | import objectPool from '../src/object-pool'; 2 | 3 | describe('object pool', function() { 4 | let newlyCreated = 0; 5 | 6 | function TestOb() { 7 | newlyCreated++; 8 | const id = Math.random(); 9 | return { 10 | getId: function() { 11 | return id; 12 | } 13 | }; 14 | } 15 | 16 | it('should pass', function() { 17 | const pool = objectPool(TestOb); 18 | let instance = pool.get(); 19 | 20 | expect(instance).to.exist; 21 | expect(instance.getId()).to.be.a('number'); 22 | expect(pool.getPool().length).to.eql(0); 23 | 24 | pool.dispose(instance); 25 | 26 | expect(pool.getPool().length).to.eql(1); 27 | 28 | instance = pool.get(); 29 | 30 | expect(instance.getId()).to.be.a('number'); 31 | expect(pool.getPool().length).to.eql(0); 32 | expect(newlyCreated).to.eql(1); 33 | 34 | pool.fill(10); 35 | expect(pool.getPool().length).to.eql(10); 36 | expect(newlyCreated).to.eql(11); 37 | 38 | for (let i = 0; i < 5; i++) { 39 | instance = pool.get(); 40 | expect(instance.getId()).to.be.a('number'); 41 | } 42 | expect(pool.getPool().length).to.eql(5); 43 | expect(newlyCreated).to.eql(11); 44 | 45 | expect(pool.getNumCreated()).to.eql(newlyCreated); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/object.spec.js: -------------------------------------------------------------------------------- 1 | import object from '../src/object'; 2 | 3 | describe('object utils', () => { 4 | 5 | it('should clone object', () => { 6 | const obj = {a: 1, b: 2, c: 3}; 7 | const cloned = object.clone(obj); 8 | obj.a = 9; 9 | expect(cloned).to.be.an('object'); 10 | expect(cloned).to.eql({a: 1, b: 2, c: 3}); 11 | expect(obj).to.eql({a: 9, b: 2, c: 3}); 12 | }); 13 | 14 | it('should filter object', () => { 15 | expect(object.filter({a: 1, b: 2, c: 3}, (key, value) => value > 1)).to.eql({b: 2, c: 3}); 16 | expect(object.filter({a: 1, b: 2, c: 3}, key => key === 'a')).to.eql({a: 1}); 17 | }); 18 | 19 | it('should map object', () => { 20 | expect(object.map({a: 1, b: 2, c: 3}, (key, value) => value * 2)).to.eql({a: 2, b: 4, c: 6}); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/particle.spec.js: -------------------------------------------------------------------------------- 1 | import Particle from '../src/particle'; 2 | import ParticleGroup from '../src/particle/particle-group'; 3 | 4 | describe('Particle', () => { 5 | 6 | it('should construct Particle instance', () => { 7 | expect(new Particle()).to.exist; 8 | }); 9 | }); 10 | 11 | describe('ParticleGroup', () => { 12 | 13 | it('should construct ParticleGroup instance', () => { 14 | expect(new ParticleGroup()).to.exist; 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/platform.spec.js: -------------------------------------------------------------------------------- 1 | import platform from '../src/platform'; 2 | import androidNative from '../src/platform/android-native'; 3 | import ieVersion from '../src/platform/ie-version'; 4 | import iosVersion from '../src/platform/ios-version'; 5 | import androidVersion from '../src/platform/android-version'; 6 | 7 | describe('platform', () => { 8 | 9 | it('should get device', () => { 10 | expect(platform.mobile).to.be.false; 11 | expect(platform.ipad).to.be.false; 12 | expect(platform.iphone).to.be.false; 13 | expect(platform.desktop).to.be.true; 14 | }); 15 | 16 | it('should get os', () => { 17 | expect(platform.ios).to.be.false; 18 | expect(platform.mac).to.be.a('boolean'); 19 | expect(platform.android).to.be.false; 20 | expect(platform.windows).to.be.false; 21 | }); 22 | 23 | it('should get browser', () => { 24 | expect(androidNative('Mozilla/5.0 (Linux; U; Android 2.3.3; de-ch; HTC Desire Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1')).to.be.true; 25 | expect(androidNative('Mozilla/5.0 (Linux; U; Android 4.2; en-us; Nexus 10 Build/JVP15I) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30')).to.be.true; 26 | expect(ieVersion()).to.eql(0); 27 | expect(platform.ie).to.be.false; 28 | }); 29 | 30 | it('should get language', () => { 31 | expect(platform.language).to.be.a('string'); 32 | }); 33 | 34 | it('should get screen props', () => { 35 | expect(platform.screen.width).to.eql(window.screen.width); 36 | expect(platform.screen.height).to.eql(window.screen.height); 37 | expect(platform.screen.dpr).to.be.a('number'); 38 | }); 39 | 40 | it('should get ie version', () => { 41 | expect(ieVersion('Mozilla/5.0 (Linux; Android 4.1; Galaxy Nexus Build/JRN84D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19')).to.eql(0); 42 | expect(ieVersion('Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 1.1.4322; InfoPath.2; .NET CLR 3.5.21022; .NET CLR 3.5.30729; MS-RTC LM 8; OfficeLiveConnector.1.4; OfficeLivePatch.1.3; .NET CLR 3.0.30729)')).to.eql(8); 43 | expect(ieVersion('Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)')).to.eql(9); 44 | expect(ieVersion('Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)')).to.eql(10); 45 | expect(ieVersion('Mozilla/5.0 (IE 11.0; Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko')).to.eql(11); 46 | }); 47 | 48 | it('should get ios version', () => { 49 | expect(iosVersion('Mozilla/5.0 (Linux; Android 4.1; Galaxy Nexus Build/JRN84D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19')).to.eql(0); 50 | expect(iosVersion('Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53')).to.eql(7); 51 | expect(iosVersion('Mozilla/5.0 (iPad; CPU OS 9_0 like Mac OS X) AppleWebKit/601.1.17 (KHTML, like Gecko) Version/8.0 Mobile/13A175 Safari/600.1.4')).to.eql(9); 52 | expect(iosVersion('Mozilla/5.0 (iPhone; CPU iPhone OS 9_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13C75 Safari/601.1')).to.eql(9.2); 53 | }); 54 | 55 | it('should get android version', () => { 56 | expect(androidVersion('Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53')).to.eql(0); 57 | expect(androidVersion('Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Build/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30')).to.eql(4); 58 | expect(androidVersion('Mozilla/5.0 (Linux; U; Android 2.3.3; de-ch; HTC Desire Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1')).to.eql(2.3); 59 | expect(androidVersion('Mozilla/5.0 (Linux; U; Android 4.2; en-us; Nexus 10 Build/JVP15I) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30')).to.eql(4.2); 60 | expect(androidVersion('Mozilla/5.0 (Linux; Android 4.1; Galaxy Nexus Build/JRN84D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19')).to.eql(4.1); 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /test/popup.spec.js: -------------------------------------------------------------------------------- 1 | import popup from '../src/popup'; 2 | 3 | describe('popup', () => { 4 | 5 | it('should pass', () => { 6 | expect(popup).to.be.a('function'); 7 | }); 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /test/quad-tree.spec.js: -------------------------------------------------------------------------------- 1 | import QuadTree from '../src/quad-tree'; 2 | 3 | describe('QuadTree', () => { 4 | 5 | it('should construct', () => { 6 | expect(new QuadTree({x: 0, y: 0, width: 100, height: 100})).to.exist; 7 | }); 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /test/share.spec.js: -------------------------------------------------------------------------------- 1 | import share from '../src/share'; 2 | 3 | describe('share', () => { 4 | 5 | it('should have share functions', () => { 6 | expect(share.email).to.be.a('function'); 7 | expect(share.facebook).to.be.a('function'); 8 | expect(share.googleplus).to.be.a('function'); 9 | expect(share.linkedin).to.be.a('function'); 10 | expect(share.pinterest).to.be.a('function'); 11 | expect(share.reddit).to.be.a('function'); 12 | expect(share.renren).to.be.a('function'); 13 | expect(share.sms).to.be.a('function'); 14 | expect(share.twitter).to.be.a('function'); 15 | expect(share.vkontakte).to.be.a('function'); 16 | expect(share.weibo).to.be.a('function'); 17 | expect(share.whatsapp).to.be.a('function'); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /test/storage.spec.js: -------------------------------------------------------------------------------- 1 | import storage from '../src/storage'; 2 | 3 | describe('storage utils', () => { 4 | 5 | const key = 'testData', 6 | testData = { 7 | id: 'foo', 8 | name: 'bar', 9 | x: 0 10 | }; 11 | 12 | it('should store object and return true', () => { 13 | const saved = storage.saveJSON(key, testData); 14 | expect(saved).to.be.true; 15 | }); 16 | 17 | it('should retrieve stored object', () => { 18 | const loaded = storage.loadJSON(key); 19 | expect(loaded).to.exist; 20 | expect(loaded.id).to.eql('foo'); 21 | expect(loaded.name).to.eql('bar'); 22 | expect(loaded.x).to.eql(0); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/string.spec.js: -------------------------------------------------------------------------------- 1 | import string from '../src/string'; 2 | 3 | describe('string utils', function() { 4 | 5 | const str = 'Hello World'; 6 | 7 | it('should query', () => { 8 | expect(string.countOf(str, 'l')).to.eql(3); 9 | expect(string.endsWith(str, 'ld')).to.be.true; 10 | expect(string.hasText(str)).to.be.true; 11 | expect(string.isNumeric(str)).to.be.false; 12 | expect(string.isNumeric('68769123214')).to.be.true; 13 | expect(string.wordCount(str)).to.eql(2); 14 | expect(string.similarity(str, str)).to.eql(1); 15 | }); 16 | 17 | it('should find editDistance', () => { 18 | expect(string.editDistance(str, str)).to.eql(0); 19 | expect(string.editDistance(str, str + 'a')).to.eql(1); 20 | }); 21 | 22 | it('should find substr', () => { 23 | expect(string.afterFirst(str, 'l')).to.eql('lo World'); 24 | expect(string.afterLast(str, 'l')).to.eql('d'); 25 | expect(string.beginsWith(str, 'H')).to.be.true; 26 | expect(string.beforeFirst(str, 'l')).to.eql('He'); 27 | expect(string.beforeLast(str, 'l')).to.eql('Hello Wor'); 28 | expect(string.between(str, 'H', 'W')).to.eql('ello '); 29 | }); 30 | 31 | it('should format', () => { 32 | expect(string.padLeft(str, '_', 12)).to.eql('_Hello World'); 33 | expect(string.padRight(str, '_', 12)).to.eql('Hello World_'); 34 | expect(string.removeExtraWhitespace('Hello World')).to.eql('Hello World'); 35 | expect(string.remove(str, 'll')).to.eql('Heo World'); 36 | // TODO: this sometime acts unexpectedly with shorter strings 37 | expect(string.truncate(str, 10)).to.eql('Hello...'); 38 | //expect(string.truncate(str, 4)).to.eql('Hello...'); 39 | expect(string.capitalize(str.toLowerCase())).to.eql('Hello world'); 40 | expect(string.properCase(str.toLowerCase())).to.eql('Hello World'); 41 | expect(string.reverse(str)).to.eql('dlroW olleH'); 42 | expect(string.reverseWords(str)).to.eql('World Hello'); 43 | expect(string.stripTags('

' + str + '

')).to.eql('Hello World'); 44 | expect(string.swapCase(str)).to.eql('hello World'); 45 | // expect(string.block(str)).to.eql('Hello World'); 46 | expect(string.timeCode(217.8)).to.eql('00:03:37'); 47 | }); 48 | 49 | it('should escape', () => { 50 | expect(string.escapePattern(str + '.')).to.eql('Hello World\\.'); 51 | expect(string.escapeHTML('')) 52 | .to.eql('<script>alert("lol")</script>'); 53 | }); 54 | 55 | it('should prevent widow', () => { 56 | expect(string.preventWidow('Hello world')).to.eql('Hello world'); 57 | expect(string.preventWidow(' Hello world ')).to.eql('Hello world'); 58 | expect(string.preventWidow('Morbi in sem quis dui placerat ornare.')) 59 | .to.eql('Morbi in sem quis dui placerat ornare.'); 60 | }); 61 | 62 | it('should convert to number', () => { 63 | expect(string.toNumber('thing_01')).to.eql(1); 64 | expect(string.toNumber('123%')).to.eql(123); 65 | expect(string.toNumber('0.10')).to.eql(0.1); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/track.spec.js: -------------------------------------------------------------------------------- 1 | import track from '../src/track'; 2 | 3 | describe('track', () => { 4 | 5 | it('should have event function', () => { 6 | expect(track.event).to.be.a('function'); 7 | }); 8 | 9 | it('should have pageview function', () => { 10 | expect(track.pageview).to.be.a('function'); 11 | }); 12 | 13 | it('should have load function', () => { 14 | expect(track.load).to.be.a('function'); 15 | expect(track.load.length).to.eql(1); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /test/tween.spec.js: -------------------------------------------------------------------------------- 1 | import Tween from '../src/tween'; 2 | 3 | describe('Tween', () => { 4 | const ob = {x: 0}; 5 | const tween = new Tween(ob, {x: 10}, 1); 6 | 7 | it('should construct', () => { 8 | expect(tween).to.exist; 9 | }); 10 | 11 | it('should change prop', () => { 12 | tween.update(0.5); 13 | tween.update(0.5); 14 | expect(ob.x).to.eql(10); 15 | }); 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /test/visibility.spec.js: -------------------------------------------------------------------------------- 1 | import api from '../src/visibility/api'; 2 | import visibility from '../src/visibility'; 3 | 4 | describe('visibility', () => { 5 | 6 | it('should get visibility api from browser', () => { 7 | expect(api.hidden).to.be.a('string'); 8 | expect(api.change).to.be.a('string'); 9 | }); 10 | 11 | it('should have hidden property', function() { 12 | expect(visibility.hidden).to.be.a('boolean'); 13 | }); 14 | 15 | it('should be emitter', function() { 16 | expect(visibility.on).to.be.a('function'); 17 | expect(visibility.off).to.be.a('function'); 18 | }); 19 | 20 | }); 21 | --------------------------------------------------------------------------------