シェイプのストローク

このエントリーは、2013 年の JavaFX Advent Calendar の 4 日目のエントリーです。

昨日はるーつにゃんの JavaFXでめくるエフェクト!!

明日は sipadan2003 さんです。

JavaFX でシェイプを扱っていると、ストロークに関連したプロパティがいろいろあるのに気がつきます。

色や太さはすぐに分かりますけど、それ以外はなじみがないものが多いのではないでしょうか。Illustrator とか使っている人であれば分かるとは思いますけど。

ということで、ここで少しまとめてみましょう。

ソースは gist にあげました。

まずは、こんなパスを描いておきましょう。

    private Path drawPath() {
        Path path = new Path(
                new MoveTo(40.0, 40.0),
                new LineTo(140.0, 40.0),
                new LineTo(40.0, 80.0)
        );

        return path;
    }

カタカナのフのような形です。このパスについては特に難しいところはないはずです。

では、プロパティをいろいろと設定していきましょう。まずは、線色と線の太さから。

  • Shape#setStrokeWidth(double) 線の太さ
  • Shape#setStroke(Paint) 線の色

これは簡単ですね。setStrokeWidth メソッドは引数が double ですが、もちろん正の値だけです。

setStroke メソッドは線色を設定します。線色の方は Paint クラスなので、グラデーションもできます。

ちなみに、デフォルトでは太さは 1、色は Color.BLACK です。

        // デフォルト (線幅: 1, 線色: 黒)
        Path path = drawPath();
        parent.add(path, 0, 0);
        
        // 線幅: 10
        path = drawPath();
        path.setStrokeWidth(10.0);
        parent.add(path, 1, 0);

        // 線幅: 10, 線色: 赤
        path = drawPath();
        path.setStrokeWidth(10.0);
        path.setStroke(Color.RED);
        parent.add(path, 2, 0);

この 3 種類を実行すると次のようになります。

さて、これからがよく分からなくなるところです。

まず、setStrokeLineJoin メソッドです。ストロークのラインがジョインするメソッドです。何のこっちゃという感じですが、角の部分をどのようにするかを指定するためのメソッドです。

setStrokeLineJoin メソッドの引数は enum の StrokeLineJoin です。この enum は 3 つの値をとります。

MITER はマイター接続は線をそのまま延ばしていって交わったところまで伸ばす接続です。ようするに、とんがっている接続ですね。これがデフォルトになります。

ROUND (ラウンド接続) は MITER とは逆に接続部が丸くなっている接続方法です。

BEVEL (ベベル接続) は直線の角と角を結んだような接続です。

これらの接続は線が細い時は目立たないのですが、太いと目立ちます。太い線が集まっていたりすると、MITER だと線がはみ出てしまうこともあるので、ROUND か BEVEL にする方がいいと思います。

ということで、ここでは太さ 10 の線を引いてみます。

    private Path drawThickPath() {
        Path path = drawPath();
        path.setFill(Color.PINK);
        path.setStroke(Color.BLACK);

        // 線の太さを10にする
        path.setStrokeWidth(20.0);

        return path;
    }

で、3 種類の接続を試してみましょう。分かりやすいように細い白い線を一緒に描画します。

        // 角のデフォルト (マイター接合)
        Group group = new Group();
        path = drawThickPath();
        path.setStrokeLineJoin(StrokeLineJoin.MITER);

        path = drawPath();
        path.setStroke(Color.WHITE);
        group.getChildren().add(group);
        parent.add(group, 0, 2);

        // 角を丸くする (ラウンド接合)
        group = new Group();
        path = drawThickPath();
        path.setStrokeLineJoin(StrokeLineJoin.ROUND);

        path = drawPath();
        path.setStroke(Color.WHITE);
        group.getChildren().add(group);
        parent.add(group, 1, 2);

        // 角を削る (ベベル結合)
        group = new Group();
        path = drawThickPath();
        path.setStrokeLineJoin(StrokeLineJoin.BEVEL);

        path = drawPath();
        path.setStroke(Color.WHITE);
        group.getChildren().add(group);
        parent.add(group, 2, 2);

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

つづいて、線端です。線端も 3 種類あります。

線端は setStrokeLineCap メソッドで設定します。引数は、こちらも enum の StrokeLineCap です。

  • SQUARE
  • ROUND
  • BUTT

SQUARE はもともとの線の線端から線の太さの分だけ四角が飛び出ているような感じです。ROUND は同じように線端が円になっています。

最後の BUTT は端がブチッと切れてしまった感じの線端になります。

これも試してみましょう。

        // 線端のデフォルト (スクエア線端)
        group = new Group();
        path = drawThickPath();
        path.setStrokeLineCap(StrokeLineCap.SQUARE);
        group.getChildren().add(path);

        path = drawPath();
        path.setStroke(Color.WHITE);
        group.getChildren().add(path);
        parent.add(group, 0, 4);

        // 線端を丸くする (ラウンド線端)
        group = new Group();
        path = drawThickPath();
        path.setStrokeLineCap(StrokeLineCap.ROUND);
        group.getChildren().add(path);
        
        path = drawPath();
        path.setStroke(Color.WHITE);
        group.getChildren().add(path);
        parent.add(group, 1, 4);

        // 線端を四角くする (BUTT線端)
        group = new Group();
        path = drawThickPath();
        path.setStrokeLineCap(StrokeLineCap.BUTT);
        group.getChildren().add(path);

        path = drawPath();
        path.setStroke(Color.WHITE);
        group.getChildren().add(path);
        parent.add(group, 2, 4);

ここでも分かりやすいように、太い線の上に線幅1の白い線を描画してあります。

最後は点線です。

点線はちょっと分かりにくいんですよね。

点線にするには、getStrokeDashArray メソッドを使います。あれっ、get と思いますよね。設定するのに、get を使うというと、JavaFX では ObservableList を返すことが多いのです。

ここでも、getStrokeDashArray メソッドでは ObservableList オブジェクトが帰ります。この ObservableList に要素を入れているいくと点線になるのです。

たとえば、10.0 の ObservableList だとします。この場合、線が 10 ピクセル、空白が 10 ピクセルの点線になります。

10.0 と 5.0 が要素の場合、線が 10 ピクセル、空白が 5.0 ピクセルになります。

