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