-
Notifications
You must be signed in to change notification settings - Fork 35
/
Engine.js
340 lines (306 loc) · 10.3 KB
/
Engine.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Owner: [email protected]
* @license MPL 2.0
* @copyright Famous Industries, Inc. 2014
*/
define(function(require, exports, module) {
/**
* The singleton object initiated upon process
* startup which manages all active Context instances, runs
* the render dispatch loop, and acts as a listener and dispatcher
* for events. All methods are therefore static.
*
* On static initialization, window.requestAnimationFrame is called with
* the event loop function.
*
* Note: Any window in which Engine runs will prevent default
* scrolling behavior on the 'touchmove' event.
*
* @static
* @class Engine
*/
var Context = require('./Context');
var EventHandler = require('./EventHandler');
var OptionsManager = require('./OptionsManager');
var Engine = {};
var contexts = [];
var nextTickQueue = [];
var deferQueue = [];
var lastTime = Date.now();
var frameTime;
var frameTimeLimit;
var loopEnabled = true;
var eventForwarders = {};
var eventHandler = new EventHandler();
var options = {
containerType: 'div',
containerClass: 'famous-container',
fpsCap: undefined,
runLoop: true
};
var optionsManager = new OptionsManager(options);
/** @const */
var MAX_DEFER_FRAME_TIME = 10;
/**
* Inside requestAnimationFrame loop, step() is called, which:
* calculates current FPS (throttling loop if it is over limit set in setFPSCap),
* emits dataless 'prerender' event on start of loop,
* calls in order any one-shot functions registered by nextTick on last loop,
* calls Context.update on all Context objects registered,
* and emits dataless 'postrender' event on end of loop.
*
* @static
* @private
* @method step
*/
Engine.step = function step() {
var currentTime = Date.now();
// skip frame if we're over our framerate cap
if (frameTimeLimit && currentTime - lastTime < frameTimeLimit) return;
var i = 0;
frameTime = currentTime - lastTime;
lastTime = currentTime;
eventHandler.emit('prerender');
// empty the queue
for (i = 0; i < nextTickQueue.length; i++) nextTickQueue[i].call(this);
nextTickQueue.splice(0);
// limit total execution time for deferrable functions
while (deferQueue.length && (Date.now() - currentTime) < MAX_DEFER_FRAME_TIME) {
deferQueue.shift().call(this);
}
for (i = 0; i < contexts.length; i++) contexts[i].update();
eventHandler.emit('postrender');
};
// engage requestAnimationFrame
function loop() {
if (options.runLoop) {
Engine.step();
window.requestAnimationFrame(loop);
}
else loopEnabled = false;
}
window.requestAnimationFrame(loop);
//
// Upon main document window resize (unless on an "input" HTML element):
// scroll to the top left corner of the window,
// and for each managed Context: emit the 'resize' event and update its size.
// @param {Object=} event document event
//
function handleResize(event) {
for (var i = 0; i < contexts.length; i++) {
contexts[i].emit('resize');
}
eventHandler.emit('resize');
}
window.addEventListener('resize', handleResize, false);
handleResize();
// prevent scrolling via browser
window.addEventListener('touchmove', function(event) {
event.preventDefault();
}, true);
/**
* Add event handler object to set of downstream handlers.
*
* @method pipe
*
* @param {EventHandler} target event handler target object
* @return {EventHandler} passed event handler
*/
Engine.pipe = function pipe(target) {
if (target.subscribe instanceof Function) return target.subscribe(Engine);
else return eventHandler.pipe(target);
};
/**
* Remove handler object from set of downstream handlers.
* Undoes work of "pipe".
*
* @method unpipe
*
* @param {EventHandler} target target handler object
* @return {EventHandler} provided target
*/
Engine.unpipe = function unpipe(target) {
if (target.unsubscribe instanceof Function) return target.unsubscribe(Engine);
else return eventHandler.unpipe(target);
};
/**
* Bind a callback function to an event type handled by this object.
*
* @static
* @method "on"
*
* @param {string} type event type key (for example, 'click')
* @param {function(string, Object)} handler callback
* @return {EventHandler} this
*/
Engine.on = function on(type, handler) {
if (!(type in eventForwarders)) {
eventForwarders[type] = eventHandler.emit.bind(eventHandler, type);
if (document.body) {
document.body.addEventListener(type, eventForwarders[type]);
}
else {
Engine.nextTick(function(type, forwarder) {
document.body.addEventListener(type, forwarder);
}.bind(this, type, eventForwarders[type]));
}
}
return eventHandler.on(type, handler);
};
/**
* Trigger an event, sending to all downstream handlers
* listening for provided 'type' key.
*
* @method emit
*
* @param {string} type event type key (for example, 'click')
* @param {Object} event event data
* @return {EventHandler} this
*/
Engine.emit = function emit(type, event) {
return eventHandler.emit(type, event);
};
/**
* Unbind an event by type and handler.
* This undoes the work of "on".
*
* @static
* @method removeListener
*
* @param {string} type event type key (for example, 'click')
* @param {function} handler function object to remove
* @return {EventHandler} internal event handler object (for chaining)
*/
Engine.removeListener = function removeListener(type, handler) {
return eventHandler.removeListener(type, handler);
};
/**
* Return the current calculated frames per second of the Engine.
*
* @static
* @method getFPS
*
* @return {Number} calculated fps
*/
Engine.getFPS = function getFPS() {
return 1000 / frameTime;
};
/**
* Set the maximum fps at which the system should run. If internal render
* loop is called at a greater frequency than this FPSCap, Engine will
* throttle render and update until this rate is achieved.
*
* @static
* @method setFPSCap
*
* @param {Number} fps maximum frames per second
*/
Engine.setFPSCap = function setFPSCap(fps) {
frameTimeLimit = Math.floor(1000 / fps);
};
/**
* Return engine options.
*
* @static
* @method getOptions
* @param {string} key
* @return {Object} engine options
*/
Engine.getOptions = function getOptions() {
return optionsManager.getOptions.apply(optionsManager, arguments);
};
/**
* Set engine options
*
* @static
* @method setOptions
*
* @param {Object} [options] overrides of default options
* @param {Number} [options.fpsCap] maximum fps at which the system should run
* @param {boolean} [options.runLoop=true] whether the run loop should continue
* @param {string} [options.containerType="div"] type of container element. Defaults to 'div'.
* @param {string} [options.containerClass="famous-container"] type of container element. Defaults to 'famous-container'.
*/
Engine.setOptions = function setOptions(options) {
return optionsManager.setOptions.apply(optionsManager, arguments);
};
/**
* Creates a new Context for rendering and event handling with
* provided document element as top of each tree. This will be tracked by the
* process-wide Engine.
*
* @static
* @method createContext
*
* @param {Node} el will be top of Famo.us document element tree
* @return {Context} new Context within el
*/
Engine.createContext = function createContext(el) {
var needMountContainer = false;
if (!el) {
el = document.createElement(options.containerType);
el.classList.add(options.containerClass);
needMountContainer = true;
}
var context = new Context(el);
Engine.registerContext(context);
if (needMountContainer) {
Engine.nextTick(function(context, el) {
document.body.appendChild(el);
context.emit('resize');
}.bind(this, context, el));
}
return context;
};
/**
* Registers an existing context to be updated within the run loop.
*
* @static
* @method registerContext
*
* @param {Context} context Context to register
* @return {FamousContext} provided context
*/
Engine.registerContext = function registerContext(context) {
contexts.push(context);
return context;
};
/**
* Queue a function to be executed on the next tick of the
* Engine.
*
* @static
* @method nextTick
*
* @param {function(Object)} fn function accepting window object
*/
Engine.nextTick = function nextTick(fn) {
nextTickQueue.push(fn);
};
/**
* Queue a function to be executed sometime soon, at a time that is
* unlikely to affect frame rate.
*
* @static
* @method defer
*
* @param {Function} fn
*/
Engine.defer = function defer(fn) {
deferQueue.push(fn);
};
optionsManager.on('change', function(data) {
if (data.id === 'fpsCap') Engine.setFPSCap(data.value);
else if (data.id === 'runLoop') {
// kick off the loop only if it was stopped
if (!loopEnabled && data.value) {
loopEnabled = true;
window.requestAnimationFrame(loop);
}
}
});
module.exports = Engine;
});