Tutorial #1: Advanced clipping
Figure 1

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.

Figure 2

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;
        }
    }
}
Listing 1

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.

Figure 3

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;
        }
    }
}
Listing 2

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)
Back