JavaFx$マークを動的に描くには

 

JavaFx2.0以降、画面上でのアニメーションが簡単に使えるようになっていて、動くオブジェクトを比較的容易に扱えることができるようになっています。

Oracleのチュートリアルページにも各種サンプルが取り揃えられていて、また巷のみなさまからも多数の作品がネット上に公開されていたりなど、参考となる例には枚挙にいとまがないですが、今回筆者がこうして筆を執ったのには、タイトルの通りのことをするのにそれなりに壁にぶつかり、それをどうやって克服したかを公開することで、類似のことを実現したい方々にとって少しでも参考情報が提供できればという考えからです。

ちなみにJavaFxを触って日が浅いこともあり、今回のこの方法以外にもっと簡単な方法等があればぜひ情報共有をお願いします。

 

筆者が実現しようとした最初の基本的なアニメーションは「JavaFx$マークを動的に描く」です。結果的に、以下のようなものを描画したかったのです。

※実際には、動きます。

 

これを静止画で書くのはそれほど大変ではありません。しかし、これを動的に描こうとすると、意外と簡単にできそうでできませんでした。Oracleのチュートリアルでは、図形(円や正方形などの矩形)を直線的に移動(PathTranstionLineToの組み合わせ)、あるいは曲線的に移動(PathTranstionCubicCurveToの組み合わせ)や、複雑なアニメーションの場合Timelineを使う事などが紹介されていましたが、そのどれで試しても動的に伸びる直線や曲線をイメージ通りに描くことがなかなかできません。最終的にこの形に至るまで、大別すると基本的なオブジェクト操作に加え、以下の要素を適用しています。

 

@LineQuadCurveCubicCurveオブジェクトのプロパティ値の変更による動的描画

Aベジェ曲線の応用

BDoubleBindingクラスのbind()メソッドで各プロパティ値に外部パラメタをバインドさせる方法による描画

CTimelineによるアニメーションオブジェクトの時系列での遅延操作

以下順に解説していきます。

 

@LineQuadCurveCubicCurveオブジェクトのプロパティ値の変更による動的描画

まず一番最初に行いたいのは直線の動的描画です。これは既存のLineオブジェクトのendXendYプロパティをTimelineで動的に操作することでも実現できるのでそれほど難しくはありませんでした。Javaでは画面の描画可能な平面(上記フォームでは、水色の枠に囲まれた白色部分)の左上の角をXY座標で(0,0)とし、そこから右に正のX座標、下に正のY座標として座標平面が定義されます。まずは、まだ$マークを動的に描けるほどの応用的技術は身に着けていないとして、傾きが-1/2Y切片が100の直線を描いてみます。JavaFxの各種Shapeオブジェクトには「プロパティ」と呼ばれる概念が登場していますが、これはその値をどういう値に設定するかという、通常のJavaオブジェクトが持つインスタンス変数的な機能に加えて、外部の別の変数とバインドすることができる要素なのですが、このバインドについては後述します(しかし、本稿ではこのバインドが後に決定的に重要な要素として再登場します)。以下にサンプルの抜粋を掲載します。

※下記サンプルの抜粋では、javafx.application.Applicationクラスの継承クラスを実装したクラスのstartメソッドのみ記載しています。

 

       @Override

       public void start(Stage stage) throws Exception {

              stage.setTitle("Line Drawer");

              Scene scene = new Scene(new Group(), 400, 400);

              stage.setScene(scene);

              stage.show();

             

              final double x1 =   5.0;

              final double y1 = 100.0;

              final double x2 = 200.0;

              final double y2 =   5.0;

             

              Line line = new Line();

              line.setStartX(x1);

              line.setStartY(y1);

              line.setEndX(x1);

              line.setEndY(y1);

              line.setFill(Color.TRANSPARENT);

              line.setStroke(Color.BLUE);

              line.setStrokeWidth(5);

              final Group group = (Group)scene.getRoot();

              group.getChildren().add(line);

             

              new Timeline(

                            new KeyFrame(Duration.seconds(3), new KeyValue(line.endXProperty(), x2))

                       , new KeyFrame(Duration.seconds(3), new KeyValue(line.endYProperty(), y2))

        ).play();

             

       }

