黒地に黄色のクローラー
この記事は、JavaFX Advent Calendar 2015 の 17 日目の記事です。
昨日は id:c9katayama さんの PDFBoxとFXGraphics2Dを使って大きなPDFをレンダリングする でした。
明日は @yumix_h さんです
タイトルの黒地に黄色のクローラーといえば、あれですよ、あれ。
明日の公開のあの映画。そう Star Wars です。こんなやつです。
過去に、プレゼンのはじめに 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