Skip to main content

Introducing Custom Paints to JavaFX

July 30, 2009

{cs.r.title}







If you've read my previous article, Introducing Custom Cursors To JavaFX, you know that I like to
introduce new features to JavaFX by taking advantage of undocumented capabilities. In this article, I leverage a couple of
undocumented capabilities to support custom paints. You can use a custom paint to render the outline and fill for any shape
(including text), and also to render the fill for the stage's scene.


Supported JavaFX Platforms
As with my custom cursors code, I've tested my custom paint code with JavaFX 1.2 and Java SE 6u12 on a Windows XP SP3
platform, the only platform available to me for development and testing.

Specifically, the article introduces you to TexturePaint, BrownianPaint, and
PlasmaPaint. Each of these javafx.scene.paint.Paint subclasses relies on an undocumented
Paint function to make its underlying java.awt.Paint available to JavaFX. Furthermore, these
classes rely on an undocumented toolkit's subclass for filling a stage's scene.

TexturePaint

When JavaFX 1.2 debuted, I was surprised by the absence of a javafx.scene.paint.TexturePaint class. After all,
this class isn't hard to implement for the desktop profile. However, it's possible that supporting TexturePaint
for the mobile (and television) profiles presents a significant challenge. Then again, maybe there wasn't enough time to
include TexturePaint in this release.

I base my TexturePaint JavaFX class on Java SE 6's java.awt.TexturePaint class. My class invokes
this Java class's public TexturePaint(BufferedImage txtr, Rectangle2D anchor) constructor to instantiate a
texture paint, and makes the resulting texture available to the JavaFX runtime:

  • txtr references a java.awt.image.BufferedImage containing the image-based texture.
    TexturePaint's Javadoc recommends that the size of this object be kept small "because the
    BufferedImage data is copied by the TexturePaint object."
  • anchor references a java.awt.geom.Rectangle2D containing the origin and extents (in user space, as
    opposed to device space) of that portion of the BufferedImage that will serve as the texture. The texture is
    anchored to the origin.

Listing 1 presents the source code for the JavaFX TexturePaint class.

Listing 1. TexturePaint.fx

/*
* TexturePaint.fx
*/

package texturepaintdemo;

import java.awt.geom.Rectangle2D;

import java.awt.image.BufferedImage;

import javax.swing.ImageIcon;

import javafx.scene.paint.Paint;

public class TexturePaint extends Paint
{
    public-init var url: String;
    public-init var x: Number;
    public-init var y: Number;
    public-init var width: Number;
    public-init var height: Number;

    public override function impl_getPlatformPaint ()
    {
        var image = new ImageIcon (new java.net.URL (url));
        var bi = new BufferedImage (image.getIconWidth (),
                                    image.getIconHeight (),
                                    BufferedImage.TYPE_INT_RGB);
        var g = bi.createGraphics ();
        g.drawImage (image.getImage (), 0, 0, null);
        g.dispose ();
        var _width = if (width == 0) then image.getIconWidth () else width;
        var _height = if (height == 0) then image.getIconHeight () else height;
        var anchor = new Rectangle2D.Double (x, y, _width, _height);
        new java.awt.TexturePaint (bi, anchor)
    }
}
TexturePaint.fx (part of a NetBeans IDE 6.5.1 TexturePaintDemo project) presents the
TexturePaint class. In addition to providing variables for the texture image file's URL, the texture image's
anchor origin, and the anchor extents, this class overrides its Paint superclass's abstract
impl_getPlatformPaint() function to return a java.awt.Paint.

The JavaFX runtime invokes this function whenever a new TexturePaint instance is assigned to a
javafx.scene.shape.Shape subclass's fill or stroke variable. (It's also invoked when a
new TexturePaint instance is assigned to javafx.scene.Scene's fill variable, but a
little help is required to make this happen, as you'll discover later in this article.)

The impl_getPlatformPaint() function uses javax.swing.ImageIcon to load the image, which is made
available via its getImage() method. It's possible that the returned java.awt.Image is actually a
BufferedImage, but just to be safe, the function creates a BufferedImage and populates it with the
Image's contents.

If you haven't assigned values to TexturePaint's width and height variables, the
image's width and height are chosen, via getIconWidth() and getIconHeight(), as the texture's
anchor extents. However, if you assign values to these variables, these values will be used as the extents.

