Chris Oliver's Weblog

Tuesday Jan 13, 2009

Collada MoonBuggy in JavaFX

Collada MoonBuggy with skin animation in JavaFX.

In the scripting API for this scene graph, animated Collada models are Nodes which are also polymorphic with Timelines. You can "play" them. Relevant code:


var model = Model {
    url: "lunar_lander_tris.dae"
    repeatCount: Timeline.INDEFINITE
}

model.play();

var zoom = 0.0;

var scene = Scene {
   background: BLACK;
   camera: PerspectiveCamera {
       heightAngle: 15
       nearDistance: 1
       farDistance: 1000
       position: bind Transform.translate(0, 0, 30+zoom);
   }
   lights: [...]
   content: model

}

...

Thursday Jan 08, 2009

Simple yet elegant vector user interfaces in JavaFX 1.0

It's very easy to create simple yet elegant custom vector user interface elements in JavaFX 1.0 by means of simple compositions of basic shapes. The above example consists entirely of compositions of simple triangles and (rounded) rectangles, together with some text.

The outer shell is a round rectangle from which two other round rectangles have been "subtracted", one for the control area, and one for the track of the slider. Behind this shape is a semi-transparent round rectangle of the same size. Due to the background color of the scene in the screenshot, you can't really tell, but the result is that you can partially "see through" these areas.

The "play", "back", and "forward", buttons are composed of a single triangle or two "added" together. The "pause" button consists of two rectangles "added" together. Finally, the thumb on the slider is simply a rectangle that's been rotated.

In JavaFX 1.0, you can declaratively compose vector shapes by means of the ShapeSubtract node. Although it's my personal opinion that this API element is poorly named and its member variables (a and b) overly obscure, nevertheless it's good enough to get the job done for now.

The a instance variable of ShapeSubtract takes a list of shapes which will be added together. Its b instance variable takes a list of shapes which will then be subtracted from that. ShapeSubtract is itself a shape and may be used in a larger composition.

Using JavaFX script, it's then very easy to factor such into reusable custom scene graph elements, and to make them interactive and/or animated.

Below is the full source code for the example.

/*
 * Main.fx
 *
 */

package moviecontrol;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.text.*;
import javafx.scene.*;
import javafx.scene.shape.*;
import javafx.scene.transform.*;
import javafx.scene.paint.*;
import javafx.scene.paint.Color.*;
import javafx.scene.input.*;
import java.lang.Math;
import java.lang.System;
import javafx.animation.*;

def defaultFillColor = Color.color(.8, .8, .8, 1);
def selectedFillColor = WHITE;

class MovieButton extends CustomNode {

    // interface
    public var action: function():Void;
    public var icon: Shape;
    public var selectedIcon: Shape;
    public var selected: Boolean;


    // implementation
   
    var mouseOver: Boolean = bind hover;
    var mousePress: Boolean = false;
    var fillColor = bind if (mouseOver and mousePress) selectedFillColor else defaultFillColor;
    var path = bind
        if (selected and selectedIcon != null)
        ShapeSubtract { fill: bind fillColor, a: selectedIcon }
        else
        ShapeSubtract { fill: bind fillColor, a: icon };
    
   
    override protected function create():Node {
        Group {
            // center it 
            translateX: bind -path.boundsInLocal.width / 2;
            translateY: bind -path.boundsInLocal.height / 2;
            // mouse behavior
            onMouseReleased: function(e) {
                if (mouseOver) {
                    if (action != null) action();
                    selected = not selected;
                }
                mousePress = false;
            }
            onMousePressed: function(e) {
                mousePress = true;
            }
            // make an internal scene consisting of the icon shape
            // and an invisiable rectangle bounding it (so mouse
            // events anywhere within its bounding box are
            // accepted
            content:
            [Rectangle { 
                height: bind path.boundsInLocal.height;
                width: bind path.boundsInLocal.width;
                opacity: 0;
                fill: Color.BLACK;
            },
            Group {
                content: bind path;
            }];
        }
    }
}


class MovieControl extends CustomNode {

    public var back: function():Void;
    public var fwd: function():Void;
    public var paused: Boolean;
    public var loaded: Duration;
    public var setPosition: function(pos:Duration):Void;

