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!