« 2012年6月 | トップページ | 2012年8月 »

2012年7月

2012年7月26日 (木)

数式表示 MathJax -6-

数式表示 MathJax -5-に不具合が有りました。そこに提示したコードを実行すると、 (, ) が数式のトリガーになって、 (, ) で挟まれた箇所を整形し、そして (, ) を見えなくしてしまうことが分かりました。

そこで、

    mjConfig.text = 'MathJax.Hub.Config(
        { tex2jax: { inlineMath: [["$","$"], ["\\(","\\)"]] } }
    )';
の内、 ["\\(,"\\)"]\ を追加して
    mjConfig.text = 'MathJax.Hub.Config(
        { tex2jax: { inlineMath: [["$","$"], ["\\\\(","\\\\)"]] } }
    )';
に変更することで不正な動作を回避できました。

この不具合は、文字列が2重にネストされているため、外側のネストで \\\ にエスケープされ、内側のネストで \(( にエスケープされたため、結局 ( が整形のトリガーになってしまったのが原因でした。 ) についても同様です。

この修正で、やっと MathJax がまともに使えるようになりました。

追記:
mjConfig.text はIEで正しく処理させるために mjConfig.innerHTML に修正しないといけないようです。

2012年7月22日 (日)

アニメーションのスキップと運動パラメータ

FlatTable のアニメーションにスキップと運動パラメータの設定機能を実装しました。スキップとは、複数のページを連続アニメーションする際に、そのページをアニメーションしないように設定する機能です(連続アニメーションはアニメーションしたい最初のページを表示してから [ Anim ][ Sequential ]で実行します)。また、運動パラメータとは

  • ラシャ抵抗
  • 球・球反発係数
  • 球・クッション反発係数
を言い、この値によって球の速度および速度の変化が影響を受けます。

アニメーションのスキップ

下図は3ページから成っていますが、2ページ目はスキップするように設定されていて、アニメーションは1・3ページのみが実行されます。



スキップを設定するには、スキップしたいページを表示してから [ Anim ][ Skip on/off ]をクリックします。スキップの解除も同様に行います。

運動パラメータ

[ Anim ][ Action Param. ]で運動パラメータが表示されます。その表示のパラメータ部分をクリックするとダイアログが表示され、パラメータを変更することが出来ます。

因みに、運動パラメータのデフォルト値は、幾つかの配置をアニメーションさせて自然な動きだと感じるような値を選びました。基本の配置は下図の2つのページに示したような、台の端から端までの移動とバンキングです。その他の配置は忘れました。



2012年7月21日 (土)

減速倍率

FlatTable のアニメーションには、必要な機能のうち実装されていないものが幾つか有ります。その内の1つ「減速倍率」を実装しました。

まず、下図を使って、減速倍率を設定した場合とそうでない場合を比べてみましょう。

1ページ目は減速倍率の指定なしです。[ Anim ][ Single ]でアニメーションさせてみて下さい。手球は、1番に当たるまでは遅くて、当たった後突然速くなっています。また、1番はゆっくりとポケットに向かって進んで行っています。これでは球の動きが不自然です。

では、2ページ目(画像左上の▲をクリック)をアニメーションさせてみて下さい。こちらは、手球が1番に当たる前も、そして手球の当たった1番も自然な速さで走っています。このカラクリが減速倍率です。減速倍率とはラシャによる減速の量を既定値の何倍にするかを指定するものです。

球は運動中、ラシャの抵抗を受けて減速して行き、最終的に停止します。 FlatTable は、次式に基づいて、停止から逆算して球の速度を計算していることは既に説明しました。
\[ v_{0} = \sqrt{v_{c}^{2} + 2\alpha \ell} \tag{1} \] \(v_0\) は減速前の速度、 \(v_c\) は減速後の速度です(停止は \(v_c = 0\))。

1ページ目の球1番は、ラシャの抵抗 \(\alpha\) を受けて(ゆっくり)減速しながらポケットに向かって行き、丁度ポケットに入った所で停止するように初速度が決定されています。そのため、走りが遅いのです。

それに対して2ページ目は、ポケットに入った球に「減速倍率」を設定しているので、速く走ることが出来るのです。減速倍率を球に設定すると、その球と1つ前の球で作られた区間は、ラシャ抵抗による減速が「減速倍率」の分大きくなります。即ち、式(1)で \(\alpha\) が大きくなるので \(v_0\) が大きくなるのです。

減速倍率の設定は次のようにします。まず、減速倍率を設定したい球をクリックします。次に [ Anim ]をクリックしてメニューから [ Decelerate ]を選択するとダイアログボックスが出るので、それに数字を入力します。これで設定完了。正の数字は減速を意味し、負の数字は加速を意味します(普通の加速度とは逆になっているので注意)。なお、0は特別な意味を持ちますが、それについては別の機会に説明します(Stop and Go)。

因みに、負の減速倍率を設定することで、クッションでの捻りによる加速を表現することが出来ます(参照: FlatTable 土曜日の実験室)。また、押し・引きの加速を表現する時も負の減速倍率を利用できます。

2012年7月20日 (金)

アニメーションを管理する4つの変数

FlatTable のアニメーションには、それを管理するために4つの変数が使われています。

  • ballList
  • knotAry[][]
  • runList
  • clusterMem (これは局所変数)
この4つの変数について説明します。

ballList

ballList は次のように、何か処理したい対象を設定する時の基礎として使われます。球の番号に対応した桁のビットが、その球のフラッグになっています。

    var propose = ballList & ~mask;

ballList は次のルーチンで値が設定されます。

// ballList の設定
function makeBallList(){
    ・・・
    ballList = 0;
    for (var i = 0; i < nBall; i++){
        ball = animPage.childNodes[(nBall - 1 - i)*2 + 3];      // (1)
        srcBall = curPage.childNodes[(nBall - 1 - i)*2 + 3];    // (2)
        if (srcBall.fLive){        // (3)
            ball.x = srcBall.x;
            ball.y = srcBall.y;
            ballMove(ball, tableAngle);
            ball.src = srcBall;
            ballList |= (1 << i);
            knotAry[0][i] = srcBall;
            knotAry[1][i] = srcBall;
            ball.setAttribute(display, inline);
        } else {
            ball.src = null;
            ballList &= ~(1 << i);
            ball.setAttribute(display, none);
        }
    }
}
(1),(2)の childNodes[(nBall - 1 - i)*2 + 3]i 番の球を表しています。(3)の fLive は、その球が台上に配置されていることのフラッグです。台上に配置されている場合に ballList に組み込まれます。

knotAry[][]

次に knotAry[][] について説明します。配置図には経路の始点から終点まで全ての節点が表現されています。しかし、経路ごとに見た場合、アニメーションの対象として、それらの節点が同時に複数個使用されることは有りません。例えば次の図において、



1番の球は、今 a から b に向かっているのか、 b から c に向かっているのか・・・ということは一意でなければなりません(複数はダメ)。その対象を保持しているのが knotAry[0][i] です( i は球の番号)。因みに、 knotAry[1][i] は、その次の局面で対象となる球が保持されており、適切なタイミングで

    knotAry[0][i] = knotAry[1][i];
が実行されます。 knotAry[1][i] が決定されるタイミングとアニメーションの局面が変わるタイミングが異なるために、バッファとして knotAry[1][i] が使用されています。アニメーションの局面が変わるとは、例えば1番が a から b に向かっていたのが b から c に向かうようになることを言います。

runList

runList は現在動いている球の配列です。次のようなルーチンで設定されます。

    ball = animPage.childNodes[(nBall - 1 - i)*2 + 3];
    target = knotAry[0][i];
    if (nextBall = target.next){
        ・・・・
        runList.push(ball);
    }
ここに target は配置図に載っている球で、 ball はアニメーション用ページにおいて target = knotAry[0][i] に相当するものです。runList に入っている球は、停止したら runList から除外されます。

clusterMem

下図のようなコンビショットをアニメーションする時に必要になるのが clusterMem です。先の3つの変数は大域変数ですが、この clusterMem は局所変数です。この変数は clusterCheck() で作成され、塊りを構成している球のリストをビットのオン・オフで保持しています。ビットの桁が球の番号に対応します。下図の場合、手球が1番に当たった後で動き始める球は、 clusterMem を構成している1番・2番の2個となっています。



2012年7月18日 (水)

散乱角の \(\sin\), \(\cos\)

先の記事「FlatTable アニメーションの流れ」の終わりで、衝突した球の速度よりも衝突された球の速度の方が速くなるという不具合が有ることを示しましたが、その不具合は解消しました。不具合の原因は、Flash 版のコードを見ながら SVG+JavaScript 版を作成した際の誤記によるものでした。問題の箇所は、衝突の散乱角の \(\cos\) を求めるルーチンに有りました。次のコードがそれに該当します(修正済み)。

function calcSin(inV, outV){
    if ( ((0 == inV.x)&&(0 == inV.y))||((0 == outV.x)&&(0 == outV.y)) )
        return 0;
    return (inV.x*outV.y - inV.y*outV.x)/
        Math.sqrt((inV.x*inV.x + inV.y*inV.y)*(outV.x*outV.x + outV.y*outV.y));
}

function calcCos(inV, outV){
    if ( ((0 == inV.x)&&(0 == inV.y))||((0 == outV.x)&&(0 == outV.y)) )
        return 0;
    return (inV.x*outV.x + inV.y*outV.y)/
        Math.sqrt((inV.x*inV.x + inV.y*inV.y)*(outV.x*outV.x + outV.y*outV.y));
}
このコードの中で「.x」や「.y」が一部入れ替わっていました。因みに、この関数は scanCollision() が再帰的に自身を呼び出した後に使用されます。

    if (scanCollision(target, propose)){
        if (!ent.fCurve){
            sin = calcSin(
                {x:(ent.x - trig.x), y:(ent.y - trig.y)},
                {x:(target.next.x - target.x), y:(target.next.y - target.y)}
            );
           cos = calcCos(
               {x:(ent.x - trig.x), y:(ent.y - trig.y)},
               {x:(target.next.x - target.x), y:(target.next.y - target.y)}
            );
        } else {
            ・・・
        }
        totalSpeedV += target.speed*sin;
        totalSpeedH += target.speed*cos;
    }

ついでなので、 \(\sin\) と \(\cos\) の計算を説明します。以下で \(\alpha\) は アニメーション機能の実装、その前にの衝突の図で入射する手球と散乱する1番の成す角度です。

\(\cos\) は内積から求めます。
\[ {\boldsymbol u}\cdot{\boldsymbol v} = \mid{\boldsymbol u}\mid\mid{\boldsymbol v}\mid \cos\alpha \tag{1} \] \[ {\boldsymbol u}\cdot{\boldsymbol v} = u_{x}v_{x} + u_{y}v_{y} \tag{2} \] この2式から、
\[ \cos\alpha = \frac{u_{x}v_{x} + u_{y}v_{y}}{\sqrt{(u_{x}^{2} + u_{y}^2)(v_{x}^{2} + v_{y}^{2})}} \tag{3} \] が求まります。

\(\sin\) は外積から求めます。
\[ {\boldsymbol u}\times{\boldsymbol v} = \mid{\boldsymbol u}\mid\mid{\boldsymbol v}\mid{\boldsymbol i}\sin\alpha \tag{4} \] \[ {\boldsymbol u}\times{\boldsymbol v} = (u_{y}v_{z} - u_{z}v_{y},\: u_{z}v_{x} - u_{x}v_{z},\: u_{x}v_{y} - u_{y}v_{x}) \tag{5} \]

式\((5)\) で \({\boldsymbol u}\) と \({\boldsymbol v}\) の \(z\) 成分を0とすると右辺は \(z\) 成分のみが非0となり、式\((4)\) の \({\boldsymbol i}\) は \(z\) 方向の単位ベクトルとなることが分かります。したがって、2つの式より、
\[ \sin\alpha = \frac{u_{x}v_{y} - u_{y}v_{x}}{\sqrt{(u_{x}^2 + u_{y}^2)(v_{x}^2 + v_{y}^2)}} \tag{6} \] が求まります。

\((3), (6)\) がコードの calcCos(), calcSin() に相当します。

2012年7月17日 (火)

数式表示 MathJax -5-

MathJax をココログで表示する件の続報です。数式表示 MathJax -4- の時点では、そこに示したスクリプトでPCは数式表示が可能ですが、タブレットでは表示できないという状態でした。今回、以下のスクリプトを記事中に記述することでタブレットでも数式を表示できることが分かりました。ただし、後で示すような制限が有ります。

<script>
<!--
(function (){
    if (!document.defMathJax){
        var mjConfig = document.createElement("script");
        mjConfig.type = "text/x-mathjax-config";
        mjConfig.text = 'MathJax.Hub.Config({ tex2jax: { inlineMath: [["$","$"], ["\\(","\\)"]] } })';
        // 追記: ["\\(","\\)"] は ["\\\\(","\\\\)"] に修正すべし
        // 追記: text は innerHTML に変更すべし

        var mathjax = document.createElement("script");
        mathjax.type = "text/javascript"
        mathjax.src = "http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML";

        var emIE7 = document.createElement("meta");
        emIE7.setAttribute("http-equiv", "X-UA-Compatible");
        emIE7.CONTENT = "IE=EmulateIE7";

        var head = document.getElementsByTagName("head")[0];
        head.appendChild(mjConfig);
        head.appendChild(mathjax);
        head.appendChild(emIE7);

        document.defMathJax = true;
    }
})();
// -->
</script>

生成するオマジナイは前回も今回も同じですが、今回は生成領域が <head> 領域になっているのが違います。

ところで、タブレットでココログにアクセスする時は、スマートフォン表示とPC表示の2種類が有るのですが、数式を表示できるのはスマートフォン表示のみのようです。では、前回のスクリプトでも、スマートフォン表示なら数式を表示できるかと試してみたのですが、それはダメでした。前回のスクリプトだと、スマートフォン表示では本文さえも表示しませんでした。表示に関してココログが何か悪さをしているのかも知れません。

なお、今回のスクリプトは MathJax の動的ロードが参考になりました。

2012年7月16日 (月)

FlatTable アニメーションの流れ

FlatTable のアニメーションは下に示した流れで動作しています。


[Single]
↓
singleAnim()
↓
prolog()
↓
 -→animate()-----------------------------------------→終了
↑  ↓       ↓                ↓                 ↓
|  aim()    makeBallList()    scanCollision() → clusterCheck()
|  ↓                         ↑__↓
|  shot()                     |
|  ↓                         |
action()-→ hit()---------------
↓             ↑
step()----------
↓
ballMove()


[Anim]
↓
epilog()

ボタン[ Anim ]のメニュー[ Sngle ]をクリックすると singleAnim() が実行され、 prolog() へと移って行きます。この prolog() でアニメーション用のページ animPage が準備されます。

次に prolog() から animate() へ処理が移り、前回説明した衝突のスキャンが実行され、手球の速度が求められます( scanCollision() )。ただし、アニメーションするページがそれ以上無い場合は、この animate() でアニメーションを終了します。

アニメーション続行ならば、処理は aim() へと進んで行きます。 aim() は手球の進行方向に向かってキューを表示するルーチンです。さらに、この aim() では shot() をインターバルに登録します。

    shotId = setInterval("shot()", 1000/FPS);
ここに FPS は1秒あたりのコマ数です。

aim() に続いて shot() が実行されます。このルーチンはキューのテイクバックからフォロースルーまでの描画を実行します。Flash 版 FlatTable を作成した時、初めはキューの表示が無かったのですが、それだと、球が動き出すまで何処へ向かって移動するのか分からないという不便さが有りました。その改善のためにキューを表示するようにしました。また、ここでは action() をインターバルに登録しています。

次に実行するのが action() です。ここで、いよいよ球が動き始めます。このルーチンでは、次の1コマの移動で次の節点に到達するかどうかを調べて、到達する場合は hit() を実行します。到達しない場合は step() を実行します。球の速度が0になったら、その球を動く球のリスト runList から除外します。そして、 runList が空になったら animate() に処理を移します。

step() は実際に球を動かすルーチンです。1コマ分、球を移動させます( ballMove() )。そして、ラシャの抵抗による減速を実施します。

hit() は、球が他の球に当たった時の処理をするルーチンです。ここでは、衝突した球と衝突された球のその後の速度を求め、衝突された球は動く球のリスト runList に追加します。

アニメーション実行中もしくは球の運動が全て終了した後でボタン[ Anim ]をクリックすると epilog() が実行されアニメーションを終了します。 epilog() ではアニメーションのためのインターバルをクリアし、アニメーション用ページ animPage を非表示、現行ページを表示します。さらに、ボタン[ Anim ]の色を通常の色に戻します。

ところで、下図のアニメーションを実行すると、手球が1番に衝突する時の速度が1番の初速度よりも遅いように見えます。まだ何かバグが有るのでしょう。因みに Flash 版では正常に動いています。(追記:バグ修正しました)

2012年7月14日 (土)

衝突のスキャン

「アニメーション機能の実装、その前に」で球の速度の式が求まったので、下図を参考にして、速度を求めるコードを見て行きましょう。



図中、手球の速度を求めるためには、1番の速度が必要になります。そして、1番の速度を求めるには2番の速度が必要になります。これは、球の衝突をスキャンすることになります。その最初の部分が次のコードです。

    // 衝突のスキャンの入り口
    var trig = animPage.childNodes[(nBall - 1 - 0)*2 + 3];    // (1)
    var mask = 1 << ballNum(trig);
    var propose = ballList & ~mask;
    var clusterMem = clusterCheck(trig, propose) | mask;
    propose &= ~clusterMem;    // (2)
    for (var i = 0; i < nBall; i++){
        if (clusterMem & (1 << i)){
            var ball = animPage.childNodes[(nBall - 1 - i)*2 + 3];
            var nextBall = knotAry[0][i].next;
            if (nextBall){
                scanCollision(knotAry[0][i], propose);    // (3)
                ball.speed = nextBall.fJump ?
                    knotAry[0][i].speed*coRestit_B : knotAry[0][i].speed;
                if (nextBall.fCurve){
                    setCurve(ball, nextBall);
                } else {
                    ball.angle = Math.atan2(nextBall.y - ball.y, nextBall.x - ball.x);
                }
                runList.push(ball);
            }
        }
    }
animPageはアニメーション用のページです。childNodes[(nBall - 1 - 0)*2 + 3] は手球を表しています。(1) は、その手球がアニメーションの起点となる球であることを意味しています。propose は何か処理してほしい球のリストを表しています。(2)では、初期配置で手球に接触している球が propose から除外されています(未使用の球は除く)。i は球の番号です。 scanCollision() は衝突をスキャンしながら球の速度を求める関数です。これを実行すると、第1引数のプロパティ speed に速度が代入されて帰って来ます。 knotAry[0][i] はアニメーションで使用する球を表しています。(3) は、 knotAry[0][i] を起点として propose を対象に衝突のスキャンを実行しています。上図の場合、
    scanCollision(knotAry[0][0], propose);
が実行されます。この時点で knotAry[0][0] は手球です。

次に、scanCollision() の動作を見て行きます。


function scanCollision(trig, propose){
    var speed = 0;
    var ent = trig.next;
    if (ent){
        var length = dist(ent, trig);
        ・・・
        propose &= ~clusterMem;
/*(4)*/ if (clusterMem){
            var target;
            for (var i = 0; i < nBall; i++){
                if (clusterMem & (1 << i)){
                    target = knotAry[0][i];
/*(5)*/             if (scanCollision(target, propose)){
                        sin, cos を計算してから・・・
                        totalSpeedV += target.speed*sin;
                        totalSpeedH += target.speed*cos;
                    }
                }
            }
            var totalSpeed = Math.sqrt(totalSpeedH*totalSpeedH + totalSpeedV*totalSpeedV);
            if (totalSpeedH){
                tan = totalSpeedV/totalSpeedH;
/*(6)*/         ent.speed = totalSpeed*Math.sqrt(1 + tan*tan)/coRestit_B;
            } else {
                ・・・
            }
/*(7)*/ } else {
            if ("0" != ent.decel){
/*(8)*/         scanCollision(ent, propose);
                if (fCushion(ent))
                    ent.speed /= coRestit_C;
                } else {
                   ent.speed = 0;
                }
            }
            ・・・
/*(9)*/     speed = ent.fCurve ? curveToSpeed(ent) : lenToSpeed(ent, length);
        }
    }
    return trig.speed = speed;    // (10)
}

trig, ent, target は、上図に示した関係になっています。起点となる trig が入って来て、次の節点が ent となり、それの衝突している球が target です。

scanCollision(trig, propose) では trig の進んだ先で他の球に衝突する (4) と、しない (7) に分けて処理を進めます。

(5) は、衝突された球 target を新たな起点として scanCollision(target, propose) を実行し、 target の速度を求めています(速度は target.speed に入って戻って来ます)。衝突された球 target の速度が求まると、「アニメーション機能の実装、その前に」の式(2) を用いて衝突した球 ent の速度が求まります。それが (6) です。

(8) は衝突しなかった場合の処理で、主にクッションでの反射に対応しています。

ent の速度が求まれば、「アニメーション機能の実装、その前に」の式(1) を用いて trig の速度が求まります。それが (9) および (11) です。

// 経路長から速度を得る
function lenToSpeed(ball, length){
    var eAccel = ball.decel ? accel*ball.decel : accel;
    return Math.sqrt(ball.speed*ball.speed + 2*eAccel*length);    // (11)
}

scanCollision() 内で求まった速度は、 (10) で trig.speed に代入して返します。素直に「 return speed; 」にしなかった理由は忘れました。 scanCollision() の戻り値を受け取れない個所が有ったのかも知れません。

2012年7月 9日 (月)

球が動きます

FlatTable のテストバージョンにアニメーション機能の一部を実装しました。FlatTable 画像左上の[ Anim ]をクリックするとメニューが出るので、その Single をクリックすると、配置図の球がアニメーションします。もちろん、その前に配置図を作成しておく必要が有ります。アニメーション実行中は、ボタン[ Anim ]は黄色になっています。アニメーションが終了すると紫色になります。そして、再度ボタンをクリックすると濃い青色に戻ります。このように色によってFlatTable の状態を知ることが出来ます。

アニメーション機能も Flash 版では既に実装していたので、それをコピペしてから SVG+JavaScript 用に修正すれば良いわけですが、つまらない間違いが有って少し手間取りました。SVG の球の指定には childNodes[] という配列を使っているのですが、球の番号と SVG データの並び順が逆になっていて、1番の球を指定したつもりが14番になっていたといった具合です。いや、エラーの発現がこういう風に分かりやすければ良いのですが、実際は「xxx が undefined です」といったエラーなので、配列の順序に問題が有るということに気付くのに時間が掛かりました。

このアニメーションに関しては何回かに分けて多少詳しく説明して行く積りです。今回は取り敢えず、動くことを示すだけにしておきます。下図はテストバージョンを使った配置図です。[ Anim ][ Single ] をクリックしてみて下さい。



手球の初速が遅いような気がします。何かバグが有るのかも知れません。(追記:バグ修正済み)

« 2012年6月 | トップページ | 2012年8月 »