※実際には、動きます!

 

ここでは、Lineオブジェクトの始点と終点を共に点P1(x1, y1)に設定し、そのあとでTimelineオブジェクトにおいて「3秒間の間隔で終点を点P2(x2, y2)に移せ」と指示しています。直線の場合、制御したい点(この場合は終点)と時間的な要素が直線的な比例関係にあるので、この設定のままでもうまく動くので比較的容易に実現できましたが、これが曲線となるととたんに難しくなります。Shapeオブジェクトのサブクラスのうち、曲線を表現するオブジェクトとしては、Arc, Circle, CubicCurve, Ellipse, QuadCurve, SVGPathなどの組み込みオブジェクトがあります。たとえばArc(円弧)クラスには終点に加えて角度がプロパティとして与えられていますので、こいつは角度を動的に変化させることで対応できそうです。

 

       @Override

       public void start(Stage stage) throws Exception {

              stage.setTitle("Arc Drawer");

              AnchorPane pane = new AnchorPane();

              Scene scene = new Scene(pane);

              stage.setScene(scene);

              stage.show();

             

              final double x1 = 300.0;

              final double y1 = 300.0;

              final double x2 = 200.0;

              final double y2 = 100.0;

              final double a1 = 45.0;

              final double a2 = 275.0;

 

              Arc arc = new Arc();

              arc.setCenterX(x1);

              arc.setCenterY(y1);

              arc.setRadiusX(x2);

              arc.setRadiusY(y2);

              arc.setStartAngle(a1);

              arc.setLength(0);

              arc.setType(ArcType.OPEN);

              arc.setStroke(Color.BLUE);

              arc.setStrokeWidth(5);

              arc.setFill(Color.TRANSPARENT);

              pane.getChildren().add(arc);

             

              new Timeline(

                     new KeyFrame(Duration.seconds(3), new KeyValue(arc.lengthProperty(), a2))

        ).play();

             

       }

 

※実際には、動きます!!

 

角度が45°からスタートし、270°回転する長半径が200、短半径が100の楕円の弧を描くことができました。これでも$マークの上下の曲線部分(楕円部分)を描くことができますが、中央付近にある波線部分を描くのが難しい。2つの楕円軌道を組み合わせれば描けないことはありませんが、角度と位置が一致する変曲点を計算して導かなければなりません。これだど変曲点がX軸に水平となる場合はまだ簡単な計算で済みますが、そうでない波線を描かなくてはけない場合、結構大変そう。う〜ん、なんとか点の指定だけで曲線が描けないものか。

 

Aベジェ曲線の応用

そこでQuadCurveCubicCurve2つの曲線を表現するオブジェクトに着目しました。両者はそれぞれ、2次と3次のベジェ曲線を始点、終点および制御点を指定することで描くことのできるクラスで、動的に描くことを考えなければ(すなわち静的に描けば)簡単に$マークの真ん中の部分が描けてしまいます。

 

       @Override

       public void start(Stage stage) throws Exception {

              stage.setTitle("CubicCurve Drawer");

              Scene scene = new Scene(new Group(), 400, 400);

              stage.setScene(scene);

              stage.show();

             

              final double x1 = 0.0;

              final double y1 = 0.0;

              final double x2 = 0.0;

              final double y2 = 100.0;

              final double x3 = 200.0;

              final double y3 = 0.0;

              final double x4 = 200.0;

              final double y4 = 100.0;

             

              CubicCurve cubic = new CubicCurve();

              cubic.setStartX(x1);

              cubic.setStartY(y1);

              cubic.setControlX1(x2);

              cubic.setControlY1(y2);

              cubic.setControlX2(x3);

              cubic.setControlY2(y3);

              cubic.setEndX(x4);

              cubic.setEndY(y4);

              cubic.setFill(Color.TRANSPARENT);

              cubic.setStroke(Color.BLUE);

              cubic.setStrokeWidth(5);

              final Group group = (Group)scene.getRoot();

              group.getChildren().add(cubic);

             

       }

 

 

※まだ動きません。

 

