Layout takes the form of a tree structure because most of layout is positioning one thing relative to another. In HTML, the tree is the DOM tree, and relative positioning is done by nesting DOM elements. In Samsara the nesting is done in JavaScript in what's called the render tree.
By doing layout in JavaScript, we can be much more flexible than CSS. For instance, with CSS alone you cannot track a
<div>
with your finger, or synchronize the transforms in two CSS classes, or get the current velocity of an animation,
etc. There are costs, however, to doing layout in JavaScript. For one, all computation must happen on the main thread,
and can't be offloaded to the UI thread. This requires us to be CPU conscious and places a high priority on performance
to achieve 60 frames per second. The critical part of the performance of Samsara is that the render tree is a low-footprint
model of the layout of Samsara elements, and it is architected to only update the parts of Samsara that change in time.
The render tree begins with a root called the Context
and is built up of nodes with various layout data.
The leaves of the tree are Surfaces
. Unlike HTML, only the leaves of the tree have a visual representation
in the DOM. This representation is modified by the nodes above them, which manifest in the DOM as
changes to opacity
, size
, and transform
CSS properties.
To calculate the final properties of a Surface
, one composes the properties of all the nodes above it.
For instance, the final opacity of a Surface
is the multiplication of all opacity values defined above it.
The Context
is the root of the render tree. It establishes a 3D context for Surfaces
that are added to it.
var Context = Samsara.DOM.Context;
var Surface = Samsara.DOM.Surface;
var context = new Context();
var surface = new Surface({
size : [100,100],
properties : {background : 'red'}
});
context.add(surface);
context.mount(document.body);
Context's
are mounted to a DOM element, and export their content, in this case a single Surface
into that element.
Contexts
also have setPerspective
and setPerspectiveOrigin
methods to apply a 3D perspective
to the Surfaces
within it.
Nodes are JSON objects that can be added to the render tree. They modify the size and layout of the tree below them. Nodes take special keys defined below, whose values can be static quantities (e.g., numbers, arrays) or dynamic streams for animation and responsive design.
Nodes shouldn't be thought of as existing in a vacuum, they are part of the larger render tree, and so can take "relative" quantities. Just as in CSS, you can define an element's width to be 50%, this only means something in the larger context of the HTML page. Node properties come in two flavors: size properties and layout properties.
Key | Type | Description |
---|---|---|
size | [w,h] |
Defines a size in pixels. Values can be numeric, or also undefined , true or false (see below) |
proportions | [w,h] |
Defines a size using ratios of the parent size dimensions |
margins | [x,y] |
Subtracts a number of pixels from the current width and height |
aspectRatio | Number |
Scales the width by the height if the height is false , or visa-versa if the width is false |
As stated above, size dimensions can take non-numeric values, which translate to the following:
Size Dimension Value | Description |
---|---|
Number | Use this value in pixels |
undefined | Use the parent node's value |
true | Use the DOM's calculated value |
false | This value will be ignored |
Note: Currently using
true
sizing only works forSurfaces
and not for nodes in general, though future versions of Samsara will remedy this.
Here we show a few examples to illustrate the flexibility of defining size in Samsara. Though we talk of nodes
as being explicitly added to the render tree, nodes effecting size are also implicitly inside of Surfaces
.
See the Pen size by SamsaraJS (@samsaraJS) on CodePen.
There are also nodes that affect layout, which encompasses modifying position, rotation, scale, skew, and opacity.
Key | Type | Default | Description |
---|---|---|---|
transform | Transform |
Transform.identity |
A CSS3 transform representing a combination of translation, rotation, scaling or skewing |
opacity | Number |
1 | Defines the opacity |
align | [x,y] |
[0,0] |
Defines a translation by a proportion of the parent size |
origin | [x,y] |
[0,0] |
Defines a translation by a proportion of the currently defined size |
Though transform
and opacity
are clear in how they modify the render tree, align
and origin
are Samsara specific. They are a shorthand for creating transforms relative to a defined size. For example,
they can be used for centering, or aligning something relative to the bottom right, etc.
In more detail, let's assume a parent size of [w, h]
and a node size of [w', h']
. We would like to align
the node relative to the parent size.
An align value of [x,y]
will produce a translation of [w * x, h * y]
, and is a shorthand for
Transform.translate([w * x, h * y])
.
An origin value of [x,y]
will produce a translation of [-w * x', -h * y']
, and is a shorthand for
Transform.translate([-w * x', -h * y'])
.
It may be easier to think of alignment as aligning the origin point. Hence to center
the top left of a Surface
all that is needed is to align it with [.5, .5]
(as the origin defaults to top left).
To center the middle of a Surface
you will also need to define an origin of [.5, .5] on the Surface
.
Here's an example demonstrating how this is used.
See the Pen alignment by SamsaraJS (@samsaraJS) on CodePen.
The render tree bottoms out in Surfaces
, which are
ultimately responsible for creating elements in the DOM
and updating their inline styles. Surfaces
come bundled with their own layout node, so they can take
node properties modifying their size (proportions
, margins
and aspectRatio
) and layout (origin
and opacity
).
However, transform
and align
are properties "external" to a Surface
and can only be defined by nodes above them
in the render tree.
Surfaces
take other DOM-related values, such as
content
- the innerHTMLattributes
- DOM attributesproperties
- CSS style propertiesclasses
- an array of comma-separated CSS classesSurfaces
can listen to any DOM events, such as click
, by calling the on
method. This is also
true of Contexts
.
surface.on('click', function(event){
console.log("I've been clicked");
});
The DOM output of Samsara is generally flat, with the heirarchical nesting happening in JavaScript, instead of HTML.
Occasionally nesting is necessary, for example, to provide clipping. For these purposes Samsara has a class
similar to a Surface
called a ContainerSurface.
A ContainerSurface
maintains its own render tree, similar to a View
and nests Surfaces
inside it,
similar to a Context
. Here's an example of using a ContainerSurface
to clip another Surface
:
var container = new ContainerSurface({
size : [150, 150],
origin : [.5,.5],
properties : {
overflow : 'hidden',
border : '1px dashed black'
}
});
var surface = new Surface({
size : [150, 150],
origin : [.5,.5],
properties : {background : 'red'}
});
container.add({
align : [.5,.5],
transform : Transform.rotateZ(Math.PI/4)
}).add(surface);
context.add({align : [.5,.5]}).add(container);
See the Pen container surface by SamsaraJS (@samsaraJS) on CodePen.
The Render Tree
is enlarged by calling the add
method with nodes
, surfaces
or views
.
By chaining add
calls we linearly build up the tree, whereas by calling add
on the same node successively we branch the tree.
By chaining nodes, their effects compound: their transforms are composed, and their opacities are multiplied, etc.
context var context = new Context();
│
node1 context.add(node1)
│ .add(node2)
node2 .add(surface);
│
surface
By branching nodes, we create independent parts of the tree.
context var context = new Context();
┌─────┴─────┐
node1 surface2 context.add(node1).add(surface1); // left branch
│
surface1 context.add(surface2); // right branch
By mixing branching and chaining, we can group surfaces
together and change their properties simultaneously. In the below example, if we change the data in node1
it will change both surfaces
. If we change the data in node2
only surface1
will be affected.
context var context = new Context()
│
node1 var relativeNode = context.add(node1);
┌─────┴─────┐
node2 surface2 relativeNode.add(node2).add(surface1); // left branch
│
surface1 relativeNode.add(surface2); // right branch
So far we've seen that you can add nodes and Surfaces to the Render Tree.
These can be thought of as the lego blocks of more complicated objects. A section
of the render tree can be encapsulated in what's called a View
. Samsara comes with a View
class that can be extended for custom views, and then added to the render tree just like nodes
and Surfaces
. A View
provides methods for constructing its own render tree, and also includes
support for receiving and broadcasting events, and taking in default options.
SamsaraJS also ships with a library of common views that we will keep adding to. You can
read more about views
here. For now we will only be concerned about how Views
are used to encapsulate their own render trees.
In the example below, we add a Scrollview.
context var context = new Context();
│
node0 context.add(node0).add(scrollview);
│
scrollview
Internally, Scrollview
has its own complex logic, but that is hidden from the
developer, who can simply include it in her project by adding it like any other node.
The scrollview
itself has its own render tree, populated with nodes and surfaces
(or other views
)
from its own custom logic.
scrollview scrollview.addItems([S0, S1, S2, ... , S9]);
┌───┬───┼───────┐
S0 S1 S2 ⋯ S9
In the above example, S9
doesn't have to be a Surface
; it could
have been a View
with its own render tree, even another scrollview itself.
Perhaps the render tree for S9
looks like this:
S9 S9.add(node1).add(surface1);
┌─────┴─────┐
node1 node2 S9.add(node2).add(surface2);
│ │
surface1 surface2
If we were to unravel the entire render tree, we would find it looks like:
context
│
node0
│
scrollview
┌───┬───┼───────┐
S0 S1 S2 ⋯ S9
┌─────┴─────┐
node1 node2
│ │
surface1 surface2
By encapsulating complex logic in Views
, understanding an app becomes more
manageable. And unlike DOM, there is no performance degradation incurred from
nesting structure. This nested structure only exists in JavaScript, and is
flattened by the time it gets to the DOM.