Binding Sample

This entry is the translation of 18 Dec. entry of JavaFX Advent Calendar.

The previous entry was Refinement for existing application GUI by JavaFX (halfway...) by @rewtheblow.

The next entry was JavaFX Utilities by @yumix_h.


I'm serializing JavaFX article in Japanese web magazine ITpro. I picked up property and binding on the article of January.

For the article, I made some samples. However, I didn't use all samples because of too much pages.

MOTTAINAI!! (What a waste!!)

So, I'll show the sample here.

The sample is connected balls.

You can drag the balls, and the connection follows up the dragged ball like rubber band. To connect the balls and the connection, edge points of the connection are bound to the center of balls.

At first, I'll explain how to drag the balls.

When starting dragging, MouseEvent is occurred, and the application stores difference between the center location of the ball and the location of mouse cursor. The application update the center location of the ball from the present mouse cursor and the stored difference, while dragging the ball.

class Ball extends Circle {
    private double dragBaseX;
    private double dragBaseY;
 
    public Ball(double centerX, double centerY, double radius) {
        super(centerX, centerY, radius);
 
        setOnMousePressed(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                // store the difference between mouse cursor and the center of the ball.
                dragBaseX = event.getSceneX() - getCenterX();
                dragBaseY = event.getSceneY() - getCenterY();
            }
        });
 
        setOnMouseDragged(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                // update the center of the ball from the present mouse cursor
                // and the stored difference.
                setCenterX(event.getSceneX() - dragBaseX);
                setCenterY(event.getSceneY() - dragBaseY);
            }
        });
    }
} 

Next is the connection. The only thing to do is that the edge points of the connection bound to the center of the balls.

class Connection extends Line {
    public Connection(Ball startBall, Ball endBall) {
        // bind the edge points to the center of the balls
        startXProperty().bind(startBall.centerXProperty());
        startYProperty().bind(startBall.centerYProperty());        
        endXProperty().bind(endBall.centerXProperty());
        endYProperty().bind(endBall.centerYProperty());        
    }
}

That's all! It is easy, isn't it?

@irof Drawing Song by JavaFX

This entry is the translation of 14 Dec. entry of JavaFX Advent Calendar.

Yesterday entry was Trying Groovy's @Vetoable bindings in JavaFX Application by mike_neck.

@irof is a famous blogger in Japan, and irof advent calendar was organized by irof's friends. This entry isn't an entry of irof advent calenar, however I dedicated this entry to irof ;-)

In this entry, I'll show irof icon drawing song. The song is based on Draing irof icon by Canvas of HTML 5 (japanese) by @yukieen.

When drawing icon by JavaFX, we have two choices: one is Shape class, the other is Canvas class. I used Shape in this entry.

At the first, drawing icon border. I used Rectangle class for the border.

        Group root = new Group();

        // Drawing icon border
        Rectangle rectangle = new Rectangle(0, 0, 300, 300);
        rectangle.setStrokeWidth(5.0);
        rectangle.setStroke(Color.BLACK);
        rectangle.setFill(Color.WHITE);
        root.getChildren().add(rectangle);

Next step is outline. Arc class is OK, but I used Path class because I'd like to connect face to mouth lines.

At first I create Path object, then add XXXTo object. XXX is varied. For example, XXX is Move when moving point, and Arc when drawing arc.

It's to be noted that ArcTo class properties are different from Arc class properties. Because of the difference, I wasted time to convert properties values :-(

        // Path object for outline
        Path path = new Path();
        path.setStrokeWidth(10.0);
        path.setStroke(Color.BLACK);
        // Clipping to fit into icon border
        path.setClip(new Rectangle(0, 0, 300, 300));

        // Start point of outline
        path.getElements().add(new MoveTo(126.5, 267));
        // face line
        ArcTo arc = new ArcTo();
        arc.setX(146); arc.setY(184.5);
        arc.setRadiusX(117); arc.setRadiusY(117);
        arc.setLargeArcFlag(true);
        path.getElements().add(arc);

Then, dwaing mouth. Mouth is constructed by line, so I added LineTo object to Path object. At the end, to close path, I added CloseTo object.

        // Lines of mouth
        path.getElements().add(new LineTo(210, 255));
        // Close path
        path.getElements().add(new ClosePath());
        root.getChildren().add(path);

Next is balloon. Balloon is also constrcted by Path class. I use Bézier curve for prickle part.

        // Balloon
        path = new Path();
        path.setStrokeWidth(10.0);
        path.setStroke(Color.BLACK);
        path.getElements().add(new MoveTo(50, 30));
        path.getElements().add(new LineTo(153, 30));
        arc = new ArcTo();
        arc.setX(153); arc.setY(90);
        arc.setRadiusX(30); arc.setRadiusY(30);
        arc.setSweepFlag(true);
        path.getElements().add(arc);
        path.getElements().add(new LineTo(105, 90));

        // Using Bézier curve for prickle part
        path.getElements().add(new CubicCurveTo(105, 90, 90, 105, 129, 141));
        path.getElements().add(new CubicCurveTo(90, 135, 66, 120, 81, 90));

        path.getElements().add(new LineTo(57, 90));
        arc = new ArcTo();
        arc.setX(50); arc.setY(30);
        arc.setRadiusX(30); arc.setRadiusY(30);
        arc.setSweepFlag(true);
        path.getElements().add(arc);
        root.getChildren().add(path);

Dots in the balloon are Circle objects.

        // Dots in the balloon
        Circle circle = new Circle(51, 60, 5, Color.BLACK);
        root.getChildren().add(circle);
        circle = new Circle(84, 60, 5, Color.BLACK);
        root.getChildren().add(circle);
        circle = new Circle(120, 60, 5, Color.BLACK);
        root.getChildren().add(circle);
        circle = new Circle(154, 60, 5, Color.BLACK);
        root.getChildren().add(circle);

At the end, draing eye. Eye is also Circle object.

        // Circle
        circle = new Circle(255, 204, 15);
        circle.setFill(null);
        circle.setStroke(Color.BLACK);
        circle.setStrokeWidth(10);
        root.getChildren().add(circle);

That's all!

I uploaded source code to gist:github.

PixelReader/PixelWriter

This entry is the translation of 4 Dec. entry of JavaFX Advent Calendar.