Finally, an anchor rectangle is created, using the specified x and y variable values
as the origin where (0, 0) is the default, and using the chosen width and height as the extents. Both the rectangle and
buffered image objects are passed to the java.awt.TexturePaint constructor; the created and initialized instance
is returned to the JavaFX runtime.

Let's play with TexturePaint. Listing 2 presents the TexturePaintDemo project's
Main.fx source code.

Listing 2. Main.fx (from a TexturePaintDemo NetBeans IDE 6.5.1 project)

/*
* Main.fx
*/

package texturepaintdemo;

import javafx.scene.Scene;

import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Reflection;

import javafx.scene.paint.Color;

import javafx.scene.shape.Rectangle;

import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextOrigin;

import javafx.stage.Stage;

Stage
{
    title: "TexturePaintDemo"
    var scene: Scene
    scene: scene = Scene
    {
        width: 600
        height: 300

        var text: Text
        content:
        [
            Rectangle
            {
                x: 20
                y: 20
                width: bind scene.width-40
                height: bind scene.height-40
                fill: TexturePaint
                {
                    url: "{__DIR__}res/craters.jpg"
                    x: 15
                    y: 15
                    width: 50
                    height: 50
                }
            }

            text = Text
            {
                content: "TexturePaintDemo"
                fill: TexturePaint
                {
                    url: "{__DIR__}res/nebula.jpg"
                }
                stroke: TexturePaint { url: "{__DIR__}res/redworld.jpg" }
                strokeWidth: 3
                translateX: bind (scene.width-text.layoutBounds.width)/2
                translateY: bind (scene.height-text.layoutBounds.height)/2
                textOrigin: TextOrigin.TOP
                font: Font { name: "Arial BOLD" size: 60 }
                effect: Reflection { input: DropShadow {} }
            }
        ]
        fill: Color.BLACK
    }
}

Listing 2 describes a simple scene consisting of a text node centered on a rectangle node, which is centered on the stage.
The reflected text is outlined with a texture based on the redworld.jpg image file, and filled with a texture
based on the nebula.jpg image file. The rectangle is filled with a texture based on the craters.jpg
image file.

Figure 1 shows you this textured scene. (Wouldn't it be nice if TexturePaint was officially part of JavaFX?)

TexturePaint

Figure 1. TexturePaint lets you leverage images for rendering shape outlines and fills.

BrownianPaint

While searching the internet for additional java.awt.Paint possibilities, I encountered the " href="http://drj11.wordpress.com/2007/04/02/subclassing-javaawtpaint/">Subclassing java.awt.Paint" blog post by David Jones. This post introduces a variety of
java.awt.Paint implementations: CheckPaint, NoisePaint, BrownianPaint,
and CamoPaint. Jones graciously allowed me to adapt his classes to JavaFX, and I chose to adapt
BrownianPaint.

I chose BrownianPaint because it reminds me of the hyperspace visual effect seen in the Babylon 5 television
series. Rather than explain the concepts behind this paint's Java implementation, I refer you to Jones's blog post for the
details. Start your copy of NetBeans 6.5.1 and complete the following steps, to provide the necessary infrastructure
for demonstrating BrownianPaint:

  1. Create a BrownianPaintDemo project with a skeletal Main.fx file as the project's initial file.
  2. Add the post's BrownianPaint.java source file to this project, after changing its filename to
    _BrownianPaint.java and its classname to _BrownianPaint, to distinguish this classname from Listing
    3's BrownianPaint classname. Also, make sure that this source file begins with a
    package 
    brownianpaintdemo;
    statement.
  3. Because _BrownianPaint.java depends upon Ken Perlin's Improved Noise reference
    implementation
    , copy Perlin's Java implementation into an ImprovedNoise.java source file, and add the file
    to the project. Also, make sure that this source file begins with a package brownianpaintdemo; statement.
  4. Introduce Listing 3 into the project.

Listing 3. BrownianPaint.fx

/*
* BrownianPaint.fx
*/

package brownianpaintdemo;

import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;

public class BrownianPaint extends Paint
{
    public-init var colorA: Color = Color.RED on replace
    {
        changed = true
    }

    public-init var colorB: Color = Color.BLACK on replace
    {
        changed = true
    }

    var changed: Boolean;
    var paintAWT: java.awt.Paint;

    function createPaint (): Void
    {
        var _colorA = new java.awt.Color (colorA.red, colorA.green, colorA.blue);
        var _colorB = new java.awt.Color (colorB.red, colorB.green, colorB.blue);
        paintAWT = new _BrownianPaint (_colorA, _colorB, 1, 2.1753974, 5);
        changed = false
    }

    public override function impl_getPlatformPaint ()
    {
        if (changed)
            createPaint ();
        paintAWT
    }
}

Although similar to TexturePaint.fx, Listing 3 attempts to be more efficient by only creating (from within
impl_getPlatformPaint()) the _BrownianPaint instance when changed is set to
true. This is done because the JavaFX runtime can invoke impl_getPlatformPaint() multiple times.

The _BrownianPaint constructor takes five arguments that are explained in the blog
post
. I've chosen to make only the two color arguments available to JavaFX via BrownianPaint variables (and
have defaulted them to Color.RED and Color.BLACK). As an exercise, introduce variables for the
three numeric arguments.