しかし、この曲線の終点だけを動かそうとすると一筋縄ではいきそうもありません。直線や円弧といった基本的な図形であれば、終点や角度といったパラメータと時間とを直線的に比例させることにのみ着目して延伸することができましたが、ベジェ曲線の場合そうはいきません。試しに以下のようなプログラムを書いたとしても、ただ大きさが膨らむような描画しかできません。

 

       @Override

       public void start(Stage stage) throws Exception {

              stage.setTitle("CubicCurve Drawer");

              Scene scene = new Scene(new Group(), 400, 400);

              stage.setScene(scene);

              stage.show();

             

              final double x1 = 0.0;

              final double y1 = 0.0;

              final double x2 = 0.0;

              final double y2 = 100.0;

              final double x3 = 200.0;

              final double y3 = 0.0;

              final double x4 = 200.0;

              final double y4 = 100.0;

             

              CubicCurve cubic = new CubicCurve();

              cubic.setStartX(x1);

              cubic.setStartY(y1);

              cubic.setControlX1(x1);

              cubic.setControlY1(y1);

              cubic.setControlX2(x1);

              cubic.setControlY2(y1);

              cubic.setEndX(x1);

              cubic.setEndY(y1);

              cubic.setFill(Color.TRANSPARENT);

              cubic.setStroke(Color.BLUE);

              cubic.setStrokeWidth(5);

              final Group group = (Group)scene.getRoot();

              group.getChildren().add(cubic);

             

              new Timeline(

                            new KeyFrame(Duration.seconds(3), new KeyValue(cubic.controlX1Property(), x2))

                       , new KeyFrame(Duration.seconds(3), new KeyValue(cubic.controlY1Property(), y2))

                       , new KeyFrame(Duration.seconds(3), new KeyValue(cubic.controlX2Property(), x3))

                       , new KeyFrame(Duration.seconds(3), new KeyValue(cubic.controlY2Property(), y3))

                       , new KeyFrame(Duration.seconds(3), new KeyValue(cubic.endXProperty(), x4))

                       , new KeyFrame(Duration.seconds(3), new KeyValue(cubic.endYProperty(), y4))

               ).play();

       }

 

 

 =>  =>

※縮尺が小さいベジェ曲線が、段々大きくなっていきます。

 

そうじゃないんです、描きたいのは。左上端からスタートしてなぞるように線を描いていってほしいのです!

 

そこで参考にさせていただいたのが「いろふさん絵描き歌 by JavaFX その 2 http://skrb.hatenablog.com/entry/2012/12/22/235800」です。線が動いている、、すごい。。どうやっているんだ、なんとbind()とかいうメソッドを使っていらっしゃるじゃないですか、これだ!これで座標の指定と単一のパラメータのみ動かすようにすることで波線を描けるようにすればいいんだ。と、着想したところでベジェ曲線を単一のパラメータで描くという、半ば数学の問題みたいになってきました。いったん、ここからベジェ曲線の数式を解くというお話になっていきます。具体的には高2あたりの代数幾何(今はもうそう言わないんだろうか、、)で習った、直線や曲線のベクトルと媒介変数(パラメータ)による表示のところです。数学嫌いの方もちょっと見ていってください。

 

原点Oから、ある2A(x1, y1)B(x2, y2)を通る直線上の点P(x, y)までのベクトルは、パラメータtと原点Oからのベクトルを用いて、

と表すことができ、それらのx,y成分は、

と表すことができる。

 

…、って読んでもちんぷんかんぷんの人は以下の値をエクセルに打ち込んで、上の数式が直線になる(tの値を0から1まで変化させることでPAからBの間を通る直線の上を移動していく)ことをグラフを書いて確かめてみてください。

 

A=(1, 4)B(3, 2)

xの数式:x=t*3+(1-t)*1

yの数式:y=t*2+(1-t)*4

x

y

0

1

4

0.1

1.2

3.8

0.2

1.4

3.6

0.3

1.6

3.4

0.4

1.8

3.2

0.5

2

3

0.6

2.2

2.8

0.7

2.4

2.6

0.8

2.6

2.4

0.9

2.8

2.2

1

3

2

 

※点Pは直線AB上をtの値に従って移動する、ABt:1-tに内分する点。

 

