@pilotlab/lux-tools
Version:
A luxurious user experience framework, developed by your friends at Pilot.
594 lines (461 loc) • 23.6 kB
text/typescript
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<IAppContext> = new Signal<IAppContext>(true);
layoutChanged:Signal<AppManager> = new Signal<AppManager>(false);
onlineStarted:Signal<AppManager> = new Signal<AppManager>(false);
offlineStarted:Signal<AppManager> = new Signal<AppManager>(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<any> {
return super.initialize(renderer, appContext, appRootElement);
}
protected p_onInitializeStarted(result:Result<any>, args:any[]):Result<any> {
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 = <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(<AppContextDesktop>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;