Recommended: Sing it, brah! 5 fabulous songs for developers
JW's Top 5
Functional programming techniques can be used in conjunction with lazy binding to rather easily and compactly express the complex multi-valued dependencies we require.
As an example, let's consider the humble HBox, a node which performs a simple horizontal layout of the nodes it contains:
public class HBox extends CustomNode {
public var content: Node[];
public var spacing: Number;
bound lazy function layout(nodes:Node[], x:Number):Node[] {
if (nodes == [])
then []
else {
def theNextToLayout = nodes[0];
def theRestToLayout = nodes[1..];
[Group {content: theNextToLayout, x: bind lazy x},
layout(theRestToLayout, x + spacing + theNextToLayout.bounds.width)]
}
}
override var internalContent = Group {
content: bind lazy layout(content, 0);
}
}
In our system CustomNode is defined like this:
public abstract class CustomNode extends Node {
protected var internalContent: Node on replace { internalContent.parentNode = this };
override var contentBounds = bind lazy internalContent.bounds;
}
Concrete subclasses of CustomNode are required to override its protected internalContent variable for their specific content, and CustomNode's contentBounds is defined as the bounds of its content.
HBox declares two public variables, "content", which is the sequence of nodes it will contain, and "spacing" which defines an additional uniform distance used to separate them. It overrides its inherited "internalContent" variable to consist of a Group containing a sequence of auxiliary nodes used to perform the layout, which are are set up in the recursive lazy bound function "layout".
The location on the x axis of a given child of HBox depends on the spacing and on the combined widths of all the nodes that precede it. All of these values can be directly or indirectly animated.
The layout function takes two parameters, a list of nodes on which to perform the layout, and an accumulated displacement along the x axis. Each time it's called, it wraps the first node in the list in a Group whose x coordinate is bound to the accumulated displacement, and then (lazily) calls itself with the remainder of the list, adding the spacing and the width of that node to the accumulator (note: because this is a bound function this expression is also bound). The layout group of the first node is then concatenated with the result of laying out the rest and returned. Note that calling a lazy bound function is quite unlike calling an unbound JavaFX function or a Java method. Basically all it does at the time of the call is build an unevaluated dependency tree.
The end result of this is that I can insert/delete/replace nodes in HBox.content, animate their transforms or other characteristics which affect their ultimate width, or animate the spacing, and the layout will be correctly recomputed - but lazily only when required.
Here's a simple example, which creates an HBox containing a variable number of spheres. The scale of the contained spheres is animated, as well as the count and the spacing.
def SPHERE_N = 30;
var count: Integer = 10;
var spacing: Number = 1;
var s: Number = 1.0;
var color: Color;
def shader = FixedFunctionShader {
diffuse: bind lazy color;
}
def t = Timeline {
keyFrames:
[KeyFrame {
time: 0s;
values:
[color => BLUE,
s => 1.0,
spacing => 1.0,
count => 10]
},
KeyFrame {
time: 5s;
values:
[color => RED tween LINEAR,
s => 3.0 tween LINEAR,
spacing => 5.0 tween LINEAR,
count => SPHERE_N tween LINEAR]
}]
autoplay: true;
autoReverse: true;
repeatCount: Timeline.INDEFINITE;
}
def spheres = bind lazy for (i in [1..SPHERE_N]) {
Sphere {
radius: 1;
transform: bind lazy scale(s, s, s);
shader: shader;
}
}
Stage {
scene: Scene {
content:
HBox {
spacing: bind lazy spacing;
content: bind lazy spheres[0..<count];
}
}
}
This approach eliminates the need for any special procedural "layout" pass or protocol. Accessing any variable which depends on the layout (for example the bounds of the HBox itself, the parent transform or world transform of any of its contained nodes, etc), will implicitly evaluate the bindings woven together in our "layout" function as required, thus effecting the layout.
This technique is quite general, and is used throughout, for example in the case of aim, parent, orient, and point transform constraints. Here's part of the implementation of "point constraint". A point (or location) constraint is an operation which positions a node based on the weighted locations of a set of other nodes. For example, "position node A halfway between node B and node C".
public class Constraint {
public var node: Node;
public var weight: Number = 1.0;
}
...
bound lazy function pointConstraint(constraints:Constraint[], translation:Vec3, weight:Number):Vec3 {
if (constraints == []) {
if (weight == 0) then Vec3.ZERO else translation / weight;
} else {
def c = constraints[0];
def cs = constraints[1..];
def location = c.node.worldTransform.getTranslation();
pointConstraint(cs,
translation + location * c.weight,
weight + c.weight)
}
}
bound lazy function pointConstraint(constraints:Constraint[]):Vec3 {
pointConstraint(constraints, Vec3.ZER0, 0);
}
...
Here's an example of the how the above mentioned constraint might be expressed:
var B:Node = ...;
var C:Node = ...;
def c1 = Constraint { node: B, weight 0.5 };
def c2 = Constraint { node: C, weight: 0.5 };
def transform = bind lazy translate(pointConstraint([c1, c2]));
def A = Cube { transform: bind lazy transform, ... };
Note that the locations of the nodes and/or the weights associated with the constraints may be animated or otherwise change. The lazy bound recursive function "pointConstraint" above receives two accumulated values as it iterates the list of constraints, the accumulated translation and the accumulated weight. When the end of the list is reached the result is produced by dividing the total displacement (translation) by the total weight.
Here's a very simple test case of a point constraint imported from Maya. The red sphere is point constrained to the 4 yellow cubes. The test animates the position of the sphere around and among the cubes by simply animating the weights of the 4 constraints.