これである直線が2ABtという単一のパラメータを用いて表現できることがわかりました。さて、ここでベジェ曲線についてです。ベジェ曲線の歴史的生い立ちはWikipedia等の著名な文献や、それよりもっと解かりやすいサイトにしっかり書いてありますのでここではかいつまんで説明しますと、上記の2点に加えて点Cを登場させて、それぞれの直線ABBC上をt:1-tに内分した状態で移動する動点P1P2を考え、さらにそのP1P2t:1-tに内分する点Pが通る軌跡が曲線になるというものです。(ちなみにこれは2次ベジェ曲線)

 

※フリーハンドで書いてますので、微妙にずれてます。本物のベジェ曲線はずれません、ごめんなさい。

 

この黄色い曲線上を動く点P(星印)を、点A,B,Cと単一のパラメータtで表せるとうれしいので数式を解いてみます。

P1P2はそれぞれABBCt:1-tに内分する点であることから、

すなわち、それらのx,y成分は、A(x1, y1)B(x2, y2)C(x3, y3)、またP1(x4, y4)P2(x5, y5)として、

またP(x0, y0)P1P2を通る直線をt:1-tに内分する点であることから、

よって、

P1P2の式を代入すると、

となり、2次のベジェ曲線上を動く点Pを、3A,B,Cの座標成分とパラメータtのみで表すことができました。

 

さらに制御点をもう一つ加え(始点=A、制御点1=B、制御点2=C、終点=D)、3本の直線をt:1-tに内分する点P1,P2,P3を、これまたt:1-tに内分する点P4P5を求め、さらにP4P5t:1-tに内分する点Pが通る軌跡が描く曲線が3次ベジェ曲線です。さすがにExcelで書くのはそろそろしんどいので、本当のフリーハンドで書いた汚い絵を載せておきます。。

D:\MyDocuments\Picture\Be.JPG

 

 

この数式も2次のときと同じように解けて、

よって、

これを成分ごとに解いて、

が得られます。3次のベジェ曲線についても動点Pを、4A,B,C,Dの座標成分とパラメータtのみで表すことができました。

 

BDoubleBindingクラスのbind()メソッドで各プロパティ値に外部パラメータをバインドさせる方法によるパラメトリック曲線としての描画

ここまで来たらあとは楽勝だ!と思っていたら意外とまだ壁は厚かったです。2次ベジェ曲線の場合、終点だけを動かしてもなぞるようには曲線が描けません。制御点も動的にコントロールしてあげる必要があります。ここで、2次ベジェ曲線のある特性に注目します。それは、2次ベジェ曲線が始点から終点に至るまでの間にある経過点においては、その経過点を2次ベジェ曲線の終点とし、ABを内分する点Aを制御点とすることで描いた動点の軌跡もまた、元の2次ベジェ曲線と同じ軌跡上を移動するというものです。この特性を用いることで経過点P2次ベジェ曲線の終点、Aを始点、P1を制御点とし、元の2次ベジェ曲線と同じ軌跡上を動的に動く経過点がなぞるベジェ曲線の始点、終点および制御点を単一のパラメータtで表すことができました。

すなわち、

・動的に動いている最中の2次ベジェ曲線の始点=元のベジェ曲線の始点A(x1, y1)

・動的に動いている最中の2次ベジェ曲線の制御点=元のベジェ曲線を導いた点P1(x4, y4):

・動的に動いている最中の2次ベジェ曲線の終点=元のベジェ曲線の経過点P(x0, y0):

です。

 

これは3次の場合も同様です。経過点Pを終点、Aを始点、P1を制御点1P4を制御点2とすれば、やはり元の3次ベジェ曲線と同じ3ベジェ曲線を単一のパラメータtで表すことができます。

すなわち、

・動的に動いている最中の3次ベジェ曲線の始点=元のベジェ曲線の始点A(x1, y1)

・動的に動いている最中の3次ベジェ曲線の制御点1=元のベジェ曲線を導いた点P1 (x4, y4):

・動的に動いている最中の3次ベジェ曲線の制御点2=元のベジェ曲線を導いた点P4 (x7, y7):

・動的に動いている最中の3次ベジェ曲線の終点=元のベジェ曲線の経過点P (x0, y0):

です。

 

