Chris Oliver's Weblog

Saturday Jul 18, 2009

Lazy binding and functional programming

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 internal 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.

Comments:

Chris -

This is a great description.

We use a similar system in our Java apps, although we had to implement our dependancy-tracking binding system to do it. I look forward to being able to leverage JavaFX bindings next time.

Can I have a little more clarification on when the final result gets evaluated?

Take the case where you change a bunch of parameters on a node. I can think of three ways to cause this node to rerender:

1) Fire off a change notice each time a property is changed, and rerender the node each time this notice is received. Naive and expensive.

2) Wrap property change notifications inside an editing session, suppressing all change notices until the session edit is completed and firing off a single aggregate notice at the end of the session. This gets messy because you forget to close the editing session.

3) Don't fire notices outside the bound system and rely on a disconnected polling system.

It sounds like you are using method 3. Is that accurate?

Does JavaFX binding have any support for method 2?

Thanks,
Willis Morse

Posted by Willis Morse on July 23, 2009 at 03:01 PM PDT #

Yes, the rendering loop is a polling mechanism by definition - which blocks on vertical sync. Nevertheless, it is important to be able to detect that no rendering of a new frame is required - in which case you can skip swapping the frame buffer and just sleep instead.

Posted by Christopher Oliver on July 23, 2009 at 05:20 PM PDT #

So you make no attempt to prerender this stuff in another thread?

After years of model-driven rendering, I'm finally coming around to the idea of rendering all my UI on a polling frame-rate pulse like this. It's really cool to see all your UI widgets update at the same time at a constant frame rate... it makes the UI look a lot more stable and robust.

My only problem with this in my apps is that it wastes a fair amount of CPU cycles even when the model doesn't change. But it sounds like you guys get the benefits of some robust caching, which should ameliorate that somewhat.

I'm a little unclear on who's doing the caching inside the binding system. Is that part of JavaFX's bindings system, or something custom you built on top of it?

Posted by Willis Morse on July 23, 2009 at 05:58 PM PDT #

This is a great description.

Posted by links of london on December 13, 2009 at 09:45 PM PST #

jean style jean style jeans stylejeans style mens jean style mens jean style skinny jean style skinny jean style blue jeans style blue jeans style seven jeans style seven jeans style new jean style new jean style latest jean style latest jean style jeans styles jeans styles boy style jeans boy style jeans men jean style men jean style jean style 

Posted by jean style on December 17, 2009 at 10:08 PM PST #

Thank you very successful and useful site I have received the necessary information ...

Posted by Bilgisayar on December 26, 2009 at 07:51 AM PST #

Thank you very successful and useful site I have received the necessary information ...

Posted by toner arşivi on December 26, 2009 at 07:51 AM PST #

Thank you very successful and useful site I have received the necessary information ...

Posted by xerox on December 26, 2009 at 07:52 AM PST #

we only require 2d, 3d, and 4d vectors and 2x2, 3x3, and 4x4 matrices. These types are provided in a new package called javafx.math, http://www.watchgy.com/ namely Vec2, Vec3, Vec4, Mat2, Mat3, Mat4. Since rotations may be represented as a pair of angle/axis, or quaternion form, in addition to matrix form, we also provide the types AngleAxis, and Quat. http://www.watchgy.com/tag-heuer-c-24.html
http://www.watchgy.com/rolex-submariner-c-8.html
http://www.watchgy.com/rolex-daytona-c-6.html

Posted by rolex replica on December 29, 2009 at 05:39 AM PST #

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
Tavla Indir
http://www.gercektavla.com/
tavla indir
online tavla indir
http://www.gercektavla.com/tavla-indir/
http://www.gercektavla.com/tag/tavla-indir/
http://www.gercektavla.com/category/tavla-indir/
http://www.gercektavla.com/2009/11/25/vip-tavla-indir/
okey indir tavla indir
okey tavla indir
http://www.gercektavla.com/2009/11/04/okey-tavla-indir/
http://www.gercektavla.com/2009/09/06/tavla-indir-lobisi/
bedava tavla indir
http://www.gercektavla.com/2009/08/16/tikla-bedava-tavla-indir/

Posted by Tavla İndir on January 05, 2010 at 12:42 PM PST #

Lan türk değilmisiniz piç ettiniz yorum kısmını.

Posted by medyum on January 30, 2010 at 02:22 PM PST #

Post a Comment:
  • HTML Syntax: NOT allowed

Calendar

Feeds

Search

Links

Navigation

Referers