Angular shitstorm: What's your opinion on the controversial plans for Angular 2.0?
Java legacy

Tutorial: A glimpse at JavaFX’s Canvas API

CarlDea
Deateaser

Appearing in November’s JAX Magazine, Carl Dea clocks some hours with Java’s rich client technology, JavaFX.

Appearing in November’s JAX Magazine, Carl Dea clocks some hours with Java’s rich client technology, JavaFX.

I recently attended the JavaOne 2012 conference hosted by Oracle this past October. My main goal was to focus on all things relating to client-side Java, even if the sessions didn’t contain “JavaFX” in the title. To my surprise, I received much more than I bargained for. I attended sessions involving technologies such as Dolphin, Griffon, Java Embedded, JavaFX 8, etc.

Figure 1: Tron Legacy style clocks with JavaFX

I couldn’t help but drool over the new JavaFX 8‘s 3D APIs. However, another one of my intentions was to look at the many new features that were added to the current release of JavaFX 2.2. In particular, one feature that comes to mind is the highly anticipated Canvas API. In this article, you will learn about some of the performance characteristics when using JavaFX’s Canvas API and later, I will share some code and teach you how to build an animated clock using only the Canvas API on JavaFX’s scene graph as opposed to the vector based counterparts (JavaFX 2.1 and earlier).

Of the many JavaFX sessions at the JavaOne 2012 conference, one session, in particular, which stood out was (CON6784 – JavaFX Graphics Tips and Tricks) with Richard Bair. Mr. Bair, a JavaFX Architect, discussed JavaFX 2 and JavaFX 8’s graphic rendering performance in comparison with all of the major browsers on popular desktop operating systems (Windows, Linux/Ubuntu, and Mac OS X). The benchmark (following the GUIMark 2) results were categorized by ‘Vector’ and ‘Bitmap’ graphics rendering strategies. Familiar to most HTML5 Web developers, using the ‘Vector’ rendering strategy is simply synonymous to the SVG (XML representation of scalable vector graphics). In addition to this, the ‘Bitmap’ rendering strategy is equivalent to the use of the popular HTML5 Canvas API using the JavaScript language.

When involving the vector rendering strategy, the performance numbers showed that JavaFX had outperformed all of its competitors with the exception of the Safari browser on Mac OS X. Similarly, when involving the bitmap rendering strategy, the performance metrics indicate that the JavaFX Canvas API outperformed all the browsers by a significant amount. Mr. Bair did a great job in explaining when, why, and how to choose between the two rendering strategies. Mr. Bair also points out that, in some scenarios, JavaFX vector graphics can even outperform JavaFX canvas. He also suggested that, when using one technique over the other, the only way to really get a more accurate picture is to profile the application. Of course, much more was discussed in his talk. However, in this article I merely wish to point out the relevant bits of information relating to the Canvas API.

In the world of JavaFX, a scene graph is mainly based on the ‘Vector’ rendering strategy which calculates shapes and paths as scene graph nodes which are easily scaled. For those developers who recall prior to JavaFX 2.2, you will remember when there was not an ability to handle bitmap graphics (pixel pushing). You are probably wondering, “How do you render using the ‘Bitmap’ strategy onto the JavaFX vector based scene graph?” Since the release of JavaFX 2.2, the JavaFX team created the Canvas API. The Canvas API is actually a JavaFX node called the Canvas class. Since the Canvas class extends from javafx.scene.Node class, it is basically a scene graph node like any other JavaFX node, except you have the ability to draw directly and manipulate pixel data using graphics primitives (Canvas API) onto the surface (GraphicsContext).

How do you draw on the Canvas node?

To draw on a Canvas, you must first instantiate a Canvas node with a width and height. After instantiating a Canvas class, you will need to obtain its GraphicsContext object. To obtain the GraphicsContext, you simply invoke its getGraphicsContext2D() method. The GraphicsContext class has methods enable the developer to access many of the graphics drawing primitives. These drawing primitives provide ways to draw shapes, fill colors, set strokes, apply images, draw text, and much more. Please refer to the JavaDoc for details relating to the GraphicsContext class. Listing 1 demonstrates the basics of drawing onto the GraphicsContext.

Canvas canvas = new Canvas(300, 300);
final GraphicsContext gc = canvas.getGraphicsContext2D();
gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());

