Edging

ここでは Macromedia Flash のモーション設定にあるイージングを考慮した移動を数学的に分析し、実際にどのようなアルゴリズムなのかを調べていきたいと思います。
イージング移動については Class LibraryEdging Mover クラスを参考にして下さい。実際にどのような動作をするのかは イージング移動サンプルが参考になると思います。

これらのページでも述べていますが、イージングとは「オブジェクトが移動するときのコマ割を決めるもの」です。直線的に移動させるとき、単に等速で移動させるのではなく、加速度を考慮したアニメーションが可能になります。JavaScript は連続的な動きは実現できず、このコマ割を変えることは非常に重要で強力なものとなります。

では、実際にイージングを考慮した直線移動について調べていきたいと思いますが、上述した通り重要なのは「コマ割」です。このコマ割の座標を算出することが目的ですが、確信に触れる前に実際のものを見たほうが良いでしょう。
チュートリアルを見てどのようなものか把握して下さい。
求めるべきは等分割した点(等速)からのオフセット量です。チュートリアルでは赤色のラインで表現されていましたよね。このラインの長さ(オフセット量)は中心の点で最大値をとり、そこから離れるにつれて小さくなっているのが分かったと思います。そして、どのような場合でも始点と終点のオフセット量は0です。
もう一つ重要なのはその量は中心点に関して、対称であるということです。
0 から始まり、中心で最大値をとり、0 で終わる、そして中心に関して対象な関数・・・
これを満たす最も簡単な関数は sin しかありません。では、式を出す前に実際のプログラムを見てみましょう。これは何の変哲もない、ただの等速移動のプログラムです(X方向のみを考慮した1次元の移動)。
始点を x0 とし、終点座標を x1 、分割数を divCount とします。
var x0 =  50 ; // 始点のX座標
var x1 = 500 ; // 終点のX座標

var tmpCount =  0 ; // カウント
var divCount = 10 ; // 分割数

function move(){
    
    if( tmpCount++ < divCount ){
        
        var c = tmpCount/divCount ;
        
        var x = ( x1-x0 )*c + x0 ;
        
        layer.setPositionX( x );
        
        setTimeout( "move();" , 10 );
        
    }
    
}
この関数を実行すれば x0 から x1 まで直線移動します。これはよく見るプログラムですよね。一応、実行例

ここで注目すべきはローカル変数の c と x です。
c は 0 <= c <= 1 が成り立ち(実際は 1 は含みませんが、場合によっては 1 を含む書き方もあるので含めています)、全体的な割合を決める値です。
そして x は毎回等分割した点を算出しています。
このプログラムの書き方によれば x に何らかの変更を加えたものがイージング移動を実現するのは分かると思います。

ここでチュートリアルを思い出してみましょう。
あの赤いラインは等分割面からのオフセット量を表していました。
オフセット量ということはつまり、上のプログラムの x の右辺に掛け算などを加えるのではなくて単に足し算をしてやれば良いことが分かります。
では、その量を c を変数とする関数として、f(c) としましょう。
この場合 x は以下で表されます(これはプログラムではありません)。

x = ( x1-x0 )*c + x0 + f(c) ( 但し、0 ≦ c ≦ 1 )

さて、この f(c) がどんな関数かが問題ですが、先ほどのオフセット量の性質を満たす関数は sin 関数であると述べました。
0 から始まり(tmpCount=0)、中心(tmpCount=divCount/2)で最大、そして 0 で終わる(tmpCount=divCount)。これは c を使えば以下のように書けます。
sin( πc )
これに振幅は掛けてやらなければいけませんが、まだ、その量が分からないので振幅値を a とします。つまり、f(c) は以下のようになります。

f(c) = a*sin( πc )

この式は確かに 0 ≦ c ≦ 1 でオフセット量の条件を満たす式です。これを用いると x は、

x = ( x1-x0 )*c + x0 + a*sin( πc ) ・・・@ 但し、0 ≦ c ≦ 1

と書けました。では、あまり変わりませんが、迷わないようにこれをプログラムに組み込んでみます。
var x0 =  50 ; // 始点のX座標
var x1 = 500 ; // 終点のX座標

var tmpCount =  0 ; // カウント
var divCount = 10 ; // 分割数

var a = ??? ;

