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!!

いろふさん絵描き歌 by JavaFX その 2

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

昨日は @tomo_taka01 さんの JavaFXでLambdaを使ってみる(ProgressBar) です。

明日は @fukai_yas さんです。

ちなみに、このエントリーも いろふ Advent Calendar ではありません。あしからず。

なぜ、その 2 なのかというと、前回の いろふさん絵描き歌 by JavaFX は悔いが残っているからです。というのも、絵描き歌なのに動きがない!

JavaFX といえばアニメーションというはずなのに、そのアニメーションがまったくないなんて、許せないわけです!!!

というわけで、動きをつけてみました。

たとえば、直線のアニメーションは Line で作っています。アニメーション開始時は Line の endX, endY プロパティを starX, startY プロパティと同じにしておきます。そして、最後に目的となる座標に設定しています。

円を描く場合は、Circle ではなく Arc を使っています。ようするに Arc の length を 0 から 360 にアニメーションで変更しているわけです。これで円や半円を書くことができます。

もちろん、これらのアニメーションは Transition ではできないので、すべて Timeline を使っています。

たとえば、はじめの枠線を描く部分は次のようになります (実際のコードではなく、抜粋でも動くように書き直してあります)。

        Line topBorder = new Line(0.0, 0.0, 0.0, 0.0);
        topBorder.setStrokeWidth(5.0);
        container.getChildren().add(topBorder);

        new Timeline(
            new KeyFrame(Duration.ZERO, new KeyValue(topBorder.endXProperty(), 0.0)),
            new KeyFrame(new Duration(500), new KeyValue(topBorder.endXProperty(), 300.0))
        ).play();

はじめの枠線は x 軸方向にしか移動しないので、endX だけを動かしています。

ただし、一番始めに線などをすべてコンテナに登録してしまうと、点だけが表示されてしまいます。そこで、一つ前のアニメーションが終わる時に、次にアニメーションする要素をコンテナに追加する処理を書いています。

たとえば、topBorder の後に rightBorder をアニメションさせるとすると、上の Timeline の部分が下のようになります。

        Line topBorder = new Line(0.0, 0.0, 0.0, 0.0);
        topBorder.setStrokeWidth(5.0);
        container.getChildren().add(topBorder);

        new Timeline(
            new KeyFrame(Duration.ZERO, new KeyValue(topBorder.endXProperty(), 0.0)),
                new KeyFrame(new Duration(500), new EventHandler<ActionEvent>() {
                        @Override
                        public void handle(ActionEvent t) {
                            container.getChildren().add(rightBorder);
                        }
                    },
                    new KeyValue(topBorder.endXProperty(), 300.0))
        ).play();

KeyFrame では、その時間に達した時に行なう処理を EventHandler で書くことができるので、そこでコンテナに追加する処理を記述しています。

さて、今回、一番苦労したのが、吹き出しの部分。特に吹き出しのベジェ曲線で描いている部分です。

1 つ目のベジェはいいのですが、次のベジェが困ってしまうわけです。なぜかというと、単に終点を移動させているだけだと、1 つ目のベジェにくっついてしまうわけです。

終点と始点を結んだ線を赤で描いてみると、前のベジェに沿うようになってしまっていることが分かります。ここを終点が移動してしまうので、下の図のように P を逆さまにしたように感じになってしまいます。

でも、本来やりたいことは下の図の赤線のところを辿っていくようにしたいわけです。

こういう曲線をアニメーションで描くには PathTransition があります。ところが PathTransition はアニメーションさせるノード全体がパス上で移動してしまいます。

ここでやりたいのはノード全体を動かすのではなく、ベジェ曲線の終点だけを動かしたいのです。

そこで、考えたのが、見えない丸をベジェ曲線の上を移動させて、その中心点と移動量を足し合わせたものに、終点をバインドさせるということ。

        final CubicCurve curve2 = new CubicCurve(129, 141, 90, 135, 66, 120, 129, 141);
        curve2.setFill(null);
        curve2.setStroke(Color.BLACK);
        curve2.setStrokeWidth(10.0);
        curve2.setStrokeLineCap(StrokeLineCap.ROUND);
        
        final CubicCurve drawPath = new CubicCurve(129, 141, 90, 135, 66, 120, 81, 90);
        final Circle circle = new Circle(129, 141, 1);

        // 円の中心と移動量を足したものをベジェ曲線の終点とバインドさせる
        curve2.endXProperty().bind(Bindings.add(circle.centerXProperty(),
                                               circle.translateXProperty()));
        curve2.endYProperty().bind(Bindings.add(circle.centerYProperty(),
                                               circle.translateYProperty()));

        // 円を動かす
        PathTransition transition = new PathTransition(new Duration(500), drawPath);
        transition.setNode(circle);