gc.setFill(Color.BLACK);
gc.setFont(Font.getDefault());
gc.fillText("hello   world!", 15, 50);

gc.setLineWidth(5);
gc.setStroke(Color.PURPLE);

gc.strokeOval(10, 60, 30, 30);
gc.strokeOval(60, 60, 30, 30);
gc.strokeRect(30, 100, 40, 40);

root.getChildren().add(canvas);
primaryStage.setScene(scene);
primaryStage.show();

 

Figure 2: The basics

When creating an instance of a Canvas class, you will have an opportunity to place it into the scene graph like any other JavaFX node. Since Canvas is a JavaFX Node class, the developer (user of the API) is free to treat it like any other node in the scene graph. This means that you have the ability to apply effects, transforms, and animations. One of the main advantages of the Canvas API is that you can draw as many things on the graphic context (bitmap surface) which is similar to the vector rendering strategy.

However, instead of many nodes on the scene graph, all of the drawings on the canvas are encapsulated in one node, thus increasing performance. Remember, cutting down on the number of node objects on the scene graph helps to achieve better efficiencies such as graph walking.

A downside to using the Canvas API is that the developer must draw things from scratch and handle efficiencies on their own (such as calculating dirty regions for optimization). This is why, in some cases, JavaFX’s vector based strategy is able to excel in performance. These advancements in performance are due to its ability to detect dirty regions, offer caching hints and perform other tricks under the covers. Everything boils down to what you are really trying to achieve and the desired effect.

If you’ve dabbled with HTML5 development, you’ll notice JavaFX’s Canvas API was designed to look similar to HTML5’s Canvas API (no coincidence). I was a bit surprised that the programming interfaces didn’t follow the Java 2D API (of which I’m a big fan). However, I quickly got over it. Many of the popular HTML5/JavaScript examples on the net can be easily ported to the JavaFX Canvas API because of their similar naming conventions. (Note: In order to use the Canvas API, you must install JavaFX 2.2 or a later version.)

With all the excitement surrounding JavaFX’s Canvas API, I decided to take it out for a spin. Since JavaFX’s Canvas API has so many features, I will only give you a quick glimpse by presenting a futuristic stylized clock reminiscing of aspects of the movie Tron Legacy. The clock was inspired by a user named Burnwell88 at DeviantArt. The clock was developed using the Rain Meter desktop theme maker. In my concluding statements, I will list some of the many features related to the Canvas API that won’t be discussed in this article.

Before we jump into the code, I want to show you a simple design of the clock and also provide a class diagram depicting the classes used in the demo. Below depicts a mockup of the parts that comprise the arc clock:

 

Figure 3: A visual design mockup comprised of the parts of a clock

Figure 3 describes the parts that comprise our demo ‘arc clock’ consisting of the following: An arc (ArcPiece), hours, minutes, seconds and meridian respectively (from left to right). Now that we visually understand what pieces make up an arc clock, let me show you a class diagram listing all of the classes we need to capture the requirements for the demo. Figure 4 lists the four classes used in this clock demo.

Figure 4: ArcClock Demo Class Diagram

The left side of the class diagram in Figure 4 show two main classes used to draw a clock using arcs. The ArcClock class is a container-type class which holds anywhere from zero to many ArcPiece instances. An ArcClock also contains the clock’s radius and diameter (bounding box width of the clock). The individual arc objects xArcPiece) will contain the arc attributes which will be later rendered onto the canvas with each update and render cycle. In Figure 3, the ArcPieceBuilder class (upper right) is a convenience class that follows an object oriented builder pattern. Using this class provides an easy way to construct an ArcPiece instance in declarative syntax style programming. This pattern enables the developer to reduce boilerplate code and allows ad hoc parameters to be specified (method chaining).

Table 1 displays all the attributes which describes an ArcPiece class:

Attribute Data Type Example Description
x double 0 Upper left x coordinate of the bounding box
y double 0 Upper left y coordinate of the bounding box
w double 200 Width of the bounding box surrounding clock
h double 200 Height of the bounding box surrounding clock
startAngle double 45 Starting angle of the arc in degrees measure
arcExtent double 240 Extent angle in degrees measure
strokeWidth double 5 The pixel thickness of the stroke of the arc line
pixelsToMove double 1 Number of pixels to animate arc appearing to rotate
strokeColor Color Color.RED The color of the stroke
clockwise Boolean true Animate arc to move clockwise otherwise counter clockwise
Table 1: The attributes contained in the ArcPiece class