function move(){
    
    if( tmpCount++ < divCount ){
        
        var c = tmpCount/divCount ;
        
        var x = ( x1-x0 )*c + x0 + a*Math.sin( Math.PI*c );
        
        layer.setPositionX( x );
        
        setTimeout( "move();" , 10 );
        
    }
    
}
これで終了!とか思っちゃ駄目です。
この式の形はほぼ完成形と言っても良いのですが、a が未知で、適当に代入するわけには行きません。a = 1000 とか入れるとおかしくなるのは目に見えてます。
次からはこの a の値の範囲を調べていきます。実際にはこの a が所謂イージング値で、これが大きい程減速、小さい程加速になります。ここからが本題です。

x = ( x1-x0 )*c + x0 + a*sin( πc ) ・・・@ 但し、0 ≦ c ≦ 1

この式を調べることにより、a の値の範囲を決定したいと思います。
では、この式のグラフを直感的に書いてみましょう。横軸に c をとり、縦軸に x をとります(注意:実際のオブジェクトの軌跡を描いたものではありません)。

x=(x1-x0)*c+x0+a*sin(πc)のグラフ(a>0)
図1.a > 0 のときの x=(x1-x0)*c+x0+a*sin(πc)のグラフ

グラフを書くとよく分かりますね。灰色の直線ラインが等速の場合の軌跡で、下の灰色の曲線がオフセット量、赤色のラインがイージングを考慮したときの軌跡です。赤のラインは最初、勢いよく上がって次第に増分が減っているのが分かります。つまり、減速しているということです。このグラフは a > 0 のときのグラフですが、 a < 0 のときは下のように赤のラインを反転した形になります。

x=(x1-x0)*c+x0+a*sin(πc)のグラフ(a<0)
図2.a < 0 のときの x=(x1-x0)*c+x0+a*sin(πc)のグラフ

このグラフは最初、緩やかに上昇し終点に近づくにつれて勢いよく増加しているので、加速ということになります。

先ほど、a = 1000 などの大きい値はまずいと書きましたが、その場合、何が起こるのでしょうか?
ここにその例を示します(上のプログラムに a = 1000 を代入し、分かり易いように divCount=50 としたもの)。
これは一体何が起きたのかというと、これもまたグラフにして見てみましょう。大体こんな↓感じです。

x=(x1-x0)*c+x0+a*sin(πc)のグラフ(a=1000)
図3.a = 1000 のときの x=(x1-x0)*c+x0+a*sin(πc)のグラフ

ということです。図3を見れば明らかな通り、オフセット量を示す灰色の曲線の値が大きいため、赤のラインは途中で x1 を越えてしまっています。例で見た通り、行って戻ってきたわけです。これはまずいですよね。
こういった現象が起きない a の値の範囲を求めなければなりませんが、いきなりは流石に無理です。
しかし、これらから、x の値の範囲は求めることは出来ます。

a > 0 のとき 0 ≦ c ≦ 1 の任意の c について x ≦ x1 が成り立たなければならない。
a < 0 のとき 0 ≦ c ≦ 1 の任意の c について x ≧ x0 が成り立たなければならない。

まとめて、@式より、

x0 ≦ ( x1-x0 )*c + x0 + a*sin( πc ) ≦ x1 ・・・A

単純に考えて最初に思い付くのはこの式ですが、ちょっと方向を変えて解いてみたいと思います。

グラフの性質から次の考え方が浮かびます。それは、常に増加しなければならないということです。
これは数学的には単調増加といい、微分したものが常に正であれば良いというわけです。
始点は常に x0 終点は常に x1 ということを考えれば、x0 の点から常に増加して、x1 に辿り着くならば、x1 を越えることもないですし、x0 より小さくなることもありません(例えば、x1 を越えた場合、どこかで減少しなければならないことになる)。
これは x1- x0 > 0 のときであるが、x1- x0 < 0 のときは単調減少である。

よって、次が導かれる。

x1- x0 > 0 のとき、x は単調増加 ・・・ B
x1- x0 < 0 のとき、x は単調減少 ・・・ C

よって、

dx/dc = y = x1-x0 + πa*cos(πc) ・・・ D

とおくと、Bについて、 y ≧ 0 であれば良いので、両辺を x1-x0(>0) で割って

1 + πa/(x1-x0)*cos(πc) ≧ 0 ・・・ E

ここで

b = a/(x1-x0) ・・・ F

とおくと、E式は

1 + πb*cos(πc) ≧ 0 ・・・ E'

これが0であるためには最小値が 0 以上であれば良い。b > 0 より最小値は c = 1 のとき、

∴ 1 - πb ≧ 0

∴ b ≦ 1/π ・・・ G

