JavaFXのアニメーション

JavaFXの特徴の一つは強力なアニメーション機構.この書き方も0.7から1.0で大きく変わったらしい.0.7を知らないのでよくわからないが.

TimelineとKeyFrame

1.0では,Timelineというオブジェクトを作ってアニメーションを表現する.TimelineにはKeyFrameオブジェクトを並べる.KeyFrameにはある変数のそのフレームでの値を指定することができる.すばらしいのは最低限のKeyFrameを指定するだけで,あとを補間してくれること.便利だ.Timelineをstart()すると,指定に従ってアニメーションが描画される.

KeyFrameオブジェクトの記法は独特で,=> というオペレータを使う.どうにもセマンティクスが釈然としないのだが,こんな感じに書く.
TimelineオブジェクトのkeyFramesというアトリビュートに二つのKeyFrameを指定している.これは,開始0s後は dyの値を0に,3s後にはdyを200にしろ,という指定になる.

    attribute launch = Timeline {
       keyFrames: [
           KeyFrame {
               time:0s
               values: dy => 0
           },
           KeyFrame {
               time: 3s
               values: dy => 200 tween Interpolator.EASEOUT
           }
       ]
    };

tweenというのもシンタックスシュガーで,2つのキーフレームの間の補間方法を指定している.EASEOUTは終点部分をゆっくりにするという指定(だと思う).

で,さらに怪しいシンタックスシュガーがあって,こんな風に書くこともできる.at というキーワードを使う.

    attribute launch = Timeline {
       keyFrames: [
           at (0s) {dy => 0},
           at (3s) {dy => 200 tween Interpolator.EASEOUT}
       ]
    };

だいぶコンパクトになるがなにがなんだか.

よく考えてみると => がどう解釈されているのかも釈然としない.ここの"dy"はTimelineが実行されるときに代入される変数になる.クロージャ的に解釈されるのだと思うのだけど,この記法を使わないで書く方法を見つけられなかった.

CustomNode

JavaFXの描画オブジェクトはすべてNodeのサブクラスになるようだ.ユーザが複数のNodeを組み合わせて新たなノードを作る際には,このCustomNodeをつくるのがお作法らしい.どうも,0.7のころにはCompositeNodeという名前だったようだ.

CustomNodeのサブクラスでは,create メソッドを実装しなければならない.このメソッドで,Groupオブジェクトをつくり,その中に子供のNodeをcontentに入れて返す.

class MyNode extends CustomNode{
...
    function create(): Node { 
        return Group {
            content: [
               ...
            ]
        }
   }
}

Transform

Nodeでは,transform というアトリビュートを指定することができる.transformで親のNodeからの相対位置や回転,スケーリングを指定することができる.さらにaffine変換もできるので,形をゆがめることも可能なはず.
Transform同士の演算方法がAPIを見ても見つけられなかったが,演算できればさらに強力.

サンプル

で,かんたんなアニメーションのサンプルを書いてみた.イメージは花火のつもり.クリックすると打ち上がって,破裂する.せっかくなので動画をキャプチャして貼ってみる.iShowUというシェアウェアを使ったのだけど,登録していないので,画面にでかでかと文字が出ているが,うしろで動いているのだけがサンプルの出力.google videoにアップロードしてみた.なんか便利だなあ...

http://video.google.com/videoplay?docid=3154407052049434018

ソースはこんな感じ.ちょっと面白いのは,打ち上げるアニメーションのtimeline launch の最後のキーフレームで,破裂のtimeline fireを起動している.このように,timelineとtimelineを連携させられるので,結構複雑なアニメーションが割に簡単に書けるかもしれない.

花火は,Circleの集合からなるCustomNodeで表現されている.中心からradius分だけずらしたものをrotateすることで,Circleを円周上に配置している.radius を調整することで花火が広がる.色もalpha値を下げていくことで徐々に背景にとけ込む.

package animationtest;

import javafx.application.*;
import javafx.input.MouseEvent;
import javafx.scene.geometry.*;
import javafx.scene.*;
import javafx.scene.transform.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.input.*;
import javafx.ext.swing.*;
import java.lang.System;
import javafx.animation.*;


class FireWork extends CustomNode{
    attribute count = 10;
    attribute x: Number;
    attribute y: Number;
    attribute dy: Number;
    attribute radius: Number = 0;
    attribute color: Color = Color.YELLOW;
    
    attribute fire = Timeline {
       keyFrames: [
           at (0s) { radius => 0; color => Color.YELLOW },
           at (3s) { 
               radius => 60 tween Interpolator.LINEAR;
               color => Color.rgb(0, 255, 255, 0) 
                        tween Interpolator.EASEOUT;
           }
       ]
    }
    
    attribute launch = Timeline {
       keyFrames: [
           KeyFrame {
               time:0s
               values: dy => 0
           },
           KeyFrame {
               time: 3s
               values: dy => 200 tween Interpolator.EASEOUT
               timelines: [fire]
           }
       ]
    };
    
    function create(): Node { 
        return Group {
            transform: bind Transform.translate(x, y - dy);
            content: for (i in [0 .. count-1]) 
            Circle {
                centerX: 0, centerY: bind radius
                radius: 5
                fill: bind color
                transform: Rotate{angle: bind i * (360.0 / count)}
                blocksMouse: true
                onMouseClicked: function( e: MouseEvent ):Void {
                    launch.start();
                }
            }
        }    
    }
}

var f2 = SwingFrame {
    content: Canvas {
        content: [
            FireWork{x: 100, y: 300},
            FireWork{x: 200, y: 300}
        ]
        background: Color.BLACK
    }
    visible: true
    width:300
    height:400
}

所感

Timeline強力.各KeyFrameにはactionという属性も定義できて,任意の関数を呼び出すこともできる.つまり,JavaでいうTimerのかわりに使うこともできるようだ.こんな感じで書くと,1秒ごとにactionで指定した関数が実行される.一時停止や再開もできるようだ.

Timeline {
 repeatCount: Timeline.INDEFINITE
 KeyFrames: KeyFrame {
   time: 1s
   action: function(){
      ...
   }
 }
}

なかなか使い出がありそう.