10, 2, 5, 2 が要素の場合、線が 10、空白 2 線が 5、空白 2 の続きます。つまり、一点鎖線になるわけです。

        // 点線 (線とスペースの長さが同じ)
        path = drawPath();
        path.getStrokeDashArray().addAll(10.0);
        path.setStrokeWidth(5.0);
        parent.add(path, 0, 6);

        // 点線 (線とスペースの長さが異なる)
        path = drawPath();
        path.getStrokeDashArray().addAll(10.0, 5.0);
        path.setStrokeWidth(5.0);
        parent.add(path, 1, 6);

        // 一点鎖線
        path = drawPath();
        path.getStrokeDashArray().addAll(10.0, 2.0, 5.0, 2.0);
        path.setStrokeWidth(5.0);
        parent.add(path, 2, 6);

すると、このようになります。

要素数が偶数だと線の長さから始まって、空白の長さで終わるので分かりやすいですけど、偶数で活けないわけではありません。ただし、奇数だとちょっと分かりにくいのです。

たとえば、10, 2, 5 だと、線 10、空白 2、線 5、空白 10、線 2、空白 5 という繰り返しになります。というように、ちょっと分かりにくいので、要素が 1 つにするか、偶数という方が分かりやすいと思います。

ということで、ちょっと地味なエントリーでした。

最後にソース全体です。


gist7761966

曲線のアニメーション

このエントリーは、2013 年の JavaFX Advent Calendar の初日のエントリーです。

明日は tomo_taka01 さんです。

JavaFX で曲線というと CubicCurve か、QuadCurve です。これらのクラスをアニメーションさせることを考えてみます。

もちろん、トランジションでもいいのですが、PathTransition 以外はあまりおもしろくないので (おもしろくない = 簡単に使えるです)、今日は扱いません。

となると、Timeline か AnimationTimer でアニメーションさせます。せっかくだからグネグネ動くようなアニメーションがいいですね ^ ^;;

Timeline でも AnimationTimer でも、クラスのプロパティが定義してあれば、その値を時間で変化させるということが可能です。

CubicCurve などのクラスのプロパティといえば、端点と制御点です。これらのプロパティを変化させてあげれば、グネグネ動かすことも可能です!

ということで、ここでは制御点をアニメーションさせてみましょう。

ちなみに、制御点というのは端点の接線上にある点で、端点から離れていればいるほど曲線は接線に近づいていきます。

ITpro で以前、端点と制御点の関係を図に描いたので、それを参照してみてください。

まず、アニメーションさせる 2 本の CubicCurve を作っておきます。

        // ベジェ曲線 1本目
        final CubicCurve curve1 = new CubicCurve();
        curve1.setStartX(40.0); curve1.setStartY(100.0);
        curve1.setEndX(360.0); curve1.setEndY(100.0);
        curve1.setStroke(Color.BLACK);
        curve1.setStrokeWidth(10.0);
        curve1.setStrokeLineCap(StrokeLineCap.ROUND);
        curve1.setFill(null);
        parent.getChildren().add(curve1);
        
        // ベジェ曲線 2本目
        final CubicCurve curve2 = new CubicCurve();
        curve2.setStartX(40.0); curve2.setStartY(100.0);
        curve2.setEndX(360.0); curve2.setEndY(100.0);
        curve2.setStroke(Color.BLACK);
        curve2.setStrokeWidth(10.0);
        curve2.setStrokeLineCap(StrokeLineCap.ROUND);
        curve2.setFill(null);
        parent.getChildren().add(curve2);

アニメーションで制御点の値を変化させるので、この時点では設定していません。

では、制御点をアニメーションさせてみましょう!!

ここでは、AnimationTimer を使用して、制御点を円運動させてみます。まず、円運動させるためのサインとコサインを決めておきます。

        // ベジェ曲線の制御点を円運動させるアニメーション
        AnimationTimer timer = new AnimationTimer() {
            @Override
            public void handle(long t) {
                double sin1 = 150 * Math.sin(t/400_000_000.0);
                double cos1 = 150 * Math.cos(t/400_000_000.0);

                // もう一方の制御点は180度ずらす
                double sin2 = 150 * Math.sin(t/400_000_000.0 + Math.PI);
                double cos2 = 150 * Math.cos(t/400_000_000.0 + Math.PI);

端点の一方を sin1/cos1 にしたら、もう一方の端点を sin2/cos2 にすると 8 の字になります。2 つの端点をどちらも sin1/cos1 にすると楕円になります。もちろん、この場合、もう一方の CubicCurve を sin2/cos2 にします。

ここでは 8 の字にしてみます。

                double controlX11 = 40.0 + sin1; 
                double controlY11 = 100.0 + cos1;
                curve1.setControlX1(controlX11);
                curve1.setControlY1(controlY11);
                
                double controlX12 = 360.0 + sin2; 
                double controlY12 = 100.0 + cos2; 
                curve1.setControlX2(controlX12);
                curve1.setControlY2(controlY12);

(40, 100) と (360, 100) は端点の座標です。

もう一方の CubicCurve はこれと逆にします (同じだとくっついてしまうので)。

                double controlX21 = 40.0 + sin2; 
                double controlY21 = 100.0 + cos2;
                curve2.setControlX1(controlX21);
                curve2.setControlY1(controlY21);
                
                double controlX22 = 360.0 + sin1; 
                double controlY22 = 100.0 + cos1; 
                curve2.setControlX2(controlX22);
                curve2.setControlY2(controlY22);
            }
        };

後は AnimationTimer をスタートさせるだけです。

        timer.start();

これでグニグニ動きます。

これと同じ動きをトランジションでも作ることができます。それは円運動のアニメーションをするダミーのノードを作ります (このノードは表示させません)。円運動をさせるには PathAnimation で path に Circle を指定します。

そして、CubicCurve の端点の座標をこのノードの座標にバインドします。

こうすることで、トランジションでも同じ動きを実現できます。そこら辺も ITpro の連載で書いたので、参照してみてください。

ソースは gist にアップしてあります。


gist7729009

JavaFX Hands on Lab

8/24 に JJUGJavaFX ユーザグループの共催で、JavaFX のハンズオンを行いました。

私がメインで説明をして、 id:aoe-tk さんと fukai_yas さんにヘルプしていただきました。

当日はなんと歩留まり 90% 以上!!

人数では隣でやっていた Java EE 7 のハンズオンに負けてしまうのですが、歩留まりや懇親会の参加率ではこちらの方が勝っていましたよww

さて、ハンズオンでは 2 つのアプリケーションを作成しました。一方が Hello, World! と、そのちょっとした拡張。もう一方が表とブラウザを使ったブックマーク的なアプリケーションです。

テキスト

当日はテキストには書いてないのですが、ちょっとした応用をやりました。また、ちょっと補足したいこともあるので、ここに書いておきます。

