ついにベールを脱いだJavaFX

第8回アニメーションを用いてより魅力的に[基礎編]

今まで使用してきたアニメーションは基本的には動きがほとんどありませんでした。しかし、最近の傾向としてユーザインターフェースにアニメーションを取り入れることが必須になってきています。

たとえば、MacOS Xでドックにアプリケーションがヒュッと吸いこまれていくのもアニメーションです。これに限らずOSレベルから個々のアプリケーションまで、さまざまなところでアニメーションが使われています。このようなアニメーションをユーザインターフェースに組み込んでいくことで、表現力を高めることができるのです。

もちろん、JavaFXでもアニメーションを扱うことが可能です。しかも、Swingなど既存のJavaのユーザインターフェース技術でアニメーションを扱うより、JavaFXは簡単にアニメーションを実現することができます。

そこで、これから2回に渡ってJavaFXのアニメーションについて解説していくことにしましょう。今回が基礎編、次回が応用編です。

アニメーションの基礎

アニメーションというのはもともとは複数の静止画像を時間に沿って切り替えることによって、動きを表現する技術です。ユーザインターフェースでも基本は同じです。時間の経過と共に複数の描画を切り替えることでアニメーションを実現することができます。

つまり、時間というのがアニメーションではとても重要になります。たとえば、図1はAdobe Flash Professionalの編集画面です。上部にあるタイムラインが時間の流れを表します。デフォルトでは12fps、つまり1秒間に12枚の描画を切り替えます。

図1 Adobe Flash Professionalの編集画面
図1 Adobe Flash Professionalの編集画面

1秒間のアニメーションを作成するには、基本的には12fpsであれば12枚の描画を用意する必要があります。しかし、コンピュータの力を使えば、その手間を大幅に減らすことができます。

図2はFlashで作成した2秒のアニメーションをループさせています。2秒ですから24枚の描画が必要です。しかし、実際に筆者が作成したのは2枚だけです。このような単純なアニメーションであれば、途中の状態はコンピュータで補間させればいいのです。

実際にFlashが補間した描画を図3に示します。緑線で表されているのがFlashが補間した部分です。このように単純なアニメーションであれば、コンピュータの力を借りることによって簡単にアニメーションが作成できるのです。

図2 振り子のアニメーション
図3 Flashによるアニメーションの補間
図3 Flashによるアニメーションの補間

JavaFX Scriptの場合、タイムラインはjavafx.animation.Timelineクラスで表します。また、特定の時間での状態を表すのがjavafx.animation.KeyFrameクラスです。インタープリタ版ではdurという演算子を使用していましたが、より高い表現を可能にするため現在のように変更になったようです。

では、この2つのクラスを使ってアニメーションを作成してみましょう。なお、今回作成したサンプルプログラムのソースを含んだNetBeansのプロジェクトは以下のリンクよりダウンロードできます。

簡単なアニメーションの作成

一番はじめのサンプルとして、ノードを移動させるアニメーションを作成します。移動はノードを描画する座標を時間に沿って変化させるだけなので、簡単に実現できます。

まず、NetBeansのプロジェクトを作成しましょう。ここでは、プロジェクト名をsimpleAnimation1とし、メインスクリプトファイルをsimpleAnimation1.fxとしました。

はじめにアニメーションのことを考えずにノードを表示するスクリプトを作成します。ここでは、ノードとして車のイメージ、つまりjavafx.scene.image.ImageViewクラスを使用します。

リスト1に、車のイメージを表示させるスクリプトを示します。

リスト1
Stage {
    title: "Simple Animation Sample"
    scene: Scene {
        width: 600
        height: 100
        
        content: [
            // 車のイメージ
            ImageView {
                x: 20
                y: 45
                image: Image {
                    url: "{__DIR__}car.png"
                }
            },
            SwingButton {
                translateX: 270
                translateY: 10
                text: "Start"
                action: function() {
                    // ボタンが押されたら
                    // アニメーションをスタートさせる予定
                }
            }
        ]
    }
}

車のイメージはソースと同じディレクトリに配置しました。この状態で実行したのが図4です。

図4 simpleAnimation1.fxの実行結果
図4 simpleAnimation1.fxの実行結果

では、ここにアニメーションを加えていきます。まず、変化させる要素を、変数として定義します。ここでは、水平方向に移動させるので、xという変数を定義しました。そして、ノードでは水平方向の移動量を示すtranslateXアトリビュートにxをバインドさせます。