Now that the supporting infrastructure is in place, we need a suitable demonstration. For example, I've created an
application that animates the font size of two text messages from an initial small setting to a cutoff value, and back to the
initial size. One of the message's fill is rendered with BrownianPaint; the other message's outline is rendered
with this class.

Listing 4 reveals the application's Main.fx source code.

Listing 4. Main.fx (from a BrownianPaintDemo NetBeans IDE 6.5.1 project)

/*
* Main.fx
*/

package brownianpaintdemo;

import javafx.animation.transition.PauseTransition;

import javafx.scene.Scene;

import javafx.scene.effect.Reflection;

import javafx.scene.input.MouseEvent;

import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

import javafx.scene.shape.Rectangle;

import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextOrigin;

import javafx.stage.Stage;

def BACKGROUND_PAINT = LinearGradient
{
    startX: 0.0
    startY: 0.0
    endX: 1.0
    endY: 1.0
    stops:
    [
        Stop { offset: 0.0 color: Color.ORANGE },
        Stop { offset: 0.5 color: Color.PINK },
        Stop { offset: 1.0 color: Color.YELLOW }
    ]
}

def CUTOFF = 46;

Stage
{
    title: "BrownianPaintDemo"

    var scene: Scene
    scene: scene = Scene
    {
        width: 600
        height: 300

        var size: Number = 8;
        var font = Font { name: "Arial BOLD" size: 8 }
        def PT = PauseTransition
        {
            duration: 75ms
            action: function (): Void
            {
                if (++size > CUTOFF)
                    size = 8;

                font = Font { name: "Arial BOLD" size: size }
            }
            repeatCount: PauseTransition.INDEFINITE
        }

        var text: Text
        var text2: Text
        content:
        [
            Rectangle
            {
                width: bind scene.width
                height: bind scene.height
                fill: BACKGROUND_PAINT
                onMouseClicked: function (me: MouseEvent): Void
                {
                    PT.play ()
                }
            }

            text = Text
            {
                content: "BrownianPaintDemo"
                fill: BrownianPaint {}
                translateX: bind (scene.width-text.layoutBounds.width)/2
                translateY: bind (scene.height-text.layoutBounds.height)/2-60
                textOrigin: TextOrigin.TOP
                font: bind font
                effect: Reflection {}
            }

            text2 = Text
            {
                content: "BrownianPaintDemo"
                stroke: BrownianPaint { colorA: Color.CYAN }
                strokeWidth: bind font.size*0.05
                translateX: bind (scene.width-text2.layoutBounds.width)/2
                translateY: bind (scene.height-text2.layoutBounds.height)/2+60
                textOrigin: TextOrigin.TOP
                font: bind font
                effect: Reflection {}
            }
        ]
    }
}

A curious thing about JavaFX 1.2 is that it cannot fill text rendered with a gradient or many custom paints
(TexturePaint is immune) past a certain font size on Windows platforms (the cutoff varies based on font). For
example, Listing 4's Brownian noise-filled text message disappears if its font size exceeds 46. (This problem probably
doesn't occur on other platforms.)

The problem has been reported to JIRA as the issue Gradient filling for the Text
node does not work
. It appears that the problem is related to Direct3D, and can be overcome by inserting
-Dsun.java2d.d3d=false (to disable Direct3D rendering) into the JVM Arguments field on the
Run panel of the NetBeans Project Properties dialog.

