Duke で Swing

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

昨日は @Yucchi_jp さんの力作、JavaFX の標準機能だけでシンプルな 3D トイピアノをつくろう でした。

明日は @peko_kun さんです。

そして、このエントリーは Java Advent Calendar 2014 の 12 日目でもあります。

昨日は @dk_masu さんの プログラミング初心者がじゃんけんのプログラムを書いてみた でした。

明日は @susumuis さんです。


以前、ITpro の JavaFX 連載で振り子のアニメーションを取りあげたことがありました。もし、よろしければ、要登録なのですが、ごらんください。

JavaFX 2ではじめる、GUI開発 第13回 タイムラインを使ったアニメーション

この時は、javafx.animation.AnimationTimer クラスを使用して、振り子のアニメーションを実現させました。

アニメーションではなく、静止画ですが、実行例はこちら。

http://itpro.nikkeibp.co.jp/article/COLUMN/20130719/492504/pendulums01.gif

これは、これでキレイなのでいいのですが... でも、いまいちリアリティにとぼしいわけです。

なぜ、リアルさが足りないのかをつらつら考えるに、糸がまっすぐなのがイヤなのかなぁと。実際の振り子などは、角度が大きくなったときにしなるじゃないですか。あれがないんです。

上の振り子の軌跡を描画したのが次の図です。

これに対して、しなりを入れてみたのが、こちら。

違いが分かりますか? 分かりますよね。

で、後者の方がそれっぽいわけです。

物理で等時性のとれた振り子はサイクロイド振り子と呼びます。重りの軌跡がサイクロイド曲線になっているので、サイクロイド振り子といいます。このサイクロイド振り子はちゃんとしなっているわけです。

今回は見た目重視なので、正しくサイクロイド振り子を表すつもりはまったくないのです。それっぽく見えれば OK。

ちなみに、上の直線で記述した振り子の糸の軌跡を描くには、次のメソッドを使用しました。ここでは Group クラスを使ってますけど、それを適当なコンテナに貼ってあげれば軌跡が表示できます。

    private void drawString(Group root) {
        DoubleStream.iterate(-180.0, t -> t + 10.0)
                    .limit(37)
                    .forEach(t -> {
                        double alpha = Math.PI * Math.sin(t * Math.PI / 180.0) / 6.0;
                        double endX = XPOSITION + LENGTH * Math.sin(alpha);
                        double endY = LENGTH * Math.cos(alpha);

                        Line line = new Line(XPOSITION, 0, endX, endY);
                        line.setStroke(Color.WHITE);

                        root.getChildren().add(line);
                    });
    }

まぁ、それはさておき、しなった糸を描画するのですから、曲線を使わなくてはいけません。

CG で曲線といったら、やっぱりベジェ曲線ですよね。

ベジェ曲線は端点と制御点から構成される曲線です。制御点が 1 つのもの 2 次ベジェ曲線、2 つのものを 3 次ベジェ曲線と呼びます。

図の赤い点が制御点です。

端点と制御点を結んだ直線が、端点での曲線の接線になります。

自由度が高いのは 3 次ベジェ曲線で、図のような S 字の曲線は 2 次ベジェ曲線では描けません (もちろん、複数の 2 次ベジェ曲線を使えば描けますけど)。

JavaFX の場合、2 次ベジェ曲線と 3 次ベジェ曲線の両方をサポートしています。それぞれ、QuadCurve クラス、CubicCurve クラスで表します。双方とも javafx.scene.shape パッケージで提供されているクラスです。

ここでは自由度の高い CubicCurve クラスを使用します。

では、とりあえずやってみましょう。

上のメソッドでは Line クラスを使っていた部分を CubicCurve に置き換えてみます。制御点はとりあえず端点と同じ場所にしてみましょう。

CubicCurve クラスはコンストラクタに開始点、第 1 制御点、第 2 制御点、終点の順に指定します。

public class DukeDeSwing extends Application {

    private static final double XPOSITION = 400.0;
    private static final double LENGTH = 500.0;

    private CubicCurve string = new CubicCurve(
            XPOSITION, 0.0,     // 開始点
            XPOSITION, 0.0,     // 第 1 制御点
            XPOSITION, LENGTH,  // 第 2 制御点
            XPOSITION, LENGTH); // 終点
    private Rotate rotate = new Rotate(0.0, 0.0, 0.0);

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

        string.setStroke(Color.WHITE);
        string.setStrokeWidth(1.0);
        string.setFill(null);
        root.getChildren().add(string);

