~/ ?.log $
返回文章列表
9 min read
更新于 2026年3月6日

我不知道的 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 = true
  • dynamic:响应事件且每帧进行命中测试,用于持续跟踪鼠标位置的场景(如拖拽)

这里有一个很多人会忽略的细节——staticdynamic 的区别不在于”能否响应事件”,而在于命中测试的频率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 事件几乎一样,上手零障碍。


本系列其他文章:

相关主题:

延伸阅读:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;