Thursday Jan 01, 2009

Graphically Challenged? JavaFX to the rescue!

Need nifty icons and buttons for your RIA but can't draw them yourself? If you're like me and

  • want cool looking icons/buttons/graphics for your GUI
  • are quite hopeless at using tools like Adobe® Photoshop®, GIMP
  • waste hours (if not days!) googling for and unsuccessfully trying out  the various Photoshop/GIMP tutorials that are on the Internet
  • can't find somebody willing to do your dirty (graphics) work
  • prefer programming to drawing

then JavaFX might just be what the doctor ordered!

After (quite) a few unsuccessful attempts, I finally succeeded at following the 8 steps in Jesse Norell's well written tutorial on creating 3D buttons using GIMP. When I tried to emulate the same steps using JavaFX, I was surprised by how easy it was to do. Here's what I did to create my first 3D button using JavaFX!

Table 1: Translating GIMP into JavaFX

Step GIMP JavaFX Result

1.

Draw a perfect circle

Group {
    var radius = 40
    content: [
        Circle {
            centerX: 50
            centerY: 50
            radius: radius
            stroke: Color.BLACK
            strokeWidth: 1
            fill: Color.WHITE
        }
    ]
};
RingButton - Step 1

2.

Fade from white to black within the circle using either a linear or a radial fade. At the same time, remove the outline of the circle (strokeWidth: 0).


The code to the right fills the circle with a linear gradient going from the south-west to the north-east.
            strokeWidth: 0
            fill: LinearGradient {
                startX: 0
                startY: 1
                endX: 1
                endY: 0
                stops: [
                    Stop {
                        offset: 0.0
                        color: Color.WHITE
                    },
                    Stop {
                        offset: 1.0
                        color: Color.BLACK
                    }
                ]
            }
RingButton - Step 2

3.

Draw an inner circle and fill it with a linear fade in the opposite direction


Instead of changing the start and end points of the gradient, I've just swapped the start and end colors.
        Circle {
            centerX: 50
            centerY: 50
            radius: radius - 4
            stroke: Color.BLACK
            strokeWidth: 0
            fill: LinearGradient {
                startX: 0
                startY: 1
                endX: 1
                endY: 0
                stops: [
                    Stop {
                        offset: 0.0
                        color: Color.BLACK
                    },
                    Stop {
                        offset: 1.0
                        color: Color.WHITE
                    }
                ]
            }
        }
RingButton - Step 3

4.

Draw another smaller circle and fill it with any colour

        Circle {
            centerX: 50
            centerY: 50
            radius: radius - 8
            stroke: Color.BLACK
            strokeWidth: 0
            fill: Color.BLACK
        }
RingButton - Step 4

5.

Smoothen out the sharp edges in the image

        effect: GaussianBlur {radius: 2}
RingButton - Step 5

That's all there is to creating a spiffy looking graphic using a programming language that is intuitively easy!

Wouldn't it be cool if the 3D ring could be put around an arbitrary line of text or graphic such as an icon? To do so, we'd have to dynamically calculate the radius of the ring based on the size of the object that it "embeds". The rest of this blog describes the implementation of a custom node named SpiffyRing that draws a 3D ring around an user-specified node.

Here's the code for the SpiffyRing node:

package foobar.gui;

import java.lang.Math;
import javafx.scene.CustomNode;
import javafx.scene.effect.GaussianBlur;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;

/\*\*
 \* A class that draws a 3-dimensional ring around a specified node. You can
 \* customize the thickness of the ring, the colors used for the ring itself.
 \*/
public class SpiffyRing extends CustomNode {

    /\*\*
     \* The node around which to draw the ring
     \*/
    public var content: Node;

    /\*\*
     \* Specifies the color used to fill the inside of the ring i.e. the space
     \* between the ring and the embedded content
     \*/
    public var fill: Color = Color.BLACK;

    /\*\*
     \* Specifies the thickness of the ring in pixels.
     \*/
    public var thickness:Integer = 8;

    /\*\*
     \* Specifies the gap (in pixels) between the ring and the embedded content
     \*/
    public var gap:Integer = 2;

    /\*\*
     \* Specifies the two colors to use for coloring the ring itself with a
     \* 3D gradient
     \*/
    public var color1:Color = Color.WHITE;
    public var color2:Color = Color.BLACK;

