Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

JavaWorld Daily Brew

Java Tutor

Java Tutor is my platform for teaching about Java 7+ and JavaFX 2.0+, mainly via programming projects.


Pixel Graphics and JavaFX

 

JavaFX's node-based infrastructure lets you create scenes based on geometric shapes (including scalable vector graphics), images, text, controls, and even media. You can also use this infrastructure to create complex pixel-oriented graphics such as fireworks, plasma, fractals, and fire. In the latter case, you either manipulate object-based pixels or work directly with Prism.

This tutorial shows you how to create pixel-oriented graphics in JavaFX. You first learn how to accomplish this task at a high object-based pixel level that is portable from one JavaFX version to another. Because of problems with this approach, I also show you how to accomplish this task at the lower Prism level, where you are briefly introduced to one of Prism's various classes.

Working with Object-Based Pixels

An object-based pixel is a pixel that's represented by an instance of the javafx.scene.shape.Line class. You specify a single pixel by passing the same integer value to each of the Line(double startX, double startY, double endX, double endY) constructor's parameters, as demonstrated below:

Line pixel = new Line(20.0, 20.0, 20.0, 20.0);

This example creates a Line object that represents a single pixel located at (20.0, 20.0). You can assign a color to this pixel by invoking the inherited void setStroke(Paint value) method (declared in the javafx.scene.shape.Shape superclass) with a specific javafx.scene.paint.Color object, as follows:

pixel.setStroke(Color.RED); // Create a red pixel

Architecting a Canvas of Object-Based Pixels

To be of any use, these Line objects must be arranged into a rectangular grid, which I will refer to as a canvas. Each Line object will specify the coordinates for a specific pixel in this grid, and its stroke property will identify its color. The easiest way to architect this canvas is to base it on the javafx.scene.Group class, which is done in Listing 1.

import javafx.scene.Group;

import javafx.scene.paint.Color;

import javafx.scene.shape.Line;

public class Canvas extends Group
{
   private Line[][] pixels;

   public Canvas(int x, int y, int width, int height)
   {
      pixels = new Line[height][];
      for (int row = 0; row < width; row++)
      {
         pixels[row] = new Line[width];
         for (int col = 0; col < height; col++)
            pixels[row][col] = new Line(x+col, y+row, x+col, y+row);
         getChildren().addAll(pixels[row]);
      }
   }
   public int getHeight()
   {
      return pixels.length;
   }
   public int getWidth()
   {
      return pixels[0].length;
   }
   public void setPixel(int x, int y, Color c)
   {
      pixels[y][x].setStroke(c);
   }
   public void setRect(int x1, int y1, int x2, int y2, int[] colors)
   {
      int index = 0;
      for (int y = y1; y <= y2; y++)
         for (int x = x1; x <= x2; x++, index++)
            pixels[y][x].setStroke(Color.rgb((colors[index]>>16)&255, 
                                   (colors[index]>>8)&255, 
                                   colors[index]&255));
   }
}

Listing 1: A Canvas instance is really a Group instance of Line-based pixels.

Canvas declares a private pixels table of Line objects, a constructor, and several methods.

The Canvas(int x, int y, int width, int height) constructor creates a canvas, specifying that it occupies that part of the stage whose upperleft corner is at (x, y) and whose pixel extents are (width, height). It initializes this table such that the pixel location a Line object stores is relative to (x, y), whereas the object itself is accessed via zero-based coordinates. Each table row is added to the group.

The int getHeight() and int getWidth() methods return the canvas's extents, the void setPixel(int x, int y, Color c) method assigns a Color object to the "pixel" located at the zero-based (x, y) coordinates, and the void setRect(int x1, int y1, int x2, int y2, int[] colors) method assigns Color objects associated with colors array entries to a rectangular region of the canvas.

Note: The length of the colors array must not be less than the number of pixels specified by x1, y1, x2, and y2. Otherwise, an exception is thrown.

Demonstrating the Canvas

I've created an application that demonstrates the canvas by animating a plasma effect. If you're wondering why I declared int[] colors instead of Color[] colors as setRect()'s final parameter, I did so because I find it easier to invoke this method with an array of integer-based colors when implementing this effect. Listing 2 presents this application's source code.

import javafx.animation.PauseTransition;

import javafx.application.Application;

import javafx.event.ActionEvent;
import javafx.event.EventHandler;

import javafx.geometry.VPos;

import javafx.scene.Group;
import javafx.scene.Scene;

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

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

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

import javafx.stage.Stage;

import javafx.util.Duration;

public class AnimPlasma extends Application
{
   private int[] palette;
   private byte[][] plasma;
   private int[] colors;