Run BrownianPaintDemo and you'll notice both text instances rendered with a small font size. Click the mouse
anywhere in the scene to begin the animation. The animation starts off fast, but slows down because it takes longer to render
Brownian noise at larger sizes -- perhaps you might want to try your hand at optimizing _BrownianPaint.java.

Figure 2 reveals the text messages filled and outlined with red and cyan Brownian noise.

The Brownian noise

Figure 2. The Brownian noise shifts around as the text's font size increases.

PlasmaPaint

I think that variations of the plasma effect lead to some of the nicest-looking custom paints. One
very cool-looking plasma effect can be seen in Robert Walsh's Plasma applet. After observing this
applet for a little while, I knew that I had to introduce this effect to JavaFX as an animated paint, and Walsh graciously
gave me permission to do so.

Perhaps you're wondering how to animate a paint in JavaFX. The solution to this problem involves a pair of
javafx.scene.paint.Paint subclass instances, a javafx.animation.transition.PauseTransition
instance, an update function in the Paint subclass, and a suitable Java paint class with a supporting
infrastructure.

Listing 5 presents the "suitable paint class with a supporting infrastructure" part of the solution.

Listing 5. _PlasmaPaint.java

// _PlasmaPaint.java

package plasmapaintdemo;

import java.awt.Paint;
import java.awt.PaintContext;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Transparency;

import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

import java.awt.image.ColorModel;
import java.awt.image.DataBufferInt;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;

import java.util.HashMap;
import java.util.Map;

import java.util.Random;

public class _PlasmaPaint implements Paint
{
    private int groupID;

    private PlasmaPaintContext context;

    public _PlasmaPaint (int groupID)
    {
        this.groupID = groupID;
    }

    @Override
    public PaintContext createContext (ColorModel cm, Rectangle deviceBounds,
                                       Rectangle2D userBounds,
                                       AffineTransform xForm,
                                       RenderingHints hints)
    {
        return context = new PlasmaPaintContext (groupID, deviceBounds);
    }

    @Override
    public int getTransparency ()
    {
        return Transparency.OPAQUE;
    }

    public void updatePlasma ()
    {
        if (context != null)
            context.updatePlasma ();
    }
}

class PlasmaPaintContext implements PaintContext
{
    private static Map<Integer, PlasmaRect> map =
        new HashMap<Integer, PlasmaRect> ();

    private int groupID;

    private WritableRaster wr;

    PlasmaPaintContext (int groupID, Rectangle deviceBounds)
    {
        this.groupID = groupID;
        int width = deviceBounds.x+deviceBounds.width;
        int height = deviceBounds.y+deviceBounds.height;
        if (map.get (groupID) == null || map.get (groupID).getWidth () != width
            || map.get (groupID).getHeight () != height)
            map.put (groupID, new PlasmaRect (width, height));
    }

    @Override
    public void dispose ()
    {
        wr = null;
    }

    @Override
    public ColorModel getColorModel ()
    {
        return ColorModel.getRGBdefault ();
    }

    @Override
    public synchronized Raster getRaster (int x, int y, int w, int h)
    {
        wr = getColorModel ().createCompatibleWritableRaster (w, h);
        DataBufferInt dbi = (DataBufferInt) wr.getDataBuffer ();
        int[] pixels = dbi.getBankData ()[0];
        int index = 0;
        for (int row = y; row < y+h; row++, index += w)
            System.arraycopy (map.get (groupID).pixels [row], x, pixels, index,
                              w);
        return wr;
    }

    void updatePlasma ()
    {
        map.get (groupID).updatePlasma ();
    }
}

class PlasmaRect
{
    int [][] pixels;

    private int GridMX, GridMY;
    private double MaxC, cScale;
    private double RX2, RY2, GX2, GY2, BX2, BY2;
    private double RXA2, RYA2, GXA2, GYA2, BXA2, BYA2;
    private double RX1, RY1, GX1, GY1, BX1, BY1;
    private double RXA1, RYA1, GXA1, GYA1, BXA1, BYA1;
    private double[][] GridR;
    private double[][] GridG;
    private double[][] GridB;
    private Random rand = new Random ();

