UNPKG

infamous

Version:

A CSS3D/WebGL UI library.

248 lines (194 loc) 9.25 kB
import Class from 'lowclass' import documentReady from '@awaitbox/document-ready' import Transformable from './Transformable' import {getWebGLRendererThree, destroyWebGLRendererThree} from './WebGLRendererThree' import {isInstanceof} from './Utility' let documentIsReady = false // TODO use Array if IE11 doesn't have Map. const webGLRenderers = new Map const Motor = Class('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 = 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 documentReady() 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 isInstanceof(node, Transformable) && // 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 !isInstanceof(node._getAncestorThatShouldBeRendered(), Transformable) && // 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. export default new Motor