   @Override
   public void start(final Stage primaryStage)
   {
      primaryStage.setTitle("Animated Plasma");
      primaryStage.setWidth(600);
      primaryStage.setHeight(600);

      final Group group = new Group();

      Text text = new Text();
      text.setText("Animated Plasma");
      text.setFill(Color.YELLOW);
      text.setFont(new Font("Arial BOLD", 24.0));
      text.setEffect(new DropShadow());
      text.xProperty().bind(primaryStage.widthProperty()
                      .subtract(text.layoutBoundsProperty().getValue().getWidth())
                      .divide(2));
      text.setY(30.0);
      text.setTextOrigin(VPos.TOP);
      group.getChildren().add(text);

      final Canvas canvas = new Canvas(100, 100, 400, 400);
      group.getChildren().add(canvas);

      group.setEffect(ReflectionBuilder.create().input(new DropShadow()).build());

      LinearGradient lg = new LinearGradient(0.0, 0.0, 0.0, 1.0, true,
                                             CycleMethod.NO_CYCLE,
                                             new Stop(0, Color.BLUE),
                                             new Stop(1.0, Color.BLUEVIOLET));
      Scene scene = new Scene(group, lg);
      primaryStage.setScene(scene);

      primaryStage.setResizable(false);
      primaryStage.show();

      setup(canvas);

      final PauseTransition pt = new PauseTransition(Duration.millis(30));
      pt.setCycleCount(1);
      pt.setOnFinished(new EventHandler()
                       {
 
                          @Override
                          public void handle(ActionEvent ae)
                          {
                             update(canvas);
                             pt.play();
                          }
                       });
      pt.play();
   }
   void setup(Canvas canvas)
   {
      palette = new int[256];
      for (int i = 0; i < palette.length; i++)
      {
          Color color = Color.hsb(i/255.0*360, 1.0, 1.0);
          int red = (int) (color.getRed()*255);
          int grn = (int) (color.getGreen()*255);
          int blu = (int) (color.getBlue()*255);
          palette[i] = (red << 16)|(grn << 8)|blu;
      }

      plasma = new byte[canvas.getHeight()][];
      for (int i = 0; i < plasma.length; i++)
          plasma [i] = new byte[canvas.getWidth()];

      colors = new int[canvas.getWidth()*canvas.getHeight()];
   }
   void update(Canvas canvas)
   {
      int width = canvas.getWidth();
      int height = canvas.getHeight();
      for (int row = 0; row < height; row++)
          for (int col = 0; col < width; col++)
              plasma[row][col] =
                     (byte) ((128.0+128.0*Math.cos(row/8.0)+
                            128.0+128.0*Math.cos(col/8.0))/2);

      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;
      }
      canvas.setRect(0, 0, width-1, height-1, colors);
   }
   public static void main(String[] args)
   {
      launch(args);
   }
}

Listing 2: AnimPlasma repeatedly calculates a table of plasma colors and paints these colors on the canvas.

AnimPlasma declares palette, plasma, and colors fields. palette stores a palette of RGB values that exhibit no discontinuities (abrupt color changes) because the palette's colors will be rotated to achieve animation, plasma stores scaled and biased cosine values, and colors stores colors associated with the scaled and biased cosine values. Each array is initialized in the setup() method.

The update() method calculates new scaled and biased cosine values for the next animation frame. 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().

Note: 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.

I'm using the javafx.animation.PauseTransition class to control the animation. Every 30 milliseconds, the action event handler registered with the pause transition invokes update() to create and render a new animation frame. I specified 1 instead of Timeline.INDEFINITE as a cycle count because I've found that the event handler is invoked only after all cycles are complete.

Compiling and Running AnimPlasma

I've compiled and run this application via JavaFX 2.0.2 and Java 7 Update 2 on a 64-bit Windows 7 platform. I used the following command, which assumes that JavaFX 2.0.2 has been installed to c:\program files (x86)\oracle\javafx 2.0 sdk, to compile AnimPlasma.java and Canvas.java:

javac -cp "c:\program files (x86)\oracle\javafx 2.0 sdk\rt\lib\jfxrt.jar";. AnimPlasma.java

I then used the following command to run this application:

java -cp "c:\program files (x86)\oracle\javafx 2.0 sdk\rt\lib\jfxrt.jar";. AnimPlasma

Figure 1 shows the application's user interface with a single animation frame.

Figure 1: The animated plasma displays symmetry because of the cosine function.

Working with Prism-Based Pixels

