================================= 4. JavaScriptによるアニメーション ================================= 4.0 目的 -------- ここでは Web ページにアニメーションを表示する。 アニメーションには、 一定時間おきにプログラムを実行するために、 タイマーを用いる。 タイマーとアニメーションの動作を理解しよう。 また、配列やオブジェクトも用いる。 アニメーションのプログラムを見ながら、 なぜプログラミングで配列やオブジェクトを使うかを考え、 これらの役割と、どう利用できるかを理解しよう。 サンプルプログラムは :ref:`sample_code` に従って、 準備してあるものとする。 VS Code で chap04 フォルダーを開くと本章のサンプルプログラムが実行できる。 すでに :ref:`xx_3_draw` についてはよく理解しているものとする。 4.1 最初のアニメーション ------------------------ サンプルプログラム sample04_01 を起動してみよう。 .. raw:: html このプログラムでは、20個の青い円が上から下に移動する。 面白い動きでしょう? script.js を見ていこう。 index.html と stylesheet.css は sample03 と同じだ。 .. literalinclude:: ../program/chap04/sample04_01/script.js :caption: sample04_01/script.js :language: JavaScript :linenos: 行16, 行17でボールの位置を計算している。 ためしに行17に以下のように修正を加えて、変化を確認してみよう。 :: y2 = (y0 * (n % 3 + 1)) % 590; :: y2 = Math.floor(y0 * n / 50 + 1) % 590; 動きが大きく変わることがわかる。 このようにプログラムによって、まったく異なる動きを作ることができる。 4.2 setInterval --------------- アニメーションの仕組みを見ていこう。 このプログラムにはこれまでと同様に draw_canvas という関数があるが、 さらに、tick1 という関数が追加されている。 draw_canvas ではこれまでのような描画はおこなっていない。 以下のように y0 に 0 をセットして、 その後は setInterval という関数を呼び出すだけで終了する。 .. literalinclude:: ../program/chap04/sample04_01/script.js :caption: sample04_01/script.js :language: JavaScript :lines: 47-51 :lineno-start: 47 実際の描画はすべて、 この後で説明する tick1 という別の関数で行っているのである。 setInterval は interval すなわち、実行間隔を指定する。 最初の引数、tick1 が実行される関数。そして2番目の引数、 ここでは33が、tick1を実行する時間間隔を指定する。 時間間隔の単位は ms (ミリ秒)、すなわち1/1000秒単位である。 これによって、tick1 が 33ms 間隔で実行されるようになる。 だいたい毎秒30回弱、tick1 が実行される。 tick1 を一回実行するごとにアニメーションの1画面を表示する。 こうして少しづつ異なる画面を毎秒30回描画することで、 アニメーションを実現している。 次に tick1 の処理を見ていこう。 tick1 は毎秒30回実行される。 実行される毎にまず y0 を1 増やしている。 .. literalinclude:: ../program/chap04/sample04_01/script.js :caption: sample04_01/script.js :language: JavaScript :lines: 32 :lineno-start: 32 これにより、毎秒 30 回 y0 が増加する。 次に n 番目の円の y 座標を計算する。 y 座標は n と y0 によって変化するようになっている。 .. literalinclude:: ../program/chap04/sample04_01/script.js :caption: sample04_01/script.js :language: JavaScript :lines: 15-17 :lineno-start: 15 この部分の計算によって y 座標は、左から何番目であるか、 何フレーム目であるか、 両方のパラメータによって変化するので、 円の高さは、場所、時間とともに変化する。 その結果このプログラムの実行画面のような動きが得られる。 行19~29は、前の単元で行った描画と同じである。 .. literalinclude:: ../program/chap04/sample04_01/script.js :caption: sample04_01/script.js :language: JavaScript :lines: 21 :lineno-start: 21 のように x 座標が n * 30 + 10 y 座標が y2 + 10 の位置に円を描画している。 4.3 配列 -------- sample04_03 ではこのようなアニメーションが表示される。 このようなアニメーションは配列を使うと作ることができる。 .. raw:: html 円がそれぞれ個別に上から下に移動していることが特徴だ。 script.js をみてみよう。 .. literalinclude:: ../program/chap04/sample04_03/script.js :caption: sample04_03/script.js :language: JavaScript :linenos: 配列は行38で作成されている。 .. literalinclude:: ../program/chap04/sample04_03/script.js :caption: sample04_01/script.js :language: JavaScript :lines: 37-38 :lineno-start: 37 ここで、長さ20の配列がつくられる。 y1 という変数は行3ですでに宣言されているが、 その時点では配列としては宣言されていない。 コメント中に Array(20) と書いてあるのは、 この変数名を配列に使うというコメントであって、 プログラムとしては意味を持たない。 JavaScript では配列をあらかじめ配列として宣言する必要がない。 :: y1 = new Array(20); という代入で配列型のデータを作成し、代入している。 ここで初めて y1 に配列が代入される。 このプログラムでは y1 はサイズ 20 の配列としている。 この配列には 20 個の円それぞれの y 座標が記録されている。 それらの y 座標は以下でランダムに設定している。 .. literalinclude:: ../program/chap04/sample04_03/script.js :caption: sample04_03/script.js :language: JavaScript :lines: 47-51 :lineno-start: 47 ランダムな数は以下の式で生成している。 :: Math.floor(Math.random() * 580) Math.random は 0 以上 1.0 未満の実数をランダムに生成する。 その数値を 580 倍し、Math.floor 関数で小数点以下を切り捨てている。 その結果, 0 ~ 579 までの整数がランダムに選ばれる。 したがって、 y1[0]からy1[19]に 0~579 のランダムな数が設定される。 .. literalinclude:: ../program/chap04/sample04_03/script.js :caption: sample04_03/script.js :language: JavaScript :lines: 26-31 :lineno-start: 26 ここで y 座標を 10 ずつ増加している。 これにより描画される円は1コマあたり10ピクセル移動していく。 しかし、ただ移動を続けると、 やがて画面で表示できる範囲を超えてしまう。 そこで 行29から行31までの if 文で、y座標が580を超えたら、 y座標を 0 に戻している。 こうして tick1 を 1回実行するたびに、y座標は 10 ずつ増加するが、 その位置に実際に描画する処理は、行14~行24で行っている。 .. literalinclude:: ../program/chap04/sample04_03/script.js :caption: sample04_03/script.js :language: JavaScript :lines: 14-24 :lineno-start: 14 このうち、円の位置と形状は行16で指定され、 行18以後は、 :ref:`rgb` による着色と、 :ref:`shadow` による影をつける処理を行っている。 ここで、 行28を以下のように修正して、どのように変化するかを確認しよう。 :: y1[n] = y1[n] + 1; :: y1[n] = y1[n] + Math.floor(n / 2); :: y1[n] = y1[n] + Math.floor(n % 3) + 1; 4.4 加速度 ---------- 次に sample04_04 を起動してみよう。 以下の画面が表示される。 .. raw:: html このプログラムでは加速度が表現されている。 プログラムを示す。 .. literalinclude:: ../program/chap04/sample04_04/script.js :caption: sample04_04/script.js :language: JavaScript :linenos: 加速度を表現するには、各円の位置と速度を変数で保持する必要がある。 そこでこのプログラムでは y1 を y座標の配列、 vy を y 方向の速度を保持する配列としている。 .. literalinclude:: ../program/chap04/sample04_04/script.js :caption: sample04_03/script.js :language: JavaScript :lines: 3-4 :lineno-start: 3 初期値として、y座標は 50~200、速度は 0~5 の実数を設定している。 .. literalinclude:: ../program/chap04/sample04_04/script.js :caption: sample04_03/script.js :language: JavaScript :lines: 53-59 :lineno-start: 53 描画時には、まず y 座標を実数から整数に変換してから描画する。 .. literalinclude:: ../program/chap04/sample04_04/script.js :caption: sample04_04/script.js :language: JavaScript :lines: 15-20 :lineno-start: 15 そして y 座標は速度分変化させ、速度は毎回 +1 する。 .. literalinclude:: ../program/chap04/sample04_04/script.js :caption: sample04_04/script.js :language: JavaScript :lines: 32-33 :lineno-start: 32 y 座標が 580 を超えた際には、y座標を0に戻す代わりに速度を反転させる。 これで、再び円は元の方向に戻っていくので、 あたかも反射したように見える。 .. literalinclude:: ../program/chap04/sample04_04/script.js :caption: sample04_04/script.js :language: JavaScript :lines: 34-36 :lineno-start: 34 .. _object: 4.5 オブジェクト ----------------- 次に sample04_05 を起動してみよう。 以下の画面が表示される。 .. raw:: html このプログラムでは上下左右に ball が運動する。 プログラムを示す。 .. literalinclude:: ../program/chap04/sample04_05/script.js :caption: sample04_05/script.js :language: JavaScript :linenos: このプログラムはオブジェクト指向のプログラムとなっている。 全体が大きく3つの領域に分かれている。 行1~行4ではプログラム全体で共有する変数を定義している。 .. literalinclude:: ../program/chap04/sample04_05/script.js :caption: sample04_04/script.js :language: JavaScript :lines: 1-4 :lineno-start: 1 このように関数などに含まれない領域で変数を定義すると、 プログラム全体で同じ変数を共有することができる。 行6~行43では ball に関するすべての処理を記述している。 まず行7~行13で ball の初期化を行っている。 .. literalinclude:: ../program/chap04/sample04_05/script.js :caption: sample04_04/script.js :language: JavaScript :lines: 6-13 :lineno-start: 6 ここで、:: this.x という記法で、オブジェクトの属性を表す。 this.x はオブジェクト毎にその属性として固有の値を保持できる。 つまり ball 変数を2個作成すれば、それぞれ別の x の値を保持できる。 それぞれの属性は、以下の目的で用いる。 +--------+---------------+ | 変数 | 意味 | +========+===============+ | x | x座標 | +--------+---------------+ | y | y座標 | +--------+---------------+ | vx | x方向速度 | +--------+---------------+ | vy | y方向速度 | +--------+---------------+ 初期化ではx,y,vx,vy にそれぞれ乱数でランダムな値を設定する。 行15~行25では ball の動きを示す。 .. literalinclude:: ../program/chap04/sample04_05/script.js :caption: sample04_04/script.js :language: JavaScript :lines: 15-25 :lineno-start: 15 は、prototype を使って、既存の関数に moveと show を、 新たなメソッドとして追加している。 また、行27~行43 では ball の表示を行う。 表示に必要な x 座標、y座標は ball オブジェクトの属性として記録されている。 そのため宣言部だけはやや面倒であるが、show メソッド自体は比較的わかりやすいだろう。 このようにオブジェクト指向ではオブジェクトの動作を記述する場合には、 オブジェクトが表している「ただ一つの対象データ」のプログラムを書けばよい。 このように、オブジェクトを使うと、以下のメリットが得られる。 (1) 個々のオブジェクトに関するプログラムを書く場合に、 オブジェクトを使う側のプログラムのことは、 ほとんど考えないでプログラムできる。 (2) オブジェクトを使ってプログラムをする場合には、 各オブジェクトの内部のプログラムについては、 考えなくてよい。 4.6 ライブラリ -------------- sample04_06 を起動してみよう。 以下の画面が表示される。 .. raw:: html このプログラムではスプリングが3個動く。 プログラムを見てみよう。 .. literalinclude:: ../program/chap04/sample04_06/script.js :caption: sample04_06/script.js :language: JavaScript :linenos: 予想より非常に簡単なプログラムだ。これでスプリングが動くのだろうか? 実はもう一つソースコードがある。 .. literalinclude:: ../program/chap04/sample04_06/index.html :caption: sample04_06/index.html :language: html :lines: 6-7 :lineno-start: 6 このように anim_lib.js という JavaScript のプログラムを読み込んでいる。 anim_lib.js をみてみよう。 .. literalinclude:: ../program/chap04/sample04_06/anim_lib.js :caption: sample04_06/anim_lib.js :language: JavaScript :linenos: ここでは、 :ref:`object` で示した方法で、以下3つのクラスを定義している。 .. literalinclude:: ../program/chap04/sample04_06/anim_lib.js :caption: sample04_06/anim_lib.js :language: JavaScript :lines: 2-4 :lineno-start: 2 script.js では行34-行42 でこのライブラリを使って、 画面に描画図形を設定している。 .. literalinclude:: ../program/chap04/sample04_06/script.js :caption: sample04_06/script.js :language: JavaScript :lines: 34-42 :lineno-start: 34 たとえば spring1 という変数は、 スプリングのアニメーションに必要な設定がされ、 2Dコンテクスト、ボールの色、x,y座標、スプリングの段数と振動周波数、 スプリングの幅と長さ、が設定される。 これらの図形に関数する情報は、 オブジェクトの内側に格納されるので、 描画の時には、 単にこれらのオブジェクトの、 show() メソッドを呼び出すだけで、 描画ができる。 そのため tick1 関数はとても簡単になってしまう。 .. literalinclude:: ../program/chap04/sample04_06/script.js :caption: sample04_06/script.js :language: JavaScript :lines: 11-21 :lineno-start: 11 たとえば spring1 の描画も、 :: spring1.show(); というメソッドの呼び出しによって、anim_lib内の :: anim_spring.prototype.show が呼ばれて、スプリングの描画が行われる。 このようにオブジェクト定義を使うと、 プログラムの部品化が可能になる。 メインプログラムでは部品化した機能を、 必要に応じて呼び出して、 利用することができる。 実はすでに多くのオブジェクトを使っていることがわかるだろうか? たとえば、:ref:`xx_numtorgb` で、数値にたいして以下のように文字列に変換した。 :: (i * 18).toString(10); これは数値というオブジェクトに対する、 メソッドとして準備されている機能である。 また、描画にたいして、 :: c1.rect(x,y,w,h); という機能もまたメソッドである。 これらは HTML5 の JavaScript ではあらかじめ定義されているライブラリを使っているのである。 4.7 スコープ ------------ ここで変数のスコープについて説明する。 これまで漠然と変数の働きを説明してきた。 たとえば、sample04_04/script.js では、 行3、行20、行28、行50に y1 という変数が現れる。 これらはすべて同じ「実体」を持つ。 つまりすべて同じ値を共有している。 行50で初期化した値が行20でも残っており、 行28で10増加することで描画位置が変化する。 しかし、sample04_05/script.js では 行9、17、23、32にthis.xという変数が出てくるが、 20個のballは別の場所に描画される。 行9では、その特定のballのthis.xの値が設定され、 行23では、その特定のballのthis.xにthis.vxが加算され、 他のballの this.x には影響はない。 たとえば太郎君の身長と次郎君の身長がそれぞれ異なるのは自然だが、 this.x というのはそれと同様に、 new ball() で新しいballが作成されるたびに、 その特定の ball が独自に this.x という値を持っている。 また sample04_06/script.js では、 行25で c1 という変数が定義されている。 c1は :ref:`2dコンテキスト` である。 この変数はこれまでのサンプルプログラムでは、 プログラムの最初で定義されていた。 しかしそうすると、c1 という名前はプログラム全体で、 共有しなければならない。 しかしsample04_06では c1 は draw_canvas の中で定義されている。 したがってこの変数は、 draw_canvas の中でだけ同一の変数として扱われればいいので、 例えば、 draw_canvasの中でだけ c1 を ctx1 と書き換えても、 まったく問題なく動く。 そして、プログラムの他の部分で、 c1 という変数を他の目的で使っても、 全く問題がない。 こうしたことを、 スコープと呼ぶ。 スコープがあることで、 同じ変数を、プログラムの別の部分では問題なく使うことができ、 他プログラムの変数名を気にすることなく、 それぞれのプログラムで変数を使うことができる。 変数が出現する場所と、その有効範囲を以下に示す。 +------------------------------+------------------------------+ | 宣言された場所 | 有効な範囲 | +==============================+==============================+ | 最外部 | プログラム全域 | +------------------------------+------------------------------+ | 関数名 | プログラム全域 | +------------------------------+------------------------------+ | 関数の仮引数 | 関数内部 | +------------------------------+------------------------------+ | 関数内のvar宣言 | 関数内部 | +------------------------------+------------------------------+ 最外部で宣言された変数名だけが、 関数内で使われた場合に、広域変数(プログラム全体で共有される変数)として、 参照できる。 最外部と関数内で同じ変数名を使う場合、 もし関数内で仮引数に現れるか var で宣言された場合は、 関数内の独自の仮引数または変数として扱われる。 それ以外の場合は広域変数として処理される。 4.8 まとめ ---------- (1) setInterval と canvas を使ってアニメーションを描画できる (2) 多くの図形を独立に動かすには配列が役立つ (3) 加速度を表すには2つの変数(配列)を使う (4) オブジェクトを使うと、物体の動きを表すプログラムとそれらを制御するプログラムを、きれいに切り分けてプログラムを作ることができる。