我不知道的 Pixi.js(03)— 交互事件的底层实现
Pixi.js 能渲染画面,但画面要能点、能拖、能悬停,才算一个完整的应用。很多人以为 Pixi.js 的事件就是"给 Sprite 绑个回调",但实际上——v8 的事件系统经历了一次彻底重构,从 InteractionManager 切换到了基于 W3C 标准的 EventS…
Pixi.js 能渲染画面,但画面要能点、能拖、能悬停,才算一个完整的应用。很多人以为 Pixi.js 的事件就是”给 Sprite 绑个回调”,但实际上——v8 的事件系统经历了一次彻底重构,从 InteractionManager 切换到了基于 W3C 标准的 EventSystem,事件模型也从自定义实现变成了和 DOM 事件高度一致的冒泡/捕获模型。
一、v8 的事件系统:从 InteractionManager 到 EventSystem
v7 时代,Pixi.js 的交互事件由 InteractionManager 插件管理。它有两套事件命名:以 mouse 开头的鼠标事件和以 pointer 开头的指针事件,两者并存,行为有微妙差异。
v8 移除了 InteractionManager,替换为 EventSystem。这不只是换了个名字——底层实现完全不同:
import { Sprite } from 'pixi.js';
const sprite = new Sprite(texture);
sprite.eventMode = 'static'; // v8:替代了 v7 的 interactive = true
sprite.cursor = 'pointer';
sprite.on('pointerdown', (event) => {
console.log('clicked at', event.global.x, event.global.y);
});
换句话说,v7 的 sprite.interactive = true 在 v8 中变成了 sprite.eventMode = 'static'。eventMode 有四个值:
none:不参与事件(默认值)passive:不直接响应事件,但不阻塞子节点的事件传播static:响应事件,等同于 v7 的interactive = truedynamic:响应事件且每帧进行命中测试,用于持续跟踪鼠标位置的场景(如拖拽)
这里有一个很多人会忽略的细节——static 和 dynamic 的区别不在于”能否响应事件”,而在于命中测试的频率。static 只在鼠标移动时才测试,dynamic 每帧都测试。对于大多数按钮、UI 元素,static 就够了;只有拖拽、悬停追踪这类需要持续感知鼠标位置的场景才需要 dynamic。
二、命中测试:怎么知道点在了谁身上
用户点击 canvas 的 (200, 150) 坐标,Pixi.js 怎么知道这个点落在了哪个 Sprite 上?答案是命中测试(Hit Testing)。
流程是这样的:EventSystem 拿到鼠标的屏幕坐标 → 从场景树的根节点开始递归向下遍历 → 对每个 eventMode 不为 none 的节点,将屏幕坐标通过其世界变换矩阵的逆矩阵转换为本地坐标 → 检查本地坐标是否在节点的边界内。
说白了,就是”把鼠标坐标翻译成每个对象的本地坐标系,再看是否在范围内”。
下面这段代码展示了自定义命中区域的用法:
import { Sprite, Rectangle, Circle } from 'pixi.js';
// 默认命中区域 = Sprite 的纹理矩形
const button = new Sprite(texture);
button.eventMode = 'static';
// 自定义命中区域为圆形(比如圆形按钮)
button.hitArea = new Circle(50, 50, 50);
// 或者指定一个比纹理更大的矩形区域(增加点击容错)
button.hitArea = new Rectangle(-10, -10, 120, 120);
问题的关键在于——默认的命中测试基于矩形边界框(Bounding Box)。对于非矩形的 Sprite(比如一个圆形按钮使用了圆形纹理),矩形边界框会把四个角落也算进去,导致”点了空白区域也能触发事件”。这种情况下需要用 hitArea 指定精确的圆形或多边形区域。
命中测试的遍历顺序是反向 DFS——先测试渲染顺序靠后的节点(视觉上在最上层的对象优先匹配)。这和 DOM 事件的 z-index 逻辑一致:看到什么就点到什么。
三、事件冒泡与捕获:和 DOM 一样的传播模型
v8 的事件传播模型与 W3C DOM Events 高度一致,分为三个阶段:
(1)捕获阶段:事件从根节点 stage 向下传播到目标节点
(2)目标阶段:事件到达实际被点击的对象
(3)冒泡阶段:事件从目标节点向上传播回 stage
const container = new Container();
container.eventMode = 'static';
const child = new Sprite(texture);
child.eventMode = 'static';
container.addChild(child);
// 冒泡阶段的监听(默认)
container.on('pointerdown', () => {
console.log('container: bubble phase');
});
// 捕获阶段的监听 — v8 新增支持
container.addEventListener(
'pointerdown',
() => {
console.log('container: capture phase');
},
{ capture: true },
);
child.on('pointerdown', (event) => {
console.log('child: target phase');
// event.stopPropagation(); // 阻止继续冒泡
});
// 点击 child 后输出顺序:
// container: capture phase
// child: target phase
// container: bubble phase
这说明了什么?v8 的事件系统已经和 DOM 事件行为一致。v7 的 InteractionManager 只有冒泡没有捕获,v8 补齐了完整的三阶段模型。对于做过 DOM 事件开发的前端来说,学习成本几乎为零。
四、事件委托:在 Container 上统一处理子事件
和 DOM 的事件委托一样,Pixi.js 也支持在父容器上监听子节点的事件。这对于大量同类对象(比如列表项、网格单元)的场景特别有用:
const grid = new Container();
grid.eventMode = 'static';
for (let i = 0; i < 100; i++) {
const cell = new Sprite(texture);
cell.eventMode = 'passive'; // 不直接响应,但允许事件冒泡到父级
cell.position.set((i % 10) * 50, Math.floor(i / 10) * 50);
cell.label = `cell-${i}`;
grid.addChild(cell);
}
// 在 grid 上统一处理
grid.on('pointerdown', (event) => {
const target = event.target;
console.log(`Clicked: ${target.label}`);
});
很多人以为每个可点击对象都需要设 eventMode = 'static',但实际上——子节点只需要 passive 就能被命中测试检测到,事件会冒泡到父级处理。这比给 100 个对象各绑一个监听器高效得多。
不过有个边界情况:passive 模式的对象不会触发 cursor 样式变化。如果需要鼠标悬停时变手型,子节点必须是 static。
五、坐标转换:全局坐标、本地坐标和事件坐标
事件对象上的坐标信息在 v8 中更加规范:
sprite.on('pointerdown', (event) => {
// 全局坐标(相对于 canvas 左上角)
console.log(event.global.x, event.global.y);
// 本地坐标(相对于当前对象的本地坐标系)
const local = event.getLocalPosition(sprite);
console.log(local.x, local.y);
// 屏幕坐标(相对于浏览器视口)
console.log(event.screen.x, event.screen.y);
});
getLocalPosition() 的原理是把全局坐标乘以目标对象世界矩阵的逆矩阵。如果你对上一篇中变换矩阵的逐级传递有印象,这里就是它的反向操作。
总结
Pixi.js v8 的事件系统从自定义的 InteractionManager 切换到了标准化的 EventSystem。核心变化是三点:eventMode 替代了 interactive 属性并提供更精细的控制、完整支持冒泡+捕获的三阶段传播模型、命中测试基于世界矩阵逆变换实现坐标转换。对于前端开发者来说,v8 的事件模型和 DOM 事件几乎一样,上手零障碍。
本系列其他文章:
- 上一篇:纹理与 Sprites 的管理机制
- 下一篇:滤镜系统的设计原理
相关主题:
- 如果你对浏览器原生事件模型感兴趣,可以看:浏览器系列 — 事件机制
延伸阅读: