Effects Playground for Mobile

By Josh Marinacci, October 22nd, 2008

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.

Source Code
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.

Source Code
    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