Recently, I had several times to give presentations about JaaFX 2.2.
I talked some features in the presentations, and showed demonstrations.
Today, I introduce one of the the features: PixelReader/PixelWriter,
and show a demonstation that was made for these presentation.

JavaFX 2.2 began support of bitmap images. The main classes that deal with
bitmap images are PixelReader class and PixelWriter class.

PixelReader reads pixels from a Image object, and PixelWriter writes pixels to a
WritableImage object. WritableImage class is a subclass of Image class. It's similar
to BufferedImage class of Java 2D.

PixelReader/PixelWriter defined some API, however I start reading 1 pixel and
writing 1 pixel.

For example, copying a image.

    Image src = new Image(...);
    PixelReader reader = src.getPixelReader();

    int width = (int)src.getWidth();
    int height = (int)src.getHeight();

    WritableImage dest = new WritableImage(width, height);
    PixelWriter writer = dest.getPixelWriter();

    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            // reading a pixel from src image,
            // then writing a pixel to dest image
            Color color = reader.getColor(x, y);
            writer.setColor(x, y, color);

            // this way is also OK
//            int argb = reader.getArgb(x, y);
//            writer.setArgb(x, y, argb);
        }
    }

getPixelReader method of Image class is used for acquiring PixelReader object.

WritableImage is generated with width and height. After that, to acquire
PixelWriter call get PixelWriter method.

PixelReader#getColor(x, y) reads (x, y) pixel, and the return value type of
getColor method is Color class. In a similar way, PixelWriter#setColor(x, y, color)
writes (x, y) pixel to the color.

Similar method getArgb is reading a pixel, and the return type is int.
Top 8 bit of return value indicates alpha, next 8 bit indicates red, next 8 bit green,
and then blue.

This sample is the simplest use of PixelReader/PixelWriter, then I try little bit
complicated sample. That is image blur.

In the sample, I use box blur. It's the simplest blur method.

How to blur is reading both neighbor pixels around target pixel, averaging these
pixels color, and then write the target pixel with the average color.

If the neighbor is square, then it is box blur.

    private void blur() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                double red = 0;
                double green = 0;
                double blue = 0;
                double alpha = 0;
                int count = 0;

                // reading neighbor pixels
                for (int i = -kernelSize; i <= kernelSize; i++) {
                    for (int j = -kernelSize; j <= kernelSize; j++) {
                        if (x + i < 0 || x + i >= width 
                           || y + j < 0 || y + j >= height) {
                            continue;
                        }
                        Color color = reader.getColor(x + i, y + j);
                        red += color.getRed();
                        green += color.getGreen();
                        blue += color.getBlue();
                        alpha += color.getOpacity();
                        count++;
                    }
                }
                Color blurColor = Color.color(red / count, 
                                              green / count, 
                                              blue / count, 
                                              alpha / count);
                writer.setColor(x, y, blurColor);
            }
        }
    }

Reading pixels around edge is little bit hard, because some pinxels of neighbor don't exist.
In the case, the system reads all pixels that is able to read, and then average them.

Moreover, I use Slider class to make the quantity of blur.

        slider.valueProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable o) {
                DoubleProperty value = (DoubleProperty)o;
                int intValue = (int) value.get();
                if (intValue != kernelSize) {
                    kernelSize = intValue;
                    blur();
                }
            }
        });

A the type of value property of Slider class si double, so the system cast it to int.

The result of execution is the following image:

But, this way is BAD parformance! Especially, when kernelSize become big, the performance
goes down prominently.

The reason is, you know, that the times of reading pixel become huge. Therefore, I should
reduce reading pixels.

How? It's bulk reading.

Bulk pixel reading

Decreasing the number of reading pixels, we can use bulk reading methods. There are three overloading getPixels method. The difference is how to store pixel data: byte, int and Buffer.

I used int in this entry.

One of arguments of PixelReader#getPixels is WritablePixelFormat. Because WritablePixelFormat is abstract class, we can't generate WritablePixelFormat object directly. Therefore, static methods defined by PixelFormat that is superclass of WritablePixelFormat are used for gettting WritablePixelFormat object.

getIntArgbInstance method and getIntArgbPreInstancecorrespond to int and I used getIntArgbInstance in this entry.

getPixes argumets are x, y, width, height, writablePixelFormat, buffer, offset, and scalingStride. Type of buffer is int[], and offset means offset of buffer. The scalingStride shows size of a line to next line.

I rewrote blur method using getPixels and blur2 method is showed blow:

    private void blur2() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();
        WritablePixelFormat<IntBuffer> format 
            = WritablePixelFormat.getIntArgbInstance();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                int centerX = x - kernelSize;
                int centerY = y - kernelSize;
                int kernelWidth = kernelSize * 2 + 1;
                int kernelHeight = kernelSize * 2 + 1;

                if (centerX < 0) {
                    centerX = 0;
                    kernelWidth = x + kernelSize;
                } else if (x + kernelSize >= width) {
                    kernelWidth = width - centerX;
                }

                if (centerY < 0) {
                    centerY = 0;
                    kernelHeight = y + kernelSize;
                } else if (y + kernelSize >= height) {
                    kernelHeight = height - centerY;
                }

                int[] buffer = new int[kernelWidth * kernelHeight];
                reader.getPixels(centerX, centerY, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);

                int alpha = 0;
                int red = 0;
                int green = 0;
                int blue = 0;

                for (int color : buffer) {
                    alpha += (color >>> 24);
                    red += (color >>> 16 & 0xFF);
                    green += (color >>> 8 & 0xFF);
                    blue += (color & 0xFF);
                }

                alpha = alpha / kernelWidth / kernelHeight;
                red = red / kernelWidth / kernelHeight;
                green = green / kernelWidth / kernelHeight;
                blue = blue / kernelWidth / kernelHeight;

                int blurColor = (alpha << 24) 
                              + (red << 16) 
                              + (green << 8) 
                              + blue;
                writer.setArgb(x, y, blurColor);
            }
        }
    }


Because region edges blur process is complicated, lenth of code became longer. However, the performance was improved.

For comparing performance, I used radio buttons to select 1 pixel reader or bulk reading.

Bulk pixel writing

PixelWriter also defines bulk writing methods, in the same way as bulk reading of PixelReader.

So, let's try tessellate image.

The application caliculates an average of neighbor pixels in the same way as blur, but set the average color to all neighbor pixels.

