├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md ├── index.html ├── media └── panda.png └── src ├── engine ├── audio.js ├── camera.js ├── core.js ├── debug.js ├── geometry.js ├── input.js ├── loader.js ├── particle.js ├── physics.js ├── pool.js ├── renderer │ ├── animation.js │ ├── container.js │ ├── core.js │ ├── fastcontainer.js │ ├── graphics.js │ ├── sprite.js │ ├── spritesheet.js │ ├── text.js │ ├── texture.js │ └── tilingsprite.js ├── scene.js ├── storage.js ├── system.js ├── timer.js └── tween.js └── game ├── config.js └── main.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Info** 24 | - OS: [e.g. Windows] 25 | - Version: [e.g. 1.5.0] 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.min.js 3 | node_modules 4 | plugins 5 | editor 6 | bamboo 7 | .jscsrc 8 | media/* 9 | !media/panda.png 10 | *.mobileprovision 11 | *.p12 12 | plugin 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing guide 2 | 3 | 1. [Fork the project](https://github.com/ekelokorpi/panda-engine/fork) 4 | 2. Checkout to `develop` branch 5 | 3. Make changes 6 | 4. Validate code 7 | 8 | $ panda lint 9 | 10 | 4. Create new [pull request](https://github.com/ekelokorpi/panda-engine/compare) 11 | 5. Wait for merge or comments 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Panda Engine 2 | 3 | [![Panda 2 Logo](https://www.panda2.io/img/logo_github.png)](https://www.panda2.io/) 4 | 5 | Panda Engine is a part of Panda 2 Game Development Platform, for more information please visit: 6 | 7 | https://www.panda2.io 8 | 9 | ## Learn 10 | 11 | - [Code examples](https://www.panda2.io/examples) 12 | - [Tutorials](https://www.panda2.io/tutorials) 13 | - [Game templates](https://www.panda2.io/templates) 14 | - [API Documentation](https://www.panda2.io/docs/api) 15 | 16 | ## Credits 17 | 18 | - Made by Eemeli Kelokorpi 19 | - Renderer based on [PixiJS](http://www.pixijs.com) by Mat Groves, Goodboy Digital 20 | 21 | ## License 22 | 23 | - Panda Engine is released under the [MIT License](http://opensource.org/licenses/MIT). 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Panda Engine 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /media/panda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekelokorpi/panda-engine/08eb1822ebe985bc6c43795aebd2baa635c02ff5/media/panda.png -------------------------------------------------------------------------------- /src/engine/audio.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module audio 3 | **/ 4 | game.module( 5 | 'engine.audio' 6 | ) 7 | .body(function() { 8 | 9 | /** 10 | Audio manager. Instance automatically created at `game.audio` 11 | @class Audio 12 | **/ 13 | game.createClass('Audio', { 14 | /** 15 | Current supported audio formats. 16 | @property {Array} formats 17 | **/ 18 | formats: [], 19 | /** 20 | Current music. 21 | @property {Music} music 22 | **/ 23 | music: null, 24 | /** 25 | Is all sounds and music muted. 26 | @property {Boolean} muted 27 | @default false 28 | **/ 29 | muted: false, 30 | /** 31 | Is music muted. 32 | @property {Boolean} mutedMusic 33 | @default false 34 | **/ 35 | mutedMusic: false, 36 | /** 37 | Is all sounds muted. 38 | @property {Boolean} mutedSound 39 | @default false 40 | **/ 41 | mutedSound: false, 42 | /** 43 | Currently playing sounds. 44 | @property {Array} sounds 45 | **/ 46 | sounds: [], 47 | /** 48 | @property {AudioContext} _context 49 | @private 50 | **/ 51 | _context: null, 52 | /** 53 | @property {GainNode} _mainGain 54 | @private 55 | **/ 56 | _mainGain: null, 57 | /** 58 | @property {GainNode} _musicGain 59 | @private 60 | **/ 61 | _musicGain: null, 62 | /** 63 | @property {Music} _pauseMusic 64 | @private 65 | **/ 66 | _pauseMusic: null, 67 | /** 68 | @property {Array} _pauseSounds 69 | @private 70 | **/ 71 | _pauseSounds: [], 72 | /** 73 | @property {GainNode} _soundGain 74 | @private 75 | **/ 76 | _soundGain: null, 77 | 78 | staticInit: function() { 79 | if (!game.Audio.enabled) return; 80 | 81 | game._normalizeVendorAttribute(window, 'AudioContext'); 82 | 83 | var audio = new Audio(); 84 | for (var i = 0; i < game.Audio.formats.length; i++) { 85 | if (audio.canPlayType(game.Audio.formats[i].type)) { 86 | this.formats.push(game.Audio.formats[i].ext); 87 | } 88 | } 89 | 90 | if (!this.formats.length) { 91 | game.Audio.enabled = false; 92 | return; 93 | } 94 | 95 | if (!window.AudioContext) return; 96 | 97 | this._context = new AudioContext(); 98 | 99 | this._mainGain = this._context.createGain(); 100 | this._mainGain.connect(this._context.destination); 101 | 102 | this._musicGain = this._context.createGain(); 103 | this._musicGain.gain.setValueAtTime(game.Audio.musicVolume, this._context.currentTime); 104 | this._musicGain.connect(this._mainGain); 105 | 106 | this._soundGain = this._context.createGain(); 107 | this._soundGain.gain.setValueAtTime(game.Audio.soundVolume, this._context.currentTime); 108 | this._soundGain.connect(this._mainGain); 109 | }, 110 | 111 | /** 112 | Mute all sounds and music. 113 | @method mute 114 | **/ 115 | mute: function() { 116 | if (!this._mainGain) return; 117 | this._mainGain.gain.setValueAtTime(0, this._context.currentTime); 118 | this.muted = true; 119 | }, 120 | 121 | /** 122 | Mute music. 123 | @method muteMusic 124 | **/ 125 | muteMusic: function() { 126 | if (!this._musicGain) return; 127 | this._musicGain.gain.setValueAtTime(0, this._context.currentTime); 128 | this.mutedMusic = true; 129 | }, 130 | 131 | /** 132 | Mute all sounds. 133 | @method muteSound 134 | **/ 135 | muteSound: function() { 136 | if (!this._soundGain) return; 137 | this._soundGain.gain.setValueAtTime(0, this._context.currentTime); 138 | this.mutedSound = true; 139 | }, 140 | 141 | /** 142 | @method playMusic 143 | @param {String} name 144 | @param {Boolean} [noLoop] Do not loop the music 145 | @return {Music} 146 | **/ 147 | playMusic: function(name, noLoop) { 148 | var music = new game.Music(name); 149 | if (noLoop) music.loop = false; 150 | music.play(); 151 | return music; 152 | }, 153 | 154 | /** 155 | @method playSound 156 | @param {String} name 157 | @param {Number} [volume] 158 | @param {Number} [rate] 159 | @return {Sound} 160 | **/ 161 | playSound: function(name, volume, rate) { 162 | var sound = new game.Sound(name); 163 | sound.volume = volume || sound.volume; 164 | sound.rate = rate || sound._rate; 165 | sound.play(); 166 | return sound; 167 | }, 168 | 169 | /** 170 | Stop all sounds. 171 | @method stopAll 172 | **/ 173 | stopAll: function() { 174 | this.stopMusic(); 175 | for (var i = 0; i < this.sounds.length; i++) { 176 | this.sounds[i].stop(); 177 | } 178 | }, 179 | 180 | /** 181 | Stop current music. 182 | @method stopMusic 183 | **/ 184 | stopMusic: function() { 185 | if (this.music) this.music.stop(); 186 | this.music = null; 187 | }, 188 | 189 | /** 190 | Toggle mute/unmute all audio. 191 | @method toggle 192 | **/ 193 | toggle: function() { 194 | if (this.muted) this.unmute(); 195 | else this.mute(); 196 | return this.muted; 197 | }, 198 | 199 | /** 200 | Unmute all audio. 201 | @method unmute 202 | **/ 203 | unmute: function() { 204 | if (!this._mainGain) return; 205 | this._mainGain.gain.setValueAtTime(1, this._context.currentTime); 206 | this.muted = false; 207 | }, 208 | 209 | /** 210 | Unmute music. 211 | @method muteMusic 212 | **/ 213 | unmuteMusic: function() { 214 | if (!this._musicGain) return; 215 | this._musicGain.gain.setValueAtTime(game.Audio.musicVolume, this._context.currentTime); 216 | this.mutedMusic = false; 217 | }, 218 | 219 | /** 220 | Unmute all sounds. 221 | @method muteSound 222 | **/ 223 | unmuteSound: function() { 224 | if (!this._soundGain) return; 225 | this._soundGain.gain.setValueAtTime(game.Audio.soundVolume, this._context.currentTime); 226 | this.mutedSound = false; 227 | }, 228 | 229 | /** 230 | @method _decode 231 | @param {XMLHttpRequest} request 232 | @param {String} path 233 | @param {Function} callback 234 | @private 235 | **/ 236 | _decode: function(request, path, callback) { 237 | this._context.decodeAudioData( 238 | request.response, 239 | this._loaded.bind(this, path, callback), 240 | this._error.bind(this, path, callback) 241 | ); 242 | }, 243 | 244 | /** 245 | @method _error 246 | @param {String} path 247 | @param {Function} callback 248 | @private 249 | **/ 250 | _error: function(path, callback) { 251 | callback('Error loading audio ' + path); 252 | }, 253 | 254 | /** 255 | @method _load 256 | @param {String} path 257 | @param {Function} callback 258 | @private 259 | **/ 260 | _load: function(path, callback) { 261 | if (!game.Audio.enabled) { 262 | callback(); 263 | return; 264 | } 265 | var ext = path.split('?').shift().split('.').pop(); 266 | if (this.formats.indexOf(ext) === -1) ext = this.formats[0]; 267 | 268 | var realPath = path.replace(/[^\.]+$/, ext + game._nocache); 269 | 270 | if (!window.AudioContext) { 271 | var audio = new Audio(); 272 | audio.src = realPath; 273 | this._loaded(path, callback, audio); 274 | } 275 | else { 276 | var request = new XMLHttpRequest(); 277 | request.open('GET', realPath, true); 278 | request.responseType = 'arraybuffer'; 279 | request.onload = this._decode.bind(this, request, path, callback); 280 | request.onerror = this._error.bind(this, path, callback); 281 | request.send(); 282 | } 283 | }, 284 | 285 | /** 286 | @method _loaded 287 | @param {String} path 288 | @param {Function} callback 289 | @param {AudioBuffer} buffer 290 | @private 291 | **/ 292 | _loaded: function(path, callback, buffer) { 293 | var id = game._getId(path); 294 | game.Audio.cache[id] = buffer; 295 | callback(); 296 | }, 297 | 298 | /** 299 | @method _systemPause 300 | @private 301 | **/ 302 | _systemPause: function() { 303 | if (this.music && this.music.playing) { 304 | this.music.pause(); 305 | this._pauseMusic = this.music; 306 | } 307 | for (var i = 0; i < this.sounds.length; i++) { 308 | if (this.sounds[i].playing) { 309 | this.sounds[i].pause(); 310 | this._pauseSounds.push(this.sounds[i]); 311 | } 312 | } 313 | }, 314 | 315 | /** 316 | @method _systemResume 317 | @private 318 | **/ 319 | _systemResume: function() { 320 | if (this._pauseMusic) this._pauseMusic.resume(); 321 | for (var i = 0; i < this._pauseSounds.length; i++) { 322 | this._pauseSounds[i].resume(); 323 | } 324 | this._pauseMusic = null; 325 | this._pauseSounds.length = 0; 326 | } 327 | }); 328 | 329 | game.addAttributes('Audio', { 330 | /** 331 | Cache for audio buffers. 332 | @attribute {Object} cache 333 | **/ 334 | cache: {}, 335 | 336 | /** 337 | Clear all audio buffers from cache. 338 | @method clearCache 339 | @static 340 | **/ 341 | clearCache: function() { 342 | for (var i in this.cache) { 343 | delete this.cache[i]; 344 | } 345 | }, 346 | 347 | /** 348 | Is audio enabled. 349 | @attribute {Boolean} enabled 350 | @default true 351 | **/ 352 | enabled: true, 353 | 354 | /** 355 | List of supported audio formats. 356 | @attribute {Array} formats 357 | **/ 358 | formats: [ 359 | { ext: 'ogg', type: 'audio/ogg; codecs="vorbis"' }, 360 | { ext: 'm4a', type: 'audio/mp4; codecs="mp4a.40.5"' }, 361 | { ext: 'mp3', type: 'audio/mp3' }, 362 | { ext: 'wav', type: 'audio/wav' } 363 | ], 364 | 365 | /** 366 | Length to trim looped audio from end (seconds). 367 | @attribute {Number} loopEnd 368 | @default 0 369 | **/ 370 | loopEnd: 0, 371 | 372 | /** 373 | Length to trim looped audio from start (seconds). 374 | @attribute {Number} loopStart 375 | @default 0 376 | **/ 377 | loopStart: 0, 378 | 379 | /** 380 | Initial music volume (0-1). 381 | @attribute {Number} musicVolume 382 | @default 1 383 | **/ 384 | musicVolume: 1, 385 | 386 | /** 387 | Initial sound volume (0-1). 388 | @attribute {Number} soundVolume 389 | @default 1 390 | **/ 391 | soundVolume: 1, 392 | 393 | /** 394 | Stop all audio, when changing scene. 395 | @attribute {Boolean} stopOnSceneChange 396 | @default true 397 | **/ 398 | stopOnSceneChange: true 399 | }); 400 | 401 | /** 402 | @class Sound 403 | @constructor 404 | @param {String} id Audio asset id 405 | **/ 406 | game.createClass('Sound', { 407 | /** 408 | Is sound looping. 409 | @property {Boolean} loop 410 | @default false 411 | **/ 412 | loop: false, 413 | /** 414 | Is sound muted. 415 | @property {Boolean} muted 416 | @default false 417 | **/ 418 | muted: false, 419 | /** 420 | Function to call, when sound is completed. 421 | @property {Function} onComplete 422 | **/ 423 | onComplete: null, 424 | /** 425 | Is sound paused. 426 | @property {Boolean} paused 427 | @default false 428 | **/ 429 | paused: false, 430 | /** 431 | Is sound playing. 432 | @property {Boolean} playing 433 | @default false 434 | **/ 435 | playing: false, 436 | /** 437 | @property {AudioBuffer} _buffer 438 | @private 439 | **/ 440 | _buffer: null, 441 | /** 442 | @property {AudioContext} _context 443 | @private 444 | **/ 445 | _context: null, 446 | /** 447 | @property {GainNode} _gainNode 448 | @private 449 | **/ 450 | _gainNode: null, 451 | /** 452 | @property {Number} _rate 453 | @private 454 | **/ 455 | _rate: 1, 456 | /** 457 | @property {AudioBufferSourceNode} _source 458 | @private 459 | **/ 460 | _source: null, 461 | /** 462 | @property {Number} _volume 463 | @private 464 | **/ 465 | _volume: 1, 466 | 467 | staticInit: function(id) { 468 | if (!game.Audio.enabled) return true; 469 | 470 | this._buffer = game.Audio.cache[id]; 471 | if (!this._buffer) throw 'Audio ' + id + ' not found'; 472 | 473 | if (!window.AudioContext) return; 474 | 475 | this._context = game.audio._context; 476 | this._gainNode = this._context.createGain(); 477 | }, 478 | 479 | init: function() { 480 | if (this._gainNode) this._gainNode.connect(game.audio._soundGain); 481 | }, 482 | 483 | /** 484 | @method fadeIn 485 | @param {Number} time Time in milliseconds 486 | **/ 487 | fadeIn: function(time) { 488 | this._fade(time, this.volume); 489 | }, 490 | 491 | /** 492 | @method fadeOut 493 | @param {Number} time Time in milliseconds 494 | **/ 495 | fadeOut: function(time) { 496 | this._fade(time, 0); 497 | }, 498 | 499 | /** 500 | @method mute 501 | **/ 502 | mute: function() { 503 | if (!this._gainNode) this._source.volume = 0; 504 | else this._gainNode.gain.setValueAtTime(0, this._context.currentTime); 505 | this.muted = true; 506 | }, 507 | 508 | /** 509 | @method pause 510 | **/ 511 | pause: function() { 512 | if (!this._source) return; 513 | if (this.paused) return; 514 | if (!this.playing) return; 515 | 516 | this.stop(); 517 | this.paused = true; 518 | this._source.pauseTime = (this._context.currentTime - this._source.startTime) % this._buffer.duration; 519 | }, 520 | 521 | /** 522 | @method play 523 | @param {Number} [when] When to start playback in seconds, 0 is now 524 | @param {Number} [offset] Offset of playback in seconds 525 | @param {Number} [duration] Duration of playback in seconds 526 | @chainable 527 | **/ 528 | play: function(when, offset, duration) { 529 | if (!this._buffer) return; 530 | 531 | this._onStart(); 532 | this.playing = true; 533 | 534 | when = when || 0; 535 | offset = offset || 0; 536 | duration = duration || this._buffer.duration - offset; 537 | 538 | if (!this._context) this._source = this._buffer; 539 | else this._source = this._context.createBufferSource(); 540 | 541 | this._source.buffer = this._buffer; 542 | this._source.loop = this.loop; 543 | if (this._source.playbackRate && this._context) this._source.playbackRate.setValueAtTime(this.rate, this._context.currentTime); 544 | this._source.onended = this._onComplete.bind(this); 545 | if (this._source.connect) { 546 | this._source.connect(this._gainNode); 547 | this._source.startTime = this._context.currentTime - offset; 548 | } 549 | 550 | if (!this._context) { 551 | this._source.volume = this.volume; 552 | this._source.currentTime = 0; 553 | this._source.play(); 554 | } 555 | else if (this.loop) { 556 | this._source.loopStart = game.Audio.loopStart; 557 | this._source.loopEnd = this._source.buffer.duration - game.Audio.loopEnd; 558 | this._source.start(this._context.currentTime + when, offset); 559 | } 560 | else { 561 | this._source.start(this._context.currentTime + when, offset, duration); 562 | } 563 | 564 | return this; 565 | }, 566 | 567 | /** 568 | @method resume 569 | **/ 570 | resume: function() { 571 | if (!this._source) return; 572 | if (!this.paused) return; 573 | 574 | this.paused = false; 575 | if (!this._source.pauseTime) return; 576 | 577 | this.play(0, this._source.pauseTime); 578 | }, 579 | 580 | /** 581 | @method stop 582 | @param {Boolean} [skipOnComplete] Skip onComplete function 583 | **/ 584 | stop: function(skipOnComplete) { 585 | if (!this._source) return; 586 | if (this.paused) return; 587 | 588 | this.playing = false; 589 | if (skipOnComplete) this.onComplete = null; 590 | if (!this._context) this._source.pause(); 591 | else if (typeof this._source.playbackState !== 'number' || this._source.playbackState === 2) this._source.stop(); 592 | }, 593 | 594 | /** 595 | @method unmute 596 | **/ 597 | unmute: function() { 598 | if (!this._gainNode) this._source.volume = this._volume; 599 | else this._gainNode.gain.setValueAtTime(this._volume, this._context.currentTime); 600 | this.muted = false; 601 | }, 602 | 603 | /** 604 | @method _fade 605 | @param {Number} time 606 | @param {Number} to 607 | @private 608 | **/ 609 | _fade: function(time, to) { 610 | if (!this._buffer) return; 611 | time = (time || 1000) / 1000; 612 | 613 | var currTime = this._context.currentTime; 614 | var from = to === this.volume ? 0 : this._gainNode.gain.value; 615 | 616 | this._gainNode.gain.linearRampToValueAtTime(from, currTime); 617 | this._gainNode.gain.linearRampToValueAtTime(to, currTime + time); 618 | }, 619 | 620 | /** 621 | @method _onComplete 622 | @private 623 | **/ 624 | _onComplete: function() { 625 | game.audio.sounds.erase(this); 626 | if (this.paused) return; 627 | if (typeof this.onComplete === 'function') this.onComplete(); 628 | }, 629 | 630 | /** 631 | @method _onStart 632 | @private 633 | **/ 634 | _onStart: function() { 635 | this.stop(); 636 | game.audio.sounds.push(this); 637 | } 638 | }); 639 | 640 | game.defineProperties('Sound', { 641 | /** 642 | Duration of audio (seconds). 643 | @property {Number} duration 644 | **/ 645 | duration: { 646 | get: function() { 647 | if (this._buffer) return this._buffer.duration; 648 | } 649 | }, 650 | 651 | /** 652 | Playback rate of audio (speed). 653 | @property {Number} rate 654 | @default 1 655 | **/ 656 | rate: { 657 | get: function() { 658 | return this._rate; 659 | }, 660 | 661 | set: function(value) { 662 | this._rate = value; 663 | if (this._source) this._source.playbackRate.value = value; 664 | } 665 | }, 666 | 667 | /** 668 | Sound volume (0-1). 669 | @property {Number} volume 670 | @default 1 671 | **/ 672 | volume: { 673 | get: function() { 674 | return this._volume; 675 | }, 676 | 677 | set: function(value) { 678 | this._volume = value; 679 | if (this._gainNode) this._gainNode.gain.setValueAtTime(value, this._context.currentTime); 680 | } 681 | } 682 | }); 683 | 684 | /** 685 | @class Music 686 | @extends Sound 687 | @constructor 688 | @param {String} id Audio asset id 689 | **/ 690 | game.createClass('Music', 'Sound', { 691 | /** 692 | @property {Boolean} loop 693 | @default true 694 | **/ 695 | loop: true, 696 | 697 | init: function() { 698 | if (this._gainNode) this._gainNode.connect(game.audio._musicGain); 699 | }, 700 | 701 | _onStart: function() { 702 | if (game.audio.music) game.audio.music.stop(); 703 | game.audio.music = this; 704 | } 705 | }); 706 | 707 | }); 708 | -------------------------------------------------------------------------------- /src/engine/camera.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module camera 3 | **/ 4 | game.module( 5 | 'engine.camera' 6 | ) 7 | .body(function() { 8 | 9 | /** 10 | @class Camera 11 | @constructor 12 | @param {Container} [target] 13 | **/ 14 | game.createClass('Camera', { 15 | /** 16 | Camera acceleration speed. Higher is faster. 17 | @property {Number} acceleration 18 | @default 3 19 | **/ 20 | acceleration: 3, 21 | /** 22 | Limit the camera movement. 23 | @property {Rectangle} limit 24 | @default Infinity 25 | **/ 26 | limit: null, 27 | /** 28 | Camera maximum move speed. 29 | @property {Number} maxSpeed 30 | @default 200 31 | **/ 32 | maxSpeed: 200, 33 | /** 34 | Camera offset. 35 | @property {Vector} offset 36 | @default game.width / 2, game.height / 2 37 | **/ 38 | offset: null, 39 | /** 40 | Current position of the camera. 41 | @property {Vector} position 42 | **/ 43 | position: null, 44 | /** 45 | Round camera position. 46 | @property {Boolean} rounding 47 | @default true 48 | **/ 49 | rounding: true, 50 | /** 51 | Position of the camera sensor. 52 | @property {Vector} sensorPosition 53 | **/ 54 | sensorPosition: null, 55 | /** 56 | Size of the camera sensor. 57 | @property {Vector} sensorSize 58 | @default 200 59 | **/ 60 | sensorSize: null, 61 | /** 62 | Current speed of camera. 63 | @property {Vector} speed 64 | **/ 65 | speed: null, 66 | /** 67 | @property {Number} threshold 68 | **/ 69 | threshold: 1, 70 | /** 71 | Container, that the camera is moving. 72 | @property {Container} container 73 | @private 74 | **/ 75 | _container: null, 76 | /** 77 | Container, that camera follows. 78 | @property {Container} target 79 | @private 80 | **/ 81 | _target: null, 82 | 83 | staticInit: function(target) { 84 | this.limit = new game.Rectangle(Infinity, Infinity, -Infinity, -Infinity); 85 | this.position = new game.Vector(); 86 | this.speed = new game.Vector(); 87 | this.offset = new game.Vector(game.width / 2, game.height / 2); 88 | this.sensorPosition = new game.Vector(this.offset.x, this.offset.y); 89 | this.sensorSize = new game.Vector(200); 90 | if (target) this.setTarget(target); 91 | }, 92 | 93 | /** 94 | Add camera to container. 95 | @method addTo 96 | @param {Container} container 97 | @chainable 98 | **/ 99 | addTo: function(container) { 100 | this._container = container; 101 | this._container.position.set(-this.position.x, -this.position.y); 102 | this._setSensorPosition(); 103 | return this; 104 | }, 105 | 106 | /** 107 | Set container, that the camera follows. 108 | @method setTarget 109 | @param {Container} target 110 | **/ 111 | setTarget: function(target) { 112 | this._target = target; 113 | this._setSensorPosition(); 114 | }, 115 | 116 | /** 117 | Set camera position. 118 | @method setPosition 119 | @param {Number|Vector|Container} x 120 | @param {Number} [y] 121 | **/ 122 | setPosition: function(x, y) { 123 | if (x instanceof game.Vector || x instanceof game.Container) { 124 | y = x.y; 125 | x = x.x; 126 | } 127 | 128 | if (this._container) { 129 | x *= this._container.scale.x; 130 | y *= this._container.scale.y; 131 | } 132 | 133 | this._setPosition(x, y); 134 | }, 135 | 136 | /** 137 | Update camera position. 138 | @method update 139 | **/ 140 | update: function() { 141 | this._moveSensor(); 142 | this._moveCamera(); 143 | }, 144 | 145 | /** 146 | @method _moveCamera 147 | @private 148 | **/ 149 | _moveCamera: function() { 150 | this.speed.x = (this.position.x - this.sensorPosition.x + this.offset.x).limit(-this.maxSpeed, this.maxSpeed); 151 | this.speed.y = (this.position.y - this.sensorPosition.y + this.offset.y).limit(-this.maxSpeed, this.maxSpeed); 152 | 153 | if (this.speed.x > this.threshold || 154 | this.speed.x < -this.threshold || 155 | this.speed.y > this.threshold || 156 | this.speed.y < -this.threshold 157 | ) { 158 | this._setPosition( 159 | this.position.x + this.offset.x - this.speed.x * this.acceleration * game.delta, 160 | this.position.y + this.offset.y - this.speed.y * this.acceleration * game.delta 161 | ); 162 | } 163 | else { 164 | this.speed.set(0, 0); 165 | } 166 | }, 167 | 168 | /** 169 | @method _moveSensor 170 | @private 171 | **/ 172 | _moveSensor: function() { 173 | if (!this._target || !this._container) return; 174 | 175 | var targetWidth = this._target.width * this._container.scale.x; 176 | var targetHeight = this._target.height * this._container.scale.y; 177 | var targetPosX = (this._target.position.x - this._target.anchor.x + this._target.width / 2 * this._target.scale.x) * this._container.scale.x; 178 | var targetPosY = (this._target.position.y - this._target.anchor.y + this._target.height / 2 * this._target.scale.y) * this._container.scale.y; 179 | 180 | if (this.sensorSize.x < targetWidth || this.sensorSize.y < targetHeight) this.sensorSize.set(targetWidth, targetHeight); 181 | 182 | if (targetPosX < this.sensorPosition.x - this.sensorSize.x / 2 + targetWidth / 2) { 183 | this.sensorPosition.x = targetPosX + this.sensorSize.x / 2 - targetWidth / 2; 184 | } 185 | else if (targetPosX + (this.sensorSize.x / 2 + targetWidth / 2) > this.sensorPosition.x + this.sensorSize.x) { 186 | this.sensorPosition.x = targetPosX + (this.sensorSize.x / 2 + targetWidth / 2) - this.sensorSize.x; 187 | } 188 | 189 | if (targetPosY < this.sensorPosition.y - this.sensorSize.y / 2 + targetHeight / 2) { 190 | this.sensorPosition.y = targetPosY + this.sensorSize.y / 2 - targetHeight / 2; 191 | } 192 | else if (targetPosY + (this.sensorSize.y / 2 + targetHeight / 2) > this.sensorPosition.y + this.sensorSize.y) { 193 | this.sensorPosition.y = targetPosY + (this.sensorSize.y / 2 + targetHeight / 2) - this.sensorSize.y; 194 | } 195 | }, 196 | 197 | /** 198 | @method _setPosition 199 | @param {Number} x 200 | @param {Number} y 201 | @private 202 | **/ 203 | _setPosition: function(x, y) { 204 | this.position.set(x - this.offset.x, y - this.offset.y); 205 | 206 | if (this.position.x < this.limit.x) { 207 | this.position.x = this.limit.x; 208 | this.speed.x = 0; 209 | } 210 | else if (this.position.x > this.limit.width) { 211 | this.position.x = this.limit.width; 212 | this.speed.x = 0; 213 | } 214 | if (this.position.y < this.limit.y) { 215 | this.position.y = this.limit.y; 216 | this.speed.y = 0; 217 | } 218 | else if (this.position.y > this.limit.height) { 219 | this.position.y = this.limit.height; 220 | this.speed.y = 0; 221 | } 222 | 223 | if (this._container) { 224 | this._container.position.x = -(this.rounding ? (this.position.x + 0.5) | 0 : this.position.x); 225 | this._container.position.y = -(this.rounding ? (this.position.y + 0.5) | 0 : this.position.y); 226 | } 227 | }, 228 | 229 | /** 230 | @method _setSensorPosition 231 | @private 232 | **/ 233 | _setSensorPosition: function() { 234 | if (!this._target || !this._container) return; 235 | var targetPosX = (this._target.position.x - this._target.anchor.x + this._target.width / 2 * this._target.scale.x) * this._container.scale.x; 236 | var targetPosY = (this._target.position.y - this._target.anchor.y + this._target.height / 2 * this._target.scale.y) * this._container.scale.y; 237 | this.sensorPosition.set(targetPosX, targetPosY); 238 | } 239 | }); 240 | 241 | }); 242 | -------------------------------------------------------------------------------- /src/engine/geometry.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module geometry 3 | **/ 4 | game.module( 5 | 'engine.geometry' 6 | ) 7 | .body(function() { 8 | 9 | /** 10 | @class Arc 11 | @constructor 12 | @param {Number} radius 13 | @param {Number} x 14 | @param {Number} y 15 | @param {Number} startAngle 16 | @param {Number} endAngle 17 | **/ 18 | game.createClass('Arc', { 19 | /** 20 | @property {Number} endAngle 21 | @default 0 22 | **/ 23 | endAngle: 0, 24 | /** 25 | Radius of arc. 26 | @property {Number} radius 27 | @default 0 28 | **/ 29 | radius: 0, 30 | /** 31 | @property {Number} startAngle 32 | @default 0 33 | **/ 34 | startAngle: 0, 35 | /** 36 | @property {Number} x 37 | @default 0 38 | **/ 39 | x: 0, 40 | /** 41 | @property {Number} y 42 | @default 0 43 | **/ 44 | y: 0, 45 | 46 | staticInit: function(radius, x, y, startAngle, endAngle) { 47 | this.startAngle = startAngle || this.startAngle; 48 | this.endAngle = endAngle || this.endAngle; 49 | this.radius = radius || this.radius; 50 | this.x = x || this.x; 51 | this.y = y || this.y; 52 | } 53 | }); 54 | 55 | /** 56 | @class Circle 57 | @constructor 58 | @param {Number} radius 59 | @param {Number} x 60 | @param {Number} y 61 | **/ 62 | game.createClass('Circle', { 63 | /** 64 | Radius of circle. 65 | @property {Number} radius 66 | @default 0 67 | **/ 68 | radius: 0, 69 | /** 70 | @property {Number} x 71 | @default 0 72 | **/ 73 | x: 0, 74 | /** 75 | @property {Number} y 76 | @default 0 77 | **/ 78 | y: 0, 79 | 80 | staticInit: function(radius, x, y) { 81 | this.radius = radius || this.radius; 82 | this.x = x || this.x; 83 | this.y = y || this.y; 84 | } 85 | }); 86 | 87 | /** 88 | @class Curve 89 | @constructor 90 | @param {Number} sx 91 | @param {Number} sy 92 | @param {Number} ex 93 | @param {Number} ey 94 | @param {Number} h1x 95 | @param {Number} h1y 96 | @param {Number} h2x 97 | @param {Number} h2y 98 | **/ 99 | game.createClass('Curve', { 100 | /** 101 | End position of curve. 102 | @property {Vector} end 103 | **/ 104 | end: null, 105 | /** 106 | Position of first control point. 107 | @property {Vector} handle1 108 | **/ 109 | handle1: null, 110 | /** 111 | Position of second control point. 112 | @property {Vector} handle2 113 | **/ 114 | handle2: null, 115 | /** 116 | Start position of curve. 117 | @property {Vector} start 118 | **/ 119 | start: null, 120 | 121 | staticInit: function(sx, sy, ex, ey, h1x, h1y, h2x, h2y) { 122 | this.start = new game.Vector(sx, sy); 123 | if (typeof ex !== 'number') ex = sx; 124 | if (typeof ey !== 'number') ey = sy; 125 | this.end = new game.Vector(ex, ey); 126 | if (typeof h1x !== 'number') h1x = sx; 127 | if (typeof h1y !== 'number') h1y = sy; 128 | if (typeof h2x !== 'number') h2x = ex; 129 | if (typeof h2y !== 'number') h2y = ey; 130 | this.handle1 = new game.Vector(h1x, h1y); 131 | this.handle2 = new game.Vector(h2x, h2y); 132 | }, 133 | 134 | /** 135 | Get point from curve. 136 | @method point 137 | @param {Number} percent Location of the point. 0 is start and 1 is the end of the curve. 138 | @param {Vector} [out] Optional vector, where the values are set. 139 | @return {Vector} 140 | **/ 141 | point: function(percent, out) { 142 | out = out || new game.Vector(); 143 | 144 | var x = this._interpolate(percent, this.start.x, this.handle1.x, this.handle2.x, this.end.x); 145 | var y = this._interpolate(percent, this.start.y, this.handle1.y, this.handle2.y, this.end.y); 146 | 147 | out.set(x, y); 148 | return out; 149 | }, 150 | 151 | /** 152 | @method _calcHandle1 153 | @param {Number} t 154 | @param {Number} p 155 | @return {Number} 156 | @private 157 | **/ 158 | _calcHandle1: function(t, p) { 159 | var k = 1 - t; 160 | return 3 * k * k * t * p; 161 | }, 162 | 163 | /** 164 | @method _calcHandle2 165 | @param {Number} t 166 | @param {Number} p 167 | @return {Number} 168 | @private 169 | **/ 170 | _calcHandle2: function(t, p) { 171 | return 3 * (1 - t) * t * t * p; 172 | }, 173 | 174 | /** 175 | @method _calcEnd 176 | @param {Number} t 177 | @param {Number} p 178 | @return {Number} 179 | @private 180 | **/ 181 | _calcEnd: function(t, p) { 182 | return t * t * t * p; 183 | }, 184 | 185 | /** 186 | @method _calcStart 187 | @param {Number} t 188 | @param {Number} p 189 | @return {Number} 190 | @private 191 | **/ 192 | _calcStart: function(t, p) { 193 | var k = 1 - t; 194 | return k * k * k * p; 195 | }, 196 | 197 | /** 198 | Get point from curve. 199 | @method _interpolate 200 | @param {Number} percent 201 | @param {Number} s 202 | @param {Number} h1 203 | @param {Number} h2 204 | @param {Number} e 205 | @return {Number} 206 | @private 207 | **/ 208 | _interpolate: function(percent, s, h1, h2, e) { 209 | s = this._calcStart(percent, s); 210 | h1 = this._calcHandle1(percent, h1); 211 | h2 = this._calcHandle2(percent, h2); 212 | e = this._calcEnd(percent, e); 213 | return s + h1 + h2 + e; 214 | } 215 | }); 216 | 217 | /** 218 | @class Polygon 219 | @constructor 220 | @param {Array} points 221 | **/ 222 | game.createClass('Polygon', { 223 | /** 224 | List of points in polygon. Can be list of numbers or vectors. 225 | @property {Array} points 226 | **/ 227 | points: [], 228 | 229 | staticInit: function(points) { 230 | if (!points) return; 231 | for (var i = 0; i < points.length; i++) { 232 | if (points[i] instanceof game.Vector) { 233 | this.points.push(points[i]); 234 | } 235 | else if (typeof points[i] === 'number') { 236 | this.points.push(new game.Vector(points[i], points[i + 1])); 237 | i++; 238 | } 239 | else { 240 | // Bezier curve 241 | this.points.push(points[i]); 242 | } 243 | } 244 | }, 245 | 246 | /** 247 | Close polygon. 248 | @method close 249 | **/ 250 | close: function() { 251 | if (this.points[0] !== this.points[this.points.length - 1]) { 252 | this.points.push(this.points[0]); 253 | } 254 | } 255 | }); 256 | 257 | /** 258 | @class Rectangle 259 | @constructor 260 | @param {Number} width 261 | @param {Number} [height] 262 | @param {Number} [x] 263 | @param {Number} [y] 264 | **/ 265 | game.createClass('Rectangle', { 266 | /** 267 | @property {Number} height 268 | @default 0 269 | **/ 270 | height: 0, 271 | /** 272 | @property {Number} width 273 | @default 0 274 | **/ 275 | width: 0, 276 | /** 277 | @property {Number} x 278 | @default 0 279 | **/ 280 | x: 0, 281 | /** 282 | @property {Number} y 283 | @default 0 284 | **/ 285 | y: 0, 286 | 287 | staticInit: function(width, height, x, y) { 288 | this.set(width, height, x, y); 289 | }, 290 | 291 | /** 292 | @method set 293 | @param {Number} width 294 | @param {Number} height 295 | @param {Number} x 296 | @param {Number} y 297 | **/ 298 | set: function(width, height, x, y) { 299 | this.width = typeof width === 'number' ? width : this.width; 300 | this.height = typeof height === 'number' ? height : this.width; 301 | this.x = typeof x === 'number' ? x : this.x; 302 | this.y = typeof y === 'number' ? y : this.y; 303 | }, 304 | 305 | /** 306 | Swap width and height values. 307 | @method swap 308 | **/ 309 | swap: function() { 310 | var height = this.height; 311 | this.height = this.width; 312 | this.width = height; 313 | } 314 | }); 315 | 316 | /** 317 | Basic vector class with two values. 318 | @class Vector 319 | @constructor 320 | @param {Number} [x] 321 | @param {Number} [y] 322 | **/ 323 | game.createClass('Vector', { 324 | /** 325 | @property {Number} x 326 | @default 0 327 | **/ 328 | x: 0, 329 | /** 330 | @property {Number} y 331 | @default 0 332 | **/ 333 | y: 0, 334 | 335 | staticInit: function(x, y) { 336 | this.set(x, y); 337 | }, 338 | 339 | /** 340 | Add to vector values. 341 | @method add 342 | @param {Number|Vector} x 343 | @param {Number} [y] 344 | @chainable 345 | **/ 346 | add: function(x, y) { 347 | this.x += x instanceof game.Vector ? x.x : x; 348 | this.y += x instanceof game.Vector ? x.y : (y || ((y !== 0) ? x : 0)); 349 | return this; 350 | }, 351 | 352 | /** 353 | Get vector angle or angle between two vectors. 354 | @method angle 355 | @param {Vector} [vector] 356 | @return {Number} 357 | **/ 358 | angle: function(vector) { 359 | if (vector instanceof game.Vector) { 360 | return Math.atan2(vector.y - this.y, vector.x - this.x); 361 | } 362 | else { 363 | return Math.atan2(this.y, this.x); 364 | } 365 | }, 366 | 367 | /** 368 | Get angle between two vectors from origin. 369 | @method angleFromOrigin 370 | @param {Vector} vector 371 | @return {Number} 372 | **/ 373 | angleFromOrigin: function(vector) { 374 | return Math.atan2(vector.y, vector.x) - Math.atan2(this.y, this.x); 375 | }, 376 | 377 | /** 378 | Clone vector. 379 | @method clone 380 | @return {Vector} 381 | **/ 382 | clone: function() { 383 | return new this.constructor(this.x, this.y); 384 | }, 385 | 386 | /** 387 | Is values same with another vector. 388 | @method compare 389 | @param {Vector} vector 390 | @return {Boolean} 391 | **/ 392 | compare: function(vector) { 393 | return (this.x === vector.x && this.y === vector.y); 394 | }, 395 | 396 | /** 397 | Copy values from another vector. 398 | @method copy 399 | @param {Vector} vector 400 | @chainable 401 | **/ 402 | copy: function(vector) { 403 | this.x = vector.x; 404 | this.y = vector.y; 405 | return this; 406 | }, 407 | 408 | /** 409 | Get distance between two points. 410 | @method distance 411 | @param {Number|Vector} x 412 | @param {Number} [y] 413 | @chainable 414 | **/ 415 | distance: function(x, y) { 416 | var x1 = x instanceof game.Vector ? x.x : x; 417 | var y1 = x instanceof game.Vector ? x.y : (y || ((y !== 0) ? x : 0)); 418 | x1 = x1 - this.x; 419 | y1 = y1 - this.y; 420 | return Math.sqrt(x1 * x1 + y1 * y1); 421 | }, 422 | 423 | /** 424 | Divide vector values. 425 | @method divide 426 | @param {Number|Vector} x 427 | @param {Number} [y] 428 | @chainable 429 | **/ 430 | divide: function(x, y) { 431 | this.x /= x instanceof game.Vector ? x.x : x; 432 | this.y /= x instanceof game.Vector ? x.y : (y || ((y !== 0) ? x : 0)); 433 | return this; 434 | }, 435 | 436 | /** 437 | Get dot of vector. 438 | @method dot 439 | @param {Vector} [vector] 440 | @return {Number} 441 | **/ 442 | dot: function(vector) { 443 | if (vector instanceof game.Vector) return this.x * vector.x + this.y * vector.y; 444 | else return this.x * this.x + this.y * this.y; 445 | }, 446 | 447 | /** 448 | Get normalized dot of vector. 449 | @method dotNormalized 450 | @param {Vector} [vector] 451 | @return {Number} 452 | **/ 453 | dotNormalized: function(vector) { 454 | var len1 = this.length(); 455 | var x1 = this.x / len1; 456 | var y1 = this.y / len1; 457 | 458 | if (vector instanceof game.Vector) { 459 | var len2 = vector.length(); 460 | var x2 = vector.x / len2; 461 | var y2 = vector.y / len2; 462 | return x1 * x2 + y1 * y2; 463 | } 464 | else return x1 * x1 + y1 * y1; 465 | }, 466 | 467 | /** 468 | Get length of vector. 469 | @method length 470 | @return {Number} 471 | **/ 472 | length: function() { 473 | return Math.sqrt(this.dot()); 474 | }, 475 | 476 | /** 477 | Limit vector values. 478 | @method limit 479 | @param {Vector} vector 480 | @chainable 481 | **/ 482 | limit: function(vector) { 483 | this.x = this.x.limit(-vector.x, vector.x); 484 | this.y = this.y.limit(-vector.y, vector.y); 485 | return this; 486 | }, 487 | 488 | /** 489 | Change values based on distance and angle. 490 | @method move 491 | @param {Number} distance 492 | @param {Vector|Number} angle 493 | **/ 494 | move: function(distance, angle) { 495 | if (angle instanceof game.Vector) angle = this.angle(angle); 496 | this.x += distance * Math.cos(angle); 497 | this.y += distance * Math.sin(angle); 498 | }, 499 | 500 | /** 501 | Multiply vector values. 502 | @method multiply 503 | @param {Number|Vector} x 504 | @param {Number} [y] 505 | @chainable 506 | **/ 507 | multiply: function(x, y) { 508 | this.x *= x instanceof game.Vector ? x.x : x; 509 | this.y *= x instanceof game.Vector ? x.y : (y || ((y !== 0) ? x : 0)); 510 | return this; 511 | }, 512 | 513 | /** 514 | Multiply and add vector values. 515 | @method multiplyAdd 516 | @param {Number|Vector} x 517 | @param {Number} [y] 518 | @chainable 519 | **/ 520 | multiplyAdd: function(x, y) { 521 | this.x += x instanceof game.Vector ? x.x * y : x * y; 522 | this.y += x instanceof game.Vector ? x.y * y : x * y; 523 | return this; 524 | }, 525 | 526 | /** 527 | Normalize vector. 528 | @method normalize 529 | @chainable 530 | **/ 531 | normalize: function() { 532 | var len = this.length(); 533 | this.x /= len || 1; 534 | this.y /= len || 1; 535 | return this; 536 | }, 537 | 538 | /** 539 | Rotate vector in radians. 540 | @method rotate 541 | @param {Number} angle 542 | @chainable 543 | **/ 544 | rotate: function(angle) { 545 | var c = Math.cos(angle); 546 | var s = Math.sin(angle); 547 | var x = this.x * c - this.y * s; 548 | var y = this.y * c + this.x * s; 549 | this.x = x; 550 | this.y = y; 551 | return this; 552 | }, 553 | 554 | /** 555 | Round vector values. 556 | @method round 557 | @param {Number} [precision] 558 | @chainable 559 | **/ 560 | round: function(precision) { 561 | this.x = this.x.round(precision); 562 | this.y = this.y.round(precision); 563 | return this; 564 | }, 565 | 566 | /** 567 | Set vector values. 568 | @method set 569 | @param {Number} x 570 | @param {Number} [y] 571 | @chainable 572 | **/ 573 | set: function(x, y) { 574 | this.x = typeof x === 'number' ? x : this.x; 575 | this.y = typeof y === 'number' ? y : this.x; 576 | return this; 577 | }, 578 | 579 | /** 580 | Subtract from vector values. 581 | @method subtract 582 | @param {Number|Vector} x 583 | @param {Number} [y] 584 | @chainable 585 | **/ 586 | subtract: function(x, y) { 587 | this.x -= x instanceof game.Vector ? x.x : x; 588 | this.y -= x instanceof game.Vector ? x.y : (y || ((y !== 0) ? x : 0)); 589 | return this; 590 | }, 591 | 592 | /** 593 | Swap vector values with another vector. 594 | @method swap 595 | @param {Vector} target 596 | **/ 597 | swap: function(target) { 598 | var x = this.x; 599 | var y = this.y; 600 | this.copy(target); 601 | target.set(x, y); 602 | } 603 | }); 604 | 605 | }); 606 | -------------------------------------------------------------------------------- /src/engine/particle.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module particle 3 | **/ 4 | game.module( 5 | 'engine.particle' 6 | ) 7 | .require( 8 | 'engine.renderer.sprite', 9 | 'engine.renderer.fastcontainer' 10 | ) 11 | .body(function() { 12 | 13 | /** 14 | Particle emitter, that emits particles using Particle class. 15 | @class Particles 16 | @extends FastContainer 17 | @constructor 18 | @param {String|Array} textures Name of texture or array of texture names. 19 | @param {Object} [options] 20 | **/ 21 | game.createClass('Particles', 'FastContainer', { 22 | /** 23 | Acceleration angle in radians. 24 | @property {Number} accelAngle 25 | @default Math.PI / 2 26 | **/ 27 | accelAngle: Math.PI / 2, 28 | /** 29 | @property {Number} accelAngleVar 30 | @default 0 31 | **/ 32 | accelAngleVar: 0, 33 | /** 34 | Acceleration speed. 35 | @property {Number} accelSpeed 36 | @default 0 37 | **/ 38 | accelSpeed: 0, 39 | /** 40 | @property {Number} accelSpeedVar 41 | @default 0 42 | **/ 43 | accelSpeedVar: 0, 44 | /** 45 | Is emitter active. 46 | @property {Boolean} active 47 | @default true 48 | **/ 49 | active: true, 50 | /** 51 | End alpha for particle. 52 | @property {Number} alphaEnd 53 | @default 0 54 | **/ 55 | alphaEnd: 0, 56 | /** 57 | Starting alpha for particle. 58 | @property {Number} alphaStart 59 | @default 1 60 | **/ 61 | alphaStart: 1, 62 | /** 63 | Emit angle in radians (0 is right). 64 | @property {Number} angle 65 | @default 0 66 | **/ 67 | angle: 0, 68 | /** 69 | Variance of emit angle in radians. 70 | @property {Number} angleVar 71 | @default Math.PI 72 | **/ 73 | angleVar: Math.PI, 74 | /** 75 | How many particles to emit. 76 | @property {Number} emitCount 77 | @default 1 78 | **/ 79 | emitCount: 1, 80 | /** 81 | How long to emit particles in milliseconds (0 is forever). 82 | @property {Number} emitDuration 83 | @default 0 84 | **/ 85 | emitDuration: 0, 86 | /** 87 | How often to emit particles in milliseconds. 88 | @property {Number} emitRate 89 | @default 100 90 | **/ 91 | emitRate: 100, 92 | /** 93 | Particle's life in ms (0 is forever). 94 | @property {Number} life 95 | @default 2000 96 | **/ 97 | life: 2000, 98 | /** 99 | Particle's life variance. 100 | @property {Number} lifeVar 101 | @default 0 102 | **/ 103 | lifeVar: 0, 104 | /** 105 | @property {Function} onComplete 106 | **/ 107 | onComplete: null, 108 | /** 109 | List of particles. 110 | @property {Array} particles 111 | **/ 112 | particles: [], 113 | /** 114 | Particle start position. 115 | @property {Vector} startPos 116 | **/ 117 | startPos: null, 118 | /** 119 | Particle start position variance. 120 | @property {Vector} startPosVar 121 | **/ 122 | startPosVar: null, 123 | /** 124 | Particle start rotation. 125 | @property {Number} startRot 126 | **/ 127 | startRot: 0, 128 | /** 129 | Particle start rotation variance. 130 | @property {Number} startRotVar 131 | **/ 132 | startRotVar: 0, 133 | /** 134 | Use random texture from textures list. 135 | @property {Boolean} randomTexture 136 | **/ 137 | randomTexture: true, 138 | /** 139 | Particle's sprite rotation speed. 140 | @property {Number} rotate 141 | @default 0 142 | **/ 143 | rotate: 0, 144 | /** 145 | Variance for particle's sprite rotation speed. 146 | @property {Number} rotateVar 147 | @default 0 148 | **/ 149 | rotateVar: 0, 150 | /** 151 | @property {Number} scaleEnd 152 | @default 1 153 | **/ 154 | scaleEnd: 1, 155 | /** 156 | @property {Number} scaleEndVar 157 | @default 0 158 | **/ 159 | scaleEndVar: 0, 160 | /** 161 | Starting scale for particle. 162 | @property {Number} scaleStart 163 | @default 1 164 | **/ 165 | scaleStart: 1, 166 | /** 167 | @property {Number} scaleStartVar 168 | @default 0 169 | **/ 170 | scaleStartVar: 0, 171 | /** 172 | Particle's initial speed. 173 | @property {Number} speed 174 | @default 100 175 | **/ 176 | speed: 100, 177 | /** 178 | Variance for particle's initial speed. 179 | @property {Number} speedVar 180 | @default 0 181 | **/ 182 | speedVar: 0, 183 | /** 184 | Target position for particles. 185 | @property {Vector} target 186 | **/ 187 | target: null, 188 | /** 189 | Target positions force. 190 | @property {Number} targetForce 191 | @default 0 192 | **/ 193 | targetForce: 0, 194 | /** 195 | Update target position to all particles every frame. If this is false and you change target, it will affect only particles created after the change. 196 | @property {Boolean} targetUpdate 197 | @default true 198 | **/ 199 | targetUpdate: true, 200 | /** 201 | List of textures. 202 | @property {Array} textures 203 | **/ 204 | textures: [], 205 | /** 206 | @property {Vector} velocityLimit 207 | @default 0,0 208 | **/ 209 | velocityLimit: null, 210 | /** 211 | Particle's velocity rotation speed. 212 | @property {Number} velRotate 213 | @default 0 214 | **/ 215 | velRotate: 0, 216 | /** 217 | Variance for particle's velocity rotation speed. 218 | @property {Number} velRotateVar 219 | @default 0 220 | **/ 221 | velRotateVar: 0, 222 | /** 223 | @property {Number} _currentTexture 224 | @private 225 | **/ 226 | _currentTexture: 0, 227 | /** 228 | @property {Number} _durationTimer 229 | @private 230 | **/ 231 | _durationTimer: 0, 232 | /** 233 | @property {Boolean} _onCompleteCalled 234 | @private 235 | **/ 236 | _onCompleteCalled: false, 237 | /** 238 | @property {String} poolName 239 | @private 240 | **/ 241 | _poolName: null, 242 | /** 243 | @property {Number} _rateTimer 244 | @private 245 | **/ 246 | _rateTimer: 0, 247 | 248 | staticInit: function(textures, options) { 249 | this.super(); 250 | if (textures) { 251 | if (typeof textures === 'string') this.textures.push(textures); 252 | else this.textures = textures; 253 | } 254 | this._poolName = game.Particles.poolName; 255 | this.startPos = new game.Vector(); 256 | this.startPosVar = new game.Vector(); 257 | this.target = new game.Vector(); 258 | this.velocityLimit = new game.Vector(); 259 | game.pool.create(this._poolName); 260 | game.merge(this, options); 261 | }, 262 | 263 | /** 264 | Emit particles. 265 | @method emit 266 | @param {Number} [count] 267 | **/ 268 | emit: function(count) { 269 | count = count || this.emitCount; 270 | for (var i = 0; i < count; i++) { 271 | this._addParticle(); 272 | } 273 | }, 274 | 275 | remove: function() { 276 | this.super(); 277 | this._remove = true; 278 | }, 279 | 280 | /** 281 | Reset emitter timer. 282 | @method reset 283 | **/ 284 | reset: function() { 285 | this._rateTimer = 0; 286 | this._durationTimer = 0; 287 | this.active = true; 288 | this._onCompleteCalled = false; 289 | }, 290 | 291 | updateTransform: function() { 292 | if (this._remove) { 293 | for (var i = this.children.length - 1; i >= 0; i--) { 294 | this._removeParticle(this.children[i]); 295 | } 296 | return; 297 | } 298 | 299 | this._durationTimer += game.delta * 1000; 300 | if (this.emitDuration > 0) { 301 | this.active = this._durationTimer < this.emitDuration; 302 | if (!this.active && this.children.length === 0 && typeof this.onComplete === 'function' && !this._onCompleteCalled) { 303 | this.onComplete(); 304 | this._onCompleteCalled = true; 305 | } 306 | } 307 | 308 | if (this.emitRate && this.active) { 309 | this._rateTimer += game.delta * 1000; 310 | if (this._rateTimer >= 0) { 311 | this._rateTimer = -this.emitRate; 312 | this.emit(); 313 | } 314 | } 315 | 316 | for (var i = this.children.length - 1; i >= 0; i--) { 317 | this.children[i]._update(); 318 | } 319 | 320 | this.super(); 321 | }, 322 | 323 | /** 324 | @method _addParticle 325 | @private 326 | **/ 327 | _addParticle: function() { 328 | if (this.randomTexture) { 329 | var texture = this.textures.random(); 330 | } 331 | else { 332 | var texture = this.textures[this._currentTexture]; 333 | this._currentTexture++; 334 | if (this._currentTexture >= this.textures.length) this._currentTexture = 0; 335 | } 336 | if (!texture) return; 337 | 338 | var particle = game.pool.get(this._poolName); 339 | 340 | if (!particle) particle = new game.Particle(texture); 341 | else particle.setTexture(texture); 342 | 343 | particle.emitter = this; 344 | particle.rotation = this.startRot + this._getVar(this.startRotVar); 345 | particle.alpha = this.alphaStart; 346 | particle.position.x = this.startPos.x + this._getVar(this.startPosVar.x); 347 | particle.position.y = this.startPos.y + this._getVar(this.startPosVar.y); 348 | 349 | var angleVar = this._getVar(this.angleVar); 350 | var angle = this.angle + angleVar; 351 | var speed = this.speed + this._getVar(this.speedVar); 352 | 353 | particle.velocity.x = Math.cos(angle) * speed; 354 | particle.velocity.y = Math.sin(angle) * speed; 355 | 356 | if (this.angleVar !== this.accelAngleVar) angleVar = this._getVar(this.accelAngleVar); 357 | angle = this.accelAngle + angleVar; 358 | speed = this.accelSpeed + this._getVar(this.accelSpeedVar); 359 | 360 | particle.accel.x = Math.cos(angle) * speed; 361 | particle.accel.y = Math.sin(angle) * speed; 362 | 363 | particle.life = this.life + this._getVar(this.lifeVar); 364 | particle.rotateAmount = this.rotate + this._getVar(this.rotateVar); 365 | particle.velRotate = this.velRotate + this._getVar(this.velRotateVar); 366 | 367 | if (this.alphaStart !== this.alphaEnd) { 368 | particle.deltaAlpha = this.alphaEnd - this.alphaStart; 369 | particle.deltaAlpha /= particle.life / 1000; 370 | } 371 | else particle.deltaAlpha = 0; 372 | 373 | var scaleStart = this.scaleStart + this._getVar(this.scaleStartVar); 374 | if (this.scaleStart !== this.scaleEnd) { 375 | particle.deltaScale = (this.scaleEnd + this._getVar(this.scaleEndVar)) - scaleStart; 376 | particle.deltaScale /= particle.life / 1000; 377 | } 378 | else particle.deltaScale = 0; 379 | particle.scale.set(scaleStart); 380 | 381 | particle.anchor.x = particle.texture.width / 2; 382 | particle.anchor.y = particle.texture.height / 2; 383 | particle.target.copy(this.target); 384 | 385 | this.addChild(particle); 386 | }, 387 | 388 | /** 389 | @method _getVar 390 | @param {Number} value 391 | @return {Number} 392 | @private 393 | **/ 394 | _getVar: function(value) { 395 | return (Math.random() * value) * (Math.random() > 0.5 ? -1 : 1); 396 | }, 397 | 398 | /** 399 | Remove particle from emitter. 400 | @method _removeParticle 401 | @param {Particle} particle 402 | @private 403 | **/ 404 | _removeParticle: function(particle) { 405 | particle.remove(); 406 | game.pool.put(this._poolName, particle); 407 | } 408 | }); 409 | 410 | game.addAttributes('Particles', { 411 | /** 412 | @attribute {String} poolName 413 | @default particle 414 | **/ 415 | poolName: 'particle' 416 | }); 417 | 418 | /** 419 | Particle sprite, that is emitted from Particles class. 420 | @class Particle 421 | @extends Sprite 422 | **/ 423 | game.createClass('Particle', 'Sprite', { 424 | /** 425 | @property {Vector} accel 426 | **/ 427 | accel: null, 428 | /** 429 | @property {Number} deltaScale 430 | @default 0 431 | **/ 432 | deltaScale: 0, 433 | /** 434 | @property {Number} deltaAlpha 435 | @default 0 436 | **/ 437 | deltaAlpha: 0, 438 | /** 439 | Particle's emitter. 440 | @property {Emitter} emitter 441 | **/ 442 | emitter: null, 443 | /** 444 | @property {Number} life 445 | @default 0 446 | **/ 447 | life: 0, 448 | /** 449 | @property {Number} rotateAmount 450 | @default 0 451 | **/ 452 | rotateAmount: 0, 453 | /** 454 | @property {Vector} target 455 | **/ 456 | target: null, 457 | /** 458 | @property {Vector} velocity 459 | **/ 460 | velocity: null, 461 | /** 462 | @property {Number} velRotate 463 | @default 0 464 | **/ 465 | velRotate: 0, 466 | 467 | staticInit: function(texture) { 468 | this.super(texture); 469 | this.accel = new game.Vector(); 470 | this.target = new game.Vector(); 471 | this.velocity = new game.Vector(); 472 | }, 473 | 474 | /** 475 | @method _update 476 | @private 477 | **/ 478 | _update: function() { 479 | if (!this.emitter) return; 480 | 481 | if (this.life > 0) { 482 | this.life -= game.delta * 1000; 483 | if (this.life <= 0) return this.emitter._removeParticle(this); 484 | } 485 | 486 | if (this.emitter.targetForce > 0) { 487 | var target = this.emitter.targetUpdate ? this.emitter.target : this.target; 488 | this.accel.set(target.x - this.position.x, target.y - this.position.y); 489 | var len = Math.sqrt(this.accel.x * this.accel.x + this.accel.y * this.accel.y); 490 | this.accel.x /= len || 1; 491 | this.accel.y /= len || 1; 492 | this.accel.x *= this.emitter.targetForce; 493 | this.accel.y *= this.emitter.targetForce; 494 | } 495 | 496 | this.velocity.x += this.accel.x * game.delta; 497 | this.velocity.y += this.accel.y * game.delta; 498 | 499 | if (this.emitter.velocityLimit.x > 0) { 500 | if (this.velocity.x > this.emitter.velocityLimit.x) this.velocity.x = this.emitter.velocityLimit.x; 501 | if (this.velocity.x < -this.emitter.velocityLimit.x) this.velocity.x = -this.emitter.velocityLimit.x; 502 | } 503 | 504 | if (this.emitter.velocityLimit.y > 0) { 505 | if (this.velocity.y > this.emitter.velocityLimit.y) this.velocity.y = this.emitter.velocityLimit.y; 506 | if (this.velocity.y < -this.emitter.velocityLimit.y) this.velocity.y = -this.emitter.velocityLimit.y; 507 | } 508 | 509 | if (this.velRotate) { 510 | var c = Math.cos(this.velRotate * game.delta); 511 | var s = Math.sin(this.velRotate * game.delta); 512 | var x = this.velocity.x * c - this.velocity.y * s; 513 | var y = this.velocity.y * c + this.velocity.x * s; 514 | this.velocity.x = x; 515 | this.velocity.y = y; 516 | } 517 | 518 | this.position.x += this.velocity.x * game.delta; 519 | this.position.y += this.velocity.y * game.delta; 520 | 521 | if (this.deltaAlpha) { 522 | this.alpha = Math.max(0, this.alpha + this.deltaAlpha * game.delta); 523 | } 524 | 525 | if (this.deltaScale) { 526 | this.scale.x += this.deltaScale * game.delta; 527 | this.scale.y += this.deltaScale * game.delta; 528 | } 529 | 530 | if (this.rotateAmount) { 531 | this.rotation += this.rotateAmount * game.delta; 532 | } 533 | } 534 | }); 535 | 536 | }); 537 | -------------------------------------------------------------------------------- /src/engine/physics.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module physics 3 | **/ 4 | game.module( 5 | 'engine.physics' 6 | ) 7 | .require( 8 | 'engine.renderer.sprite' 9 | ) 10 | .body(function() { 11 | 12 | /** 13 | Physics body. 14 | @class Body 15 | @constructor 16 | @param {Object} [properties] 17 | **/ 18 | game.createClass('Body', { 19 | /** 20 | Group numbers that body collides against. 21 | @property {Array} collideAgainst 22 | @default [] 23 | **/ 24 | collideAgainst: [], 25 | /** 26 | Body's damping. Should be number between 0 and 1. 27 | @property {Number} damping 28 | @default 0 29 | **/ 30 | damping: 0, 31 | /** 32 | Body's force. 33 | @property {Vector} force 34 | @default 0,0 35 | **/ 36 | force: null, 37 | /** 38 | Body's position on last frame. 39 | @property {Vector} last 40 | **/ 41 | last: null, 42 | /** 43 | Body's mass. 44 | @property {Number} mass 45 | @default 1 46 | **/ 47 | mass: 1, 48 | /** 49 | Position of body. 50 | @property {Vector} position 51 | **/ 52 | position: null, 53 | /** 54 | Body's shape. 55 | @property {Rectangle|Circle} shape 56 | **/ 57 | shape: null, 58 | /** 59 | Is body static. 60 | @property {Boolean} static 61 | @default false 62 | **/ 63 | static: false, 64 | /** 65 | Body's velocity. 66 | @property {Vector} velocity 67 | **/ 68 | velocity: null, 69 | /** 70 | Body's maximum velocity. 71 | @property {Vector} velocityLimit 72 | @default 980,980 73 | **/ 74 | velocityLimit: null, 75 | /** 76 | Body's physic world. 77 | @property {World} world 78 | **/ 79 | world: null, 80 | /** 81 | @property {Array} _collides 82 | @private 83 | **/ 84 | _collides: [], 85 | /** 86 | @property {Number} _collisionGroup 87 | @private 88 | **/ 89 | _collisionGroup: 0, 90 | 91 | staticInit: function() { 92 | this.force = new game.Vector(); 93 | this.position = new game.Vector(); 94 | this.velocity = new game.Vector(); 95 | this.velocityLimit = new game.Vector(980, 980); 96 | this.last = new game.Vector(); 97 | }, 98 | 99 | init: function(properties) { 100 | game.merge(this, properties); 101 | return true; 102 | }, 103 | 104 | /** 105 | Add shape to body. 106 | @method addShape 107 | @param {Rectangle|Circle} shape 108 | @chainable 109 | **/ 110 | addShape: function(shape) { 111 | this.shape = shape; 112 | return this; 113 | }, 114 | 115 | /** 116 | Add body to world. 117 | @method addTo 118 | @param {World} world 119 | @chainable 120 | **/ 121 | addTo: function(world) { 122 | if (this.world) return; 123 | world.addBody(this); 124 | return this; 125 | }, 126 | 127 | /** 128 | This is called after hit response. 129 | @method afterCollide 130 | @param {Body} body body that it collided with. 131 | **/ 132 | afterCollide: function() { 133 | }, 134 | 135 | /** 136 | Apply impulse to body. 137 | @method applyImpulse 138 | @param {Number|Vector} x 139 | @param {Number} [y] 140 | @chainable 141 | **/ 142 | applyImpulse: function(x, y) { 143 | this.velocity.add(x, y); 144 | return this; 145 | }, 146 | 147 | /** 148 | This is called, when body collides with another body. 149 | @method collide 150 | @param {Body} body body that it collided with. 151 | @param {String} dir direction of collision. 152 | @return {Boolean} Return true, to apply hit response. 153 | **/ 154 | collide: function() { 155 | return true; 156 | }, 157 | 158 | /** 159 | Remove body from it's world. 160 | @method remove 161 | @chainable 162 | **/ 163 | remove: function() { 164 | if (this.world) this.world.removeBody(this); 165 | return this; 166 | }, 167 | 168 | /** 169 | Remove collision from body. 170 | @method removeCollision 171 | @chainable 172 | **/ 173 | removeCollision: function() { 174 | if (this.world) this.world._removeBodyCollision(this); 175 | return this; 176 | }, 177 | 178 | /** 179 | Update body position and velocity. 180 | @method update 181 | @param {Number} [delta] 182 | **/ 183 | update: function(delta) { 184 | delta = delta || game.delta; 185 | this.last.copy(this.position); 186 | 187 | if (this.static) return; 188 | 189 | if (this.world) { 190 | this.velocity.x += this.world.gravity.x * this.mass * delta; 191 | this.velocity.y += this.world.gravity.y * this.mass * delta; 192 | } 193 | this.velocity.x += this.force.x * delta; 194 | this.velocity.y += this.force.y * delta; 195 | 196 | if (this.damping > 0 && this.damping < 1) { 197 | var damping = Math.pow(1 - this.damping, delta); 198 | this.velocity.x *= damping; 199 | this.velocity.y *= damping; 200 | } 201 | 202 | if (this.velocityLimit.x > 0) { 203 | if (this.velocity.x > this.velocityLimit.x) this.velocity.x = this.velocityLimit.x; 204 | if (this.velocity.x < -this.velocityLimit.x) this.velocity.x = -this.velocityLimit.x; 205 | } 206 | if (this.velocityLimit.y > 0) { 207 | if (this.velocity.y > this.velocityLimit.y) this.velocity.y = this.velocityLimit.y; 208 | if (this.velocity.y < -this.velocityLimit.y) this.velocity.y = -this.velocityLimit.y; 209 | } 210 | 211 | this.position.x += this.velocity.x * delta; 212 | this.position.y += this.velocity.y * delta; 213 | } 214 | }); 215 | 216 | game.defineProperties('Body', { 217 | /** 218 | Collision group for body. 219 | @property {Number} collisionGroup 220 | @default 0 221 | **/ 222 | collisionGroup: { 223 | get: function() { 224 | return this._collisionGroup; 225 | }, 226 | 227 | set: function(value) { 228 | if (this._collisionGroup === value) return; 229 | 230 | if (this.world && typeof this._collisionGroup === 'number') this.world._removeBodyCollision(this); 231 | this._collisionGroup = value; 232 | if (this.world) this.world._addBodyCollision(this); 233 | } 234 | } 235 | }); 236 | 237 | /** 238 | Physics world. 239 | @class Physics 240 | @constructor 241 | @param {Number} [x] Gravity x 242 | @param {Number} [y] Gravity y 243 | @param {Boolean} [manualUpdate] Don't update physics automatically 244 | **/ 245 | game.createClass('Physics', { 246 | /** 247 | List of bodies in world. 248 | @property {Array} bodies 249 | **/ 250 | bodies: [], 251 | /** 252 | Gravity of physics world. 253 | @property {Vector} gravity 254 | @default 0,980 255 | **/ 256 | gravity: null, 257 | /** 258 | @property {Object} _collisionGroups 259 | @private 260 | **/ 261 | _collisionGroups: {}, 262 | 263 | staticInit: function(x, y, manualUpdate) { 264 | x = typeof x === 'number' ? x : 0; 265 | y = typeof y === 'number' ? y : 980; 266 | this.gravity = new game.Vector(x, y); 267 | if (game.scene && !manualUpdate) game.scene.physics.push(this); 268 | }, 269 | 270 | /** 271 | Add body to world. 272 | @method addBody 273 | @param {Body} body 274 | **/ 275 | addBody: function(body) { 276 | body.world = this; 277 | body._remove = false; 278 | this.bodies.push(body); 279 | this._addBodyCollision(body); 280 | }, 281 | 282 | /** 283 | Perform collision for body. 284 | @method collide 285 | @param {Body} body 286 | **/ 287 | collide: function(body) { 288 | var g, i, b, group; 289 | 290 | for (g = 0; g < body.collideAgainst.length; g++) { 291 | body._collides.length = 0; 292 | group = this._collisionGroups[body.collideAgainst[g]]; 293 | 294 | if (!group) continue; 295 | 296 | for (i = group.length - 1; i >= 0; i--) { 297 | if (!group) break; 298 | b = group[i]; 299 | if (body !== b) { 300 | if (this.hitTest(body, b)) { 301 | body._collides.push(b); 302 | } 303 | } 304 | } 305 | for (i = body._collides.length - 1; i >= 0; i--) { 306 | if (this.hitResponse(body, body._collides[i])) { 307 | body.afterCollide(body._collides[i]); 308 | } 309 | } 310 | } 311 | }, 312 | 313 | /** 314 | Hit response a versus b. 315 | @method hitResponse 316 | @param {Body} a 317 | @param {Body} b 318 | @return {Boolean} Returns true, if body is moved. 319 | **/ 320 | hitResponse: function(a, b) { 321 | if (a.static) return false; 322 | if (a.shape.width && b.shape.width) { 323 | if (a.last.y + a.shape.height / 2 <= b.last.y - b.shape.height / 2) { 324 | if (a.collide(b, 'DOWN')) { 325 | a.position.y = b.position.y - b.shape.height / 2 - a.shape.height / 2; 326 | return true; 327 | } 328 | } 329 | else if (a.last.y - a.shape.height / 2 >= b.last.y + b.shape.height / 2) { 330 | if (a.collide(b, 'UP')) { 331 | a.position.y = b.position.y + b.shape.height / 2 + a.shape.height / 2; 332 | return true; 333 | } 334 | } 335 | else if (a.last.x + a.shape.width / 2 <= b.last.x - b.shape.width / 2) { 336 | if (a.collide(b, 'RIGHT')) { 337 | a.position.x = b.position.x - b.shape.width / 2 - a.shape.width / 2; 338 | return true; 339 | } 340 | } 341 | else if (a.last.x - a.shape.width / 2 >= b.last.x + b.shape.width / 2) { 342 | if (a.collide(b, 'LEFT')) { 343 | a.position.x = b.position.x + b.shape.width / 2 + a.shape.width / 2; 344 | return true; 345 | } 346 | } 347 | else { 348 | // Inside 349 | if (a.collide(b)) return true; 350 | } 351 | } 352 | else if (a.shape.radius && b.shape.radius) { 353 | var angle = b.position.angle(a.position); 354 | if (a.collide(b, angle)) { 355 | var dist = a.shape.radius + b.shape.radius; 356 | a.position.x = b.position.x + Math.cos(angle) * dist; 357 | a.position.y = b.position.y + Math.sin(angle) * dist; 358 | return true; 359 | } 360 | } 361 | else { 362 | if (a.collide(b)) return true; 363 | } 364 | return false; 365 | }, 366 | 367 | /** 368 | Hit test a versus b. 369 | @method hitTest 370 | @param {Body} a 371 | @param {Body} b 372 | @return {Boolean} return true, if bodies hit. 373 | **/ 374 | hitTest: function(a, b) { 375 | if (a.shape.width && b.shape.width) { 376 | return !( 377 | a.position.y + a.shape.height / 2 <= b.position.y - b.shape.height / 2 || 378 | a.position.y - a.shape.height / 2 >= b.position.y + b.shape.height / 2 || 379 | a.position.x - a.shape.width / 2 >= b.position.x + b.shape.width / 2 || 380 | a.position.x + a.shape.width / 2 <= b.position.x - b.shape.width / 2 381 | ); 382 | } 383 | if (a.shape.radius && b.shape.radius) { 384 | return (a.shape.radius + b.shape.radius > a.position.distance(b.position)); 385 | } 386 | if (a.shape.width && b.shape.radius || a.shape.radius && b.shape.width) { 387 | var rect = a.shape.width ? a : b; 388 | var circle = a.shape.radius ? a : b; 389 | 390 | var x = Math.max(rect.position.x - rect.shape.width / 2, Math.min(rect.position.x + rect.shape.width / 2, circle.position.x)); 391 | var y = Math.max(rect.position.y - rect.shape.height / 2, Math.min(rect.position.y + rect.shape.height / 2, circle.position.y)); 392 | 393 | var dist = Math.pow(circle.position.x - x, 2) + Math.pow(circle.position.y - y, 2); 394 | return dist < (circle.shape.radius * circle.shape.radius); 395 | } 396 | return false; 397 | }, 398 | 399 | /** 400 | Remove body from world. 401 | @method removeBody 402 | @param {Body} body 403 | **/ 404 | removeBody: function(body) { 405 | if (!body.world) return; 406 | body.world = null; 407 | body._remove = true; 408 | }, 409 | 410 | /** 411 | @method _addBodyCollision 412 | @param {Body} body 413 | @private 414 | **/ 415 | _addBodyCollision: function(body) { 416 | if (typeof body.collisionGroup !== 'number') return; 417 | this._collisionGroups[body.collisionGroup] = this._collisionGroups[body.collisionGroup] || []; 418 | if (this._collisionGroups[body.collisionGroup].indexOf(body) !== -1) return; 419 | this._collisionGroups[body.collisionGroup].push(body); 420 | }, 421 | 422 | /** 423 | @method _removeBodyCollision 424 | @param {Body} body 425 | @private 426 | **/ 427 | _removeBodyCollision: function(body) { 428 | if (typeof body.collisionGroup !== 'number') return; 429 | if (!this._collisionGroups[body.collisionGroup]) return; 430 | if (this._collisionGroups[body.collisionGroup].indexOf(body) === -1) return; 431 | this._collisionGroups[body.collisionGroup].erase(body); 432 | }, 433 | 434 | /** 435 | @method _update 436 | @private 437 | **/ 438 | _update: function() { 439 | var i, j; 440 | for (i = this.bodies.length - 1; i >= 0; i--) { 441 | if (this.bodies[i]._remove) { 442 | this._removeBodyCollision(this.bodies[i]); 443 | this.bodies.splice(i, 1); 444 | } 445 | else { 446 | this.bodies[i].update(); 447 | } 448 | } 449 | }, 450 | 451 | /** 452 | @method _updateCollision 453 | @private 454 | **/ 455 | _updateCollision: function() { 456 | for (i in this._collisionGroups) { 457 | if (this._collisionGroups[i].length === 0) { 458 | delete this._collisionGroups[i]; 459 | continue; 460 | } 461 | for (j = 0; j < this._collisionGroups[i].length; j++) { 462 | if (this._collisionGroups[i][j] && this._collisionGroups[i][j].collideAgainst.length > 0) { 463 | this.collide(this._collisionGroups[i][j]); 464 | } 465 | } 466 | } 467 | } 468 | }); 469 | 470 | /** 471 | @class PhysicsSprite 472 | @extends Sprite 473 | @constructor 474 | @param {Texture|String} texture 475 | @param {Number} [width|radius] 476 | @param {Number} [height] 477 | **/ 478 | game.createClass('PhysicsSprite', 'Sprite', { 479 | /** 480 | @property {Body} body 481 | **/ 482 | body: null, 483 | /** 484 | @property {String} shape 485 | @default Rectangle 486 | **/ 487 | shape: 'Rectangle', 488 | 489 | staticInit: function(texture, width, height) { 490 | this.super(texture); 491 | this.anchorCenter(); 492 | width = width || this.width; 493 | height = height || this.height; 494 | var shape = new game[this.shape](width, height); 495 | this.body = new game.Body(); 496 | this.body.addShape(shape); 497 | this.body.collide = this.collide.bind(this); 498 | this.position = this.body.position; 499 | }, 500 | 501 | /** 502 | @method addTo 503 | @param {Container} container 504 | @param {Physics} physics 505 | **/ 506 | addTo: function(container, physics) { 507 | physics = physics || game.scene.world; 508 | if (!physics) throw 'addTo: Physics world not defined'; 509 | this.body.addTo(physics); 510 | return this.super(container); 511 | }, 512 | 513 | /** 514 | This is called, when body collides with another body. 515 | @method collide 516 | @param {Body} body body that it collided with. 517 | @param {String} dir direction of collision. 518 | @return {Boolean} Return true, to apply hit response. 519 | **/ 520 | collide: function() { 521 | return true; 522 | }, 523 | 524 | /** 525 | Removes sprite from it's parent and also body from it's physics world. 526 | @method remove 527 | **/ 528 | remove: function() { 529 | this.body.remove(); 530 | return this.super(); 531 | } 532 | }); 533 | 534 | }); 535 | -------------------------------------------------------------------------------- /src/engine/pool.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module pool 3 | **/ 4 | game.module( 5 | 'engine.pool' 6 | ) 7 | .body(function() { 8 | 9 | /** 10 | Pool manager. Instance automatically created at `game.pool` 11 | @class Pool 12 | **/ 13 | game.createClass('Pool', { 14 | /** 15 | @property {Object} pools 16 | **/ 17 | pools: {}, 18 | 19 | /** 20 | Create new pool. 21 | @method create 22 | @param {String} pool Name of the pool. 23 | @return {Boolean} Returns false, if pool already exists. 24 | **/ 25 | create: function(pool) { 26 | if (!this.pools[pool]) { 27 | this.pools[pool] = []; 28 | return true; 29 | } 30 | return false; 31 | }, 32 | 33 | /** 34 | Get object from pool. 35 | @method get 36 | @param {String} pool Name of the pool. 37 | @return {Object} 38 | **/ 39 | get: function(pool) { 40 | if (this.pools[pool] && this.pools[pool].length) { 41 | return this.pools[pool].pop(); 42 | } 43 | }, 44 | 45 | /** 46 | Put object to pool. 47 | @method put 48 | @param {String} pool Name of the pool. 49 | @param {Object} object Object to put to the pool. 50 | @return {Boolean} Returns false, if pool not found. 51 | **/ 52 | put: function(pool, object) { 53 | if (!this.pools[pool]) return false; 54 | this.pools[pool].push(object); 55 | return true; 56 | } 57 | }); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /src/engine/renderer/animation.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module renderer.animation 3 | **/ 4 | game.module( 5 | 'engine.renderer.animation' 6 | ) 7 | .require( 8 | 'engine.renderer.sprite' 9 | ) 10 | .body(function() { 11 | 12 | /** 13 | Animation that is generated from multiple textures. Animation can also contain multiple animations created with addAnim method. 14 | @class Animation 15 | @extends Sprite 16 | @constructor 17 | @param {Array|String} textures Array of textures or name of atlas file 18 | **/ 19 | game.createClass('Animation', 'Sprite', { 20 | /** 21 | List of animations. 22 | @property {Object} anims 23 | **/ 24 | anims: {}, 25 | /** 26 | Current active animation. 27 | @property {Animation} currentAnim 28 | **/ 29 | currentAnim: null, 30 | /** 31 | Current frame index. 32 | @property {Number} currentFrame 33 | @default 0 34 | **/ 35 | currentFrame: 0, 36 | /** 37 | Is animation looping. 38 | @property {Boolean} loop 39 | @default true 40 | **/ 41 | loop: true, 42 | /** 43 | Function that is called, when animation is completed. 44 | @property {Function} onComplete 45 | **/ 46 | onComplete: null, 47 | /** 48 | Is animation playing. 49 | @property {Boolean} playing 50 | @default false 51 | **/ 52 | playing: false, 53 | /** 54 | Play animation in random order. 55 | @property {Boolean} random 56 | @default false 57 | **/ 58 | random: false, 59 | /** 60 | Play animation in reverse. 61 | @property {Boolean} reverse 62 | @default false 63 | **/ 64 | reverse: false, 65 | /** 66 | Animation speed (frames per second). 67 | @property {Number} speed 68 | @default 10 69 | **/ 70 | speed: 10, 71 | /** 72 | List of textures. 73 | @property {Array} textures 74 | **/ 75 | textures: null, 76 | /** 77 | @property {Number} _frameTime 78 | @private 79 | **/ 80 | _frameTime: 0, 81 | 82 | staticInit: function(textures) { 83 | this.currentAnim = this; 84 | this.textures = this.textures || textures; 85 | if (!this.textures || this.textures.length === 0) throw 'Unable to create animation without textures'; 86 | 87 | if (typeof this.textures === 'string' && this.textures.indexOf('atlas') !== -1) { 88 | var json = game.getJSON(this.textures); 89 | this.textures = []; 90 | for (var name in json.frames) { 91 | this.textures.push(name); 92 | } 93 | } 94 | 95 | var newTextures = []; 96 | for (var i = 0; i < this.textures.length; i++) { 97 | var texture = this.textures[i]; 98 | if (!texture instanceof game.Texture) texture = game.Texture.fromAsset(texture); 99 | newTextures.push(texture); 100 | } 101 | this.textures = newTextures; 102 | 103 | this.super(this.textures[0]); 104 | }, 105 | 106 | /** 107 | Add new animation. 108 | @method addAnim 109 | @param {String} name Name of animation. 110 | @param {Array|Number|String} frames List of invidual frame indexes | List of frame names | Start frame index | Name that each frame starts with. 111 | @param {Number|Object} [frameCount] Number of frames or animation properties. 112 | @param {Object} [props] Animation properties. 113 | @chainable 114 | **/ 115 | addAnim: function(name, frames, frameCount, props) { 116 | if (!name || typeof frames === undefined) return; 117 | 118 | if (typeof frameCount === 'object') props = frameCount; 119 | 120 | var textures = []; 121 | if (typeof frames === 'string') { 122 | for (var i = 0; i < this.textures.length; i++) { 123 | var texture = this.textures[i]; 124 | if (texture.indexOf(frames) === 0) textures.push(texture); 125 | } 126 | if (textures.length === 0) throw 'No textures found starting with ' + frames; 127 | } 128 | else if (frames.length) { 129 | for (var i = 0; i < frames.length; i++) { 130 | if (typeof frames[i] === 'number') textures[i] = this.textures[frames[i]]; 131 | else if (typeof frames[i] === 'string') { 132 | var index = this.textures.indexOf(frames[i]); 133 | if (index !== -1) textures[i] = this.textures[index]; 134 | } 135 | } 136 | } 137 | else if (typeof frames === 'number' && typeof frameCount === 'number') { 138 | for (var i = 0; i < frameCount; i++) { 139 | textures[i] = this.textures[frames + i]; 140 | } 141 | } 142 | 143 | var anim = new game.Animation(textures); 144 | anim.loop = this.loop; 145 | anim.random = this.random; 146 | anim.reverse = this.reverse; 147 | anim.speed = this.speed; 148 | game.merge(anim, props); 149 | 150 | this.anims[name] = anim; 151 | return this; 152 | }, 153 | 154 | /** 155 | Jump to specific frame. 156 | @method gotoFrame 157 | @param {String|Number} name Name of animation or frame index 158 | @param {Number} frame Frame index 159 | @chainable 160 | **/ 161 | gotoFrame: function(name, frame) { 162 | if (typeof name === 'string') this.currentAnim = this.anims[name] || this; 163 | if (typeof name === 'number') frame = name; 164 | 165 | if (!this.currentAnim.textures) throw 'No textures found for animation'; 166 | if (!this.currentAnim.textures[frame]) return; 167 | this.currentFrame = frame; 168 | this._frameTime = 0; 169 | this.setTexture(this.currentAnim.textures[frame]); 170 | return this; 171 | }, 172 | 173 | /** 174 | Play animation. 175 | @method play 176 | @param {String|Number} [name] Name of animation or frame index 177 | @param {Number} [frame] Frame index 178 | @chainable 179 | **/ 180 | play: function(name, frame) { 181 | this.playing = true; 182 | this.currentAnim = this.anims[name] || this; 183 | 184 | if (typeof name === 'number') frame = name; 185 | if (typeof frame !== 'number' && this.reverse) { 186 | frame = this.textures.length - 1; 187 | } 188 | 189 | this.gotoFrame(frame || 0); 190 | return this; 191 | }, 192 | 193 | /** 194 | Stop animation. 195 | @method stop 196 | @param {String|Number} [name] Name of animation or frame index 197 | @param {Number} [frame] Frame index 198 | @chainable 199 | **/ 200 | stop: function(name, frame) { 201 | this.playing = false; 202 | this.currentAnim = this.anims[name] || this; 203 | 204 | if (typeof name === 'number') frame = name; 205 | if (typeof frame === 'number') this.gotoFrame(frame); 206 | return this; 207 | }, 208 | 209 | updateTransform: function() { 210 | if (this.playing) this._updateAnimation(); 211 | this.super(); 212 | }, 213 | 214 | /** 215 | @method _updateAnimation 216 | @private 217 | **/ 218 | _updateAnimation: function() { 219 | if (game.scene.paused && game.scene._pausedAnims.indexOf(this) !== -1) return; 220 | var anim = this.currentAnim; 221 | if (!anim.textures) throw 'No textures found for animation'; 222 | this._frameTime += anim.speed * game.delta; 223 | 224 | if (this._frameTime >= 1) { 225 | this._frameTime = this._frameTime % 1; 226 | 227 | if (anim.random && anim.textures.length > 1) { 228 | var nextFrame = this.currentFrame; 229 | while (nextFrame === this.currentFrame) { 230 | nextFrame = Math.round(Math.random(0, anim.textures.length - 1)); 231 | } 232 | 233 | this.currentFrame = nextFrame; 234 | this.setTexture(anim.textures[nextFrame]); 235 | return; 236 | } 237 | 238 | var nextFrame = this.currentFrame + (anim.reverse ? -1 : 1); 239 | 240 | if (nextFrame >= anim.textures.length) { 241 | if (anim.loop) { 242 | this.currentFrame = 0; 243 | this.setTexture(anim.textures[0]); 244 | } 245 | else { 246 | this.playing = false; 247 | if (anim.onComplete) anim.onComplete(); 248 | } 249 | } 250 | else if (nextFrame < 0) { 251 | if (anim.loop) { 252 | this.currentFrame = anim.textures.length - 1; 253 | this.setTexture(anim.textures.last()); 254 | } 255 | else { 256 | this.playing = false; 257 | if (anim.onComplete) anim.onComplete(); 258 | } 259 | } 260 | else { 261 | this.currentFrame = nextFrame; 262 | this.setTexture(anim.textures[nextFrame]); 263 | } 264 | } 265 | } 266 | }); 267 | 268 | game.addAttributes('Animation', { 269 | /** 270 | Create animation from textures starting with name. 271 | @method fromTextures 272 | @static 273 | @param {String} name 274 | @return {Animation} 275 | **/ 276 | fromTextures: function(name) { 277 | var textures = []; 278 | for (var texture in game.Texture.cache) { 279 | if (texture.indexOf(name) !== -1) { 280 | textures.push(texture); 281 | } 282 | } 283 | if (textures.length === 0) { 284 | for (var texture in game.BaseTexture.cache) { 285 | if (texture.indexOf(name) !== -1) { 286 | textures.push(texture); 287 | } 288 | } 289 | } 290 | if (textures.length > 0) { 291 | textures.sort(game.compare); 292 | return new game.Animation(textures); 293 | } 294 | else { 295 | throw 'No textures found for ' + name; 296 | } 297 | } 298 | }); 299 | 300 | }); 301 | -------------------------------------------------------------------------------- /src/engine/renderer/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module renderer.core 3 | **/ 4 | game.module( 5 | 'engine.renderer.core' 6 | ) 7 | .require( 8 | 'engine.renderer.animation', 9 | 'engine.renderer.container', 10 | 'engine.renderer.fastcontainer', 11 | 'engine.renderer.graphics', 12 | 'engine.renderer.sprite', 13 | 'engine.renderer.spritesheet', 14 | 'engine.renderer.text', 15 | 'engine.renderer.texture', 16 | 'engine.renderer.tilingsprite' 17 | ) 18 | .body(function() { 19 | 20 | /** 21 | Canvas renderer. Instance automatically created at `game.renderer` 22 | @class Renderer 23 | @constructor 24 | @param {Number} width 25 | @param {Number} height 26 | **/ 27 | game.createClass('Renderer', { 28 | /** 29 | @property {HTMLCanvasElement} canvas 30 | **/ 31 | canvas: null, 32 | /** 33 | @property {CanvasRenderingContext2D} context 34 | **/ 35 | context: null, 36 | /** 37 | @property {String} _smoothProperty 38 | @private 39 | **/ 40 | _smoothProperty: null, 41 | 42 | init: function(width, height) { 43 | if (!game.device.cocoonCanvasPlus) { 44 | this.canvas = document.getElementById(game.System.canvasId); 45 | } 46 | 47 | if (!this.canvas) { 48 | this.canvas = document.createElement('canvas'); 49 | this.canvas.id = game.System.canvasId; 50 | this.canvas.style.display = 'block'; 51 | this.canvas.style.outline = 'none'; 52 | if (game.Renderer.scaleMode === 'nearest') this.canvas.style.imageRendering = 'pixelated'; 53 | this.canvas.tabIndex = 1; 54 | document.body.appendChild(this.canvas); 55 | if (!game.System.center) document.body.style.margin = 0; 56 | } 57 | 58 | game._normalizeVendorAttribute(this.canvas, 'requestFullScreen'); 59 | 60 | this._initContext(); 61 | 62 | if ('imageSmoothingEnabled' in this.context) this._smoothProperty = 'imageSmoothingEnabled'; 63 | else if ('webkitImageSmoothingEnabled' in this.context) this._smoothProperty = 'webkitImageSmoothingEnabled'; 64 | else if ('mozImageSmoothingEnabled' in this.context) this._smoothProperty = 'mozImageSmoothingEnabled'; 65 | else if ('oImageSmoothingEnabled' in this.context) this._smoothProperty = 'oImageSmoothingEnabled'; 66 | else if ('msImageSmoothingEnabled' in this.context) this._smoothProperty = 'msImageSmoothingEnabled'; 67 | 68 | this._resize(width, height); 69 | }, 70 | 71 | /** 72 | Clear canvas. 73 | @method _clear 74 | @private 75 | **/ 76 | _clear: function() { 77 | if (game.scene.backgroundColor) { 78 | this.context.fillStyle = game.scene.backgroundColor; 79 | this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); 80 | } 81 | else { 82 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 83 | } 84 | }, 85 | 86 | /** 87 | Hide canvas. 88 | @method _hide 89 | @private 90 | **/ 91 | _hide: function() { 92 | this.canvas.style.display = 'none'; 93 | }, 94 | 95 | /** 96 | @method _initContext 97 | @private 98 | **/ 99 | _initContext: function() { 100 | this.context = this.canvas.getContext('2d'); 101 | }, 102 | 103 | /** 104 | Set canvas position with CSS. 105 | @method _position 106 | @param {Number} x 107 | @param {Number} y 108 | @private 109 | **/ 110 | _position: function(x, y) { 111 | this.canvas.style.position = 'absolute'; 112 | this.canvas.style.left = x + 'px'; 113 | this.canvas.style.top = y + 'px'; 114 | }, 115 | 116 | /** 117 | Render container to canvas. 118 | @method _render 119 | @param {Container} container 120 | @private 121 | **/ 122 | _render: function(container) { 123 | this.context.setTransform(1, 0, 0, 1, 0, 0); 124 | this.context.globalAlpha = 1; 125 | this.context.globalCompositeOperation = 'source-over'; 126 | if (game.Renderer.clearBeforeRender) this._clear(); 127 | container._updateChildTransform(); 128 | container._render(this.context); 129 | }, 130 | 131 | /** 132 | Resize canvas. 133 | @method _resize 134 | @param {Number} width 135 | @param {Number} height 136 | @private 137 | **/ 138 | _resize: function(width, height) { 139 | this.canvas.width = width; 140 | this.canvas.height = height; 141 | if (this._smoothProperty) this.context[this._smoothProperty] = (game.Renderer.scaleMode === 'linear'); 142 | }, 143 | 144 | /** 145 | Show canvas. 146 | @method _show 147 | @private 148 | **/ 149 | _show: function() { 150 | this.canvas.style.display = 'block'; 151 | }, 152 | 153 | /** 154 | Set canvas size with CSS. 155 | @method _size 156 | @param {Number} width 157 | @param {Number} height 158 | @private 159 | **/ 160 | _size: function(width, height) { 161 | this.canvas.style.width = width + 'px'; 162 | this.canvas.style.height = height + 'px'; 163 | } 164 | }); 165 | 166 | game.addAttributes('Renderer', { 167 | /** 168 | Clear canvas on start of every frame. 169 | @attribute {Boolean} clearBeforeRender 170 | @default true 171 | **/ 172 | clearBeforeRender: true, 173 | /** 174 | Use round positions. 175 | @attribute {Boolean} roundPixels 176 | @default false 177 | **/ 178 | roundPixels: false, 179 | /** 180 | Set scaleMode to nearest to disable smoothing (great for scaled pixel art). 181 | @attribute {String} scaleMode 182 | @default linear 183 | **/ 184 | scaleMode: 'linear' 185 | }); 186 | 187 | game.createClass('Matrix', { 188 | a: 1, 189 | b: 0, 190 | c: 0, 191 | d: 1, 192 | tx: 0, 193 | ty: 0, 194 | 195 | reset: function() { 196 | var proto = this.constructor.prototype; 197 | this.a = proto.a; 198 | this.b = proto.b; 199 | this.c = proto.c; 200 | this.d = proto.d; 201 | this.tx = proto.tx; 202 | this.ty = proto.ty; 203 | return this; 204 | } 205 | }); 206 | 207 | game.addAttributes('Matrix', { 208 | empty: new game.Matrix() 209 | }); 210 | 211 | }); 212 | -------------------------------------------------------------------------------- /src/engine/renderer/fastcontainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module renderer.fastcontainer 3 | **/ 4 | game.module( 5 | 'engine.renderer.fastcontainer' 6 | ) 7 | .require( 8 | 'engine.renderer.container' 9 | ) 10 | .body(function() { 11 | 12 | /** 13 | Special version of Container, that renders faster if it's sprites are not rotated. 14 | @class FastContainer 15 | @extends Container 16 | **/ 17 | game.createClass('FastContainer', 'Container', { 18 | /** 19 | @property {Boolean} _isRotated 20 | @private 21 | **/ 22 | _isRotated: true, 23 | 24 | _getBounds: function() { 25 | this._worldBounds.x = 0; 26 | this._worldBounds.y = 0; 27 | this._worldBounds.width = 0; 28 | this._worldBounds.height = 0; 29 | return this._worldBounds; 30 | }, 31 | 32 | /** 33 | @method _renderBatch 34 | @param {Sprite} child 35 | @param {CanvasRenderingContext2D} context 36 | @private 37 | **/ 38 | _renderBatch: function(child, context) { 39 | context.globalAlpha = this._worldAlpha * child.alpha; 40 | 41 | var wt = this._worldTransform; 42 | var texture = child.texture; 43 | if (child._cachedSprite) texture = child._cachedSprite.texture; 44 | var tx = texture.position.x * game.scale; 45 | var ty = texture.position.y * game.scale; 46 | var tw = texture.width * game.scale; 47 | var th = texture.height * game.scale; 48 | 49 | if (child.rotation % (Math.PI * 2) === 0) { 50 | if (this._isRotated) { 51 | context.setTransform(wt.a, wt.b, wt.c, wt.d, wt.tx * game.scale, wt.ty * game.scale); 52 | this._isRotated = false; 53 | } 54 | 55 | var x = (child.position.x - child.anchor.x * child.scale.x) * game.scale; 56 | var y = (child.position.y - child.anchor.y * child.scale.y) * game.scale; 57 | 58 | context.drawImage(texture.baseTexture.source, tx, ty, tw, th, x, y, tw * child.scale.x, th * child.scale.y); 59 | } 60 | else { 61 | this._isRotated = true; 62 | 63 | child.updateTransform(); 64 | var cwt = child._worldTransform; 65 | var x = cwt.tx * game.scale; 66 | var y = cwt.ty * game.scale; 67 | 68 | if (game.Renderer.roundPixels) { 69 | x = x | 0; 70 | y = y | 0; 71 | } 72 | 73 | context.setTransform(cwt.a, cwt.b, cwt.c, cwt.d, x, y); 74 | context.drawImage(texture.baseTexture.source, tx, ty, tw, th, 0, 0, tw, th); 75 | } 76 | }, 77 | 78 | _renderChildren: function(context) { 79 | this._isRotated = true; 80 | 81 | for (var i = 0; i < this.children.length; i++) { 82 | var child = this.children[i]; 83 | var hasTexture = child.texture || child._cachedSprite; 84 | if (!child.visible || child.alpha <= 0 || !child.renderable || !hasTexture) continue; 85 | 86 | this._renderBatch(child, context); 87 | } 88 | }, 89 | 90 | _updateChildTransform: function() { 91 | for (var i = this.children.length - 1; i >= 0; i--) { 92 | var child = this.children[i]; 93 | if (!child.visible || child.alpha <= 0) continue; 94 | if (child.updateAnimation) child.updateAnimation(); 95 | } 96 | } 97 | }); 98 | 99 | }); 100 | -------------------------------------------------------------------------------- /src/engine/renderer/graphics.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module renderer.graphics 3 | **/ 4 | game.module( 5 | 'engine.renderer.graphics' 6 | ) 7 | .require( 8 | 'engine.renderer.container' 9 | ) 10 | .body(function() { 11 | 12 | /** 13 | @class Graphics 14 | @extends Container 15 | **/ 16 | game.createClass('Graphics', 'Container', { 17 | /** 18 | @property {String} fillColor 19 | @default #fff 20 | **/ 21 | fillColor: '#fff', 22 | /** 23 | @property {Number} fillAlpha 24 | @default 1 25 | **/ 26 | fillAlpha: 1, 27 | /** 28 | @property {Number} lineAlpha 29 | @default 1 30 | **/ 31 | lineAlpha: 1, 32 | /** 33 | @property {String} lineColor 34 | @default #fff 35 | **/ 36 | lineColor: '#fff', 37 | /** 38 | @property {Number} lineWidth 39 | @default 0 40 | **/ 41 | lineWidth: 0, 42 | /** 43 | @property {String} blendMode 44 | @default source-over 45 | **/ 46 | blendMode: 'source-over', 47 | /** 48 | @property {Array} shapes 49 | **/ 50 | shapes: [], 51 | 52 | /** 53 | @method beginFill 54 | @param {String} [color] 55 | @param {Number} [alpha] 56 | @chainable 57 | **/ 58 | beginFill: function(color, alpha) { 59 | this.fillColor = color || this.fillColor; 60 | this.fillAlpha = alpha || this.fillAlpha; 61 | return this; 62 | }, 63 | 64 | /** 65 | @method clear 66 | @chainable 67 | **/ 68 | clear: function() { 69 | this.shapes.length = 0; 70 | return this; 71 | }, 72 | 73 | /** 74 | @method drawArc 75 | @param {Number} x 76 | @param {Number} y 77 | @param {Number} radius 78 | @param {Number} startAngle 79 | @param {Number} endAngle 80 | @chainable 81 | **/ 82 | drawArc: function(x, y, radius, startAngle, endAngle) { 83 | radius *= game.scale; 84 | var shape = new game.Arc(radius, x, y, startAngle, endAngle); 85 | this._drawShape(shape); 86 | return this; 87 | }, 88 | 89 | /** 90 | @method drawCircle 91 | @param {Number} x 92 | @param {Number} y 93 | @param {Number} radius 94 | @chainable 95 | **/ 96 | drawCircle: function(x, y, radius) { 97 | radius *= game.scale; 98 | var shape = new game.Circle(radius, x, y); 99 | this._drawShape(shape); 100 | return this; 101 | }, 102 | 103 | /** 104 | Draw bezier curve. 105 | @method drawCurve 106 | @param {Curve|Number} sx 107 | @param {Number} sy 108 | @param {Number} ex 109 | @param {Number} ey 110 | @param {Number} h1x 111 | @param {Number} h1y 112 | @param {Number} h2x 113 | @param {Number} h2y 114 | @chainable 115 | **/ 116 | drawCurve: function(sx, sy, ex, ey, h1x, h1y, h2x, h2y) { 117 | this.lineWidth = this.lineWidth || 1; 118 | var shape = typeof sx === 'number' ? new game.Curve(sx, sy, ex, ey, h1x, h1y, h2x, h2y) : sx; 119 | this._drawShape(shape, true); 120 | return this; 121 | }, 122 | 123 | /** 124 | @method drawLine 125 | @param {Number} sx Start x 126 | @param {Number} sy Start y 127 | @param {Number} tx End x 128 | @param {Number} ty End y 129 | @chainable 130 | **/ 131 | drawLine: function(sx, sy, tx, ty) { 132 | if (!tx && !ty) { 133 | tx = sx; 134 | ty = sy; 135 | sx = 0; 136 | sy = 0; 137 | } 138 | this.lineWidth = this.lineWidth || 1; 139 | tx *= game.scale; 140 | ty *= game.scale; 141 | var shape = new game.Rectangle(tx, ty, sx, sy); 142 | this._drawShape(shape, true); 143 | return this; 144 | }, 145 | 146 | /** 147 | @method drawPolygon 148 | @param {Array} points List of points. 149 | @param {Boolean} [close] Close the polygon. 150 | @chainable 151 | **/ 152 | drawPolygon: function(points, close) { 153 | var poly = new game.Polygon(points); 154 | if (close) poly.close(); 155 | this._drawShape(poly); 156 | return this; 157 | }, 158 | 159 | /** 160 | @method drawRect 161 | @param {Number} x 162 | @param {Number} y 163 | @param {Number} width 164 | @param {Number} height 165 | @chainable 166 | **/ 167 | drawRect: function(x, y, width, height) { 168 | height = height || width; 169 | width *= game.scale; 170 | height *= game.scale; 171 | var shape = new game.Rectangle(width, height, x, y); 172 | this._drawShape(shape); 173 | return this; 174 | }, 175 | 176 | /** 177 | @method lineStyle 178 | @param {Number} [width] 179 | @param {String} [color] 180 | @param {Number} [alpha] 181 | @chainable 182 | **/ 183 | lineStyle: function(width, color, alpha) { 184 | this.lineWidth = width || this.lineWidth; 185 | this.lineColor = color || this.lineColor; 186 | this.lineAlpha = alpha || this.lineAlpha; 187 | return this; 188 | }, 189 | 190 | /** 191 | @method _drawShape 192 | @param {Rectangle|Circle} shape 193 | @private 194 | **/ 195 | _drawShape: function(shape, isLine) { 196 | var data = new game.GraphicsShape(this.lineWidth, this.lineColor, this.lineAlpha, this.fillColor, this.fillAlpha, shape, isLine); 197 | this.shapes.push(data); 198 | }, 199 | 200 | _getBounds: function() { 201 | var wt = this._worldTransform; 202 | var a = wt.a; 203 | var b = wt.b; 204 | var c = wt.c; 205 | var d = wt.d; 206 | var tx = wt.tx; 207 | var ty = wt.ty; 208 | var width = 0; 209 | var height = 0; 210 | 211 | for (var i = 0; i < this.shapes.length; i++) { 212 | var data = this.shapes[i]; 213 | var sx = data.shape.x; 214 | var sy = data.shape.y; 215 | 216 | if (data.shape.radius) { 217 | sx += data.shape.radius / game.scale; 218 | sy += data.shape.radius / game.scale; 219 | } 220 | else { 221 | sx += data.shape.width / game.scale; 222 | sy += data.shape.height / game.scale; 223 | } 224 | 225 | width = Math.max(width, sx); 226 | height = Math.max(height, sy); 227 | } 228 | 229 | var x2 = a * width + tx; 230 | var y2 = b * width + ty; 231 | var x3 = a * width + c * height + tx; 232 | var y3 = d * height + b * width + ty; 233 | var x4 = c * height + tx; 234 | var y4 = d * height + ty; 235 | 236 | var minX = Math.min(tx, x2, x3, x4); 237 | var minY = Math.min(ty, y2, y3, y4); 238 | var maxX = Math.max(tx, x2, x3, x4); 239 | var maxY = Math.max(ty, y2, y3, y4); 240 | 241 | this._worldBounds.x = minX; 242 | this._worldBounds.y = minY; 243 | this._worldBounds.width = maxX - minX; 244 | this._worldBounds.height = maxY - minY; 245 | return this._worldBounds; 246 | }, 247 | 248 | _renderCanvas: function(context) { 249 | var wt = this._worldTransform; 250 | var tx = wt.tx * game.scale; 251 | var ty = wt.ty * game.scale; 252 | 253 | context.globalCompositeOperation = this.blendMode; 254 | context.setTransform(wt.a, wt.b, wt.c, wt.d, tx, ty); 255 | 256 | for (var i = 0; i < this.shapes.length; i++) { 257 | this.shapes[i]._render(context, this._worldAlpha); 258 | } 259 | }, 260 | 261 | /** 262 | @method _renderMask 263 | @param {CanvasRenderingContext2D} context 264 | @param {Matrix} transform 265 | @private 266 | **/ 267 | _renderMask: function(context, transform) { 268 | var wt = transform; 269 | var tx = wt.tx * game.scale; 270 | var ty = wt.ty * game.scale; 271 | 272 | context.save(); 273 | context.setTransform(wt.a, wt.b, wt.c, wt.d, tx, ty); 274 | context.beginPath(); 275 | for (var i = 0; i < this.shapes.length; i++) { 276 | this.shapes[i]._renderShape(context); 277 | } 278 | context.closePath(); 279 | context.clip(); 280 | } 281 | }); 282 | 283 | /** 284 | @class GraphicsShape 285 | @constructor 286 | @param {Number} lineWidth 287 | @param {String} lineColor 288 | @param {Number} lineAlpha 289 | @param {String} fillColor 290 | @param {Number} fillAlpha 291 | @param {Rectangle|Circle} shape 292 | **/ 293 | game.createClass('GraphicsShape', { 294 | /** 295 | @property {Number} fillAlpha 296 | **/ 297 | fillAlpha: 1, 298 | /** 299 | @property {String} fillColor 300 | **/ 301 | fillColor: '', 302 | /** 303 | @property {Boolean} isLine 304 | **/ 305 | isLine: false, 306 | /** 307 | @property {Number} lineAlpha 308 | **/ 309 | lineAlpha: 0, 310 | /** 311 | @property {String} lineColor 312 | **/ 313 | lineColor: '', 314 | /** 315 | @property {Number} lineWidth 316 | **/ 317 | lineWidth: 0, 318 | /** 319 | @property {Arc|Circle|Rectangle} shape 320 | **/ 321 | shape: null, 322 | 323 | staticInit: function(lineWidth, lineColor, lineAlpha, fillColor, fillAlpha, shape, isLine) { 324 | this.lineWidth = lineWidth; 325 | this.lineColor = lineColor; 326 | this.lineAlpha = lineAlpha; 327 | this.fillColor = fillColor; 328 | this.fillAlpha = fillAlpha; 329 | this.shape = shape; 330 | this.isLine = isLine || this.isLine; 331 | }, 332 | 333 | /** 334 | @method _render 335 | @param {CanvasRenderingContext2D} context 336 | @param {Number} alpha 337 | @private 338 | **/ 339 | _render: function(context, alpha) { 340 | context.globalAlpha = this.fillAlpha * alpha; 341 | context.fillStyle = this.fillColor; 342 | context.strokeStyle = this.lineColor; 343 | context.lineWidth = this.lineWidth * game.scale; 344 | context.beginPath(); 345 | 346 | this._renderShape(context); 347 | 348 | if (this.fillColor && this.fillAlpha && !this.isLine) context.fill(); 349 | if (this.lineWidth) { 350 | context.globalAlpha = this.lineAlpha * alpha; 351 | context.stroke(); 352 | } 353 | }, 354 | 355 | /** 356 | @method _renderShape 357 | @param {CanvasRenderingContext2D} context 358 | @private 359 | **/ 360 | _renderShape: function(context) { 361 | var shape = this.shape; 362 | var x = shape.x * game.scale; 363 | var y = shape.y * game.scale; 364 | 365 | if (this.isLine && shape.start) { 366 | context.moveTo(shape.start.x * game.scale, shape.start.y * game.scale); 367 | context.bezierCurveTo( 368 | shape.handle1.x * game.scale, 369 | shape.handle1.y * game.scale, 370 | shape.handle2.x * game.scale, 371 | shape.handle2.y * game.scale, 372 | shape.end.x * game.scale, 373 | shape.end.y * game.scale 374 | ); 375 | } 376 | else if (this.isLine) { 377 | context.moveTo(x, y); 378 | context.lineTo(shape.width, shape.height); 379 | } 380 | else if (shape.width) { 381 | context.rect(x, y, shape.width, shape.height); 382 | } 383 | else if (shape.radius) { 384 | if (typeof shape.startAngle === 'number' && typeof shape.endAngle === 'number') { 385 | context.moveTo(x, y); 386 | context.arc(x, y, shape.radius, shape.startAngle, shape.endAngle); 387 | context.closePath(); 388 | } 389 | else { 390 | context.arc(x, y, shape.radius, 0, Math.PI * 2); 391 | } 392 | } 393 | else if (shape.points) { 394 | context.moveTo(0, 0); 395 | for (var i = 0; i < shape.points.length; i++) { 396 | var point = shape.points[i]; 397 | var x = point.x; 398 | var y = point.y; 399 | if (x === undefined && point.length === undefined) { 400 | x = point; 401 | y = shape.points[i + 1]; 402 | i++; 403 | } 404 | else if (x === undefined) { 405 | context.bezierCurveTo(point[0], point[1], point[2], point[3], point[4], point[5]); 406 | continue; 407 | } 408 | context.lineTo(x * game.scale, y * game.scale); 409 | } 410 | } 411 | } 412 | }); 413 | 414 | }); 415 | -------------------------------------------------------------------------------- /src/engine/renderer/sprite.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module renderer.sprite 3 | **/ 4 | game.module( 5 | 'engine.renderer.sprite' 6 | ) 7 | .require( 8 | 'engine.renderer.container' 9 | ) 10 | .body(function() { 11 | 12 | /** 13 | @class Sprite 14 | @extends Container 15 | @constructor 16 | @param {Texture|String} texture 17 | @param {Object} [props] 18 | **/ 19 | game.createClass('Sprite', 'Container', { 20 | /** 21 | Blend mode for sprite rendering. 22 | @property {String} blendMode 23 | @default source-over 24 | **/ 25 | blendMode: 'source-over', 26 | /** 27 | Texture for sprite. 28 | @property {Texture} texture 29 | **/ 30 | texture: null, 31 | /** 32 | Tint sprite with color. 33 | @property {String} tint 34 | **/ 35 | tint: null, 36 | /** 37 | Alpha of sprite tint. 38 | @property {Number} tintAlpha 39 | @default 1 40 | **/ 41 | tintAlpha: 1, 42 | /** 43 | Crop tint area. x is from left, y is from top, width is from right and height is from bottom. 44 | @property {Rectangle} tintCrop 45 | **/ 46 | tintCrop: null, 47 | /** 48 | @property {Number} _tintAlpha 49 | @private 50 | **/ 51 | _tintAlpha: 1, 52 | /** 53 | @property {String} _tintColor 54 | @private 55 | **/ 56 | _tintColor: null, 57 | 58 | staticInit: function(texture, props) { 59 | this.tintCrop = new game.Rectangle(); 60 | this.super(props); 61 | this.setTexture(this.texture || texture); 62 | }, 63 | 64 | /** 65 | Change texture to sprite. 66 | @method setTexture 67 | @param {Texture|String} texture Asset id or instance of Texture 68 | **/ 69 | setTexture: function(texture) { 70 | this.texture = texture instanceof game.Texture ? texture : game.Texture.fromAsset(texture); 71 | }, 72 | 73 | /** 74 | @method _destroyTintedTexture 75 | @private 76 | **/ 77 | _destroyTintedTexture: function() { 78 | if (this._tintedTexture) { 79 | this._tintedTexture.baseTexture.remove(); 80 | this._tintedTexture.remove(); 81 | } 82 | this._tintedTexture = null; 83 | this._tintedTextureGenerated = false; 84 | }, 85 | 86 | /** 87 | @method _generateTintedTexture 88 | @param {String} color 89 | @return {Texture} 90 | @private 91 | **/ 92 | _generateTintedTexture: function(color, alpha) { 93 | var canvas = game.Sprite._canvas; 94 | var context = game.Sprite._context; 95 | 96 | canvas.width = this.texture.width * game.scale; 97 | canvas.height = this.texture.height * game.scale; 98 | 99 | this._tintColor = color; 100 | this._tintAlpha = alpha || 1; 101 | 102 | context.fillStyle = this._tintColor.substr(0, 7); 103 | context.globalAlpha = this._tintAlpha; 104 | var x = this.tintCrop.x * game.scale; 105 | var y = this.tintCrop.y * game.scale; 106 | var width = canvas.width - x - this.tintCrop.width * game.scale; 107 | var height = canvas.height - y - this.tintCrop.height * game.scale; 108 | context.fillRect(x, y, width, height); 109 | context.globalAlpha = 1; 110 | 111 | var blendMode = this.blendMode; 112 | this.blendMode = 'destination-atop'; 113 | alpha = this._worldAlpha; 114 | this._worldAlpha = 1; 115 | this._renderCanvas(context, game.Matrix.empty); 116 | this._worldAlpha = alpha; 117 | this.blendMode = blendMode; 118 | 119 | var texture = game.Texture.fromImage(canvas.toDataURL()); 120 | texture.width = canvas.width; 121 | texture.height = canvas.height; 122 | game.Sprite._tintedTextures.push(texture); 123 | return texture; 124 | }, 125 | 126 | _getBounds: function(transform) { 127 | if (this._cachedSprite) { 128 | this._worldBounds.x = this._worldTransform.tx + this._cachedSprite.position.x; 129 | this._worldBounds.y = this._worldTransform.ty + this._cachedSprite.position.y; 130 | this._worldBounds.width = this._cachedSprite.texture.width * this._worldTransform.a; 131 | this._worldBounds.height = this._cachedSprite.texture.height * this._worldTransform.d; 132 | return this._worldBounds; 133 | } 134 | 135 | if (this._lastTransformUpdate < game.Timer._lastFrameTime) this.updateTransform(); 136 | 137 | var width = this.texture.width; 138 | var height = this.texture.height; 139 | var lt = transform || this._worldTransform; 140 | var a = lt.a; 141 | var b = lt.b; 142 | var c = lt.c; 143 | var d = lt.d; 144 | var tx = lt.tx; 145 | var ty = lt.ty; 146 | var x2 = a * width + tx; 147 | var y2 = b * width + ty; 148 | var x3 = a * width + c * height + tx; 149 | var y3 = d * height + b * width + ty; 150 | var x4 = c * height + tx; 151 | var y4 = d * height + ty; 152 | 153 | var minX = Math.min(tx, x2, x3, x4); 154 | var minY = Math.min(ty, y2, y3, y4); 155 | var maxX = Math.max(tx, x2, x3, x4); 156 | var maxY = Math.max(ty, y2, y3, y4); 157 | 158 | for (var i = 0; i < this.children.length; i++) { 159 | if (!this.children[i].visible || this.children[i].alpha <= 0) continue; 160 | var childBounds = this.children[i]._getBounds(); 161 | var childMinX = childBounds.x; 162 | var childMaxX = childMinX + childBounds.width; 163 | var childMinY = childBounds.y; 164 | var childMaxY = childMinY + childBounds.height; 165 | minX = Math.min(minX, childMinX); 166 | minY = Math.min(minY, childMinY); 167 | maxX = Math.max(maxX, childMaxX); 168 | maxY = Math.max(maxY, childMaxY); 169 | } 170 | 171 | this._worldBounds.x = minX; 172 | this._worldBounds.y = minY; 173 | this._worldBounds.width = maxX - minX; 174 | this._worldBounds.height = maxY - minY; 175 | return this._worldBounds; 176 | }, 177 | 178 | /** 179 | @method _renderCanvas 180 | @param {CanvasRenderingContext2D} context 181 | @param {Matrix} [transform] 182 | @param {Rectangle} [rect] 183 | @param {Rectangle} [offset] 184 | @private 185 | **/ 186 | _renderCanvas: function(context, transform, rect, offset) { 187 | if (!this.texture) return true; 188 | if (!this.texture.baseTexture.loaded) return true; 189 | 190 | if (!this.texture.width && this.texture.baseTexture.width) { 191 | this.texture.width = this.texture.baseTexture.width; 192 | } 193 | if (!this.texture.height && this.texture.baseTexture.height) { 194 | this.texture.height = this.texture.baseTexture.height; 195 | } 196 | 197 | if (!this.texture.width || !this.texture.height) return true; 198 | 199 | if (this.tint && !this._tintedTexture && !this._tintedTextureGenerated && this.tintAlpha > 0) { 200 | this._tintedTextureGenerated = true; 201 | this._tintedTexture = this._generateTintedTexture(this.tint, this.tintAlpha); 202 | } 203 | else if (this.tint && this.tint !== this._tintColor && this.tintAlpha > 0 || this.tint && this.tintAlpha !== this._tintAlpha && this.tintAlpha > 0) { 204 | this._destroyTintedTexture(); 205 | this._tintedTextureGenerated = true; 206 | this._tintedTexture = this._generateTintedTexture(this.tint, this.tintAlpha); 207 | } 208 | else if (!this.tint && this._tintedTexture || this.tintAlpha === 0 && this._tintedTexture) { 209 | this._destroyTintedTexture(); 210 | } 211 | 212 | context.globalCompositeOperation = this.blendMode; 213 | context.globalAlpha = this._worldAlpha; 214 | 215 | var t = this._tintedTexture || this.texture; 216 | var wt = transform || this._worldTransform; 217 | var tx = wt.tx; 218 | var ty = wt.ty; 219 | 220 | if (game.Renderer.roundPixels) { 221 | tx = tx | 0; 222 | ty = ty | 0; 223 | } 224 | 225 | tx *= game.scale; 226 | ty *= game.scale; 227 | 228 | var x = t.position.x * game.scale; 229 | var y = t.position.y * game.scale; 230 | var width = t.width * game.scale; 231 | var height = t.height * game.scale; 232 | 233 | if (rect) { 234 | x = rect.x; 235 | y = rect.y; 236 | width = rect.width; 237 | height = rect.height; 238 | } 239 | 240 | if (offset) { 241 | tx += offset.x; 242 | ty += offset.y; 243 | } 244 | 245 | context.setTransform(wt.a, wt.b, wt.c, wt.d, tx, ty); 246 | 247 | if (t._trim.x) width = t._trim.x; 248 | if (t._trim.y) height = t._trim.y; 249 | var sx = t._offset.x - t._anchor.x; 250 | var sy = t._offset.y - t._anchor.y; 251 | 252 | context.drawImage(t.baseTexture.source, x, y, width, height, sx, sy, width, height); 253 | } 254 | }); 255 | 256 | game.defineProperties('Sprite', { 257 | width: { 258 | get: function() { 259 | return Math.abs(this.scale.x) * this.texture.width; 260 | }, 261 | 262 | set: function(value) { 263 | this.scale.x = value / this.texture.width; 264 | } 265 | }, 266 | 267 | height: { 268 | get: function() { 269 | return Math.abs(this.scale.y) * this.texture.height; 270 | }, 271 | 272 | set: function(value) { 273 | this.scale.y = value / this.texture.height; 274 | } 275 | } 276 | }); 277 | 278 | game.addAttributes('Sprite', { 279 | /** 280 | @attribute {HTMLCanvasElement} _canvas 281 | @private 282 | **/ 283 | _canvas: null, 284 | /** 285 | @attribute {CanvasRenderingContext2D} _context 286 | @private 287 | **/ 288 | _context: null, 289 | /** 290 | @attribute {Array} _tintedTextures 291 | @private 292 | **/ 293 | _tintedTextures: [], 294 | 295 | /** 296 | @method _clearTintedTextures 297 | @static 298 | @private 299 | **/ 300 | _clearTintedTextures: function() { 301 | for (var i = 0; i < this._tintedTextures.length; i++) { 302 | this._tintedTextures[i].baseTexture.remove(); 303 | this._tintedTextures[i].remove(); 304 | } 305 | this._tintedTextures.length = 0; 306 | } 307 | }); 308 | 309 | if (typeof document !== 'undefined') { 310 | game.Sprite._canvas = document.createElement('canvas'); 311 | game.Sprite._context = game.Sprite._canvas.getContext('2d'); 312 | } 313 | 314 | }); 315 | -------------------------------------------------------------------------------- /src/engine/renderer/spritesheet.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module renderer.spritesheet 3 | **/ 4 | game.module( 5 | 'engine.renderer.spritesheet' 6 | ) 7 | .require( 8 | 'engine.renderer.sprite' 9 | ) 10 | .body(function() { 11 | 12 | /** 13 | Sprite which contains multiple textures from sprite sheet with fixed frame size. 14 | @class SpriteSheet 15 | @extends Sprite 16 | @constructor 17 | @param {String} id Asset id 18 | @param {Number} width Sprite frame width 19 | @param {Number} height Sprite frame height 20 | **/ 21 | game.createClass('SpriteSheet', 'Sprite', { 22 | /** 23 | Width of frame. 24 | @property {Number} frameWidth 25 | **/ 26 | frameWidth: 0, 27 | /** 28 | Height of frame. 29 | @property {Number} frameHeight 30 | **/ 31 | frameHeight: 0, 32 | /** 33 | List of textures. 34 | @property {Array} textures 35 | **/ 36 | textures: [], 37 | 38 | staticInit: function(id, frameWidth, frameHeight) { 39 | this.frameWidth = this.frameWidth || frameWidth; 40 | this.frameHeight = this.frameHeight || frameHeight; 41 | if (!this.frameHeight) this.frameHeight = this.frameWidth; 42 | var baseTexture = game.BaseTexture.cache[game.paths[id]]; 43 | if (!baseTexture) throw 'No texture found for ' + id; 44 | var sx = Math.floor(baseTexture.width / this.frameWidth); 45 | var sy = Math.floor(baseTexture.height / this.frameHeight); 46 | this.frames = sx * sy; 47 | 48 | for (var i = 0; i < this.frames; i++) { 49 | var x = (i % sx) * this.frameWidth; 50 | var y = Math.floor(i / sx) * this.frameHeight; 51 | var texture = new game.Texture(baseTexture, x, y, this.frameWidth, this.frameHeight); 52 | this.textures.push(texture); 53 | } 54 | 55 | this.super(this.textures[0]); 56 | }, 57 | 58 | /** 59 | Set texture to specific frame. 60 | @method frame 61 | @param {Number} index Frame index 62 | **/ 63 | frame: function(index) { 64 | if (!this.textures[index]) return; 65 | this.texture = this.textures[index]; 66 | return this; 67 | } 68 | }); 69 | 70 | game.addAttributes('SpriteSheet', { 71 | /** 72 | Create animation from spritesheet. 73 | @method anim 74 | @static 75 | @param {String} id Asset id 76 | @param {Number} width Sprite frame width 77 | @param {Number} height Sprite frame height 78 | @param {Number|Array} frames List or number of frames 79 | @param {Number} [startIndex] The index to begin with, default to 0 80 | @param {Boolean} [onlyTextures] Return only textures in array 81 | @return {Animation|Array} 82 | **/ 83 | anim: function(id, frameWidth, frameHeight, frames, startIndex, onlyTextures) { 84 | var sprite = new game.SpriteSheet(id, frameWidth, frameHeight); 85 | 86 | startIndex = startIndex || 0; 87 | frames = frames || sprite.textures.length; 88 | var textures = []; 89 | if (frames.length > 0) { 90 | for (var i = 0; i < frames.length; i++) { 91 | textures.push(sprite.textures[startIndex + frames[i]]); 92 | } 93 | } 94 | else { 95 | for (var i = 0; i < frames; i++) { 96 | textures.push(sprite.textures[startIndex + i]); 97 | } 98 | } 99 | if (onlyTextures) return textures; 100 | return new game.Animation(textures); 101 | } 102 | }); 103 | 104 | }); 105 | -------------------------------------------------------------------------------- /src/engine/renderer/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module renderer.text 3 | **/ 4 | game.module( 5 | 'engine.renderer.text' 6 | ) 7 | .require( 8 | 'engine.renderer.sprite' 9 | ) 10 | .body(function() { 11 | 12 | /** 13 | @class Font 14 | @constructor 15 | @param {XML|JSON} data 16 | **/ 17 | game.createClass('Font', { 18 | /** 19 | @property {BaseTexture} baseTexture 20 | **/ 21 | baseTexture: null, 22 | /** 23 | @property {Object} chars 24 | **/ 25 | chars: {}, 26 | /** 27 | @property {Number} letterSpacing 28 | @default 0 29 | **/ 30 | letterSpacing: 0, 31 | /** 32 | @property {Number} lineHeight 33 | **/ 34 | lineHeight: 0, 35 | /** 36 | @property {Number} spaceWidth 37 | **/ 38 | spaceWidth: 0, 39 | 40 | staticInit: function(data) { 41 | if (data.getElementsByTagName) { 42 | var image = data.getElementsByTagName('page')[0].getAttribute('file'); 43 | var info = data.getElementsByTagName('info')[0]; 44 | var common = data.getElementsByTagName('common')[0]; 45 | var chars = data.getElementsByTagName('char'); 46 | } 47 | else { 48 | var image = data.pages[0].file; 49 | var info = data.info; 50 | var common = data.common; 51 | var chars = data.chars; 52 | } 53 | 54 | this.baseTexture = game.BaseTexture.fromImage(game._getFilePath(image)); 55 | if (data.getElementsByTagName) this.lineHeight = parseInt(common.getAttribute('lineHeight')); 56 | else this.lineHeight = parseInt(common.lineHeight); 57 | 58 | for (var i = 0; i < chars.length; i++) { 59 | if (data.getElementsByTagName) { 60 | var xadvance = parseInt(chars[i].getAttribute('xadvance')); 61 | var id = parseInt(chars[i].getAttribute('id')); 62 | } 63 | else { 64 | var xadvance = parseInt(chars[i].xadvance); 65 | var id = parseInt(chars[i].id); 66 | } 67 | 68 | if (id === 32) { 69 | this.spaceWidth = xadvance; 70 | continue; 71 | } 72 | 73 | if (data.getElementsByTagName) { 74 | var xoffset = parseInt(chars[i].getAttribute('xoffset')); 75 | var yoffset = parseInt(chars[i].getAttribute('yoffset')); 76 | var x = parseInt(chars[i].getAttribute('x')) / game.scale; 77 | var y = parseInt(chars[i].getAttribute('y')) / game.scale; 78 | var width = parseInt(chars[i].getAttribute('width')) / game.scale; 79 | var height = parseInt(chars[i].getAttribute('height')) / game.scale; 80 | } 81 | else { 82 | var xoffset = parseInt(chars[i].xoffset); 83 | var yoffset = parseInt(chars[i].yoffset); 84 | var x = parseInt(chars[i].x) / game.scale; 85 | var y = parseInt(chars[i].y) / game.scale; 86 | var width = parseInt(chars[i].width) / game.scale; 87 | var height = parseInt(chars[i].height) / game.scale; 88 | } 89 | 90 | var texture = new game.Texture(this.baseTexture, x, y, width, height); 91 | 92 | this.chars[id] = { 93 | texture: texture, 94 | xadvance: xadvance, 95 | xoffset: xoffset, 96 | yoffset: yoffset 97 | }; 98 | } 99 | } 100 | }); 101 | 102 | game.addAttributes('Font', { 103 | /** 104 | @attribute {Object} cache 105 | **/ 106 | cache: {}, 107 | 108 | /** 109 | @method fromData 110 | @static 111 | @param {XML|JSON} data 112 | **/ 113 | fromData: function(data) { 114 | if (data.getElementsByTagName) { 115 | var info = data.getElementsByTagName('info')[0]; 116 | var face = info.getAttribute('face'); 117 | } 118 | else { 119 | var face = data.info.face; 120 | } 121 | 122 | var font = game.Font.cache[face]; 123 | 124 | if (!font) { 125 | font = new game.Font(data); 126 | game.Font.cache[face] = font; 127 | if (!game.Text.defaultFont) game.Text.defaultFont = face; 128 | } 129 | 130 | return font; 131 | }, 132 | 133 | /** 134 | @method clearCache 135 | @static 136 | **/ 137 | clearCache: function() { 138 | for (var i in this.cache) { 139 | delete this.cache[i]; 140 | } 141 | } 142 | }); 143 | 144 | /** 145 | Text that uses bitmap fonts for rendering. 146 | @class Text 147 | @extends Container 148 | @constructor 149 | @param {String} text 150 | @param {Object} [props] 151 | **/ 152 | game.createClass('Text', 'Container', { 153 | /** 154 | Align for multi-lined text. Can be left, center or right. 155 | @property {String} align 156 | **/ 157 | align: 'left', 158 | /** 159 | Name of the font that this text is using. 160 | @property {String} font 161 | **/ 162 | font: null, 163 | /** 164 | Font class that this text is using. 165 | @property {Font} fontClass 166 | **/ 167 | fontClass: null, 168 | /** 169 | If text height is higher than maxHeight value, text will be scaled down to fit maxHeight. 0 to disable. 170 | @property {Number} maxHeight 171 | @default 0 172 | **/ 173 | maxHeight: 0, 174 | /** 175 | If text width is higher than maxWidth value, text will be scaled down to fit maxWidth. 0 to disable. 176 | @property {Number} maxWidth 177 | @default 0 178 | **/ 179 | maxWidth: 0, 180 | /** 181 | Current text value. 182 | @property {String} text 183 | **/ 184 | text: null, 185 | /** 186 | If text width is higher than wrap value, text will be wrapped to multiple lines. 0 to disable. 187 | @property {Number} wrap 188 | **/ 189 | wrap: 0, 190 | /** 191 | @property {Object} _lines 192 | @private 193 | **/ 194 | _lines: null, 195 | 196 | staticInit: function(text, props) { 197 | this.super(); 198 | text = (typeof text === 'string' || typeof text === 'number') ? text : ''; 199 | this.text = this.text || text.toString(); 200 | game.merge(this, props); 201 | this.font = this.font || game.Text.defaultFont; 202 | if (this.font) this.setFont(this.font); 203 | }, 204 | 205 | /** 206 | Set new font for text. 207 | @method setFont 208 | @param {String} fontName 209 | @chainable 210 | **/ 211 | setFont: function(fontName) { 212 | this.font = fontName; 213 | this.fontClass = game.Font.cache[fontName]; 214 | if (!this.fontClass) throw 'Font ' + fontName + ' not found'; 215 | if (this.text) this.setText(this.text); 216 | return this; 217 | }, 218 | 219 | /** 220 | Set new text. 221 | @method setText 222 | @param {String|Number} text 223 | @chainable 224 | **/ 225 | setText: function(text) { 226 | this.text = text.toString(); 227 | this.updateText(); 228 | return this; 229 | }, 230 | 231 | /** 232 | Update text texture. 233 | @method updateText 234 | **/ 235 | updateText: function() { 236 | if (!this.fontClass) return; 237 | 238 | this.removeAll(); 239 | 240 | var SPACE = 0; 241 | var WORD = 1; 242 | var lines = [{ words: [], width: 0 }]; 243 | var curLine = 0; 244 | var curWordWidth = 0; 245 | var wordText = ''; 246 | var wordNum = 1; 247 | 248 | for (var i = 0; i < this.text.length; i++) { 249 | var charCode = this.text.charCodeAt(i); 250 | 251 | // Space or line break 252 | if (charCode === 32 || charCode === 10) { 253 | if (curWordWidth > 0) { 254 | // Word before space or line break 255 | var lineWidth = lines[curLine].width + curWordWidth; 256 | if (lineWidth > this.wrap && this.wrap > 0 && lines[curLine].words.length > 0) { 257 | // Insert new line 258 | curLine++; 259 | lines.push({ words: [], width: 0 }); 260 | } 261 | 262 | // Insert new word 263 | lines[curLine].words.push({ 264 | width: curWordWidth, 265 | type: WORD, 266 | text: wordText, 267 | num: wordNum 268 | }); 269 | lines[curLine].width += curWordWidth; 270 | wordText = ''; 271 | wordNum++; 272 | } 273 | } 274 | else { 275 | wordText += this.text[i]; 276 | } 277 | 278 | if (charCode === 32) { 279 | // Insert space 280 | lines[curLine].words.push({ 281 | width: this.fontClass.spaceWidth, 282 | type: SPACE, 283 | text: ' ', 284 | num: wordNum 285 | }); 286 | lines[curLine].width += this.fontClass.spaceWidth; 287 | curWordWidth = 0; 288 | wordNum++; 289 | continue; 290 | } 291 | 292 | if (charCode === 10) { 293 | // Insert line break 294 | curLine++; 295 | lines.push({ words: [], width: 0 }); 296 | curWordWidth = 0; 297 | continue; 298 | } 299 | 300 | var charObj = this.fontClass.chars[charCode]; 301 | if (!charObj) continue; 302 | 303 | curWordWidth += charObj.xadvance + this.fontClass.letterSpacing; 304 | } 305 | 306 | // Add last word 307 | if (curWordWidth > 0) { 308 | var lineWidth = lines[curLine].width + curWordWidth; 309 | if (lineWidth > this.wrap && this.wrap > 0 && lines[curLine].words.length > 0) { 310 | // New line 311 | curLine++; 312 | lines.push({ words: [], width: 0 }); 313 | } 314 | 315 | lines[curLine].words.push({ 316 | width: curWordWidth, 317 | type: WORD, 318 | text: wordText, 319 | num: wordNum 320 | }); 321 | lines[curLine].width += curWordWidth; 322 | } 323 | 324 | var width = 0; 325 | for (var i = 0; i < lines.length; i++) { 326 | var line = lines[i]; 327 | if (line.width > width) width = line.width; 328 | for (var o = line.words.length - 1; o >= 0; o--) { 329 | if (line.words[o].type === SPACE) { 330 | line.width -= this.fontClass.spaceWidth; 331 | } 332 | else break; 333 | } 334 | } 335 | 336 | this._lines = lines; 337 | this._generateText(width); 338 | }, 339 | 340 | /** 341 | @method _generateText 342 | @param {Number} width 343 | @private 344 | **/ 345 | _generateText: function(width) { 346 | var x = 0; 347 | var y = 0; 348 | var curLine = 0; 349 | var curWord = 0; 350 | var prevChar = 0; 351 | 352 | if (this.align === 'center') x = width / 2 - this._lines[0].width / 2; 353 | if (this.align === 'right') x = width - this._lines[0].width; 354 | 355 | for (var i = 0; i < this.text.length; i++) { 356 | var line = this._lines[curLine]; 357 | 358 | // End of line 359 | if (!line.words[curWord]) { 360 | if (line.words.length === 0) y += this.fontClass.lineHeight; // Empty line 361 | curLine++; 362 | if (!this._lines[curLine]) curLine--; 363 | curWord = 0; 364 | 365 | if (x > 0) y += this.fontClass.lineHeight; 366 | x = 0; 367 | 368 | if (this.align === 'center') x = width / 2 - this._lines[curLine].width / 2; 369 | if (this.align === 'right') x = width - this._lines[curLine].width; 370 | } 371 | 372 | var charCode = this.text.charCodeAt(i); 373 | 374 | // Space 375 | if (charCode === 32) { 376 | // Only add space if not beginning of line 377 | if (x > 0) { 378 | x += this.fontClass.spaceWidth; 379 | if (prevChar !== 32) curWord++; 380 | } 381 | curWord++; 382 | } 383 | 384 | // Line break 385 | if (charCode === 10 && x > 0) { 386 | y += this.fontClass.lineHeight; 387 | x = 0; 388 | curWord++; 389 | } 390 | 391 | prevChar = charCode; 392 | 393 | var charObj = this.fontClass.chars[charCode]; 394 | if (!charObj || charCode === 10) continue; 395 | 396 | var texture = charObj.texture; 397 | if (i === 0) x -= charObj.xoffset; 398 | 399 | var sprite = new game.Sprite(texture); 400 | sprite.position.x = (x + charObj.xoffset) / game.scale; 401 | sprite.position.y = (y + charObj.yoffset) / game.scale; 402 | this.addChild(sprite); 403 | 404 | x += charObj.xadvance + this.fontClass.letterSpacing; 405 | } 406 | 407 | this.updateTransform(); 408 | 409 | if (!this.maxWidth && !this.maxHeight) return; 410 | 411 | var scale = 1; 412 | 413 | if (this.maxWidth && this.width > this.maxWidth) { 414 | scale = this.maxWidth / this.width; 415 | } 416 | if (this.maxHeight && this.height > this.maxHeight) { 417 | scale = this.maxHeight / this.height; 418 | } 419 | 420 | this.scale.set(scale); 421 | } 422 | }); 423 | 424 | game.addAttributes('Text', { 425 | /** 426 | Default font for text. 427 | @attribute {String} defaultFont 428 | **/ 429 | defaultFont: null 430 | }); 431 | 432 | /** 433 | Text that uses canvas fillText for rendering. 434 | @class SystemText 435 | @extends Container 436 | @constructor 437 | @param {String} text 438 | @param {Object} [props] 439 | **/ 440 | game.createClass('SystemText', 'Container', { 441 | /** 442 | Align of the text. Can be left, right or center. 443 | @property {String} align 444 | @default left 445 | **/ 446 | align: 'left', 447 | /** 448 | Baseline alignment. 449 | @property {String} baseline 450 | @default alphabetic 451 | **/ 452 | baseline: 'alphabetic', 453 | /** 454 | Color of the text. 455 | @property {String} color 456 | @default #fff 457 | **/ 458 | color: '#fff', 459 | /** 460 | Font used for the text. 461 | @property {String} font 462 | @default Arial 463 | **/ 464 | font: 'Arial', 465 | /** 466 | Size of the text. 467 | @property {Number} size 468 | @default 14 469 | **/ 470 | size: 14, 471 | /** 472 | Current text. 473 | @property {String} text 474 | **/ 475 | text: '', 476 | 477 | staticInit: function(text, props) { 478 | this.super(props); 479 | this.text = text || this.text; 480 | }, 481 | 482 | _renderCanvas: function(context) { 483 | var wt = this._worldTransform; 484 | 485 | context.globalAlpha = this._worldAlpha; 486 | context.setTransform(wt.a, wt.b, wt.c, wt.d, wt.tx * game.scale, (wt.ty + this.size) * game.scale); 487 | context.fillStyle = this.color; 488 | context.font = this.size * game.scale + 'px ' + this.font; 489 | context.textAlign = this.align; 490 | context.textBaseline = this.baseline; 491 | context.fillText(this.text, 0, 0); 492 | } 493 | }); 494 | 495 | }); 496 | -------------------------------------------------------------------------------- /src/engine/renderer/texture.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module renderer.texture 3 | **/ 4 | game.module( 5 | 'engine.renderer.texture' 6 | ) 7 | .body(function() { 8 | 9 | /** 10 | @class BaseTexture 11 | @constructor 12 | @param {HTMLImageElement|HTMLCanvasElement} source 13 | @param {Function} loadCallback 14 | **/ 15 | game.createClass('BaseTexture', { 16 | /** 17 | @property {Number} height 18 | **/ 19 | height: 0, 20 | /** 21 | @property {Boolean} loaded 22 | @default false 23 | **/ 24 | loaded: false, 25 | /** 26 | @property {HTMLImageElement|HTMLCanvasElement} source 27 | **/ 28 | source: null, 29 | /** 30 | @property {Number} width 31 | **/ 32 | width: 0, 33 | /** 34 | @property {Number|String} _id 35 | @private 36 | **/ 37 | _id: null, 38 | /** 39 | @property {Function} _loadCallback 40 | @private 41 | **/ 42 | _loadCallback: null, 43 | 44 | staticInit: function(source, loadCallback) { 45 | this.source = source; 46 | this._loadCallback = loadCallback; 47 | 48 | if (source.getContext) { 49 | this._onload(); 50 | } 51 | else { 52 | source.onload = this._onload.bind(this); 53 | source.onerror = this._onerror.bind(this); 54 | } 55 | }, 56 | 57 | /** 58 | Remove base texture from cache. 59 | @method remove 60 | **/ 61 | remove: function() { 62 | for (var name in game.BaseTexture.cache) { 63 | if (game.BaseTexture.cache[name] === this) { 64 | delete game.BaseTexture.cache[name]; 65 | return; 66 | } 67 | } 68 | }, 69 | 70 | /** 71 | @method _onerror 72 | @private 73 | **/ 74 | _onerror: function() { 75 | if (this._loadCallback) this._loadCallback('Error loading image ' + this._id); 76 | else throw 'Error loading image ' + this._id; 77 | }, 78 | 79 | /** 80 | @method _onload 81 | @private 82 | **/ 83 | _onload: function() { 84 | this.loaded = true; 85 | this.width = this.source.width / game.scale; 86 | this.height = this.source.height / game.scale; 87 | if (this._loadCallback) this._loadCallback(); 88 | } 89 | }); 90 | 91 | game.addAttributes('BaseTexture', { 92 | /** 93 | @attribute {Object} cache 94 | **/ 95 | cache: {}, 96 | /** 97 | Default crossOrigin property to use in all Image sources 98 | @attribute {String} crossOrigin 99 | @default '' 100 | **/ 101 | crossOrigin: '', 102 | /** 103 | @method clearCache 104 | @static 105 | **/ 106 | clearCache: function() { 107 | for (var i in this.cache) { 108 | delete this.cache[i]; 109 | } 110 | }, 111 | /** 112 | @method fromAsset 113 | @static 114 | @param {String} id 115 | @return {BaseTexture} 116 | **/ 117 | fromAsset: function(id) { 118 | var path = game.paths[id]; 119 | var baseTexture = this.cache[path]; 120 | 121 | if (!baseTexture) baseTexture = this.fromImage(path); 122 | 123 | return baseTexture; 124 | }, 125 | /** 126 | @method fromCanvas 127 | @static 128 | @param {HTMLCanvasElement} canvas 129 | @return {BaseTexture} 130 | **/ 131 | fromCanvas: function(canvas) { 132 | if (!canvas._id) canvas._id = 'canvas_' + this._id++; 133 | 134 | var baseTexture = this.cache[canvas._id]; 135 | 136 | if (!baseTexture) { 137 | baseTexture = new game.BaseTexture(canvas); 138 | baseTexture._id = canvas._id; 139 | this.cache[canvas._id] = baseTexture; 140 | } 141 | 142 | return baseTexture; 143 | }, 144 | /** 145 | @method fromImage 146 | @static 147 | @param {String} path 148 | @param {Function} loadCallback 149 | @return {BaseTexture} 150 | **/ 151 | fromImage: function(path, loadCallback) { 152 | var baseTexture = this.cache[path]; 153 | 154 | if (!baseTexture) { 155 | var source = document.createElement('img'); 156 | if (this.crossOrigin) source.crossOrigin = this.crossOrigin; 157 | 158 | var sourcePath = path; 159 | if (path.indexOf('data:image') === -1) sourcePath += game._nocache; 160 | source.src = sourcePath; 161 | 162 | baseTexture = new game.BaseTexture(source, loadCallback); 163 | baseTexture._id = path; 164 | this.cache[path] = baseTexture; 165 | } 166 | else if (loadCallback) loadCallback(); 167 | 168 | return baseTexture; 169 | }, 170 | /** 171 | @attribute {Number} _id 172 | @private 173 | **/ 174 | _id: 1 175 | }); 176 | 177 | /** 178 | @class Texture 179 | @constructor 180 | @param {BaseTexture|String} baseTexture 181 | @param {Number} [x] 182 | @param {Number} [y] 183 | @param {Number} [width] 184 | @param {Number} [height] 185 | **/ 186 | game.createClass('Texture', { 187 | /** 188 | @property {BaseTexture} baseTexture 189 | **/ 190 | baseTexture: null, 191 | /** 192 | @property {Number} height 193 | **/ 194 | height: 0, 195 | /** 196 | @property {Vector} position 197 | **/ 198 | position: null, 199 | /** 200 | @property {Number} width 201 | **/ 202 | width: 0, 203 | /** 204 | @property {Vector} _anchor 205 | @private 206 | **/ 207 | _anchor: null, 208 | /** 209 | @property {Vector} _offset 210 | @private 211 | **/ 212 | _offset: null, 213 | /** 214 | @property {Vector} _trim 215 | @private 216 | **/ 217 | _trim: null, 218 | 219 | staticInit: function(baseTexture, x, y, width, height) { 220 | this._anchor = new game.Vector(); 221 | this._offset = new game.Vector(); 222 | this._trim = new game.Vector(); 223 | this.baseTexture = baseTexture instanceof game.BaseTexture ? baseTexture : game.BaseTexture.fromAsset(baseTexture); 224 | this.position = new game.Vector(x, y); 225 | this.width = width || this.baseTexture.width; 226 | this.height = height || this.baseTexture.height; 227 | }, 228 | 229 | /** 230 | Remove texture from cache. 231 | @method remove 232 | **/ 233 | remove: function() { 234 | for (var name in game.Texture.cache) { 235 | if (game.Texture.cache[name] === this) { 236 | delete game.Texture.cache[name]; 237 | return; 238 | } 239 | } 240 | } 241 | }); 242 | 243 | game.addAttributes('Texture', { 244 | /** 245 | @attribute {Object} cache 246 | **/ 247 | cache: {}, 248 | /** 249 | @method clearCache 250 | @static 251 | **/ 252 | clearCache: function() { 253 | for (var i in this.cache) { 254 | delete this.cache[i]; 255 | } 256 | }, 257 | /** 258 | @method fromAsset 259 | @static 260 | @param {String} id 261 | @return {Texture} 262 | **/ 263 | fromAsset: function(id) { 264 | var path = game.paths[id] || id; 265 | var texture = this.cache[path]; 266 | 267 | if (!texture) { 268 | texture = game.Texture.fromImage(path); 269 | } 270 | 271 | return texture; 272 | }, 273 | /** 274 | @method fromCanvas 275 | @static 276 | @param {HTMLCanvasElement} canvas 277 | @return {Texture} 278 | **/ 279 | fromCanvas: function(canvas) { 280 | var texture = this.cache[canvas._id]; 281 | 282 | if (!texture) { 283 | var baseTexture = game.BaseTexture.fromCanvas(canvas); 284 | texture = new game.Texture(baseTexture); 285 | this.cache[canvas._id] = texture; 286 | } 287 | 288 | return texture; 289 | }, 290 | /** 291 | @method fromImage 292 | @static 293 | @param {String} path 294 | @return {Texture} 295 | **/ 296 | fromImage: function(path) { 297 | var texture = this.cache[path]; 298 | 299 | if (!texture) { 300 | texture = new game.Texture(game.BaseTexture.fromImage(path)); 301 | this.cache[path] = texture; 302 | } 303 | 304 | return texture; 305 | } 306 | }); 307 | 308 | }); 309 | -------------------------------------------------------------------------------- /src/engine/renderer/tilingsprite.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module renderer.tilingsprite 3 | **/ 4 | game.module( 5 | 'engine.renderer.tilingsprite' 6 | ) 7 | .require( 8 | 'engine.renderer.sprite' 9 | ) 10 | .body(function() { 11 | 12 | /** 13 | @class TilingSprite 14 | @extends Container 15 | @constructor 16 | @param {Texture|String} texture 17 | @param {Number} [width] 18 | @param {Number} [height] 19 | **/ 20 | game.createClass('TilingSprite', 'Container', { 21 | /** 22 | @property {Texture} texture 23 | **/ 24 | texture: null, 25 | /** 26 | @property {Vector} tilePosition 27 | **/ 28 | tilePosition: null, 29 | /** 30 | @property {Vector} _pos 31 | @private 32 | **/ 33 | _pos: [], 34 | /** 35 | @property {Vector} _rect 36 | @private 37 | **/ 38 | _rect: [], 39 | /** 40 | @property {Sprite} _sprite 41 | @private 42 | **/ 43 | _sprite: null, 44 | /** 45 | @property {Boolean} _updateSprites 46 | @private 47 | **/ 48 | _updateSprites: false, 49 | 50 | staticInit: function(texture, width, height) { 51 | this.super(); 52 | this.tilePosition = new game.Vector(); 53 | this._pos = new game.Vector(); 54 | this._rect = new game.Vector(); 55 | 56 | this.texture = this.texture || texture; 57 | this.texture = this.texture instanceof game.Texture ? this.texture : game.Texture.fromAsset(this.texture); 58 | 59 | this._width = this._width || width || this.texture.width; 60 | this._height = this._height || height || this.texture.height; 61 | 62 | this._generateSprites(); 63 | }, 64 | 65 | updateTransform: function() { 66 | this.super(); 67 | if (this._updateSprites) this._generateSprites(); 68 | }, 69 | 70 | /** 71 | @method _generateSprites 72 | @private 73 | **/ 74 | _generateSprites: function() { 75 | if (!this.texture.baseTexture.loaded) return; 76 | if (this._sprite) { 77 | delete game.TilingSprite.cache[this._sprite.texture.baseTexture._id]; 78 | this._sprite.texture.baseTexture.remove(); 79 | this._sprite.texture.remove(); 80 | } 81 | var canvas = game.TilingSprite._canvas; 82 | var context = game.TilingSprite._context; 83 | 84 | var tx = Math.ceil(this.width / this.texture.width); 85 | var ty = Math.ceil(this.height / this.texture.height); 86 | 87 | var width = tx * this.texture.width; 88 | var height = ty * this.texture.height; 89 | 90 | canvas.width = width * game.scale; 91 | canvas.height = height * game.scale; 92 | 93 | this._pos.set(0); 94 | 95 | var sprite = new game.Sprite(this.texture); 96 | for (var y = 0; y < ty; y++) { 97 | for (var x = 0; x < tx; x++) { 98 | this._pos.x = x * sprite.width * game.scale; 99 | this._pos.y = y * sprite.height * game.scale; 100 | sprite._renderCanvas(context, null, null, this._pos); 101 | } 102 | } 103 | 104 | var texture = game.Texture.fromImage(canvas.toDataURL()); 105 | texture.width = canvas.width; 106 | texture.height = canvas.height; 107 | this.tw = texture.width; 108 | this.th = texture.height; 109 | game.TilingSprite.cache[this.texture.baseTexture._id] = texture; 110 | 111 | this._sprite = new game.Sprite(texture); 112 | this._sprite._parent = this; 113 | 114 | this._updateSprites = false; 115 | }, 116 | 117 | _getBounds: function() { 118 | if (this.rotation) { 119 | var width = this.width; 120 | var height = this.height; 121 | var wt = this._worldTransform; 122 | var a = wt.a; 123 | var b = wt.b; 124 | var c = wt.c; 125 | var d = wt.d; 126 | var tx = wt.tx; 127 | var ty = wt.ty; 128 | var x2 = a * width + tx; 129 | var y2 = b * width + ty; 130 | var x3 = a * width + c * height + tx; 131 | var y3 = d * height + b * width + ty; 132 | var x4 = c * height + tx; 133 | var y4 = d * height + ty; 134 | 135 | var minX = Math.min(tx, x2, x3, x4); 136 | var minY = Math.min(ty, y2, y3, y4); 137 | var maxX = Math.max(tx, x2, x3, x4); 138 | var maxY = Math.max(ty, y2, y3, y4); 139 | } 140 | else { 141 | var minX = this._worldTransform.tx; 142 | var minY = this._worldTransform.ty; 143 | var maxX = minX + this.width; 144 | var maxY = minY + this.height; 145 | } 146 | 147 | for (var i = 0; i < this.children.length; i++) { 148 | var child = this.children[i]; 149 | var childBounds = child._getBounds(); 150 | var childMaxX = childBounds.x + childBounds.width; 151 | var childMaxY = childBounds.y + childBounds.height; 152 | if (childBounds.x < minX) minX = childBounds.x; 153 | if (childBounds.y < minY) minY = childBounds.y; 154 | if (childMaxX > maxX) maxX = childMaxX; 155 | if (childMaxY > maxY) maxY = childMaxY; 156 | } 157 | 158 | this._worldBounds.x = minX; 159 | this._worldBounds.y = minY; 160 | this._worldBounds.width = maxX - minX; 161 | this._worldBounds.height = maxY - minY; 162 | return this._worldBounds; 163 | }, 164 | 165 | _renderCanvas: function(context) { 166 | if (!this.texture) return true; 167 | if (!this.texture.baseTexture.loaded) return true; 168 | 169 | if (!this.texture.width && this.texture.baseTexture.width) { 170 | this.texture.width = this.texture.baseTexture.width; 171 | } 172 | if (!this.texture.height && this.texture.baseTexture.height) { 173 | this.texture.height = this.texture.baseTexture.height; 174 | } 175 | 176 | if (!this.texture.width || !this.texture.height) return true; 177 | 178 | var scaleX = this._worldTransform.a / this._cosCache; 179 | var scaleY = this._worldTransform.d / this._cosCache; 180 | var tw = this.tw; 181 | var th = this.th; 182 | var width = this.width / scaleX * game.scale; 183 | var height = this.height / scaleY * game.scale; 184 | var tileX = this.tilePosition.x * game.scale; 185 | var tileY = this.tilePosition.y * game.scale; 186 | 187 | var x = tileX % tw; 188 | var y = tileY % th; 189 | if (x > 0) x -= tw; 190 | if (y > 0) y -= th; 191 | 192 | for (var i = 0; i < 4; i++) { 193 | if (y >= height) break; 194 | 195 | this._rect.x = 0; 196 | this._rect.y = 0; 197 | this._rect.width = tw; 198 | this._rect.height = th; 199 | this._pos.x = x * scaleX; 200 | this._pos.y = y * scaleY; 201 | 202 | if (x + tw > width) { 203 | this._rect.width = Math.ceil(width - x); 204 | } 205 | 206 | if (y + th > height) { 207 | this._rect.height = Math.ceil(height - y); 208 | } 209 | 210 | if (x < 0) { 211 | this._rect.x = -x; 212 | this._pos.x = 0; 213 | } 214 | 215 | if (y < 0) { 216 | this._rect.y = -y; 217 | this._pos.y = 0; 218 | } 219 | 220 | if (this._rect.width > width) { 221 | this._rect.width = width; 222 | } 223 | 224 | if (this._rect.height > height) { 225 | this._rect.height = height; 226 | } 227 | 228 | if (this._rect.x + this._rect.width > tw) { 229 | this._rect.width = tw - this._rect.x; 230 | } 231 | 232 | if (this._rect.y + this._rect.height > th) { 233 | this._rect.height = th - this._rect.y; 234 | } 235 | 236 | this._sprite._worldAlpha = this._worldAlpha; 237 | this._sprite._renderCanvas(context, this._worldTransform, this._rect, this._pos); 238 | 239 | x += tw; 240 | if (x >= width) { 241 | x = tileX % tw; 242 | if (x > 0) x -= tw; 243 | y += th; 244 | } 245 | } 246 | } 247 | }); 248 | 249 | game.addAttributes('TilingSprite', { 250 | /** 251 | @attribute {Object} cache 252 | **/ 253 | cache: {}, 254 | /** 255 | @attribute {HTMLCanvasElement} _canvas 256 | @private 257 | **/ 258 | _canvas: null, 259 | /** 260 | @attribute {CanvasRenderingContext2D} _context 261 | @private 262 | **/ 263 | _context: null, 264 | 265 | /** 266 | @method clearCache 267 | @static 268 | **/ 269 | clearCache: function() { 270 | for (var i in this.cache) { 271 | this.cache[i].baseTexture.remove(); 272 | this.cache[i].remove(); 273 | delete this.cache[i]; 274 | } 275 | } 276 | }); 277 | 278 | if (typeof document !== 'undefined') { 279 | game.TilingSprite._canvas = document.createElement('canvas'); 280 | game.TilingSprite._context = game.TilingSprite._canvas.getContext('2d'); 281 | } 282 | 283 | game.defineProperties('TilingSprite', { 284 | width: { 285 | get: function() { 286 | var scaleX = this._worldTransform.a / this._cosCache; 287 | return this._width * scaleX; 288 | }, 289 | 290 | set: function(value) { 291 | if (this._width !== value) this._updateSprites = true; 292 | this._width = value; 293 | } 294 | }, 295 | 296 | height: { 297 | get: function() { 298 | var scaleY = this._worldTransform.d / this._cosCache; 299 | return this._height * scaleY; 300 | }, 301 | 302 | set: function(value) { 303 | if (this._height !== value) this._updateSprites = true; 304 | this._height = value; 305 | } 306 | } 307 | }); 308 | 309 | }); 310 | -------------------------------------------------------------------------------- /src/engine/scene.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module scene 3 | **/ 4 | game.module( 5 | 'engine.scene' 6 | ) 7 | .body(function() { 8 | 9 | /** 10 | Game scene. Instance of current scene is at `game.scene` 11 | @class Scene 12 | **/ 13 | game.createClass('Scene', { 14 | /** 15 | Background color of scene. 16 | @property {String} backgroundColor 17 | @default null 18 | **/ 19 | backgroundColor: null, 20 | /** 21 | @property {Boolean} isMouseDown 22 | @default false 23 | **/ 24 | isMouseDown: false, 25 | /** 26 | List of objects in scene. 27 | @property {Array} objects 28 | **/ 29 | objects: [], 30 | /** 31 | Is scene paused. 32 | @property {Boolean} paused 33 | @default false 34 | **/ 35 | paused: false, 36 | /** 37 | List of physics worlds in scene. 38 | @property {Array} physics 39 | **/ 40 | physics: [], 41 | /** 42 | Main container for scene. 43 | @property {Container} stage 44 | **/ 45 | stage: null, 46 | /** 47 | List of timers in scene. 48 | @property {Array} timers 49 | **/ 50 | timers: [], 51 | /** 52 | List of tweens in scene. 53 | @property {Array} tweens 54 | **/ 55 | tweens: [], 56 | /** 57 | @property {Object} _backgroundColorRgb 58 | @private 59 | **/ 60 | _backgroundColorRgb: null, 61 | /** 62 | @property {Number} _mouseDownTime 63 | @private 64 | **/ 65 | _mouseDownTime: null, 66 | /** 67 | @property {Number} _mouseDownX 68 | @private 69 | **/ 70 | _mouseDownX: null, 71 | /** 72 | @property {Number} _mouseDownY 73 | @private 74 | **/ 75 | _mouseDownY: null, 76 | /** 77 | @property {Array} _pausedAnims 78 | @private 79 | **/ 80 | _pausedAnims: [], 81 | /** 82 | @property {Array} _pausedObjects 83 | @private 84 | **/ 85 | _pausedObjects: [], 86 | /** 87 | @property {Array} _pausedTimers 88 | @private 89 | **/ 90 | _pausedTimers: [], 91 | /** 92 | @property {Array} _pausedTweens 93 | @private 94 | **/ 95 | _pausedTweens: [], 96 | /** 97 | @property {Array} _updateOrder 98 | @private 99 | **/ 100 | _updateOrder: [], 101 | 102 | staticInit: function() { 103 | this.backgroundColor = this.backgroundColor || game.Scene.backgroundColor; 104 | if (!this.backgroundColor && game.device.cocoonCanvasPlus) { 105 | this.backgroundColor = '#000'; 106 | } 107 | 108 | if (game.input) game.input._reset(); 109 | if (game.keyboard) game.keyboard._reset(); 110 | 111 | game.scene = this; 112 | 113 | this.stage = new game.Container(); 114 | this.stage.stage = this.stage; 115 | 116 | for (var i = 0; i < game.Scene.updateOrder.length; i++) { 117 | this._updateOrder.push(game.Scene.updateOrder[i].ucfirst()); 118 | } 119 | }, 120 | 121 | /** 122 | Add object to scene, so it's update function get's called every frame. 123 | @method addObject 124 | @param {Object} object 125 | **/ 126 | addObject: function(object) { 127 | if (this.objects.indexOf(object) === -1) { 128 | object._remove = false; 129 | this.objects.push(object); 130 | } 131 | }, 132 | 133 | /** 134 | Called, when mouse or touch is released and no swipe is triggered. 135 | @method click 136 | @param {Number} x 137 | @param {Number} y 138 | @param {Number} id 139 | @param {InputEvent} event 140 | **/ 141 | click: function() {}, 142 | 143 | /** 144 | Called, before scene is changed. 145 | @method exit 146 | @param {String} sceneName 147 | @return {Boolean} Return true to abort exit. 148 | **/ 149 | exit: function() {}, 150 | 151 | /** 152 | Called, when key is pressed. 153 | @method keydown 154 | @param {String} key 155 | @param {Boolean} shift 156 | @param {Boolean} ctrl 157 | @param {Boolean} alt 158 | @return {Boolean} return true to prevent default keydown action. 159 | **/ 160 | keydown: function() {}, 161 | 162 | /** 163 | Called, when key is released. 164 | @method keyup 165 | @param {String} key 166 | **/ 167 | keyup: function() {}, 168 | 169 | /** 170 | Called, when mouse or touch is down. 171 | @method mousedown 172 | @param {Number} x 173 | @param {Number} y 174 | @param {Number} id 175 | @param {InputEvent} event 176 | **/ 177 | mousedown: function() {}, 178 | 179 | /** 180 | Called, when mouse or touch is moved. 181 | @method mousemove 182 | @param {Number} x 183 | @param {Number} y 184 | @param {Number} id 185 | @param {InputEvent} event 186 | **/ 187 | mousemove: function() {}, 188 | 189 | /** 190 | Called, when mouse goes out of canvas. 191 | @method mouseout 192 | @param {InputEvent} event 193 | **/ 194 | mouseout: function() {}, 195 | 196 | /** 197 | Called, when mouse or touch is released. 198 | @method mouseup 199 | @param {Number} x 200 | @param {Number} y 201 | @param {Number} id 202 | @param {InputEvent} event 203 | **/ 204 | mouseup: function() {}, 205 | 206 | /** 207 | Called, when scene is paused. 208 | @method onPause 209 | **/ 210 | onPause: function() {}, 211 | /** 212 | Called, when system is resized. 213 | @method onResize 214 | **/ 215 | onResize: function() {}, 216 | /** 217 | Called, when paused scene is resumed. 218 | @method onResume 219 | **/ 220 | onResume: function() {}, 221 | 222 | /** 223 | Pause scene. All current objects, timers and tweens are saved and restored when pause is resumed. Also physics are not updated when scene is paused. 224 | @method pause 225 | **/ 226 | pause: function() { 227 | if (this.paused) return; 228 | this._pausedAnims.length = 0; 229 | this._pausedObjects.length = 0; 230 | this._pausedTimers.length = 0; 231 | this._pausedTweens.length = 0; 232 | for (var i = 0; i < this.objects.length; i++) { 233 | this._pausedObjects.push(this.objects[i]); 234 | } 235 | for (var i = 0; i < this.timers.length; i++) { 236 | this.timers[i]._pauseCache = this.timers[i]._pause; 237 | this.timers[i].pause(); 238 | this._pausedTimers.push(this.timers[i]); 239 | } 240 | for (var i = 0; i < this.tweens.length; i++) { 241 | this._pausedTweens.push(this.tweens[i]); 242 | } 243 | this._getPausedAnims(); 244 | this.objects.length = 0; 245 | this.timers.length = 0; 246 | this.tweens.length = 0; 247 | this.paused = true; 248 | this.onPause(); 249 | }, 250 | 251 | /** 252 | Remove object from scene, so it's update function doesn't get called anymore. 253 | @method removeObject 254 | @param {Object} object 255 | **/ 256 | removeObject: function(object) { 257 | object._remove = true; 258 | }, 259 | 260 | /** 261 | Remove timer from scene. 262 | @method removeTimer 263 | @param {Timer} timer 264 | @param {Boolean} [doCallback] 265 | **/ 266 | removeTimer: function(timer, doCallback) { 267 | if (!timer) return; 268 | if (!doCallback) timer.callback = null; 269 | timer.repeat = false; 270 | timer.set(0); 271 | }, 272 | 273 | /** 274 | Remove all timers from scene. 275 | @method removeTimers 276 | @param {Boolean} [doCallback] 277 | **/ 278 | removeTimers: function(doCallback) { 279 | for (var i = this.timers.length - 1; i >= 0; i--) { 280 | this.removeTimer(this.timers[i], doCallback); 281 | } 282 | }, 283 | 284 | /** 285 | Remove all tweens from scene. 286 | @method removeTweens 287 | **/ 288 | removeTweens: function() { 289 | for (var i = 0; i < this.tweens.length; i++) { 290 | this.tweens[i]._shouldRemove = true; 291 | } 292 | }, 293 | 294 | /** 295 | Resume paused scene. 296 | @method resume 297 | **/ 298 | resume: function() { 299 | if (!this.paused) return; 300 | for (var i = 0; i < this._pausedObjects.length; i++) { 301 | this.objects.push(this._pausedObjects[i]); 302 | } 303 | for (var i = 0; i < this._pausedTimers.length; i++) { 304 | if (this._pausedTimers[i]._pauseCache > 0) this._pausedTimers[i].pause(); 305 | else this._pausedTimers[i].resume(); 306 | this.timers.push(this._pausedTimers[i]); 307 | } 308 | for (var i = 0; i < this._pausedTweens.length; i++) { 309 | this.tweens.push(this._pausedTweens[i]); 310 | } 311 | this._pausedAnims.length = 0; 312 | this.paused = false; 313 | this.onResume(); 314 | }, 315 | 316 | /** 317 | Callback for swipe. 318 | @method swipe 319 | @param {String} direction 320 | **/ 321 | swipe: function() {}, 322 | 323 | /** 324 | This is called every frame. 325 | @method update 326 | **/ 327 | update: function() {}, 328 | 329 | /** 330 | @method _exit 331 | @param {String} sceneName 332 | @private 333 | **/ 334 | _exit: function(sceneName) { 335 | if (game.audio && game.Audio.stopOnSceneChange) { 336 | game.audio.stopMusic(); 337 | for (var i = 0; i < game.audio.sounds.length; i++) { 338 | game.audio.sounds[i].stop(true); 339 | } 340 | } 341 | 342 | var exit = this.exit(sceneName); 343 | return exit; 344 | }, 345 | 346 | _getPausedAnims: function(container) { 347 | container = container || this.stage; 348 | for (var i = 0; i < container.children.length; i++) { 349 | var child = container.children[i]; 350 | if (child instanceof game.Animation) this._pausedAnims.push(child); 351 | if (child.children.length) this._getPausedAnims(child); 352 | } 353 | }, 354 | 355 | /** 356 | @method _mousedown 357 | @param {Number} x 358 | @param {Number} y 359 | @param {Number} id 360 | @param {InputEvent} event 361 | @private 362 | **/ 363 | _mousedown: function(x, y, id, event) { 364 | this.isMouseDown = true; 365 | this._mouseDownTime = game.Timer.time; 366 | this._mouseDownX = x; 367 | this._mouseDownY = y; 368 | this.mousedown(x, y, id, event); 369 | }, 370 | 371 | /** 372 | @method _mousemove 373 | @param {Number} x 374 | @param {Number} y 375 | @param {Number} id 376 | @param {InputEvent} event 377 | @private 378 | **/ 379 | _mousemove: function(x, y, id, event) { 380 | this.mousemove(x, y, id, event); 381 | if (!this._mouseDownTime) return; 382 | if (x - this._mouseDownX >= game.Scene.swipeDist) this._swipe('RIGHT'); 383 | else if (x - this._mouseDownX <= -game.Scene.swipeDist) this._swipe('LEFT'); 384 | else if (y - this._mouseDownY >= game.Scene.swipeDist) this._swipe('DOWN'); 385 | else if (y - this._mouseDownY <= -game.Scene.swipeDist) this._swipe('UP'); 386 | }, 387 | 388 | /** 389 | @method _mouseup 390 | @param {Number} x 391 | @param {Number} y 392 | @param {Number} id 393 | @param {InputEvent} event 394 | @private 395 | **/ 396 | _mouseup: function(x, y, id, event) { 397 | this.isMouseDown = false; 398 | if (this._mouseDownTime) { 399 | var time = game.Timer.time - this._mouseDownTime; 400 | if (game.Input.clickTimeout === 0 || time < game.Input.clickTimeout) this.click(x, y, id, event); 401 | } 402 | this._mouseDownTime = null; 403 | this.mouseup(x, y, id, event); 404 | }, 405 | 406 | /** 407 | @method _pause 408 | @private 409 | **/ 410 | _pause: function() { 411 | if (game.audio) game.audio._systemPause(); 412 | }, 413 | 414 | /** 415 | @method _resume 416 | @private 417 | **/ 418 | _resume: function() { 419 | if (game.audio) game.audio._systemResume(); 420 | }, 421 | 422 | /** 423 | @method _swipe 424 | @param {String} dir 425 | @private 426 | **/ 427 | _swipe: function(dir) { 428 | var time = game.Timer.time - this._mouseDownTime; 429 | this._mouseDownTime = null; 430 | if (time <= game.Scene.swipeTime || game.Scene.swipeTime === 0) { 431 | this.swipe(dir); 432 | } 433 | }, 434 | 435 | /** 436 | @method _update 437 | @private 438 | **/ 439 | _update: function() { 440 | for (var i = 0; i < this._updateOrder.length; i++) { 441 | this['_update' + this._updateOrder[i]](); 442 | } 443 | }, 444 | 445 | /** 446 | @method _updateCollision 447 | @private 448 | **/ 449 | _updateCollision: function() { 450 | if (this.paused) return; 451 | for (var i = 0; i < this.physics.length; i++) { 452 | this.physics[i]._updateCollision(); 453 | } 454 | }, 455 | 456 | /** 457 | @method _updateObjects 458 | @private 459 | **/ 460 | _updateObjects: function() { 461 | for (var i = this.objects.length - 1; i >= 0; i--) { 462 | if (typeof this.objects[i].update === 'function' && !this.objects[i]._remove) this.objects[i].update(); 463 | if (this.objects[i]._remove) this.objects.splice(i, 1); 464 | } 465 | }, 466 | 467 | /** 468 | @method _updatePhysics 469 | @private 470 | **/ 471 | _updatePhysics: function() { 472 | if (this.paused) return; 473 | for (var i = 0; i < this.physics.length; i++) { 474 | this.physics[i]._update(); 475 | } 476 | }, 477 | 478 | /** 479 | @method _updateRenderer 480 | @private 481 | **/ 482 | _updateRenderer: function() { 483 | if (game.renderer) game.renderer._render(this.stage); 484 | }, 485 | 486 | /** 487 | @method _updateScene 488 | @private 489 | **/ 490 | _updateScene: function() { 491 | this.update(); 492 | }, 493 | 494 | /** 495 | @method _updateTimers 496 | @private 497 | **/ 498 | _updateTimers: function() { 499 | for (var i = this.timers.length - 1; i >= 0; i--) { 500 | if (this.timers[i].time() === 0) { 501 | if (typeof this.timers[i].callback === 'function') this.timers[i].callback(); 502 | if (this.timers[i].repeat) this.timers[i]._base = game.Timer.time; 503 | else this.timers.splice(i, 1); 504 | } 505 | } 506 | }, 507 | 508 | /** 509 | @method _updateTweens 510 | @private 511 | **/ 512 | _updateTweens: function() { 513 | for (var i = this.tweens.length - 1; i >= 0; i--) { 514 | if (!this.tweens[i]._update()) this.tweens.splice(i, 1); 515 | } 516 | } 517 | }); 518 | 519 | game.addAttributes('Scene', { 520 | /** 521 | Default background color. 522 | @attribute {String} backgroundColor 523 | @default #000 524 | **/ 525 | backgroundColor: '#000', 526 | /** 527 | Minimum distance to trigger swipe. 528 | @attribute {Number} swipeDist 529 | @default 100 530 | **/ 531 | swipeDist: 100, 532 | /** 533 | Maximum time to trigger swipe (ms). 534 | @attribute {Number} swipeTime 535 | @default 500 536 | **/ 537 | swipeTime: 500, 538 | /** 539 | Update order for each frame. 540 | @attribute {Array} updateOrder 541 | @default physics,tweens,collision,timers,scene,objects,renderer 542 | **/ 543 | updateOrder: [ 544 | 'physics', 545 | 'tweens', 546 | 'collision', 547 | 'timers', 548 | 'scene', 549 | 'objects', 550 | 'renderer' 551 | ] 552 | }); 553 | 554 | }); 555 | -------------------------------------------------------------------------------- /src/engine/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module storage 3 | **/ 4 | game.module( 5 | 'engine.storage' 6 | ) 7 | .body(function() { 8 | 9 | /** 10 | Local storage manager. Instance automatically created at `game.storage`, when `Storage.id` is defined 11 | @class Storage 12 | @constructor 13 | @param {String} [id] 14 | **/ 15 | game.createClass('Storage', { 16 | /** 17 | @property {String} id 18 | **/ 19 | id: '', 20 | /** 21 | Is local storage supported. 22 | @property {Boolean} supported 23 | **/ 24 | supported: false, 25 | 26 | init: function(id) { 27 | this.id = id || game.Storage.id; 28 | this.supported = this._isSupported(); 29 | }, 30 | 31 | /** 32 | Clear storage. This removes ALL keys. 33 | @method clear 34 | **/ 35 | clear: function() { 36 | for (var i = localStorage.length - 1; i >= 0; i--) { 37 | var key = localStorage.key(i); 38 | if (key.indexOf(this.id + '.') !== -1) localStorage.removeItem(key); 39 | } 40 | }, 41 | 42 | /** 43 | Get value from storage. 44 | @method get 45 | @param {String} key 46 | @param {*} [defaultValue] 47 | @return {*} value 48 | **/ 49 | get: function(key, defaultValue) { 50 | var val = localStorage.getItem(this.id + '.' + key); 51 | if (val === null) return defaultValue; 52 | try { 53 | return this._decode(val); 54 | } 55 | catch (e) { 56 | return val; 57 | } 58 | }, 59 | 60 | /** 61 | Check if a key exists in storage. 62 | @method has 63 | @param {String} key 64 | @return {Boolean} 65 | **/ 66 | has: function(key) { 67 | return localStorage.getItem(this.id + '.' + key) !== null; 68 | }, 69 | 70 | /** 71 | Remove key from storage. 72 | @method remove 73 | @param {String} key 74 | **/ 75 | remove: function(key) { 76 | localStorage.removeItem(this.id + '.' + key); 77 | }, 78 | 79 | /** 80 | Set value to storage. 81 | @method set 82 | @param {String} key 83 | @param {*} value 84 | @return {*} value 85 | **/ 86 | set: function(key, value) { 87 | if (this.supported) localStorage.setItem(this.id + '.' + key, this._encode(value)); 88 | return value; 89 | }, 90 | 91 | /** 92 | @method _decode 93 | @private 94 | **/ 95 | _decode: function(str) { 96 | return JSON.parse(str); 97 | }, 98 | 99 | /** 100 | @method _encode 101 | @private 102 | **/ 103 | _encode: function(val) { 104 | return JSON.stringify(val); 105 | }, 106 | 107 | /** 108 | @method _isSupported 109 | @private 110 | **/ 111 | _isSupported: function() { 112 | if (typeof localStorage !== 'object') return false; 113 | try { 114 | localStorage.setItem('localStorage', 1); 115 | localStorage.removeItem('localStorage'); 116 | } 117 | catch (e) { 118 | return false; 119 | } 120 | return true; 121 | } 122 | }); 123 | 124 | game.addAttributes('Storage', { 125 | /** 126 | @attribute {String} id 127 | **/ 128 | id: '' 129 | }); 130 | 131 | }); 132 | -------------------------------------------------------------------------------- /src/engine/system.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module system 3 | **/ 4 | game.module( 5 | 'engine.system' 6 | ) 7 | .body(function() { 8 | 9 | /** 10 | System manager. Instance automatically created at `game.system` 11 | @class System 12 | **/ 13 | game.createClass('System', { 14 | /** 15 | Canvas height. 16 | @property {Number} canvasHeight 17 | **/ 18 | canvasHeight: 0, 19 | /** 20 | Canvas width. 21 | @property {Number} canvasWidth 22 | **/ 23 | canvasWidth: 0, 24 | /** 25 | Time since last frame (seconds), shorthand game.delta 26 | @property {Number} delta 27 | **/ 28 | delta: 0, 29 | /** 30 | Height of the game canvas (pixels), shorthand game.height 31 | @property {Number} height 32 | **/ 33 | height: 0, 34 | /** 35 | Is engine in HiRes mode. 36 | @property {Boolean} hires 37 | **/ 38 | hires: false, 39 | /** 40 | Original height. 41 | @property {Number} originalHeight 42 | **/ 43 | originalHeight: 0, 44 | /** 45 | Original width. 46 | @property {Number} originalWidth 47 | **/ 48 | originalWidth: 0, 49 | /** 50 | Is engine paused. 51 | @property {Boolean} paused 52 | **/ 53 | paused: false, 54 | /** 55 | Current scene (game.scene). 56 | @property {Scene} scene 57 | **/ 58 | scene: null, 59 | /** 60 | Name of current scene. 61 | @property {String} sceneName 62 | **/ 63 | sceneName: null, 64 | /** 65 | Width of the game canvas (pixels), shorthand game.width 66 | @property {Number} width 67 | **/ 68 | width: 0, 69 | /** 70 | @property {String} _newSceneName 71 | @private 72 | **/ 73 | _newSceneName: null, 74 | /** 75 | @property {Boolean} _pausedOnHide 76 | @private 77 | **/ 78 | _pausedOnHide: false, 79 | /** 80 | @property {Boolean} _rotateScreenVisible 81 | @default false 82 | @private 83 | **/ 84 | _rotateScreenVisible: false, 85 | /** 86 | @property {Number} _runLoopId 87 | @private 88 | **/ 89 | _runLoopId: 0, 90 | /** 91 | @property {Boolean} _running 92 | @private 93 | **/ 94 | _running: false, 95 | /** 96 | @property {Number} _windowWidth 97 | @private 98 | **/ 99 | _windowWidth: 0, 100 | /** 101 | @property {Number} _windowHeight 102 | @private 103 | **/ 104 | _windowHeight: 0, 105 | 106 | init: function() { 107 | this._updateWindowSize(); 108 | 109 | game.width = this.width = this.originalWidth = game.System.width; 110 | game.height = this.height = this.originalHeight = game.System.height; 111 | game.delta = this.delta; 112 | 113 | var realWidth = this.originalWidth; 114 | var realHeight = this.originalHeight; 115 | if (game.System.hidpi) { 116 | realWidth /= game.device.pixelRatio; 117 | realHeight /= game.device.pixelRatio; 118 | } 119 | 120 | for (var i = 2; i <= game.System.hires; i += 2) { 121 | var ratio = game.System.hiresRatio * (i / 2); 122 | var width = game.System.hiresDeviceSize ? game.device.screen.width : this._windowWidth; 123 | var height = game.System.hiresDeviceSize ? game.device.screen.height : this._windowHeight; 124 | if (width >= realWidth * ratio && height >= realHeight * ratio) { 125 | this.hires = true; 126 | game.scale = i; 127 | } 128 | } 129 | 130 | this.canvasWidth = this.originalWidth * game.scale; 131 | this.canvasHeight = this.originalHeight * game.scale; 132 | 133 | if (typeof document === 'undefined') return; 134 | 135 | document.addEventListener(this._getVisibilityChangeFunction(), this._visibilityChange); 136 | 137 | if (game.System.resize) game.System.center = false; 138 | 139 | this._initRenderer(); 140 | 141 | if (game.System.hidpi) { 142 | this.canvasWidth /= game.device.pixelRatio; 143 | this.canvasHeight /= game.device.pixelRatio; 144 | } 145 | 146 | this._onWindowResizeFunc = this._onWindowResize.bind(this); 147 | if (game.device.WKWebView) window.addEventListener('orientationchange', this._onWindowResizeFunc); 148 | else window.addEventListener('resize', this._onWindowResizeFunc); 149 | this._onWindowResize(); 150 | }, 151 | 152 | /** 153 | Request fullscreen mode. 154 | @method fullscreen 155 | **/ 156 | fullscreen: function() { 157 | if (game.renderer.canvas.requestFullscreen) game.renderer.canvas.requestFullscreen(); 158 | else if (game.renderer.canvas.requestFullScreen) game.renderer.canvas.requestFullScreen(); 159 | }, 160 | 161 | /** 162 | Test fullscreen support. 163 | @method fullscreenSupport 164 | @return {Boolean} Return true, if browser supports fullscreen mode. 165 | **/ 166 | fullscreenSupport: function() { 167 | return !!(game.renderer.canvas.requestFullscreen || game.renderer.canvas.requestFullScreen); 168 | }, 169 | 170 | /** 171 | Load new scene using default loader. 172 | @method loadScene 173 | @param {String} scenenName 174 | **/ 175 | loadScene: function(sceneName) { 176 | if (!game.mediaQueue.length) this.setScene(sceneName); 177 | else this.setScene(game.System.loader, sceneName); 178 | }, 179 | 180 | /** 181 | Resize system. 182 | @method resize 183 | @param {Number} width 184 | @param {Number} height 185 | **/ 186 | resize: function(width, height) { 187 | if (this.width === width && this.height === height) return; 188 | game.width = this.width = width / game.scale; 189 | game.height = this.height = height / game.scale; 190 | game.renderer._resize(width, height); 191 | if (this.scene && this.scene.onResize) this.scene.onResize(); 192 | }, 193 | 194 | /** 195 | Pause game engine. 196 | @method pause 197 | @param {Boolean} onHide 198 | **/ 199 | pause: function(onHide) { 200 | if (this.paused) return; 201 | if (onHide) this._pausedOnHide = true; 202 | else this.paused = true; 203 | if (this.scene && this.scene._pause) this.scene._pause(); 204 | }, 205 | 206 | /** 207 | Resume game engine. 208 | @method resume 209 | @param {Boolean} onHide 210 | **/ 211 | resume: function(onHide) { 212 | if (onHide && this.paused) return; 213 | if (!onHide && !this.paused) return; 214 | if (onHide) this._pausedOnHide = false; 215 | else this.paused = false; 216 | game.Timer.last = Date.now(); 217 | if (this.scene && this.scene._resume) this.scene._resume(); 218 | }, 219 | 220 | /** 221 | Change current scene. If you don't define sceneName, current scene will be restarted. 222 | @method setScene 223 | @param {String} sceneName 224 | @param {String} [param] 225 | **/ 226 | setScene: function(sceneName, param) { 227 | if (!sceneName && this.scene) sceneName = this.scene.constructor._name; 228 | if (!game[sceneName]) throw 'Scene ' + sceneName + ' not found'; 229 | if (this._running && !this.paused) { 230 | this._newSceneName = sceneName; 231 | this._newSceneParam = param; 232 | } 233 | else this._setSceneNow(sceneName, param); 234 | }, 235 | 236 | /** 237 | @method _getVisibilityChangeFunction 238 | @private 239 | **/ 240 | _getVisibilityChangeFunction: function() { 241 | if (typeof document.hidden !== 'undefined') { 242 | return 'visibilitychange'; 243 | } 244 | else if (typeof document.mozHidden !== 'undefined') { 245 | return 'mozvisibilitychange'; 246 | } 247 | else if (typeof document.msHidden !== 'undefined') { 248 | return 'msvisibilitychange'; 249 | } 250 | else if (typeof document.webkitHidden !== 'undefined') { 251 | return 'webkitvisibilitychange'; 252 | } 253 | }, 254 | 255 | /** 256 | @method _hideRotateScreen 257 | @private 258 | **/ 259 | _hideRotateScreen: function() { 260 | if (!this._rotateScreenVisible) return; 261 | this._rotateScreenVisible = false; 262 | game.renderer._show(); 263 | document.body.className = ''; 264 | }, 265 | 266 | /** 267 | @method _initRenderer 268 | @private 269 | **/ 270 | _initRenderer: function() { 271 | game.renderer = new game.Renderer(this.canvasWidth, this.canvasHeight); 272 | }, 273 | 274 | /** 275 | @method _onWindowResize 276 | @private 277 | **/ 278 | _onWindowResize: function() { 279 | this._updateWindowSize(); 280 | if (this._toggleRotateScreen()) return; 281 | 282 | var scalePercent = game.System.scalePercent / 100; 283 | this._scale(this._windowWidth * scalePercent, this._windowHeight * scalePercent); 284 | this._resize(this._windowWidth, this._windowHeight); 285 | 286 | if (game.System.center) { 287 | game.renderer._position((this._windowWidth - this.canvasWidth) / 2, (this._windowHeight - this.canvasHeight) / 2); 288 | } 289 | 290 | if (game.System.scale || game.System.resize || game.System.hidpi && game.device.pixelRatio > 1) { 291 | game.renderer._size(this.canvasWidth, this.canvasHeight); 292 | } 293 | 294 | if (game.isStarted && !game.scene) game.onStart(); 295 | }, 296 | 297 | /** 298 | Remove all event listeners. 299 | @method _remove 300 | @private 301 | **/ 302 | _remove: function() { 303 | document.removeEventListener(this._getVisibilityChangeFunction(), this._visibilityChange); 304 | if (game.device.WKWebView) window.removeEventListener('orientationchange', this._onWindowResizeFunc); 305 | else window.removeEventListener('resize', this._onWindowResizeFunc); 306 | }, 307 | 308 | /** 309 | @method _resize 310 | @param {Number} width 311 | @param {Number} height 312 | @private 313 | **/ 314 | _resize: function(width, height) { 315 | if (!game.System.resize) return; 316 | if (!game.System.scale) { 317 | this.resize(width, height); 318 | this.canvasWidth = width; 319 | this.canvasHeight = height; 320 | return; 321 | } 322 | 323 | var widthSpace = width - this.canvasWidth; 324 | var heightSpace = height - this.canvasHeight; 325 | 326 | if (widthSpace > 0) { 327 | var ratio = widthSpace / this.canvasWidth; 328 | var newWidth = ~~(this.originalWidth * game.scale + this.originalWidth * ratio * game.scale); 329 | 330 | this.resize(newWidth, this.originalHeight * game.scale); 331 | this.canvasWidth += widthSpace; 332 | } 333 | else if (heightSpace > 0) { 334 | var ratio = heightSpace / this.canvasHeight; 335 | var newHeight = ~~(this.originalHeight * game.scale + this.originalHeight * ratio * game.scale); 336 | 337 | this.resize(this.originalWidth * game.scale, newHeight); 338 | this.canvasHeight += heightSpace; 339 | } 340 | }, 341 | 342 | /** 343 | @method _run 344 | @private 345 | **/ 346 | _run: function() { 347 | if (this.paused || this._pausedOnHide) return; 348 | 349 | game.Timer.update(); 350 | game.delta = this.delta = game.Timer.delta / 1000; 351 | 352 | if (game.input) game.input._update(); 353 | this.scene._update(); 354 | 355 | if (this._newSceneName) this._setSceneNow(this._newSceneName, this._newSceneParam); 356 | }, 357 | 358 | /** 359 | @method _scale 360 | @param {Number} width 361 | @param {Number} height 362 | @private 363 | **/ 364 | _scale: function(width, height) { 365 | if (!game.System.scale) return; 366 | 367 | if (width / this.originalWidth < height / this.originalHeight) { 368 | this.canvasWidth = width; 369 | this.canvasHeight = ~~(width * (this.originalHeight / this.originalWidth)); 370 | } 371 | else { 372 | this.canvasWidth = ~~(height * (this.originalWidth / this.originalHeight)); 373 | this.canvasHeight = height; 374 | } 375 | 376 | if (game.System.scaleMax > 0) { 377 | var maxWidth = this.originalWidth * (game.System.scaleMax / 100); 378 | var maxHeight = this.originalHeight * (game.System.scaleMax / 100); 379 | if (this.canvasWidth > maxWidth) this.canvasWidth = maxWidth; 380 | if (this.canvasHeight > maxHeight) this.canvasHeight = maxHeight; 381 | } 382 | }, 383 | 384 | /** 385 | @method _setSceneNow 386 | @param {String} sceneName 387 | @param {*} [param] 388 | @private 389 | **/ 390 | _setSceneNow: function(sceneName, param) { 391 | this.sceneName = sceneName; 392 | this._newSceneName = null; 393 | if (this.scene && this.scene._exit(sceneName)) return; 394 | if (this.paused) this.paused = false; 395 | game.TilingSprite.clearCache(); 396 | game.Sprite._clearTintedTextures(); 397 | this.scene = new game[sceneName](param); 398 | this._startRunLoop(); 399 | }, 400 | 401 | /** 402 | @method _showRotateScreen 403 | @private 404 | **/ 405 | _showRotateScreen: function() { 406 | if (this._rotateScreenVisible) return; 407 | this._rotateScreenVisible = true; 408 | game.renderer._hide(); 409 | document.body.className = game.System.rotateScreenClass; 410 | }, 411 | 412 | /** 413 | @method _startRunLoop 414 | @private 415 | **/ 416 | _startRunLoop: function() { 417 | if (!this.scene) return; 418 | if (this._runLoopId) this._stopRunLoop(); 419 | this._runLoopId = game._setGameLoop(this._run.bind(this)); 420 | this._running = true; 421 | }, 422 | 423 | /** 424 | @method _stopRunLoop 425 | @private 426 | **/ 427 | _stopRunLoop: function() { 428 | game._clearGameLoop(this._runLoopId); 429 | this._running = false; 430 | }, 431 | 432 | /** 433 | @method _toggleRotateScreen 434 | @private 435 | **/ 436 | _toggleRotateScreen: function() { 437 | if (!game.device.mobile || !game.System.rotateScreen) return false; 438 | 439 | if (this.originalWidth > this.originalHeight && this._windowWidth < this._windowHeight || 440 | this.originalHeight > this.originalWidth && this._windowHeight < this._windowWidth) { 441 | this._showRotateScreen(); 442 | return true; 443 | } 444 | 445 | this._hideRotateScreen(); 446 | return false; 447 | }, 448 | 449 | /** 450 | @method _updateWindowSize 451 | @private 452 | **/ 453 | _updateWindowSize: function() { 454 | if (typeof document === 'undefined') return; 455 | if (game.device.android && window.innerHeight - this._windowHeight === 24) return; // Fix Android 6 screen size bug 456 | this._windowWidth = game.device.WKWebView ? document.documentElement.clientWidth : window.innerWidth; 457 | this._windowHeight = game.device.WKWebView ? document.documentElement.clientHeight : window.innerHeight; 458 | if (game.device.crosswalk && this._windowWidth === 0) { 459 | this._windowWidth = window.screen.width; 460 | this._windowHeight = window.screen.height; 461 | } 462 | if (Math.abs(window.orientation) === 90 && game.device.iPhone && game.device.safari) { 463 | // Fix iPhone Safari landscape fullscreen 464 | this._windowHeight++; 465 | } 466 | }, 467 | 468 | /** 469 | @method _visibilityChange 470 | @private 471 | **/ 472 | _visibilityChange: function() { 473 | if (game.System.pauseOnHide) { 474 | var hidden = !!game._getVendorAttribute(document, 'hidden'); 475 | if (hidden) game.system.pause(true); 476 | else game.system.resume(true); 477 | } 478 | } 479 | }); 480 | 481 | game.addAttributes('System', { 482 | /** 483 | Id for canvas element, where game is placed. If none found, it will be created. 484 | @attribute {String} canvasId 485 | @default canvas 486 | **/ 487 | canvasId: 'canvas', 488 | /** 489 | Position canvas to center of window. 490 | @attribute {Boolean} center 491 | @default true 492 | **/ 493 | center: true, 494 | /** 495 | Set custom frame rate for game loop. 496 | @attribute {Number} frameRate 497 | @default null 498 | **/ 499 | frameRate: null, 500 | /** 501 | System height. 502 | @attribute {Number} height 503 | @default 768 504 | **/ 505 | height: 768, 506 | /** 507 | Scale canvas for HiDPI screens. 508 | @attribute {Boolean} hidpi 509 | @default true 510 | **/ 511 | hidpi: true, 512 | /** 513 | HiRes mode multiplier. 514 | @attribute {Number} hires 515 | @default 0 516 | **/ 517 | hires: 0, 518 | /** 519 | Use device size instead of window size on HiRes mode. 520 | @attribute {Boolean} hiresDeviceSize 521 | @default false 522 | **/ 523 | hiresDeviceSize: false, 524 | /** 525 | Ratio value, when HiRes mode is used. 526 | @attribute {Number} hiresRatio 527 | @default 2 528 | **/ 529 | hiresRatio: 2, 530 | /** 531 | Default loader class. 532 | @attribute {String} loader 533 | @default Loader 534 | **/ 535 | loader: 'Loader', 536 | /** 537 | Pause engine, when page is hidden. 538 | @attribute {Boolean} pauseOnHide 539 | @default true 540 | **/ 541 | pauseOnHide: true, 542 | /** 543 | Resize canvas to fill window. 544 | @attribute {Boolean} resize 545 | @default false 546 | **/ 547 | resize: false, 548 | /** 549 | Use rotate screen on mobile. 550 | @attribute {Boolean} rotateScreen 551 | @default true 552 | **/ 553 | rotateScreen: true, 554 | /** 555 | Class name for document body, when rotate screen visible. 556 | @attribute {String} rotateScreenClass 557 | @default rotate 558 | **/ 559 | rotateScreenClass: 'rotate', 560 | /** 561 | Scale canvas to fit window. 562 | @attribute {Boolean} scale 563 | @default true 564 | **/ 565 | scale: true, 566 | /** 567 | Maximum percent of scaling (0 = disabled). 568 | @attribute {Number} scaleMax 569 | @default 0 570 | **/ 571 | scaleMax: 0, 572 | /** 573 | @attribute {Number} scalePercent 574 | @default 100 575 | **/ 576 | scalePercent: 100, 577 | /** 578 | Name of start scene. 579 | @attribute {String} startScene 580 | @default Main 581 | **/ 582 | startScene: 'Main', 583 | /** 584 | System width. 585 | @attribute {Number} width 586 | @default 1024 587 | **/ 588 | width: 1024 589 | }); 590 | 591 | }); 592 | -------------------------------------------------------------------------------- /src/engine/timer.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module timer 3 | **/ 4 | game.module( 5 | 'engine.timer' 6 | ) 7 | .body(function() { 8 | 9 | /** 10 | @class Timer 11 | @constructor 12 | @param {Number} [time] Timer's target time (milliseconds) 13 | **/ 14 | game.createClass('Timer', { 15 | /** 16 | Function to call, when timer's time is reached. 17 | @property {Function} callback 18 | **/ 19 | callback: null, 20 | /** 21 | Is timer paused. 22 | @property {Boolean} paused 23 | **/ 24 | paused: false, 25 | /** 26 | Should timer repeat. 27 | @property {Boolean} repeat 28 | **/ 29 | repeat: false, 30 | /** 31 | Timer's target time. 32 | @property {Number} target 33 | **/ 34 | target: 0, 35 | /** 36 | @property {Number} _base 37 | @private 38 | **/ 39 | _base: 0, 40 | /** 41 | @property {Number} _last 42 | @private 43 | **/ 44 | _last: 0, 45 | /** 46 | @property {Number} _pauseTime 47 | @private 48 | **/ 49 | _pauseTime: 0, 50 | 51 | init: function(time) { 52 | this._last = game.Timer.time; 53 | this.set(time); 54 | }, 55 | 56 | /** 57 | Clear timer. 58 | @method clear 59 | **/ 60 | clear: function() { 61 | this.callback = null; 62 | this.repeat = false; 63 | this.set(0); 64 | }, 65 | 66 | /** 67 | Get time since last frame. 68 | @method delta 69 | @return {Number} delta 70 | **/ 71 | delta: function() { 72 | var delta = game.Timer.time - this._last; 73 | this._last = game.Timer.time; 74 | return this.paused ? 0 : delta; 75 | }, 76 | 77 | /** 78 | Pause timer. 79 | @method pause 80 | **/ 81 | pause: function() { 82 | if (this.paused) return; 83 | this._pauseTime = game.Timer.time; 84 | this.paused = true; 85 | }, 86 | 87 | /** 88 | Reset timer. 89 | @method reset 90 | **/ 91 | reset: function() { 92 | this._base = game.Timer.time; 93 | this._pauseTime = 0; 94 | this.paused = false; 95 | }, 96 | 97 | /** 98 | Resume paused timer. 99 | @method resume 100 | **/ 101 | resume: function() { 102 | if (!this.paused) return; 103 | this._base += game.Timer.time - this._pauseTime; 104 | this._pauseTime = 0; 105 | this.paused = false; 106 | }, 107 | 108 | /** 109 | Set target time for timer. 110 | @method set 111 | @param {Number} ms 112 | **/ 113 | set: function(ms) { 114 | if (typeof ms !== 'number') ms = 0; 115 | this.target = ms; 116 | this.reset(); 117 | }, 118 | 119 | /** 120 | Get time left. 121 | @method time 122 | @return {Number} time 123 | **/ 124 | time: function() { 125 | var time = this._base + this.target - (this._pauseTime || game.Timer.time); 126 | return time < 0 ? 0 : time; 127 | } 128 | }); 129 | 130 | game.addAttributes('Timer', { 131 | /** 132 | Main timer's delta (ms). 133 | @attribute {Number} delta 134 | **/ 135 | delta: 0, 136 | /** 137 | Main timer's minimum fps. 138 | @attribute {Number} minFPS 139 | @default 20 140 | **/ 141 | minFPS: 20, 142 | /** 143 | Main timer's speed factor. 144 | @attribute {Number} speed 145 | @default 1 146 | **/ 147 | speed: 1, 148 | /** 149 | Current time. 150 | @attribute {Number} time 151 | **/ 152 | time: 0, 153 | /** 154 | @attribute {Number} _last 155 | @private 156 | **/ 157 | _last: 0, 158 | /** 159 | @attribute {Number} _lastFrameTime 160 | @private 161 | **/ 162 | _lastFrameTime: 0, 163 | /** 164 | @attribute {Number} _realDelta 165 | @private 166 | **/ 167 | _realDelta: 0, 168 | /** 169 | Add timer to scene. 170 | @method add 171 | @static 172 | @param {Number} time Time (ms). 173 | @param {Function} callback Callback function to run, when timer ends. 174 | @param {Boolean} [repeat] 175 | @param {Boolean} [instant] 176 | @return {Timer} 177 | **/ 178 | add: function(time, callback, repeat, instant) { 179 | var timer = new game.Timer(time); 180 | timer.repeat = !!repeat; 181 | timer.callback = callback; 182 | game.scene.timers.push(timer); 183 | if (instant) callback(); 184 | return timer; 185 | }, 186 | /** 187 | Update main timer. 188 | @method update 189 | @static 190 | **/ 191 | update: function() { 192 | var now = Date.now(); 193 | if (!this._last) this._last = now; 194 | this._realDelta = now - this._last; 195 | this.delta = Math.min(this._realDelta, 1000 / this.minFPS) * this.speed; 196 | this._lastFrameTime = this.time; 197 | this.time += this.delta; 198 | this._last = now; 199 | } 200 | }); 201 | 202 | }); 203 | -------------------------------------------------------------------------------- /src/game/config.js: -------------------------------------------------------------------------------- 1 | game.config = { 2 | system: { 3 | width: 1024, 4 | height: 768, 5 | center: true, 6 | scale: true 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/game/main.js: -------------------------------------------------------------------------------- 1 | game.module( 2 | 'game.main' 3 | ) 4 | .body(function() { 5 | 6 | game.addAsset('panda.png'); 7 | 8 | game.createScene('Main', { 9 | init: function() { 10 | var sprite = new game.Sprite('panda.png'); 11 | sprite.addTo(this.stage); 12 | } 13 | }); 14 | 15 | }); 16 | --------------------------------------------------------------------------------