Introducing Custom Paints to JavaFX Blog


    If you've read my previous article, Introducing Custom Cursors To JavaFX, you know that I l ike 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 toTexturePaint, BrownianPaint, andPlasmaPaint. Each of thesejavafx.scene.paint.Paint subclasses relies on an undocumented Paint function to make its underlyingjava.awt.Paint available to JavaFX. Furthermore, these classes rely on an undocumented toolkit's subclass for filling a stage's scene.


    When JavaFX 1.2 debuted, I was surprised by the absence of ajavafx.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 includeTexturePaint in this release.

    I base my TexturePaint JavaFX class on Java SE 6'sjava.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 ajava.awt.image.BufferedImage containing the image-based texture. TexturePaint's Javadoc recommends that the size of this object be kept small "because theBufferedImage data is copied by theTexturePaint object."
    • anchor references ajava.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 JavaFXTexturePaint 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 (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.1TexturePaintDemo project) presents theTexturePaint 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 itsPaint superclass's abstractimpl_getPlatformPaint() function to return ajava.awt.Paint.

    The JavaFX runtime invokes this function whenever a newTexturePaint instance is assigned to ajavafx.scene.shape.Shape subclass's fillor stroke variable. (It's also invoked when a newTexturePaint instance is assigned tojavafx.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 usesjavax.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 aBufferedImage, but just to be safe, the function creates a BufferedImage and populates it with theImage's contents.

    If you haven't assigned values to TexturePaint'swidth and height variables, the image's width and height are chosen, via getIconWidth() andgetIconHeight(), 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.TexturePaintconstructor; the created and initialized instance is returned to the JavaFX runtime.

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

    Listing 2. Main.fx (from aTexturePaintDemo 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 theredworld.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 ifTexturePaint was officially part of JavaFX?)


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


    While searching the internet for additionaljava.awt.Paint possibilities, I encountered the "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 demonstratingBrownianPaint:

    1. Create a BrownianPaintDemo project with a skeletalMain.fx file as the project's initial file.
    2. Add the post's source file to this project, after changing its filename 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 depends upon Ken Perlin's Improved Noise reference implementation, copy Perlin's Java implementation into an 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 (,,; var _colorB = new java.awt.Color (,,; 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 withinimpl_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 andColor.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 aBrownianPaintDemo 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 { () } } 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 JIRAas 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 theRun 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

    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.


    I think that variations of the plasma effectlead 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 ofjavafx.scene.paint.Paint subclass instances, ajavafx.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.

    // 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 implementsjava.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 PlasmaPaintContexthelper class, to the Java/JavaFX runtime.

    PlasmaPaintContext first creates a static map of integer IDs and PlasmaRect instances. ID is thegroupID 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 PlasmaPaintinstances (I present this class later) that share the same group ID to associate with the same PlasmaRect instance. Animating the plasma requires that the PlasmaPaintinstances be alternately assigned to a shape's fill orstroke variable, and they must associate with the samePlasmaRect instance.

    Next, PlasmaPaintContext declares agroupID 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. Awr 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'screateContext() method with groupID's value and createContext()'s deviceBoundsargument (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 storedPlasmaRect'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 preventArrayIndexOutOfBoundsExceptions in thegetRaster() method.

    Because PlasmaPaintContext implements thePaintContext 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 returnColorModel.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. Thex, y, w, and harguments specify the origin and extents (all in device space) of the rectangular area that is to contain the plasma color data.

    Additionally, PlasmaPaintContext provides anupdatePlasma() 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 JavaFXPlasmaPaint class. Listing 6 presentsPlasmaPaint'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 0default, or to an explicit ID, its replace trigger assignstrue to changed. This happens prior to the impl_getPlatformPaint() function being invoked by the JavaFX runtime.

    When impl_getPlatformPaint() is invoked, it invokescreatePaint() if changed is set totrue. This function instantiates_PlasmaPaint with the groupID value, and caches the instance, which impl_getPlatformPaint()returns. The changed variable is reset tofalse 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'supdatePlasma() function, which invokes the appropriatePlasmaRect's updatePlasma() function.

    I've created a NetBeans IDE 6.5.1 PlasmaPaintDemoproject that consists of Listings 5 and 6, and Listing 7'sMain.fx code.

    Listing 7. Main.fx (from aPlasmaPaintDemo 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 { (); } } 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 { (); } } ] fill: Color.BLACK } }

    Listing 7 specifies a scene consisting of Rectangleand Text nodes. Each node's fillattribute is ultimately assigned one of twoPlasmaPaint 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 aPauseTransition instance. Every durationmilliseconds, the transition's action() function is invoked. It responds by invoking the currentPlasmaPaint instance's updatePlasma()function to create the next plasma animation frame, and assigning the alternate PlasmaPaint instance to the shape'sfill 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'sfill 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 aPaintToolkit NetBeans IDE 6.5.1 project)

    /* * PaintToolkit.fx */ package painttoolkit; import javafx.scene.paint.*; import; 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 (located in thejavafx-ui-swing.jar archive, in my platform'sC:\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'simpl_getPlatformPaint() function, returning the result.

    To create this project in NetBeans 6.5.1, selectPaintToolkit as a JavaFX application project, andPaintToolkit.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'sdist directory will contain PaintToolkit.jar.

    You'll need to introduce this JAR file toTexturePaintDemo and any other project whose custom paint is to be added to the stage's scene. AssumingTexturePaintDemo is the main project, activate itsProject Properties dialog box, select this dialog's Libraries panel, and click the panel'sAdd JAR/Folder button to add the JAR file to the classpath.

    There's one crucial item left to perform. Select theProject Properties dialog'sRun panel, and enter-Djavafx.toolkit=painttoolkit.PaintToolkit into theJVM Arguments text field. This option tells JavaFX to use PaintToolkit instead ofSwingToolkit. 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'sfill variable in Listing 2.

    textured image fill

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


    TexturePaint, BrownianPaint, andPlasmaPaint 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 mentionedCheckPaint, NoisePaint, andCamoPaint Java classes; this latter class subclassesBrownianPaint. Also, check out "Glossy and Shiny Shapes with Paint and PaintContext" for another possibility.