There are four overloaded PixelWriter#setPixels methods: byte, int, ByteBuffer and PixelReader for pixel data. And I used int[] version of setPixels here.

    private void mosaic() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();
        WritablePixelFormat<IntBuffer> format 
            = WritablePixelFormat.getIntArgbInstance();

        for (int x = kernelSize; 
             x < width - kernelSize * 2; 
             x += kernelSize * 2 + 1) {
            for (int y = kernelSize; 
                 y < height - kernelSize * 2; 
                 y += kernelSize * 2 + 1) {

                int kernelWidth = kernelSize * 2 + 1;
                int kernelHeight = kernelSize * 2 + 1;

                int[] buffer = new int[kernelWidth * kernelHeight];
                reader.getPixels(x, y, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);

                int alpha = 0;
                int red = 0;
                int green = 0;
                int blue = 0;

                for (int color : buffer) {
                    alpha += (color >>> 24);
                    red += (color >>> 16 & 0xFF);
                    green += (color >>> 8 & 0xFF);
                    blue += (color & 0xFF);
                }
                alpha = alpha / kernelWidth / kernelHeight;
                red = red / kernelWidth / kernelHeight;
                green = green / kernelWidth / kernelHeight;
                blue = blue / kernelWidth / kernelHeight;

                int blurColor = (alpha << 24) 
                              + (red << 16) 
                              + (green << 8) 
                              + blue;
                Arrays.fill(buffer, blurColor);
                writer.setPixels(x, y, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);
            }
        }
    }

The result is shown below:

All sourcecode is shown below:

