import { is } from '@pilotlab/lux-is'; import {AudioManager} from '@pilotlab/lux-audio'; import {Animation, IAnimationEaseFunction, AnimationEaseQuartic} from '@pilotlab/lux-animation'; import {Initializable} from '@pilotlab/lux-initializable'; import {CommandManager, ThrottlingCommand} from '@pilotlab/lux-commands'; import Debug from '@pilotlab/lux-debug'; import Detector from '@pilotlab/lux-detector'; import {IFileDirectory} from '@pilotlab/lux-files'; import {FontManager} from '@pilotlab/lux-fonts'; import {IElement, ElementRoot, IElementRenderer} from '@pilotlab/lux-elements'; import Result from '@pilotlab/lux-result'; import {Signal} from '@pilotlab/lux-signals'; import {Color} from '@pilotlab/lux-types'; import {IAppContext, AppContextDesktop, AppContextMobile,DeviceType} from './appContext'; import {BuildEnvironment, Orientation} from './appEnums'; export class AppManager extends Initializable { constructor() { super(); } /*====================================================================* START: Properties *====================================================================*/ get isOnline():boolean { return this._isOnline; } private _isOnline:boolean = false; get appContext():IAppContext { return this._appContext; } private _appContext:IAppContext; get viewport():any { return this._viewport; } private _viewport:any; get buildEnvironment():BuildEnvironment { return this._buildEnvironment; } set buildEnvironment(value:BuildEnvironment) { this._buildEnvironment = value; Debug.isProduction = (value !== BuildEnvironment.DEVELOPMENT && value !== BuildEnvironment.TESTING); } private _buildEnvironment:BuildEnvironment = BuildEnvironment.DEVELOPMENT; //===== App layout properties (will be set automatically) get appWidth():number { return this._appWidth; } private _appWidth:number = 1920; get appHeight():number { return this._appHeight; } private _appHeight:number = 1080; get appScale():number { return this._appScale; } private _appScale:number = 1; //===== App controls get root():ElementRoot { return this._root; } private _root:ElementRoot; get deviceScreen():IElement { return this._deviceScreen; } private _deviceScreen:IElement = new Element(); get appRootElement():IElement { return this._appRootElement; } private _appRootElement:IElement; get commands():CommandManager { return this.p_commands; } protected p_commands:CommandManager = new CommandManager(true); get fonts():FontManager { return this.p_fonts; } protected p_fonts = new FontManager(); get audio():AudioManager { return this.p_audio; } protected p_audio = new AudioManager(); isAudioOn:boolean = false; private _deviceScaleFactor:number = null; //===== Animation Settings durationQuick:number = 0.5; durationNormal:number = 1.0; durationLong:number = 1.5; animEaseDefault:IAnimationEaseFunction = AnimationEaseQuartic.out; //===== Colors get colorVeryLight():Color { return new Color(255, 255, 255); } get colorLight():Color { return new Color(240, 240, 240); } get colorDark():Color { return new Color(80, 80, 80); } get colorVeryDark():Color { return new Color(35, 31, 32); } get colorGrayLight():Color { return Color.fromHex('#e2e2e2'); } get colorGrayMediumLight():Color { return Color.fromHex('#c5c5c5'); } get colorGray():Color { return Color.fromHex('#9e9e9e'); } get colorGrayMediumDark():Color { return Color.fromHex('#6f6f6f'); } get colorGrayDark():Color { return Color.fromHex('#9e9e9e'); } get colorAccent():Color { return new Color(25, 230, 140); } //===== App component Colors get colorBody():Color { return this.colorVeryLight; } get colorAppBackground():Color { return new Color(0, 0, 0); } assetDirectory:IFileDirectory; assetDirectoryRemote:IFileDirectory; /*====================================================================* START: Signals *====================================================================*/ initialized:Signal = new Signal(true); layoutChanged:Signal = new Signal(false); onlineStarted:Signal = new Signal(false); offlineStarted:Signal = new Signal(false); /*====================================================================* START: Static Properties and Methods *====================================================================*/ //===== Fullscreen static get isFullscreen():boolean { let doc:any = document; let isFullscreen:boolean = false; if ( doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement ) isFullscreen = true; else if( window.innerHeight == screen.height) isFullscreen = true; else if( screen.availHeight <= window.innerHeight) isFullscreen = true; return isFullscreen; } static fullscreenToggle():void { if (AppManager.isFullscreen) AppManager.fullscreenExit(); else AppManager.fullscreenEnter(); } static fullscreenEnter():void { //----- Either not Chrome, or not as an app window let docElem:any = document.documentElement; if (Detector.isChromeApp) { let win:any = window; win.chrome.app.window.current().fullscreen(); } else if (docElem.requestFullscreen) docElem.requestFullscreen(); else if (docElem.mozRequestFullScreen) docElem.mozRequestFullScreen(); else if (docElem.webkitRequestFullScreen) docElem.webkitRequestFullScreen(); } static fullscreenExit():void { let doc:any = document; if (Detector.isChromeApp) { let win:any = window; win.chrome.app.window.current().restore(); } else if (doc.exitFullscreen) doc.exitFullscreen(); else if (doc.webkitExitFullscreen) doc.webkitExitFullscreen(); else if (doc.webkitCancelFullscreen) doc.webkitCancelFullscreen(); else if (doc.mozCancelFullScreen) doc.mozCancelFullScreen(); else if (doc.msExitFullscreen) doc.msExitFullscreen(); } /*====================================================================* START: Public Methods *====================================================================*/ initialize(renderer?:IElementRenderer, appContext?:IAppContext, appRootElement?:IElement):Result { return super.initialize(renderer, appContext, appRootElement); } protected p_onInitializeStarted(result:Result, args:any[]):Result { this.p_initializeApp(args[0], args[1], args[2]); result.resolve(); return result; } protected p_initializeApp(renderer?:IElementRenderer, appContext?:IAppContext, appRootElement?:IElement):boolean { if (this.isInitialized) return false; Animation.initialize(); if (is.empty(renderer)) renderer = new RendererCanvas(); if (is.empty(appContext)) appContext = new AppContextDesktop(); if (is.empty(appRootElement)) appRootElement = new Element(); this._appContext = appContext; if (is.notEmpty(this._appContext) && Detector.isMobile) { //---- Browser supports 'deviceorientation' events if (Detector.isSupportedDeviceOrientationEvent) { window.addEventListener('deviceorientation', () => { this.p_layoutChanged(); }, false); } else window.addEventListener('orientationchange', () => { this.p_layoutChanged(); }); } //----- listen for resize event let resizeCommand:ThrottlingCommand = new ThrottlingCommand(() => { //----- Android doesn't guarantee an orientation event so we need to listen to the resize event if (Detector.isAndroid) { let dif = window.innerWidth / this.root.size.x; let round = Math.round(dif * 100) / 100; if ( (window.orientation !== undefined && window.orientation !== this.deviceScreen.rotation.z) || round !== 1 ) this.p_layoutChanged(); } else this.p_layoutChanged(); }, null, this, 100 /* throttle to about 10 times per second */ ); if (!Detector.isAppleMobile) { // Don't use for iOS devices as it will cause the app to crash window.addEventListener('resize', (e) => resizeCommand.throttle()); } document.body.style.backgroundColor = this.colorBody.toHex(); //===== root this._root = new ElementRoot(renderer, this); this.root.mouseMoveInterval = Detector.isMobile ? 1 : 0; this.root.style.background.color = Color.empty; this.root.isPropagateUserInputEvents = true; document.body.appendChild(this.root.element); //===== viewport if (Detector.isMobile) { let viewport:any = document.head.querySelector('meta[name=viewport]'); if (is.notEmpty(viewport)) this._viewport = viewport; else { this._viewport = document.createElement('meta'); this._viewport.name = 'viewport'; document.head.appendChild(this._viewport); } } //===== deviceScreen this.deviceScreen.pivot.x = 0.5; this.deviceScreen.pivot.y = 0.5; this.deviceScreen.isClipChildren = true; this.deviceScreen.isPropagateUserInputEvents = true; this.deviceScreen.style.background.color = Color.empty; this.root.children.addByKey(this.deviceScreen, 'deviceScreen'); //===== appRootElement this._appRootElement = appRootElement; this.deviceScreen.children.addByName(this.appRootElement, 'appRoot'); //===== audio this.root.animate.ticked.listen((elapsedMilliseconds:number) => { if (!this.isAudioOn) return; this.p_audio.tick(elapsedMilliseconds); }); //===== online/offline window.addEventListener('online', () => { this._isOnline = true; this.p_online(); this.onlineStarted.dispatch(this); }, false); window.addEventListener('offline', () => { this._isOnline = false; this.p_offline(); this.offlineStarted.dispatch(this); }, false); //----- set the layout this.p_layoutChanged(); return true; } setLayout( appContext:IAppContext, w:number, h:number, browserZoom:number, screenOrientation:Orientation ):void { if (is.empty(this.root) || is.empty(appContext)) return; //===== Is the device target desktop or mobile? if (appContext.deviceTarget === DeviceType.MOBILE) { //----- mobile app target let appContextMobile:AppContextMobile = appContext; //===== Are we locked to portrait, landscape, or neither? if (appContextMobile.orientationMode === Orientation.VERTICAL) { //----- Locked to portrait. //===== Is the mobile device in portrait or landscape mode? if (screenOrientation === Orientation.VERTICAL) { //----- portrait mode this.setMobileLayout(appContextMobile, w, h, false); } else { //----- landscape mode this.setMobileLayout(appContextMobile, w, h, true); } } else if (appContextMobile.orientationMode === Orientation.HORIZONTAL) { //----- Locked to landscape. //===== Is the mobile device in portrait or landscape mode? if (screenOrientation === Orientation.HORIZONTAL) { //----- landscape mode this.setMobileLayout(appContextMobile, w, h, false); } else { //----- portrait mode this.setMobileLayout(appContextMobile, w, h, true); } } else { //----- Orientation isn't locked. this.setMobileLayout(appContextMobile, w, h, false); } //===== Are we running inside a mobile image shell? if (!Detector.isMobile && is.notEmpty(appContextMobile.emulationContext)) { //----- We are running inside a mobile app image shell. this._appScale = 0.5; } } else { //----- desktop app target this.setDesktopRootLayout(appContext, w, h, browserZoom, true); } } setDesktopRootLayout( appContextDesktop:AppContextDesktop, screenWidth:number, screenHeight:number, browserZoom:number = 1, isSetAppDimensions:boolean = false ):void { //===== Fit to parent or fixed aspect ratio for the root control? if ( is.empty(appContextDesktop) || (is.empty(appContextDesktop.widthPreferred) && is.empty(appContextDesktop.heightPreferred)) ) { //----- layout mode: fit parent this.fitRoot(screenWidth, screenHeight, browserZoom); if (isSetAppDimensions) { this._appWidth = screenWidth; this._appHeight = screenHeight; } } else if ( is.notEmpty(appContextDesktop.widthPreferred) && is.notEmpty(appContextDesktop.heightPreferred) ) { //----- layout mode: fixed aspect ratio let rootAspectRatio:number = appContextDesktop.heightPreferred / appContextDesktop.widthPreferred; this.forceRootAspectRatio(screenWidth, screenHeight, rootAspectRatio, browserZoom); if (isSetAppDimensions) { this._appWidth = appContextDesktop.widthPreferred; this._appHeight = appContextDesktop.heightPreferred; } } else if (is.notEmpty(appContextDesktop.widthPreferred)) { //----- layout mode: fixed width, height fit to parent let widthFinal:number = this.appContext.widthPreferred; if (this.appContext.widthPreferred <= screenWidth) widthFinal = this.appContext.isConstrainMax ? this.appContext.widthPreferred : screenWidth; else widthFinal = this.appContext.isConstrainMin ? this.appContext.widthPreferred : screenWidth; this.fitRoot(widthFinal, screenHeight, browserZoom); //----- IMPORTANT: No need to set viewport attributes for this mode, if we're actually running on a mobile device? if (isSetAppDimensions) { this._appWidth = widthFinal; this._appHeight = screenHeight; } } else if (is.notEmpty(appContextDesktop.heightPreferred)) { //----- layout mode: fixed height, width fit to parent let heightFinal:number = this.appContext.heightPreferred; if (this.appContext.heightPreferred <= screenHeight) heightFinal = this.appContext.isConstrainMax ? this.appContext.heightPreferred : screenHeight; else heightFinal = this.appContext.isConstrainMin ? this.appContext.heightPreferred : screenHeight; this.fitRoot(screenWidth, heightFinal, browserZoom); //----- IMPORTANT: No need to set viewport attributes for this mode, if we're actually running on a mobile device? if (isSetAppDimensions) { this._appWidth = screenWidth; this._appHeight = heightFinal; } } } setMobileLayout( appContextMobile:AppContextMobile, screenWidth:number, screenHeight:number, isRotated:boolean = false, browserZoom:number = 1 ):void { //----- Preferred Width is set if (is.notEmpty(appContextMobile.widthPreferred)) { this._appWidth = appContextMobile.widthPreferred; if (is.notEmpty(appContextMobile.heightPreferred)) { this._appHeight = appContextMobile.heightPreferred } else { this._appHeight = isRotated ? screenWidth * (this._appWidth / screenHeight) : screenHeight * (this._appWidth / screenWidth); } } else { this._appWidth = isRotated ? screenHeight : screenWidth; this._appHeight = isRotated ? screenWidth : screenHeight; } this._appScale = 1; //===== Mobile if (Detector.isMobile) { this.p_initializeViewport(screenWidth, screenHeight, isRotated); if (isRotated) this.fitRoot(this._appHeight, appContextMobile.widthPreferred, browserZoom); else this.fitRoot(appContextMobile.widthPreferred, this._appHeight, browserZoom); } else { //===== Desktop if (is.notEmpty(appContextMobile.emulationContext)) { this._appWidth = appContextMobile.emulationContext.appWidthPreferred; this._appHeight = appContextMobile.emulationContext.appHeightPreferred; this._appScale = 1; } else { if (is.notEmpty(appContextMobile.widthPreferred)) { this._appScale = (isRotated ? screenHeight : screenWidth) / appContextMobile.widthPreferred; if (is.empty(appContextMobile.heightPreferred)) { this._appHeight /= this._appScale; } } } this.setDesktopRootLayout(appContextMobile.emulationContext, screenWidth, screenHeight, browserZoom, false); } //===== Set rotation if ( Detector.isMobile || is.empty(appContextMobile.emulationContext) || appContextMobile.emulationContext.mobileOrientationMode === Orientation.BOTH ) { //----- Mobile this.deviceScreen.rotation.z = isRotated ? 90 : 0; } else { //----- Desktop this.deviceScreen.rotation.z = appContextMobile.emulationContext.mobileOrientationMode === Orientation.HORIZONTAL ? 90 : 0; } } protected p_initializeViewport(screenWidth:number, screenHeight:number, isRotated:boolean):void { if (this.viewport) { //----- Width relative to orientation let width = isRotated ? this._appHeight : this._appWidth; //----- Scale relative to orientation let scale = (isRotated ? screenHeight : screenWidth) / this._appWidth; let viewScale:number; if (is.empty(this._deviceScaleFactor)) viewScale = scale; else if (Math.round(scale * 10) / 10 === 1) { // Round to nearest tenth viewScale = 1; } else { viewScale = Math.round(scale * this._deviceScaleFactor * 10) / 10; // Round to nearest tenth } //---- Set the device scale factor on the first pass if (this._deviceScaleFactor == null) this._deviceScaleFactor = scale; this._viewport.setAttribute('content', 'width=' + width + ', minimum-scale=' + viewScale + ', maximum-scale=' + viewScale + ', user-scalable=no'); } } fitRoot(w:number, h:number, browserZoom:number):void { this.root.size.x = w / browserZoom; this.root.size.y = h / browserZoom; this.root.position.left = (w / 2) - (this.root.size.x / 2); this.root.position.top = (h / 2) - (this.root.size.y / 2); } forceRootAspectRatio(w:number, h:number, rootAspectRatio:number, browserZoom:number = 1):void { if (h / w > rootAspectRatio) { //----- parent area aspect ratio is taller than the root aspect ratio. //----- Set the root width to the actual width of the browser's viewable area. this.root.position.left = 0; let widthFinal:number = w / browserZoom; if (this.appContext.widthPreferred <= widthFinal) this.root.size.x = this.appContext.isConstrainMax ? this.appContext.widthPreferred : widthFinal; else this.root.size.x = this.appContext.isConstrainMin ? this.appContext.widthPreferred : widthFinal; this.root.size.y = this.root.size.x * rootAspectRatio; this.root.position.top = (h / 2) - (this.root.size.y / 2); this._appScale = this.root.size.x / this._appContext.widthPreferred; } else { //----- parent area aspect ratio is wider than the root aspect ratio. //----- Set the root height to the actual height of the browser's viewable area. this.root.position.top = 0; let heightFinal:number = h / browserZoom; if (this.appContext.heightPreferred <= heightFinal) this.root.size.y = this.appContext.isConstrainMax ? this.appContext.heightPreferred : heightFinal; else this.root.size.y = this.appContext.isConstrainMin ? this.appContext.heightPreferred : heightFinal; this.root.size.x = this.root.size.y / rootAspectRatio; this.root.position.left = (w / 2) - (this.root.size.x / 2); this._appScale = this.root.size.y / this._appContext.heightPreferred; } } /*====================================================================* START: Protected Methods *====================================================================*/ protected p_layoutChanged(eventData?:any):void { let w:number = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; let h:number = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; //----- Detect whether the browser window is zoomed. //----- NOTE: This may not work on all browsers. let browserZoom:number = 1; // window.innerWidth / window.outerWidth; let screenOrientation:Orientation = w > h ? Orientation.HORIZONTAL : Orientation.VERTICAL; this.setLayout(this._appContext, w, h, browserZoom, screenOrientation); //===== deviceScreen if (is.notEmpty(this.deviceScreen)) { this.deviceScreen.size.x = this._appWidth; this.deviceScreen.size.y = this._appHeight; this.deviceScreen.scale = this._appScale; this.deviceScreen.position.x = this.root.size.x / 2; this.deviceScreen.position.y = this.root.size.y / 2; } //===== appRoot if (is.notEmpty(this.appRootElement)) { this.appRootElement.size.x = this._appWidth; this.appRootElement.size.y = this._appHeight; } this.p_onLayoutChanged(); this.layoutChanged.dispatch(this); //----- center, if necessary if (this.root.size.x < w) this.root.left = (w / 2) - (this.root.size.x / 2); if (this.root.size.y < h) this.root.top = (h / 2) - (this.root.size.y / 2); } protected p_onLayoutChanged():void {} protected p_online():void {} protected p_offline():void {} } // End of class export default AppManager;