Painter's Canvas
JavaFX 1.2's node-based infrastructure lets you create interesting scenes based on geometric shapes, images, text, controls, and even media. However, this infrastructure doesn't support complex possibilities such as fireworks, plasma, fractals, and even fire. Fortunately, it's easy to solve this problem by using painters and canvases.Painting on a Canvas Node
A painter is an object that knows how to render an image of a specific size. Although it's possible to implement the painter in JavaFX via an abstract class, I've found it more useful to use Listing 1's Java interface.
// Painter.java
package canvasdemo;
import java.awt.image.BufferedImage;
public interface Painter
{
public BufferedImage renderImage (int width, int height);
}
Listing 1: Painter.java
The Java class that implements this interface renders an image
into a java.awt.image.BufferedImage when
renderImage() is invoked. The
BufferedImage's dimensions must match
width and height.
The returned BufferedImage's contents are rendered
onto a canvas, which is an object that functions as an
image-rendering surface. Listing 2 presents the source code to the
canvas's Canvas class.
/*
* Canvas.fx
*/
package canvasdemo;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.ext.swing.SwingUtils;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import javafx.scene.image.ImageView;
public class Canvas extends CustomNode
{
public var width: Number;
public var height: Number;
public var fill: Paint = Color.BLACK;
public var painter: Painter;
public function paint (): Void
{
iv.image = SwingUtils.toFXImage (painter.renderImage (width, height))
}
public override function create (): Node
{
Group
{
content:
[
Rectangle
{
x: 0
y: 0
width: bind width
height: bind height
fill: bind fill
}
iv
]
}
}
var iv: ImageView = ImageView
{
fitWidth: bind width
fitHeight: bind height
}
}
Listing 2: Canvas.fx
Canvas introduces itself as a node by subclassing
javafx.scene.CustomNode and overriding this class's
create() function. This function returns the canvas's
scenegraph: a javafx.scene.image.ImageView on a
javafx.scene.shape.Rectangle.
You can size the canvas by assigning values to its
width and height variables, select the
canvas's initial fill color by assigning a
javafx.scene.paint.Paint instance to its
fill variable, and assign a Painter
instance to Canvas's painter variable.
Subsequent calls to the canvas's paint() function
result in calls to the painter's renderImage()
function. Each returned BufferedImage result is
converted to a javafx.scene.image.Image, which is
assigned to ImageView's image variable.
JavaFX then repaints the canvas.
Plasma Painter
I've created aPlasmaPainter class as a useful
implementation of Painter. This class generates a
plasma effect, which is described by Lode Vandevenne in
his
Plasma tutorial. Check out Listing 3.
// PlasmaPainter.java
package canvasdemo;
import static java.lang.Math.*;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
public class PlasmaPainter implements Painter
{
private int [] palette;
private BufferedImage bi;
private int width, height;
private byte [][] plasma;
private int [] colors;
public PlasmaPainter ()
{
palette = new int [256];
for (int i = 0; i < palette.length; i++)
palette [i] = Color.HSBtoRGB ((float) i/255.0f, 1.0f, 1.0f);
}
@Override
public BufferedImage renderImage (int width, int height)
{
if (bi == null || width != this.width || height != this.height)
{
bi = new BufferedImage (width, height, BufferedImage.TYPE_INT_RGB);
this.width = width;
this.height = height;
plasma = new byte [height][];
for (int i = 0; i < plasma.length; i++)
plasma [i] = new byte [width];
for (int row = 0; row < height; row++)
for (int col = 0; col < width; col++)
plasma [row][col] =
(byte) ((128.0f+128.0f*cos (row/8f)+
128.0f+128.0f*cos (col/8f))/2);
colors = new int [width*height];
}
int shift = (int) System.currentTimeMillis ()/5;
int base = 0;
for (int row = 0; row < height; row++)
{
for (int col = 0; col < width; col++)
colors [base+col] = palette [(plasma [row][col]+shift)&255];
base += width;
}
//bi.setRGB (0, 0, width, height, colors, 0, width);
WritableRaster raster = bi.getRaster ();
raster.setDataElements (0, 0, width, height, colors);
return bi;
}
}
Listing 3: PlasmaPainter.java
PlasmaPainter provides a constructor for one-time
initialization. In this case, it's necessary to create a palette
of RGB values that exhibit no discontinuities (abrupt color
changes) because the palette's colors will be rotated to achieve
animation.
If the user is able to resize a JavaFX application's scene,
renderImage()'s width and
height arguments can differ from one invocation to
another. In response, this function must recreate the
BufferedImage and rebuild the plasma
array to reflect dimension changes.
The shift variable is assigned an offset for the
purpose of rotating colors. Its value differs from one invocation
to the next because of its dependence on the system time, which is
obtained via System.currentTimeMillis().
Colors are rotated faster when
System.currentTimeMillis() is divided by a smaller
value than by a larger value because a smaller value results in a
larger shift offset, which allows the palette to be cycled through
faster.
The code initially invoked BufferedImage's
public void setRGB(int x, int y, int rgb) method for
each inner for (int col...) loop iteration. However,
many setRGB() calls and this method's interplay with
the internal raster and color model hurt performance.
To improve performance, I refactored the code to bypass
setRGB() in favor of using the
java.awt.image.WritableRaster class and its
public void setDataElements(int x, int y, int w, int h,
Object inData) method.
The code now pre-creates an array of colors, obtains the buffered
image's writable raster, and invokes the raster's
setDataElements() method to assign the colors array
to the raster -- setDataElements() is much faster
than the commented-out setRGB() method call.
The Painter.java, Canvas.fx, and
PlasmaPainter.java source files are part of a
NetBeans IDE 6.5.1 CanvasDemo project. This project's
Main.fx source file (see Listing 4) brings these
files together to demonstrate canvas and the plasma painter.
/*
* Main.fx
*/
package canvasdemo;
import javafx.animation.transition.PauseTransition;
import javafx.scene.Group;
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.paint.LinearGradient;
import javafx.scene.paint.Stop;
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: 0.0
endY: 1.0
stops:
[
Stop { offset: 0.0 color: Color.BLUE },
Stop { offset: 1.0 color: Color.BLUEVIOLET }
]
}
Stage
{
title: "Canvas Demo"
var scene: Scene
scene: scene = Scene
{
width: 300
height: 300
fill: BACKGROUND_PAINT
var canvas: Canvas
var group: Group
var text: Text
def animation = PauseTransition
{
duration: 30ms
action: function (): Void
{
canvas.paint ();
}
repeatCount: PauseTransition.INDEFINITE
}
content: group = Group
{
content:
[
text = Text
{
content: "PlasmaPainter"
fill: Color.YELLOW
font: Font { name: "Arial BOLD" size: 18 }
opacity: 0.75
effect: DropShadow {}
translateX: bind (scene.width-text.layoutBounds.width)/2
translateY: bind canvas.translateY-40
textOrigin: TextOrigin.TOP
}
canvas = Canvas
{
width: bind scene.width/2
height: bind scene.height/2
translateX: bind (scene.width-canvas.width)/2
translateY: bind (scene.height-canvas.height)/2
painter: new PlasmaPainter ()
}
]
onMouseClicked: function (me: MouseEvent): Void
{
if (animation.running and not animation.paused)
animation.pause ()
else
animation.play ()
}
effect: Reflection { input: DropShadow {} }
}
}
}
Listing 4: Main.fx
Listing 4 places the scene's javafx.scene.text.Text
and Canvas nodes into a
javafx.scene.Group. Clicking either of these nodes
results in the function assigned to the Group's
onMouseClicked variable being invoked, to pause or
play the animation.
The animation is an instance of the
javafx.animation.transition.PauseTransition class.
Every 30 milliseconds, this instance's action
function invokes canvas.paint() to update the canvas
-- see Figure 1.
Figure 1: The plasma painter renders a cool-looking neon-like effect.
Conclusion
This article's painter and canvas infrastructure is helpful for turning plasma, and other complex renderings into JavaFX nodes. Although I could have usednativearray and other
JavaFX features to code the painter architecture in JavaFX, I
chose Java for performance. For example, (plasma
[row][col]+shift)&255 runs faster than
javafx.util.Bits.bitAnd (plasma [row][col]+shift,
255).
Note: Application created with JavaFX 1.2 (via NetBeans IDE 6.5.1) on top of Java SE 6u12.









