Sunday, January 6, 2013

Accumulating JavaFX Transforms With Xtend

I was a bit vague in my last post on JavaFX's transformation API, so I decided to write a separate post on this topic.

In JavaFX, each node in the scenegraph can be translated, rotated, scaled or sheared relative to its parent. In mathematical terms, each node maintains a transformation describing its own local coordinate system. This transformation can be defined with a matrix represented as a Transform object in JavaFX.

The JavaFX API offers some convenience methods to manipulate this transformation. E.g., you can scale a node in x-direction and translate it by 42 units in y-direction by calling
  node.setScaleX(2);
  node.setTranslateY(42)
Unfortunately, these convenience methods do not accumulate as one would expect. Instead of multiplying the matrices for subsequent transformations, the only set specific entries in the matrix. As a result, translating an object before rotating yields the same result as applying these transformations in reverse order. This is mathematically wrong and does also not match the users expectations.

In my diagram editor application, I want to scroll (translate), zoom (scale) and rotate the canvas using mouse gestures. From the user's point of view it's imperative that each transformation builds on the previous state. The convenience methods don't match this usecase.

The correct solution to this problem is to multiply the transformation matrices. Unfortunately, JavaFX lacks any kind of calculation API for Transform and its subclasses.

Once again, this is where Xtend comes to our aid. An extension method in Xtend can be used to define functions for existing (closed) types, which syntactically look like being methods of the type on the caller's side. Affine is a subtype of Transform that allows its matrix entries to change. So I wrote extension methods to translate, rotate, scale and shear an existing Affine by multiplying the respective transformation matrix, e.g.
class TransformExtensions {
  def static scale(Affine it, double x, double y) {
    // left multiply a scale matrix to it
    // highly optimized as there are many zeros in the scale matrix
    mxx = x * mxx 
    xy = x * mxy 
    mxz = x * mxz
    tx = x * tx // take existing translation into account 

    myx = y * myx
    myy = y * myy
    myz = y * myz
    ty = y * ty 
 }
}
Importing these as extensions
import static extension ...TransformExtensions.*
now allows to accumulate the transformations, e.g.
val diagram = scene.root
val diagramTransform = new Affine
diagram.transforms.clear
diagram.transforms += diagramTransform

val EventHandler scrollHandler = [
  diagramTransform.translate(deltaX, deltaY)
] 
scene.onScrollStarted = scrollHandler 
scene.onScroll = scrollHandler
scene.onScrollFinished = scrollHandler
  
val EventHandler rotateHandler = [
  diagramTransform.rotate(angle, sceneX, sceneY)
] 
scene.onRotationStarted = rotateHandler
scene.onRotate = rotateHandler
scene.onRotationFinished = rotateHandler
...
The resulting behavior looks like the following screenshot. Note that the mouse position is the pivot for rotations and zoom. 

video 

The same mechanism can be used to properly place labels along connections:

PS: I went for mutable transformation matrices, but the same mechnism will hold for immutable matrices. The extension methods then return a new Affine instead of modifying the receiver.

Tuesday, January 1, 2013

JavaFX Loves Xtend

Inspired by a talk by Gerrit Grunwald and the work of Tom Schindl I started to dig a bit deeper into JavaFX. Being one of the committers of the Xtend language, it was a matter of honour to use Xtend in my experiments. The result: JavaFX and Xtend seem to be made for each other.

If you know me a bit, you won't be surprised that I tried implementing a simple graph editor first. I will blog on that particular application in a separate post. Even though JavaFX is not primarily a graphical editing framework, it offers a lot of the required features, e.g.
  • State of the art rendering and CSS styling
  • A life scene-graph with affine transformations in all nodes
  • An easy way to do data-binding and event propagation
  • Easy to use APIs for effects and animations
  • Built-in support for tablets and touch-aware devices
The developers of JavaFX did a pretty good job in creating a clear Java API, but sometimes Java's rigid syntax was a bit in their way. This is where Xtend comes into play. The resulting code is astonishingly short and easy to read.

Lambda Expressions For Event Listeners

Each JavaFX Node has a couple of convenience methods to react on on UI events, such as mouse clicks, key strokes or multi-touch gestures. A mouse click can for example be handled as
Node node = new Rectangle();
node.setOnMouseClicked(new EventHandler<MouseEvent>() {
   @Override
   public void handle(MouseEvent event) {
      System.out.println(event.getButton() + " button clicked");  
  }
});
Xtend's lambda expressions (aka closures) are automatically coerced to interfaces with a single method. Using the implicit parameter it and the shortcut syntax for property access the above becomes
val node = new Rectangle
node.onMouseClicked = [
   println(button + " button clicked")
]

The With Operator => Obsoletes Builders

JavaFX has a couple of objects that need quite a few parameters to be customized. Instead of implementing a new constructor with a huge number of parameters for each use case, the JavaFX developers generated fluent builder APIs, e.g.
Rectangle rectangle = RectangleBuilder.create()
  .width(80)
  .height(30)
  .fill(Color.BLUE)
  .stroke(Color.RED)
  .strokeWidth(1.2)
  .arcWidth(12)
  .arcHeight(12)
  .build();
In Xtend, we have the with operator '=>' that binds the preceding argument to the parameter of a lambda expression and executes the latter. So we can achieve the same as above with an even shorter syntax and without the need for an accompanying builder class:
val rectangle = new Rectangle => [
   width = 80
   height = 30
   fill = Color::BLUE
   stroke = Color::RED
   strokeWidth = 1.2
   arcWidth = 12
   arcHeight = 12
]
The with operator also facilitates the creation of object trees, e.g. subtrees of JavaFX's scenegraph.

Extension Methods For High-Level Property Binding

In JavaFX, properties can be derived from other properties by means of another fluent API for the calculation. The result is the best you can achieve with pure Java, e.g. given two DoubleProperties a and b, you can bind a mean value property which will be automatically updated when a or be change:
mean.bind(a.add(b).divide(2));
If these calculations get more difficult and you do a lot of them it will get quite unreadable. This is the right moment to think about overloaded operator extensions for DoubleProperties. With these, the last line can become as simple as
mean << a + b / 2
Extension methods and operator overloading can also be used to add the missing APIs for geometry calculation, e.g. to apply a Transform to a Point3D or to accumulate Transforms.