ラベルとテキストフィールドのバインド

ここで作ったサンプルはテキストフィールドとボタンとラベルから構成されていて、テキストフィールドに文字を入力して、リターンを入力するか、ボタンをクリックすると、ラベルにその入力した文字が反映されるというものでした。

でも、エンターキーやボタンをクリックしなくても、文字を入力したらすぐにラベルに反映させたくなることもありますよね。

そういう場合、Swing/AWT であれば、KeyEvent を拾って、それをもとにラベルに文字を追加するということを行ってきました。でも、これがまた、結構めんどうくさい。

JavaFX であれば、もっと簡単にリアルタイムの反映ができます。

どうやって? バインドです。

バインドは 2 つのプロパティを自動的に同期させる機構です。片方向のバインドもありしますし、双方向のバインドもあります。

たとえば、以下のコードでは、整数のプロパティの y が x にバインドしています。

        IntegerProperty x = new SimpleIntegerProperty();
        IntegerProperty y = new SimpleIntegerProperty();
        
        y.bind(x); // y を x にバインドする

        // x に 10 を代入                
        x.set(10);
        System.out.println(y.get()); // 10 が出力
        // x に 20 を代入                
        x.set(20);
        System.out.println(y.get()); // 20 が出力

ここでは片方向のバインドなので、y に値を設定することはできません。

さて、これをテキストフィールドとラベルにもおこなってしまおうというわけです。

Label クラスで表示する文字列のプロパティは textProperty() メソッドで取得できます。同じように、TextField クラスも textProperty() メソッドで取得できます。

そこで、コントローラクラスの initialize メソッドに次のように記述します。

        label.textProperty().bind(textField.textProperty());

これでテキストフィールドに何か入力すれば、すぐにラベルに反映されます。

KeyEvent を使うのに比べると、すごい簡単ですね。

しかし、本来やりたかったこととはちょっと違います。ここでは、Hello, X! の X の部分だけをテキストフィールドに入力させるということをやっていました。

しかし、先ほどのコードだとテキストフィールドの文字列とラベルの文字列が同一になってしまいます。

ではどうするかというと、プロパティ同士の演算をやってしまえばいいのです。

プロパティ同士の演算は、ユーティリティクラスの Bindings で定義されています。文字列の連結には、concat メソッドが使えます。

そこで、上のコードを下のように書き換えます。

        label.textProperty().bind(Bindings.concat("Hello, ", textField.textProperty(), "!"));

これで OK!

ChangeListener と InvalidationListener

後半のテーブルとブラウザを使用したサンプルでは、テーブルの選択行が変更されると、ブラウザで表示するというものでした。

この時、テーブルの選択行が変わったことを検知する部分を次のように記述していました。

        TableView.TableViewSelectionModel<Bookmark> selectionModel = table.getSelectionModel();
        selectionModel.selectedItemProperty().addListener(new ChangeListener<Bookmark>() {
            @Override
            public void changed(ObservableValue<? extends Bookmark> value, Bookmark old, Bookmark next) {
                String url = next.getUrl();
                engine.load(url);
            }
        });

しかし、これではまれに NullPointerException 例外が発生してしまうことがあります。といのも、chaged メソッドの第 2, 第 3 引数に null が渡ってくることがあるからです。

そのため、ここには null check が必要です。

            public void changed(ObservableValue<? extends Bookmark> value, Bookmark old, Bookmark next) {
                if (next != null) {
                    String url = next.getUrl();
                    engine.load(url);
                }
            }

ところで、ここでは ChangeListener インタフェースを使用しましたが、InvalidationListener インタフェースを使用することもできます。

両者の違いは、ChangeListener インタフェースがプロパティの値が変化したことを検知することに対し、InvalidationListener インタフェースがプロパティが使用されるときに値をチェックするということです。

このため、ChangeListener インタフェースではイベントが発生しても、InvalidationListener インタフェースではイベントが発生しないということがあります。

逆にいうと、InvalidationListener インタフェースを使用すれば、無駄なイベントを処理せずにすみます。

ただ、InvalidationListener インタフェースはちょっと使いにくい。といのも、InvalidationListener インタフェースの invalidated メソッドの引数の方が Observable インタフェースだからです。

Observable インタフェースを実装したクラスがプロパティになるのですが、Observable インタフェースにはリスナー登録のメソッドしか定義していないため、結局プロパティにキャストしてあげなくてはいけません。その前に instanceof で型のチェックをするわけですけど、instanceof にはジェネリクスの型が指定できません。

結局、めんどくさいので引数は使用しないということになりがち。

で、InvalidationListener インタフェースで書くとしたら、次のようになります。

        final TableView.TableViewSelectionModel<Bookmark> selectionModel = table.getSelectionModel();

        selectionModel.selectedItemProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                Bookmark bookmark = selectionModel.getSelectedItem();
                if (bookmark != null) {
                    engine.load(bookmark.getUrl());
                }
            }
        });

さて、ここでの例は ChangeListener インタフェースで書いても、InvalidationListener インタフェースで書いても、差はありません。

でも、ほんとに画面を更新する場合だけ値をチェックすればいい場合も多く、こういう場合は InvalidationListener インタフェースが有効になります。どちらも使えるようにしておけるといいですね。

ページのロード完了時のアニメーション

GitHub にあげてあるサンプルでは、Web ページのロード完了時にフェードインするアニメーションが書いてあります。でも、ハンズオンではもうちょっと派手なアニメーションを書きました。

派手といっても、複数のアニメーションを同時に行うことで派手に見せているだけです。

ここではフェードインと回転とスケーリングを一緒にやってみます。

もともとのコードはこれ。

        // ページのローディングが完了したら、フェードイン
        FadeTransition fadeIn = new FadeTransition(Duration.millis(1_000), webView);
        fadeIn.setToValue(1.0);
        fadeIn.play();

これを次のように変えました。

        FadeTransition fadeIn = new FadeTransition(Duration.millis(1_000), webView);
        fadeIn.setToValue(1.0);
                    
        RotateTransition rotate = new RotateTransition(Duration.millis(1_000), webView);
        rotate.setFromAngle(-360.0);
        rotate.setToAngle(0.0);
                    
        ScaleTransition scale = new ScaleTransition(Duration.millis(1_000), webView);
        scale.setFromX(0.1); scale.setFromY(0.1);
        scale.setToX(1.0); scale.setToY(1.0);
                    
        new ParallelTransition(fadeIn, rotate, scale).play();

