'use strict';
var assert = function (condition, message) {
if (!condition) {
throw message || 'assertion failed';
}
};
var emptyFn = function () {};
var extend = require('extend');
var Stats = require('T3.Stats');
var dat = require('T3.dat');
var THREE = require('THREE');
var Coordinates = require('../model/Coordinates');
var Keyboard = require('./Keyboard');
var LoopManager = require('./LoopManager');
var THREEx = require('../lib/THREEx/');
/**
* @module controller/Application
*/
/**
* Each instance controls one element of the DOM, besides creating
* the canvas for the three.js app it creates a dat.gui instance
* (to control objects of the app with a gui) and a Stats instance
* (to view the current framerate)
*
* @class
* @param {Object} config An object containing the following:
* @param {string} [config.id=null] The id of the DOM element to inject the elements to
* @param {number} [config.width=window.innerWidth]
* The width of the canvas
* @param {number} [config.height=window.innerHeight]
* The height of the canvas
* @param {boolean} [config.renderImmediately=true]
* False to disable the game loop when the application starts, if
* you want to resume the loop call `application.loopManager.start()`
* @param {boolean} [config.injectCache=false]
* True to add a wrapper over `THREE.Object3D.prototype.add` and
* `THREE.Object3D.prototype.remove` so that it catches the last element
* and perform additional operations over it, with this mechanism
* we allow the application to have an internal cache of the elements of
* the application
* @param {boolean} [config.fullScreen=false]
* True to make this app fullscreen adding additional support for
* window resize events
* @param {string} [config.theme='dark']
* Theme used in the default scene, it can be `light` or `dark`
* @param {object} [config.ambientConfig={}]
* Additional configuration for the ambient, see the class {@link
* Coordinates}
* @param {object} [config.defaultSceneConfig={}] Additional config
* for the default scene created for this world
*/
function Application(config) {
config = extend({
selector: null,
width: window.innerWidth,
height: window.innerHeight,
renderImmediately: true,
injectCache: false,
fullScreen: false,
theme: 'dark',
helpersConfig: {},
defaultSceneConfig: {
fog: true
}
}, config);
this.initialConfig = config;
/**
* Scenes in this world, each scene should be mapped with
* a unique id
* @type {Object}
*/
this.scenes = {};
/**
* The active scene of this world
* @type {THREE.Scene}
*/
this.activeScene = null;
/**
* Reference to the cameras used in this world
* @type {Array}
*/
this.cameras = {};
/**
* The world can have many cameras, so the this is a reference to
* the active camera that's being used right now
* @type {T3.model.Camera}
*/
this.activeCamera = null;
/**
* THREE Renderer
* @type {Object}
*/
this.renderer = null;
/**
* Keyboard manager
* @type {Object}
*/
this.keyboard = null;
/**
* Dat gui manager
* @type {Object}
*/
this.datgui = null;
/**
* Reference to the Stats instance (needed to call update
* on the method {@link module:controller/Application#update})
* @type {Object}
*/
this.stats = null;
/**
* Reference to the local loop manager
* @type {LoopManager}
*/
this.loopManager = null;
/**
* Colors for the default scene
* @type {Object}
*/
this.theme = null;
/**
* Application cache
* @type {Object}
*/
this.__t3cache__ = {};
Application.prototype.initApplication.call(this);
}
/**
* Getter for the initial config
* @return {Object}
*/
Application.prototype.getConfig = function () {
return this.initialConfig;
};
/**
* Bootstrap the application with the following steps:
*
* - Enabling cache injection
* - Set the theme
* - Create the renderer, default scene, default camera, some random lights
* - Initializes dat.gui, Stats, a mask when the application is paised
* - Initializes fullScreen events, keyboard and some helper objects
* - Calls the game loop
*
*/
Application.prototype.initApplication = function () {
var me = this,
config = me.getConfig();
me.injectCache(config.injectCache);
// theme
me.setTheme(config.theme);
// defaults
me.createDefaultRenderer();
me.createDefaultScene();
me.createDefaultCamera();
me.createDefaultLights();
// utils
me.initDatGui();
me.initStats();
me.initMask()
.maskVisible(!config.renderImmediately);
me.initFullScreen();
me.initKeyboard();
me.initCoordinates();
// game loop
me.gameLoop();
};
/**
* Sets the active scene (it must be a registered scene registered
* with {@link #addScene})
* @param {string} key The string which was used to register the scene
* @return {this}
*/
Application.prototype.setActiveScene = function (key) {
this.activeScene = this.scenes[key];
return this;
};
/**
* Adds a scene to the scene pool
* @param {THREE.Scene} scene
* @param {string} key
* @return {this}
*/
Application.prototype.addScene = function (scene, key) {
console.assert(scene instanceof THREE.Scene);
this.scenes[key] = scene;
return this;
};
/**
* Creates a scene called 'default' and sets it as the active one
* @return {this}
*/
Application.prototype.createDefaultScene = function () {
var me = this,
config = me.getConfig(),
defaultScene = new THREE.Scene();
if (config.defaultSceneConfig.fog) {
defaultScene.fog = new THREE.Fog(me.theme.fogColor, 2000, 4000);
}
me
.addScene(defaultScene, 'default')
.setActiveScene('default');
return me;
};
/**
* Creates the default THREE.WebGLRenderer used in the world
* @return {this}
*/
Application.prototype.createDefaultRenderer = function () {
var me = this,
config = me.getConfig();
var renderer = new THREE.WebGLRenderer({
// antialias: true
});
renderer.setClearColor(me.theme.clearColor, 1);
renderer.setSize(config.width, config.height);
document
.querySelector(config.selector)
.appendChild(renderer.domElement);
me.renderer = renderer;
return me;
};
Application.prototype.setActiveCamera = function (key) {
this.activeCamera = this.cameras[key];
return this;
};
Application.prototype.addCamera = function (camera, key) {
console.assert(camera instanceof THREE.PerspectiveCamera ||
camera instanceof THREE.OrthographicCamera);
this.cameras[key] = camera;
return this;
};
/**
* Create the default camera used in this world which is
* a `THREE.PerspectiveCamera`, it also adds orbit controls
* by calling {@link #createCameraControls}
*/
Application.prototype.createDefaultCamera = function () {
var me = this,
config = me.getConfig(),
width = config.width,
height = config.height,
defaults = {
fov: 38,
ratio: width / height,
near: 1,
far: 10000
},
defaultCamera;
defaultCamera = new THREE.PerspectiveCamera(
defaults.fov,
defaults.ratio,
defaults.near,
defaults.far
);
defaultCamera.position.set(500, 300, 500);
// transparently support window resize
if (config.fullScreen) {
THREEx.WindowResize.bind(me.renderer, defaultCamera);
}
me
.createCameraControls(defaultCamera)
.addCamera(defaultCamera, 'default')
.setActiveCamera('default');
return me;
};
/**
* Creates OrbitControls over the `camera` passed as param
* @param {THREE.PerspectiveCamera|THREE.OrtographicCamera} camera
* @return {this}
*/
Application.prototype.createCameraControls = function (camera) {
var me = this;
camera.cameraControls = new THREE.OrbitControls(
camera,
me.renderer.domElement
);
// avoid panning to see the bottom face
//camera.cameraControls.maxPolarAngle = Math.PI / 2 * 0.99;
//camera.cameraControls.target.set(100, 100, 100);
camera.cameraControls.target.set(0, 0, 0);
return me;
};
/**
* Creates some random lights in the default scene
* @return {this}
*/
Application.prototype.createDefaultLights = function () {
var light,
scene = this.scenes['default'];
light = new THREE.AmbientLight(0x222222);
scene.add(light).cache('ambient-light-1');
light = new THREE.DirectionalLight( 0xFFFFFF, 1.0 );
light.position.set( 200, 400, 500 );
scene.add(light).cache('directional-light-1');
light = new THREE.DirectionalLight( 0xFFFFFF, 1.0 );
light.position.set( -500, 250, -200 );
scene.add(light).cache('directional-light-2');
return this;
};
/**
* Sets the theme to be used in the default scene
* @param {string} name Either the string `dark` or `light`
* @todo Make the theme system extensible
* @return {this}
*/
Application.prototype.setTheme = function (name) {
var me = this,
themes = require('../').themes;
assert(themes[name]);
me.theme = themes[name];
return me;
};
/**
* Creates a mask on top of the canvas when it's paused
* @return {this}
*/
Application.prototype.initMask = function () {
var me = this,
config = me.getConfig(),
mask,
hidden;
mask = document.createElement('div');
mask.className = 't3-mask';
// mask.style.display = 'none';
mask.style.position = 'absolute';
mask.style.top = '0px';
mask.style.width = '100%';
mask.style.height = '100%';
mask.style.background = 'rgba(0,0,0,0.5)';
document
.querySelector(config.selector)
.appendChild(mask);
me.mask = mask;
return me;
};
/**
* Updates the mask visibility
* @param {boolean} v True to make it visible
*/
Application.prototype.maskVisible = function (v) {
var mask = this.mask;
mask.style.display = v ? 'block' : 'none';
};
/**
* Inits the dat.gui helper which is placed under the
* DOM element identified by the initial configuration selector
* @return {this}
*/
Application.prototype.initDatGui = function () {
var me = this,
config = me.getConfig(),
gui = new dat.GUI({
autoPlace: false
});
extend(gui.domElement.style, {
position: 'absolute',
top: '0px',
right: '0px',
zIndex: '1'
});
document
.querySelector(config.selector)
.appendChild(gui.domElement);
me.datgui = gui;
return me;
};
/**
* Init the Stats helper which is placed under the
* DOM element identified by the initial configuration selector
* @return {this}
*/
Application.prototype.initStats = function () {
var me = this,
config = me.getConfig(),
stats;
// add Stats.js - https://github.com/mrdoob/stats.js
stats = new Stats();
extend(stats.domElement.style, {
position: 'absolute',
zIndex: 1,
bottom: '0px'
});
document
.querySelector(config.selector)
.appendChild( stats.domElement );
me.stats = stats;
return me;
};
/**
* Binds the F key to make a world go full screen
* @todo This should be used only when the canvas is active
*/
Application.prototype.initFullScreen = function () {
var config = this.getConfig();
// allow 'f' to go fullscreen where this feature is supported
if(config.fullScreen && THREEx.FullScreen.available()) {
THREEx.FullScreen.bindKey();
}
};
/**
* Initializes the coordinate helper (its wrapped in a model in T3)
*/
Application.prototype.initCoordinates = function () {
var config = this.getConfig();
this.scenes['default']
.add(
new Coordinates(config.helpersConfig, this.theme)
.initDatGui(this.datgui)
.mesh
);
};
/**
* Initis the keyboard helper
* @return {this}
*/
Application.prototype.initKeyboard = function () {
this.keyboard = new Keyboard();
return this;
};
/**
* Initializes the game loop by creating an instance of {@link LoopManager}
* @return {this}
*/
Application.prototype.gameLoop = function () {
var config = this.getConfig();
this.loopManager = new LoopManager(this, config.renderImmediately)
.initDatGui(this.datgui)
.animate();
return this;
};
/**
* Update phase, the world updates by default:
*
* - The stats helper
* - The camera controls if the active camera has one
*
* @param {number} delta The number of seconds elapsed
*/
Application.prototype.update = function (delta) {
var me = this;
// stats helper
me.stats.update();
// camera update
if (me.activeCamera.cameraControls) {
me.activeCamera.cameraControls.update(delta);
}
};
/**
* Render phase, calls `this.renderer` with `this.activeScene` and
* `this.activeCamera`
*/
Application.prototype.render = function () {
var me = this;
me.renderer.render(
me.activeScene,
me.activeCamera
);
};
/**
* Wraps `THREE.Object3D.prototype.add` and `THREE.Object3D.prototype.remove`
* with functions that save the last object which `add` or `remove` have been
* called with, this allows to call the method `cache` which will cache
* the object with an identifier allowing fast object retrieval
*
* @example
*
* var instance = t3.Application.run({
* injectCache: true,
* init: function () {
* var group = new THREE.Object3D();
* var innerGroup = new THREE.Object3D();
*
* var geometry = new THREE.BoxGeometry(1,1,1);
* var material = new THREE.MeshBasicMaterial({color: 0x00ff00});
* var cube = new THREE.Mesh(geometry, material);
*
* innerGroup
* .add(cube)
* .cache('myCube');
*
* group
* .add(innerGroup)
* .cache('innerGroup');
*
* // removal example
* // group
* // .remove(innerGroup)
* // .cache();
*
* this.activeScene
* .add(group)
* .cache('group');
* },
*
* update: function (delta) {
* var cube = this.getFromCache('myCube');
* // perform operations on the cube
* }
* });
*
* @param {boolean} inject True to enable this behavior
*/
Application.prototype.injectCache = function (inject) {
var me = this,
lastObject,
lastMethod,
add = THREE.Object3D.prototype.add,
remove = THREE.Object3D.prototype.remove,
cache = this.__t3cache__;
if (inject) {
THREE.Object3D.prototype.add = function (object) {
lastMethod = 'add';
lastObject = object;
return add.apply(this, arguments);
};
THREE.Object3D.prototype.remove = function (object) {
lastMethod = 'remove';
lastObject = object;
return remove.apply(this, arguments);
};
THREE.Object3D.prototype.cache = function (name) {
assert(lastObject, 'T3.Application.prototype.cache: this method' +
' needs a previous call to add/remove');
if (lastMethod === 'add') {
lastObject.name = lastObject.name || name;
assert(lastObject.name);
cache[lastObject.name] = lastObject;
} else {
assert(lastObject.name);
delete cache[lastObject.name];
}
lastObject = null;
return me;
};
} else {
THREE.Object3D.prototype.cache = function () {
return this;
};
}
};
/**
* Gets an object from the cache if `injectCache` was enabled and
* an object was registered with {@link #cache}
* @param {string} name
* @return {THREE.Object3D}
*/
Application.prototype.getFromCache = function (name) {
return this.__t3cache__[name];
};
/**
* @static
* Creates a subclass of Application whose instances don't need to
* worry about the inheritance process
* @param {Object} options The same object passed to the {@link Application}
* constructor
* @param {Object} options.init Initialization phase, function called in
* the constructor of the subclass
* @param {Object} options.update Update phase, function called as the
* update function of the subclass, it also calls Application's update
* @return {t3.QuickLaunch} An instance of the subclass created in
* this function
*/
Application.run = function (options) {
options.init = options.init || emptyFn;
options.update = options.update || emptyFn;
assert(options.selector, 'canvas selector required');
var QuickLaunch = function (options) {
Application.call(this, options);
options.init.call(this, options);
};
QuickLaunch.prototype = Object.create(Application.prototype);
QuickLaunch.prototype.update = function (delta) {
Application.prototype.update.call(this, delta);
options.update.call(this, delta);
};
return new QuickLaunch(options);
};
module.exports = Application;