黒地に黄色のクローラー

この記事は、JavaFX Advent Calendar 2015 の 17 日目の記事です。

昨日は id:c9katayama さんの PDFBoxとFXGraphics2Dを使って大きなPDFをレンダリングする でした。

明日は @yumix_h さんです

タイトルの黒地に黄色のクローラーといえば、あれですよ、あれ。

明日の公開のあの映画。そう Star Wars です。こんなやつです。

http://img.lum.dolimg.com/v1/images/episode-3-crawl_4d843f60.jpeg

過去に、プレゼンのはじめに Star Wars のパロディのムービーを作ったことがある櫻庭ですから、この波に乗らないわけにはいきません。

ということで、JavaFX であのクローラーを作ってみようというわけです。

3D でやってみる

あのクローラーは文字列が奥のほうに移動しているので、まずは単純に 3D の API を使って、z 軸方向に移動させてみます。

でも、単に文字列を移動させても、台形の形にはならないので、文字列を x 軸を回転軸にして回転させます。ようするに、文字列を寝かせるわけですね。そして、z 軸方向に移動させるわけです。

まず、3D にするにはカメラを設定しなくてはいけません。

通常、2D の GUI では平行透視法のカメラが設定されています。このため、奥にあるノードでも小さく表示されることはありません。でも、3D の場合は、奥にあるノードは小さく表示しなくてはなりません。そのためには、それようのカメラ、つまり透視法を使用したカメラを設定します。

これは簡単で、Scene に PerspectiveCamera を設定します。

        Scene scene = new Scene(root);
        scene.setCamera(new PerspectiveCamera());

これで、奥の方のノードは小さく表示されるようになりました。

さて、次は文字列を寝かせる処理です。これはノードの setRotate メソッドで行います。しかし、単に setRotate メソッドをコールすると、z 軸を中心に回転してしまいます。これを x 軸を中心にさせるには setRoateAxis メソッドを使用します。

        Text text = new Text(crawl);

        text.setRotationAxis(new Point3D(1.0, 0.0, 0.0));
        text.setRotate(-80.0);

2 行目で、x 軸を表す Point3D オブジェクトを生成して、setRotationAxis メソッドに指定しています。そして、3 行目で後ろに 80 度倒す処理をします。JavaFX では角度はラジアンではなくてディグリーを使用することに注意してください。

準備ができたので、z 軸方向に移動するアニメーションを作成してみましょう。z 軸方向に移動する場合でも、移動は移動なので、TransTranslateTransition を使用します。

        TranslateTransition trans = new TranslateTransition(Duration.millis(20_000), text);
        trans.setToZ(1_000);
        trans.setInterpolator(Interpolator.LINEAR);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();

プログラムの全体はブログの最後の方にまとめて載せました。

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

確かに、奥に移動する感じはできましたけど、なんかちょっと違う...

奥の方に移動すると、x 軸方向に回転させた角度が変化してしまうのが気になります。3D なので、当たり前といえば当たり前なのですが。

また、いくら z 軸上で奥に移動させても、見えなくなるほどにはなりません。そのため、さいごの方の挙動が止まっているように見えてしまいます。

単純に 3D で移動させるよりも、もうちょっといい方法があるような気がします。もうちょっと模索してみましょう。

3D と 2D のアニメーションを組み合わせる

次に思いついたのが、3D の表現と 2D のアニメーションを組み合わせる方法です。

文字列を後ろに寝かせるのは 3D で行うとして、奥に移動するアニメーションは疑似的に 2D で表せないかということです。具体的には y 軸方向の上に移動するアニメーションと、縮小するアニメーションを組み合わすということです。

y 軸方向に移動させるアニメーションは先ほどと同じように TranslateTransition を使用します。また、拡大縮小には ScaleTransition を使用します。

まずは、移動のアニメーションです。

        TranslateTransition trans = new TranslateTransition(Duration.millis(20_000), text);
        trans.setFromY(1_000);
        trans.setToY(-100);
        trans.setInterpolator(Interpolator.EASE_OUT);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();

単に y 軸上を動かしているだけです。

そして、拡大縮小です。はじめは拡大しておいて、徐々に縮小します。ただし、文字列を寝かしているため、z 軸方向にも大きさがあります。そのため、z 軸方向も拡大縮小します。

        ScaleTransition scale = new ScaleTransition(Duration.millis(20_000), text);
        scale.setFromX(5.0);
        scale.setFromY(5.0);
        scale.setFromZ(5.0);
        scale.setToX(0.01);
        scale.setToY(0.01);
        scale.setToZ(0.01);
        scale.setInterpolator(Interpolator.LINEAR);
        scale.setCycleCount(Animation.INDEFINITE);
        scale.play();

