Effects Playground for Mobile
The EffectsPlayground example shows how to use the javafx.scene.effects.* API. You cannot run this example directly on a mobile device because effects is not in the common API. It is desktop only. However, the mobile profile does support some other cool effects that you can use to build a mobile-specific version of EffectsPlayground by using the same theme and colors.
Understanding the Code
The effects example is only in the desktop profile, but you can still use transforms, opacity, and images to do some fun things on mobile devices. For this sample, I chose a new set of effects that a user might want to apply to a photo on a mobile device:
- Cropping
- Flip and Rotate
- Lighten, Darken, and Sepia tone
- Add frames and fun overlays
All of these effects can be applied with transforms, opacity changes, and image overlays by using only the common profile. The effects themselves are actually implemented on the photo by using the PhotoView class. This class defines each effect by using a set of control variables, as shown in Figure 1. The class also defines some utility methods for the effects like flipHorz and flipVert. The onscreen buttons and sliders are custom components that bind to these control variables.
public class PhotoView extends CustomNode {
public var scaleFactor = 1.0;
public var image:Image;
var scale = bind zoomValue / 100.0;
public var zoomValue = 100.0;
public var cropActive = false;
var clipping:Rectangle = null;
var flipTransform:Transform = Transform.scale(1.0,1.0);
var flipTransformTranslate:Transform = Transform.translate(0,0);
var rotateAngle = 0.0;
var horzFlip = 1.0;
var vertFlip = 1.0;
public var lightness = 0.0;
public var darkness = 0.0;
public var sepia = 0.0;
public var frameWidth = 0;
public var frameColor = Color.TRANSPARENT;
public var photoWidth = bind image.width*scaleFactor;
public var photoHeight = bind image.height*scaleFactor;
var viewport:Rectangle = Rectangle { x: 0 y: 0 width: bind photoWidth height: bind photoHeight };
public var frameImage:Image;
public var funImage:Image;
public var funActive = false;
var startX = 0.0;
var startY = 0.0;
var cropX = 0.0;
var cropY = 0.0;
public function resetZoom():Void {
zoomValue = 100;
}
public function crop():Void {
if (clipping == null) {
clipping = Rectangle { x: 0, y: 0, width: photoWidth, height: photoHeight }
}
def b = iv.parentToLocal(viewport.boundsInLocal);
def x1 = Math.max(b.minX, clipping.x);
def y1 = Math.max(b.minY, clipping.y);
def x2 = Math.min(b.maxX, clipping.x + clipping.width);
def y2 = Math.min(b.maxY, clipping.y + clipping.height);
clipping = Rectangle { x: x1, y: y1, width: x2 - x1, height: y2 - y1 }
}
public function resetCrop():Void {
clipping = null;
cropX = 0;
cropY = 0;
}
public function flipHorz():Void {
horzFlip = horzFlip * -1.0;
}
public function flipVert():Void {
vertFlip = vertFlip * -1.0;
}
public function resetFlip():Void {
horzFlip = 1.0;
vertFlip = 1.0;
}
public function rotateCW():Void {
rotateAngle += 90.0;
}
public function rotateCCW():Void {
rotateAngle -= 90.0;
}
public function resetRotate():Void {
rotateAngle = 0.0;
}
Figure 1: PhotoView Control Variables
The actual effects are implemented by binding transforms and overlays to the underlying ImageView by using the control variables. When the control variables change, the photo changes on screen. The flip, crop, and rotation effects are all implemented by using transforms. For the lightness, darkness, and sepia effects, I put translucent rectangles of white, black, and brown over the photo. The rectangles start off completely transparent, but as the user increases the effects intensity, the rectangles become more opaque. The final two effects, frames and fun images, are just images overlayed on top of the photo. Figure 2 shows the complete implementation.
public override function create():Node {
Group {
clip: bind viewport
content: [
iv = ImageView {
clip: bind clipping
transforms: bind [
Transform.translate(cropX,cropY),
Transform.scale(horzFlip * scale, vertFlip * scale, photoWidth / 2, photoHeight / 2),
Transform.rotate(rotateAngle, photoWidth/2, photoHeight/2)
]
image: bind image
onMousePressed: function(e:MouseEvent) {
if(cropActive) {
startX = e.sceneX-cropX;
startY = e.sceneY-cropY;
}
}
onMouseDragged: function(e:MouseEvent) {
if(cropActive) {
cropX = e.sceneX-startX;
cropY = e.sceneY-startY;
}
}
fitWidth: bind image.width*scaleFactor
fitHeight: bind image.height*scaleFactor
},
//lightness overlay
Rectangle { width: bind photoWidth height: bind photoHeight fill: bind Color.rgb(255,255,255,lightness/100.0) },
//darkness overlay
Rectangle { width: bind photoWidth height: bind photoHeight fill: bind Color.rgb(0,0,0,darkness/100.0) },
//sepia overlay
Rectangle { width: bind photoWidth height: bind photoHeight fill: bind Color.rgb(164,110,44,sepia/100.0) },
// frame border
Rectangle { width: bind photoWidth height: bind photoHeight fill: null stroke: bind frameColor strokeWidth: bind frameWidth },
// frame image
ImageView {
image: bind frameImage
fitWidth: bind frameImage.width*scaleFactor
fitHeight: bind frameImage.height*scaleFactor
},
// fun image
ImageView {
image: bind funImage
var sfix = 0.0;
var sfiy = 0.0;
var cfix = 0.0;
var cfiy = 0.0;
translateX: bind cfix
translateY: bind cfiy
onMousePressed: function(e:MouseEvent) {
if(funActive) {
sfix = e.sceneX-cfix;
sfiy = e.sceneY-cfiy;
}
}
onMouseDragged: function(e:MouseEvent) {
if(funActive) {
cfix = e.sceneX-sfix;
cfiy = e.sceneY-sfiy;
}
}
fitWidth: bind funImage.width*scaleFactor
fitHeight: bind funImage.height*scaleFactor
}
]
};
}
Figure 2: PhotoView: Implementing the Effects
Josh Marinacci