すると、円が動くとベジェ曲線の終点がそれに応じてちゃんと曲線上を移動していくわけです。上の図がそうですね。

ただし、ベジェ曲線の制御点を最終的なポイントにしてしまうとアニメーション中に曲線が大きく曲がってしまうことがあります。そこで、制御点もアニメーションと一緒に移動させています。

ただ、この制御点のアニメーションはいまいちで、アニメーションの終わりに急にベジェ曲線が膨らむようなアニメーションになってしまっています ^ ^;;

下に表示されている歌詞は ゆきーんさんの歌 そのままです。ゆきーんさん (id:lpczclt)、無断で使ってしまってスイマセン。

歌があればもっといいのですが...

id:taizy さんの JavaFX Media playerのちょっと面白い機能 で紹介されたように MediaPlayer でタイマーでイベントを起こすことができ、これを使えば適当な歌詞のところでアニメーションが始めるようなこともできたはずです。

でも、おべんとうばこのうたはメロディがないんですよね (ラップか?)。フリーの MIDI がないかどうか探してみたのですが、メロディがないので MIDI を作りにくいらしく見つけられませんでした。だれか作ってくれれば、それに合わせてアニメーションするように改造します!!
ということで、できたのはこちらです。

ソースは gist においてあります。

https://gist.github.com/49b71f2371570be55cd5

gist 記法がなぜか動かないので、URL です。スイマセン。

バインドのサンプル

このエントリーは 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);
    }
}

第 7 回 JavaFX 勉強会

7/2 に JavaFX 勉強会 を開催しました。

当日はいつものように Oracle に会場を貸していただきました。ありがとうございます。

また、Ustream 職人の @yusukey さんにストリーミング、Togetter は @kimukou2628 さんにまとめていただきました。ありがとうございます!!

Togetter http://togetter.com/li/330960

前半、片貝さんが NetBeans、私が Scene Builder という JavaFX の 2 大ツールについて説明をおこないました。

NetBeans はもうすぐ 7.2 がリリースされるので、7.2 の話題がメインでした。7.2 ではパフォーマンスの改善と、エディタ周りが強化されるようです。

JavaFX に関する部分だと、Scene Builder との連携ができるようになったことや、CSSJavaFX 対応ができたことなどがあります。

後半は、5 人の方に LT をしていただきました。

まず、 @aoetk さんは Mac の話。

特にネイティブバンドルは情報がまだ少ないだけに貴重です。JDK が全部入るというのも、Jigsaw が入ればどうにかなると思うんですけどね。

Jigsaw はすでに Developer Preview が出ているので、ぜひ確かめて欲しいなぁ。

次は @mike_neck さんの JavaFXJavaScript のテストの話。

LT は 5 分なのに 75 枚というのは...

私も blog で書きましたけど、JavaScript のテストに JavaFX は結構使えそうです。

3 番手が @taiz77 さんの JavaFX での画面遷移に関して。

画面遷移のライブラリとして、JFXFLow を紹介され、その後 @taiz77 さんが開発されている JavaFX アプリケーションフレームワーク Creamy について。

時間が足りなくて (というか JFXFLow に時間をかけすぎましたね) 十分に紹介できなかったのが残念でした。ぜひ、どこかでちゃんと紹介して欲しいです。

4 番手が @kimukou2628 さんの Griffon での JavaFX について。

Griffon10 in groovy_fx
View more presentations from kimukou_26   

JavaFX Script 世代の櫻庭にしてみると、GroovyFX はかなりいいですね。FXML も扱えるので、FXML の補完的に使うのがいいかもしれません。

さて、最後の寺田さん。

エンタープライズアプリケーションのクライアントとしての JavaFX について。