リスト2
// 車の位置を表す変数
var x = 0;
 
        <<省略>>
        
            // 車のイメージ
            ImageView {
                // 移動量を x にバインドする
                translateX: bind x
 
                x: 20
                y: 45
                image: Image {
                    url: "{__DIR__}car.png"
                }
            },
 
        <<以下、省略>>

後は、TimelineクラスとKeyFrameクラスを使用して、変数xの値を変化させるだけです。Timelineクラスは、KeyFrameクラスのシーケンスであるkeyFramesアトリビュートを持ちます。このkeyFramesアトリビュートに、KeyFrameオブジェクトで指定した時間における値を表します。

リスト3
var timeline = Timeline {
    keyFrames: [
        KeyFrame {
            // はじめは 0 の位置
            time: 0s
            values: x => 0
        },
        KeyFrame {
            // 2 秒後に 450 まで進む
            time: 2s
            values: x => 450
        }
    ]
};

KeyFrameクラスは経過時間を表すtimeアトリビュートと、timeアトリビュートで表した時間における値を表すvaluesアトリビュートを持ちます。timeアトリビュートの型は時間間隔を表すDurationです。ここでは、2つの時間、0秒と2秒での値を表します。

valuesアトリビュートでは、=>演算子で値を代入します。この=>演算子はtimeアトリビュートに指定した時間までに、指定した値にするという意味を持ちます。たとえば、2つめのKeyFrameオブジェクトでは2秒後までにxを450にするという意味です。

valuesアトリビュートはアトリビュート名が複数形になっていることから分るように、シーケンスです。ここでは単一の値だけ代入しましたが、複数の値を設定することもできます。

Timelineオブジェクトは生成しただけではアニメーションは始まりません。play関数がアニメーションの開始、stop関数がアニメーションの停止です。ここでは、ボタンをクリックしたらアニメーションを開始するようにしました。

リスト4
SwingButton {
    translateX: 270
    translateY: 10
    text: "Start"
    action: function() {
        // ボタンが押されたら
        // アニメーションをスタート
        timeline.play();
    }
}

これで完成です。ボタンを押したら、左から右に車が移動するはずです。

実際に実行してみると、はじめてボタンを押した時は車が移動しますが、2回目からは車がはじめの位置に止まったままです。

この結果だけだと、Timelineクラスは一度playしてしまうと、再実行ができないように思われてしまうかもしれません。しかし、それは違います。play関数をコールして2秒後にはタイムラインが終了してしまうように感じますが、実際にはタイムラインはそのまま動作し続けます。再度アニメーションを実行するには、一度タイムラインを終了させる、つまりstop関数をコールする必要があります。

リスト5
SwingButton {
    translateX: 270
    translateY: 10
    text: "Start"
    action: function() {
        // ボタンが押されたら
        // アニメーションをストップさせてからスタート
        timeline.stop();
        timeline.play();
    }
}

もしくは、タイムラインをリセットして開始するplayFromStart関数を使用します。

リスト6
SwingButton {
    translateX: 270
    translateY: 10
    text: "Start"
    action: function() {
        // ボタンが押されたら
        // アニメーションをスタート
        timeline.playFromStart();
    }
}

これで、繰り返しアニメーションを実行することが可能になりました。

自然に動かす

simpleAnimation1.fxを実行させると、車の動きがぎこちないような気がしませんか? なぜ、ぎこちなく感じるのでしょう。それは補間の方法に問題があるからです。

simpleAnimation1.fxでは移動量を時間で均等に割って、アニメーションさせています。時間と移動量をグラフで表すと、図5のように線形の直線となります。つまり、等速運動です。

図5 線形補間
図5 線形補間

しかし、自然界には等速運動以外の運動が多くあります。たとえば、ロケットを考えてみましょう。ロケットのエンジンを点火すると、一定の力が加わります。この場合、ロケットは等加速度運動で進みます。

ある程度の速度が出た後、ロケットエンジンを停止すると、慣性の法則に則って、等速で運動します。この2種類の運動を組み合わせると、はじめは等加速度なのでゆっくり進みますが、徐々にスピードがあがります。そして、あるスピードに達したら一定速度で運動します。

