
In this tutorial I'm going to show you something very simple: how to
use a scene graph Node's clip variable to
combine an interesting colour effect with a non-trivial shape. The end
result will be the bubble you see in figure 1. The bubble shape is
fully transparent in the middle, gradually changing to fully opaque
at its rim. The colour effect is the rainbow pattern running
diagonally across its surface.

The secret to this effect is to learn how to combine the rainbow paint pattern onto a circle with pixels of varying opacity. In this part we'll focus entirely on how to create the plain bubble (as seen in figure 2); in the next part we'll adapt the code to add the rainbow.
Both the rainbow and the bubble are achieved using different types of
gradient paint. As I'm sure you already know, a gradient paint draws
an area of pixels that transitions between several colours, creating a
linear or radial pattern. The bubble uses a RadialGradient
that transitions from totally transparent at its core to opaque at its
edge. The source code for this bubble can be seen in listing 1, below.
package tutorial1;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.Circle;
public class Bubble1 extends CustomNode {
public-init var radius:Number = 100;
public def color1:Color = Color.WHITE;
public def color2:Color = Color.rgb(255,255,255 , 0);
override function create() : Node {
def highlight:Ellipse = Ellipse {
radiusX: radius/2.5;
radiusY: radius/5;
centerY: 0-radius/2;
fill: RadialGradient {
proportional: true;
centerX: 0.5; centerY: 0.25;
stops: [
Stop { offset: 0.0; color: color1; } ,
Stop { offset: 0.5; color: color2; }
]
}
opacity: 0.5;
}
def bubble:Circle = Circle {
radius: radius;
fill: RadialGradient {
proportional: true;
centerX: 0.5; centerY: 0.45;
stops: [
Stop { offset: 0.35; color: color2; } ,
Stop { offset: 0.95; color: color1; }
]
}
}
Group {
content: [ highlight , bubble ]
cache: true;
}
}
}
The bubble is actually created from two shapes, both using a
RadialGradient paint. The first is the aforementioned
Circle, which you can see as
blue in listing 1. The second is an
Ellipse (red in listing 1)
that acts as a highlight, with a radial paint from opaque (core) to
transparent (edge). Their creation should be easy enough to follow
for anyone with a basic grasp of JavaFX Script.
Interesting note:
Near the top of listing 1 I create a transparent color object called
color2. The JavaFX Color class contains a
pre-built constant for a transparent colour named, appropriately enough,
Color.TRANSPARENT. So why did I need to create my own?
The problem is Color.TRANSPARENT is a transparent
black — you might not think this matters, and indeed it doesn't
when the colour is used in its solid form, but when used as part of a
gradient paint it creates a minor hiccup. Using
Color.TRANSPARENT would mean not only are the pixels
transitioning from opaque to transparent, but also from white to black.
This actually creates a noticeable dull tint to the gradient as it
moves towards full transparency — to prevent this I created
color2, to ensure only the alpha part of the colour is
changed during the gradient.

Having creating our bubble, all we need to do now is to add a rainbow
stripe across its surface. This would be simple, if we weren't already
using the bubble's fill to achieve the transparency effect.
To combine the bubble's RadialGradient with the rainbow's
LinearGradient we need to apply a little bit of lateral
thinking. Figure 3 and listing 2 show how.
package tutorial1;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.Circle;
public class Bubble2 extends CustomNode {
public-init var radius:Number = 100;
public def color1:Color = Color.WHITE;
public def color2:Color = Color.rgb(255,255,255 , 0);
override function create() : Node {
def highlight:Ellipse = Ellipse {
radiusX: radius/2.5;
radiusY: radius/5;
centerY: 0-radius/2;
fill: RadialGradient {
proportional: true;
centerX: 0.5; centerY: 0.25;
stops: [
Stop { offset: 0.0; color: color1; } ,
Stop { offset: 0.5; color: color2; }
]
}
opacity: 0.5;
}
def bubble:Circle = Circle {
radius: radius;
fill: LinearGradient {
proportional: true;
endX: 0.5; endY: 1;
stops: [
Stop { offset: 0.1; color: Color.RED; } ,
Stop { offset: 0.2; color: Color.ORANGE; } ,
Stop { offset: 0.3; color: Color.YELLOW; } ,
Stop { offset: 0.4; color: Color.LIGHTGREEN; } ,
Stop { offset: 0.5; color: Color.DODGERBLUE; } ,
Stop { offset: 0.6; color: Color.PINK; } ,
Stop { offset: 0.7; color: Color.VIOLET; }
Stop { offset: 0.8; color: color1; }
]
}
clip: Circle {
radius: radius;
fill: RadialGradient {
proportional: true;
centerX: 0.5; centerY: 0.45;
stops: [
Stop { offset: 0.35; color: Color.TRANSPARENT; } ,
Stop { offset: 0.95; color: Color.WHITE; }
]
}
}
}
Group {
content: [ highlight , bubble ]
cache: true;
}
}
}
The bubble's fill has been replaced by the rainbow
LinearGradient (see red
in the listing), and a new Circle has been added as its
clip with the same properties as our original bubble (see
blue in the listing). In effect the
original Circle is now being used as a transparency mask,
giving us the freedom to apply whatever paint or fill pattern we need
to the main Circle.
Note: because the clipping circle doesn't actually determine the colour
of any pixels, only their opacity, it doesn't matter what colour we
use in its RadialGradient.
And so that is how we combine clever paint effects with opacity effects on a single shape. Easy, once you know how!
Source code (2k) |