import java.nio.IntBuffer;
import java.util.Arrays;
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Slider;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.image.WritablePixelFormat;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class WritableImageDemo extends Application {

    private Image src;
    private WritableImage dest;
    private int kernelSize = 1;
    private int width;
    private int height;
    
    private RadioButton blurButton;
    private RadioButton blur2Button;
    private RadioButton mosaicButton;

    @Override
    public void start(Stage stage) {

        AnchorPane root = new AnchorPane();

        initImage(root);

        Scene scene = new Scene(root);

        stage.setTitle("WritableImage Demo");
        stage.setResizable(false);
        stage.setScene(scene);
        stage.show();
    }

    private void initImage(AnchorPane root) {
        src = new Image("macaron.jpg");
        ImageView srcView = new ImageView(src);
        root.getChildren().add(srcView);
        AnchorPane.setTopAnchor(srcView, 0.0);
        AnchorPane.setLeftAnchor(srcView, 0.0);

        width = (int) src.getWidth();
        height = (int) src.getHeight();
        root.setPrefSize(width * 2.0, height + 50);

        dest = new WritableImage(width, height);
        ImageView destView = new ImageView(dest);
        destView.setTranslateX(width);
        root.getChildren().add(destView);
        AnchorPane.setTopAnchor(destView, 0.0);
        AnchorPane.setRightAnchor(destView, (double) width);

        Slider slider = new Slider(0, 10, kernelSize);
        slider.setPrefSize(width, 50);
        slider.setShowTickLabels(true);
        slider.setShowTickMarks(true);
        slider.setSnapToTicks(true);
        slider.setMajorTickUnit(1.0);
        slider.setMinorTickCount(0);

        slider.valueProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable o) {
                DoubleProperty value = (DoubleProperty) o;
                int intValue = (int) value.get();
                if (intValue != kernelSize) {
                    kernelSize = intValue;
                    if (blurButton.isSelected()) {
                        blur();
                    } else if (blur2Button.isSelected()) {
                        blur2();
                    } else {
                        mosaic();
                    }
                }
            }
        });

        root.getChildren().add(slider);
        AnchorPane.setBottomAnchor(slider, 0.0);
        AnchorPane.setRightAnchor(slider, 10.0);

        HBox hbox = new HBox(10);
        hbox.setAlignment(Pos.CENTER);
        hbox.setPrefWidth(width);
        hbox.setPrefHeight(50);
        root.getChildren().add(hbox);
        AnchorPane.setBottomAnchor(hbox, 0.0);
        AnchorPane.setLeftAnchor(hbox, 10.0);

        ToggleGroup group = new ToggleGroup();
        blurButton = new RadioButton("Blur");
        blurButton.setToggleGroup(group);
        blurButton.setSelected(true);
        hbox.getChildren().add(blurButton);
        blur2Button = new RadioButton("Blur2");
        blur2Button.setToggleGroup(group);
        hbox.getChildren().add(blur2Button);
        mosaicButton = new RadioButton("Mosaic");
        mosaicButton.setToggleGroup(group);
        hbox.getChildren().add(mosaicButton);

        blur();
    }

    private void blur() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                double red = 0;
                double green = 0;
                double blue = 0;
                double alpha = 0;
                int count = 0;
                for (int i = -kernelSize; i <= kernelSize; i++) {
                    for (int j = -kernelSize; j <= kernelSize; j++) {
                        if (x + i < 0 || x + i >= width 
                           || y + j < 0 || y + j >= height) {
                            continue;
                        }
                        Color color = reader.getColor(x + i, y + j);
                        red += color.getRed();
                        green += color.getGreen();
                        blue += color.getBlue();
                        alpha += color.getOpacity();
                        count++;
                    }
                }
                Color blurColor = Color.color(red / count, 
                                              green / count, 
                                              blue / count, 
                                              alpha / count);
                writer.setColor(x, y, blurColor);
            }
        }
    }

    private void blur2() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();
        WritablePixelFormat<IntBuffer> format 
            = WritablePixelFormat.getIntArgbInstance();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                int centerX = x - kernelSize;
                int centerY = y - kernelSize;
                int kernelWidth = kernelSize * 2 + 1;
                int kernelHeight = kernelSize * 2 + 1;

                if (centerX < 0) {
                    centerX = 0;
                    kernelWidth = x + kernelSize;
                } else if (x + kernelSize >= width) {
                    kernelWidth = width - centerX;
                }

                if (centerY < 0) {
                    centerY = 0;
                    kernelHeight = y + kernelSize;
                } else if (y + kernelSize >= height) {
                    kernelHeight = height - centerY;
                }

                int[] buffer = new int[kernelWidth * kernelHeight];
                reader.getPixels(centerX, centerY, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);

                int alpha = 0;
                int red = 0;
                int green = 0;
                int blue = 0;

                for (int color : buffer) {
                    alpha += (color >>> 24);
                    red += (color >>> 16 & 0xFF);
                    green += (color >>> 8 & 0xFF);
                    blue += (color & 0xFF);
                }
                alpha = alpha / kernelWidth / kernelHeight;
                red = red / kernelWidth / kernelHeight;
                green = green / kernelWidth / kernelHeight;
                blue = blue / kernelWidth / kernelHeight;

                int blurColor = (alpha << 24) 
                              + (red << 16) 
                              + (green << 8) 
                              + blue;
                writer.setArgb(x, y, blurColor);
            }
        }
    }

    private void mosaic() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();
        WritablePixelFormat<IntBuffer> format 
            = WritablePixelFormat.getIntArgbInstance();

        for (int x = kernelSize; x < width - kernelSize * 2; x += kernelSize * 2 + 1) {
            for (int y = kernelSize; y < height - kernelSize * 2; y += kernelSize * 2 + 1) {
                int kernelWidth = kernelSize * 2 + 1;
                int kernelHeight = kernelSize * 2 + 1;

                int[] buffer = new int[kernelWidth * kernelHeight];
                reader.getPixels(x, y, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);

                int alpha = 0;
                int red = 0;
                int green = 0;
                int blue = 0;

                for (int color : buffer) {
                    alpha += (color >>> 24);
                    red += (color >>> 16 & 0xFF);
                    green += (color >>> 8 & 0xFF);
                    blue += (color & 0xFF);
                }
                alpha = alpha / kernelWidth / kernelHeight;
                red = red / kernelWidth / kernelHeight;
                green = green / kernelWidth / kernelHeight;
                blue = blue / kernelWidth / kernelHeight;

                int blurColor = (alpha << 24) 
                              + (red << 16) 
                              + (green << 8) 
                              + blue;
                Arrays.fill(buffer, blurColor);
                writer.setPixels(x, y, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);
            }
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

JavaFX で Merry Christmas!

このエントリーは JavaFX Advent Calendar の 25 日目のエントリーです。

昨日は @masafumi_ohta さんの Java FX on PI so far ‘beta’ to use でした。

明日が最終日で @aoetk さんです。

@aoetk さんには、初日だけでなくおおとりまで務めていただいて、ほんとありがとうございます。

さて、今日はクリスマスなので、クリスマスらしいものを考えてみました。動くクリスマスカードです。

でも、作るだけではなんなので、JavaFX 2.2 の新機能である Canvas を使ってみました。

とりあえず、完成形はこちら。雪が降っていて、ネオンっぽいイルミネーションはアニメーションでついたり消えたりするようになっています。

背景は写真で、白くて丸いのが雪です。この雪を Canvas で描いています。イルミネーションは Canvas ではなく、Shape で描いています。

さて、雪をどうやって描いているかということですが、たいしたことはしていません。単に Canvas に GraphicsContext で丸を描いているのですが、乱数で半径、透明度、ぼかし量を決めています。

    private double x;
    private double y;
    private double radius;
    private Color color;
    private BoxBlur blur;
    
    private Random random = new Random();
    
    public SnowParticle(double initX) {
        x = initX;
        y = 0.0;
        radius = random.nextDouble() * MAX_RADIUS;
        color = Color.rgb(255, 255, 255, random.nextDouble());
        double blurSize = random.nextDouble() * MAX_BLUR + MAX_BLUR;  
        blur = new BoxBlur(blurSize, blurSize, 2);
    }
    
    public void draw(GraphicsContext context) {
        context.setFill(color);
        context.setEffect(blur);
        
        context.fillOval(x, y, radius, radius);
    }

コンストラクタの引数の initX は一番始めの位置です。雪は y = 0 から降るので、x 座標だけになっています。

半径、透明度、ぼかし量をランダムにしていることで、雪の見え方がまちまちになり、それによって遠近感を醸し出すようにしています。

雪が降るようなアニメーションには Timeline や Transition ではなくて、AnimationTimer を使用しています。

AnimationTimer は適当な間隔で handle メソッドコールするので、そこで座標を再計算し、描画しています。

座標の再計算は SnowParticle クラスの update メソッドで行なっています。

    public double update() {
        x += random.nextDouble() * MAX_STAGGER - MAX_STAGGER / 2.0;
        y += random.nextDouble() * MAX_STAGGER;
        
        return y;
    }

雪をちらちら降らせるために、次の座標を決めるのにも乱数を使用しています。落ちる量も乱数、左右へのぶれも乱数で決めています。

Snowparticle クラスの update メソッドと draw メソッドをコールしているのが、ChristmasCard クラスの update メソッドです。

雪は 1 つぶではないので、List で保持させています。y 座標が画面からはみ出てしまったら、雪を削除しています。また、update がコールされたら、再び SnowParticle オブジェクトを生成させています (これも乱数で決めていますが)。

    private void update() {
        GraphicsContext context = snowCanvas.getGraphicsContext2D();
        
        context.setFill(Color.rgb(0, 0, 0, 0.0));
        context.fillRect(0, 0, snowCanvas.getWidth(), snowCanvas.getHeight());

        Iterator<SnowParticle> it = particles.iterator();
        while (it.hasNext()) {
            SnowParticle particle = it.next();
            
            double y = particle.update();
            if (y >= snowCanvas.getHeight()) {
                it.remove();
            } else {       
                particle.draw(context);
            }
        }
        
        if (random.nextInt(3) == 0) {
            particles.add(new SnowParticle(random.nextDouble() * snowCanvas.getWidth()));
        }
    }

Canvas に描く場合、そのまま描画すると前回の描画に上書きされてしまいます。そこで、はじめに透明色で塗りつぶしてから、雪を描くようにしています。

次のイルミネーションなんですが、Text クラスだけでやろうかと思ったのですが、ストロークとフィルを分けて扱いたかったので、SVG を使ってしまいました。

SVG をロードする部分は SVGLoader を使っています。SVGLoader については SVGLoader のエントリ をご覧ください。

SVG からロードした Shape はアニメーションでストロークとフィルを別々に色を変えるということを行なっています。文字が 14 文字あるので、ループで 14 文字分の KeyValue オブジェクトを作っておいて、後から KeyFrame に追加しようと思ったのですが、できませんでした。

というのも KeyFrame.getValues メソッドで返ってくる Set オブジェクトがイミュタブルで変更不可だからです。

そのため、ちょっと見づらいのですが、ループの中で KeyFrame オブジェクトを作って、Timeline オブジェクトに追加するという方法にしました。

    private void initIllumination() {
        Timeline timeline = new Timeline();
        
        SVGContent svgContent = SVGLoader.load(getClass().getResource("illumination.svg").toString());
        
        for (int i = 1; i < 15; i++ ) {
            Shape ch = (Shape)svgContent.getNode(String.format("merry%02d", i));
            ch.setEffect(new DropShadow(BlurType.GAUSSIAN, Color.YELLOW, 20.0, 0.4, 0.0, 0.0));
            ch.setStroke(TRANSPARENT);
            ch.setFill(TRANSPARENT);
            ch.setTranslateX(50);
            ch.setTranslateY(40);

            illuminationPane.getChildren().add(ch);

            KeyFrame frame0 = new KeyFrame(Duration.ZERO,
                                           new KeyValue(ch.strokeProperty(), TRANSPARENT),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));
            KeyFrame frame1 = new KeyFrame(Duration.seconds(2),
                                           new KeyValue(ch.strokeProperty(), TRANSPARENT),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));
            KeyFrame frame2 = new KeyFrame(Duration.seconds(5),
                                           new KeyValue(ch.strokeProperty(), Color.YELLOW),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));
            KeyFrame frame3 = new KeyFrame(Duration.seconds(6),
                                           new KeyValue(ch.strokeProperty(), Color.YELLOW),
                                           new KeyValue(ch.fillProperty(), Color.WHITE));
            KeyFrame frame4 = new KeyFrame(Duration.seconds(10),
                                           new KeyValue(ch.strokeProperty(), Color.YELLOW),
                                           new KeyValue(ch.fillProperty(), Color.WHITE));
            KeyFrame frame5 = new KeyFrame(Duration.seconds(12),
                                           new KeyValue(ch.strokeProperty(), Color.YELLOW),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));
            KeyFrame frame6 = new KeyFrame(Duration.seconds(15),
                                           new KeyValue(ch.strokeProperty(), TRANSPARENT),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));

            timeline.getKeyFrames().addAll(frame0, frame1, frame2, frame3, frame4, frame5, frame6);
        }
        
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.play();
    }