RotateTransition クラスが回転、ScaleTransition クラスがスケーリングです。

そして、最後の ParallelTransition クラスで、複数のアニメーションを同時に行います。

まぁ、派手にしても意味はないんですけどね ^ ^;;

さて、ハンズオンですが、アンケートでも好評だというご意見が多かったです。その一方、Java EE と同時開催はやめて欲しいという意見も。

今回は、たまたま大きい会場がとれてしまったものの、そんな人数でハンズオンするのも大変なので、半分に分けましょうということで、こうなったのでした。次回はもうちょっと考えます。

とりあえず、ハンズオンの企画はこれからも続けようと思っているので、もしこういうハンズオンをやって欲しいという意見があれば、JJUG の ML や Twitter の @JJUG へお願いします!!

巻物プレゼン

7/22 に JJUG のナイトセミナ Inside Lambda で「Project Lambda の基礎」というタイトルでプレゼンをしてきました。

内容は SlideShare で見てもらうとして、今回は私は前座で、とりは宮川さんの Lambda の内部構造。なので、今回はちょっと遊ばせてもらいました。

何を遊んだかというと、プレゼンの資料です ^ ^;;

この講演の前に、映画の Short Peace で大友克洋が巻物風のアニメーションをやっているということをテレビで見たのです。絵コンテも巻物ということで、すごい横長。これはおもしろいなぁと思ったわけです。

で、プレゼンでもやってみたくなってしまったわけです、巻物を。

でも、さすがに下の絵のように左から右へと動くわけにはいきません。というのも、そうすると縦書きにしなくては行けないからです。

かといって、上から下に動かすと巻物というより、掛け軸かすだれみたいになってしまいます。

ということで、下から上に動かすようにしてみました。

動きが決まったら、それをどうやって JavaFX で実現させるかです。

いちおう、3D でほんとに巻物を実現することも考えたのですが、すぐに止めました >< だって、そんなアニメーション作るの大変なんだもん。

では、どうやったかというと、簡単です。

手前に円筒を配置して、紙送りするときには、円筒を回転させるアニメーションをさせます。そして、円筒の後ろに平面のノードを配置して、円筒の回転と一緒に上に移動させるアニメーションを行います。

ただし、それだけだと円筒よりも下の部分が見えてしまって、円筒とノードが別々だということが分かってしまうので、円筒の中心より下は見えないようにクリッピングしてしまいます。

絵で描くと、こんな感じ。

ちなみに、下に配置するノードはほんとの巻物ではないので、長くする必要はないです。その代わりに、紙送りをするときに、今表示しているノードと次に表示するノードをピッタリくっつけて、移動させます。

では、これを JavaFX で書いてみます。

まず、円筒。JavaFX 8 だと、円筒やキューブのようなプリミティブな 3D のオブジェクトを表すクラスが追加されています。

円筒は javafx.scene.shape.Cylinder クラスです。

Cylinder クラスをそのまま描画させると円筒が立って表示されます。そのため、回転を行って水平方向に表示させたいのです。ところが、アニメーションで RotateTransition クラスを使いたいため、回転に rotate プロパティは使えません。というのも RotateTransition クラスが行うアニメーションは rotate プロパティを使用して回転を行わせているからです。

そのため、javafx.scene.transfom.Rotate クラスを使用して、回転を表現し、それをノードにセットするようにしました。

また、画面下方に表示させるために移動も行うのですが、アニメーションでは回転のみなので、こちらは setTranslateX/setTranslateY メソッドで行っています。

なお、ここでは表示サイズを 1024x768 と想定して記述しています。

        cylinder = new Cylinder(40, 1000.0);
        Rotate rotate = Transform.rotate(90, 0, 0);
        cylinder.getTransforms().add(rotate);
        cylinder.setTranslateX(510);
        cylinder.setTranslateY(740);

これをシーングラフに追加すればいいのですが、これだけだと円筒のようには見えません。というのも、JavaFX のデフォルトでは平行投影されているため、円筒が傾いていない限り、単なる四角に見えてしまうためです。

ちゃんと円筒のように膨らみを持たせるには、一点透視にする必要があります。これを行っているのが javafx.scene.Camera クラスです。

Camera クラスのサブクラスには平行投影する ParallelCamera クラスと、一点透視を行う PerspectiveCamera クラスがあります。デフォルトでは ParallelCamera クラスが使われているので、PerspectiveCamera クラスに切り替えます。

        Scene scene = new Scene(root, constants.getWidth(), constants.getHeight());
        scene.setCamera(new PerspectiveCamera());

さて、ここまでで実行してみます。

ちゃんと円筒と分かりますね。しかし、白い円筒ではちょっとあじけない。

そこで、テクスチャーマッピングという手法を使用します。簡単にいえば、イメージを描画オブジェクトの表面に貼ってしまうと手法です。

適当にフリーのイメージで巻物っぽい絵を探してきて、それを貼ってみました。

        PhongMaterial mat = new PhongMaterial();
        Image diffuseMap = new Image(getClass().getResource("images.jpg").toString());
        mat.setDiffuseMap(diffuseMap);
        cylinder.setMaterial(mat);

テクスチャーマッピングで貼るものは javafx.scene.paint.PhongMaterial クラスで表します。そこにイメージをセットして、Cylinder オブジェクトに setMatrial メソッドでセットします。

これでテクスチャーマッピングのできあがりです。実行してみると、こんな感じ。

それっぽくなってきました。

後は後ろにノードを配置させて動かすだけです。これは今までのプレゼンツールの機能を使っています。

ナイトセミナでは SVG を使ったのですが、ここでは FXML で書いてます。単に上に動かすアニメーションです。

そして、それと同時に円筒もアニメーションさせます。

でも、ただ円筒を回転させるのもつまらないし、いくらたっても減らない魔法の円筒になってしまいます。そこで、回転するアニメーションと一緒に、円筒の半径を減らしていくというアニメーションを同時に行っています。

また、半径が減れば、同じ量の紙送りするにはより回転させる必要があるので、回転量もだんだんと増えるようにしてみました。

    public void roll() {
        angle *= 1.1;
        
        RotateTransition trans = new RotateTransition(Duration.millis(2_000));
        trans.setAxis(new Point3D(1.0, 0.0, 0.0));
        trans.setNode(cylinder);
        trans.setByAngle(angle);
        trans.setInterpolator(Interpolator.LINEAR);
        trans.play();
        
        Timeline timeline = new Timeline(
            new KeyFrame(Duration.millis(2_000),
                         new KeyValue(cylinder.radiusProperty(), cylinder.getRadius()*.95))
        );
        timeline.play();
    }