Cについても同様にして

b ≧ -1/π ・・・ H

よって、GH式より

-1/π ≦ b ≦ 1/π ・・・ I ,,

これで終わりです。@式にこれを代入すると、F式より

x = ( x1-x0 )*c + x0 + ( x1- x0 )*b*sin( πc ) 但し、0 ≦ c ≦ 1 , -1/π ≦ b ≦ 1/π

x = ( x1-x0 )*( c + b*sin( πc ) ) + x0 ・・・ J

Flash のイージング値は -100 から 100 の値で指定できたので、その値を edging とし、これを変更すると

x = ( x1-x0 )*( c + edging/(100*π)*sin( πc ) ) + x0 但し、-100 ≦ edging ≦ 100

となります。先ほどのプログラムにこの式を用いると次のようになります。
var x0 =  50 ; // 始点のX座標
var x1 = 500 ; // 終点のX座標

var tmpCount =  0 ; // カウント
var divCount = 20 ; // 分割数

var edging = 100 ; // イージング値 -100 <= edging <= 100

function move(){
    
    if( tmpCount++ < divCount ){
        
        var c = tmpCount/divCount ;
        
        var x = ( x1-x0 )*( c + edging/(100*Math.PI)*Math.sin( Math.PI*c ) ) + x0 ;
        
        layer.setPositionX( x );
        
        setTimeout( "move();" , 10 );
        
    }
    
}
このプログラムの実際の例 はこちらです。
これはX方向のみを考えた一次元の動きですが、Y方向を加えた場合も同様です。一応、プログラムを書くと。
// このプログラムの実例 はこちら

var x0 =  50 ; // 始点のX座標
var x1 = 500 ; // 終点のX座標

var y0 =  50 ; // 始点のY座標
var y1 = 300 ; // 終点のY座標

var tmpCount =  0 ; // カウント
var divCount = 20 ; // 分割数

var edging = 100 ; // イージング値 -100 <= edging <= 100

function move(){
    
    if( tmpCount++ < divCount ){
        
        var c = tmpCount/divCount ;
        
        var x = ( x1-x0 )*( c + edging/(100*Math.PI)*Math.sin( Math.PI*c ) ) + x0 ;
        var y = ( y1-y0 )*( c + edging/(100*Math.PI)*Math.sin( Math.PI*c ) ) + y0 ;
        
        layer.setPosition( x , y );
        
        setTimeout( "move();" , 10 );
        
    }
    
}

最後に edging 値がどのような値をとるとき、顕著に加速、減速が表れるのかを調べてみたいと思います。-100 と 100 をとるとき一番加速、減速するのは容易に想像がつきますが、これもまた数学的に計算で求めたいと思います。図1を見れば分かりますが、最も減速(加速)して止まるのは赤ラインが最も上(下)に突き出た状態になったときです。つまり、0 ≦ c ≦ 1 で積分値が最も大きく(小さく)なるような edging 値を求めてやれば良いことが分かります。

よって、J式より

∫[0,1]xdc = [ ( x1-x0 )*( (1/2)c^2 - (b/π)*cos(πc) ) + x0*c ]

= { ( x1-x0 )( 1/2 + b/π ) + x0 }-{ ( x1-x0 )( -b/π ) }

= ( x0+x1 )/2 + ( 2/π )*( x1-x0 )*b

∴ I式より、この式が最大、最小をとる b の値は

b = ±1/π ,,

つまり、最も顕著に加速が現れる edging 値は -100 、減速は 100 であることが分かる。
いうまでもなく、b = 0 ( edging = 0 ) のときは等速である。これはJ式よりあきらか。

最後の最後に Class LibraryEdging Mover クラス では他クラスを使用し、以下のような感じで書いています。
// このプログラムの実例 はこちら

var s = new Dimension(  50 ,  50 ); // 始点の座標
var e = new Dimension( 500 , 300 ); // 始点の座標

var tmpCount =  0 ; // カウント
var divCount = 20 ; // 分割数

var edging = 100 ; // イージング値 -100 <= edging <= 100

edging /= 100*Math.PI ;

function move(){
    
    if( tmpCount++ < divCount ){
        
        var c = tmpCount/divCount ;
        
        var p = new Dimension( e );
        
        p.sub( s );
        p.scale( c + edging*Math.sin( Math.PI*c ) );
        p.add( s );
        
        layer.setPosition( p );
        
        setTimeout( "move();" , 10 );
        
    }
    
}