    public var duration: Duration = 0s on replace {
        updateAlpha();
    }

    public var position: Duration  = 0s on replace {
        updateAlpha();
    }

    function updateAlpha():Void {
        if (duration != null and position != null and duration != 0s) {
            positionAlpha = position.toMillis() / duration.toMillis();
        }
    }

    var positionAlpha: Number;

    override protected function create():Node {
        Group {
            translateX: -150;
            translateY: -32;
            var bg:Rectangle;
            content:
            [bg = Rectangle { // semi-transparent background
                height: 64;
                width: 300;
                arcHeight: 20;
                arcWidth: 20;
                fill: Color.color(0, 0, 0, 0.2);
            },
            ShapeSubtract { // subtract the control area and slider track from the main body
                fill: defaultFillColor;
                a: Rectangle {
                    height: 64;
                    width: 300;
                    arcHeight: 20;
                    arcWidth: 20;
                }
                b:
                [Rectangle {
                   x: 1;
                   y: 1;
                   arcHeight: 20;
                   arcWidth: 20;
                   width: 298;
                   height: 48;
                },
                Rectangle {
                    x: 50;
                    y: 50;
                    height: 13;
                    width: 200;
                    arcHeight: 13;
                    arcWidth: 13;
                }]
             },
             Group { // place the text for the elapsed time and duration
                translateY: 52;
                var font = Font {size: 11};
                content:
                [Text {
                   x: 10;
                   y: 0;
                   textOrigin: TextOrigin.TOP
                   font: font;
                   fill: BLACK;
                   content: bind "{%tM position}:{%tS position}";
               },
               Text {
                  x: 254;
                  y: 0;
                  textOrigin: TextOrigin.TOP
                  font: font;
                  fill: BLACK;
                  content: bind if (duration == null or position == null) "" else "-{%tM duration.sub(position)}:{%tS duration.sub(position)}";
               }]
             },
             Group { // handle the slider thumb
                 var thumbX: Number = bind positionAlpha * 190;
                 translateX: bind 51 + thumbX;
                 translateY: 52.5;
                 var thumb: Rectangle;
                 var startX = 0.0;
                 onMousePressed: function(e) {
                     startX = thumbX;
                 }
                 onMouseDragged: function(e) {
                     var x = startX + e.dragX;
                     x = Math.max(Math.min(x, 190), 0);
                     positionAlpha = x / 190;
                     if (setPosition != null) { setPosition(position); };
                 }
                 content: thumb = Rectangle {
                     var c = 8.0;
                     transforms: Transform.rotate(45, c/2, c/2);
                     height: c;
                     width: c;
                     var thumbMousePress = false;
                     onMousePressed: function(e) {
                         thumbMousePress = true;
                     }
                     onMouseReleased: function(e) {
                         thumbMousePress = false;
                     }
                     fill: bind if (thumbMousePress) selectedFillColor else defaultFillColor;
                 }
             },
             Group { // construct the various buttons
                 translateX: 100;
                 translateY: 24;
                 // functions for basic shape elements that
                 // are composed below
                 var u = 16.0;
                 var bar = function() {
                     Rectangle {
                        height: u;
                        width: u/3
                     }
                 };
                 var leftArrow = function() {
                     Polygon {
                        points: [0, u/2, u, 0, u, u];
                     }
                 };
                 var rightArrow = function() {
                     Polygon {
                         points: [0, 0, u, u/2, 0, u];
                     }
                 }
                 var backIcon = function() {
                     ShapeSubtract {
                         a:
                         [leftArrow(),
                          ShapeSubtract {
                             translateX: u;
                             a: leftArrow()
                         }]

                     }
                 };
                 var fwdIcon = function() {
                     ShapeSubtract {
                         a: [rightArrow(),
                             ShapeSubtract {
                                   translateX: u;
                                   a: rightArrow()
                             }];

                    }
                 };
                 var playIcon = function() {
                     ShapeSubtract {
                         transforms:
                         [Transform.scale(1.5, 1.5)];
                         a: rightArrow();
                     }
                 };
                 var pauseIcon = function() {
                     ShapeSubtract {
                         transforms:
                         [Transform.scale(1.5, 1.5)];
                         a:
                         [bar(), ShapeSubtract { translateX: u/2; a: bar()}];
                     }
                 };
                 content:
                 Group {
                     var buttons = 
                     [MovieButton {
                         icon: backIcon();
                         action: bind back;
                     },
                     MovieButton {
                         icon: pauseIcon();
                         selected: bind paused with inverse;
                         selectedIcon: playIcon()
                     },
                     MovieButton {
                         icon: fwdIcon();
                         action: bind fwd;
                     }];
                     content: for (i in buttons)
                     Group {
                         translateX: indexof i * 42;
                         content: i;
                     }
                 }
             }]
         }
    }
}




