Painter's Canvas Mobile Edition
Last June, I introduced my Painter's Canvas article, which presents a technique for rendering complex graphics (such as fireworks, plasma, fractals, and fire). Instead of relying on nodes andjavafx.scene.shape.Path objects (such as
javafx.scene.shape.LineTo) to render the graphics,
this technique relies on the concepts of canvas and painter for
this task.
The crux of the article's code is the line iv.image =
SwingUtils.toFXImage (painter.renderImage (width, height))
(excerpted from the Canvas class's
paint() function). This line, which is repeatedly
invoked in an animation loop, performs several tasks to ensure
that the next animation frame is displayed:
-
Invokes
painter.renderImage (width, height)to render the next animation frame and returns its contents in a reusablejava.awt.BufferedImageinstance -- the same instance is used most of the time; a new instance is created whenever thewidthand/orheightarguments change. -
Converts the
BufferedImageinstance to ajavafx.scene.image.Imageinstance by invokingSwingUtils.toFXImage()with theBufferedImageinstance as this method call's argument. -
Assigns the
Imageinstance toiv'simageproperty. Variableivcontains an instance of thejavafx.scene.image.ImageViewclass. When a newImageinstance is assigned to theimageproperty, the scenegraph is forced to repaint the scene, which causes the image to be displayed.
Although new objects are still created during the animation loop,
the idea is to minimize object creation. For example, imagine
rendering the article's plasma scene using only nodes and
Path objects such as LineTo. You might
end up creating thousands more objects than are created using the
article's canvas/painter technique. Much more memory would
probably be needed, and performance could suffer.
Unfortunately, the technique presented in my Painter's
Canvas article is restricted to the desktop and browser
profiles. The article's CanvasDemo application won't
run on the mobile profile mainly because of its dependence on the
Abstract Window Toolkit and SwingUtils.toFXImage().
This article presents a mobile-friendly variant of that
application.
Revisiting Essential Types
Before I revealCanvasDemo running on the JavaFX
SDK's mobile emulator, I'll briefly revisit the four types that
make up this application. Specifically, I focus only on the
necessary changes made to these types, starting with the single
change made to Listing 1's Painter interface.
// Painter.java
import javafx.scene.image.Image;
public interface Painter
{
public Image renderImage (int width, int height);
}
Listing 1: Painter.java
The Painter interface's renderImage()
method no longer returns a BufferedImage instance
because this latter class isn't supported by the mobile profile.
I've modified this method's signature to indicate that it returns
an instance of JavaFX's Image class.
Listing 2 presents the modified Canvas class.
/*
* Canvas.fx
*/
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
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 = 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
As with Listing 1, only a single change was made to the
Canvas class. The previous iv.image =
SwingUtils.toFXImage (painter.renderImage (width, height))
has been shortened to iv.image = painter.renderImage (width,
height).
The largest number of changes are employed in the
PlasmaPainter class, presented in Listing 3.
// PlasmaPainter.java
import static java.lang.Math.*;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import com.sun.fxme.image.ImageProgressListener;
import com.sun.fxme.runtime.scenegraph.SGImage;
public class PlasmaPainter implements Painter
{
private int [] palette;
private Image image;
private boolean toggle;
private int width, height;
private byte [][] plasma;
private int [] colors;
private ImageProgressListener ipl;
private javafx.scene.image.Image image1 = new javafx.scene.image.Image ();
private javafx.scene.image.Image image2 = new javafx.scene.image.Image ();
public PlasmaPainter ()
{
palette = new int [256];
for (int i = 0; i < palette.length; i++)
palette [i] = HSBtoRGB ((float) i/255.0f, 1.0f, 1.0f);
ipl = new ImageProgressListener ()
{
@Override public void setError (boolean b)
{
}
@Override public void setImageSize (int x, int y)
{
}
@Override public void setProgress (int x)
{
}
};
}
// I excerpted the following function from the java.awt.Color class because
// MIDP (upon which JavaFX mobile is partly based) doesn't provide this
// class nor this function.
public static int HSBtoRGB (float hue, float saturation, float brightness)
{
int r = 0, g = 0, b = 0;
if (saturation == 0)
r = g = b = (int) (brightness*255.0f+0.5f);
else
{
float h = (hue -(float) Math.floor (hue))*6.0f;
float f = h-(float) java.lang.Math.floor (h);
float p = brightness*(1.0f-saturation);
float q = brightness*(1.0f-saturation*f);
float t = brightness*(1.0f-(saturation*(1.0f-f)));
switch ((int) h)
{
case 0: r = (int) (brightness*255.0f+0.5f);
g = (int) (t*255.0f+0.5f);
b = (int) (p*255.0f+0.5f);
break;
case 1: r = (int) (q*255.0f+0.5f);
g = (int) (brightness*255.0f+0.5f);
b = (int) (p*255.0f+0.5f);
break;
case 2: r = (int) (p*255.0f+0.5f);
g = (int) (brightness*255.0f+0.5f);
b = (int) (t*255.0f+0.5f);
break;
case 3: r = (int) (p*255.0f+0.5f);
g = (int) (q*255.0f+0.5f);
b = (int) (brightness*255.0f+0.5f);
break;
case 4: r = (int) (t*255.0f+0.5f);
g = (int) (p*255.0f+0.5f);
b = (int) (brightness*255.0f+0.5f);
break;
case 5: r = (int) (brightness*255.0f+0.5f);
g = (int) (p*255.0f+0.5f);
b = (int) (q*255.0f+0.5f);
break;
}
}
return 0xff000000|(r << 16)|(g << 8)|(b << 0);
}
@Override
public javafx.scene.image.Image renderImage (int width, int height)
{
if (image == null || width != this.width || height != this.height)
{
image = Image.createImage (width, height);
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;
Graphics g = image.getGraphics ();
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];
g.setColor (colors [base+col]);
g.drawLine (col, row, col, row);
}
base += width;
}
SGImage sgi = new SGImage ();
sgi.setProgressListener (ipl);
sgi.loadingFinished (image);
if (toggle)
{
toggle = !toggle;
image1.set$image (sgi);
return image1;
}
else
{
toggle = !toggle;
image2.set$image (sgi);
return image2;
}
}
}
Listing 3: PlasmaPainter.java
Because the mobile profile doesn't support the Abstract Window
Toolkit, Listing 3 relies on the Java ME Mobile Information Device
Profile's javax.microedition.lcdui.Graphics and
javax.microedition.lcdui.Image classes to perform
rendering.
Listing 3 also relies on an undocumented feature to store
javax.microedition.lcdui.Image content in one of two
javafx.scene.image.Image instances -- these
precreated instances are alternately populated/returned so that
the scenegraph repaints:
-
An instance of the
com.sun.fxme.runtime.scenegraph.SGImageclass is instantiated. This class describes a scenegraph image node. -
SGImage'ssetProgressListener()method is invoked with a precreated instance of a class that implements thecom.sun.fxme.image.ImageProgressListenerinterface. This is necessary to prevent aNullPointerException. -
SGImage'sloadingFinished()method is invoked to store thejavax.microedition.lcdui.Imagecontent in theSGImageinstance. -
Regardless of which
javafx.scene.image.Imageinstance is being returned, this class'sset$image()method is invoked to store theSGImageinstance; thejavafx.scene.image.Imageinstance is returned.
Finally, Listing 4 presents the modified Main class.
/*
* Main.fx
*/
import javafx.animation.transition.PauseTransition;
import javafx.scene.Group;
import javafx.scene.Scene;
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
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 ()
}
}
}
}
Listing 4: Main.fx
Main.fx is essentially the same as before, except
that it no longer references the
javafx.scene.effect.DropShadow and
javafx.scene.effect.Reflection classes. These classes
cannot be referenced because the mobile profile doesn't support
effects.
Constructing and Running CanvasDemo
Perhaps you're wondering why I haven't included package statements in the aforementioned source files. These statements are missing because, unlike in my former article, I'm not using NetBeans to construct the mobile edition. Instead, I'm working with the JavaFX 1.2.3 SDK at the Windows XP command line, and don't want to bother with packages.Assuming that you're using a similar setup, complete the following steps to build and run this application:
- Create a directory that contains the four aforementioned files. Ensure that this directory is current.
-
Execute the following command line, which assumes that the
JavaFX runtime is stored in
C:\Program Files\JavaFX\javafx-sdk1.2.3:javac -cp "c:\Program Files\JavaFX\javafx-sdk1.2.3\lib\mobile\*.jar";"c:\Program Files\JavaFX\javafx-sdk1.2.3\emulator\runtimes\cldc-hi-javafx\lib\classes.zip";. PlasmaPainter.java -
Execute the following command line:
javafxc Main.fx. -
Execute
javafxpackager -p MOBILE -src . -appClass Mainto createMain.jarandMain.jadfiles in adistsubdirectory. -
Make
distthe current directory, and executeemulator -Xdescriptor:Main.jad. If all goes well, you should seeCanvasDemorunning on the emulator. See Figure 1.
Figure 1: CanvasDemo is now
mobile-friendly.
I normally don't like to work with undocumented features, but had
no choice to accomplish my task of getting CanvasDemo
to run in the mobile profile. Unfortunately, this undocumented
dependency means that this application might not run on the mobile
emulator in a future version of JavaFX. If this should happen,
I'll try to remedy the situation.
Conclusion
Now that you've seen desktop/browser and mobile versions of myCanvasDemo application, you're probably wishing that
you didn't have to maintain two similar but different code bases
to have a single application run on all of these profiles.
Fortunately, I have a solution to this problem, and will present
that solution in the forthcoming conclusion to my
Painter's Canvas articles trilogy.
Note: Application created with JavaFX 1.2.3 SDK on top of Java SE 6u16.









