1 goog.provide('lime.Director'); 2 3 4 goog.require('lime.CoverNode'); 5 goog.require('goog.array'); 6 goog.require('goog.dom'); 7 goog.require('goog.dom.ViewportSizeMonitor'); 8 goog.require('goog.dom.classes'); 9 goog.require('goog.events'); 10 goog.require('goog.math.Box'); 11 goog.require('goog.math.Coordinate'); 12 goog.require('goog.math.Size'); 13 goog.require('goog.math.Vec2'); 14 goog.require('goog.style'); 15 goog.require('lime'); 16 goog.require('lime.Node'); 17 goog.require('lime.events.EventDispatcher'); 18 goog.require('lime.helper.PauseScene'); 19 goog.require('lime.scheduleManager'); 20 goog.require('lime.transitions.Transition'); 21 22 23 /** 24 * Director object. Base object for every game. 25 * @param {Element} parentElement Parent element for director. 26 * @param {number=} opt_width Optionaly define what height and width the director should have. 27 * @param {number=} opt_height Optionaly define what height and width the director should have. 28 * @constructor 29 * @extends lime.Node 30 */ 31 lime.Director = function(parentElement) { 32 lime.Node.call(this); 33 34 // Unlike other nodes Director is always in the DOM as 35 // it requires parentNode in the constructor 36 this.inTree_ = true; 37 38 this.setAnchorPoint(0, 0); 39 // todo: maybe easier if director just positions itselt 40 // to the center of the screen 41 42 /** 43 * Array of Scene instances. Last one is the active scene. 44 * @type {Array.<lime.Scene>} 45 * @private 46 */ 47 this.sceneStack_ = []; 48 49 /** 50 * Array of CoverNode instances. 51 * @type {Array.<lime.CoverNode>} 52 * @private 53 */ 54 this.coverStack_ = []; 55 56 this.domClassName = goog.getCssName('lime-director'); 57 this.createDomElement(); 58 parentElement.appendChild(this.domElement); 59 60 61 62 if (goog.userAgent.WEBKIT && goog.userAgent.MOBILE) { 63 //todo: Not pretty solution. Cover layers may not be needed at all. 64 this.coverElementBelow = document.createElement('div'); 65 goog.dom.classes.add(this.coverElementBelow, 66 goog.getCssName('lime-cover-below')); 67 goog.dom.insertSiblingBefore(this.coverElementBelow, this.domElement); 68 69 this.coverElementAbove = document.createElement('div'); 70 goog.dom.classes.add(this.coverElementAbove, 71 goog.getCssName('lime-cover-above')); 72 goog.dom.insertSiblingAfter(this.coverElementAbove, this.domElement); 73 } 74 75 if (parentElement.style['position'] != 'absolute') { 76 parentElement.style['position'] = 'relative'; 77 } 78 parentElement.style['overflow'] = 'hidden'; 79 80 if (parentElement == document.body) { 81 goog.style.installStyles('html,body{margin:0;padding:0;height:100%;}'); 82 83 var meta = document.createElement('meta'); 84 meta.name = 'viewport'; 85 var content = 'width=device-width,initial-scale=1.0,minimum-scale=1,' + 86 'maximum-scale=1.0,user-scalable=no'; 87 if ((/android/i).test(navigator.userAgent)) { 88 content += ',target-densityDpi=device-dpi'; 89 } 90 91 meta.content = content; 92 document.getElementsByTagName('head').item(0).appendChild(meta); 93 94 95 //todo: look for a less hacky solution 96 if(goog.userAgent.MOBILE && !goog.global['navigator'].standalone){ 97 var that = this; 98 setTimeout( 99 function(){window.scrollTo(0, 0);that.invalidateSize_()} 100 ,100); 101 } 102 } 103 104 var width, parentSize = goog.style.getSize(parentElement); 105 106 this.setSize(new goog.math.Size( 107 width = arguments[1] || parentSize.width || lime.Director.DEFAULT_WIDTH, 108 arguments[2] || parentSize.height * width / parentSize.width || lime.Director.DEFAULT_HEIGHT 109 )); 110 111 // --define goog.debug=false 112 this.setDisplayFPS(goog.DEBUG); 113 this.setPaused(false); 114 115 116 var vsm = new goog.dom.ViewportSizeMonitor(); 117 goog.events.listen(vsm, goog.events.EventType.RESIZE, 118 this.invalidateSize_, false, this); 119 goog.events.listen(goog.global, 'orientationchange', 120 this.invalidateSize_, false, this); 121 122 123 lime.scheduleManager.schedule(this.step_, this); 124 125 126 this.eventDispatcher = new lime.events.EventDispatcher(this); 127 128 goog.events.listen(this, ['touchmove','touchstart'], 129 function(e) {e.event.preventDefault();}, false, this); 130 131 // todo: check if all those are really neccessary as Event code 132 // is much more mature now 133 goog.events.listen(this, ['mouseup', 'touchend', 'mouseout', 'touchcancel'], 134 function() {},false); 135 136 137 this.invalidateSize_(); 138 139 if(goog.DEBUG){ 140 goog.events.listen(goog.global,'keyup',this.keyUpHandler_,false,this); 141 } 142 143 }; 144 goog.inherits(lime.Director, lime.Node); 145 146 147 /** 148 * Milliseconds between recalculating FPS value 149 * @const 150 * @type {number} 151 */ 152 lime.Director.FPS_INTERVAL = 100; 153 154 /** 155 * Default width of the Director 156 * @const 157 * @type {number} 158 */ 159 lime.Director.DEFAULT_WIDTH = 400; 160 161 162 /** 163 * Default height of the Director 164 * @const 165 * @type {number} 166 */ 167 lime.Director.DEFAULT_HEIGHT = 400; 168 169 170 /** 171 * Returns true if director is paused? On paused state 172 * the update timer doesn't fire. 173 * @return {boolean} If director is paused. 174 */ 175 lime.Director.prototype.isPaused = function() { 176 return this.isPaused_; 177 }; 178 179 180 /** 181 * Pauses or resumes the director 182 * @param {boolean} value Pause or resume. 183 * @return {lime.Director} The director object itself. 184 */ 185 lime.Director.prototype.setPaused = function(value) { 186 this.isPaused_ = value; 187 lime.scheduleManager.changeDirectorActivity(this, !value); 188 if (this.isPaused_) { 189 this.pauseScene = new lime.helper.PauseScene(); 190 this.pushScene(this.pauseScene); 191 } 192 else if (this.pauseScene) { 193 this.popScene(); 194 delete this.pauseScene; 195 } 196 return this; 197 }; 198 199 200 /** 201 * Returns true if FPS counter is displayed 202 * @return {boolean} FPS is displayed. 203 */ 204 lime.Director.prototype.isDisplayFPS = function() { 205 return this.displayFPS_; 206 }; 207 208 /** 209 * Show or hide FPS counter 210 * @param {boolean} value Display FPS? 211 * @return {lime.Director} The director object itself. 212 */ 213 lime.Director.prototype.setDisplayFPS = function(value) { 214 if (this.displayFPS_ && !value) { 215 goog.dom.removeNode(this.fpsElement_); 216 } 217 else if (!this.displayFPS_ && value) { 218 this.frames_ = 0; 219 this.accumDt_ = 0; 220 221 this.fpsElement_ = goog.dom.createDom('div'); 222 goog.dom.classes.add(this.fpsElement_, goog.getCssName('lime-fps')); 223 this.domElement.parentNode.appendChild(this.fpsElement_); 224 } 225 226 this.displayFPS_ = value; 227 return this; 228 }; 229 230 231 /** 232 * Get current active scene 233 * @return {lime.Scene} Currently active scene. 234 */ 235 lime.Director.prototype.getCurrentScene = function() { 236 return this.sceneStack_.length ? 237 this.sceneStack_[this.sceneStack_.length - 1] : null; 238 }; 239 240 /** @inheritDoc */ 241 lime.Director.prototype.getDirector = function() { 242 return this; 243 }; 244 245 /** @inheritDoc */ 246 lime.Director.prototype.getScene = function() { 247 return null; 248 }; 249 250 /** 251 * Timeline function. 252 * @param {number} delta Milliseconds since last step. 253 * @private 254 */ 255 lime.Director.prototype.step_ = function(delta) { 256 if (this.isDisplayFPS()) { 257 258 this.frames_++; 259 this.accumDt_ += delta; 260 if (this.accumDt_ > lime.Director.FPS_INTERVAL) { 261 this.fps = ((1000 * this.frames_) / this.accumDt_); 262 goog.dom.setTextContent(this.fpsElement_, this.fps.toFixed(2)); 263 this.frames_ = 0; 264 this.accumDt_ = 0; 265 } 266 } 267 lime.updateDirtyObjects(); 268 }; 269 270 271 /** 272 * Replace current scene with new scene 273 * @param {lime.Scene} scene New scene. 274 * @param {function(lime.Scene,lime.Scene,boolean=)=} opt_transition Transition played. 275 * @param {number=} opt_duration Duration of transition. 276 */ 277 lime.Director.prototype.replaceScene = function(scene, opt_transition, 278 opt_duration) { 279 280 scene.setSize(this.getSize().clone()); 281 282 var transitionclass = opt_transition || lime.transitions.Transition; 283 284 var outgoing = null; 285 if (this.sceneStack_.length) 286 outgoing = this.sceneStack_[this.sceneStack_.length - 1]; 287 288 289 290 var removelist = []; 291 var i = this.sceneStack_.length; 292 while (--i >= 0) { 293 this.sceneStack_[i].wasRemovedFromTree(); 294 removelist.push(this.sceneStack_[i].domElement); 295 this.sceneStack_[i].parent_ = null; 296 } 297 this.sceneStack_.length = 0; 298 299 this.sceneStack_.push(scene); 300 scene.domElement.style['display']='none'; 301 this.domElement.appendChild(scene.domElement); 302 scene.parent_ = this; 303 scene.wasAddedToTree(); 304 305 var transition = new transitionclass(outgoing, scene); 306 307 goog.events.listen(transition,'end',function() { 308 var i = removelist.length; 309 while (--i >= 0) { 310 goog.dom.removeNode(removelist[i]); 311 } 312 },false,this); 313 314 if (goog.isDef(opt_duration)) { 315 transition.setDuration(opt_duration); 316 } 317 318 transition.start(); 319 return transition; 320 321 }; 322 323 /** @inheritDoc */ 324 lime.Director.prototype.updateLayout = function() { 325 // debugger; 326 this.dirty_ &= ~lime.Dirty.LAYOUT; 327 }; 328 329 /** 330 * Push scene to the top of scene stack 331 * @param {lime.Scene} scene New scene. 332 */ 333 lime.Director.prototype.pushScene = function(scene) { 334 scene.setSize(this.getSize().clone()); 335 this.sceneStack_.push(scene); 336 this.domElement.appendChild(scene.domElement); 337 scene.parent_ = this; 338 scene.wasAddedToTree(); 339 340 }; 341 342 343 /** 344 * Remove current scene from the stack 345 */ 346 lime.Director.prototype.popScene = function() { 347 if (!this.sceneStack_.length) return; 348 this.sceneStack_[this.sceneStack_.length - 1].wasRemovedFromTree(); 349 this.sceneStack_[this.sceneStack_.length - 1].parent_ = null; 350 goog.dom.removeNode( 351 this.sceneStack_[this.sceneStack_.length - 1].domElement); 352 this.sceneStack_.pop(); 353 354 }; 355 356 357 /** 358 * Add CoverNode object to the viewport 359 * @param {lime.CoverNode} cover Covernode. 360 * @param {boolean} opt_addAboveDirector Cover is added above director object. 361 */ 362 lime.Director.prototype.addCover = function(cover, opt_addAboveDirector) { 363 //mobile safari performes much better with this hack. needs investigation. 364 if (goog.userAgent.WEBKIT && goog.userAgent.MOBILE) { 365 if (opt_addAboveDirector) { 366 this.coverElementAbove.appendChild(cover.domElement); 367 } 368 else { 369 this.coverElementBelow.appendChild(cover.domElement); 370 } 371 372 } 373 else { 374 if (opt_addAboveDirector) { 375 goog.dom.insertSiblingAfter(cover.domElement, this.domElement); 376 } 377 else { 378 goog.dom.insertSiblingBefore(cover.domElement, this.domElement); 379 } 380 } 381 cover.director = this; 382 this.coverStack_.push(cover); 383 }; 384 385 /** 386 * Remove CoverNode object from the viewport 387 * @param {lime.CoverNode} cover Cover to remove. 388 */ 389 lime.Director.prototype.removeCover = function(cover) { 390 goog.array.remove(this.coverStack_, cover); 391 goog.dom.removeNode(cover.domElement); 392 }; 393 394 395 /** 396 * Return bounds of director, 397 * @param {goog.math.Box} box Edges. 398 * @return {goog.math.Box} new bounds. 399 */ 400 lime.Director.prototype.getBounds = function(box) { 401 //todo:This should basically be same as boundingbox on lime.node 402 var position = this.getPosition(), 403 scale = this.getScale(); 404 return new goog.math.Box( 405 box.top - position.y / scale.y, 406 box.right - position.x / scale.x, 407 box.bottom - position.y / scale.y, 408 box.left - position.x / scale.x 409 ); 410 }; 411 412 /** 413 * @inheritDoc 414 */ 415 lime.Director.prototype.screenToLocal = function(c) { 416 var coord = c.clone(); 417 coord.x -= this.domOffset.x + this.position_.x; 418 coord.y -= this.domOffset.y + this.position_.y; 419 420 coord.x /= this.scale_.x; 421 coord.y /= this.scale_.y; 422 return coord; 423 }; 424 425 /** 426 * @inheritDoc 427 */ 428 lime.Director.prototype.localToScreen = function(c) { 429 var coord = c.clone(); 430 coord.x *= this.scale_.x; 431 coord.y *= this.scale_.y; 432 433 coord.x += this.domOffset.x + this.position_.x; 434 coord.y += this.domOffset.y + this.position_.y; 435 436 return coord; 437 }; 438 439 440 /** 441 * @inheritDoc 442 */ 443 lime.Director.prototype.update = function() { 444 lime.Node.prototype.update.call(this); 445 446 var i = this.coverStack_.length; 447 while (--i >= 0) { 448 this.coverStack_[i].update(true); 449 } 450 }; 451 452 453 /** 454 * Update dimensions based on viewport dimension changes 455 * @private 456 */ 457 lime.Director.prototype.invalidateSize_ = function() { 458 459 var stageSize = goog.style.getSize(this.domElement.parentNode); 460 461 if (this.domElement.parentNode == document.body) { 462 window.scrollTo(0, 0); 463 if (goog.isNumber(window.innerHeight)) { 464 stageSize.height = window.innerHeight; 465 } 466 } 467 468 var realSize = this.getSize().clone().scaleToFit(stageSize); 469 470 var scale = realSize.width / this.getSize().width; 471 this.setScale(scale); 472 473 if (stageSize.aspectRatio() < realSize.aspectRatio()) { 474 this.setPosition(0, (stageSize.height - realSize.height) / 2); 475 } 476 else { 477 this.setPosition((stageSize.width - realSize.width) / 2, 0); 478 } 479 480 this.updateDomOffset_(); 481 482 // overflow hidden is for hiding away unused edges of document 483 // height addition is because scroll(0,0) doesn't work any more if the 484 // document has no edge @tonis todo:look for less hacky solution(iframe?). 485 if(goog.userAgent.MOBILE && this.domElement.parentNode==document.body){ 486 if(this.overflowStyle_) goog.style.uninstallStyles(this.overflowStyle_); 487 this.overflowStyle_ = goog.style.installStyles( 488 'html{height:'+(stageSize.height+120)+'px;overflow:hidden;}'); 489 } 490 491 }; 492 493 /** 494 * Add support for adding game to Springboard as a 495 * web application on iOS devices 496 */ 497 lime.Director.prototype.makeMobileWebAppCapable = function() { 498 499 var meta = document.createElement('meta'); 500 meta.name = 'apple-mobile-web-app-capable'; 501 meta.content = 'yes'; 502 document.getElementsByTagName('head').item(0).appendChild(meta); 503 504 meta = document.createElement('meta'); 505 meta.name = 'apple-mobile-web-app-status-bar-style'; 506 meta.content = 'black'; 507 document.getElementsByTagName('head').item(0).appendChild(meta); 508 509 var visited = false; 510 if (goog.isDef(localStorage)) { 511 visited = localStorage.getItem('_lime_visited'); 512 } 513 514 var ios = (/(ipod|iphone|ipad)/i).test(navigator.userAgent); 515 if (ios && !window.navigator.standalone && COMPILED && !visited && this.domElement.parentNode==document.body) { 516 alert('Please install this page as a web app by ' + 517 'clicking Share + Add to home screen.'); 518 if (goog.isDef(localStorage)) { 519 localStorage.setItem('_lime_visited', true); 520 } 521 } 522 523 }; 524 525 /** 526 * Updates the cached value of directors parentelement position in the viewport 527 * @private 528 */ 529 lime.Director.prototype.updateDomOffset_ = function() { 530 this.domOffset = goog.style.getPageOffset(this.domElement.parentNode); 531 }; 532 533 /** 534 * @private 535 */ 536 lime.Director.prototype.keyUpHandler_ = function(e){ 537 if(e.altKey && String.fromCharCode(e.keyCode).toLowerCase()=='d'){ 538 if(this.debugModeOn_){ 539 goog.style.uninstallStyles(this.debugModeOn_); 540 this.debugModeOn_ = null; 541 } 542 else { 543 this.debugModeOn_ = goog.style.installStyles('.lime-scene div,.lime-scene img,'+ 544 '.lime-scene canvas{border: 1px solid #c00;}'); 545 } 546 e.stopPropagation(); 547 e.preventDefault(); 548 } 549 } 550 551 /** 552 * @inheritDoc 553 */ 554 lime.Director.prototype.hitTest = function(e) { 555 if (e && e.screenPosition) 556 e.position = this.screenToLocal(e.screenPosition); 557 return true; 558 }; 559