文字が輝いているように見えるのはドロップシャドウを施しているからです。DropShadow ではなく、Bloom でも輝いているようなエフェクトをつけられるはずなのですが、いまいち。ですので、いろいろと調整のきく DropShadow にしてあります。

ソースは近いうちに GitHub に上げようと思います。 GitHub に上げました。JavaFXChristmasCardです。

というわけで、Merry Christmas!!

バインドのサンプル

このエントリーは JavaFX Advent Calendar の 18 日目のエントリーです。

昨日は @rewtheblow さんの JavaFXによる既存アプリケーションGUIのリファイン(志半ば・・) です。

明日は @yumix_h さんです。


さて、みなさまご存知のとおり、櫻庭は ITpro で Java 技術最前線 という連載を持っており、現在は「JavaFX 2で始めるGUI開発」と題して JavaFX について書いています。

そして、来月のはプロパティとバインドの解説を行う予定です。

そのために、いくつかサンプルを作ったのですが、分量の関係で載せられなかったサンプルがあります。それをここで公開しちゃいます。

どういうサンプルかというと、結び付けられたボールのサンプルです。

ボールはドラッグできるようになっていて、ボールの間のコネクションの両端はボールの中心にバインドしてあります。すると、ボールをドラッグしても、コネクションがゴムのように伸び縮みします。

ボールをドラッグするようにするには、まずマウスボタンが押されたときにその場所を覚えておきます。そして、ドラッグのイベントで現在のマウスの位置と覚えておいた位置からボールの位置を設定します。

これでドラッグできます。

class Ball extends Circle {
    private double dragBaseX;
    private double dragBaseY;
 
    public Ball(double centerX, double centerY, double radius) {
        super(centerX, centerY, radius);
 
        setOnMousePressed(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                dragBaseX = event.getSceneX() - getCenterX();
                dragBaseY = event.getSceneY() - getCenterY();
            }
        });
 
        setOnMouseDragged(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                setCenterX(event.getSceneX() - dragBaseX);
                setCenterY(event.getSceneY() - dragBaseY);
            }
        });
    }
} 

次にコネクションですが、こちらはとても単純でボールの中心位置にバインドするだけです。

class Connection extends Line {
    public Connection(Ball startBall, Ball endBall) {
        startXProperty().bind(startBall.centerXProperty());
        startYProperty().bind(startBall.centerYProperty());        
        endXProperty().bind(endBall.centerXProperty());
        endYProperty().bind(endBall.centerYProperty());        
    }
}

これだけで結び付けられたボールが実現できます。
この原理を使うとお絵かきソフトなどで、ラインを他の要素にバインドさせることなどができるようになると思います。

いろふさん絵描き歌 by JavaFX

このエントリはいろふ Advent Calendar ではなくて、JavaFX Advent Calendar の 12/14 担当分になります。

昨日は mike_neck さんの JavaFXでGroovyのVetoableが機能するか試してみた です。

ということで、いろふさんのアイコンを描いてみましょう!!

というのも、ゆきーんさんが HTML 5 の Canvas でいろふアイコンを描いていたので、これは JavaFX でも描かなくてはということなわけです。

JavaFX で描画といったら Shape ですよね。最近になって HTML 5 と同じように Canvas も導入されましたけど...

ということで、描いてみました。

まずは枠線。単純に四角形です。

        Group root = new Group();

        // 枠線
        Rectangle rectangle = new Rectangle(0, 0, 300, 300);
        rectangle.setStrokeWidth(5.0);
        rectangle.setStroke(Color.BLACK);
        rectangle.setFill(Color.WHITE);
        root.getChildren().add(rectangle);

次に輪郭。輪郭は Arc クラスをつかうこともできますが、口の部分とつなげたいので Path クラスで描いていきます。Path クラスは XXXXTo クラスで線をつなげていきます。

始点は MoveTo で、円弧は ArcTo です。ArcTo のプロパティが HTML 5 の Canvas と違うので、ちょっと苦労しました ^ ^;;

        // 輪郭
        Path path = new Path();
        path.setStrokeWidth(10.0);
        path.setStroke(Color.BLACK);
        // 枠線を越えないようにクリッピングする
        path.setClip(new Rectangle(0, 0, 300, 300));

        // 輪郭の始点
        path.getElements().add(new MoveTo(126.5, 267));
        // 円弧
        ArcTo arc = new ArcTo();
        arc.setX(146); arc.setY(184.5);
        arc.setRadiusX(117); arc.setRadiusY(117);
        arc.setLargeArcFlag(true);
        path.getElements().add(arc);

