'use strict';
var assert = function (condition, message) {
if (!condition) {
throw message || 'assertion failed';
}
};
var emptyFn = function () {};
var THREE = window.THREE;
var extend = require('extend');
var Stats = require('stats-js');
var dat = require('dat-gui');
var shell = require('game-shell');
var Coordinates = require('./model/Coordinates');
var themes = require('./themes/');
/**
* @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
*
* @class
* @param {Object} options An object containing the following:
* @param {string} [options.selector=null] A css selector to the element to inject the demo
* @param {number} [options.width=window.innerWidth]
* The width of the canvas
* @param {number} [options.height=window.innerHeight]
* The height of the canvas
* @param {boolean} [options.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 {string} [options.theme='dark']
* Theme used in the default scene, it can be `light` or `dark`
* @param {object} [options.helperOptions={}]
* Additional options for the ambient, see the class {@link
* Coordinates}
* @param {object} [options.defaultSceneOptions={}] Additional options
* for the default scene created for this world
*/
function Application(options) {
if (!(this instanceof Application)) {
return new Application(options);
}
this.options = extend(true, {
target: null,
width: window.innerWidth,
height: window.innerHeight,
injectCache: false,
fullScreen: false,
theme: 'dark',
helperOptions: {},
defaultSceneOptions: {
fog: true
},
init: emptyFn,
tick: emptyFn,
render: emptyFn,
shellOptions: {}
}, options);
/**
* 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;
/**
* Dat gui instance
* @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 a game-shell instance
* @type {LoopManager}
*/
this.shell = null;
/**
* Colors for the default scene
* @type {Object}
*/
this.theme = null;
/**
* Application cache
* @type {Object}
*/
this.__t3cache__ = {};
this.gameShell();
}
/**
* Initializes the game loop by creating an instance of game-shell
* @return {this}
*/
Application.prototype.gameShell = function () {
this.shell = shell(extend(this.options.shellOptions, {
element: this.options.target
}));
this.shell.on('init', this.init.bind(this));
this.shell.on('tick', this.tick.bind(this));
this.shell.on('render', this.render.bind(this));
this.shell.on('resize', this.resize.bind(this));
return this;
};
/**
* Getter for the initial options
* @return {Object}
*/
Application.prototype.getOptions = function () {
return this.options;
};
/**
* 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.init = function () {
var me = this;
var options = me.getOptions();
me.injectCache(options.injectCache);
// theme
me.setTheme(options.theme);
// defaults
me.createDefaultRenderer();
me.createDefaultScene();
me.createDefaultCamera();
me.createDefaultLights();
// utils
me.initDatGui();
me.initStats();
// models
me.initCoordinates();
me.options.init.call(this);
};
/**
* Creates the default THREE.WebGLRenderer used in the world
* @return {this}
*/
Application.prototype.createDefaultRenderer = function () {
var me = this;
var options = me.getOptions();
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setClearColor(me.theme.clearColor, 1);
renderer.setSize(options.width, options.height);
this.shell.element.appendChild(renderer.domElement);
me.renderer = renderer;
return me;
};
/**
* Updates the camera to be used to render the scene
*
* @param {string} key
* @return {this}
*/
Application.prototype.setActiveCamera = function (key) {
this.activeCamera = this.cameras[key];
return this;
};
/**
* Adds a camera to the pool of cameras, it needs to be a THREE.PerspectiveCamera
* or a THREE.OrthographicCamera
*
* @param {string} key
* @param {THREE.PerspectiveCamera|THREE.OrthographicCamera} camera
* @returns {Application}
*/
Application.prototype.addCamera = function (key, camera) {
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,
options = me.getOptions(),
width = options.width,
height = options.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 (options.fullScreen) {
THREEx.WindowResize.bind(me.renderer, defaultCamera);
}
me
.createCameraControls(defaultCamera)
.addCamera('default', defaultCamera)
.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;
};
/**
* 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 (key, scene) {
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,
options = me.getOptions(),
defaultScene = new THREE.Scene();
if (options.defaultSceneOptions.fog) {
defaultScene.fog = new THREE.Fog(me.theme.fogColor, 2000, 4000);
}
me
.addScene('default', defaultScene)
.setActiveScene('default');
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`
*
* @return {this}
*/
Application.prototype.setTheme = function (name) {
var me = this;
if (themes[name]) {
me.theme = themes[name];
} else {
console.warn('theme not found!');
}
return me;
};
/**
* 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;
var gui = new dat.GUI({
autoPlace: false
});
// attach dat.gui dom
extend(gui.domElement.style, {
position: 'absolute',
top: '0px',
right: '0px',
zIndex: '1'
});
this.shell.element.appendChild(gui.domElement);
me.datgui = gui;
// game-shell
// dat gui controller
var folder = this.datgui.addFolder('game shell');
folder.add(this.shell, 'startTime');
folder.add(this.shell, 'tickCount').listen();
folder.add(this.shell, 'frameCount').listen();
folder.add(this.shell, 'frameSkip').listen();
folder.add(this.shell, 'tickTime').listen();
folder.add(this.shell, 'paused').listen();
folder.add(this.shell, 'fullscreen').listen();
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,
options = me.getOptions(),
stats;
// add Stats.js - https://github.com/mrdoob/stats.js
stats = new Stats();
extend(stats.domElement.style, {
position: 'absolute',
zIndex: 1,
bottom: '0px'
});
this.shell.element.appendChild(stats.domElement);
me.stats = stats;
return me;
};
/**
* Initializes the coordinate helper (its wrapped in a model in T3)
*/
Application.prototype.initCoordinates = function () {
var options = this.getOptions();
this.scenes['default']
.add(
new Coordinates(options.helperOptions, this.theme)
.initDatGui(this.datgui)
.mesh
);
};
/**
* tick, the place where the game logic happens, t3 updates the following for you
*
* - the stats helper
* - the camera controls if the active camera has one
*
*/
Application.prototype.tick = function () {
var me = this;
me.options.tick.call(this);
};
/**
* Render phase, calls `this.renderer` with `this.activeScene` and
* `this.activeCamera`
*/
Application.prototype.render = function (delta) {
var me = this;
// stats helper
me.stats.update();
// camera update
if (me.activeCamera.cameraControls) {
me.activeCamera.cameraControls.update(delta);
}
me.renderer.render(me.activeScene, me.activeCamera);
// hook
me.options.render.call(this, delta);
};
/**
* 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];
};
Application.prototype.resize = function () {
// notify the renderer of the size change
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.activeCamera.aspect = window.innerWidth / window.innerHeight;
this.activeCamera.updateProjectionMatrix();
};
module.exports = Application;