├── .gitignore ├── LICENSE ├── README.md ├── images ├── bigger_screen.gif ├── horizontal_1.gif ├── horizontal_2.gif ├── smaller_screen.gif ├── vertical_1.gif └── vertical_2.gif ├── package-lock.json ├── package.json ├── src ├── css │ └── styles.css ├── index.html └── js │ ├── balls.js │ ├── bouncing-balls.js │ ├── choice.js │ └── vector2d.js └── test ├── balls-tests.js └── vector2d-tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Meto Trajkovski 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 | # Bouncing Balls 2 | 3 | Simple bouncing balls simulation using plain JavaScript. 4 | Drawing in HTML Canvas, also plain CSS is used. (without third-part frameworks/code) 5 | 6 | **[Try it here](https://mtrajk.github.io/bouncing-balls/)** 7 | 8 |

9 | 10 |

11 | 12 | 13 | ## Description 14 | 15 | - With a mouse click on the canvas a new ball is created, aim with holding the mouse down and moving it (drag the mouse further from the start point for greater speed), shoot the new ball with releasing the mouse button. (also touch screens/devices are supported, the same rules are used for touch events) 16 | - The canvas is updated (redrawn) 60 times in 1 second. (60 fps) 17 | - The canvas is responsive, with help from CSS media queries. 18 | - Because of that, the whole physics engine (all maths and logics inside) works with local coordinates/units. The local width is always 100 local units, and the height is always 66.6667 local units. (because the canvas ratio is 3:2) 19 | - The simulation is not a 100% real-world simulation, because there are many more factors for moving/colliding in the real world like the ball spinning, the softness of balls, the type of walls, even the weather, and sound waves have influence in the real world. 20 | - More description of the physics you can find inside the code, for example, when the balls collide these formulas are used, [link](https://en.wikipedia.org/wiki/Elastic_collision). 21 | - Known issue: In "vertical" space/direction when the bottom is full with balls (when there is no space for a new ball) adding a new ball will make all balls go crazy (jumping randomly). This is because the balls will always collide and won't lose energy from colliding (at this moment I'm not sure how to solve this). 22 | 23 | 24 | ## Repo structure 25 | 26 | - [images](images) - several gifs from the simulations 27 | - [test](test) - unit tests, [Mocha](https://mochajs.org/) framework is used for unit testing.This is how to run these tests using Mocha: 28 | * install [NodeJS](https://nodejs.org/) 29 | * install mocha in this project ``npm install mocha`` 30 | * run the tests from this project ``npm test`` 31 | - [src](src) - the source code of the application 32 | * [index.html](https://github.com/MTrajK/bouncing-balls/tree/master/src/index.html) - a simple HTML page, JS and CSS files are imported and the choices and canvas are defined here 33 | * [css/styles.css](https://github.com/MTrajK/bouncing-balls/tree/master/src/css/styles.css) - used to define media queries (for responsiveness), and other very simple CSS rules 34 | * [js/choice.js](https://github.com/MTrajK/bouncing-balls/tree/master/src/js/choice.js) - used to initialize and stop the simulation (using the values from the choices) 35 | * [js/bouncing-balls.js](https://github.com/MTrajK/bouncing-balls/tree/master/src/js/bouncing-balls.js) - handles the screen interaction (mouse and touch events) and draws in the canvas (some kind of mini game engine) 36 | * [js/balls.js](https://github.com/MTrajK/bouncing-balls/tree/master/src/js/balls.js) - balls collision and movement logics/physics 37 | * [js/vector2d.js](https://github.com/MTrajK/bouncing-balls/tree/master/src/js/vector2d.js) - 2 dimensional vector class, all vector related things are located here 38 | 39 | 40 | ## License 41 | 42 | This project is licensed under the MIT - see the [LICENSE](LICENSE) file for details -------------------------------------------------------------------------------- /images/bigger_screen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MTrajK/bouncing-balls/a535cfaf24b66e51141ce399d6a2fef7737ea461/images/bigger_screen.gif -------------------------------------------------------------------------------- /images/horizontal_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MTrajK/bouncing-balls/a535cfaf24b66e51141ce399d6a2fef7737ea461/images/horizontal_1.gif -------------------------------------------------------------------------------- /images/horizontal_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MTrajK/bouncing-balls/a535cfaf24b66e51141ce399d6a2fef7737ea461/images/horizontal_2.gif -------------------------------------------------------------------------------- /images/smaller_screen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MTrajK/bouncing-balls/a535cfaf24b66e51141ce399d6a2fef7737ea461/images/smaller_screen.gif -------------------------------------------------------------------------------- /images/vertical_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MTrajK/bouncing-balls/a535cfaf24b66e51141ce399d6a2fef7737ea461/images/vertical_1.gif -------------------------------------------------------------------------------- /images/vertical_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MTrajK/bouncing-balls/a535cfaf24b66e51141ce399d6a2fef7737ea461/images/vertical_2.gif -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bouncing-balls", 3 | "requires": true, 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "ansi-colors": { 7 | "version": "3.2.3", 8 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", 9 | "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", 10 | "dev": true 11 | }, 12 | "ansi-regex": { 13 | "version": "3.0.0", 14 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 15 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", 16 | "dev": true 17 | }, 18 | "ansi-styles": { 19 | "version": "3.2.1", 20 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 21 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 22 | "dev": true, 23 | "requires": { 24 | "color-convert": "^1.9.0" 25 | } 26 | }, 27 | "anymatch": { 28 | "version": "3.1.1", 29 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", 30 | "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", 31 | "dev": true, 32 | "requires": { 33 | "normalize-path": "^3.0.0", 34 | "picomatch": "^2.0.4" 35 | } 36 | }, 37 | "argparse": { 38 | "version": "1.0.10", 39 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 40 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 41 | "dev": true, 42 | "requires": { 43 | "sprintf-js": "~1.0.2" 44 | } 45 | }, 46 | "balanced-match": { 47 | "version": "1.0.0", 48 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 49 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 50 | "dev": true 51 | }, 52 | "binary-extensions": { 53 | "version": "2.0.0", 54 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", 55 | "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", 56 | "dev": true 57 | }, 58 | "brace-expansion": { 59 | "version": "1.1.11", 60 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 61 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 62 | "dev": true, 63 | "requires": { 64 | "balanced-match": "^1.0.0", 65 | "concat-map": "0.0.1" 66 | } 67 | }, 68 | "braces": { 69 | "version": "3.0.2", 70 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 71 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 72 | "dev": true, 73 | "requires": { 74 | "fill-range": "^7.0.1" 75 | } 76 | }, 77 | "browser-stdout": { 78 | "version": "1.3.1", 79 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 80 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 81 | "dev": true 82 | }, 83 | "camelcase": { 84 | "version": "5.3.1", 85 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", 86 | "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", 87 | "dev": true 88 | }, 89 | "chalk": { 90 | "version": "2.4.2", 91 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 92 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 93 | "dev": true, 94 | "requires": { 95 | "ansi-styles": "^3.2.1", 96 | "escape-string-regexp": "^1.0.5", 97 | "supports-color": "^5.3.0" 98 | }, 99 | "dependencies": { 100 | "supports-color": { 101 | "version": "5.5.0", 102 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 103 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 104 | "dev": true, 105 | "requires": { 106 | "has-flag": "^3.0.0" 107 | } 108 | } 109 | } 110 | }, 111 | "chokidar": { 112 | "version": "3.3.0", 113 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", 114 | "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", 115 | "dev": true, 116 | "requires": { 117 | "anymatch": "~3.1.1", 118 | "braces": "~3.0.2", 119 | "fsevents": "~2.1.1", 120 | "glob-parent": "~5.1.0", 121 | "is-binary-path": "~2.1.0", 122 | "is-glob": "~4.0.1", 123 | "normalize-path": "~3.0.0", 124 | "readdirp": "~3.2.0" 125 | } 126 | }, 127 | "cliui": { 128 | "version": "5.0.0", 129 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", 130 | "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", 131 | "dev": true, 132 | "requires": { 133 | "string-width": "^3.1.0", 134 | "strip-ansi": "^5.2.0", 135 | "wrap-ansi": "^5.1.0" 136 | }, 137 | "dependencies": { 138 | "ansi-regex": { 139 | "version": "4.1.0", 140 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 141 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 142 | "dev": true 143 | }, 144 | "string-width": { 145 | "version": "3.1.0", 146 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 147 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 148 | "dev": true, 149 | "requires": { 150 | "emoji-regex": "^7.0.1", 151 | "is-fullwidth-code-point": "^2.0.0", 152 | "strip-ansi": "^5.1.0" 153 | } 154 | }, 155 | "strip-ansi": { 156 | "version": "5.2.0", 157 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 158 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 159 | "dev": true, 160 | "requires": { 161 | "ansi-regex": "^4.1.0" 162 | } 163 | } 164 | } 165 | }, 166 | "color-convert": { 167 | "version": "1.9.3", 168 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 169 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 170 | "dev": true, 171 | "requires": { 172 | "color-name": "1.1.3" 173 | } 174 | }, 175 | "color-name": { 176 | "version": "1.1.3", 177 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 178 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 179 | "dev": true 180 | }, 181 | "concat-map": { 182 | "version": "0.0.1", 183 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 184 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 185 | "dev": true 186 | }, 187 | "debug": { 188 | "version": "3.2.6", 189 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 190 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 191 | "dev": true, 192 | "requires": { 193 | "ms": "^2.1.1" 194 | } 195 | }, 196 | "decamelize": { 197 | "version": "1.2.0", 198 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 199 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", 200 | "dev": true 201 | }, 202 | "define-properties": { 203 | "version": "1.1.3", 204 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 205 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 206 | "dev": true, 207 | "requires": { 208 | "object-keys": "^1.0.12" 209 | } 210 | }, 211 | "diff": { 212 | "version": "3.5.0", 213 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 214 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 215 | "dev": true 216 | }, 217 | "emoji-regex": { 218 | "version": "7.0.3", 219 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 220 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", 221 | "dev": true 222 | }, 223 | "es-abstract": { 224 | "version": "1.17.4", 225 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", 226 | "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", 227 | "dev": true, 228 | "requires": { 229 | "es-to-primitive": "^1.2.1", 230 | "function-bind": "^1.1.1", 231 | "has": "^1.0.3", 232 | "has-symbols": "^1.0.1", 233 | "is-callable": "^1.1.5", 234 | "is-regex": "^1.0.5", 235 | "object-inspect": "^1.7.0", 236 | "object-keys": "^1.1.1", 237 | "object.assign": "^4.1.0", 238 | "string.prototype.trimleft": "^2.1.1", 239 | "string.prototype.trimright": "^2.1.1" 240 | } 241 | }, 242 | "es-to-primitive": { 243 | "version": "1.2.1", 244 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", 245 | "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", 246 | "dev": true, 247 | "requires": { 248 | "is-callable": "^1.1.4", 249 | "is-date-object": "^1.0.1", 250 | "is-symbol": "^1.0.2" 251 | } 252 | }, 253 | "escape-string-regexp": { 254 | "version": "1.0.5", 255 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 256 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 257 | "dev": true 258 | }, 259 | "esprima": { 260 | "version": "4.0.1", 261 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 262 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 263 | "dev": true 264 | }, 265 | "fill-range": { 266 | "version": "7.0.1", 267 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 268 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 269 | "dev": true, 270 | "requires": { 271 | "to-regex-range": "^5.0.1" 272 | } 273 | }, 274 | "find-up": { 275 | "version": "3.0.0", 276 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", 277 | "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", 278 | "dev": true, 279 | "requires": { 280 | "locate-path": "^3.0.0" 281 | } 282 | }, 283 | "flat": { 284 | "version": "4.1.0", 285 | "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", 286 | "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", 287 | "dev": true, 288 | "requires": { 289 | "is-buffer": "~2.0.3" 290 | } 291 | }, 292 | "fs.realpath": { 293 | "version": "1.0.0", 294 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 295 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 296 | "dev": true 297 | }, 298 | "fsevents": { 299 | "version": "2.1.2", 300 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", 301 | "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", 302 | "dev": true, 303 | "optional": true 304 | }, 305 | "function-bind": { 306 | "version": "1.1.1", 307 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 308 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 309 | "dev": true 310 | }, 311 | "get-caller-file": { 312 | "version": "2.0.5", 313 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 314 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 315 | "dev": true 316 | }, 317 | "glob": { 318 | "version": "7.1.3", 319 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 320 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 321 | "dev": true, 322 | "requires": { 323 | "fs.realpath": "^1.0.0", 324 | "inflight": "^1.0.4", 325 | "inherits": "2", 326 | "minimatch": "^3.0.4", 327 | "once": "^1.3.0", 328 | "path-is-absolute": "^1.0.0" 329 | } 330 | }, 331 | "glob-parent": { 332 | "version": "5.1.0", 333 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", 334 | "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", 335 | "dev": true, 336 | "requires": { 337 | "is-glob": "^4.0.1" 338 | } 339 | }, 340 | "growl": { 341 | "version": "1.10.5", 342 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 343 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 344 | "dev": true 345 | }, 346 | "has": { 347 | "version": "1.0.3", 348 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 349 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 350 | "dev": true, 351 | "requires": { 352 | "function-bind": "^1.1.1" 353 | } 354 | }, 355 | "has-flag": { 356 | "version": "3.0.0", 357 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 358 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 359 | "dev": true 360 | }, 361 | "has-symbols": { 362 | "version": "1.0.1", 363 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", 364 | "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", 365 | "dev": true 366 | }, 367 | "he": { 368 | "version": "1.2.0", 369 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 370 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 371 | "dev": true 372 | }, 373 | "inflight": { 374 | "version": "1.0.6", 375 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 376 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 377 | "dev": true, 378 | "requires": { 379 | "once": "^1.3.0", 380 | "wrappy": "1" 381 | } 382 | }, 383 | "inherits": { 384 | "version": "2.0.4", 385 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 386 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 387 | "dev": true 388 | }, 389 | "is-binary-path": { 390 | "version": "2.1.0", 391 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 392 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 393 | "dev": true, 394 | "requires": { 395 | "binary-extensions": "^2.0.0" 396 | } 397 | }, 398 | "is-buffer": { 399 | "version": "2.0.4", 400 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", 401 | "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", 402 | "dev": true 403 | }, 404 | "is-callable": { 405 | "version": "1.1.5", 406 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", 407 | "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", 408 | "dev": true 409 | }, 410 | "is-date-object": { 411 | "version": "1.0.2", 412 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", 413 | "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", 414 | "dev": true 415 | }, 416 | "is-extglob": { 417 | "version": "2.1.1", 418 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 419 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 420 | "dev": true 421 | }, 422 | "is-fullwidth-code-point": { 423 | "version": "2.0.0", 424 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 425 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 426 | "dev": true 427 | }, 428 | "is-glob": { 429 | "version": "4.0.1", 430 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 431 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 432 | "dev": true, 433 | "requires": { 434 | "is-extglob": "^2.1.1" 435 | } 436 | }, 437 | "is-number": { 438 | "version": "7.0.0", 439 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 440 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 441 | "dev": true 442 | }, 443 | "is-regex": { 444 | "version": "1.0.5", 445 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", 446 | "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", 447 | "dev": true, 448 | "requires": { 449 | "has": "^1.0.3" 450 | } 451 | }, 452 | "is-symbol": { 453 | "version": "1.0.3", 454 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", 455 | "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", 456 | "dev": true, 457 | "requires": { 458 | "has-symbols": "^1.0.1" 459 | } 460 | }, 461 | "isexe": { 462 | "version": "2.0.0", 463 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 464 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 465 | "dev": true 466 | }, 467 | "js-yaml": { 468 | "version": "3.13.1", 469 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", 470 | "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", 471 | "dev": true, 472 | "requires": { 473 | "argparse": "^1.0.7", 474 | "esprima": "^4.0.0" 475 | } 476 | }, 477 | "locate-path": { 478 | "version": "3.0.0", 479 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", 480 | "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", 481 | "dev": true, 482 | "requires": { 483 | "p-locate": "^3.0.0", 484 | "path-exists": "^3.0.0" 485 | } 486 | }, 487 | "lodash": { 488 | "version": "4.17.15", 489 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 490 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", 491 | "dev": true 492 | }, 493 | "log-symbols": { 494 | "version": "2.2.0", 495 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", 496 | "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", 497 | "dev": true, 498 | "requires": { 499 | "chalk": "^2.0.1" 500 | } 501 | }, 502 | "minimatch": { 503 | "version": "3.0.4", 504 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 505 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 506 | "dev": true, 507 | "requires": { 508 | "brace-expansion": "^1.1.7" 509 | } 510 | }, 511 | "minimist": { 512 | "version": "0.0.8", 513 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 514 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 515 | "dev": true 516 | }, 517 | "mkdirp": { 518 | "version": "0.5.1", 519 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 520 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 521 | "dev": true, 522 | "requires": { 523 | "minimist": "0.0.8" 524 | } 525 | }, 526 | "mocha": { 527 | "version": "7.0.1", 528 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.0.1.tgz", 529 | "integrity": "sha512-9eWmWTdHLXh72rGrdZjNbG3aa1/3NRPpul1z0D979QpEnFdCG0Q5tv834N+94QEN2cysfV72YocQ3fn87s70fg==", 530 | "dev": true, 531 | "requires": { 532 | "ansi-colors": "3.2.3", 533 | "browser-stdout": "1.3.1", 534 | "chokidar": "3.3.0", 535 | "debug": "3.2.6", 536 | "diff": "3.5.0", 537 | "escape-string-regexp": "1.0.5", 538 | "find-up": "3.0.0", 539 | "glob": "7.1.3", 540 | "growl": "1.10.5", 541 | "he": "1.2.0", 542 | "js-yaml": "3.13.1", 543 | "log-symbols": "2.2.0", 544 | "minimatch": "3.0.4", 545 | "mkdirp": "0.5.1", 546 | "ms": "2.1.1", 547 | "node-environment-flags": "1.0.6", 548 | "object.assign": "4.1.0", 549 | "strip-json-comments": "2.0.1", 550 | "supports-color": "6.0.0", 551 | "which": "1.3.1", 552 | "wide-align": "1.1.3", 553 | "yargs": "13.3.0", 554 | "yargs-parser": "13.1.1", 555 | "yargs-unparser": "1.6.0" 556 | } 557 | }, 558 | "ms": { 559 | "version": "2.1.1", 560 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 561 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", 562 | "dev": true 563 | }, 564 | "node-environment-flags": { 565 | "version": "1.0.6", 566 | "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", 567 | "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", 568 | "dev": true, 569 | "requires": { 570 | "object.getownpropertydescriptors": "^2.0.3", 571 | "semver": "^5.7.0" 572 | } 573 | }, 574 | "normalize-path": { 575 | "version": "3.0.0", 576 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 577 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 578 | "dev": true 579 | }, 580 | "object-inspect": { 581 | "version": "1.7.0", 582 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", 583 | "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", 584 | "dev": true 585 | }, 586 | "object-keys": { 587 | "version": "1.1.1", 588 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 589 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 590 | "dev": true 591 | }, 592 | "object.assign": { 593 | "version": "4.1.0", 594 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", 595 | "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", 596 | "dev": true, 597 | "requires": { 598 | "define-properties": "^1.1.2", 599 | "function-bind": "^1.1.1", 600 | "has-symbols": "^1.0.0", 601 | "object-keys": "^1.0.11" 602 | } 603 | }, 604 | "object.getownpropertydescriptors": { 605 | "version": "2.1.0", 606 | "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", 607 | "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", 608 | "dev": true, 609 | "requires": { 610 | "define-properties": "^1.1.3", 611 | "es-abstract": "^1.17.0-next.1" 612 | } 613 | }, 614 | "once": { 615 | "version": "1.4.0", 616 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 617 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 618 | "dev": true, 619 | "requires": { 620 | "wrappy": "1" 621 | } 622 | }, 623 | "p-limit": { 624 | "version": "2.2.2", 625 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", 626 | "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", 627 | "dev": true, 628 | "requires": { 629 | "p-try": "^2.0.0" 630 | } 631 | }, 632 | "p-locate": { 633 | "version": "3.0.0", 634 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", 635 | "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", 636 | "dev": true, 637 | "requires": { 638 | "p-limit": "^2.0.0" 639 | } 640 | }, 641 | "p-try": { 642 | "version": "2.2.0", 643 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 644 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 645 | "dev": true 646 | }, 647 | "path-exists": { 648 | "version": "3.0.0", 649 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 650 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 651 | "dev": true 652 | }, 653 | "path-is-absolute": { 654 | "version": "1.0.1", 655 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 656 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 657 | "dev": true 658 | }, 659 | "picomatch": { 660 | "version": "2.2.1", 661 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", 662 | "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", 663 | "dev": true 664 | }, 665 | "readdirp": { 666 | "version": "3.2.0", 667 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", 668 | "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", 669 | "dev": true, 670 | "requires": { 671 | "picomatch": "^2.0.4" 672 | } 673 | }, 674 | "require-directory": { 675 | "version": "2.1.1", 676 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 677 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", 678 | "dev": true 679 | }, 680 | "require-main-filename": { 681 | "version": "2.0.0", 682 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", 683 | "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", 684 | "dev": true 685 | }, 686 | "semver": { 687 | "version": "5.7.1", 688 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 689 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 690 | "dev": true 691 | }, 692 | "set-blocking": { 693 | "version": "2.0.0", 694 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 695 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", 696 | "dev": true 697 | }, 698 | "sprintf-js": { 699 | "version": "1.0.3", 700 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 701 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 702 | "dev": true 703 | }, 704 | "string-width": { 705 | "version": "2.1.1", 706 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 707 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 708 | "dev": true, 709 | "requires": { 710 | "is-fullwidth-code-point": "^2.0.0", 711 | "strip-ansi": "^4.0.0" 712 | } 713 | }, 714 | "string.prototype.trimleft": { 715 | "version": "2.1.1", 716 | "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", 717 | "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", 718 | "dev": true, 719 | "requires": { 720 | "define-properties": "^1.1.3", 721 | "function-bind": "^1.1.1" 722 | } 723 | }, 724 | "string.prototype.trimright": { 725 | "version": "2.1.1", 726 | "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", 727 | "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", 728 | "dev": true, 729 | "requires": { 730 | "define-properties": "^1.1.3", 731 | "function-bind": "^1.1.1" 732 | } 733 | }, 734 | "strip-ansi": { 735 | "version": "4.0.0", 736 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 737 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 738 | "dev": true, 739 | "requires": { 740 | "ansi-regex": "^3.0.0" 741 | } 742 | }, 743 | "strip-json-comments": { 744 | "version": "2.0.1", 745 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 746 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", 747 | "dev": true 748 | }, 749 | "supports-color": { 750 | "version": "6.0.0", 751 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", 752 | "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", 753 | "dev": true, 754 | "requires": { 755 | "has-flag": "^3.0.0" 756 | } 757 | }, 758 | "to-regex-range": { 759 | "version": "5.0.1", 760 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 761 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 762 | "dev": true, 763 | "requires": { 764 | "is-number": "^7.0.0" 765 | } 766 | }, 767 | "which": { 768 | "version": "1.3.1", 769 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 770 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 771 | "dev": true, 772 | "requires": { 773 | "isexe": "^2.0.0" 774 | } 775 | }, 776 | "which-module": { 777 | "version": "2.0.0", 778 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", 779 | "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", 780 | "dev": true 781 | }, 782 | "wide-align": { 783 | "version": "1.1.3", 784 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 785 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 786 | "dev": true, 787 | "requires": { 788 | "string-width": "^1.0.2 || 2" 789 | } 790 | }, 791 | "wrap-ansi": { 792 | "version": "5.1.0", 793 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", 794 | "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", 795 | "dev": true, 796 | "requires": { 797 | "ansi-styles": "^3.2.0", 798 | "string-width": "^3.0.0", 799 | "strip-ansi": "^5.0.0" 800 | }, 801 | "dependencies": { 802 | "ansi-regex": { 803 | "version": "4.1.0", 804 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 805 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 806 | "dev": true 807 | }, 808 | "string-width": { 809 | "version": "3.1.0", 810 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 811 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 812 | "dev": true, 813 | "requires": { 814 | "emoji-regex": "^7.0.1", 815 | "is-fullwidth-code-point": "^2.0.0", 816 | "strip-ansi": "^5.1.0" 817 | } 818 | }, 819 | "strip-ansi": { 820 | "version": "5.2.0", 821 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 822 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 823 | "dev": true, 824 | "requires": { 825 | "ansi-regex": "^4.1.0" 826 | } 827 | } 828 | } 829 | }, 830 | "wrappy": { 831 | "version": "1.0.2", 832 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 833 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 834 | "dev": true 835 | }, 836 | "y18n": { 837 | "version": "4.0.0", 838 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", 839 | "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", 840 | "dev": true 841 | }, 842 | "yargs": { 843 | "version": "13.3.0", 844 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", 845 | "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", 846 | "dev": true, 847 | "requires": { 848 | "cliui": "^5.0.0", 849 | "find-up": "^3.0.0", 850 | "get-caller-file": "^2.0.1", 851 | "require-directory": "^2.1.1", 852 | "require-main-filename": "^2.0.0", 853 | "set-blocking": "^2.0.0", 854 | "string-width": "^3.0.0", 855 | "which-module": "^2.0.0", 856 | "y18n": "^4.0.0", 857 | "yargs-parser": "^13.1.1" 858 | }, 859 | "dependencies": { 860 | "ansi-regex": { 861 | "version": "4.1.0", 862 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 863 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 864 | "dev": true 865 | }, 866 | "string-width": { 867 | "version": "3.1.0", 868 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 869 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 870 | "dev": true, 871 | "requires": { 872 | "emoji-regex": "^7.0.1", 873 | "is-fullwidth-code-point": "^2.0.0", 874 | "strip-ansi": "^5.1.0" 875 | } 876 | }, 877 | "strip-ansi": { 878 | "version": "5.2.0", 879 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 880 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 881 | "dev": true, 882 | "requires": { 883 | "ansi-regex": "^4.1.0" 884 | } 885 | } 886 | } 887 | }, 888 | "yargs-parser": { 889 | "version": "13.1.1", 890 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", 891 | "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", 892 | "dev": true, 893 | "requires": { 894 | "camelcase": "^5.0.0", 895 | "decamelize": "^1.2.0" 896 | } 897 | }, 898 | "yargs-unparser": { 899 | "version": "1.6.0", 900 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", 901 | "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", 902 | "dev": true, 903 | "requires": { 904 | "flat": "^4.1.0", 905 | "lodash": "^4.17.15", 906 | "yargs": "^13.3.0" 907 | } 908 | } 909 | } 910 | } 911 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bouncing-balls", 3 | "description": "Simple bouncing balls physics using plain JavaScript.", 4 | "keywords": [ 5 | "bouncing-balls", 6 | "physics-simulation", 7 | "javascript", 8 | "game-development", 9 | "canvas" 10 | ], 11 | "author": "Meto Trajkovski ", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/MTrajK/bouncing-balls.git" 15 | }, 16 | "scripts": { 17 | "test": "mocha" 18 | }, 19 | "devDependencies": { 20 | "mocha": "^7.0.1" 21 | }, 22 | "license": "MIT" 23 | } 24 | -------------------------------------------------------------------------------- /src/css/styles.css: -------------------------------------------------------------------------------- 1 | @media only screen and (max-width: 480px) { 2 | #dimensions { 3 | margin: 50px auto 0; 4 | width: 200px; 5 | height: 133px; 6 | } 7 | } 8 | 9 | @media only screen and (min-width: 481px) and (max-width: 630px) { 10 | #dimensions { 11 | margin: 50px auto 0; 12 | width: 400px; 13 | height: 266px; 14 | } 15 | } 16 | 17 | @media only screen and (min-width: 631px) and (max-width: 1500px) { 18 | #dimensions { 19 | margin: 75px auto 0; 20 | width: 600px; 21 | height: 399px; 22 | } 23 | } 24 | 25 | @media only screen and (min-width: 1501px) { 26 | #dimensions { 27 | margin: 100px auto 0; 28 | width: 900px; 29 | height: 600px; 30 | } 31 | } 32 | 33 | #canvas:active { 34 | cursor: -webkit-grab; 35 | cursor: grab; 36 | } 37 | 38 | #simulation { 39 | display: none; 40 | } 41 | 42 | #choices, #simulation { 43 | text-align: center; 44 | } 45 | 46 | #choices { 47 | margin-top: 100px; 48 | } 49 | 50 | #start, #back { 51 | font-size: 18px; 52 | padding: 5px 10px; 53 | font-weight: bold; 54 | margin-top: 50px; 55 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Bouncing Balls 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 | 23 | 24 |
25 |

Bouncing Balls Simulation


26 | 27 |

Direction:

28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 | 37 |

Collisions:

38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 |
46 | 47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/js/balls.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 'use strict'; 3 | 4 | /********************************************************* 5 | Notes about the physics in the simulations: 6 | The balls are equally hard (and have equal weight), so they don't lose energy when bouncing between themself. 7 | In the horizontal simulation, a ball loses energy when bouncing from a wall (the wall is harder and stationary) and air resistence. 8 | The ball also loses energy from the air resistence, hitting the ground, rolling on the ground and gravity in the vertical simulation 9 | (but not from spinning and some other 3d things possible in billiard and basketball). 10 | 11 | Known issue: 12 | In "vertical" space/direction when the bottom is full with balls (when there is no space for a new ball) 13 | adding a new ball will make all balls go crazy (jumping randomly). This is because the balls will always 14 | collide and won't lose energy from colliding (I'm not sure how to solve this). 15 | *********************************************************/ 16 | 17 | /************** 18 | * Ball class * 19 | ***************/ 20 | function Ball(position, velocity, radius, localDimensions) { 21 | // base constructor 22 | this.position = position; 23 | this.velocity = velocity; 24 | this.radius = radius; 25 | this._borderCoords = { 26 | top: radius, 27 | bottom: localDimensions.height - radius, 28 | left: radius, 29 | right: localDimensions.width - radius 30 | }; 31 | } 32 | 33 | function moveBallsOutOfCollision(ball1, ball2) { 34 | /********************************************************* 35 | Find the positions of the balls when the collision occurred. 36 | (because right they have collided - they're overlapping) 37 | 38 | old ball1.position = ball1.position - T * ball1.velocity 39 | old ball2.position = ball2.position - T * ball2.velocity 40 | 41 | In this moment T is unknown. Solve this equation to find T: 42 | distance(old ball1.position, old ball2.position) = (ball1.radius + ball2.radius) 43 | 44 | This can be solved using the Quadratic formula, because after simplifying 45 | the left side of the equation we'll get something like: a*(T^2) + b*T + c = 0 46 | *********************************************************/ 47 | var v = ball1.velocity.sub(ball2.velocity); 48 | var p = ball1.position.sub(ball2.position); 49 | var r = ball1.radius + ball2.radius; 50 | 51 | // quadratic formula coeficients 52 | var a = v.X*v.X + v.Y*v.Y; 53 | var b = (-2)*(p.X*v.X + p.Y*v.Y); 54 | var c = p.X*p.X + p.Y*p.Y - r*r; 55 | 56 | // quadratic formula discriminant 57 | var d = b*b - 4*a*c; 58 | 59 | // t1 and t2 from the quadratic formula (need only the positive solution) 60 | var t = (-b - Math.sqrt(d)) / (2*a); 61 | if (t < 0) 62 | t = (-b + Math.sqrt(d)) / (2*a); 63 | 64 | // calculate the old positions (positions when the collision occurred) 65 | var oldPosition1 = ball1.position.sub(ball1.velocity.mult(t)); 66 | var oldPosition2 = ball2.position.sub(ball2.velocity.mult(t)); 67 | 68 | var maxChange = ball1.radius * 3; 69 | 70 | if ((a == 0) || (d < 0) || 71 | (oldPosition1.distance(ball1.position) > maxChange) || 72 | (oldPosition2.distance(ball2.position) > maxChange)) { 73 | // 1) if 'a' is zero then both balls have equal velocities, no solution 74 | // 2) the discriminant shouldn't be negative in this simulation, but just in case check it 75 | // 3) the chages are too big, something is wrong 76 | 77 | if (ball1.position.distance(ball2.position) == 0) { 78 | // move only one ball up 79 | ball1.position = ball1.position.add(new Vector2D(0, -r)); 80 | } else { 81 | // move both balls using the vector between these positions 82 | var diff = (r - ball1.position.distance(ball2.position)) / 2; 83 | ball1.position = ball1.position.add(ball1.position.sub(ball2.position).tryNormalize().mult(diff)); 84 | ball2.position = ball2.position.add(ball2.position.sub(ball1.position).tryNormalize().mult(diff)); 85 | } 86 | } else { 87 | // use the old positions 88 | ball1.position = oldPosition1; 89 | ball2.position = oldPosition2; 90 | } 91 | } 92 | 93 | Ball.prototype.collision = function(ball) { 94 | if (this.position.distance(ball.position) <= ball.radius + this.radius) { 95 | moveBallsOutOfCollision(this, ball); 96 | 97 | var positionSub = this.position.sub(ball.position); 98 | var distance = positionSub.length(); 99 | 100 | /********************************************************* 101 | The formula could be found here: https://en.wikipedia.org/wiki/Elastic_collision 102 | velocityA -= (dot(velocityAB_sub, positionAB_sub) / distance^2) * positionAB_sub 103 | velocityB -= (dot(velocityBA_sub, positionBA_sub) / distance^2) * positionBA_sub 104 | but this thing (dot(velocityAB_sub, positionAB_sub) / distance^2) is same for 2 velocities 105 | because dot and length methods are commutative properties, and velocityAB_sub = -velocityBA_sub, same for positionSub 106 | *********************************************************/ 107 | var coeff = this.velocity.sub(ball.velocity).dot(positionSub) / (distance * distance); 108 | this.velocity = this.velocity.sub(positionSub.mult(coeff)); 109 | ball.velocity = ball.velocity.sub(positionSub.opposite().mult(coeff)); 110 | } 111 | } 112 | 113 | /************************ 114 | * HorizontalBall class * 115 | *************************/ 116 | var horizontalMovementProperties = { 117 | airResistance: 0.99, // slows down the speed in each frame 118 | hitResistance: 0.8, // slows down the speed when a wall is hitted 119 | velocityFactor: 0.2 // velocity factor (converts vector from the mouse dragging to this environment) 120 | }; 121 | 122 | function HorizontalBall(position, velocity, radius, localDimensions) { 123 | // HorizontalBall constructor 124 | // call the base constructor 125 | Ball.call(this, position, velocity.mult(horizontalMovementProperties.velocityFactor), radius, localDimensions); 126 | } 127 | 128 | // HorizontalBall inherits from the Ball class 129 | HorizontalBall.prototype = Object.create(Ball.prototype); 130 | HorizontalBall.prototype.constructor = HorizontalBall; // keep the constructor 131 | 132 | HorizontalBall.prototype.move = function() { 133 | if (this.velocity.isNearZero() && !this.velocity.isZero()) 134 | this.velocity = Vector2D.zero(); // the ball is staying in place 135 | 136 | // move the ball using the velocity 137 | this.position = this.position.add(this.velocity); 138 | 139 | if (this.position.X <= this._borderCoords.left || this.position.X >= this._borderCoords.right) { 140 | // move ball inside the borders 141 | this.position.X = (this.position.X <= this._borderCoords.left) ? 142 | this._borderCoords.left : this._borderCoords.right; 143 | 144 | // apply hit resistance 145 | this.velocity = this.velocity.mult(horizontalMovementProperties.hitResistance); 146 | 147 | // reflection angle is an inverse angle to the perpendicular axis to the wall (in this case the wall is Y axis) 148 | this.velocity.X = -this.velocity.X; 149 | } 150 | if (this.position.Y <= this._borderCoords.top || this.position.Y >= this._borderCoords.bottom) { 151 | // move ball inside the borders 152 | this.position.Y = (this.position.Y <= this._borderCoords.top) ? 153 | this._borderCoords.top : this._borderCoords.bottom; 154 | 155 | // apply hit resistance 156 | this.velocity = this.velocity.mult(horizontalMovementProperties.hitResistance); 157 | 158 | // reflection angle is an inverse angle to the perpendicular axis to the wall (in this case the wall is X axis) 159 | this.velocity.Y = -this.velocity.Y; 160 | } 161 | 162 | // apply air resistance 163 | this.velocity = this.velocity.mult(horizontalMovementProperties.airResistance); 164 | } 165 | 166 | /********************** 167 | * VerticalBall class * 168 | ***********************/ 169 | var verticalMovementProperties = { 170 | airResistance: 0.995, // slows down the speed in each frame 171 | hitResistance: 0.8, // slows down the Y speed when the surface is hitted 172 | rollingResistance: 0.98, // slows down the X speed when rolling on the ground 173 | gravity: 0.05, // pulls the ball to the ground in each frame 174 | velocityFactor: 0.07 // velocity factor (converts vector from the mouse dragging to this environment) 175 | }; 176 | 177 | function VerticalBall(position, velocity, radius, localDimensions) { 178 | // VerticalBall constructor 179 | // call the base constructor 180 | Ball.call(this, position, velocity.mult(verticalMovementProperties.velocityFactor), radius, localDimensions); 181 | } 182 | 183 | // VerticalBall inherits from the Ball class 184 | VerticalBall.prototype = Object.create(Ball.prototype); 185 | VerticalBall.prototype.constructor = VerticalBall; // keep the constructor 186 | 187 | VerticalBall.prototype.move = function() { 188 | if (this.velocity.isNearZero() && this.position.Y == this._borderCoords.bottom && !this.velocity.isZero()) 189 | this.velocity = Vector2D.zero(); // the ball is staying in place 190 | 191 | // move the ball using the velocity 192 | this.position = this.position.add(this.velocity); 193 | 194 | if (this.position.X <= this._borderCoords.left || this.position.X >= this._borderCoords.right) { 195 | // move ball inside the borders 196 | this.position.X = (this.position.X <= this._borderCoords.left) ? 197 | this._borderCoords.left : this._borderCoords.right; 198 | 199 | // reflection 200 | this.velocity.X = -this.velocity.X; 201 | } 202 | if (this.position.Y <= this._borderCoords.top || this.position.Y >= this._borderCoords.bottom) { 203 | // move ball inside the borders 204 | this.position.Y = (this.position.Y <= this._borderCoords.top) ? 205 | this._borderCoords.top : this._borderCoords.bottom; 206 | 207 | if (this.position.Y == this._borderCoords.bottom) { 208 | // when ball is on the ground, update resistances 209 | this.velocity.Y *= verticalMovementProperties.hitResistance; 210 | this.velocity.X *= verticalMovementProperties.rollingResistance; 211 | } 212 | 213 | // reflection 214 | this.velocity.Y = -this.velocity.Y; 215 | } 216 | 217 | // apply air resistance 218 | this.velocity = this.velocity.mult(verticalMovementProperties.airResistance); 219 | 220 | if (this.position.Y == this._borderCoords.bottom && Math.abs(this.velocity.Y) <= Vector2D.NEAR_ZERO) 221 | // the ball isn't falling or jumping 222 | this.velocity.Y = 0; 223 | else 224 | // apply gravity if falling or jumping 225 | this.velocity.Y += verticalMovementProperties.gravity; 226 | } 227 | 228 | /* Save these classes as global */ 229 | var Balls = { 230 | HorizontalBall: HorizontalBall, 231 | VerticalBall: VerticalBall 232 | }; 233 | 234 | // Establish the root object, `window` (`self`) in the browser, `global` 235 | // on the server, or `this` in some virtual machines. 236 | var root = typeof self == 'object' && self.self === self && self || 237 | typeof global == 'object' && global.global === global && global || 238 | this || {}; 239 | 240 | // Export the Balls object for **Node.js**, with 241 | // backwards-compatibility for their old module API. If we're in 242 | // the browser, add Balls as a global object. 243 | // (`nodeType` is checked to ensure that `module` 244 | // and `exports` are not HTML elements.) 245 | if (typeof exports != 'undefined' && !exports.nodeType) { 246 | if (typeof module != 'undefined' && !module.nodeType && module.exports) { 247 | exports = module.exports = Balls; 248 | } 249 | exports.Balls = Balls; 250 | } else { 251 | root.Balls = Balls; 252 | } 253 | 254 | }()); -------------------------------------------------------------------------------- /src/js/bouncing-balls.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | /************** 5 | ** CONSTANTS ** 6 | ***************/ 7 | var fps = 60; // Note: if you change this, you'll need to addapt gravity and resistance logic in ball.js 8 | var intervalMs = 1000 / fps; 9 | var localDimensions = { 10 | width: 100, // 1 localDimensions.width is 1 local unit 11 | height: 100 * (2/3) // the canvas ratio is always 3:2 12 | }; 13 | var ballProperties = { 14 | radius: 1, // local units 15 | startAngle: 0, 16 | endAngle: 2 * Math.PI, 17 | color: '#000000' 18 | }; 19 | var aimProperties = { 20 | shrink: 0.6, 21 | maxSpeed: 30, // local units 22 | headPart: 0.2, 23 | strokeAngle: Math.PI / 5, 24 | color: '#000000' 25 | }; 26 | 27 | /****************************************************************************************** 28 | ** PROPERTIES USED FOR COMUNICATION BETWEEN HELPERS, EVENTS, UPDATE AND PUBLIC FUNCTIONS ** 29 | *******************************************************************************************/ 30 | var updateInterval, canvas, context, canvasDimensions, isAiming, balls, 31 | ballType, enabledCollisions, mousePosition, newBallPosition, newBallDirection; 32 | 33 | /************ 34 | ** HELPERS ** 35 | *************/ 36 | function getCanvasDimensions() { 37 | return { 38 | width: canvasDimensions.offsetWidth, 39 | height: canvasDimensions.offsetHeight, 40 | top: canvasDimensions.offsetTop, 41 | left: canvasDimensions.offsetLeft, 42 | scaleRatio: canvasDimensions.offsetWidth / localDimensions.width 43 | } 44 | } 45 | 46 | function addNewBall() { 47 | isAiming = false; 48 | 49 | // save the new ball 50 | var newBall = new ballType( 51 | newBallPosition.clone(), 52 | newBallDirection.clone(), 53 | ballProperties.radius, 54 | localDimensions 55 | ); 56 | balls.push(newBall); 57 | 58 | // reset values 59 | newBallDirection = Vector2D.zero(); 60 | newBallPosition = new Vector2D(); 61 | } 62 | 63 | /************ 64 | ** DRAWING ** 65 | *************/ 66 | function drawCanvasBorder(dimensions) { 67 | context.strokeStyle = '#000000'; 68 | context.strokeRect(0, 0, dimensions.width, dimensions.height); 69 | } 70 | 71 | function drawBall(ballCoords, scaleRatio) { 72 | var scaledCoords = ballCoords.mult(scaleRatio); // convert the coordinates in CANVAS size 73 | 74 | context.beginPath(); 75 | context.arc(scaledCoords.X, scaledCoords.Y, ballProperties.radius * scaleRatio, // convert the radius too 76 | ballProperties.startAngle, ballProperties.endAngle); 77 | context.closePath(); 78 | 79 | context.fillStyle = ballProperties.color; 80 | context.fill(); 81 | } 82 | 83 | function drawAim(scaleRatio) { 84 | if (newBallDirection.isNearZero()) 85 | return; // no direction, the mouse is in the start position 86 | 87 | var directionLength = newBallDirection.length(); 88 | var radiusRatio = ballProperties.radius / directionLength; 89 | var scaledShrink = aimProperties.shrink * scaleRatio; 90 | 91 | // convert start and end points in CANVAS coordinates (using scaleRatio) 92 | // move the start point on the ball border (using the ball direction) 93 | // and adjust end point (using the start point) 94 | var startPoint = newBallPosition.add(newBallDirection.mult(radiusRatio)).mult(scaleRatio); 95 | var endPoint = startPoint.add(newBallDirection.mult(scaledShrink)); 96 | 97 | // calculate head strokes angle 98 | var headLength = directionLength * scaledShrink * aimProperties.headPart; 99 | var arrowAngle = newBallDirection.angle(); // angle between Y axis and the arrow direction 100 | var leftStrokeAngle = arrowAngle - aimProperties.strokeAngle; 101 | var rightStrokeAngle = arrowAngle + aimProperties.strokeAngle; 102 | 103 | context.beginPath(); 104 | // draw the body 105 | context.moveTo(startPoint.X, startPoint.Y); 106 | context.lineTo(endPoint.X, endPoint.Y); 107 | // draw the head strokes 108 | context.lineTo(endPoint.X - headLength * Math.sin(leftStrokeAngle), 109 | endPoint.Y - headLength * Math.cos(leftStrokeAngle)); 110 | context.moveTo(endPoint.X, endPoint.Y); 111 | context.lineTo(endPoint.X - headLength * Math.sin(rightStrokeAngle), 112 | endPoint.Y - headLength * Math.cos(rightStrokeAngle)); 113 | 114 | context.strokeStyle = aimProperties.color; 115 | context.stroke(); 116 | } 117 | 118 | /******************** 119 | ** EVENT LISTENERS ** 120 | *********************/ 121 | function onMouseMove(event) { 122 | if (isAiming) { 123 | var eventDoc, doc, body; 124 | event = event || window.event; // IE-ism 125 | 126 | // if pageX/Y aren't available and clientX/Y are, calculate pageX/Y - logic taken from jQuery. 127 | // (this is to support old IE) 128 | if (event.pageX == null && event.clientX != null) { 129 | eventDoc = (event.target && event.target.ownerDocument) || document; 130 | doc = eventDoc.documentElement; 131 | body = eventDoc.body; 132 | 133 | event.pageX = event.clientX + 134 | (doc && doc.scrollLeft || body && body.scrollLeft || 0) - 135 | (doc && doc.clientLeft || body && body.clientLeft || 0); 136 | event.pageY = event.clientY + 137 | (doc && doc.scrollTop || body && body.scrollTop || 0) - 138 | (doc && doc.clientTop || body && body.clientTop || 0 ); 139 | } 140 | 141 | // convert mouse coordinates to local coordinates 142 | var dimensions = getCanvasDimensions(); 143 | mousePosition = new Vector2D(event.pageX, event.pageY).convertToLocal(dimensions); 144 | if (newBallPosition.isUndefined()) 145 | newBallPosition = mousePosition.clone(); // start aiming 146 | 147 | // check where the pointer is located 148 | if (mousePosition.X <= 0 || mousePosition.X >= localDimensions.width 149 | || mousePosition.Y <= 0 || mousePosition.Y >= localDimensions.height) { 150 | addNewBall(); 151 | } else { 152 | // calculate aim direction 153 | newBallDirection = mousePosition.direction(newBallPosition); 154 | 155 | // directionLength shoud be smaller or equal to aimProperties.maxSpeed 156 | var directionLength = newBallDirection.length(); 157 | if (directionLength > aimProperties.maxSpeed) 158 | newBallDirection = newBallDirection.mult(aimProperties.maxSpeed / directionLength); 159 | } 160 | } 161 | } 162 | 163 | function onMouseDown(event) { 164 | // button=0 is left mouse click, button=1 is middle mouse click, button=2 is right mouse click 165 | if (event.button == 0) { 166 | isAiming = true; 167 | onMouseMove(event); // calculate the start position 168 | } else if (isAiming) { 169 | addNewBall(); 170 | } 171 | } 172 | 173 | function onMouseUp() { 174 | if (isAiming) 175 | addNewBall(); 176 | } 177 | 178 | function onTouchMove(event) { 179 | // isAiming will be true ONLY if 1 finger touches the screen 180 | onMouseMove(event.touches[0]); 181 | } 182 | 183 | function onTouchStart(event) { 184 | if (event.touches.length == 1) { 185 | event.touches[0].button = 0; // imitate a left mouse button click 186 | onMouseDown(event.touches[0]); 187 | } else { 188 | onMouseUp(); 189 | } 190 | event.preventDefault(); 191 | } 192 | 193 | function onTouchEnd() { 194 | onMouseUp(); 195 | event.preventDefault(); 196 | } 197 | 198 | /****************** 199 | ** MAIN FUNCTION ** 200 | *******************/ 201 | function update() { 202 | // check dimensions and clear canvas 203 | // the canvas is cleared when a new value is attached to dimensions (no matter if a same value) 204 | var dimensions = getCanvasDimensions(); 205 | canvas.width = dimensions.width; 206 | canvas.height = dimensions.height; 207 | 208 | // draw canvas border 209 | drawCanvasBorder(dimensions); 210 | 211 | // aiming mode 212 | if (isAiming) { 213 | // draw new ball 214 | drawBall(newBallPosition, dimensions.scaleRatio); 215 | // draw aim 216 | drawAim(dimensions.scaleRatio); 217 | } 218 | 219 | if (enabledCollisions) 220 | // check collisions and update positions & velocities 221 | // O(N^2) but this can be much faster, O(N*LogN) searching in quadtree structure, (or sort the points and check the closest O(N*LogN)) 222 | for (var i=0; i