|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
■Twitter: @FumioNonaka / Facebook Page: CreateJS
CreateJS勉強会〜末〜 CreateJS今日この頃:
|
|
EaselJS 0.7.0からは、マウスイベントのリスナーを加えるインスタンスはDisplayObjectにまとめられた。リスナーを除くために、イベントオブジェクトの参照をもたなくて済む。ただし、イベント名が変わったことに注意しなければならない(表001)。また、EaselJS 0.7.0には、MouseEventクラスのイベントがなくなった()。
表001■EaselJS 0.7.0からドラッグ&ドロップに用いるマウスイベントMouseEventクラスの イベント(0.6.1) |
DisplayObjectクラスの イベント(0.7.0) |
マウス操作 |
mousedown | インスタンス上でマウスボタンが押された。 | |
pressmove | インスタンス上でマウスボタンが押されたまま、ポインタが動かされた。 | |
pressup | インスタンス上で押されたマウスボタンが放された。 |
つぎのコード01-002では、イベントリスナーを加えたインスタンスはEvent.targetプロパティから得られるので、その参照からリスナーを除いている(第14〜16行目)。
コード01-002■EaselJS 0.7.1でインスタンスをドラッグ&ドロップする
|
バブリングというのは、泡のように上っていくこと。EaselJS 0.7.0から、ほとんどのマウスイベントは、イベントが起こったインスタンス(ターゲット)から表示リストの階層を上って伝わるようになった。その流れに先立って、表示リストの頂点からターゲットに向かって伝わることをキャプチャという(図002)。
図002■イベントのバブリングとキャプチャイベントの流れは止められる。バブリングが採り入れられたのに合わせて、Eventクラスにはイベントの流れを扱うメソッドが備わった(表002)。
表002■EaselJS 0.7.0からEventクラスに備わったイベントの流れを扱うメソッドEventクラスでイベントの流れを扱うメソッド | |
Event.preventDefault() | イベントが起こったときに行われるべきデフォルトの動きを止める。 |
Event.stopImmediatePropagation() | イベントが後続のリスナーに伝わるのを止める。 |
Event.stopPropagation() | イベントが後続のオブジェクトに伝わるのを止める。 |
Event.stopImmediatePropagation()メソッドはイベントを直ちに(immediate)止めて、リスナーが加えられた現行オブジェクト(Event.currentTargetプロパティの参照)のつぎのリスナーにもイベントは送らない。それに対して、Event.stopPropagation()メソッドは、表示リストのつぎの階層のオブジェクトへのイベントを止める(「EaselJS 0.7.0: Eventクラスでイベントの流れを扱うメソッド」参照)。
図003■現行オブジェクトと表示リストのつぎの階層へのイベントの流れ
サンプルとして後にコード01-003を掲げた。ボタンにするShape(button)と背景画像のBitmapインスタンスを、Containerオブジェクト(container)に入れ子にした。そして、ボタンをクリックするとURLが開き、親Containerオブジェクトは画像とボタンをまとめてドラッグできるようにしたい(図004)。
図004■ボタンのクリックと背景画像のドラッグ
ボタンはクリック |
背景画像はドラッグ |
ドラッグのスクリプトは、基本的に前掲コード01-002と変わらない。ただし、ドラッグするインスタンスは、マウス操作をしたBitmapではない。このインスタンスをボタン(button)とともに入れ子にした親Containerオブジェクト(container)だ。そして、イベントが起こったインスタンスはEvent.targetプロパティで参照を得るのに対して、リスナーが加えられてイベントを処理しているインスタンスはEvent.currentTargetプロパティで参照する(第6、第13、および第19行目)。
ボタンのShapeインスタンス(button)については、DisplayObject.clickイベントに加えたリスナー関数(navigateToURL())でURLを開けばよい(第3および第23〜25行目)。ただし、このスクリプトの組立てだけでは、意図しない動きが生じる。Shapeインスタンスの上でマウスボタンを押すと、イベントはバブリングして親Containerオブジェクト(container)に伝わる。すると、そのリスナー関数(startDrag())が呼出され、ドラッグを始めたことになる。つまり、ひとたびボタンを押すと、Shapeインスタンスの外にマウスポインタが出せなくなってしまうのだ。
- container.addEventListener("mousedown", startDrag);
- button.addEventListener("click", navigateToURL);
- function startDrag(eventObject) {
- var instance = eventObject.currentTarget;
- instance.addEventListener("pressmove", drag);
- instance.addEventListener("pressup", stopDrag);
- }
- function drag(eventObject) {
- var instance = eventObject.currentTarget;
- instance.x = eventObject.stageX;
- instance.y = eventObject.stageY;
- stage.update();
- }
- function stopDrag(eventObject) {
- var instance = eventObject.currentTarget;
- instance.removeEventListener("pressmove", drag);
- instance.removeEventListener("pressup", stopDrag);
- }
- function navigateToURL(eventObject) {
- window.open("http://plus.adobe-adc.jp/2_10/", "_blank");
- }
そこで、マウスイベントのバブリングを止める。ボタンのShapeインスタンス(button)にも、DisplayObject.mousedownイベントのリスナー(stopEvent())を加える(第4行目)。この関数がやることは、Event.stopPropagation()メソッドで親オブジェクトへのイベントを止めることだけだ(第26〜28行目)。
コード01-003■EaselJS 0.7.1で入れ子インスタンスのマウスイベントがバブリングするのを止める
|
マウスイベントのバブリングについて、ひとつ気をつけるべきことがある。EaselJS 0.7.1でつぎのようなコードを書いたとき、手前に重ねたインスタンスがリスナーをもたなければ、後ろのインスタンス(instance)にマウスイベントが起こってそのリスナー(rotate())は呼出される。つまり、インスタンスの重なりをクリックすれば、後ろのインスタンスが回る(図005)。
- instance.addEventListener("click", rotate);
- function rotate(eventObject) {
- var instance = eventObject.currentTarget;
- instance.rotation += 30;
- stage.update();
- }
図005■手前に重ねたインスタンスがリスナーをもたなければ後ろのインスタンスにイベントが起こる
ところが、以下のコード01-004のように、ふたつのインスタンスがマウスイベントのリスナーをもつ親Containerオブジェクト(container)の入れ子になっていたとする(第2〜4行目)。すると、手前のインスタンス(no_listener)は自らにリスナーがなくても、マウスイベントを起こしてバブリングしてしまう。つまり、インスタンスが重なった部分をクリックしたマウスイベントは、前のインスタンスが奪って、後ろのインスタンス(instance)のリスナー(rotate())に渡らなくなる(図006)。
図006■親インスタンスがリスナーをもつと子インスタンスのマウスイベントはバブリングする
|
EaselJS 0.7.0のEventDispatcherクラスには、新たにメソッドがふたつ加わった(表003)。また、これらのメソッドと関わるEventクラスの新しいメソッドも併せてご紹介する。なお、EventDispatcherクラスの他のメソッドも細かいところで変わっているので、詳しくは「EaselJS 0.7.1: イベントリスナーを扱うEventDispatcherクラス」をお読みいただきたい。
表003■EaselJS 0.7.0でEventDispatcherとEventクラスに加わったイベントリスナーを扱う新しいメソッドEventDispatcherクラス | |
EventDispatcher.on() | 第1引数のイベントに第2引数のリスナーを加える。残りの引数で、さらに細かな定めが加えられる。EventDispatcher.addEventListener()メソッドの拡張。 |
EventDispatcher.off() | 第1引数のイベントに加えた第2引数のリスナーを除く。EventDispatcher.removeEventListener()メソッドと同じ。 |
Eventクラス | |
Event.remove() | そのイベントオブジェクトが渡されたリスナー関数をイベントリスナーから除く。 |
例として、EaselJSとPreloadJSで外部画像ファイルを読込む場合について考える。以下のコード01-005は、EaselJS 0.6.1とPreloadJS 0.3.1でロードした画像のBitmapインスタンス(instance)をステージに配置する処理の枠組みだ。このコードはEaselJS 0.7.1とPreloadJS 0.4.1でも、とくに問題なく動作する。
コード01-005■EaselJS 0.6.1とPreloadJS 0.3.1で外部画像ファイルを読込む
|
ここで問われるのは、LoadQueue.fileloadイベントのリスナー(draw())からどうやって画像が読込まれたBitmapインスタンスを参照するかだ。このコード01-005では、Bitmapインスタンスの参照をLoadQueue.loadFile()メソッドの引数のオブジェクトにdataプロパティとして与えた(第3行目)。すると、LoadQueue.fileloadイベントのリスナーが引数に受取るイベントオブジェクトのitemプロパティから取出せる(第7行目)。
さて、EventDispatcher.on()メソッドには、第3引数でリスナー関数がthis参照とすべきオブジェクトを定められる。すると、リスナー関数から参照したいインスタンスは、このメソッドの第3引数に与えることもできる。
オブジェクト.on(イベント名, リスナー, this参照先)
前掲コード01-005で、EventDispatcher.on()メソッドを用いれば、第3引数によりBitmapインスタンスをリスナー関数(draw())のthis参照に定められる。後に掲げるコード01-006には、その書替えを施した(第2〜3行目)。
- var loader = new createjs.LoadQueue(false);
// loader.addEventListener("fileload", draw);- loader.on("fileload", draw, instance);
// loader.loadFile({src:file, data:instance});- loader.loadFile({src:file});
これで、LoadQueue.fileloadイベントのリスナー関数(draw())は、Bitmapインスタンスをthis参照で扱える。ただし、リスナーを除くときは気をつけなければならない。EventDispatcher.on()メソッドがリスナーに加えるのは、第2引数の関数から第3引数のオブジェクトをthis参照にして新たにつくる関数だ。つまり、リスナーを除くメソッドに、第2引数の関数を渡してもイベントからは消せない。ふたつの関数が別だからだ。
- function draw(eventObject) {
// var loader = eventObject.target;// var instance = eventObject.item.data;
// Bitmapインスタンスへの処理
// loader.removeEventListener("fileload", draw);
loader.off("fileload", draw); // 関数がリスナーではないため除けない- stage.update();
- }
EventDispatcher.on()も、EventDispatcher.addEventListener()メソッドと同じく、インスタンスに加えたリスナーを返す。だったら、その参照をとっておいて、イベントリスナーから除けばよい。もっとも、今回のお題は、Bitmapインスタンスの参照を使わないのが売りだった。替わりにリスナー関数の参照をもたなければならないのでは意味がない。
そこで使えるのが、新たなEvent.remove()メソッドだ。そのイベントオブジェクトを受取った関数が、リスナーから除かれる。イベントオブジェクトに対してメソッドを呼出すので、リスナー関数の参照は求められない(第6行目)。これで、Bitmapインスタンスもリスナーも、参照をとっておかなくて済む。
コード01-006■EaselJS 0.7.1のメソッドとPreloadJS 0.4.1で外部画像ファイルを読込む
|
ひとつ気をつけなければならないのは、Event.remove()メソッドが直ちにイベントリスナーを消すのではないことだ。そのイベントリスナーをもう呼出さないというフラグが与えられる。内部的には、つぎにイベントが配信されるとき、フラグのついたリスナーが除かれる。そのため、Event.remove()メソッドを呼出したすぐ後にEventDispatcher.hasEventListener()メソッドでリスナーがあるか調べると、trueが返されてリスナーは残っている。
そもそも、EventDispatcher.on()メソッドに第3引数を渡すと、呼出すたびに新たなリスナー関数がつくられる。したがって、第3引数を与える呼出しは、できるだけ少なくすることが望ましい。
EaselJS 0.7.1では、イベントに関わるつぎのようなメソッドとプロパティが加えられた(表004)。
表004■EaselJS 0.7.1でEventDispatcherとMouseEventクラスに加わった新しいメソッドとプロパティEventDispatcherクラス | |
EventDispatcher.willTrigger() | 指定したイベントをフローの中で受取るリスナーがあるかどうかをブール(論理)値で返す。 |
MouseEventクラス | |
MouseEvent.localX MouseEvent.localY |
[読取り専用] イベントリスナーが加えられたオブジェクトから見たマウスポインタのxy座標値。 |
EaselJS 0.7.0に備わったMatrix2D.transformPoint()メソッドは、Matrix2Dオブジェクトの変換行列でxy座標を変換して返す。gihyo.jp連載「HTML5のCanvasでつくるダイナミックな表現―CreateJSを使う」の第15回「Matrix2Dクラスで座標を回す」と第16回「3次元空間で座標を回す」でつくった作例にもとづいて、メソッドの使い方をご紹介したい。
Matrix2D.transformPoint()メソッドは参照したMatrix2Dの変換行列で、はじめのふたつの引数に渡したxy座標を変換する。第3引数にPointなどのオブジェクトを渡せば、そのxとyのプロパティに対して変換された座標値が与えられる。第3引数を省くと、新たなオブジェクトにxyのプロパティ値が納められて返される。
Matrix2Dオブジェクト.transformPoint(x座標, y座標, Pointオブジェクト)
以下のサンプル003はつぎの関数(rotate())で、配列(points)に入れた星形の頂点座標のPointインスタンスをすべてforループで取出し、Matrix2D.transformPoint()メソッドにより回転している。変換行列による回転は、数多くの座標を同じ角度回すとき効率に優れる(行列演算では「三角関数で座標を回転するふたつの計算方法」の2の式を用いている)。なお、Matrix2D.identity()メソッドは、Matrix2Dオブジェクトを初期化する。オブジェクトは使い回した方がお得だ。
- function rotate(eventObject) {
- var count = points.length;
- matrix.identity().rotate(angle);
- for (var i = 0; i < count; i++) {
- var point = points[i];
- matrix.transformPoint(point.x, point.y, point);
- }
- }
サンプル003は、マウスポインタの水平位置によって星形の頂点座標を回す角度の速さは変えて、頂点を直線で結んでいる(コードの詳しい解説は「Matrix2Dクラスで座標を回す」参照)。
サンプル003■マウスポインタの水平位置に応じてMatrix2D.transformPoint()メソッドで星形の頂点座標を回転するつぎに、同じ星形をy軸で回す。ただし、CreateJSに3次元座標を扱うクラスはない。そこで、座標はObjectインスタンスとし、xyzのプロパティをもたせる。z座標値は初め0でよい。
{x:x座標値, y:y座標値, z:0}
座標をy軸で回すということは、y座標の値は変えず、残るふたつの座標値をxz平面で変換するということだ(図007)。
図007■xz平面で座標を回す
以下のサンプル004はつぎの関数(rotate())で、配列(points)に入れた星形の3次元頂点座標のObjectインスタンスをすべてforループで取出す。そして、そのxz座標をMatrix2D.transformPoint()メソッドにより回転している。そのうえで、変換したPointインスタンス(_point)のxy座標値を、3次元座標のObject(point)のxとzプロパティに与えた。なお、座標を変換するPointオブジェクトは、ただObjectの3次元座標に値を戻す中継用なので、予め変数にひとつつくって使い回している。
- var _point = new createjs.Point();
- function rotate(eventObject) {
- var count = points.length;
- matrix.identity().rotate(angle);
- for (var i = 0; i < count; i++) {
- var point = points[i];
- matrix.transformPoint(point.x, point.z, _point);
- point.x = _point.x;
- point.z = _point.y;
- }
- }
サンプル004は、このようにして3次元頂点座標をy軸で回している。ただし、星形のワイヤーフレームを描くには、透視投影の座標計算をしないと遠近感が加わらない。それらのコードの詳しい解説については、「3次元空間で座標を回す」をお読みいただきたい。
サンプル004■Matrix2D.transformPoint()メソッドにより星形の頂点座標をy軸で回転するTckerクラスはタイミングのAPIに、setTimeoutかrequestAnimationFrameを使うことができる。EaselJS 0.7.0では、タイミングを決めるプロパティとしてTicker.useRAFに替わってTicker.timingModeが備わり、3つのモードから選べるようになった。
Ticker.timingModeは、Ticker.tickイベントのタイミングをsetTimeoutとrequestAnimationFrameのどちらのAPIで決めるか、および用いるモードを示す静的プロパティだ。つぎの3つの定数から選ぶ(表001)。setTimeoutは間隔のばらつきが小さく、requestAnimationFrameはブラウザの描画とタイミングが合う。ブラウザがrequestAnimationFrameのAPIをサポートしていないときは、Ticker.TIMEOUTモードが用いられる。
表005■Tickerクラスに定められたタイミングモードの定数Tickerクラスの定数 | API | 説明 |
TIMEOUT | setTimeout | フレームレートに合わせた時間間隔(デフォルト)。 |
RAF | requestAnimationFrame | フレームレートによらず、requestAnimationFrameの呼出しにもとづく。アニメーションは経過時間を調べて進める。 |
RAF_SYNCHED | requestAnimationFrame | フレームレートに合わせてTicker.tickイベントを間引く。ただし、間隔はばらつく。また、基本となるリフレッシュレート60Hzの約数にフレームレートを定めないと効果は少ない。 |
Ticker.timingModeプロパティをTicker.RAFモードにすると、Ticker.tickイベントの間隔がばらつく。その調整をしたい場合、Eventオブジェクトのdeltaプロパティから経過ミリ秒数が得られる。フレームレート(FPS)を基準にして変化を与えるなら、つぎの調整係数を乗じればよい。
deltaプロパティ値×FPS / 1000
つぎのコード03-001は、フレームレートを変数(fps)に与え、フレームあたりの角度(5)にEventオブジェクトのdeltaプロパティからから求めた上記調整係数を乗じて、インスタンス(instance)が回転する速さを保っている。
コード03-001■Ticker.RAFモードでインスタンスを一定の速さで回すアニメーション
|
EaselJS 0.7.1は0.0.1のアップデートなので、バグ修正が多くを占める。その中でもEaselJS 0.7.0から直された大きなバグを2件ご紹介する。
Ticker.addEventListener()とTicker.on()は、ともにイベントリスナーを加える静的メソッドだ。どちらも、登録されたリスナー関数を返すと定められている。ところが、EaselJS 0.7.0では戻り値はundefinedで、関数が返されない。
var func = createjs.Ticker.addEventListener("tick", function (eventObject) {});
alert(func); // undefined
EaselJS 0.7.0のTicker.addEventListener()メソッドはつぎのように実装されている。関数を返すreturnステートメントがない。Ticker.on()メソッドも内部的にTicker.addEventListener()を呼んでいるので、同じ問題が生じる。
- Ticker.addEventListener = function() {
- !Ticker._inited && Ticker.init();
- Ticker._addEventListener.apply(Ticker, arguments);
- };
EaselJS 0.7.1では、この実装はつぎのように改められ、リスナー関数が正しく返される(詳しくは、「EaselJS 0.7.0のTicker.addEventListener()やTicker.on()メソッドが関数を返さない」参照)。
- Ticker.addEventListener = function() {
- !Ticker._inited && Ticker.init();
- return Ticker._addEventListener.apply(Ticker, arguments);
- };
ColorMatrixクラスはカラー変換行列を表し、ColorMatrixFilterのカラー変換に用いられる。EaselJS 0.7.0で見つかったつぎの3つのメソッドのバグが直された(「EaselJS 0.7.0: バグが確認されているColorMatrixクラスのメソッド」)。
ColorMatrix.toArray()とColorMatrix.concat()のバグは同じ原因なので、確かめやすい前者のメソッドで見てみよう。カラー変換行列は5行×5列の正方行列で、ColorMatrix.toArray()メソッドはその25成分をエレメントに納めた配列にして返す。ところが、戻り値は空の配列になってしまう。
var _array = new createjs.ColorMatrix();
console.log(_array.toArray()); // []
EaselJS 0.7.0のColorMatrix.toArray()メソッドは、実質的につぎのように実装されていた。ColorMatrix()コンストラクタ関数のFunction.prototypeプロパティにArrayオブジェクト(配列)を与えている(第4行目)。これでそのオブジェクトのクラスは継承できるのが原則だ。ところが、Array.lengthプロパティが正しい値に書替えられないため、ColorMatrix.toArray()メソッドから内部的に呼出されるArray.slice()メソッドが25成分値を取出せない。
- function ColorMatrix(brightness, contrast, saturation, hue) {
- this.initialize(brightness, contrast, saturation, hue);
- }
- ColorMatrix.prototype = [];
- ColorMatrix.prototype.toArray = function() {
- return this.slice(0, ColorMatrix.LENGTH);
- };
EaselJS 0.7.1のColorMatrixクラスは、もはやArrayクラスを継承しない。ColorMatrix.toArray()メソッドは、ColorMatrixオブジェクトがもつカラー変換行列の成分値をforループで取出して、新たな配列に納めたうえで返すように修正された(第2〜6行目)。JavaScriptではArrayクラスを完全に継承するのは難しい(FumioNonaka.com Newsletter: no.119「スクリプト覚え書き」参照)。
ColorMatrix.concat()メソッド
- ColorMatrix.prototype.toArray = function() {
// return this.slice(0, ColorMatrix.LENGTH);- var arr = [];
- for (var i = 0, l = ColorMatrix.LENGTH; i < l; i++) {
- arr[i] = this[i];
- }
- return arr;
- };
ColorMatrix.concat()メソッドは、引数のカラー変換行列と合成(乗算)する。引数のカラー変換行列は、配列またはColorMatrixオブジェクトで渡す。ところが、EaselJS 0.7.0のColorMatrix.concat()メソッドは、引数にColorMatrixオブジェクトを渡すと行列演算が行われない。
EaselJS 0.7.0のColorMatrix.toArray()メソッドは、実質的につぎのように実装されていた。内部的には、引数を25エレメントの配列として扱うために、データの型を整えるメソッド(ColorMatrix._fixMatrix())が呼ばれる。このメソッドが、ColorMatrixオブジェクトから配列に換えるときArray.slice()メソッドを用いるため、ColorMatrix.toArray()メソッドと同じ理由で正しい演算が行われない。
- createjs.ColorMatrix.prototype.concat = function(matrix) {
- matrix = this._fixMatrix(matrix);
- };
- createjs.ColorMatrix.prototype._fixMatrix = function(matrix) {
- if (matrix instanceof ColorMatrix) {
- matrix = matrix.slice(0);
- };
- return matrix;
- };
EaselJS 0.7.1では、ColorMatrixオブジェクトを配列に換えるとき、ColorMatrix.toArray()メソッドを呼出すよう修正された。
ColorMatrix.clone()メソッド
- createjs.ColorMatrix.prototype._fixMatrix = function(matrix) {
- if (matrix instanceof ColorMatrix) {
// matrix = matrix.slice(0);- matrix = matrix.toArray();
- };
- return matrix;
- };
ColorMatrix.clone()メソッドは、同じカラー変換行列のColorMatrixオブジェクトを新たにつくって返す。
EaselJS 0.7.0のColorMatrix.clone()メソッドは、実質的につぎのように実装されていた。しかし、内部的に呼出しているColorMatrix()コンストラクタに渡す引数は、ColorMatrixオブジェクトひとつでなく、つぎのような4つの数値でなければならない。引数の数とそのデータの型が違っているのだから、正しいオブジェクトは返らない。
new ColorMatrix(明度, コントラスト, 彩度, 色相)
- ColorMatrix.prototype.clone = function() {
- return new ColorMatrix(this);
- };
EaselJS 0.7.1のColorMatrix.clone()メソッドは、コンストラクタでつくった新たなColorMatrixオブジェクトに、ColorMatrix.copyMatrix()メソッドでカラー変換行列の成分値をコピーするように修正された。
- ColorMatrix.prototype.clone = function() {
// return new ColorMatrix(this);- return (new ColorMatrix()).copyMatrix(this);
- };
作成者: 野中文雄
更新日 2013年12月23日 細かな加筆・補正。
作成日: 2013年12月20日