    PlasmaRect (int width, int height)
    {
        GridMX = width;
        GridMY = height;

        pixels = new int [GridMY][];
        for (int i = 0; i < pixels.length; i++)
             pixels [i] = new int [GridMX];

        MaxC = Math.sqrt (((GridMX-1.0)*(GridMX-1.0))+
                          ((GridMY-1.0)*(GridMY-1.0)));
        cScale = MaxC/100.0;

        GridR = new double [GridMX+1][GridMY+1];
        GridG = new double [GridMX+1][GridMY+1];
        GridB = new double [GridMX+1][GridMY+1];

        RX1 = rand.nextInt (GridMX);
        RY1 = rand.nextInt (GridMY);
        GX1 = rand.nextInt (GridMX);
        GY1 = rand.nextInt (GridMY);
        BX1 = rand.nextInt (GridMX);
        BY1 = rand.nextInt (GridMY);

        RX2 = rand.nextInt (GridMX);
        RY2 = rand.nextInt (GridMY);
        GX2 = rand.nextInt (GridMX);
        GY2 = rand.nextInt (GridMY);
        BX2 = rand.nextInt (GridMX);
        BY2 = rand.nextInt (GridMY);

        double xr = GridMX/20.0;
        double yr = GridMY/20.0;

        RXA1 = rand.nextInt ((int)(xr*10))/xr-(xr/2.0);
        RYA1 = rand.nextInt ((int)(yr*10))/yr-(yr/2.0);
        GXA1 = rand.nextInt ((int)(xr*10))/xr-(xr/2.0);
        GYA1 = rand.nextInt ((int)(yr*10))/yr-(yr/2.0);
        BXA1 = rand.nextInt ((int)(xr*10))/xr-(xr/2.0);
        BYA1 = rand.nextInt ((int)(yr*10))/yr-(yr/2.0);

        RXA2 = rand.nextInt ((int)(xr*10))/xr-(xr/2.0);
        RYA2 = rand.nextInt ((int)(yr*10))/yr-(yr/2.0);
        GXA2 = rand.nextInt ((int)(xr*10))/xr-(xr/2.0);
        GYA2 = rand.nextInt ((int)(yr*10))/yr-(yr/2.0);
        BXA2 = rand.nextInt ((int)(xr*10))/xr-(xr/2.0);
        BYA2 = rand.nextInt ((int)(yr*10))/yr-(yr/2.0);

        for (int x = 0; x < GridMX; x++)
            for (int y = 0; y < GridMY; y++)
            {
                GridR [x][y] = (int)(((float)x/(float)GridMX)*(float)255);
                GridG [x][y] = (int)(((float)y/(float)GridMY)*(float)255);
                GridB [x][y] = 127;
            }

        updatePlasma ();
    }

    int getHeight () { return GridMY; }

    int getWidth () { return GridMX; }
   
    void updatePlasma ()
    {
        if (((RX1+RXA1) >= GridMX) || ((RX1+RXA1) < 0)) RXA1 = -RXA1;
        if (((RY1+RYA1) >= GridMY) || ((RY1+RYA1) < 0)) RYA1 = -RYA1;
        if (((GX1+GXA1) >= GridMX) || ((GX1+GXA1) < 0)) GXA1 = -GXA1;
        if (((GY1+GYA1) >= GridMY) || ((GY1+GYA1) < 0)) GYA1 = -GYA1;
        if (((BX1+BXA1) >= GridMX) || ((BX1+BXA1) < 0)) BXA1 = -BXA1;
        if (((BY1+BYA1) >= GridMY) || ((BY1+BYA1) < 0)) BYA1 = -BYA1;

        if (((RX2+RXA2) >= GridMX) || ((RX2+RXA2) < 0)) RXA2 = -RXA2;
        if (((RY2+RYA2) >= GridMY) || ((RY2+RYA2) < 0)) RYA2 = -RYA2;
        if (((GX2+GXA2) >= GridMX) || ((GX2+GXA2) < 0)) GXA2 = -GXA2;
        if (((GY2+GYA2) >= GridMY) || ((GY2+GYA2) < 0)) GYA2 = -GYA2;
        if (((BX2+BXA2) >= GridMX) || ((BX2+BXA2) < 0)) BXA2 = -BXA2;
        if (((BY2+BYA2) >= GridMY) || ((BY2+BYA2) < 0)) BYA2 = -BYA2;

        RX1 += RXA1;
        RY1 += RYA1;
        GX1 += GXA1;
        GY1 += GYA1;
        BX1 += BXA1;
        BY1 += BYA1;

        RX2 += RXA2;
        RY2 += RYA2;
        GX2 += GXA2;
        GY2 += GYA2;
        BX2 += BXA2;
        BY2 += BYA2;

        for (int x = 0; x < GridMX; x++)
            for (int y = 0; y < GridMY; y++)
            {
                GridR [x][y] += GetShade (x-RX1, y-RY1);
                GridG [x][y] += GetShade (x-GX1, y-GY1);
                GridB [x][y] += GetShade (x-BX1, y-BY1);

                GridR [x][y] -= GetShade (x-RX2, y-RY2);
                GridG [x][y] -= GetShade (x-GX2, y-GY2);
                GridB [x][y] -= GetShade (x-BX2, y-BY2);

                if (GridR [x][y] > 255) GridR [x][y] = 255;
                if (GridG [x][y] > 255) GridG [x][y] = 255;
                if (GridB [x][y] > 255) GridB [x][y] = 255;

                if (GridR [x][y] < 0) GridR [x][y] = 0;
                if (GridG [x][y] < 0) GridG [x][y] = 0;
                if (GridB [x][y] < 0) GridB [x][y] = 0;

                pixels [y][x] = 0xff000000 | ((int)GridR [x][y]<<16) |
                                ((int)GridG [x][y]<<8) | ((int)GridB [x][y]);
            }
    }

