- Effects Overview
- The JavaFX Effects Classes
- Blending
- Lighting
The JavaFX Effects Classes
The JavaFX SDK provides 17 different effects that can be applied to any node. This section describes and illustrates all the effects, with the exception of Blend and Lighting, which have sections of their own at the end of the chapter. Each effect has a set of variables that you can use to customize it. As we examine each effect, we'll take a look at the variables available and roughly consider what each of them does. There are too many combinations to illustrate them all in this chapter, so in most cases we limit ourselves to some typical examples. It is easy to experiment with these effects—all you need to do is modify the example source code. You can also use the Effects Playground application that you'll find among the samples at http://javafx.com.
GaussianBlur
The GaussianBlur effect produces a blurred version of its input. The "Gaussian" part of the name refers to the fact that the output pixels are calculated by applying a Gaussian function to the source pixel and a group of pixels surrounding it. If you are interested in the details, you'll find them at http://en.wikipedia.org/wiki/Gaussian_blur. The size of the group of adjacent pixels that are used to calculate the result is controlled by the radius variable (see Table 20-1). The larger the value of the radius variable, the greater the blur effect will be. When the value of this variable is 0, there is no blur at all.
Table 20-1. Variables of the GaussianBlur Class
Variable |
Type |
Access |
Default |
Description |
input |
Effect |
RW |
null |
The input to this effect |
radius |
Number |
RW |
10.0 |
The radius of the area containing the source pixels used to create each target pixel, in the range 0 to 63, inclusive |
Two example of the GaussianBlur effect applied to the image used in the previous two sections are shown in Figure 20-6. Here's the code used to create this effect, which you'll find in the file javafxeffects/GaussianBlur1.fx:
ImageView { image: Image { url: "{__DIR__}image1.jpg" } effect: GaussianBlur { radius: bind radiusSlider.value } }
Figure 20-6 The GaussianBlur effect
The image on the left has a blur radius of 10, while the one on the right has radius 40.
BoxBlur
GaussianBlur is a high-quality effect, but it is also a relatively expensive one. The BoxBlur effect is a cheaper way to produce a blur, albeit one of lower quality. The variables that you can use to control the BoxBlur effect are listed in Table 20-2.
Table 20-2. Variables of the BoxBlur Class
Variable |
Type |
Access |
Default |
Description |
input |
Effect |
RW |
null |
The input to this effect. |
height |
Number |
RW |
5.0 |
The vertical size of the box used to create the blur, in the range 0 to 255, inclusive. |
width |
Number |
RW |
5.0 |
The horizontal size of the box used to create the blur, in the range 0 to 255, inclusive. |
iterations |
Number |
RW |
1 |
The number of averaging iterations, in the range 0 to 3, inclusive. Higher values produce a smoother blur effect. |
This effect works by replacing each pixel of the input by the result of averaging its value with those of its neighboring pixels. The pixels that take part in the operation are those in a rectangular area surrounding the source pixel, the dimensions of which are given by the width and height variables. You can see the effects of changing these variables by running the code in the file javafxeffects/BoxBlur1.fx. This example applies the BoxBlur effect to the same image as we used to illustrate GaussianBlur. The width, height, and iterations variables are set from three sliders that allow you to test the full ranges of values for each variable. Here's how the BoxBlur is applied:
ImageView { image: Image { url: "{__DIR__}image1.jpg" } effect: BoxBlur { height: bind heightSlider.value width: bind widthSlider.value iterations: bind iterationSlider.value } }
Increasing the value of the height variable produces a vertical blur, as shown on the left of Figure 20-7. Similarly, the width variable controls the extent of the blur in the horizontal direction.
Figure 20-7 The Box Blur effect
You can use the iterations variable to increase the quality of the blur at the expense of greater CPU utilization. When this variable has the value 2 or 3, the averaging operation is repeated the specified number of times. On the second iteration, the averaged pixels are averaged against each other, which tends to smooth out any sharp differences that might exist near to edges in the input source. A third iteration produces an even smoother result. You can see the result of applying three iterations to a horizontal blur of the input image on the right of Figure 20-7. A BoxBlur with three iterations produces a result that is close to that of a GaussianBlur, but at a slightly lower cost.
MotionBlur
MotionBlur creates the effect that you would see if you look out of the window of a fast-moving vehicle. Like GaussianBlur, it has a radius variable that determines how much of a blur is to be applied. It also has an angle variable that lets you specify the direction of the motion. These variables are described in Table 20-3.
Table 20-3. Variables of the MotionBlur Class
Variable |
Type |
Access |
Default |
Description |
input |
Effect |
RW |
null |
The input to this effect |
angle |
Number |
RW |
0 |
The angle of the motion blur |
radius |
Number |
RW |
10.0 |
The radius of the area containing the source pixels used to create each target pixel, in the range 0 to 63 inclusive |
There are no restrictions on the value of the angle variable, but values greater than 360 are treated modulo 360, while negative values are first reduced modulo 360 and then have 180 added to them, so that -90 is the same as 270. The following extract shows how to apply a MotionBlur to a node.
image: Image { url: "{__DIR__}image1.jpg" } effect: MotionBlur { angle: bind angleSlider.value radius: bind radiusSlider.value } }
If you run the code in the file javafxeffects/MotionBlur1.fx, you can experiment with the effects of different radius and angle values. Two examples with different angles are shown in Figure 20-8. The angle slider lets you vary the value of this variable from -180 when the thumb is at the far left to +180 at the far right. In the example on the left of the figure, the angle variable is 0, which gives a horizontal blur. In the example on the right, the angle variable has the value 90, and the result is a vertical blur. As is the case elsewhere in the JavaFX API, angles are measured with 0 at the 3 o'clock position and increase as you move in a clockwise direction.
Figure 20-8 The MotionBlur effect
DropShadow
As you have already seen, the DropShadow effect draws a shadow that appears to be behind and partly obscured by the node to which it is applied. By using this effect with the appropriate variable settings, you can give the impression that the node is floating above a nearby surface or one slightly farther away. You can also change the nature of the shadow to indicate whether the light source is close to or a long way from the node. The variables that you can use to configure the DropShadow class are listed in Table 20-4.
Table 20-4. Variables of the DropShadow Class
Variable |
Type |
Access |
Default |
Description |
blurType |
BlurType |
RW |
THREE_PASS_BOX |
The type of blur to be used |
color |
Color |
RW |
Color.BLACK |
The color to be used for the shadow |
offsetX |
Number |
RW |
0.0 |
The x-offset of the shadow |
offsetY |
Number |
RW |
0.0 |
The y-offset of the shadow |
radius |
Number |
RW |
10 |
The radius of the blur effect if a GaussionBlur is used |
width |
Number |
RW |
21 |
The width of the blur if BoxBlur is used |
height |
Number |
RW |
21 |
The height of the blur if BoxBlur is used |
spread |
Number |
RW |
0.0 |
The proportion of the radius (or box for BoxBlur) over which the shadow is fully opaque (see text) |
The variables that you will most commonly set are color, offsetX, and offsetY. The color variable simply determines the color of the solid part of the shadow, which will generally be slightly darker than the background behind the node. By default, the shadow is black. The offsetX and offsetY variables control the displacement of the shadow relative to the node itself.
The blurType variable controls which of the supported types of blur is used at the edges of the shadow. This variable is of type javafx.scene.effects.BlurType, which has the following possible values:
- BlurType.GAUSSIAN: A GaussianBlur
- BlurType.ONE_PASS_BOX: A BoxBlur with one iteration
- BlurType.TWO_PASS_BOX: A BoxBlur with two iterations
- BlurType.THREE_PASS_BOX: A BoxBlur with three iteration
The code in the file javafxeffects/DropShadow1.fx creates a scene containing a rectangle with a DropShadow effect and a GaussianBlur. There are four sliders that let you control some of the variables listed in Table 20-5. You can use this program to experiment with various settings to see how they work. Figure 20-9 shows a typical example.
Table 20-5. Variables of the Shadow Class
Variable |
Type |
Access |
Default |
Description |
blurType |
BlurType |
RW |
THREE_PASS_BOX |
The type of blur to be used |
input |
Effect |
RW |
null |
The input to this effect |
color |
Color |
RW |
Color.BLACK |
The color to be used for the shadow |
radius |
Number |
RW |
10.0 |
The radius of the blur effect if a GaussianBlur is used |
width |
Number |
RW |
21 |
The width of the blur if a BoxBlur is used |
height |
Number |
RW |
21 |
The height if the blur if a BoxBlur is used |
Figure 20-9 Configuring a DropShadow effect
The size of the shadow is determined by the values of the offsetX, offsetX, and radius variables. When the radius is 0, the shadow has a sharp edge as shown on the left of Figure 20-10. In this case, the offsetX and offsetY values are both 15, so the shadow is offset by 15 pixels to the right of and below the top-left corner of the node, which gives the impression of a light source that is to the left of and above the top of the node. Negative values for the offsetX variable would be used for a light source to the right of the node, and negative offsetY values for a light source that is below the node.
Figure 20-10 Effects of the offsetX, offsetY, and radius variable of the DropShadow effect
When the radius value is non-0, the edge of the shadow is blurred by blending pixels of the shadow color with those of the background color. The radius determines the size of this blurred area. Increasing the radius value makes the blurred region, and the size of the shadow, larger, as shown on the right of Figure 20-10. As you can see, the blurring fades out with increasing distance from the original shadow area. The radius value can be anywhere between 0 and 63, inclusive.
By default, the blurred area starts with the shadow color on its inside edge and progresses to the background color on its outside edge. If you want, you can arrange for a larger part of the blurred area to have the shadow color, resulting in a larger, darker shadow. You do this by setting the spread value, which ranges from 0.0, the default, to 1.0. This value represents the proportion of the blurred area into which the shadow color creeps. On the right side of Figure 20-10, the spread variable has value 0, and you can see that the shadow gets lighter very rapidly as you move your eyes away from the edge of the rectangle. On the left side of Figure 20-11, the spread variable has been set to 0.5. Now you can see that the darker region of the shadow has increased in size as it encroaches into the blurred area. On the right of Figure 20-11, the spread is at of 0.9, and you can see that almost all the blurred area has been taken over by the shadow color.
Figure 20-11 The effect of the spread variable
The idea of the spread variable is to allow a proper emulation of what would happen if you moved a light source quite close up to the node. A light source nearby would cause a wide shadow, corresponding to a larger blurred area, but it would also cause the darker part of the shadow to increase in size. You simulate the former effect by increasing the blur radius and the latter by increasing the spread.
InnerShadow
InnerShadow is very similar to DropShadow, the difference being that the shadow is inside the boundaries of the node to which it is applied, rather than outside. This gives the impression of depth within the node, because it appears to have built-up sides. The variables of this class, which are listed in Table 20-6, are almost the same as those of DropShadow.
Table 20-6. Variables of the InnerShadow Class
Variable |
Type |
Access |
Default |
Description |
blurType |
BlurType |
RW |
THREE_PASS_BOX |
The type of blur to be used |
color |
Color |
RW |
Color.BLACK |
The color to be used for the shadow |
offsetX |
Number |
RW |
0.0 |
The x-offset of the shadow |
offsetY |
Number |
RW |
0.0 |
The y-offset of the shadow |
radius |
Number |
RW |
10 |
The radius of the blur effect if a GaussianBlur is used |
width |
Number |
RW |
21 |
The width of the blur if a BoxBlur is used |
height |
Number |
RW |
21 |
The height of the blur if a BoxBlur is used |
choke |
Number |
RW |
0.0 |
The proportion of the radius (or box for BoxBlur) over which the shadow is fully opaque (see text) |
You can see an example of this effect in Figure 20-12. This screenshot shows the result of running the code in the file javafxeffects/InnerShadow1.fx. As with the DropShadow examples, you can use the sliders to vary the effect parameters and see the results. The choke variable is equivalent to the spread variable of the DropShadow class.
Figure 20-12 Configuring an InnerShadow effect
Shadow
The Shadow effect produces a single-colored and blurred shadow from the node or input effect on which it operates. The extent of the blur depends on the value of the radius, which is one of the three variables that control this effect, all of which are listed in Table 20-5 on page 661.
You can see an example of this effect as applied to some text in Figure 20-13. You can experiment with different radius values by running this example, which you'll find in the file javafxeffects/Shadow1.fx:
Text { textOrigin: TextOrigin.TOP x: 30 y: 30 content: "JavaFX Shadow Effect" font: Font { size: 24 } effect: Shadow { color: Color.BLUE radius: bind radiusSlider.value } }
Figure 20-13 The Shadow effect
Unlike the other two shadow effects, this one replaces its input instead of augmenting it, so the original text node is not drawn.
Bloom
The Bloom effect adds a glow to those areas of its input that are made up of pixels for which the luminosity value is above a given threshold. This effect has only two controlling variable, which are listed in Table 20-7.
Table 20-7. Variables of the Bloom Class
Variable |
Type |
Access |
Default |
Description |
input |
Effect |
RW |
null |
The input to this effect. |
threshold |
Number |
RW |
0.3 |
The luminosity above which the glow effect will be applied, from 0.0 (all pixels will glow) to 1.0. (No pixels will glow.) |
The luminosity of a pixel is a measure of how bright it seems to the human eye. You can see an example of this effect in Figure 20-14, which shows the Bloom effect applied to an ImageView node 3:
ImageView { image: Image { url: "{__DIR__}image1.jpg" } effect: bloom = Bloom { threshold: bind (thresholdSlider.value as Number) / 10 } }
Figure 20-14 The Bloom effect
In the image on the left, the threshold value is 1.0. Because no pixel has a luminosity that is greater than 1.0, what you see here is the original image. On the right of the figure, the slider has been moved so that the threshold is now set to 0.3. The blue regions of the image, in particular the sky, are now noticeably brighter. Notice that this effect spills over onto adjacent pixels so that the leaves on the trees near the top of the image have also been brightened.
Glow
Glow is very similar to Bloom, except that the controlling parameter works in the reverse order. The glow effect makes bright pixels appear brighter. The more of the effect that you apply, as determined by the value of the level variable, the brighter those pixels appear. The two variables that control this effect are listed in Table 20-8.
Table 20-8. Variables of the Glow Effect
Variable |
Type |
Access |
Default |
Description |
input |
Effect |
RW |
null |
The input to this effect. |
level |
Number |
RW |
0.3 |
Controls the intensity of the glow effect. 0.0 gives no glow; 1.0 gives maximum glow. |
You'll find an example that allows you to vary the level parameter in the file javafxeffects/Glow1.fx. The following extract from that file shows how the glow effect is applied to a node:
ImageView { image: Image { url: "{__DIR__}image1.jpg" } effect: Glow { level: bind (levelSlider.value as Number) / 10 } }
Figure 20-15 shows this effect applied to the same image as that used in our discussion of bloom in the previous section. In the image on the left of the figure, the level variable is 0, so no glow is applied. In the image on the right, the level is set to 0.6, and the result is almost exactly the same as the result of applying a small amount of bloom to the image, which you can see at the bottom of Figure 20-14. To apply more glow in this example, you move the slider farther to the right, whereas to apply more bloom in the example in the previous section, you moved it farther to the left.
Figure 20-15 The Glow effect
Identity
The Identity effect is a little different from the effects that you have seen so far—its sole purpose is to allow an Image object to be used as the input to another effect. It is always linked to a node, but that node does not appear in the scene; the result of applying one or more effects to the source image is seen instead. Table 20-9 lists the variables that control the behavior of this class.
Table 20-9. Variables of the Identity Class
Variable |
Type |
Access |
Default |
Description |
source |
Image |
RW |
null |
The source Image |
x |
Number |
RW |
0 |
The x coordinate of the Image relative to the source Node |
y |
Number |
RW |
0 |
The y coordinate of the Image relative to the source Node |
The simplest way to explain how these variables work is by using an example. The following code, which you'll find in the file javafxeffects/Identity1.fx, applies a GaussianBlur effect to an image and places it in the Scene.
1 Stage { 2 title: "Identity #1" 3 scene: Scene { 4 width: 380 5 height: 280 6 var image = Image { url: "{__DIR__}image1.jpg" } 7 content: [ 8 Circle { 9 centerX: 100 centerY: 100 10 effect: GaussianBlur { 11 input: Identity { 12 source: image 13 x: 10 y: 10 14 } 15 radius: 10 16 } 17 } 18 ] 19 } 20 }
The result of running this code is shown in Figure 20-16.
Figure 20-16 The Identity effect
The Identity effect on lines 11 to 14 converts the image to an Effect that is then used as the input to the GaussianBlur, resulting in a blurred version of the image. These two effects are both linked with a circle, but because the circle is not an input to either of the effects, it does not influence the output, and the blurred image appears instead of it. The only property of the circle that is inherited is its coordinate system, which, in this case, is the same as the coordinate system of the scene. The x and y variables of the Identity effect, which are set on line 13, determine where its output would be drawn relative to the circle's coordinate system. In this case, these values cause the image to be placed a little to the right of and below the coordinate origin.
The result of an Identity effect, like that of the Flood effect that is described in the next section, is often used as one of the inputs to a Blend effect, which is discussed later in this chapter.
Flood
Like Identity, the purpose of the Flood effect is to create an input to another effect, in this case a rectangular area filled with a Paint or a solid color. The variables that determine the fill color and the bounds of the filled area are listed in Table 20-10.
Table 20-10. Variables of the Flood Class
Variable |
Type |
Access |
Default |
Description |
paint |
Paint |
RW |
Color.RED |
The Paint used to flood the area |
x |
Number |
RW |
0 |
The x coordinate of the filled area relative to the source Node |
y |
Number |
RW |
0 |
The y coordinate of the filled area relative to the source Node |
width |
Number |
RW |
0 |
The width of the area to be filled |
height |
Number |
RW |
0 |
The height of the area to be filled |
The coordinates and lengths are specified in the coordinate system of the node that this effect is linked with. As with Identity, the node itself is replaced in the scene by the result of the effect. The code in the file javafxeffects/Flood1.fx uses the Flood effect to fill an area with a solid blue color and then applies a MotionBlur, giving the result shown in Figure 20-17.
Figure 20-17 The Flood effect
ColorAdjust
The ColorAdjust effect produces an output that is the result of adjusting some or all the hue, saturation, brightness, and contrast values of its input. The input may be either another effect or a node of any kind, but most commonly an image in an ImageView object. The variables of this class are listed in Table 20-11.
Table 20-11. Variables of the ColorAdjust Class
Variable |
Type |
Access |
Default |
Description |
input |
Effect |
RW |
null |
The input to this effect. |
hue |
Number |
RW |
0.0 |
The amount by which the hue of each pixel should be adjusted, in the range -1.0 to 1.0. Value 0 leaves the hue unchanged. |
saturation |
Number |
RW |
0.0 |
The amount by which the saturation of each pixel should be adjusted, in the range -1.0 to 1.0. Value 0 leaves the saturation unchanged. |
brightness |
Number |
RW |
0.0 |
The amount by which the brightness of each pixel should be adjusted, in the range -1.0 to 1.0. Value 0 leaves the brightness unchanged. |
contrast |
Number |
RW |
1.0 |
The amount by which the contrast should be adjusted, in the range 0.25 to 4. Value 1 leaves the contrast unchanged. |
You can experiment with this effect by running the example in the file javafxeffects/ColorAdjust.fx, which binds a slider to each of the hue, saturation, brightness, and contrast variables of a ColorAdjust object that is associated with an ImageView node. The values of the hue, saturation, and brightness sliders range from -1.0 on the left to 1.0 on the right, while the contrast slider provides the value 0.25 in its minimum position and 4.0 at its maximum position. On the left of Figure 20-18, you can see the result of applying almost the maximum brightness, and on the right you see the result of applying the maximum contrast.
Figure 20-18 The ColorAdjust effect
InvertMask
The InvertMask effect takes another Effect as its input and produces a result in which all the transparent pixels from the input are opaque and all the opaque pixels are transparent. The output is typically used as one of the inputs to a Blend effect, which is discussed later in this chapter. The variables of the InvertMask class are listed in Table 20-12.
Table 20-12. The Variables of the InvertMask Class
Variable |
Type |
Access |
Default |
Description |
input |
Effect |
RW |
null |
The input to this effect |
pad |
Number |
RW |
0 |
The padding to add to the sides of the resulting image |
Reflection
The Reflection effect provides an easy way to create a reflection of a node or group of nodes. The variables that you can use to specify the required characteristics of the reflection are listed in Table 20-13.
Table 20-13. The Variables of the Reflection Class
Variable |
Type |
Access |
Default |
Description |
input |
Effect |
RW |
null |
The input to this effect |
topOffset |
Number |
RW |
0 |
The distance between the bottom of the input and the beginning of the reflection |
fraction |
Number |
RW |
0 |
The fraction of the input that is used to create the reflection |
topOpacity |
Number |
RW |
0.5 |
The opacity used for the topmost row of pixels in the reflection |
bottomOpacity |
Number |
RW |
0 |
The opacity of the bottom row of pixels in the reflection |
The example code in the file javafxeffects/Reflection1.fx allows you to experiment with different values of these variables. A typical result, which is equivalent to the following code, is shown in Figure 20-19.
Text { content: "JavaFX Developer's Guide" x: 20 y: 50 fill: Color.WHITE font: Font { size: 24 } effect: Reflection { topOffset:0 fraction: 0.8 topOpacity: 0.3 bottomOpacity: 0.0 } }
Figure 20-19 The Reflection effect
The topOffset variable lets you set the distance between the source object, here the text "JavaFX Developer's Guide" (and its reflection). Increasing this distance makes it seem that the source is further away from the reflecting surface. In Figure 20-19, the topOffset value is 0, which places the reflection as close as possible to the original. In this case, the reflected text might seem to be farther away than it should be with this value—that is because of the descender on the letter p, which is the closest point of contact with the reflection.
The fraction variable determines how much of the source appears in the reflection. Typically, unless the reflecting surface is very shiny, you will not want the whole of the source object to be reflected. In Figure 20-19, the fraction variable has the value 0.8, so about 80% of the source is reflected.
The topOpacity and bottomOpacity values give the opacity of the reflection at its top and bottom extents, respectively. In Figure 20-19, the topOpacity has been set to 0.3 and bottomOpacity to 0.0.
SepiaTone
The SepiaTone effect is used to give images (or any group of nodes) an "Olde Worlde" look, as if they have been photographed by an old black-and-white camera, or washed out by the effects of exposure to sunlight over a long period. This effect provides only the two variables listed in Table 20-14.
Table 20-14. Variables of the SepiaTone Class
Variable |
Type |
Access |
Default |
Description |
input |
Effect |
RW |
null |
The input to this effect |
level |
Number |
RW |
1.0 |
The level of this effect, from 0.0 to 1.0 |
The level variable determines the extent to which the image is affected. The example code in the file javafxeffects/SepiaTone1.fx creates a Scene containing an image and a slider that lets you vary the value of the level variable and observe the result. The screenshot on the left of Figure 20-20 has level set to the value 0.4, while the one on the right has level set to 1.0.
Figure 20-20 The SepiaTone effect
PerspectiveTransform
The PerspectiveTransform is a useful effect that you can use to create the impression of a rotation in the direction of the z-axis—that is, into and out of the screen. It operates by deforming a node or group of nodes by moving its corners to specified locations and relocating the other pixels in such a way that straight lines drawn on the original nodes are mapped to straight lines in the result. Unlike the affine transforms that you saw in Chapter 17, "Coordinates, Transforms, and Layout," this effect does not guarantee that lines that are parallel in the source will be parallel in the result and, in fact, the perspective effect requires that some parallel lines be made nonparallel.
The variables that control the perspective effect are listed in Table 20-15.
Table 20-15. Variables of the PerspectiveTransform Class
Variable |
Type |
Access |
Default |
Description |
input |
Effect |
RW |
null |
The input to this effect. |
llx |
Number |
RW |
0 |
The x coordinate of the location to which the lower-left corner of the input is moved |
lly |
Number |
RW |
0 |
The y coordinate of the location to which the lower-left corner of the input is moved |
ulx |
Number |
RW |
0 |
The x coordinate of the location to which the upper-left corner of the input is moved |
uly |
Number |
RW |
0 |
The y coordinate of the location to which the upper-left corner of the input is moved |
lrx |
Number |
RW |
0 |
The x coordinate of the location to which the lower-right corner of the input is moved |
lry |
Number |
RW |
0 |
The y coordinate of the location to which the lower-right corner of the input is moved |
urx |
Number |
RW |
0 |
The x coordinate of the location to which the upper-right corner of the input is moved |
ury |
Number |
RW |
0 |
The y coordinate of the location to which the upper-right corner of the input is moved |
To see what these variables represent, refer to Figure 20-1. Here, imagine that the image is mounted vertically and can rotate about its vertical axis, as shown by the white dashed line. In the figure, the black outline represent the view of the image after it has been rotated a few degrees so that the right edge has moved closer to the viewer and the left edge farther away. This would cause the right edge to appear larger and the left edge correspondingly smaller, giving the impression of perspective.
You can use a PerspectiveTransform to create the rotated image from the original by moving the corners of the original to the new positions, as shown in Figure 20-21. The corner at the top left is the upper-left corner, and its position is given by the ulx and uly variables. The corner at the top right is the upper-right corner, and its position is specified by the urx and ury variables, and so on.
Figure 20-21 Illustrating the variables of the PerspectiveTransform class
It's easy to create a PerspectiveTransform that will rotate an image (or any other node or group) around a vertical axis that is a specified distance along its horizontal edge. It requires only a small amount of mathematics. We'll deal with the x and y coordinates separately, to make it easier to understand what is going on. The information needed to work out how to calculate the values of the x coordinates is shown in Figure 20-22.
Figure 20-22 Rotating an image (top-down view)
Here, we are looking down at the image from above. The thick horizontal line, labeled APB, is the image before rotation, whereas the diagonal line, labeled A'PB', is the image after it has been rotated through an angle (shown here as angle) about a pivot point, marked P, that is l1 pixels from its left side and l2 pixels from its right side. In this case, the pivot point is almost equidistant from the sides of the image, but the same calculation works even if this is not the case. The x-axis is shown at the bottom of the figure.
The x coordinate of the left side of the image after rotation is given by the distance AC, while the x coordinate of the right side is given by A. The distance AC is the same as AP - CP. Because AP has the value l1, elementary trigonometry gives the following:
AC = AP - CP = l1 - l1 * cos(angle)
Similarly,
AD = BP + PD = l1 + l2 * cos(angle)
AC is actually the value of both ulx and llx, while AD is the value that we need for urx and ulx. To make this simpler when using the PerspectiveTransform, we introduce two new parameters:
- imgWidth: The width of the image. This corresponds to the length AB and is equal to l1 + l2.
- pivot: The position of the pivot point along the line AB, as a ratio of l1 to the total length AB. To place the pivot point in the center, set pivot to 0.5.
Given these parameters, we so far have the following PerspectiveTransform:
PerspectiveTransform { ulx: l1 - l1 * Math.cos(angle) uly: ?? // Not yet determined llx: l1 - l1 * Math.cos(angle) lly: ?? // Not yet determined urx: l1 + l2 * Math.cos(angle) ury: ?? // Not yet determined lrx: l1 + l2 * Math.cos(angle) lry: imgHeight ?? // Not yet determined }
Now let's move on to the y coordinates. This part is slightly easier. Essentially, what we need to do is make the length of the side of the image that moves toward us larger and that of the side that moves away from us smaller. We can choose by how much we want each side to grow or shrink—the closer we are to the image, the more each side would grow or shrink. We'll make this a parameter of the transform and say that we want each side to grow or shrink by htFactor of its actual value at each end. That means, for example, that if the image is 100 pixels tall and we choose htFactor to be 0.2, the side of the image that is nearer to us after the image has rotated through 90 degree will be larger by 0.2 * 100 = 20 pixels at each end, or a total of 140 pixels tall. Similarly, the side that is farther away will shrink to 60 pixels in height.
Now refer to Figure 20-23. Here, we are looking at the image from the front again. The solid shape is the image after it has been rotated. The dashed vertical line is the axis of rotation, and the dashed extension that is outside the rectangle represents the maximum apparent height of the image when it has rotated through 90 degrees—that is, when it is edge-on to the viewer.
Figure 20-23 Rotating an image (front view)
In its current position, the y coordinate of the upper-right corner would be -B'D. This coordinate is negative because the y-axis runs along the top of the image, as shown. The length of B'D is l2 * sin(angle), but because we are limiting the maximum vertical extension of each side by htFactor, we use the value htFactor * l1 * sin(angle) instead. Applying the same logic to each of the four corners gives us the following as the final transform, installed in an ImageView and with specific values assigned for the variables pivot and htFactor:
ImageView { translateX: bind (scene.width - imgWidth) / 2 translateY: bind (scene.height - 30 - imgHeight) / 2 image: image = Image { url: "{__DIR__}image1.jpg" } var angle = bind Math.toRadians(slider.value); var pivot = 0.5; var htFactor = 0.2; var l1 = bind pivot * imgWidth; var l2 = bind imgWidth - l1; effect: bind PerspectiveTransform { ulx: l1 - l1 * Math.cos(angle) uly: htFactor * l1 * Math.sin(angle) llx: l1 - l1 * Math.cos(angle) lly: imgHeight - l1 * htFactor * Math.sin(angle) urx: l1 + l2 * Math.cos(angle) ury: -l2 * htFactor * Math.sin(angle) lrx: l1 + l2 * Math.cos(angle) lry: imgHeight + l2 * htFactor * Math.sin(angle) } }
The file javafxeffects/PerspectiveTransform1.fx contains an example that incorporates this transform and provides a slider that allows you to vary the value of the angle variable from -90 degrees to +90 degrees. Figure 20-24 shows a couple of screenshots taken from this example with the image rotated by two different angular amounts. You can experiment with this example by changing the value of the pivot variable to get a rotation about a different point. Setting pivot to 0 causes a rotation around the left edge, while the value 1 gives rotation about the right edge.
Figure 20-24 Examples of images rotated using a PerspectiveTransform
DisplacementMap
The DisplacementMap effect is, at first glance, the most complex of the effects that are provided by the JavaFX SDK, but it is also one of the most powerful. As its name suggests, this effect displaces pixels from their locations in the input image to different positions in the output image. Let's begin by listing the variables that you can use to parameterize the effect (see Table 20-16), and then we'll take a look at how they work.
Table 20-16. Variables of the DisplacementMap Class
Variable |
Type |
Access |
Default |
Description |
input |
Effect |
RW |
null |
The input to this effect |
mapData |
FloatMap |
RW |
Empty map |
The map that determines how input pixels are mapped to output pixels |
offsetX |
Number |
RW |
0.0 |
A fixed displacement along the x-axis applied to all pixel offsets |
offsetY |
Number |
RW |
0.0 |
A fixed displacement along the y-axis applied to all pixel offsets |
scaleX |
Number |
RW |
1.0 |
A scale factor applied to the map data along the x-axis |
scaleY |
Number |
RW |
1.0 |
A scale factor applied to the map data along the y-axis |
wrap |
Boolean |
RW |
false |
Whether the displacement operation should wrap at the boundaries |
How the DisplacementMap Effect Works
The reason for the apparent complexity of this effect is the equation that controls how the pixels are moved:
dst[x, y] = src[x + (offsetX + scaleX * map[x, y][0]) * srcWidth, y + (offsetY + scaleY * map[x, y][1]) * srcHeight]
At first sight, this probably looks quite daunting, but in fact it turns out to be quite simple. Basically, it says each pixel in the output (here represented by the symbol dst) derives from a single pixel in the input (represented by src). The pixel value at coordinates (x, y) in the output is obtained from a source pixel whose coordinates are displaced from those of the destination pixel by an amount that depends on a value obtained from a map, together with some scale factors and an offset. The values srcWidth and srcHeight are respectively the width and height of the input source.
Let's start by assuming that the offset values are both 0 and the scale values are both 1. In this simple case, the equation shown above is reduced to this more digestible form:
dst[x, y] = src[x + map[x, y][0] * srcWidth, y + map[x, y][1] * srcHeight]
The map is a two-dimensional data structure that is indexed by the x and y coordinates of the destination point, relative to the top-left corner of the output image. Each element of this structure may contain a number of floats, which is why the class that holds these values is called a FloatMap. The FloatMaps that are used with a DisplacementMap must have two floats in each position, 5 the first of which is used to control the displacement along the x-axis and the second the displacement along the y-axis. Suppose, for the sake of argument, that we have a FloatMap in which every element has the values (-0.5, -0. 5). In this case, the equation above can be written as follows:
dst[x, y] = src[x - 0.5 * srcWidth, y - 0.5 * srcHeight]
Now, you should be able to see that the pixel at any given position in the output is obtained from the source pixel that is a half of the width or height of the source away from it. If we assume that the source is 100 pixels square, we can make our final simplification:
dst[x, y] = src[x - 50, y - 50]
This says that the output pixel at any point comes from the source pixel that is 50 pixels above it and to its left. The reason for using srcWidth and srcHeight as multipliers is that the values in the map can then be encoded as fractions of the width and height of the input respectively and therefore would normally be in the range -1 to +1. A map value of -1 or +1 would move a point by the complete width or height of the input source.
A Simple Example
Let's look at how you would implement the example that you have just seen. You'll find the code in the file javafxeffects/DisplacementMap1.fx. Let's start by creating the map:
1 var image: Image = Image { url: "{__DIR__}image1.jpg" }; 2 var imgWidth = image.width as Integer; 3 var imgHeight = image.height as Integer; 4 var map: FloatMap = FloatMap { 5 width: imgWidth 6 height: imgHeight 7 } 8 9 for (i in [0..<map.width]) { 10 for (j in [0..<map.height]) { 11 map.setSample(i, j, 0, -0.50); 12 map.setSample(i, j, 1, -0.50); 13 } 14 }
In this example, we are going to use an image as the input source, so we create a map that has the same dimensions as the image itself. The code on lines 4 to 6 declares the FloatMap, setting its dimensions from the width and height of the image. The nested loops on lines 9 to 14 initialize the FloatMap, assigning two samples for each element. Each sample has the value -0.5, which is the offset that we require. Note how these samples are installed:
map.setSample(i, j, 0, -0.50); // The x offset map.setSample(i, j, 1, -0.50); // The y offset
FloatMap has several overloaded variants of the setSample() function that you can use. In the variant that we use here, the first two arguments are the x and y coordinates of the element, the third argument is the band number, and the fourth argument is the offset for that band. Band 0 is used for the x-offset and band 1 for the y offset. 6
Now, here's the code that creates and uses the DisplacementMap effect:
var scene: Scene; Stage { title: "DisplacementMap #1" scene: scene = Scene { width: 500 height: 380 fill: Color.BLACK content: [ ImageView { translateX: bind (scene.width - imgWidth) / 2 translateY: bind (scene.height - 30 - imgHeight) / 2 image: image effect: DisplacementMap { mapData: map } } ] } }
As you can see, the effect is applied simply by creating a DisplacementMap based on the map data and installing it in an ImageView that contains the source image. We don't need to set the scale or offset values because we are using the defaults in this case. You can see the result in Figure 20-25.
Figure 20-25 A simple DisplacementMap effect
The original image is shown on the left of the figure and the result of applying the DisplacementMap on the right. As you can see, the image has been moved halfway across and halfway down the area occupied by the source. It's easy to see why this has happened if you look back at the equation that describes this effect:
dst[x, y] = src[x - 0.5 * srcWidth, y - 0.5 * srcHeight]
This says that the pixel at (x, y) comes from the source pixel that is half the source width to its left and half the source height above it. In other words, the image is moved down and to the right. To make this more obvious still, let's add some concrete numbers. We'll start by with the pixel at (0, 0) in the destination image. According to the equation above, the color for this pixel comes from the pixel at (0 - 0.5 * 340, 0 - 0.5 * 255) = (-170, -127). Because there is no such point, this pixel is not set, so this part of the destination is transparent. In fact, every pixel for which either of the source coordinates is negative will be transparent. The first pixel in the destination image that will not be transparent is the one at (170, 127), which gets its color from the pixel at (0, 0) in the source. By following this reasoning for any given pixel in the destination image, it is easy to see why the result of this effect is to move the source down and to the right, as shown in Figure 20-25.
The wrap Variable
You can achieve a slightly different effect to that shown above by setting the wrap variable of the DisplacementMap object to true. When you do this, the parts of the destination that would have been transparent because they correspond to points in the source image that are outside of its bounds (for example, those with negative coordinates) are populated by wrapping the coordinates modulo the size of the source. This means, for example, that the pixel at (0,0), which should come from (-170, -127) in the source, will actually come from (-170 + 340, -127 + 128), or (170, 1). You can see the overall effect of this by running the code in the file javafxeffects/DisplacementMap2.fx, which gives the result shown in Figure 20-26.
Figure 20-26 A DisplacementMap with wrap enabled
The offset Variables
Now that you have seen how the values in the map work in the simplest case, we'll make things a little more complex by adding back the offsetX and offsetY values. These values simply add a fixed offset to the distance between the destination pixel and the source pixel that supplies its color. Like the entries in the map, each offsets is scaled by the width or height of the source, as appropriate.
For example, let's suppose that we were to set the offsetX variable to 0.1 and leave offsetY as 0. Then, using the same map as we did for the previous example, the equations that relate the source and destination pixel locations would now be as follows:
dst[x, y] = src[x + 0.1 * srcWidth- 0.5 * srcWidth, y - 0.5 * srcHeight]
If, as before, the source is 340 pixels wide, this change would produce an additional offset of 34 pixels between the source and destination pixels.
You can see how the offsetX value works by running the code in the file javafxeffects/DisplacementMap3.fx. This example uses the same FloatMap as the previous one, but adds a slider that allows you to vary the offsetX value from 0 up to 1.0, with the initial value being 0.0. Initially, the result looks the same as before, because the offsetX value is still 0—compare the image on the left of Figure 20-27 with that on the right of Figure 20-25 to see that this is the case.
Figure 20-27 Varying the offsetX value of a DisplacementMap effect
Now if you move the offset slider to the right, you will see that the output image moves to the left. This is the offset at work. The farther you move the slider to the right, the more the result shifts to the left. The same effect would be seen along the y-axis if we had added a slider that allowed you to vary the offsetY value.
The scale Variables
The scaleX and scaleY variables are multipliers that are applied to the values from the FloatMap. If you use a scaleX value that is greater than 1, you make the offset between the source and destination pixels larger than that specified in the map. A scaleX value of 2 would double the offsets specified in the map. Similarly, if you use a value that is less than 1, the offset gets smaller. It is also possible to use a negative value, which would reverse the effect of the map.
The code in javafxeffects/DisplacementMap3.fx also includes a slider that lets you change the value of the scaleX variable over the range 0 to 2, with 1 as its initial value. If you move the slider, you will find that the output image also moves to reflect the magnified or reduced offset values. In this case, because every entry in the map has the same value, the effect is very similar to that obtained by changing the offsetX value, but this is not always the case, as you'll see later in this section.
Using the DisplacementMap to Create a Warp
The example that we have been using has the same value in every element of the map. This is a rather unusual case and it doesn't produce a very interesting effect. In this section, we'll take a look at how to create a warp effect by populating the map with values that depend on their position in the map. The completed effect is shown in Figure 20-28.
Figure 20-28 Using DisplacementMap to create a warped effect
As you can probably tell, the effect is produced by simulating the effect of a wave moving in the direction of the y-axis, which causes successive pixel rows to be displaced to the left or right of their initial positions. As there is no movement of any kind in the y direction, you can immediately conclude that all the y values (those in the second band) in the map are 0. The wave effect is, in fact, a sine wave. Here's the code that populates the map 7:
1 var image: Image = Image { url: "{__DIR__}image1.jpg" }; 2 var imgWidth = image.width as Integer; 3 var imgHeight = image.height as Integer; 4 var map: FloatMap = FloatMap { 5 width: imgWidth 6 height: imgHeight 7 } 8 9 for (i in [0..<map.width]) { 10 for (j in [0..<map.height]) { 11 var value = (Math.sin(j/30.0 * Math.PI)/10; 12 map.setSample(i, j, 0, value); 13 } 14 }
The part that does all the interesting work is on line 11. It is obvious that this is creating a sine wave by supplying the horizontal displacement (in the first band of the map) for each row of the input source based on the value of the Math.sin() function. The value of this function varies from 0 at 0 radians to 1 at PI/2 radians, back to zero at PI radians, to -1 at 3*PI/2 radians, and then back to zero at 2 *PI radians, and so on. In the inner loop, the value represents the pixel row. We divide it by 30 and multiply it by PI so that we get a complete wave over the space of 30 pixels. If you make this number larger, you will find that the wave spaces out more. This code would place values ranging from +1 to -1 in every element of the map. Remembering that these offsets are multiplied by the width of the source, this would mean that the image would be distorted by up to its full width. To reduce the distortion, we divide every value by 10, so we end up with values in the range -0.1 to +0.1. That's all we need to do to create a warp effect.
If you run the code in the file javafxeffects/DisplacementMap4.fx, you can use the offset and scale sliders to change the parameters of the DisplacementMap. Notice that changing the scale increases or decreases the amplitude of the sine wave, which results in more or less distortion.