では、この単一のパラメータを2次、3次ベジェ曲線を描くJavaFxQuadCurveオブジェクトおよびCubicCurveオブジェクトにどうやった適用できるか。そのための方法が、次に説明するプロパティをShapeオブジェクトにバインドするやり方です。

 

BDoubleBindingクラスのbind()メソッドで各プロパティ値に外部パラメータをバインドさせる方法による描画

ベジェ曲線の場合、Timelineクラスを用いてそのまま制御点や終点を最終的な値に直線的に増加させても形が膨らむだけで目標とするベジェ曲線をなぞるような曲線を動的に描くことはできませんでした。そこである外部パラメータ:tを直線的に(時間に比例して)増加させると、それに応じて各制御点および終点が非直線的にベジェ曲線の特性に沿って動いてくれればいい。どのように非直線的に動かせばよいかは先の計算結果から判明しているので、後はこれを曲線を表現するクラスに指示してあげればいいだけです。JavaFxで外部パラメータに従ってオブジェクトのプロパティ値を変更させるには、DoubleBindingクラスなどで提供されるbind()メソッドを用います。Shapeオブジェクトのあるプロパティが外部プロパティによってひとたびバインドされると、そのプロパティはそれ以降値を外部からセッターなどで直接設定することができなくなり、ずっと外部プロパティの値に従属することになります。では、早速外部プロパティをバインドするやり方でまずは直線を描いてみましょう。さきほどの直線を描いたコードに少し手を加えます。

 

       private DoubleProperty t0 = new SimpleDoubleProperty();

 

       @Override

       public void start(Stage stage) throws Exception {

              stage.setTitle("Line Drawer");

              Scene scene = new Scene(new Group(), 400, 400);

              stage.setScene(scene);

              stage.show();

             

              final double x1 =   5.0;

              final double y1 = 100.0;

              final double x2 = 200.0;

              final double y2 =   5.0;

             

              Line line = new Line();

              line.setStartX(x1);

              line.setStartY(y1);

              line.setEndX(x1);

              line.setEndY(y1);

              line.setFill(Color.TRANSPARENT);

              line.setStroke(Color.BLUE);

              line.setStrokeWidth(5);

              final Group group = (Group)scene.getRoot();

              group.getChildren().add(line);

             

              DoubleBinding x0 = new DoubleBinding() {

                     {

                            super.bind(t0);

                     }

                     @Override

                     protected double computeValue() {

                            double t = t0.get();

                            return t*x2+(1-t)*x1;

                     }

              };

              line.endXProperty().bind(x0);

             

              DoubleBinding y0 = new DoubleBinding() {

                     {

                            super.bind(t0);

                     }

                     @Override

                     protected double computeValue() {

                            double t = t0.get();

                            return t*y2+(1-t)*y1;

                     }

              };

              line.endYProperty().bind(y0);

             

              Timeline timeline = new Timeline(

                            new KeyFrame(Duration.seconds(3), new KeyValue(t0, 1))

        );

              timeline.play();

             

       }

 

冒頭のDoubleProperty t0 = new SimpleDoubleProperty()のところが外部プロパティの宣言部で、このt0DoubleBindingクラスに内部パラメータとしてbindしています。これでDoubleBindingクラスがt0の関数となります。そして関数の式をcomputeValueメソッド内部で以下のように定義しています。

t*x2+(1-t)*x1

ここでは直線のx座標をパラメータtで表現していますが、応用すれば非線形のプロパティでも操作できることがわかります。

 

ここまでくれば、あとはこのcomputeValue内の関数をさきほどのベジェ曲線の各制御点に適用すればいい。2次ベジェ曲線の場合、こんな感じです。(QuadCurveクラスの派生クラスを新規作成しています)

public class ParametoricQuadCurve extends QuadCurve {

      

       private DoubleProperty paramT = new SimpleDoubleProperty();

      

 

