One of the first things you’ll notice about Famo.us is how little we expose HTML and the DOM to the developer. Interacting with the DOM is riddled with performance issues. Famo.us abstracts away DOM management by maintaining a representation of it in JavaScript called the render tree.
If you inspect a website running Famo.us, you’ll notice the DOM is very flat: most elements are siblings of one another. Inspect any other website, and you’ll see the DOM is highly nested. Famo.us takes a radically different approach. We keep the structure of HTML in JavaScript. To Famo.us, HTML is more like a list of things to draw to the screen than the source of truth.
Developers are used to nesting HTML elements because that’s the way to get relative positioning, event bubbling, and semantic structure. However, there is a cost to each of these: relative positioning causes slow page reflows on animating content; event bubbling is expensive when event propagation is not carefully managed; and semantic structure is not well separated from visual rendering in HTML.
Famo.us promises a rich 60 FPS experience, and to do so, we needed to circumvent these inefficiencies. When we decided to abstract away the DOM, we needed a way to maintain the expectations every web developer has of the DOM, but in a way that doesn’t compromise on performance. The render tree is our solution to relative positioning and semantic structure.
A tree’s starting point is called its root. In HTML, this root is the <body>
tag. In Famo.us, the root is a context. We instantiate a context via the engine’s .createContext()
method. This creates a <div>
with the CSS class famous-container
. We can also pass in a pre-existing DOM element, e.g. .createContext(myElement)
, if we want to create a context only within that element.
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context1
// @famous-block-option textPanelActive true
var Engine = famous.core.Engine;
var context = Engine.createContext();
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context1
// @famous-block-filename main.css
.famous-container {
outline: 1px dashed white;
margin: 1px;
padding: 1px;
}
.famous-container::after {
font-family: monospace;
content: "context (.famous-container)";
}
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context1
// @famous-block-filename en.md
# A simple context
Here, we illustrate creating the _root_ of the render tree by creating a context with nothing in it. Pretty boring!
We've added dotted lines and text to better illustrate what's going on here. In real-world practice, contexts themselves are invisible.
A tree is made up of nodes. In HTML, those nodes would be tags like <div>
or <button>
. In Famo.us, nodes come in two flavors: renderables and modifiers. Renderables are nodes that actually get drawn to the screen. Modifiers aren’t themselves drawn, but affect how renderables get drawn. We use both of these types of nodes in conjunction to get content onto the screen. This process is known as extending the render tree.
A context has no visual representation; it merely provides a starting point for Famo.us’ render cycle. To get something on the screen, we will need to extend the render tree by adding nodes via the context’s .add()
method. A Famo.us surface is one kind of node, which loosely corresponds to a <div>
in HTML. This <div>
will be nested inside of the <div>
allocated to the context.
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context2
// @famous-block-option textPanelActive true
var Engine = famous.core.Engine;
var context = Engine.createContext();
var Surface = famous.core.Surface;
var surface = new Surface({
size: [100, 100]
});
context.add(surface);
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context2
// @famous-block-filename main.css
.famous-container {
outline: 1px dashed white;
margin: 1px;
padding: 1px;
background-color: #111;
}
.famous-container::after {
font-family: monospace;
content: "context (.famous-container)";
}
.famous-surface {
outline: 1px solid white;
margin: 1px;
padding: 1px;
background-color: #222;
}
.famous-surface::after {
font-family: monospace;
content: "surface (.famous-surface)";
}
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context2
// @famous-block-filename en.md
# Adding a node
Calling `.add()` on the _context_ and passing it a _renderable_ has the effect of adding a node to the render tree.
If you inspect the _surface_ element that appears in the demo area, you'll see that it's just a `<div>` element.
We've added some lines and text here for illustration that would not be present in a normal Famo.us app.
A modifier is another type of Famo.us render node that is capable of modifying the nodes below it in the render tree. (Surfaces are “dumb.” They don’t know where they are in the page, or whether they’re even visible; that is the job of the modifier.) By using modifiers in conjuntion with transforms, we can change the size, position, scale, and opacity of renderables.
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context3
// @famous-block-option textPanelActive true
var Engine = famous.core.Engine;
var context = Engine.createContext();
var Surface = famous.core.Surface;
var surface = new Surface({
size: [100, 100]
});
var Transform = famous.core.Transform;
var Modifier = famous.core.Modifier;
var modifier = new Modifier({
transform: Transform.translate(100, 200)
});
context.add(modifier).add(surface);
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context3
// @famous-block-filename main.css
.famous-container {
outline: 1px dashed white;
margin: 1px;
padding: 1px;
background-color: #111;
}
.famous-container::after {
font-family: monospace;
content: "context (.famous-container)";
}
.famous-surface {
outline: 1px solid white;
margin: 1px;
padding: 1px;
background-color: #222;
}
.famous-surface::after {
font-family: monospace;
content: "surface (.famous-surface)";
}
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context3
// @famous-block-filename en.md
# Adding a modifier
Here, we add a modifier above the surface in the render tree. The modifier applies changes to the surface.
The modifier has a single property set: its _transform_ property. We're assigning it the value of a transform matrix. That matrix describes a _translation_ of 100 pixels to the right and 200 pixels downward.
As we’ve already seen, modifiers affect the part of the render tree beneath them. Modifiers can affect other modifiers, too. By chaining modifiers, their effects compound: their transforms are composed, and their opacities are multiplied. This makes separating state easy to do. One modifier can handle opacity, while another can handle rotation. For example:
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context4
// @famous-block-option textPanelActive true
var Engine = famous.core.Engine;
var context = Engine.createContext();
var Surface = famous.core.Surface;
var surface = new Surface({
size: [100, 100]
});
var Transform = famous.core.Transform;
var Modifier = famous.core.Modifier;
var modifier1 = new Modifier({
transform: Transform.rotateZ(0.5)
});
var modifier2 = new Modifier({
opacity: 0.5
});
context.add(modifier1).add(modifier2).add(surface);
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context4
// @famous-block-filename main.css
.famous-container {
outline: 1px dashed white;
margin: 1px;
padding: 1px;
background-color: #111;
}
.famous-container::after {
font-family: monospace;
content: "context (.famous-container)";
}
.famous-surface {
outline: 1px solid white;
margin: 1px;
padding: 1px;
background-color: #222;
}
.famous-surface::after {
font-family: monospace;
content: "surface (.famous-surface)";
}
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context4
// @famous-block-filename en.md
# Rotation and opacity
In this demo, we apply both a rotation and an opacity to the surface.
The first modifier applies a rotational transform to all nodes below it (including `modifier2`). The second modifier applies an opacity of `0.5` to all nodes below (the `surface`).
The end effect is a rotated, semi-transparent surface.
Render trees don’t have to be linear. They can branch, too. Below is a simple example demonstrating how to branch the tree by calling .add()
successively on the same node.
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context5
// @famous-block-option textPanelActive true
var Engine = famous.core.Engine;
var context = Engine.createContext();
var Surface = famous.core.Surface;
var surface1 = new Surface({
size: [100, 100]
});
var surface2 = new Surface({
size: [150, 150]
});
var Transform = famous.core.Transform;
var Modifier = famous.core.Modifier;
var modifier = new Modifier({
transform: Transform.translate(100, 200)
});
context.add(modifier).add(surface1);
context.add(surface2);
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context5
// @famous-block-filename main.css
.famous-container {
outline: 1px dashed white;
margin: 1px;
padding: 1px;
background-color: #111;
}
.famous-container::after {
font-family: monospace;
content: "context (.famous-container)";
}
.famous-surface {
outline: 1px solid white;
margin: 1px;
padding: 1px;
background-color: #222;
}
.famous-surface::after {
font-family: monospace;
content: "surface (.famous-surface)";
}
// @famous-block
// @famous-block-option preset famous-0.3.0-globals
// @famous-block-group context5
// @famous-block-filename en.md
# Separate branches
Here, we arranged the render tree such that the modifier only effects one of the two surfaces.
When we call `.add()` on the root node (the `context`), we are adding a branch _from that node_, as opposed to the last node added to it.
The `.add()` method returns the node added, so chaining calls to `.add()` has the effect of adding branches one after another.
It’s modifiers all the way down – Anon
In all the examples above, you’ll notice a pattern: a render tree starts with a context, branches into a bunch of modifiers, and ends with surfaces. Unlike the DOM, where nodes mix visual representation with syntactic clustering, the render tree makes a clear separation between layout (modifiers), content (surfaces), and structure (.add()
).
In fact, if you want to know what the position, or opacity of a surface at the bottom of the render tree, you just have to multiply the opacities and transforms of the modifiers above it!
Another point of divergence to note is that the DOM executes a redraw whenever a node’s styling or content is changed (i.e., immediate mode). In Famo.us, the render tree batches changes behind the scenes by buffering them against the window.requestAnimationFrame
API (i.e., retain mode). This ensures that changes are resolved at the most optimal time (synced to your monitor’s refresh rate).
To recap, here’s a comparison between traditional DOM and the Famo.us render tree.
Famo.us render tree | DOM | |
---|---|---|
Tree structure | Yes | Yes |
Nodes | Renderables | HTML elements |
Reflows | No | Yes |
Encapsulation | Views | Shadow DOM |
Meaning | Structure | Structure & rendering |
Render cycle | Retain mode | Immediate mode |
Language | JavaScript | HTML |
Copyright © 2013-2015 Famous Industries, Inc.