/**
 * @author coliver
 */

// As a test simulate playing movies with a timeline


var duration = 5m;

function reset():Void {
    simulator.stop();
    paused = true;
}

var simulator = Timeline {
    keyFrames:
    KeyFrame {
        time: duration
    }
    repeatCount: Timeline.INDEFINITE;
};

var paused = true on replace {
    if (paused) { simulator.pause() } else { simulator.play() }
}


Stage{
    title: "Movie Control"
    width: 500
    height: 400

    scene: Scene{
         fill: BLACK;
         content: MovieControl {
             translateX: 250
             translateY: 180
             setPosition: function(pos:Duration) {               
                 simulator.time = pos;
             }
             fwd: reset
             back: reset
             paused: bind paused with inverse;
             duration: bind duration;
             position: bind simulator.time with inverse;
         }
    }
}

Wednesday Jan 07, 2009

JavaFX Script Keywords and Java Interoperability

In JavaFX script any sequence of characters enclosed in "french quotes" is treated as a lexical identifier, and thus may be used as a valid name of a variable, function, or class.

  var <<this is a variable>> = "Hello World";
  <<this is a variable>>.toUpperCase();

This mechanism may be used to access Java methods which conflict with JavaFX keywords, for example:

  var textArea = new javax.swing.JTextArea();
  textArea.<<insert>>("Hello World", 0);

This mechanism is also useful for code generators in translating symbols from other languages having incompatible lexical rules to JavaFX script.

Monday Jan 05, 2009

From F3 to JavaFX 1.0 - Effects

An important and impressive innovation between F3 and JavaFX is the Effects framework created by Chris Campbell.

F3 had a simple system of software pixel filters, which could be applied to any Node or group of Nodes in a scene. However, thanks to Chris, JavaFX 1.0 includes a much more complete set of effects, and a sophisticated framework that enables GPU hardware acceleration where available.

Underlying the simple declarative expression of effects at the JavaFX script level, effect implementations are described in a GPU-shader-like procedural language, which Chris created, called JSL. Chris's JSL compiler then compiles to various targets, either GPU-based (GLSL/HLSL), or CPU-based (Java/Native).

Friday Jan 02, 2009

Performance matters - 25x for JavaFX script over Groovy and JRuby

JavaFX script


function tak(x:Number, y:Number, z:Number): Number {
    if (y >= x) z else tak(tak(x-1, y, z),
                           tak(y-1, z, x),
                           tak(z-1, x, y));
}

for (i in [1..1000]) {
    tak(24, 16, 8);
}
time javafx -server -cp . Tak

real    0m10.724s
user    0m10.105s
sys     0m0.173s
Groovy
def tak(double x, double y, double z) {
    return y >= x ? z : tak(tak(x-1, y, z),
                            tak(y-1, z, x),
                            tak(z-1, x, y));
}


int i = 0;
while (i++ < 1000) {
    tak(24, 16, 8);
}

time java -Djava.ext.dirs=./groovy-1.6-RC-1/lib -server groovy.lang.GroovyShell tak.groovy

real    4m36.674s
user    4m29.272s
sys     0m3.842s

JRuby


def tak x, y, z
  unless y < x
    z
  else
    tak( tak(x-1, y, z),
         tak(y-1, z, x),
         tak(z-1, x, y))
  end
end

i = 0
while i<1000
        tak(24, 16, 8)
        i+=1
end

time ./jruby-1.1.6RC1/bin/jruby -J-server tak.rb

real    4m24.735s
user    4m22.203s
sys     0m1.069s

Summary

For this benchmark, as you can see both JRuby and Groovy are around 25x slower than JavaFX script.

Calendar

Feeds

Search

Links

Navigation

Referers