       public ParametoricQuadCurve(double startX, double startY, double controlX,

                     double controlY, double endX, double endY) {

              super(startX, startY, controlX, controlY, endX, endY);

             

              final double x1 = startX;

              final double y1 = startY;

              final double x2 = controlX;

              final double y2 = controlY;

              final double x3 = endX;

              final double y3 = endY;

             

              // set all moving points to start point(x1, x2)

              this.setStartX(x1);

              this.setStartY(y1);

              this.setControlX(x1);

              this.setControlY(y1);

              this.setEndX(x1);

              this.setEndY(y1);

             

              DoubleBinding dbx4 = new DoubleBinding() {

                     {

                            super.bind(paramT);

                     }

                     @Override

                     protected double computeValue() {

                            double t = paramT.doubleValue();

                            return t*x2 + (1-t)*x1;

                     }

              };

              this.controlXProperty().bind(dbx4);

             

              DoubleBinding dby4 = new DoubleBinding() {

                     {

                            super.bind(paramT);

                     }

                     @Override

                     protected double computeValue() {

                            double t = paramT.doubleValue();

                            return t*y2 + (1-t)*y1;

                     }

              };

              this.controlYProperty().bind(dby4);

             

              final DoubleBinding dbx0 = new DoubleBinding() {

                     {

                            super.bind(paramT);

                     }

                     @Override

                     protected double computeValue() {

                            double t = paramT.doubleValue();

                            return pow(t, 2.0)*x3 + 2*t*(1-t)*x2 + pow(1-t, 2.0)*x1;

                     }

              };

              this.endXProperty().bind(dbx0);

             

              final DoubleBinding dby0 = new DoubleBinding() {

                     {

                            super.bind(paramT);

                     }

                     @Override

                     protected double computeValue() {

                            double t = paramT.doubleValue();

                            return pow(t, 2.0)*y3 +2*t*(1-t)*y2 + pow(1-t, 2.0)*y1;

                     }

              };

              this.endYProperty().bind(dby0);

             

       }

 

       public final double getParamT() {

              return paramT.get();

       }

 

       public final void setParamT(double value) {

              this.paramT.set(value);

       }

             

       public DoubleProperty paramTProperty() {

              return paramT;

       }

}

 

3次ベジエ曲線も同様。

public class ParametoricCubicCurve extends CubicCurve {

      

       private DoubleProperty paramT = new SimpleDoubleProperty();

      

 

       public ParametoricCubicCurve(double startX, double startY, double controlX1,

                     double controlY1, double controlX2, double controlY2, double endX, double endY) {

              super(startX, startY, controlX1, controlY1, controlX2, controlY2, endX, endY);

             

              final double x1 = startX;

              final double y1 = startY;

              final double x2 = controlX1;

              final double y2 = controlY1;

              final double x3 = controlX2;

              final double y3 = controlY2;

              final double x4 = endX;

              final double y4 = endY;

             

              // set all moving points to start point(x1, x2)

              this.setStartX(x1);

              this.setStartY(y1);

              this.setControlX1(x1);

              this.setControlY1(y1);

              this.setControlX2(x1);

              this.setControlY2(y1);

              this.setEndX(x1);

              this.setEndY(y1);

             

              DoubleBinding dbx5 = new DoubleBinding() {

                     {

                            super.bind(paramT);

                     }

                     @Override

                     protected double computeValue() {

                            double t = paramT.get();

                            return t*x2 + (1-t)*x1;

                     }

              };

              this.controlX1Property().bind(dbx5);

             

              DoubleBinding dby5 = new DoubleBinding() {

                     {

                            super.bind(paramT);

                     }

                     @Override

                     protected double computeValue() {

                            double t = paramT.get();

                            return t*y2 + (1-t)*y1;

                     }

              };

              this.controlY1Property().bind(dby5);

             

              final DoubleBinding dbx8 = new DoubleBinding() {

                     {

                            super.bind(paramT);

                     }

                     @Override

                     protected double computeValue() {

                            double t = paramT.get();

                            return pow(t, 2.0)*x3 + 2*t*(1-t)*x2 + pow(1-t, 2.0)*x1;

                     }

              };

              this.controlX2Property().bind(dbx8);

             

              final DoubleBinding dby8 = new DoubleBinding() {

                     {

                            super.bind(paramT);

                     }

                     @Override

                     protected double computeValue() {

                            double t = paramT.get();

                            return pow(t, 2.0)*y3 +2*t*(1-t)*y2 + pow(1-t, 2.0)*y1;

                     }

              };

              this.controlY2Property().bind(dby8);

             

              final DoubleBinding dbx0 = new DoubleBinding() {

                     {

                            super.bind(paramT);

                     }

                     @Override

                     protected double computeValue() {

                            double t = paramT.get();

                            return pow(t, 3.0)*x4 + 3*pow(t, 2.0)*(1-t)*x3

                                          + 3*t*pow(1-t, 2.0)*x2 + pow(1-t, 3.0)*x1;

                     }

              };

              this.endXProperty().bind(dbx0);

             

              final DoubleBinding dby0 = new DoubleBinding() {

                     {

                            super.bind(paramT);

                     }

                     @Override

                     protected double computeValue() {

                            double t = paramT.get();

                            return pow(t, 3.0)*y4 + 3*pow(t, 2.0)*(1-t)*y3

                                          + 3*t*pow(1-t, 2.0)*y2 + pow(1-t, 3.0)*y1;

                     }

              };

              this.endYProperty().bind(dby0);

             

       }

 