JavaFX Enterprise
View more presentations from Oracle Fusion Middleware

Application Client Container を使っていたのですが、その後 @nekop

といわれてしまって、寺田さんかたなしですよ ^ ^;;

さて、私のセッションですが、Scene Builder がメインです。でも、ATND の参加者一覧を見ていると初心者の方も多いようでしたので、Scene Builder が扱う FXML についての説明をはじめにしました。

Scene BuilderでFXML
View more presentations from skrb

とはいいつつ、資料は Scene Builder についてはほとんど書いてません ^ ^;; ツールの説明は実際にやってみた方がいいと思って、ライブコーディングをしたのでした。

ところで、本題が Scene Builder だったので、あまりちゃんと説明しなかったのですが、FXML に対応するクラスを JavaFX ではコントローラ (Controller というクラスがあるわけではないです) と呼びます。

これがすごい違和感を感じるわけです。私自身は Java が登場する前に、SmalltalkMVC を覚えたくちなので、C はそこじゃないだろうと思うわけです。

コントローラと呼ぶことによって、いろいろと誤解されてしまうように思うんですが...

まぁ、それはそれとして、今回は参加者が多くて、ほんとによかったです。80 名が入る部屋がいっぱいになったのは、JavaFX 勉強会でははじめて。それだけ、JavaFX が注目されていることだと思います。

そういえば、来月から ITpro の連載 で、JavaFX 2.x を取りあげることにしました。現状、日本語の JavaFX の情報はかなり少ないのですが、その状況を打破すべくがんばります。

JavaFX でプレゼンツール その 3

その 1
その 2

プレゼンツールのその 3 です。

今回はプレゼンツールの中でのデモです。プレゼンツールが JavaFX で書かれていることの最大の利点は JavaFX のデモをシームレスに行えることです。

でも、シームレスにデモすることは、そんなに難しいことではないです。

まずは、用意しておいたデモを起動させることを考えます。ここでは、Duke のイメージを回転させるというデモを使います。

public class AnimationDemo {
    public void start(Stage stage) {
        Group root = new Group();
        Scene scene = new Scene(root, 600, 500);
        
        ImageView image 
            = new ImageView(new Image(this.getClass().getResourceAsStream("DukeWithHelmet.png")));
        image.setLayoutX(50); image.setLayoutY(20);
        root.getChildren().add(image);
        
        RotateTransition rotate
            = new RotateTransition(new Duration(3000));
        rotate.setNode(image);
        rotate.setToAngle(360);
        rotate.setAutoReverse(true);
        rotate.setCycleCount(Animation.INDEFINITE);
        rotate.setInterpolator(Interpolator.EASE_BOTH);
        rotate.play();
        
        stage.setScene(scene);
        stage.show();
    }
}

start メソッドは使っていますが、Application クラスのサブクラスというわけではないです。というのも、JavaFX では Application.launch メソッドは 1 度しかコールできないからです。

ProcessBuilder クラスで別プロセスとして JavaFX のアプリケーションを起動するのであればいざ知らず、同じプロセスの中で実行するのであればアプリケーションスレッドも同じものを使用します。

さて、FXML にはデモを起動するためのボタンを作っておきます。FXML の一部分をだけを示しておくと、こんな感じです。

    <Button fx:id="p2" onAction="#executeDemo" text="Execute">

onAction 属性で # がついている文字列はコントローラクラスのメソッドを示しています。ようするに、ボタンがクリックされると、コントローラクラスの executeDemo メソッドがコールされます。

executeDemo メソッドはこうなります。

    public void executeDemo(ActionEvent event) {
        // デモの実行
        AnimationDemo demo = new AnimationDemo();
        Stage stage = new Stage();
        demo.start(stage);
    }

先ほどの AnimationDemo クラスのオブジェクトを生成した後、Stage オブジェクトを生成して AnimationDemo クラスの start メソッドをコールしています。

すると、別ウィンドウが開いて Duke が回転します。

f:id:skrb:20120630011233p:image

その場でコンパイル & 実行

ここまではたいしたことありません。では、次にその場でプログラムをコンパイルして、実行できるようにしてみましょう。

ここで使うのが、Compiler API と Script API です。

Compiler APIJava のソースファイルをコンパイルするための API、Script APIJava からスクリプト言語を実行するための API です。

本当は Compiler API だけでいいのですが、Compiler API は使うのがとても面倒くさいのです。

普通のファイルであればそれほど面倒ではないのですが、今は文字列として保持しているコードをコンパイルしなくてはいけません。そうすると、文字列を対象とした仮想ファイルシステムを構築してと文字列をあたかもファイルのように扱ってやなければいけません。

でも、そんなの面倒なので、Script API を使うわけです。

ただし、デフォルトで使用できるスクリプトは JavaScript だけなので、Java 用のスクリプトエンジンが必要です。

Java 用のスクリプトエンジンの JAR ファイルは公開されていないので、自分で JAR ファイルを作る必要があります。Java 用のスクリプトエンジンのソースは java.net の Scripting Project にあります。

Java のスクリプトエンジンは http://java.net/projects/scripting/sources/svn/show/trunk/engines/java です。

make ディレクトリに Ant の build.xml が入っているので、これを使用してコンパイルし、JAR ファイルを作ります。できた JAR ファイルの java-engine.jar を NetBeans のクラスパスに含めます。

また、Script API を使用するには JAVA_HOME/lib/tools.jar が必要なので、これもクラスパスに含めておいてください。

なお、Compiler API については ITproJava 技術最前線で取りあげているので、くわしくはそちらをご覧ください。Script API を使った場合についても書いてあります。

「Java SE 6完全攻略」第89回 プログラムからコンパイル - Compiler API その1
「Java SE 6完全攻略」第89回 プログラムからコンパイル - Compiler API その2
「Java SE 6完全攻略」第89回 プログラムからコンパイル - Compiler API その3
「Java SE 6完全攻略」第89回 プログラムからコンパイル - Compiler API その4
「Java SE 6完全攻略」第89回 プログラムからコンパイル - Compiler API その5

Script API を使った場合、スクリプトエンジンを取得して eval します。ただし、Java の場合はファイル名とメインクラスが必要なので、コンテキストで指定しておきます。

    public void initialize(URL url, ResourceBundle rb) {
        // スクリプトエンジンの取得
        ScriptEngineManager manager = new ScriptEngineManager();
        engine = manager.getEngineByName("java");

        ScriptContext context = engine.getContext();
        // メインクラスとファイル名を設定
        context.setAttribute("mainClass",
                "AnimationDemo",
                ScriptContext.ENGINE_SCOPE);
        context.setAttribute(ScriptEngine.FILENAME,
                "AnimationDemo.java",
                ScriptContext.ENGINE_SCOPE);

                <<省略>>

スクリプトエンジンは ScriptEngineManger クラスから取得します。そして、ファイル名などは ScriptContext クラスで指定します。

UI ではテキストエリアとボタンを用意しておきます。ボタンがクリックされると次のメソッドがコールされます。

    public void compileAndExecuteDemo(ActionEvent event) {
        try {
            // スクリプトの実行
            engine.eval(p3.getText());
        } catch (ScriptException ex) {
            System.err.println("スクリプトの実行に失敗しました");
            ex.printStackTrace();
        }
    }

TextArea オブジェクトから getText メソッドで表示している文字列を取得して、それを eval します。

これでその場でコンパイルして、実行できます。

デフォルトではテキストエリアに先ほどの AnimationDemo クラスがそのまま表示されています。なので、実行すると、先ほどと一緒に Duke が回転するアニメーションが表示されます。

f:id:skrb:20120630230520p:image

そこで、image.setScaleX(0.4); をテキストエリアに追加してから、ボタンをクリックして起動すると細長い Duke が回転します。

f:id:skrb:20120630230521p:image

ソースは GitHub にあげてあります。

https://github.com/skrb/SimplePresenter

ついでにポップアップメニューで終了できるようにするなど、チョコチョコと変更してあります。

追記

その場でコンパイルして、実行は Web Start や Applet だと実行できないことを書くのを忘れていました。

これをできるようにしてしまうと、任意のコードを埋め込んで、実行できるようにするわけですからセキュリティ的にかなりやばいです。まぁ、できないのが当然ですね。

もし、どうしてもやりたいという場合は、署名して、ポリシーを設定すればできますけど、お勧めはできないです。