For brevity‘s sake, I felt it was better to expose attributes as public and not to implement getter and setter methods which are similar to the Java bean specification (convention). Also, I chose to use simple primitive data types instead of JavaFX Properties APIs. Using primitives can help lower overhead (the need to bind against many values). At some point, you may want to run the demo with an embedded version of Java on a more constrained device such as Raspberry Pi, BeagleBoard, etc.

Now that you know the attributes to draw and arc, let’s see what it would look like. Figure 5 depicts an arc drawn using the attributes x, y, w, h, startAngle, arcExtent, strokeColor, and ArcType. For our demo, I have hard-coded all the arcs to be ArcType.OPEN.

Figure 5 shows the information of an arc as it is to be drawn on the canvas. 

Figure 5: Arc Anatomy

The listing below shows the code snippet used to draw an arc onto the GraphicsContext.

final GraphicsContext gc = canvas.getGraphicsContext2D();
...
gc.setStroke(strokeColor);
gc.setLineWidth(strokeWidth);
gc.strokeArc(x, // upper left x coord   
        y,              // upper left y coord
        w,              // width
        h,              // height
        startAngle,
        arcExtent,
        ArcType.OPEN);

The main driver or JavaFX Application which runs our demo is housed in the TronClockDemo class (Listing 3). This main driver class contains an AnimationTimer instance which will periodically update the arc clock’s attributes and draw the parts onto the GraphicsContext surface.

The listing below shows the main driver class, TronClockDemo.java, and launches the JavaFX application: 

/** Driver class to run the demo which extends from
 * JavaFX's Application class. The demo will make use of
 * JavaFX's Canvas API. This class creates three clocks
 * which are animated using an AnimationTimer class.
 *
 * Inspired by http://burnwell88.deviantart.com/art/Clock-136761577
 * and http://rainmeter.net/cms/
 *
 * Created with IntelliJ IDEA.
 * User: cdea
 * Date: 10/15/12
 * Time: 12:20 AM
 *
 */
public class TronClockDemo extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(final Stage primaryStage) {

        Group root = new Group();
        Scene scene = new Scene(root, 650, 220, Color.rgb(0,0,0));

        // create a canvas node
        Canvas canvas = new Canvas();

        // bind the dimensions when the user resizes the window.
        canvas.widthProperty().bind(primaryStage.widthProperty());
        canvas.heightProperty().bind(primaryStage.heightProperty());

        // obtain the GraphicsContext (drawing surface)
        final GraphicsContext gc = canvas.getGraphicsContext2D();

        // create three clocks
        final ArcClock blueClock = new ArcClock(20, BLUE1, BLUE2, 200);
        final ArcClock greenClock = new ArcClock(20, BLUE1, GREEN1, 200);
        final ArcClock redClock = new ArcClock(20, BLUE1, RED1, 200);

        // create an animation (update & render loop)
        new AnimationTimer() {
            @Override
            public void handle(long now) {
                // update clocks
                blueClock.update(now);
                greenClock.update(now);
                redClock.update(now);

                // clear screen
                gc.clearRect(0, 0, primaryStage.getWidth(),
                        primaryStage.getHeight());

                // draw blue clock
                blueClock.draw(gc);
                // save the origin or the current state
                // of the Graphics Context.
                gc.save();

                // shift x coord position the width of a clock plus 20 pixels
                gc.translate(blueClock.maxDiameter + 20, 0);
                greenClock.draw(gc);

                // shift x coord position past the first clock
                gc.translate(blueClock.maxDiameter + 20, 0);
                redClock.draw(gc);

                // reset Graphics Context to last saved point.
                // Translate x, y to (0,0)
                gc.restore();

            }
        }.start();

        // add the single node onto the scene graph
        root.getChildren().add(canvas);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

}

 

Initially, the application’s main method will launch the JavaFX application thread via the Application.launch() method. Once the application thread is ready (post init() method), the start() method gets invoked. To see other lifecycle methods, go to the JavaDocs on Application class . You’ll notice in Listing 3 that all the action occurs in the start() method. This is where an AnimationTimer instance is created using an anonymous inner class and started using the AnimationTimer.start() method. As each cycle occurs, the handle() method is invoked by the JavaFX Application thread. When invoking the handle() method, you’ll notice the inbound parameter ‘now’ of type long is the current time in nanoseconds for one frame.