これを図5と同じようにグラフで表したのが図6です。図6のように、はじめゆっくり進み、だんだんと早くさせることをイーズインといいます。

図6 イーズイン
図6 イーズイン

次に減速を考えてみましょう。先ほどの例であれば、ロケットのエンジンを逆噴射させた場合に相当します。

この場合も、運動方向とは逆向きの一定の力が作用すると考えることができます。つまり、この時も等加速度運動になります。したがって、等速運動から等加速度運動に移行して、徐々にスピードを落としながら停止します。

グラフで表すと図7のようになります。このような変化をイーズアウトといいます。

図7 イーズアウト
図7 イーズアウト

アニメーションを行う場合でも、イーズイン、イーズアウトで補間した方が自然に見える場合が多くあります。simpleAnimation1.fxで使用した車も、実際にはイーズイン、イーズアウト的な運動を行います。したがって、アニメーションする場合、移動開始時はイーズイン、移動終了時はイーズアウトを施すとより自然に見えるのです。

実をいうと、前述した振り子も、補間にイーズイン、イーズアウトを施してあります。そのため、振り子が地面に垂直の時に移動量が大きく、降りが大きくなると移動量が小さくなっています。

それでは、simpleAnimation1.fxにもイーズイン、イーズアウトを設定してみましょう。イーズイン、イーズアウトなどの補間方法を表すために使用するのが、javafx.animation.Interpolatorクラスです。Interpolatorオブジェクトは、KeyFrameオブジェクトのvaluesアトリビュートの設定時に、tween演算子で指定します。

リスト7
var timeline = Timeline {
    keyFrames: [
        KeyFrame {
            // はじめは 0 の位置
            time: 0s
            values: x => 0
        },
        KeyFrame {
            // 2 秒後に 450 まで進む
            // イーズイン、イーズアウトを指定
            time: 2s
            values: x => 450 tween Interpolator.EASEBOTH
        }
    ]
};

Interpolatorクラスには定数としてEASEIN、EASEOUTなどが定義してあります。上のスクリプトで使用したEASEBOTHはイーズインとイーズアウトの両方を表します。

定数ではなく、自分で補間曲線を定義することもできます。InterpolatorクラスのSPLINE関数を使用することで、ベジェ曲線で補間曲線を定義できます。ベジェ曲線とは2つの端点にそれぞれ接線を設定して、定義する曲線のことです。接線を定義するための点を制御点といいます。タイムラインでベジェ曲線で補間する場合、座標(0,0)から(1,1)までというように相対的に制御点を設定します。

