New demo! Marrying the Scene Graph with the DOM

Continuing the discussion from Writing a scene graph with HTML:

Hello everyone! I’ve developed a very interesting concept in my HTML interface prototype.

Demo

Try the demo by running the following (currently doesn’t work in Safari, and you’ll need some tools pre-installed like Meteor and Node.js).

git clone git@github.com:trusktr/site.git
cd site
git checkout infamous-motor-html-2
npm install # make sure npm is up to date!
meteor

and visiting http://localhost:3000, then read on to find out what’s so interesting about this concept!

Once you have the demo running you should see something like this:

This demo is nothing fancy; it just rotates a few things indefinitely to show that it works (thanks to my 6-year-old nephew for contributing by drawing graphics while I showed him the code to make it rotate). Look at client/home/index.js to see an example of selecting <motor-node> elements and rotating them; and look at client/home/home.html to see how Meteor’s Blaze templating is used to construct a scene! I didn’t do anything fancy like use {{#each}} to render a <motor-node> element for each item in an array, but as you can imagine that will work just fine! I’ll make a better demo soon.

The demo uses infamous v5.0.0 from NPM. I’m following semver strictly (I’m not doing that stay-at-0.x.x-until-first-official-release thing anymore), and the API isn’t stable yet so the major number will probably be bumped a lot at the moment (though I’m striving to do it as least often as possible).

What’s so interesting?

A user of the library can choose to use the imperative API, the HTML API, or both, and how the API is implemented is what makes it interesting. When a scene graph is created using the imperative API, it looks something like this:

import Scene from 'infamous/motor/Scene'

const scene = new Scene('.mount-point-for-the-scene')

const node = new Node({
    size: [100,100],
    align: [0.5,0.5,0.5],
    mountPoint: [0.5,0.5,0.5],
    rotation: [0,30,0],
})

const div = $('<div>hello</div>')[0]
node._el.element.appendChild(div) // The node._el.element API is currently likely to change

scene.addChild(node)

(Note: For now, creating a new Node instance automatically creates a corresponding DOM element, unlike in Famous Engine where a DOMElement has to be manually created and added to the Node.)

What is really interesting about what happens in this example is that when the Scene and Node instances are created with the imperative API, a corresponding <motor-scene> and <motor-node> element are created for each imperative instance, respectively. The above imperative code generates the following DOM, that you’d be able to verify by looking in your element inspector.

<div class=".mount-point-for-the-scene">
    <motor-scene>
        <motor-node size="100, 100, 0" align="0.5, 0.5, 0.5" mountPoint="0.5, 0.5, 0.5" rotation="0, 30, 0" >
            <div>hello</div>
        </motor-node>
    </motor-scene>
</div>

Now, if instead of using the imperative API we just write the above HTML only, then for each <motor-scene> and <motor-node> element is also created a corresponding imperative API Scene and Node instance, respectively. In the end, two parallel tree structures exist, each having the same shape: the imperative API tree, and the DOM tree, married together.

If we write with the imperative API we create the Custom-Element-based DOM tree at the same time, and if we write with the HTML API we create the imperative API’s scene graph tree at the same time!

Suppose that now after we’ve made the scene using the imperative API as in the first example we want to add an animation. We can continue to use the imperative API directly, or we can query the custom elements in the DOM. Here are two examples, each showing one or the other.

Here’s a continuation of the first example, using the direct imperative API:

/* An animation loop managed by infamous is not implemented yet, so we make our
 * own simple loop here for now.
 */
let r = 0
requestAnimationFrame(function loop() {
    requestAnimationFrame(loop)
    node.rotation = [0,r++,0]
})

And here’s an example of using the API on the HTML side of things (imperative in this case, but it’s not the direct imperative API of our library, it’s the imperative API of the browser’s HTML engine used on our Custom Elements):

/* An animation loop managed by infamous is not implemented yet, so we make our
 * own simple loop here for now.
 */
const $node = $('motor-node') // jQuery!
let r = 0
requestAnimationFrame(function loop() {
    requestAnimationFrame(loop)
    $node.attr('', [0,r++,0])
})

In this last example, modifying the attributes using jQuery’s $().attr() method updates the attribute on the custom <motor-node> element in the DOM, which updates the associated Node instance of the imperative API. In example before this last one, updating the rotation of the imperative API Node currently does not update the string-based values of the corresponding HTML element attributes, in order to avoid the performance cost. I’m still pondering the best way to handle this. I think that maybe when an attribute value is requested off of a custom element that the element can query its corresponding imperative API instance at that very moment, rather than being continuously updated.

Lastly, the <motor-scene> and <motor-node> elements are simply just HTML elements. This means we can use normal HTML attributes on them:

<motor-node style="..." class="..." id="..."></motor-node>

This automatically solves the desire that was raised in this thread by @gadicc.

What do you think about this concept? Thanks for reading! :blush:

2 Likes

Oops, there was an error in the demo. Fixed and updated!

Just tried this, but getting

modules.js?hash=833c890…:15893 Uncaught Error: Invalid array. Must be an array of numbers of length 3.

on every rAF, to do with

node2[0].setAttribute('rotation',0, ${r*0.5}, 0)

Same changing to

node2[0].setAttribute('rotation', [0, r*0.5, 0])

Full trace

making lowercase aliases. There is no route for the path: / <motor-node> createdCallback() <motor-scene> createdCallback() <motor-node> createdCallback() Uncaught Error: Invalid array. Must be an array of numbers of length 3. Uncaught TypeError: Cannot read property 'split' of undefined <motor-node> createdCallback() Uncaught Error: Invalid array. Must be an array of numbers of length 3. Uncaught TypeError: Cannot read property 'split' of undefined <motor-node> createdCallback() <motor-scene> createdCallback() <motor-node> createdCallback() Uncaught TypeError: Cannot read property 'split' of undefined Uncaught Error: Invalid array. Must be an array of numbers of length 3. <motor-node> createdCallback() Uncaught Error: Invalid array. Must be an array of numbers of length 3. <motor-node> attachedCallback() scene1 <motor-node> attachedCallback() rotator <motor-node> attachedCallback() <motor-node> attachedCallback() scene2 <motor-node> attachedCallback() whatever <motor-node> attachedCallback() -- node scene (after timeout)? Scene Scene {_el: ElManager, _mounted: true, _removedChildren: Array[0], _parent: null, _scene: Scene…} -- node scene (after timeout)? Node Scene {_el: ElManager, _mounted: true, _removedChildren: Array[0], _parent: null, _scene: Scene…} -- node scene (after timeout)? Node Scene {_el: ElManager, _mounted: true, _removedChildren: Array[0], _parent: null, _scene: Scene…} -- node scene (after timeout)? Scene Scene {_el: ElManager, _mounted: true, _removedChildren: Array[0], _parent: null, _scene: Scene…} -- node scene (after timeout)? Node Scene {_el: ElManager, _mounted: true, _removedChildren: Array[0], _parent: null, _scene: Scene…} -- node scene (after timeout)? Node Scene {_el: ElManager, _mounted: true, _removedChildren: Array[0], _parent: null, _scene: Scene…} Uncaught Error: Invalid array. Must be an array of numbers of length 3. ...

Better yet

@jiku Did you try the latest commit on that branch? I fixed that one already (oops!). xD

@jiku Double oops, I forgot I was npm linked, so I forgot to publish the changes that were working here locally. x]

Update with

git clean -xfd && npm install && meteor

@jiku Ahhh! Sorry, it’s

git pull && git clean -xfd && npm install && meteor
1 Like

@joe that works. Had me confused there for a sec. :smiley: Seems brilliant. Have to do some background reading and playing around with this before I have any valid feedback.

1 Like

I noticed that using an earlier version of NPM might result in an error in the browser:

Uncaught Error: Cannot find module 'babel-runtime/helpers/asyncToGenerator'

The above steps for trying the demo should work if using the latest NPM (v3.8.3). I’m not sure why this happens yet. This means using meteor npm will fail since Meteor uses NPM v2.14.22.

@dmvaldman @Steveblue @andrewreedy @talves @oldschooljarvis @gadicc Critiques/ideas/input needed. :]