ParallelTransition クラスは使っていないのですが、一緒に動かせばだいたい同時にアニメーションしてくれます。そんなに厳密なものではないので、これで十分。

忘れてましたけど、デフォルトだと回転軸は z 軸方向になっているので、これを x 軸方向にしておいてやる必要があります。

という部分をプレゼンツールにくっつけて、プレゼンしたのでした。

ただ、今までは 1 枚のページの手前になにか表示するという発想がなかったので、やっつけ感あふれる実装になっています。

もし、今後もこういうようにページの手前に表示する必要が出てきたら、ちゃんとした実装にします ^ ^;;;

ここで使ったものは GitHub にアップしてあります。いつも使っているプレゼンツールのサブセットだと思ってください。

makimono
https://github.com/skrb/makimono

Duke 007

もうずいぶん前のことなのですが、Java Day Tokyo 2013Java the Night でデモをしてきました。

何をデモしたかというと、いつもプレゼンテーションで使用している JavaFX のプレゼンテーションツール Caribe。

Caraibe 自体は自分がプレゼンでやりたいことができるように、自由度をかなり上げていて、普通の人が使うのはかなりつらいと思うので、単体では公開していないのです。でも、プレゼンごと GitHub にアップ してあったりするので、そちらを見ていただければと思います。

で、今日はそのオープニングで作ったアニメーションについて。

去年は Star Wars のアニメーション作ったので、今年は 007 です。

Java the Night の音なしバージョンは YouTube にアップしてあるので、そのはじめの部分を見ていただければ分かるはず。

この 007 風 Duke のアニメーションについてどうやって作ったか、解説していきます。

なお、今回のアニメーションだけとりだしたバージョンを GitHub で公開しているので、そちらも参照していただければと思います。

Duke007

なお、今回は NetBeans ではなく、IntelliJ IDEA を使用しています。NetBeansJava SE 8 対応は 7.4 からになると思うのですが、現在は Nightly Build しか公開されていません。

この Nightly Build がほんとうにダメダメ。ビルドが進めば進むほど安定度が悪くなるってどういうこと? 6/30 の段階では、Java SE 8 はなんとか使えても、JavaFX 8 は全然使えなくなってしまいました ><

ということで、はじめての IntelliJ IDEA のプロジェクトでした。

全体構成

いつものように絵は Illustrator で描いて、SVG に変換してあります。

アニメーションしたいパーツごとに、レイヤーを分けてあります。こんな感じ。

それを自作の SVGLoader で読み込んでいます。たとえば、背景の黒を読み込んでいる部分はこんな感じ。

        svgContent = SVGLoader.load(getClass().getResource(SVG_FILE).toString());

        Node background = svgContent.getNode("background");
        root.getChildren().add(background);

ただし、一番始めの円だけは単純なので、コードで書いています。

アニメーションさせるノード群がそろったら、アニメーションのコードを書いていきます。

ここでは全部 Timeline を使って書いてます。メインのタイムラインを SequentialTransition で書いてもいいのですが、ちょっと複雑なことをやろうとすると、SequentialTransition は結構めんどうくさいんですよね。

Java the Night のためにこれを作るときは、時間があまりなかったので、Timeline に直書きしています。より柔軟にやるには以前 複雑なアニメーション で書いたように、メインのタイムラインと、子アニメーションの構成で書く方が柔軟性が高いです。

さて、全体の流れはこんな感じです。

        Timeline timeline = new Timeline(
                new KeyFrame(Duration.ZERO,
                        new KeyValue(propertyX, initX),
                        new KeyValue(propertyY, initY)),
                new KeyFrame(Duration.millis(  500),
                        new KeyValue(propertyX, nextX),
                        new KeyValue(propertyY, nextY)),

            <<省略>>

        );

        timeline.play();

では、部分のアニメーションについて見ていきます。

円のアニメーション

一番始めは、2 つの円がアニメーションする部分です。1 つの円が等速運動していて、もう 1 つの円が等速運動している円に追いついていくような感じ。

2 つ目の円は実際には一定期間同じ場所にいて、一瞬で移動、再び一定期間同じ場所というのを繰り返しています。

