Explore Java and more with Jeff 'JavaJeff' Friesen

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 a PlasmaPainter 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.

The plasma painter renders a cool-looking neon-like effect.

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 used nativearray 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).


Download code.zip

Note: Application created with JavaFX 1.2 (via NetBeans IDE 6.5.1) on top of Java SE 6u12.


LEARN JAVA FROM APRESS