        Scene scene = new Scene(root, XPOSITION * 2.0, LENGTH + 100.0);
        scene.setFill(Color.BLACK);

        stage.setScene(scene);
        stage.setTitle("Pendulum");
        stage.show();

        startSwingAnimation();
    }

    private void startSwingAnimation() {
        AnimationTimer timer = new AnimationTimer() {
            @Override
            public void handle(long now) {
                double alpha = Math.PI * Math.sin(now / 500_000_000.0) / 6.0;

                // 終点の位置の更新
                double endX = XPOSITION + LENGTH * Math.sin(alpha);
                double endY = LENGTH * Math.cos(alpha);
                string.setEndX(endX);
                string.setEndY(endY);
            }
        };

        timer.start();
    }

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

}

実行してみると、こんな感じになりました。

なんか思っていたのと違う!

というか、しなりすぎ!!

という予定調和のツッコミをしてみました。

これはなぜかというと制御点を動かしていないからです。下の図のように、開始点に制御点はあるままなので、終点がそこに引っ張られるようになってしまうのです。

では、どうすればよいでしょうか。

答えは簡単で制御点も動かせばいいのです。

でも、どうやって?

とりあえず、振り子っぽく動けばいいので、下の図のようにしたいと思います。

終点は円運動しているのですから、第 1 制御点、第 2 制御点とも円運動をさせてみます。ただし、その角度は徐々に減らすようにしてみました。

これを行うのは簡単です。改造した startSwingAnimation メソッドを下に示します。

    private void startSwingAnimation() {
        AnimationTimer timer = new AnimationTimer() {
            @Override
            public void handle(long now) {
                double alpha = Math.PI * Math.sin(now / 500_000_000.0) / 6.0;

                // 終点の位置の更新
                double endX = XPOSITION + LENGTH * Math.sin(alpha);
                double endY = LENGTH * Math.cos(alpha);
                string.setEndX(endX);
                string.setEndY(endY);

                // 第2制御点の更新
                double controlX2 = XPOSITION + 300 * Math.sin(alpha * .8);
                double controlY2 = 300 * Math.cos(alpha * .8);
                string.setControlX2(controlX2);
                string.setControlY2(controlY2);

                // 第1制御点の更新
                double controlX1 = XPOSITION + 100 * Math.sin(alpha * .4);
                double controlY1 = 100 * Math.cos(alpha * .4);
                string.setControlX1(controlX1);
                string.setControlY1(controlY1);
            }
        };

        timer.start();
    }

第 2 制御点が終点の 80% の角度、第 1 制御点が 40% の角度に制限してみました。

では、実行してみましょう。

なかなかいい感じではないですか。

と、ここまでが前振りです。長すぎですね。

ブランコ

このエントリーのタイトルが Duke で Swing なのに、どこにも Swing が出てこないじゃないかと文句を言われている方も多いかもしれません。

でも、この Swing はあの Swing ではなくて、ブランコの Swing のことなのです。そう、あの Swing も、もとをただせばブランコの Swing なのです。

下の図はサンタクララにある往年の Sun の本社の Swing チームの壁に描かれた Duke です。

Sun Microsystems, Santa Clara

そう、Swing はブランコだったのです。

で、JavaFXDuke の Swing を描画させてしまおうというのが、このエントリーの趣旨なわけです。

さて、今までは振り子として描画させてきたわけですが、これをブランコにしましょう。

といっても、線を太くするぐらいですが。

で、Duke の絵を用意しましょう。ここでは Illustrator で下のような Duke の絵を描きました。

ブランコの板に座っている Duke です。

これをイメージとしてエクスポートして、読み込んでもいいのですが、せっかくのベクター画像なのでそのまま JavaFX にインポートしようと思います。

そう、SVG です。

以前作成した SVGLoader を使用して読み込んであげましょう。

SVG の使い方については以前書いたのでこちらをご参照ください。

SVGLoader

この時は JavaFX 2.x ですが、使い方はそのまま同じです。この Duke de Swing のプロジェクトにも SVGLoader を同胞したので、そのまま使うことができます。

さて、SVG を使うと、Illustrator のレイヤーをそのまま使用することができます。

レイヤーなしに Duke のノードを描画させてからブランコの紐を描画すると、紐を持たない Duke になってしまいます。

そこで、ここでは Duke の本体と手の部分をレイヤーに分け、奥から Duke 本体、ブランコの紐、Duke の手となるように描画させます。