たとえば、図8は、最初の制御点が(0.5, 0.1⁠⁠、次の制御点が(0.5, 0.9)になります。

図8 ベジェ曲線による補間
図8 ベジェ曲線による補間

SPLINE関数の引数は制御点の座標です。引数の順番は最初の制御点のx座標、y座標、次の制御点のx座標、y座標となります。たとえば、図8の曲線をスクリプトで表すと、次のようになります。

リスト8
var timeline = Timeline {
    keyFrames: [
        KeyFrame {
            // はじめは 0 の位置
            time: 0s
            values: x => 0
        },
        KeyFrame {
            // 2 秒後に 450 まで進む
            // イーズイン、イーズアウトを指定
            time: 2s
            values: x => 450 tween Interpolator.SPLINE(0.5, 0.1, 0.5, 0.9)
        }
    ]
};

これで自然な動きに見えるはずです。図9はアプレットページへのリンクになっています。ぜひ、実際に実行させてみてください。また、完成したsimpleAnimation1.fxのソースもアプレットページに示しておきました。

図9 simpleAnimation1.fxの実行結果

図9 simpleAnimation1.fxの実行結果

繰り返し、そして一時停止

simpleAnimation1.fxはアニメーションは1度しか行いません。もちろん、Startボタンを押せば再度アニメーションが実行されます。しかし、単純にアニメーションを何度も繰り返したいこともあります。そのような場合、TimelineクラスのrepeatCountアトリビュートに繰り返しする回数を設定します。repeatCountアトリビュートの値をTimeline.INDEFINITEに設定すると、無限にアニメーションを繰り返します。

では、先ほどの車のアニメーションも無限に繰り返すようにしてみます。ウィンドウの左側から出てきて、右側に消えるというアニメーションを延々と続けます。

リスト9
var timeline = Timeline {
    // INDEFINITE にすることで無限に繰り返す
    repeatCount: Timeline.INDEFINITE
 
    keyFrames: [
        KeyFrame {
            time: 0s
            values: [
                x => 0
            ]
        },
        KeyFrame {
            time: 2s
            values: [
                x => 710
            ]
        }
    ]
};

左側から車が表れるようにするため、車の起動時の位置をウィンドウから見えない位置に設定しています。また、走り続けているという設定にするため、イーズイン、イーズアウトは設定せず、一定速度でアニメーションさせています。

ここでは使用しませんでしたが、TimelineクラスのautoReverseアトリビュートをtrueにすると、アニメーションを反転します。上記のスクリプトでautoReverseをtrueにした場合、xが0から710に変化した後、710から0に戻ります。

次に行うのが、アニメーションの一時停止です。一時停止を行うにはTimelineクラスのpause関数をコールします。アニメーションを再開するには、もう一度play関数をコールします。

ここでは、アニメーションが動作中にボタンをクリックすると一時停止し、またクリックすると再開するようにしてみます。

リスト10
SwingButton {
    var running = false;
    translateX: 270
    translateY: 10
    text: bind if (running) "Stop" else "Start"
    action: function() {
        if (running) {
            // アニメーション中であれば一時停止
            timeline.pause();
            running = false;
        } else {
            // アニメーションを一時停止していたら、再開
            timeline.play();
            running = true;
        }
    }
}

ボタンをトグルにするため、runnigというBooleanの変数を用意しました。変数runningがtrueならば、pause関数をコールします。falseの場合、play関数をコールします。

では、実行してみましょう。図10に実行結果を示しました。この図もアプレットページへのリンクになっています。

図10 アニメーションの繰り返し

図10 アニメーションの繰り返し

タイムラインで処理を行う

今までのサンプルでは単純に値を変えるだけのアニメーションでした。しかし、もうちょっと複雑なアニメーションにする場合、値の変更だけではいかんともしがたい場合があります。

そのような場合、TimeFrameクラスのactionアトリビュートに何らかの処理を行う関数を代入します。たとえば、次のスクリプトは50ミリ秒ごとにaction関数がコールされます。

リスト11
var timeline = Timeline {
    repeatCount: Timeline.INDEFINITE

    keyFrames: [
        KeyFrame {
            // 50ミリ秒ごとにactionをコール
            time: 50ms
            
            action: function() {
                // 処理
            }
        }
    ]
};

// アニメーションの開始
timeline.play();

では、サンプルで確かめてみましょう。ここでは、マウスポインタを追いかけるイメージを作成します。

マウスポインタを追いかけているように見せるには、周期的にマウスポインタの位置を取得し、取得した位置にイメージを描画するという手順で行います。ここでは、5枚のイメージを用意しました。はじめのイメージは現在のマウスポインタの位置、次のイメージは前回取得したマウスポインタの位置、というように順々に描画することにより、5枚のイメージがあたかもマウスポインタを追いかけているように見えます。

マウスポインタの位置はx座標、y座標ごとにシーケンスに保持させました。

リスト12
// マウスポインタの位置を保持するシーケンス
var previousX: Number[] = [-10000, -10000, -10000, -10000, -10000];
var previousY: Number[] = [-10000, -10000, -10000, -10000, -10000];
 
// マウスポインタを追いかけるイメージ群
var images: ImageView[] = for (i in [4..0 step -1]) {
    println("I {i}");
    ImageView {
        x: bind previousX[i]
        y: bind previousY[i]
        image: Image {
            url: "{__DIR__}duke{i}.gif"
        }
    }
};

previousXとpreviousYの各要素に-10000を代入しているのは、初期状態でイメージがへんなところに表示されることを防止するためです。イメージのxアトリビュートとyアトリビュートは変数previousXと変数previousYの対応する要素にバインドすることで、previousXとpreviousYが更新したときに自動的にイメージの位置を更新することができます。

次に、イメージを描画する部分を示します。

リスト13
var stage = Stage {
    title: "Animated Cursor Sample"
    scene: Scene {
        width: 300
        height: 300
        content: [
            Rectangle {
                // カーソルを表示しない
                // カーソルの代わりにイメージを使用
                cursor: Cursor.NONE

                x: 20 y: 20
                width: 260 height: 260
                fill: Color.AQUAMARINE

                onMouseExited: function(event: MouseEvent) {
                    // 領域から出たらイメージを消す
                    for (image in images) {
                        image.visible = false;
                    } 
                }

                onMouseEntered: function(event: MouseEvent) {
                    // 領域に入ったらイメージを描画する
                    for (image in images) {
                        image.visible = true;
                    } 
                }
            },
            images
        ]
    }
}

JavaFXでは任意のマウスカーソルを設定することができません。Preview SDKまでは可能だったのですが、JavaFX 1.0ではできなくなってしまいました。そこで、ここではカーソルをCursor.NONEに設定することで、カーソルの表示を行わないようにしてしまいました。描画するイメージがカーソルの代わりになります。

また、このマウスポインタを追いかけるイメージは緑の四角の中だけ表示するようにしました。そのため、四角の外に出たら、イメージの描画を行わないようにしています。

最後にタイムラインの部分を示します。

リスト14
var timeline = Timeline {
    repeatCount: Timeline.INDEFINITE

    keyFrames: [
        KeyFrame {
            // 100ミリ秒ごとにactionをコール
            time: 100ms
            
            action: function() {
                // 1. マウスポインタの位置を取得
                var point:PointerInfo = MouseInfo.getPointerInfo();
                var x: Number = point.getLocation().x;
                var y: Number = point.getLocation().y;

                // 2. マウスポインタの位置がルートウィンドウに対する座標なので
                // ローカル座標に変換
                x = x - stage.x - stage.scene.x;
                y = y - stage.y - stage.scene.y;

                // 3. 直近の位置を先頭にし、最後の位置を廃棄する
                insert x before previousX[0];
                delete previousX[5];
                
                insert y before previousY[0];
                delete previousY[5];
            }
        }
    ]
};
 
// アニメーションの開始
timeline.play();

100ミリ秒ごとにactionアトリビュートにセットした関数がコールされます。

関数の中ではまずマウスポインタの位置を取得します。マウスポインタの位置を取得するには、JavaのAWTで定義されるjava.awt.MouseInfoクラスを使用しました。

MouseInfoクラスのgetPointerInfoメソッドで取得できるマウスポインタの座標はルートウィンドウに対する座標です。そのため、2.で示したようにStageオブジェクトの位置とSceneオブジェクトの位置からローカル座標に変換を行います。

最後に、3.に示したように、取得したマウスポインタの位置を変数previousX、変数previousYに挿入します。挿入した後、最も古いマウスポインタの位置を廃棄します。これで、最新5つのマウスポインタの位置を保持することができます。

では、実行してみましょう。図11に示すように小さいDukeが大きいDukeを追いかけていきます。

なお、MouseInfoクラスのgetPointerInfoメソッドでマウスポインタの位置を取得するのはセキュリティ的に問題があるので、AppletやJava Web Startで実行する場合はJARファイルへの署名とポリシーファイルが必要になります。

図11 マウスカーソルを追いかけるイメージ
図11 マウスカーソルを追いかけるイメージ

おまけ

とくに解説はしませんが、2つのサンプルを紹介して今回は終わりにしましょう。

図12はクリックすると、ボタンが震えるというサンプルです。2つのタイムラインを使用し、x軸方向の移動とy軸方向の移動を別々に行っているため、一見複雑そうな動きをするようにしてあります。なお、図12は複数のイメージを重ねて震えるような感じを出しているので、実際のアプリケーション画面とは若干異なります。

他のサンプルと同じように、図12もアプレットページへのリンクになっています。アプレットページにはスクリプトも示してありますので、参考になさってください。

図12 震えるボタン

図12 震えるボタン

もう1つはフェードイン、フェードアウトするアプリケーションバーです。図13では黒いアプリケーションバーが表示されていますが、マウスポインタがこの領域から外れるとフェードアウトして消えてしまいます。また、この領域に入ると、フェードインしながら表れます。

フェードイン、フェードアウトは透明度を変化させることにより実現しています。フェードインしている途中にマウスポインタが領域から出たときには、その透明度からフェードアウトするようにしてあります。

図13はJava Web Startでアプリケーションが起動するページへのリンクがあります。スクリプトも示しましたので、こちらも参考になさってください。

図13 消えるアプリケーションバー

図13 消えるアプリケーションバー

おすすめ記事

記事・ニュース一覧