    // Calculate the radius of the ring using the dimensions of the embedded
    // node as well as the thickness of the ring itself.
    var radius = bind ((Math.hypot(content.boundsInLocal.width, content.boundsInLocal.height))/2 + thickness + gap);

    public override function create(): Node {
        var ring = Group {
            // Center the ring with respect to the embedded node
            translateX: bind content.boundsInLocal.width/2 - 1
            translateY: bind content.boundsInLocal.height/2 - 1

            content: [
                // Draw the outer circle of the ring fading from color1-->color2
                Circle {
                    centerX: 0
                    centerY: 0
                    radius: bind radius
                    stroke: Color.BLACK
                    strokeWidth: 0
                    fill: LinearGradient {
                        startX: 0
                        startY: 1
                        endX: 1
                        endY: 0
                        stops: [
                            Stop {
                                offset: 0.0
                                color: color1
                            },
                            Stop {
                                offset: 1.0
                                color: color2
                            }
                        ]
                    }
                },
                // Draw the inner circle of the ring, fading in the opposite
                // direction
                Circle {
                    centerX: 0
                    centerY: 0
                    radius: bind (radius - thickness/2)
                    stroke: Color.BLACK
                    strokeWidth: 0
                    fill: LinearGradient {
                        startX: 0
                        startY: 1
                        endX: 1
                        endY: 0
                        stops: [
                            Stop {
                                offset: 0.0
                                color: color2
                            },
                            Stop {
                                offset: 1.0
                                color: color1
                            }
                        ]
                    }
                },
                // Create the ring by filling the interior with the specified
                // fill color
                Circle {
                    centerX: 0
                    centerY: 0
                    radius: bind (radius - thickness)
                    stroke: bind fill
                    strokeWidth: 0
                    fill: bind fill
                }
            ]
            effect: GaussianBlur {  // smoothen the sharp edges
                radius: 2
            }
        };

        // Put the ring and the embedded node together, adjusting their
        // positions to account for the drawing radius of the ring itself
        return Group {
            translateX: bind (radius - content.boundsInLocal.width/2 + 2)
            translateY: bind (radius - content.boundsInLocal.height/2 + 2)
            content: [ ring, content ]
        };
    }
}

As you can see from the public attributes of SpiffyRing, there are several properties of the ring that you can customize including the thickness of the ring itself, the colors used to shade the ring and the node that you want to embed in the ring. Some aspects of the code are discussed below.

  • The radius of the ring itself is calculated using the hypotenuse of the embedded content and the properties of the ring itself such as its thickness.

