前回は、BoxBlurFilterによりぼかしたイメージのインスタンスに、AlphaMaskFilterでマスクしたもとのイメージのインスタンスを重ね、マウスのドラッグでアルファマスクを描いた(EaselJS「ALPHAMASK FILTER」参照)。参考までに、jsdo.itのサンプルコードを改めて掲げる。この表現そのものは基本的に変えない。今回は、連続する座標を滑らかな軌跡で描く手法について考えたい。
軌跡の描き方をお題と比べる
まず、前掲jsdo.itのコードのもととなった前回の第9回コード2がどのようにマウスポインタの座標の軌跡を描いたか、確かめておこう。ステージ上でマウスボタンを押すStage.stagemousedownイベントでドラッグが始まり(startWipe())、ドラッグしている間のアルファマスクのアニメーションはTicker.tickイベントのリスナー(wipe())が行う。
ドラッグを始めるリスナー関数(startWipe())は、マウスポインタの座標をPointオブジェクトの変数(oldPoint)に納め、描画するGraphicsオブジェクト(wipingShape)の準備を整える。アニメーションのリスナー関数(wipe())は、変数(oldPoint)の座標と今現在のマウスポインタの座標(mouseXとmouseY)を、Graphics.lineTo()メソッドにより直線で結ぶ。そして、新たなマウスポインタの座標で、変数(oldPoint)の値を書き替える。
お題の「ALPHAMASK FILTER」も、ドラッグを始めるリスナー関数(handleMouseDown())とアニメーションのリスナー関数(handleMouseMove())で軌跡を描いている[1]。マウスポインタの座標を変数(oldPt)にとって用いているのは第9回コード2と同じだ。しかし、大きく異なることがふたつある。第1に、Graphics.curveTo()メソッドで曲線を描いていることだ。第2に、もうひとつ座標を変数(oldMidPt)に納めている。これは、今現在のマウスポインタの座標と古い座標の平均、つまり中点を計算している。「>> 1」は「/ 2」とほぼ同じと捉えてよい[2]。
細かな中身はこれから順を追って説明する。だから、ふたつのコードの間に違いがふたつあるということだけ、取りあえず頭に留めてほしい。なお、このコードには実は重大な問題がある。それは、本稿の最後に明かしたい。
2次ベジエ曲線を描く
Graphics.curveTo()メソッドは、内部的にはGraphics.quadraticCurveTo()メソッドを参照する。ActionScriptと同じ名前のメソッドを設けることで、ユーザーになじみやすくした。「2次ベジエ」(Quadratic Bezier)の曲線を描くメソッドだ。以降はGraphics.quadraticCurveTo()メソッドで代表する。2次ベジエは、始点と終点のふたつの座標に加えて、ひとつのコントロールポイントで曲線を定める(図1左図)。Adobe Illustratorなどのベクターグラフィックスを描くアプリケーションでおなじみのベジエ曲線は、コントロールポイントがふたつある「3次ベジエ」(Cubic Bezier)だ(図1右図)。
2次ベジエは、3次ベシエよりコントロールポイントがひとつ少ない分、プログラムの扱いは楽だ。2次ベジエのコントロールポイントは、両端の座標から曲線の接線を延ばし、その交点に置く(図2)。Graphics.quadraticCurveTo()メソッドの引数には、コントロールポイントのxy座標と曲線で結ぶ先のxy座標を渡す。
曲線の設定や引き始めの座標決めなどは、Graphics.lineTo()メソッドで直線を描く場合と基本的に同じだ。Graphics.quadraticCurveTo()メソッドの文法や曲線の描き方について、詳しくは「EaselJSのGraphicクラスで2次ベジエ曲線を描く」をお読みいただきたい。
さて、2次ベジエ曲線を描くには、始点と終点(これらを合わせて「アンカーポイント」と呼ぶことにする)のほかに、コントロールポイントを与えなければならない。すると、連続した座標をひとつおきでアンカーポイントとコントロールポイントに振分けることが思いつく。けれど、ジグザグの軌跡については、滑らかな曲線にはならない(図3)。
そこで、ひとつ工夫を加える。連続した座標は、すべてコントロールポイントにしてしまう。そして、座標の中点をアンカーポイントとして結ぶのだ(図4)。こうすれば、軌跡は滑らかな曲線になる[3]。ただし、軌跡の曲線が座標の上を通過しないことには注意しておこう。それでも、座標の変化が緩やかなら軌跡はそれらの近くをとおり、変化が激しくても滑らかな曲線を描く。
前項で確かめたお題の「ALPHAMASK FILTER」で座標の中点を求めていたのは、このアンカーポイントの座標を決めるためだったのだ。
2次ベジエ曲線で滑らかな軌跡を描く
それでは、前回の第9回コード2に手を加えて、2次ベジエ曲線でマウスポインタの軌跡を滑らかに描いてみよう。マウスポインタの座標を納める変数(oldPoint)に加えて、中点のアンカーポイントの座標も変数(oldMidPoint)にとっておく。マウスボタンを押してドラッグを始めるリスナー関数(startWipe())は、これらふたつの変数に取りあえずマウスポインタの座標を与える。
そして、ドラッグのアニメーションのリスナー関数(wipe())は、変数(oldPoint)の座標と今現在のマウスポインタの座標の中点(midXとmidY)を求める。そして、変数のマウスポインタ座標(oldXとoldY)をコントロールポイントとして、中点座標にGraphics.quadraticCurveTo()メソッドで2次ベジエ曲線を描く。最後に、ふたつの変数のマウスポインタ座標と中点座標を新たな値に書替える。
書き直したスクリプト全体は、つぎのコード1のとおりだ。これで、マウスポインタの軌跡が滑らかな曲線で描かれる(図5)。これで、ひとまずお題はでき上がりだ。jsdo.itにもサンプルコードを掲げた。
本当に滑らかな曲線が描けているのか
これでは腑に落ちない読者がおられよう。前掲コード1を試しても、第9回コード2との違いが感じられない。本当に滑らかな曲線が描けているのか確かめるには、コードにつぎのような手を加えればよい。まず、フレームレートが高いと座標が細かくつなげられるため、曲線に近く見える。Ticker.setFPS()メソッドを使えば、フレームレートが下げられる。つぎに、描く線が太く、また角や端は丸められている。これを細く、シャープにすれば角が目立つ。さらに、線が見やすいように、アルファは1(デフォルト)にしよう。
前掲jsdo.itのコードでも、これらのステートメントはコメントアウトして加えてある。興味がある読者は、ぜひお試しいただきたい。マウスをジクザグに動かしても、軌跡は曲線で描かれるはずだ。
しかし、逆にいえば、そこまでしなければ差はわからない。このお題についていえば、第9回コード2ででき上がりとしても差し支えなかったということになる。それでも、滑らかな軌跡の描き方は、覚えておいて損はない。ということで、今回あえてご紹介した次第だ[3]。
もうひとつ謎が残っていた。お題の「ALPHAMASK FILTER」の「重大な問題」だ。このコードも前掲コード1と同じく、マウスポインタと中点の座標を変数(oldPtとoldMidPt)にとっている。問題は、マウスボタンを押したときのリスナー関数(handleMouseDown())で、変数にPointオブジェクトを代入するステートメントだ。
JavaScriptも含めて多くのプログラミング言語は、変数にはオブジェクトの参照を代入する。つまり、ふたつの変数(oldPtとoldMidPt)は、同じオブジェクトを参照している。そのため、Graphics.curveTo()メソッドに渡したコントロールポイントとアンカーポイントの座標は同じになる。その場合、このメソッドは座標を直線で結ぶことになる。
線を描いた後、新たな座標値を変数に与えるステートメントでも、Pointオブジェクトはつくり直していないので、ふたつの変数は同じオブジェクトの参照をもったままということになる。
奇しくも、このお題では座標を直線で結んでも、わからないし差し支えないということを示している。まぁ、こういうこともあるだろう。次回は、また新たなお題に取り組みたい。