    private double GetShade (double a, double b)
    {
        return (1.0-(Math.sqrt (a*a+b*b)/MaxC))*cScale;
    }
}

Listing 5 introduces a java.awt.Paint subclass named _PlasmaPaint. This device-independent class
provides a groupID field (I explain group IDs later), and a constructor that saves its groupID
argument in this field. This class also provides a context field and an updatePlasma() method that
works with this field to update plasma colors.

_PlasmaPaint also implements java.awt.Paint's
PaintContext createContext(ColorModel cm, 
Rectangle deviceBounds, Rectangle2D userBounds, AffineTransform xform, RenderingHints hints)
method to return a
java.awt.PaintContext, specifically an instance of the device-dependent PlasmaPaintContext helper
class, to the Java/JavaFX runtime.

PlasmaPaintContext first creates a static map of integer IDs and PlasmaRect instances. ID is the
groupID value that's originally passed to the _PlasmaPaint constructor. PlasmaRect is
a helper class that knows how to render and update a plasma rectangle's pixels -- I discuss this class later.

The map allows an arbitrary number of PlasmaPaint instances (I present this class later) that share the same
group ID to associate with the same PlasmaRect instance. Animating the plasma requires that the
PlasmaPaint instances be alternately assigned to a shape's fill or stroke variable,
and they must associate with the same PlasmaRect instance.

Next, PlasmaPaintContext declares a groupID field that stores the ID of the group to which the
context associates. It will need this field's value to obtain the appropriate PlasmaRect instance from the map.
A wr writable raster field is also declared, and its purpose will become clear in a little while.

We now arrive at PlasmaPaintContext's constructor, which is invoked from _PlasmaPaint's
createContext() method with groupID's value and createContext()'s
deviceBounds argument (passed to this method from the Java/JavaFX runtime). In addition to saving the group ID
value, the constructor determines if it needs to create a PlasmaRect instance and save it in the map.

If the map doesn't contain a PlasmaRect instance for the associated group ID value, the instance must be
created. I've discovered that an instance must also be created if the stored PlasmaRect's width or height
doesn't match the width or height calculated from deviceBounds values -- x and y must
be included in the calculations to prevent ArrayIndexOutOfBoundsExceptions in the getRaster()
method.

Because PlasmaPaintContext implements the PaintContext interface, it's required to provide the
following three methods:

  • void dispose() releases any resources that have been allocated by the plasma paint context. This method is
    implemented to garbage collect the most recently allocated writable raster.
  • ColorModel getColorModel() returns the raster's associated color model. This method is implemented to return
    ColorModel.getRGBdefault(), so that the raster that's created (in the getRaster() method) is
    guaranteed to be organized according to the RGB packed-integer model.
  • Raster getRaster(int x, int y, int w, int h) returns the raster containing the plasma color data. The
    x, y, w, and h arguments specify the origin and extents (all in device
    space) of the rectangular area that is to contain the plasma color data.

Additionally, PlasmaPaintContext provides an updatePlasma() method. This method is invoked from
within _PlasmaPaint's updatePlasma() method, and it invokes the updatePlasma() method
in the PlasmaRect instance associated with the current group ID. This method is what allows the next plasma
animation frame to be generated.