var radius = bind ((Math.hypot(content.boundsInLocal.width, content.boundsInLocal.height))/2 + thickness + gap);
  • When drawing the circles that comprise the ring, their coordinates are adjusted so that the ring is drawn around the mid-point of the embedded node.

        var ring = Group {
            // Center the ring with respect to the embedded node
            translateX: bind content.boundsInLocal.width/2 - 1
            translateY: bind content.boundsInLocal.height/2 - 1
  • Hardcoded values from the listing in Table 1 have now been replaced using the dynamic attributes of the SpiffyRing custom node.
  • And finally, when putting the ring and embedded content together, their coordinates are adjusted to account for the fact that the ring needs to be positioned such that it appears to be drawn all around the content.

        return Group {
            translateX: bind (radius - content.boundsInLocal.width/2 + 2)
            translateY: bind (radius - content.boundsInLocal.height/2 + 2)
            content: [ ring, content ]
        };

Let's see what SpiffyRing can do!

JavaFX

Result

SpiffyRing {
    fill: Color.YELLOW
    content: ImageView {
        image: Image {
            url: "https://duke.dev.java.net/images/iconSized/duke.gif"
        }
    }
};
SpiffyRing around Duke
SpiffyRing {
    fill: Color.CYAN
    thickness: 4
    content: Text {
        textOrigin: TextOrigin.TOP
        fill: Color.BLACK
        content: "JavaFX"
        font: Font { embolden: true }
    }
};⁞
SpiffyRing around some text
SpiffyRing {
    color1: Color.RED
    color2: Color.WHITE
    fill: Color.BLACK
    gap: 0
    content: Rectangle {
        height: 20
        width: 20
        stroke: Color.RED
        strokeWidth: 1
        fill: LinearGradient {
            startX: 0  startY: 0
            endX: 0.5  endY: 0.5
            stops: [
                Stop { offset: 0.0 color: Color.WHITE },
                Stop { offset: 0.5 color: Color.RED }
            ]
        }
    }
};
SpiffyRing around a shape

This was my first try at dynamically sizing and positioning JavaFX nodes and I found it to be quite straightforward. I enjoy dabbling with JavaFX and hope that JavaFX has a great 2009!

Thursday Jun 05, 2008

Problems Using LinearGradients in Compiled JavaFX Script?

If you find that the javafx.ui.LinearGradient class in Compiled JavaFX Script is not working, then you are probably running into JFXC-531.

The tutorial demo that was part of the distribution for the interpreted version of the JavaFX language described a linear gradient as a color transition along a line given by two end points. The coordinates of the end points are specified as fractional values between 0 and 1. However, for the values of the startX, startY, endX and endY attributes of the javafx.ui.LinearGradient class (in Compiled JavaFX Script) you will need to specify geometric coordinates and not fractional values between 0 and 1.

The following examples illustrate how you can specify linear gradients in different directions:

Description
JavaFX
Image

A square that is simply filled with white colour.

The (x, y) coordinates (in the Group's coordinate system) of the four corners of the square (starting at the top-left corner and proceeding in a clockwise direction) are (20, 20), (120, 20), (120, 120) and (20, 120)

import javafx.ui.\*;
import javafx.ui.canvas.\*;

Canvas {
    content:
    Group {
        transform: Translate { x: 40 y: 20 }
        content: [
        Rect {
            x: 20
            y: 20
            height: 100
            width: 100
            stroke: Color.BLACK
            strokeWidth: 2
            fill: Color.WHITE
        }
        ]
    }
};
Square with no gradient fill

The same square filled with a horizontal (left-to-right) linear gradient that transitions from yellow to orange to red. The gradient starts at the top left corner (20, 20) of the square and ends at the top right corner (120, 20).

             fill: LinearGradient {
                startX: 20
                startY: 20
                endX: 120
                endY: 20
                stops: [
                Stop {
                    offset: 0.0
                    color: Color.YELLOW
                    },
                Stop {
                    offset: 0.5
                    color: Color.ORANGE
                },
                Stop {
                    offset: 1.0
                    color: Color.RED
                }
                ]
                spreadMethod: SpreadMethod.PAD
            }
Square with left-to-right gradient fill

Horizontal (right-to-left) gradient fill (swap the start and end coordinates)

            fill: LinearGradient {
                startX: 120
                startY: 20
                endX: 20
                endY: 20
Square with right-to-left gradient fill

Vertical (top to bottom) gradient fill

            fill: LinearGradient {
                startX: 20
                startY: 20
                endX: 20
                endY: 120
Square with top-to-bottom gradient fill

Diagonal (south-west to north-east) gradient fill

            fill: LinearGradient {
                startX: 20
                startY: 120
                endX: 120
                endY: 20
Square with diagonal gradient fill

I plan on using gradients for drawing some cool JavaFX buttons/icons!

Sunday Jun 01, 2008

Timer Trouble

I wrote a simple GUI to test/demo my Timer class that I had blogged about previously. In doing so, I discovered a bug in my implementation.

TimerDemo (Unsynchronized)
 Figure 1a: Unsynchronized timer in action

Figure 1a is a screenshot of the TimerDemo application that lets one configure the various attributes of the Timer class and test the timer by starting/stopping it. This TimerDemo application simply logs a couple of lines of text every time the timer action is invoked. The lines highlighted in grey show the timer action being invoked every 20 seconds.

TimerDemo (Synchronized)
Figure 1b: Synchronized timer in action

Figure 1b shows how the synchronized attribute of the Timer class affects when the first timer action is invoked. The highlighted portions in the above screenshot show that when a synchronized timer is used, the first invocation of the action occurs at the start of the next minute and thereafter when the configured interval expires.

The portion highlighted in yellow in Figure 1a shows a bug in my Timer implementation. It shows that the unsynchronized timer (incorrectly) invokes the action when the timer is started instead of after the expiry of the configured interval. The following diff fixes this bug:

--- Timer.fx.org    2008-05-31 23:52:40.000000000 +0530
+++ Timer.fx    2008-05-31 23:51:01.000000000 +0530
@@ -105,7 +105,6 @@
             };
             sync.start();
         } else {
-            alarm();
             tick.start();
         }
     }

And now my Timer works better!

TimerDemo (post bugfix)

Attachments:

  1. TimerDemo.zip (consolidated source files)

About

arvindsrinivasan

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