UNPKG

infamous

Version:

A CSS3D/WebGL UI library.

225 lines (181 loc) 8.58 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _lowclass = _interopRequireDefault(require("lowclass")); var _documentReady = _interopRequireDefault(require("@awaitbox/document-ready")); var _Transformable = _interopRequireDefault(require("./Transformable")); var _WebGLRendererThree = require("./WebGLRendererThree"); var _Utility = require("./Utility"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } let documentIsReady = false; // TODO use Array if IE11 doesn't have Map. const webGLRenderers = new Map(); const Motor = (0, _lowclass.default)('Motor', ({ Public, Private }) => ({ constructor() { Private(this).allRenderTasks = []; Private(this).nodesToBeRendered = []; Private(this).modifiedScenes = []; Private(this).treesToUpdate = []; }, /** * When a render tasks is added a new requestAnimationFrame loop will be started if there * isn't one currently. * * A render task is simply a function that will be called over and over * again, in the Motor's animation loop. That's all, nothing special. * However, if a Node setter is used inside of a render task, then the Node * will tell Motor that it needs to be re-rendered, which will happen at * the end of the current frame. If a Node setter is used outside of a * render task (i.e. outside of the Motor's animation loop), then the Node * tells Motor to re-render the Node on the next animation loop tick. * Basically, regardless of where the Node's setters are used (inside or * outside of the Motor's animation loop), rendering always happens inside * the loop. * * @param {Function} fn The render task to add. * @return {Function} A reference to the render task. Useful for saving to * a variable so that it can later be passed to Motor.removeRenderTask(). */ addRenderTask(fn) { if (typeof fn != 'function') throw new Error('Render task must be a function.'); const self = Private(this); if (self.allRenderTasks.includes(fn)) return; self.allRenderTasks.push(fn); self.numberOfTasks += 1; // If the render loop isn't started, start it. if (!self.animationLoopStarted) self.startAnimationLoop(); return fn; }, removeRenderTask(fn) { const self = Private(this); const taskIndex = self.allRenderTasks.indexOf(fn); if (taskIndex == -1) return; self.allRenderTasks.splice(taskIndex, 1); self.numberOfTasks -= 1; if (taskIndex <= self.taskIterationIndex) self.taskIterationIndex -= 1; }, // in the future we might have "babylon", "playcanvas", etc, on a // per scene basis. getWebGLRenderer(scene, type) { if (webGLRenderers.has(scene)) return webGLRenderers.get(scene); let rendererGetter = null; if (type === "three") rendererGetter = _WebGLRendererThree.getWebGLRendererThree;else throw new Error('invalid WebGL renderer'); const renderer = rendererGetter(scene); webGLRenderers.set(scene, renderer); renderer.initGl(scene); return renderer; }, // A Node calls this any time its properties have been modified (f.e. by the end user). setNodeToBeRendered(node) { const self = Private(this); if (self.nodesToBeRendered.includes(node)) return; self.nodesToBeRendered.push(node); // noop if the loop's already started self.startAnimationLoop(); }, setFrameRequester(requester) { Private(this).requestFrame = requester; }, private: { animationLoopStarted: false, taskIterationIndex: null, numberOfTasks: 0, allRenderTasks: [], nodesToBeRendered: [], modifiedScenes: [], // A set of nodes that are the root nodes of subtrees where all nodes // in each subtree need to have their world matrices updated. treesToUpdate: [], // default to requestAnimationFrame for regular non-VR/AR scenes. requestFrame: window.requestAnimationFrame.bind(window), /** * Starts a requestAnimationFrame loop and runs the render tasks in the allRenderTasks stack. * As long as there are tasks in the stack, the loop continues. When the * stack becomes empty due to removal of tasks, the * requestAnimationFrame loop stops and the app sits there doing nothing * -- silence, crickets. */ async startAnimationLoop() { if (this.animationLoopStarted) return; this.animationLoopStarted = true; if (!documentIsReady) { await (0, _documentReady.default)(); documentIsReady = true; } let timestamp = null; while (this.animationLoopStarted) { timestamp = await this.animationFrame(); this.runRenderTasks(timestamp); // wait for the next microtask before continuing so that SkateJS // updated methods (or any other microtask handlers) have a // chance to handle changes before the next renderNodes call. // // TODO add test to make sure behavior size change doesn't // happen after render await Promise.resolve(); this.renderNodes(timestamp); // If no tasks are left, stop the animation loop. if (!this.allRenderTasks.length) this.animationLoopStarted = false; } }, animationFrame() { return new Promise(r => this.requestFrame(r)); }, runRenderTasks(timestamp) { for (this.taskIterationIndex = 0; this.taskIterationIndex < this.numberOfTasks; this.taskIterationIndex += 1) { const task = this.allRenderTasks[this.taskIterationIndex]; if (task(timestamp) === false) Public(this).removeRenderTask(task); } }, renderNodes(timestamp) { if (!this.nodesToBeRendered.length) return; for (let i = 0, l = this.nodesToBeRendered.length; i < l; i += 1) { const node = this.nodesToBeRendered[i]; node._render(timestamp); // If the node is root of a subtree containing updated nodes and // has no ancestors that were modified, then add it to the // treesToUpdate set so we can update the world matrices of // all the nodes in the subtree. if ( // a node could be a Scene, which is not Transformable (0, _Utility.isInstanceof)(node, _Transformable.default) && // and if ancestor is not instanceof Transformable, f.e. // `false` if there is no ancestor that should be rendered or // no Transformable parent which means the current node is the // root node !(0, _Utility.isInstanceof)(node._getAncestorThatShouldBeRendered(), _Transformable.default) && // and the node isn't already added. !this.treesToUpdate.includes(node)) { this.treesToUpdate.push(node); } // keep track of which scenes are modified so we can render webgl // only for those scenes. // TODO FIXME: at this point, a node should always have a scene, // otherwise it should not ever be rendered here, but turns out // some nodes are getting into this queue without a scene. We // shouldn't need the conditional check for node._scene, and it // will save CPU by not allowing the code to get here in that case. // UPDATE: it may be because we're using `node._scene` which is // null unless `node.scene` was first used. Maybe we just need to // use `node.scene`. if (node._scene && !this.modifiedScenes.includes(node._scene)) this.modifiedScenes.push(node._scene); } // Update world matrices of the subtrees. const treesToUpdate = this.treesToUpdate; for (let i = 0, l = treesToUpdate.length; i < l; i += 1) { treesToUpdate[i]._calculateWorldMatricesInSubtree(); } treesToUpdate.length = 0; // render webgl of modified scenes. // TODO PERFORMANCE: store a list of webgl-enabled modified scenes, and // iterate only through those so we don't iterate over non-webgl // scenes. const modifiedScenes = this.modifiedScenes; for (let i = 0, l = modifiedScenes.length; i < l; i += 1) { const scene = modifiedScenes[i]; if (scene.experimentalWebgl) webGLRenderers.get(scene).drawScene(scene); } modifiedScenes.length = 0; const nodesToBeRendered = this.nodesToBeRendered; for (let i = 0, l = nodesToBeRendered.length; i < l; i += 1) { nodesToBeRendered[i]._willBeRendered = false; } nodesToBeRendered.length = 0; } } })); // export a singleton instance rather than the class directly. var _default = new Motor(); exports.default = _default;