├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── background.png ├── games-background.png ├── package-lock.json ├── package.json ├── src ├── EngineObject.js ├── Point.js └── index.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | webpack.config.js -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Yaroslav Ivanov 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 | # Elpy.js - 2D JavaScript game engine. 2 | 3 | ![logo](background.png) 4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://github.com/space2pacman/elpy/blob/master/LICENSE.md) 6 | [![JavaScript](https://img.shields.io/badge/Language-JavaScript-brightgreen.svg)](https://en.wikipedia.org/wiki/JavaScript) 7 | [![Size](https://img.shields.io/bundlephobia/min/elpy?color=brightgreen&label=Size)](https://img.shields.io/bundlephobia/min/elpy?color=brightgreen&label=Size) 8 | [![Downloads](https://img.shields.io/npm/dt/elpy?label=Downloads)](https://img.shields.io/npm/dt/elpy?label=Downloads) 9 | [![npm](https://img.shields.io/npm/v/elpy?color=brightgreen)](https://www.npmjs.com/package/elpy) 10 | 11 | | [Demo](https://space2pacman-misc.github.io/elpy-examples/docs/) | [Game examples](https://space2pacman-misc.github.io/elpy-examples/docs/examples.html) | 12 | | :---: | :---: | 13 | 14 | ## Docs 15 | - Install 16 | - Basic usage example 17 | - Engine 18 | - create() 19 | - add() 20 | - key() 21 | - keydown() 22 | - keyup() 23 | - mousemove() 24 | - click() 25 | - tick() 26 | - nextTick() 27 | - checkObjectInViewport() 28 | - fixingCamera() 29 | - unfixingCamera() 30 | - destroy() 31 | - on() 32 | - Event `'load'` 33 | - Event `'animation'` 34 | - load() 35 | - Engine getters 36 | - Object 37 | - run() 38 | - move() 39 | - fly() 40 | - jump() 41 | - fall() 42 | - push() 43 | - rotate() 44 | - stop() 45 | - destroy() 46 | - collision() 47 | - on() 48 | - Event `'collision'` 49 | - Event `'move'` 50 | - Event `'rotate'` 51 | - Event `'destroy'` 52 | - Event `'state'` 53 | - Event `'jump'` 54 | - Event `'fall'` 55 | - Event `'fly'` 56 | - Event object 57 | - removeCollision() 58 | - Object getters 59 | - Object setters 60 | - Development 61 | - License 62 | 63 | ![games](games-background.png) 64 | 65 | ## Install 66 | #### Download 67 | Latest builds are available in the project [releases page](https://github.com/space2pacman/elpy/releases/latest). 68 | #### CDN 69 | ```js 70 | https://unpkg.com/elpy/dist/elpy.min.js 71 | ``` 72 | #### NPM 73 | ```js 74 | npm install elpy 75 | ``` 76 | ## Basic usage example 77 | ```html 78 | 79 | 80 | 81 | Elpy.js 82 | 83 | 84 | 85 | 86 | 110 | 111 | 112 | ``` 113 | ## Engine 114 | ### Create engine instance 115 | ```js 116 | const elpy = new Elpy( 117 | "#element", // id element canvas or HTML object element get by document.querySelector(). 118 | 500, // width. 119 | 500, // height. 120 | // options. 121 | { 122 | preload, // default - true, enable / disable preloader. 123 | favicon // default - true, enable / disable favicon. 124 | } 125 | ) 126 | ``` 127 | --- 128 | ### create(name, x, y, width, height, options) 129 | | name | type | description | 130 | | :---: | :---: | :--- | 131 | | **`name`** | `` | The object name must be unique. | 132 | | **`x`** | `` | Position of the object along the x-axis. | 133 | | **`y`** | `` | Position of the object along the y-axis. | 134 | | **`width`** | `` | Object width in pixels. | 135 | | **`height`** | `` | Object height in pixels. | 136 | | **`options`** | `` | Additional object parameters. | 137 | 138 | > Creates and returns an engine object. 139 | 140 | _min example_ 141 | ```js 142 | const player = elpy.create('player', 10, 10, 20, 20); 143 | ``` 144 | _max example_ 145 | ```js 146 | const player = elpy.create('player', 10, 10, 20, 20, { 147 | obstacle: true, // default - true. 148 | pushing: false, // default - false. 149 | disabledEvents: false, // default - false. 150 | type: 'player object', // default - null. 151 | custom: {}, // default - null. 152 | color, // default - 'black'. 153 | // image: '' or image: { path: '', repeat: false }. 154 | image: { 155 | path: '', // default - null. 156 | repeat: false // default - false. 157 | }, 158 | // default - null. 159 | images: [ 160 | { 161 | paths: ['images/player_left.png', 'images/player_right.png'], // path to image. 162 | state: 'move:left', // in what condition are the images available. player.state = 'move:left'. 163 | time: 100 // if player.animate = true - switching time between images. 164 | } 165 | ] 166 | }); 167 | ``` 168 | | name | type | description | 169 | | :---: | :---: | :--- | 170 | | **`obstacle`** | `` | To determine if an object is an obstacle if there is a collision with the object. If the object was not added to the collision then the object will pass through another object. If an object has been added to a collision, then by default the object will stop on collision. If it is necessary that the collision event occur and the object passes through the object, then the obstacle property can be switched to false. | 171 | | **`pushing`** | `` | Will the object move if it is pushed through the push method. | 172 | | **`disabledEvents`** | `` | Disables all events for an object. | 173 | | **`type`** | ``, `` | A simple string that allows you to add your own data. It is convenient to use to set the type of an object in order to distinguish them from each other later. | 174 | | **`custom`** | ``, `` | An object where you can add your fields and use them via `object.options.custorm`. | 175 | | **`color`** | `` | Set object color. | 176 | | **`image`** | ``, `` | Set image. Two data types can be used: String or Object.
`image: 'path/to/image/'`
or for repeat image
`image: { path: 'path/to/image/', repeat: true }`. | 177 | | **`images`** | `` | Can be used if the object has several images that can be changed through the state. For example, the image of the position when the player goes to the right or left. You can also make animation of switching frames through the animate property (Object getters). The switching `time` is set in the time property. | 178 | --- 179 | ### add(object) 180 | | name | type | 181 | | :---: | :---: | 182 | | **`object`** | `` | 183 | 184 | > Add an object to the engine. 185 | 186 | ```js 187 | elpy.add(player); 188 | ``` 189 | --- 190 | ### key(callback) 191 | | name | type | 192 | | :---: | :---: | 193 | | **`callback`** | `` | 194 | 195 | > Listen for keydown and keyup events. Сallback is always called when a key is pressed. 196 | 197 | ```js 198 | elpy.key(key => { 199 | if (key === 'ArrowUp') { 200 | // ... 201 | } 202 | }); 203 | ``` 204 | --- 205 | ### keydown(callback) 206 | | name | type | 207 | | :---: | :---: | 208 | | **`callback`** | `` | 209 | 210 | > Сallback fires once on click. 211 | 212 | ```js 213 | elpy.keydown(key => { 214 | if (key === 'ArrowUp') { 215 | // ... 216 | } 217 | }); 218 | ``` 219 | --- 220 | ### keyup(callback) 221 | | name | type | 222 | | :---: | :---: | 223 | | **`callback`** | `` | 224 | 225 | > Сallback fires once on click. 226 | 227 | ```js 228 | elpy.keyup(key => { 229 | if (key === 'ArrowUp') { 230 | // ... 231 | } 232 | }); 233 | ``` 234 | --- 235 | ### mousemove(callback) 236 | | name | type | 237 | | :---: | :---: | 238 | | **`callback`** | `` | 239 | 240 | > Handles mouse movement on the canvas. 241 | 242 | ```js 243 | elpy.mousemove((x, y) => { 244 | // x, y - coordinates inside the canvas. 245 | }); 246 | ``` 247 | --- 248 | ### click(callback) 249 | | name | type | 250 | | :---: | :---: | 251 | | **`callback`** | `` | 252 | 253 | > Handles mouse click on the canvas. 254 | 255 | ```js 256 | elpy.click((x, y) => { 257 | // x, y - coordinates inside the canvas. 258 | }); 259 | ``` 260 | --- 261 | ### tick(callback) 262 | | name | type | 263 | | :---: | :---: | 264 | | **`callback`** | `` | 265 | 266 | > Called recursively. The next call is queued after the scene is updated (render). To cancel the call inside the callback, return false. 267 | 268 | ```js 269 | let delta = 0; 270 | // tick will be called 100 times. 271 | elpy.tick(() => { 272 | if (delta === 100) { 273 | return false; // stop tick. 274 | } 275 | 276 | delta++; 277 | 278 | player.move(player.x + delta, player.y + delta); 279 | }); 280 | ``` 281 | --- 282 | ### nextTick(callback) 283 | | name | type | 284 | | :---: | :---: | 285 | | **`callback`** | `` | 286 | 287 | > Adds a callback to the queue after the scene is updated (rendered) and calls callback once. 288 | 289 | ```js 290 | elpy.nextTick(() => { 291 | // ... 292 | }); 293 | ``` 294 | --- 295 | ### checkObjectInViewport(object) 296 | | name | type | 297 | | :---: | :---: | 298 | | **`object`** | `` | 299 | 300 | > Checking if the object is in the visible area. 301 | 302 | ```js 303 | elpy.checkObjectInViewport(player); // returns true or false. 304 | ``` 305 | --- 306 | ### fixingCamera(object, fixedCamera) 307 | | name | type | 308 | | :---: | :---: | 309 | | **`object`** | `` | 310 | | **`fixedCamera`** | `` | 311 | 312 | > Fix the camera behind the object. You can fix both in one coordinate and in two. 313 | 314 | _min example_ 315 | ```js 316 | elpy.fixingCamera(player, { 317 | x: true 318 | }); 319 | ``` 320 | _max example_ 321 | ```js 322 | elpy.fixingCamera(player, { 323 | x: true, 324 | y: true 325 | }); 326 | ``` 327 | --- 328 | ### unfixingCamera() 329 | > Unfix the camera from the one previously fixed behind the object. 330 | 331 | ```js 332 | elpy.unfixingCamera(); 333 | ``` 334 | --- 335 | ### destroy() 336 | > Destroying all objects and stopping rendering. 337 | 338 | ```js 339 | elpy.destroy(); 340 | ``` 341 | --- 342 | ### on(event, callback) 343 | | name | type | 344 | | :---: | :---: | 345 | | **`event`** | `` | 346 | | **`callback`** | `` | 347 | 348 | > Listen to the engine event. 349 | 350 | ```js 351 | elpy.on('eventName', () => { 352 | // event handling. 353 | }); 354 | ``` 355 | #### Event: `'load'` 356 | 357 | > Called when the application has loaded all resources and is ready. 358 | 359 | ```js 360 | elpy.on('load', () => { 361 | // event handling. 362 | }); 363 | ``` 364 | #### Event: `'animation'` 365 | | name | type | 366 | | :---: | :---: | 367 | | object | `` | 368 | | image | `` | 369 | | images | `` | 370 | 371 | > Called when an object images are switched. 372 | 373 | ```js 374 | elpy.on('animation', (object, image, images) => { 375 | // object - object to be animated. 376 | // image - current image. Examlpe: 'images/player_left.png'. 377 | // images - a list of all images that were passed to paths when the object was created. Example: ['images/player_left.png', 'images/player_right.png']. 378 | }); 379 | ``` 380 | --- 381 | ### load() 382 | > The method is called automatically when `document.readyState === 'complete'`. If the engine has not loaded and is in a black window state, you can call load manually after all operations with the engine (creating objects, adding objects, etc.). 383 | 384 | ```js 385 | elpy.load(); 386 | ``` 387 | ## Engine getters 388 | | name | type | description | 389 | | :---: | :---: | :--- | 390 | | width | `` | Returns the width of the canvas. | 391 | | height | `` | Returns the height of the canvas. | 392 | | offset | `` | Returns information on object and field offset. | 393 | | objects | `` | Returns all added objects. | 394 | 395 | ```js 396 | elpy.width; 397 | ``` 398 | ## Object 399 | ### run(step) 400 | | name | type | default | 401 | | :---: | :---: | :---: | 402 | | **`step`** | `` | `1` | 403 | 404 | > Vector movement. Moves in different directions depending on positive or negative values. 405 | 406 | _min example_ 407 | ```js 408 | player.run(); 409 | ``` 410 | _max example_ 411 | ```js 412 | player.run(-1); 413 | ``` 414 | --- 415 | ### move(x, y) 416 | | name | type | 417 | | :---: | :---: | 418 | | **`x`** | `` | 419 | | **`y`** | `` | 420 | 421 | > Move at coordinates. 422 | 423 | ```js 424 | player.move(10, 10); 425 | ``` 426 | --- 427 | ### fly(degrees, distance, step) 428 | | name | type | default | 429 | | :---: | :---: | :---: | 430 | | **`degrees`** | `` | `0` | 431 | | **`distance`** | `` | `0` | 432 | | **`step`** | `` | `1` | 433 | 434 | > Vector flight. 435 | 436 | _min example_ 437 | ```js 438 | player.fly(0); 439 | ``` 440 | _max example_ 441 | ```js 442 | player.fly(0, 100, 10); 443 | ``` 444 | --- 445 | ### jump(height, multiplier, forced) 446 | | name | type | default | 447 | | :---: | :---: | :---: | 448 | | **`height`** | `` | `0` | 449 | | **`multiplier`** | `` | `0.1` | 450 | | **`forced`** | `` | `false` | 451 | 452 | > Object jumps with further fall. 453 | 454 | _min example_ 455 | ```js 456 | player.jump(10); 457 | ``` 458 | _max example_ 459 | ```js 460 | player.jump(10, 0.5, true); 461 | ``` 462 | --- 463 | ### fall(multiplier) 464 | | name | type | default | 465 | | :---: | :---: | :---: | 466 | | **`multiplier`** | `` | `0.1` | 467 | 468 | > Free fall. 469 | 470 | _min example_ 471 | ```js 472 | player.fall(); 473 | ``` 474 | _max example_ 475 | ```js 476 | player.fall(0.5); 477 | ``` 478 | --- 479 | ### push(object) 480 | | name | type | 481 | | :---: | :---: | 482 | | **`object`** | `` | 483 | 484 | > Pushing an object. The one who pushes must have a collision with what he pushes. 485 | 486 | ```js 487 | player.push(object); 488 | ``` 489 | --- 490 | ### rotate(degrees, x, y) 491 | | name | type | default | 492 | | :---: | :---: | :---: | 493 | | **`degrees`** | `` | `0` | 494 | | **`x`** | `` | `0` | 495 | | **`y`** | `` | `0` | 496 | 497 | > Object rotation. 498 | 499 | _min example_ 500 | ```js 501 | player.rotate(90); 502 | ``` 503 | _max example_ 504 | ```js 505 | player.rotote(90, 10, 20); 506 | ``` 507 | --- 508 | ### stop() 509 | 510 | > Stop actions: jump, fall, fly. 511 | 512 | ```js 513 | player.stop(); 514 | ``` 515 | --- 516 | ### destroy() 517 | 518 | > Destroy object. 519 | 520 | ```js 521 | player.destroy(); 522 | ``` 523 | --- 524 | ### collision(object) 525 | | name | type | 526 | | :---: | :---: | 527 | | **`object`** | ``, `` | 528 | 529 | > Add collision object. 530 | 531 | ```js 532 | player.collision(object); 533 | ``` 534 | --- 535 | ### on(event, callback) 536 | | name | type | 537 | | :---: | :---: | 538 | | **`event`** | `` | 539 | | **`callback`** | `` | 540 | 541 | > Listen to the object event. 542 | 543 | ```js 544 | player.on('eventName', () => { 545 | // event handling. 546 | }); 547 | ``` 548 | #### Event: `'collision'` 549 | | name | type | 550 | | :---: | :---: | 551 | | **`object`** | `` | 552 | | **`side`** | `` | 553 | 554 | > Called in the collision of the object. 555 | 556 | ```js 557 | player.on('collision', (object, side) => { 558 | // object - collision object. 559 | // side - side of the object that was collided. 560 | }); 561 | ``` 562 | #### Event: `'move'` 563 | 564 | > Called when moving the object. 565 | 566 | ```js 567 | player.on('move', () => { 568 | // event handling. 569 | }); 570 | ``` 571 | #### Event: `'rotate'` 572 | 573 | > Called when rotating the object. 574 | 575 | ```js 576 | player.on('rotate', () => { 577 | // event handling. 578 | }); 579 | ``` 580 | #### Event: `'destroy'` 581 | 582 | > Called when the object is destroyed. 583 | 584 | ```js 585 | player.on('destroy', () => { 586 | // event handling. 587 | }); 588 | ``` 589 | #### Event: `'state'` 590 | 591 | > Called when changing the condition of the object. 592 | 593 | ```js 594 | player.on('state', () => { 595 | // event handling. 596 | }); 597 | ``` 598 | #### Event: `'jump'` 599 | | name | type | description | 600 | | :---: | :---: | :--: | 601 | | **`event`** | `` | link | 602 | 603 | > Called when jumping an object. 604 | 605 | ```js 606 | player.on('jump', event => { 607 | // event - event object. 608 | }); 609 | ``` 610 | #### Event: `'fall'` 611 | | name | type | description | 612 | | :---: | :---: | :--: | 613 | | **`event`** | `` | link | 614 | 615 | > Called when the object falls. 616 | 617 | ```js 618 | player.on('fall', event => { 619 | // event - event object. 620 | }); 621 | ``` 622 | #### Event: `'fly'` 623 | | name | type | description | 624 | | :---: | :---: | :--: | 625 | | **`event`** | `` | link | 626 | 627 | > Called when the object flies. 628 | 629 | ```js 630 | player.on('fly', event => { 631 | // event - event object. 632 | }); 633 | ``` 634 | #### Event object 635 | | name | type | description | 636 | | :---: | :---: | :--- | 637 | | `stopped` | `` | The property will be `true` if the object has been stopped. | 638 | | `paused` | `` | The property will be `true` if the object has been paused. | 639 | | `stop()` | `` | Stop object. If he was in a state: `jump`, `fall`, `fly`. | 640 | | `pause()` | `` | Pause object. If he was in a state: `jump`, `fall`, `fly`. | 641 | | `resume()` | `` | Resume object. If it was paused. | 642 | 643 | > The object that is returned in the event callback: `jump`, `fall`, `fly`. 644 | 645 | ```js 646 | player.on('jump', event => { 647 | event.stopped; // returns true or false. 648 | event.paused; // returns true or false. 649 | event.stop(); 650 | event.pause(); 651 | event.resume(); 652 | }); 653 | ``` 654 | --- 655 | ### removeCollision(object) 656 | | name | type | 657 | | :---: | :---: | 658 | | **`object`** | `` | 659 | 660 | > Remove collision object from collision list. 661 | 662 | ```js 663 | player.removeCollision(object); 664 | ``` 665 | ## Object getters 666 | | name | type | description | 667 | | :---: | :---: | :--- | 668 | | name | `` | Returns the name of the object. | 669 | | options | `` | Returns object options. | 670 | | track | `` | Returns the object's previous moves. | 671 | | dest | `` | Returns the coordinates where the object is moving. | 672 | | offset | `` | Returns the offset coordinates of an object. | 673 | | isPushing | `` | Is it possible to push an object. | 674 | | isJumping | `` | Is the object in a jump. | 675 | | isFlying | `` | Is the object in fly. | 676 | | isExist | `` | Does the object exist. | 677 | | x | `` | Position of the object along the x-axis. | 678 | | y | `` | Position of the object along the y-axis. | 679 | | width | `` | Object width in pixels. | 680 | | height | `` | Object height in pixels. | 681 | | state | `` | Returns the current state of the object. | 682 | | animate | `` | Is the object animated. | 683 | | ghost | `` | Whether collision is canceled with other objects that go to it. | 684 | | degrees | `` | The degrees the object is rotated. | 685 | | added | `` | Is the object added. | 686 | 687 | ```js 688 | player.name; 689 | ``` 690 | ## Object setters 691 | | name | type | description | 692 | | :---: | :---: | :--- | 693 | | x | `` | Position of the object along the x-axis. | 694 | | y | `` | Position of the object along the y-axis. | 695 | | width | `` | Object width in pixels. | 696 | | height | `` | Object height in pixels. | 697 | | state | `` | Used to switch textures. | 698 | | animate | `` | Whether to animate the object. | 699 | | ghost | `` | Cancels collision with other objects that go to it. | 700 | | added | `` | Is the object added. | 701 | 702 | ```js 703 | player.width = 10; 704 | ``` 705 | ## Development 706 | ``` 707 | npm run serve 708 | ``` 709 | ## License 710 | [MIT](https://github.com/space2pacman/elpy/blob/master/LICENSE.md) 711 | -------------------------------------------------------------------------------- /background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space2pacman/elpy/232d9072fb61a1886995763ce74fefd9b0c2ba5c/background.png -------------------------------------------------------------------------------- /games-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/space2pacman/elpy/232d9072fb61a1886995763ce74fefd9b0c2ba5c/games-background.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elpy", 3 | "version": "1.1.16", 4 | "description": "2D JavaScript game engine.", 5 | "author": "space2pacman", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/space2pacman/elpy.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/space2pacman/elpy/issues" 13 | }, 14 | "keywords": [ 15 | "html5", 16 | "game", 17 | "rendering", 18 | "engine", 19 | "2d", 20 | "canvas", 21 | "web", 22 | "javascript" 23 | ], 24 | "main": "src/index.js", 25 | "scripts": { 26 | "build": "webpack", 27 | "serve": "webpack serve" 28 | }, 29 | "devDependencies": { 30 | "webpack": "^5.64.2", 31 | "webpack-cli": "^4.9.1", 32 | "webpack-dev-server": "^4.10.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/EngineObject.js: -------------------------------------------------------------------------------- 1 | const point = require('./Point'); 2 | 3 | class EngineObject { 4 | constructor(name, x, y, width, height, options = {}) { 5 | this._name = name; 6 | this._x = x; 7 | this._y = y; 8 | this._width = width; 9 | this._height = height; 10 | this._events = {}; 11 | this._collision = {}; 12 | this._isJumping = false; 13 | this._isFalling = false; 14 | this._isFlying = false; 15 | this._isStopped = false; 16 | this._state = null; 17 | this._ghost = false; 18 | this._animate = false; 19 | this._added = false, 20 | this._exist = true; 21 | this._track = { 22 | x: null, 23 | y: null 24 | }; 25 | this._dest = { 26 | x: null, 27 | y: null 28 | }; 29 | this._offset = { 30 | x: 0, 31 | y: 0, 32 | object: null, 33 | rotate: { 34 | x: 0, 35 | y: 0 36 | } 37 | }; 38 | this._positions = { 39 | start: { 40 | x: null, 41 | y: null 42 | } 43 | } 44 | this._degrees = 0; 45 | this._options = { 46 | obstacle: typeof options.obstacle === 'boolean' ? options.obstacle : true, 47 | pushing: typeof options.pushing === 'boolean' ? options.pushing : false, 48 | disabledEvents: typeof options.disabledEvents === 'boolean' ? options.disabledEvents : false, 49 | type: options.type || null, 50 | custom: options.custom || null, 51 | color: options.color || 'black', 52 | image: { 53 | path: (typeof options.image === 'object' && options.image !== null ? options.image.path : options.image) || null, 54 | repeat: (typeof options.image === 'object' && options.image !== null ? options.image.repeat : false) || false, 55 | rendering: false, 56 | cached: null 57 | }, 58 | images: { 59 | list: options.images || null, 60 | rendering: false, 61 | cached: {} 62 | }, 63 | fixedCamera: { 64 | x: false, 65 | y: false 66 | } 67 | }; 68 | this._params = { 69 | movement: { 70 | acceleration: 0 71 | }, 72 | jump: { 73 | multiplier: 0 74 | }, 75 | fall: { 76 | multiplier: 0 77 | } 78 | } 79 | this._MAX_ACCELERATION = 10; 80 | 81 | this._init(); 82 | } 83 | 84 | run(step = 1) { 85 | const x = this._x + Math.cos((this._degrees + 90) * Math.PI / 180) * step; 86 | const y = this._y + Math.sin((this._degrees + 90) * Math.PI / 180) * step; 87 | 88 | this.move(x, y); 89 | } 90 | 91 | move(x, y) { 92 | this._dest.x = x; 93 | this._dest.y = y; 94 | this._track.x = this._x; 95 | this._track.y = this._y; 96 | 97 | for(const name in this._collision) { 98 | const object = this._collision[name]; 99 | 100 | if (this !== object 101 | && this.isExist 102 | && object.isExist 103 | && !object.ghost 104 | && (x + this._width) > object.x 105 | && x < (object.x + object.width) 106 | && (y + this._height) > object.y 107 | && y < (object.y + object.height)) { 108 | 109 | const side = this._getCollisionSide(x, y, object); 110 | 111 | this._dispatchEvent('collision', object, side); 112 | 113 | if (object.options.obstacle) { 114 | return false; 115 | } 116 | } 117 | } 118 | 119 | if (this._options.fixedCamera.x && x > this._track.x) { 120 | this._offset.x += Math.abs(this._track.x - x); 121 | } 122 | 123 | if (this._options.fixedCamera.x && x < this._track.x) { 124 | this._offset.x -= Math.abs(this._track.x - x); 125 | } 126 | 127 | if (this._options.fixedCamera.y && y > this._track.y) { 128 | this._offset.y += Math.abs(this._track.y - y); 129 | } 130 | 131 | if (this._options.fixedCamera.y && y < this._track.y) { 132 | this._offset.y -= Math.abs(this._track.y - y); 133 | } 134 | 135 | this._x = x; 136 | this._y = y; 137 | 138 | this._dispatchEvent('move'); 139 | } 140 | 141 | fly(degrees = 0, distance = 0, step = 1) { 142 | const event = this._getEventObject(); 143 | 144 | this._positions.start.x = this._x; 145 | this._positions.start.y = this._y; 146 | 147 | this._nextTick(() => { 148 | this._isStopped = false; 149 | this._isFlying = true; 150 | this._tick(this._onFly.bind(this, event, degrees, distance, step)); 151 | }); 152 | } 153 | 154 | jump(height = 0, multiplier = 0.1, forced = false) { 155 | const event = this._getEventObject(); 156 | 157 | if (forced) { 158 | this._isJumping = false; 159 | } 160 | 161 | if (this._isJumping) { 162 | return false; 163 | } 164 | 165 | this._isFalling = false; 166 | this._isJumping = true; 167 | this._params.movement.acceleration = this._getMaxJumpAccelerationValue(height, multiplier); 168 | this._params.jump.multiplier = multiplier; 169 | 170 | this._nextTick(() => { 171 | this._isStopped = false; 172 | this._tick(this._onJump.bind(this, event)); 173 | }); 174 | } 175 | 176 | fall(multiplier = 0.1) { 177 | const event = this._getEventObject(); 178 | 179 | this._isFalling = true; 180 | this._params.fall.multiplier = multiplier; 181 | 182 | this._nextTick(() => { 183 | this._isStopped = false; 184 | this._tick(this._onFall.bind(this, event)); 185 | }); 186 | } 187 | 188 | push(pusher) { 189 | let direction; 190 | let distance; 191 | 192 | if (pusher.dest.y < pusher.y) { 193 | direction = 'up'; 194 | distance = Math.abs(pusher.dest.y - pusher.y); 195 | } 196 | 197 | if (pusher.dest.y > pusher.y) { 198 | direction = 'down'; 199 | distance = Math.abs(pusher.dest.y - pusher.y); 200 | } 201 | 202 | if (pusher.dest.x > pusher.x) { 203 | direction = 'right'; 204 | distance = Math.abs(pusher.dest.x - pusher.x); 205 | } 206 | 207 | if (pusher.dest.x < pusher.x) { 208 | direction = 'left'; 209 | distance = Math.abs(pusher.dest.x - pusher.x); 210 | } 211 | 212 | switch (direction) { 213 | case 'up': 214 | this.move(this.x, this.y - distance); 215 | 216 | if (this.track.y !== this.y) { 217 | pusher.move(pusher.x, pusher.y - distance); 218 | } 219 | 220 | break; 221 | case 'down': 222 | this.move(this.x, this.y + distance); 223 | 224 | if (this.track.y !== this.y) { 225 | pusher.move(pusher.x, pusher.y + distance); 226 | } 227 | 228 | break; 229 | case 'right': 230 | this.move(this.x + distance, this.y); 231 | 232 | if (this.track.x !== this.x) { 233 | pusher.move(pusher.x + distance, pusher.y); 234 | } 235 | 236 | break; 237 | case 'left': 238 | this.move(this.x - distance, this.y); 239 | 240 | if (this.track.x !== this.x) { 241 | pusher.move(pusher.x - distance, pusher.y); 242 | } 243 | 244 | break; 245 | } 246 | } 247 | 248 | rotate(degrees = 0, x = 0, y = 0) { 249 | this._offset.rotate.x = x; 250 | this._offset.rotate.y = y; 251 | this._degrees = degrees; 252 | 253 | this._dispatchEvent('rotate'); 254 | } 255 | 256 | stop() { 257 | this._isStopped = true; 258 | } 259 | 260 | destroy() { 261 | delete this._collision[this._name]; 262 | this._exist = false; 263 | 264 | this._dispatchEvent('destroy'); 265 | } 266 | 267 | collision(object) { 268 | if (Array.isArray(object)) { 269 | object.forEach(item => { 270 | if (item.isExist) { 271 | this._collision[item.name] = item; 272 | } 273 | }); 274 | } else { 275 | if (object.isExist) { 276 | this._collision[object.name] = object; 277 | } 278 | } 279 | } 280 | 281 | on(name, callback) { 282 | if (!this._events[name]) { 283 | this._events[name] = []; 284 | } 285 | 286 | this._events[name].push(callback); 287 | } 288 | 289 | setOffsetObject(object) { 290 | this._offset.object = object; 291 | } 292 | 293 | removeCollision(object) { 294 | delete this._collision[object.name]; 295 | } 296 | 297 | get name() { 298 | return this._name; 299 | } 300 | 301 | get options() { 302 | return this._options; 303 | } 304 | 305 | get obstacles() { 306 | return Object.values(this._collision); 307 | } 308 | 309 | get track() { 310 | return this._track; 311 | } 312 | 313 | get dest() { 314 | return this._dest; 315 | } 316 | 317 | get offset() { 318 | return this._offset; 319 | } 320 | 321 | get isPushing() { 322 | return this._options.pushing; 323 | } 324 | 325 | get isJumping() { 326 | return this._isJumping; 327 | } 328 | 329 | get isFalling() { 330 | return this._isFalling; 331 | } 332 | 333 | get isFlying() { 334 | return this._isFlying; 335 | } 336 | 337 | get isExist() { 338 | return this._exist; 339 | } 340 | 341 | get x() { 342 | return this._x; 343 | } 344 | 345 | set x(value) { 346 | this._x = value; 347 | } 348 | 349 | get y() { 350 | return this._y; 351 | } 352 | 353 | set y(value) { 354 | this._y = value; 355 | } 356 | 357 | get width() { 358 | return this._width; 359 | } 360 | 361 | set width(value) { 362 | return this._width = value; 363 | } 364 | 365 | get height() { 366 | return this._height; 367 | } 368 | 369 | set height(value) { 370 | return this._height = value; 371 | } 372 | 373 | get state() { 374 | return this._state; 375 | } 376 | 377 | set state(state) { 378 | this._state = state; 379 | 380 | this._dispatchEvent('state'); 381 | } 382 | 383 | get animate() { 384 | return this._animate; 385 | } 386 | 387 | set animate(value) { 388 | if (value) { 389 | if (Array.isArray(this.options.images.list) && this.options.images.list.length > 0) { 390 | requestAnimationFrame(this._animation.bind(this)); 391 | } 392 | } 393 | 394 | this._animate = value; 395 | } 396 | 397 | 398 | get ghost() { 399 | return this._ghost; 400 | } 401 | 402 | set ghost(value) { 403 | this._ghost = value; 404 | } 405 | 406 | get degrees() { 407 | return this._degrees; 408 | } 409 | 410 | get added() { 411 | return this._added; 412 | } 413 | 414 | set added(value) { 415 | return this._added = value; 416 | } 417 | 418 | get _isAccelerationMovementStopped() { 419 | return this._params.movement.acceleration <= 0; 420 | } 421 | 422 | _dispatchEvent(name, ...data) { 423 | if (this._events[name] && Array.isArray(this._events[name]) && this._events[name].length > 0) { 424 | this._events[name].forEach(callback => { 425 | callback(...data); 426 | }); 427 | } 428 | } 429 | 430 | _animation() { 431 | if (this._animate) { 432 | this._dispatchEvent('state'); 433 | 434 | requestAnimationFrame(this._animation.bind(this)); 435 | } 436 | } 437 | 438 | _takeoff() { 439 | const acceleration = Math.floor(this._params.movement.acceleration * 10); 440 | const multiplier = this._params.jump.multiplier * 10; 441 | 442 | this._params.movement.acceleration = (acceleration - multiplier) / 10; 443 | 444 | this.move(this._x, Math.floor(this._y - this._params.movement.acceleration)); 445 | } 446 | 447 | _landing() { 448 | if (this._params.movement.acceleration <= this._MAX_ACCELERATION) { 449 | const acceleration = Math.floor(this._params.movement.acceleration * 10); 450 | const multiplier = this._params.fall.multiplier * 10; 451 | 452 | this._params.movement.acceleration = (acceleration + multiplier) / 10; 453 | } 454 | 455 | const moving = this.move(this._x, Math.floor(this._y + this._params.movement.acceleration)); 456 | 457 | if (moving === false) { 458 | this._params.movement.acceleration = 0; 459 | 460 | return true; 461 | } 462 | } 463 | 464 | _getCollisionSide(x, y, object) { 465 | let side = null; 466 | 467 | if ((x + this._width) > object.x 468 | && x < (object.x + object.width) 469 | && ((this._y + this._height) <= object.y || this._y >= (object.y + object.height))) { 470 | const top = object.y - (y + this._height); 471 | const bottom = y - (object.y + object.height); 472 | 473 | if (top > bottom) { 474 | side = 'top'; 475 | } else { 476 | side = 'bottom'; 477 | } 478 | } 479 | 480 | if ((y + this._height) > object.y 481 | && y < (object.y + object.height) 482 | && ((this._x + this._width) <= object.x || this._x >= (object.x + object.width))) { 483 | const left = object.x - (x + this._width); 484 | const right = x - (object.x + object.width); 485 | 486 | if (left > right) { 487 | side = 'left'; 488 | } else { 489 | side = 'right'; 490 | } 491 | } 492 | 493 | return side; 494 | } 495 | 496 | _getMaxJumpAccelerationValue(max, multiplier) { 497 | let min = 0; 498 | let acceleration = 0; 499 | 500 | while(min <= max) { 501 | min += acceleration; 502 | acceleration = (Math.floor(acceleration * 10) + (multiplier * 10)) / 10; 503 | } 504 | 505 | return acceleration; 506 | } 507 | 508 | _onCollisionSide(object, side) { 509 | if (object.options.obstacle && side === 'bottom') { 510 | this._params.movement.acceleration = 0; 511 | } 512 | } 513 | 514 | _tick(callback) { 515 | const response = callback(); 516 | 517 | if (response !== false) { 518 | requestAnimationFrame(this._tick.bind(this, callback)); 519 | } 520 | } 521 | 522 | _nextTick(callback) { 523 | requestAnimationFrame(callback); 524 | } 525 | 526 | _getEventObject() { 527 | const event = { 528 | _stopped: false, 529 | _paused: false, 530 | get stopped() { 531 | return this._stopped; 532 | }, 533 | get paused() { 534 | return this._paused; 535 | }, 536 | stop() { 537 | this._stopped = true; 538 | }, 539 | pause() { 540 | this._paused = true; 541 | }, 542 | resume() { 543 | this._paused = false; 544 | } 545 | } 546 | 547 | return event; 548 | } 549 | 550 | _onJump(event) { 551 | if (this._isStopped) { 552 | return false; 553 | } 554 | 555 | if (event.paused) { 556 | this._dispatchEvent('jump', event); 557 | 558 | return; 559 | } 560 | 561 | if (event.stopped) { 562 | this._isFalling = true; 563 | 564 | return false; 565 | } 566 | 567 | if (this._isAccelerationMovementStopped) { 568 | this._isFalling = true; 569 | 570 | return false; 571 | } else { 572 | this._takeoff(); 573 | this._dispatchEvent('jump', event); 574 | } 575 | } 576 | 577 | _onFall(event) { 578 | if (this._isStopped) { 579 | return false; 580 | } 581 | 582 | if (event.paused) { 583 | this._dispatchEvent('fall', event); 584 | 585 | return; 586 | } 587 | 588 | if (event.stopped) { 589 | this._isJumping = false; 590 | 591 | return false; 592 | } 593 | 594 | if (this._isFalling) { 595 | const landed = this._landing(); 596 | 597 | if (landed === true) { 598 | this._isJumping = false; 599 | } 600 | 601 | this._dispatchEvent('fall', event); 602 | } 603 | } 604 | 605 | _onFly(event, degrees, distance, step) { 606 | if (this._isStopped) { 607 | this._isFlying = false; 608 | 609 | return false; 610 | } 611 | 612 | if (event.paused) { 613 | this._dispatchEvent('fly', event); 614 | 615 | return; 616 | } 617 | 618 | if (event.stopped) { 619 | this._isFlying = false; 620 | 621 | return false; 622 | } 623 | 624 | if (distance > 0 && point.distance(this._positions.start.x, this._positions.start.y, this._x, this._y) > distance) { 625 | this.destroy(); 626 | 627 | return false; 628 | } 629 | 630 | let x = this._x + parseFloat(Math.cos(degrees * Math.PI / 180).toFixed(10)) * step; 631 | let y = this._y + parseFloat(Math.sin(degrees * Math.PI / 180).toFixed(10)) * step; 632 | 633 | this.move(x, y); 634 | this._dispatchEvent('fly', event); 635 | } 636 | 637 | _init() { 638 | this.on('collision', this._onCollisionSide.bind(this)); 639 | } 640 | } 641 | 642 | module.exports = EngineObject; -------------------------------------------------------------------------------- /src/Point.js: -------------------------------------------------------------------------------- 1 | class Point { 2 | distance(x1, y1, x2, y2) { 3 | return Math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2)); 4 | } 5 | } 6 | 7 | module.exports = new Point(); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const EngineObject = require('./EngineObject'); 2 | 3 | class Engine { 4 | constructor(link, width, height, options = {}) { 5 | this._link = link; 6 | this._width = width || window.innerWidth; 7 | this._height = height || window.innerHeight; 8 | this._preload = typeof options.preload === 'boolean' ? options.preload : true; 9 | this._favicon = typeof options.favicon === 'boolean' ? options.favicon : true; 10 | this._field = null; 11 | this._ctx = null; 12 | this._exist = true; 13 | this._keys = []; 14 | this._events = {}; 15 | this._storage = { 16 | images: [] 17 | }; 18 | this._objects = {}; 19 | this._offset = { 20 | object: null, 21 | x: 0, 22 | y: 0 23 | }; 24 | 25 | this._init(); 26 | } 27 | 28 | create(name, x, y, width, height, options = {}) { 29 | this._objects[name] = new EngineObject(name, x, y, width, height, options); 30 | 31 | if (!this._objects[name].options.disabledEvents) { 32 | this._objects[name].on('destroy', this._onDestroyObject.bind(this, name)); 33 | } 34 | 35 | return this._objects[name]; 36 | } 37 | 38 | add(object) { 39 | if (Array.isArray(object)) { 40 | object.forEach(item => { 41 | item.added = true; 42 | 43 | this._addObjectImages(item); 44 | this._render(); 45 | }); 46 | } else { 47 | object.added = true; 48 | 49 | this._addObjectImages(object); 50 | this._render(); 51 | } 52 | } 53 | 54 | key(callback) { 55 | document.addEventListener('keydown', this._onMultiKeydown.bind(this)); 56 | document.addEventListener('keyup', this._onMultiKeyup.bind(this)); 57 | 58 | requestAnimationFrame(this._streamKeys.bind(this, callback)); 59 | } 60 | 61 | keydown(callback) { 62 | document.addEventListener('keydown', event => { 63 | callback(event.code); 64 | }); 65 | } 66 | 67 | keyup(callback) { 68 | document.addEventListener('keyup', event => { 69 | callback(event.code); 70 | }); 71 | } 72 | 73 | mousemove(callback) { 74 | this._field.addEventListener('mousemove', event => { 75 | callback(event.x - this._field.offsetLeft, event.y - this._field.offsetTop); 76 | }); 77 | } 78 | 79 | click(callback) { 80 | this._field.addEventListener('click', event => { 81 | callback(event.x - this._field.offsetLeft, event.y - this._field.offsetTop); 82 | }); 83 | } 84 | 85 | tick(callback) { 86 | const response = callback(); 87 | 88 | if (response !== false) { 89 | requestAnimationFrame(this.tick.bind(this, callback)); 90 | } 91 | } 92 | 93 | nextTick(callback) { 94 | requestAnimationFrame(callback); 95 | } 96 | 97 | checkObjectInViewport(object) { 98 | return this._checkObjectInViewportX(object) && this._checkObjectInViewportY(object); 99 | } 100 | 101 | fixingCamera(object, fixedCamera = {}) { 102 | this._setOffsetObject(object); 103 | this._setOffsetObjectToObjects(); 104 | 105 | object.options.fixedCamera.x = typeof fixedCamera === 'object' && fixedCamera !== null ? fixedCamera.x === undefined ? false : fixedCamera.x : false; 106 | object.options.fixedCamera.y = typeof fixedCamera === 'object' && fixedCamera !== null ? fixedCamera.y === undefined ? false : fixedCamera.y : false; 107 | 108 | if (object.options.fixedCamera.x) { 109 | object.offset.x = (this._offset.x - ((this._width / 2) - (object.width / 2))); 110 | this._offset.x = ((this._width / 2) - (object.width / 2)); 111 | } 112 | 113 | if (object.options.fixedCamera.y) { 114 | object.offset.y = (this._offset.y - ((this._height / 2) - (object.height / 2))); 115 | this._offset.y = ((this._height / 2) - (object.height / 2)); 116 | } 117 | } 118 | 119 | unfixingCamera() { 120 | Object.values(this._objects).forEach(object => { 121 | if (object !== this._offset.object) { 122 | object.x = object.x - this._offset.object.offset.x; 123 | object.y = object.y - this._offset.object.offset.y; 124 | } 125 | }); 126 | 127 | this._offset.object.options.fixedCamera.x = false; 128 | this._offset.object.options.fixedCamera.y = false; 129 | this._offset.object.x = this._offset.object.x - this._offset.object.offset.x; 130 | this._offset.object.y = this._offset.object.y - this._offset.object.offset.y; 131 | this._offset.x = 0; 132 | this._offset.y = 0; 133 | } 134 | 135 | destroy() { 136 | Object.values(this._objects).forEach(object => { 137 | object.destroy(); 138 | }); 139 | 140 | this.nextTick(() => { 141 | this._exist = false; 142 | }); 143 | } 144 | 145 | on(event, callback) { 146 | if (!this._events[event]) { 147 | this._events[event] = []; 148 | } 149 | 150 | this._events[event].push(callback); 151 | } 152 | 153 | async load() { 154 | if (this._imagesIsLoading) { 155 | await this._render(); 156 | 157 | requestAnimationFrame(this.load.bind(this)); 158 | } else { 159 | this._preload = false; 160 | 161 | await this._render(); 162 | 163 | this._dispatchEvent('load'); 164 | } 165 | } 166 | 167 | get width() { 168 | return this._width; 169 | } 170 | 171 | get height() { 172 | return this._height; 173 | } 174 | 175 | get offset() { 176 | return this._offset; 177 | } 178 | 179 | get objects() { 180 | return this._objects; 181 | } 182 | 183 | get _imagesIsLoaded() { 184 | if (this._preload) { 185 | const loadedImages = this._storage.images.filter(item => item.loaded); 186 | const isLoaded = loadedImages.length === this._storage.images.length; 187 | 188 | return isLoaded; 189 | } else { 190 | return true; 191 | } 192 | } 193 | 194 | get _imagesIsLoading() { 195 | if (this._preload) { 196 | const loadedImages = this._storage.images.filter(item => item.loaded); 197 | const isLoading = loadedImages.length !== this._storage.images.length; 198 | 199 | return isLoading; 200 | } else { 201 | return false; 202 | } 203 | } 204 | 205 | _setOffsetObject(object) { 206 | this._offset.object = object; 207 | this._offset.x = object.x; 208 | this._offset.y = object.y; 209 | } 210 | 211 | _setOffsetObjectToObjects() { 212 | for (const name in this._objects) { 213 | const object = this._objects[name]; 214 | 215 | object.setOffsetObject(this._offset.object); 216 | } 217 | } 218 | 219 | _onMultiKeydown(event) { 220 | if (!this._keys.includes(event.code)) { 221 | this._keys.push(event.code); 222 | } 223 | } 224 | 225 | _onMultiKeyup(event) { 226 | const index = this._keys.indexOf(event.code); 227 | 228 | if (index !== -1) { 229 | this._keys.splice(index, 1); 230 | } 231 | } 232 | 233 | _addObjectImages(object) { 234 | const images = object.options.images.list; 235 | const image = object.options.image.path; 236 | 237 | if (image) { 238 | this._addObjectImageToStorage(object); 239 | } 240 | 241 | if (images) { 242 | this._addObjectImagesToStorage(object); 243 | } 244 | } 245 | 246 | _addObjectImageToStorage(object) { 247 | const id = `${object.name}:${object.options.image.path}`; 248 | 249 | if (!this._storage.images.find(image => image.id === id)) { 250 | this._storage.images.push({ id, loaded: false }); 251 | } 252 | } 253 | 254 | _addObjectImagesToStorage(object) { 255 | object.options.images.list.forEach(images => { 256 | images.paths.forEach(path => { 257 | const id = `${object.name}:${images.state}:${path}`; 258 | 259 | if (!this._storage.images.find(image => image.id === id)) { 260 | this._storage.images.push({ id, loaded: false }); 261 | } 262 | }); 263 | }); 264 | } 265 | 266 | _loadImage(url, object, state) { 267 | return new Promise(resolve => { 268 | const image = new Image(); 269 | 270 | image.src = url; 271 | 272 | image.addEventListener('load', () => { 273 | if (state) { 274 | const id = `${object.name}:${state}:${url}`; 275 | const item = this._storage.images.find(item => item.id === id); 276 | 277 | item.loaded = true; 278 | } else { 279 | const id = `${object.name}:${url}`; 280 | const item = this._storage.images.find(item => item.id === id); 281 | 282 | item.loaded = true; 283 | } 284 | 285 | resolve(image); 286 | }); 287 | }); 288 | } 289 | 290 | _loadImages(listImages, object) { 291 | return new Promise(async resolve => { 292 | const images = {}; 293 | 294 | for(let i = 0; i < listImages.length; i++) { 295 | const listImageItem = listImages[i]; 296 | 297 | if (!images[listImageItem.state]) { 298 | images[listImageItem.state] = this._getImageParams(listImageItem); 299 | } 300 | 301 | for(let j = 0; j < listImageItem.paths.length; j++) { 302 | const path = listImageItem.paths[j]; 303 | 304 | images[listImageItem.state].list[j] = await this._loadImage(path, object, listImageItem.state); 305 | 306 | if (i === listImages.length - 1 && j === listImageItem.paths.length - 1) { 307 | resolve(images); 308 | } 309 | }; 310 | }; 311 | }); 312 | } 313 | 314 | _getImageParams(image) { 315 | const params = { 316 | list: [], 317 | time: image.time || 0, 318 | currentImage: null, 319 | lastRenderTime: 0 320 | } 321 | 322 | return params; 323 | } 324 | 325 | _checkObjectInViewportX(object) { 326 | return this._offset.object 327 | && object.x < (this._offset.object.x + this._width) 328 | && object.x > (this._offset.object.x - this._width); 329 | } 330 | 331 | _checkObjectInViewportY(object) { 332 | return this._offset.object 333 | && object.y < (this._offset.object.y + this._height) 334 | && object.y > (this._offset.object.y - this._height); 335 | } 336 | 337 | async _render() { 338 | if (!this._preload) { 339 | this._ctx.clearRect(0, 0, this._width, this._height); 340 | } 341 | 342 | for(const name in this._objects) { 343 | const object = this._objects[name]; 344 | const images = object.options.images.list; 345 | const image = object.options.image.path; 346 | 347 | if (!object.added) { 348 | return; 349 | } 350 | 351 | if (image) { 352 | await this._renderImage(object); 353 | } 354 | 355 | if (images) { 356 | await this._renderImages(object); 357 | } 358 | 359 | if (!image && !images && this._imagesIsLoaded) { 360 | this._renderShape(object); 361 | } 362 | } 363 | 364 | if (this._preload) { 365 | const images = this._storage.images.length; 366 | const imagesLoaded = this._storage.images.filter(item => item.loaded).length; 367 | 368 | this._showLoadingScreen(images, imagesLoaded); 369 | } 370 | } 371 | 372 | async _renderImage(object) { 373 | if (!object.isExist || object.options.image.rendering) { 374 | return; 375 | } 376 | 377 | await this._renderingImage(object); 378 | 379 | const offset = { 380 | x: 0, 381 | y: 0 382 | } 383 | 384 | if (this._offset.object) { 385 | offset.x = this._offset.object.offset.x; 386 | offset.y = this._offset.object.offset.y; 387 | } else { 388 | offset.x = this._offset.x; 389 | offset.y = this._offset.y; 390 | } 391 | 392 | if (this._offset.object && !this._offset.object.options.fixedCamera.x) { 393 | offset.x = 0; 394 | } 395 | 396 | if (this._offset.object && !this._offset.object.options.fixedCamera.y) { 397 | offset.y = 0; 398 | } 399 | 400 | if (object.options.image.repeat) { 401 | this._drawRepeatImage(object, offset); 402 | 403 | return; 404 | } 405 | 406 | const x = (object.x - offset.x) + object.width / 2; 407 | const y = (object.y - offset.y) + object.height / 2; 408 | const angle = object.degrees * Math.PI / 180; 409 | 410 | if ((object.x > (offset.x + this._width) || (object.x + object.width) < offset.x) 411 | || (object.y > (offset.y + this._height) || (object.y + object.height) < offset.y)) { 412 | return; 413 | } 414 | 415 | this._ctx.save(); 416 | this._ctx.translate(x - object.offset.rotate.x, y - object.offset.rotate.y); 417 | this._ctx.rotate(angle); 418 | this._ctx.translate(-x + object.offset.rotate.x, -y + object.offset.rotate.y); 419 | this._ctx.drawImage(object.options.image.cached, (object.x - offset.x), (object.y - offset.y), object.width, object.height); 420 | this._ctx.restore(); 421 | } 422 | 423 | async _renderImages(object) { 424 | if (!object.isExist || object.options.images.rendering) { 425 | return; 426 | } 427 | 428 | await this._renderingImages(object); 429 | 430 | if (!object.state) { 431 | object.state = this._getFirstState(object.options.images.cached); 432 | } 433 | 434 | const cached = object.options.images.cached[object.state]; 435 | 436 | if (object.animate) { 437 | this._calculateRenderTime(cached); 438 | this._notifyAboutRenderedImage(object, cached); 439 | } else { 440 | cached.currentImage = cached.list[0]; 441 | } 442 | 443 | if (this._imagesIsLoading) { 444 | return; 445 | } 446 | 447 | const offset = { 448 | x: 0, 449 | y: 0 450 | } 451 | 452 | if (this._offset.object) { 453 | offset.x = this._offset.object.offset.x; 454 | offset.y = this._offset.object.offset.y; 455 | } else { 456 | offset.x = this._offset.x; 457 | offset.y = this._offset.y; 458 | } 459 | 460 | if (this._offset.object && !this._offset.object.options.fixedCamera.x) { 461 | offset.x = 0; 462 | } 463 | 464 | if (this._offset.object && !this._offset.object.options.fixedCamera.y) { 465 | offset.y = 0; 466 | } 467 | 468 | const x = (object.x - offset.x) + object.width / 2; 469 | const y = (object.y - offset.y) + object.height / 2; 470 | const angle = object.degrees * Math.PI / 180; 471 | 472 | if (!this._offset.object) { 473 | this._ctx.save(); 474 | this._ctx.translate(x - object.offset.rotate.x, y - object.offset.rotate.y); 475 | this._ctx.rotate(angle); 476 | this._ctx.translate(-x + object.offset.rotate.x, -y + object.offset.rotate.y); 477 | this._ctx.drawImage(cached.currentImage, object.x, object.y, object.width, object.height); 478 | this._ctx.restore(); 479 | 480 | return; 481 | } 482 | 483 | this._ctx.save(); 484 | this._ctx.translate(x - object.offset.rotate.x, y - object.offset.rotate.y); 485 | this._ctx.rotate(angle); 486 | this._ctx.translate(-x + object.offset.rotate.x, -y + object.offset.rotate.y); 487 | 488 | if (object === this._offset.object) { 489 | let x; 490 | let y; 491 | 492 | if (this._offset.object.options.fixedCamera.x) { 493 | x = this._offset.x; 494 | } else { 495 | x = object.x; 496 | } 497 | 498 | if (this._offset.object.options.fixedCamera.y) { 499 | y = this._offset.y; 500 | } else { 501 | y = object.y; 502 | } 503 | 504 | this._ctx.drawImage(cached.currentImage, x, y, object.width, object.height); 505 | } else { 506 | this._ctx.drawImage(cached.currentImage, (object.x - offset.x), (object.y - offset.y), object.width, object.height); 507 | } 508 | 509 | this._ctx.restore(); 510 | } 511 | 512 | async _renderingImage(object) { 513 | object.options.image.rendering = true; 514 | 515 | if (!object.options.image.cached) { 516 | object.options.image.cached = await this._loadImage(object.options.image.path, object); 517 | } 518 | 519 | object.options.image.rendering = false; 520 | } 521 | 522 | async _renderingImages(object) { 523 | object.options.images.rendering = true; 524 | 525 | if (this._isEmpty(object.options.images.cached)) { 526 | object.options.images.cached = await this._loadImages(object.options.images.list, object); 527 | } 528 | 529 | object.options.images.rendering = false; 530 | } 531 | 532 | _renderShape(object) { 533 | const offset = { 534 | x: 0, 535 | y: 0 536 | } 537 | 538 | if (this._offset.object) { 539 | offset.x = this._offset.object.offset.x; 540 | offset.y = this._offset.object.offset.y; 541 | } else { 542 | offset.x = this._offset.x; 543 | offset.y = this._offset.y; 544 | } 545 | 546 | if (this._offset.object && !this._offset.object.options.fixedCamera.x) { 547 | offset.x = 0; 548 | } 549 | 550 | if (this._offset.object && !this._offset.object.options.fixedCamera.y) { 551 | offset.y = 0; 552 | } 553 | 554 | const x = (object.x - offset.x) + object.width / 2; 555 | const y = (object.y - offset.y) + object.height / 2; 556 | const angle = object.degrees * Math.PI / 180; 557 | 558 | this._ctx.save(); 559 | this._ctx.translate(x - object.offset.rotate.x, y - object.offset.rotate.y); 560 | this._ctx.rotate(angle); 561 | this._ctx.translate(-x + object.offset.rotate.x, -y + object.offset.rotate.y); 562 | this._ctx.fillStyle = object.options.color; 563 | this._ctx.fillRect((object.x - offset.x), (object.y - offset.y), object.width, object.height); 564 | this._ctx.restore(); 565 | } 566 | 567 | _getFirstState(states) { 568 | return Object.keys(states)[0]; 569 | } 570 | 571 | _calculateRenderTime(cached) { 572 | if (performance.now() > (cached.lastRenderTime + cached.time)) { 573 | let index = cached.list.indexOf(cached.currentImage); 574 | 575 | if ((index + 1) > (cached.list.length - 1) || index === -1) { 576 | index = 0; 577 | } else { 578 | index = index + 1; 579 | } 580 | 581 | cached.lastRenderTime = performance.now(); 582 | cached.currentImage = cached.list[index]; 583 | } 584 | 585 | if (!cached.currentImage) { 586 | cached.currentImage = cached.list[0]; 587 | } 588 | } 589 | 590 | _notifyAboutRenderedImage(object, cached) { 591 | const image = cached.currentImage.getAttribute('src'); 592 | const images = object.options.images.list.find(i => i.state === object.state).paths; 593 | 594 | this._dispatchEvent('animation', object, image, images); 595 | } 596 | 597 | _streamKeys(callback) { 598 | this._keys.forEach(key => { 599 | callback(key); 600 | }); 601 | 602 | requestAnimationFrame(this._streamKeys.bind(this, callback)); 603 | } 604 | 605 | _onDestroyObject(name) { 606 | delete this._objects[name]; 607 | } 608 | 609 | _drawRepeatImage(object, offset) { 610 | const pattern = this._ctx.createPattern(object.options.image.cached, 'repeat'); 611 | const delta = this._getRepeatImageDelta(object); 612 | 613 | this._ctx.fillStyle = pattern; 614 | this._ctx.save(); 615 | this._ctx.translate(-offset.x + delta.x, -offset.y + delta.y); 616 | this._ctx.fillRect(object.x - delta.x, object.y - delta.y, object.width, object.height); 617 | this._ctx.restore(); 618 | } 619 | 620 | _getRepeatImageDelta(object) { 621 | const delta = { 622 | x: 0, 623 | y: 0 624 | } 625 | const difference = { 626 | x: object.width - Math.abs(object.x), 627 | y: object.height - Math.abs(object.y) 628 | } 629 | 630 | if (object.x < 0) { 631 | delta.x = difference.x % object.options.image.cached.width; 632 | } else { 633 | delta.x = -(difference.x % object.options.image.cached.width); 634 | } 635 | 636 | if (object.y < 0) { 637 | delta.y = difference.y % object.options.image.cached.height; 638 | } else { 639 | delta.y = -(difference.y % object.options.image.cached.height); 640 | } 641 | 642 | return delta; 643 | } 644 | 645 | _showLoadingScreen(current, max) { 646 | const x = (this._width / 2) - ((this._width / 2) / 2); 647 | const y = (this._height / 2) - (((this._height / 100) * 10) / 2); 648 | const width = this._width / 2; 649 | const height = (this._height / 100) * 10; 650 | 651 | this._ctx.fillStyle = 'black'; 652 | this._ctx.fillRect(0, 0, this._width, this._height); 653 | this._ctx.fillStyle = 'white'; 654 | this._ctx.fillRect(x, y, width, height); 655 | this._ctx.fillStyle = 'black'; 656 | this._ctx.fillRect(x + 5, y + 6, width - 10, height - 12); 657 | this._ctx.fillStyle = 'white'; 658 | this._ctx.fillRect(x + 4, y + 5, ((width - 8) / current) * max, height - 10); 659 | } 660 | 661 | _isEmpty(object) { 662 | return Object.values(object).length === 0; 663 | } 664 | 665 | _setFavIcon() { 666 | const favicon = 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAk1JREFUOE9jZKAQMJKqv6c4UqSkd/kbmD64AY9XhXLeu/Yl8s7xR17MfxifsrOy9EfuuPAApDDGQNF1yYX7u/uyQlXef/nC27xo+3kMA0ACfaGWnIzPn5s//cO0SICFRVyTj3OxtJhY5/xL97vkuFmznojIlHx//7t6wYEDP7AaABOsidFx+Pj+5543z78zR0mJ/z337f89ht+/VzwXFHWasWmfDbK3cYZBTbRh249P3yq/fmFgkGXmYuD8+ffvw59/jk04dc2egYHhP14XgCRzcz3ZPz58Z/Xl298yM30Tj1fXbjA8fHif4R8Hm9b6c7ev4zWgsSTO/8fXb7P+/P0v9vcvI4OUiDjDh9c/GO69ecJw5e5dv0tX72zGaUB1dog/CwvL2h8/fjDzcPEyvP30leHPr38MzAwsDPdfvv5879lbpytXrpzBakBDQwPTx8dnbv7981eFk52T4d///wyvPn1jePzsPcPb958e/mP853H58q0bOAOxIjPYhYHh3+4nz94x/GX4x8DLxcPw+cd/hkfPXzG8+/i16/r1m+XoCQ8lFlKj/PL//P4x4dGzNwy8vFwMChJyDNcePGZ49/kLAxsLk/uxk2d34TUgLtBLjZuH+8b563dOMzH85VWQlhP6//+fOBcnG8PDZ2899hw+vBOvAVkp4ZpCbNzXrt178Pf20+chFrpa8oz/GSd8/f6d4frDh8fPnb9ijZwGQIahJyRGTzd7j3sPHzv8+PFjo5ayxstP3941vHzz4drPfz93P773/CwhA0jNnAwAsYwMINM2tAQAAAAASUVORK5CYII='; 667 | const head = document.getElementsByTagName('head')[0]; 668 | const link = document.createElement('link'); 669 | 670 | link.rel = 'shortcut icon'; 671 | link.href = 'data:image/png;base64,' + favicon; 672 | 673 | head.appendChild(link); 674 | } 675 | 676 | _setDefaultStyle() { 677 | document.body.style.margin = 0; 678 | } 679 | 680 | _setFieldStyle() { 681 | if (typeof this._link === 'string') { 682 | this._field = document.querySelector(this._link); 683 | } 684 | 685 | if (this._link instanceof HTMLCanvasElement) { 686 | this._field = this._link; 687 | } 688 | 689 | this._field.width = this._width; 690 | this._field.height = this._height; 691 | this._field.style.display = 'block'; 692 | this._ctx = this._field.getContext('2d'); 693 | } 694 | 695 | _checkReadyStateChange() { 696 | document.addEventListener('readystatechange', () => { 697 | if (document.readyState === 'complete') { 698 | this.load(); 699 | } 700 | }); 701 | } 702 | 703 | _dispatchEvent(name, ...data) { 704 | if (this._events[name] && Array.isArray(this._events[name]) && this._events[name].length > 0) { 705 | this._events[name].forEach(callback => { 706 | callback(...data); 707 | }); 708 | } 709 | } 710 | 711 | _frameRender() { 712 | this.tick(() => { 713 | if (!this._exist) { 714 | return false; 715 | } 716 | 717 | this._render(); 718 | }); 719 | } 720 | 721 | _init() { 722 | if (this._favicon) { 723 | this._setFavIcon(); 724 | } 725 | 726 | this.on('load', this._frameRender.bind(this)); 727 | this._setDefaultStyle(); 728 | this._setFieldStyle(); 729 | this._checkReadyStateChange(); 730 | } 731 | } 732 | 733 | module.exports = Engine; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: { 6 | main: path.resolve(__dirname, './src/index.js') 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, './dist'), 10 | filename: 'elpy.min.js', 11 | library: 'Elpy' 12 | }, 13 | devServer: { 14 | static: [ 15 | { 16 | directory: path.join(__dirname, './public') 17 | }, 18 | { 19 | directory: path.join(__dirname, './dist'), 20 | publicPath: '/dist' 21 | }, 22 | { 23 | directory: path.join(__dirname, './examples'), 24 | publicPath: '/examples' 25 | } 26 | ], 27 | port: 8080, 28 | } 29 | } --------------------------------------------------------------------------------