そして、口。口は直線なので、LineTo です。最後に閉じたパスにするため CloseTo を使います。

        // 口の部分の直線
        path.getElements().add(new LineTo(210, 255));
        // パスを閉じる
        path.getElements().add(new ClosePath());
        root.getChildren().add(path);

次に吹き出し。ここも Path を使ってます。吹き出しの飛び出ている部分はベジェ曲線 CubicCurveTo を使ってます。

        // 吹き出し
        path = new Path();
        path.setStrokeWidth(10.0);
        path.setStroke(Color.BLACK);
        path.getElements().add(new MoveTo(50, 30));
        path.getElements().add(new LineTo(153, 30));
        arc = new ArcTo();
        arc.setX(153); arc.setY(90);
        arc.setRadiusX(30); arc.setRadiusY(30);
        arc.setSweepFlag(true);
        path.getElements().add(arc);
        path.getElements().add(new LineTo(105, 90));
        // 吹き出しの飛び出ている部分はベジェ曲線で記述
        path.getElements().add(new CubicCurveTo(105, 90, 90, 105, 129, 141));
        path.getElements().add(new CubicCurveTo(90, 135, 66, 120, 81, 90));
        path.getElements().add(new LineTo(57, 90));
        arc = new ArcTo();
        arc.setX(50); arc.setY(30);
        arc.setRadiusX(30); arc.setRadiusY(30);
        arc.setSweepFlag(true);
        path.getElements().add(arc);
        root.getChildren().add(path);

吹き出しの中の点は Circle です。

        // 吹き出しの点
        Circle circle = new Circle(51, 60, 5, Color.BLACK);
        root.getChildren().add(circle);
        circle = new Circle(84, 60, 5, Color.BLACK);
        root.getChildren().add(circle);
        circle = new Circle(120, 60, 5, Color.BLACK);
        root.getChildren().add(circle);
        circle = new Circle(154, 60, 5, Color.BLACK);
        root.getChildren().add(circle);

最後に目。これも Circle。

        // 目
        circle = new Circle(255, 204, 15);
        circle.setFill(null);
        circle.setStroke(Color.BLACK);
        circle.setStrokeWidth(10);
        root.getChildren().add(circle);

これで完成!!

PixelReader/PixelWriter

このエントリは JavaFX Advent Calendar の 12/4 担当分になります。

最近、いろいろなところで JavaFX 2.2 の新機能について話をしているのですが、今日はその中の 1 つである PixelReader/PixelWriter クラスについて使い方を説明していきます。

JavaFX 2.2 ではビットマップに関する機能がやっと導入されました。その中でも中心になるのが、ビットマップのピクセルを読み込む PixelReader クラスと、ピクセルを書き込む PixelWriter クラスです。

PixelReader クラスは Image クラスのピクセルを読み込むことができます。PixelWriter クラスが書き込むのは WritableImage クラスです。WritableImage クラスは Image クラスのサブクラスで、Java 2D でいうところの BufferedImage クラスのようなクラスです。

PixelReader/PixelWriter クラスではいくつかメソッドが定義されていますが、まずは 1 ピクセルの読み書きから。

たとえば、あるイメージをコピーしたイメージを作ってみましょう。

    Image src = new Image(...);
    PixelReader reader = src.getPixelReader();

    int width = (int)src.getWidth();
    int height = (int)src.getHeight();

    WritableImage dest = new WritableImage(width, height);
    PixelWriter writer = dest.getPixelWriter();

    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            // srcのイメージのピクセルを読み込んで、destに書き込む
            Color color = reader.getColor(x, y);
            writer.setColor(x, y, color);

            // こちらでも OK
//            int argb = reader.getArgb(x, y);
//            writer.setArgb(x, y, argb);
        }
    }

PixelReader オブジェクトは Image オブジェクトの getPixelReader メソッドで取得できます。

WritableImage オブジェクトは縦横を指定して生成します。そして、PixelWriter オブジェクトは getPixelWriter メソッドで取得します。

後は、PixelReader#getColor(x, y) で (x, y) 座標の Color オブジェクトを取得できます。同様に、PixelWriter#setColor(x, y, color) で、(x, y) 座標を Color オブジェクトが示す色にします。

同じように PixelReader#getArgb メソッド、PixelWriter#setArgb メソッドというのもあります。これは色を int で扱います。int の先頭 8 ビットが alpha、次の 8 ビットが赤、同様に 8 ビットずつ緑、青となります。

さて、イメージをコピーするのだけでは味気ないので、ここではイメージをぼかしてみます。ここではあまりキレイなぼかしにならないボックスブラーを扱います。

ブラーはあるピクセルの周りのピクセルも一緒に読み込んで、色の平均をとって、そのピクセルの色とするという処理で実現します。

周りのピクセルの取り方を四角にするとボックスブラーになります。

では、書いてみましょう。

    private void blur() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                double red = 0;
                double green = 0;
                double blue = 0;
                double alpha = 0;
                int count = 0;
                for (int i = -kernelSize; i <= kernelSize; i++) {
                    for (int j = -kernelSize; j <= kernelSize; j++) {
                        if (x + i < 0 || x + i >= width 
                           || y + j < 0 || y + j >= height) {
                            continue;
                        }
                        Color color = reader.getColor(x + i, y + j);
                        red += color.getRed();
                        green += color.getGreen();
                        blue += color.getBlue();
                        alpha += color.getOpacity();
                        count++;
                    }
                }
                Color blurColor = Color.color(red / count, 
                                              green / count, 
                                              blue / count, 
                                              alpha / count);
                writer.setColor(x, y, blurColor);
            }
        }
    }

ちょっと面倒くさいのが、縁の部分です。ここは周りのピクセルを全部読み込めないので、読み込めるところだけ読み込んで平均しています。

さて、せっかくなので、スライダでぼかし量を決められるようにしてみます。

        slider.valueProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable o) {
                DoubleProperty value = (DoubleProperty)o;
                int intValue = (int) value.get();
                if (intValue != kernelSize) {
                    kernelSize = intValue;
                    blur();
                }
            }
        });

