もうずいぶん前のことなのですが、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);
}
}