普通に Timeline - KeyFrame - KeyValue で書いてしまうとずっと移動してしまうので、工夫が必要です。たとえば...

        Circle circle1 = new Circle(-100.0, 384.0, 50.0);
        circle1.setFill(Color.WHITE);
        root.getChildren().add(circle1);

        Circle circle2 = new Circle(-100.0, 384.0, 50.0);
        circle2.setFill(Color.WHITE);
        root.getChildren().add(circle2);

        Timeline timeline = new Timeline(
                new KeyFrame(Duration.ZERO,
                        new KeyValue(circle1.translateXProperty(), 0.0),
                        new KeyValue(circle2.translateXProperty(), 0.0)),
                // 490ms まで同じ場所に留めて、500ms までの 10ms で移動
                new KeyFrame(Duration.millis(  490),
                        new KeyValue(circle2.translateXProperty(), 0.0)),
                new KeyFrame(Duration.millis(  500),
                        new KeyValue(circle1.translateXProperty(), 200.0),
                        new KeyValue(circle2.translateXProperty(), 200.0)),
                // 990ms まで同じ場所に留めて、1000ms までの 10ms で移動
                new KeyFrame(Duration.millis(  990),
                        new KeyValue(circle2.translateXProperty(), 200.0)),
                new KeyFrame(Duration.millis(1_000),
                        new KeyValue(circle1.translateXProperty(), 400.0),
                        new KeyValue(circle2.translateXProperty(), 400.0)),

何をやっているかというと、500ms ごとに移動をさせているのですが、その 10ms 前まで同じ位置にいるということをわざと書いています。

一方は 500 ミリ秒までの間等速で移動していますが、一方は 0 から 490ms まで同じ位置、490ms から 500ms で移動ということを繰り返しているわけです。

これでもうまくいくのですが、10ms 前の場所を書かなくてはいけないのがちょっと...

そこで、使うのが Interpolator です。

えっ、Interpolator はイージングの時使うんだけじゃないの、と思われるかもしれません。でも、Interpolator には DISCRETE というのがあるのです。

DISCRETE は離散という意味です。つまりパラパラマンガを作るときに使います。ぎりぎりまで同じ場所で、次に違う場所というのは、パラパラマンガと同じなわけですね。


で上の Timeline がこうなりました。

        Timeline timeline = new Timeline(
            new KeyFrame(Duration.ZERO,
                         new KeyValue(circle1.translateXProperty(), 0.0),
                        new KeyValue(circle2.translateXProperty(), 0.0)),
            new KeyFrame(Duration.millis(  500),
                        new KeyValue(circle1.translateXProperty(), 200.0),
                        new KeyValue(circle2.translateXProperty(), 200.0, 
                                     Interpolator.DISCRETE)),
            new KeyFrame(Duration.millis(1_000),
                        new KeyValue(circle1.translateXProperty(), 400.0),
                        new KeyValue(circle2.translateXProperty(), 400.0, 
                                     Interpolator.DISCRETE)),

Transition の場合は setInterpolator メソッドを使用しますが、Timeline の場合は KeyValue で Interpolator を指定します。

つまり KeyFrame ごとに補間方法を変化させられるわけです。

これで、追いかけっこする円ができました。

歩くDuke

次は銃身の穴と一緒に歩く Duke です。実をいうと、この部分は Java the Night に間に合わなくて、やらなかったんです ^ ^;;

歩くアニメーションは足踏みをするパラパラマンガと、移動のアニメーションを組み合わせて行ないます。足踏みの方は繰り返しアニメーションしておきます。

Duke は手が短いので、足だけ動いている絵をまず用意しました。ここでは、5 枚の絵でアニメーションさせています。

移動は、全体の Timeline で一緒に行なっているのですが、足踏みだけは別の Timeline で表してます。

    private Animation prepareWalking(Group root) {
        walkingDuke = new Group();

        // 足踏みしているイメージを読み込み、
        // 透明にしておく
        for (int index = 0; index < 5; index++) {
            Node duke = svgContent.getNode(String.format("walk%02d", index));
            duke.setOpacity(0.0);
            walkingDuke.getChildren().add(duke);
        }

        Timeline walkingAnimation = new Timeline();

        // 一定時間ごとに、透明度を変化させて、表示させるイメージを切り替える
        KeyFrame keyFrame0 = new KeyFrame(Duration.millis(0),
                new KeyValue(walkingDuke.getChildren().get(0).opacityProperty(), 
                             1.0,
                             Interpolator.DISCRETE),
                new KeyValue(walkingDuke.getChildren().get(4).opacityProperty(), 
                             0.0, 
                             Interpolator.DISCRETE));
        walkingAnimation.getKeyFrames().add(keyFrame0);

        for (int i = 1; i < 5; i++) {
            KeyFrame keyFrame = new KeyFrame(Duration.millis(200*i),
                new KeyValue(walkingDuke.getChildren().get(i).opacityProperty(), 
                             1.0, 
                             Interpolator.DISCRETE),
                new KeyValue(walkingDuke.getChildren().get(i-1).opacityProperty(), 
                             0.0, 
                             Interpolator.DISCRETE));
            walkingAnimation.getKeyFrames().add(keyFrame);
        }

        // 無限に繰り返し
        walkingAnimation.setCycleCount(Timeline.INDEFINITE);

        return walkingAnimation;
    }


足踏みのパラパラマンガは、一定時間ごとにイメージを切り替えることで実現します。ここではイメージの切り替えに透明度を変化させることで行ないました。

もちろん、Interpolator は DISCRETE です。

そして、setCycleCount メソッドの引数に INDEFINITE を指定することで、無限回アニメーションさせています。

さて、移動の方です。そちらは前述したように、メインの Timeline でやってます。なので、先ほどの続きから。

                new KeyFrame(Duration.millis(3_000),
                        e -> {
                            // コンテナにこれから移動させるノードを追加
                            root.getChildren().add(barrelHole);
                            root.getChildren().add(walkingDuke);
                            root.getChildren().add(duke);
                            root.getChildren().add(riffle);
                            root.getChildren().add(blood);

                            // 足踏みアニメーションをスタート
                            walkingAnimation.play();
                        },
                        new KeyValue(circle1.translateXProperty(), 1200.0),
                        new KeyValue(circle2.translateXProperty(), 1200.0, Interpolator.DISCRETE),
                        new KeyValue(barrelHole.translateXProperty(), 1400.0),
                        new KeyValue(walkingDuke.translateXProperty(), 1400.0),
                        new KeyValue(riffle.translateXProperty(), 1400.0)),
                new KeyFrame(Duration.millis(7_000),
                        e -> {
                            // 足踏みアニメーションをストップ
                            walkingAnimation.stop();
                        },
                        // 足踏みしている横向きのDukeを非表示にする
                        new KeyValue(walkingDuke.opacityProperty(), 0.0, Interpolator.DISCRETE),
                        new KeyValue(duke.opacityProperty(), 1.0, Interpolator.DISCRETE),
                        new KeyValue(barrelHole.translateXProperty(), 0.0),
                        new KeyValue(walkingDuke.translateXProperty(), 0.0),
                        new KeyValue(riffle.translateXProperty(), 0.0)),

e -> { ... } の部分は Lambda 式で、もともとは EventHandler を表してます。KeyFrame が指定している時間に行なう処理を記述します。

ここでは、3 秒の時にこれから移動する要素をコンテナに追加し、7 秒の時に無限に続く足踏みアニメーションを停止させています。

さて、残りは移動のアニメーションだけで構成されているので、たいしたことないです。

最後にとりあえず、コード載せておきます。なんか、Timeline すごいことになってますねww

package net.javainthebox.dukeanimation;

import com.sun.scenario.animation.shared.ClipInterpolator;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.media.AudioClip;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;
import net.javainthebox.caraibe.svg.SVGContent;
import net.javainthebox.caraibe.svg.SVGLoader;

public class Duke007 extends Application {
    private final static String SVG_FILE = "duke007.svg";

    private SVGContent svgContent;
    private Group walkingDuke;

    @Override
    public void start(Stage stage) {
        Group root = new Group();

        svgContent = SVGLoader.load(getClass().getResource(SVG_FILE).toString());

        Node background = svgContent.getNode("background");
        root.getChildren().add(background);

        starAnimation(root);

        Scene scene = new Scene(root, 1024, 768);
        stage.setScene(scene);
        stage.show();
    }

    private void starAnimation(Group root) {
        Circle circle1 = new Circle(-100.0, 384.0, 50.0);
        circle1.setFill(Color.WHITE);
        root.getChildren().add(circle1);

        Circle circle2 = new Circle(-100.0, 384.0, 50.0);
        circle2.setFill(Color.WHITE);
        root.getChildren().add(circle2);

        Node barrelHole = svgContent.getNode("barrelhole");
        Node duke = svgContent.getNode("dukestand");
        duke.setOpacity(0.0);
        Node riffle = svgContent.getNode("riffle");
        Node blood = svgContent.getNode("blood");

        Animation walkingAnimation = prepareWalking(root);

        Timeline timeline = new Timeline(
                new KeyFrame(Duration.millis(  500),
                        new KeyValue(circle1.translateXProperty(), 200.0),
                        new KeyValue(circle2.translateXProperty(), 200.0, Interpolator.DISCRETE)),
                new KeyFrame(Duration.millis(1_000),
                        new KeyValue(circle1.translateXProperty(), 400.0),
                        new KeyValue(circle2.translateXProperty(), 400.0, Interpolator.DISCRETE)),
                new KeyFrame(Duration.millis(1_500),
                        new KeyValue(circle1.translateXProperty(), 600.0),
                        new KeyValue(circle2.translateXProperty(), 600.0, Interpolator.DISCRETE)),
                new KeyFrame(Duration.millis(2_000),
                        new KeyValue(circle1.translateXProperty(), 800.0),
                        new KeyValue(circle2.translateXProperty(), 800.0, Interpolator.DISCRETE)),
                new KeyFrame(Duration.millis(2_500),
                        new KeyValue(circle1.translateXProperty(), 1000.0),
                        new KeyValue(circle2.translateXProperty(), 1000.0, Interpolator.DISCRETE)),
                new KeyFrame(Duration.millis(3_000),
                        e -> {
                            root.getChildren().add(barrelHole);
                            root.getChildren().add(walkingDuke);
                            root.getChildren().add(duke);
                            root.getChildren().add(riffle);
                            root.getChildren().add(blood);

                            walkingAnimation.play();
                        },
                        new KeyValue(circle1.translateXProperty(), 1200.0),
                        new KeyValue(circle2.translateXProperty(), 1200.0, Interpolator.DISCRETE),
                        new KeyValue(barrelHole.translateXProperty(), 1400.0),
                        new KeyValue(walkingDuke.translateXProperty(), 1400.0),
                        new KeyValue(riffle.translateXProperty(), 1400.0)),
                new KeyFrame(Duration.millis(7_000),
                        e -> {
                            walkingAnimation.stop();
                        },
                        new KeyValue(walkingDuke.opacityProperty(), 0.0, Interpolator.DISCRETE),
                        new KeyValue(duke.opacityProperty(), 1.0, Interpolator.DISCRETE),
                        new KeyValue(barrelHole.translateXProperty(), 0.0),
                        new KeyValue(walkingDuke.translateXProperty(), 0.0),
                        new KeyValue(riffle.translateXProperty(), 0.0)),
                new KeyFrame(Duration.millis(7_500),
                        e -> {
                            AudioClip clip = new AudioClip(getClass().getResource("OMT004_02S005.wav").toString());
                            clip.play();
                        }),
                new KeyFrame(Duration.millis(8_000),
                        new KeyValue(barrelHole.translateXProperty(), 0.0),
                        new KeyValue(barrelHole.translateYProperty(), 0.0),
                        new KeyValue(riffle.translateXProperty(), 0.0),
                        new KeyValue(riffle.translateYProperty(), 0.0),
                        new KeyValue(blood.translateYProperty(), 0.0)),
                new KeyFrame(Duration.millis(9_000),
                        new KeyValue(barrelHole.translateXProperty(), -200.0),
                        new KeyValue(barrelHole.translateYProperty(), 100.0),
                        new KeyValue(riffle.translateXProperty(), -200.0),
                        new KeyValue(riffle.translateYProperty(), 100.0)),
                new KeyFrame(Duration.millis(10_000),
                        new KeyValue(barrelHole.translateXProperty(), -100.0),
                        new KeyValue(barrelHole.translateYProperty(), 200.0),
                        new KeyValue(riffle.translateXProperty(), -100.0),
                        new KeyValue(riffle.translateYProperty(), 200.0)),
                new KeyFrame(Duration.millis(11_000),
                        new KeyValue(barrelHole.translateXProperty(), 100.0),
                        new KeyValue(barrelHole.translateYProperty(), 400.0),
                        new KeyValue(riffle.translateXProperty(), 100.0),
                        new KeyValue(riffle.translateYProperty(), 400.0)),
                new KeyFrame(Duration.millis(12_000),
                        new KeyValue(barrelHole.translateXProperty(), 0.0),
                        new KeyValue(barrelHole.translateYProperty(), 900.0),
                        new KeyValue(riffle.translateXProperty(), 0.0),
                        new KeyValue(riffle.translateYProperty(), 900.0)),
                new KeyFrame(Duration.millis(15_000),
                        new KeyValue(blood.translateYProperty(), 1700.0))
        );

        timeline.play();
    }

    private Animation prepareWalking(Group root) {
        walkingDuke = new Group();

        // 足踏みしているイメージを読み込み、
        // 透明にしておく
        for (int index = 0; index < 5; index++) {
            Node duke = svgContent.getNode(String.format("walk%02d", index));
            duke.setOpacity(0.0);
            walkingDuke.getChildren().add(duke);
        }

        Timeline walkingAnimation = new Timeline();

        // 一定時間ごとに、透明度を変化させて、表示させるイメージを切り替える
        KeyFrame keyFrame0 = new KeyFrame(Duration.millis(0),
                new KeyValue(walkingDuke.getChildren().get(0).opacityProperty(), 1.0, Interpolator.DISCRETE),
                new KeyValue(walkingDuke.getChildren().get(4).opacityProperty(), 0.0, Interpolator.DISCRETE));
        walkingAnimation.getKeyFrames().add(keyFrame0);

        for (int i = 1; i < 5; i++) {
            KeyFrame keyFrame = new KeyFrame(Duration.millis(200*i),
                    new KeyValue(walkingDuke.getChildren().get(i).opacityProperty(), 1.0, Interpolator.DISCRETE),
                    new KeyValue(walkingDuke.getChildren().get(i-1).opacityProperty(), 0.0, Interpolator.DISCRETE));
            walkingAnimation.getKeyFrames().add(keyFrame);
        }

        // 無限に繰り返し
        walkingAnimation.setCycleCount(Timeline.INDEFINITE);

        return walkingAnimation;
    }

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

Merry Christmas by JavaFX

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

The previous entry was Java FX on PI so far ‘beta’ to use by @masafumi_ohta.

The original entry was published on Christmas, so I wrote Christmas Card application by JavaFX.

The image of accomplished application is shown below:

I wrote two animations: one is snow flying, and the other is neon illumination .

Background image is ImageView. I used Canvas class for snow flying and shapes for llumination.

Snow flake is just circle, drawn by GraphicsContext#fillOval method. To add blur, GraphicsContext#setEffect method was used.

The snow radius, transparency, and size of blur is decided by random value.

    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);
    }

One of constructor argument, initX is initial x position of snow. Because every snow flies from y = 0, y is not assigned.

Because of random determination of radius, transparency and size of blur, appearances of snow flakes vary. This shows a sense of perspective.

I didn't use Timeline class nor Transition, but AnimationTimer class.

AnimationTimer calls handle method in appropriate cycle. Therefore, the coordinate of snow is updated by random value and drawn.

The coordinate updating process is described in SnowParticle#update method.

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

To snow lightly, the next coordinate of snow is defined by random. As well as y-coordinate, x-coordinate is defined by random.

The method that calls update method and draw method of SnowParticle class is update method of ChristmasCard class.

There are many snow flakes in the scene, so List object store the SnowParticle objects. If y-coordinate of SnowParticle object is bigger than height of Scene, the SnowParticle object is removed from List. When calling ChristmasCard#update method, SnowParticle object is created randomly.

When drawing to Canvas, we should paint the Canvas with transparency color at first, then draw snow.

    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()) {
                // When SnowParticle move to the bottom of Scene
                // it is removed
                it.remove();
            } else {       
                particle.draw(context);
            }
        }
        
        // Creating SnowParticle object randomly
        if (random.nextInt(3) == 0) {
            particles.add(new SnowParticle(random.nextDouble() * snowCanvas.getWidth()));
        }
    }