Slier クラスの value プロパティは double なので、int に変換しています。実際に blur メソッドをコールしているのは、value プロパティが double なので 0.0 から 0.1 に変化しても invalidated メソッドがコールされてしまうのですが、int としてみれば変化していないとみなせるからです。

実行すると、下のようになります。


ところが、この方法だととてもパフォーマンスが悪いのです。特にぼかし量を増やした時は、顕著にパフォーマンスが落ちます。

まぁ、当たり前ですね。1 ピクセルを書き込むのに、何度も読んでいるわけですから。パフォーマンスを向上させるには、もっと読み込み回数を減らす必要があります。

じゃあ、どうするか。まとめて読み込めばいいのです。

複数ピクセルの読み込み

PixelReader クラスには getPixels というまとめてピクセルを読み込むメソッドがあります。これを使えば読み込み回数を減らすことができます。

getPixels は読み込んだピクセルの格納方法により 3 種類のオーバーロードがあります。byte、int そして Buffer を使う 3 種類です。

ここでは int を使ってみます。

int を使うには、ピクセルのフォーマットを指定する必要があります。フォーマットは WritablePixelFormat クラスで指定します。WritablePixelFormat クラスのスーパークラスである PixelFormat の static メソッドで WritablePixelFormat オブジェクトのファクトリメソッドがあるので、それを使用します。

int が使えるのは getIntArgbInstance メソッドか getIntArgbPreInstance メソッドのどちらかです。しかし、getIntArgbPreInstance メソッドの Pre がよく分からないんですよね。PixelFormat.Type という enum にpremultiplied と記述されているのですが、どういう意味なんだろう?

ということで、getIntArgbPreInstance メソッドを使用します。

getPixels メソッドの引数は x, y, width, height, writablepixelformt, buffer, offset, scalingStride となります。buffer が int で、offset が buffer のオフセットを指定します。最後の scalingStride は列から次の列までの幅を示すのですが、width でいいと思うんですけどねぇ。

ということで、blur メソッドを書き換えた blur2 メソッドです。

    private void blur2() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();
        WritablePixelFormat<IntBuffer> format 
            = WritablePixelFormat.getIntArgbInstance();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                int centerX = x - kernelSize;
                int centerY = y - kernelSize;
                int kernelWidth = kernelSize * 2 + 1;
                int kernelHeight = kernelSize * 2 + 1;

                if (centerX < 0) {
                    centerX = 0;
                    kernelWidth = x + kernelSize;
                } else if (x + kernelSize >= width) {
                    kernelWidth = width - centerX;
                }

                if (centerY < 0) {
                    centerY = 0;
                    kernelHeight = y + kernelSize;
                } else if (y + kernelSize >= height) {
                    kernelHeight = height - centerY;
                }

                int[] buffer = new int[kernelWidth * kernelHeight];
                reader.getPixels(centerX, centerY, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);

                int alpha = 0;
                int red = 0;
                int green = 0;
                int blue = 0;

                for (int color : buffer) {
                    alpha += (color >>> 24);
                    red += (color >>> 16 & 0xFF);
                    green += (color >>> 8 & 0xFF);
                    blue += (color & 0xFF);
                }

                alpha = alpha / kernelWidth / kernelHeight;
                red = red / kernelWidth / kernelHeight;
                green = green / kernelWidth / kernelHeight;
                blue = blue / kernelWidth / kernelHeight;

                int blurColor = (alpha << 24) 
                              + (red << 16) 
                              + (green << 8) 
                              + blue;
                writer.setArgb(x, y, blurColor);
            }
        }
    }

かなり長くなっているのは端の処理が面倒くさくなっているからです。

それでも、1 ピクセルごとに読み込んでいたのに比べると、かなり処理が軽くなっています。

比較できるように blur メソッドを使うか、blur2 メソッドを使うかをラジオボタンで指定できるようにしてみました。

複数ピクセルの書き込み

PixelReader クラスが複数ピクセルの読み込みがあるのですから、もちろん PixelWriter にも複数ピクセルをまとめて書き込むメソッドがあります。

これを使ってモザイク処理をしてみましょう。

ぼかしの時と同じようにピクセルの平均を取るのですが、それをそのピクセルすべての色にしてしまえばモザイクになります。

PixelWriter#setPixels メソッドは、PixelReader#getPixels メソッドと同じように、複数のオーバーロードがあります。byte、int、Buffer の 3 種類に加えて、PixelReader オブジェクトから読み込んだものを書き込むというオーバーロードもあります。

もちろんここでは int[] を使うものを使用します。

    private void mosaic() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();
        WritablePixelFormat<IntBuffer> format 
            = WritablePixelFormat.getIntArgbInstance();

        for (int x = kernelSize; 
             x < width - kernelSize * 2; 
             x += kernelSize * 2 + 1) {
            for (int y = kernelSize; 
                 y < height - kernelSize * 2; 
                 y += kernelSize * 2 + 1) {

                int kernelWidth = kernelSize * 2 + 1;
                int kernelHeight = kernelSize * 2 + 1;

                int[] buffer = new int[kernelWidth * kernelHeight];
                reader.getPixels(x, y, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);

                int alpha = 0;
                int red = 0;
                int green = 0;
                int blue = 0;

                for (int color : buffer) {
                    alpha += (color >>> 24);
                    red += (color >>> 16 & 0xFF);
                    green += (color >>> 8 & 0xFF);
                    blue += (color & 0xFF);
                }
                alpha = alpha / kernelWidth / kernelHeight;
                red = red / kernelWidth / kernelHeight;
                green = green / kernelWidth / kernelHeight;
                blue = blue / kernelWidth / kernelHeight;

                int blurColor = (alpha << 24) 
                              + (red << 16) 
                              + (green << 8) 
                              + blue;
                Arrays.fill(buffer, blurColor);
                writer.setPixels(x, y, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);
            }
        }
    }

同じようにラジオボタンで選択できるようにしてみました。

ということで、PixelReader クラスと PixelWriter クラスの使い方でした。

明日は @halcat0x15a さんです。