When you run AnimPlasma, you'll probably discover slow and jerky animation. Much of the delay is caused by the use of objects and method calls -- it's not the result of Math.cos() (as you'll soon discover). Another problem is significant memory use. For a 400-by-400-pixel canvas, 160,000 Line objects are created; each object occupies more than 100 bytes for its fields.

AnimPlasma can be sped up significantly by using Prism (that part of the JavaFX runtime that's responsible for the rasterization and rendering of JavaFX scenes) to implement the canvas. Specifically, Prism provides an Image class whose pixels can be modified. Unfortunately, the use of this class is dependent on a deprecated feature, which will not be available at some point past JavaFX 2.0.2.

Architecting a Canvas of Prism-Based Pixels

Instead of a two-dimensional array of Line objects, this section's canvas is implemented as a one-dimensional array of byte integers (where three consecutive byte integers define a single RGB pixel) stored in a com.sun.prism.Image instance's pixel buffer. Listing 3 presents this canvas, which is based on the javafx.scene.image.Image and javafx.scene.image.ImageView classes.

import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

public class Canvas extends ImageView
{
   private int width;
   private int height;
   private int stride;
   private byte[] im;
   private com.sun.prism.Image pimage;

   public Canvas(int width, int height)
   {
      this.width = width;
      this.height = height;
      this.stride = width*3;
      im = new byte[stride*height];
   }
   public int getHeight()
   {
      return height;
   }
   public int getWidth()
   {
      return width;
   }
   public void repaint()
   {
      pimage = com.sun.prism.Image.fromByteRgbData(im, width, height);
      setImage(Image.impl_fromPlatformImage(pimage));
   }
   public void setPixel(int x, int y, int c)
   {
      int index = y*stride+x+x+x;
      im[index++] = (byte) (c >> 16);
      im[index++] = (byte) (c >> 8);
      im[index] = (byte) c;
   }
   public void setRect(int x1, int y1, int x2, int y2, int[] colors)
   {
      int colorIndex = 0;
      int pixelIndex = y1*stride+x1+x1+x1;
      int deltaX = (x2+x2+x2-x1-x1-x1+3);
      for (int y = y1; y <= y2; y++)
      {
         for (int x = x1; x <= x2; x++)
         {
            im[pixelIndex++] = (byte) (colors[colorIndex] >> 16);
            im[pixelIndex++] = (byte) (colors[colorIndex] >> 8);
            im[pixelIndex++] = (byte) colors[colorIndex++];
         }
         pixelIndex += stride-deltaX;
      }
   }
}

Listing 3: A Canvas instance is really an ImageView instance that's based on a dynamically-changing Image instance.

Canvas declares private width, height, stride, im, and pimage fields. The first two fields identify the canvas extents in pixels, stride identifies the width in bytes (3*width for RGB), im stores the bytes that constitute the canvas contents, and pimage provides access to Prism's Image class.

The constructor initializes these fields, the int getHeight() and int getWidth() methods return the canvas's extents, and void setPixel(int x, int y, int c) and void setRect(int x1, int y1, int x2, int y2, int[] colors) directly modify the im array as efficiently as possible. The most interesting method is void repaint().

void repaint(), which must be called to paint the canvas's content to the stage, first invokes the com.sun.prism.Image fromByteRgbData(byte[] bytes, int width, int height) class method in Prism's Image class to return an RGB-formatted com.sun.prism.Image instance wrapped around byte array im. It then executes setImage(Image.impl_fromPlatformImage(pimage)) to return a javafx.scene.Image object whose contents are based on the Prism Image instance via javafx.scene.Image's deprecated Image impl_fromPlatformImage(Object image) class method, and assign this object to the ImageView instance via ImageView's void setImage(Image image) method.

Note: I found it necessary to create a new Prism Image object and a new JavaFX Image object for each call to repaint(); otherwise, I could not see the updated canvas on the stage. Because repaint() only needs to be called once per animation frame, this might not be too problematic from a garbage collection perspective.

Demonstrating the Canvas

I've created an application that demonstrates the canvas by animating a plasma effect. Because the code is nearly identical to the code shown in Listing 2, I'm going to focus on the two differences only. First, the Canvas is instantiated and configured via the following code fragment, which reveals the shorter Canvas constructor:

final Canvas canvas = new Canvas(400, 400);
canvas.setX(100);
canvas.setY(100);

The second difference is the addition of a canvas.repaint() method call following the canvas.setRect(0, 0, width-1, height-1, colors) method call in the update(Canvas canvas) method. I specified canvas.repaint() to ensure that the canvas's contents are shown on the stage.

You would compile and run this application in an identical manner to the application presented earlier. Running the application reveals a user interface that's similar to what appears in Figure 1. As well as observing faster and smoother animation, you'll observe Figure 2's brighter pixels. (I'm not sure why Figure 1's pixels appear dimmer because the opacity setting is at maximum.)

Figure 2: The canvas's pixels appear brighter than their counterparts in Figure 1.

Exercises

  1. What is an object-based pixel?
  2. Earlier, I said that Math.cos() is not the main reason for the first AnimPlasma application's animation being so slow. However, it is a contributor. The animation can be sped up by creating an array of cosines in the setup() method and replacing the update() method's Math.cos() method calls with code that indexes the array. Implement this technique for both AnimPlasma versions.

Code

You can download this post's code and answers here. Code was developed and tested with JDK 7u2 and JavaFX SDK 2.0.2 on a Windows 7 platform.

* * *

I welcome your input to this blog, and will write about relevant topics that you suggest. While waiting for the next blog post, check out my TutorTutor website to learn more about Java and other computer technologies (and that's just the beginning).

Learn more about the Java 7 language and many of its APIs by reading my book Beginning Java 7. You can obtain information about this book here and here.