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
on
e 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 x
ArcPiece) 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