This leaves us with the PlasmaRect class. Because I've tried to follow Walsh's coding model, I've made few
changes to his code, and refer you to his plasma tutorial page to learn how this code works. Once
you familiarize yourself with how the code works, I challenge you to adapt the logic to use integer calculations instead of
floating-point calculations, to speed up the animation.

_PlasmaPaint is instantiated from a JavaFX PlasmaPaint class. Listing 6 presents
PlasmaPaint's source code.

Listing 6. PlasmaPaint.fx

/*
* PlasmaPaint.fx
*/

package plasmapaintdemo;

import javafx.scene.paint.Paint;

public class PlasmaPaint extends Paint
{
    public-init var groupID: Integer on replace
    {
        changed = true
    }

    var changed: Boolean;
    var paintAWT: java.awt.Paint;

    function createPaint (): Void
    {
        paintAWT = new _PlasmaPaint (groupID);
        changed = false
    }

    public override function impl_getPlatformPaint ()
    {
        if (changed)
            createPaint ();
        paintAWT
    }

    public function updatePlasma ()
    {
        (paintAWT as _PlasmaPaint).updatePlasma ()
    }
}

Listing 6 provides a groupID variable for specifying a PlasmaPaint instance's group ID. In response
to this variable initializing to its 0 default, or to an explicit ID, its replace trigger assigns
true to changed. This happens prior to the impl_getPlatformPaint() function being
invoked by the JavaFX runtime.

When impl_getPlatformPaint() is invoked, it invokes createPaint() if changed is set to
true. This function instantiates _PlasmaPaint with the groupID value, and caches the
instance, which impl_getPlatformPaint() returns. The changed variable is reset to
false so that createPaint() isn't called unnecessarily.

Listing 6 also provides the updatePlasma() function for generating the next plasma animation frame. This
function call invokes _PlasmaPaint's updatePlasma() function, which invokes its
PlasmaPaintContext's updatePlasma() function, which invokes the appropriate
PlasmaRect's updatePlasma() function.

I've created a NetBeans IDE 6.5.1 PlasmaPaintDemo project that consists of Listings 5 and 6, and Listing 7's
Main.fx code.

Listing 7. Main.fx (from a PlasmaPaintDemo NetBeans IDE 6.5.1 project)

/*
* Main.fx
*/

package plasmapaintdemo;

import javafx.animation.transition.PauseTransition;

import javafx.scene.Scene;

import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Reflection;

import javafx.scene.input.MouseEvent;

import javafx.scene.paint.Color;

import javafx.scene.shape.Rectangle;

import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextOrigin;

import javafx.stage.Stage;

Stage
{
    title: "PlasmaPaintDemo"
    var scene: Scene
    scene: scene = Scene
    {
        width: 600
        height: 300

        var text: Text

        content:
        [
            Rectangle
            {
                def pp1 = PlasmaPaint { groupID: 1 }
                def pp2 = PlasmaPaint { groupID: 1 }
                var pp = pp1;

                def pause = PauseTransition
                {
                    duration: 65ms
                    action: function (): Void
                    {
                        pp.updatePlasma ();
                        pp = if (pp == pp1) then pp2 else pp1
                    }
                    repeatCount: PauseTransition.INDEFINITE
                }

                x: 20
                y: 20
                width: bind scene.width-40
                height: bind scene.height-40
                arcWidth: 25
                arcHeight: 25
                fill: bind pp

                onMouseClicked: function (me: MouseEvent): Void
                {
                    pause.play ();
                }
            }

            text = Text
            {
                content: "PlasmaPaintDemo"
                fill: bind pp
                translateX: bind (scene.width-text.layoutBounds.width)/2
                translateY: bind (scene.height-text.layoutBounds.height)/2
                textOrigin: TextOrigin.TOP
                font: Font { name: "Arial BOLD" size: 46 }
                effect: Reflection { input: DropShadow {} }
                blocksMouse: true
                def pp1 = PlasmaPaint { groupID: 2 }
                def pp2 = PlasmaPaint { groupID: 2 }
                var pp = pp1;

                def pause = PauseTransition
                {
                    duration: 60ms
                    action: function (): Void
                    {
                        pp.updatePlasma ();
                        pp = if (pp == pp1) then pp2 else pp1
                    }
                    repeatCount: PauseTransition.INDEFINITE
                }
                onMouseClicked: function (me: MouseEvent): Void
                {
                    pause.play ();
                }
            }
        ]
        fill: Color.BLACK
    }
}

