Monday, January 5, 2009

Creating a custom fill in JavaFX


JavaFX 1.0: Creating a Custom Fill That Changes Over Time
Difficulty: Beginner


This tutorial will demonstrate how to create a custom fill and use it to fill a custom shape. We can create a star with our fill by adding the star polygon below as part of the Stage's Scene content.


Free Image Hosting


var Diff:DiffusionEffect=
DiffusionEffect{ imageRect: Rectangle{height:50,width:50} };

var star:Polygon = Polygon{
points: [
0,75,
200,75,
40,200,
100,0,
170,200,
0,75
]
fill: bind Diff.paint;

};

We'll create two classes, Diffusion.fx which creates a fill texture and DiffusionEffect.fx as a model class that holds the Paint variable and changes it using a timer. Let's start by looking at Diffusion.fx


Visible items in JavaFX are Nodes. The fill property of a Node takes an object that extends javafx.scene.paint.Paint which must override the function getAWTPaint() and return java.awt.Paint. We'll do this with java.awt.TexturePaint. We can create the TexturePaint object using a java.awt.image.BufferedImage and java.awt.Rectangle as arguments. The rectangle is used to adjust the dimensions of the image. We can tile the image by adjusting the dimensions of the rectangle to a size smaller than the shape it will fill.


Diffusion.fx is a Paint object. So we could already assign an instance of Diffusion to the fill property of our shape ( eg.: fill: Diffusion{} ). But it would be static. Since we're creating a fill that changes over time, we have to create a model class, DiffusionEffect, and give it a timer to change the image. The timer will get a new instance of the Diffusion class which will render a different randomized image every hundred milliseconds. Let's look at DiffusionEffect.fx


By assigning our javafx.scene.paint.Paint variable a new Diffusion instance every time the timer fires we can bind the paint variable to the fill property of any Node. By using binding, the node's fill is automatically updated everytime the variable changes creating the animated effect. All we have to do is make sure we start the timer after the node has been initialized (in postinit).


For fun, adjust the dimensions of the rectangle passed to DiffusionEffect (best to maintain a square). Start with 5 and increment. You'll get something that resembles an energy effect we saw in 80's cartoons like Transformers. This is also how we'd go about tiling an image in a custom shape or node.


Here are Diffusion.fx and DiffusionEffect.fx respectively:



/*
* Diffusion.fx
*
* Author: Matt Coatney
*
*/


package diffusionfill;

import java.awt.image.BufferedImage;
import java.lang.Math;
import javafx.scene.shape.Rectangle;

public class Diffusion extends
javafx.scene.paint.Paint
{

public var imageRect : Rectangle;
var img:java.awt.image.BufferedImage;

// Gets a random RGB color
function getColor(): Integer
{
var c:java.awt.Color = new java.awt.Color(
Math.random() * 255,
Math.random() * 255,
Math.random() * 255);

return
c.getRGB();
}

// Creates and returns a buffered image
// with randomized pixel colors
function getImage():
java.awt.image.BufferedImage
{

var bi:java.awt.image.BufferedImage =
new BufferedImage(
imageRect.width as Integer,
imageRect.height as Integer,
BufferedImage.TYPE_INT_ARGB);

var i = 0;
var j = 0;
while (i < imageRect.height) {
while (j < imageRect.width) {
bi.setRGB(i, j, getColor());
j++
}
j = 0;
i++
};
return bi;
}

public override function getAWTPaint():
java.awt.Paint
{

img = getImage();

// Translate javafx Rectangle to
// java.awt.Rectangle
var rect: java.awt.Rectangle =
new java.awt.Rectangle();
rect.x = imageRect.x as Integer;
rect.y = imageRect.y as Integer;
rect.width = imageRect.width as Integer;
rect.height = imageRect.height as Integer;

return new java.awt.TexturePaint(img,rect);

}

postinit{
if(imageRect == null){
imageRect = Rectangle {
height:50,
width:50};
}
}
}




/*
* DiffusionEffect.fx
*
* Author: Matt Coatney
*
*/


package diffusionfill;

import diffusionfill.Diffusion;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javafx.scene.shape.Rectangle;
import javax.swing.Timer;


public class DiffusionEffect{
var d:Diffusion;
var timerListener:ActionListener =
ActionListener
{
override public function actionPerformed
(evt:ActionEvent): Void
{
d = Diffusion{imageRect: imageRect};
paint = d;
}
}
var timer:Timer =
new Timer(100, timerListener);
public var imageRect : Rectangle;
public var paint: javafx.scene.paint.Paint = d;

postinit
{
d = Diffusion{ imageRect: imageRect };
timer.start();
}
}





In this way, we can perform all sorts of interesting animations and let JavaFX perform clipping for us by creating the animation effect as a custom fill. I hope this tutorial will be useful. When I looked for information about custom effects and fills it was pretty scarce.
~M