LoginSignup
2
3

More than 1 year has passed since last update.

【PixiJS 覚書】第五回 クリックやドラッグに反応させよう(インタラクション)

Last updated at Posted at 2021-12-16

これまでのあらすじ

PixiJSの基本となる仕組みを把握し、
図形、スプライト、文字の描画方法を学び、
それをアニメーションさせる方法まで追ってきました。(超ざっくり)

今回はクリックやドラッグなど、ユーザーの操作に反応させる方法を見ていきます。

PIXI.utils.EventEmitter

PIXI.DisplayObjectの基本クラス

さて、これまで見てきた描画オブジェクトはどれもPIXI.DisplayObjectの派生クラスでした。
しかしPIXI.DisplayObjectにもまだ上の基本クラスが存在します。
それがPIXI.utils.EventEmitterです。

では早速、PIXI.utils.EventEmitterがどのような機能を持っているのかAPI Documentationを見てみましょう。
(リファレンス:PIXI.utils.EventEmitter
eventemitter.png
なんでか知りませんが、PIXI.utils.EventEmitterについてはリファレンスで何も触れられていないんですよね。
そしてただ"See"(見ろ)と一言そえてgithubのリンクがあるのみ。

・・・ソースを読め、自力でソースから読み取れ。
そうおっしゃいますか、そうですか。

ならば仕方ない、がんばってソースから内容を把握しましょう。アタイ負けないわ。

リンクからgithubを開くと「index.d.ts」に型定義が、「index.js」に実装が記述されています。
ここから必要な情報を拾えそうなので、本記事は以後、このソースを頼りに記述しています。

・・・少し話が脱線したので本題に戻します。
描画オブジェクトの基本クラスであるPIXI.DisplayObjectは、PIXI.utils.EventEmitterの派生クラスでもありました。
そして掲題のクリックやドラッグなどのユーザーインタラクションを担っているのがこのPIXI.utils.EventEmitterです。
つまり描画オブジェクトはどれでもインタラクション機能を継承しており、スプライトはもちろん、コンテナや図形、文字なども全てイベントに反応させることができるわけです。

要点1:PIXI.DisplayObjectはPIXI.utils.EventEmitterの派生クラスであり、PIXI.utils.EventEmitterが各インタラクションイベントの反応を担っている。

要点2:つまりPIXI.DisplayObjectの派生クラスである各描画オブジェクトであれば、どれもインタラクションイベントに関する機能を継承している。

サンプルコード

Examplesにちょうど良いサンプルが用意されていたので、今回はそれをそのまま題材として使わせてもらうことにします。
コード全容は リンク先 をご確認ください。

冒頭の雑多なあれこれ

記事の主旨からは外れるのでさらっと流します。

const app = new PIXI.Application({ backgroundColor: 0x1099bb });
document.body.appendChild(app.view);

// create a texture from an image path
const texture = PIXI.Texture.from('examples/assets/bunny.png');

// Scale mode for pixelation
texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;

for (let i = 0; i < 10; i++) {
    createBunny(
        Math.floor(Math.random() * app.screen.width),
        Math.floor(Math.random() * app.screen.height),
    );
}

まずはお約束通りにPIXI.Applicationをインスタンス化し、viewをHTMLのbody要素にappendChildしています。(第一回の内容)
次にURLから直接テクスチャを生成しています。第三回でPIXI.Splite#fromメソッドについて言及しましたが、PIXI.Textureにも同じようなメソッドが用意されていたわけです。

次のtexture.baseTexture.scaleModeについてですが、まず、テクスチャ(PIXI.Textureインスタンス)はそのプロパティとして、更にPIXI.BaseTextureというテクスチャに関する様々な設定情報を一まとめにしたオブジェクトを持っています。
scaleModeプロパティはその設定の内の一つです。
これはテクスチャの拡大縮小を行った際にスムージングを行うか否かの設定であり、「LINEAR(線形補完)」「NEAREST(近似)」の二つの値を選べます。
scalemode.png
おわかりいただけるだろうか。
LINEARの方は良く言えばなめらか、悪く言えばボヤけています。
なので、ドット絵をカチッと見せるのであればNEARESTに、写真などを自然に見せる場合はLINEARですね。

さて本筋に戻ってサンプルコードの続きですが、後はforループ内でx、y座標をランダムに用意してcreateBunnyファンクションに投げ、バニーさんをたくさん生成しています。

interactiveプロパティ

function createBunny(x, y) {
    // create our little bunny friend..
    const bunny = new PIXI.Sprite(texture);

    // enable the bunny to be interactive... this will allow it to respond to mouse and touch events
    bunny.interactive = true;
// (後略)
}

まず冒頭で用意したテクスチャーでスプライトを生成しています(第三回の内容)

その次が今回のキモ、interactiveプロパティです。
これはPIXI.DisplayObjectに用意されたプロパティであり、描画オブジェクトならどれも継承しています。
(リファレンス:PIXI.DisplayObject#interactive
この後、スプライトにイベントハンドラを追加していくことになるのですが、このinteractiveプロパティがfalseのままだと仮にイベントハンドラを登録済でも何も反応しません。(初期値がfalse)
これにより、状況に応じてイベントへの反応をオン/オフできます。

要点3:インタラクションイベントに反応させるには、まずinteractiveプロパティをtrueにする必要がある。

buttonModeプロパティ

    // this button mode will mean the hand cursor appears when you roll over the bunny with your mouse
    bunny.buttonMode = true;

buttonModeプロパティはPIXI.DisplayObjectのプロパティであり、trueに設定するとマウスオーバーした際にカーソルを指(pointer)の状態にしてくれます。
これもインタラクションの一種なので、上記のinteractiveプロパティがfalseの場合は反応しません。

途中、雑多な部分

    // center the bunny's anchor point
    bunny.anchor.set(0.5);

    // make it a bit bigger, so it's easier to grab
    bunny.scale.set(3);

anchorsceleをセットしています。(第二回と第三回の内容)
はい、次いきます。

onメソッド

    // setup events for mouse + touch using
    // the pointer events
    bunny
        .on('pointerdown', onDragStart)
        .on('pointerup', onDragEnd)
        .on('pointerupoutside', onDragEnd)
        .on('pointermove', onDragMove);

本日のキモ、その2です。
onメソッドはPIXI.utils.EventEmitterで定義されたメソッドであり、イベントリスナの登録を行います。DOMで言うところのaddEventListenerのようなものです。

・on(event, fn, context)
引数を3つ取ります。

event:イベント種類
fn:イベントハンドラ
context:イベントハンドラをbindする対象

eventはDOMのイベントと良く似ていますが独自の機能も持っています。
具体的にどのような項目が用意されているかはPIXI.DisplayObjectのリファレンスにまとめられています。
今回は
「pointerdown(マウスクリック、タッチパネルでのタッチ)」
「pointerup(マウスでクリックを離した、タッチパネルでタッチを離した)」
「pointerupoutside(描画オブジェクトの外側でクリック、タッチを離した)」
「pointermove(描画オブジェクト上でマウス、タッチが動いた)」
の4つのイベントを続けて登録しています。
onメソッドは、戻り値に自身(this)をリターンする特徴があるので、それを利用してメソッドチェーンで記述しています。

fnはイベント発生時に呼び出されるイベントハンドラです。これについては特に説明することはありません。

contextですが、イベントハンドラを呼び出す際にbindする対象となるobjectです。たいていは省略して問題なく、省略した場合はイベントの発生した描画オブジェクトが「this」となります。

要点4:PIXI.DisplayObject派生クラスはonメソッドを持っており、このonメソッドでイベントリスナの登録を行う。

イベント種類についてもう少し掘り下げ

    // For mouse-only events
    // .on('mousedown', onDragStart)
    // .on('mouseup', onDragEnd)
    // .on('mouseupoutside', onDragEnd)
    // .on('mousemove', onDragMove);

    // For touch-only events
    // .on('touchstart', onDragStart)
    // .on('touchend', onDragEnd)
    // .on('touchendoutside', onDragEnd)
    // .on('touchmove', onDragMove);

全てコメントアウトされていますが、割と重要な部分です。
コメントの内容通りではあるのですが、マウス操作にのみ反応する「mouse****」系イベント、タッチパネルのタッチ操作にのみ反応する「touche****」系イベントがそれぞれ用意されています。
実際にはそのどちらにも対応した「pointer****」系イベントが扱い易いですが、マウス操作とタッチパネル操作の両方に対応させようとすると、その挙動の差分を考慮しつつイベントハンドラを記述する必要が発生し、それはそれで大変だったりもします。

余談:offメソッド、onceメソッド、removeAllListenersメソッド等々

EventEmitterのソースを見ていたら、他にもイベントリスナを取り除くoffメソッド、一度だけしか反応しないリスナを登録するonceメソッド、全てのイベントリスナを一括除去するremoveAllListenersメソッドなんてのもあります。
ここでは深く触れませんが、必要に応じてどのような機能があるのか探せるよう、ソースを読みに行く姿勢は常に意識したいですね(自戒)

雑多な処理、後半

    // move the sprite to its designated position
    bunny.x = x;
    bunny.y = y;

    // add it to the stage
    app.stage.addChild(bunny);

createBunnyファンクションで受け取った引数をスプライトのx、yにセットし、addChildeしています。
特に言及すべきこともないので、ささっと次に行きます。

各イベントハンドラ

function onDragStart(event) {
    // store a reference to the data
    // the reason for this is because of multitouch
    // we want to track the movement of this particular touch
    this.data = event.data;
    this.alpha = 0.5;
    this.dragging = true;
}

function onDragEnd() {
    this.alpha = 1;
    this.dragging = false;
    // set the interaction data to null
    this.data = null;
}

function onDragMove() {
    if (this.dragging) {
        const newPosition = this.data.getLocalPosition(this.parent);
        this.x = newPosition.x;
        this.y = newPosition.y;
    }
}

ここからは各イベントハンドラについて見ていきます。

onDragStartは引数としてeventを受け取っていますが、これはDOMのイベントオブジェクトと似て否なるPIXI.InteractionEventです。
今回のdataプロパティのようにDOMにはない要素があったり、逆にtimeStampプロパティのようなDOMにはあるのにPIXI.InteractionEventにはないプロパティもあるので、注意が必要です。

さて、コードの内容に戻ります。
onDragStartではeventのdataを、thisのdataプロパティとして持たせています。
onメソッドの項でも触れましたが、contextを省略している場合、イベントハンドラはイベントの発生した描画オブジェクトに結び付けられます。そのため、ここでの「this」はクリックしたバニーさんインスタンスです。

要点5:(onメソッドでのイベントリスナ登録時にcontextを省略した場合、)イベントハンドラはイベントの発生した描画オブジェクトにbindされる。そのため、イベントハンドラ内の「this」はその描画オブジェクトを指す。

後は、this(つまり対象のバニーさん)のalphaプロパティ(不透明度)を0.5にして半透明にし、ドラッグ中のフラグをtrueにしています。alphaはPIXI.DisplayObjectで定義されているプロパティです。

残りのイベントハンドラの内容は本記事の主旨から逸れるので駆け足で流します。

onDragEndはクリックまたはタッチが離れた時の処理であり、半透明にしていたalphaを1に戻し、ドラッグ中のフラグを解除、イベントデータをnullにして空の状態にしています。

onDragMoveはドラッグ中のフラグがtrueかどうかを判定し、trueの場合にのみ動くようにしています。これによりマウスカーソルがスプライト上を素通りしようとしただけで反応してしまうことを防いでいます。

位置座標newPositionthis.dataを介して取得しているのは、タッチパネル操作の場合を想定しての対応です。マウス操作の場合はカーソル位置は一つしかありませんが、タッチパネルの場合はマルチタッチの可能性が存在するため、タッチしたイベント情報をdataとして保持させることでタッチと該当スプライトを紐付けしているわけです。

まとめ

要点1:PIXI.DisplayObjectはPIXI.utils.EventEmitterの派生クラスであり、PIXI.utils.EventEmitterが各インタラクションイベントの反応を担っている。

要点2:つまりPIXI.DisplayObjectの派生クラスである各描画オブジェクトであれば、どれもインタラクションイベントに関する機能を継承している。

要点3:インタラクションイベントに反応させるには、まずinteractiveプロパティをtrueにする必要がある。

要点4:PIXI.DisplayObject派生クラスはonメソッドを持っており、このonメソッドでイベントリスナの登録を行う。

要点5:(onメソッドでのイベントリスナ登録時にcontextを省略した場合、)イベントハンドラはイベントの発生した描画オブジェクトにbindされる。そのため、イベントハンドラ内の「this」はその描画オブジェクトを指す。


今回はここまで!

この第五回までで
PixiJSの基本の仕組み、
図形、スプライト、文字などの描画、
tickerでのアニメーション、
イベントによるインタラクション処理、
と本当に浅くではありますが、一通り全体を横断してきました。

次回は、これまで触れてこなかったけど基本として押さえておきたい、落穂拾い的な部分を拾っていきます。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3