├── .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 |
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