このようにすることで、Duke が紐を握っているように見えるわけです。

もう 1 つ工夫があります。

Illustrator の原点 (0, 0) を Duke の下のブランコの板の中心にしてあります。原点を Duke の左上などにしてしまうと、紐の端点と一緒に動かすときに補正をする必要が出てきてしまいます。

それを避けるためにも、はじめからブランコの板を原点に持ってきているわけです。

では、Duke のノードをロードするところです。これは start メソッドで行いました。

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

        SVGContent content = SVGLoader.load(getClass().getResource("duke.svg").toString());

        // Duke のロード
        Node duke = content.getNode("duke");
        root.getChildren().add(duke);
        
        // Duke の位置を紐の終点とバインドする
        duke.translateXProperty().bind(string.endXProperty());
        duke.translateYProperty().bind(string.endYProperty());

        // ブランコの紐
        string.setStroke(Color.PERU);
        string.setStrokeWidth(10.0);
        string.setFill(null);
        root.getChildren().add(string);

        // Duke の手のロード
        Node hand = content.getNode("hand");
        root.getChildren().add(hand);

        // Duke の手の位置を紐の終点とバインドする
        hand.translateXProperty().bind(string.endXProperty());
        hand.translateYProperty().bind(string.endYProperty());

        Scene scene = new Scene(root, XPOSITION * 2.0, LENGTH + 100.0);
        scene.setFill(Color.BLACK);

        stage.setScene(scene);
        stage.setTitle("Duke de Swing");
        stage.show();

        startSwingAnimation();
    }

javaFX では、コンテナにはじめに追加したものが奥に描画されます。そのため、Duke 本体、紐、手の順番にコンテナに追加しています。

では、紐と一緒に一緒に場所を更新する処理を書かなくては... と思うかもしれませんが、必要ありません。

そう、ここで、バインドです!

duke や hand の移動量を表すプロパティ translateX/translateY を紐の終点にバインドすれば OK。これで紐の終点と一緒に動きます。

先ほど、原点について言及しましたけど、このバインドの処理が単純になるわけです。

では実行してみましょう。

えっ...

Duke が回転していない ><

ということで、回転させましょう。

ここではブランコの紐の終点から第 2 制御点に至る直線を使用して Duke の傾きを決めたいと思います。というのもこの直線が終点での紐の接線になるからです。

それと直角になるように Duke を傾ければいいわけです。つまり Duke から見ると、その直線は法線になるわけです。

この直線の傾きを出すには、アークタンジェントです。

アークタンジェントが何かを忘れている方も多いと思いますが、角度が分かるわけです。

ここでは Math クラスの atan2 メソッドを使用します。atan2 メソッドは第 1 引数に y 軸方向の差分、第 2 引数に x 軸方向の差分を指定します。

回転にはノードの setRotate を使いたいところですが、これは使えません。なぜかというと、setRotate はノードの真ん中を中心にして回転させるからです。

ここでは、ブランコの板を中心にして回転させたいのです。こういう場合には javafx.scene.transform.Rotate クラスを使用します。Rotate クラスでは回転の中心や、回転軸を指定することができます。

ここでは次のような rotate 変数をフィールドとして定義しました。

    // Duke の回転用
    private Rotate rotate = new Rotate(0.0, 0.0, 0.0);

Rotate クラスのコンストラクタの第 1 引数は回転角度、第 2、第 3 引数で回転の中心を指定します。ここでも、Duke の原点をブランコの板にしたのが効いてきます。つまり、回転の中心も原点でいいわけです。

次に duke と hand にこの rotate をセットしなくてはなりません。これは start メソッドの中で行っています。

        duke.getTransforms().add(rotate);

        hand.getTransforms().add(rotate);

Roate クラスのような変換は複数指定することができるので、getTransforms メソッドでリストを取り出して、そこに追加するようにします。

最後に角度の更新です。これは startSwingAnimation メソッドで行っています。

                // Dukeの回転の更新
                double theta = 180.0 * Math.atan2(endY - controlY2, endX - controlX2) / Math.PI - 90.0;
                rotate.setAngle(theta);

Rotate クラスの回転角は度なのに対し、atan2 の返り値はラジアンなので、変換が必要です。90 度引いているのは、法線だからです。

これで完璧なはず。さっそく実行してみましょう。

というわけで、Duke で Swing 完成です!!

ソースや DukeIllustrator のファイルは GitHub で公開しています。

Duke de Swing

それにしても、Duke 見てると癒やされるなあ。1 日中眺めていたいww