The body of the handle() method will take the following steps:

  1. Update clock models (blue clock, green clock, and red clock)

  2. Clear screen

  3. Draw blue clock

  4. Save GraphicsContext state

  5. Translate X coordinates to the right of the blue clock

  6. Draw the green clock

  7. Translate X coordinates to the right of the green clock

  8. Draw the red clock

  9. Restore or return to the last save GraphicsContext state

The listing below shows the source code for a simple Java class, which is responsible for containing an arc’s model.

 

/** ArcPiece is an object representing the state (model)
 * of an arc shape that will be drawn on the Graphics Context. During
 * an animation loop values in the model will often be
 * updated. The update() method has the ability to calculate
 * elapsed time to allow the arc to later be animated based on
 * frames per second. The draw() method simply renders the arc
 * shape onto the Graphics Context (surface).
 *
 * Created with IntelliJ IDEA.
 * User: cdea
 */
public class ArcPiece {
    public double x;
    public double y;
    public double w;
    public double h;
    public double startAngle;
    public double arcExtent;
    public double strokeWidth = 2;
    public double pixelsToMove = 2;
    public Color strokeColor;
    public boolean clockwise=false;

    long startTime = 0;
    public long displayTimePerFrameMillis = 60;
    private long displayTimePerFrameNano = 60  * 1000000;

    public void update(long now) {
        if (startTime == 0){
            startTime = now;
            displayTimePerFrameNano = displayTimePerFrameMillis * 1000000;
        }

        long elapsed = now - startTime;
        if (elapsed > displayTimePerFrameNano) {
            if (!clockwise){
                startAngle = startAngle + pixelsToMove;
                if (startAngle > 360){
                    startAngle = 0;
                }
            } else {
                startAngle = startAngle - pixelsToMove;
                if (startAngle < -360){
                    startAngle = 0;
                }
            }
            startTime = 0;
        }
    }

    public void draw(GraphicsContext gc) {
        gc.setStroke(strokeColor);
        gc.setLineWidth(strokeWidth);
        gc.strokeArc(x,
                y,
                w,
                h,
                startAngle,
                arcExtent,
                ArcType.OPEN);
    }
}

This listing also contains two methods update() and draw()which will assist the animation cycle to update the values and render the arc onto the drawing surface (GraphicsContext).

The next listing displays the source code for the ArcClock.java file which is the container class representing the model of a clock.

/** This class is a container class that maintains
 * all of the parts that comprised of a clock. It is
 * also responsible for updating and drawing itself
 * at each animation frame cycle.
 * The ArcClock contains zero to many ArcPiece objects
 * to be drawn. The clock will also display the time
 * of day.
 *
 * User: cdea
 */
public class ArcClock {
    public static Color BLUE1 = Color.rgb(126, 166, 212, 0.6);
    public static Color BLUE2 = Color.rgb(126, 166, 222, 0.5);
    public static Color BLUE3 = Color.rgb(130, 166, 230, 0.5);
    public static Color GREEN1 = Color.rgb(130, 230, 166, 0.5);
    public static Color RED1 = Color.rgb(230, 130, 166, 0.5);

    public ArcPiece longPiece;
    public ArcPiece[] arcPieces;
    public int maxDiameter;
    public double radius;

    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMddhhmmssa");

    public ArcClock(int numArcs, Color longPieceColor, Color manyPieceColor, int maxDiameter) {
        this.maxDiameter = maxDiameter;
        radius = maxDiameter / 2;
        longPiece = ArcPieceBuilder.create()
                .strokeColor(longPieceColor)
                .strokeWidth(5)
                .x(0)
                .y(0)
                .w(maxDiameter)
                .h(maxDiameter)
                .startAngle(45)
                .arcExtent(240)
                .displayTimePerFrameMillis(1000)
                .pixelsToMove(1)
                .build();

        arcPieces = createRandomArcs(numArcs, manyPieceColor, maxDiameter / 2);
    }

    public void update(long now){
        longPiece.update(now);
        for (ArcPiece ap:arcPieces) {
            ap.update(now);
        }
    }