Listing 7 specifies a scene consisting of Rectangle and Text nodes. Each node's fill
attribute is ultimately assigned one of two PlasmaPaint instances. You'll notice that each pair of instances has
the same groupID value. These values must be identical or the shape's plasma fill won't animate properly.

Each shape's animation is controlled via a PauseTransition instance. Every duration milliseconds,
the transition's action() function is invoked. It responds by invoking the current PlasmaPaint
instance's updatePlasma() function to create the next plasma animation frame, and assigning the alternate
PlasmaPaint instance to the shape's fill variable (via binding), which causes JavaFX to repaint the
shape.

Figure 3 reveals one frame from the plasma-shape animations.

animate plasma paint

Figure 3. Click each shape to animate its plasma paint.

Supporting Custom Paints with PaintToolkit

There's a problem with TexturePaint, BrownianPaint, PlasmaPaint, and any custom
Paint that you create. If you assign an instance of a custom paint class to Scene's
fill variable, you won't observe the scene painted with the custom paint. I won't delve into why this happens,
except to say that JavaFX 1.2's runtime architecture is responsible.

Listing 8 provides a solution to this problem.

Listing 8. PaintToolkit.fx (from a PaintToolkit NetBeans IDE 6.5.1 project)

/*
* PaintToolkit.fx
*/

package painttoolkit;

import javafx.scene.paint.*;

import com.sun.javafx.tk.swing.SwingToolkit;

public class PaintToolkit extends SwingToolkit
{
    public override function createPaint (paint: Paint): java.lang.Object
    {
        println (paint);

        if (paint instanceof Color or paint instanceof LinearGradient or
            paint instanceof RadialGradient)
            return super.createPaint (paint);

        paint.impl_getPlatformPaint ()
    }
}

Listing 8 solves the problem by subclassing com.sun.javafx.tk.swing.SwingToolkit (located in the
javafx-ui-swing.jar archive, in my platform's C:\Program Files\NetBeans
6.5.1\javafx2\javafx-sdk\lib\desktop
directory), and overriding its inherited createPaint(paint: Paint)
function to invoke a custom paint instance's impl_getPlatformPaint() function, returning the result.

To create this project in NetBeans 6.5.1, select PaintToolkit as a JavaFX application project, and
PaintToolkit.fx as the main source file. Copy Listing 8 over the NetBeans-generated PaintToolkit.fx
skeletal source code and build the project. Assuming success, this project's dist directory will contain
PaintToolkit.jar.

You'll need to introduce this JAR file to TexturePaintDemo and any other project whose custom paint is to be
added to the stage's scene. Assuming TexturePaintDemo is the main project, activate its

Project 
Properties
dialog box, select this dialog's Libraries panel, and click the panel's
Add JAR/Folder button to add the JAR file to the classpath.

There's one crucial item left to perform. Select the Project Properties dialog's Run
panel, and enter -Djavafx.toolkit=painttoolkit.PaintToolkit into the JVM Arguments text
field. This option tells JavaFX to use PaintToolkit instead of SwingToolkit. You can now assign a
texture paint to the scene's fill variable.

Figure 4 shows the result of assigning TexturePaint { url: "{__DIR__}res/nebula.jpg" } to the
Scene's fill variable in Listing 2.

textured image fill

Figure 4. The scene's textured image fill surrounds the rectangle.

Conclusion

TexturePaint, BrownianPaint, and PlasmaPaint prove that you're not limited to solid
color and gradient paints in JavaFX. As an exercise, implement new custom paints that are based on the previously mentioned
CheckPaint, NoisePaint, and CamoPaint Java classes; this latter class subclasses
BrownianPaint. Also, check out "Glossy and Shiny Shapes with Paint and PaintContext" for
another possibility.

Resources


width="1" height="1" border="0" alt=" " />
Jeff Friesen is a freelance software developer and educator specializing in Java technology. Check out his site at javajeff.mb.ca.
Related Topics >> GUI   |   Programming   |   Featured Article   |   

Comments

Please, pretty please,

Please, pretty please, whenever you post javafx related stuff include the live staff as well! Wonder why this is the only comment so far!?