Next is the illumination. I thought I used Text class for illumination at first. But I used SVG.

I used SVGLoader for loading SVG file.

Shape objects loaded from SVG file are divided into stroke and fill. Strokes and fills are animated separately.

There are 14 shapes (14 characters), so I used for loop to set animation. I didn't know that I wasn't able to add KeyValue objects to KeyFrame object after KeyFrame instanciation, because Set object gotten by KeyFrame#getValues was immutable.

Therefore, I make KeyFrame object in the loop, and then add the KeyFrame object to Timeline object.

    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();
    }

To shine characters, I used DropShadow. Bloom is also OK, but I prefer DropShadow class because DropShadow has properties for appearance adjustment.

I uploaded this project to GitHub: JavaFXChristmasCard.

@irof Drawing Song by JavaFX #2

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

The previous entry was JavaFX using JDK1.8 Lambda Expression by @tomo_taka01.

The next entry was irof Advent Calendar 23rd by @fukai_yas.

The reason I wrote @irof Drawing Song by JavaFX #2 is that I regretted the previous @irof Drawing Song. I used JavaFX, but NO ANIMATION!!

Animation is the one of most important JavaFX features, so I used animation on this entry.

However, how do I make shape animation? For this purpose, Transitions aren't suitable. Therefore, I used Timeline class.

