pixijs-events について
PixiJS のシーンでインタラクションを実装する開発者向けのスキルです。イベントモード、ポインター処理、伝播、ヒットテスト、カーソル、性能設定を解説します。
主な機能
- 01イベントモードとリスナーの設定
- 02ポインター、マウス、タッチ、ライフサイクルイベント
- 03伝播とキャプチャリスナーの解説
- 04カスタムヒット領域とカーソルスタイル
- 05イベント処理の性能改善ガイド
活用シーン
- 01UI要素のクリック処理
- 02ドラッグ操作の実装
- 03入れ子のシーンでのイベント伝播管理
- 04大規模シーンでのヒットテスト負荷の軽減
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ pixijs/ pixijs-skills --skill pixijs-eventsインストール後、Claude Code で/pixijs-eventsを入力して読み込まれたか確認します。
詳細
PixiJS のフェデレーテッドイベントシステムは、シーングラフ上の DOM イベントを模倣します。オブジェクトを対象にするには container.eventMode = 'static' を設定し、.on()、addEventListener()、または onEventName プロパティのハンドラーでイベントをリッスンします。移動イベントはリッスン対象のオブジェクト上でのみ発生します。ドラッグには globalpointermove を使用してください。
クイックスタート
const button = new Sprite(await Assets.load("button.png"));
button.eventMode = "static";
button.cursor = "pointer";
app.stage.addChild(button);
button.on("pointertap", (event) => {
console.log("clicked at", event.global.x, event.global.y);
});
let dragging = false;
button.on("pointerdown", () => {
dragging = true;
});
button.on("pointerup", () => {
dragging = false;
});
button.on("pointerupoutside", () => {
dragging = false;
});
button.on("globalpointermove", (event) => {
if (dragging) button.parent.toLocal(event.global, undefined, button.position);
});
関連スキル: pixijs-accessibility(スクリーンリーダー+キーボード)、pixijs-scene-dom-container(HTML オーバーレイ)、pixijs-performance(イベントの多いシーン)。
基本パターン
eventMode の値
import { Sprite } from "pixi.js";
const sprite = new Sprite();
// No interaction at all; children also ignored
sprite.eventMode = "none";
// Default. Self not interactive; interactive children still work
sprite.eventMode = "passive";
// Hit tested only when a parent is interactive
sprite.eventMode = "auto";
// Standard interaction: receives pointer/mouse/touch events
sprite.eventMode = "static";
// Like static, but also fires synthetic events from the ticker
// when the pointer is stationary (for animated objects under cursor)
sprite.eventMode = "dynamic";
ボタン、UI 要素、ドラッグ対象には 'static' を使用します。静止したカーソルの下で移動し、継続的なホバー更新が必要なオブジェクトに限り 'dynamic' を使用します。
オブジェクトがイベントを受け取れるかどうかは isInteractive() で確認できます。
sprite.eventMode = "static";
sprite.isInteractive(); // true
sprite.eventMode = "passive";
sprite.isInteractive(); // false
イベントの種類
ポインターイベント(デバイス間の互換性を確保するため推奨):pointerdown、pointerup、pointerupoutside、pointermove、pointerover、pointerout、pointerenter、pointerleave、pointertap、pointercancel。
マウスイベント:mousedown、mouseup、mouseupoutside、mousemove、mouseover、mouseout、mouseenter、mouseleave、click、rightdown、rightup、rightupoutside、rightclick、contextmenu、wheel。PixiJS はブラウザーのコンテキストメニューを抑止しません。メニューを閉じたままにするには、contextmenu リスナー内で event.preventDefault() を呼び出してください。rightdown、rightup、rightclick をキャンセルしても、メニューは閉じたままになりません。macOS では、ctrl キーを押しながらクリックした場合も contextmenu が発生し、ブラウザーが左クリックとして報告することがあります。
タッチイベント:touchstart、touchend、touchendoutside、touchmove、touchcancel、tap。各タッチにはネイティブの TouchEvent からコピーされた altKey、ctrlKey、metaKey、shiftKey が含まれるため、修飾キーはマウスイベントやポインターイベントと同様に機能します。
グローバル移動イベント:globalpointermove、globalmousemove、globaltouchmove。これらはポインターがリッスン対象のオブジェクト上にあるかどうかにかかわらず、ポインターが移動するたびに発生します。
コンテナーのライフサイクルイベント(eventMode は不要):added、removed、destroyed、childAdded、childRemoved、visibleChanged。
リッスン方法
import { Sprite } from "pixi.js";
const sprite = new Sprite();
sprite.eventMode = "static";
// EventEmitter style (recommended)
const handler = (e) => console.log("clicked");
sprite.on("pointerdown", handler);
sprite.once("pointerdown", handler); // one-time
sprite.off("pointerdown", handler);
// DOM style
sprite.addEventListener(
"click",
(event) => {
console.log("Clicked!", event.detail);
},
{ once: true },
);
// Property-based handlers
sprite.onclick = (event) => {
console.log("Clicked!", event.detail);
};
ポインターイベントと伝播
import { Sprite, Container } from "pixi.js";
const parent = new Container();
parent.eventMode = "static";
const child = new Sprite();
child.eventMode = "static";
parent.addChild(child);
child.on("pointerdown", (event) => {
console.log("child pressed");
event.stopPropagation(); // prevent parent from receiving this event
});
parent.on("pointerdown", () => {
console.log("parent pressed (only if child did not stop propagation)");
});
キャプチャフェーズのイベント
すべてのイベントで、イベント名に capture を付けるとキャプチャフェーズをサポートできます(例:pointerdowncapture、clickcapture)。キャプチャリスナーは、イベントがターゲットに到達する前のキャプチャフェーズで発火します。
container.addEventListener(
"pointerdown",
(event) => {
event.stopImmediatePropagation(); // blocks event from reaching children
},
{ capture: true },
);
ヒットテスト
ポインターイベントが発生すると、PixiJS はディスプレイツリーをたどり、ポインターの下にある最前面のインタラクティブ要素を探します。探索には次のルールが適用されます。
- コンテナーで
eventMode = 'none'にすると、その要素とサブツリー全体がスキップされます。 - コンテナーで
interactiveChildren = falseにすると、その子要素がスキップされます(コンテナー自体は引き続きテスト対象になります)。 hitAreaを設定すると、境界に基づくテストより優先され、形状のみがチェックされます。- 表示されていないオブジェクト、描画できないオブジェクト、または測定できないオブジェクトはスキップされます。
カスタムの hitArea を設定すると、境界に基づくテストを上書きできます。また、チェックするジオメトリを減らせるため、大きなオブジェクトや複雑なオブジェクトのヒットテストも高速化されます。
import { Sprite, Rectangle, Circle, Polygon } from "pixi.js";
const sprite = new Sprite();
sprite.eventMode = "static";
// Rectangular hit area
sprite.hitArea = new Rectangle(0, 0, 100, 50);
// Circular hit area
sprite.hitArea = new Circle(50, 50, 40);
// Polygon hit area
sprite.hitArea = new Polygon([0, 0, 100, 0, 50, 100]);
// Custom hit test via contains()
sprite.hitArea = {
contains(x: number, y: number): boolean {
return x >= 0 && x <= 100 && y >= 0 && y <= 100;
},
};
グローバル移動イベントとドラッグ
import { Sprite, FederatedPointerEvent } from "pixi.js";
const sprite = new Sprite();
sprite.eventMode = "static";
sprite.cursor = "grab";
let dragging = false;
sprite.on("pointerdown", (event: FederatedPointerEvent) => {
dragging = true;
sprite.cursor = "grabbing";
});
// globalpointermove fires even when pointer leaves the object
sprite.on("globalpointermove", (event: FederatedPointerEvent) => {
if (dragging) {
sprite.position.set(event.global.x, event.global.y);
}
});
sprite.on("pointerup", () => {
dragging = false;
sprite.cursor = "grab";
});
sprite.on("pointerupoutside", () => {
dragging = false;
sprite.cursor = "grab";
});
カーソルスタイル
基本的な使い方では、オブジェクトごとに cursor プロパティを設定します。再利用可能なカーソルを使うには、イベントシステムに名前付きスタイルを登録します。
app.renderer.events.cursorStyles.default = "url('bunny.png'), auto";
app.renderer.events.cursorStyles.hover = "url('bunny_saturated.png'), auto";
sprite.eventMode = "static";
sprite.cursor = "hover"; // uses the registered 'hover' style
カーソルスタイルには、文字列(CSS カーソル値)、オブジェクト(CSS スタイルとして適用)、または関数(モード文字列を引数に呼び出されるもの)を指定できます。
イベントプロパティ
FederatedPointerEvent には豊富な入力データが含まれます。特に有用なフィールドは次のとおりです。
sprite.on("pointerdown", (event: FederatedPointerEvent) => {
event.global; // scene-space Point where the event happened
event.client; // CSS-pixel Point relative to the viewport
event.offset; // Point w.r.t. target Container in world space (not supported at the moment)
event.target; // the Container that received the event
event.currentTarget; // the Container whose listener is running
event.pointerType; // 'mouse' | 'pen' | 'touch'
event.pointerId; // unique id for multi-touch tracking
event.isPrimary; // first pointer in a multi-pointer gesture
event.persistentDeviceId; // always 0; present for DOM PointerEvent type compatibility
event.pressure; // 0-1 pen/touch pressure
event.button; // 0 left, 1 middle, 2 right
event.buttons; // bitmask of held buttons
event.altKey; // modifier key state
event.ctrlKey;
event.shiftKey;
event.metaKey;
event.nativeEvent; // the underlying DOM PointerEvent / MouseEvent / Touch
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
});
FederatedWheelEvent には deltaX、deltaY、deltaZ、deltaMode も含まれます。ホイールイベントは、ポインターイベントと同じヒットテスト対象オブジェクトで発生します。
イベント機能
パフォーマンス向上のため、イベントカテゴリをグローバルに切り替えられます。
await app.init({
eventFeatures: {
move: true, // pointer/mouse/touch move events
globalMove: true, // global move events (globalpointermove, etc.)
click: true, // click/tap/press and contextmenu events
wheel: true, // mouse wheel events
},
});
// or configure after init
app.renderer.events.features.globalMove = false;
パフォーマンスのヒント
- インタラクションのないサブツリーでは、
eventMode = 'none'を設定してヒットテストを完全にスキップします。 - コンテナー自体だけがインタラクションを必要とする場合は、
interactiveChildren = falseを設定します。 - 大きいオブジェクトや複雑なオブジェクトでは
hitAreaを使用し、境界ベースのヒットテストを低コストな形状チェックに置き換えます。 - 静止した要素には
'static'を使用し、静止したポインターの下で移動またはアニメーションするオブジェクトにのみ'dynamic'を使用します。 eventFeatures(例:globalMove: false)で未使用のイベント機能を無効にし、フレームごとの処理を削減します。
よくある間違い
[HIGH] デフォルトの eventMode は passive です
誤り:
const sprite = new Sprite(texture);
sprite.on("pointerdown", () => {
console.log("clicked");
});
正しい例:
const sprite = new Sprite(texture);
sprite.eventMode = "static";
sprite.on("pointerdown", () => {
console.log("clicked");
});
デフォルトの eventMode は 'passive' で、オブジェクト自体はイベントを受け取りません。リスナーが動作するには、eventMode を 'static' または 'dynamic' に明示的に設定する必要があります。
[HIGH] buttonMode は削除されました。cursor を使用してください
誤り:
sprite.interactive = true;
sprite.buttonMode = true;
正しい例:
sprite.eventMode = "static";
sprite.cursor = "pointer";
buttonMode は v8 で削除されました。ホバー時に手の形のカーソルを表示するには、cursor = 'pointer' を使用してください。interactive = true は eventMode = 'static' のエイリアスとして引き続き使用できますが、eventMode の使用が推奨されます。
[HIGH] v8 ではオブジェクト上でのみ移動イベントが発生します
誤り:
sprite.eventMode = "static";
sprite.on("pointermove", (event) => {
// expects to fire everywhere; only fires inside sprite bounds
updateDrag(event.global.x, event.global.y);
});
正しい例:
sprite.eventMode = "static";
sprite.on("globalpointermove", (event) => {
// fires everywhere, even outside sprite bounds
updateDrag(event.global.x, event.global.y);
});
v8 では、pointermove、mousemove、touchmove はポインターが表示オブジェクト上にある場合にのみ発生します。v7 では、キャンバス上で移動するたびに発生していました。ドラッグ操作やグローバルな追跡には、globalpointermove、globalmousemove、または globaltouchmove を使用してください。
[MEDIUM] カーソルは親から継承されません
親コンテナーに cursor を設定しても、その子には影響しません。ヒット対象となった要素の cursor 値のみが適用されます。
// This does NOT make children show a pointer cursor
parent.cursor = "pointer";
// Each interactive child needs its own cursor
child.eventMode = "static";
child.cursor = "pointer";
すべての子に同じカーソルを適用する場合は、各インタラクティブな子に個別に cursor を設定するか、親に hitArea を設定して子を非インタラクティブにします。