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;
         }
    }
}
Comments:

That's really really interesting. The result is very neat and has a "pro" look. Adding or substracting shapes really seems to be very simple.

I'm not a Flash expert, but creating custom components with behavior (and really being able to reuse them) is much more complex in XUL (XBL is not so simple to use and understand). I'm not speaking of Javascript where it is really a nightmare ;-)
I guess that, in JavaFX, merging declarative programming and logic is "natural". I'm sure the ability to bind everything is one of the keys to this simplicity.

Posted by Hervé on January 08, 2009 at 06:02 AM PST #

You've got to get rid of that awful public/private stuff out of the language.

And case sensitivity if you can.

Posted by Cas on January 18, 2009 at 12:33 AM PST #

You've got to get rid of that awful public/private stuff out of the language.

And case sensitivity.

Posted by Cas on January 18, 2009 at 05:40 AM PST #

http://www.Sohbetizm.Net
thank you very much.. very good index.

Posted by çet on May 17, 2009 at 09:15 PM PDT #

http://www.smsmatbaa.com

Posted by matbaa on June 22, 2009 at 03:01 AM PDT #

important and awake! great job bro thanks.

Posted by Egitim on December 11, 2010 at 06:21 AM PST #

Simple and Nice example !

Posted by شات on December 15, 2010 at 03:56 AM PST #

Post a Comment:
  • HTML Syntax: NOT allowed
About

user12610627

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today