For example, I explain line stretching animation: one edge point of line moves, the other doesn't move.

Line class has 4 properties about Line location: startX, startY, endX, and endY. For line stretching, endX and endY move from the same position of startX and startY to target point.

For instance, an animation that end point moves from (0, 0) to (300, 100) is indicated by the following code:

        Line line = new Line(0.0, 0.0, 0.0, 0.0);
        container.getChildren().add(line);

        // end point moves from (0, 0) to (300, 100)
        new Timeline(
            new KeyFrame(Duration.ZERO, new KeyValue(line.endXProperty(), 0.0),
                                        new KeyValue(line.endYProperty(), 0.0)),
            new KeyFrame(new Duration(500), new KeyValue(line.endXProperty(), 300.0),
                                            new KeyValue(line.endYProperty(), 100.0))
        ).play();

In case of drawing circle, I don't use Circle class, but Arc class. That is the animation that length property increases from 0 to 360.

        Arc arc = new Arc(100.0, 100.0, 50.0, 50.0, 0.0, 0.0);
        arc.setFill(null);
        arc.setStroke(Color.BLACK);
        container.getChildren().add(arc);

        // drawing circle: length increases from 0 to 360
        new Timeline(
            new KeyFrame(Duration.ZERO, new KeyValue(arc.lengthProperty(), 0.0)),
            new KeyFrame(new Duration(500), new KeyValue(arc.lengthProperty(), 360.0))
        ).play();

However, if adding all of curves or lines at first, the dots are plotted. Therefore, when the previous animation finish, the next animated shape is added to the container.

In case that rightBorder is animated after drawing topBorder, the source code is indicated below:

        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) {
                                 // add next animated shape is added
                                 container.getChildren().add(rightBorder);
                             }
                         },
                         new KeyValue(topBorder.endXProperty(), 300.0))
        ).play();

KeyFrame object calls EventHandler#handle method when duration reached the target time, so I discribed the procedure of adding the next shape to the container into the handle method.


The hardest point was the balloon when I made this drawing song animation. Especially, Bézier curve animation.

I used two Bézier curves in the sample. First one wasn't hard, but second one was too hard for me. The reason was that end point moves along the first Bézier curves while moving end point.

The following draw is indicated the reason. The red line is between the start point and the end point of the second Bézier curves. The red line is located on the Bézier curves, so the end poinnt moves along the first Bézier curves like revers P.

However, the animation I want to do is that the end point moves along the red line of the following figure.

PathTransition is used for animation that a node moves along a path, and PathTransition makes whole node move along path. But, I'd like to move only end point of Bézier curve along path.

An idea came to me!

I used PathTransition to move hidden circle, and bound end point to center of hidden circle plus quantity of transition.

        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);
        // Hidden circle (not include in Scene Graph)
        final Circle circle = new Circle(129, 141, 1);

        // end point bind to center of circle plus quantiti of transition
        curve2.endXProperty().bind(Bindings.add(circle.centerXProperty(),
                                               circle.translateXProperty()));
        curve2.endYProperty().bind(Bindings.add(circle.centerYProperty(),
                                               circle.translateYProperty()));

        // Move hidden circle
        PathTransition transition = new PathTransition(new Duration(500), drawPath);
        transition.setNode(circle);

When the hidden circle moves, the end point moves simultaneously!

However, if I don't made control points of the Bézier curves move, the Bézier curves curvature is too big at first. So, I made control points also move. But the control points animation was not good, so the Bézier curves bend suddenly at the end of animation.

I upload Completed animation to YouTube.

I also update source code to gist:github.

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