       public final double getParamT() {

              return paramT.get();

       }

 

       public final void setParamT(double value) {

              this.paramT.set(value);

       }

             

       public DoubleProperty paramTProperty() {

              return paramT;

       }

}

 

あとはこのクラスを使ったApplicationを作成すれば、完成です。

下図が、各点の座標位置です。(清書するの手間だったので、これも汚いフリーハンドのままです、、申し訳ありません。。)

 

実際に座標位置を踏まえてアプリケーションに落としていきます。

public class DrawDollar extends Application {

 

       @Override

       public void start(Stage stage) throws Exception {

              stage.setTitle("Dollar Drawer");

              Scene scene = new Scene(new Group(), 400, 400);

              scene.setFill(Color.WHITE);

              stage.setScene(scene);

              stage.setX(700);

              stage.setY(300);

              stage.show();

             

              final double xL1 = 175.0;

              final double yL1 =  75.0;

              final double xL2 = 175.0;

              final double yL2 = 325.0;

              final double xL3 = 225.0;

              final double yL3 =  75.0;

              final double xL4 = 225.0;

              final double yL4 = 325.0;

             

              final double xU1 = 275.0;

              final double yU1 = 150.0;

              final double xU2 = 275.0;

              final double yU2 = 100.0;

              final double xU3 = 125.0;

              final double yU3 = 100.0;

              final double xU4 = 125.0;

              final double yU4 = 150.0;

              final double xU5 = 200.0;

              final double yU5 = 100.0;

             

              final double xD1 = 275.0;

              final double yD1 = 250.0;

              final double xD2 = 275.0;

              final double yD2 = 300.0;

              final double xD3 = 125.0;

              final double yD3 = 300.0;

              final double xD4 = 125.0;

              final double yD4 = 250.0;

              final double xD5 = 200.0;

              final double yD5 = 300.0;

             

              final double xM1 = 125.0;

              final double yM1 = 200.0;

              final double xM2 = 275.0;

              final double yM2 = 200.0;

 

              // left line

              final ParametoricLine lineL = new ParametoricLine(xL1, yL1, xL2, yL2);

              lineL.setStrokeWidth(5);

              lineL.setStroke(Color.BLUE);

              // right line

              final ParametoricLine lineR = new ParametoricLine(xL3, yL3, xL4, yL4);

              lineR.setStrokeWidth(5);

              lineR.setStroke(Color.BLUE);

              // upper ellipse right

              final ParametoricQuadCurve quadUR = new ParametoricQuadCurve(xU1, yU1, xU2, yU2, xU5, yU5);

              quadUR.setStrokeWidth(5);

              quadUR.setStroke(Color.BLUE);

              quadUR.setFill(Color.TRANSPARENT);

              // upper ellipse left

              final ParametoricQuadCurve quadUL = new ParametoricQuadCurve(xU5, yU5, xU3, yU3, xU4, yU4);

              quadUL.setStrokeWidth(5);

              quadUL.setStroke(Color.BLUE);

              quadUL.setFill(Color.TRANSPARENT);

              // middle wave

              final ParametoricCubicCurve cubic = new ParametoricCubicCurve(xU4, yU4, xM1, yM1, xM2, yM2, xD1, yD1);

              cubic.setStrokeWidth(5);

              cubic.setStroke(Color.BLUE);

              cubic.setFill(Color.TRANSPARENT);

              // lower ellipse right

              final ParametoricQuadCurve quadDR = new ParametoricQuadCurve(xD1, yD1, xD2, yD2, xD5, yD5);

              quadDR.setStrokeWidth(5);

              quadDR.setStroke(Color.BLUE);

              quadDR.setFill(Color.TRANSPARENT);

              // lower ellipse left

              final ParametoricQuadCurve quadDL = new ParametoricQuadCurve(xD5, yD5, xD3, yD3, xD4, yD4);

              quadDL.setStrokeWidth(5);

              quadDL.setStroke(Color.BLUE);

              quadDL.setFill(Color.TRANSPARENT);

             

              final Group group = (Group)scene.getRoot();

              group.getChildren().addAll(lineL, lineR, quadUR);

             

              new Timeline(

                            new KeyFrame(Duration.seconds(3.0), new KeyValue(lineL.paramTProperty(), 1)),

                            new KeyFrame(Duration.seconds(3.0), new KeyValue(lineR.paramTProperty(), 1)),

                            new KeyFrame(Duration.seconds(0.5), new KeyValue(quadUR.paramTProperty(), 1)),

                            new KeyFrame(Duration.seconds(0.5), new EventHandler<ActionEvent> () {

                                   @Override

                                   public void handle(ActionEvent event) {

                                          group.getChildren().add(quadUL);

                                          new Timeline(

                                                 new KeyFrame(Duration.seconds(0.5), new KeyValue(quadUL.paramTProperty(), 1))

                                          ).play();

                                   }

                            }),

                            new KeyFrame(Duration.seconds(1.0), new EventHandler<ActionEvent> () {

                                   @Override

                                   public void handle(ActionEvent event) {

                                          group.getChildren().add(cubic);

                                          new Timeline(

                                                 new KeyFrame(Duration.seconds(1.0), new KeyValue(cubic.paramTProperty(), 1))

                                          ).play();

                                   }

                            }),

                            new KeyFrame(Duration.seconds(2.0), new EventHandler<ActionEvent> () {

                                   @Override

                                   public void handle(ActionEvent event) {

                                          group.getChildren().add(quadDR);

                                          new Timeline(

                                                 new KeyFrame(Duration.seconds(0.5), new KeyValue(quadDR.paramTProperty(), 1))

                                          ).play();

                                   }

                            }),

                            new KeyFrame(Duration.seconds(2.5), new EventHandler<ActionEvent> () {

                                   @Override

                                   public void handle(ActionEvent event) {

                                          group.getChildren().add(quadDL);

                                          new Timeline(

                                                 new KeyFrame(Duration.seconds(0.5), new KeyValue(quadDL.paramTProperty(), 1))

                                          ).play();

                                   }

                            })

        ).play();

             

       }

 

       public static void main(String[] args) {

              launch(args);

       }

 

}

 

CTimelineによるアニメーションオブジェクトの時系列での遅延操作

最後に補足説明ですが、それぞれ別々の曲線を連続的に描いているように見せるため、時間をおいて順々に曲線を描いているのですが、これをTimelineに渡すKeyFrameの中で設定しているEventHandlerによるイベント制御により実現しています。イベントの発生順番のタイムチャートはこんな感じ。

 

0秒〜3秒:右と左の直線

0秒〜0.5秒:上部右側の楕円部分

0.5秒〜1秒:上部左側の楕円部分

1秒〜2秒:中央の波線部分

2秒〜2.5秒:下部右側の楕円部分

2.5秒〜3秒:下部左側の楕円部分

 

実際にはこんな感じで描くことができました。

http://www.youtube.com/watch?v=7mFdmqGc0bg

 

JavaFx2.2APIでは、このようにパラメータを指定して曲線全体の一部を描くという機能は今のところ提供されてはいないようです(結構探したんだけど今のところ見つけられませんでした)ので、それまでこの書き方が一手法かと思います。将来こういった機能もAPIに組み込まれるとありがたいですけどね。