import java.nio.IntBuffer;
import java.util.Arrays;
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Slider;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.image.WritablePixelFormat;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class WritableImageDemo extends Application {

    private Image src;
    private WritableImage dest;
    private int kernelSize = 1;
    private int width;
    private int height;
    
    private RadioButton blurButton;
    private RadioButton blur2Button;
    private RadioButton mosaicButton;

    @Override
    public void start(Stage stage) {

        AnchorPane root = new AnchorPane();

        initImage(root);

        Scene scene = new Scene(root);

        stage.setTitle("WritableImage Demo");
        stage.setResizable(false);
        stage.setScene(scene);
        stage.show();
    }

    private void initImage(AnchorPane root) {
        src = new Image("macaron.jpg");
        ImageView srcView = new ImageView(src);
        root.getChildren().add(srcView);
        AnchorPane.setTopAnchor(srcView, 0.0);
        AnchorPane.setLeftAnchor(srcView, 0.0);

        width = (int) src.getWidth();
        height = (int) src.getHeight();
        root.setPrefSize(width * 2.0, height + 50);

        dest = new WritableImage(width, height);
        ImageView destView = new ImageView(dest);
        destView.setTranslateX(width);
        root.getChildren().add(destView);
        AnchorPane.setTopAnchor(destView, 0.0);
        AnchorPane.setRightAnchor(destView, (double) width);

        Slider slider = new Slider(0, 10, kernelSize);
        slider.setPrefSize(width, 50);
        slider.setShowTickLabels(true);
        slider.setShowTickMarks(true);
        slider.setSnapToTicks(true);
        slider.setMajorTickUnit(1.0);
        slider.setMinorTickCount(0);

        slider.valueProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable o) {
                DoubleProperty value = (DoubleProperty) o;
                int intValue = (int) value.get();
                if (intValue != kernelSize) {
                    kernelSize = intValue;
                    if (blurButton.isSelected()) {
                        blur();
                    } else if (blur2Button.isSelected()) {
                        blur2();
                    } else {
                        mosaic();
                    }
                }
            }
        });

        root.getChildren().add(slider);
        AnchorPane.setBottomAnchor(slider, 0.0);
        AnchorPane.setRightAnchor(slider, 10.0);

        HBox hbox = new HBox(10);
        hbox.setAlignment(Pos.CENTER);
        hbox.setPrefWidth(width);
        hbox.setPrefHeight(50);
        root.getChildren().add(hbox);
        AnchorPane.setBottomAnchor(hbox, 0.0);
        AnchorPane.setLeftAnchor(hbox, 10.0);

        ToggleGroup group = new ToggleGroup();
        blurButton = new RadioButton("Blur");
        blurButton.setToggleGroup(group);
        blurButton.setSelected(true);
        hbox.getChildren().add(blurButton);
        blur2Button = new RadioButton("Blur2");
        blur2Button.setToggleGroup(group);
        hbox.getChildren().add(blur2Button);
        mosaicButton = new RadioButton("Mosaic");
        mosaicButton.setToggleGroup(group);
        hbox.getChildren().add(mosaicButton);

        blur();
    }

    private void blur() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                double red = 0;
                double green = 0;
                double blue = 0;
                double alpha = 0;
                int count = 0;
                for (int i = -kernelSize; i <= kernelSize; i++) {
                    for (int j = -kernelSize; j <= kernelSize; j++) {
                        if (x + i < 0 || x + i >= width 
                           || y + j < 0 || y + j >= height) {
                            continue;
                        }
                        Color color = reader.getColor(x + i, y + j);
                        red += color.getRed();
                        green += color.getGreen();
                        blue += color.getBlue();
                        alpha += color.getOpacity();
                        count++;
                    }
                }
                Color blurColor = Color.color(red / count, 
                                              green / count, 
                                              blue / count, 
                                              alpha / count);
                writer.setColor(x, y, blurColor);
            }
        }
    }

    private void blur2() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();
        WritablePixelFormat<IntBuffer> format 
            = WritablePixelFormat.getIntArgbInstance();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                int centerX = x - kernelSize;
                int centerY = y - kernelSize;
                int kernelWidth = kernelSize * 2 + 1;
                int kernelHeight = kernelSize * 2 + 1;

                if (centerX < 0) {
                    centerX = 0;
                    kernelWidth = x + kernelSize;
                } else if (x + kernelSize >= width) {
                    kernelWidth = width - centerX;
                }

                if (centerY < 0) {
                    centerY = 0;
                    kernelHeight = y + kernelSize;
                } else if (y + kernelSize >= height) {
                    kernelHeight = height - centerY;
                }

                int[] buffer = new int[kernelWidth * kernelHeight];
                reader.getPixels(centerX, centerY, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);

                int alpha = 0;
                int red = 0;
                int green = 0;
                int blue = 0;

                for (int color : buffer) {
                    alpha += (color >>> 24);
                    red += (color >>> 16 & 0xFF);
                    green += (color >>> 8 & 0xFF);
                    blue += (color & 0xFF);
                }
                alpha = alpha / kernelWidth / kernelHeight;
                red = red / kernelWidth / kernelHeight;
                green = green / kernelWidth / kernelHeight;
                blue = blue / kernelWidth / kernelHeight;

                int blurColor = (alpha << 24) 
                              + (red << 16) 
                              + (green << 8) 
                              + blue;
                writer.setArgb(x, y, blurColor);
            }
        }
    }

    private void mosaic() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();
        WritablePixelFormat<IntBuffer> format 
            = WritablePixelFormat.getIntArgbInstance();

        for (int x = kernelSize; x < width - kernelSize * 2; x += kernelSize * 2 + 1) {
            for (int y = kernelSize; y < height - kernelSize * 2; y += kernelSize * 2 + 1) {
                int kernelWidth = kernelSize * 2 + 1;
                int kernelHeight = kernelSize * 2 + 1;

                int[] buffer = new int[kernelWidth * kernelHeight];
                reader.getPixels(x, y, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);

                int alpha = 0;
                int red = 0;
                int green = 0;
                int blue = 0;

                for (int color : buffer) {
                    alpha += (color >>> 24);
                    red += (color >>> 16 & 0xFF);
                    green += (color >>> 8 & 0xFF);
                    blue += (color & 0xFF);
                }
                alpha = alpha / kernelWidth / kernelHeight;
                red = red / kernelWidth / kernelHeight;
                green = green / kernelWidth / kernelHeight;
                blue = blue / kernelWidth / kernelHeight;

                int blurColor = (alpha << 24) 
                              + (red << 16) 
                              + (green << 8) 
                              + blue;
                Arrays.fill(buffer, blurColor);
                writer.setPixels(x, y, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);
            }
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}