Duke 007
もうずいぶん前のことなのですが、Java Day Tokyo 2013 の Java the Night でデモをしてきました。
何をデモしたかというと、いつもプレゼンテーションで使用している JavaFX のプレゼンテーションツール Caribe。
Caraibe 自体は自分がプレゼンでやりたいことができるように、自由度をかなり上げていて、普通の人が使うのはかなりつらいと思うので、単体では公開していないのです。でも、プレゼンごと GitHub にアップ してあったりするので、そちらを見ていただければと思います。
で、今日はそのオープニングで作ったアニメーションについて。
去年は Star Wars のアニメーション作ったので、今年は 007 です。
Java the Night の音なしバージョンは YouTube にアップしてあるので、そのはじめの部分を見ていただければ分かるはず。
この 007 風 Duke のアニメーションについてどうやって作ったか、解説していきます。
なお、今回のアニメーションだけとりだしたバージョンを GitHub で公開しているので、そちらも参照していただければと思います。
なお、今回は NetBeans ではなく、IntelliJ IDEA を使用しています。NetBeans の Java 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); } }