    public void draw(GraphicsContext gc) {
        longPiece.draw(gc);
        for (ArcPiece ap:arcPieces) {
            ap.draw(gc);
        }
        // draw hour
        gc.setFont(Font.font("Calibri", 40));
        gc.setFill(Color.WHITE);
        gc.setTextAlign(TextAlignment.CENTER);

        String dateTimeStr = DATE_FORMAT.format(new Date());
        //yyyyMMddhhmmssa
        gc.fillText(dateTimeStr.substring(8, 10) , radius, radius + 18 );
        gc.setFont(Font.font("Calibri", 20));
        gc.fillText(dateTimeStr.substring(10, 12) + " " + dateTimeStr.substring(14) , maxDiameter - 40, radius - 40 );
        gc.fillText(dateTimeStr.substring(12, 14) , maxDiameter - 40, maxDiameter - 40 );

    }

    public static int randomIntRange(int min, int max) {
        Random rand = new Random();
        int range = max - min + 1;
        return rand.nextInt(range) + min;
    }

    public static ArcPiece[] createRandomArcs(int num, Color color, double radius) {
        final ArcPiece[] manyPieces = new ArcPiece[num];
        for (int i=0; i<num; i++) {
            manyPieces[i] = randomArcPiece(color, radius);
        }
        return manyPieces;
    }
    public static ArcPiece randomArcPiece(Color color, double radius) {

        int width =  randomIntRange(60, (int) radius * 2);
        int randomStrokeWidth = randomIntRange(1,10);
        int randomStartAngle = randomIntRange(1, 270);
        int randomExtentAngle = randomIntRange(10, 360-randomStartAngle);
        long randomMillis = randomIntRange(0, 33);
        Color someColor = color;
        if (color == null) {
            someColor =  BLUE1;
        }
        final ArcPiece arcPiece = ArcPieceBuilder.create()
                .strokeColor(someColor)
                .strokeWidth(randomStrokeWidth)
                .x(radius - (width/2))
                .y(radius - (width/2))
                .w(width)
                .h(width)
                .startAngle(randomStartAngle)
                .arcExtent(randomExtentAngle)
                .displayTimePerFrameMillis(randomMillis)
                .pixelsToMove(2)
                .build();
        arcPiece.clockwise = new Random().nextBoolean();

        return arcPiece;
    }
}

This listing begins with the predefined colors that will be used in our three demo clocks. The instance variables are the following: longPiece, arcPieces, maxDiameter, and radius. Table 2 describes the attributes of an instance of an ArcClock class. 

 

Attribute Data Type Example Description
longPiece ArcPiece   A single open arc drawn on the outer rim of the clock
arcPieces ArcPiece   Many randomly drawn open arcs in the clock
maxDiameter int 200 Width of the clock
radius double 100 Half width of the clock

To create an instance of an ArcClock, the constructor will call many static methods to build random open arcs shapes with varied start angles and extent angles. Another thing to point out is the update() and draw() methods which are used to assist in the animation cycle (AnimationTimer). At each animation cycle, the clock will update the model and render the arcs onto the graphics context. In addition to rendering all the arc pieces for the arc clock, the ArcClock class will also display (draw) the time using the GraphicsContext’s fillText() method.

Well, there you have it, a quick glimpse of the JavaFX Canvas API. Although we’ve only touched the surface of the capabilities of the Canvas API, I have listed some of the other interesting features of JavaFX 2.2 and the Canvas API.

Snapshot – Any Scene graph node can capture a node’s as a bitmap image.

PixelWriter – Ability to modify pixel information on a bitmap image.

PixelReader – Ability to read pixel information from a bitmap image.

Blending – Blending modes when drawing on the canvas (graphics context).

Layers – Ability to have many graphics contexts to provide layers (z order).

Happy coding! I trust you will dig deeper into these interesting features.

Author
CarlDea
Carl P. Dea is currently a Sr. Software Engineer, co-author of Java 7 Recipes, author of JavaFX 2 Introduction by Example, and technical reviewer of Pro JavaFX 2 from Apress publishing. Carl has been developing software for over 15 years with many clients from fortune 500 companies to nonprofit organizations. He has written software ranging from mission critical applications to e-commerce-based Web applications. Carl has been using Java since the very beginning and he also is a huge JavaFX enthusiast dating back when it used to be called F3. His current software development interests are: UI/UX, game programming, data visualization, microcontrollers, smart phones and tablet computers. When he's not working he and his wife love to watch their daughters perform at gymnastics meets. Carl lives on the East Coast in Pasadena, Maryland USA.
Comments
comments powered by Disqus