关于 pixijs-events
面向 PixiJS 开发者的场景交互技能,介绍事件模式、指针处理、传播、命中测试、光标和性能控制。
主要功能
- 01配置事件模式和监听器
- 02涵盖指针、鼠标、触摸及生命周期事件
- 03讲解事件传播与捕获监听器
- 04设置自定义命中区域和光标样式
- 05提供事件性能优化建议
适用场景
- 01处理 UI 元素的点击
- 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 对其子级没有影响。只有直接命中的目标的 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 并将子级设为非交互。