なお、複数のアニメーションを同時にやるためには ParallelTransition を使うこともありますが、そんなにきっちり合わす必要がなければ、単に複数のアニメーションを play してあげれば大丈夫です。

さて、これで実行してみましょう。

先ほどよりはいい感じになっていると思うのですが、どうでしょう。

しかし、これもアニメーションで移動させていると、文字列の寝ている角度が変わってしまうのが気になります。まぁ、こちらも、当たり前といったら当たり前ですが。

そこで、移動と同時に角度を変化させるアニメーションも一緒に行ってみます。

角度を変化させるアニメーションは RotateTransition です。

        RotateTransition rotate = new RotateTransition(Duration.millis(20_000), text);
        rotate.setToAngle(-80.0);
        rotate.setInterpolator(Interpolator.EASE_OUT);
        rotate.setCycleCount(Animation.INDEFINITE);
        rotate.play();

角度を変化させているといっても、ほんのちょっとです。それでも、見た目はかなり変化します。

ここでは回転の中心を設定していませんが、それははじめに文字列に対して設定しているからです。

これで同時に行うアニメーションは 3 つになりましたが、効果はどうでしょう。さっそく実行してみましょう。

かなりよくなったと思いませんか。これはこれでいいかもしれませんが、他の方法も模索してみましょう。

すべて 2D のアニメーションで行う

さいごに、3D はあきらめて、すべて 2D で行ってみましょう。もちろん、Star Wars の Episode IV の頃には CG はなかったのですから、2D のアニメーションでやっているはずです。

ということは、2D のアニメーションで実現するということは、原点回帰になるのかな?

もちろん、2D なので、Scene にカメラを設定させる必要もありません。

しかし、問題になるのは文字列を寝かせることができるかどうかです。実をいうと、これはエフェクトを使うことで、比較的簡単にできます。

どんなエフェクトかというと PerspectiveTransform です。先ほどまで使用していた PerspectiveCamera と同じく、透視法が使えるエフェクトというわけです。

PerspectiveTransform はノードを囲む四隅を任意の点に写像することができるエフェクトです。AffineTransform が実現するアフィン変換だと平行は平行に変換されますが、PerspectvieTransform では平行は維持されません。つまり長方形を台形などに変換することができるのです。

変換には四隅の座標を指定します。

        PerspectiveTransform perspective = new PerspectiveTransform();
        perspective.setUlx(300.0);
        perspective.setUly(200.0);

        perspective.setUrx(500.0);
        perspective.setUry(200.0);

        perspective.setLlx(0.0);
        perspective.setLly(500.0);

        perspective.setLrx(800.0);
        perspective.setLry(500.0);

        text.setEffect(perspective);

それぞれ 2 行ずつで、上左、上右、下左、下右の座標を指定しています。ここでは、ルートのコンテナの座標を台形にするような変形になります。

そして、後は先ほどと同じように y 軸上を移動するアニメーションと拡大縮小を組み合わせます。ただし、拡大縮小はノードの真ん中を中心に拡大縮小してしまうため、上記の変換と合わせると変な位置に変換されてしまいます。

そこで、Scale を使用して、拡大縮小の中心を変化させています。Scale を使ってアニメーションをするため、Transition ではなく Timeline を使用しています。

        TranslateTransition trans = new TranslateTransition(Duration.millis(20_000), text);
        trans.setFromY(400);
        trans.setToY(0);
        trans.setInterpolator(Interpolator.EASE_OUT);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();

        Scale scale = Transform.scale(4.0, 4.0, 400.0, 200.0);
        text.getTransforms().add(scale);
        Timeline timeline = new Timeline(
                new KeyFrame(Duration.millis(19_000),
                        new KeyValue(scale.xProperty(), 0.3), 
                        new KeyValue(scale.yProperty(), 0.3)),
                new KeyFrame(Duration.millis(20_000),
                        new KeyValue(scale.xProperty(), 0.001), 
                        new KeyValue(scale.yProperty(), 0.001))
        );
        timeline.setCycleCount(Animation.INDEFINITE);
        timeline.play();

拡大縮小のアニメーションを 2 段階にしているのは、よりそれっぽさを出すためです。

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

PerspectiveTransform と Scale を組み合わせているせいか、アニメーション開始時の拡大している状態では少しノードの表示が荒れてしまっていますが、動きは一番それらしいのではないでしょうか。

ということで、3 種類の方法で Star Warsクローラーを実現してみました!

これで、時代の波に乗れたかなww

プログラム

3D でやってみる


Crawler Demo 1

3D と 2D のアニメーションを組み合わせる


Crawler Demo 2

すべて 2D のアニメーションで行う


Crawler Demo 3